<!--{"pin": "bottom", "order": 1}-->

# Changelog

All notable changes to **tjs-lang** are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Security

- **AJS: a custom atom tagged `pure` can only return guest data** (#3991). `pure` skips the
  capability membrane, which is right for core atoms (they work on data already inside the VM)
  but let a HOST atom tagged `pure` put a live `Map`, class instance or getter-bearing object
  into guest state and into `run.result`, breaking the closed guest value domain. A custom pure
  atom's return is now checked, not copied, by the membrane's own rules (JSON data plus an exact
  `Date`, own data properties only); anything else ends the run, naming what was returned.
  Core atoms are exempt. **What to do:** an atom that returns host objects should be tagged
  `effects: 'io'`, so its return is copied. Copying every custom atom's return is a planned
  opt-in (#4007).

- **AJS: an allowlist entry alone no longer opens the private network (BREAKING)** (#3991).
  `allowedFetchDomains` was the whole destination policy: naming a loopback, private or
  link-local address (a typo, a config copied from a dev setup) let agents reach the internal
  network, and the private-range block applied only when no allowlist was set, when nothing
  was fetchable anyway. Those destinations are now refused even when allowlisted, on the
  built-in client, a host's own fetch and the vision battery, unless the host opts in with
  `allowPrivateNetwork: true` in the run context (`fetchPolicy.privateNetwork` for `Eval` and
  `SafeFunction`). **What to do:** a host that deliberately fetches a local service adds the
  opt-in. Resolving hostnames to catch one that points inside the network is a planned opt-in
  (#4006).

- **AJS: `agentRun` did not hold a sub-agent to its declared inputs or result** (#3987).
  **Affected: every published version with `agentRun`, through 0.14.0-rc.10.** `vm.run`
  refuses arguments that do not match an agent's declared inputs, but `agentRun` ran a
  sub-agent on the same VM (from a stored-procedure token, or an AST passed inline) without
  that check, so one agent could hand another input its signature forbids: `double({ n: 0 })`
  refused `{ n: 'x' }` from a host and returned `{ n: 'xx' }` from an agent. Both contracts are
  now checked on both routes, input before the sub-agent runs and the declared result after,
  and the checks are charged like every guest-schema validation. Sub-agents the host runs (the
  `agent.run` capability) go through `vm.run` and were already checked.

- **AJS: `timeoutMs` and the caller's `AbortSignal` did not stop a run that only computes**
  (#3986). **Affected: every published version through 0.14.0-rc.10.** The run's deadline was
  a timer raced against execution. A step that does no IO completes at once, and each `await`
  resumes as a microtask, which the event loop runs before any timer. So a compute-only run
  never let the deadline fire: a 3-million-step loop with `timeoutMs: 200` ran 3.6 s and
  completed. The same starvation meant the caller's signal could not interrupt it, and the
  host's event loop was frozen for the whole run, stalling every other agent and request in the
  process. Fuel still bounded the work, so a host with a tight fuel budget was protected; one
  that set generous fuel and relied on `timeoutMs` as the hard wall-clock limit was not.
  Now the step boundary, which every step and loop iteration passes, reads the clock every 64
  steps and aborts past the deadline, and every 10 ms of computing it yields one macrotask, so
  timers, the caller's signal and the host's other work all run. **What to do:** upgrade.
  Until then, size `fuel` as the limit you rely on.

- **AJS: declared input contracts did not check every kind of input (fail-open)** (#3835).
  **Affected: every published version from 0.2.0 through 0.14.0-rc.10.** `vm.run` validates
  an agent's arguments against the input schema the transpiler derives from its signature,
  but that schema mapped only strings, floats, booleans, lists, objects and unions; every
  other kind of example became `{}`, which admits anything. So `function f(n: 10)` accepted
  `'x'`, `n: +0` accepted `-1`, and `m: 'a' | 'b'` accepted `'z'`. The sandbox itself was not
  breached (fuel, the heap ceiling, the membrane and capabilities were unaffected), but an
  agent, or a host relying on its declared signature to reject bad input before the agent ran,
  could receive a value of the wrong type or outside its declared set.
  Now integer examples become `{ type: 'integer' }`, `+0` becomes
  `{ type: 'integer', minimum: 0 }`, and literal unions become `{ enum: […] }`. Inputs JSON
  cannot carry (a bigint, a function) and regex examples (guest schemas may not hold host
  patterns) are refused at transpile time with the remedy. The schema switch is exhaustive,
  so a new kind of example is a compile error rather than a silent `{}`.
  Two more holes of the same class, found while fixing #3834: an object example never listed
  its members as `required`, so `p: { x: 0 }` accepted `{}` (TJS refuses it); and an
  `undefined` union member mapped to `{}`, so `s: '' | undefined` accepted `5`. Members are
  now required unless their example allows absence (`T | undefined`), and `undefined` leaves
  the union.
  **BREAKING, toward correctness:** an agent called with input its signature never allowed
  (`n: 10` given a string) is now refused. **What to do:** upgrade, and RE-TRANSPILE stored
  agents. The schema is written into the AST at transpile time, so an AST persisted by an
  affected version keeps its permissive schema until it is transpiled again. Until then,
  validate arguments in the host before `vm.run`.

### Changed

- **Native TJS refuses `new String`, `new Number` and `new Boolean` (BREAKING, toward
  correctness)** (#4012). They build an object wrapper, which is truthy even when it holds
  `false` or `0`, and are almost always a slip for `String(x)` and friends. The deliberate uses
  (a primitive by reference, a tagged value) keep a spelling that says what it means:
  `Object(x)`. `==` and truthiness still unwrap boxed values, which arrive from JavaScript
  libraries, converted code and `Object(x)`. Plain JavaScript and converted TypeScript keep the
  constructors; conversion now writes them as `Object(…)` (`new String(x)` as
  ``Object(`${x}`)``, which still throws on a Symbol as the constructor does), so graduation
  keeps working.

- **A typed AJS agent's declared result is enforced (BREAKING, toward correctness)** (#3834).
  `function f(n: 0): { x: 0 }` recorded its return type in the signature and nowhere else, so
  `return { x: 'str', extra: 1 }` ran clean. A typed function now emits what it declares or
  the run ends in an error (`Result does not match the declared result type`). The transpiler
  writes the declared result into the AST as `outputSchema`, beside `inputSchema`. `vm.run`
  checks it on success, metered from the fuel the run has left, and a failed run keeps its
  own error. Members are required and closed, as for inputs. An agent returns an object, so a
  declared result that is not one (`): 0`) is refused at transpile time. Untyped agents are
  unchanged. In an agent with helpers, the entry's annotation is now the one that counts: the
  parser used to hand over the FIRST function's, which is a helper's. A helper's own declared
  return is not enforced yet. An AST persisted before this has no `outputSchema`; re-transpile
  it to get the check.

### Fixed

- **`fromTS` reads `/* @tjs TjsStrict */` only from a real block comment.** The scan matched
  raw source, so a file whose test fixtures held that text in a template string converted as a
  TjsStrict file (TJS semantics throughout, no conversion rewrites); a comments-kept view would
  still have matched it quoted inside a line comment. Each block comment is now matched whole.

- **AJS: reading an undeclared name is a transpile error (BREAKING, toward correctness)**
  (#4002). `return { v: coutn }` compiled without a word and read `undefined`, so a typo became
  a silent wrong answer. Now: "'coutn' is not defined. Declare it with let or const, or take it
  as a parameter." A helper or an atom read as a value is refused by name too (`let g = double`
  gave `undefined`). Assigning an undeclared name still declares it, as before. A host that
  puts names in scope outside the source passes them as `transpile(src, { globals })`, as
  `Eval` now does for its context keys.

- **AJS: `function agent({ city: string })` declares a typed input.** A type name as a
  destructured member was read as a JavaScript rename (to a variable called `string`), so
  `city` was never imported or checked and read `undefined`. It now means what `city: string`
  means at the top level: a required string. Found by the undeclared-name check.

- **TJS: an object example's quoted and computed keys are read correctly** (#3990). Inference
  read a computed key by the key expression's name, so a dictionary default
  `opts = { [k]: 1, b: 2 }` gained a member `k` (`{ a: 1, b: 2, k: 1 }` for `k = 'a'`). A key
  known only at run time now makes the object an ordinary one, and as a default an ordinary
  JavaScript default. Literal keys (`'a b': 1`, `['a']`), which were silently ignored, are now
  members, checked and merged like any other; `'__proto__'` is never one.

- **AJS: a computed object key is refused instead of silently renamed** (#3989).
  `{ [k]: 1 }` with `k = 'z'` produced `{ k: 1 }`. A key known only at run time is now a
  transpile error pointing at `Object.fromEntries([[k, v]])`; a literal computed key
  (`['a b']`) still works.

- **AJS: a helper function no longer reads the run's arguments by name** (#3988). A name a
  helper does not declare fell back to the agent's inputs, so `function g(x) { return x + n }`
  saw the agent's `n`. Helpers are top-level functions, not closures: they see their parameters
  only.

- **AJS: an atom called inside an expression is refused at transpile time** (#3984).
  `return { id: uuid() }` transpiled and failed only when it ran; a local helper in the same
  position was already refused with the remedy. Now atoms are too: "Atom 'uuid' cannot be
  called inside an expression. Assign its result to a variable first." Host atoms are covered
  when they are passed to `transpile(src, { atoms })`.

- **AJS: `try … finally` runs the `finally` block** (#3983). The transpiler dropped it without a
  word, so cleanup a program relied on never ran. The `try` node now carries a `finally` block
  (an AST format addition: a VM older than this release ignores it, as before), run with
  JavaScript's semantics: after the try and catch whatever happened, a pending return or error
  restored afterwards, its own return or error winning. A run halted at the capability
  boundary takes no further steps, `finally` included.

- **AJS: `Error()` keeps a computed message** (#3982). `Error('bad ' + x)`, `Error(msg)` and a
  template reached the error as `'[object Object]'`; only a literal survived. The message is now
  evaluated; a number or boolean becomes its text, and an object is refused by name rather than
  coerced.

- **An AJS parameter whose example is `{}` accepts any object.** The input schema closed an
  empty property set, so `{ writer = {} }` refused every object it was given. `{}` as an example
  means "an object", as it already did for an agent that declares no parameters.

- **An AJS input written `'' | null` admits `null`.** Inference reads it as a nullable string,
  and the schema ignored the flag, so `null` was refused. Unions are now `anyOf`, not `oneOf`
  (exactly one), which refused any value matching two members.

- **A literal union inside an AJS declared result keeps its values.** The return-type scanner
  writes `|` as `||`, and `parseReturnType` read that as JavaScript's `||`, keeping only the
  left side: `{ m: 'a' | 'b' }` became `{ m: string }`.

- **A class can construct itself in a static initializer** (#3841). A static field or static
  block runs while the class is being defined, before the callable wrapper exists, so a
  self-call there threw `__tjs_callable is not a function`. That is exactly what graduating a
  converted `static zero = new Money(0)` produced. There, the call now constructs directly.

- **Enum members read as properties, as in TypeScript** (#3840). `Enum` kept its members only
  under `.members`, so a converted TypeScript `enum Color { Red = 'red' }` read `Color.Red` as
  `undefined`: conversion changed what the program did. Members are now properties of the enum
  (`Color.Red`), and numeric members map back to their names (`Dir[0] === 'Up'`), in both the
  inline runtime and `tjs-lang/runtime`. A member named like one of the enum's own fields
  (`check`, `values` …) does not shadow it; `.members` still has it.

- **Infix `Is` / `IsNot` find their operands by structure** (#3830). The operand pattern
  treated parentheses as identifier characters, unbalanced, so `console.log(a Is b)` compiled
  to `Is(console.log(a, b))`: a different program, with no error. `[1, 2] Is [1, 2]` and
  `Point(1, 2) Is Point(1, 2)` did not compile at all. Operands are now postfix chains with
  balanced groups, or bracketed literals. Where another operator touches an operand
  (`1 + a Is b`, `!a Is b`), precedence would decide the meaning, so it is refused with the
  remedy, `Is(a, b)`, instead of guessed.

- **`typeof` on an undeclared name is `'undefined'` in native TJS, as in JavaScript** (#3822).
  `typeof x` is rewritten to `TypeOf(x)` (so `typeof null` is `'null'`), and a call evaluates
  its argument, so the feature-detection idiom `typeof window === 'undefined'` threw a
  ReferenceError in a `.tjs` file. A bare identifier now asks JavaScript's own `typeof` first.

- **Generated `.d.ts` keeps `static` and typed defaults on class members** (#3620). The method
  scan matched `name(`, so `static of(…)` lost `static` and was declared an instance method
  (`Box.of(2)` failed with TS2339 in a consumer), and `scale(k: 0 = 1)` was declared a REQUIRED
  `k: any`. Now `static of(v: number)` and `scale(k?: number)`.

- **Graduating a converted file no longer changes what it does** (#1992, #2022, #2040).
  `fromTS` output carries a `tjs <- …` annotation meaning JavaScript semantics; `graduate()`
  removes it and every native-TJS rule switches on. A differential test found nine ways that
  changed behaviour: `'5' == 5` turned false, `typeof null` turned `'null'` (so a
  `typeof v === 'object'` branch skipped null), `f(n = 5)` refused `2.5`, an object-literal
  default started merging, and `var` and `new Date()` made the graduated file fail to compile.
  Conversion now writes each reliance in a spelling that means the same under both semantics:
  `DangerousLegacyEquals(a, b)` (not against `null`, where TJS agrees), `LegacyTypeOf(x)`
  (not where the answer cannot be `'object'`), `LegacyDate(…)`, `n = 5.0`,
  `LegacyDefault({…})`, and `var` hoisted to a `let` at the top of its function (the same
  scoping, closures over a loop variable included). `graduate()` now REFUSES what nothing can
  preserve (`eval`, `new Function`), naming it and returning the input unchanged. The one
  accepted change: truthiness unwraps boxed primitives. `TjsStrict` files are not rewritten.
  **New:** `LegacyTypeOf(x)`, JavaScript's `typeof`, in the family of named escapes.

- **Every named escape works when it is a file's only runtime use.** The emitter's list of what
  needs the inline runtime had `LegacyDate` but not `DangerousLegacyEquals`, `DangerousLegacyNot`,
  `LegacyExactly`, `LegacyNotExactly` or `LegacyDefault`, so a file using only one of those threw
  a ReferenceError. Each is now tested alone, in both dialects.

## [0.14.0-rc.10] — 2026-10-31

On the `rc` dist-tag. Mostly what tosijs-native found building natively in `.tjs` from the
first line: typed parameters with defaults, class methods that validate, `.d.ts` that
compiles across files, a `Bun.build` plugin and an optional shared runtime per bundle. Pin
`0.14.0-rc.10` exactly to test it.

### Read this first: what changes since rc.9

- **Class methods and constructors validate their parameters (BREAKING, toward
  correctness).** `Box('s')` for `constructor(v: 0)` returns a MonadicError. A platform
  callback whose return value is discarded silently skips its body on a refused call:
  `attributeChangedCallback(name = '', old = '', value = '')` refuses the platform's
  `old = null`. Spell `old = null`.
- **`name: Type = default`** works in TJS (#3596), and is REFUSED on the AJS path with a
  remedy, until the type can reach the agent's input schema.
- **A bare `null` default accepts any value** (#3609), as a null dictionary member did.
- **Emitted `__tjs` names a parameter's Type** (`typeName`) and keeps a function type's
  signature; a consumer reading `__tjs` sees more fields.
- **New entry points:** `tjs-lang/bun-build-plugin` (`tjsPlugin()` for `Bun.build`) and
  `tjs-lang/prelude` (with `prelude: 'import'` / `sharedPrelude`; only for a bundle built by
  the same tjs-lang).
- **`tjs emit` rewrites relative `./x.tjs` imports to `./x.js`** (#3592), in the `.js` and
  the `.d.ts`.
- **Doc comments:** `findDocCommentSpans` / `extractDocComments` are exported (#3604), and the
  scan is linear (it was quadratic before fuel on the AJS path).

### Added since rc.9

- **A shared prelude: `prelude: 'import'` and `tjsPlugin({ sharedPrelude: true })`** (#3594,
  from tosijs-native). Every emitted module carries its own inline runtime, so a bundle of N
  modules carried N copies. Opted in, a module instead imports a factory from the new
  `tjs-lang/prelude` and builds its own instance: the code is in the bundle once, the state
  (the `extend` projection table, the call-stack ring) stays per module. The behaviour is the
  inline runtime's, checked by a differential corpus run both ways. A module compiled by a
  different tjs-lang is refused at load, by content hash. Measured on tosijs-native (seven
  modules): 102.6 KB → 72.3 KB raw, one prelude instead of seven; gzip is unchanged
  (15.4 → 15.6 KB), because compression had already been folding the copies. So the win is
  parse size, not transfer. Default output is unchanged and still standalone.
  **This output is for a bundle built by the same tjs-lang**, which is what the plugin does:
  it names an exact prelude, so do not publish `prelude: 'import'` files unless the package
  pins tjs-lang to that exact version. A consumer's tree resolving a different copy gets
  "compiled against prelude X" at load. Signature examples and `test` blocks still run under
  it (they run against the inline form: the rc.10 review found every one inconclusive, so a
  wrong example passed the build).

- **`name: Type = default`** (#3596, from tosijs-native): a type and a separate default,
  TypeScript's spelling. The parameter is checked against the type, optional, and defaults to
  the default; it is how a literal union gets a default (`mode: 'grant' | 'deny' = 'grant'`),
  which no other spelling could express. It was a bare `Unexpected token`. A passed object
  must satisfy the type whole (not merge-on-partial, which stays the untyped `opts = { … }`),
  a literal default that fails its own type is a compile error, and the `.d.ts` declares the
  parameter optional with its type. TypeScript conformance's "param with default" row passes.

- **`tjs-lang/bun-build-plugin`: `tjsPlugin()` for `Bun.build`** (#3593, from tosijs-native).
  `tjs-lang/bun-plugin` is a runtime registration (Bun's `plugin()`), which `Bun.build`
  ignores, so a project bundling `.tjs` had to write its own. `Bun.build({ plugins:
  [tjsPlugin()] })` bundles a `.tjs` graph that runs in plain Node; the preload now uses the
  same plugin.
- **Return examples can run on load, opt-in** (#3599, from tosijs-native): `tjsPlugin({
  runTests: true })`, or `TJS_RUN_TESTS=1` with the preload. A wrong return example (or a
  failing `test` block) then fails the build or the import, instead of passing `bun test` and
  failing only `tjs check`. Off by default, since it runs a file's tests on every load.

- **`findDocCommentSpans` and `extractDocComments` are exported** from `tjs-lang`,
  `tjs-lang/lang` and `tjs-lang/browser` (#3604). They are TJS's own doc-comment grammar
  (`/# … #/`: literal-masked and nestable), so a doc system can read a `.tjs` file's
  documentation the way the language does. tosijs-ui feature-detects them, and a `.tjs` file
  documented the TJS way gets a page.

### Fixed since rc.9

- **Explaining a rejected value costs a bounded multiple of checking it** (#3619, rc.10
  review). The explainer behind "where did it fail" (#3597) asked whether each named Type's
  whole subtree passed before descending into it, so a rejected recursive value cost
  subtree × depth: 37× the accept at depth 60. It now walks the shape first and asks only
  where the walk found nothing (a predicate refusing a well-shaped value), at most 16 times;
  past that it stops explaining and the error keeps its plain message. About 3× the accept at
  any depth or width, counted in property reads. The messages are the old explainer's,
  checked against it over every leaf of a nested config broken nine ways.

- **The doc-comment scan is linear** (rc.10 review). `findDocCommentSpans` (public since
  #3604) and `blankDocComments` run before any fuel on the AJS path, and were quadratic:
  `'/#'.repeat(k)` took 28s at 128 KB, 11.6ms now. The new scan returns exactly the old one's
  spans, checked against the old implementation over 20,000 generated sources, and a
  work-counting row fails if it stops being linear. Exposure was bounded by the 8 KB default
  source cap (about 60ms); a host that raised `maxSourceBytes`, or called `transpile()`
  directly, got super-linear work before any budget.

- **AJS refuses `name: Type = default`, with a remedy** (rc.10 review). The new spelling
  (#3596) reached AJS through the parameter code it shares with TJS and was accepted with the
  type dropped, so `n: number = 5` admitted `'x'` into the agent: a declared input contract
  silently unenforced. rc.9 refused it as a syntax error; it is refused again, now saying
  what to write (`n = 5`), until the AST emitter carries the type into the input schema. The
  AJS construct ratchet lists it, so the next leak through the shared code fails by name.

- **An emitted function's `__tjs` says WHICH named Type a parameter has.** `serializeType`
  copied a fixed field list that left out `typeName`, so a `declared` parameter serialized as
  `{ kind: 'declared' }` and `wrap(fn, fn.__tjs)` reported "named Type 'undefined'". A
  function-typed parameter lost its signature (`params`, `returns`) the same way, and a
  degraded `any` lost its `unresolved` text. The test meant to catch it had been passing on
  another row's record; CI's test order exposed it.

- **The flight recorder's module-level ring mirrors into an installed instance.** With
  `globalThis.__tjs = createRuntime()` (the documented setup), what the runtime module itself
  records, such as a `wrap()` notice, went to a ring nothing read. Instances already mirrored
  into the global; the module recorder was the one sink without. `installRuntime()` (the
  module itself on the global) is skipped, so nothing loops.

- **A bare `null` or `undefined` parameter default means any value** (#3609, from
  tosijs-native): `function render(doc = null)` accepted ONLY null, refusing ordinary
  callers, while a null dictionary member accepted anything. It now means "any value,
  defaulting to null", the usual JavaScript meaning. `doc: null` still means only null.
  **Behaviour change** for code relying on the refusal.

- **A class can call itself from its own body** (#3608): `static of(v) { return Box(v) }`
  threw "Cannot call a class constructor without new". TJS makes a class callable by
  rebinding its outer name to a wrapper; inside the body the name is the raw class. The call
  now goes through the wrapper (recorded on the class as a non-enumerable
  `__tjs_callable`), so overloaded constructors still dispatch. The callee is resolved by
  scope: a method that binds its own `Box` (a local, a destructured or default parameter, an
  inner function, `catch (Box)`, a loop binding) calls its own (rc.10 review: those threw
  `Box.__tjs_callable is not a function`), and whether a class is wrapped is read from the
  wrapper's statements, not from source text a string can spell.
- **A `}` in a string or regex inside a method no longer ends the class** for the
  callable-class wrapper (literal blindness). The wrapper landed inside the class body, where
  it parsed by accident as a class field named after the class, so that class was never made
  callable.

- **A dictionary default may have an `undefined` member** (#3591, from tosijs-native),
  meaning "any value, defaulting to undefined", as a `null` member already did. It was
  refused as "not a clonable literal value", so "absent means use the global" had to be
  spelled `null`. The `.d.ts` declares it `any`.

- **A declared Type's failure says where** (#3597, from tosijs-native). A config type whose
  one bad field was deep inside reported `Expected Config for '…config', got object`; it now
  says `Expected Config for '…config': products[1].kind: expected string, got number`
  (missing members, nested shapes, the integer rule, function members and recursive Types
  included). The explanation runs only after the check has failed, and never throws.
  **Size:** it adds ~1.95KB (~750 bytes gzipped) to each emitted module that names a
  declared Type; sharing one runtime per bundle is #3594.

- **Class methods validate their parameters, the constructor included** (#3598, from
  tosijs-native). **Behaviour change:** a method or constructor with typed parameters now
  returns a MonadicError for a wrong argument (`Box('s')` for `constructor(v: 0)`, which used
  to build a Box with `v = 's'`), as the same signature on a function always did. Methods
  were only stripped of their annotations. Static methods and destructured method
  parameters are covered; a derived constructor's checks run before `super()`, which
  JavaScript allows since they return only on failure. Setters are NOT checked (their return
  value is discarded, so a refusal would silently drop the write), nor is `dialect: 'js'`.
  **BREAKING for platform callbacks whose return value the platform discards.** A refused
  call returns a MonadicError instead of running the body, and the host never looks:
  `attributeChangedCallback(name = '', old = '', value = '')` is called with `old = null` the
  first time an attribute is set, so the body silently does not run. The refusal is recorded
  in the flight recorder (`__tjs.records()`). Spell what the platform actually passes:
  `old = null` (a null default accepts any value), or mark the method `/* unsafe */`.

- **A function-valued member of a Type's example means "a function here"** (#3595, from
  tosijs-native). `Type Bridge { example: { call: (a) => 1 } }` refused every value, one of
  exactly that shape included, in both checkers: the inline one compared a function example
  by identity, and the runtime `Type`'s inference has no schema for a function. Both now
  check `typeof === 'function'` there (a Type, which is also callable, is still a Type).

- **A package written in `.tjs` emits as a package TypeScript and plain Node can use**
  (#3587, #3588, #3589, #2749; found by tosijs-native, the first project written natively in
  `.tjs`). Five defects, each a reason such a package could not be published:
  - a barrel `index.tjs` of `export { … } from './x.tjs'` emitted an EMPTY `index.d.ts`, so
    the package's entry point had no types (re-exports now pass through);
  - a Type imported from another file was declared `any` (it is now named, and imported
    with `import type`);
  - a Type named inside another Type's example was `any[]` (`products: Product[]`);
  - a `null` dictionary-default member, which admits any value at runtime, was declared
    `null`, so a TypeScript caller passing a real value failed to compile (now `any`);
  - `tjs emit` wrote `./x.tjs` imports into the `.js` it emitted beside `x.js`, so the
    output loaded only where a `.tjs` loader was installed. Relative `.tjs` specifiers are
    now rewritten to `.js` in emitted files (JavaScript and `.d.ts`; found by parsing, so a
    mention in a string or comment is untouched), which `moduleResolution: nodenext` also
    requires. The library's `tjs()` keeps source specifiers, since its output runs where
    `.tjs` sources do.
  Pinned end to end: a multi-file package is emitted with the CLI, a consumer is compiled
  against it with TypeScript (`nodenext`, strict, skipLibCheck off), and the `.js` runs in
  plain `node`.

## [0.14.0-rc.9] — 2026-10-28

On the `rc` dist-tag. A VM security fix (#3531), destructured and array-pattern parameters
checked as the one argument they are at any position, `wrap()` working with emitted
metadata, `:?` returns checked like parameters, tosijs proxies seen through everywhere, Type
values as defaults, and sharper error messages. Pin `0.14.0-rc.9` exactly to test it.

### Read this first: what changes since rc.8

- **Published metadata (BREAKING, toward correctness):** a destructured or array-pattern
  parameter beside others is ONE entry in `__tjs.params`, keyed by its binding pattern
  (`{ w, h }, k`, not `w, h, k`); a lone destructured parameter's `__tjs` gains
  `destructured: true`.
- **A `:?` return with a wrong member now returns an error** (#3408).
- **A list element failure names the element** (`f.xs[1].t`), not the list (#3407).
- **`function f(xs = [Book])` takes a list of Types**, not of Books (#3411).
- **`dialect: 'js'` no longer runs return examples** (#3412).

### Security since rc.8

- **The VM's equality runs no host code on the values it compares** (#3531). rc.7's tosijs
  see-through (`unwrapBoxed`) added a property GET of `Symbol.for('xin-value')`, and the VM's
  `==` used it: a GET runs a getter or a Proxy trap, which the VM's equality promises never to
  do. A guest cannot make such a value (guest values are membraned data), but a host object
  returned by an atom tagged `effects: 'pure'`, which skips the membrane, reached the
  comparison and its trap ran, unmetered, inside guest evaluation. The `instanceof` checks it
  replaced also consulted a Proxy's `getPrototypeOf` trap. The VM now unwraps boxed primitives
  by slot reads only (`unwrapBoxedSlots`) and does not see through tosijs proxies, which no
  guest value is. Affected: 0.14.0-rc.7 and rc.8 for the GET; the `instanceof` trap reads are
  older. Not a sandbox escape: the code that ran is the embedder's own object.
  **0.14.0-rc.7 and rc.8 are deprecated on npm** (2026-10-29), pointing at 0.14.0-rc.9 or
  later. No GitHub advisory: a guest program cannot trigger it, only a host object returned
  by an embedder's own `effects: 'pure'` atom.

### Fixed since rc.8

- **A Type in a default is a value: functions can take Types** (#3411). In
  `function shelf(types = [Book])` or `function accepts(t = Book)`, the default is the Type
  itself, and a function may take Types and operate on them. The default was read as an
  EXAMPLE ("a list of Book-shaped values"), so the parameter refused its own default and
  every other Type passed in. A declared Type named in a default (including a
  dictionary default's members) is now checked as a Type (`Expected Type for …` when it is
  not one). The colon form keeps its meaning: `b: [Book]` is a list of Books. **Behaviour change:**
  `function f(xs = [Book])` used to check its argument as a list of Books (and refuse its
  own default); it now takes a list of Types. For a list of Books write `xs: [Book]`.
- **The `.d.ts` declares a parameter with a non-literal default as optional** (`t = Book`,
  `x = now()` were required), and parenthesizes a list of functions (`(() => any)[]`, not
  `() => any[]`).
- **Signature examples run under one condition, at build and in the test runner** (#3412):
  native TJS. Under `dialect: 'js'` the build ran a return example (and failed the
  transpile on a wrong one) while `tjs().testRunner` never did. A return annotation is a
  worked example only in native TJS, so the `js` dialect now runs neither; native TJS still
  runs both. **Behaviour change:** a `dialect: 'js'` source with an inconsistent return
  example now transpiles.
- **tosijs proxies are seen through everywhere a value is checked** (#3406). rc.7 listed three
  places that still refused one: a proxy held inside a plain object or list
  (`{ a: calculator.x }`, `[calculator.x]`), an overloaded function's dispatch (adding an
  overload made `f(calculator.x)` "no matching overload"), and a `:?` return check. Each is
  now checked as the value the proxy wraps, while the body, the chosen overload and the
  caller still receive the proxy itself. Member and element checks pay nothing on the happy
  path (the unwrap runs only after the raw check has failed); dispatch unwraps each argument
  it tests, measured at a few percent on a dispatch-only micro-benchmark. A message names
  the wrapped value's type, not "object".
- **A type error names the contract and the place** (#3407). A member of a named Type said
  `Expected declared`; it says `Expected Book`. A list whose ELEMENT failed said
  `Expected array of object …, got array of object`; it now names the element and, for a
  shape, the member: `Expected string for 'f.xs[1].t', got number` (found on the failure
  path only, carrying the whole list as `received`). A dictionary-default member says
  `array of string`, not `array`. **Message change:** code that matched the old list-level
  wording for an element failure will see the element-level one.
- **A checked return (`:?`) is checked like a parameter** (#3408). **Behaviour change,
  toward correctness:** a `:?` function returning an object with a wrong member now gets an
  error back. Named Types were resolved only for parameters, so `:? Book`, `:? [Book]` and
  `:? { top: Book }` checked nothing; and the return check tested only the outer kind, so
  even `:? { top: 0 }` returned `{ top: 'x' }` unrefused. Both are fixed; a key written
  `key = value` (a return default) may still be absent, `:!` is still the opt-out, and a
  union with a member that cannot be resolved stays unchecked, as for parameters.
- **An array-pattern parameter is one parameter, checked as one** (#3497). `g([a, b], k)`
  had no metadata entry for its array parameter (it listed only `k`), so position-based
  readers misaligned; a TS-style annotation (`[a, b]: [0, 0]`) was erased unchecked, and
  `null` THREW. It is now named, checked as an array (against its annotation when there is
  one), then destructured, like an object pattern (#3443); the `.d.ts` declares it.
- **A `Type` with an object or list example is aliased to its shape in the `.d.ts`** (#3482).
  `export Type R { example: { x: 0, y: 0 } }` was declared `type R = Record<string, any>`, and
  a list example `any[]`, so TypeScript consumers got no member types for any parameter of
  such a Type. The declaration finder's patterns stopped at the first `,` or `}`, and the
  alias was guessed from the example's first character. The example is now read as an
  expression (acorn decides where it ends) and inferred as a parameter's example is:
  `type R = { x: number; y: number }`. Class methods with a list example now agree with
  functions (`string[]`, not `any[]`).
- **Pushing a release tag no longer re-runs the full suite it was attested on** (#3445).
  `release:ready` attests every lane on the tagged tree; the pre-push hook re-ran ~8 minutes
  of suite anyway, git's connection to the remote sat idle until GitHub dropped it, and a
  green hook ended in exit 141 with the tag not pushed. The hook now accepts a valid
  attestation (`scripts/attested.ts`), and says what a 141 means if a long run still ends in
  one.
- **`wrap(fn, fn.__tjs)` works** (#3496). `wrap` is documented to take a function's `__tjs`
  metadata and refused every call given it, `wrap((a, b) => a + b, add.__tjs)(1, 2)`
  included: its `checkType` understood kind names (`'number'`) and Type objects, and emitted
  metadata carries transpiler descriptors (`{ kind: 'integer' }`). `checkType` now checks a
  descriptor strictly, every kind handled. A named `Type` cannot be resolved from metadata
  alone, so it passes and the flight recorder says so once per path. And `wrap` treats a
  single object argument as named parameters only when the metadata says the function
  destructures it: emitted `__tjs` now carries `destructured: true` for a lone destructured
  parameter. It used to GUESS from the call, which read `area(r: { w, h })` called with
  `{ w, h }` as `r: undefined`.
- **The `.d.ts` declares a destructured parameter as one parameter** (#3444).
  `export function area({ w: 0, h: 0 })` was declared `area(w: number, h: number)`, so a
  TypeScript consumer was told to call `area(2, 3)`, which the runtime refuses, and the
  correct `area({ w: 2, h: 3 })` did not type-check. It is now
  `area({ w, h }: { w: number; h: number })`: a member with a default is optional, a
  default for the whole argument makes the parameter optional, and the TypeScript spelling
  keeps its declared type (`({ x }: R)`).
- **A destructured parameter beside others is one parameter, checked as one** (#3443).
  **BREAKING (toward correctness), metadata:** `function area3({ w: 0, h = 2 }, k: 0)` listed
  its parameters as `w, h, k`, flattening the pattern's members in among the positional
  ones, so everything reading `__tjs.params` by position misread it (the `.d.ts` declared
  three parameters; callback-shape checks compared the wrong positions). It is now
  `{ w, h }, k`: one entry, keyed by its binding pattern and typed as the object it
  destructures. A lone destructured parameter's metadata is unchanged (its members, with
  `isDestructuredParam`). The parameter itself now behaves like a lone one: named, checked
  as an object (a monadic error passes through; `null` is a returned error, no longer a
  throw; a failing member carries the object as `received`), then destructured, and the
  TypeScript spelling `(x, { b }: { b: 0 })` is checked in any position instead of erased.
  An ARRAY pattern is now one parameter too (#3497).

## [0.14.0-rc.8] — 2026-10-20

On the `rc` dist-tag. A type error now carries what you passed (`err.received`), destructured
parameters are checked as the one argument they are (in both the TJS and the TypeScript
spelling), converted TypeScript gets native TJS for them, and runtime errors name the right
line again after a declaration block. Pin `0.14.0-rc.8` exactly to test it.

### Read this first: what changes since rc.7

- **A destructured parameter returns an error instead of throwing** for `null` or a missing
  argument, and passes a monadic error through. Code that caught the throw gets a value.
- **`({ w, h }: Shape)` is now checked.** Converted TypeScript under `TjsStrict` that passed
  ill-typed objects to a destructured parameter now gets errors back.
- **AJS refuses `({ a }: T = d)`**, as it already refused `({ a } = d)`. It used to accept it
  and silently drop the default.

### Added since rc.7

- **A type error carries the argument it refused, as `err.received`.** It said only "got
  object", so the receiver could not see what it got: in particular an `Error` the caller
  made, which a typed function refuses (only TJS's own errors pass along) and which was then
  lost. When a member fails, `received` is the whole argument (`{ width: 3 }` for a missing
  `r.height`), not the member's `undefined`: the error is that the thing passed was not a
  rectangle. A destructured parameter (`({ w: 0, h: 0 })`) is the same: see below. `received` is non-enumerable, so logging or `JSON.stringify` of the error never walks
  a large or cyclic value. Set at every site: the inline runtime in emitted code, the global
  and per-instance `typeError`, and the callback-shape checks.
  The refused value is held by the error, so the flight recorder's ring (the last 64
  errors) keeps it reachable until the ring wraps or `__tjs.clearRecords()`; mind that for
  large or sensitive payloads.

### Fixed since rc.7

- **`({ w, h }: Shape)` is checked, and `fromTS` writes native TJS for it where it can**
  (#3434). The TypeScript spelling of a typed destructured parameter had its annotation
  erased, behind a comment claiming it was "captured for metadata"; nothing captured it, so
  `d({ w: 'x' })` returned `NaN`. It is the shape `fromTS` emitted for every typed
  destructured TypeScript parameter, so converted code checked none of them, even under
  `TjsStrict`. Now the argument is checked against `Shape` (an object shape, a named
  `Type`, a union) and then destructured, renames included, and a default written after
  the type (`({ a }: T = {})`) is kept, where it used to be erased along with the type.
  `fromTS` now emits `({ w: 0.0, h: '' | undefined })` for an inline type whose members
  native syntax can hold, and keeps `: Shape` for a named type, a rename or a pattern
  default (in a pattern, `{ w: R }` is a RENAME, so a pattern cannot name a type).
- **A destructured parameter is checked as the argument it destructures** (#3432). It is one
  parameter expecting an object, and now behaves like every other: a monadic error passes
  through unchanged, and anything else that is not an object (`null`, nothing, a string, an
  `Error` you made) is a returned type error carrying it as `received`. Before, the
  destructuring happened in the parameter list, before any check could run: `null` or a
  missing argument THREW ("Cannot destructure"), an error passed in became a new "got
  undefined" error, and a member's failure could not name the object. **BREAKING, toward
  correctness:** code that caught that throw now gets an error value. The emitted function
  takes `__tjs_arg` and destructures it in the body after the checks; `.length`, defaults,
  extra arguments, `this` and `arguments` are unchanged (`dialect: 'js'` is untouched).
- **A runtime type error names the right line after a declaration block** (#3404). A `Type`,
  `Generic`, `FunctionPredicate`, `Union` or `Enum` declaration became one line of output, so
  every line below it moved up: a function on line 5 under a four-line `Type` reported
  `<source>:2:…`. The rewrites now keep the source's line count (`lineKeeper`).

## [0.14.0-rc.7] — 2026-10-19

On the `rc` dist-tag. TJS now works with tosijs models (typed functions, `==`, truthiness and
`given` see through tosijs proxies), live pages check signature examples, list parameters check
their elements, and the TJS book's Part I, "Learn to Program", is complete. Pin
`0.14.0-rc.7` exactly to test it.

### Read this first: what changes since rc.6

- **A list parameter checks every element** against a named `Type` or an object shape
  (`[Book]`, `[{ title: '', pages: 0 }]`). Code that passed a malformed element to one now
  gets an error value back.
- **Checks accept a tosijs proxy as the value it wraps.** Additive for code that never meets
  tosijs; nothing else is unwrapped.

### Fixed since rc.6

- **A list parameter checks its elements against a named Type or an object shape** (#3403).
  `books: [Book]` was "array of any" and accepted `[5]`: a declared `Type` was resolved for a
  lone parameter and for union members, but not inside a list or an object member. And
  `[{ title: '', pages: 0 }]` checked only that each element was an object. Both are now
  checked element by element (members, nested shapes and nested lists), the same order of
  work as `[0.0]`, which was always checked. Code that passed malformed elements to such a
  parameter now gets an error value instead.
- **`Book | null` admits null again wherever a declared Type is promoted** (rc.7 review): as a
  parameter, a list item (`[Book | null]`) and an object member (`{ top: Book | null }`).
  Promotion replaced the type with a bare declared one and dropped the `| null`; the
  top-level case had done so since before rc.6.

### Added since rc.6

- **TJS works with tosijs models.** tosijs hands back proxies (`calculator.x` wraps a
  number). JavaScript uses them dynamically; TJS did not, which is exactly the friction it
  exists to remove: a typed `press(state: { x: 0 })` said "expected integer, got object" for a
  model whose `x` was a good number, `calculator.op == '+'` was false, and `if
  (calculator.op)` was true for an empty op. Now parameter checks, `Type.check` (and
  `Generic`/`Enum`/`Union`), `==`, truthiness, `given` and the test harness's `expect` all
  see the value a tosijs proxy wraps, while a function still RECEIVES the proxy, so writing to
  it updates the model and the page. Recognised by tosijs's `Symbol.for('xin-value')` (no
  dependency on tosijs); a `Date`, a `valueOf` object or a boxed primitive is checked as
  before. Checked with real tosijs proxies on the doc site.
  **Not yet:** a proxy held INSIDE a plain object or list (`{ a: calculator.x }`,
  `[calculator.x]`), an overloaded function's dispatch, and a `:?` return check still see the
  proxy and refuse it. Pass the model or the member directly, as the book does.

- **`tjs().testRunner` checks signature examples** (#3375). A return example
  (`greet(name: 'Ada'): 'Hello, Ada!'`) is a test the build runs, but the runner `tjs()`
  returns, which hosts such as tosijs-ui run for live examples, carried only `test` blocks, so
  a wrong example was never reported on a live page. It now carries them too, compared with
  the shared `expect().toEqual`. An example whose call throws is inconclusive (PRINCIPLES.md):
  it goes to a new `inconclusive` list in the runner's result, never to `results`, which hosts
  count. Async examples are not checked, as at build time. A differential test requires the
  page and the build to agree on every signature example in the docs.

### Documentation since rc.6

- **The TJS Programming Language, Part I, "Learn to Program", is complete** (chapters 0 to
  11): values and names, functions, checking your work (tests, taught right after
  functions), decisions, repetition, lists, things with parts, responding to the reader, a
  calculator (XY registers, a tested brain driving a tosijs model) and when things go wrong.
  Every example runs on tjs.tosijs.net and ends in tests the reader can see pass, and
  `doc-snippets.test.ts` runs them with stand-ins for the page.

## [0.14.0-rc.6] — 2026-10-17

On the `rc` dist-tag. One breaking change: `||` in a type is a compile error (a union is
spelled `|`). Plus a fix for annotations on nested functions, and documentation whose every
example now runs its tests. Pin `0.14.0-rc.6` exactly to test it.

### Read this first: what breaks since rc.5

- **`||` in a type is a compile error**, in TJS and AJS alike. `x: '' || null` never meant
  "string or null" (it rejected null); write `'' | null`. The error names the corrected
  spelling. A default (`x = a || b`) is still JavaScript, but its type is still read from the
  left side, so `x = '' || null` is a string parameter that defaults to null and then rejects
  it: write `x = '' | null` for a nullable string with a default.

### Changed since rc.5

- **BREAKING: `||` in a type is a compile error; a union is spelled `|`** (#3148).
  `x: '' || null` and `): T || null` were taught by three docs as "string or null" and never
  meant it: an example's `||` is JavaScript's OR, so the type was only the left side and
  `null` was REJECTED. Every type position (an annotation, an object member, an array
  element, a union member, a return annotation) now fails with the corrected spelling:
  `` `'' || null` reads as JavaScript OR … Write `'' | null` (one bar).`` This covers
  optional annotations (`x?:`) and AJS too, and the error points at the annotation's own
  line. A default (`x = a || b`) is still JavaScript (see "what breaks" above). The docs now
  use `|`, and a docs-tombstones row keeps it.

### Fixed since rc.5

- **An annotation on a function inside an expression is no longer a default value** (#3147).
  `items.map((x: 0) => x * 2)`, `a || ((x: '' | null) => x)`, `{ f: (x: 0) => x }` and
  every other arrow or function expression that is an operand, argument, array element,
  property or return value emitted `(x = 0) => …`: the example became a runtime default, so
  a missing argument was silently `0` (or, for `'' | null`, the number `0`). The annotation
  is now stripped, as it already was for declarations, named arrows, methods and class
  fields. These functions are not validated yet; that is a larger change.

### Documentation since rc.5

- **Every book snippet runs its own tests.** Twelve snippets across TJS-SYNTAX, TJS-FOR-JS,
  TJS-FOR-TS and the TJS guide wrote a return example as a type (`greet(name: 'World'): ''`,
  which TJS checks as a worked example: `greet('World')` must return `''`), or used the `||`
  union that never worked. Each now has a real worked result or `:!` (type only), and
  `doc-snippets.test.ts` runs every doc's tests: its opt-in list is gone.
- **TJS-FOR-JS described the return test wrongly.** It said TJS checks the result "is a
  number"; it checks the result equals the example (deep equality). It now says so, and
  shows `:!` for a type-only return.
- **The TJS Programming Language opens with "Learn to Program"**, chapters 0 to 2 (How This
  Book Works, Hello Screen, Values and Names): programming from scratch, in the page, each
  example ending in a test the reader can see pass. A draft; more chapters follow.

## [0.14.0-rc.5] — 2026-10-09

On the `rc` dist-tag. `==` and `!=` now group their operands as JavaScript does (the rewrite
is an AST pass), `tjs()` returns its runtime prelude separately for REPL hosts, and npm links
to the documentation site. Pin `0.14.0-rc.5` exactly to test it. This section is what changed
since rc.4; the notes for 0.14.0 as a whole, rc.1 to rc.4, are under the 0.14.0 heading
below.

### Read this first: what changes since rc.4

- **`==` and `!=` group their operands as JavaScript does.** Expressions the old rewrite
  grouped wrongly now compute what JavaScript would: `await f() == 3` compares the awaited
  value, `a & b == c` is `a & (b == c)`, and nested or chained `==` work. Code that relied
  on the old grouping gets a different, correct, answer. Everything else is additive.

### Added since rc.4

- **`tjs()` returns its runtime prelude separately**: `prelude` (the inline-runtime setup at
  the head of `code`, or `''`) and `body` (`code` without it). `code` is unchanged. A REPL
  host evaluating one line needs them apart: with the setup in front, a declaration evaluated
  to the setup's last assignment, and a line needing `await` had no value (tosijs-ui's
  console, #3134). Evaluate `prelude`, then `body`, in one scope.

### Fixed since rc.4

- **npm, the README and `llms.txt` name the documentation site** (tjs.tosijs.net) as the
  project's home. `package.json` had no `homepage`, so npm linked nowhere, and the README
  and `llms.txt` sent readers only to the older playground. A test keeps the three in
  agreement.
- **`==` and `!=` group their operands as JavaScript does.** The rewrite to `Eq`/`NotEq` was a
  text scanner that re-derived operator precedence by hand, and got it wrong in several
  shapes, all in released versions:
  - `await f() == 3` compared the Promise: `false` when `f()` resolved to 3. Reported by
    tosijs-ui's review of its dialect-aware console.
  - `(a == b) == (b == c)` emitted garbage that threw at run time, and `a == b == c` did not
    parse.
  - `a & b == c` (and `|`, `^`) grouped as `Eq(a & b, c)`; JavaScript reads `a & (b == c)`.
  - `void a == b` became `void Eq(a, b)`.

  The rewrite is now an AST pass in the emitter, beside the truthiness rewrite
  (`bool-coercion.ts`), so acorn decides the grouping. A differential test generates 928
  expressions across the precedence table and requires TJS to agree with JavaScript
  (`equality-precedence.test.ts`). Test and mock bodies that use `await` now get the
  truthiness rewrite too: their wrapper did not parse `await`, so such a body was skipped.

  Inside a `wasm {}` body, `==` and `!=` stay JavaScript's, which is what the compiled WASM
  does (`x != x` is the NaN test), so the body and its implicit JavaScript fallback agree. An
  explicit `fallback {}` is the author's own code and has TJS semantics, as before.

## [0.14.0] — unreleased

> **Release candidate: `0.14.0-rc.4`** (2026-10-07), on the `rc` dist-tag. WASM blocks mean
> what they say (a block that cannot compile is a compile error, and the WASM path and the
> fallback give the same answer), `predicate =>` is removed, and the deprecated `unsafe` prefix
> no longer escapes `var` or `eval`. Read "what breaks since rc.3" first. Pin `0.14.0-rc.4`
> exactly to test it.

### Read this first: what breaks since rc.3

Each of these breaks toward correctness; none has a compatibility switch.

- **A `wasm {}` block that cannot compile stops the transpile** (it used to ship its
  `fallback {}` with a warning). Fix the block, or drop the `wasm` wrapper. The error names
  the construct, the function and the line.
- **`%` and bitwise operators on f64 operands in WASM are refused**; `/` is now JavaScript's
  division even for two `i32` operands (`Math.trunc(a / b)` for integer division).
- **`predicate => expr` is a compile error**: write `predicate(x) { return expr }`.
- **`unsafe var` and `unsafe eval(…)` are refused**, as the docs already said: there is no
  escape for either. `unsafe` itself is deprecated; `LegacyDate(x)` replaces `unsafe new
  Date(x)`, and `new X(…)` on a class declared in the file needs no escape (drop `new`).
- **Emitted JavaScript has a different shape.** Test blocks, doc comments and the hashbang are
  blank lines in the output rather than removed, so line N of the output's body is line N of
  the source. Anything matching exact emitted text will see different line numbers.

### Changed since rc.3 — a `wasm {}` block means what its `fallback {}` means

Found by reviewing the WASM QuickStart, whose first example printed `undefined` (it promised
7). Each is pinned by `src/lang/wasm-semantics.test.ts`, which runs functions through WASM and
through the fallback and requires the same answer.

- **A statement that computes a value and throws it away is a compile error** that names the
  fix (`write \`return a + b\``). A block returns only with `return`, as in JavaScript; a bare
  final `a + b` compiled to a function with NO result. ASSUMPTIONS L6 decided this in 2026-08
  and it was never built. It found the same bug in `examples/wasm-demo.tjs` (three functions
  returning `undefined`) and in the prose of the `wasm-basics` example.
- **`/` is JavaScript's division** in a wasm block, even with two integer operands (`7 / 2` is
  `3.5`, not `3`). The old integer-division warning is retired: there is nothing left to warn
  about.
- **`let` is block-scoped**: two sibling `for (let i …)` loops compile, instead of colliding
  ("Duplicate local declaration") and silently running the fallback.
- **The WAT comment says `(result f64)` only when the function returns a value.**
- An unsafe (`!`) function no longer warns that a typed-array parameter "could not be resolved
  to a runtime type": it asked for no checks.
- **A `wasm` block that cannot compile is a compile error** (BREAKING for code that relied on
  the quiet fallback). `wasm { }` is a request for WASM; an unsupported construct used to ship
  the block's JavaScript `fallback {}` with a warning. The error names the construct. `fallback {}`
  remains for a RUNTIME that cannot run WASM. Turning this on found four test fixtures and a
  DOCS-TJS example that had never compiled (each passed by running its fallback), the DOCS-TJS
  page teaching a `wasm (…)` form that was never implemented, and the bugs below.
- **One bad block could take WASM away from a whole file.** `%` (or a bitwise operator) on f64
  operands emitted an `i32` instruction on f64 values; the module was invalid, so every block in
  the file ran its fallback while each reported success. It is now refused, naming the operator,
  and every emitted module is validated, so a future codegen bug of that kind is a compile error
  rather than a module that fails to load.
- **`f32x4_load`/`f32x4_store` need a typed-array parameter**, as `arr[i]` did: on a plain array
  they compiled and then threw a WebAssembly `RuntimeError` on the first call.
- **A file with WASM loads where WebAssembly is unavailable** (iOS Lockdown Mode): its memory
  was created unguarded at load, so a file whose blocks use typed arrays threw before any
  fallback could run. Its blocks now run their fallbacks and `wasmBuffer` hands out ordinary
  typed arrays.
- DOCS-WASM documents `i32` (in `wasm function` declarations) with its measured semantics:
  truncating and wrapping at the call, wrapping `+ - *`, a throwing `% 0`.

### Fixed since rc.3

- **Compile errors name the right line.** `extractTests` cut test blocks out, collapsed blank
  lines and trimmed, so every error below a `test` block or a few comment lines pointed at the
  wrong line (`src/lang/diagnostic-lines.test.ts`).
- The WASM QuickStart is rewritten: every example asserts its result and compiles to WASM
  (checked), and it states the supported subset and that anything else is a compile error.
  DOCS-WASM no longer lists `Math.sin`/`cos`/`log`/`exp`/`pow` as supported; a block using one
  does not compile.
- The TJS-vs-TypeScript page shows what each compiler actually did: what the program printed,
  or its compile error quoted verbatim (checked by `differences.test.ts`), instead of a
  one-row table where **rejected** sat where a printed value would go.
- Diagnostics no longer recommend the deprecated `unsafe` marker: `var` and `eval` have no
  escape, a raw `Date` is `LegacyDate(x)`, and `new P` simply says to drop the keyword. The
  docs no longer teach it either (a `docs-tombstones` row keeps it that way), and the
  CodeMirror integration no longer offers it as a completion; it offers `LegacyDate`.
- **`unsafe var` and `unsafe eval(…)` compiled** although every diagnostic said there was no
  escape: the checks ran on source with the `unsafe` spans masked. They now run on the raw
  source. `dialect: 'js'` still accepts both (TJS ⊇ JS).
- **`wasm` is TJS syntax only.** Under `dialect: 'js'`, `return wasm` followed by a block on
  the next line (legal JavaScript, by ASI) was rewritten and then refused.
- **The wasm compile error wins over a failing signature example**: the block is compiled
  before the transpile-time tests run, so `%` on f64 operands no longer reports "signature
  example is inconsistent… got undefined".
- **Each failing wasm block is reported at its own line**, and a `wasm function` by name. Two
  blocks sharing a first line both pointed at the first, and a repeated body pointed at the
  previous block's `fallback {}`.
- **`wasm function f(x: 0.0): 0.0` parses.** The return annotation accepted only an
  identifier (`: f64`), so the idiomatic numeric example was passed through as JavaScript and
  failed with "Unexpected token".
- A WASM module that is invalid with no single block responsible fails every block (a
  compile error) rather than being returned as success.
- **`stripModuleSyntax` parses** instead of using line regexes: a template literal with a line
  starting `export` survives, and top-level `await` and imports are handled. It now shares its
  implementation with `stripExports`.
- **Blanked regions cost lines, not bytes.** The padding that keeps offsets during the rewrite
  is dropped from the output (trailing whitespace only, never inside a string or template), so
  100 lines of doc comment no longer triple a module's size.

### Added since rc.3

- **`stripExports`** from `tjs-lang/lang` and `tjs-lang/browser` (#3109): turns a module into a
  script body by dropping `export` and removing imports, literal-safely. tosijs-ui's live
  examples need it to run a module's code inline.

### Removed since rc.3

- **`predicate => expr` is gone.** It saved only `{ return }` over `predicate { return expr }`
  and was a third predicate spelling to learn. It is now a compile error that names the
  replacement, in `Type` and `Generic` alike (so it can never be silently ignored and accept
  every value). It never worked in a release: in 0.13.x it was accepted-and-ignored, then
  refused; only the 0.14 release candidates ran it.

> **Release candidate: `0.14.0-rc.3`** (2026-10-06), on the `rc` dist-tag. It adds the doc-site
> work below: every playground example runs on tjs.tosijs.net, and four library fixes it found.
> 0.14.0 final follows once the books (the site's four volumes) are reviewed as self-hosted
> tutorials. Pin `0.14.0-rc.3` exactly to test it.

Found by sweeping every page of the doc site (tjs.tosijs.net) with a headless checker; the
full account is `docs/reviews/0.14.0-site-readiness.md`.

### Fixed since rc.2

- **`tjs(source).testRunner` runs tests with TJS semantics.** It was built from the raw test
  bodies, before the `==` / boxed-primitive / `extend` rewrites `tjs test` applies, so a caller
  running it (tosijs-ui's live examples) reported passing tests as failing. A module whose only
  `==` was in its tests also left `Eq` out of its inline runtime.
- **`extractTests` ignores `/# … #/` doc comments itself.** It is public, and on raw source an
  apostrophe in a doc block's prose opened a string in its scanner, so every test after the
  block vanished (37 playground examples open with one); a `test` quoted inside a doc block
  was extracted, which TJS-SYNTAX.md says is inert.
- **The import resolver loads packages that have dependencies.** jsDelivr's `+esm` bundles
  import their dependencies by root-relative path (`"/npm/acorn@…/+esm"`), which resolved to
  OUR origin and 404ed. `rewriteCdnBody` (new, exported) points every CDN body's root-relative
  imports back at that CDN, literal-safely; `rewriteEsmShBody` delegates to it and now also
  handles minified output.

### Added since rc.2

- `generateDocs` / `generateDocsMarkdown` in `tjs-lang/browser`, which tosijs-ui's built-in
  `tjs` dialect feature-detects to show an example's Docs tab.
- `bun run preview:site` (the deploy build, served locally, not published),
  `scripts/check-site-examples.ts` (does every live example run and show something), and
  `scripts/build.ts --only <targets>`.

> **Release candidate: `0.14.0-rc.2`** (2026-10-05), on the `rc` dist-tag (`npm i tjs-lang@rc`).
> It carries everything since rc.1 below: the membrane rebuilt to construct its own copy (JSON
> data plus `Date`, no `structuredClone`), capability-boundary refusals that end the run, IO
> atoms that must declare an input schema, the method table, the linear regex engine, and the
> per-atom timer changes. Pin `0.14.0-rc.2` exactly to test it.
>
> **Release candidate: `0.14.0-rc.1`** (2026-09-26), on the `rc` dist-tag (`npm i tjs-lang@rc`);
> `latest` stays on 0.13 until 0.14.0 is final. It carries everything below, including the
> admission work from the final re-reviews (the 8KB source cap, the deprecation of
> `vm.run(source)`, and every run option read once and checked). `^0.14.0` does NOT admit a
> prerelease, so pin `0.14.0-rc.1` exactly to test it. Published through the OIDC workflow,
> vouched for by `release-attestation.json`.
>
> **Since rc.1 — `tosijs-schema` floor raised to `^1.12.0`** ([#58](https://github.com/tonioloewald/tjs-lang/issues/58)).
> tjs-lang unwrapped schema builders by duck-typing (`x?.schema ?? x`, `'schema' in x`) in eight
> places, so a value that merely HAD a `schema` key was read as a builder: a Type example such as
> `{ label: '', schema: { version: 0 } }` had its `schema` field used as the type, and a JSON Schema
> with a stray `schema: true` became accept-all. The same check sat in the VM's guest-facing
> `Schema.isValid` / `Schema.response`. tosijs-schema 1.12 brands builders (`isBuilder`); every
> site now asks it. The old `^1.5.1` range was also floating fresh installs onto 1.12 (four
> breaking validation releases later) while the code assumed 1.5, so the range now names the
> version the code is written against. The LLM batteries' output schema is back to named fields
> (`{ role?, content?, tool_calls? }`) via `.open`, still admitting provider additions such as
> `reasoning_content`, where `s.record(s.any)` rejected nothing at all.
>
> **Since rc.1 — AJS AST format v2: a bare string is always a literal** (board #1860). v1 stored a
> literal string and a variable reference the same way, and the VM read it as a reference when a
> variable by that name was in scope: `let x = 5; const s = 'x'` made `s` 5, `['x', 'z']` became
> `[5, 'z']`, and input that chose variable names could redirect a literal. The transpiler now
> writes `$ajs: 2`, where every reference is an explicit node and a bare string is data; an
> object literal shaped like a VM node (`{ $expr: … }`) stays data too. A `while` body is now a
> block, so a `const` in it no longer fails on the second iteration (#2343). **Compatibility:**
> v1 ASTs — every stored procedure, everything the fluent builder writes (it still writes v1;
> its strings mean references) — keep v1 meaning, read by the same VM; an older VM refuses a v2
> AST by version, by name. Frozen by golden fixtures for both versions.
>
> **Since rc.1 — custom atoms receive VALUES.** An atom defined with `defineAtom` and written like
> the documented example (`async ({ url }) => fetch(url)`) received the variable's NAME when a
> program passed one — `echo({ v: local })` got `"local"` — in every earlier release. The VM now
> resolves inputs before calling it, and runs it under a context where `resolveValue` is the
> identity: an atom written the old way, calling `resolveValue` on its own inputs, keeps working
> and cannot re-evaluate guest data shaped like `{ $expr: … }` as code. Dynamic `cost` and
> `timeoutMs` functions see the same resolved input — so a cost function written against raw
> nodes (testing for `$expr`, or for a variable name) now receives values and must be updated.
> Steps an atom builds and runs itself — on the context it received, or on a scope from
> `createChildScope(ctx)` — run normally under either setting: the VM never runs a step under
> the resolved-inputs context. Running them on a COPY of that context, or after replacing its
> `state`, is refused by name rather than silently writing the caller's scope. **An atom that
> takes steps AS INPUT** must pass `{ resolveInputs: false }`, or its steps arrive evaluated
> as values.
>
> **Since rc.1 — the VM's budgets are invariants, not patches** ([`docs/vm-budgets.md`](docs/vm-budgets.md)).
> Five rc.2 review rounds in a row found one more place where guest code could allocate or hold
> memory that neither fuel nor `maxHeapBytes` saw — each fix closed one place and the next
> review found another. They are now three rules, each checked by a test that probes behaviour:
>
> - **Nothing allocates before it is charged.** Every operation whose allocation depends on
>   runtime data bounds it from its INPUTS and charges fuel and heap first. `'x'.repeat(5e8)`
>   built 1GB and then charged; `Array.from({ length: 3e8 })` charged nothing — any guest could
>   take the host's memory for under one unit of fuel, no capability needed.
> - **Every byte that outlives its step is charged where it lands** — a bind, an in-place
>   insertion (`push`, `fill`, a Set's `add`), a `memoize` store, a loop's results.
> - **Everything holding guest values while guest steps run is measured** — scopes, memo caches,
>   arguments, and the arrays a loop holds. Before a run fails, the VM measures the live heap
>   from those (each object once, every slot a pointer) plus the in-flight allocations of the
>   steps still running, and fails only if THAT is over; the error reports it.
>
> **The guest method allowlist is now a table,** and a method without a declared allocation
> bound is not callable. Gone (breaking, though none of them could be used well): the legacy
> HTML string methods (`bold`, `link`, …), callback methods that guest code has no functions to
> pass to (`forEach`, `every`, `flatMap`, … — use `for…of` or `map`/`filter`/`find`/`reduce`),
> `matchAll` and the array `keys`/`values`/`entries` (they returned host iterators), the
> `__lookupGetter__` family (it returned a host function), and `toString` on a builtin function —
> `Date.toString()` returned the host's SOURCE CODE. A long string held under several names
> still counts once per name (fails closed). With everything since rc.1 below, the VM grew ~30%:
> `tjs-lang/vm-ast` is 37 KB gzipped, `tjs-lang/vm` 88 KB (68 KB at rc.1); the regex engine is
> about 7 KB of it, the closed schema dialect and data-only `Schema` about 2 KB.
>
> **Since rc.1 — `$predicate` runs only source the host trusts (breaking).** A predicate compiles
> to native JavaScript, and a syntactic verifier cannot make hostile JavaScript safe: one review
> round found five more routes from a "verified" predicate to the host's regex engine
> (`['(a+)+$',''].reduce(RegExp)`, a method passed as a callback, a tagged template, a class
> shadowing a name, overwriting a field). So the `$predicate` paths — `createPredicateEvaluator`
> (what `tjs-lang/schema` installs), `compilePredicateSchema`, `validatePredicateSchema` — run a
> source only if the host registered it with `trustPredicate(source)` (`tjs-lang/css`'s schema
> builders register theirs), or passed `trustAllPredicates: true`. Anything else fails closed,
> saying why. **Migration:** wrap your own `$predicate` sources in `trustPredicate(…)`; never
> trust a source that arrived as data. Running untrusted predicates on the AJS VM, whose surface
> is a closed table, is planned and will lift this. A source trusted after a first refusal runs at
> once (the evaluator checks trust on every call); its cache is bounded. In predicates, a
> string-literal pattern (`s.search('@')`) runs on the metered engine rather than being refused.
>
> **Since rc.1 — a Set, a Date and a regex are DATA (breaking).** A guest `Set` is an array of its
> items (`s.size` is its length, by every route; `Array.isArray(s)` is true), and a `Date` is a
> frozen object of its fields — `value` (ISO), `timestamp`, `year`, `month` (1–12), `day`,
> `hours`, `minutes`, `seconds`, `dayOfWeek` — all in UTC, so a program's output does not depend
> on the host's time zone (fields, `add` and `format` used local time). A Date has ONE form:
> guest `JSON.stringify`, a capability's input and the run result all see the object, where rc.1
> wrote the ISO string — write `d.value` for that. `Date(x)` takes every form a date takes: a
> guest Date, a stored one (an object with a numeric `timestamp`, which is what a Date becomes
> after JSON, a store or a capability), a number of ms within the Date range, or a string that
> parses; `Date.parse` is the same door. `format`'s keywords (`'ISO'`, `'date'`, `'time'`) are
> case-insensitive. A regex is an object of its `source` and flags. Their methods are the VM's, dispatched by kind, so a value never carries a
> function: the sealed wrappers and the run-result conversion that kept their methods away from
> guest code are gone, with the four defects the seventeenth re-review found in them (a recursive
> conversion that a deep or self-containing Set crashed, a `__proto__` key, an uncharged walk of
> an `Object.assign` target, conversion only at the result). `push` and `Object.assign` into a Set
> are refused (use its methods), and reading a method as a value (`const f = s.add`) is refused as
> it is on a string. An in-place insertion is now checked and charged BEFORE it happens, so a
> refused value is never left behind for a `catch` to reach. `agentRun` hands a sub-agent exactly
> what it was passed: it resolved the input twice (a string that named a caller variable became
> that variable's value) and evaluated an inline sub-agent's expressions in the CALLER's scope.
> Sub-agent and `runCode` arguments are null-prototype, like `vm.run`'s. `pick` again copies any
> own data property (`pick(arr, ['length'])`), never a function. An AST's `inputSchema` is
> validated before the run's timer exists, so it is now capped on the product — 1M argument paths
> and 1e8 schema-node × path steps (about 0.6s) — and refused past either: validate in the host,
> or pass smaller arguments. A guest-program fuzzer (`src/vm/guest-fuzz.test.ts`) now checks the
> invariants over random programs.
>
> **Since rc.1 — `httpFetch` follows no redirects and admits only known headers; the VM never
> writes to your capabilities (security, breaking).** In allowlist mode `httpFetch` compared only
> the hostname, so on Bun or Deno `file://<allowed-host>/etc/hosts` read a host file. Every request
> is now admitted by one rule: the scheme must be `http:` or `https:` in both modes, then the
> allowlist, and without an allowlist nothing is fetched.
> - **No redirects are followed.** A redirect (301, 302, 303, 307 or 308 with a `Location`) comes
>   back to the agent as data, `{ redirect: true, status, location }`, with `location` resolved to
>   an absolute URL, and fetching it is a new request, admitted like any other with its headers
>   and body chosen afresh. A 304 Not Modified, or any other 3xx, is an ordinary response. In a
>   browser `location` is null, because the browser hides it. Four review rounds each found another redirect rule the VM had
>   re-implemented wrong (credentials re-sent, then the body re-sent on 307/308, then on 301/302
>   for PUT, PATCH and DELETE), so the built-in client stopped following redirects. A host that
>   wants them followed supplies a `fetch` capability.
> - **Guest request headers are a closed list:** `Accept`, `Accept-Language`, `Authorization`,
>   `Cache-Control`, `Content-Language`, `Content-Type`, the `If-*` validators, `Range`,
>   `User-Agent`, `X-API-Key`, `X-Request-Id`, `X-Correlation-Id` and `X-Requested-With`. A host
>   adds names for a run with `context.allowedRequestHeaders`, as it does with
>   `allowedFetchDomains`. Anything else is refused by name, never dropped, and `X-Agent-Depth` is
>   refused even if a host lists it. A guest `Host` was sent as given, and on Bun behind a
>   Host-routing proxy it reached virtual hosts outside the allowlist. A denylist kept growing, and
>   so did an "X- except the routing ones" rule (`X-Envoy-Original-Dst-Host`, `X-Originating-IP`, …),
>   so neither survived.
> - **A host's own `fetch` capability** receives the agent's request after the same admission.
>   The request's SHAPE is admitted on every path, always: `http:`/`https:` only (`file://` read
>   a host file through the documented `safeFetch` on Bun), the method and the headers. Its
>   DESTINATION (`allowedFetchDomains`, with ports) is enforced on every path when set; without
>   it, a host fetch owns the destination. Redirects and `credentials` are then the host's; the
>   documented `safeFetch` refuses redirects and omits credentials.
> - **`Eval` and `SafeFunction` take a host-only `fetchPolicy`** (`{ domains, methods, headers }`),
>   forwarded to the run and never visible to guest code. Their `context` option is guest
>   variables, so before this they could not allow a POST at all.
> - **Migration, hosts with their own `fetch`:** an agent's POST, PUT or DELETE now needs
>   `context.allowedFetchMethods` (or `fetchPolicy.methods`), and headers outside the closed list
>   need `context.allowedRequestHeaders` (or `fetchPolicy.headers`).
> - **The built-in `httpFetch` is a demonstration capability, highly constrained by default**
>   (Tonio). A real deployment supplies its own `fetch`. **Methods:** GET and HEAD by default;
>   a host enables more for a run with `context.allowedFetchMethods`. CONNECT, TRACE and TRACK
>   are never admitted. **Ports:** an `allowedFetchDomains` entry admits the scheme's default port
>   only, unless it names one (`api.example.com:8443`); matching the hostname alone admitted
>   every port of an allowed host. **One admission, every path:** the agent's method and headers
>   are admitted the same way when a host's own `fetch` sends the request, so a host that needs
>   `Cookie` or PROPFIND declares it.
> - **No browser credentials:** cookies and HTTP auth are not sent (`credentials: 'omit'`); a
>   custom `fetch` can opt in.
> - **Bounded body:** the body is read under `membraneMaxBytes` (a larger declared
>   `Content-Length` is refused before reading) and aborted once the cap is exceeded, not
>   buffered whole first.
>
> Separately, `vm.run` used to write its default in-memory store INTO the capabilities object you
> passed, so two runs sharing that object shared the store, and a frozen object made the run throw.
> The VM now uses your capabilities object exactly as passed: it is never copied, wrapped or
> written, so a class instance with private fields or getters works as it always did.
> - The default store belongs to the run and is used only when your object has no `store`.
> - **Custom atoms:** read the store through `storeOf(ctx)`, not `ctx.capabilities.store`, which
>   is now the host's own object and undefined when the host gave none.
> - Each run, nested runs included, has its own default store.
> - A non-object `capabilities` is refused.
>
> In AJS, an atom input named `op`, `result`, `resultConst` or `resultAssign` is refused at
> transpile time: it was silently read as a step control field.
>
> Image URLs follow the same rule as a fetch, in `llmVision` and in `llmPredictBattery` message
> arrays: the model server may fetch an http(s) `image_url` itself. Inline base64 image data passes
> (`data:image/…` or `data:application/octet-stream`, parameters allowed, which is what
> `httpFetch`'s `dataUrl` returns). http(s) passes only against the run's `allowedFetchDomains`,
> and is refused without one. What reaches the model server is the ADMITTED, normalised URL: a
> backslash-userinfo URL that one parser reads as an allowed host and another as `169.254.169.254`
> is never forwarded as written. `llmPredictBattery` takes a string or a message array, which the
> VM REBUILDS from a closed shape (`{ role, content }`, content a string or `text`/`image_url`
> parts; any other part type, such as `video_url`, is refused) and forwards with the admitted image
> URLs; the shipped battery sends it as the conversation itself. The `{ text, images }` vision form
> is refused there (use `llmVision`). With an allowlist set, a fetch URL that is not absolute is
> refused, because it names no host to check. A NETWORK-PATH reference (`//host/x` and its
> backslash forms) is refused on every path, since it borrows the page's scheme. A host's `fetch`
> always receives the ADMITTED, normalised URL, never the raw input. A policy list that is not an
> array of strings is refused (a string was read one character at a time). Rebinding a value already in scope (a
> per-iteration `let p = o`) costs one pointer again: in rc.2's heap accounting it re-measured
> the whole value, so a loop was quadratic in fuel where rc.1 was linear.
>
> **Since rc.1 — an outbound membrane on capability calls (security, breaking for some custom
> atoms).** What an IO atom hands a capability is now a budgeted deep COPY of its resolved input,
> checked against the input schema the atom declares (Tonio). The inbound membrane already did this
> for what a capability returns. The outbound direction kept being found one site at a time:
> - a `system` or `prompt` that was really an array of `video_url` parts;
> - `tools` and `responseFormat` forwarded live, so a guest that caught a timeout changed them
>   before a slow host serialised them;
> - raw URLs.
>
> It applies to every core, battery and custom IO atom whose inputs the VM resolves, and fails by
> refusing (`'<op>': its input does not have the shape the atom declares`). **Embedders:** a custom
> IO atom now receives plain-data copies, and is called only with inputs matching its declared
> schema; a `storeGet` key must be the string it declares. An IO atom with
> `resolveInputs: false` should pass what it hands a capability through the newly exported
> `egressValue`. `agentRun`'s capability route and `cache` now do (both handed the host live guest
> objects), and a `cache` key must be a string.
>
> The copy is paid for. Its walk is budgeted by the run's remaining fuel and by
> `membraneMaxBytes`, which **now caps both directions** (it was documented for returns only).
> The copy is charged as an allocation before it is made, and a call that is refused is charged
> for the walk it caused. The first version of this membrane charged nothing, so a loop over a
> large value used about 100× the CPU for the same fuel (cumulative review 5).
>
> **`cache` and `memoize` require a key (breaking).** A key-less block derived its key by calling
> the `hash` atom's step wrapper, which returns nothing, so every key-less block was stored under
> `undefined` and returned ANOTHER block's result; a `cache` entry is shared across runs for 24
> hours by default. Keying by a digest of the steps is no better: the steps are the same when the
> data they read is not. Only the author knows what a result depends on, so the builder's `key`
> is now required and the VM refuses a block without one. A store written by an earlier version
> may hold a stale `cache:undefined` entry; it is safe to delete. A key larger than
> `membraneMaxBytes` is refused like any other capability input, and a result too large to cross
> fails the step rather than being returned uncached.
>
> **IO atoms declare their inputs (breaking for embedders).** `defineAtom` refuses an IO atom
> (`effects: 'io'`, the default) with no input schema (use `s.object({})` for none), or whose input
> schema contains `pattern`/`patternProperties`: the outbound membrane validates every call
> against that schema, so a pattern there ran on the host's regex engine over guest-chosen
> strings, outside fuel. Check patterns in the atom's body. (The JSON Schema direction is Pattern:
> `docs/pattern.md`.) The schema is admitted as a frozen plain-JSON COPY when the atom is
> defined, and every call is validated against that copy: a `pattern` is refused whatever its
> value (a RegExp included), any other non-JSON value is refused rather than converted, and
> changing `atom.inputSchema` afterwards changes nothing. An atom tagged `io` after definition is
> admitted at its first call. Migration:
>
> ```js
> defineAtom('ping', undefined, s.any, fn)               // refused
> defineAtom('ping', s.object({}), s.any, fn)            // takes nothing
> defineAtom('lookup', s.object({ id: s.string.pattern('^[a-z]+$') }), …) // refused:
> defineAtom('lookup', s.object({ id: s.string }), …)    // check the id in the body
> ```
>
> A refused outbound copy is billed for the work its walk did, whatever refused it, never more
> than the fuel present, and keeps its own error unless fuel genuinely ran out. Round 31 billed
> only a refusal for size (a depth refusal after a megabyte was free); round 32 billed the whole
> budget on top of what the copy had already been charged (cumulative reviews 6 and 7).
>
> **A refusal at the capability boundary that is a bug ends the run (breaking).** An atom call
> whose input does not match the atom's declared schema, a value refused on its way out to a
> capability (too large, too deep, not plain data, over the heap ceiling or the fuel), and a
> capability's RETURN that JSON could not express (a function, a getter, a class instance: the
> host's contract broken) can no longer be caught
> by `try`/`catch`. A return that is merely too large or too deep (things JSON can do), or does not
> match the atom's declared output, is the world's doing and stays catchable (Tonio), and every inbound copy is billed in fuel so a
> retry loop pays. A value an atom THROWS reaches a guest `catch` as a capped string: its
> `message` (read once, so a `DOMException`'s works), or `String(value)` for a thrown primitive;
> never an object (a thrown `{ message: obj }` used to hand over a live host object). A typed
> array over a `SharedArrayBuffer`, or any subclass of a typed array, `DataView` or `Date`, is
> refused at the boundary: `structuredClone` would SHARE that memory with the guest, not copy it. Refusals that end the run: no catch block, later step or parent-agent step runs, and the
> run reports the refusal. This includes a `cache` entry or key that cannot cross, and persisted
> ASTs or builder programs that wrapped such a call in `try`. Ordinary failures (a fetch that fails, a store that throws) are caught as before. Four
> review rounds in a row (cumulative reviews 5 to 8) each found a refusal path billed short of the
> work it did, and each mattered only because a guest could catch the refusal and retry it in a
> loop. A refusal that ends the run costs at most one bounded walk (Tonio).
>
> **The membrane builds its copy: JSON plus Date (breaking).** Every value crossing the capability
> boundary (capability returns, IO-atom inputs, run arguments) is now a copy the membrane builds
> from exactly what it read, instead of a `structuredClone` of the original. `structuredClone`
> copies internal slots the check never saw, so a typed array over a `SharedArrayBuffer` with a
> shadowed `buffer`, a prototype-swapped buffer, or a 1-byte view over 50MB was checked as one
> thing and forwarded as another (cumulative review 13). What crosses is plain objects, arrays,
> strings, numbers, booleans, null/undefined, and `Date` (by its internal time). **`Map`, `Set`,
> typed arrays, `ArrayBuffer`, `RegExp` and `Error` no longer cross**: a capability returning one,
> or a run argument containing one, is refused. Convert them to JSON first (a Map to an object or
> an array of pairs). Shared references and cycles are preserved, as before. A `Date` crosses only
> when it is exactly an intrinsic `Date` (a subclass, or one with own fields, would arrive thinned).
> Cost, measured: a 300-document RAG return copies in ~1.9ms (3.5ms before); a 400k-number array
> in ~143ms (~127ms before), where the per-element descriptor read that keeps getters from
> running dominates, as it did.
>
> **The membrane's copy is built safely, and the VM loads in any page.** Copies are built with a
> null prototype and given their real one when complete, so a polluted `Object.prototype` setter
> never runs on capability data and frozen intrinsics (SES) do not refuse ordinary JSON. A
> crossing is bounded by `membraneMaxBytes` and (outbound) the remaining fuel; the heap ceiling
> applies where the value lands. A refusal for size names the limit that set it. Fixed in the
> same rounds: rc.2 builds briefly read `SharedArrayBuffer` at module scope, which threw on
> import in any browser page that is not cross-origin isolated (every bundle is now load-tested
> without optional globals). An array's `length` is read from its own descriptor and validated:
> a Proxy whose `length` trap returned an object could put an unwalked host object into the copy
> (cumulative review 17). Object copies are built on their real prototype with each key defined,
> so they stay in V8's fast mode. An `Array` subclass, like any class instance, is refused rather than thinned
> to a plain array (its prototype getters would read as undefined), and so is an array or object
> from another realm (an iframe, a `node:vm` context): pass plain data, e.g. through a JSON
> round-trip.
>
> **Faster hot loops: a per-atom timer is armed only for a call still pending.** Every atom step
> used to arm a `setTimeout` and a `Promise.race`, though a call whose body completes synchronously
> can never be overtaken by its timer. A 20,000-iteration loop went from 54ms to 32ms, and from
> 40,004 timers to 1 (the run's deadline), at the same fuel. An atom that awaits real work is timed
> as before, with its deadline counted from the call on a monotonic clock (`performance.now()`), so a
> wall-clock step cannot time it out early.
>
> **Persisted ASTs:** a stored AST (`procedureStore`, a saved builder program) containing a
> key-less `memoize` or `cache` now fails when it runs, with "a key is required". Add a key.
>
> A fetch URL that is relative is decided by the URL parser itself (resolved against an http and
> a `file:` sentinel). It passes only as a plain path naming no host, and contains no
> tab/LF/CR/backslash a parser would rewrite. A hand-written "two leading slashes" check missed
> `/\t/evil.com/x`.
>
> **Since rc.1 — AsyncJS checks every atom call against the atom's declared inputs (breaking).**
> A call to an atom is a single object literal, `storeSet({ key, value })`, or no arguments,
> `random()`. The transpiler now refuses, with a message naming what the atom takes:
> - a positional call (`storeSet('k', v)`, `foo(opts)`);
> - a spread (`foo({ ...opts })`, which was silently dropped);
> - an input the atom does not declare (`httpFetch({ url, cache })`; four playground examples
>   asked for a `cache` that `httpFetch` never had);
> - a missing required input.
>
> These all used to compile and run the atom with inputs undefined, reporting success. Core atoms
> are checked from a table generated from their schemas (`src/vm/core-atom-inputs.ts`). Pass your
> own atoms to check calls to them too: `transpile(src, { atoms: vm.atoms })`. Every entry point
> that holds a VM does this itself (`vm.run(source)`, `createAgent`, `Eval`, `SafeFunction`), so
> a host atom that overrides a core name is checked against its own inputs. Without `atoms`, a
> refusal on a core name says to pass them. An open schema (no `additionalProperties: false`)
> still requires what it lists. An AST persisted under rc.1 with a positional atom call (e.g. in
> `procedureStore`) never delivered its inputs; re-transpile it. Local functions
> and `Error('message')` keep positional arguments. **The VM does not check parameters; it runs
> an AST as written.** Its job is to be correct and safe; preventing bad code is the
> transpiler's job (Tonio). An AST built by hand or by another front end is that producer's
> responsibility. Four review rounds tried to make the VM decide what a call meant from each
> atom's schema, and each broke a call that worked. **Embedders:** an atom you called
> positionally (an `args` input) is now called `foo({ args: [a, b] })`.
>
> **Since rc.1 — the AsyncJS value domain is closed (breaking).** A guest value is data — JSON
> values, with the VM's own Set, Date and regex as data (above) — and never a host function or a builtin
> namespace. It is checked where values ENTER guest state: the walk that charges every bind,
> insertion and reconcile now refuses a function or namespace anywhere in the value, so no
> producer (an expression, an argument read, an atom result such as `pick`) can bypass it.
> Arguments are a null-prototype object read by own key (`args.constructor` was the host's
> `Object`, and `varsImport` could hand a capability `Object.prototype`). A Set or Date in the
> run's RESULT is plain data, so a structuredClone at a worker or process boundary keeps it
> (wrapper methods had reduced a Set to `{ size }`); a host that read wrapper methods off the
> result reads the data instead. Validation costs about 0.002
> fuel per schema-node × data-node step (some 0.11 fuel per 6-field row): raise `fuel`, or
> validate at your own edge, for large payloads. Earlier holes closed in this round of review: `const g = parseInt`, `Object.values(Math)`,
> `{ toJSON: encodeURIComponent }`, `[Set].join('')` (which printed VM source) and a dot-path
> `'s.add'` all handed out host functions, and a stolen `Set.add` called through a guest object's
> own `hasOwnProperty` property grew a Set past `maxHeapBytes`. Method calls now dispatch to the
> receiver kind's INTRINSIC, never to a property the guest owns; a namespace can no longer reach the host's result (where
> `JSON.stringify` threw). `llmPredict` options are allowlisted (`model`, `temperature`,
> `maxTokens`, `topP`, `stop`, `seed`, `responseFormat`, `tools`) and tools must be
> `{ type: 'function', function: { name, description?, parameters?, strict? } }`. Validation is
> charged at the VM's own rate (it bought ~38× more host time per fuel), and an AST's
> `inputSchema` counts its arguments per path, as validation walks them.
>
> **Since rc.1 — in AsyncJS, `Schema` is data, and a method is not a value (breaking).** Guest
> code held live tosijs-schema builders, whose methods are host closures: a `validate` read as a
> value and spliced next to a harmless `schema` validated against a pattern smuggled in through
> `meta` (51s on Node, at fuel 50). Now `Schema.string`, `Schema.email`, … are frozen plain JSON
> schemas, and `Schema.object`, `array`, `record`, `union`, `tuple`, `enum`, `const`,
> `fromExample`, `response` and `isValid` are VM-implemented, returning plain JSON built only from
> admitted parts. **Migration:** builder chaining is gone — write the keyword:
> `.int` → `type: 'integer'`; `.min`/`.max` → `minimum`/`maximum` (or `minLength`/`maxLength`,
> `minItems`/`maxItems`); `.step` → `multipleOf`; `.title`/`.describe` → `title`/`description`;
> `.default` → `default`; `.open` → `additionalProperties: true`; `Schema.pattern` → refused (use
> `regexMatch`). `.optional` → a nullable type, `{ type: ['string', 'null'] }`, which
> `Schema.object` leaves out of `required` — note that a nullable key may be absent; a present key
> must be the type or `null`. And reading a method as a value (`const f = s.trim`,
> `'a'.toUpperCase`) is refused: call it. Validation is charged as schema size × data size, before
> it runs, at every door (an `anyOf` of 3,300 branches over a 97×97 array ran 630ms for 63 fuel
> when charged as a sum), including the AST's `inputSchema`, paid at admission. Every LLM door —
> the core `llmPredict` too — admits its `responseFormat` (shape allowlisted: `json_schema`,
> `json_object`, `text`) and every tool's `parameters` schema.
>
> **Since rc.1 — guest schemas are a closed dialect (breaking).** Every schema that comes from
> guest code or the guest AST — `Schema.*` and `filter` arguments (after an example is converted),
> an AST's `inputSchema`, a `return` step's schema, an LLM `responseFormat` — must be a plain JSON
> tree using only the keywords the VM admits, each with exactly its value type: `type`,
> `properties`, `items`, `additionalProperties`, `anyOf`, `oneOf`, `required`, `enum`, `const`,
> the numeric bounds, `format` (only the formats tosijs-schema enforces, all linear), `title`,
> `description`, `default`, `examples`. Anything else is refused, naming its path — including
> `pattern` (except the library's own, e.g. `Schema.emoji`), `patternProperties` and
> `$predicate`. A denylist failed: tosijs-schema compiles `pattern` with `new RegExp(value)`, so an
> ARRAY or a regex object walked past a string-only check and ran on the host's backtracking
> engine, at the inputSchema door before fuel or the timeout existed. **Migration:** a builder or
> persisted AST whose `.take()`/`.return()` schema uses `.pattern()`, `patternProperties` or
> `$predicate` (the `tjs-lang/css` schemas included) is now refused by the VM; validate those on
> the host after `vm.run`, or match inside the program with `regexMatch`. Also refused, and failing
> at `vm.run` for a persisted AST: `$schema`, `$id`, `$ref`, `definitions`, `uniqueItems`,
> `prefixItems`, `nullable`, `additionalItems`, any other keyword, an accessor, a non-plain object,
> a non-finite number, more than 10,000 nodes or 64 levels. An object with a string `type` field
> (or an array of type names) is read as a schema, not an example. The predicate trust registry
> moved from a string global to a `Symbol.for` slot, so bundles of different 0.14 prereleases in
> one realm do not share trust.
>
> **Since rc.1 — every resource a regex uses is charged.** Everything a regex costs is now a
> function of its program size and its input. A compiled regex is charged its program's size
> against `maxHeapBytes` as the program is built, and counted while it is held (it used to keep
> its matching table for as long as it lived: 1000 held regexes kept ~690MB under an 8MB
> ceiling). Regex literals in one source share a compile budget of max(250k, 200 × source bytes) steps, every byte compiling allocates counted as work,
> with each distinct literal compiled once; an over-budget source is refused with "The regexes
> in this source are too large to compile". That holds in AJS helper functions too, and for
> `$predicate` sources. In predicates, `__fuel` and `__rx` are reserved identifiers, and **the
> pattern passed to `match`/`search`/`matchAll` must be a regex or string literal** (or a `const` bound to
> one): JavaScript compiles a string passed there as a regex on its own engine, which no fuel can
> see inside. A string LITERAL is compiled by the metered engine instead; any other non-regex
> argument is refused (`replace`/`split` take a string literally and are unaffected).
>
> **Since rc.1 — the regex engine is metered by construction, and predicates use it too.** A
> linear algorithm is not a bounded one unless its work is CHARGED: the first version charged one
> step per thread per position, and the work it did not see (following zero-width instructions,
> copying capture arrays, testing a 400k-character class, expanding `(?:){1e12}` at compile time)
> ran for seconds on a few fuel. Every unit of engine work is now charged — each instruction
> visited, each capture copy in proportion to its width, each class probe, compilation itself —
> and every pattern-sized quantity is capped (counts over `{10000}`, nesting, program size,
> capture slots). Classes are merged range tables, binary-searched. Case-insensitive matching
> uses equivalence classes DERIVED from the host engine (generated, freshness-tested), so `i` and
> `iu` agree with JavaScript on every code point (ǅ ~ Ǆ, ſ ~ s under `u`, dotless ı). A
> long-standing divergence on nested quantifiers over empty-matchable bodies is fixed: dedup on
> the program counter alone let a path whose empty check would fail block one whose check would
> pass. `replace` charges each substitution at its exact length BEFORE building it. Unsupported
> regex literals are a transpile error at their source location. **Predicates (`compilePredicate`,
> `$predicate`, `tjs-lang/css`) run regex literals on the same engine**, charged to the predicate's
> fuel — `/a*a*c/`, which the old star-height screen certified, took 13s on 6k characters inside a
> `$predicate` that travels as data. A `Type` predicate with a regex is no longer emitted as a
> verified guard (standalone output cannot carry the engine; it still runs, unverified). **That
> means a warning from `tjs check`, so a `--max-warnings 0` gate fails on such a file.** Write
> the check with string methods (`includes`, `indexOf`, `startsWith`, `split`) to keep it
> verified — the Email example in the declarations guide now does. Cost: `tjs-lang/css`
> validates a theme in 7.3ms (0.20ms on the host's engine, measured — ~36× slower; ~15× per value), still inside a frame. Making this fast is card #2767, and `Pattern` (`docs/pattern.md`) is the longer-term answer. Also:
> the ASI guard recognises a control header from its parenthesis structure, so a header spanning
> lines, `label: for (…)` and `for await (…)` keep their brace-less bodies.
>
> **Since rc.1 — native TJS accepts a brace-less control body that starts with `[` or `(`.** The ASI
> guard put a `;` in front of such a line — which, after `if (…)` / `while (…)` / `for (…)` /
> `else`, BECAME the statement's body: `if (c)\n  [a, b] = …\nelse …` orphaned the `else`, so valid
> JavaScript was refused (a TJS ⊇ JS violation, found when the VM's regex parser entered the
> dogfood corpus). A bare header now keeps its body; a complete one-line `if (a) foo(b)` still
> separates the next line, as before.
>
> **Since rc.1 — guest regexes run on the VM's own engine; methods take exactly-typed arguments
> (breaking).** Guest regexes (literals, `regexMatch`, `match`/`search`/`replace`/`replaceAll`/
> `split`) no longer run on the host's backtracking engine: a linear-time Pike VM, fuel per step,
> makes ReDoS ordinary work instead of screening for it (the screen missed polynomial shapes —
> `/a*a*c/` over 6k characters ran 12.9s for 2.7 fuel). Unsupported, and refused when the regex
> is compiled (a literal, at transpile time): backreferences, lookahead/lookbehind, `\p{…}`, the `d`/`v` flags. A schema `pattern` supplied by
> guest code is refused (it would validate on the host's engine) — use `regexMatch`. Methods now
> take arguments of exactly the type they read: a count is a number (`'x'.repeat('1e8')` is
> refused; it built 191MB under a 1MB cap), positions are typed (`arr.indexOf(v, obj)` converted
> `obj`), and extra arguments are refused. The regex input cap (100k characters) is gone — input
> is charged, not capped. Also: a Set's `remove(NaN)` removed the last element; `JSON.stringify`
> is bounded by its escaped size; printers charge their walk as they walk.
>
> **Since rc.1 — AJS refuses implicit coercion of objects and arrays (breaking).** Operators
> (`+ - * / % ** < > <= >=`, unary `+`/`-`), computed keys, and methods that take primitives now
> refuse an object/array operand instead of converting it to its string form, recursively and
> for free: `arr < 5`, `'x' + arr`, `` `${arr}` ``, `obj[arr]`, `'s'.includes(arr)`,
> `Math.max(arr)`, `parseInt(arr)`, `[[2],[1]].sort()`. Write the conversion: `arr.join(',')`,
> `JSON.stringify(obj)`. Equality still compares objects, by identity. Printers (`join`,
> `JSON.stringify`, `flat`, `toString`) are now bounded by the TREE they print — a shared-reference
> DAG of a few KB joined to 400M characters for 2.5 fuel. **Regex literals** are now data in the
> AST (`{ $expr: 'regex' }`), built by the VM after the ReDoS screen; they were host RegExp objects
> the transpiler built from guest source, never screened, and serialized as `{}`. `match`/`search`
> with a string pattern are screened too (one was 634ms past a 50ms timeout), and a RegExp runs
> only over inputs up to the existing 100k-character cap. Also: a Set's `has`/`intersection`/`diff`
> are O(1)/O(n+m) (they were linear/quadratic for a linear charge); `slice`/`substring`/`split`
> are charged for what they select; a helper's extra arguments are dropped, not held unbound.
>
> **Since rc.1 — one implementation per operation (v2).** Method calls compile to the VM's
> gated method call and template literals to its gated `+`, instead of `push`/`split`/`join`/
> `template` atoms with their own budgets and their own semantics: `let n = arr.push(x)` is now
> the new length (it was the array), `` `${null}` `` is `'null'` (it was `''`), and an unknown
> method is a transpile error (it became a call to an atom named after the method). An
> expression statement compiles to a new `evaluate` step — it bound `_`, clobbering a guest's
> own `_`, and refused a program with `const _`. v1 ASTs keep the data atoms, now thin wrappers
> over the same gate; the `template` atom reads only its own vars (`{{constructor}}` printed
> `Object`'s source). `costOverrides`/`quotas` keyed on a data op apply to v1 ASTs only.
> Hosts running untrusted code at scale should still give each run a hard engine-enforced
> floor (a Node `Worker` with `resourceLimits`) — see "Why not just ask the runtime?" in the
> budgets doc.
>
> **Since rc.1 — every v2 block is a scope.** `if`/`else`, `try` and `catch` blocks now scope
> their declarations, as `while` bodies already did and as JavaScript does: sibling blocks may
> each declare `const t`, a block `let x` may shadow an outer `const x`, and `catch (e)` shadows an
> outer `e` (with `errorOp`, bound in the catch block). v1 ASTs keep unscoped blocks. **A raw atom**
> (`{ op, exec }`) that writes `ctx.state[step.result]` itself now declares in the innermost block
> — use `defineAtom`, which honours assignment (`resultAssign`). A default parameter is now filled
> by assignment, not a declaration in the `if` that tests it. `var` is function-scoped, as in JavaScript: it is
> declared once at function entry and `var x = v` assigns it, in any block (lowering it like
> `let` lost `if (c) { var q = 5 }` silently); a callback's `var`s are the callback's own.
> **`for (var x of …)` and `for (x of …)` are refused** with a pointer to `const`/`let`: the loop
> variable is bound in the loop's scope, so they cannot mean what JavaScript means (assign an
> outer `x`), and they silently did not. `errorOp`, like the catch parameter, is bound in
> the catch block and is not visible after it.
>
> **Since rc.1 — `const` follows the scope a write lands in.** One rule, enforced where every
> scope write passes: a write may not land on a `const` of the scope it writes. A block `let x`
> beside an outer `const x` was refused as a reassignment — legal JavaScript rejected — while
> `varsImport`, `varsLet` and `catch` checked nothing and could overwrite a `const`.
>
> **Since rc.1 — guest code cannot set an object's prototype.** `__proto__` is refused as an object
> key — at transpile time, in object literals, in resolved values, in atom inputs, and in the
> guest `Object.assign` (which used the native one, and so the `__proto__` setter), and in every
> atom that builds an object from guest keys (`pick`, `omit`, `varsExport`, `agentRun`'s input,
> the return projection). Set as a key, it replaced the object's prototype, hiding whatever hung
> off it from `maxHeapBytes` (~19MB held under an 8MB cap). Every computed-key write in the VM
> now goes through one guarded function, and a source scan (`guest-key-writes.test.ts`) fails on
> any new one. `pick` also reads own properties only: `pick(o, ['constructor'])` returned the
> inherited `Object` function, a live host function in guest hands.
>
> **Since rc.1 — AJS assignment means what it means in JavaScript** ([#59](https://github.com/tonioloewald/tjs-lang/issues/59)).
> Found by tosijs-platform; every one of these was a SILENT wrong answer:
> - Every compound assignment stored only its right-hand side: `n -= 2` gave `2`, `s += 'b'` gave
>   `'b'`, `n *= 3` gave `3`. They now lower to `x = x op y`.
> - `i++` / `i--` / `++i` as a statement were dropped (a warning, then nothing). They now assign.
>   Used as a VALUE (`j = i++`) they are refused, as is any expression AJS does not support — the
>   transpiler used to turn an unrecognised expression into `null` and carry on.
> - Assigning an outer variable inside a `for…of` body was lost (`s = s + w` left `s` empty):
>   assignment and declaration compiled to the same step, which wrote the loop body's own
>   scope. Assignment now compiles to a new `varAssign` step that writes the scope that OWNS the
>   variable, as JavaScript does, charged to that scope's heap ledger. Declarations are
>   unchanged, and ASTs already stored keep their meaning. An AST from this transpiler is format
>   v2 (below), so an older VM refuses it by version, never silently.
>
> **Since rc.1 — `runCode` is its own program. Breaking.** Code run by `runCode` sees only the
> `args` it is given; it no longer sees the caller's variables or local helpers. It ran in a
> child of the caller's scope, so model-written source could read every caller binding (keys,
> tokens), reassign them (stopped at a scope boundary in the first rc.2 fix), and — what that
> boundary could not stop — mutate them in place (`allowed.push('evil')`). A fresh scope has
> nothing on the other side to reach. **Symptom:** a caller variable that runCode source used to
> read now reads as `undefined`. Pass what the code needs in `args`; what it is given it
> holds by reference, as a function holds its arguments. Likewise `agentRun` now runs a
> sub-agent's OWN local helpers — it was handed the caller's, so a call by name ran the wrong
> body.
>
> **Since rc.1 — two exports removed. Technically breaking; neither had a real use.**
> - **`setTranspiler` is no longer exported** from any entry. It lived in `vm.ts`, which every
>   entry re-exports, so it was public everywhere — including `tjs-lang/vm-ast`, whose whole
>   promise is that it contains no parser, while `setTranspiler` let any importer arm one in the
>   sandbox's process. It is now internal; `tjs-lang/vm`, the main entry and `tjs-lang/eval` still
>   wire the transpiler in exactly as before. If you called it to give `vm-ast` a parser, import
>   `AgentVM` from `tjs-lang/vm` instead — that is what it is for.
> - **The `tjs-lang/src` export is gone.** It pointed at `src/index.ts` under every condition, so
>   in Node (with type stripping) it loaded TypeScript whose relative imports have no extension
>   and failed; every other `src/` path is behind the `bun` condition. Nothing we could find
>   imported it. Use `tjs-lang` (the `bun` condition already resolves it to source).
>
> **The syntax reference is now `TJS-SYNTAX.md`** (was `CLAUDE-TJS-SYNTAX.md`), so its page on
> the doc site is `/TJS-SYNTAX/`. The old file stays as a one-line pointer.
>
> **Since rc.1 — `httpFetch` no longer breaks cross-origin requests in a browser.** The default
> fetch added `X-Agent-Depth` to every request; in a browser that is a non-simple header, so any
> cross-origin API that does not allow it failed preflight with "Failed to fetch" (found running the
> AJS weather example live on the doc site). In a browser the header now goes only to the page's
> own origin, where agent endpoints live; servers send it everywhere as before, and a custom
> `fetch` capability still receives the depth.
>
> **Packaging (rc.1):** `typescript` is now a declared OPTIONAL peer (`^5`) — `tjs-lang/lang/from-ts`
> imports it, and it was only a devDependency, so a Node consumer of that entry without it
> installed failed at import. Dev-only scripts (`bin/*.ts`, `editors/build-*.ts`, the
> CodeMirror demo component) and unreachable editor `.ts` sources are no longer shipped; every
> exported path is unchanged. Found by the new publish workflow's release-doctor, not by a user.
>
> Published first as **`0.14.0-rc.0`** on the `rc` dist-tag, so tosijs-ui could verify against it
> before its peer range admitted 0.14 ([tosijs-ui#182](https://github.com/tonioloewald/tosijs-ui/issues/182)).
> It did: **tosijs-ui 1.15.2** declares `tjs-lang: ^0.13.1 || ^0.14.0`, so npm consumers of both
> resolve cleanly. That rc verified PEER RESOLUTION; the `Type`-example and `TjsStrict` changes
> below came after it, through several review rounds (`docs/reviews/0.14.0-*`).

**Untrusted AJS source is capped at 8KB by default (was 64KB) — and the real answer is to not
accept source at all.** `maxSourceBytes` for `Eval`, `SafeFunction`, `vm.run(source)`,
`runCode` and `transpileCode` now defaults to 8KB. Nine review rounds in this release each found
another input shape that the AJS preprocessor or acorn parses super-linearly — nested
destructuring took 19s at 60KB, a `function` head followed by whitespace 2.9s — and patching
them did not converge. A quadratic cost shrinks with the square of the cap: at 8KB the worst
known shape is ~455ms (densely nested destructuring, measured 2026-09-26). Raise it per call
for trusted source, and `transpile(source, { maxSourceBytes })` (new, opt-in) caps an
in-process transpile. For source the GUEST builds (`runCode`/`transpileCode`), `vm.run`'s
`maxSourceBytes` can only **lower** the 8KB cap, never raise or disable it: that text can come
from `llmPredict` output, and one option cannot speak for two trust domains. **If you
run agents from untrusted callers, take an AST instead:** transpile on the caller's side
(`tjs-lang/browser` or `tjs-lang/lang`) or in a worker or process you can afford to lose, send
the AST, and run it with `tjs-lang/vm-ast`, which has no parser in it at all — the parse cost
then lands on whoever sent the source, and what remains on the host is linear. (This project's
own hosted endpoints still accept source; they get the 8KB cap when `functions/` is bumped to
0.14 and redeployed — a 0.x minor is outside `^0.13.x` — and moving them to ASTs is next.)
`ajs`, `createAgent`, `tjs` and the exported parser functions are for TRUSTED source and take
no cap.

**Deprecated: `vm.run(source)` — the VM parsing AJS.** It still works (capped at 8KB) and
notes itself once in the flight recorder. The VM should never be the thing that parses:
transpile separately (`transpile` from `tjs-lang/lang`, in a worker, another process, or on the
caller's machine) and run the AST with `tjs-lang/vm-ast`, which contains no parser — so a bad
payload can only take down the step that parsed it, never the endpoint. `Eval` and
`SafeFunction` keep taking source: they are for source you trust, or a process you can afford
to lose.

**A language release.** The tosijs-ui-hosted site was what 0.14.0 was originally reserved for;
that work is real but lands separately, as a non-breaking change to build tooling that does not
touch the published surface. The version number follows the narrative rather than a name
reserved in advance.

**If you only read one line:** a runtime type now reports `typeof === 'function'` instead of
`'object'`. `.check()`, `isRuntimeType()`, `Object.keys()` and spread are unchanged, and
serialisation improved from _throwing_ to working — so unless you branch on `typeof`, that
one moves nothing under you.

**What may newly RUN** — error propagation is decided at the type check (native `.tjs` and
converted code). A `MonadicError` passed where it does not fit a parameter's type is still
returned unchanged without running the body — including through overloads, arrays and
options bags. But every validated function used to begin with a pre-check that returned ANY
`Error` from ANY parameter, whatever its declared type:

- **A function whose parameter admits errors now RUNS with one.** `describe(e: Error)`,
  `isErr(x: unknown)`, `log(msg: '', detail: any)`, unsafe `!` functions, and every
  catch-handler helper used to hand their argument straight back; their bodies now run.
- **A plain `Error` (from `new Error()` or a `catch`) where something else is expected is a
  new TYPE ERROR** — it used to be passed through as if it were a TJS error, identity intact.
- **A `MonadicError` is never an object shape**, so `o: {}` or an all-optional shape
  propagates it instead of running on it; an error inside an options bag (`{ x: err }`)
  propagates the same way in every parameter form.
- **`vm.run` arguments cross the capability membrane**, like capability returns (so do
  `Eval` context values and `SafeFunction` arguments, which become them). They arrived
  LIVE, and `methodCall`'s allowlist filters method NAMES, not owners: a class instance
  whose class defines `slice` had it invoked by guest code (`svc.slice(0)` ran host code).
  **Migration — what is now refused, loudly, with an `AgentError` naming the argument:**
  - A CLASS INSTANCE (any prototype but a plain object's, `null`, `Array`, `Date`, `Map`,
    `Set`, `RegExp`, `Error` or a typed array). Copying only its own data would thin it
    silently: a Firestore Timestamp's `seconds` getter read as `undefined`, and a negated
    rule over it flipped deny to allow. Convert it to plain data first (`ts.toMillis()`).
    The same rule now applies to what a capability RETURNS.
  - An own function or getter, a Proxy (including a raw tosijs state proxy — pass
    `.value`), a `URL`, and anything else `structuredClone` cannot copy.
    **Arguments are metered and capped.** Admission walks the data before any atom runs, so
    it is budgeted by the run's own fuel — about 8,000 bytes per unit, the rate binding the
    same data costs — and capped by the new `argsMaxBytes` run option, **default 4MB**, the
    same as the capability direction. The cap bounds that pre-budget work absolutely (about
    180ms at the walk's worst, whatever the fuel). What crosses is charged to fuel, and so is
    a refusal. A host passing large arguments must pass fuel in proportion (a 1MB string needs
    about 250) and, above 4MB, raise `argsMaxBytes`.
- **`vm.run` refuses invalid budget options.** A `fuel`, `timeoutMs`, `argsMaxBytes`,
  `membraneMaxBytes` or `maxHeapBytes` that is not a non-negative number is an `AgentError`
  ("Invalid run option fuel: …"). `fuel: 'abc'` made the argument budget `NaN`, which no
  size ever exceeds, so the walk it bounds ran unbounded.
- **Admission is one module (`src/vm/admission.ts`), and every entry path goes through it.**
  The release cycle blocked five times on one class — work proportional to caller input,
  done before any budget could stop it — because each fix guarded one door. Now:
  `vm.run(source)` is capped like `Eval` (new `maxSourceBytes` run option, default 8KB), and
  its options are checked before the source is even resolved; cost, timeout and quota
  overrides are validated too (a negative cost override MINTED fuel; a `NaN` quota read as
  unlimited; a `NaN` timeout disabled the timeout; `timeoutMs: Infinity` fired after 1ms and
  now means no timer; a run-level `timeoutMs: 0` still means the deadline has passed). `src/admission.test.ts` runs every hostile shape through every entry
  path and asserts each is cheap — a new entry path belongs in that table.
- **The AJS preprocessor is linear on hostile input.** Every quadratic pass the release
  reviews measured is fixed at its cause, not bounded: two look-backs that ran per character
  over blanked comments (64KB of `//`: 54s → ~12ms); an unmatched `(` rescanned to EOF from
  every later `(` (60-90s; now each partner — or "never closes" — is recorded as it is proven,
  held equal to a fresh scan at every `(` of a real-file corpus); the method-head regex tried
  at every character; `trimEnd()` of all output on every `/`; a class-heritage scan to EOF per
  header; and the ternary-colon test, which walked back over the whole expression for every
  `:` and is now one forward pass (held equal to the old walk at every `:` of the corpus).
  **AJS** paren nesting is bounded at 64 by the recursion itself (the deepest in 3,372 real
  files is 19); the TJS compiler, which takes the author's own source, has no limit.
  After five consecutive review blocks each found another such scan, the AJS transform also
  carries a deterministic **work budget** (64× the source length, shared by every recursion
  level; real code uses a median of 2×, at most 8.8×) — fuel for the parser, so a
  super-linear scan anywhere in it is refused at a bounded cost instead of trusted to be
  linear. The lexical layer's regex scan remembers which (position, state) pairs it has
  proven fail, so a line of `/[/[…` or `/\/\…` no longer rescans per `/`.
  `src/admission.test.ts` pushes every hostile shape it knows, a GENERATED grid of 41 tokens
  and a grid of 289 token PAIRS, each repeated to the cap, through the source entries; the
  worst measured at the old 64KB cap was ~114ms for the gridded shapes (see the 8KB cap above
  for the shapes that were not).
- **An aborted run takes no further step.** Every atom now checks the run's abort signal
  before it runs, so after a deadline or a caller's abort no capability is called and no
  guest code continues — `vm.run` used to stop WAITING while straight-line steps carried on
  unobserved. A run-level `timeoutMs: 0` fails before the first step (0.13.x let a
  compute-only agent complete), and a caller `signal` that is ALREADY aborted stops the run
  (it was ignored: an `abort` listener never fires for a signal already aborted). A caller's
  abort is reported as "Execution aborted by the caller", no longer as a timeout.
- **`Eval` and `SafeFunction` take `argsMaxBytes`**, so a host can raise the new 4MB
  argument ceiling through the safe-eval API (strings count two bytes per character).
- **Every budget option is validated, including outside a run.** `compilePredicate` and
  `emitVerifiedPredicate` read `fuel` raw, so `fuel: NaN` removed the runaway guard (`--fuel <
0` is never true for NaN — an exponentially recursive predicate ran unbounded) and
  `emitVerifiedPredicate` spliced the unvalidated value into emitted source. Both now refuse
  a `fuel` that is not a non-negative number (`Infinity` means no limit), and
  `compilePredicate` splices only verified names into its generated function. `defineAtom`
  refuses an invalid static `timeoutMs` where it is written, and an atom with `timeoutMs:
Infinity` no longer makes every run on its VM unbounded. Refusals name the bad value
  (`NaN`, not `null`). **Quota tables are checked over exactly what the VM reads.** A `NaN`,
  negative or string `quotaUsed` counter switched the quota off (`NaN >= 3` is false; `-100`
  granted a hundred extra calls), and so did a `quotas` or `quotaUsed` table whose bad value
  was inherited, non-enumerable or behind a getter — validation walked own enumerable keys
  while `table[op]` reads through the prototype. **`quotas`, `costOverrides`,
  `timeoutOverrides` and `quotaUsed` must now be plain objects of data properties** (a `Map`
  or class instance is refused; a `Map` never worked — `map[op]` is `undefined`, so it
  silently meant "no quota"). **`vm.run` reads its options exactly once**: each option is
  read a single time (an accessor on the options object is refused — a getter could answer
  the check one way and the run another), checked, and frozen, and the run reads only that
  record. The three static tables are built from the entries the check saw, so changing them
  after `vm.run` starts has no effect. `quotaUsed` is shared by design, so it is the caller's
  own object, checked at every read: a corrupted value refuses the next step, a frozen or
  read-only counter is refused up front, and a run never counts BELOW its own tally — a
  shared counter can raise a run's count (that is how a quota holds across nested runs) but
  not lower it. Across nested runs the shared counter is trusted: it is host code. A quota
  slot is spent before the fuel check, so a call can never happen uncounted. A custom atom named like an
  `Object.prototype` member (`toString`) is no longer charged by that member's function.
  **Every `vm.run` option is classified** in a table keyed by the options type, so an option
  added without saying whether it is a budget fails to compile — the class blocked this
  release five review rounds running. `src/budget-funnel.test.ts` is a second line for
  budgets read outside a run, and names its blind spots. **`new AgentVM` now throws** on a
  hand-built atom (one not made with `defineAtom`) whose `timeoutMs` is not a number or a
  function — in 0.13 a `null` quietly took the 1000ms default and `NaN` disabled the
  atom's timeout; build the VM inside a try if its atoms come from configuration.
  `compilePredicate` throws on an export name the verifier did not certify;
  `emitVerifiedPredicate` refuses `fuel: Infinity`, because emitted code runs in someone
  else's program.
- **`runCode` and `transpileCode` refuse guest-built source over the source cap (8KB)** before the host's
  transpiler sees it, and charge per character. Transpilation is super-linear and runs
  before fuel or timeout can stop it: 160KB of generated comments took 284 seconds.
- **Emitted code uses an installed `globalThis.__tjs` only if it speaks the same runtime ABI**
  (`abi`, now 2); otherwise it uses its own inline runtime. A 0.13 runtime's `typeError`
  ignores the propagation argument, so 0.14 code running under one replaced the caller's
  error with a fresh "got object" error. `versionsCompatible` now follows semver below 1.0,
  so 0.13 and 0.14 count as incompatible. **Embedders that pin tjs-lang in more than one
  place (tosijs-ui pins it twice) should bump every pin together.**

**What may newly REJECT something** — each is a fix, and each can turn a value that used to
pass into a returned `MonadicError`:

- **`Type` examples mean what they say.** A float example (`0.0`) no longer means integer, a
  `+N` is non-negative, `'' | undefined` is a union (it was bitwise OR, `0`), type names and
  references to other types are checked, and a recursive type is checked all the way down.
  A value your example only matched by accident may now be rejected; one it wrongly
  rejected (9.99 against `0.0`) now passes. A recursive type is checked to any depth (a
  50,000-deep list, on Node as on Bun) in time linear in the data; cyclic data is accepted
  when every node satisfies its type.
- **`default:` inside a `Type` block is a transpile error.** It was never read. Write
  `Type T = 0` (or `Type T = 0 { … }`).
- **An `example:` that cannot be read, and a type defined only in terms of itself
  (`Type T = T`, `A = B` with `B = A`), are transpile errors.** The first used to become a
  type that accepted everything; the second accepts every object under coinduction.
- **A `Type` with a predicate enforces its example with no runtime installed.** Standalone
  emitted code used to skip the example and run only the predicate, so a predicate wider
  than its example now rejects what the example rejects.
- **`TjsStrict` on converted TypeScript now validates arguments.** A TS `object` parameter is
  checked only loosely (anything is accepted): TJS has no "non-primitive" type yet, and
  under-checking is the safe direction.
- **`Type X = …` reads the whole default expression.** `Type Opt = '' | undefined` used to emit
  `Type(…, '') | undefined` — bitwise OR, so `Opt` was the number 0 — and an object default
  stopped at its first `}`.
- **A `.tjs` template literal is no longer rewritten.** The first-assignment auto-`const` ran
  over raw source, so a template line like `Red = 'red'` got `const ` written into the STRING.
- An anonymous `export default function` is reported as `default` in `result.types` (it was
  `anonymous`, attached to a binding that did not exist), with its return type in
  `result.types` and the `.d.ts`, and `default` in its error paths.
- Size, measured against the published v0.13.13 (KiB, zlib default — the README's method):
  `tjs-lang/lang` grew 6.2 KB gzipped (98.2 → 104.4), the full `tjs-lang` entry 7.5 KB,
  `tjs-lang/browser` 6.2 KB, `tjs-lang/lang/from-ts` 1.7 KB, `tjs-lang/eval` 1.0 KB and
  `tjs-lang/vm` 0.9 KB. (An earlier draft of this entry said +2.1 KB, measured against a
  build that already had some of the growth.) In EMITTED code: a file whose `Type` examples
  carry a marker (`0.0`, a union, `+0`) carries ~1.7 KB (0.7 KB gzipped) more inline
  runtime, and only a file with a recursive reference also carries the solver, ~5.8 KB
  (2.1 KB gzipped) in all; a file with no marker carries nothing. Per call, a marked
  non-recursive Type measured ~15% slower than an unmarked one (0.039 vs 0.034 µs), and a
  recursive Type ~5× an unmarked one — the price of checking a value that can recurse.

### Changed

- **The documentation is organised as a book-shaped site.** Every doc now carries its place in
  a navigation hierarchy and in one or more books, for the tosijs-ui doc site that will
  replace the playground:

  | section                                         | contents                                                                                                  |
  | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
  | **TJS**                                         | the language guide, TJS for JS developers, and the examples, grouped                                      |
  | **AJS**                                         | the AJS guide, **Safe Eval**, the LLM prompt, and the examples                                            |
  | **TypeScript**                                  | TJS for TypeScript developers, and **14 TypeScript examples** as pages                                    |
  | **The TJS Language**                            | why TJS exists, its principles, **Declarations**, the syntax reference, and the design notes from `docs/` |
  | **TypeScript: the Good, the Bad, and the Ugly** | the measured comparison with TypeScript, plus two design notes; being written                             |

  Four books build from the same source: the whole site, _The TJS Programming Language_,
  _TypeScript: the Good, the Bad, and the Ugly_, and _AJS and Safe Eval_.

  Two chapters are new. **Declarations** grows the old "Type Declarations" example into a chapter:
  what each of `Type`, `Enum`, `Union`, `Generic` and `FunctionPredicate` declares, why each is
  a callable predicate, and why a `FunctionPredicate` is a function's _type_ rather than a value
  predicate. **Safe Eval** gathers what was spread across the README and two guides behind one
  argument: `eval` is too powerful to allow, so the goal was the most powerful eval that is
  still safe, and it ended up the one part of a program guaranteed to halt.

  **Renamed files** (the doc site identifies pages by bare filename, and these collided):
  `guides/benchmarks.md` → `guides/performance.md`,
  `guides/examples/ajs/error-handling.md` → `ajs-error-handling.md`,
  `guides/examples/tjs/dictionary-defaults.md` → `dictionary-defaults-example.md`, and
  `guides/examples/tjs/type-declarations.md` → `declarations.md`. Agent instructions, TODO lists
  and working notes are marked hidden and will not be published on the site.

- **`tosijs-ui` 1.14.1 → 1.15.0, `tosijs` 1.6.1 → 1.10.3** (devDependencies; no effect on your
  install). 1.15.0 fixes two of the three defects that were blocking the doc-system migration:
  `SiteConfig` gained `ignoreDocPaths`, and `extractDocs`'s frontmatter match is anchored to
  line start — so `CLAUDE.md`, which documents the format, is no longer classified as a
  playground example by its own illustration. The third (`docsJson` defaulting outside
  `outputDir`) is documented rather than fixed, which is workable since we set it explicitly.

  The `tosijs` upgrade tightened `Component`'s typing and caught two long-standing misuses in
  our demo: reading `contentVisible` (a static initAttribute) instead of the intended
  `navVisible`, and assigning a raw boolean to a `BoxedScalar`. Both had "worked" only because
  the old types permitted arbitrary property access.

- **Runtime types are now FUNCTIONS with properties, not objects with a callable `check`.**
  `Type`, `Enum`, `Union` and `FunctionPredicate` all return callables:

  ```js
  const Age = Type('Age', 0)
  Age(5)                    // true      — callable, the natural JS idiom
  Age.check(5)              // true      — the SAME function; one implementation
  Age instanceof Predicate  // true      — a real prototype chain, no Symbol.hasInstance
  Age.name                  // 'Age'     — introspection, for autocomplete and messages
  Age.example               // 0         — a Type is a Predicate carrying a witness
  ```

  That block is **library** code (`import { Type, Predicate } from 'tjs-lang'`). Types declared
  in a `.tjs` file are callable, `check`-is-the-function and named the same way — but they come
  from the file's inline runtime, which does not share the library's class, so an emitted type
  is **not** `instanceof Predicate`, and has no `.example` (the stub calls it `__ex`). Both are
  recorded in `docs/type-identity.md` under "Surface, not decisions" and pinned by tests;
  joining emitted types to the brand is tracked in `TODO.md`.

  **Additive in practice.** `.check()` still works, `isRuntimeType()` still recognises them,
  and object-shaped types from older emitted output still match — the two `typeof === 'object'`
  guards were widened rather than replaced. Emitted types are file-local `const`s that never
  cross a module boundary, so there is no old-stub/new-shape skew to migrate.

  The point is not ergonomics. A verified plain function becomes a predicate by **attaching
  properties** — no wrapping, no lifting, no two populations to reconcile — and `check` _being_
  the function means one implementation that cannot drift from itself. `name` is set
  deliberately because it is real introspection; `length` is inherited from `Function` and says
  nothing.

  **Runtime types now serialise**, which they never did. `JSON.stringify(Type('Age', 0))`
  previously **threw** — "Maximum call stack size exceeded", because a Type carries a `schema`
  object with internal cycles. Making types callable turned that throw into a silent
  `undefined` (JSON drops functions), which is how the gap was noticed; a `toJSON` now emits
  the type's FACTS:

  ```js
  JSON.stringify(Type('Age', 0))
  // {"description":"Age","example":0,"default":0,"__runtimeType":true}
  ```

  **In emitted code too** — and that is not a footnote. The `toJSON` first landed only in the
  real runtime, while emitted files call the inline `__pred` stub through their own `__tjs_rt`,
  so the stub always wins and **is** the shipped semantics (`docs/type-identity.md`). For a
  release cycle the claim above was true of the library and false of every emitted `.tjs`,
  where `JSON.stringify(Age)` returned `undefined` and `JSON.stringify({field: Age})` returned
  `{}` — the key vanishing with no error, which is exactly the silent data loss the `toJSON`
  exists to prevent. The stub now serialises to **byte-identically what a pre-0.14.0
  plain-object type produced**, so this is a restoration rather than a new format, and the
  emitted and real `Enum` serialise to the same facts.

  One measured divergence remains and is pinned rather than glossed: the stub names a Type's
  witness `__ex` where the real runtime says `example`. Same information, two names, so
  `Age.example` is `undefined` in emitted code. Unifying them changes the serialised shape, so
  it is tracked in `TODO.md` as a deliberate change rather than made in passing.

  New capability rather than restored behaviour — worth stating precisely, because it looks
  like a regression fix and is not.

  **The one genuine break** is `typeof`: a runtime type reports `'function'` where it used to
  report `'object'`. Everything else a consumer is likely to touch is unchanged — `.check()`,
  `isRuntimeType()`, `Object.keys()`, spread — and serialisation improved from throwing.

  The inline stub emits callables too — **all five constructors**, not just `Type` — so emitted
  code and the real runtime agree on shape as well as on decisions. Pinned by
  `src/lang/predicate-callable.test.ts`, whose library and emitted assertions are driven from
  **one shared table**, so a form covered on one side must pass on the other.

  `Predicate` is exported from `tjs-lang` and `tjs-lang/css`, and is claimed through a
  shape-versioned global slot (the `MonadicError` treatment, `docs/runtime-fusion.md`) so it is
  the **same class** across bundles — without that, `isColor instanceof Predicate` would be
  false for a consumer importing the brand from one entry and the predicate from another.

- **Verified predicates are `Predicate`s too** — `isColor instanceof Predicate` is `true`, and
  `tjs-lang/css`'s nineteen unary validators all carry the brand.

  This is the payoff of predicates being functions rather than objects: `isColor` was already
  an ordinary function, so becoming a `Predicate` meant **attaching facts**, not lifting it
  into a different shape. `isColor` and `Type('age', 0)` are now the same kind of thing, which
  is what lets one concept cover both in `$predicate`, in the documentation, and in editor
  introspection.

  A bare predicate takes the defaults and claims **no structure** — `toJSONSchema()` returns
  `{ $predicate: … }`, and it carries neither `example` nor `values`, because it has neither.
  That is the progressive-enhancement story exactly: a naive validator sees "anything", an
  aware one runs the predicate.

  `isStyleValueFor(prop, val)` is deliberately **not** branded: it is a binary relation, and a
  predicate's contract is `check(v)` over one value. Branding it would degrade `instanceof
Predicate` to "callable and boolean-ish".

  **That rule is now enforced in `brandPredicate`, which declines any function declaring more
  than one parameter and hands it back untouched.** It previously existed only as a hand-written
  carve-out in `src/css/index.ts`, while `compilePredicate` — the public API that _produces_
  functions of exactly that shape — branded every cluster export unconditionally. One rule, two
  addresses, drifted at birth.

  It was not cosmetic. `checkType` dispatches on the presence of `check`, and branding sets
  `check` to the function itself, so a branded relation took the runtime-type branch and was
  invoked with **one** argument, its second parameter silently `undefined`. Measured:
  `compilePredicate('function differsFrom(a, b) { return a !== b }', …)` then
  `checkType(v, differsFrom)` returned `null` for every `v` — a validator that always says yes,
  with no diagnostic. Rearranged, the same bug fails closed instead, or throws.

  The brand also **erased the evidence**: `compilePredicate` wraps each export in a rest-args
  fuel closure, so `fn.length` read `0`, removing the one defence a careful consumer had and
  leaving the rule nothing to act on. The wrapper now carries the underlying function's arity
  and name, so a declined relation is still properly introspectable. Arity `0` is branded —
  `(...args) => …` and `() => …` both report it, so zero means _unknown_, not _not a predicate_.
  Pinned by `src/lang/predicate-arity.test.ts`, which includes a sweep asserting no binary
  export anywhere in `tjs-lang/css` carries the brand.

### Added

- **The AJS AST carries a format version** — `{"$ajs": 1, "op": "seq", …}` — and the VM acts on
  it: an AST declaring a version this build does not understand is **refused**, not executed.

  An AST is a **persisted artifact** — `procedureStore` maps `proc_…` tokens to stored ASTs, and
  consumers serialise agents — so data written today is read by code written later.

  To be precise about the deadline, since an earlier draft of this entry overstated it:
  **"absent means 1" is a total, unambiguous rule**, so adding the field late would have been
  perfectly sound _provided the format had not changed first_. The real hazard is shipping a v2
  format without the field, at which point absent becomes genuinely ambiguous and permanently
  so. The window is "before the format first changes", and this is comfortably inside it — cheap
  insurance bought early, not a catastrophe averted. The standing obligation: **never change the
  AST format without bumping the version.**

  It does not eliminate unversioned ASTs — already-persisted ones have no field and keep
  working. What it does is **stop the population growing**, turning an unbounded set into a
  finite, shrinking one.

  That claim holds only if **every producer** stamps the field, and one did not: the
  transpiler stamped it while `TypedBuilder.toJSON()` did not, so `Agent.take(…)…toJSON()`
  minted an unversioned AST on every call. The builder is not a lesser path — its output goes
  to `vm.run` and `storeProcedure` on identical terms, so it is persisted on identical terms,
  and a single unstamped producer falsifies the claim outright rather than partially. Now
  stamped, at the **root only**: nested branches splice `.steps` rather than re-serialising, so
  an inner `seq` never carries a field that would describe nothing there. Guarded the same way
  the boundaries are, by `src/vm/ast-version-producers.test.ts` — both producers asserted
  behaviourally, plus a scan that fails on **any** file constructing a `seq` root without the
  stamp, so a third producer lands there by name. The assertion is `'$ajs' in ast`, never
  `astVersionOf(ast) === 1`: the latter passes for exactly the unversioned ASTs it is meant to
  catch, since absent reads back as the legacy default, which is 1.

  The refusal is the part that makes the field real rather than decorative: running an AST whose
  format we cannot read means guessing at the meaning of untrusted code. The version is checked
  **before** the root shape, so a newer AST reports its version rather than "Root AST must be
  `'seq'`" — a diagnosis that would send the reader to entirely the wrong problem.

  **The gate applies at every boundary an AST enters through**, not just `AgentVM.run()`:
  `resolveProcedureToken` (which is how `agentRun` receives one) and `storeProcedure` are
  gated too. `storeProcedure` matters most — it validates at **storage**, because persisting
  an AST this build cannot run defers the failure to whoever resolves the token later, who did
  not write it and has no context for the error.

  The general defect was that nothing made the boundary set **enumerable**, so the next entry
  point would have been missed the same way while every existing test stayed green.
  `src/vm/ast-version-boundaries.test.ts` closes that: it drives a future-version AST through
  each boundary, asserts the gate is present in each function body, and flags **any** other
  reader of `procedureStore` that is ungated — so a fourth boundary nobody listed fails there.
  Mutation-tested: removing any one gate turns it red.

  Groundwork for `docs/ajs-native-vm.md` (a Rust → wasm VM), where a second implementation
  reading the same ASTs makes versioning load-bearing rather than merely prudent.

### Fixed

- **Converted TypeScript that opts into validation now accepts valid data.** Found by running
  the TypeScript examples, which had only ever been compiled: three promised "invalid calls
  return error objects" and printed `Hello, 42!`, and "The Full Picture" threw on its own
  bad-input demo. Three defects stacked, each hiding the next:

  - **`TjsStrict` did not turn on input validation.** Documented as "opts TS-originated code
    into full TJS; .tjs has this already", it set every mode and left converted code's
    `safety none` in place — so the one opt-in a TypeScript author can write validated
    nothing. It now restores the native default; an explicit `safety` directive still wins.
  - **A `Type` example lost everything the source said that its value cannot.** Examples
    are evaluated and matched by value, so `0.0 === 0` made `Type Price = 0.0` reject 9.99
    (narrowed to integer), `{ count: +0 }` accepted -1, `'' | undefined` evaluated as
    bitwise OR (to `0`), and `string` or a later-declared type was a `ReferenceError`.
    Parameter types never had the problem — they read the AST. `Type` examples now do too,
    with the same rules: floats, `+N`, unions (an all-literal union is still a closed set),
    sound type names, optional members (`T | undefined` may be absent), and references to
    other types, read lazily so forward and recursive references work. **Native TJS is
    affected as well** — `Type Price = 0.0` was wrong in every file. An example with none of
    these emits exactly the code it did before.
  - **`fromTS` wrote interface examples that rejected valid data.** `any`, and every
    reference to another interface, became `null` ("must be null"), so `Product[]` accepted
    only arrays of nulls; optional members were required. Interface and type-alias examples
    now say `any`, name the referenced type, and mark optional members — which also makes
    converted code read like the TypeScript it came from.

  The TypeScript examples now run in CI, and the four that demonstrate a rejection must show
  it while accepting their valid input. A `default:` member inside a `Type` block — never
  read, so `Type T { default: 0 }` accepted every value — is now an error naming the real
  spelling, `Type T = 0`.

- **A Generic's parameter defaults are types, too.** A default like `<T = number | bigint>`
  became `0.0 | 0n` — bitwise OR, which throws `Cannot mix BigInt and other types` at load.
  It now gets the same reading as a `Type` example. (A Generic _instantiation_, `Box(0.0)`,
  is still ordinary call code, so a float argument there still narrows to integer — deciding
  that a call argument is a type needs scope analysis; see TODO. Use `Box(1.5)`.)

- **An anonymous `export default function (…)` could not be imported.** Its metadata was
  attached to a binding named `anonymous` that does not exist. It now gets a local binding,
  exports the same hoisted function, and keeps the `.name` JavaScript gives it, `'default'`.

- **Four `fromTS` defects that kept zod from loading at all**, found by running zod's own
  suite for the first time (it had been skipped on every run for want of `pnpm`):

  - `export enum` lost its `export`;
  - an interface whose name the file uses as a value — zod's `interface File` beside
    `instanceof File`, the global — was promoted to a runtime `Type` that shadowed it;
  - a default import beside named ones was dropped (`import config, { A }` → `import { A }`),
    and `import def, { type T }` vanished whole;
  - plus the Generic default and anonymous default export above.

  Zod now runs **1959 of its tests, all passing**; before, 105 of its suites failed to load.
  That was invisible because **the compat harnesses counted only failed TESTS**: a suite that
  cannot load contributes none, so zod reported "551/552 passed". All five vitest harnesses
  now fail on a failed suite.

- **`Eval` context values were invisible to atoms.** `Eval` wrapped code in a function with
  no parameters, so `context` reached plain expressions but not atoms: `items.filter(…)`
  failed with "items is not an array". Context keys are now declared parameters. Found by
  running the README's own example, which the rewritten Safe Eval chapter now executes along
  with every example in it. The keys are imported as variables at the AST level (a
  `varsImport` step), never spliced into source: declaring them as parameters put
  caller-controlled text into the transpiled source, where `maxSourceBytes` never measured
  it, and 80k keys took over a minute to transpile before fuel or timeout applied (the
  hosted endpoints pass request arguments as context). Only keys the code uses as
  IDENTIFIERS are imported — read from the parse, after the size gate, so a name inside a
  template interpolation counts and a string literal does not. A key never rebinds a builtin
  or a global value (`Math`, `JSON`, `NaN`, `undefined`…), and code may declare a local
  (`let` or `const`) that shares a key's name, which shadows it. A non-string `code` is an
  error result, not a throw, and an expression containing a callback with `return`
  (`xs.map(v => { return v * k })`) is treated as an expression.
- **AJS: a block-bodied callback works.** `[1, 2].map(v => { return v * 3 })` failed the
  whole run with "Agent must return an object" — the callback's `return` was held to the
  AGENT's rule — and a returned object was dropped (`[null, null]`), because `map` and
  `reduce` read only the `result` an expression body binds. Only expression-bodied arrows
  worked. A callback body now runs as a function: its `return`, early or not, is its value.
- **AJS: `const` is per binding, per scope.** A block-level `const` could not shadow ANY
  outer binding (redeclaration was checked up the whole scope chain), and an inner `const x`
  made an unrelated outer `x` unassignable for the rest of the run (const-ness was one set of
  names). Redeclaration is now refused in the same scope only, and reassignment only when the
  binding a write resolves to is a `const`. **Still open:** a `const` inside a `while` body
  fails on the second iteration, because the body does not get a scope of its own — part
  of the AJS AST v2 work (TODO), with assignment to an outer variable from inside a block
  or loop.
- **AJS: a `for...of` body is a loop, not a callback.** A `return` inside it now ends the
  agent, as in JavaScript: an object return (`return { allowed: false }`) was silently
  swallowed and the agent carried on to the code after the loop. The transpiler marks the
  node `loop: true`; `map` without it keeps callback semantics.
- **AJS: `memoize` and `cache` bodies are callbacks too** — a scalar `return` is the value,
  not an agent-rule violation — and a sub-agent or `runCode` started inside a callback is
  held to the agent rule again (the callback's exemption leaked into it).

- **`fromTS` class metadata keeps what the code erases**: OVERLOAD signatures — of methods,
  static methods AND constructors — and `abstract`. Both are rightly erased from the emitted
  code — neither exists at runtime — but metadata is where TJS keeps types, and both were being
  lost there too. Members were recorded by name, so the implementation silently overwrote each
  signature: `m(string): string` and `m(number): number` collapsed to the implementation's
  `m(a: any)`. Methods and abstract classes now carry `abstract: true`. Read from
  `fromTS(…).classes`.

  **One rule for the shape**, documented on `FunctionTypeInfo.overloads`: `overloads` is the
  full list of CALLABLE signatures, present only when there is more than one; the entry is a
  summary — the implementation, or the FIRST signature when there is none (ambient or abstract).
  Render `overloads` when present. Note for consumers of a group WITHOUT an implementation: the
  entry used to be the LAST signature and is now the first, matching declaration order.

  **Scope, stated plainly:** this is `fromTS`'s metadata. TJS's OWN introspection — runtime
  `__tjs` metadata, the playground's autocomplete — comes from TJS source, which has no way yet
  to express an abstract member or an overload signature, so those facts do not survive
  TS → TJS → JS. Carrying them through needs TJS syntax for a signature-only member; tracked in
  `TODO.md`.

- **`fromTS` no longer fabricates runtime values from ambient declarations.** `declare
function f(…)` became `function f(…) { }` and `declare enum E` a real `Enum` — values
  TypeScript never emits, which shadow the real one that lives elsewhere. Two `export declare
function f` signatures became two `export function f`: a duplicate declaration that an ES
  module refuses to load. Ambient VALUE declarations (`function`, `enum`, `const`/`let`/`var`,
  `namespace`) now contribute metadata and no code, matching `declare class`. Ambient TYPE
  declarations (`declare interface`, `declare type`) are unaffected — in TJS a type IS a runtime
  `Type`, and `declare` adds nothing to one.

- **`fromTS` now THROWS on constructs it refuses**, where it used to return (lossy) output:
  decorators anywhere in the file, `accessor`, and any modifier or class member kind it has no
  rule for. If you convert a whole tree, catch per file. A refusal is a `FromTSRefusal` with
  `code: 'FROMTS_REFUSED'`, so it can be told apart from a converter bug; the message names the
  file, line and a remedy. The `tjs convert` CLI already reports per file.

- **`fromTS` no longer silently drops class constructs it does not understand.** It rebuilds a
  class from parts, from a closed whitelist, and anything off the list simply did not come out —
  no error, no warning. Measured, every one converting "successfully":

  | TypeScript                                  | was                                      | now                                                 |
  | ------------------------------------------- | ---------------------------------------- | --------------------------------------------------- |
  | decorators — class, method, property, param | **dropped**, behaviour deleted           | **refused**, with the line and a remedy             |
  | `static { … }`                              | **dropped** — the code never ran         | converted                                           |
  | `export default class F`                    | `export class F` — default imports break | `export default class F`                            |
  | `abstract m(): T`                           | an **empty method** `m() { }`            | erased, as TypeScript erases it                     |
  | method overload signatures                  | one extra empty method each              | erased — one method                                 |
  | `declare class X { … }` (ambient)           | a **fabricated** runtime class           | erased — it describes a class that exists elsewhere |
  | `accessor x`                                | dropped                                  | refused                                             |
  | `constructor(override a: number)`           | `this.a = a` **dropped**                 | converted — `override` makes a parameter property   |

  Decorators are refused rather than converted because they cannot be converted faithfully:
  `fromTS` reads no tsconfig, so it cannot tell legacy `experimentalDecorators` semantics from
  TC39's (the TypeScript 5 default) — which call them differently — and TJS cannot carry `@`
  syntax. The class transform now classifies every member kind and modifier — on the class, its
  members, and constructor parameters — and **refuses anything it cannot classify**, so new
  syntax fails loudly instead of vanishing. Found through a permanently skipped test whose
  reason ("does not parse") had quietly become false, and whose assertion would have gone
  GREEN on the lossy output had anyone simply unskipped it. The ambient-class case was found
  by the compat corpus (kysely) the moment the modifier table existed.

- **`agentRun` with an inline AST, and `runCode`, now apply the AST version gate.** Both
  executed an AST of a format this build cannot read — `{op:'agentRun', agentId: <a $ajs:99
AST>}` ran its body, with no capability required, and `runCode` ran whatever a host
  transpiler returned. Harmless today (`AST_VERSION` is 1) and wrong-semantics execution of
  untrusted code the day a v2 format ships. Every execution of an AST that arrived from outside
  is now gated, and `ast-version-boundaries.test.ts` sweeps every such site, so a new one fails
  there by name.

- **Emitted `Generic` instances are callable**, like every other runtime type. `Box(0)` was a
  plain object in emitted code — `Box(0)(v)` threw `is not a function` — while the library's
  is a callable `Predicate`. The factory itself is still not a predicate, in both.

- **`Object.keys()` and spread really are unchanged from 0.13.13.** Serialisation support added
  `toJSON` as an ordinary enumerable property, which added a key and made a spread copy carry
  a `toJSON` bound to the ORIGINAL — so `{...Age, description: 'X'}` serialised as `Age`. It
  is now non-enumerable; `JSON.stringify` still finds it. Pinned against the key list the
  published 0.13.13 produces.

- **A non-constructor in `Predicate`'s global slot is no longer adopted.** If something other
  than a class sits under `globalThis.__tjs_Predicate_1`, this bundle uses its own class and
  records a warning, instead of breaking every predicate at once. Any constructor found there
  is still adopted, unchecked — the same trust model as `MonadicError`
  (`docs/runtime-fusion.md` §7).

- **A present-but-unreadable `$ajs` field is refused, not read as version 1.** `{"$ajs":"2"}` —
  a v2 AST whose number a JSON codec stringified in transit — was read as legacy and
  **executed**, the exact misreading the field exists to prevent. Reading a field as legacy
  is not distrusting it; legacy means "run it as v1". Absence proves an AST predates
  versioning, but a present-and-malformed field proves the opposite: something that stamps
  versions wrote it. Now: absent → legacy, a positive integer → itself, anything else →
  refused, with a message naming the likely codec cause.

- **`package.json` `sideEffects` named one module; five have module-scope effects.** Everything
  an allowlist omits is asserted pure, so a bundler honouring it could skip `src/index.ts`,
  `src/vm/index.ts` or `src/lang/eval.ts` — each wires the transpiler at load — and hand a
  consumer a VM that refuses source, visible only in a production bundle. Worse,
  `tjs-lang/bun-plugin`, whose documented usage is a bare `import`, which is the exact import an
  allowlist lets a bundler drop. The list is now **derived**: `src/side-effects-allowlist.test.ts`
  scans every module for module-scope calls over a literal-masked view and fails on any that is
  neither listed nor exempt with a reason. Deriving is what found the plugin; the review that
  prompted this named only the other three.

- **Emitted `Union` and `Exactly` named themselves `f`.** Their inline stubs called `__pred`
  without the name argument that `Type` and `Enum` pass, so `.name` was the stub's closure
  name, in stack traces and autocomplete alike. The real runtime names both from the
  description; the emitted code now does too.

- **`expect(str).toContain(nonString)` reported a comparison that never happened** —
  `Expected "12345" to contain 234` for a needle refused on type. It now says so:
  `toContain on a string needs a string needle, got number (234)`.

- **`expect(…).toContain(…)` now does substring on strings.** It was array-only — the guard
  read `!Array.isArray(actual) || …`, so a string could never pass however plainly it
  contained the argument, and the failure was reported as
  `Expected "a.tjs:17:sized.opts.x" to contain "opts.x"`: a **content** message for what was
  really a **type** refusal. Every comparable harness (bun:test, jest) does substring, so the
  spelling that works everywhere else failed here and misdescribed why.

  A non-string, non-array haystack now says `toContain expects a string or an array`, rather
  than describing a comparison that never happened.

  Found by writing a documentation example, not by a test — which is the argument for
  examples being executed rather than illustrative.

### Added

- **`tjs-lang/vm-ast` — the VM with no parser in the bundle.** The same `AgentVM`, built
  without the transpiler wiring: **56 KB against 221 KB** (measured at the time; ~65 KB raw / ~21 KB gzipped at 0.14.0 rc), and no acorn. It takes an **AST**;
  transpile on the caller's side with `tjs-lang/lang` and send the AST.

  ```ts
  import { transpile } from 'tjs-lang/lang' // where the source is YOURS
  import { AgentVM } from 'tjs-lang/vm-ast' // where the guest code runs

  const { ast } = transpile(source)
  await new AgentVM().run(ast, args)
  ```

  The size is the smaller half of the argument. `AgentVM.run()` accepts source, and resolving
  it meant a static import of the transpiler — so **a sandbox shipped a parser, reachable from
  untrusted input, upstream of fuel, timeouts, capabilities and the membrane.** That is the
  position the `test`-block leak occupied in 0.13.10; the leak was closed, but the shape that
  permitted it survived. This removes the shape, by not shipping the code.

  It also makes good on what "code travels to data" already claimed: if the AST is the wire
  format, the string never crosses the boundary and the far side has no reason to read one.

  **Purely additive — `tjs-lang/vm` is untouched** and still accepts source. The transpiler is
  now injected (`setTranspiler`), and every batteries-included entry supplies it explicitly.

  **The guarantee, stated precisely:** _no parser is present in this bundle_ — **not** "this
  object refuses source even when a parser is loaded". The binding is module-level, so an app
  importing both entries shares it. That is the guarantee worth having (anyone importing
  `tjs-lang/vm` already has the parser), but it is not the stronger one, and the difference is
  load-bearing: the test asserting refusal has to run in its own process, because it passed
  alone and failed under `bun test` once another file imported the main entry.

## [0.13.13] — 2026-09-13

### Read this first if you use `unsafe`

**A patch release normally cannot take anything away. This one can, so it is named here
rather than left for you to discover.** `var` and `eval` were reachable in a `.tjs` file
behind the `unsafe` marker; they are now refused outright, so source that compiled under
0.13.12 can fail under 0.13.13:

```
unsafe var x = 1      // 0.13.12: compiled     0.13.13: refused
unsafe eval(src)      // 0.13.12: compiled     0.13.13: refused
```

The full reasoning is under **Changed** below; briefly, `eval` could not be given a named
replacement even in principle, because direct `eval` reads the CALLER's scope and any wrapper
would silently be a different operation. `unsafe` itself is only **deprecated** — it warns and
keeps working.

It is a patch deliberately: the version line stays on 0.13.x until the tosijs-ui migration and
`tjs doc` land, and 0.14.0 is reserved for that release. Pinning `~0.13.12` will not protect
you from this; pin exactly if you rely on either escape.

### Added

- **`/# … #/` — a doc comment TJS actually owns.** Multi-line, nestable, and able to quote any
  syntax without escaping.

  `/*# … */` is an ordinary JavaScript block comment, so it inherits the rule that block
  comments do not nest and ends at the first `*/` inside it. A doc comment that cannot contain
  `*/` cannot document comment syntax — which is exactly what a language's own documentation
  must do. Escaping works and is what this repo did; an escape you must remember is a trap that
  fails as a parse error some distance from the cause.

  **Claiming this syntax takes nothing away from anyone**, and that is measured rather than
  estimated:

  ```
  const re = /# comment #/       parses — a regex matching #comment#
  const re = /# one
               two #/            SyntaxError: Unterminated regular expression
  ```

  A regex literal cannot contain a raw newline, so the multi-line form is _already_ illegal
  JavaScript. The newline requirement is not a style rule — it is the whole
  subset-preservation argument (`PRINCIPLES.md` invariant 1), and single-line `/#…#/` stays a
  regex forever. Under `dialect: 'js'` the syntax is left entirely alone.

  **Doc comments are blanked at the first point any pass touches the source**, which is the
  structural payoff: a doc comment exists to QUOTE syntax, so it is the place in a file most
  likely to contain the constructs every scanner is hunting for. Blanking it up front means the
  ~30 downstream passes cannot see into it at all — a structural fix for this project's
  dominant defect class rather than one more scanner that has to remember. A `test '…' { … }`,
  a `wasm function`, an `unsafe` marker or a `Type` declaration written inside one is inert.

  `/*# … */` is **retired as a doc comment** but remains a perfectly good block comment, and
  stays the convention in `.ts` sources, where `/# … #/` cannot parse.

  **AJS has them too.** The bar for adding a step to AJS's parser is _"does AJS have this
  construct"_, never _"is it harmless"_ — that distinction exists because seven TJS constructs
  once leaked onto the AJS path and one executed submitted source. A doc comment clears it:
  AJS source deserves to carry its own documentation exactly as TJS source does, and blanking
  a comment is lexical rather than semantic. Withholding it would have broken the subset in
  the awkward direction — a documented `.ajs` file would be legal TJS and illegal AJS.

  All 34 TJS examples are migrated (47 doc comments), and `src/rbac/rules.tjs` /
  `src/linalg/index.tjs` are the first real sources to use it. `.tjs` files now publish their
  own doc comments into the corpus, so a `.tjs` file is a literate program on its own terms —
  the documentation is in the language, and rendering it does not depend on anyone's build
  system.

  One thing the migration exposed: `/*# … */` doc comments were **shipping into emitted
  JavaScript**, because they are valid JS. `js-footgun-fixes` emitted 265 characters, all of
  them comment and none of them code — and a test asserting `code.length > 0` had been passing
  on that padding. It now emits nothing, correctly, and the assertion tests what it meant to.

- **`unsafe` is deprecated.** Every remaining use now warns, naming its replacement. The
  marker is the only escape in this language that does not say WHICH rule it suspends, and it
  is what drags `/* @tjs-unsafe */` along as a comment channel.

  `var` and `eval` are now refused outright rather than offered an escape, and their
  diagnostics say so and say why. `eval` in particular **could not** have been given a named
  replacement: direct `eval` is a syntactic form that sees the CALLER's scope, so any wrapper
  would silently be a different operation — reaching the caller's scope is precisely the part
  that is dangerous.

  The reasoning is now a principle (`PRINCIPLES.md`, _"An on-ramp and an off-ramp. No
  craters."_): TJS provides a total file-level on-ramp and a total off-ramp, and a
  per-construct hole in an otherwise-native file is a third kind of thing that relieves the
  pressure to graduate properly. Strict mode and ESM set the precedent — measured, they still
  permit `var` and `eval`, so the lesson is not which constructs went but that **where
  something was removed it was removed outright**.

- **`LegacyDate(x)` — a named confession replacing `unsafe new Date(x)`.** Raw `Date` is banned
  in native TJS (mutable, timezone-dependent) and `Timestamp` is the remedy, but a deliberate
  exception has to be expressible. Until now the only way to say it was the generic marker
  `unsafe`, which is the odd one out in this language: every other escape —
  `DangerousLegacyEquals`, `LegacyExactly`, `LegacyDefault` — is a named, greppable,
  deliberately-ugly callable that announces what debt it is taking on. `unsafe` says only
  "some rule does not apply here" without saying which.

  The diagnostic teaches the new form and still points at `Timestamp` first — the escape is
  not the advice, and there is a test pinning that ordering, because the diagnostic is the
  teaching moment.

- **Prism language definitions for TJS and AJS** — `tjs-lang/editors/prism/tjs` and
  `/prism/ajs`, generated by `bun run build:grammars` from the same `editors/tjs-syntax.ts`
  that already feeds VSCode, Monaco, CodeMirror and Ace. A fifth emitter, not a new grammar,
  so a keyword added to the source reaches all five.

  **A colon example is highlighted as a VALUE, not a type**, and that is the point rather than
  a detail. In `function greet(name: 'Alice')` the `'Alice'` is a real string that survives to
  runtime, and reading it as a type annotation is the single most common mistake people make
  with TJS. Prism renders non-executable fences in doc systems and is being baked into printed
  and ePub output — where a wrong token colour is permanent and highlighting is the main thing
  a reader uses to decode unfamiliar syntax. So it gets its own `example-value` token, aliased
  to `string` so existing themes already render it correctly.

  Also distinguished: safety-marked returns (`:!` / `:?`), block constructs (`test`, `mock`,
  `wasm`, `given`, `extend`), `/*# … */` doc comments, the declaration forms, forbidden
  keywords, and the deliberately-ugly escapes (`unsafe`, `DangerousLegacyEquals`,
  `LegacyDefault`) — which are marked `important` because looking alarming is their job.

  Guarded by `editors/prism.test.ts`, which asserts TOKEN TYPES rather than rendered HTML
  (the class names are what a theme styles against), and pins that every keyword in the source
  of truth reaches the generated definition.

- **`tjs-lang/rbac` — the RBAC rule primitives are now importable.** `interpretRuleResult` and
  the role/shortcut helpers had **no built output at all** (`dist/src/rbac/` carried a lone
  `.d.ts`), so they were reachable only by reading `rules.tjs` out of the tarball. That is how
  #54 arrived: as a fail-open in a _reference implementation nobody could import_ — which also
  meant nobody could receive the fix by upgrading. They now ship as `dist/tjs-rbac.js`,
  verified from a Node consumer.

  Scope is the RULE layer only. `src/rbac/index.ts` imports `tosijs/rbac` and `tosijs/store`,
  and `tosijs` is a **devDependency** here, so exporting that entry would ship a subpath that
  cannot resolve in a consumer's install. `rules.tjs` is self-contained, which is what makes
  it publishable.

- **`./package.json` is exported.** Node refuses `require('tjs-lang/package.json')` without it,
  and several tools read the manifest (bundler plugins, version probes). Found while verifying
  the 0.13.12 tarball, when my own check hit it.

### Changed

- **The build compiles `.tjs` entry points in-process.** esbuild has no `.tjs` loader and
  `buildSync` refuses plugins, so a `.tjs` entry is transpiled and written to a scratch file
  the bundler then reads. Deliberately NOT the shell form `functions/` uses
  (`tjs emit "$f" > "${f%.tjs}.js"`), because `>` truncates the target _before_ the command
  runs — a failed transpile there once left an EMPTY module that bundled and shipped. Writing
  only after a successful transpile means this cannot half-succeed.

  Incidentally now dogfooding: the build of `tjs-lang` compiles `.tjs` with `tjs-lang`.

- **`marked` moved 9 → 18** (a devDependency; it does not affect your install). tosijs-ui
  peer-requires `^16 || ^17 || ^18` and we had 9.1.6, so the renderer behind our doc system was
  a version tosijs-ui does not claim to support — and the doc system is what the next release
  migrates onto.

  Nine majors is a big jump, so it was measured rather than assumed: all 109 docs were rendered
  through both versions. 31 byte-identical, 75 differing only in whitespace between block
  elements, and **3 substantive — two of which were marked 9 getting our prose wrong.** It
  treated a pair of single tildes on one line as strikethrough, so `~0.13 ms (~8K themes/s)`
  rendered as `<del>0.13 ms (</del>8K themes/s)`. Our performance docs use `~` for
  "approximately" throughout. Real `~~strikethrough~~` is unaffected in both.

### Fixed

- **`tjs-lang/rbac` promised TypeScript declarations that no build step produced.** The new
  export's `types` condition pointed at `dist/src/rbac/rules.tjs.d.ts`, and nothing generated
  it: `tsc -p tsconfig.build.json` only sees `.ts`, and `rules.tjs` is not a TypeScript file.
  A TS consumer of a brand-new export got no types at all, while the build reported success.

  Caught by `scripts/prepublish-check.ts`, which resolves every path `exports` names — but
  only at publish time. The reason no _test_ caught it is worth recording: the guard in
  `package-exports.test.ts` excluded `types` conditions **and** excluded `./dist/` paths, so
  a `types` path pointing into `dist/` was invisible twice over. Both exclusions are now
  covered by tests that were mutation-checked (delete the file, watch them go red).

  The build now generates declarations for any `.tjs` entry point whose `types` condition
  names one, driven off the exports map rather than a list, so the next `.tjs` subpath is
  handled without anyone remembering.

- **`demo/docs.json` was ordered by the filesystem, not by its inputs.** The `.tjs` doc walk
  used bare `readdirSync`, which returns APFS order locally and ext4 order in CI, so two
  entries could trade places with no content change. That made the committed-artifact check
  fail for anyone whose filesystem enumerated differently, reporting a stale artifact that was
  perfectly current. Sorted at both levels; verified by generating twice and comparing.

- **The AJS grokkability lane could never run cold, and was scoring repaired output.** Two
  problems in the one harness:

  Its `beforeAll` probes every downloaded model against bun's default **5s** hook timeout, so a
  cold run died before measuring anything — and presented as "pin model not loaded", the
  harness blaming the environment for a question it never got to ask.

  It also ran `fixCommonMistakes` over model output **before** scoring, making every published
  rate a post-repair rate wearing a raw rate's label. Two of its three repairs were rewriting
  `: string` → `: ''` and `: number` → `: 0` — spellings the language has accepted for some
  time via `TYPE_NAMES`. So the harness had been repairing something already fixed, which is
  exactly why the fix could never appear in the number meant to measure it. The rate is now the
  RAW rate; repairs are named and report separately what they would have recovered. First
  honest run against the pin: **20/20**.

  Bare type names are now pinned deterministically by `src/lang/ajs-type-annotations.test.ts`,
  asserting the validator REJECTS the wrong type — accepting the annotation and inferring `any`
  would pass a parses-without-throwing test while validating nothing.

### Security

- **All 11 dependency advisories cleared; both trees report clean.** In the DEPLOYED Cloud
  Functions tree, `qs` (6.15.3 → 6.16.0) and `uuid` (9.0.1 → 11.1.1) were **runtime** scope, so
  "dev-only, not shipped" was never a mitigation for them. At the root: `flatted` → 3.4.4,
  `form-data` → 2.5.6, `protobufjs` → 7.6.6, `esbuild` → 0.28.2. None of these reach a
  consumer's install — the published runtime dependencies (`acorn`, `acorn-loose`,
  `acorn-walk`, `tosijs-schema`) carry no advisories and are unchanged.

- **`bun audit` now runs in CI**, because Dependabot structurally cannot cover this repo's main
  tree. Measured, not assumed: Dependabot had produced alerts for exactly one manifest ever
  (`functions/package-lock.json`), and GitHub's dependency graph held 307 packages with **none
  of the root's own devDependencies** — the root has `bun.lock` and no `package-lock.json`, so
  it is invisible to the graph. At that moment `bun audit` found 9 root advisories (3 high)
  that Dependabot reported as 0. The gate was previously reachable only from the pre-tag lane,
  since `test:fast` sets `SKIP_AUDIT=1`, so between tags nothing checked.

- **`AUDIT_EXEMPTIONS` is now empty**, and the reason is worth recording: all seven entries
  were dated `2026-10-27` and **every one was already fixable**, six with a published fix
  available for months. A dated exemption reads as "handled" — the gate stays green, and green
  is indistinguishable from fixed. The file had already recorded this exact lesson about
  `brace-expansion` and it repeated verbatim; both times the trigger was accidental. The
  guidance now prefers an `overrides` entry, which _fixes_ an advisory, over an exemption,
  which only agrees to ignore it.

## [0.13.12] — 2026-09-06

**Two security-relevant fixes against published 0.13.11, and the end of a defect class.**

`Eval`/`SafeFunction` returned plausible wrong values (#52) — spread was silently dropped and a
dotted read came back as its own source text — and in the reference RBAC layer that inverted
into a **grant** (#54). Both were reported by a consumer running against a known-good oracle,
which is the only way either could have been seen: every wrong value had the right _shape_, so
structural checks, `typeof` and length checks all passed. Separately, `verifyPredicate` was
certifying impure functions as pure, so "verified" meant less than the badge claimed.

The rest is one class of defect, finally closed. **Eight scanners that misread code merely
MENTIONING the syntax they scan for** — a `test` block quoted as data being executed and
deleted, a comment containing `export` consuming the real one, a template's `${…}`
desynchronising the shared literal scanner, a quoted `wasm function` being compiled, a quoted
`Is` operator being transformed. Each was found somewhere other than where it did its damage.

**The dogfood behaviour gate went from 108 broken tests to zero.** Every test suite we ship now
converts to TJS, runs, and preserves every assertion — all three 1.0 self-hosting gates at
zero, both ratchets pinned at 1.0. Roughly two thirds of that distance was defects in the gate
rather than in the language, which is recorded at the baseline so the next bad number there is
read as a question about the apparatus first.

### Fixed — silent wrong values in `Eval`/`SafeFunction`

- **`Eval`/`SafeFunction` returned plausible wrong values (#52).** Two defects, both silent,
  both shipped in 0.13.11. Neither was a runtime bug — the AST emitter built an AST that did
  not represent the source:

  ```
  return data.a      ->  {"op":"return","value":"data.a"}                <- a bare STRING
  return data["a"]   ->  {"op":"return","value":{"$expr":"member",...}}  <- correct
  return { ...d }    ->  {"op":"return","value":{}}                      <- spread DROPPED
  ```

  **Spread is now compiled instead of skipped.** Both object/array literal handlers matched
  `Property` only, so a `SpreadElement` fell off with no error: `{ ...doc, rev: 1 }` returned
  `{ rev: 1 }` and lost every original field, and `[...a]` returned `[null]` — length 1, the
  hole presenting as a value. It now desugars to the call it means (`Object.assign({}, …)`,
  `[].concat(…)`) and recurses, so the whole path is the one already tested and there is no
  second implementation to drift. Source order is preserved, which is the semantics:
  `{ a: 1, ...d }` lets `d` win and `{ ...d, a: 1 }` does not. `DOCS-AJS.md` documents spread
  under "What's Allowed", so refusing it would have made the doc wrong — the doc was right and
  the emitter was not.

  **A dotted read returns the value, not its source text.** Non-computed member access in
  value position emitted a dot-path string; the computed branch a few lines above already
  emitted a proper node _and said why_ — "so the runtime evaluates the index rather than
  treating it as a string path" — the same reasoning simply had not been applied. The string
  reached `resolveValue`, failed to resolve (the root came from `context`, i.e. args, and the
  traversal only checks state), and fell through to "return the literal string". So the caller
  got back the characters they had written, as data.

  That one line explains every asymmetry in the report: `typeof data.a`, `data.a * 2` and
  `data.a.valueOf()` were all correct because they build real nodes; only the bare return
  substituted. Pure boolean predicates were unaffected, which is why a permissions layer
  looked fine.

  **The string-path optimisation survives where it is provably safe.** It is deliberate and
  has a test to its name, and it is correct whenever the root is a local or a parameter —
  those land in state, where the traversal looks. The emitter now asks (`TransformContext`
  carries `locals`/`parameters` up a scope chain) instead of assuming.

  **`resolveValue`'s literal fallback is recorded, not changed.** It cannot simply become an
  error: a hand-built AST legitimately says `value: 'obj.prop'` (the builder API, 35+ call
  sites) and a program just as legitimately says `'not.a.path'` meaning a string — once both
  are strings they are indistinguishable. So the semantics stand and the near-miss is now
  reported to the flight recorder, which is what it is for.

  Guarded by `src/lang/eval-value-fidelity.test.ts`. Found by tosijs-platform against a
  known-good oracle — worth recording, because both bugs returned values of the right SHAPE,
  so every structural check passed and only a differential comparison could see them.

### Security

- **A rule returning a non-boolean granted access (#54).** `interpretRuleResult` in the
  reference RBAC layer ended with `allowed: !!result`, so every truthy non-boolean was coerced
  to a **grant**. Combined with #52 — a dotted read returning its own source text — the most
  obvious rule anyone would write inverted:

  ```
  return doc.published        // doc.published === false
  ->  'doc.published'         // a non-empty string (#52)
  ->  !!'doc.published'       // true
  ->  ACCESS GRANTED          // no error, no warning
  ```

  Bracket access, `!doc.published`, `doc.published === true` and `if (doc.published)` were all
  unaffected, which is exactly why a test suite can miss it — every case in the reporter's own
  baseline happened to use a surviving shape.

  A non-boolean result now **denies**, and the object form requires `allow` to be a boolean
  rather than coercing it. The reason string distinguishes _"the rule said no"_ from _"the rule
  did not answer"_, because a denial that reads like an ordinary policy decision hides a broken
  rule.

  Fixed **separately from #52 and on its own terms**, even though #52's fix means that
  particular input can no longer arrive: _"the input cannot be corrupted any more"_ is not the
  same as _"the interpretation is correct"_. A security property must not depend on the
  language never having a bug — a nullish-coalescing chain, an accidental object or a
  forgotten `await` must all fail closed. This also restores a claim the surrounding design
  already made, and which this function was the one place not to honour: _fuel exhaustion, a
  thrown error, or a non-boolean return all evaluate as `false`_.

  No known exposure: reported by tosijs-platform with no ajs-backed endpoint exported yet.
  Guarded by `src/rbac/fail-closed.test.ts` and by inline `test` blocks in `rules.tjs`, so the
  guarantee travels with the code.

- **`verifyPredicate` certified impure functions as pure.** The verifier checked **calls** —
  effectful globals, unknown methods, ReDoS — and never looked at **assignments** or at
  references to bindings outside the function. All of these were reported `safe: true`:

  ```js
  function f(a) { globalThis.hit = a; return true }   // writes a global
  function f(a) { window.hit = a; return true }       // writes a global
  let count = 0
  function f(a) { count += 1; return count > 0 }      // mutates outer scope
  const seen = []
  function f(a) { seen[0] = a; return true }          // mutates outer scope
  ```

  So "verified pure" actually meant _"calls nothing effectful"_ — a far weaker claim than the
  badge makes. `redos-lint.test.ts` states this file's doctrine and it applies unchanged:
  **over-flagging only costs the badge; certifying a dangerous pattern is a broken promise.**
  This was that, in a dimension nobody had checked.

  Purity is the entire contract behind the badge: a verified predicate compiles to native JS
  and is trusted without further checking, and `docs/type-system-north-star.md` has
  predicates travelling to other runtimes as serialized ASTs, where being pure is precisely
  what makes them portable. An impure predicate does not port, and nothing said so.

  Not rated a vulnerability, because predicates come from source the developer wrote rather
  than from untrusted input — but it is a promise this project makes and did not keep.

  The check is **scope-correct**, not name-based, because the cheap version fails both ways:
  a parameter named `count` shadowing a module-level `count` is local and must keep the
  badge, while a nested arrow's own binding must not make an outer one writable.

  Mutability is keyed on whether a module-level binding is ever **written**, not on its
  declaration keyword. The first implementation used `kind !== 'const'` and **rejected 115
  tests in `src/css/`** — the library that is the main consumer of the badge — because its
  keyword tables ship as `var CSS_NAMED_COLORS = [...]` and are never touched again. The
  keyword is wrong in both directions: that `var` is constant in every sense that matters,
  while `const rows = []` followed by `rows.push(…)` is a `const` that mutates.

  Found while sizing the purity gate for the `:!` promotion (`TODO.md` A4c), which needs the
  same analysis — and which cannot use this verifier for a second reason recorded there: it
  is also too STRICT, rejecting `for` loops and `.push()` on a local array.

  Guarded by `src/lang/predicate-purity.test.ts`, which pins both directions — the seven
  impure shapes above, and eleven ordinary pure ones that must keep the badge.

### Fixed — the emitted runtime and the TypeScript bridge

- **The emitted runtime no longer declares its helpers in your namespace (#39).** The
  preamble declared `Eq`, `Is`, `IsNot`, `NotEq`, `TypeOf`, `Type`, `Generic`, `Enum`,
  `Union`, `FunctionPredicate`, `MonadicError`, `tjsEquals` and the `Legacy*` family at
  **module scope**. Those are ordinary JavaScript bindings in the author's own module, so a
  file that imported one and also used the syntax that generates it declared the name twice:

  ```
  import { Eq } from 'tjs-lang/runtime'
  export function same(a: 0, b: 0): true { return a == b }
  -> SyntaxError: Identifier 'Eq' has already been declared
  ```

  Node refuses to LOAD such a module, so nothing in it runs — no error from your code,
  because your code never started. #39 named five of these; the dogfood gate found thirteen.

  The inline runtime now lives inside one `__tjs_rt` IIFE and generated code calls
  `__tjs_rt.Eq(…)`. The bodies are untouched: they still call each other by bare name inside
  the IIFE, so this is a scoping change, not a rewrite of the semantics.

  **The rename has to happen at generation time**, which is the whole reason this was not a
  one-line fix. Once the preamble and your code are one string, a compiler-generated `Eq(`
  and a hand-written one are the same five characters — no later pass can separate them.

  **The ambient surface is preserved.** `Is(a, b)`, `DangerousLegacyEquals(a, b)`,
  `LegacyDefault({…})`, `Exactly('a')` and the type constructors are documented as simply
  available, and they were available _because_ of those module-scope declarations. Emitted
  files still bind them — but only when you have not bound the name yourself, which is a
  question about your AST and therefore decidable, unlike the one above.

  **Measured on the dogfood behaviour gate: 1076 assertions lost → 313, and 38 broken tests
  → 30.** The disparity between those two numbers is the interesting part: ten suites were
  failing at LOAD time, so the gate had been reporting six "broken tests" for what was really
  ~760 lost assertions. A load-time failure is under-reported by any metric denominated in
  tests, by roughly the size of the file.

  Guarded by `src/lang/rt-namespace.test.ts`, which pins all three properties — no collision,
  ambient names still work, and nothing inside the IIFE is unreachable.

- **A quoted `wasm function` declaration was COMPILED.** `extractWasmFunctions` detected on
  raw source, so a `wasm function` written inside a string was compiled to WebAssembly and
  the fixture replaced by the JS wrapper it was supposed to produce:

  ```
  const SRC = `wasm function total(a: Float32Array, n: i32): f64 { … }`
  ->  const SRC = `function total(a, n) { return globalThis.__tjs_wasm_total(a, n) }`
  ```

  Four suites were being corrupted this way, and only ONE of them showed up as a conversion
  failure. That is the part worth keeping: a scanner that mangles a fixture is visible only
  when the mangling breaks the parse. When it produces valid code — as it did here, four
  times — the file converts cleanly and lies at runtime instead, asserting against a fixture
  that is no longer the fixture. One had a real compiled WebAssembly module emitted into the
  test file itself.

  Its regex also opened with `^\b` against a SLICED string, where `\b` is satisfied by the
  first character being a word character — so it never looked at what preceded the match, and
  the comment claiming it stopped `mywasm` was wrong. Same pair of defects, in the same
  order, as `extractAndRunTests`.

- **A quoted `Is` / `IsNot` operator was transformed.** `transformIsOperators` was two plain
  `source.replace(…)` calls, so `a Is b` inside a string became `__tjs_rt.Is(a, b)`. It was
  the last failure standing on the dogfood behaviour gate — in
  `eval-no-transpile-execution.test.ts`, whose entire job is to hold TJS constructs as data
  and check which ones reach the AJS path. A scanner that edits its own fixtures makes that
  suite measure itself.

  Masking is safe for the operands even though the pattern accepts string literals:
  `maskLiterals` blanks a literal's interior and keeps its quotes, so `c Is 'lit'` still
  matches while `'x Is y'` no longer does.

- **A quoted `/* @tjs-unsafe */` annotation was applied as a real one.**
  `applyUnsafeAnnotations` was a `code.replace(…)` over raw source, so it rewrote the
  annotation wherever it appeared — including inside a string that merely quotes it. The
  damage is delayed and lands nowhere near the cause: `unsafe` is TJS-only syntax, so once
  it is inside a TypeScript fixture that a test feeds back to `tsc`, the parser reads it as
  a bare expression statement and ASI splits it from the expression it was meant to mark.

  ```
  `const d = /* @tjs-unsafe */ new Date(x)`   ->   `const d = unsafe new Date(x)`
                                             ->   const d = unsafe; new Date(x);
  ```

  The annotation's own test suite carries exactly that fixture, so the file documenting the
  feature was the file it broke. Matched over `maskLiteralsKeepComments` now — comments
  intact (the annotation IS a comment), literals blanked. Rows added to
  `literal-blindness.test.ts`.

### Self-hosting

- **The dogfood behaviour gate reached zero on all three of its 1.0 gates.**

  ```
  original : 4251 pass, 0 fail, 9703 assertions
  converted: 4251 pass, 0 fail, 9703 assertions
  ```

  Every test suite we ship converts to TJS, runs, and preserves every assertion.
  `KNOWN_CONVERSION_FAILURES` is empty. Both ratchet rates are now pinned at exactly 1.0 —
  there is no slack left to give back, so any regression fails immediately.

  It read **108 broken tests** two days ago. The honest accounting of that distance: roughly
  two thirds of it was defects in the GATE, not the language — a `test` block quoted as data
  being executed and deleted, `relocate()` rewriting import paths inside template literals,
  `relocate()` not rewriting `require` at all. The language defects were real (the
  `__tjs_rt` namespace collision, and three scanners reading fixtures as code) but the
  measurement was the bigger liar. Recorded at the baseline so the next person reads a bad
  number here as a question about the apparatus first.

### Changed — the dogfood gate stopped discarding its own evidence

- **The dogfood behaviour gate stopped throwing away its own work list.** Three things, all
  of which had cost real time every session the gate was worked:

  - `relocate()` did not rewrite `require('./x')`, only `import`. All 15 failures in
    `lang/features.test.ts` — the largest remaining cluster by a factor of seven — were
    `Cannot find module './parser'` from test bodies reaching for un-exported helpers.
    **313 assertions lost → 16, and 30 broken tests → 5.**
  - `testsBroken` was printed as a bare integer, which is the defect the block above it had
    already fixed for the baseline ("a number with no name attached"). It now prints the
    failures clustered **by suite**, which is the axis that has carried the signal every
    time.
  - `DOGFOOD_KEEP=1` leaves the converted tree in place, and the temp directory is created
    inside the test rather than in the `describe` body — a describe body runs at collection
    time, so merely loading the file made a directory that any filtered run then left
    behind. Twenty-two empty `.dogfood-*` directories had accumulated in the repo.

  That `relocate()` gap is the **third** time this gate's own defects dominated its output,
  and they are now the majority of every movement it has ever recorded. A bad number here is
  a question about the apparatus first.

### Fixed — the literal-blindness class

- **A comment mentioning `export` consumed the real one (#51, reported by tosijs).**
  `fromTS` decided whether an arrow-function const was already exported with
  `tjsFunc.includes('export ')` — a substring test over the whole rendered function, leading
  comments included. So a body comment quoting `` `export interface Sub extends …` `` satisfied
  the guard and the declaration's own `export` was never added:

  ```
  export const withAttributes = <A …>(…) => { … }   ->   function withAttributes(…) { … }
  ```

  No error, no warning; the failure surfaces at LINK time in a consumer's bundler, and only if
  something imports that name. Rewording the comment brought the export back, which is what
  "comment text changes the emitted code" looks like from outside. Now decided from the
  declaration.

  The report says it could not be reduced to a small file. It reduces fine — the reduction was
  measured by looking for the FUNCTION, which is still emitted. Only the `export` is lost.

- **A `test '…' { … }` quoted as DATA was executed and deleted.** `extractAndRunTests`
  detected on RAW source, so a test block written inside a template literal or a
  double-quoted string was taken for a real one: its body ran at transpile time via
  `new Function`, and the text was removed from the emitted output. Both silent.

  ```
  tjs(`test 'uses toBe' {\n  expect(1).toBe(1)\n}`)   ->   tjs(``)
  ```

  Any `.tjs` file that quotes a test block — every fixture in this repo's language tests,
  and any documentation example — lost the quoted text. Detection now runs over
  `maskLiteralsKeepComments`: literals blanked, comments INTACT, because `/*test … */` is a
  deliberate TS-compat spelling. Body extents come from `matchingBrace`, retiring the fifth
  hand-rolled literal scanner in this codebase.

  **Measured effect on the dogfood behaviour gate: 108 broken tests to 59.** Three of the
  four worst-affected suites are language tests whose fixtures are TJS source held in
  strings, so the gate had been reporting their deleted fixtures as "conversion loses
  assertions" — 52 of 99 failures were this defect and nothing to do with conversion.
  Assertion preservation 87.2% → 88.0%, tests 88.9% → 90.2%; baseline ratcheted down.

- **`test` was not actually required to be a whole word.** The check was
  `source.slice(i).match(/^\btest\s+/)`, where `\b` sits at the start of the sliced string
  and is therefore always satisfied, so `mytest 'x' { }` matched.

- **`tjs-playground --port N --force` could have signalled a developer's test run.**
  `OUR_SERVERS` matched `cli/playground` as a bare substring, so it also matched
  `src/cli/playground.test.ts` — and since the second identity rule only additionally requires
  the argv to reference this checkout, any process whose command line merely NAMED that file
  was identified as our server. Test runs genuinely hold ports (several suites call
  `Bun.serve`). The generic entry markers now have to end at a token boundary, so a path that
  merely contains one no longer qualifies. Same defect as the `OUR_ROOT` anchor one level in:
  a marker identifying a file that exists rather than a process that is running.

  Found sitting in the dogfood gate's baseline, which spawns `bun test <every suite path>` and
  so is the only invocation that builds that argv. The gate printed `1 fail` on the
  **unconverted** corpus and neither named it nor failed on it; it now does both, and the
  conversion-damage figures are no longer measured against a baseline that is allowed to be
  red.

- **A template literal's first `${ … }` desynchronised the bang-access transform for the rest
  of the file.** `transformBangAccess` hand-rolled its own literal tracking, and its `${` arm
  switched to code and never switched back. From the first substitution onward the remainder
  of that template was scanned as code, so its closing backtick was read as an _opening_ one
  and every literal after it carried inverted parity — a `!.` quoted as data was then rewritten
  in place:

  ```
  ['bang access', 'function f(o) { return o!.a }']
  ->  ['bang access', 'function f(o) { return __tjs.bang(o,'a') }']
  ```

  which injects bare quotes into a single-quoted string and fails the parse tens of lines from
  the cause. The scanner now keeps one frame per template, so `${ … }` returns to the template
  body at its matching `}` (correct through nesting), and it skips regex literals, which could
  open a phantom string the same way. Bang access inside a substitution — `` `${o!.a}` `` —
  still transforms, now including nested templates, where previously only the first
  substitution behaved.

  This is the literal-blindness class (`docs`: `src/lang/literal-blindness.test.ts`), and it is
  the last entry but one on the dogfood conversion ratchet: `lang/eval-no-transpile-execution.test.ts`
  now converts, taking `KNOWN_CONVERSION_FAILURES` from two back to one.

- **The shared literal scanner had the same defect, with twelve consumers.** `scanLiterals`
  ended a template at the next bare backtick, so `` `${ x === "`" ? a : b }` `` ended at the
  quoted one — and from there the mask inverted: code masked, later literals exposed. Template
  ends are now found past their own `${ … }` substitutions, through nesting, strings and
  comments. What consumers see masked is unchanged: a substitution's interior stays blanked,
  which is a separate and much wider decision. Regex literals inside a substitution remain
  untracked, and the limitation is now stated at the function rather than left implicit.

### Added

- **The dogfood converter gate now checks that every exported value survives conversion**, as
  the #51 report suggested. A dropped export passes conversion, compilation and graduation, so
  none of the three existing stages could see it. Validated across the compat corpus first
  (2,085 real files, zero violations once ambient `export declare const` is excluded), so the
  bar is not one this codebase happens to clear. Pinned at 100%, not ratcheted — unlike
  graduation, there is no legitimate reason for it to be below.

### Fixed — the dogfood gate itself (not shipped code)

- **The dogfood behaviour gate's own relocation step was literal-blind.** `relocate()`
  rewrote import specifiers with a regex over raw text, so it also rewrote the ones inside
  TEMPLATE LITERALS that suites write out as fixture modules at runtime — repointing them at
  a source directory where no such file exists. Those suites then failed and were scored as
  _conversion_ losing tests. It now asks acorn which string literals are import sources,
  because masking cannot help here: an import specifier IS a string literal, so blanking
  literals blanks the very text to rewrite.

  **59 broken tests → 38.** Combined with the `test`-block fix above, the gate went from 108
  to 38 in one day, and **two of those three movements were defects in the gate rather than
  in conversion**.

### Changed — cleanups

- `extractAndRunTests` no longer calls `source.slice(i)` once per character, and no longer
  carries its own literal scanner. Both are cleanups, **not** a speed-up: measured against the
  previous implementation on a 459KB file the two are indistinguishable (2926ms vs 2940ms),
  because JS engines represent `str.slice(i)` as an O(1) view rather than a copy. Recorded
  because the opposite was the obvious guess, and the compat scan's four
  `preprocess is quadratic` skips still stand — whatever makes `preprocess` blow up past ~1MB
  is somewhere else and is still unidentified.

## [0.13.11] — 2026-09-04

Three findings from the 0.13.10 pre-release review, all pre-existing, none introduced by
0.13.10. Each was reproduced before being fixed and is pinned by a test.

### Security

- **The VM resolved inherited names to host values.** Every lookup keyed by a
  guest-controlled name used `in` / bare bracket access on a plain object, and `in` walks the
  prototype chain — so `constructor`, `toString`, `valueOf`, `hasOwnProperty` and seven other
  `Object.prototype` members resolved to real host functions, past an allowlist that only ever
  enumerated own keys. With no capabilities and 400 fuel:

  ```
  return { v: constructor('abc') }   ->  { "v": "abc" }   // the host Object, CALLED
  return { v: toString() }           ->  "[object Undefined]"
  ```

  No escalation to `Function` (`assertSafeProperty` and `SAFE_METHOD_NAMES` hold) and no
  prototype pollution, so this was not an escape — but host functions crossing into guest
  scope is the wrong side of the boundary. Fixed at all three lookup sites; `builtins` and
  `unsupportedBuiltins` are `Object.create(null)` as well.

  **Guest scopes are prototype-linked on purpose** (`Object.create(parentState)`, so a child
  write shadows), so the fix walks the scope chain and stops at `Object.prototype` rather than
  demanding own properties — a plain own-property check severs lexical scoping, which is how
  the first attempt at this failed.

- **`Eval`/`SafeFunction` now cap source length** (`maxSourceBytes`, default 64 KB), refused
  before transpiling. `fuel` and `timeoutMs` are properties of `vm.run`, and transpilation
  happens first, so neither bounded it: with `{ fuel: 10, timeoutMs: 1 }`, 500 KB took 3.9s
  and 1.8 MB took ~145s, charging 0.2 fuel throughout. Measured in bytes, not string length.
  Set `0` to opt out for trusted source. The hosted endpoints name the limit explicitly.

### Fixed

- **The two `transpile()` entry points gave OPPOSITE input contracts for the documented AJS
  entry shape.** For `function agent({ apiKey: 'sk-example' })`, `tjs-lang` produced
  `required: ['apiKey']` and rejected a missing input, while `tjs-lang/lang` produced no
  `required` and `vm.run(ast, {})` returned `{ apiKey: 'sk-example' }` — the example value,
  credential-shaped for a parameter called `apiKey`, silently substituted for an input the
  caller never supplied.

  The emitter's required-parameter scan inspected only top-level `AssignmentPattern` params,
  so a destructured `ObjectPattern` — _the_ documented AJS entry shape — matched nothing; and
  because that branch was an `else if`, the name-based fallback was skipped too. It now
  descends into destructured members, and falls back whenever the scan yields nothing, since
  an empty result means "could not index this shape", not "nothing is required".

## [0.13.10] — 2026-09-03

### Changed — AJS parses through its own core, not `parse()` with a flag

**The structural fix behind 0.13.7's `test`-block RCE.** That vulnerability was one missing
`!options.vmTarget` among ~30 source transforms of which exactly two checked the flag, so ~28
TJS transforms ran when compiling AJS. A gate fails open, and this one had for months.

AJS now parses through `parseAgentSource()` in the new `src/lang/parser-agent.ts` — four steps
(hashbang, line comments, colon shorthand, param markers), because that is what AJS is: a
JavaScript subset plus typed signatures. Adding a TJS transform means editing `parser.ts`,
which AJS does not call, so there is nothing left to remember.

All seven previously-leaking TJS constructs — bang access, `Is`, inline `wasm function`,
`Type`, `Generic`, `extend`, `FunctionPredicate` — are rejected on the AJS path now, closed as
a group rather than one at a time. They were inert, but accepting syntax the language does not
have is how the last one arrived.

**`vmTarget` is removed** from `ParseOptions` and `PreprocessOptions`. It was internal (both
AJS entry points set it themselves) and is not part of the documented API, so no supported
usage changes. `parse()` is TJS's parser and says so.

One visible behaviour change: two same-name top-level functions in AJS source are now rejected
by acorn (`Identifier 'x' has already been declared`) rather than by TJS's polymorphic-merge
pass (`ambiguous signatures`). Same rejection, from the JavaScript rule that actually governs
a JavaScript subset.

Guarded two ways in `src/lang/eval-no-transpile-execution.test.ts`: an **acorn-parsed** pin on
the AJS pipeline's import set, so a new transform fails at the import rather than waiting for
someone to think of a construct that exercises it; and the existing behavioural ratchet, whose
known-leak list is empty.

**Scope of that claim, stated honestly.** `parser-params.ts` is shared with TJS's parser, so
"absent by construction" is true of the transform _list_, not of every behaviour reachable
through it: TJS safety markers (`!`/`?` on params, `:!`/`:?` on returns) are still accepted and
silently discarded on the AJS path. No execution, no capability, no fuel bypass — but an author
writing `function main(!apiKey: '')` gets the opposite of what the marker documents. Tracked in
`TODO.md`.

**Shipped as a patch, with the breakage named.** Three things could bite:

- **If you passed `vmTarget` to `parse()` or `preprocess()`**, it now **throws**, naming
  `parseAgentSource()` as the replacement. It is not ignored: the flag used to _suppress_
  transpile-time `new Function()`, so silently dropping it would have re-enabled execution for
  a caller who had explicitly asked for the protection. TypeScript only ever caught the inline
  object-literal case, so a JS consumer or an `as any` needed a runtime refusal.
- **If AJS source you ship contains one of the seven constructs**, it now fails at transpile
  instead of parsing. They failed later and less clearly before; a transpile error is louder.
- **Two further acceptance narrowings**, found by the pre-release review and not by the seven-
  construct list: a typed `let` (`let x: 0 = n`) and a leading `safety none` directive now fail
  with an acorn `Unexpected token` on the AJS path. Neither is documented AJS.

Neither the flag nor the constructs are a documented API, which is why this is a patch rather
than a minor.

### Fixed

- **`tjs-lang/vm` is 23% smaller and `tjs-lang/eval` is 40% smaller** (283→218 KB and
  164→99 KB raw; 88→66 KB and 54→32 KB gzipped). Not an optimisation — the VM had been
  bundling ~26 TJS-only source transforms it never legitimately ran, and giving AJS its own
  parser dropped them. Less code on the path that compiles untrusted input is a security
  property before it is a size one. The README table is updated and re-measured.

## [0.13.9] — 2026-09-03

### SECURITY — a TJS example value is parsed, never executed

**Second transpile-time escape, and a different path from 0.13.7's.** A return-type annotation
containing `=` was **evaluated** rather than parsed:

```js
function f(a: 0): { x = (globalThis.PWNED = 1) } { return { x: a } }
```

`tjs check` on that file ran it. So did `tjs emit`, the bun `.tjs` plugin, the module loader
and the playground — anything that transpiles source it did not write. Eight emitter sites
used `new Function(\`return ${text}\`)()` to turn an example into a value.

This is the **ordinary `tjs()` path**, so 0.13.7's `vmTarget` gate did not touch it. The AJS
path rejects the carrier, so the VM and the hosted endpoints were not affected — the blast
radius is developer and CI machines transpiling untrusted `.tjs`.

**Fix:** `src/lang/literal-value.ts` parses examples with acorn and accepts only literals —
strings, numbers, booleans, `null`, regex, arrays, plain objects, signed numbers, and
non-interpolated templates. No allowlist of dangerous names and no sanitising of source text:
anything that can compute is not an example, and that is decidable from the AST. Ordinary
return defaults are unaffected.

Found by the post-remediation review (`reviews/0.13.8-post-remediation-review.md`).

### Fixed

- **Two self-hosting canaries had been red on `main` for five CI runs.** They transpile ~275KB
  of parser modules at ~79% of bun's 5s default and carried no explicit budget. A known-red
  baseline hides every real failure behind it.

### Changed

- The structural guard in `eval-no-transpile-execution.test.ts` now scans the **emitters** and
  `predicate.ts`, not only the four parser files. It had asserted that test extraction was
  "the only dynamic-execution site in the entire parse path" — a claim broader than what it
  checked, which is the failure that test exists to prevent. Widening it surfaced the eight
  emitter sites above plus two legitimate ones, each now named with its justification.

### Known

`tjs-lang <= 0.13.8` all carry at least one transpile-time execution path. No GitHub advisory
has been filed and prior versions are not deprecated: this project has no known consumers, so
the automated-notification machinery would be ceremony rather than protection. Revisit if that
changes.

## [0.13.8] — 2026-09-03

### SECURITY — 0.13.7 shipped the fix in `src/` but not in `dist/`

**If you installed 0.13.7 from npm and use it under Node, upgrade.** 0.13.7's security fix
(a VM-target transpile no longer executes the code it is transpiling) was real in source and
absent from the published bundles: `dist/` had been built 35 minutes before the fix landed.

Bun resolves this package to `src/`, so every local check passed. Node resolves it to
`dist/`, so **every Node consumer of 0.13.7 got the vulnerable build** — including anyone
importing `tjs-lang/eval`. Verified by fresh-installing the published tarball and running
the exploit against it, which is the only check that would have caught it.

No source change from 0.13.7. This release is the same code, correctly built.

### Added

- `src/dist-freshness.test.ts` — the committed `dist/` bundles must not be older than the
  source they are built from. `editors/**` has had this guard for months; `dist/` is larger,
  is published, and had none. The mechanical release check had already reported
  `artifact freshness — no build script` as a SKIP, on a tool whose summary says "skips are
  NOT passes"; it was read as a pass anyway.

## [0.13.7] — 2026-09-02

### SECURITY — a VM-target transpile no longer executes the code it is transpiling

**If you call `Eval`, `SafeFunction`, or expose either over a network, upgrade.**

`parse()` ran every `test '…' { … }` block with `new Function(body)()`. `Eval` and
`SafeFunction` transpile the _submitted_ string before `vm.run`, so the payload executed
with full ambient authority **before** fuel, timeout, capabilities and the membrane existed:

```js
Eval({ code: "test 'x' { globalThis.__PWNED__ = true } return 1", fuel: 10, timeoutMs: 1 })
// -> { result: 1, fuelUsed: 0.2 }     and __PWNED__ === true
```

`timeoutMs: 1` was irrelevant and 0.2 fuel was charged, because the whole sandbox is
downstream of transpilation.

It was a category error before it was a vulnerability. **AJS has never had test blocks** —
they are a TJS feature, and the AJS path inherited them only by sharing `parse()`, where
every other TJS-only transform is gated on `vmTarget` and this one was not. An agent
language whose premise is that code travels as _data_ and runs with no ambient authority
was calling `new Function` on submitted source.

**What changed:** a VM-target transpile rejects `test` blocks as the syntax error they are.
**TJS inline tests are unaffected** — no `.tjs` behaviour changed, and the global
`runTests` default was deliberately _not_ touched, because that would have altered
documented TJS behaviour to fix a bug that only ever existed on the other path.

Verified two ways: every TJS construct a VM-target transpile still accepts was probed for
transpile-time execution (none executes), and a source-level test asserts that test
extraction is the **only** dynamic-execution site in the entire parse path.

Still open and tracked: a VM-target transpile _accepts_ seven TJS constructs AJS does not
have (inert, ratcheted). The structural fix — an AJS core that TJS wraps, rather than one
shared function with flags — is planned. A gate fails open; layering fails closed.

### Fixed

- **An annotated arrow after a keyword failed to parse.** `return (x: 0) => x`,
  `export default (x: 0) => x`, `throw (x: 0) => x` and curried
  `(a: 0) => { return (b: 0) => a + b }` all raised `Unexpected token`. A guard read the
  single preceding _character_, so the `n` of `return` read as a callee. Introduced in this
  cycle; not present in 0.13.6.
- **A TypeScript optional parameter no longer acquires a runtime default.** `fromTS` emitted
  `name?: T`, and TJS's `?:` is documented as "same as `name = value`" — so every optional
  gained its type example as a default. For a predicate type that meant a truthy object
  where TypeScript gives `undefined`, and radash's `unique` threw
  `TypeError: toKey is not a function`. Optionals convert to `T | undefined` again, and
  `required: false` is reported correctly.
- **Converted TypeScript overloads no longer change behaviour for unmatched input.**
  TypeScript erases overload signatures, so the implementation receives every call. We
  emitted runtime dispatch variants that _rejected_ input matching no signature —
  `inRange(null, 0, 20)` returned an error where TypeScript returns `false`. Conversion now
  emits the implementation and suggests the upgrade instead of performing it. The variants
  could never have earned their cost: with one body, every variant was a pass-through, so
  dispatch routed nothing and only gated. It also leaked `...__args` into the generated
  `.d.ts`, producing a declaration you could not legally call.
- **Same-named functions in different scopes are no longer merged.** The polymorphic merge
  grouped by name across the whole file, so two ordinary local helpers were rejected as
  "ambiguous variants" — legal JavaScript that TJS refused, a subset violation.
- **A type-only class field no longer absorbs the next member.** `readonly get: T` above
  `modify(f) {…}` emitted a bare `get`, producing a getter named `modify`. Private (`#x`)
  fields are still emitted — they are load-bearing, not type-level.
- **`$` is a legal identifier character.** `function $constructor(…)` and `Array$` were not
  recognised as declarations at all, so their parameters were never transformed. Also fixed
  in type-name position (`: Record$`).
- **A ternary's `:` is no longer read as an arrow's return type.**
  `flag ? ((r) => f(r)) : (r) => {…}` had its alternative deleted.
- **A generic type-parameter list is split depth-aware.** `<Config = { a: X, b: Y }>` was cut
  inside its own braces, emitting a brace closed by a bracket. A default containing `=>` was
  silently dropped.
- **A generated diagnostic can no longer be terminated by the code it quotes.** TypeScript
  type text often contains `*/`, which closed the comment early and turned the remainder into
  stray code.
- **`convert --emit-tjs` no longer reports success for output it cannot read.** It now
  validates its own result, exits non-zero, names the file, and does not write the artifact.
- **`given` no longer warns you to use `given`.** It lowers to a `switch` before parsing, so
  the advice fired on it — quoting the internal lowering back at the author, and failing
  `--max-warnings 0`.
- **A reasoning model's answer is read from whichever channel holds it.** Asking for
  structured output can leave `content` empty with the answer in `reasoning_content`; six
  call sites read `content` directly.

### Changed

- **`MonadicError` is shared across modules** via a shape-versioned global slot, so
  `instanceof` now works across a module boundary. `isMonadicError` remains the contract for
  anything crossing one. See `docs/runtime-fusion.md` for the rule this establishes: code
  fuses, data unions.
- **README bundle sizes remeasured** — they had drifted 17–23%.

### Added

- `tjs-lang/lang` exports `isTernaryColon` (`src/lang/expression-context.ts`), the first
  syntactic primitive above the lexical layer. See `docs/parser-primitives.md`.
- `docs/parser-primitives.md`, `docs/runtime-fusion.md`.
- `bun run test:compat-scan` — a ratchet over the whole compatibility corpus.

### Compatibility

Every file in zod, effect, kysely, radash, superstruct and ts-pattern now converts and
parses — **1973/1973**, up from 1951. All six projects' own test suites pass against our
output.

## [0.13.6] — 2026-08-26

### BREAKING — `defineAtom` now defaults to `effects: 'io'`

**If you define custom atoms, read this.** It ships as a **patch, deliberately**, and the
reasoning is worth stating because the obvious call is the wrong one.

Gating a security correctness fix behind a version bump means every adopter on `^0.13.x`
keeps the hole until they choose to move. Here the failure mode is the _silent absence of
protection_, so the person who never upgrades is the one who stays exposed — while the
person who does gets, at worst, a loud error telling them their atom was handing live host
references to guest code. There are no bad surprises in that trade, only good ones, and
they should arrive automatically. Breaking toward correctness, **loudly**, is a bugfix.

So: no bad surprises, but not silent ones either — hence this entry, the `BREAKING` marker,
and the migration notes below.

`effects` defaulted to `'pure'`, and `'pure'` skips the capability membrane. Not a
lighter check — `membraneValue` has exactly one call site, inside
`if (atom.effects === 'io')`, so an atom that didn't opt in bypassed the boundary
**entirely**: host objects reached guest scope by reference, getters intact, with
`methodCall` standing right there.

One default was serving two populations with opposite needs. Core atoms (`len`,
`jsonStringify`, `map`) work on data already inside the VM, so `'pure'` is right for them.
Atoms defined through the public `defineAtom` exist to bring **host** data in — Firestore
snapshots, Elasticsearch hits, SDK responses — which is precisely the data the membrane
exists to sanitise, and precisely the shape that carries accessors. The default served the
first and silently disabled the boundary for the second, whose authors are outside our audit
surface.

It failed quietly, which is what settles it: nothing warned, nothing broke, the atom worked
and the hardening was absent. snowfox-app upgraded _specifically_ for the 0.12.0
prototype-strip and later found all four of its custom atoms untagged ([#38]). When the
people who read the release note and acted on it still don't get the protection,
documentation is not a control.

**What changes for you.** An atom you define without `effects` now has its return
deep-copied through `structuredClone` before it reaches guest state. Three consequences:

- **Identity is not preserved.** The guest gets a copy. If you relied on handing through a
  live reference, that stops working — deliberately.
- **Non-cloneable returns are now rejected** with a `MonadicError`
  (`Capability boundary rejected the return of '<op>'`) instead of silently succeeding.
  Functions and **accessor properties** are refused; build the object literally, naming each
  field. `{ ...someResponse }` is not the fix and fails silently — a `Response` keeps
  `ok`/`status` on its _prototype_, so the spread is `{}`.
- **The atom is no longer callable from a verified predicate**, since predicates may only
  call pure atoms.

**If your atom really is pure**, say so and nothing changes:

```js
defineAtom('slugify', inSchema, outSchema, fn, { effects: 'pure' })
```

That is the honest fix, not a workaround — and it is now an explicit claim rather than
something you get by forgetting. Core atoms are classified the same way, by an explicit
sweep in both directions, so their class no longer depends on which default is in force.

### Fixed

- **`convert` stripped `new` from locally-declared classes** ([#37]). Converted modules threw
  `Class constructor X cannot be invoked without 'new'` and could not be imported at all — a
  `static zero = new Thing(0)` field fails at module _evaluation_ time, before any of the
  module's own code runs. Regressed in 0.13.0; 0.12.x and earlier are correct.

  Dropping `new` is only safe where a class is callable, and in native `.tjs` it is (the
  emitter Proxy-wraps it). But every `fromTS` output carries the `/* tjs <- … */`
  annotation, and that annotation means **JS semantics** — no Proxy wrap — so the `new` being
  removed was load-bearing. The transform now runs at _graduation_ (where the annotation is
  stripped and the class becomes callable), which is the step whose job it is.

  This was also corrupting converted **test suites**: preserved assertions 0.88 → 0.898,
  preserved tests 0.89 → 0.912. Both dogfood ratchets raised to lock it in.

### Added

- **A type can declare `asCompared()` on itself** ([#33]), beneath the `extend`/global
  registries in the projection chain. A registered projection still wins — the registries are
  keyed by `constructor.name`, which is a third party describing a type it does not own,
  whereas a method is the type answering for itself.

  This is the layer a **Proxy** can reach, and nothing above it can be. A proxy over
  `new Number(0)` has no internal slot (and slots are not forwarded to the target), so the
  slot read throws while `instanceof Number` says yes; and it reports its _target's_
  `constructor.name`, so the only registerable key is `'Number'` — which would claim that key
  for every boxed Number in the process. A `get` trap can serve a method.

  ```js
  class Money { constructor(c) { this.cents = c }  asCompared() { return this.cents } }
  new Money(500) == new Money(500)   // true
  if (new Money(0))                  // falsy
  ```

  Containment is unchanged: the probe and the call are both fail-soft, and a projection that
  is not a primitive is ignored rather than honoured.

### Fixed (same change)

- **A literal union disagreed with `==` about the same value.** `__oneOf` (the check emitted
  for `mode: 'a' | 'b'`) walked `unwrapBoxed` alone while every comparator walked the
  projection chain first — so a value could satisfy `v == 'b'` and _fail_ the check
  `'a' | 'b'`. The emitter had listed unions as a reason to emit the projection table since
  0.13.4; the union check just never read it.

[#33]: https://github.com/tonioloewald/tjs-lang/issues/33
[#38]: https://github.com/tonioloewald/tjs-lang/issues/38
[#37]: https://github.com/tonioloewald/tjs-lang/issues/37

## [0.13.5] — 2026-08-25

> **Fixes two defects in 0.13.4's `asCompared`, both found by a nine-lens review run after
> that release was published.** If you use `asCompared`, upgrade. If you do not, the second
> one still affects you — the projection table is emitted into every file that uses `==`,
> `Is` or truthiness, whether or not it declares a projection.
>
> 0.13.4 also went out without its tag being pushed, so the full-suite pre-push gate never
> ran for it. That is how it shipped.

### Fixed

- **A projection declared in one module silently changed another module's `if`.** The
  projection table was at MODULE scope rather than per-runtime, and `extend` fed it
  unconditionally — so a module with no `extend` at all had its control flow change when an
  unrelated module loaded:

  ```
  B before A loads: truthy
  B after  A loads: FALSY     ← B has no `extend`
  ```

  Four shipped documents asserted the opposite, including this changelog's own 0.13.4 entry
  ("one module's projection cannot reach another's comparators"). The global write is gone.

  The structural half, which is why the first attempt got it wrong: `==` and `Is` are emitted
  bare and read the file-local table, but truthiness went through `__tjs.toBool` — the SHARED
  implementation, which cannot see it. So a value could be falsy without being equal to
  `false`, inside one module. The emitted runtime binding now overrides its own `toBool` to
  consult the file-local table, so all three comparators read one table and it is this file's.

- **An attacker-controlled `constructor.name` reached `Object.prototype`.** The emitted table
  was a bare `{}` and the lookup was `__ac[k]`, so a key from `JSON.parse` — which creates a
  real _own_ `constructor` property — walked the prototype chain. `__ac['toString']` resolved
  to `Object.prototype.toString`, which returns a conforming primitive for any object, so two
  distinct objects with different contents compared **equal** under emitted `==`:

  ```js
  eq(JSON.parse('{"constructor":{"name":"toString"},"x":1}'),
     JSON.parse('{"constructor":{"name":"toString"},"x":2}'))   // was true
  ```

  The table is now `Object.create(null)` with an own-property check, and the resolved value
  must be a function. Verified against `toString`, `valueOf`, `hasOwnProperty`,
  `isPrototypeOf` and `toLocaleString`.

- **`wasm { } fallback { }` threw instead of falling back when the engine has no wasm
  compiler** ([#36]). The async retry lived inside the `catch` of the sync attempt with
  nothing around it — `WebAssembly.instantiate` normally rejects, so a `.catch` was assumed
  sufficient, but under memory pressure SpiderMonkey throws `no WebAssembly compiler
available` SYNCHRONOUSLY and that escaped, taking down the whole module. So `fallback`
  covered "this module failed to validate" but not "this engine has no wasm compiler right
  now" — the broader case, and the one an author cannot code around: intermittent,
  resource-dependent, ~1 run in 6 on a loaded Firefox. Reported from tosijs-ui's Playwright
  lane.

- **The dependency-audit gate did not cover `functions/`.** It ran `bun audit` with no `cwd`
  override and there is no `workspaces` field, so the deployed Cloud Functions tree was
  outside it — which is how an ecosystem sweep found 3 criticals there while the gate
  reported green. Both trees are audited now.

### Changed

- **Dev-tree advisories cleared** ([#31]): `firebase` devDep ^10 → ^12 removes all eleven
  `undici` advisories; root `firebase-admin`/`firebase-functions` aligned with the versions
  `functions/` already moved to. Root audit 23 → 7, all dev-only.

### Documentation

- **The optional-peer re-export trap** ([#28]) is documented in the README. `import type` is
  erased from emitted JS but not from emitted `.d.ts`, so a package re-exporting a tjs-lang
  type makes tjs-lang a hard typecheck dependency for _its_ consumers — and declaring the
  optional peer does not help, because optionality governs installation, not type resolution.

[#28]: https://github.com/tonioloewald/tjs-lang/issues/28
[#36]: https://github.com/tonioloewald/tjs-lang/issues/36
[#31]: https://github.com/tonioloewald/tjs-lang/issues/31

## [0.13.4] — 2026-08-25

### Added

- **`asCompared` — a type can say what it IS, for comparison.** Declared with `extend`, so
  you can attach one to a type you do not own without touching its prototype:

  ```js
  extend Timestamp { asCompared() { return this.seconds * 1000 + this.nanos / 1e6 } }

  ts1 == ts2          // true when they project equally
  Is({ when: ts1 }, { when: ts2 })   // composes into the deep walk
  if (failedResult)   // false, when the type projects to false
  ```

  **Consumed by `Eq`, `Is` AND `toBool`** — that last one is the point as much as the first.
  An errored service result is an object, objects are truthy, and until now the type had no
  way to say otherwise: `if (result)` took the success branch on a failure.

  A **projection**, not an `equals(other)` predicate, deliberately. A projection composes —
  the deep walk normalises each node, so a projected value nested three levels inside an
  object just works — and it generalises to ordering, neither of which a comparison
  predicate gives you.

  **Must project to a primitive, or to nothing**: `number`, `string`, `boolean`, `null`,
  `undefined`. An object projection defers the question rather than answering it. `bigint`
  is excluded on purpose — `1n === 1` is false, so two projections disagreeing on
  number-vs-bigint would compare unequal for values that are equal, and nobody needs
  nanosecond-exact `==`. A non-conforming or throwing projection is IGNORED rather than
  thrown: a hook that breaks `==` for every value is worse than one that does not apply.

  **It works in standalone emitted code**, not only under a shared runtime. Emitted files
  declare their own comparators and call them bare, so the projection table is emitted
  per-file — which also makes it file-local by construction. One module's projection cannot
  reach another's comparators; a single shared mutable type→behaviour table would be
  prototype pollution by another name, which is what `extend` exists to avoid.
  **⚠️ This claim was FALSE as shipped in 0.13.4** — the table was process-global and
  projections did leak across modules. Fixed in 0.13.5; the note is left here rather than
  rewritten, because a changelog that quietly corrects itself is worse than one that says
  what happened.

  This is not a new mechanism so much as an opened one: `unwrapBoxed` was already a
  comparison-projection table with three hardcoded entries (a `String` instance compares as
  a string, `Number` as a number, `Boolean` as a boolean). Those are the root of the chain;
  `asCompared` is the layer above them. Design and rationale in
  [`docs/type-system-north-star.md`](https://github.com/tonioloewald/tjs-lang/blob/main/docs/type-system-north-star.md).

### Fixed

- **`functions/`: 3 critical advisories cleared** ([#30]). `firebase-admin` 13→14,
  `firebase-functions` →7.3.2, and `tjs-lang` off `^0.2.8` — a range we deprecated
  ourselves. 3 critical / 5 high / 21 moderate → 0 / 1 / 16. The remaining `undici` high is
  pinned transitively by `@firebase/auth`.

- **An abolished mode directive emitted a bare identifier instead of erroring.** The guard
  scanned the file preamble and stopped at the first line that was not itself a directive,
  skipping only line comments and block-comment openers — so a markdown line inside a doc
  comment ended the scan at the top of the file. `tjs emit` then exited 0 and wrote a module
  that threw `ReferenceError: TjsSafeEval is not defined` on load. Our own Cloud Functions
  shipped that way; the committed bundle worked only because it predated the abolition.

[#30]: https://github.com/tonioloewald/tjs-lang/issues/30

## [0.13.3] — 2026-08-24

> Found by an ecosystem security sweep and by playing with the deployed playground —
> neither by a test here. Both defects were in published 0.13.2.

### Fixed

- **`typeof obj[key]` compiled to `(typeof obj)[key]`, so every guard of that shape was
  silently always true** ([#29]). `typeof` lowered to `TypeOf(…)` consuming only `.name` /
  `?.name` chains, so a COMPUTED access fell outside the call:

  ```js
  typeof obj[k] !== 'function'   //  ->  TypeOf(obj)[k] !== 'function'
  ```

  `TypeOf(obj)` is the string `'object'`, `'object'[k]` is `undefined`, and
  `undefined !== 'function'` is **always true**. No parse error, no type error, no warning,
  and the source reads correctly — the reporter's filter returned every key instead of
  dropping the function.

  The report named one form; **six** were broken. `x[k]`, `x[0]`, `x['lit']`, `x[k].foo`,
  `x.foo[k]` and `x[k][j]` were all silently wrong, and `typeof f()` became `TypeOf(f)()` —
  calling a string, which at least failed loudly. The operand now consumes balanced `[…]`
  and `(…)` by depth over a masked view, so nesting is safe and a bracket inside a string is
  not structure.

  This also restores something that never worked: `typeof o[k]` where the value is `null`
  now returns `'null'` rather than `'object'`. `TypeOf` exists to fix exactly that footgun
  and the value was never reaching it through a computed access.

  Reproduces back to 0.8.1 — not a regression, always wrong.

- **The Schema Validation playground example shipped a wrong worked example.** Its return
  annotation described a SHAPE (`{ name: '', … }`) where TJS reads a worked example and
  compares by deep equality — the single most common mistake with this syntax, in a teaching
  example on the public playground. The guard that should have caught it exists, but the
  example sat on a skip list whose stated reason is "examples that use imports"; it has no
  imports, nor did three other entries. A test now derives that justification instead of
  trusting it.

### Changed

- **`demo/` is no longer published.** It shipped 30 files and ~1.9 MB of playground source
  that no consumer of the package needs, including the Firebase web key in three of them
  ([#31] — a public project identifier by design, not a secret, but noise that trips secret
  scanners). Unpacked size 15.2 MB → 13.3 MB. The playground is deployed from `.demo/` and
  is unaffected.

[#29]: https://github.com/tonioloewald/tjs-lang/issues/29
[#31]: https://github.com/tonioloewald/tjs-lang/issues/31

## [0.13.2] — 2026-08-21

> A **ninth** pass then blocked this one on three more
> ([report](https://github.com/tonioloewald/tjs-lang/blob/main/docs/reviews/0.13.2-review-9.md)),
> two of them sibling-site misses in the fixes above: the symlink guard protected only the
> LEAF, so a symlinked output DIRECTORY still escaped and destroyed a file outside the named
> tree; and moving the `#!` line to the file-write seam fixed `-o` while silently regressing
> `tjs emit bin.tjs > bin.js`, which is the first example in `--help`. Both fixed here, with
> containment (a symlinked `-o` ROOT stays legal — that is a normal setup) and with stdout
> treated as the artifact sink it is. A fourth finding — that `port.test.ts` is red — was
> checked and **refuted**: `isOurServer` rejects the test runner under both relative and
> absolute argv.
>
> An eighth review pass, run after 0.13.1 shipped
> ([report](https://github.com/tonioloewald/tjs-lang/blob/main/docs/reviews/0.13.1-review-8.md)),
> found that 0.13.1's hashbang fix had been applied at the wrong seam — breaking `tjsx`, a
> published bin. **Six of eight rounds have now found a defect introduced by the previous
> round's fixes.** That is the argument for the round, not against it.

### ⚠️ Upgrade from 0.13.1

- **If you use `tjsx`, the playground, or `new Function(result.code)` on a file with a `#!`
  line, 0.13.1 threw `Invalid character: '#'`.** Fixed. `result.code` is now always a clean
  fragment and the shebang is carried separately as `result.hashbang`.
- **`tjs emit` now exits non-zero when it fails.** If a build script relied on it exiting 0,
  it was relying on a bug: 0.13.1 reported `2 emitted, 0 failed` with output missing.

### Fixed

- **The `#!` line was baked into `result.code`, breaking every embedder.** `code` is a
  FRAGMENT — `tjsx` wraps it in `new Function`, CLAUDE.md documents that idiom, and the
  playground evaluates it — and a hashbang is legal only at offset 0 of a whole script. All
  three died with `Invalid character: '#'`, an error naming neither the shebang nor the
  file. The line now travels beside the code as `result.hashbang` and is re-attached only
  where a FILE is written.

- **`tjs convert` silently stripped the `#!` line.** It never had any hashbang handling; the
  TS→TJS→JS chain loses it at the first step, so the restore added in 0.13.1 never saw it.
  `convert` is the command the migration docs point TypeScript users at — the one most
  likely to meet a real bin script.

- **`tjs emit` reported success and exited 0 with output missing.** `emitFile` swallowed its
  own error whenever `-o` was given, so the directory walk's failure branch was unreachable:
  a directory with one broken file printed `2 emitted, 0 failed`, exit 0, with that file's
  output absent. Nested tallies were also dropped at the recursion boundary. `tjs emit` is
  the documented production build path, so a CI step went green having produced a missing
  module. `convert` fixed exactly this in #24; its structural twin never got the same fix.

- **A symlinked output DIRECTORY still escaped the named tree.** The first guard unlinked a
  symlinked LEAF, but with `out/sub` a link elsewhere, `lstat` on `out/sub/b.js` resolves
  `sub` through the link and sees an ordinary file — so the write went through and destroyed
  the target while reporting `1 emitted, 0 failed`, exit 0. Writes are now CONTAINED: the
  resolved destination must sit under the resolved `-o` root. A symlinked root itself stays
  legal (`-o dist` where `dist -> /build/dist` is a normal setup).

- **`tjs emit`/`convert` to STDOUT dropped the `#!` line.** Moving the hashbang to the
  file-write seam fixed `-o` and regressed the pipe — and `tjs emit bin.tjs > bin.js` is the
  first example in `--help`, appears in two guides, and is what `functions/`'s own build
  script runs. A file and a pipe are two consumers of one decision.

- **`tjs emit -o out/a.mjs` overwrote the module with its own generated docs.** The sibling
  paths were derived with `replace(/\.js$/, …)`, which does not match `.mjs` — the normal
  ask for a `"type": "module"` package — so the docs path equalled the artifact path. Now
  derived from the stem, and a derived path equal to the output is refused as the bug it is.

- **`emit --dts` and `emit --docs` wrote THROUGH symlinks too.** The first fix for this
  routed `emit`'s JS output through a safe boundary and left its two sibling writes on raw
  `writeFileSync`, twenty lines below — so `emit --dts` still destroyed a symlink's target
  and reported success. Every file the CLI writes now goes through one `writeEmitted`, and
  `src/cli/write-boundary.test.ts` enumerates the command files and fails on any raw
  `writeFileSync` rather than relying on anyone to remember the sweep. A companion check
  enumerates the `result.code` contract (never starts with `#!`, always embeddable in
  `new Function`), because that fragment has twelve consumer sites and the previous round
  tested four.

- **`emit`/`convert` wrote THROUGH symlinks in the output directory.** With `out/a.js` a
  link to `precious/keep.txt`, `tjs emit src -o out -r` overwrote the target and reported
  `1 emitted, 0 failed`, exit 0 — data loss reported as success, in the same command whose
  READ half refuses that exact escape by construction. Writes now go through one
  `writeEmitted` boundary that unlinks a symlink rather than following it.

## [0.13.1] — 2026-08-21

> **0.13.0 was published by mistake.** It was meant to be a release candidate and the
> version was labelled `0.13.0`; the publish also ran from the working tree rather than a
> pushed tag, so the full-suite gate in `.githooks/pre-push` never fired for it. `v0.13.0`
> has since been tagged retroactively at the commit the registry actually has, and 0.13.0
> **will be deprecated on npm** in favour of this release.
>
> A **seventh** pass then reviewed this patch before it shipped and blocked it on three more,
> all introduced by the fixes below
> ([report](https://github.com/tonioloewald/tjs-lang/blob/main/docs/reviews/0.13.1-review-7.md)):
> `tjs check` had started silently SKIPPING symlinked source files (green because it did not
> look), `emit`/`convert` still followed symlinks and could write output derived from outside
> the tree they were given, and `tjs emit` silently stripped the `#!` line this release
> advertises as newly supported. All three are fixed here. Five of seven rounds have now
> found a defect introduced by the previous round's fixes — which is the argument for the
> round, not against it.
>
> A sixth review pass, run AFTER publication
> ([report](https://github.com/tonioloewald/tjs-lang/blob/main/docs/reviews/0.13.0-review-6-post-publish.md)),
> found the blocker below plus every major fixed here. Reviewing after shipping is not the
> plan; it is what caught this.

### ⚠️ Upgrade from 0.13.0

- **`{ mode: 'a' | 'b', other: 1 }` did not compile in 0.13.0.** If you pinned it and hit a
  `signature example is inconsistent` error on an options object, this is why — and the
  workaround (reordering members) is no longer needed.

### Fixed

- **A nested literal union discarded every sibling after it.** `collapseUnions` handed the
  whole bracket body back to itself, found the first depth-0 `|` inside the container, and
  returned only what was left of it. Whether a file compiled depended on the ORDER of its
  members: `function cfg(o: { mode: 'a' | 'b', other: 1 })` threw, while the same members
  reversed passed. The array form was worse because it was SILENT — `['a' | 'b', 'c']`
  collapsed to a one-element array, so the signature test ran against a shorter argument and
  passed while checking less than it claimed. Every nested case in the test file put the
  union in a container by itself, which is why the whole multi-member class — the ordinary
  shape — shipped untested and green.

- **`IsNot` without `Is` emitted a module that threw on first call.**
  `needsIs = code.includes('Is(')` is false for `IsNot(`, and `IsNot` is implemented as
  `!Is(a,b)`, so emitted code raised `ReferenceError: Is is not defined` in Node and Bun,
  for both the call form and the `a IsNot b` infix form. `NotEq` escaped only by luck:
  `'NotEq('` does contain `'Eq('`.

- **A `#!` line was rejected everywhere except `tjs check`.** Hashbang is standard
  ECMAScript (ES2023) and acorn already parses it, so rejecting it made
  `tjs(src, { dialect: 'js' })` refuse legal JavaScript — a `PRINCIPLES.md` TJS ⊇ JS subset
  violation. It was handled in the `check` command alone, so `check` green-lit bin scripts
  that `emit`, `run`, `types` and `test` all died on. Now handled in `preprocess`, which
  every path goes through.

- **The shared directory walk mishandled symlinks — in three ways, across three commands.**
  A link to a real FILE is now collected (skipping it made `tjs check <dir>` print a tick and
  exit 0 without reading a source file that was there); a link to a DIRECTORY is not
  descended (descending escaped the tree the user named — `emit -r` wrote output derived from
  a file outside it while reporting success — and a cycle recursed 33 levels before `ELOOP`);
  a DANGLING link is skipped rather than fatal (it aborted the run and emitted zero files,
  losing valid siblings). `emit` and `convert` had been left on the old `statSync` walk when
  `check` moved, so the three disagreed; they now share one `readEntries`.

- **`tjs emit` silently stripped the `#!` line.** `preprocess` blanks it for offset
  stability and nothing put it back, so an emitted bin script opened with 19 spaces and died
  with a shell syntax error — exit 0, no warning. Worse than the bug it replaced: 0.13.0
  rejected such a file loudly, which at least names the problem.

- **`tjs convert <dir>` mirrored `node_modules` and dot-directories into its output** — 913
  real `.ts` files in this repo alone. It applied neither exclusion while `emit` applied
  both. Hoisted as `shouldDescend`, now shared by all three walks.

- **`--max-warnings` silently suppressed narration** for a single file, while `--help`
  asserted "a single file always narrates". A warning-budget flag doubling as a hidden
  verbosity switch; `--verbose` is the narration control.

- **The array diagnostic's `…` marker was missing on the four-kinds path**, so
  `[1,'a',true,null,{},Symbol()]` reported an exhaustive-looking list after scanning 4 of 6
  elements — contradicting the invariant the marker was added to establish.

- **`chargeHeapWalk` could refuse a heap write without setting `ctx.error`**, breaking the
  contract its callers document: the caller stopped and the run reported success with no
  explanation.

- **`tjs check` on an all-TypeScript directory** said "No source files found", which is true
  and reads like the path is wrong. It now names the extensions it wants and points at
  `tjs convert`.

- **`tjs-playground` ignored `TJS_CACHE_DIR`** — `defaultOutDir` was an independent copy of
  the cache-path policy `resolveCacheDir` was extracted in 0.13.0 to own, and it was the
  copy that writes tens of megabytes under `$HOME`.

### Performance

- **`rejectAt` was O(violations × filesize)** — `locAt` scans from offset 0, so 3200
  violations in a 343KB file took 540ms to build a 44,498-character message it was about to
  throw. One forward pass now, with the location list capped at 20 plus a total count.

### Internal

- One balanced-bracket matcher instead of four: `matchingBrace` takes its closer from its
  opener and handles `{`/`[`/`(`. The fourth private copy had been added in the same window
  that hoisted `splitTopLevelTrimmed` to prevent exactly that — and the untested copy was
  the one the nested-literal-unions feature ran on.
- `src/cli/walk.ts` and `tjs check`'s three 0.13.0 behaviours gained the tests they shipped
  without; `emitted-module-scope.test.ts` now parses, RUNS and loads emitted output in a
  real `node` subprocess, having previously only parsed — which is how it held `IsNot` in
  its corpus and passed.
- `perf.test.ts`'s intensive-loop test no longer prints a ratio: measured in-process it
  varied 54× for identical code depending on JIT history (2.18× alone, 116.75× in a full
  run), because the baseline folds away. The file's other ratios are unchanged and carry the
  same caveat, stated in its header; `bun run bench` is the authority for any number you
  plan to quote.

### Compatibility

- **Bun 1.4**: full suite green. `Bun.build()`'s native memory leak
  ([oven-sh/bun#34053](https://github.com/oven-sh/bun/issues/34053)) is fixed upstream — 300
  builds now cost ~15MB total rather than growing without bound. The `fetch` error-shape and
  shared-reference-recursion issues we filed remain open; our workarounds stay.

## [0.13.0] — 2026-08-19

> Reviewed FIVE times before tagging, each pass over the full diff since
> `v0.13.0-beta.1`. The first ([report](https://github.com/tonioloewald/tjs-lang/blob/main/docs/reviews/0.13.0-pre-release-review.md))
> returned BLOCK on five blockers; the second
> ([report](https://github.com/tonioloewald/tjs-lang/blob/main/docs/reviews/0.13.0-review-2-af46fa2.md)) BLOCKed on two more that the first
> round of fixes had introduced; the third ([report](https://github.com/tonioloewald/tjs-lang/blob/main/docs/reviews/0.13.0-review-3-full.md))
> BLOCKed on a parenthesised-arrow emit bug; the fourth
> ([report](https://github.com/tonioloewald/tjs-lang/blob/main/docs/reviews/0.13.0-review-4-final.md)) BLOCKed on four, **two of them
> regressions introduced by the third round's own fixes** — a `maxHeapBytes` bypass and a
> build failure that would have shipped a stale bundle; the fifth
> ([report](https://github.com/tonioloewald/tjs-lang/blob/main/docs/reviews/0.13.0-review-5-delta.md))
> BLOCKed on two — a `let` arrow with `:?` that crashed at module load (a regression from
> the fourth round's own fixes) and the shipped `tjs` binary hard-failing for anyone without
> the TypeScript compiler. All are fixed, along with every major and most minors each pass
> confirmed.
>
> That pattern — a fix round introducing the next blocker — happened in FOUR of the five
> rounds, and is the honest argument for reviewing again after fixing rather than treating
> the last green run as the answer.
>
> _Links are absolute because `docs/reviews/` is deliberately excluded from the npm
> package (`"!docs/reviews"` in `files`): relative links would be dead on npm and unpkg._

### ⚠️ Upgrading — read this first

11 changes alter behaviour. Most produce no type error, so recompiling does not catch
them; **one of them — a `Type` block that declares no example, predicate or default — is a
hard compile error on source that used to transpile**. All but the VM-budget change affect code that ran under **0.12.0**.

(The count and the "the last one" pointer were both stale: bullets were appended to this
list after it was written, so the positional reference had drifted off the item it named.
Positional references into a list that grows are a standing trap; the item is named now.)

- **`Timestamp` and `isValidTimestamp` now mean epoch MILLISECONDS, not an ISO 8601
  string.** Both signatures WIDENED (`(v: string)` → `(v: unknown)`), and
  `RuntimeType<T>.check(value: unknown)` never references `T`, so `tsc --strict` reports
  zero diagnostics — `isValidTimestamp(isoString)` simply returns `false` where it
  returned `true`. Use **`isValidISOTimestamp`** / **`TimestampISO`** for the string form,
  or `Date.parse(s)` to convert. As of 0.13.0 the epoch form warns once per process when
  handed a valid ISO string, naming the replacement.
- **`tosijs-schema` ≥ 1.5.0 enforces `additionalProperties: false`**, which the battery
  atoms' output schemas did not account for. An OpenAI-shaped message carrying
  `refusal`/`annotations` failed with `Output validation failed for 'llmPredictBattery'` —
  a hard `AgentError`, not a warning. Fixed here. **This affects already-released
  versions:** 0.12.0 and 0.13.0-beta.1 both declare `tosijs-schema: ^1.4.0`, which
  resolves to 1.5.x today, so a fresh `npm i` on an older tjs-lang can break with no
  change on the consumer's side. Upgrading to 0.13.0 is the fix.
- **VM budgets:** loop bindings (`map`/`filter`/`find`/`reduce`) no longer re-account the
  loop variable, so per-iteration fuel is size-insensitive again and matches 0.12.x. If
  you tuned a `fuel` budget against a 0.13.0 beta, it buys MORE work now, not less.
- **A declared `Type` now CHECKS when used as an annotation.** `Type Even { example: 0
predicate(v) { return v % 2 === 0 } }` followed by `function double(n: Even)` — before,
  `double(3)` returned `6`; now it returns a MonadicError. The annotation always looked
  like a contract; it now is one. Nothing to migrate if your values were already valid,
  but a previously-silent violation becomes a visible error at the call site.

- **`:?` validates the return value at runtime**, not only in the signature test. Two
  consequences worth knowing, because the wrapper REBINDS the function:

  - `f.length` becomes `0` (the wrapper takes `...args`), so anything reflecting on arity
    sees a different number.
  - a declared `async function` becomes a plain `Function` object — `await` still works
    and it still returns a promise, but `fn.constructor.name` is no longer
    `AsyncFunction`.

- **A `Type` block that declares no example, predicate or default is now an ERROR.** The
  interface spelling — `Type User { name: '' \n age: 0 }` — used to parse, silently
  discard its members, and produce a type that accepted **every** value (`User.check(42)`
  → `true`), while the real runtime threw at construction. Source that transpiled now
  fails, with the fix shown as code. An EMPTY or comments-only block is still accepted:
  that is what `fromTS` emits for a TypeScript type it cannot express, and it discards
  nothing.

- **`tjs check --max-warnings` exits 2 on a fumbled argument**, where it used to exit 1.
  A missing or non-numeric value made `Number()` produce `NaN`, and `0 > NaN` is false, so
  a bare `--max-warnings` failed a CLEAN file with "0 warnings exceeds --max-warnings
  NaN". Exit 2 now means "you invoked me wrong" and 1 still means "the check failed" —
  worth knowing if a script branches on the code.

- **Excess object keys are now accepted everywhere, and dictionary defaults no longer
  strip them.** `place({ x: 5, z: 9 })` against `place(args = { x: 0, y: 0 })` returns
  `{ x: 5, y: 0, z: 9 }`; in 0.12.0 the `z` was silently deleted (WebIDL §5.4 semantics,
  with a once-per-site recorder notice). A declared type with a non-empty object example
  likewise no longer rejects an extra key.

  Three reasons, in `docs/dictionary-defaults.md` → **"Where we diverge from WebIDL"**:
  WebIDL strips because a dictionary is a _wire format_, whereas a TJS `= {…}` parameter
  is an options bag inside one program; the notice was silent where it mattered (a log,
  once per site); and TypeScript cannot express the closed type this enforced — its
  excess-property check is a freshness lint on literals, not a property of the type, and
  there is no `Exact<T>`.

  Members are still validated, missing members still fill recursively, and the
  prototype-pollution keys are still rejected — that one is a security guard, not a
  normalisation policy. If you relied on a dictionary default to sanitise a payload,
  destructure explicitly: `const clean = ({ x, y }) => ({ x, y })`. The
  `dict-default-excess-key` lint still fires, now worded as the typo check it always
  really was.

- **Destructured `:` members are genuinely required now, and destructured params generate
  signature tests.** `function f({ a: 2, b = 3 })` called as `f({ a: 2 })` used to return
  `5`; a call omitting a `:` member now returns a MonadicError. Separately, a destructured
  parameter used to produce NO signature test at all (the extractor fell through and threw,
  and the throw was swallowed), so a previously-green build can go red on a signature that
  was never actually being checked. Both are the same correction: the annotation always
  looked like a contract and now is one.
- **Emitted `Is` and `Eq` no longer run a hostile `valueOf`, and no longer throw.** A
  boxed-primitive subclass that overrides `valueOf` used to have that method CALLED by
  emitted comparisons (`Is(new Liar(1), 999)` was `true`), and a Proxy faking
  `Boolean.prototype` made `==` throw a raw `TypeError` — out of an operator whose whole
  contract is that errors are RETURNED. Both now read the internal slot and fail soft. If
  you relied on `valueOf` being consulted by `==`, use `Is` with a
  `[Symbol.for('tjs.equals')]` method, which is the supported seam.
- **`MonadicError.actual` now describes an array's ELEMENTS, not just `'array'`.** It is a
  public field on a public error type, and the string changed: `f([1, 'bad', 3])` against
  `xs: [0]` reports `array of number | string` where it used to report `array`. That names
  the intruder next to the expectation, which is the pair a reader can act on — but any test
  asserting `err.actual === 'array'`, or matching the full message, will need updating. Very
  long arrays are summarised from the first 64 elements and marked with a trailing `…`, so
  the value never claims to have looked at more than it did.

Two bodies of work. **First**, the language stabilised in its own direction: the guiding
rule became _a form that parses must mean something_, and every construct that parsed
while validating nothing was either built or removed. **Second** (below, from "Everything
below came out of the full pre-release review"), the pre-release review of
`0.13.0-beta.1`.

The through-line is worth stating because it explains why so many entries are small: the
language now claims less by accident and checks more on purpose, and everything it claims
is asserted by something that runs.

### Added

- **`predicate => expr` and `predicate { return expr }`.** The terse spellings are real.
  The type name binds to the value under test, so `Type Even { example: 2\n predicate =>
Even % 2 === 0 }` reads as "an `Even` is a value where…". Both normalise into the
  function form, so they inherit predicate verification, fuel bounding and the
  `$predicate` schema path rather than re-implementing them. `predicate =>` previously
  parsed and accepted **every value**; it was then rejected outright; now it works.

- **A parameterized type derives its predicate from its example.**
  `Type Box<T> { example: { value: T } }` now checks its parameter — writing
  `predicate(x, T) { return T(x.value) }` alongside it was restating what the example
  already said. Previously a predicate-less parameterized type emitted
  `Generic([...], () => true)`: it accepted everything while looking like it checked.

- **Type arguments in an annotation: `b: Box<int>`.** A parameterized type applied to
  arguments is a call at run time, and a primitive argument becomes a **predicate** —
  the only representation available for a type that is not a value (`int` compiles to an
  inline check, so `Box(int)` would reference nothing). Predicates compose, so
  `Box<Box<int>>` works. The applied type is hoisted to a module-level `const` named after
  the annotation, so it is built once rather than per call and errors read
  `Expected Box_int`.

- **`T[]` — the most common annotation in TypeScript.** `function f(xs: number[])` did not
  parse at all. It now rewrites to the array-example spelling (`[0.0]`), inheriting item
  checking, `.d.ts` emit and JSON-Schema. Nests (`string[][]`), and `int[]` narrows where
  `number[]` deliberately does not.

- **Literal unions narrow.** `function f(x: 'yes' | 'no')` rejects `'maybe'`. This is the
  one place the examples model bends, and the line is **vacuity**: read as examples,
  `'a' | 'b'` widens to `string | string` and means what `''` means, so it says nothing and
  must have been meant as a set — whereas `0 | ''` widens to `integer | string`, which is a
  real statement, and stays a union of types. Membership is the language's `==`, which is
  a decision with consequences: `new String('yes')` is a member, `+0 | +1` is identical to
  `0 | 1`, and `1 | 1.0` is a ONE-member union.

### Changed

- **`new` is not allowed on a class declared in the same file.** A TJS class is _called_ —
  `P(1)` and `new P(2)` produce identical objects — so `new` was decoration with the look
  of significance, and it was meanwhile a hard error for `Date`. `unsafe new P(1)` is the
  per-site escape. Scoped to locally-declared classes: for a built-in, `new` is
  **mandatory** (`new Float32Array(4)` throws without it).

- **A rest parameter cannot have a default.** `...xs: number[] = [1]` compiled and did
  nothing — `f()` returned `[]`. A rest parameter is always bound, to `[]` when nothing is
  passed, so there is no absent case for a default to fill. JS rejects `...xs = [1]`
  already; only the annotated spelling slipped through.

### Fixed

- **An emitted module using both `==` and `Is` would not load in Node.** `Eq`, `Is` and
  `__oneOf` each prepended their own copy of the shared boxed-primitive unwrap, so a file
  using two of them declared `function __ub` twice at module top level. Bun runs that; Node
  refuses (`SyntaxError: Identifier '__ub' has already been declared`). Emitted modules were
  therefore dead on arrival for Node consumers while the whole suite stayed green — the same
  shape as the `typescript` import snowfox hit in production. Emitted output is now parsed as
  a MODULE in the guard, where a duplicate top-level declaration is an error rather than a
  shrug, and the helpers are driven pairwise because the defect only existed in combination.

- **A literal-union type check THREW on a hostile value** instead of returning a
  `MonadicError`. `__oneOf` carried the fifth hand-inlined copy of the boxed-primitive unwrap
  and was the only one still missing the fail-soft guard, so
  `pick(new Proxy({}, { getPrototypeOf: () => Number.prototype }))` produced a raw
  `TypeError: thisNumberValue called on incompatible object`. A throw out of a type check
  breaks the promise that errors are returned, not thrown, and it was reachable from any
  untrusted input. It now calls the shared `__ub`.

- **The shipped `tjs` binary hard-failed without the TypeScript compiler**, including
  `--help` and `--version`, which touch no TypeScript at all. `typescript` is a
  devDependency; the CLI entry imported `./commands/convert` statically, which reaches
  `emitters/from-ts`. `src/index-tsfree.test.ts` had guarded the library entry against
  exactly this since snowfox hit it in production; the CLI had no equivalent. `convert` is
  now loaded lazily and still fails clearly, naming the missing package, when actually
  invoked.

- **One exempt `new X\n  .Y()` silenced the no-`new` rule for an entire file.** The rewriter
  and the checker each had their own answer to "does this `new X` construct X?", and the two
  disagreed: the rewriter tested for `.` with no whitespace allowed (so
  `new Shape\n  .Circle(2)` became `Shape()\n  .Circle(2)`, changing what the program means,
  and `new Reg[key]()` became `Reg()[key]()`), while the reporter re-matched with `\s*\.`
  and so matched nothing. `tjs check` accepted files containing genuine violations. Both now
  call one shared predicate, and every violation is reported rather than only the first.

- **A `let` arrow with a `:?` return annotation crashed at module load** — a temporal
  dead-zone error introduced by the previous review round's own fix.

- **`tjs check <dir>` walked into `.ts` files** it cannot parse, and a shebang line broke
  offsets. It now takes the directory its own error text recommends.

- **Exported arrow consts reach the `.d.ts` ([GitHub #4](https://github.com/tonioloewald/tjs-lang/issues/4)).** `export const id = (x: 0) => x`
  emitted `id: any`, so a consumer got no types for exactly the declaration style modern TS
  code is written in. Guarded by `src/lang/dts-compiles.test.ts` ("exported arrows reach the
  .d.ts (issue #4)"), which compiles the emitted declarations rather than string-matching
  them.

- **Nested literal unions, and `| null` dropped from `.d.ts` output.**

- **A string containing `/* unsafe */` turned validation off for the whole function.**
  Per-function safety was decided by a raw substring search over the parameter source, and
  a parameter DEFAULT is part of that source — so `function h(n: 0, s = '/* unsafe */')`
  emitted `unsafe: true` and checked nothing, while the same function without the literal
  validated `n`. A nested arrow's default disarmed the OUTER function the same way. This
  is the one member of the literal-blindness class that turns checks OFF rather than
  garbling output, so its entire effect is the absence of something.

- **Five declaration transforms rewrote TJS syntax inside string literals** — `Type`,
  `FunctionPredicate`, `Generic`, `Union`, `Enum`, plus `const!`. A declaration written
  inside a template came out as the string's CONTENTS transformed; the single-quoted form
  injected unescaped quotes and failed to parse, **rejecting legal JavaScript** even under
  `dialect: 'js'`. For a language whose own docs and tests are full of illustrative
  declarations, "a declaration inside a string" is the normal case.

- **A declaration inside a doc template became a phantom exported type in the `.d.ts`.**
  Two `dts.ts` scanners detected on raw source while brace-matching a masked view, so a
  documentation template could emit `export type Ghost = (x: number) => any` — a type with
  no runtime, degraded as well, which a consumer's editor autocompletes and their build
  accepts.

- **The `expect` harness differed depending on which runner ran your test.** Two copies had
  drifted in opposite directions: one had `toThrow` and no `toBeNaN`, the other the
  reverse. `tjs test file.tjs` failed with `expect(...).toThrow is not a function` on a
  test that passed in the playground. Both documented as working. There is now one harness.

- **The converter emitted `.tjs` that does not parse for builtin return types.** Any class
  method returning `Response`, `URL`, `AbortSignal` (and others) produced
  `make():! new Response() {` — `new` is abolished in TJS — or `make():! AbortSignal.abort() {`.
  Three sites emitted return annotations and only one filtered anything.

- **The emitted call-stack array grew without bound.** The inline runtime's `pushStack`
  appended one entry per call forever — 201,000 calls left 201,000 entries — because the
  matching `popStack()` is emitted after the `return`. It is now the same bounded 64-entry
  ring the real runtime uses. Standalone emitted files are the shipping configuration, so
  this was a live leak in any long-running program built from them.

- **`tjs-playground` built into its own installed package directory.** Under pnpm and bun
  on Linux that is a hardlink into the machine-wide store. It now builds into an OS cache
  directory keyed by version, announces the path on stderr, and accepts `--out-dir`. It
  also no longer regenerates `demo/docs.json` when installed, which produced a DEGRADED
  file from a tarball that does not carry every markdown source.

- **`--force` could stop a stranger's dev server.** Port reclaim decided ownership from the
  executable name (`bun`/`node`/`deno`), which is an ecosystem, not an identity — so
  `tjs-playground --port 3000 --force` would `SIGTERM`→`SIGKILL` a consumer's Vite or Next
  dev server and report it as reclaiming its own. Identity is now the full command line,
  and reclaim refuses to signal its own process.

- **The model-audit cache littered the consumer's project and never expired on logic
  changes.** It was written to `process.cwd()/.models.cache.json` — a dotfile dropped into
  whatever repo you ran from — and keyed only on base URL plus a 24h TTL, so when
  `looksLikeVisionModel` was corrected this release an upgrader kept `vision: false` for a
  multimodal model for up to a day. It now lives in the OS cache directory
  (`~/Library/Caches/tjs-lang`, `$XDG_CACHE_HOME/tjs-lang`, `%LOCALAPPDATA%`), announces its
  path, and carries a probe version. **Override it with the new `TJS_CACHE_DIR`** if you
  want it somewhere specific; with no home directory at all (scratch containers, some CI
  images) it falls back to the OS temp dir rather than to `.`, which would have been the
  original bug wearing a different hat.

- **`extractTDoc` re-scanned the whole file prefix for every function.** Doc comments are
  now located once per source and binary-searched: 128.9ms → 4.5ms over 58 functions on a
  176KB file, and ~12% off a full TS→TJS→JS transpile of it.

- **Arrow and function-expression parameters are validated.** The same annotation was
  enforced or ignored depending purely on spelling: `function decl(n: 0)` rejected `'x'`
  while `const arrow = (n: 0) => n` accepted it. Only top-level `function` declarations
  ever got boundary checks. Arrows are most of real TypeScript, which made this the
  largest silent hole in the language. `const f = function (n: 0) {}` did not even parse.

- **Rest parameters enforce their element type.** `...xs: number[]` accepted `f(1, 'x')`.
  (Not a rest-param bug: `...xs: [0]` was correct all along — the failure was `T[]`.)

- **`Enum` and `Union` annotations are enforced.** `function f(c: Color)` emitted **no
  check** and warned that `Color` "could not be resolved to a runtime type" — the type
  declared three lines above it. `Color.check()` worked the whole time; nothing asked it.

- **`Type N { example: +0 }` rejects negatives.** `+0` means non-negative integer, and
  `+0 === 0`, so passing the example through as a value destroyed the narrowing before any
  runtime saw it — the idiomatic way to declare a count accepted `-1` everywhere, while
  `n: +0` (which reads the source token) was correct. The emitter now writes the check
  into the emitted code.

- **The inline runtime and the real runtime agree.** Emitted standalone `.js` accepted
  values the real runtime rejects: an integer example accepting a float, directly and
  through objects and arrays. Since emitted code calls `Type` **bare**, the inline stub
  always wins — so those were the shipped semantics, not a fallback. All disagreements
  are closed and the corpus is empty. (The fourth case, excess keys, was first closed by
  making the stub REJECT them and then resolved the other way — both checkers now ACCEPT
  them; see "Upgrading" above. Agreement is the invariant; the excess-key POLICY is the
  open one.)

- **`fromTS` emits TypeScript parameter properties.**
  `class P { constructor(public x: number) {} }` converted to an empty constructor body,
  so every such field was `undefined` at run time. Silent, and large for TS ports: a
  parameter property is the idiomatic dependency-injected field.

- **The `new Date()` remedy is runnable.** It advised `Timestamp.now()` without mentioning
  that `Timestamp` must be imported, so following it produced a second error —
  `Timestamp is not defined` — from the message meant to resolve the first. It now shows
  the import and both call forms.

- **The editors described a language that is not TJS.** The syntax lists were AJS plus a
  handful of JS keywords: no `Type`, `Generic`, `Enum`, `Union`, `FunctionPredicate`,
  `predicate`, `extend`, `wasm`, or any type name — `int`, `unsigned`, `float`. Meanwhile
  `->` shipped as a valid operator, and the return-type pattern matched `) -> Type`, so
  **real** return types went unhighlighted while an abandoned form was highlighted.
  Completions had the same shape: nothing for the declaration forms, the deprecated
  `isError` as the only error check, and the non-canonical `test('x')` snippet.

### Performance

- **`extractParamMarkers` was quadratic twice over.** A per-character `regions.some(…)`
  literal guard made it O(n × literals), and removing that exposed a second one: `out += …`
  built a rope while `out.endsWith(' ')` flattened it once per marker. A 622KB file took
  152ms and its per-byte cost ROSE with file size — the failure mode nobody notices until
  their file is big. Now 0.65ms and flat. Pinned by a differential test against the old
  implementation plus a growth-ratio assertion.

- **Array diagnostics no longer walk the whole array.** `describeActual` and the emitted
  `__arrKinds` stopped after four distinct element types, which bounds the message but not
  the work: a homogeneous `number[]` never reaches four, so a ten-million-element array was
  scanned end to end to conclude "array of number" — on the ERROR path, where a failure
  inside a loop pays it every iteration. Capped at 64 elements, with `…` marking a sampled
  answer so the message never overclaims.

### Documentation

- **[TJS vs TypeScript vs JavaScript](docs/tjs-vs-typescript.md)** — a generated comparison
  where every row is **executed** against `tsc --strict` and TJS on each test run. 18 rows.
  It doubles as the language-design surface: a row can be marked `proposed`, which asserts
  it red until someone builds it. All four proposals raised this cycle are now shipped.

- **[Type identity](docs/type-identity.md)** — which mechanism answers "does value `v`
  satisfy type `T`", which is authoritative, and where they disagreed. The load-bearing
  fact: the inline stub is not a fallback, it always wins in emitted code.

- **Equality invariants are pinned** (`src/lang/equality-invariants.test.ts`): `a === b`
  implies `a == b`, verified exhaustively rather than argued; `+1`, `1` and `1.0` are one
  value; and `==` is **not** `TypeOf(a) === TypeOf(b) && a == b` — under that rule
  `null == undefined` would be false, and it is deliberately true.

Everything below came out of the full pre-release review of `0.13.0-beta.1`. Seven
findings were blockers; the review also caught that the beta's own changelog entry
had omitted four VM security fixes entirely (now written up in their own section,
below, since they shipped there).

### Changed

- **A required destructured member is now genuinely required.** `function f({a: 2, b: 3})`
  called as `f({a: 2})` returned **5** — `b` had silently defaulted to its own example, so
  `:` (required) was unenforceable in the one parameter shape people destructure most. The
  colon value is a **type and a worked example, not a default**; conflating the two made
  "required" mean nothing. It now returns a `MonadicError`. `=` members are unaffected:
  `f({a: 2, b = 3})` called as `f({a: 17})` still gives 20.

- **Destructured parameters generate signature tests.** `function f({a: 2, b: 3}): 5`
  silently produced **no** signature test, while the near-identical
  `function f(o = {a: 2, b: 3}): 5` produced one — so the language's headline promise, that
  the annotation _is_ the test, quietly did not hold for destructuring. The generated call
  supplies required (`:`) members and **omits defaulted (`=`) ones**, so the defaults are
  exercised rather than bypassed — which is what would have caught the corrupted `'hello,'`
  default below without anyone writing a test.

### Fixed

- **A comma inside a string literal split the parameter list mid-literal.**
  `{what = 'hello,', who: 'alice'}` was split inside the string and the pieces rejoined with
  `', '`, putting the comma back **with a space after it, inside the literal** — so
  `greeting({who: 'fred'})` returned `"hello,  fred!"`. Silent: the output parsed, ran, and
  returned a plausible wrong answer, and the corrupted value reached the emitted `__tjs`
  metadata, the `.d.ts` and the JSON Schema. Templates and regexes were hit too —
  `a = /,/` became `a = /, /`, a **different regex** — and a lone brace in a string
  (`a = '{'`) broke bracket depth and failed the transpile outright.

  Same literal-blindness family as the fifteen call sites consolidated earlier in this cycle;
  it survived that sweep because it splits on **commas** rather than scanning quotes, so it
  did not look like a literal scanner. Both parameter splitters now scan a `maskLiterals()`
  view and slice from the original.

- **`tjs test <file>` ignored its argument and always exited 0.** It was built around
  `.test.tjs` wrapper files, so any other path fell into a branch that discarded it and
  reinterpreted it as a filter pattern — printing "No .test.tjs files found" and exiting
  **0**. A real file with a failing inline test and a path that did not exist produced
  identical output and the same success code, which made it useless as a CI gate and
  actively misleading as the first command a reader types. It now runs the file's inline
  tests and signature tests, reports each with a line number, and exits non-zero on failure,
  on a missing path, or on a directory scan that matches nothing.

### Security

- **The OOM guard allocated like the thing it was guarding against.** Rejecting an array
  that exceeded a 1,024-byte `membraneMaxBytes` by four orders of magnitude cost 549ms and
  103MB at 2,000,000 elements, linear in N — the walk called `Object.keys` first and
  checked the budget after. It now scans incrementally and abandons on the first overflow:
  **0ms and 0MB** at every size tested. A caller who set a small budget to bound their
  exposure was wrong in the one case they set it for.

- **An array's `length` was never budgeted.** A capability could return an array with
  `length = 1e9` holding three values; it passed the membrane on ~40 bytes and
  `structuredClone` then spent **6.5 seconds** materialising a billion-slot array —
  synchronous host work invisible to fuel, atom timeouts and `membraneMaxBytes` alike,
  behind the guard whose stated job is to reject before the clone allocates.

- **Two `vm.run` exit paths leaked the timeout timer and the caller's abort listener.** The
  root-op throw and the input-schema rejection both happened after the timer was created
  and before the `try`, so neither cleared it — while the comment beside the `finally`
  claimed it "guarantees on every exit path". A pending timer also keeps the event loop
  alive, so a host that validates a batch of agents and then exits did not exit.

- **The capability membrane billed array elements for the string form of their own
  index, cutting effective array capacity ~3.4×.** `readOwnData` charged
  `k.length * 2 + 8` for every own key, and an array's key list is its indices — but
  `structuredClone` copies an element as a slot and never materialises `"199999"`, so the
  charge was for bytes that do not exist. It compounded with length: **500,000 floats are
  3.81MB of data and were charged 13.14MB**, so an ordinary RAG return (300 documents ×
  768 floats, ~1.84MB) came back as `Capability boundary rejected the return of
'storeVectorSearch'` under the documented 4MB default — and the only remedy on offer was
  to raise `membraneMaxBytes`, i.e. to weaken the OOM guard to buy back capacity that was
  never being used. A canonical array index now costs **nothing**: the element's value is
  already priced when the walk reaches it. Non-index own properties on an array
  (`arr.meta`) keep their name charge, since those really are serialised by name. The guard
  is unchanged in strength — 1M floats are 8MB and still refused.

- **The capability membrane ran host code and defeated the byte budget on two of its
  three walk branches.** The object branch was hardened, then array _indices_ the next
  morning; two paths were never revisited. Affects **0.12.0 and earlier**, and both
  branches of `0.13.0-beta.1`.

  - An array's **non-index own enumerable properties were never visited at all**, while
    `structuredClone` serialises them. A capability returning `[1,2,3]` with an
    enumerable getter `meta` ran that getter and delivered `'HOST-CODE-RAN'` into guest
    state; a throwing variant leaked host exception text into `result.error.message`,
    which `malicious-actor.test.ts` explicitly forbids for the object branch. It was a
    budget bypass too — `arr.big = 'x'.repeat(5MB)` crossed a 4 MB cap cleanly.
  - **Map/Set were read with `for…of`**, which dispatches to a guest-overridable
    `Symbol.iterator`, while `structuredClone` reads the internal slots. A
    `class extends Map` with a lying iterator presented itself as **empty** to the walk
    while 20,000 entries crossed a 1024-byte `membraneMaxBytes` intact (verified in both
    JSC and V8). Three guarantees failed at once: the documented OOM guard was simply not
    enforced for Map/Set, `MEMBRANE_MAX_DEPTH` was evadable by nesting, and host code ran.

  One `readOwnData()` helper now serves the array and object branches, so they cannot
  diverge again; collections are read through `Map.prototype.entries.call(v)` /
  `Set.prototype.values.call(v)`, and a Map/Set whose prototype is not exactly the
  intrinsic is refused.

- **`xmlParse` was tagged `effects: 'pure'` while calling `ctx.capabilities.xml.parse`.**
  That tag is what routes a return through the membrane _and_ what the predicate verifier
  reads to certify a cluster safe to compile to native JS — so a `DOMParser` result reached
  guest state as a **live host `Document`**, prototype chain and all, with `methodCall`
  standing right there. Every atom in `atoms/browser.ts` was untagged as well.
  `atom-effects.test.ts` could not have caught it: it iterates the same constant that
  _assigns_ the tag, so it proves the list agrees with itself. New
  `atom-effects-scan.test.ts` reads each atom's **body** and asks whether it touches a
  capability, randomness, the clock, the network or the console.

- **The live-heap ceiling was bypassed completely by two of the four binding atoms.**
  `varSet`/`constSet` had both the prototype-pollution guard and heap accounting;
  `varsLet` and `varsImport` — the two atoms whose entire job is binding variables — had
  only the first, as did the loop binds and the catch binding. Verified: the identical
  doubling program routed through `varsLet` held a **1 GB string under the 64 MB default
  cap**. Every guest-scope write now goes through one `setStateVar()` helper, and
  `state-writes.test.ts` fails on any bare `ctx.state[…] =` outside it.

- **The heap-ceiling walker reintroduced the size-proportional fuel bypass** — the `==`
  bug class, in the function next door to the commit that closed it. `estimateBytes` was
  called with the absolute ceiling rather than remaining headroom, per-key accounting
  _replaced_ rather than accumulated (so re-binding an unchanged object re-walked it in
  full, forever), and the walk charged no fuel while being synchronous, so no timeout
  could preempt it. Measured: **500 rebinds of a 300k-element array cost 28,838 ms of
  pegged CPU for 50.2 fuel** — 574 ms per fuel unit, against 1 ms for a benign program
  charged identically. Now **53 ms**. Also fixes an accounting split where
  `createChildScope` copied the running total by value while sharing the per-key ledger by
  reference, letting the total drift negative and silently buy back budget.

- **`hash` and `omit` were flat-charged regardless of operand size.** `hash` digests every
  byte and returns 64 chars, so result accounting sees nothing — 1 KB and 1 MB both cost
  1.20 fuel. `omit` must walk the whole source object to know what to keep, so 100,000 keys
  in and one key out cost the same as 1,000. `cost-invariant.test.ts` now **enumerates the
  atom registry**: every atom needs either a case demonstrating marginal fuel growth or a
  `SIZE_INSENSITIVE` entry saying why not.

- **`vm.run` leaked an abort listener onto the caller's signal** — no `once`, no removal.
  Measured **41.6 MB retained after 20,000 runs** against one shared signal, versus 1.59 MB
  with no signal at all; a host running many short agents under a single cancellation scope
  is the normal case. Now 1.46 MB.

### Fixed

- **`compareVersions` ordered prereleases lexically**, so `'beta.2' > 'beta.10'` — the
  tenth beta looked _older_ than the second. Combined with `installRuntime`'s wholesale
  replacement, an older beta "upgrades" over a newer one and **discards the flight
  recorder and any applied `configure()`**. Now follows semver §11, pinned against the
  specification's own worked ordering example. (The equality case was fixed in the beta;
  this is the ordering case left behind.) Affects **every consumer of `0.13.0-beta.1`**.

- **`unsafe` stole the identifier `unsafe` from JavaScript** — a `PRINCIPLES.md` TJS ⊇ JS
  violation shipped by the escape hatch that exists to uphold compatibility, and present
  under `dialect: 'js'` too. `{unsafe: Date}` + `o.unsafe instanceof Function`,
  `let unsafe = Date; if (unsafe instanceof Function)`, and `unsafe in {a:1}` were all
  `SyntaxError`. A related span defect let one marker un-ban an entire nested closure:
  `unsafe makeHandler({ onClick: () => { eval(src); var leaked = 1 } })` transpiled with
  zero warnings, quietly reinventing the whole-file mode that `unsafe` replaced.

- **The literal-blindness class, consolidated onto one scanner — not ended.** A
  source-processing pass hand-rolls its own literal tracking and silently mis-reads code
  that _mentions_ the syntax it scans for. Since tjs-lang is code about code, its own
  source, tests and documentation hit this constantly. Every scanning call site now
  consumes one `scanLiterals()` in `src/strip-comments.ts`.

  This heading previously read "ended", and that was wrong — **at least fourteen instances
  landed in this cycle**, the last six found by review after the consolidation: a raw
  `.replace` rewriting the contents of user strings; all five declaration scanners
  detecting on unmasked source (the single-quoted form injected unescaped quotes and
  _rejected legal JavaScript_); `/* unsafe */` read out of a parameter default, turning
  validation off for the whole function; and two `.d.ts` scanners emitting a phantom
  exported type. A further nine surfaced at once when the dogfood conversion ratchet was
  finally given a CI lane — six of them labelled "undiagnosed" for weeks, all nine the same
  defect. See [#25](https://github.com/tonioloewald/tjs-lang/issues/25), whose
  pre-registered counter this answers, and `ASSUMPTIONS.md` E1.

  - `isInsideComment` had no notion of strings, so `const OPEN = '/*'` — or the ordinary
    glob `'**/*.ts'` — convinced it the rest of the file was one giant comment and **every
    `test { }` block after it vanished**: no error, no warning, no recorder entry. For a
    language whose thesis is that tests live in the source, silently reporting zero tests
    is the worst available failure mode.
  - The naive escape lookback `source[i-1] !== '\\'` in **fifteen** scanners across five
    files. It is wrong for exactly the input that matters: in `'\\'` the character before
    the closing quote _is_ a backslash, but an escaped one. `sep == '\\'` failed with
    "Unexpected token" forty characters away.
  - `computeBraceDepths` tracked strings but not regexes, so one `const R = /}/` — or the
    very common `/\$\{([^}]+)\}/g` — drove the depth negative and `generateDocs` returned
    an **empty document**. `tjs emit` writes a sidecar `.md` per file, so users' docs came
    out blank.
  - `extractEmbeddedTestComments` produced both a false negative and a false positive from
    one blind spot: `const q = /['"]/` **dropped** a real embedded test, and `const q = /'/`
    above a JSDoc **promoted a documentation example into a real emitted test**.
  - `maskWasmBodies` matched `wasm {` inside a string and brace-counted past it, swallowing
    the real code that followed. No error — the output still parsed.
  - Fixing `findFunctionBodyEnd` exposed three latent bugs it had been accidentally
    compensating for: the class, polymorphic-constructor and `extend` scanners were all
    matching declarations written inside `/*# … */` doc comments — the language's own
    documentation of those features.

  New `src/lang/literal-blindness.test.ts` pins the class in `test:fast` (41 cases): each
  trigger placed in a string, template, regex, comment and `wasm{}` body in turn.

- **The `Eval`/`SafeFunction` auto-import emitted JavaScript that does not parse.** Usage
  was detected with `\bEval\s*\(` over masked source, which never checked whether the name
  was already bound and matches after a `.`. `import { Eval } from 'tjs-lang/eval'` — the
  **documented** form in README, CLAUDE.md and TJS-FOR-TS.md — produced two imports and a
  `SyntaxError`, so the documented TS → TJS → JS chain had no correct authoring path. It
  also fired under `dialect: 'js'`, making legal JavaScript containing `function Eval(){}`
  un-transpilable. Now decided from the AST: inject only for a call whose callee is a bare
  identifier with no binding anywhere in the module.

- **`bigint` was inverted in both directions.** `TS_TYPE_NAMES` mapped it to
  `{ kind: 'number' }`, so `f(10n)` returned "Expected number … got bigint" and `f(10)` —
  a plain number — **passed**. In 0.12.0 the annotation degraded to `any` and simply
  worked, so this was working → 100% broken, on a type the beta's changelog advertised as
  checking at runtime. Two adjacent defects made it unusable end to end even once the
  check was right: `fromTS` emitted `x: 0n` that the return-position scanner rejected, and
  a single `0n` anywhere in a file took down the whole transpile with "JSON.stringify
  cannot serialize BigInt", naming no file and no line.

- **`n?: number` emitted JavaScript that throws on the happy path.** The colon shorthand
  rewrites an optional parameter to `n = <annotation>`, right for an example (`n?: 0` →
  `n = 0`) and a **dangling identifier** for a type name — so `g()` threw
  `number is not defined`. Long-standing, but this release made it far more likely: bare
  TS names now produce real checks, so the annotation _looks_ like it works, and
  `int`/`unsigned`/`float` are newly encouraged.

- **Predicate canonicalization alpha-renamed object-literal keys.** Field names are
  literals, not variables, and the two node types spell them differently, so the guard's
  `Property` arm was dead: `{ n: 10 }` canonicalized to `{ $0: 10 }` — the canonical AST
  read a field the source never named. That AST is forwarded verbatim by `storeQueryWhere`
  to `store.queryPredicate`, making it the silent filter failure its own comment calls "an
  authorization bug".

- **`tjs check` hid the release's flagship diagnostic.** The command CI and coding agents
  run printed `✓ file` for a file whose type had silently degraded, while `tjs run` on
  identical source printed the full remedy; `tjs emit` was silent too. That directly
  undercuts this release's own measured finding — a shown remedy is repaired ~80% of the
  time, a bare diagnostic 0%. Warnings now go to **stderr** from check/emit/convert (so
  `tjs emit f.tjs > out.js` still produces clean output), plus `--max-warnings N`.

- **`tjs convert` reported success while dropping files** (#24). `convertFile` caught its
  own error and returned normally whenever `outputPath` was set, so the caller's `catch`
  was unreachable: one good file and one bad file reported "2 converted, 0 failed" and
  exited 0, with the bad file silently missing. The failure surfaced two steps later as a
  bundler resolution error.

- **Unconditional rejections reported no location and stopped at the first occurrence.**
  Fixing a file with three violations took three `tjs check` runs, each printing an
  identical positionless message — in validator order, not source order. `var`, `new Date`
  and `eval` now throw a located error at the first occurrence and list the rest beneath it.

- **`LegacyDefault(...)` erased the parameter's type to `any`** — weaker than the plain-JS
  equivalent it exists to reproduce. The caller asked for atomic default semantics; they
  did not ask for the type to disappear.

- **The shipped editor artifacts taught a language that isn't TJS.**

  - The generated TextMate grammars **could never match anything**: `\\\\b` in a template
    literal produces the string `\\b`, a literal backslash — so every keyword, forbidden
    and builtin rule in both grammars was incapable of matching any input, for as long as
    they have existed. The extension's advertised red-squiggle highlighting was dead.
  - **`.tjs` had no VS Code support at all** — the grammar was regenerated on every build
    and referenced by nothing, in the release whose central idea is that the file extension
    _is_ the language gate.
  - The TJS keyword model encoded **AJS's** restrictions. Measured against the real
    compiler, **41 of 42 painted-red tokens are legal TJS**: `switch`/`case`/`default` are
    ordinary control flow, `type`/`module`/`is`/`as`/`keyof`/`never` ordinary identifiers.
  - The CodeMirror completion inserted `unsafe { … }`, a form the language rejects, and
    offered no completion for `unsafe <expr>` or any `Legacy*` bridge — so this release's
    entire escape vocabulary was undiscoverable in the editor.

- **Two AJS playground examples shipped truncated**, and the flagship `wasm-functions`
  example never compiled. `extractCodeBlock` was not fence-length aware, so examples
  opening with four backticks (precisely because their code contains three) were cut
  mid-expression; `extractWasmBlocks` compiled a `wasm { }` written in a **doc comment**,
  so the example whose prose necessarily says "inline `wasm { … }` blocks" printed "did not
  compile — running the fallback{} (JS)" on every run.

### Changed

- **`Timestamp` the runtime type is now a number, matching `Timestamp` the module.**
  `0.13.0-beta.1` flipped the representation to epoch milliseconds and announced it, but
  the runtime type in `Type.ts` — the one re-exported into `__tjs`, and therefore the one a
  `.tjs` file annotating `t: Timestamp` is checked against — still validated an ISO
  **string**. So `Timestamp.check(Timestamp.now())` was `false`: the type rejected the only
  value its own constructor produces, while the compiler's `new Date()` diagnostic pointed
  users at that constructor **by name**.

  | Before (`0.13.0-beta.1`)            | Now                                    |
  | ----------------------------------- | -------------------------------------- |
  | `Timestamp` — ISO 8601 string       | `Timestamp` — epoch milliseconds       |
  | (no equivalent)                     | `TimestampISO` — the ISO 8601 string   |
  | `isValidTimestamp(v: string)` — ISO | `isValidTimestamp(v)` — epoch ms       |
  | (no equivalent)                     | `isValidISOTimestamp(v: string)` — ISO |

  If you were annotating an ISO string as `Timestamp`, use `TimestampISO`. Both spellings
  now share one predicate with `src/types/Timestamp.ts`, so they cannot drift apart again.

- **The npm package now ships its own documentation.** `llms.txt` is the agent-facing
  navigation index and it ships — but **29 of its 43 links were 404 in the tarball**,
  including `CLAUDE-TJS-SYNTAX.md`, the file it names as the thing to read first. The
  guarding test resolved links against the repo root, certifying an artifact nobody
  installs. The user-facing docs (`DOCS-*`, `TJS-FOR-*`, `PRINCIPLES`, `ASSUMPTIONS`,
  `CHANGELOG`, `guides/`, `examples/`, `tjs-src/`) are now in `files`; repo-process docs
  (TODO/PLAN/AGENTS/UPSTREAM) are linked absolutely on GitHub instead; and
  `docs-index.test.ts` now resolves against `npm pack`'s own file list.

## [0.13.0-beta.1] — 2026-08-03

**Beta.** The language changed shape: all nine mode directives are gone and the file
extension is the only gate. Escapes are per-construct, so an accidental use is still
caught. Published as a beta because that shape change deserves real use before it is
called stable.

### Removed — BREAKING

- **All nine mode directives are abolished.** `TjsEquals`, `TjsClass`, `TjsDate`,
  `TjsNoeval`, `TjsNoVar`, `TjsStandard`, `TjsDictDefaults`, `TjsSafeEval` and
  `TjsSafeAssign` no longer exist. **The file extension is the gate** — a `.tjs` file gets
  every rule, unconditionally — the way ESM made `"use strict"` implicit. Writing an
  abolished directive is now an error that names the replacement rather than a bare
  identifier that fails at runtime.

  `TjsCompat` and `TjsStrict` survive, because they answer a different question — _which
  language is this?_ That is **dialect**, not a rule. Plain JS, TS-originated code and
  AJS/VM code still get JS semantics by default, so TJS remains a superset of JavaScript.

  **Migration is per-construct, not per-file.** The old ladder ("turn the rules off, then
  re-enable one at a time") is replaced by marking the individual sites that need the old
  behaviour — which is strictly better, because a modes-off file also silenced the _next_,
  accidental use.

### Security

> The four items immediately below shipped in `0.13.0-beta.1` and were **omitted from this
> entry at the time** — found by the pre-release review of the beta. They are recorded here,
> under the version that actually contains them, rather than backdated into `[Unreleased]`.

- **The capability membrane executed host code while inspecting it.** The pre-walk read
  every own key with `v[k]`, which **invokes a getter** — so the machinery whose entire job
  is keeping host code out of guest state was itself running host code, before
  `structuredClone` was reached and regardless of whether the value was ultimately accepted.
  The rejection path ran them too, so even a refused value had already executed. A getter
  can throw, mutate, or stall, making this a side-effect vector on the boundary rather than
  only a data leak. The walk now reads `Object.getOwnPropertyDescriptor` and **rejects
  accessor properties outright** — not evaluated-then-checked, because there is no way to
  learn what a getter returns without running it. **Breaking for capability authors:** see
  the migration note under _Changed_. Affects **0.12.0 and earlier**.

- **The run's `AbortController` was aborted only when the timeout fired.** Any other
  ending — fuel exhaustion, an atom error, or plain success — cleared the timer and left
  in-flight requests alive with nothing left to cancel them. A time box you can only rely on
  when it expires is not a time box. It now aborts in `finally`, on every exit path.
  Teardown **signals**; it does not await cleanup, because waiting is exactly how
  cancellation becomes a path that starts unmetered work. Affects **0.12.0 and earlier**.

- **Per-atom call quotas** (`quotas: { llmPredict: 3, httpFetch: 10 }`). Fuel meters work
  done _inside_ the VM and is blind to what an atom summons outside it: an `llmPredict`
  costing 50 fuel may cost real money, a `httpFetch` costing 10 may hammer someone else's
  service. Enforced in the atom exec wrapper **before** both fuel and execution — an
  exhausted quota must not have already made the call it exists to prevent, and must not
  also drain the budget. An unset op is unlimited, so this is purely additive.

  **Scope, honestly: a quota counts calls within ONE run.** A capability that starts a _new_
  `vm.run` gets a fresh counter, so an agent able to trigger re-entrancy can multiply its
  allowance. Pass the same `quotaUsed` object to each nested run to enforce a shared cap.
  Across a process or network boundary no such enforcement is possible — budget does not
  travel, only tokens and data do. Documented in DOCS-AJS.md and pinned by
  `src/vm/quotas.test.ts`.

- **`installRuntime` replaced the runtime with itself and discarded the flight recorder.**
  Identical prerelease versions did not compare equal, so a second import of the same
  version counted as an upgrade and wholesale-replaced the installed runtime — taking the
  error history and any applied `configure()` with it. (The _ordering_ half of this bug
  survived into the beta; see `[Unreleased]`.)

- **Fuel bypass: size-proportional atoms charged a flat cost** — the `==` bug class, found
  again by a cost-model audit. `defineAtom`'s `cost:` is charged once per call regardless of
  operand width, so any atom whose work scales with input size was effectively unmetered.
  Measured before the fix: **`jsonStringify` serialized a 2,000,000-element array for 1.2 fuel
  and completed under a 10-fuel budget.** `join`, `split`, `jsonParse` and `template` were the
  same. (The expression path already charged proportionally in `methodCall` — the atom path
  had diverged, which is how it survived.) Those atoms now charge via a shared
  `chargeForSize()` on both operand and allocated result, using the same per-char/per-element
  constants as the expression path; fuel now scales linearly with N. Affects 0.12.0 and
  earlier — anyone relying on fuel as a DoS bound against untrusted input should upgrade.
- **Live-heap ceiling (`maxHeapBytes`, default 64 MB)** — the _space_ budget to fuel's _time_
  budget. Fuel meters cumulative work, which bounds how much a program allocates over its
  lifetime but says nothing about how much it holds at once: `x = x + x` charges honestly, yet
  at ~10 KB-per-fuel a legitimate 100,000-fuel budget still buys roughly a gigabyte of live
  string. A run that exhausts host memory has taken the process down regardless of how honestly
  it paid. Guest scope writes (`varSet`/`constSet` and atom-result bindings) are now accounted
  against the ceiling with a bounded, cycle-safe estimator; accounting is **per key**, so
  overwriting a variable frees its budget and ordinary loops don't false-positive. Verified: 26
  doublings of 1 KB (~64 GB unchecked) stops at the ceiling with unlimited fuel.
- **New `src/vm/cost-invariant.test.ts`** pins the invariant mechanically: each size-sensitive
  atom is driven at growing N and must show _marginal_ fuel scaling (a flat-charged atom scores
  exactly 0 marginal fuel — the bug's signature). Cheap stand-in for a mechanized proof of the
  cost model; it catches the next flat-charged O(n) atom rather than relying on someone
  noticing. **Adding a size-sensitive atom means adding a case there.**

### Security / Chore

- **`bun audit` gate with time-gated exemptions.** A new pre-tag lane
  (`src/dependency-audit.test.ts`) fails the suite on any **high or critical** advisory that
  isn't covered by a live entry in `audit-exemptions.ts`. Exemptions are deliberate and dated:
  each carries a `reason` and an `until` date, and **lapses on that date** (the advisory then
  fails the gate again, forcing a re-fix or a renewed justification) — not a permanent silence.
  A dead exemption (advisory no longer reported) warns to be removed. The gate runs in the full
  `bun test` (pre-tag) run, is skipped by `test:fast` (`SKIP_AUDIT=1` — it needs the network),
  and self-skips offline so a network blip can't red the suite. The current exemptions are all
  dev/deploy-only transitive advisories (eslint→brace-expansion/flatted, firebase→undici/form-data)
  with no upstream fix yet.
- **Dropped `vitest` and `valibot` from devDependencies** — removing a whole vulnerable
  dependency chain (incl. a critical `vitest` UI-server advisory) that was dev-only and
  unused: the repo's framework is `bun:test`. Six files that imported `{ describe, it, expect }`
  from `vitest` (and thereby **errored out under `bun test`** — a silent coverage hole across
  the timeout/cost-override/request-context/store tests) were migrated to `bun:test` and now
  run. `valibot` was used only by a compile-only type-inference file, switched to the shipped
  `tosijs-schema`. `@happy-dom/global-registrator` bumped to a fixed happy-dom (≥20.8.9).
  (The published package's runtime deps — `acorn`/`acorn-loose`/`acorn-walk`/`tosijs-schema` —
  carry no advisories; consumers were never exposed.)

### Added

- **`unsafe <expression>` — the per-construct escape.** Marks one construct as deliberate
  at the site: `unsafe new Date(x)`, `unsafe var x = 1`, `unsafe eval(s)`. Zero runtime
  cost. Recognised only in expression position and only on the same line as its expression,
  so a variable named `unsafe` remains legal JavaScript.
- **`/* @tjs-unsafe */`** — the same marker for TypeScript source, which cannot contain
  TJS-only syntax because `tsc` rejects it.
- **Legacy equality bridges** — `DangerousLegacyEquals`, `DangerousLegacyNot`,
  `LegacyExactly`, `LegacyNotExactly`. A fixed _operator_ has no construct to mark, so the
  escape is a name. The coercing pair is named "Dangerous" because `==` invokes
  `valueOf()`/`toString()` on any object and can therefore throw or run arbitrary code; the
  strict pair is not, because `===` cannot.
- **`LegacyDefault(value)`** — per-parameter escape from dictionary defaults, restoring
  JavaScript's atomic semantics for one parameter rather than disabling a whole function's
  validation.
- **ASI guidance.** Statement boundaries are the one place TJS and JavaScript disagree
  (`const x = g` / `(a)` calls `g(a)` in JS, two statements in TJS). That case now warns at
  the site with a line number instead of changing meaning silently.

- **`int` and `unsigned` — the numeric types TypeScript never had.** TS has a single
  numeric type, so "this is a count / index / id" is inexpressible and ends up policed by
  comments or hand-written asserts. `n: int` rejects a float, `n: unsigned` (alias `uint`)
  rejects a negative, and `float` is an explicit spelling of `number`. These **extend**
  TypeScript rather than narrowing it — `number` still means number, so pasted TS is
  unaffected.

  - **The example forms are shorthand for exactly these, and carry a worked value too:**
    `n: int` ≡ `n: 5`, `n: unsigned` ≡ `n: +5`, `n: number` ≡ `n: 5.0`. That equivalence
    is pinned by a test: two spellings of one type that disagree would mean one of them is
    lying to the reader.

- **Canonical form for verified predicates** (`canonicalizePredicate` / `predicateKey`,
  exported from `tjs-lang/lang`). A verified predicate is pure, total, serializable and
  composable; giving it a **canonical form** makes it an _identity_, which is what lets one
  object serve as **cache key**, **pushdown payload** (send the predicate to `store.query`
  instead of dragging rows to the code), **auth object** (a permission _is_ a predicate), and
  the substrate for safe macro splicing. Predicates differing only in formatting, comments or
  local variable names now share a key; differences in operator, literal _value_, field name,
  or any helper in the cluster do not.

  - **Verification is a precondition, not an option** — identity implies "same input ⇒ same
    result", which an impure predicate doesn't satisfy however identical its syntax, so
    canonicalizing an unverified cluster throws `PredicateNotVerifiedError`.
  - **Deliberately not an optimizer:** commutative operands are _not_ reordered. That would be
    a claim about totality and cost, not just purity — and a canonicalizer you can't trust
    isn't usable as an auth object.
  - The convenience `key` is FNV-1a and **documented as non-cryptographic**: fine for cache
    bucketing (a collision costs a miss), insufficient where an adversary picks the input
    (cache poisoning, auth) — hash the `canonical` string with SHA-256 for those.

- **Predicate pushdown (`storeQueryWhere` + `store.queryPredicate`)** — send the _predicate_
  to the data instead of dragging rows to the code. The atom takes a **canonical verified
  predicate** and forwards it as data; the store evaluates it and can cache on its stable
  `key`, so two spellings of the same rule hit the same cache entry. **The VM never parses
  it** — that's what keeps the acorn-dependent canonicalizer out of the lean `tjs-lang/vm`
  bundle and lets the same payload travel to a remote store. `queryPredicate` is **optional**
  (progressive enhancement, like `$predicate` in JSON Schema); a store without it makes
  `storeQueryWhere` **fail loudly** rather than degrade to an unfiltered read — silently
  returning rows the caller meant to exclude is a data-exposure bug, not a fallback.
  - Known, deliberate limitation: canonicalization is **structural**, so refactoring a
    predicate (hoisting a subexpression into a local) mints a new identity. Collapsing those
    would mean inlining, i.e. optimizing — and a canonicalizer that rewrites more than
    spelling isn't one you can trust as an auth object.

### Changed

- **BREAKING for capability authors: the boundary takes plain data only — no accessor
  properties.** A getter is host code, so a membrane that ran one while inspecting a
  payload would be executing the thing it exists to keep out. This bites the obvious shape,
  which is exactly what a host wrapping a `Response` tends to write:

  ```js
  // Rejected: `status` is a getter — code wearing a data costume.
  return { ok: res.ok, get status() { return res.status }, body }

  // Fix: read it once, hand over the value.
  return { ok: res.ok, status: res.status, body }
  ```

  Spreading is not the fix and fails **silently**: a `Response` keeps `ok`/`status`/
  `headers` on its _prototype_, so `{ ...res }` is `{}` — it crosses cleanly and delivers
  nothing. Build the object literally, naming each field. Rejection is a `MonadicError`
  (`Capability boundary rejected the return of '<op>': … accessor property '<name>'`), not
  a throw.

- **`Timestamp` is a number (epoch milliseconds), not an ISO string.** `diff` is `a - b`,
  `isBefore` is `a < b`, sorting is the default comparator — and `Timestamp.now()` is a
  genuine drop-in for `Date.now()`, which it was not before. `iso()` renders the readable
  form; `isValidISO` validates it.

- **Diagnostics for constructs AJS deliberately lacks now SHOW the fix.** `Unsupported
statement type: ForStatement` was accurate and useless: an A/B over diagnostic text
  (`experiments/agent-legibility/error-message-ab.ts`) measured the repair rate each message
  actually produces — worked example **80%**, prose remedy 50%, our shipped message **0%**,
  saying nothing at all **0%**. On the `for`-loop case, prose advice scored 0/5 while the
  same remedy shown as code scored 5/5. `for`, `for...in`, `switch` and `do...while` errors
  now carry a worked correction. Pure message text; no compiler change.
  - Guarded by `src/lang/diagnostic-remedy.test.ts` — deterministic, no model needed: every
    remedy must contain real code, name a supported alternative, reach the thrown message,
    and **correspond to a construct the transpiler actually rejects**. That last check caught
    a first draft claiming `for...of` was unsupported (it isn't) — a diagnostic for a
    restriction that doesn't exist teaches a false limit and is worse than none.

### Fixed

- **`Eq` can no longer be made to run user code.** It unwrapped boxed primitives with
  `a.valueOf()`, which a subclass can override — so a comparison could throw, mutate, or
  lie about the value. It now reads the internal slot via the prototype method.
- **Optional chaining broke the `==`/`!=` rewrite.** `o?.b != null` did not compile: the
  operand scanner treated `?.` as a ternary boundary. Every form was affected.
- **Regex literals were read as comments.** A regex containing `*/` or `//` desynced the
  scanner, and an escaped backslash (`'\\'`) desynced the string scanner — between them
  these broke conversion of several of our own files.

- **Sound TypeScript type names now produce real runtime checks** — restoring a stated
  design goal that had quietly gone missing: _implement the parts of TypeScript that aren't
  Turing-complete damage, and best-effort only the rest._ In native TJS,
  `function f(s: string)` inferred **`any`**, so it transpiled cleanly, looked typed, and
  validated **nothing** — the worst possible outcome in a language whose pitch is that types
  survive to runtime, and it hit the annotation newcomers and models reach for first
  (ASSUMPTIONS.md A7). `string`, `number`, `boolean`, `bigint`, `object`, `null`,
  `undefined` and unions of them now check at runtime, agreeing exactly with the equivalent
  example type (`s: string` ≡ `s: ''`). `any`/`unknown`/`void`/`never` remain unconstrained
  because that is what they mean; an unresolvable user type still degrades to best-effort
  rather than erroring, which preserves TJS ⊇ JS.

  - **Deliberately still best-effort:** conditional types, mapped types, recursive
    templates, `infer` — the undecidable type-level metaprogramming TJS answers with a
    _predicate function_ you can read, test and run.
  - Known gap: `string[]` doesn't parse (use `['']`). It fails **loudly**, which is the
    acceptable interim state — a parse error tells you to fix something; the old silent
    `any` removed your type checking and said nothing.

- **Best-effort type degradation now teaches instead of happening silently.** When an
  annotation can't be resolved to a runtime type it still degrades to `any` (by design —
  TJS ⊇ JS), but the transpiler now emits a warning naming what was dropped and showing the
  ladder back to safety: an example (`foo: 3`), a sound type (`foo: number`), or a
  `Type … { predicate(v) { … } }`. The suggestion is **shown as code**, per the measured
  finding that a remedy shown repairs 80% where the same advice as prose repairs 50% and a
  bare diagnostic 0%. No warning when `any`/`unknown` was asked for explicitly — honouring
  `any` isn't a degradation, and warning there would train people to ignore the channel.

- **Bare-assignment auto-`const` no longer captures an all-caps alias** (#22). In native tjs,
  `B = BABYLON` was rewritten to `const B = …`; when `B` was declared in an enclosing/host
  scope the source-level transform can't see (e.g. a `/*# */` example inside a module that
  already `let B`s), the injected `const` shadowed the outer binding — it bit tosijs-3d demos.
  A bare-identifier RHS is now treated as an alias/reassignment and left alone; the feature
  still fires for definition RHSs (`Foo = Type(…)`, `Foo = { … }`, `Bar = mk()`).
- **`configure()` after a converted module loaded now warns instead of silently doing nothing**
  (#23). A converted module snapshots its config when it captures the runtime
  (`globalThis.__tjs.createRuntime()`) at import, so `configure()` called after the module graph
  evaluated reached nothing — which made tosijs's debug/safe bundles inert. It now emits a loud
  one-time `console.warn` (+ a recorder `warning`) pointing at the import-order requirement,
  reliably distinguishing the install (bare module-level `createRuntime()`) from a module's
  capture (the instance's `createRuntime()`), so configuring before any module loads never warns.
  (Making config a live post-eval read is a deeper change to the intentional per-instance
  isolation — deferred to 0.13.0; a silent no-op was the worst outcome and is now gone.)
- **`==` inside an inline `wasm { }` block is no longer rewritten to `Eq(...)`** (L807). In
  native tjs the `==`→`Eq()` (and `Is`/`IsNot`→call) transforms ran before inline wasm-block
  extraction, so a `wasm { if (a == b) … }` body became `Eq(a, b)` — which the wasm compiler
  can't compile, silently falling back to JS. Wasm bodies are now masked across just those two
  operator transforms and restored before extraction, so the wasm compiles; a following
  `fallback { }` (real JS) still gets the normal rewrite. `wasm function` declarations were
  already unaffected (extracted earlier).

## [0.12.0] — 2026-07-20

Minor bump with **breaking changes** — see **Changed**. Lands the `TjsDictDefaults` mode and
five VM-security fixes from a two-round adversarial review. Closes **zero** open GitHub issues
(this release is security + dict-defaults, both internally driven); the per-mode opt-out those
security/dict-defaults changes make more acute is tracked as #7, still open.

### Security

> **Affected versions:** the SSRF, ReDoS, capability-membrane, `methodCall`, and scope-name
> fixes below address vulnerabilities present in **0.11.0 and all earlier releases**. Pinned
> consumers (VM embedders) should upgrade.

- **Capability-boundary membrane on the VM.** Every value an `effects: 'io'` atom
  returns (`httpFetch`, `storeGet`/`storeQuery`/vector search, `llmPredict`, `agentRun`,
  `runCode`/`transpileCode`, …) is now deep-copied through a structured-clone membrane
  before it enters guest state, at a single choke point in the atom exec wrapper. This
  closes a defense-in-depth hole surfaced by an adversarial review: previously a
  capability could hand the guest a **live host reference** — an object carrying callable
  methods (e.g. a `Response` with `.json()`/`.text()`, or any object with a function
  property) — which the guest could then invoke via `methodCall` to reach the host realm,
  or mutate while the host still held it. The membrane rejects functions, symbols, and
  other non-cloneable host references with a `MonadicError` (`Capability boundary rejected
the return of '<op>'`), and gives clean data fresh identity so guest mutation can't
  alias host state. A budgeted, cycle-safe pre-walk caps the estimated payload size
  (`membraneMaxBytes` run option, default 4 MB) and **rejects oversized returns before the
  copy allocates**, so a hostile or broken capability can't OOM the VM through the
  capability boundary. **Contract change:** custom capabilities must return
  structured-cloneable data — a capability that returned a live `Response` must now return
  the fields the guest reads as a plain object (`{ ok, status, body }`); the default fetch
  path already normalizes to parsed body / text / data-URL and is unaffected.
- **`methodCall` is now allowlisted, not blocklisted.** Guest method invocations
  (`str.toUpperCase()`, `arr.includes(x)`, `d.format()`, …) are restricted to an allowlist
  computed from the standard built-in prototypes, the curated builtin statics, and the VM's
  own wrapper types (Date/Set) — replacing the previous name-blocklist that admitted any
  method not literally named `__proto__`/`constructor`/`prototype`. Behind the membrane
  (guest values are plain data) this permits everything a guest legitimately calls and
  nothing else; the teeth are that `call`/`apply`/`bind` live only on `Function.prototype`
  and are therefore rejected, so a leaked function reference can't be re-invoked with a
  chosen `this`.
- **SSRF guard (`isBlockedUrl`) now covers full private/loopback ranges.** Previously only
  `127.0.0.1` (not the rest of `127.0.0.0/8`, so `127.0.0.2` passed) and the single cloud
  metadata IP were blocked, and IPv6 private ranges weren't checked at all. Now blocks all
  of loopback `127/8`, `0/8`, private `10/8` · `172.16/12` · `192.168/16`, link-local
  `169.254/16` (the whole cloud-metadata range), and — for IPv6 — `::1`/`::`, unique-local
  `fc00::/7`, link-local `fe80::/10`, and IPv4-mapped addresses (`::ffff:7f00:1` = 127.0.0.1)
  that embed a blocked IPv4. WHATWG URL normalization already collapses shorthand/decimal
  IPv4 (`127.1`, `2130706433`) to canonical form before the check.
- **`regexMatch` ReDoS hardening — length caps + a wider heuristic.** The regex engine's
  backtracking is opaque to the fuel counter, so `regexMatch` now fails closed on three
  fronts: a pattern-length cap (1000 chars), an input-length cap (100 000 chars, checked
  after coercing the value to a string), and an extended suspicious-pattern check that also
  catches a quantified group repeated by an unbounded outer quantifier (`(a+){2,}`) in
  addition to the existing `(a+)+`/`(.*)+` forms. Safe patterns (including bounded
  `(abc){3}` and grouped captures like `(\d{3})-(\d{4})`) are unaffected.
- **VM scope variables can't be named a forbidden property.** Binding a variable named
  `__proto__`/`constructor`/`prototype` (via `varSet`/`constSet`/`varsLet`/`varsImport` or
  an atom result `.as('__proto__')`) is now rejected — previously such a name would mutate
  the scope object's own prototype chain (`createChildScope` uses `Object.create(state)`)
  instead of creating a binding. No global prototype pollution was possible, but the scope
  corruption is now closed at the write sites, mirroring the member-access guard.

### Added

- **Dictionary defaults — the `TjsDictDefaults` mode** (`docs/dictionary-defaults.md`).
  In native tjs, `(args = {x: 0, y: 0})` now has WebIDL-dictionary semantics: each member
  individually defaulted, partial payloads merged per member (recursively —
  `place({pos: {x: 5}})` keeps `pos.y` and every other default), members type-checked with
  precise error paths, `undefined` members treated as absent, example-`null` members
  nullable-any, arrays replaced wholesale (element-checked), excess keys stripped with a
  once-per-site flight-recorder notice naming them, and prototype-pollution keys
  (`__proto__`/`constructor`/`prototype`) rejected outright. Complete payloads return by
  identity — zero allocation.
  - **Faster than hand-rolling it, correct or not:** the merge is emitted as
    shape-specialized code per signature; measured 91 ns/op on a complete 8-member/3-nested
    payload vs 276 for the careful hand-written spread merge and 107 for the _incorrect_
    shallow spread — while validating every member (three-tier methodology in
    `experiments/dictionary-defaults/perf.bench.test.ts`).
  - **Mode-gated per PRINCIPLES.md:** ON in native `.tjs` (like `TjsEquals`), OFF under
    `dialect: 'js'`, `fromTS`, VM targets, and `TjsCompat` — JS-legal source keeps atomic
    JS default semantics exactly. `TjsStrict` enables it; `TjsDictDefaults` is a standalone
    directive. Impure object-literal defaults (`{x: mkX()}`) are a compile error in native
    mode (compute in the body, or use a colon-form param); non-literal defaults
    (`args = live`, `x = 0`, `list = []`) are untouched.
  - Required-ness needs no new syntax: `:` params are required (member-validated since
    Stage 0), `=` params are defaulted — mixed shapes use separate params, the platform
    convention.
  - **`.d.ts` output is deep-partial for dictionary params:** `generateDTS` emits
    `args?: { pos?: { x?: number; y?: number }; label?: string }` so TypeScript callers can
    pass the partials tjs accepts. Mode-gated (the transpile result now carries `tjsModes`);
    dialect-js output keeps required members, where partials genuinely aren't valid.
  - **Lint catches excess keys at literal call sites** (`dict-default-excess-key`). The
    runtime strips an undeclared key with a once-per-site notice, but at a literal call site
    (`place({x, y, treshold})`) it's almost always a typo — the linter now flags it
    statically, recursing into nested object literals (`move({pos: {x, z}})` →
    `move.pos`). Mode-gated on `TjsDictDefaults`; skips arguments carrying a spread (the
    spread may supply the key) and non-literal arguments; covers named functions and
    arrow/function expressions bound to a const.

### Changed

- **Behavior change (native `.tjs` only): existing `= {object literal}` params now
  merge-on-partial, validate members, and strip excess keys.** This is the visible face of
  the `TjsDictDefaults` mode above, called out separately because it changes code that was
  already legal. Before, `function f(o = {x: 0, tag: ''})` treated the object as an atomic
  JS default with no validation; now:

  - `f({x: 5})` → `{x: 5, tag: ''}` (was `{x: 5}` — `tag` is filled from the default),
  - `f({x: 's'})` → `MonadicError` (was `{x: 's'}` — members are type-checked),
  - `f({x: 1, extra: 9})` → `{x: 1, tag: ''}` + a once-per-site recorder notice (was
    `{x: 1, tag: '', extra: 9}` — excess keys are stripped).

  It transpiles either way, so a break is only visible at runtime. **Migration:** to keep
  the old atomic-default semantics, set `dialect: 'js'`, add the `TjsCompat` directive, mark
  the function `unsafe` (skips all its validation, the merge included), or use a non-object
  default. There is no per-mode "off" directive to disable only `TjsDictDefaults` yet (see
  #7). The new excess-key lint (`dict-default-excess-key`) flags stray literal-call-site keys
  statically.

- **Behavior change (VM embedders): capability returns must be structured-cloneable data,
  and guest `methodCall` is allowlisted.** Repeated here from **Security** because it breaks
  custom-capability consumers: a capability that returned a live `Response` (or any object
  carrying methods / host references) now hard-fails at the boundary with `Capability
boundary rejected the return of '<op>'`. **Migration:** normalize returns to plain data
  (`Response` → `{ ok, status, body }`); the default `httpFetch` already does. Tune the size
  cap with the `membraneMaxBytes` run option (default 4 MB).
- **Colon-form object params now enforce their member contract** (Stage 0 of
  dictionary defaults, `docs/dictionary-defaults.md`). `function f(args: {x: 0, y: 0})`
  has always documented "an object with integer x and y," but the emitted check was
  `typeof args === 'object'` only — partial payloads, wrong member types, and garbage
  members all passed while the full shape sat unused in `fn.__tjs.params`. Members are
  now required and type-checked (recursively, arrays included) with precise error paths
  (`f.args.pos.y`), matching `typeMatches` and the inline `Type.check` semantics. Excess
  members are still ignored (the excess-key policy belongs to the forthcoming merge
  mode). **Scope:** required (colon-form) params only — the JS-legal `=` form keeps
  plain-JS semantics, and code that hasn't opted into validation is unaffected. For
  TS-originated code this makes the runtime contract match what TypeScript itself
  enforces statically (`greet({name})` against `{name: string; age: number}` is a TS
  compile error — and now a runtime `MonadicError` too).

## [0.11.0] — 2026-07-18

Minor bump — two new entry points (`./import-resolver`, `./import-resolver/worker`),
no breaking changes. This is the release tosijs-ui's doc system builds against.

### Added

- **`tjs-lang/import-resolver`** (#20) — the playground's bundler-free bare-import
  machinery (TFS), promoted from `demo/` to a real export so doc systems (tosijs-ui's
  live-example) can own import resolution instead of hand-rolling it. `rewriteImports`
  rewrites bare specifiers to a configurable same-origin prefix (`/tfs/` default, e.g.
  `/lib/`); a service worker resolves them to a CDN — JSDelivr `/+esm` by default, an
  esm.sh allowlist for peer-dep dedup (react/react-dom), `jsdelivr/`·`esmsh/`·`unpkg/`·
  `github/` hints — and caches via the Cache API.
  - The worker ships as the raw classic-script asset **`tjs-lang/import-resolver/worker`**
    (`dist/import-resolver-worker.js`, esbuild IIFE, 2.9KB): a service worker is
    origin-scoped, so consumers copy it into their public root and call
    `registerImportResolver({ prefix, workerUrl, scope })`. Config travels to the worker
    as a **query string on its registered script URL** — available before the first
    intercepted fetch and durable across worker restarts — so the client rewrite and the
    worker's routing derive from one `ResolverConfig` and cannot disagree.
  - **The routing now has exactly one implementation** (`src/import-resolver/resolve.ts`,
    pure, zero-dependency). It previously lived in three diverged copies: the demo
    client, the demo service worker, and a materially different reimplementation in the
    dev server (raw JSDelivr + its own package.json-exports resolution — a package could
    resolve differently through the fallback than through the worker). The dev server and
    the playground worker now consume the shared core; the playground's `/iframe/`
    protocol stays demo-only, composed on top.
  - The previously-untested routing core is now covered (`resolve.test.ts`: parsing, CDN
    routing, hints, config round-trip, a client↔worker prefix-agreement guard, and an
    anti-drift smoke that parses the built worker as a classic script and checks the
    routing is embedded).

### Documentation

- `docs/dictionary-defaults.md` — design spec for **merge-on-partial object arguments**
  (WebIDL-dictionary semantics for options bags), a gated native-TJS mode. Includes the
  measured finding that member-level object-param validation doesn't exist yet (the
  emitted check is typeof-only; the full shape metadata goes unused). Spike A (semantics
  harness + 33-case table suite) lives in `experiments/dictionary-defaults/`.

## [0.10.1] — 2026-07-17

Patch — a critical fix, no API changes. One behavior change (`Is()` on cyclic
graphs now answers instead of crashing), noted under Changed.

### Fixed

- **Exponential blowup in deep-equal/format on shared-reference object graphs** (#21 —
  critical; same defect class as [oven-sh/bun#34178](https://github.com/oven-sh/bun/issues/34178),
  and since tjs ships its own `expect`, Bun's fix did not cover us). A DAG built as
  `{a: n, b: n}` per level has O(depth) nodes but a 2^depth unfolded tree:
  - `format()` re-serialized shared references via raw `JSON.stringify` — 21MB at depth 20,
    verified OOM at depth 28 under bun/JSC — whenever an assertion **failed**. It now marks
    revisited objects as `[shared]` (which also fixes true cycles, where `JSON.stringify`
    used to throw and eat the assertion message) and hard-caps output at 16KB.
  - `deepEqual` walked all 2^depth paths on **every** assertion (~61s at depth 30). It now
    memoizes visited pairs — a revisit is assumed equal (sound: any `false` short-circuits
    to the top) — collapsing the walk to O(nodes).
  - Fixed in all **five** copies (the issue named one): the injected `expectFunction`
    (`tests.ts`), the transpile-time harness's `__deepEqual` and `__format`/`formatValue`
    (`js-tests.ts`, which also had no depth bound), the runtime `Is()`, and the emitted
    inline `Is`. Guarded by `src/lang/dag-safety.test.ts`, calibrated so a regression fails
    cleanly (timeouts / message-length assertions) instead of killing the machine.
  - `Is()` stays allocation-free on the hot path: pair memoization only engages past
    recursion depth 8 (exponential blowup requires depth; a shallow shared graph pays at
    most a small constant factor). Measured: flat/nested small-object compares within noise
    of pre-fix (~29ns/58ns per call); `Is(dag(30), dag(30))` went from **101s to 3.2ms**.

### Changed

- **`Is()` on two distinct-but-cyclic graphs now terminates and returns their structural
  equality** (bisimulation semantics). Previously it recursed until stack overflow
  (`RangeError`). This is a behavior change to a language primitive, strictly in the
  direction of "gives an answer instead of crashing" — but if anything relied on the throw,
  note it here. Same applies to the test-harness `deepEqual`s.

## [0.10.0] — 2026-07-16

Minor bump — additive features and fixes, no breaking changes.

### Added

- **Framework-free editor primitives** — a new `tjs-lang/editors` entry point exporting
  `collectScopeSymbols` (AST scope extraction, destructuring included, carries `origin`),
  `introspectValue` (live value → members), and `scopeCaptureEpilogue` (capture a run's
  top-level bindings in-run, no re-execution). Acorn-only, no CodeMirror/Monaco/Ace
  dependency. Closes **#10** — downstream consumers (tosijs-ui) were hand-rolling a worse
  regex copy of the scope extractor because it wasn't exported.
- **`tjs-lang/editors/codemirror` now ships types.** The editor build emits `.d.ts` and the
  export declares a `types` condition, so consumers stop re-declaring `AutocompleteConfig` by
  hand (**#12**). The five `@codemirror/*` packages the CodeMirror integration imports are now
  declared as optional `peerDependencies` — an undeclared import resolved locally by hoisting
  and hard-failed in a consumer's isolated install (**#16**).
- **`functionMetaToJSONSchema` is now exported from `tjs-lang/lang`** (it was only on
  `src/lang/index.ts`, which the subpath doesn't resolve to — the documented import failed
  with "Export not found"). Emitted standalone code also carries `.toJSONSchema()` / `.strip()`
  on its inline `Type`/`Enum`/`Union` stubs when a file uses them, so a TJS type can describe
  itself at runtime from inside emitted `.js`.
- **Flight recorder** (#17). The `__tjs` error ring buffer is now a black box for the whole
  runtime, not just a type-error log. New API on the module, the runtime object, and every
  `createRuntime()` instance: `record(entry)`, `records(filter?)`, `clearRecords()`,
  `getRecordCount()`, `getDroppedCount()`. Records carry a `source`
  (`type`/`wasm`/`vm`/`app`/…) and a `severity` (`error`/`warning`/`notice`), and can be
  filtered by either.
  - **Reports today:** type errors; `wasm{}` blocks that fell back to JS or failed to
    instantiate (surfacing the previously-silent fallback, **#15**); typed arrays copied in
    and out on every call because they weren't allocated with `wasmBuffer()` — previously
    silent and can be slower than plain JS (**#9**); every VM failure — fuel exhaustion,
    atom timeout, capability denial.
  - **Records once per site, never per call** — a recorder that fires inside a hot loop
    becomes the performance problem it exists to detect.
  - **`errors()` is unchanged** and still returns type errors _only_, so the documented
    `clearErrors()` → run → expect-none idiom keeps working. Notices never leak into it.
  - Emitted modules mirror their records into the installed global runtime, so a page with
    several TJS modules has one flight history rather than N isolated ones. Standalone
    emitted code (inline fallback runtime) starts reporting as soon as a runtime is
    installed, even if it loaded before one existed.
  - Recording never throws, never logs unbidden, and never alters control flow.
- Type-system north-star design note (`docs/type-system-north-star.md`):
  JSON-Schema + `$predicate` as the single source of truth for TJS types.

### Changed

- **LLM tests restructured into three lanes by what they prove**, cutting the LLM cost of
  the pre-tag gate from ~82s (two files) to ~4s while _adding_ deterministic coverage:
  - **Plumbing → `test:fast`.** The real LM Studio HTTP client (`getLLMCapability`) now has
    deterministic coverage (`src/batteries/llm-transport.test.ts`, ~40ms) against an
    in-process fixture server. It was previously exercised _only_ live — backwards for code
    we own. (`batteries.test.ts` didn't cover it either: it mocks a _reimplementation_ of
    predict/embed, not the real client.)
  - **Live smoke pared.** `models.integration.test.ts` audited five times (once per test);
    it now audits once in `beforeAll` and keeps only predict + embed shape checks. ~28s → ~4s.
  - **AJS grokkability is its own advisory lane** (`bun run test:grok`, behind
    `RUN_GROK_TESTS`, not in the gate). It measures whether a pinned small model
    (gemma-4-e2b) can write valid AJS — a load-bearing AJS premise — as a success _rate_
    over N samples vs a bar, and **never fails on the rate** (model variance ≠ code
    regression). Replaces `transpiler-llm.test.ts`, whose `withRetry(1-of-3)` passed on a
    33% success rate and couldn't tell a healthy 90% from a degraded 35%.

### Fixed

- **The pre-tag gate no longer fails on LM Studio flakiness.** The live playground-example
  LLM tests (`demo/examples.test.ts`) hit a real LM Studio, which is prone to transient
  400s and dropped connections while models swap under memory pressure — a bad server
  moment, not a code regression, could block a release tag. They now retry the live call
  and degrade to the existing mock (with a visible warning) on persistent failure, so the
  gate blocks on code, never on server health. Safe because the LLM client's request/response
  shape is guarded deterministically by `llm-transport.test.ts` — a real malformed-request
  regression fails there, loudly; and a broken example still fails via its transpile/VM
  error. The fallback logic is itself covered by deterministic tests.
- **The friendly "start LM Studio" error was dead under Bun.** `getLLMCapability` detected a
  refused connection via `e.cause?.code === 'ECONNREFUSED'` (Node's shape), but Bun — our
  primary runtime — surfaces it as `e.code === 'ConnectionRefused'`, so users got a raw
  "Unable to connect" instead of the actionable message. Now detects both. (Found by the new
  deterministic transport tests.)
- **Every file in `examples/` works again, and a guardrail keeps it that way**
  (`src/examples.test.ts` runs each through `tjs check` _and_ `tjs run`). Five of the
  seven were broken; nothing caught it because nothing ran them. Beyond the `tjs run`
  and WASM bugs below, this surfaced:
  - **`tjs run` could not run any file with an `import` or an `export`.** It evaluated
    emitted code with `new AsyncFunction(code)`, and `import`/`export` are module-only
    syntax — a `SyntaxError` inside a function body. It even reported the failure as a
    syntax error in the _source_, pointing at a line the user never wrote. The emitted
    module is now written beside the source and imported, so relative and bare imports
    resolve exactly as they would for the original file.
  - **`tjs run` executed your program twice.** The transpile-time test harness _evaluates
    the module_ to run signature tests, and then `run` evaluated it again — so every
    top-level side effect fired twice (`console.log('hi')` printed `hi` twice). Running a
    program no longer tests it; that is what `tjs test` / `tjs check` are for (the same
    position the Bun plugin already took).
  - **Generics were dead on arrival in emitted code.** A generic's predicate receives its
    type parameters as **check functions** — `Generic Box<T> { predicate(obj, T) { … T(obj.value) } }`
    — but the inline runtime spread the raw type _arguments_ in, so `T` was the string
    `''` and calling it threw `checkT is not a function`.
  - **A runtime type's `check()` accepted anything of the right `typeof`.** For an object
    example that means _any_ object passed: `User.check({ name: 'Alice' })` returned `true`
    for a type requiring `name`+`age`+`email`. It now matches the example structurally. A
    validator that answers "yes" to everything is worse than no validator.
  - **`.toJSONSchema()` / `.strip()` did not exist in emitted code**, so a TJS type could
    not describe itself from inside TJS — the "types are examples that survive to runtime"
    claim, unmet. Both are now emitted (only for files that use them).
  - **`tjs-lang/lang` did not export `functionMetaToJSONSchema`.** `src/lang/index.ts` did,
    but the subpath resolves to `src/lang/transpiler.ts`, and the two had drifted — so the
    documented import failed with "Export not found".
- **WASM now instantiates synchronously**, so an exported `wasm function` can be called
  the moment its module is imported. The bootstrap was a fire-and-forget `async` IIFE, so
  nothing was bound to `globalThis` until a microtask later. An inline `wasm{} fallback{}`
  block survives that window (it runs the JS fallback), but a `wasm function` declaration
  has **no** fallback — it calls the global directly. So
  `import { dot } from 'tjs-lang/linalg'; dot(a, b, 3)` threw
  `__tjs_wasm_dot is not a function`: a shipped entry point that could not be imported and
  used. `new WebAssembly.Module` is synchronous everywhere except a browser main thread
  with a >4KB module, which is now the only case that takes the async path —
  `__tjs_wasm_ready()` still resolves in both and remains what to await in a browser.
- **Inline `wasm{}` block ids are no longer a per-file counter.** Every module's first
  block claimed `globalThis.__tjs_wasm_0`, so two modules with inline wasm blocks
  overwrote each other's binding — and since the emitted call site guards the wasm path
  on that global merely _existing_, module A could find module B's compiled function and
  call it with A's captured variables. Ids are now salted with a content hash of the
  module (`__tjs_wasm_<hash>_<n>`), which is deterministic, so the metadata cache is
  unaffected. Named `wasm function` declarations keep their exact `__tjs_wasm_<name>` id —
  that name is the cross-file composition contract.
- **A `wasm{}` block that failed to compile could still be called.** It was left in the
  module as a stub (correct — function indices must stay stable for other blocks'
  `call <i>`) but was _also_ exported and bound to `globalThis`, which made the call
  site's guard see a function and take the wasm path into a body that never compiled,
  invoking it with captures that don't exist in that scope. Failed blocks are no longer
  bound. (Reachable before this release too: the async instantiation window merely hid it
  from any caller that ran synchronously.)
- **`tjs run` was preprocessing every file twice** — it called `preprocess()` and then
  handed the already-preprocessed source to `transpileToJS`, which preprocesses
  internally. The first pass consumes the `wasm` blocks, so the emitter never saw them,
  emitted no wasm bootstrap, and ran the file with `wasmBuffer` undefined while every
  `wasm{}` block silently fell back to JS. It produced correct answers, which is why it
  went unnoticed.
- **`tjs run` injected a runtime prelude that collided with the emitted code.** It
  declared `const { Type, Generic, ... }`, while emitted code inlines its own
  `function Type` fallback — `const Type` plus `function Type` in one scope is a
  `SyntaxError`, reported against a line number the source file did not have. Emitted code
  is standalone by design; the prelude is gone.
- WASM module instantiation failures were swallowed by a bare `.catch(() => {})` in the
  emitted bootstrap — the module vanished without a trace while every `wasm{}` block in
  the file silently ran its JS fallback. Now recorded as a warning.
- The inline runtime core (`MonadicError` + `typeError` + `isMonadicError`) was emitted
  from three copy-pasted source strings. A file needing `checkFnShape` **and** bang access
  without `typeError` would have declared `class MonadicError` twice in one scope (a
  `SyntaxError` in the emitted output). Not reachable in practice — but held shut by
  coincidence, not design. Now one definition, emitted once.

### Performance

- The Bun plugin (`preload`ed by `bunfig.toml`) no longer loads the whole transpiler at
  startup just to register a `.tjs` `onLoad` hook that most invocations never fire. The
  import moved inside the callback, cutting `bun` startup **in this repo** from ~34ms to
  ~18ms (bun's cold floor is ~11ms, so the preload had made it start _slower than node_).
  This is a saving per invocation — every `bun test`, every CLI run. It defers the
  transpiler rather than adding work: a run that does import a `.tjs` pays the same total.

### Documentation

- `MEMORY-PROFILE.md`: what transpilation actually costs under bun vs node. `fromTS` calls
  only the TypeScript **parser and emitter** (`createSourceFile` + `transpileModule`),
  never `createProgram` or a type checker, so its memory is bounded by the largest file
  seen rather than by project size — a whole 36.7k-line project costs about half of what
  `tsc` costs to check it once. Also records a measured, unfixed inefficiency:
  `transpileModule` is called once per top-level statement **and per class member** (89
  times for one 1,930-line file), which is ~70–80% of `fromTS` wall time and roughly 3×
  the cost of a single whole-file call.
- CLAUDE.md now defers cross-project defaults to `../tosijs-coding-practices`, recording
  only tjs-lang-specific divergences.
- Explained why the full build is named `make`, not `build` (`bun build` is a Bun
  builtin — a `build` script would be silently shadowed).
- `src/docs-index.test.ts` enforces that `llms.txt` indexes every top-level/`docs/`
  markdown file and every `package.json` entry point, and that its links resolve.
- Added a `pre-commit` hook (`.githooks/`, enabled by the `prepare` script) that checks
  **staged files only** with Prettier and ESLint, plus a repo-wide `bun run format:check`.

## [0.9.1] — 2026-07-11

No breaking changes.

### Added

- **Inline-WASM developer feedback** (from tosijs-ui adoption):
  - Silent `wasm{}` fallback now surfaces in `result.warnings` (UI-#1).
  - `await __tjs_wasm_ready()` awaitable ready signal (UI-#2).
  - `__tjs_wasm_enabled` enable/disable toggle (UI-#3).
  - `f32x4` min/max, comparisons, and `select` for data-dependent SIMD (UI-#6).
- Auto-lint for `i32 / i32` integer division, a WASM footgun (UI-#4), plus
  supported-control-flow-subset docs (UI-#5).

### Changed

- `TjsStrict` now escalates an unverifiable predicate to a **transpile error**
  (default remains warn-only, preserving the subset invariant).

## [0.9.0] — 2026-07-06

### Added

- **Predicate verification** wired into `Type` **and** `Generic` guards — verified
  predicates compile to fuel-bounded, DoS-safe native JS, with graceful fallback.
- Per-predicate verification status on the `tjs()` result (`result.predicates`,
  mirrored into `result.warnings`); exported `PredicateVerification` from `tjs-lang/lang`.
- ReDoS lint: the verifier rejects ReDoS-prone regexes.
- New package subpaths: **`tjs-lang/css`** (verified-predicate CSS validators —
  colors, dimensions, order-flexible shorthands, recursive style structure,
  `$predicate` schema builders, property-aware validation), **`tjs-lang/schema`**
  (tosijs-schema pre-wired with `$predicate` support), **`tjs-lang/runtime`**, and
  **`tjs-lang/bun-plugin`**.
- `$predicate` JSON-Schema keyword + `createPredicateEvaluator` (the tosijs-schema bridge).
- `generateDTS` reachable from `tjs-lang/lang`; `editors/*` rebuilt from source.

### Fixed

- `.d.ts` emitter: bare params are required positions, not optional (valid TS).
- TS→TJS (`fromTS`) no longer leaks raw TS into `Type`/`Generic` blocks.

### Changed (mildly breaking)

- **`fromTS` is no longer re-exported from the main entry** — import it from
  `tjs-lang/lang/from-ts` (keeps the TypeScript compiler out of the main bundle).

## [0.8.7] — 2026-07-01

### Fixed

- Bare-assignment auto-`const` must not touch plain JS or redeclare bindings.
- A doc comment must start a line (mid-line `/*#` and `/**` are ignored).

## [0.8.6] — 2026-06-30

### Fixed

- TS→TJS (`fromTS`) never leaks raw TS into `Type`/`Generic` blocks.

## [0.8.5] — 2026-06-30

### Added

- Self-contained browser bundles for in-browser transpilation (`tjs-lang/browser`).

### Fixed

- AJS `==` is footgun-free (not structural), consistent with TJS. (Recorded that the
  old structural `==` was also a fuel-bypass DoS; a future `Is` atom must be fuel-metered.)

## [0.8.4] — 2026-06-26

### Added

- First-class **predicate-safety verifier** (`src/lang/predicate.ts`) + fuel-bounded,
  global-shadowed native predicate compiler.
- Atom `effects: 'pure' | 'io'` tag — the predicate-safety keystone.
- The `$predicate` JSON-Schema keyword + reference evaluator.
- `suggest()` — autocomplete completions mined from predicate clusters.
- Introspection-driven, destructuring-aware playground autocomplete (scope-aware
  symbol model + runtime-truth member completion via an introspection bridge).

### Changed

- `==` is footgun-free (not structural) — stale docs corrected and pinned with tests.

_(No `0.8.3` was tagged — the version was skipped.)_

## [0.8.2] — 2026-06-24

### Added

- Explicit **source dialect** (`js | tjs`) + extension-based resolution; restores the
  **TJS ⊇ AJS** subset invariant.
- AJS local helper functions.
- Playground surfaces inconclusive signature tests as a distinct state.

### Fixed

- Run-level default timeout = `max(atom timeout) × 2` (was a fixed 60s).
- Generous timeouts on embedding IO atoms (`storeVectorize`/`storeVectorAdd`).
- `.prettierignore` narrowed so `format` isn't ~50× slower.

## [0.8.1] — 2026-06-10

### Fixed

- Broken npm main entry (index bundle) + structured-output `predict` fix.
- `predict()` omits an empty `tools` array so structured output works.
- Robust SIMD speedup timing in the demo (no more "Infinityx" in Firefox).

### Changed

- Migrated to ESLint 10 + typescript-eslint 8 flat config.

## [0.8.0] — 2026-05-14

### Added

- **Cross-file WASM libraries**: composable `wasm function` declarations,
  transpile-time module composition (wasm-to-wasm `call` resolution), and the
  `tjs-lang/linalg` SIMD stdlib subpath. See `wasm-library-plan.md`.

> Entries below 0.8.0 are **backfilled coarsely from the git tags and log** (they
> predated this changelog). Only `v0.2.0`, `v0.7.6`, `v0.7.7`, `v0.7.8` were tagged
> before 0.8.0, so the long `0.2.0 → 0.7.6` span is summarized as one entry rather
> than split across untagged versions.

## [0.7.8] — 2026-04-30

### Added / Fixed

- AJS agent-loop fixes (PR #2): computed member access `arr[i]` in expressions,
  `while` error propagation (no more infinite fuel burn on a failing body), battery
  user type widened for multi-turn messages.
- Runtime: validate function-typed params on every call, pass-time function-shape
  checks, deep array validation (`arr: [0]` checks element types, not just
  array-ness), array-error propagation through nested params.
- Inference: rich function shapes (params + return types); `function` kind for
  arrow/function-expression defaults.
- Docs: classes and `test { }` blocks render as documentation; function-extraction fixes.
- Playground: SW-served iframe (all iframe fetches route through TFS), per-package /
  per-import CDN routing (JSDelivr `/+esm` default, esm.sh for React peer-dep dedup).

## [0.7.7] — 2026-04-27

### Fixed

- Protect string literals from code transformations.

## [0.7.6] — 2026-04-26

The long feature-accretion phase (202 commits since 0.2.0, no intermediate tags —
coarse summary):

### Added

- **Inline WASM**: compiled at transpile time (with WAT comments), SIMD (v128/f32x4)
  intrinsics, `wasmBuffer()` zero-copy memory + vector search (~5× speedup), iframe
  instantiation.
- **FunctionPredicate**: first-class function types, generic `FunctionPredicate<T>`,
  structural validation, `.d.ts` emission.
- **JSON Schema generation** from TJS types + function signatures; `Type.strip()`;
  ecosystem compat tests (Zod, Effect, Radash, Superstruct, ts-pattern, Kysely).
- **Complete `.d.ts` emission**: constants, type aliases, rest params, auto-populated
  declaration blocks for round-tripping; DOM type handling in `fromTS` (130+ types).
- **Error-history ring buffer** (flight recorder) for catching silent monadic errors.
- **Honest equality**: `==` split into `Eq` (honest equality) vs `Is` (structural),
  `tjsEquals` symbol protocol, VM structural equality; `typeof null === 'null'`;
  `NaN == NaN` is true; `IsBounded()`.
- `TjsNoVar` + `const!` (compile-time immutability, zero runtime cost); standalone JS
  output (emitted code runs without runtime setup); `@tjs` annotations and
  `/* @tjs ... */` mode directives in TS source.
- **Playground TFS service worker**: dynamic module resolution, specifiers rewritten
  directly to `/tfs/` URLs (import maps dropped); Firebase infra (Auth, Cloud
  Functions with `Eval`).

### Changed

- ASI protection fixes (was breaking WASM multiline expressions); predicate reason
  strings in diagnostic type errors.

## [0.2.0] — 2026-01-29

The foundational release: the TJS→JS transpiler (runtime type metadata), the
TypeScript→TJS converter, the AJS gas-metered VM (fuel metering, capability
injection, monadic errors), the builder API, stored procedures (AST-as-token),
`Eval()` safe eval, proportional fuel charging for memory-allocating ops, and the
playground + editor integrations (Monaco / CodeMirror / Ace, linter, autocomplete POC).

### Changed

- **BREAKING — VM return flattened to value-based** (`202e72a`): `return` now takes a
  value directly (`{ op: 'return', value: {...} }`) instead of schema-based state
  extraction. Removed the `__result__` wrapper and the nested `seq` blocks around
  returns, so `vm.run()`'s result is exactly the value you return — no envelope, no
  intermediate wrapping. This is the VM-return-flattening change; it landed in **0.2.0**
  (before 0.7.8) and was only recorded in the git log until this backfill.

[Unreleased]: https://github.com/tonioloewald/tjs-lang/compare/v0.14.0...HEAD
[0.14.0]: https://github.com/tonioloewald/tjs-lang/compare/v0.13.13...v0.14.0
[0.13.13]: https://github.com/tonioloewald/tjs-lang/compare/v0.13.12...v0.13.13
[0.13.12]: https://github.com/tonioloewald/tjs-lang/compare/v0.13.11...v0.13.12
[0.13.11]: https://github.com/tonioloewald/tjs-lang/compare/v0.13.10...v0.13.11
[0.13.10]: https://github.com/tonioloewald/tjs-lang/compare/v0.13.9...v0.13.10
[0.12.0]: https://github.com/tonioloewald/tjs-lang/compare/v0.11.0...v0.12.0
[0.11.0]: https://github.com/tonioloewald/tjs-lang/compare/v0.10.1...v0.11.0
[0.10.1]: https://github.com/tonioloewald/tjs-lang/compare/v0.10.0...v0.10.1
[0.10.0]: https://github.com/tonioloewald/tjs-lang/compare/v0.9.1...v0.10.0
[0.9.1]: https://github.com/tonioloewald/tjs-lang/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/tonioloewald/tjs-lang/compare/v0.8.7...v0.9.0
[0.8.7]: https://github.com/tonioloewald/tjs-lang/compare/v0.8.6...v0.8.7
[0.8.6]: https://github.com/tonioloewald/tjs-lang/compare/v0.8.5...v0.8.6
[0.8.5]: https://github.com/tonioloewald/tjs-lang/compare/v0.8.4...v0.8.5
[0.8.4]: https://github.com/tonioloewald/tjs-lang/compare/v0.8.2...v0.8.4
[0.8.2]: https://github.com/tonioloewald/tjs-lang/compare/v0.8.1...v0.8.2
[0.8.1]: https://github.com/tonioloewald/tjs-lang/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/tonioloewald/tjs-lang/compare/v0.7.8...v0.8.0
[0.7.8]: https://github.com/tonioloewald/tjs-lang/compare/v0.7.7...v0.7.8
[0.7.7]: https://github.com/tonioloewald/tjs-lang/compare/v0.7.6...v0.7.7
[0.7.6]: https://github.com/tonioloewald/tjs-lang/compare/v0.2.0...v0.7.6
[0.2.0]: https://github.com/tonioloewald/tjs-lang/releases/tag/v0.2.0
