Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed .eslintignore
Empty file.
78 changes: 78 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# TypeØmatica — Agent Guide

## What this project is

Runtime strict-type enforcement for JavaScript objects using Proxies and decorators. The core idea: once a property is assigned a value, its type is locked; later assignments of a different type throw.

## File map

| File | Purpose |
|------|---------|
| `src/index.ts` | Core: `BaseClass`, `BaseConstructorPrototype` (default export), `@Strict` decorator, proxy handlers, CJS export setup. |
| `src/esm.ts` | ESM entry point. Re-exports default + named bindings from `src/index.ts`. |
| `src/fields.ts` | `FieldConstructor` class for custom property descriptors. |
| `src/errors.ts` | Error message constants. |
| `src/types/*.ts` | Type-category handlers: `primitives`, `objects`, `functions`, `special`, `nullish`. |
| `src/types/index.ts` | Aggregates and exports `isPrimitive`. |
| `test/index.ts` | Jest CJS test suite. Must keep 100% coverage. |
| `test/esm/imports.test.ts` | Vitest tests for true ESM imports. |
| `test/noJest.ts` | Node native test file (currently requires a TS loader to run). |
| `examples/` | Runnable integration examples. |
| `lib/` | CJS build output. |
| `lib/esm/` | ESM build output. |

## Build & test commands

```bash
npm run build # rm -rf lib/ && tsc (CJS) && tsc -p tsconfig.esm.json (ESM)
npm run test:cov # Jest CJS tests with coverage; must be 100%
npm run test:esm # Vitest true-ESM import tests
npm run examples # run every example in examples/
npm run lint:src # ESLint on src/
npm run lint:lib # ESLint on lib/
```

## Module architecture

- CJS consumers get `lib/index.js` via `main` / `exports.require`.
- ESM consumers get `lib/esm/esm.js` via `exports.import`.
- `src/esm.ts` is excluded from the CJS build (`tsconfig.json` `exclude`) and built only by `tsconfig.esm.json` using `module: esnext` + `moduleResolution: bundler` so TypeScript emits ESM syntax without requiring `"type": "module"` in `package.json`.
- `lib/esm/package.json` is generated during build with `{"type":"module"}`.
- `src/index.ts` contains a `setupCommonJS()` block guarded by `typeof module !== 'undefined'`; it redefines `module.exports` to `BaseConstructorPrototype` and attaches named getters for `BaseClass`, `FieldConstructor`, etc.

## Coverage rules

- **Jest must stay at 100%** for statements, branches, functions, and lines.
- Jest instruments the built `lib/index.js` and maps coverage back to `src/`.
- Istanbul ignore hints (`/* istanbul ignore next */`) must survive into `lib/`, so `tsconfig.json` sets `removeComments: false`.
- Any code path only reachable through true ESM imports should be covered by a Vitest test in `test/esm/`.

## Conventions

- Use **tabs** for indentation.
- Relative imports in `src/` must include the `.js` extension (`'./errors.js'`, `'./types/index.js'`). This is required for the ESM build.
- Class properties that should be type-checked must be declared with `declare prop: Type;` and assigned inside the constructor. Initialized class fields (`prop: Type = value`) bypass the Proxy.
- `src/esm.ts` must not introduce new runtime logic; it only re-exports.

## What to discuss before changing

- Proxy handler logic in `createHandlers()`.
- `module.exports` redefinition or named export setup.
- Decorator signatures in `strict()`.
- `SymbolTypeomaticaProxyReference` semantics.
- Changes to `FieldConstructor` that affect how it is detected in `createProperty()`.

## How to add a new example

1. Create `examples/NN-descriptive-name.js`.
2. Make it runnable with `node examples/NN-descriptive-name.js`.
3. Add it to the `examples` script in `package.json`.
4. Add an entry to `EXAMPLES.md`.

## How to add a new source file

1. Place it under `src/` or `src/types/`.
2. Use `.js` extensions in all relative imports.
3. If it is ESM-only, exclude it from `tsconfig.json` and include it in `tsconfig.esm.json`.
4. Add tests in `test/index.ts` (Jest) or `test/esm/` (Vitest) as appropriate.
5. Run `npm run build && npm run test:cov && npm run test:esm && npm run examples`.
64 changes: 64 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# TypeØmatica Examples

All examples are standalone Node.js scripts in [`examples/`](./examples/). Run the full set with:

```bash
npm run examples
```

Or run a single example:

```bash
node examples/01-function-constructor-getter.js
```

## Existing examples

| # | File | Concept |
|---|------|---------|
| 01 | [`function-constructor-getter.js`](./examples/01-function-constructor-getter.js) | Subclass `FieldConstructor` using a function constructor instead of a `class`. |
| 02 | [`buffer-toJSON.js`](./examples/02-buffer-toJSON.js) | Wrap a `Buffer` so it serializes cleanly with `JSON.stringify`. |
| 03 | [`shared-array-buffer.js`](./examples/03-shared-array-buffer.js) | Share a single `FieldConstructor` instance across multiple objects. |
| 04 | [`uint-fields.js`](./examples/04-uint-fields.js) | Typed unsigned-integer fields (`UInt8`, `UInt16`, `UInt32`) with clamping. |
| 05 | [`stdin-stdout.js`](./examples/05-stdin-stdout.js) | Strictly typed stdin/stdout-style field handles. |
| 06 | [`shared-field-prototype-chain.js`](./examples/06-shared-field-prototype-chain.js) | Shared field behavior across a prototype chain. |
| 07 | [`shared-field-different-objects.js`](./examples/07-shared-field-different-objects.js) | Shared field state synchronized across different object instances. |

## Planned examples

| # | File | Concept | Tier |
|---|------|---------|------|
| 08 | [`async-field.js`](./examples/08-async-field.js) | Async getter / deferred async setter with `Promise` descriptors. | Practical |
| 09 | [`pub-sub-field.js`](./examples/09-pub-sub-field.js) | Pub/sub field: subscribers are notified on every set. | Practical |
| 10 | [`iterator-field.js`](./examples/10-iterator-field.js) | Queue-like field: setter enqueues, getter dequeues. | Reactive |
| 11 | [`time-to-live-field.js`](./examples/11-time-to-live-field.js) | Field value auto-invalidates to `null` after N seconds. | Reactive |
| 12 | [`loop-field.js`](./examples/12-loop-field.js) | Fixed-point loop: setter provides the step function, getter advances one cycle. | CS exploration |
| 13 | [`recursive-field.js`](./examples/13-recursive-field.js) | Y-combinator-style recursion through field references. | CS exploration |
| 14 | [`async-loop-field.js`](./examples/14-async-loop-field.js) | Async variant of the loop field. | CS exploration |
| 15 | [`async-recursive-field.js`](./examples/15-async-recursive-field.js) | Async variant of the recursive field. | CS exploration |

## Design notes

### Async getters and setters

JavaScript property descriptors can return Promises, but the engine does **not** automatically await them. An async getter returns a `Promise`, and an async setter returns a `Promise` that the caller must handle if they need to wait for completion. Examples make this explicit.

### Class-field gotcha

Most examples use `declare prop: Type;` plus constructor assignment:

```typescript
class Example extends BaseClass {
declare value: number;
constructor() {
super();
this.value = 0; // goes through the Proxy
}
}
```

Initialized class fields (`value = 0`) bypass the Proxy and are not type-checked.

### Tier 3: CS exploration

The loop and recursive examples are conceptual demonstrations of fixed-point combinators and lazy evaluation using TypeØmatica descriptors. They are labeled as exploration rather than production patterns.
116 changes: 116 additions & 0 deletions FOR_HUMANS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# TypeØmatica — For Humans

> Runtime type enforcement that feels like TypeScript, but happens while your code is running.

## What is this?

**TypeØmatica** is a small library that locks the type of every property on an object after its first assignment. If something later tries to put the wrong kind of value there, it throws — even if the bad assignment comes through `@ts-ignore`, `eval`, a malformed JSON payload, or a third-party module that doesn’t know about your types.

It is not a replacement for TypeScript. It is a **runtime safety net** for the places TypeScript cannot reach.

## Why would I want it?

TypeScript is excellent at compile-time checks, but JavaScript is still what actually runs. Here are a few places where runtime enforcement helps:

- **Dynamic data** — parsing JSON, reading from `localStorage`, receiving events from a WebSocket.
- **API boundaries** — data coming from another service may claim to be a number but arrive as a string.
- **`@ts-ignore` and casts** — code reviews miss them; TypeØmatica does not.
- **Teaching and exploration** — it makes the idea of "types" tangible at runtime.

And the nicest part: for many classes it is a **one-line change**.

```typescript
import { BaseClass } from 'typeomatica';

class User extends BaseClass { // ← one line
declare name: string;
declare age: number;
constructor() {
super();
this.name = '';
this.age = 0;
}
}
```

## The mental model

TypeØmatica puts a **Proxy** on the prototype chain of your class. Every property write goes through that Proxy first.

The Proxy asks a simple question: *"Has this property been seen before, and if so, is the new value the same type as the old one?"*

- First assignment → type is recorded.
- Same type later → allowed.
- Different type later → `TypeError: Type Mismatch`.

The Proxy also blocks prototype mutation, property redefinition, and deletion, because those are common ways to sneak around type checks.

## The one big gotcha: class fields

Modern TypeScript/JavaScript class fields are initialized as **own properties** on the instance. Own properties do not trigger the Proxy on the prototype chain, so TypeØmatica cannot enforce their types.

So instead of this:

```typescript
class User extends BaseClass {
name: string = ''; // ❌ bypasses the proxy
}
```

do this:

```typescript
class User extends BaseClass {
declare name: string; // type-only, no runtime field
constructor() {
super();
this.name = ''; // ✅ goes through the proxy
}
}
```

Or use `FieldConstructor` if you need custom getters/setters.

## Five-minute example

```typescript
import { BaseClass } from 'typeomatica';

class User extends BaseClass {
declare name: string;
declare age: number;
constructor() {
super();
this.name = '';
this.age = 0;
}
}

const user = new User();

user.name = 'John'; // ✓ fine
user.age = 25; // ✓ fine

// @ts-ignore
user.age = '25'; // ✗ throws TypeError: Type Mismatch
```

## When it shines most

Any TypeScript or typed JavaScript project where runtime validation is valuable. It is especially useful when:

- the data comes from outside your code,
- you are experimenting and want fast feedback,
- you want a cheap, drop-in guardrail (`extends BaseClass`).

It is less ideal for:

- **Hot paths** — Proxy interception adds overhead.
- **Browser bundles** — the library assumes a Node.js environment (`util`, `module`).
- **Purely static, fully trusted code** — TypeScript alone may be enough.

## Where to go next

- [`README.md`](./README.md) for the full API reference.
- [`EXAMPLES.md`](./EXAMPLES.md) for runnable patterns and advanced ideas.
- [`AGENTS.md`](./AGENTS.md) if you are an AI agent or contributor looking for the project map.
Loading
Loading