# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed

- **Obligation walkers descend into fresh-scope bodies** ([#779](https://github.com/aallan/vera/issues/779), [#985](https://github.com/aallan/vera/issues/985)). The primitive-op and `@Nat`-binding walkers stopped at closure (`AnonFn`), quantifier (`forall`/`exists`), and handler (`handle`) boundaries, so a trapping op or a narrowing/widening binding inside one — `array_map(arr, fn { 10 / @Int.0 })`, an index in a `forall` predicate, a `let @Nat = @Int...` in a closure, a division in a handler clause — carried **no static obligation at all**: `vera verify --json`'s tier counts omitted runtime checks codegen actually emits, and even a manifest `5 / 0` in a closure body rode a verify-clean program. The walkers now recurse with scope-honest precision: a quantifier **domain** and a handler's **state-init and body** are enclosing-scope code and walk with the enclosing slot environment at full precision (a divisor there proves Tier-1 from the function's `requires`), while closure bodies, quantifier predicates, and handler clause bodies bind fresh slots and walk under an **empty** slot environment — a slot reference inside one never resolves onto an outer same-named slot (the false-Tier-1 hazard that kept the walkers shallow), so every slot-dependent obligation falls to the honest runtime-guarded Tier-3 leg while literal-only shapes still classify exactly (`5 / 0` is now a loud E526, matching direct position). The same descent closes #985: a closure nested inside another closure's body re-enters the `AnonFn` arm, so its `@Nat`→`@Int` return widening (`nat_to_int_coerce`) and `@Int`→`@Nat` return narrowing (`nat_bind`) are reported, matching the `_compile_lifted_closure` guards codegen was already emitting. The review round also closed a sibling gap the new coverage gate surfaced: the `@Nat`-binding walker did not descend `assert`/`assume` conditions (the primitive-op and calls walkers already did), so a provably-negative narrowing argument inside one — `assert(takes_nat(0 - 5) > 0)` — carried no obligation; it now fires the same loud E503 as direct position, and the walker carries the #597 `WALKER_COVERAGE` marker so the gate enforces its case split from now on. Across the 42 examples the corpus gains 22 obligations, all honest Tier-3 (across the full 218-program corpus: +67, the largest share `int_overflow`), with Tier-1 unchanged — the empty fresh-scope environment can neither prove a false Tier-1 nor lose an existing proof. Captured-array bounds inside closures remain Tier-3 pending the Tier-2 work in [#427](https://github.com/aallan/vera/issues/427). Both `KNOWN_ISSUES.md` limitation rows are retired. The review round's two discoveries are fixed in the same PR: **every handler write boundary into a `@Nat` state cell is now obligated and runtime-guarded** ([#1203](https://github.com/aallan/vera/issues/1203)) — the state-init (enclosing-scope precision: a refutable narrowing is a loud E503, a `requires` proves it Tier-1), the builtin `put` argument and the `resume` value (side-table-driven, matching the codegen guards added at the put store, the `with` override, and the get-resume result), and the `with` state update (fresh clause scope, Tier-3) — where previously a verify-clean program stored `-7` through a `State<Nat>` cell with zero obligations; and **an array-typed quantifier domain is now a check-time E128** ([#1204](https://github.com/aallan/vera/issues/1204)) — the spec's `@BoundExpr` is an integer count, and the array form previously type-checked then died at codegen with a raw WASM translation error (check-green ⇒ compilable restored; new negative conformance program). The fold-in's own adversarial pass then surfaced — and this PR also fixes — three more defects in the same handler machinery: **scalar type aliases as `State<T>` / `Exn<E>` compiled to invalid WASM** ([#1205](https://github.com/aallan/vera/issues/1205)) — the host-import/tag family NAME never resolved the alias while its WASM type did (`state_put_Count` carried i64 values into i32-typed uses; a refined alias `type Pos = { @Int | ... }` likewise), so the family now collapses scalar aliases into the base family every host binding already provides (never minting a new import name; composite names stay opaque per the #914 full-name invariant), clause scopes bind slots under the SOURCE pattern/annotation names (fixing the alias-equal annotation's dangling-slot E699 and the `old(State<Count>)` postcondition, whose comparison typed its operands off the unresolved name), and every #1203 boundary guard and obligation keys through the alias; **a handler state declaration diverging from the `State<T>` cell type is now a loud E336** ([#1206](https://github.com/aallan/vera/issues/1206)) — `(@Int = ...)` on `handle[State<Nat>]` type-checked while the annotation lied about the cell the obligations and guards key off; structural equality of the RESOLVED types is the test (`is_subtype` is deliberately blind here — `Int <: Nat` holds both ways and refinements erase to their bases), so aliases of `T` stay accepted, `Int`-for-`Nat` and refinement-decorated annotations are rejected, and a TypeVar cell defers to instantiation (the E128 lesson); and **a stateless handler's clause scope skewed every slot binding** — codegen unconditionally captured the pre-store cell as the last-bound clause slot where the checker (with no state declaration) binds the op ARGUMENT, silently wrong values wherever the types align. New conformance programs: the E336 negative and a run-level alias-cell positive (suite now 179). A second adversarial pass over the fold-in then drove a hardening round, all in this PR: **parameterised aliases resolving to scalars no longer split the family** (`type Id<T> = T` at `State<Id<Nat>>`, an alias of `Id<Nat>`, and `Exn<Id<Int>>` were all still invalid WASM — the family now resolves through a TypeExpr-level walk with parameterised substitution shared by registration and lowering); **handler clause scopes now mirror the checker exactly** — clause slot names canonicalize type-argument alias spellings the way the checker's binding rule does (mixed spellings were silent wrong values or dangling E699s on check-green programs), a patternless `put()`/`throw()` clause binds nothing (codegen previously pushed the argument/payload anyway, silently rebinding same-typed references), and clause bodies compile against the **handler-declaration scope** rather than the op call-site's (a clause reference past its own bindings silently re-resolved against bindings the handled body made before the op call); **`State<Byte>` cells work for direct literals** — the family imports were correctly i32 all along, but an int literal at a write boundary (init, either put dispatch path, the `with` update — whose expression now type-checks with the declared state type expected, fixing the coercion inconsistency that had made byte literals E335 there — and the get-clause resume) emitted the default `i64.const` into them; a Byte literal inside an `if`/`match` branch remains the pre-existing #865 class shared with the `let` arm ([#1212](https://github.com/aallan/vera/issues/1212)); **`handle[State]`/`handle[State<A, B>]` arity is a check-time E337** (previously the type-param zip truncated silently and codegen died later); **a refined-vs-refined state-declaration divergence is rejected** — `types_equal` compares refined types by base only, so `(@{> 3} = ...)` on `State<{< 10}>` passed while the honest plain spelling was rejected; predicates now compare structurally (span-insensitive), shared between the checker gate and **the verifier's new per-instantiation recheck (E533)**: a generic handler's concrete state declaration deferred E336 at the generic site (TypeVar cell) and nothing re-checked it once `T` went concrete — the monomorphized-clone walk now records a violated `state_decl` obligation naming the failing instantiation. E336's fix text and the spec/SKILL sentences steer refined cells to a **named** refinement alias (an inline refinement literal in the `State<T>` argument is not compilable). Three pre-existing discoveries are tracked: [#1207](https://github.com/aallan/vera/issues/1207) (mono-discovery vs effect-op naming desync, loud E602), [#1208](https://github.com/aallan/vera/issues/1208) (the global slot-naming alias-argument split outside clause scopes, loud E699), [#1209](https://github.com/aallan/vera/issues/1209) (composite `State<Alias>` keys a distinct cell family — a design question). A third adversarial round on the hardening itself then drove a final fix set: the alias resolver resolves **arguments before heads** (a seen-set head-follow truncated `Id<Id<Nat>>`-style finite expansions one level short — a silent handler bypass where a callee's differently-spelled ops landed in an unmanaged cell, and the same invalid-WASM family split reachable via an innocent wrapper alias); slot-reference resolution tries the **checker-canonical rendering first** (a parameterised-alias ref spelling now finds its own clause binding; source-keyed parameter bindings still hit via the opaque fallback); a REFINED-resolving clause argument keeps its **source spelling** (the checker-mirrored predicate-elided key was unreachable by every writable reference, turning working refined-arg handlers into dangling E699s); **`State.put(...)`/`State.get(...)` — the qualified spellings — delegate to the unqualified dispatcher** (they previously skipped the clause `with` transform, stored negatives into `@Nat` cells silently, and emitted Byte literals at i64); the get-clause resume obligation is gated on the **builtin** State effect (a user effect declaring `get` no longer borrows the cell type or claims a guard promise); a `resume(...)` inside a `with` expression is a **loud E602 skip** instead of silently ignored; the crafted E533 diagnostic now **survives per-instance aggregation** (obligation and diagnostic share one anchor — the fallback's generic assert/requires guidance could never fix a type divergence); and the differential test harness **asserts check-cleanliness** (a reviewer caught a fixture validating a check-rejected program). Three more pre-existing discoveries tracked: [#1210](https://github.com/aallan/vera/issues/1210) (State/Exn registration never walks clause bodies, init expressions, or i32_pair cells — loud unknown-func/tag at compile), [#1211](https://github.com/aallan/vera/issues/1211) (a bare op in a NESTED handler's clause body: checker targets the outer cell, codegen the inner — silent wrong value needing a spec-level alignment decision), [#1212](https://github.com/aallan/vera/issues/1212) (Byte literals in value-position joins, shared with `let`). A fifth adversarial round against the round-4 code then forced a **principled retreat at the reference layer**: canonical-first slot-reference resolution — global first, then scoped to clause bodies — is unsound BOTH ways (a canonical hit sees only the canonically-keyed subset of the checker's merged equivalence class, so it lands on the wrong member whenever any same-class binding is spelled differently: a clause-body `let`, an outer parameter, a nested handle body inheriting the scope flag), so references resolve by the opaque syntactic rendering ONLY — divergent spellings with no same-keyed sibling dangle loudly, and the pattern/annotation seam is gated in both directions (the general merged-class seam remains [#1208](https://github.com/aallan/vera/issues/1208)'s, which now documents the sibling-shadow shapes that stay silent) — the full bind+ref canonicalization is [#1208](https://github.com/aallan/vera/issues/1208)'s job by design. The two shapes that could still go silent got dedicated loud gates: a clause pattern and state annotation naming ONE checker class through two different aliases is a codegen skip with spell-both-with-one-alias guidance, and an alias chain past the resolver's depth bound is a loud per-function skip instead of an opaque fallback that would silently split the family. The round-4 qualified-State delegation gained the round-5 review's gate: it fires only when the dispatcher will actually resolve the op, so a user function shadowing `put`/`get` in a delegated context fails loudly at module compile (the pre-delegation behaviour) instead of silently dispatching the checker's builtin-op reading to the user function. The obligation walkers, family resolution, boundary guards, and all four fold-in fixes are unaffected — the retreat removes the one speculative layer the rounds proved unsound, and the round's probe corpus is captured under `tests/probes/` as the promotion pool for #1213. The closing CodeRabbit round then landed three more substantive fixes: `state_cell_decl_equal` compares refined predicates at EVERY depth (nested `Option<{@Int | P}>` divergence silently passed both E336 and E533 — the top-level-only check was blind inside ADT type arguments); the bare-`put` obligation fallback resolves WHICH effect's `put` a call targets through a handled-effect stack mirroring the checker's innermost-first rule (a pure fn handling a user effect named `put` previously picked up the builtin State's op via registration-order lookup, claimed its runtime guard, and labeled the site "State-op" — it now discloses honestly as an unguarded effect-operation argument); and the shared verifier test helper asserts fixture check-cleanliness, which immediately exposed twenty-three fixtures validating check-rejected programs (bare `random_int` calls predating the E217 gate, a fn named `e` colliding with the built-in constant, and an ill-typed generic-handler control) — all repaired, so every verifier test now pins behaviour on a well-typed premise. A final spot-check round on the two new functions then landed two refinements: `_refined_predicates_agree` recurses **FunctionType** parameters and returns (a refined predicate inside a fn-typed cell position compiled), and the handled-effect stack wraps the **body walk only**, mirroring the checker's own push scope — the wider push claimed the builtin State guard for a bare `put` in a nested handler's init or clause body that the checker types as the enclosing user effect's op (the disclosure now reads `effect-operation argument`, unguarded, exactly as the checker binds it). Two more pre-existing discoveries tracked: [#1214](https://github.com/aallan/vera/issues/1214) (a Unit-literal call argument weakens the verifier's call summary — provable violations demote to Tier-3) and [#1215](https://github.com/aallan/vera/issues/1215) (bare effect-op resolution is hash-seed-dependent when a fn's effect row holds two effects declaring one op name — the op-name sibling of the fixed State-type-mapping determinism).
- **A call precondition at or after a `let`-destructure is statically checked again** ([#764](https://github.com/aallan/vera/issues/764)). `_translate_block` returned `None` at the first `LetDestruct`, truncating SMT translation of everything from the destructure onward — a violating call verified `tier1`-clean with no `E501`, and a postcondition over the block's result demoted to the runtime tier (`E522`), with the callee's runtime `requires(...)` as the only backstop. The destructure is now modelled: each component binds to the RHS datatype's accessor term, leftmost-first to match the checker's De Bruijn order — so for `let Tuple<@Int, @Int> = ...` the slot `@Int.1` is the first (leftmost) component and `@Int.0` the second, the most recent binding — with the conservative bail kept for a source the SMT layer cannot project. Alongside it, the **builtin** tuple pseudo-constructor now translates in expression position — a literal `Tuple(5, 3)` was untranslatable everywhere (the #747 on-demand tuple-sort synthesis existed but was never wired to constructor calls) — guarded on the ADT registry, so a user-declared `data Tuple<A, B>` (legal; the codegen twin of the collision is the FIX-3 discrimination) keeps the registry-backed constructor path and its never-newly-enables reuse posture, pinned by an isomorphic-rename differential. The same review round's probing surfaced and fixed a pre-existing false E500 in the neighbouring builtin-tuple *match* path ([#1201](https://github.com/aallan/vera/issues/1201)): the builtin carrier has no registry constructor entry, so a match over a `Tuple<Nat, Nat>` parameter bound its `@Nat` components with no source fact — a valid ensures over a component was reported **violated** (while the isomorphic user-`data` twin proved), and the dual direction — an `Int` component bound as `@Nat` — was silently unobligated. `_instantiated_field_types` now resolves the builtin carrier's field types from the scrutinee's tuple component types, so the components carry their declared facts and a genuine narrowing fires a loud per-component `E503`, exactly like a registry ADT's fields. The synthesis wiring also makes an `if`-expression over tuple literals projectable, so an unprovable `@Int`→`@Nat` destructure narrowing from that shape now fires a loud per-component `E503` instead of falling silently to a guarded Tier-3 disclosure. The other half of the truncation is closed too ([#1199](https://github.com/aallan/vera/issues/1199)): a `let` whose value the SMT layer cannot translate — an effect-op result such as `IO.read(...)` or `random_int(...)` — now binds a fresh span-keyed opaque constant of the value's recorded type and translation continues, so a later call's `E501` is checked against it (unprovable fires, matching the posture an opaque *function* result already had; an `assert`/`assume` on the value repairs the proof via the #804 fact threading, and two effect-op lets bind distinct constants so they are never provably equal). The same opaque path covers an unprojectable destructure source (a tuple of effect-op results). Classification is taint-aware: a postcondition or refined-return proof that *fails* over a model containing an opaque constant demotes to the Tier-3 runtime check (`E522` / the guarded `refine_bind` leg) instead of claiming a definite violation — a countermodel over an unconstrained stand-in refutes nothing the effect actually produces, and without this gate eleven conformance programs and two examples flipped to false `E500`s. Preconditions stay strict: establishing them is the caller's obligation. The `KNOWN_ISSUES.md` limitation row is retired.

## [0.1.9] - 2026-08-04

### Added

- **`scripts/check_doc_counts.py` pins FAQ.md's headline test count.** The "by the numbers" line in FAQ.md carried a total-test figure no gate checked — only the conformance half of the sentence was pinned — and it drifted silently through two releases before being caught by hand both times. The oracle now reads the number the same way it reads README.md's status row, so the next drift fails pre-commit and CI instead of shipping.

### Fixed

- **`vera run` no longer executes a different function when the entry was dropped** ([#1183](https://github.com/aallan/vera/issues/1183)). When the `[E620]` skip propagation dropped `main` — or an explicit `--fn` target — and any public sibling survived, `execute()` fell through to `result.exports[0]`: the sibling's body ran, its result printed, exit 0, nothing on stderr. A regression of the #1178 review class, and the one outcome the loud-skip design exists to prevent, since the user could not tell the answer came from a different function. A dropped entry is now a refusal: `vera run` exits nonzero and names both the requested function and the root `[E602]`/`[E620]` diagnostic that removed it (`--json` reports `ok: false` with the same text). Auto-selection survives only for the never-declared case — no `main` anywhere in the source — and announces itself with a one-line `Note:` on stderr naming the function it picked, so the choice is never invisible. The `Compilation notes:` block is no longer gated on an empty export list; it prints on every run that has skip/drop diagnostics, which is exactly the case (a surviving sibling) where the user was least likely to notice something went missing. The same review's sibling surfaces are closed alongside: `vera compile` exits nonzero when a program declares a public non-generic function and the module ends up exporting nothing (a file of private helpers, or a cross-module generic library, still compiles clean — neither has an entry point to lose), and `vera compile --target browser` refuses to write a bundle without a `main` export — whether `main` was declared and dropped (the refusal quotes the E620 chain) or never declared at all (it names what is exported instead) — since the generated `index.html` calls `main()` on load either way. `CompileResult` gains `dropped_fns`, mapping each dropped user function to the diagnostic that explains it, so the refusal quotes the root cause rather than re-deriving it.

- **`[E602]` diagnostics for imported function bodies locate in their own module** ([#1186](https://github.com/aallan/vera/issues/1186)). The root skip for a function compiled through the Pass 2.5 / 2.6 import doors carried the MAIN file's path with the MODULE's line and column, so the rendered source line quoted whatever happened to sit at that line in the importer — a stray `}` in the reported repro. It also kept a branch of `_drop_dangling_callers` permanently dark: the `[E620]` caller message prefixes the root location with its file when the root came from elsewhere, but the comparison was against a path that always matched. Imported bodies now compile under the module's own file and source, so the coordinates and the file agree and a cross-file drop reads `… (see the [E602] warning at path/to/module.vera, line 5, column 5)`. Relatedly, `vera test` reported a PUBLIC function that codegen had dropped as `not exported (private)` — advice to fix a visibility modifier that was already correct, behind a `# pragma: no cover` claiming the branch only saw private functions. It now names the actual `[E602]`/`[E620]` root and where to find it.

- **Runtime traps inside imported functions name the module's file** ([#1189](https://github.com/aallan/vera/issues/1189)). `fn_source_map` — the table `vera run` resolves a wasmtime trap frame against — was populated by `_register_fn`, which stamps every entry with the file the generator was constructed for, and that pass runs before the per-module source scope is entered. An imported non-generic function never reached the main generator's registration at all (Pass 0.5 registers module declarations into a throwaway generator), so its frame printed `in scaled  (<unknown>)`; the `mod$…` emission of a locally-shadowed import fared the same way, since the resolver's rightmost-`$` strip yields a base that is nobody's entry. A monomorphized clone of an imported generic *was* registered, and so came out worse: the importer's path paired with the module's line range, coordinates that in the reported repro named a real-but-unrelated function in the importer — a backtrace that reads as correct and is not. The Pass-0.5 registrar is now given the module's own file, its source map is harvested (and mirrored onto the mangled name for a shadowed import), and clone registration runs under the same `_module_source_scope` Pass 2.5/2.6 already use, so registration and emission agree on which file a body belongs to. Only the file component moves; the line and column were already module-local, and main-file entries are unchanged. This completes the class [#1186](https://github.com/aallan/vera/issues/1186) opened: PR [#1190](https://github.com/aallan/vera/pull/1190) fixed the `[E602]`/`[E620]` diagnostic locations, this fixes the source maps behind runtime traps.

- **`old(...)` and `new(...)` applied to an expression report a dedicated diagnostic** ([#1173](https://github.com/aallan/vera/issues/1173)), `[E030]` and `[E031]`, instead of a generic `[E005]` unexpected-token error. Vera's `old`/`new` take an *effect* reference — `old(State<Int>)`, spec §7.9.2 — so a model reaching for Dafny's `old(<expression>)` wrote `requires(old(@Int.0) > 0)` and got a caret on the `@` inside the argument, "Expected one of: UPPER_IDENT", boilerplate fix text about missing delimiters, and a pointer to the formal-grammar chapter. Nothing named `old`, and nothing said what its argument has to be. Found by the VeraBench v0.0.18 sweep (`VB-T5-009`).

  The caret now lands on `old`/`new` itself. The message names the construct, states that the argument is an effect reference and that the call belongs in an `ensures()` clause, and the rationale gives the reason both rules exist: Vera has no mutable variables, so a slot holds one value for the whole call and effect state is the only thing a call can change. The fix shows both repairs — drop the wrapper for a parameter's value, or name the effect inside `ensures()`.

  The diagnostic is raised at parse time, where the failure occurs. Letting the grammar accept `old(<expression>)` and rejecting it in the checker is not available: `old_expr: "old" "(" expr ")"` stops `old(State<Int>)` parsing at all (the `<` reads as a comparison), and carrying both alternatives is a reduce/reduce collision between `effect_ref` and `fn_call` on `UPPER_IDENT`. The detector fires only when the parse failed on the *first* token of an `old(`/`new(` argument, so `old(State<Int> > 0)` — whose real fault is the missing `)` — still reports `[E005]` rather than being blamed on `old`.

- **`[E174]` and `[E175]` explain why a precondition cannot host `old()`/`new()`** ([#1173](https://github.com/aallan/vera/issues/1173)). The rationale now says that a `requires()` or `decreases()` clause is itself evaluated before the body runs, so every expression in it already observes the pre-state and the after-state `new()` names does not yet exist. The fix adds the fact that closes off the obvious retry: a precondition cannot constrain effect state at all, because contract predicates must be pure and `old()`/`new()` are the only contract forms that name state.
- **Redeclaring a built-in effect is now a compile error, `E152`** ([#1149](https://github.com/aallan/vera/issues/1149)). An `effect IO { ... }` block — or `State<T>`, `Exn<E>`, `Http`, `Random`, `Inference`, `DB`, `Diverge`, `Async`, `HttpServer` — used to override the built-in at checker level, which spec §9.5.1 sanctioned "for backward compatibility". Code generation never honoured it: a qualified `IO.print(...)` is lowered to the fixed host import selected by the *qualifier name*, and the declaration is never read. So a block whose operation signature diverged from the built-in (`op print(String, String -> Unit)`, `op query(String -> ...)`) passed both `vera check` and `vera compile` with exit 0 and no diagnostic, then trapped at `vera run` on structurally invalid WebAssembly. An ordinary typo in the idiomatic-but-optional declaration reached it.

  The gate is name-keyed and unconditional — a faithful redeclaration is rejected too, because it is a second textual spelling of the same program (DESIGN.md, spec §0.2 design goal 3, one canonical form). It is the sibling of `E151` for built-in functions, and it reads the name set from the live effect registry rather than a hand-list, so a future built-in effect is covered the moment it is registered; a differential test pins that set equal to what `vera effects --json` publishes. The rejected block is not registered, so the built-in stays canonical and call sites resolve against it instead of cascading arity errors. A module redeclaring a built-in effect surfaces `E152` into its importer, as `E151` already did.

  `Exn<E>` becomes a real entry in the effect registry as part of this. It was recognised only by code generation (`handle[Exn<E>]`), so `handle` and `throw` needed the very `effect Exn<E> { op throw(E -> Never); }` block the new rule forbids; it is now in scope with no declaration, like `IO` and `State<T>`. `vera effects --json` reports its `type_params` as `["E"]`, matching spec §7.7.2, where the hand-written entry it replaces said `["T"]`.

  Spec §9.5.1 is amended: the backward-compatibility override sentence is deleted and the strict rule stated in its place. §7.7 opens with the rule as one blanket statement over the whole registered set — including the operationless markers `Diverge`, `HttpServer` and `Async`, where having nothing to declare does not make the block legal — rather than a per-effect sentence or a name list that would drift from the registry. §7.2 gains it as a numbered constraint on effect declarations and its examples move to user-defined effect names; §7.7 and §9.5 present each built-in's operations as a table rather than in declaration form, since that form is no longer writable. The `effect IO {}` / `effect Exn<E> {}` blocks are removed from 12 examples and 20 conformance programs, and new conformance program `ch09_builtin_effect_redefinition_rejected` (170, was 169) pins the divergent-`print` repro as an `E152` negative. The #309 SQL literal-provenance gate is untouched: it still keys on the codegen routing axis rather than built-in `OpInfo` identity, and its tests now assert that `E207` fires alongside `E152` on a user `effect DB` shadow — defence in depth behind the new rule, not a replacement for it.

- **`fn old` / `fn new` declarations are rejected at the declaration site, `E153`** ([#1181](https://github.com/aallan/vera/issues/1181)). The grammar reserves `old(` and `new(` in expression position for the contract state forms — `old_expr` / `new_expr` in `vera/grammar.lark`, each of which demands an effect reference — so a bare call `old(5)` is always read as a malformed state reference (`[E030]`/`[E031]`, [#1173](https://github.com/aallan/vera/issues/1173)) and never resolves to a function — not in the declaring file, and not inside the declaring module either. One route did reach such a function: a module-qualified `mod::old(...)` parses through the module-call rule, so a module export named `old` was callable cross-module (and only cross-module) — adversarial review of the fix confirmed the shape checked and ran. The declaration is now refused outright, reserving the whole identifier rather than leaving it a trap in every unqualified position, the sibling of `E151` (built-in functions) and `E152` (built-in effects) under the same one-canonical-form rule. **Breaking**: a module export named `old` or `new` that was called via the qualified route must be renamed. The gate covers top-level and `private` functions, generic `forall<T>` functions, `where`-helpers (called in expression position exactly like top-level functions), and modules — a module declaring `fn old` surfaces `E153` into its importer, as `E151` and `E152` already do. The reservation is on the whole identifier, so `older` / `renew` / `news` stay legal.

  This half of the gate covers the two contract state forms; the keyword class the contextual lexer also admits as a function name is reserved separately, by [#1187](https://github.com/aallan/vera/issues/1187) below. The probe record lives in the test docstrings. Spec §5.2 states the rule; new conformance program `ch05_reserved_fn_name_rejected` (172, was 171) pins it as an `E153` negative. Mutation-validated: emptying the reserved set, dropping the `where`-helper recursion, dropping the module surfacing, and matching on prefix rather than whole identifier each flip their targeted tests RED.

- **A function named after a grammar keyword is rejected at the declaration site, `E153`** ([#1187](https://github.com/aallan/vera/issues/1187)). Lark's contextual lexer re-lexes `assert`, `assume`, `forall`, `exists`, `match`, `if`, `let`, `fn`, `true` and `false` as ordinary identifiers after `fn`, so each declares cleanly — and none can be written in expression position, where the spelling is always the keyword: a bare `match(3)` does not parse at all (`[E005]`), and `assert(3)` / `assume(3)` are read as the statement forms and collide (`[E121]` plus `[E172]`/`[E173]`). Every one is a declarable trap, so the reservation refuses the mistake at its source rather than letting it surface as whichever call-site error the spelling happens to produce — the same one-canonical-form rule that already covers the contract state forms ([#1181](https://github.com/aallan/vera/issues/1181)), built-in functions (`E151`) and built-in effects (`E152`). **Breaking**: a module-qualified `mod::match(...)` parses through the module-call rule rather than any keyword rule, so a module export under one of these names was callable cross-module (and only cross-module) — probed on the pre-fix tree, the shape checked and ran. Such an export must be renamed; the breakage is loud and located at the module's declaration.

  `handle` is carved out and stays legal: `public fn handle(@Request -> @Response)` is the entry point the host invokes under `vera serve` and `wasi:http` (spec §9.5.6, `examples/http_server.vera`), so being uncallable from Vera source does not make it dead code. It lives in a named `_HOST_INVOKED_FN_NAMES` set subtracted from the reservation, so a future host-invoked entry point joins it deliberately rather than by editing a flat list. The `E153` rationale branches with the reason — a keyword is not described as a contract state form — while the fix stays "rename" on both. The gate inherits the #1181 shape: top-level, `private` and generic `forall<T>` functions, `where`-helpers, and modules (a module declaring `fn match` surfaces `E153` into its importer, carrying the module's own file path). Matching is on the whole identifier, so `matched` / `letter` / `iffy` stay legal, and `op <keyword>(...)` inside an `effect` block never reaches the gate — the lexer refuses that spelling at parse (`[E005]`), pinned so a grammar change that admits it shows up as a failure to widen. Spec §5.2 states both halves of the rule; new conformance program `ch05_reserved_keyword_fn_rejected` (176, was 175) pins it as an `E153` negative. Mutation-validated: emptying the keyword set flips the ten keyword tests RED with the `old`/`new` tests still green, and emptying the carve-out flips the `handle` control RED (and breaks `examples/http_server.vera` and `ch09_http_server`).

- **`Vera`-prefixed type names are reserved for the prelude, `E154`** ([#1184](https://github.com/aallan/vera/issues/1184) review). The prelude's combinators resolve their parameter types through generated declarations in that namespace (`VeraOptionMapFn`; type parameters `VeraA`/`VeraB`, #869), and `inject_prelude` skips any of its declarations whose name a user program already spells — so `type VeraOptionMapFn = Int;` silently re-typed the prelude's own signatures: check-green, then a WebAssembly validation failure at run. Declaring a type or alias whose name begins with `Vera` plus an uppercase letter or digit is now refused at the declaration, with module declarations surfacing the error into their importer as the E151/E152/E153 family does. Ordinary names merely containing the letters (`Veranda`, `MyVeraThing`) are unaffected, and shadowing the *unprefixed* prelude aliases (`OptionMapFn`) remains legal. New conformance program `ch08_reserved_vera_prefix_rejected` (175, was 174) pins the rule.

- **`scripts/check_site_assets.py` gates the facts `docs/index.html` and `docs/index.md` both state** ([#1154](https://github.com/aallan/vera/issues/1154)). The Markdown companion agents fetch via `rel="alternate"` and `llms.txt` is emitted by `build_index_md()` in `scripts/build_site.py`, which holds the landing page's substance as a hand-maintained f-string. The staleness check called that generator and compared the result against the committed file — the generator on both sides of the comparison — so a generator that missed an edit to the hand-designed HTML produced a committed asset stale in exactly the same way, and the gate passed. A v0.0.7-era benchmark section survived every intervening release that way.

  `check_fact_coherence()` now extracts the load-bearing facts from each file independently and fails when they diverge: the VeraBench version and the Vera release it was measured against, the landing-page version badge, the problem/tier/model/provider counts, the headline perfect-Vera count, the nine-row results table (model name, tier and all three figures per row, plus each file's row count against its own stated model count), and the three editor names. An error names the fact, both values and both paths. A fact that cannot be located is itself a failure rather than a silent skip, so a reworded sentence cannot switch its own check off.
- **Type aliases are module-local in codegen, matching spec §8.4.1** ([#1111](https://github.com/aallan/vera/issues/1111)). Codegen merged every imported module's type aliases into one flat bare-name map (`setdefault`, first module won) and let the main file's aliases overwrite it, so two modules legally reusing one alias name for different targets — or a main-file alias sharing a module's alias name — re-typed one side's declarations through the other's namespace: wrong WASM signatures, invalid modules at `run`, an import-order-dependent victim, and (where representations coincide) silently wrong values. Each module's aliases are now captured in a per-module namespace and its declarations — Pass 2.5/2.6 bodies and monomorphized clones of its generics alike — compile and register under `{prelude, module's own}`; harvested return types are canonicalized against the defining module's maps before entering the shared registries, so no consumer can re-resolve them against the wrong namespace.
- **A user type alias named after one of the prelude's fn-type aliases no longer disables the combinator it names** ([#1184](https://github.com/aallan/vera/issues/1184)). The prelude spells its closure-taking combinators' parameters through type aliases — `option_map(@Option<VeraA>, @OptionMapFn<VeraA, VeraB>)` — because a slot reference needs a type *name*, and those names were injected into the same namespace user code declares into. So a `type OptionMapFn = Int;` (or `OptionBindFn`, or `ResultMapFn`) re-typed the *prelude's* declaration: the mirror of the [#1111](https://github.com/aallan/vera/issues/1111) defect spec §8.4.1 forbids in the other direction. The two namespaces then disagreed about it, and neither was right. In the main file the user's alias won the flat maps outright, so `option_map` was emitted with an `Int` (i64) parameter where the call site passes a closure funcref, and `vera run` died at WASM validation. In an imported module the same collision was silent: Pass-1.5 instantiation discovery ran under the flat maps and created `option_map$Int_JInt`, while the module body — compiled under `{prelude, module's own}`, where the module's alias won — bound the combinator's return type parameter to the phantom-var default and called `option_map$Int_JBool`. A missing call target is an `[E602]` *warning*, so the function was skipped and its caller dropped, and `check`, `verify` and `compile` all reported success over a program with empty exports.

  The prelude's own combinators now resolve through reserved `Vera`-prefixed twins of those aliases (`VeraOptionMapFn`, `VeraOptionBindFn`, `VeraResultMapFn`), derived mechanically from the public declarations rather than restated, and injected with the bodies that need them rather than with the user-facing block a program can suppress. This is [#869](https://github.com/aallan/vera/issues/869)'s remedy — reserved names no ordinary user declaration spells, keeping prelude internals invisible to user namespace decisions — applied to the alias names rather than the type-parameter names. The user-facing `OptionMapFn`, `OptionBindFn`, `ResultMapFn`, `ArrayMapFn`, `ArrayFilterFn` and `ArrayFoldFn` names stay injected and stay the user's to shadow: a colliding alias keeps meaning exactly what the user wrote, in both namespaces, and the combinator keeps working alongside it. The per-module alias scope also overlays its two maps as a pair in the same change, so a module alias shadowing a parameterized prelude alias with a non-parameterized one can no longer inherit the prelude's stale type-parameter list. Spec §8.4.1 states the rule.

- **`decreases` clauses are enforced at run time — E525's promise is no longer empty** ([#1172](https://github.com/aallan/vera/issues/1172)). A Tier-3 termination obligation warned that the metric "will be checked at runtime", but `decreases` had no runtime lowering at all: a non-terminating recursion passed `check`, passed `verify`, and hung at `run` (found by the VeraBench v0.0.18 sweep, `VB-T4-006`). A function with a `decreases` clause now carries an entry guard (whenever the backend can express its measure — the honest limits below): on re-entry, the measure — scalars by value, ADT measures by structural size (a generated `$dec_size_<T>` helper), lexicographic tuples componentwise per spec §5.6.1 — must be strictly less than the previous activation's and non-negative, or the program traps through the contract-violation channel with a message naming the function. Guard state is per function and restored at every exit, and tail-call optimization is preserved for self-recursion — a self-recursive `return_call` carries a call-site check (arguments captured, the measure evaluated over them against the live chain state, the activation's guard state closed out before transfer), so the documented pure-iteration idiom keeps its constant-stack depth (#517's 1M-iteration property is regression-tested); only mutual-tail recursion between guarded functions falls back to plain calls, also closing [#1176](https://github.com/aallan/vera/issues/1176) in the same change. Alongside: the checker now rejects measures with no well-founded ordering (`Float64`/`String`/`Bool` and friends, new `E127`, previously accepted silently as decorative), and spec §5.6.1's own lexicographic Ackermann example is corrected — as printed its permuted arguments made it non-terminating, exactly the class its `decreases` clause was meant to rule out. The guard also caught `examples/gc_pressure.vera` declaring its accumulator as the measure (it grows every hop) — corrected to the counter. A measure of a *parameterized* ADT type is not yet runtime-ranked (the registered generic layout does not describe concrete construction) and stays honestly Tier-3-disclosed, as does any function declaring `Exn` — an unwinding `throw` would bypass the exit restores and leave a stale baseline that traps a later terminating call — and any measure the backend cannot translate. An independent adversarial review then hardened the tier contract: tail calls from a guarded function to an unguarded one are demoted to plain calls (an unguarded trampoline could re-enter with the chain zeroed and loop unchecked), and the verifier's decreases call-walker now descends into handler clauses, closure bodies, constructor/qualified/module-call arguments, index expressions, array literals, interpolation, and assert/assume — a measure-violating recursive call hidden there previously rode a false Tier-1 proof that the new guard exposed as a verify-green run-time trap.

- **A codegen skip now propagates to its (transitive) callers instead of surfacing a raw wasmtime error** ([#1100](https://github.com/aallan/vera/issues/1100)).  An `[E602]`-class skip drops a function from the emitted module, but every caller's `call $f` / `return_call $f` was still emitted, so a check- and verify-clean program whose skipped construct sat in a *called* helper failed at `compile`/`run` with `WAT compilation failed: unknown func: failed to find name $f` — loud and never a wrong answer, but a WAT internals dump instead of a Vera diagnostic (found in the [#1098](https://github.com/aallan/vera/issues/1098) adversarial review, whose negative test sidestepped it by putting the skipped construct directly in `main`).  A new pre-assembly pass (`_drop_dangling_callers`, `vera/codegen/core.py`) now walks the emitted WAT — the exact symbol stream wasmtime resolves, so mono-mangled (`f$Int`), module-qualified (`mod$f`), and where-helper call targets are matched without re-deriving any renaming logic — and drops the whole doomed caller subgraph to a fixed point: each dropped caller gets its own **new `[E620]` warning** ("Caller of a skipped function", registered in `vera errors`) naming the ROOT skipped function and its skip location plus the direct call edge (`calls function 'mid', which was dropped because function 'sunk' was skipped … at line 2, column 34`), so the module always assembles and the user is pointed at the construct to fix rather than at generated WAT (DESIGN.md principle 1).  Propagation follows the same graceful-degradation semantics as the root `[E602]` skip itself (spec §11.4.1): warnings, not errors — a caller-of-skipped program now behaves exactly like the skipped-construct-directly-in-`main` shape always did (clean compile, the subgraph absent from the exports, `vera run` reporting the missing export with the explanatory notes).  Lifted closures participate on both sides: a closure body holding the only `call $skipped` dooms its parent through an explicit construction edge (the parent's WAT carries only a function-table index, invisible to a symbol scan), and a doomed closure's body is replaced by an `unreachable` stub rather than removed, because later closures' `closure_id` ↔ table-index correspondence depends on every earlier `(elem …)` slot staying occupied.  Functions outside the doomed subgraph — including their exports — are untouched, and call-graph cycles (mutual recursion) terminate at the fixed point.  Pinned by `tests/test_codegen_skip_propagation_1100.py` (the repro, depth-2 transitive drops in BOTH declaration-order permutations so a single-sweep propagation goes RED, an untouched-sibling run, mutual-recursion termination, the closure shape, root-cause naming with location, and a no-callers control), with four mutations — single-sweep, dropped closure edge, dropped stubbing, hop-instead-of-root threading — each killed by a named test; the CLI legs (`tests/test_cli.py`) pin the clean text and JSON envelopes, and the #1004 codegen-error-path warning flush keeps coverage via a typed-hole fixture, since the original dangling-caller fixture now takes the clean-drop path.

- **A `call_indirect` is never emitted without a function table to dispatch on** ([#1185](https://github.com/aallan/vera/issues/1185)).  The unclosed half of the [#1100](https://github.com/aallan/vera/issues/1100) class: an indirect call names no symbol, so the caller-drop pass — which scans the emitted WAT for `call $f` — could not see it.  When an `[E602]` skip swallowed a module's **only** closure, the lift rolled back and module assembly suppressed the `(table)`/`(elem)` sections, but every surviving carrier kept its `call_indirect` into a table that no longer existed.  The result was an *uninstantiable* module emitted with zero error diagnostics: running any unrelated export — a function with nothing to do with closures — raised a raw `WasmtimeError: … unknown table 0: table index out of bounds`.  Two emission sites reproduce it, since a carrier need hold no closure of its own: the `apply_fn` special form, which lowers a closure-typed parameter to `call_indirect` unconditionally, and a monomorphized clone of a prelude combinator such as `option_map`.  The drop now propagates to the carriers exactly as it does to ordinary callers — with no table in the module a carrier's indirect call can only have targeted a dropped closure, so the carrier is seeded into the same fixed point and dropped with its own `[E620]` naming the `[E602]` root that emptied the table, and its own callers drop transitively behind it.  Emitting an empty table instead was rejected: an instantiable module whose `call_indirect` traps at call time with an opaque wasmtime error is strictly worse than a located refusal (DESIGN.md principle 1, fail loud).  A program that applies a closure-typed parameter while never writing a closure at all — no skip, no `[E602]`, previously *no diagnostic of any kind* — is the same absent table and drops the same way, with an `[E620]` that explains the absence on its own terms.  The invariant is enforced as a differential over the two sides that must agree, the instruction stream and the table section: `tests/codegen_helpers.py::_assert_no_orphan_call_indirect` runs on **every** compile in the codegen suite, so a future desync between them cannot hide behind a green unit test the way this one did for the whole #1100 cycle.  #1100's acceptance helper is hardened alongside: it checked the diagnostics plus a non-empty `wasm_bytes` and never handed the module to wasmtime, which is precisely why it passed on this shape — it now loads the module, so the helper catches the class itself.

### Documentation

- **The VeraBench section carries the v0.0.18 sweep** ([#1169](https://github.com/aallan/vera/pull/1169)), the first in which all 60 problems are graded — v0.0.17 took the gradeable set from 36 to 46 and v0.0.18 closed it. One problem is now worth 1.7 percentage points rather than 2.8. Six of the nine models solve every Vera problem, and Vera is highest or level with it for six of the nine. Measured against [Vera v0.1.8](https://github.com/aallan/vera/releases/tag/v0.1.8). The section also gains the reading the wider gradeable set supports: Python is dynamically typed and TypeScript is not, Vera sits with TypeScript and goes further, and sorting the three by how much they constrain the model rather than by how much of them it has read puts the two constraining languages ahead — TypeScript with training data behind it, Vera without.

  Every figure was cross-checked cell by cell against [vera-bench#120](https://github.com/aallan/vera-bench/pull/120), the pending results rewrite in the benchmark repo; all 27 published cells agree, as do both headline counts. The landing page's numbers and the benchmark repo's are the same measurement, not two independent transcriptions of it.

  Propagated to every surface that carries the figures, which the HTML edit alone does not reach: `build_index_md()` in `scripts/build_site.py` — the generator that *is* `docs/index.md`, since that file is not derived from the HTML — plus `README.md` and `FAQ.md`, then `docs/index.md` and `docs/llms-full.txt` regenerated. This is the drift class [#1154](https://github.com/aallan/vera/issues/1154) describes: `check_site_assets.py` regenerates from the same function it compares against, so a stale generator validates as up to date and only a reader notices.

## [0.1.8] - 2026-07-27

### Added

- **Vim and Neovim support** ([#1155](https://github.com/aallan/vera/pull/1155), contributed by [@chromy](https://github.com/chromy)). A Vim 8+/Neovim package under `editors/vim-veralang/` — `ftdetect`, `ftplugin` and `syntax` — ported from the VS Code TextMate grammar. It registers the filetype as **`veralang`**, not `vera`: Vim has shipped an unrelated `vera` filetype since 2005 for the Synopsys hardware verification language, and because `$VIMRUNTIME` precedes `pack/*/start` in `runtimepath`, claiming that name would let the built-in syntax set `b:current_syntax` first and this plugin's own files would then exit silently through their own guard. It is the most current of the three editor integrations: it knows all ten effects in `vera effects --json`, where the VS Code and TextMate grammars are four behind. The remaining drift — those two grammars, and the `Eq`/`Hash`/`Ord`/`Show` abilities that no grammar knows — is tracked in [#1156](https://github.com/aallan/vera/issues/1156).

### Changed

- **The ruff rule set is declared explicitly instead of inherited, and five of the rules 0.16 turned on are adopted** ([#1166](https://github.com/aallan/vera/issues/1166)). The project had no `[tool.ruff]` section, so its lint policy was whatever ruff shipped as the default — meaning any ruff release could redefine the project's standards without review. 0.16.0 did exactly that, replacing the default selection wholesale (isort, pylint, simplify, blind-except, pyupgrade, pyi, perflint, pie, tryceratops, datetimez, refurb) and taking a clean tree to 805 errors across 36 rules; the `<0.16` version cap had been suppressing the symptom. `E4`/`E7`/`E9`/`F` are ruff's pre-0.16 default, verified byte-identical against seeded violations under both 0.15.21 and 0.16.0, so declaring them preserves today's behaviour rather than narrowing it. A ruff upgrade is now a tool change rather than a policy change, and the version range widens to `<0.17`.

  Adopted on top of that pin, each because it names a defect class the project already treats as real. Every site was triaged before the edit; none of the five was silenced with a blanket ignore:

  | Rule | Sites | What the sweep found |
  |---|---|---|
  | `PLW1510` | 107 | Every site turned out correct: each asserts `returncode` itself, inspects stdout for the CLI's `OK:` sentinel, or is a helper returning `CompletedProcess` for a caller to inspect. The value was the forced triage; `check=False` now records its result in the code. |
  | `RUF100` | 28 | Suppressions aimed at nothing — `N802`, `N815`, `N818`, `SIM117`, dead `E402` — which would have masked the next real violation on those lines. Two carried a real justification in their prose, kept as ordinary comments. |
  | `BLE001` | 32 | All deliberate boundaries: a host FFI call whose every failure must become a `Result.Err` value, an SMT projection falling back to Tier 3, a doc-example runner that reports a failure instead of dying. Narrowing any would turn a graceful fallback into a crash, so each states its reason instead. |
  | `RUF012` | 25 | Read-only lookup tables — operator maps in the WASM emitters, fixture tables on pytest classes — now `ClassVar`, which is both the documentation the rule asks for and something mypy enforces. |
  | `SIM115` | 13 | All the Windows fixture pattern `TESTING.md` mandates (`delete=False` + explicit close + `finally` unlink, because Windows cannot reopen a held file). No handle leaks; the rule's worth is that a *new* unclosed `open()` cannot be added silently. |

  Review turned two of the `BLE001` sites from justifications into narrowings, which is the better answer where it is available. `_parse_interp_expr` in `vera/transform.py` caught every exception around the synthesized wrapper's parse and reported it as "Invalid expression in string interpolation" — so a compiler bug anywhere inside `parse` blamed the user's source for something it did not cause. `parse` funnels every genuine syntax failure through `ParseError` (its own malformed-comment diagnostics included), verified against syntax errors, unterminated block comments, null bytes and 20,000-deep nesting, so that is the only thing worth converting and anything else now propagates as itself. The two `tests/test_browser.py` parity tests caught bare `Exception` and asserted only that *something* was raised, which an unrelated Python-side failure would have satisfied; they now catch `WasmTrapError` and pin `kind` to `contract_violation` and `overflow` respectively. `scripts/check_examples.py` gained the exit-code half of its success check — the same defect class `PLW1510` names, in a gate that had been reading only stdout.

  `S` is declared alongside them, because a command-line `--select` *replaces* the configured list rather than extending it: the security gate runs `ruff check --select S vera/`, so without `S` in the config `RUF100` would read `vera/`'s 57 security suppressions as aimed at nothing and strip them, failing the next CI run. Declaring it means `ruff check .` reproduces that gate instead of depending on how CI spells its arguments; `tests/` and `scripts/` opt out, as they were never in its scope. The remaining ~500 findings are style and modernisation — `I001` import ordering chief among them, which has no basis here while the project does not run `ruff format` — and [#1166](https://github.com/aallan/vera/issues/1166) records which were declined and why.

### Security

- **`brace-expansion` 5.0.7 → 5.0.8 in the VS Code extension**, closing [GHSA-mh99-v99m-4gvg](https://github.com/advisories/GHSA-mh99-v99m-4gvg) — a high-severity denial of service where an unbounded expansion length crashes the process out of memory. Affects `<= 5.0.7`; 5.0.8 is the first patched version. This is a **runtime** dependency, not build tooling: the chain is `vscode-languageclient` (a root `dependencies` entry) → `minimatch` → `brace-expansion`, and `esbuild.js` bundles with `external: ["vscode"]` alone, so everything else is carried into the shipped `dist/extension.js`. The published 0.2.0 extension therefore contains the vulnerable version, so the extension is bumped to **0.2.1** — `package.json`, both version fields in `package-lock.json`, and its own `CHANGELOG.md` — and the fix reaches users when that build is uploaded to the Marketplace, not when this merges. The 0.2.0 section is left describing what 0.2.0 actually was, vulnerable dependency included, rather than being retroactively corrected. Whether the vulnerable path is reachable from how the language client uses `minimatch` was not established either way, and the fix does not depend on the answer. The dependency's `node` engines range narrows with the bump (18 dropped), which changes nothing here — the extension targets VS Code `^1.91.0` and both CI workflows build on Node 22. Dependabot proposed the identical lockfile change as [#1167](https://github.com/aallan/vera/pull/1167), closed in favour of this PR after confirming the two produce a byte-identical `package-lock.json`.

### Fixed

- **Only `let` bindings can carry compile-time provenance, enforced rather than documented** ([#1164](https://github.com/aallan/vera/issues/1164)). `Binding.literal_str` (#309) and `array_len` (#1160) are populated solely for `let` bindings; that was a comment plus discipline at one call site. For `literal_str` it is the `E207` gate itself — probed by bypassing it and giving `param` bindings a literal value, after which `DB.execute(@String.0, [])` type-checks clean, i.e. the textbook injection is accepted. A `__post_init__` guard now rejects provenance on any non-`let` source, placed on the dataclass rather than in `TypeEnv.bind` because `vera/checker/control.py` constructs a `Binding` directly for match patterns and bypasses `bind` entirely. `ValueError`, not `assert`: a load-bearing guard must survive `-O`, and the `ruff --select S` lint rejects asserts used this way. Covered for every binding source the checker uses, with positive controls for the `""` and `0` edge values a truthiness-based guard would wrongly reject.

- **`E208` now follows a `let` chain, as `E207` already did** ([#1160](https://github.com/aallan/vera/issues/1160)). The SQL placeholder/parameter arity check only looked at the syntax *at the call site*, so moving a params array into a `let` for readability silently dropped a compile-time check — identical array, identical static size, only an indirection differs:

  ```vera
  DB.query("... a = ? AND b = ?", [Some(@String.0)])        -- E208
  let @Array<Option<String>> = [Some(@String.0)];
  DB.query("... a = ? AND b = ?", @Array<Option<String>>.0)  -- accepted
  ```

  `Binding` gains an eager `array_len` alongside `literal_str`, computed at the same moment and for the same reason — in the value's own scope, before `bind()` shifts slot indices — and a new `resolve_array_len` in `vera/checker/sql.py` reads it. A completeness fix, not a soundness one: the mismatch already failed at run time as `Result.Err`, so the change only converts a runtime failure into a compile-time one. Every unresolvable shape still returns `None` and defers to the driver; the resolver deliberately does *not* fold `array_concat` or any other builtin, since each extra shape is another way to compute a wrong length. Spec §9.5.7, SKILL.md and AGENTS.md describe the check in those terms; the `let` path is what they were missing, though the wording still reads wider than the check is — a block-wrapped or `if`-wrapped literal array is statically sized and deliberately defers. Review also found and fixed a second instance of the same bug class introduced by the first cut of this fix: the resolvers looked bindings up through the *syntactic* renderer in `vera/slots.py`, while `TypeEnv.bind` keys them through the checker's alias-**resolving** one, so a type alias inside a type argument (`@Array<Option<Txt>>` where `type Txt = String`) missed and silently deferred. Both resolvers now take the checker's `_slot_ref_key`, which is also what makes the ordinary parameterised case work at all: `SlotRef.type_name` is only the *base* name — `@Array<Option<String>>` is `"Array"` — so the bare lookup the first draft used matched no binding and silently resolved to `None`. Conformance `ch09_sql_placeholder_let_mismatch_rejected`.

### Documentation

- **Vera Language 0.2.0 is now available from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=veralang.vera-language)** ([#1106](https://github.com/aallan/vera/issues/1106)). The public `veralang.vera-language` package was downloaded and compared file-for-file with a fresh build from `main`, then installed successfully through VS Code's Marketplace CLI in a clean isolated profile. The installation docs now expose that verified route and distinguish the extension from the separate `veralang[lsp]` Python extra it launches for diagnostics, hover, slot navigation, and typed-hole completion.

- **The landing page's project facts are gated against the live codebase** ([#528](https://github.com/aallan/vera/issues/528)). `docs/index.html` states counts in prose — built-in functions, algebraic effects, spec chapters, conformance programs, worked examples — that drift silently as the codebase moves; two were stale before anyone noticed ("six algebraic effects" when there were seven, a 77-program suite when there were 80). `scripts/check_doc_counts.py` now checks each against its live source, and the page stays hand-edited rather than becoming a template, which is the convention for that file. Effects are checked as a count *and* a membership list, in **both** places the page enumerates them — the status paragraph and the reference card, a second hand-maintained mirror that had no gate at all. That matters because the historical drift was a name missing from a list rather than a wrong total, which a count-only check cannot see. A pattern matching nothing is itself an error, so a reworded sentence fails loudly instead of silently switching its own check off. The version string is left to `scripts/check_version_sync.py`, which already owns this file for it.

- **Every diagnostic code now reports the release that introduced it** ([#1157](https://github.com/aallan/vera/issues/1157)). `vera errors --json` carried a `since` field that was null for all 145 codes — deliberately, as attribution was judged high-effort. It is not: codes are stable identifiers the registry never renames, so the version derives mechanically from the release tags — for each tag, the `ERROR_CODES` keys in `git show <tag>:vera/errors.py`; a code's `since` is the first tag containing it. That is more reliable than the built-in attribution it sits beside, which had to trace names through the #288 rename pass by hand. The result is cross-checked against HISTORY.md's independent record of each release: the scan attributes exactly `E207`/`E208`/`E209`/`E217` to v0.1.7 and the `E02x` comment diagnostics to v0.1.6, matching what those rows say without having read them. 80 codes date to v0.0.43, which is not a floor — that release introduced error codes at all, and no `error_code=` call site exists before it. Unlike the built-in table this one is **complete rather than best-effort**, and held that way: `ERROR_CODES` is a closed enumeration, so a new code added without a `since` entry fails `test_since_covers_every_code` rather than silently reporting null.

- **The `vera/README.md` module map is gated instead of hand-maintained** ([#1150](https://github.com/aallan/vera/issues/1150)). Its per-module line counts had drifted silently after every refactor — `checker/calls.py` cited 610 lines against a real 1,556 — and ten modules had no row at all, including `checker/sql.py` (#309) and `runtime/db.py` (#229), which shipped without one. `scripts/check_doc_counts.py` now checks the table on two independent axes: cited line counts against the tree, with the same ±10% band the `KNOWN_ISSUES.md` refactoring table uses (the numbers convey relative scale, so exact pinning would tax every compiler PR with a doc edit), and **coverage** — exact, no tolerance — that every module on disk has a row. Coverage is the half a count check cannot do: a missing row has no cited number to be wrong. A `pkg/` row aggregates that package's modules, and the per-effect host-binding row pins its own ×N multiplicity, so adding an effect family trips the gate. All 54 counts refreshed, the ten missing rows added, and the runtime family row corrected from ×13 to ×14.

- **Three compiler docstrings that still described the pre-v0.1.7 world are corrected** ([#1161](https://github.com/aallan/vera/issues/1161)). `vera/runtime/db.py` announced the #309 checker gate as forthcoming and the runtime `?`-parameterisation as "the guarantee in force" — #309 shipped in v0.1.7, so the host now describes itself as the second layer under a compile-time one. `count_placeholders` in `vera/checker/sql.py` said its `None` return defers the arity check to sqlite3 at run time; `vera/checker/calls.py` turns that `None` into a hard `E209`, so no such program reaches run time at all, and the docstring now says `None` means "not countable" rather than "allowed through". `TESTING.md` carried the same deferral framing.

- **The SQL-injection guarantee is now advertised everywhere the language is, not just implemented** — the post-v0.1.7 documentation sweep found the v0.1.7 flagship (`<DB>` + `E207`) fully documented in SKILL.md and the spec but absent from every code-sample showcase and orientation surface — the landing page carried only a reference-card line, and README, FAQ, EXAMPLES, PYPI_README, TOOLCHAIN, AGENTS, and ENVIRONMENT had nothing. README gains a fourth "What Vera looks like" block (SQL injection won't compile), the landing page gains a matching showcase sample (mirrored in `docs/index.md` via `build_index_md`), FAQ gains "Is SQL injection really a compile-time error?", EXAMPLES.md gains the SQL tour stop with the real `E207` diagnostic, TOOLCHAIN.md gains the `VERA_DB_URL` run recipe, and PYPI_README states the claim. `VERA_DB_URL` joins ENVIRONMENT.md (table + section — its own add-a-variable checklist had been skipped), AGENTS.md gains Essential rule 8 (literal SQL + `?` placeholders, `E207`/`E208`), DESIGN.md's effects row and SKILL.md's wasi-p2 rejected-family list add the missing `DB`. The introspection registries were audited against their compiler sources and are current (`effects`/`builtins` derive from the live `TypeEnv` tables, plus the documented hand-listed `Exn` entry; `errors` has 145/145 parity incl. `E207`–`E209`/`E217`) — the one gap, an always-null `since` field on error codes, is filed as [#1157](https://github.com/aallan/vera/issues/1157). ROADMAP reconciled against the tracker: [#1103](https://github.com/aallan/vera/issues/1103), [#1106](https://github.com/aallan/vera/issues/1106), [#1126](https://github.com/aallan/vera/issues/1126), [#1156](https://github.com/aallan/vera/issues/1156), and [#1157](https://github.com/aallan/vera/issues/1157) gain rows; the closed pip-upgrade-audit item leaves. The examples/tests workaround sweep found nothing stale.

- **The VeraBench results are refreshed** to the [current sweep](https://github.com/aallan/vera-bench#results), run on benchmark [v0.0.16](https://github.com/aallan/vera-bench/releases/tag/v0.0.16), across the website, `docs/index.md`, README, FAQ, and `DE_BRUIJN.md`. The lineup is nine models across three providers over 60 problems, and the metric is **% solved** (pass@1): a refusal, a compile failure, a crash and a wrong answer all count alike as not solved. Seven of the nine models write 100% correct Vera, and against Python Vera wins outright for four of the nine, draws with three and loses two. The figures these replace came from the v0.0.7 sweep — six models, 50 problems, Kimi K2.5 as the headline — and reported `run_correct`, which was measured only over attempts that compiled, so a model that refused or failed to compile shrank its own denominator and scored *higher* for answering less. The site section also gains a regenerated delta chart on a transparent background. `FAQ.md` drops the claim that De Bruijn slot ordering is the dominant failure mode — a v0.0.7-era finding the current report does not support — and cites the Vera-against-Aver comparison in its place: two languages absent from every training set, differing chiefly in that [Aver](https://averlang.dev) has ordinary variable names and Vera has none. `DE_BRUIJN.md` likewise scopes its slot-ordering result to the early snapshot it came from, rather than asserting it as current, and cites that comparison, which tests the document's thesis directly.
- **`docs/index.md` — the Markdown companion agents fetch instead of the landing page — carries the refreshed benchmark section.** It is generated by `build_index_md()` in `scripts/build_site.py`, which holds the landing page's substance as hand-maintained prose rather than deriving it from `docs/index.html`, so an edit to the HTML does not propagate. `check_site_assets.py` cannot catch the resulting drift: it regenerates from that same function and compares, so both sides of the comparison move together and a stale generator validates as up-to-date. The benchmark block in the generator is updated to match the HTML; the structural gap is tracked in [#1154](https://github.com/aallan/vera/issues/1154).

## [0.1.7] - 2026-07-24

### Fixed

- **A bare (unqualified) effect-operation call the WASM backend cannot route is now a clean checker error** ([#1148](https://github.com/aallan/vera/issues/1148)). An effect op called without its qualifier — `query("SELECT ...", [])`, `print("hi")` — type-checked but failed `vera compile` with a confusing `Function 'query' is not defined in this module`, a check-green→codegen-fail disagreement. Codegen routes a bare op call only for the built-in `State` / `Exn` ops (`get` / `put` / `throw`, backed by host cells) or when the effect is handled by an enclosing `handle` block; every other bare op — `IO` / `DB` / `Http` / `Inference` / `Random` and user effects — has no bare route. The checker now rejects the unroutable case at check time (`E217`, the safe direction of the disagreement), steering to the qualified `Effect.op(...)` spelling or a `handle[Effect]` block, via a handled-effect stack in `_check_handle` that mirrors codegen's routing capability. The State/Exn carve-out keys on the op name as well as the effect name, so a user `effect State` / `effect Exn` shadow declaring some other op is still rejected rather than silently miscompiled. Surfaced by the #309 adversarial reviews. Spec §7.4.
- **A guest-controlled out-of-bounds `(ptr, len)` no longer crashes the host with `SIGBUS`** ([#1145](https://github.com/aallan/vera/issues/1145)). `_read_wasm_string` — the reader behind every host import that takes a `String` argument (42 call sites across 12 runtime modules) — sliced WASM linear memory through a raw `ctypes` pointer (`bytes(buf[ptr:ptr + length])`) with **no bounds check**, so an out-of-range pair (from a codegen bug, or a program that computes a bad `(ptr, len)`) read past the memory region into wasmtime's guard page and killed the whole process with `SIGBUS` / `KERN_PROTECTION_FAILURE`. The `safe_utf8_decode` contract (#589 / #592) did not cover it: the crash happens inside `bytes(buf[...])`, before decoding is ever reached. `_read_wasm_string` now bounds-checks `(ptr, len)` against the live memory size — as its post-run sibling `_read_string_export` already did — and raises a `wasmtime.WasmtimeError` carrying the `out of bounds memory access` reason, which `execute()` classifies as a clean `out_of_bounds` trap (backtrace, `Fix:` paragraph, buffered output preserved) identical to a native guest OOB, rather than a host crash.

- **The `@Float64` FP-soundness regression tests no longer flake on a slow CI runner** ([#1121](https://github.com/aallan/vera/issues/1121)). `test_rounding_relation_not_proved` and its reflexive-equality sibling asserted the obligation came back *exactly* `violated`, but the docstring — and the soundness property they exist to guard — only require that a false FP property is **not** proved. When Z3 exhausted its budget on the slowest matrix cell and the obligation fell to the conservative Tier-3 runtime check (`timeout`), the assertion failed spuriously and red-flagged unrelated PRs. Widened to `status != "verified"` (proving a false property remains a hard failure), with a guard that stubs the validity query to `unknown` — pinning the Tier-3 branch deterministically, without depending on solver latency — so the accept-set cannot silently narrow back.

- **SKILL.md no longer teaches the pre-#1003 State-handler `with` idiom** ([#1141](https://github.com/aallan/vera/issues/1141)). The `sum_with_state` example and the handler-syntax reference block showed `put(@Int) -> { resume(()) } with @Int = @Int.0`; post-#1003 (spec §7.5.2) the state slot in a `put` clause is the value *before* the store, so `with @Int = @Int.0` overrides the store back to the old state — a `with` that silently undoes the `put`. SKILL.md is fetched live as the reference LLMs get at benchmark time, so a model copying the idiom into a direct handled-body `put` produced code that checks, verifies, and computes the wrong answer. The canonical `put(@Int) -> { resume(()) }` (intrinsic store) is now shown throughout, and the reference block documents `with` as a store *override* for transforming the written value (`@T.0` = pre-store state, `@T.1` = the argument; example `with @Int = @Int.1 * 2`).

- **`vera fmt --check` and the corpus gate agree on line endings.** `--check` read files with universal-newline translation, so a CRLF file compared equal to its LF formatting and was reported canonical while the byte-reading corpus gate rejected it. `cmd_fmt` now reads bytes and treats any carriage return as non-canonical (spec §1.8 rule 10, which now names LF explicitly). A `.gitattributes` marks `*.vera` as `eol=lf` so a Windows checkout does not arrive as CRLF and fail the pre-commit gate. Spec §1.8 rule 2's scope sentence is corrected: the rule is stated for statement, block-result, arm-body and binding-value positions, with sub-expression flattening tracked as [#1139](https://github.com/aallan/vera/issues/1139) rather than claimed as already unconditional.

- **`vera fmt` refuses to emit output that breaks its own contract** — `format_source` now checks three postconditions on every call and raises a `FormatterPostconditionError` naming the violated invariant instead of returning corrupt output: the output re-parses, the comment-content multiset is conserved, and a second pass is a fixed point (DESIGN principle 5 applied to the toolchain itself; measured ~3.5 ms per file over the 209-program corpus). A structural backstop makes comment deletion impossible by construction: any comment bucket no emitter consumed is emitted at the end of its enclosing declaration, rule 11's sanctioned fallback, rather than being dropped.
- **`vera fmt` no longer deletes own-line comments in value position** — a comment between `let @T =` and a multi-line `match`/`if`/`handle` value, inside a flattened redundant block, or inside a flattened match-arm block was silently discarded; a statement-bearing block as a `let` value additionally produced output that failed to parse. All value paths now flush the construct's comment bucket before emission, and `if`/`handle` gained the span self-anchors `match` already had, so a comment above any of the three binds to the construct rather than drifting into its interior. Comments above a next-line plain arm body and inside multi-line handler-clause bodies now attach to their own arm and clause.
- **`vera fmt` emits floats the lexer can read** — values below 1e-4 or at/above 1e16 were emitted in Python scientific notation, which `FLOAT_LIT` cannot lex, so a check-clean program stopped parsing after formatting. Exponents are expanded to positional decimal form with round-trip value equality.
- **`vera fmt` parenthesizes an indexed collection when precedence demands it** — `(x |> f())[0]` reformatted to `x |> f()[0]`, a different program. Collections outside the postfix-safe set are re-parenthesized, and a block in sub-expression position keeps its braces instead of silently changing evaluation.
- **`blank_source_lines` ignores blank lines inside block comments**, so a `{- -}` spanning an empty line no longer manufactures a phantom paragraph break, and `format_source` formats the source it was passed (the `file` argument is a diagnostic label only). `FnDecl.where_span` now follows the span-field convention — excluded from equality and repr, serialized as a structured span in `ast --json`.
- **The corpus gate rejects CRLF and bare-CR line endings** (canonical form is LF) and reports unreadable files — invalid UTF-8, dangling symlinks — in its broken-file list instead of aborting the sweep with a traceback.
- **`vera fmt` keeps own-line comments above the construct they document** ([#1136](https://github.com/aallan/vera/issues/1136)). A comment was bound to the innermost construct whose span *contained* it rather than to the construct that *followed* it, so the three positions with no anchor of their own — contract and `effects` clauses, `where` blocks, and `match` arms — sent their comments to the enclosing declaration's backstop, which re-emitted them at the top of the function body. `data`, `effect` and `ability` declarations already anchored their members, which is why those positions looked safe. Contract, effect, arm and `where` spans are now anchors (`where` needed a new `FnDecl.where_span`, since the keyword had no span at all), and a blank line separating a comment block from what it documents survives instead of being swallowed. Spec §1.8 rule 11 now states leading-comment attachment explicitly; the blank-line half of it is rule 13.
- **`vera fmt` preserves the blank lines between statements** (new §1.8 rule 13). Only the gap *under* a comment block survived the fix above, which left the formatter inconsistent: it kept a paragraph break below a comment and deleted one between two plain statements. `examples/file_io.vera` lost the break before its trailing `()` and `examples/io_operations.vera` lost two. The AST records no separation at all — two statements written a page apart parse to the tree two written back to back do — so neither keeping every gap nor discarding every one is recoverable after the fact. The formatter now reads the source's blank lines directly and reproduces exactly one wherever there was one or more: between statements in a block, before a block's trailing result expression, and above an own-line comment. A gap the source did not have is never introduced, and one held against a brace is dropped, since rule 2 already gives the brace its own line. The comment-adjacent case is no longer special-cased: `Comment.blank_after` is gone and both halves read the same source map, so a single source gap cannot be reproduced twice.
- **`vera fmt` no longer flattens a nested or statement-position `match`** against §1.8 rule 2. The multi-line branch keyed on `arm.body.statements` being non-empty, but a block whose whole content is a single trailing expression keeps it in `expr` with `statements` empty — so `{ match ... }` read as empty and the entire construct collapsed onto one line, closing brace and all. Statement position had the same hole. `examples/file_io.vera` went from 21 lines to 11 under the old formatter; a 150-character line carrying three brace pairs is now properly nested.
- **`vera fmt` escapes characters that cannot be read in source** (new §1.8 rule 12). Only six characters were re-encoded, so a parsed `\u{200B}` re-emitted as an invisible zero-width space. Unicode categories `Cc`, `Cf`, `Cs`, `Co`, `Cn`, `Zl`, `Zp` and non-ASCII `Zs` now emit as `\u{...}`; printable non-ASCII stays literal, so `café 😀` is unchanged. A bidirectional override or zero-width joiner can no longer hide in a program.

- **`vera fmt` no longer destroys a `handle` in sub-expression position.** `_fmt_handle_inline` was a stub returning a literal `handle[E] { ... }` — the state initialiser, every clause and the `in` body deleted, and the output no longer parsed ([E005]). It carried `# pragma: no cover` on the belief the path was unreachable, but `handle_expr` is a bare alternative of `primary_expr`, so it is reachable from every operand, argument and element position. The renderer now reads the node, and the clause and state renderings are shared with the multi-line emitter so the two cannot drift apart again. Its braces share a line, which rule 2 would not choose: unparseable output is strictly worse than badly-shaped output, and giving nested constructs a multi-line path so this renderer is never reached is tracked separately.

- **`vera fmt` applies §1.8 rule 2 in value position, not only in statement position.**  A `match`, `if` or `handle` bound by a `let` was flattened onto one line with its braces sharing it, while the identical construct written as a statement, as a block's result, or as a match-arm body expanded over its own lines.  `LetStmt` and `LetDestruct` rendered their value through the single-line `_fmt_expr` path, so position — not the construct — decided which of two textual forms came out, and `examples/array_utilities.vera` held both a five-line `if` and a flat one.  One construct with two textual representations is what [DESIGN.md](DESIGN.md) principle 3 ("every construct has exactly one textual representation") and its technical-decisions row ("no equivalent alternatives") exist to rule out, and a position-dependent form also obliges a generator to decide per site which to emit rather than applying one rule everywhere (principle 6).  The expanded form is longer, which is not an argument against it: principle 2 ranks explicitness over convenience.  All three statement kinds now share one rule-2 path, with the binding text (`let @Int = `) on the opening line and the `;` riding the closing brace; §1.8 rule 2 states the scope explicitly so it no longer has to be inferred.  Separately, the comment-anchor walk now descends into a statement's *value*: only the statement's own start line was an anchor, so a comment written above an arm of a `let`-bound or statement-position `match` fell through to the next statement and silently documented something it was not written for.  Expanding let values turns those arms into real lines, which makes the misattribution visible rather than latent.  Re-canonicalising the corpus replaces 32 flattened lines with 160 across 11 programs, all of which still `check` and `verify` unchanged.

### Added

- **SQL injection is a compile-time error** ([#309](https://github.com/aallan/vera/issues/309)). The SQL argument of `DB.query` / `DB.execute` must be *literal-provenance* — a string literal, a `string_concat` of literals, or a `let` chain of those. A SQL string assembled from a runtime value (a slot, a function result, or a `\(expr)` interpolation) is the SQL injection vector, so the checker rejects it with `E207`. The guarantee is a deterministic type error, not an SMT obligation: no solver, immune to timeout flakes, and effective even inside handled code where solver-based claims cannot reach. The gate keys on the effect qualifier — the same axis codegen routes a call to the host database on — so it also holds for a user-declared `effect DB { ... }` (the idiomatic way to use a host effect, as with `effect IO`), which still reaches the host and must not bypass the check. Runtime data flows only through the `?` placeholders and the `Array<Option<String>>` params array; when both the SQL and that array are statically sized, a placeholder/parameter count mismatch is also caught at compile time (`E208`), while a dynamically-sized array defers the arity check to the driver. Implemented as a new leaf checker `vera/checker/sql.py` (`resolve_literal_string` — conservative-reject on every unhandled expression shape, so it can only false-reject, never wrong-accept — plus a quote-aware `count_placeholders` pinned differentially against sqlite3). Only anonymous `?` placeholders are accepted; a numbered (`?NNN`) or named (`:name` / `@name` / `$name`) form is a compile-time error (`E209`), since parameters bind positionally — and placeholder detection follows sqlite3's own identifier rule, so a named parameter whose first character is non-ASCII or `$` (`:€x`, `$$x`) is caught too. The gate runs on codegen's host-routing axis *alone* — the `DB.query` / `DB.execute` spelling codegen marshals to the host, independent of the argument's static type — so a runtime string laundered through a generic `@T` parameter, a user `effect DB` shadow declaring a non-`String` param, a generic `effect DB<T>` at unbound arity, and an *imported* library body carrying a non-literal query all reach the same `E207`: every path a runtime string could take to the database. Hardened under three external adversarial reviews (CodeRabbit, Kimi K3, Cortex) and a self-authored adversarial workflow that found and closed a generic-parameter injection bypass. No mainstream language prevents SQL injection at compile time this way. Spec §9.5.7; conformance `ch09_sql_injection_rejected` (E207), `ch09_sql_placeholder_mismatch_rejected` (E208), and `ch09_sql_numbered_placeholder_rejected` (E209).

- **A built-in `<DB>` effect for SQL database access** ([#229](https://github.com/aallan/vera/issues/229)). `DB.execute(sql, params)` runs writes (`CREATE`/`INSERT`/`UPDATE`/`DELETE`) and returns the affected-row count; `DB.query(sql, params)` runs a `SELECT` and returns the result grid. Both return `Result<_, String>`, so a driver error — malformed SQL, a constraint violation, an unreachable database — is the `Err` arm rather than a trap, so callers either propagate the `Result` or `match` its arms. A row grid marshals as `Array<Array<Option<String>>>`: a cell is `Some(text)`, or `None` for SQL `NULL`, so `NULL` and `""` stay distinct (DESIGN principle 2, no implicit behaviour). Parameters are the `Array<Option<String>>` second argument, bound positionally to the `?` placeholders (`Some(v)` a value, `None` a `NULL`), so data is never spliced into the SQL text. The host reader bounds-checks every guest-controlled pointer in a parameter array — the outer array, each element pointer, and the full `Some(String)` cell — before it is dereferenced, so a malformed array surfaces as a clean out-of-bounds trap rather than a host `SIGBUS` (the [#1145](https://github.com/aallan/vera/issues/1145) class, one level deeper than the string reader). The effect is host-backed on Python's `sqlite3`; `VERA_DB_URL` selects the connection (`sqlite::memory:` by default, or `sqlite:///path`). Phase 1 is SQLite-only, single-connection, and stringly-typed — named columns and typed cells are tracked in [#1143](https://github.com/aallan/vera/issues/1143). `handle[DB]` is not yet available (host effects are un-mockable, [#372](https://github.com/aallan/vera/issues/372)); the browser runtime returns `Err` for every `DB` operation, and `vera compile --target wasi-p2` rejects `<DB>` at compile time. Spec §7.7.7 and §9.5.7; examples `examples/database.vera` (in-memory) and `examples/sqlitedb.vera` (a committed on-disk SQLite file).

- **The corpus is gated on canonical form** ([#1124](https://github.com/aallan/vera/issues/1124)). `scripts/check_corpus_canonical.py` compares all 209 programs (recursively) in `examples/` and `tests/conformance/` against `vera fmt`, wired into the CI lint job and pre-commit. Nothing ran `vera fmt --check` over the corpus before, which is how #1112 and #1123 stayed invisible — a regression that deleted every inline comment in the language passed the whole gate. A comment-count sweep cannot replace it: counting cannot see a comment that *moved*, and formatting reaches a fixed point either way, so both invariants stay green while a comment drifts out of the construct it documents.

- **`npm run check:package` pins what ships inside the built VSIX** (`editors/vscode/check-package-contents.js`, wired into the `VS Code extension` workflow after the packaging step).  It reads the archive's own zip central directory rather than `vsce ls`: the two differ, because vsce synthesises `extension.vsixmanifest` and `[Content_Types].xml`, renames README/CHANGELOG/LICENSE on the way in, and re-runs `vscode:prepublish` while packaging — so a check run against the working tree grades a directory that no longer exists once the artifact is built.  Three assertions, none derived from another: the archive's entries must match an explicit list exactly; every entry's extension must be on an allowlist, so an unrecognised type fails rather than passes; and no entry may carry an executable mode bit, which vsce preserves into the archive.  The last is the only one that can see a file with a wholly inert extension arriving as mode `0755`.  Covered by `npm test` (`node --test`, no new dependencies) over the failure cases, since a guard that only ever passes is indistinguishable from no guard.

- **An `npm` ecosystem entry in `.github/dependabot.yml`, and an npm audit step in CI** — the VS Code extension's dependencies were previously monitored by neither.  Neither existed: Dependabot covered only `uv` and `github-actions`, and nothing audited npm at all (`dependency-audit` in CI is `pip-audit`, Python-only).  Security updates fire off GitHub's advisory database regardless of configuration, so the tree was not unwatched — but *version* updates need the entry, which is why a `vscode-languageclient` major bump first arrived bundled into a CVE fix rather than as a routine bump months earlier.  The audit is scoped to production dependencies, which are what esbuild bundles into the VSIX: an advisory against a build-only package is worth knowing about but should not block an unrelated PR, since the fix is rarely ours to make.  Checked in both directions — the scoped gate passes on the current tree, where the one open advisory is dev-only, and fails on the lockfile as it stood before [#1129](https://github.com/aallan/vera/pull/1129), where `brace-expansion` was a runtime transitive.

### Documentation

- **The README's licence table covers everything Vera redistributes, and its project structure names every pipeline stage.**  The table listed only the three Python runtime dependencies, omitting the `[lsp]` extra (`pygls`, Apache-2.0; `lsprotocol`, MIT) and the npm packages bundled into the `.vsix` — where `minimatch` is BlueOak-1.0.0 and `semver` is ISC, so the previous claim that all dependencies were "MIT or Apache-2.0" did not hold for the shipped extension.  "Licence compliance is enforced by CI" also overstated: `scripts/check_licenses.py` runs `pip-licenses` and has no npm coverage at all, which the text now says.  The note about `chardet` arriving under LGPL via `cyclonedx-bom` was stale in a way worth spelling out — `cyclonedx-bom` is not a Vera dependency and appears in no manifest; it is installed ad hoc by the separate `sbom` CI job, which is not the job the licence gate runs in, so neither package has ever been in the environment being checked.  The same claim inside `check_licenses.py` is corrected.  `wasmtime` is Apache-2.0 **WITH LLVM-exception**.  Separately, the project-structure listing sat directly beneath the words "seven-stage pipeline" while naming six of the seven stages: `resolver.py` is now among them.
- **v0.1.7 release documentation sweep.** Aligned the spec §9.5 built-in-effects summary with the shipped set — `Async`, `Inference`, `HttpServer`, and `DB`, replacing the stale "future effects for concurrency and LLM inference" wording — removed the now-shipped `#309` (contract-verified SQL) from the ROADMAP standard-library horizon, and dropped the fixed `#1121` row from the KNOWN_ISSUES bugs table. The drifted, ungated `vera/README` module-map line counts are tracked as [#1150](https://github.com/aallan/vera/issues/1150).

### Changed

- **The VS Code extension moves to `vscode-languageclient` 10, and the packaged VSIX no longer contains a shell script** ([#1130](https://github.com/aallan/vera/pull/1130)).  Version 10 declares an `exports` map that exposes only `.`, `./node`, `./browser` and `./$test/common/*`, which makes every other subpath unreachable; `editors/vscode/esbuild.js` resolved `lib/node/terminateProcess.sh` out of `node_modules`, copied it into `dist/` and marked it `0755`, so a plain dependency bump failed with `ERR_PACKAGE_PATH_NOT_EXPORTED` before esbuild ran.  Separately, and not the cause of that failure, v10 also removed the helper: termination is unchanged in substance, because v10 holds the same recursive `pgrep` / `kill -9` process-tree walk as a string in `lib/node/processes.js` and pipes it to `/bin/sh`, with a pid regex guard added, and Windows still uses `taskkill /T /F`.  The extension source needs no changes — `LanguageClient`, `start()`, `stop()`, `documentSelector` and `outputChannelName` are unchanged.  The packaged artifact drops from 11 entries to 10 — JSON, JavaScript, Markdown, XML manifests and a PNG, none of them executable — which bears on the Marketplace rejection tracked in [#1106](https://github.com/aallan/vera/issues/1106) without being established as its cause.  The extension's VS Code floor rises to **1.91** (from 1.82), v10's own requirement.

### Security

- **`brace-expansion` raised to `2.1.2` in the VS Code extension lockfile, off the range affected by [GHSA-3jxr-9vmj-r5cp](https://github.com/advisories/GHSA-3jxr-9vmj-r5cp)** (CVE-2026-13149; GitHub severity high, CVSS 3.1 base 5.3 — availability only, `AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L`).  The advisory is exponential-time expansion of consecutive non-expanding `{}` groups, reachable only by whoever supplies the glob pattern.  It arrived as a runtime transitive of the language client — `vscode-languageclient@9.0.1` → `minimatch@5.1.9` → `brace-expansion@2.1.1` — so it ships inside the VSIX, but every pattern reaching that `minimatch` is a document selector written in the extension's own source rather than anything a workspace or document can influence; practical exposure is correspondingly low.  `minimatch@5.1.9` already declares `brace-expansion: ^2.0.1`, which admits the patched `2.1.2`, so the immediate fix was a three-field lockfile bump.  The `vscode-languageclient` 10 upgrade in this same release then carried the dependency further still, to `brace-expansion` 5.0.7 by way of `minimatch` 10 — which is the version the shipped lockfile records.

## [0.1.6] - 2026-07-20

### Fixed

- **`vera fmt` no longer deletes inline comments** ([#1123](https://github.com/aallan/vera/issues/1123)). A comment with code before it on the same line -- `--`, `{- -}` and `/* */` alike -- was classified, filed into `_Attached.inline`, and then dropped, because nothing ever read that store; only comments occupying a whole line survived. `vera fmt --check` compounded it by reporting such a file as non-canonical, so the remedy it prescribed (`vera fmt --write`) was what performed the deletion, in place and with nothing on stderr. Placement is now decided at emission time by a single rule -- a comment is claimed by the **innermost construct whose span contains it** -- with claim points after each statement, block result, signature, contract clause and effects clause, and a declaration-level backstop so a comment that fits no inner construct (on a brace or `where` line, say) is relocated rather than discarded. Claims compare full (line, column) positions and stop at the next construct's start, since two statements may share a line and a line-granular claim would hand both their trailing comments to whichever is emitted first. Because nested constructs emit first, claiming greedily yields innermost-wins, and the result is a fixed point: re-formatting finds each comment already trailing its construct. Annotation labels on a parameter or return slot are exempt -- they come from the AST -- and the signature claim skips them so they are not emitted twice. A claimed comment is collapsed onto one physical line when its text spans several — a `{- -}` can be multi-line and still inline, and appending it verbatim would leave the continuation carrying its original source indentation through the final join, against spec 1.8's two-spaces-per-level rule. Spec 1.8 gains the preservation rule it never stated, which is why the deletion went unnoticed.
- **`vera fmt` preserves comments in every declaration form, not just functions** ([#1123](https://github.com/aallan/vera/issues/1123)). The claim points and the declaration-level backstop all lived inside `_emit_fn_decl`, so `data`, `type`, `effect`, `ability`, `module` and `import` had none: a trailing comment in any of them was filed into the write-only inline store and dropped, exactly as before the fix, while the release note claimed otherwise. Every declaration now claims, with per-item points on constructors and effect/ability operations so a comment trails the item it belongs to rather than the declaration's last line. A comment written *above* a `module` or `import` was likewise filed into `before[line]` and never read — `examples/vera/math.vera` and `collections.vera` were losing their header lines — so those buckets are read too. Two guards keep it honest: a sweep asserting no corpus file loses a comment when formatted, and a fixture carrying one in every position the emitter has.
- **Match-arm comments stay on their arm**, and formatting is a fixed point for every comment shape. An arm is not a `Block`, so the block-body claim never reached it and every arm comment fell to the declaration backstop. Separately, overflow comments were spilled onto a *new* line, which turned an inline comment into an own-line one — and own-line comments attach to the nearest anchor *after* them, so a comment following the last statement in a block walked out of its function on each pass until it landed at top level. Everything claimed at one point now stays on that one line, with line comments last and any non-final one re-delimited as `{- ... -}` so it survives as a distinct comment instead of being absorbed. Guarded by two invariants over every comment shape — comment count preserved, and `fmt(fmt(x)) == fmt(x)` — because deletion and merging show up only in the first, drift and relocation only in the second.
- **A `--` comment no longer swallows the comments after it.** Comments claimed at one point were joined onto a single physical line, and since `--` runs to end of line, anything following it became part of its text: four distinct comments re-read as one, irreversibly and idempotently. At most one line comment may share a physical line now, and it must be last; the rest go on their own lines. A multi-line annotation label is collapsed on the AST-driven emission path too, which previously spliced raw text and left a continuation carrying its original source indentation.
- **`{-` unambiguously opens a block comment.** `{-1}` parsed before the nesting work and is now `[E020]`, since `{-` is a comment opener per spec 1.3 whatever follows it. The diagnostic's fix text names the real remedy — write `{ -1 }` with a separating space — and spec 1.9 now states the precedence rather than leaving it implicit.
- **Annotation comments are preserved in the AST, as the specification has always said** ([#1112](https://github.com/aallan/vera/issues/1112)). Spec 1.3 describes `/* ... */` as "optional human-readable labels for bindings" that are "preserved in the AST"; they were in fact `%ignore`d by the grammar — absent from the AST entirely, and silently deleted by `vera fmt`. A label on a function parameter or on the return slot is now carried on the `FnDecl` as a tuple aligned with `params`, so it belongs to a **slot index** rather than to a source line: an unlabelled slot holds `None` instead of being omitted, since collapsing the gaps would shift every later label onto the wrong parameter. That positional storage is what De Bruijn addressing requires and what the formatter's line-keyed inline store could never have provided — `fn area(@Int /* width */, @Int /* height */ -> @Int)` puts two labels on one line, and a `dict[int, Comment]` holds one. `vera fmt` re-emits labels from the AST, so formatting is now a fixed point over them rather than a deletion. Because annotation comments are `%ignore`d and never reach the parse tree, `parse()` carries the scanned labels on the tree for `transform()` to attach, which keeps every AST-building path — `check`, `verify`, `fmt`, the LSP — consistent without threading a `source` argument through every `transform()` call site. A label is recognised only inside the signature's parentheses — the shared scanner now records each comment's parenthesis depth, so a comment written after the closing paren stays an ordinary trailing comment instead of being adopted as the return label and re-emitted inside the parens. Only the annotations the label walk actually consumes are retired from the comment stream, so a leading `/* ... */` before a slot, or a second one behind an already-labelled slot, survives as an ordinary comment rather than being deleted. Labels in other positions remain accepted and ignored; spec 1.3 now states precisely which positions retain.
- **A dedicated `E02x` block for malformed comments** ([#1112](https://github.com/aallan/vera/issues/1112)). All three cases previously surfaced as a token-level complaint naming the wrong culprit, because the grammar only ever sees the wreckage a malformed comment leaves behind: an unterminated `{-` was reported as an unexpected `{`, an unterminated `/*` as an unexpected `/`, and a nested `/* a /* b */ */` as an unexpected `*` at the *trailing* delimiter. They are now detected during the shared pre-lex scan that already knows where every comment begins and ends — `[E020]` unterminated block comment, `[E021]` unterminated annotation comment, and `[E023]` annotation comments do not nest, the last pointing at the inner `/*` the author expected to nest rather than at the wreckage.
- **Block comments nest, as the specification has always said** ([#1112](https://github.com/aallan/vera/issues/1112)). Spec 1.3 documents `{- outer {- inner -} still outer -}` as a single comment, but the grammar ignored block comments with a non-greedy regex that closes at the *first* `-}` — and a regular expression cannot match balanced delimiters at all, so nesting was impossible by construction. The identical text was therefore one comment to `vera fmt`, whose extractor counts depth (and whose suite asserted it), and a syntax error to `vera check`; the spec's own nested example did not parse, and SKILL.md's comment-syntax block carried a `vera:skip-parse` annotation for the same reason (now removed -- the gate flags it as stale once the example parses). Nesting is now resolved before the grammar sees the source by a single scanner in the new `vera/lexical.py`, which the formatter's `extract_comments` is re-based on so the two cannot drift apart again. Block comments are blanked to spaces with newlines preserved, so every line, column, and `propagate_positions` offset — and with them the formatter's span-based comment attachment — stays byte-faithful; an unterminated `{-` now reports at the opening delimiter instead of at end of input.
- **`vera fmt` keeps comments inside `data`, `effect`, and `ability` declarations** ([#1113](https://github.com/aallan/vera/issues/1113)). Comments immediately preceding constructors or operations now stay attached to those items instead of being hoisted after the enclosing declaration.
- **The concurrent `await` lowering resolves type aliases in its fused-handle classification** ([#1109](https://github.com/aallan/vera/issues/1109), found probing [#1095](https://github.com/aallan/vera/issues/1095)). A future bound through an alias-typed `let` (`type F = Future<Result<String, String>>; let @F = async(Http.get(url)); await(@F.0)`) or returned from a helper declared `-> @F` compiled **and** verified clean, fused its `async` (call-shape fusion ignores the binding type), but identity-lowered the `await`: the classifier matched the literal type `Future<Result<String, String>>` only, so the kind-4 handle wrapper was read as the `Result` ADT and every two-arm match took `Err` on a successful request — a silent wrong answer, no trap, exit 0. The `[E602]` skip spec §9.5.4 documented as the guard against exactly this mis-lower no longer fired once the v0.1.5 alias-payload work let the alias-typed `let` compile. The classifier now resolves aliases transitively (param-substituting, cycle-guarded) before the literal check — a new shared `resolve_type_alias` walk in `vera/monomorphize.py` that `resolve_fn_type_alias` is re-based on, so the fn-type and Future classifications cannot drift — covering the `await` slot arm, both declared-return registries (bare and module-qualified), and the #843 `apply_fn` closure-return arm. Review (PR [#1110](https://github.com/aallan/vera/pull/1110)) found the same literal-matching gap one level down — an alias INSIDE the future's payload (`Future<R>` with `type R = Result<String, String>`) — so the terminal check now canonicalizes type arguments recursively too, with a path-local cycle guard whose probe unwraps refinement layers (a cyclic payload alias like `type A = Future<A>` or `type A = Future<{ @A | true }>` conservatively fails classification rather than spinning; the checker rejects such cycles upstream with [E132], #1059); payload discrimination is unchanged (only the exact terminal `Future<Result<String, String>>` classifies). The spec §9.5.4 limitation sentence, the KNOWN_ISSUES row, and the ROADMAP Stage 20 row are removed; regression tests pin the `async_await` import and byte-exact Ok payloads for the alias-let, alias-fn-return, alias-chain, and payload-alias shapes, plus a genuinely-concurrent two-request overlap test through an alias chain.
- **The install section's language-server note names both routes** — the source `.[dev]` install already includes the server, while the PyPI route needs the `veralang[lsp]` extra — instead of an ambiguous "the `[lsp]` extra shown above."
- **The install section's source-route explanation spans the full column** on veralang.dev. The paragraph carried a `max-width:56ch` cap that left a two-sentence block crammed into the left half beside the full-width code blocks; the cap is removed so it fills the column like the blocks it sits between.
- **The release workflow's tag-existence probe treats a 404 as "tag absent"** instead of capturing the error body. `gh api` prints the 404 JSON on stdout, so the first production run of the tag/Release job compared `{"message":"Not Found"...}` against the merge SHA, tripped the immutability guard, and failed after PyPI had already accepted the v0.1.5 files; the probe result is now shape-validated to a 40-hex SHA. The v0.1.5 tag and GitHub Release were completed manually from the run's registry-verified artifact per the RELEASING.md recovery runbook.

## [0.1.5] - 2026-07-17

### Added

- **Release documentation-consistency sweep.** The spec's §9.3/§9.4 Map-key and Set-element lists no longer offer `Unit` (contradicting §2.2.2's E135 zero-size rejection), and SKILL's Map/Set sections state the E135 constraint; SKILL's conformance count is re-anchored to `check_doc_counts.py` (the gate's regex had silently stopped matching a reworded sentence, letting the count drift 18 stale — the re-anchored pattern is mutation-validated); FAQ leads with the PyPI install route and carries live test/example counts plus the `<Diverge>` effect; TOOLCHAIN drops stale release-relative claims; CONTRIBUTING's release checklist names every `check_version_sync.py` surface including README and the unconditional `uv.lock` regen.

- **The `veralang` distribution ships to production PyPI** ([#737](https://github.com/aallan/vera/issues/737)), through the approval-gated Trusted Publishing workflow, and the installation documentation carries both routes with an accurate account of what each installs: `python -m pip install veralang` (LSP extra: `"veralang[lsp]"`) delivers the compiler and the `vera` command only, while the GitHub source checkout additionally provides the bundled examples, the conformance suite, and the specification — so the source route is the recommended default in the agent-facing docs (SKILL.md, the FAQ) whose teaching material lives in the checkout, and the PyPI route leads on the toolchain-oriented surfaces (PYPI_README, the site's install section). The distribution is named `veralang`; the installed command remains `vera` and Python code imports `vera` — `pip install vera` is an unrelated PyPI project and is never the right command.

- **`check_doc_counts.py` gates the README project-status test count** (PR #1088 review). The README's "N tests" figure in the Project status section matched none of the oracle's README patterns, so it had silently drifted hundreds of tests stale; a new anchored pattern locks it to the live collection count, mutation-validated (a stale figure fails the gate).
- **Approval-gated, tokenless release automation** ([#481](https://github.com/aallan/vera/issues/481)) turns a strictly increasing version on `main` into one tested wheel/sdist artifact, publishes that exact artifact through PyPI Trusted Publishing, verifies registry filenames and SHA-256 hashes, then creates the immutable tag and GitHub Release. A separate manual TestPyPI path stages the current version; a guarded recovery path cannot overwrite a published version or bypass package changes. Build, OIDC publication, registry verification, and GitHub release privileges remain separate jobs, and release policy now forbids tag-moving/fold-in releases after publication.
- **PyPI publication readiness** ([#737](https://github.com/aallan/vera/issues/737)) renames the Python distribution to `veralang` while preserving the `vera` command and import package, adds a dedicated registry README, and gates the built sdist/wheel contents plus an installed-wheel CLI smoke test in CI. The existing GitHub-source installation path remains supported; the first live production package remains the completion gate for #737.
- **Inference + JSON composition example** ([#379](https://github.com/aallan/vera/issues/379)) demonstrates an effectful model call flowing through pure JSON parsing, typed integer extraction, and a statically proved 0–100 normalization contract. Raw JSON and tagged or untagged fenced responses are accepted, with the original completion retained in malformed-response diagnostics.

### Fixed

- **A mismatch diagnostic renders a leaked type variable as `?`, not a bare letter** ([#1069](https://github.com/aallan/vera/issues/1069)). A refinement alias over a generic container — `type M = { @Map<String, Int> | ... }` — read through an element derivation inference cannot thread (`map_values(@M.0)[0]`) leaves the built-in's value-type variable unsubstituted; the mismatch message then stripped its internal `#b` namespacing marker (#970/#982) down to a bare `V` that reads as a real type (`body has type V, expected Int`). Such a leaked internal placeholder — a namespaced built-in var or a fresh inference hole — now renders as the unknown marker `?` (`body has type ?, expected Int`) at the *actual*-type slot of every reachable type-mismatch message — the subsumption sites (function body [E121], `let` value [E170], anonymous-function body [E171], constructor field [E213], effect- and ability-operation argument [E204]/[E241], `apply_fn` argument [E202], Ord-operation operand [E242], handler state initial value / `with` update [E331]/[E335]), the operator/index/interpolation family (arithmetic operands [E140], equality and ordering comparison [E142]/[E143], logical operands [E144]/[E145], unary `!` and `-` [E146]/[E147], string-interpolation part [E148], array index [E160], non-indexable collection [E161]), `assert`/`assume` arguments [E172]/[E173], `if` condition and branch types [E300]/[E301] (description and fix text), and the contract and refinement predicates (`requires` [E123], `ensures` [E124], refinement [E126]) — recursing into type arguments (`Array<?>`) and into a function type's effect row (`effects(<State<?>>)`; `pretty_effect` renders each effect instance's type arguments, so an unscrubbed row would surface the same bare letter). The handful of actual-type renders a leaked placeholder provably cannot reach keep plain `pretty_type`: the numeric-join [E141] and ordering-compatibility [E142] arms (operands proven numeric/orderable first), match-arm unification [E302] (`is_subtype` accepts a TypeVar arm), the Eq-ability operand message (`contains_typevar` early-return), `apply_fn`'s arity message (the value is proven a function type), and the `data`-invariant message (`invariant` in `data` is grammar-rejected, #686). The change is message text only: what the checker accepts or rejects is unchanged, a genuine user `forall` var (plain `T`) keeps its own spelling, and a built-in's unsubstituted *expected* signature (`Array<T>`) is deliberately left untouched.
- **A fn-typed slot works as the closure argument to `array_map` / `array_mapi`** ([#1056](https://github.com/aallan/vera/issues/1056)). `array_map([1, 2, 3], @Mapper.0)` — where `type Mapper = fn(Int -> Int);` and `@Mapper.0` is a let-bound or parameter slot — was check-green but dropped the enclosing function via [E602] ("could not infer array_map closure return type"), while an inline `fn(...) { ... }` at the same position compiled and `apply_fn` over the identical slot already resolved fine. The map emission inferred the output element type only from an inline `AnonFn`; it now routes through the same `_closure_arg_return_type` resolver `apply_fn` uses, recovering the return type from the slot's `FnType` alias signature (including a type-changing `Int -> String` mapper, whose output element type must come from the slot rather than the input array) and emitting the `call_indirect` exactly as for an inline closure. The shared inference also covers `array_mapi` and the `array_fold` accumulator.
- **A self-referential type alias through a type argument is rejected at check time (E132)** ([#1059](https://github.com/aallan/vera/issues/1059)). `type F = Future<F>;`, the mutual `type A = Future<B>; type B = Future<A>;`, and `type L = Array<L>;` were all admitted by `vera check` — the #648 cyclic-alias walk followed only bare `type A = B` references and never descended into a generic's type arguments — and then either crashed code generation with an unguarded `RecursionError` in `_type_expr_to_wasm_type` (the `Future` spellings, whose type argument the WAT-type mapper recurses through) or compiled to a degenerate type inhabited only by `[]` (`Array<L>`). The cycle detector now walks the full alias-reference graph, descending into every `type_arg` and `RefinementType` base while excluding a generic alias's own type parameters, so every such previously-admitted self-referential alias is now the same [E132] `Cyclic type alias` diagnostic the direct `type A = B; type B = A` cycle already produced. The rule is structural — a self-reference inside a type argument the generic alias discards (`type Wrap<T> = Int; type C = Wrap<C>;`) is rejected on the same rule, and the edge set covers arbitrarily nested type arguments (`type A = Future<Array<B>>; type B = A;`). Function-type parameter/return positions are exempt (`type FA = fn(FA -> Int) effects(pure);` registers — a function value is a table-index indirection, the same exemption `data` ADTs get; spec §2.6.3 now says so). The cycle walk runs on an explicit stack, so a thousand-alias legal chain checks clean instead of crashing the checker with a `RecursionError` (PR #1066 review). A legal acyclic nesting (`type A = Future<Int>; type B = Future<A>;`) still checks.
- **Container builtins over alias-spelled or user-fn arguments emit with the right host tag** ([#1063](https://github.com/aallan/vera/issues/1063)). The **Map/Set builtins' emission inference** (`map_keys` / `map_values` / `map_get` / `set_to_array`) fell through to the `"b"` (i32) host-import tag — the empty-collection escape hatch — for the same aliased and user-fn argument spellings, and returned silently WRONG values on a check-green, verify-green program (i64 values truncated to their low 32 bits, String keys garbled; pre-existing on `main`, reproduced there before fixing; #1055's aliased index pins passed over it only by fresh-heap luck). All three container inference helpers now consult the shared rebuilder first, with values above 2^32 pinning the tags; the genuinely-unknown empty-collection shape keeps its permissive fall-through, also pinned.
- **An alias to a Future (`type FA = Future<Option<Int>>;`) compiles at the match-scrutinee and constructor-argument sites** ([#1054](https://github.com/aallan/vera/issues/1054)). The alias spelling was check- and verify-green but E602-skipped where the direct spelling compiled: the SlotRef/ResultRef WAT-type mapper resolved the slot name through the name-only base-name hop (dropping the alias's type arguments) and its Future-transparency arm guarded on AST type arguments an alias-spelled ref does not carry. The mapper now canonicalizes an alias to its target's full compound spelling first (the #1037 walk) and adds the string-form Future arm its sibling scalar mapper carries — the alias analog of #1046, closing the last mapper in the family without the canonicalizer. Found by this PR's adversarial review.
- **Indexing directly on a builtin call result compiles** ([#1048](https://github.com/aallan/vera/issues/1048)). `array_concat([1, 2], [3, 4])[2]` — a subscript applied straight to a builtin call, for any element type — was check-green but dropped the enclosing function via [E602], while the let-bound form (`let @Array<Int> = array_concat(...); @Array<Int>.0[2]`) compiled. `_infer_index_element_type_expr`'s FnCall arm resolved only user-function returns (the `_fn_ret_type_exprs` registry holds no builtins), so element-type inference returned `None` and the function was skipped. The arm now recovers a builtin call's return `NamedType` through the same `_get_arg_type_info_wasm` consultor the sibling Vera-type inference and instantiation discovery use — arg-forwarding for `array_concat`/`array_append`/`array_slice`/`array_filter` and the `_BUILTIN_PARAMETERIZED_RETURNS` table for the concrete-`Array` builtins (`array_range`, `string_split`, `json_keys`, …) — then extracts the element type exactly as the user-function path does; chained indexing through a nested `Array<Array<T>>` result resolves too. Builtins whose element is a type variable (`array_map`/`array_mapi`/`array_flatten`) stay absent from that consultor and still route through the let-bound form.
- **Alias-spelled container arguments to the element-type derivations canonicalize** ([#1055](https://github.com/aallan/vera/issues/1055)). `map_values(@M.0)[0]` via `type M = Map<String, Int>;` (and `set_to_array` via a Set alias) E602-dropped where the direct spelling compiled: the argument reached the #1051 derivations as its bare alias name with no type arguments, so the class arms never saw the container shape. The shared argument-type rebuilder now canonicalizes a bare alias name to its target's full compound spelling (the #1037 walk) before parsing. The array builtins with aliased arguments (`array_flatten(@Grid.0)`) additionally drop earlier, in the builtin's call emission — that alias extension rides #1053.
- **Indexing directly on a type-variable-element builtin call result compiles** ([#1051](https://github.com/aallan/vera/issues/1051), the type-variable follow-up to #1048). The eight `Array`-returning builtins whose element type depends on the call's arguments — `array_reverse`, `array_sort_by`, `array_flatten`, `map_keys`, `map_values`, `set_to_array`, `array_map`, `array_mapi` — were check-green but dropped the enclosing function via [E602] when indexed directly (`array_reverse([10, 20, 30])[0]`), while the let-bound form (`let @Array<Int> = array_reverse(...); @Array<Int>.0[0]`) compiled. They are deliberately absent from the shared `_get_arg_type_info_wasm` consultor #1048 reuses — `_BUILTIN_PARAMETERIZED_RETURNS` is registry-locked to type-variable-free returns (a `TypeVar`-carrying entry would bind a phantom var in clone-name discovery, rejected by `test_parameterized_table_matches_registry`) — so `_builtin_call_ret_named_type` now derives their return `NamedType` from the call's arguments, per mechanism class: argument-forwarding (`array_reverse`/`array_sort_by` return arg0's type verbatim, `array_flatten` unwraps one `Array<>` layer), container-arg-derived (`map_keys`/`map_values`/`set_to_array` read K/V/T off the `Map`/`Set` argument), and closure-return-derived (`array_map`/`array_mapi` take the element from the closure argument's declared return type). The derivation stays off the shared consultor, so clone-name discovery is untouched; a genuinely unresolvable argument shape (e.g. a nested type-variable-element builtin, whose call already fails to emit) keeps the loud [E602] skip.
- **`array_flatten` of an inline nested array literal compiles** ([#1052](https://github.com/aallan/vera/issues/1052)). `array_flatten([[10, 20], [30, 40]])` — a nested array literal passed straight as a call argument — was check-green but dropped the enclosing function via [E602] ("could not recover inner element type for array_flatten"), while the let-bound-argument form (`let @Array<Array<Int>> = [[..], [..]]; array_flatten(@Array<Array<Int>>.0)`) compiled. `_translate_array_flatten` recovered the inner element type `T` only from a `SlotRef` `@Array<Array<T>>` argument; an inline literal fell through to the loud skip. `T` is now taken from the inner literal's element type through the same `_infer_array_element_type` recovery a nested literal in a `let` position already uses; the `Array<String>` inner variant resolves too. An empty inline literal (`array_flatten([])`) carries no element type and still keeps the loud skip.
- **A type-variable-element builtin nested as another builtin's call argument compiles** ([#1053](https://github.com/aallan/vera/issues/1053), the call-emission sibling of #1051). `array_reverse(array_flatten(x))` — a type-variable-element `Array` builtin passed straight as another combinator's argument — was check-green but dropped the enclosing function via [E602] ("could not infer array_reverse element type"), even fully let-bound. The outer combinator's element-type probe (`_infer_concat_elem_type`) dropped the inner `<T>` layer of a `SlotRef` `@Array<Array<T>>` (it returned the bare `Array`), so the inner `array_flatten` could not be unwrapped; inference now falls back to the shared #1051 `_builtin_call_ret_named_type` derivation and reads back the element name, so `array_reverse`/`array_sort_by` of an inner `array_flatten` resolve. The converse nesting — a builtin call as `array_flatten`'s own argument (`array_flatten(array_map(xs, |x| [..]))`) — recovers `T` the same way in `_translate_array_flatten`. Consultor-resolvable inner calls (`array_reverse(array_concat(a, b))`) and typevar-in-typevar (`array_reverse(array_reverse(x))`) already worked; a genuinely unresolvable inner call keeps the loud [E602] skip. Three more call-argument spellings ride the same fix: an **alias-spelled array argument** (`type Grid = Array<Array<Int>>; array_flatten(@Grid.0)`, `type Row = Array<Int>; array_reverse(@Row.0)`) reached the combinators' emission probes as its bare alias name and dropped — they now canonicalize through the same shared rebuilder [#1055](https://github.com/aallan/vera/issues/1055) taught the index-side derivations (the call-emission dual of that fix). A **user-fn call argument** with a flat `Array<T>` return (`array_reverse(mk())`, `array_sort_by(mk(), cmp)`) already resolved through this fix's fallback and is pinned; a NESTED return (`array_flatten(mkn())` with `mkn(-> @Array<Array<Int>>)`) stayed unresolvable because the shared consultor deliberately blanks nested type-arg positions (clone-name-discovery lockstep) — the rebuilder now reads the registered fn's declared return type directly, off the consultor, so discovery is untouched.
- **Directly indexing a nested type-variable-element builtin result compiles** ([#1094](https://github.com/aallan/vera/issues/1094), completing the #1048/#1051/#1053 family). `array_reverse(array_reverse([10, 20, 30]))[0]`, `array_sort_by(map_values(m), cmp)[0]`, `array_reverse(map_values(m))[0]`, and `array_flatten(array_reverse(x))[0]` — a subscript applied straight to a type-variable-element `Array` builtin whose own argument is *another* type-variable-element builtin — were check- and verify-green but dropped the enclosing function via [E602], while every let-bound spelling (`let @Array<Int> = array_reverse(array_reverse(x)); @Array<Int>.0[0]`) ran. The #1051 per-class element-type derivation resolved its argument through the shared `_get_arg_type_info_wasm` consultor only, which deliberately cannot report a type-variable-element inner call (its element type depends on the call's arguments), so the derivation returned `None` and the index dropped. The class-1 argument-forwarding arm (`array_reverse`/`array_sort_by`), the `array_flatten` unwrap arm, and the class-2 container arm now resolve a `FnCall` argument through `_builtin_call_ret_named_type` — the shared consultor first, then the same per-class derivation, recursing — so the inner call's element type is recovered; every other argument shape (`SlotRef`, array/nested literal) still routes through the consultor, and a genuinely unresolvable argument keeps the loud [E602] skip. A `Block`-wrapped argument (`array_reverse({ array_reverse(x) })[0]`) resolves via its tail expression on both the element inference and the deep resolution, matching the container emissions' Block handling. The new `examples/scoreboard.vera` reads a top score with the natural direct-index spelling `array_sort_by(map_values(@Board.0), cmp)[0]` over a `type Board = Map<String, Int>;` alias.
- **Alias-spelled inner element names classify as their target's representation at the array emissions** ([#1067](https://github.com/aallan/vera/issues/1067)). With `type Row = Array<Int>; type Grid = Array<Row>;`, `array_reverse(@Grid.0)[0][0]` returned 4626322722586886145 and `array_length(array_reverse(@Grid.0)[0])` returned 81948 — check-green, verify-green, garbage: the emission probes recovered the element as the bare alias name `Row`, and the size/pair classification fell to the 4-byte opaque-pointer default for what is really an 8-byte (ptr, len) pair, so every combinator copied half of each element; `array_concat` and the depth-2 `array_flatten` (`@Array<Array<Row>>`) read past their allocations (unreachable traps), and the `@Row`-comparator `array_sort_by` trapped mid-sort. Element names now canonicalize to the target's compound spelling at the element-probe exit, in `array_flatten`'s input gate and T-recovery (whose alias-layer unwrap also sees through an alias-spelled middle layer, so `array_flatten(@Grid.0)` and `array_flatten(@Array<Row>.0)` compile instead of dropping), and all five shapes plus the sort compute correct values. The direct spellings (`@Array<Row>`, `@Array<Array<Row>>`) mis-classified identically through the pre-existing direct arm — latent on `main`, where those shapes still E602-dropped for unrelated reasons — and are fixed and pinned alongside the alias spellings.
- **A generic alias of a container as a declared return derives through its target, never its own type args** ([#1068](https://github.com/aallan/vera/issues/1068)). `type MyMap<V> = Map<String, V>; fn mk(-> @MyMap<Int>)` then `map_keys(mk())[0]` handed the runner a VALIDATION-FAILING module while `vera compile` exited 0: the container derivation consumed `("MyMap", ("Int",))`'s type args positionally with no container-name check, deriving key element `Int` where the target says `String`, and the emission contradicted the derivation in the same function. The shared rebuilder now substitutes a generic alias's args through its target (`substitute_type_vars`) and canonicalizes — so `MyMap<Int>` resolves to `Map<String, Int>` and both `map_keys` and `map_values` over it compute correct values — and the container derivation additionally verifies the resolved argument IS `Map`/`Set` before reading K/V/T off it, so an unresolvable alias shape stays a loud [E602] skip rather than an invalid module. Caught before merge — the consuming derivation is this PR's own (#1051) machinery, so no released version is affected.
- **Bare-alias user-fn returns and Block-wrapped arguments resolve through the container emissions** ([#1071](https://github.com/aallan/vera/issues/1071), completing the #1063 family). A user fn whose DECLARED return is a bare alias of a container (`fn mkm(-> @M)` with `type M = Map<String, Int>;`) silently truncated through `map_keys` / `map_values` / `map_get` / `set_to_array` — the shared consultor's user-fn arm only reports parameterized returns, so the bare alias exited unresolved before any alias handling and the emission fell to the `"b"` mis-tag (values returned as their low 32 bits, String keys garbled; pre-existing on `main`, reproduced there before fixing). The rebuilder now recovers a registered non-generic user fn's declared return directly when the consultor reports nothing, off the consultor, and a Block-wrapped container argument (`map_values({ @M.0 })`) resolves via its tail expression instead of riding the same mis-tag. The `_map_wasm_tag` fall-through comments now state exactly which argument shapes resolve and which still reach the permissive tag.
- **A `Map` or `Set` with a zero-size key, value, or element type is a checker error (E135)** ([#1075](https://github.com/aallan/vera/issues/1075)). `Map<String, Unit>` — direct or alias spelling — checked clean and compiled exit-0 to INVALID WASM ("expected i32 but nothing on stack": the zero-size value pushes no operand where the container's host import expects one); pre-existing on `main`, surfaced by the PR #1061 adversarial delta review. The determination follows spec §2.2's declared-vs-materialized line: `Map` keys/values and `Set` elements are raw, unboxed host-serialized values — representationally the `Array<Unit>` element case its E135 gate (#945) already rejects with this exact rationale — not the boxed-ADT-field case (`Box<Unit>` / `Option<Unit>` fields live inside a heap layout and stay legal, as does `Map<String, Option<Unit>>`, pinned). Rejection also keeps one canonical form (DESIGN.md principle 3): `Map<K, Unit>` is informationally a `Set<K>`, which the fix text names. The type-resolution gate now covers `Map` (both positions, reported per position) and `Set`, keyed on erasure (`Future<Unit>` rejects identically); the annotation-free spellings, whose types exist only through inference, are backstopped at codegen — `_map_wasm_tag` refuses the tag through the same RECURSIVE erasure oracle the checker keys on (`_slot_name_erases_to_unit`: alias chains canonicalized, `Future<...>` payloads recursed), and container entry types resolve rebuilder-first so a parameterized user-fn return reaches the tag as its full spelling rather than a bare `Future` head — a literal name comparison here was defeated by any indirection (`async(async(()))`, a fn returning `@Future<Unit>` directly or behind `type FU = Future<Unit>` / `type Task = Future<Unit>`, all previously exit-0 invalid modules; PR #1083 adversarial review) — so the emission takes the loud [E602] skip instead of emitting an invalid module, while the genuinely element-type-free empty-collection shape (`map_keys(map_new())`) keeps its permissive fall-through. The same entry resolver routes an alias-of-`Array` user-fn return (`type Names = Array<String>`) into the existing Array-typed loud skip instead of a mis-tagged scalar slot. Spec §2.2 names the container rule alongside the Array one, and `ch02_map_unit_value_rejected` joins the conformance suite's negative fixtures (161 programs).
- **A nested constructor pattern after a zero-size component selects the right match arm** ([#1042](https://github.com/aallan/vera/issues/1042)). `match Tuple((), MkBox(4242)) { Tuple(@Unit, MkBox(@Int)) -> ..., Tuple(@Unit, MkNot(@Int)) -> ... }` silently ran the `MkNot` arm — check-green, verify-green, wrong value. Construction stores nothing for a zero-size component, but the nested-pattern tag walk gave it four bytes (the registered layout's generic type or an `i32` default), so every nested constructor tag after it was checked at the wrong offset, failed against garbage, and match fall-through picked a later arm — whose extraction (computed correctly) then returned the real value through the wrong arm. The walk now gives erased components zero width, mirroring construction and the extraction walks; the same shape through a transparent `Future<Unit>` and through a user-ADT parent is pinned. Pre-existing for bare `Unit` since zero-size components landed (#902); surfaced by the #1035 review. The registered-layout side of the divergence (wildcard sub-patterns, structural `Eq`) is [#1043](https://github.com/aallan/vera/issues/1043).
- **A `Future<Unit>` component in a `let`-destructure or match binding erases like bare `Unit`** ([#1031](https://github.com/aallan/vera/issues/1031)). A zero-size `Future<Unit>` tuple/ADT component was check-green but `CodegenSkip`ped the whole enclosing function (E602): the three codegen declaration-position guards — and the constructor-argument WASM-type inference that gates reachability (constructing a `Future<Unit>` field needs `async(())`) — compared the bare string `Unit`, so the transparent `Future<Unit>` wrapper (representation-identical to its `Unit` payload, #841) missed the zero-size branch and fell through to the skip. All four sites now key on erasure (mirroring `erases_to_unit` / codegen's `_type_expr_to_wasm_type`), so a declaration binding a `Future<Unit>` component compiles and binds nothing, exactly like bare `Unit`, with the non-zero-size sibling component landing at the correct offset. An alias **to** the compound (`type FU = Future<Unit>;`, including alias-of-alias chains) erases the same way: the keying canonicalizes an alias to its target's full compound spelling rather than resolving the base name only, which dropped the type arguments (`FU` resolved to `Future`, not `Future<Unit>`) and skipped the function — an alias *inside* `Future<...>` already worked. Reads of the erased component stay checker-rejected (E182, #1005), unchanged.
- **An alias to a representable compound (`type FI = Future<Int>;`) maps to its WAT type** ([#1037](https://github.com/aallan/vera/issues/1037)). A `FI` destructure/match component or standalone `let @FI = async(41);` binding was check+verify green but E602-skipped the whole enclosing function, while the direct `Future<Int>` spelling compiled: `_slot_name_to_wasm_type` resolved aliases through the same name-only recursion the zero-size keying had (`FI` resolved to bare `Future`, matched no branch, returned no representation). The mapper now canonicalizes an alias to its target's full compound spelling through the walk shared with the zero-size keying, so an alias binds exactly like its target written directly — the component binds a real local (`await` recovers the payload) and siblings land at the correct offsets. Zero-size erasure is unaffected, and a genuinely unrepresentable name still skips loudly.
- **`await` of a `Future<T>` compiles in match-scrutinee position** ([#1038](https://github.com/aallan/vera/issues/1038)). `match await(@Future<T>.0) { ... }` was check/verify-green but skipped the enclosing function (E602, "could not infer match scrutinee WASM type") for every non-`Unit` payload — an `Option<Int>`, a user ADT, and a scalar `Int` alike — because the SlotRef/ResultRef WASM-type mapper did not treat `Future<T>` as transparent the way the let-binding path already does. It now unwraps `Future<T>` to `T`'s representation, so awaiting a future directly in scrutinee position matches the await-into-a-`let`-then-match form that already worked. (`Future<String>` in that position remains blocked upstream by its own let-binding limitation, a separate gap.)
- **Reading a zero-size slot is a checker error (E182)** ([#1005](https://github.com/aallan/vera/issues/1005)). A `@Unit.n` read — a normal function's `@Unit` param or a `get` clause's `@Unit` op parameter alike — passed `check` and died at codegen with the E699 dangling-slot internal error, since a Unit value compiles to no WASM local. The checker now rejects the read everywhere with a fix pointing at the unit literal `()`, keyed on representation (`erases_to_unit`) so a `Future<Unit>` slot is rejected the same way; declaring the parameter stays legal, mirroring the declare-vs-read line the E206 generic-at-Unit guard draws.
- **Effect ops in array-literal elements compile; a `let` of a zero-size type is a checker error (E183)** ([#1006](https://github.com/aallan/vera/issues/1006)). `[get(()), 1]` was check/verify-green but skipped the enclosing function (E602) because codegen's element-type inference did not know effect ops — both op-injection sites now record `get`'s Vera result type (State`<T>`'s T) alongside the WAT type they already recorded, and the element dispatches through the clause-aware lowering (a transforming `get` clause applies to the element). `let @Unit = put(5);`, whose binding could never be read, is now rejected at check with a fix pointing at the statement form `put(5);` — uniformly for any zero-size binding, effect-op or plain RHS.
- **A `let` of a pair-represented `Future<T>` payload binds two locals** ([#1039](https://github.com/aallan/vera/issues/1039)). `let @Future<String> = async("hello");` — and any `Future<T>` whose payload is pair-represented (`String`, `Array<T>`) — was check/verify-green but skipped the enclosing function (E602, "has no WASM representation"). `Future<T>` is representation-transparent ([#841](https://github.com/aallan/vera/issues/841)), so `Future<String>` is an `i32_pair`, but the let translator's pair detection keyed on the literal `String`/`Array` names rather than the transparent payload, so the wrapper fell to the scalar branch — where the scalar mapper recursed through it to a pair inner and returned no WASM type. The pair predicate is now Future-transparent, mirroring the existing Future arm in the scalar mapper, so a pair-payload Future binds and reads two locals exactly like a bare String/Array let; a scalar (`Future<Int>`) or pointer (`Future<Box>`) payload stays a single local, and a zero-size payload (`Future<Unit>`) still skips loudly.
- **A closure that captures a `Future<T>` free variable serialises it at the payload's width** ([#1044](https://github.com/aallan/vera/issues/1044)). Capturing a `Future<String>` returned 0 and capturing a `Future<Int>` trapped at WASM validation (`expected i32, found i64`): the capture-width decision in the closure free-variable walker matched only the literal `String`/`Array` names and routed everything else — including a representation-transparent `Future<T>` wrapper ([#841](https://github.com/aallan/vera/issues/841)) — through the scalar mapper that maps unknowns to `i32`, so a pair payload stored ptr-only (its length read back as adjacent zero) and an i64 payload pushed an i64 into an `i32.store`. The decision now runs through the same Future-transparent `_is_pair_type_name` / `_slot_name_to_wasm_type` deciders the let-binding and slot-read paths use; both the capture-store and capture-load sides read the corrected width from the one capture record, so they stay in agreement.
- **`Array<Future<T>>` elements are sized, loaded, and stored at the payload's representation** ([#1045](https://github.com/aallan/vera/issues/1045)). `Array<Future<Int>>` trapped at WASM validation (`expected i32, found i64`) and `Array<Future<String>>` trapped (`values remaining on stack`): the five module-level array-element helpers matched `String` / `Array` / the scalar dict with no `Future` strip, so an i64 payload was sized as a 4-byte i32 and a pair payload as a single word. Since `Future<T>` is representation-transparent ([#841](https://github.com/aallan/vera/issues/841)), the helpers now strip `Future<…>` (nesting included) to the payload before deciding. The parallel index-read path needed the same fix: element-type inference dropped the wrapper's type argument, collapsing `Future<Int>` to a bare `Future` that fell to the i32 default — it now preserves the `Future<…>` payload so the read stride and load op match the store.
- **An alias to a pair-payload Future (`type FS = Future<String>;`) binds like its target** ([#1046](https://github.com/aallan/vera/issues/1046)). `let @FS = async("hello");` was check/verify-green but E602-skipped the enclosing function even with the #1039 and #1037 fixes in place: the pair predicate resolved the alias through the name-only base-name hop, which drops type arguments — `FS` resolved to bare `Future`, missed the Future-transparency arm, and fell to the scalar mapper, whose canonical `String` spelling has no scalar representation. The predicate now canonicalizes an alias to its target's full compound spelling first (the #1037 `_canonicalize_alias_slot_name` walk, cycle-cut threaded through the Future recursion), so an alias — including alias-of-alias chains — behaves exactly like its target written directly; the scalar-alias sibling (`type FI = Future<Int>;`, #1037) is unchanged.
- **`vera run` displays a bare `Future<String>` return as its string** ([#1047](https://github.com/aallan/vera/issues/1047)). A function returning `Future<String>` printed a raw heap pointer instead of the string, because the return-is-`String` predicate that populates `fn_string_returns` (so `execute()` decodes the `(ptr, len)` pair for display) lacked the `Future` strip. `Future<T>` is representation-transparent ([#841](https://github.com/aallan/vera/issues/841)), so a `Future<String>` return has the same pair shape as a `String`; the predicate now recurses through the wrapper. The emitted WASM was already sound — a caller that awaits the future gets the right value; only the top-level display was affected.
- **Array combinators over `Array<Future<T>>` copy elements at the payload's representation** ([#1057](https://github.com/aallan/vera/issues/1057)). Once [#1045](https://github.com/aallan/vera/issues/1045) made `Array<Future<T>>` literals and index reads sound, the array combinators became reachable on those arrays and returned garbage on check/verify-green programs: `array_concat` / `array_slice` / `array_reverse` / `array_filter` over `Array<Future<Int>>` returned wrong values (a 4-byte stride copied over the 8-byte i64 payloads), `Array<Future<String>>` concat half-read the two-word pair, `array_append` trapped at WASM validation (`expected i32, found i64`), and `array_flatten` flattened garbage. Each combinator recovered its element type through a site that returned the bare head `Future` with the type argument dropped — `_infer_concat_elem_type` for concat/slice/reverse/filter, a separate element-local decision for `array_append`, and a separate inner-type AST walk for `array_flatten` — so the element helpers could not strip `Future<…>` ([#841](https://github.com/aallan/vera/issues/841)) and collapsed to the 4-byte i32 default. Every site now preserves the full `Future<…>` spelling (and `array_append` strips the wrapper before typing its element local), mirroring the #1045 index-read fix.
- **An aliased `Future<T>` array element is stored at the payload's representation** ([#1058](https://github.com/aallan/vera/issues/1058)). `type FI = Future<Int>; let @Array<FI> = [async(1)];` was check-green then trapped at WASM validation (`expected i32, found i64`): the array-literal store resolved the element type through the name-only base-name hop, which drops type arguments, so `FI` resolved to bare `Future` and the i64 payload was stored with `i32.store`. The literal store now canonicalizes an alias to its target's full compound spelling (`Future<Int>`) before resolving, mirroring the #1046 `_canonicalize_alias_slot_name` order, so an aliased Future element — scalar or pair payload — stores exactly like its target spelled directly.
- **Array combinators over alias-spelled element types copy at the target's representation** ([#1062](https://github.com/aallan/vera/issues/1062)). The alias-spelled sibling of [#1057](https://github.com/aallan/vera/issues/1057), one layer down: once the [#1058](https://github.com/aallan/vera/issues/1058) fix made `Array<FI>` (`type FI = Future<Int>`) arrays constructible, the combinators became reachable on them and received the bare alias name as the element type — from the shared element-type probe (concat/slice/reverse/filter), the `array_append` element inference, and the `array_flatten` inner-type walk. The module-level element helpers have no alias table, so the raw name fell to the 4-byte i32 default on check/verify-green programs: silent wrong values for Future aliases (concat returned 9448928052300 for an expected 1122), an `expected i32, found i64` validation trap for `array_append`, and — reachable even before #1058 — a wrong element for a scalar alias like `type Flag = Bool`, whose 1-byte elements were copied at the 4-byte stride. Each site now canonicalizes a bare alias name to its target's full compound spelling before resolving, mirroring the #1058 literal-store fix; the index-read path was already alias-clean via its canonical element resolution (#559).
- **Array combinators over alias-named collections resolve the real element type** ([#1064](https://github.com/aallan/vera/issues/1064)). Pre-existing on main (no #1057/#1058/#1062 enabler involved): with `type Flags = Array<Bool>;`, `array_concat(@Flags.1, @Flags.0)` was check/verify-green but wrong — the collection slot's own name misses the `Array` match in the element-type probe, so the probe returned None and concat fell back to an 8-byte default stride. The fallback is coincidentally correct for Int, String, and array elements (why the shape survived unnoticed) and silently wrong for 1-byte Bool/Byte elements; `array_slice` and the map-family combinators loudly skipped the enclosing function instead. A literal whose elements are alias-typed slots (`array_concat([@Flags.1], [@Flags.0])`) hit the sibling hole in the probe's literal branch — the bare alias name sized the two-word pair elements at 4 bytes and indexing the result trapped `unreachable`. The probe now canonicalizes an alias-named collection to its target's full spelling and takes the element from it, canonicalizing a bare element name in turn (so `type Grid = Array<Row>` — whose target spelling keeps `Row` opaque — resolves to the row's real pair shape rather than regressing to the 4-byte default), and the literal branch canonicalizes bare inferred names the same way. Aliased collections of Int and of aliased rows keep their previous values, now by construction rather than by the 8-byte coincidence.
- **An array element spelled `Future<Alias>` is sized at the payload's representation** ([#1074](https://github.com/aallan/vera/issues/1074)). The alias-inside-`Future<…>` sibling of [#1062](https://github.com/aallan/vera/issues/1062), one layer deeper: an element written `Array<Future<FlagA>>` (`type FlagA = Bool`) — or hidden behind another alias, `type X = Future<FlagA>; Array<X>` — was check+verify+compile-green but mis-sized. The alias canonicalization resolved only a top-level alias name (or peeled the `Future<…>` wrapper), never an alias sitting *inside* the wrapper's type argument, so the element deciders `_strip_future`'d `Future<FlagA>` to a bare `FlagA` the name-keyed size dict fell to the 4-byte i32 default: a Bool/Byte payload's 1-byte-packed data was read at a 4-byte stride (silent wrong values — every element misread), while an Int/Nat/Float64/String payload trapped at WASM validation (`expected i32, found i64` / `values remaining on stack`) at instantiation, behind an exit-0 compile. Pre-existing, not a regression: before this branch's combinator commits the same sites returned the bare head `Future`, whose element size is the identical 4-byte i32 default — the same wrong stride; #1057/#1058/#1062 fixed the non-alias and alias-of-`Future` payloads but the alias-*inside*-`Future` variant was never within the canonicalizer's reach and had no fixture. `_canonicalize_alias_slot_name` now recurses into the transparent `Future<…>` payload (`Future<FlagA>` → `Future<Bool>`) and the four element-type deciders (`_infer_concat_elem_type` for concat/slice/reverse/filter, the array-literal store, the `array_flatten` inner walk, and the index-read element inference) route through it uniformly; `array_append`'s element inference already consulted the shared canonicalizer. Found by the adversarial delta review of this PR's combinator commits; the fix ships on the same branch.
- **Registered constructor layouts erase zero-size fields** ([#1043](https://github.com/aallan/vera/issues/1043)). A declared `Unit` field — or a transparent `Future<Unit>`, or an alias/alias-chain to either — was given a spurious 4-byte `i32` slot in the registered `ConstructorLayout` (the `_resolve_field_wasm_type` `wt is None` fallback returned `"i32"`), while construction lays such a field out erasure-aware: zero-size, storing nothing. Every consumer trusting the registered offsets then read a shifted address on a check-green program: a wildcard over the erased field plus a nested constructor pattern matched the wrong arm off zeroed fresh-alloc memory, and structural `Eq`/`show`/`hash` compared, rendered, and folded the wrong bytes — all silently wrong. Registration now returns the `"unit"` sentinel for any erases-to-Unit field (mirroring construction), `_wasm_type_size`/`_wasm_type_align` learn it as size 0 / align 1, the erased field's Vera type name canonicalises to `Unit` (so a `Future<Unit>` field is `Eq`-derivable and rendered like the zero-size value it is), and the pattern-match and structural-eq/show/hash consumers treat a zero-size field as the equal-by-definition value it represents.
- **A wildcard over a type-parameter field instantiated to `Unit` reads the right offset** ([#1060](https://github.com/aallan/vera/issues/1060)). A generic constructor's type-parameter field (`Box<T>` field `T`) registers as the generic 4-byte `i32` placeholder, but construction lays each instantiation out concretely — `Box<Unit>` *erases* the field to zero bytes (it is not boxed). A WILDCARD sub-pattern over that field advanced the match offset walk by the generic `i32` width regardless, so on a `Box<Unit>` every field *after* the erased one was read four bytes too high — silently, on a check-green program with no diagnostics: `match b { MkB(@Int, _, @Int) -> @Int.0 }` returned `0` instead of the real trailing `Int`, `N(_, @String)` on `Named<Unit>` read the `String` header at the wrong offset, and `En(_, MkBox(@Int))` on `Entry<Unit>` read the nested constructor tag at a shifted address and matched the wrong arm. This is the type-parameter sibling of #1043's *declared*-`Unit` field: only the two wildcard walks (`_extract_constructor_fields` and `_sub_pattern_wasm_type`) were wrong — `@Unit` bindings, let-destructures, and structural `Eq`/`show`/`hash` already recompute each field from the concrete instantiation and were correct for the same shapes *spelled with the literal `Unit` argument* (a non-literal erases-to-Unit argument — an alias, `Future<Unit>` — hit the same class of hole in `Eq` and in the width recomputation itself; that is [#1070](https://github.com/aallan/vera/issues/1070), fixed below). The wildcard walks now recompute a bare type-parameter field's width from the scrutinee's concrete type arguments, mirroring the eq/show recomputation. Where the instantiation is unrecoverable (a direct-call scrutinee whose inferred type dropped its arguments) *and* a later field is read, the function now LOUD-skips (E602) rather than reading a wrong offset; a trailing unrecoverable wildcard — whose width is never consumed — still compiles.
- **Erases-to-Unit type arguments work spelled non-literally: `Box<U>`, `Box<Future<Unit>>`, aliases, chains** ([#1070](https://github.com/aallan/vera/issues/1070)). The #1060 wildcard width recomputation resolved a type-parameter field's concrete type from the scrutinee's type arguments — but the width function's zero-size test was the literal name `Unit`. Registration canonicalises a *declared* erased field to `Unit` (#1043); a type *argument* keeps its use-site spelling, so `Box<U>` (`type U = Unit;`), `Box<Future<Unit>>`, `Box<FU>` (`type FU = Future<Unit>;`), and alias chains got 4 bytes and every later field was again read at a shifted offset — silently, check-green: the trailing `Int` read 0 instead of 22, and the nested-constructor variant matched off garbage. The same literal-test disease broke structural `Eq` over the same spellings — **pre-existing, not introduced by #1060** (the #1060 claim that eq/show/hash "were already correct" held only for the literal spelling): the `==` dispatch's concreteness gate and free-type-variable heuristic both classified `U` as an unresolved type variable, so the comparison silently fell back to the scalar *pointer* compare and two structurally equal values compared unequal; `show`/`hash` over the same shapes loud-skipped (E602). Every site now keys on erasure rather than the literal name — the width function (`_eq_field_wasm_type`), the resolved-field-type canonicalisation (`_resolve_field_type_for_eq`, mirroring registration), the dispatch gates (`_eq_type_name_fully_concrete`, `_type_arg_is_free_var`), and the `Eq`-derivability gate (`_type_eq_derivable`, kept in lockstep with the `$eq` generator per the #732 differential) — so all these spellings behave exactly like the literal `Unit`: wildcards read the constructed offsets, `Eq` compares structurally, and `show`/`hash` render and fold the real fields. Rider: a zero-size *binding* (`@Unit`) after an unrecoverable type-parameter wildcard no longer counts as a "later read" (it binds nothing and loads nothing), so that trailing shape compiles instead of over-conservatively loud-skipping; a genuine read beyond the erased binding still loud-skips.
- **Structural `==` over non-Unit alias type arguments compares values, not pointers** ([#1076](https://github.com/aallan/vera/issues/1076)). `Box<MyInt>` (`type MyInt = Int;`), `Box<MyStr>`, `Box<MyBool>`, `Box<Future<Int>>`, and alias chains: the `==` dispatch's free-type-variable heuristic and concreteness gate classified the alias spelling as an unresolved type variable, so the comparison silently fell back to the scalar *pointer* compare — two structurally equal values compared unequal (0) on a check-green program, while the literal spellings compared correctly. This is the non-Unit half of the heuristic #1070 patched for erases-to-Unit spellings; pre-existing alongside it. The fix *grounds* a spelling — alias chains resolved, transparent `Future<...>` wrappers peeled to their payload — at every site the #1070 pass keyed on erasure: the classifier and concreteness gate (so the dispatch routes structurally), the E613 derivability gate (kept in lockstep with the `$eq` generator per the #732 differential — a wrong loud E613 was the alternative failure), the field-type resolution (so a `MyStr` field dispatches to the String *content* comparison and a `Future<Int>` field compares its payload), and the width function (so the #1060 wildcard walks size a `MyInt`/`Future<Int>` field as i64, not the 4-byte pointer default — a following `Bool` read the right offset). Distinct values keep comparing unequal, including payloads that collide in their low 32 bits (`2^32 + 7` vs `7`), and a genuine free type variable still classifies free (the dead base-generic clone keeps its harmless scalar lowering, #912). Grounding the shared field resolution also lets `show`/`hash` render and fold such fields instead of loud-skipping.
- **`show`/`hash` accept aliased-Unit spellings at the two sites the #1070 pass missed** ([#1077](https://github.com/aallan/vera/issues/1077)). `show`/`hash` of a `Tuple<U, Int>` (`type U = Unit;`) loud-skipped the enclosing function: the Tuple-variadic plan branch passed *raw* type arguments to the per-field dispatch — unlike the registered-ADT branch #1070 fixed — so the unknown name `U` abandoned the whole render; the arguments are now grounded exactly like ADT field resolutions, and the erased component takes zero width so the following `Int` is read at its constructed offset. A *bare* value of an erases-to-Unit alias type (a function returning `@U`) missed the top-level show/hash `Unit` arms, which compared the literal name; both arms now key on erasure. All four shapes were loud drops (E602), never wrong values; the literal spellings were already correct and stay pinned.
- **Element-wise `==` on arrays of parameterized ADTs compares structurally** ([#1078](https://github.com/aallan/vera/issues/1078)). `Array<Box<Int>>` — literal spelling included — silently pointer-compared its elements: an `IndexExpr` operand's element type reaches the dispatch as the bare head (`Box`; the element-type inference drops the recovered `NamedType`'s type arguments), so a parameterized element was classified as a lost-type-argument clone (#772/#912 residue class) and `==` took the scalar fallback — equal elements compared unequal on a check-green program, while the same values compared directly (no array) and arrays of *non-generic* ADTs were correct. The `==` recovery chain now re-derives the full element spelling from the indexed collection, which carries its complete type arguments; aliased instantiations (`Array<Box<U>>`, `Array<Box<MyInt>>`) ride the #1070/#1076 grounding from there. Distinct elements keep comparing unequal, i64-wide.
- **Structural `==` over an alias of a WHOLE ADT compares values, not pointers** ([#1085](https://github.com/aallan/vera/issues/1085)). `type MyBox = Box<Int>;` then `@MyBox.0 == @MyBox.1`: the operand reached the `==` dispatch as the bare alias name `MyBox`, absent from `_adt_type_names`, so the structural branch was skipped and `==` fell to the scalar *pointer* compare — two structurally equal, distinct-pointer values compared unequal (0) on a check-green program, while the array-element form and the direct `Box<Int>` spelling compared correctly. This is the whole-OPERAND sibling of #1076, which grounded type ARGUMENTS (`Box<MyInt>`) but not an operand whose own type is an alias of the ADT. The operand's spelling is now grounded through the shared `_canonical_field_type` canonicalizer at the dispatch site (alias chains resolved, transparent `Future<...>` peeled), so `MyBox` → `Box<Int>` and the base `Box` routes to the structural `$eq` helper; an alias to a non-generic ADT and an alias chain ground the same way. Distinct values keep comparing unequal, i64-wide (`2^32 + 7` vs `7`), and a registered ADT or a genuine free `T` is returned unchanged, so the lost-type-arg and dead base-generic-clone controls still take the harmless scalar fallback (#912). The same grounding also fixes `==` through a **refinement of a whole ADT** (`type NB = { @Box<Int> | true };` then `@NB.0 == @NB.1`) — the refinement alias resolves to its base spelling in the same walk, where it previously fell to the same silent pointer compare (equal values → 0 on a check-green program; pinned equal-and-distinct). Pre-existing alongside #1076; found by the adversarial review of PR #1084.
- **A `forall<T where Eq<T>>` instantiated at an Eq alias is accepted, not wrong-loud E613'd** ([#1086](https://github.com/aallan/vera/issues/1086)). `same(mk(), mk())` where the argument is `@Box<MyInt>` (`type MyInt = Int;`) was rejected with a wrong-loud [E613] "Type 'MyInt' does not satisfy ability 'Eq'": the monomorphizer's top-level constraint gate (`_check_constraints`) tested `concrete in type_set` (a primitive) then `_adt_satisfies_eq` (a registered ADT layout) — an alias name is neither, so a legal program the alias-resolving checker accepted was refused before codegen. The gate now grounds the spelling through the shared `_type_eq_derivable` oracle — the SAME derivability check the `$eq` generator's field resolution mirrors, so the #732 checker↔codegen differential holds at this entry point too — accepting `MyInt` (→ `Int`), an alias of a whole ADT, and a transparent-`Future` alias (`FI` / `Future<Int>` → `Int`) alike. A genuinely non-Eq alias (`type BadArr = Array<Int>;`) grounds to a non-Eq type and still raises the correct [E613] with no codegen [E699] invariant hit (the two sides stay in lockstep). The emitted clone name and message keep the un-ground spelling (the #772/#932 hard constraint). Pre-existing; found by the adversarial review of PR #1084.
- **`show` / `hash` of a bare or aliased `Future` value renders the payload, not E602** ([#1087](https://github.com/aallan/vera/issues/1087)). `show(@FI.0)` (`type FI = Future<Int>;`) and `show(@Future<Int>.0)` loud-skipped the enclosing function ([E602]): the argument's inferred type reached the top-level show/hash dispatch as `FI` / `Future<Int>`, matched no primitive / Unit / composite arm, and abandoned the render — while `show` of the literal `Int` payload rendered fine. #1077 keyed the top-level Unit arm on erasure (covering aliased-*Unit* spellings); this is the non-Unit bare-`Future` sibling at the same dispatch. `_translate_show` / `_translate_hash` now ground the inferred type through the shared `_canonical_field_type` canonicalizer (alias chains resolved, transparent `Future<...>` peeled to its payload — `FI` → `Int`), so the render / fold dispatches exactly as the literal payload would; a non-alias, non-`Future` name is returned unchanged, leaving every other arm untouched. Distinct payloads still hash distinctly at i64 width (`2^32 + 7` vs `7`). The grounding also covers a bare **aliased-primitive** (`type MyInt = Int;`, `show(@MyInt.0)`) and **refinement** (`type Pos = { @Int | ... };`) value, which loud-skipped the same way. That top-level grounding alone still missed two spellings (PR #1090 review): a **composite** `Future` payload — `show(@FOI.0)` with `type FOI = Future<Option<Int>>;`, bare spelling included — and an **alias of a whole ADT** (`type MyBox = Box;`, [#1091](https://github.com/aallan/vera/issues/1091), found during this PR), because the composite path then recovers the argument's PARAMETERIZED type from its *declared* spelling (`_parameterized_arg_type`), undoing the grounding before `_show_value` / `_hash_value` resolves constructor plans. The recovered spelling is now grounded at both dispatch sites, and the composite render's Array arms ground their *element* type the same way (a `@Array<FI>` slot's element reached the size table, the element load, and the recursive render as the raw alias — `[mkf(), mkf()]` array literals included). A `Tuple` component and a generic-ADT constructor argument of an aliased-`Future` type already rendered correctly — the recovery's raw component spellings are grounded at consumption by the #1076/#1077 plan resolutions — and stay pinned. All these shapes were loud drops ([E602]), never wrong values. Pre-existing; found by the adversarial review of PR #1084 and CodeRabbit's review of PR #1090.
- **An int literal coerced into a `@Byte`-instantiated generic constructor field is stored at the field's width** ([#1092](https://github.com/aallan/vera/issues/1092)). `let @Box<Byte> = MkB(0);` — the one literal-at-Byte-field spelling the checker admits (an in-range `0..255` literal coerces to `@Byte` through the generic instantiation; a *declared* `Byte` field rejects the literal with [E213], and out-of-range, negative, or non-literal `@Int` arguments are loud [E170] rejections) — was constructed at the literal's own i64 width: construction sized the field from the ARGUMENT's inferred WASM type and stored at the 8-byte-aligned i64 slot, while every READER — field extraction, the structural-`$eq` helper, `show`/`hash` — sizes the field from the *instantiated* type (`Byte` → i32 at the i32 offset). Extraction read `0` for a stored `255`, and `MkB(0) == MkB(255)` compared the same wrong bytes on both sides and returned *equal* — silently, on a check-green program; the `@Byte`-slot passthrough (`MkB(@Byte.0)`) always stored i32 and was correct. Construction now stores the coerced literal at the field's i32 Byte width, keyed on the checker-recorded target type (the #820 side-table; the target's argument name is grounded, so `Box<MB>` via `type MB = Byte;` — the spelling the #1086 fix newly admits under `forall<T where Eq<T>>` — coerces like the literal `Byte`, and an `Int`-instantiated literal field keeps its i64 store, `2^32` staying distinct from `0`). Pre-existing on the base (direct spelling included); found by the independent adversarial review of PR #1090. #1060 made the type-parameter wildcard walks instantiation-aware for a *slot* scrutinee, but a *direct-call* scrutinee — `match mk() { MkB(@Int, _, @Int) -> @Int.0 }` where `mk() -> @Box<Unit>` — still LOUD-skipped (E602) whenever a later field was read: codegen's `_infer_vera_type(FnCall)` returns the bare base head (`"Box"`) for a parameterized i32-pointer return, dropping the `<Unit>` the walk needs, so it refused to guess a shifted offset — the sound interim behavior. The match lowering now recovers the scrutinee's full concrete type (`Box<Unit>`) from the callee's declared return type (`_fn_ret_type_exprs`, which a non-generic signature declares fully — no type variables to resolve), matching the slot form, so those shapes compile and read the real value (a trailing `Int`, a nested constructor tag, a `Named<Unit>` `String` read-back).
- **A generic-call `match` scrutinee resolves its instantiation from the call site** ([#1072](https://github.com/aallan/vera/issues/1072)). The generic sibling of #1065: `match wrap(5) { … }` where `forall<T> fn wrap(@T -> @P2<T, Unit>)` — the declared return carries type variables, so the #1065 non-generic recovery did not apply and the wildcard walk could not learn the erased field's width. On `main` this family read shifted offsets **silently** (the pre-#1060 walk; the repro returns `0` instead of `22` on a check-green program); the #1049 stack turned it into a sound E602 LOUD-skip. The match lowering now binds the callee's type variables from the call site — the same `_unify_param_arg_wasm` unification the generic call-rewrite performs — substitutes them into the declared return, and renders the full instantiation (`P2<Int, Unit>`), covering the concrete-`Unit`-type-arg return, a var-typed field at `String` (i32_pair width), a fully concrete parameterized return on a generic fn, and the nested-constructor and `String`-read-back variants. An unresolved variable still falls back to the LOUD-skip (sound); instantiating the variable itself at `Unit` stays E206-rejected at check, and a phantom-var callee stays E121-rejected.
- **A module-call `match` scrutinee recovers its instantiation through the shared resolver** ([#1073](https://github.com/aallan/vera/issues/1073)). The module door of the same family: `match boxlib::mk() { … }` never entered the #1065/#1072 recovery (it dispatches on the bare-name call form) and fell to the same bare-base-head collapse — a **silent wrong value** on `main`, a sound E602 LOUD-skip on the #1049 stack. The match lowering now resolves the qualified target through the single shared module-call resolver (the #774-reviewed source of truth) and recurses into the same declared-return recovery, covering imported non-generic and imported generic (#1072 x #1073 compound) scrutinees.
- **`array_map` / `array_mapi` / `array_fold` over `Array<Future<T>>` size the output element (and fold accumulator) at the payload's representation** ([#1079](https://github.com/aallan/vera/issues/1079)). The bare-head family (#1057's mechanism) at the closure-return / fold-accumulator inference site, found during the #1074 work: `_infer_closure_return_vera_type` (and `_infer_fold_init_vera_type`'s SlotRef-init fallback) returned the canonical NamedType's bare `.name` — `"Future"`, the type argument dropped — *before* the element deciders run, so the #1045/#1057 element fixes and the #1074 payload canonicalizer never reached this path, and the DIRECT `Future<Int>` spelling mis-sized too, not only aliases. `array_map` rejects an async closure, so the trigger is a pure closure forwarding an existing Future value — the identity/reshuffle pattern. Failure modes behind a check+verify-green, exit-0 compile split by payload: Int/Nat/Float64/String payloads **trapped** — `indirect call type mismatch` at run for map/mapi (the registered `call_indirect` signature said the closure returns i32 while the closure compiled returning i64/f64/i32_pair), `expected i32, found i64` at WASM validation for a fold accumulator (the acc local was typed i32 while the init pushes i64) — while Bool payloads were **silently wrong** (both signatures agree at i32, so the 4-byte-stride stores simply misread through the 1-byte-packed index reads). Both sites now render the FULL compound spelling — `_format_named_type` plus the #1074-extended `_canonicalize_alias_slot_name` payload walk — covering the direct, aliased-element (`type FI = Future<Int>`), and aliased-payload (`Future<Big>`, `type Big = Int`) spellings, the `array_mapi` and `array_fold` twins, a block-wrapped closure literal reaching the SlotRef-init fallback, and a chained `array_concat(array_map(...), …)`. Twelve regression tests, each mutation-validated against its exact pre-fix failure.
- **`array_map` over `Array<Future<Bool>>` reads back the values it wrote** ([#1081](https://github.com/aallan/vera/issues/1081)). A silent regression the #1041 merge introduced and the #1079 write-side fix closes: #1041's element-READ stripping sizes `Future<Bool>` index reads at the payload's 1-byte stride, but `array_map`'s element WRITE path still inferred the bare head `"Future"` (#1079's mechanism) and stored at the 4-byte default — the previously-consistent 4/4 round-trip became 1/4, and an identity map over `[false, true, false, true]` read back all-false (2222 for the base-correct 2121) with every downstream element misread. Pinned with the base-correct 2121-pattern regression test plus a no-map control from the adversarial review of the merge.
- **Collection-alias `Array<Future<Alias>>` elements canonicalize through the array combinators** ([#1082](https://github.com/aallan/vera/issues/1082)). The #1074 residual arm, found by the #1041-merge adversarial review: `_infer_concat_elem_type`'s collection-alias arm (#1064) resolves `type Rows = Array<Future<FlagA>>` to the target spelling and extracts the element — but returned a COMPOUND element (`Future<FlagA>`) verbatim, skipping the payload canonicalization the sibling bare-element branch performs (`type FF = Future<FlagA>; type Rows = Array<FF>` already worked). `_strip_future` then handed the size dict the unresolved alias `FlagA`, which fell to the 4-byte default: silent wrong values through `array_concat` / `array_reverse` for Bool AND i64 payloads (concat of two distinct >2^32 futures returned 0; reverse returned byte-scrambled garbage) and coincidence-dependent reads through `array_slice` / `array_filter`, all check+verify-green. On this branch the arm's verbatim return is neutralized by the #1067 single-exit canonicalization (`_infer_concat_elem_type` canonicalizes every raw result, and the #1074-extended payload walk resolves `Future<FlagA>` → `Future<Bool>`); six regression tests — including the previously-coincidence-green slice shape — pin the behavior, four of them flipping RED if the exit canonicalization is removed.
- **Nested generic where-helpers are parent-qualified mono bases** ([#1014](https://github.com/aallan/vera/issues/1014)). Two same-named `forall` helpers under different parents were keyed flat, first-seen-wins, so the second parent's call silently ran the first parent's body (check-green, wrong value). A shared transform now qualifies every nested generic base (`a$where$g`) and rewrites its lexically-visible calls, on both the codegen and verifier sides — the same collision also merged the two helpers' instance sets in the verifier, which now keys them distinctly.
- **Imported module where-helpers get the parent-qualified hoist** ([#1015](https://github.com/aallan/vera/issues/1015)). Two imported functions each carrying a same-named non-generic `where`-helper silently resolved to the first one's body; each resolved module's AST now receives the same #991 hoist the main program gets before registration.
- **A grandchild helper calling an aunt inside a generic clone's where-tree compiles** ([#1012](https://github.com/aallan/vera/issues/1012)). The generic clone hoister rewrote calls with this-level names only; it now threads the full ancestor scope, mirroring the non-generic hoister, so the call resolves to the hoisted aunt instead of dangling at WAT assembly.
- **A `forall` helper under a generic ancestor is instantiated per clone** ([#1002](https://github.com/aallan/vera/issues/1002)). Clone hoisting substituted only the ancestor's type variables, leaving an own-`forall` helper as a still-generic template whose call dangled on a check-green, verify-green program; a per-clone instantiation worklist now clones it at its concrete call sites, and the verifier discovers and verifies the same instances (a lying nested contract is a loud E500).
- **Imported function bodies seed generic-instantiation discovery** ([#999](https://github.com/aallan/vera/issues/999)). An imported non-generic function calling its own nested `forall` helper compiled to a dangling name because discovery only scanned the importer's declarations; resolved-module bodies are now qualified (module-namespaced, so two modules' same-named nested helpers stay distinct bases), harvested, and seeded on both the codegen and verifier sides, with #998 origin threading so clones compile against their module's span tables; nested-helper instances key on one canonical concrete-free module-qualified chain across emission, discovery, and per-instance verification.
- **Private module generics reached transitively through a public generic are harvested — module-qualified** ([#1000](https://github.com/aallan/vera/issues/1000), supersedes draft PR #1026). The public generic's clone body call now routes to the module's own private helper under `mod$<path>$<name>` — never a bare name, so a same-named local function keeps its body in BOTH directions (pre-fix the transitive call was silently captured by the local). Every imported declaration's private-generic calls are routed — non-generic callers and private-to-private chains included — and the verifier discovers and verifies the same qualified instances on every path (including through a locally-shadowed public generic), so a lying private contract is a loud E500 even when another module has a truthful same-named one.
- **A module function constructing its own ADT compiles regardless of the importer's type imports** ([#1008](https://github.com/aallan/vera/issues/1008)). Constructor layouts were registered per the importer's filter, so `make`'s body dropped to `unknown constructor` unless the importer also named the type — module-own layouts (public, private, or out-of-filter) now register unconditionally, while importer visibility and the checker's E210/E320 rails are unchanged.
- **`vera compile` prints the E602 "function skipped" warning on the text error path** ([#1004](https://github.com/aallan/vera/issues/1004)). When a `CodegenSkip` drops a *called* function, the caller's dangling `call $f` fails WAT assembly with an opaque `unknown func`; the warning explaining *why* the function was dropped was suppressed on the text error path (the `--json` envelope already carried it) and is now printed alongside the error. The type-error and `--target wasi-p2` family-gate text paths are corrected the same way, so a warning alongside those errors is no longer dropped.
- **The `@Int -> @Nat` narrowing into an `apply_fn` closure formal is now obligated and runtime-guarded** ([#1017](https://github.com/aallan/vera/issues/1017)). A provably-negative `@Int` argument narrowing into a `@Nat` closure formal via `apply_fn` verified clean (a false Tier 1) and, for a runtime value, entered the formal reinterpreted with no trap. The verifier's `apply_fn` branch now emits the `@Nat` narrowing obligation (mirroring the generic call-argument path — E503 for a provable negative, a guarded Tier 3 otherwise) and code generation guards the `call_indirect` argument — the narrowing dual of the #820 argument-widening handler at this site.
- **The refinement-predicate narrowing into an `apply_fn` closure formal is now obligated and runtime-guarded** ([#1024](https://github.com/aallan/vera/issues/1024)). `apply_fn(f, 0)` where `f`'s formal is `{ @Nat | @Nat.0 > 0 }` verified clean (a false Tier 1) and ran silently: a refinement over `@Nat` *is* a `@Nat` type, so the #1017 handler claimed the site and discharged only the base's `>= 0` — which `0` satisfies — leaving the strict predicate unchecked with no runtime backstop. The verifier's `apply_fn` branch now discharges the *full* predicate refined-first (ahead of the #1017 `@Nat` arm, mirroring the generic call-argument path — E505 for a provable violation, a guarded Tier 3 otherwise), and code generation guards each refined closure formal at the lifted body's prologue — the closure-side dual of the refined-parameter guard named functions already carry.
- **A refined closure RETURN is now obligated and runtime-guarded** ([#1032](https://github.com/aallan/vera/issues/1032), the return-side dual of #1024, found while fixing it). `fn(@Int -> @Pos) { @Int.0 }` (with `Pos = { @Nat | @Nat.0 > 0 }`) applied to `-5` — or to `0`, which clears the `@Nat` base's `>= 0` — returned the violating value through the refined slot on a verify-clean program: the verifier's closure-return handling covered the widening (#820) and bare-`@Nat` narrowing (#984) directions but no refinement, and the lifted closure emitted no return guard while the #984 gate excluded refinements assuming a boundary guard that did not exist. The verifier now records a refined closure return as a guarded Tier 3 (the closure body is opaque to the SMT layer — never a false Tier 1), and code generation checks the lifted body's return value against the full predicate, mirroring the named path's refined-return guard, so a violating value traps with the refinement message instead of leaking. The refinement obligation stream is also now honest about the boundaries codegen cannot guard: a refined base with a non-plain type argument (`Array<{ @Int | ... }>`-style) records `tier3_unguarded` (E506 disclosure) at every parameter/return boundary — named or closure — instead of claiming a runtime guard that never fires ([#1036](https://github.com/aallan/vera/issues/1036) tracks emitting the guard itself).
- **`Map` / `Set` entries and `array_concat` over `map_values` handle representation-transparent `Future<T>` values** ([#1097](https://github.com/aallan/vera/issues/1097)). A `Map<String, Future<Int>>` value round-trip (`map_insert` / `map_get` / `map_values`) was check- and verify-green but compiled exit-0 to an INVALID module. `Future<T>` is representation-transparent ([#841](https://github.com/aallan/vera/issues/841)) — a `Future<Int>` IS an i64, a `Future<String>` an i32_pair — but the Map/Set host-tag classifier (`_map_wasm_tag`, shared by keys, values, and Set elements) did not strip the transparent wrapper, so a `Future<Int>` value fell through to the `"b"` (single-i32) tag while the value expression pushed an i64: the registered host import disagreed with the stack ("type mismatch: expected i32, found i64", or "values remaining on stack" for the pair payload). The classifier now canonicalizes an alias name (`type FI = Future<Int>;`) and strips `Future<…>` wrappers to the payload before tagging — so a `Future<Unit>` value still takes the zero-size loud [E602] skip and a `Future<Array<Int>>` value the existing Array-reject skip, never an invalid module — and the same fix covers `Set<Future<T>>` elements and `Future`-typed map keys. A second, latent site rode behind it: `array_concat(map_values(m), map_values(m))` over such a map ran a wrong-stride copy (silent garbage) — the two rebuilt arms of the array combinators' element-type inference (`_infer_concat_elem_type`) returned the bare head `"Future"` with the type argument dropped, so the copy loop strided the 8-byte i64 payloads at the 4-byte i32 default, while the let-bound `@Array<Future<Int>>` SlotRef form was already correct ([#1057](https://github.com/aallan/vera/issues/1057)). Both arms now mirror the direct SlotRef arm through a shared helper, preserving the full `Future<…>` spelling.

### Documentation

- **Spec §11.8 now matches code generation: every non-trivial contract is compiled as a runtime check regardless of the verifier's tier** ([#958](https://github.com/aallan/vera/issues/958)). The section previously claimed Tier-1-proved contracts are omitted from the compiled output; code generation is tier-agnostic (`vera/codegen/contracts.py` has no notion of tiers), so a Tier-1 proof means the emitted check provably never fires, not that it is absent — the runtime guard is the deliberate backstop that keeps a *false* Tier 1 a loud trap rather than a silent wrong answer.
- **`scripts/check_pypi_readme_examples.py` gates the PyPI project page's Vera blocks** at parse + check + verify (pre-commit hook `pypi-readme-examples` + a CI lint step): the Try-it program on PyPI is now held to the same standard as the other doc surfaces, and a broken contract in it fails the gate.
- Spec §0.5.1 and SKILL.md now state that `?` inside a type printed by a diagnostic marks a component the checker could not infer (`Array<?>`) — a rendering marker distinct from the typed-hole expression `?` (§4.17) written in source.
- The concurrent-await alias-`Future` limitation (spec §9.5.4) is now anchored to a tracking issue ([#1095](https://github.com/aallan/vera/issues/1095)), with a matching `KNOWN_ISSUES.md` limitation row: confirming it still reproduces requires a genuinely concurrent repro, since an eager-path probe never reaches the handle-check.

## [0.1.4] - 2026-07-11

### Fixed

- **Duplicate `where`-helper names now compile and verify correctly via parent-qualified mangling instead of crashing WAT assembly or proving against the wrong helper** ([#991](https://github.com/aallan/vera/issues/991)).  Non-generic where-helper WAT names were flat and unmangled and `_fn_sigs` registration was last-wins, so two same-named helpers in different parent subtrees (a `leaf` under each of two siblings), or a helper named like a top-level function, collided in the single flat WAT namespace: `vera compile` failed with a raw `duplicate func identifier` on a check-green program (before the #978 nested-emission fix one variant was worse — a colliding grandchild was silently dropped and its call bound to the same-named top-level function, a silent wrong value).  A related verifier facet: the flat, last-wins `env.functions` lookup assumed the WRONG same-named helper's `ensures` at a call site, reporting a false E500 in a diamond of same-named helpers with different postconditions.  The DESIGN call is parent-qualified mangling, not a checker rejection: spec §5 (`spec/05-functions.md`) makes where-helpers "always local to the parent function", so two same-named helpers under different parents are a semantically VALID program — the collision was codegen's flat namespace leaking, not a user error — and mangling gives one canonical treatment of helper symbols across the generic and non-generic paths (the generic path already parent-qualifies its per-clone hoists, `gid$Int$where$helper`; DESIGN principle 3).  Codegen now hoists every non-generic where-helper to a parent-qualified top-level decl (`compute$where$branchA$where$leaf`) before registration, rewriting every lexically-visible helper call in the parent's body and in each nested helper's body to the mangled target — the non-generic mirror of the #904 clone hoist — so registration, monomorphization discovery, and Pass-2 emission all see collision-free names; generic helpers stay structurally nested for the mono path (each is a monomorphization base, `#990`/`#904`), with their bodies rewritten shadow-aware (see below).  Resolution is lexical throughout: a helper's bare call binds to the NEAREST same-named helper in the enclosing `where`-tree — its own children, then any ancestor's (a grandchild calling an "aunt" — a sibling of its parent — is redirected across scope levels), with an inner helper shadowing an outer same-named one for its subtree.  The verifier and the CHECKER resolve a bare helper call the same way — the nearest same-named helper in the enclosing `where`-tree (own children first, then each ancestor's), then the top-level function, then the flat registry — so each parent proves and type-checks against its OWN helper: the review's third round found the checker still resolving through the flat last-wins registry, which falsely REJECTED (E121) a valid diamond whose two same-named `leaf`s differ in signature (branchA's `@Int -> @Int` call synthesized against branchB's `@Int -> @String` leaf, registered last); the checker now threads a `_fn_scope_stack` through `_check_fn` and resolves calls (and the commutativity analysis's effect-row lookups) lexically, completing the issue's checker/verifier/codegen agreement clause — with the #969/#977 slot-isolation and #815 builtin-redefinition (E151) invariants pinned intact by the negative conformance fixtures.  Top-level names stay bare (exports and `execute(fn_name=…)` lookups depend on them) and generic clones are never double-mangled.  The hoist also corrects the IMPORT door (review round 4): a non-generic helper's name no longer suppresses a same-named import's bare emission — the `_local_shadowed_fn_names` collection now walks the POST-hoist program, so outside the parent the import wins, exactly as the lexical resolution rules state.  At base this shape was a **silent wrong body**: the helper's bare emission captured a top-level import-bound call (`leaf(0)` ran the helper's `+ 1` instead of the import's `+ 7` — 101 where the correct result is 701); the first cut upgraded it to a loud `unknown func` (the stale pre-hoist shadow suppressed the import while the helper no longer occupied the bare name); the fix resolves it correctly — the import through its bare emission, the parent's call through its own parent-qualified helper.  A top-level local sharing an import's name still shadows it (§8.5.2), and a RETAINED generic helper's name still shadows: an uninstantiated T-unused generic template still emits under its bare name, which would otherwise collide with the import's bare emission (pinned by `tests/test_xmod_where_helper_import_991.py`).  The PR #1013 review then closed three residuals of the first cut, all in how the rewrite met GENERIC subtrees: (1) the ancestor-scope rewrite descended into retained generic helpers' bodies without re-applying their own inner shadowing, so a generic helper's call to its OWN nested `shared` was captured onto an ancestor's `p$where$shared` — mono cloned it with the call pre-rewritten, so the per-clone redirect never fired: a silent wrong value (base returned 35, the capture returned 25) and, contracted, a **false Tier-1** (verify proved the contract against the right helper while the compiled program ran the wrong one and trapped) — the descent is now shadow-aware per level, and a generic helper's NAME also erases a same-named ancestor entry for its level's subtree; (2) a fully-concrete (T-unused) generic helper template compiles, and its dead emission dangled once its own helpers moved to per-clone symbols (pre-#991 it WAT-validated only by resolving to a same-named ancestor's bare emission) — the dead template WAT is now dropped when clones are registered, keeping the `@T`-template warning surface and the uninstantiated-generic fallback; (3) the verifier's per-instantiation dispatch dropped the `enclosing` ancestor chain, so a nested generic helper's clone resolved an unshadowed ancestor-helper call through the flat last-wins registry, where a same-named decoy helper under any other function captured it (a false E500 against the decoy's contract on a correct program) — `_verify_generic_instances` now threads the chain into each clone's verification.  Pinned by `tests/test_codegen_where_helper_mangling_991.py` (sibling-`leaf` collision, helper-shadows-top-level with both bodies independently reachable, ancestor-scope and shadowing shapes, a collision coexisting with a nested generic, the generic-subtree capture shapes including the false-Tier-1 verify+run differential, and the checker-leg differing-signature diamond checked AND run — every run assertion checks a value that only its OWN helper body yields) and `tests/test_verifier_where_helper_scope_991.py` (the diamond verifies, a genuine-violation guardrail still caught, and the decoy-capture clone-scope shape verifies with per-function Tier-1 obligation checks and runs both doors), with each mechanism — emission mangling, call-name rewriting, verifier scoping, checker scoping, shadow-aware generic descent, generic-name erasure, and the clone-chain threading — reverted to a RED test.
- **The static E503 `@Nat`-narrowing obligation now fires for the concrete components of a partially-generic constructor argument** ([#1010](https://github.com/aallan/vera/issues/1010)).  Every re-synthesis in the generic-call argument loop was gated on the WHOLE parameter type being typevar-free, so a constructor argument against `@Pair<Nat, T>` was never re-typed against its instantiated parameter — the concrete `Nat` component's narrowing target went unrecorded, and `wants(MkPair(0 - 5, None))` checked, verified **clean**, and silently stored the negative (the fully-concrete `@Pair<Nat, Int>` analogue was E503 at verify).  Constructor-expression arguments are now re-synthesized against the instantiated parameter even when type variables remain — the #971 fill adopts component-wise, typevar components stay unconstrained, and each field's target is recorded exactly as on the concrete path, so the provably-negative shape is E503, an unconstrained `@Int` param into the component is E503 with its counterexample, and the `requires(@Int.0 >= 0)` variant discharges Tier-1.  The runtime-guard half for generic fields is unchanged and stays documented under the #754/#757 limitation rows (the static promise is what #1010 restored).  Found by the PR #1009 review; pinned by five tests in `tests/test_verifier_nat_obligations.py` (three fixed shapes, a no-false-positive guard, and the concrete control) with a revert-the-fix mutant killed.
- **A bare `None` (nullary constructor) as a call argument now adopts the expected type** ([#993](https://github.com/aallan/vera/issues/993)).  Final mechanism of the fresh-ctor-var family (return/`let`/match [#971](https://github.com/aallan/vera/issues/971), nested ctor fields [#979](https://github.com/aallan/vera/issues/979), comparison operands [#981](https://github.com/aallan/vera/issues/981)): five argument-position sites still minted an unresolvable `T$n` and rejected well-typed programs.  (1) The #971 bidirectional fill now overrides a tentative type-arg binding whose value is a *bare fresh var* — the same fresh-is-tentative precedence `_unify_for_inference` applies between arguments (#293) — fixing `MkA(None)` under a bare forall var (E121, the nested nullary ctor's own placeholder had leaked into the instantiation).  (2) The generic-call argument loop treats a residual type variable on either side as a structural wildcard before rejecting — `option_unwrap_or(nothing(()), None)` (E202) is satisfiable at some instantiation when every argument is itself polymorphic, while a cross-ADT or concrete-leaf mismatch is still rejected.  (3) Ability-op constructor arguments re-synthesize against the resolved parameter type — `ensures(eq(@Option<Int>.result, None))` (E241) now checks at concrete *and* forall expected types, matching the operator form fixed by #981.  (4) The handler-state initializer synthesizes with the declared state type expected, so `handle[State<Option<Int>>](@Option<Int> = None)` (E331) checks.  (5) A both-constructor comparison may adopt from a *resolved* constructor sibling — `Some(5) == None` (E142) checks, while `None == None` (no side to adopt from) stays rejected.  The PR #1009 review round then hardened all five against the rigid/fresh distinction: the wildcard is scoped to genuinely *unresolved* vars — fresh `T$n`, `#b`-marked builtin vars, and callee forall vars leaked from an uninferrable call — while the enclosing function's own declared forall params stay **rigid** (passing `@Option<T>.0` where `Option<Int>` is required is E202 again; the first cut wildcarded every TypeVar, a soundness regression caught in review), a function type's effect row is never wildcarded (`<IO>` cannot satisfy a `pure` formal), the ability-op mapping no longer locks onto the first argument's fresh var (`eq(None, Some(5))` and `ensures(eq(None, @Option<Int>.result))` re-anchor on the later resolved argument and check, while `eq(None, None)` stays rejected — a fresh-holed param is not a resolved type to adopt), and the both-ctor adoption guard distinguishes rigid from fresh (`Some(@T.0) == None` under `forall<T where Eq<T>>` checks; `None == None` still rejected).  Pinned by eighteen tests in `tests/test_checker_types.py` (twelve fixed shapes incl. every reversed operand order + six guardrail rejections) with a twelve-mutant battery: each mechanism reverted flips its test RED, and every over-broadening mutant (cross-ADT wildcard, rigid-var wildcard, effect-row wildcard, unresolved-sibling adoption, fresh-param adoption) is killed by a guardrail pin.  The review also surfaced a **pre-existing** static-obligation gap, filed as [#1010](https://github.com/aallan/vera/issues/1010): the E503 `@Nat`-narrowing obligation is lost when a constructor argument's parameter type contains a typevar (`MkPair(0 - 5, None)` as `@Pair<Nat, T>` verifies clean and silently stores the negative) — identical at the integration base; fixed by the #1010 entry above.
- **`eq(...)`/`compare(...)` in imported function bodies now compile** ([#992](https://github.com/aallan/vera/issues/992)).  The Pass-1.6 ability-op rewrite canonicalized the two AST-rewritten ability operations for the top-level program's declarations and the mono clones but never for the imported populations, which compile directly in Pass 2.5/2.6 — an `eq` anywhere in an imported body (top-level, where-helper, or nested grandchild) stayed a raw call codegen cannot lower, the body was dropped, and the importer's call dangled at WAT assembly (`unknown func`) on a check-green, verify-green pair.  The rewrite now runs over every `_imported_fn_decls` / `_shadowed_module_fns` entry (the flattened where-tree, so each helper is its own entry; the rewrite is idempotent, so a parent's carried subtree cannot double-transform).  Pinned by `tests/test_xmod_ability_ops_992.py` — `eq` at all three nesting depths, `compare`, and the shadowed (`mod$…`) door — with a drop-the-rewrite mutant killed.
- **A nested `handle[State<T>]`'s initial-state expression now observes the ENCLOSING scope's state** ([#976](https://github.com/aallan/vera/issues/976) review, pre-existing).  `_translate_handle_state` pushed the fresh inner cell BEFORE evaluating the init expression, so a `get(())` in the init — lexically an outer handler's operation — read the new inner cell's default 0 instead of the outer state: `handle[State<Int>](@Int = get(()) + 5)` under an outer handler holding 100 silently initialized to 5, check- and verify-green.  The init expression is now evaluated first (it belongs to the enclosing scope), then the fresh cell is pushed and the value stored.  Found by PR #1003's adversarial panel; pinned by ledger tests (105 and 205 shapes) in `tests/test_state_clause_semantics.py`.
- **`handle[State<T>]` operation clauses now execute, with intrinsic-hybrid semantics** ([#976](https://github.com/aallan/vera/issues/976), closing the §7.5 spec contradiction [#988](https://github.com/aallan/vera/issues/988)).  Clause bodies and `with` state-update expressions were type-checked but never lowered — `_translate_handle_state` compiled the handled body against the builtin host-side state cell and dropped `expr.clauses` wholesale, so a clause that transformed `resume`'s argument or a `with` value differing from `put`'s argument was silently discarded (the corpus never noticed: every fixture's clauses mimicked the builtin semantics exactly).  Under the maintainer-pinned option-C semantics: `put` stores / `get` reads **intrinsically** (the operations' declared meaning, independent of the clauses); the matching clause body **executes** with its `resume(value)` as the op's result at the call site; `with @T = expr` **overrides** the intrinsic store; and the clause's `@T.0` is captured **pre-store**, so `with @T = @T.0` means *keep the old state*.  The lowering inlines the clause at each get/put call site over the existing host-cell imports (capture → intrinsic store → clause body with `resume(v)` lowered to the value → optional override) — no host changes, wasmtime and browser in lockstep by construction, wasi-p2 unchanged (State stays unsupported there).  `resume` in a `State` clause is single-shot and tail-position enforced (a missing, repeated, or non-tail `resume` skips the function loudly; multi-shot stays FUTURE).  Because `with @T = @T.0` flips from a silent no-op to a meaningful keep-old override, the corpus's redundant `with @T = @T.0` clauses (written as no-ops under the dropped-clause behaviour) are migrated to the canonical no-`with` form, which is an exact identity under the new semantics.  Verifier posture unchanged: obligations inside `handle` bodies stay Tier-3 (#439), and the runtime checks now observe the clause-transformed values.  Spec §7.5.1/§7.5.2 rewritten to state the semantics explicitly (closes #988; the §7.5.3 example's result is unchanged).  Pinned by `tests/test_state_clause_semantics.py` (transform, override, keep-old canary, composite-state transform incl. a captured-pointer-after-alloc shape probed green under `VERA_EAGER_GC`, non-tail-resume rejection, and canonical-identity controls) and a run-level conformance program (`tests/conformance/ch07_state_clause_transform.vera`).
- **The `@Nat` → `@Int` per-component widening guards now fire for imported module bodies, closing the cross-module half of the #820 guard wave where a library's `vera verify` promise was silently dropped through the import door** ([#987](https://github.com/aallan/vera/issues/987)).  #820 threaded the checker's `expr_target_types` table into code generation, but that table is keyed by bare span (no file identity) and computed for the top-level program only — so an imported function body compiled into the importer's flat WASM module (`vera run`/`compile`/`test`, browser, and wasi-p2 all share the artifact) found no entry and dropped the array-element and tuple-construction widen guards, and a `@Nat` above `2^63 - 1` reinterpreted to a negative `@Int` (`u64.MAX` → `-1`) with no trap even though the library's own `vera verify` reported the site Tier-3.  (The tuple-*destructure* guard, recovered structurally from the `let Tuple<@Int, …>` binding pattern rather than the span table, already crossed the door, as did the closure positions, recovered from the closure's function type.)  PR #986's interim mitigation *suppressed* span-keyed lookups for imported bodies entirely — necessary because an engineered cross-file span collision could otherwise hand an all-`@Nat` imported body the importer's `Tuple<Int, Int>` target and emit a *spurious* guard that false-trapped a legal `@Nat`.  The fix computes each resolved module's OWN span-keyed target/semantic side-tables (`CheckArtifacts.module_artifacts`, keyed by module path, with each module's `direct` imports re-derived so its check matches a standalone one) and threads them through `compile()` → `CodeGenerator` → `_compile_fn(module_tables=…)`, so an imported body is resolved against *its* module's table (correct spans, no cross-file collision) rather than the importer's — flipping the collision guarantee from suppression to correctness (the collision fixtures now prove no guard because the module's own target is genuinely `Tuple<Nat, Nat>`, `tests/test_xmod_span_collision.py`), and reaching transitively-imported and shadowed (`mod$…`) module bodies too.  A module with no threaded table falls back to the #986 suppression, never a wrong-file guard.  The per-module collection is **opt-in** (`collect_module_artifacts=` on `typecheck_with_artifacts`, default off): it is O(N²) sub-checks in the module count (each of N resolved modules gets a full `check_program` re-registering the other N−1), so only the codegen-bound callers (`vera compile`/`run`/`serve`/`test`) request it — `vera verify` and the warm `VerificationSession`, which read only the top-level target tables, skip the pass entirely (PR #997 review).  The verifier needs no change — its per-module classification was already correct; the promise the codegen artifact was breaking.  Pinned by a cross-module verifier↔codegen differential run *through the import door* (`tests/test_xmod_widening_differential.py`: array-element, tuple-construction, transitive 3-level, and shadowed-import shapes each trap at `u64.MAX` and round-trip `2^63 - 1`/`42`, with a tuple-destructure control) and a run-level conformance program (`tests/conformance/ch08_xmod_widen.vera`).  A separate pre-existing residual is unchanged and out of scope: a compile-time `@Nat` *literal* above `2^63 - 1` folded directly into an `Array<Int>`/`Tuple<Int, …>` still constant-folds to a `verify`-clean value that runs negative at both same-file and cross-module — a const-fold disclosure gap, not this cross-module threading.  A second pre-existing residual is disclosed here and tracked as [#998](https://github.com/aallan/vera/issues/998): an imported **generic** function's mono clones compile on the monomorphization path *without* the threaded module table, so a concrete `Array<Int>`/`Tuple<Int, Int>` widening inside a `forall<T>` import — which the library's own `vera verify` still classifies Tier-3 once instantiated — is *not* runtime-guarded at any instantiation reached through the import door (the `@Nat` above `i64.MAX` silently reads back as `-1`, the same broken-promise shape this entry closes for *monomorphic* imports).  Unlike the const-fold gap it was not verifier-disclosed (still reported Tier-3), making it an unsound residual — closed in this release by the #998 entry below.
- **A `forall<T>` `where`-helper under a non-generic parent is now monomorphized, so its concrete call sites compile instead of dangling at an internal unknown-func error** ([#990](https://github.com/aallan/vera/issues/990)).  Generic discovery collected mono bases from top-level declarations only, so a generic helper nested in a non-generic function's `where` block was invisible: no clone was emitted, the parent's concrete-typed call lowered to the bare unmangled name, and `vera compile` failed WAT assembly on a check-green, verify-green program.  Both sides now build their base set with a SHARED collector (`vera/monomorphize.py` `collect_nested_generic_decls`): codegen Pass 1.5 emits clones for nested generics whose whole ancestor chain is non-generic (stopping at generic helpers — their subtrees are carried per-clone and hoisted by the #904 path, so nothing double-emits), and the verifier's instance discovery collects the identical set, so each nested instantiation is verified per-monomorphization exactly like a top-level generic's (`_verify_fn`'s forall dispatch already keys on the decl name).  The Pass-2 where-fn sweep stops at a generic template's subtree (the template itself still surfaces the standard uncompilable-template warning, and the clone-compiled suppression set now includes nested templates).  Codegen⊇verifier lockstep is pinned by a `nested_generic_where_helper` corpus entry in the #732 differential (`tests/test_monomorphize_differential.py`), behaviour by `tests/test_generic_where_helper_990.py` (direct, grandchild, two-instantiation, own-where-child, and #904-control shapes with WAT-level single-emission assertions) and a run-level conformance program (`tests/conformance/ch09_generic_where_nongeneric_parent.vera`).  Two sibling gaps found by this fix's grounding probes are pre-existing and tracked separately: a nested generic inside an *imported* function ([#999](https://github.com/aallan/vera/issues/999)) and a *private* module generic reached transitively from an exported one ([#1000](https://github.com/aallan/vera/issues/1000)).
- **Monomorphized clones of imported generic functions now carry their origin module and compile against that module's span-keyed tables, so the #820 per-component `@Nat` → `@Int` widen guards fire at every instantiation through the import door** ([#998](https://github.com/aallan/vera/issues/998)).  #987 threaded per-module tables into non-generic imported bodies, but clones of a `forall<T>` import compiled on the monomorphization path with the IMPORTER's tables (no entries for the library body's spans), so a library generic widening `@Nat` into a concrete `Array<Int>`/`Tuple<Int, Int>` component ran unguarded (`u64.MAX` → `-1`, no trap) while the library's standalone `vera verify` promised the site Tier-3 — the same broken-promise shape #987 closed for monomorphic imports.  The module harvest now records each unshadowed imported generic's origin path (`_imported_generic_origins`), every emitted clone of an imported base is tagged with it (`_mono_clone_origins` — the main worklist, the shadowed `mod$…` clones, the shadowed-body transitive chase, and the per-clone hoisted `where`-helpers, which inherit their clone's origin), and the mono compile loop threads `imported=True` + that module's `module_artifacts` table for tagged clones exactly like Pass 2.5/2.6 bodies (monomorphization preserves node spans, so the template module's table keys the clone's body correctly; absent tables fall back to the #986 suppression, never a wrong-file guard).  A local generic's clones carry no tag and keep the main-file tables — pinned by an explicit control.  `tests/test_xmod_generic_widen_gap.py` flips from the honest gap pin to the guarded differential: array-element and tuple-construction sites × `T=Bool`/`T=Int` instantiations trap `unreachable` at `u64.MAX` and round-trip in-range values, through both the bare-call and shadowed (`lib::wrap` → `mod$…`) doors, plus a hoisted-where-helper-widen scenario and the local-clone control.
- **An `@Int` → `@Nat` narrowing at a lifted closure's return is now obligated and runtime-guarded, closing the last residual of the #758 return-narrowing hole** ([#984](https://github.com/aallan/vera/issues/984)).  `fn(@Int -> @Nat) { @Int.0 }` applied to a negative value returned it through the `@Nat` slot silently on a `verify`-clean program: top-level functions and `where`-helpers were covered by #758's return obligation + guard, but the closure body's return leaf — reachable only through `_compile_lifted_closure` — was neither obligated by the verifier nor guarded by codegen.  This is the narrowing dual of the #820 closure-return `@Nat` → `@Int` widening and hangs off the same lifted-closure hook.  The verifier's `AnonFn` arm gains the narrowing case (a `_is_nat_type` resolved return whose body `_return_narrows_into_nat`): because the closure body is opaque to the SMT layer (translating it against the outer slot env could mis-resolve a closure parameter and prove a false Tier 1), it is obligated shallow-syntactically as a Tier-3 `nat_bind`, never a false Tier-1 `verified`.  Codegen guards the closure return PER NARROWING LEAF (`_nat_return_leaf_ids` threaded into the lifted body, exactly as `_compile_fn` does for the top-level return) rather than as a whole-body wrap — a wrap would false-trap a legitimate `@Nat` leaf of a heterogeneous body (a captured `@Nat` above `i64.MAX` reads as a negative `i64`); an alias-aware, refinement-excluded gate mirrors the top-level narrow-return gate, so an already-`@Nat` closure return and an `@Int` → `@Int` closure stay untouched.  The verifier↔codegen agreement is pinned by an extended `tests/test_nat_narrowing_return_differential.py` battery (trapping, over-guarded-but-safe abs, and non-narrowing control shapes, plus a pin of the #985 nested-closure residual) and a run-level conformance program (`tests/conformance/ch05_closure_nat_return.vera`).  One residual is tracked honestly: a closure nested inside *another closure's body* is guarded by codegen but not obligated by the verifier (the `AnonFn` walk is deliberately shallow) — [#985](https://github.com/aallan/vera/issues/985), whose scope now covers this narrowing direction alongside its original widening one.
- **A `where`-helper that carries its OWN `where`-helpers now has those nested helpers emitted by the non-generic codegen path, closing a check/verify-green-then-compile-fail divergence** ([#978](https://github.com/aallan/vera/issues/978)).  A non-generic `fn outer { … } where { fn child { … } where { fn grandchild … } }` passed `vera check` and `vera verify` but crashed `vera compile` with `unknown func: $grandchild` at WAT assembly: the checker (`_check_fn`), verifier (`_verify_fn`), and registration (`_register_fn`) all recurse into nested `where` blocks — so `grandchild`'s name was registered and `child`'s body lowered its call to `return_call $grandchild` — but the Pass-2 emission loop in `vera/codegen/core.py` compiled only ONE level of `decl.where_fns`, so a nested helper's body was never emitted and the reference dangled.  Single-level `where` blocks were unaffected, and the generic-parent path already recursed (`monomorphize._hoist_where_fns_under` flattens the whole helper tree per clone), so the identical program under a `forall<T>` parent compiled and ran.  The non-generic loop now flattens `decl.where_fns` recursively to arbitrary depth (pre-order, with an `id`-keyed visited guard) and emits every helper.  The verifier already descended (a false `ensures` on a grandchild is caught at Tier 1), so this is a codegen-only completeness fix, no verify change.  Pinned end-to-end by `tests/conformance/ch05_nested_where_helpers.vera` (a two-level and a three-level chain, each helper carrying a meaningful `ensures` so the leaf's contribution is proven up the chain at Tier 1 and the whole runs to a leaf-dependent value) and unit coverage over the two- and three-level shapes plus the generic-parent control (`tests/test_codegen_monomorphize.py::TestNestedWhereHelperEmission978`).  PR review ([#989](https://github.com/aallan/vera/issues/989)) found and closed two residuals of the same divergence.  (1) **Ability-op rewrite recursion**: Pass 1.6 (`_rewrite_where_fns`, in `vera/codegen/core.py`) rewrote each DIRECT child helper's body + contracts (`eq`/`compare` → operator form) but never recursed into a helper's OWN `where_fns` — so a grandchild using `eq`/`compare` in its body or contracts kept a raw `FnCall`, `_compile_fn` tripped `CodegenSkip` and dropped its body, and the parent's `return_call $grandchild` dangled even though the emission loop had reached it; `_rewrite_where_fns` now recurses (body, contracts, and nested `where_fns`) mirroring the flatten walk.  (2) **Imported-module registration recursion**: `_register_modules` (`vera/codegen/modules.py`) collected an imported function's where-helpers with a one-level loop, so an imported `libfn -> child -> grandchild` chain registered only `child` for Pass-2.5 emission — `grandchild` checked and verified green but never emitted, dangling `child`'s call to it; the registration walk now reuses the same `_flatten_where_fns` the local Pass-2 loop uses, reaching helpers at any depth.  Pinned by three ability-op grandchild shapes plus a branching two-nested-helper shape (`TestNestedWhereHelperEmission978`) and a check/verify/compile/run differential over imported two- and three-level chains (`tests/test_codegen_modules.py::TestImportedNestedWhereEmission989`).
- **The `@Nat` → `@Int` widening is now obligated and runtime-guarded at the array-element, tuple-component, heterogeneous-arm, and closure argument/return sites, closing the last silent `@Nat` widening residuals** ([#820](https://github.com/aallan/vera/issues/820)).  A `@Nat` above `i64.MAX` bit-reinterprets to a negative `@Int` when widened; #813 guarded the sites where code generation can statically see the source `@Nat` (return, `let`, call-argument, concrete `@Int` field, ADT sub-pattern, match-bind), but five sites still widened on a `verify`-clean program — two *silently* (a closure argument/return/capture, and a heterogeneous `if`/`match` whose alternative is a genuine `@Int`-*slot*, both carrying no obligation at all) and three E531-*disclosed* but runtime-unguarded (the array-literal element and the tuple construction/destructure component).  The enabler is a new per-component **target-type** table: the checker's `expr_target_types` side-table (the `expected` type each expression was checked against) is now threaded into code generation — the dual of the verifier's `_target_type_of` — so the erased WASM layouts recover the `@Int` target that the source-typed layout cannot.  With it, code generation guards the array element (target `Array<Int>`), the tuple component at construction and at the destructure read (target `Tuple<…, Int, …>`), the `@Nat` arm of a heterogeneous `@Int`-join `if`/`match` per-arm (the boundary guard cannot fire without false-trapping the genuine `@Int` arm), the closure argument (formal type recovered from the closure's function-type, `call_indirect`-guarded), and the closure return/capture (the closure body's `@Int` return guarded in `_compile_lifted_closure`, obligated shallow-syntactically because the body is opaque to the verifier's SMT layer).  The verifier obligates exactly these sites (`nat_to_int_coerce`, Tier-3 runtime-guarded), and the verifier↔codegen agreement is pinned by the extended `tests/test_int_widening_differential.py` battery.  The per-arm machinery is target-aware and TCO-safe, hardened by the PR's adversarial review: the arm guard fires only when the join's *target* is `@Int` (mirroring the verifier's `_is_hetero_int_widen_join` — without the target check, a heterogeneous join in a `@Nat`-returning function false-trapped a legal `@Nat` above `2^63 - 1`), and an arm whose `@Nat` value is a **tail call** lowers to a plain `call` so the appended guard stays live (a `return_call` would skip it — the widening dual of the #983 per-leaf narrowing lesson; the genuine `@Int` arm's recursive `return_call` is preserved, both pinned by `tests/test_hetero_widen_tailcall.py`).  `vera test` now compiles and verifies with the same artifact tables as the other CLI doors (`tests/test_tester_artifacts.py`), and a user-defined `data Tuple` no longer takes the builtin variadic carrier's target-table path (`Tuple` gated on the carrier's empty layout, not the name).  Two residuals are tracked honestly: [#985](https://github.com/aallan/vera/issues/985) — a closure nested inside *another closure's body* is guarded by codegen but not yet obligated by the verifier (the `AnonFn` walk is deliberately shallow), so `vera verify` under-reports that one runtime check (sound over-guarding, not a widening hole) — and [#987](https://github.com/aallan/vera/issues/987) — the array-element and tuple-construction component guards were initially not emitted for imported module bodies (the target-type table was single-module and span-keyed), now closed by threading each resolved module's own table into code generation (see the dedicated `#987` entry).  The one component site still unguarded — a generic-instantiated `@Int` field (`Some(@Nat.0)` into `Option<Int>`, erased to i64 with no per-field mono metadata) — stays honestly E531-disclosed and is tracked with its narrowing dual (#757); the same target-type enabler now unblocks the deferred `@Int` → `@Nat` narrowing guards (#754/#757/#765) and the closure-return narrowing (#984).
- **A `@Int` value narrowing into a `@Nat` **return** slot is now statically obligated and runtime-guarded, closing a soundness hole where a `vera verify`-clean function could return a negative `@Nat`** ([#758](https://github.com/aallan/vera/issues/758)).  `fn to_nat(@Int -> @Nat) { @Int.0 }` verified clean at Tier 1, yet `to_nat(0 - 5)` returned `-5` through the `@Nat` slot with no trap: the narrowing walker obligated every *binding* site (`let`, call-argument, constructor-field, match-bind, destructure — #552/#747) but never the function's own return slot, and codegen emitted no return coercion guard.  The verifier now emits the `nat_bind` `result >= 0` obligation at the return position — the dual of #813's `@Nat -> @Int` widen-return — discharged under the body's path conditions (so an `if @Int.0 >= 0 then @Int.0 else -@Int.0` tail and `examples/absolute_value.vera` prove at Tier 1, an unconstrained narrowing is a loud `E503`, and an opaque one is an honest Tier-3), and codegen emits the mirroring return guard so an unverified compile traps rather than returning a reinterpreted negative.  Detection descends `if`/`match` joins to their leaf return expressions (the whole target-typed body reads as `@Nat` in the checker's side-table, masking a narrowing arm), and a genuine `@Nat -> @Nat` tail call (`count_down(@Nat.0 - 1)`) is excluded via the side-table / declared-return-type classifier so its `return_call` tail-call optimization is preserved.  The verifier↔codegen site sets are pinned by a return-position differential (`tests/test_nat_narrowing_return_differential.py`) and `tests/conformance/ch04_nat_return_obligation.vera`.  PR review closed two follow-on gaps in the same fix: the codegen return gates now resolve type **aliases** (via the resolver the `let`-site guard already uses) — a `type Count = Nat` return is guarded (narrow) and a `type MyInt = Int` return with a `@Nat` body is guarded (widen), matching the verifier's alias-resolving gates — while an alias-to-refinement (`type Pos = { @Nat | ... }`) stays on its single refinement-boundary guard rather than double-guarding; and the narrowing guard is emitted **per narrowing leaf** during body translation instead of as a whole-body wrap, so a mixed-arm recursion (`drain(@Int -> @Nat) { if @Int.0 == 0 then @Int.0 else drain(@Int.0 - 1) }`) keeps its non-narrowing `@Nat -> @Nat` recursive `return_call` and runs constant-stack — the whole-body wrap had reverted *every* `return_call`, so `drain` lost TCO and stack-exhausted at ~35k depth.
- **A user `forall` type-variable name (`T`, `E`, `A`, `B`, `K`, `U`, `V`) no longer collides with a built-in generic's internal name, so generic-builtin calls over compound argument types check clean** ([#970](https://github.com/aallan/vera/issues/970)).  The inference skip-guard in `_unify_for_inference` compared a concrete argument's type-args against the callee's `forall_vars` *by name*, and the built-in registry named its internal generics `T`/`U`/`A`/`B`/`E`/`K`/`V` — the same letters a user reaches for.  The *filed* bare-`@Array<T>` repro was masked by a name coincidence (the unsubstituted parameter `@Array<T>` happened to equal the argument), but the defect was live whenever the colliding user var was the **immediate** type-argument of a *compound* argument type: `array_length(@Array<Option<T>>.0)` under a user `forall<T>` was rejected with a spurious `E202`, and the issue's own suggested workaround (`forall<E>`) re-triggered it against the `result_*` built-ins.  It fired across every generic-builtin family (`array_*`/`option_*`/`result_*`/`set_*`/`map_*`) and in function bodies, `requires`/`ensures` clauses, and `where`-helpers — a false *rejection* of well-typed programs, never a false accept.  Every internal registry generic name is now alpha-renamed at registration to a parser-unwritable form (suffix `#b`, outside the `UPPER_IDENT` grammar and distinct from the `$` used for fresh inference placeholders), so a user name can never coincide; the skip-guard itself is unchanged, and the marker is stripped from every user-facing surface — `pretty_type` type rendering and the diagnostics in both text and `--json` output never show `#b`.  The same rename exposed and closed a dual completeness gap — given a user-defined `forall<T> fn nothing(@Unit -> @Option<T>)`, a concrete argument now correctly overrides the bare type variable that leaks unresolved from a nested generic call (`option_unwrap_or(nothing(()), 11)`, where `nothing(())` returns `@Option<T>`), which the old name coincidence had been hiding; the fix is pinned in both argument orders (the leak-first order routes through the concrete-wins rule, the concrete-first order through the #898 position-wise merge).  Pinned end-to-end by `tests/conformance/ch09_generic_builtin_typevar.vera` (a `forall<T>` and a `forall<E>` generic over compound element types, monomorphized and run) and a 19-case collide-vs-control differential battery (29 tests total).
- **A bare `None` under `forall<T>` now resolves its type argument from the declared context instead of being rejected against a type that unifies trivially** ([#971](https://github.com/aallan/vera/issues/971)).  A nullary constructor whose type argument is fully determined by the surrounding declaration — a `forall<T>` return type `@Option<T>`, a `let @Option<T> = None`, or the common type of match arms — minted an unrelated fresh constructor variable `T$n` and then refused the well-typed program (`None` in return position failed `E121` `body has type Option<T$1>, expected Option<T>`; the same miss produced `E170` in a `let` and `E302` across match arms).  The checker lacked any var-to-var unification, so the fresh ctor var was never tied to the declared `forall` var.  The bidirectional fill in `_ctor_result_type` now adopts an expected `TypeVar` — guarded, as before, by `expected.name == ci.parent_type`, so a constructor only ever adopts the variable its own parent's declaration names at that position and two ADTs sharing a parameter name still cannot cross-contaminate (the fresh-var minting for genuinely-unresolved variables is unchanged).  Regression pinned end-to-end by `tests/conformance/ch09_generic_none_return.vera`, which monomorphizes all three shapes at `T = Int` and runs them.
- **A NESTED bare `None` under `forall<T>` now threads the declared forall var through the nested-constructor field path, so `Some(None)` returned as `@Option<Option<T>>` checks clean** ([#979](https://github.com/aallan/vera/issues/979)).  #971 fixed the top-level result / `let` / match positions, but the same fresh-var pathology survived one level down: the inner `None`'s expected field type (`Option<T>`) still carries the declared var, and the constructor argument loop's field-propagation guard (`not contains_typevar(ft)`) suppressed any typevar-bearing expected type, so the inner constructor was typed with no expected, minted an unrelated `T$n`, and the well-typed program was rejected (return position failed `E121` `body has type Option<Option<T$1>>, expected Option<Option<T>>`, a `let` `E170`, match arms `E302`).  The argument loop now forwards a typevar-bearing expected field type when — and only when — the argument is itself a constructor (`ConstructorCall` / `NullaryConstructor`), so the nested constructor's own bidirectional fill in `_ctor_result_type` can adopt the declared var.  Feeding a typevar-bearing expected is safe solely into a nested constructor because that fill's per-level `expected.name == ci.parent_type` guard means an inner constructor adopts a variable only from ITS OWN parent's declared position, so two ADTs sharing a parameter name still cannot cross-contaminate (an inner constructor of a different parent sees a name mismatch, mints fresh, and the field-type check then rejects the genuinely ill-typed nesting).  Deeper nesting (`Some(Some(None))` at `@Option<Option<Option<T>>>`) descends every level, and the ill-typed direction is unchanged (`Some(Some(5))` stays `Option<Option<Nat>>`, still `E121`).  Pinned end-to-end by `tests/conformance/ch09_generic_none_nested.vera` (monomorphized at `T = Int` and run) and `tests/test_checker_types.py::TestForallNullaryCtorNested979` (return / `let` / match / three-level / alternate-param-name shapes plus cross-ADT-resolution and ill-typed rejection pins).
- **A bare `None` compared with `==` / `!=` against a known `Option` type now adopts that type instead of minting a fresh variable, so `ensures(@Option<T>.result == None)` checks clean** ([#981](https://github.com/aallan/vera/issues/981)).  The comparison-synthesis path typed each operand with no expected type, so a nullary `None` operand minted an unrelated `T$n` that could not be compared: `@Option<T>.result == None` was rejected `E142` `Cannot compare Option<T> with Option<T$1>` — in BOTH operand orders, in `requires` as well as `ensures`, and (the defect was wider than the `forall` case) even at a concrete `@Option<Int>`, which minted `Option<T$1>` and failed identically.  For `==` / `!=`, when the initially-synthesized operands do not unify and exactly one is a constructor expression whose type still carries an unresolved variable while the other has a concrete `AdtType`, that constructor operand is re-synthesized against the sibling's type as expected, so the #971 fill in `_ctor_result_type` adopts the sibling's type arguments.  Restricting the re-synth to `==` / `!=` (ADTs are neither numeric nor orderable) and to a single still-unresolved constructor operand keeps it from re-typing an already-well-typed operand, and the fill's `expected.name == ci.parent_type` guard still rejects a genuinely cross-ADT comparison (`@Result<T, Int>.result == None` stays `E142`).  This is a type-checking fix only — the verifier is untouched: a true `== None` postcondition proves at Tier 1, and a false `!= None` one is correctly deferred to a Tier-3 runtime check that traps.  Pinned by `tests/conformance/ch09_generic_none_nested.vera` (both operand orders, verified and run at `T = Int`) and `tests/test_checker_types.py::TestForallNullaryCtorComparison981` (both orders across `==` / `!=`, the `requires` and concrete-`Option<Int>` shapes, plus non-`Option` and cross-ADT rejection pins).
- **A NESTED payload-less constructor result (`Some(None) : Option<Option<Int>>`) compared against `None` / `Some(None)` in a contract no longer crashes `vera verify`, closing a check-green-then-verify-crash the #979/#981 checker adoption newly reached** ([#994](https://github.com/aallan/vera/pull/994) review, F1).  A bare `None` carries no payload for the verifier's SMT sort recovery, so `_find_sort_for_ctor`'s base-name scan picked whichever `Option<...>` instantiation cached first — with both `Option<Int>` and `Option<Option<Int>>` live it returned the wrong sort, and `_datatype_value_eq`'s structural `left == right` raised an uncaught `z3.z3types.Z3Exception: sort mismatch` — a Python traceback out of `vera verify` (exit 1, no JSON, so `--json` too) on a `vera check`-green program.  The nullary-ctor translation now hints its sort from the checker's recorded (instance-substituted) semantic type — routed through the gated #918 pinning, which PREFERS an already-cached instantiation so the hint disambiguates among live sorts without perturbing the warm/cold sort-creation order the obligations differential pins — resolving `None`'s exact `Option<Option<Int>>`; a residual sort mismatch in `_datatype_value_eq` now degrades to an honest Tier-3 rather than crash, so `vera verify --json` always emits JSON.  A true `!= None` / `== Some(None)` postcondition PROVES at Tier 1 (both operand orders, concrete and `forall`); a false `== None` one is disproved (E500).  Pinned by `tests/test_verifier_nullary_ctor_sort_994.py` (proof / disproof against a match-based oracle, a `--json`-always-JSON subprocess test, and a direct `_datatype_value_eq` sort-mismatch degradation backstop), mutation-validated in both halves.
- **A NESTED payload-less constructor in a `forall<T>` `==` / `!=` contract (`ensures(Some(None) == @Option<Option<T>>.result)`) now compiles and runs instead of a spurious E613** ([#994](https://github.com/aallan/vera/pull/994) review, F2).  The same #979/#981 shape passed `check` and `verify` but `vera compile` raised `Type 'Option<Option>' does not satisfy ability 'Eq'`: the runtime-check lowering of the contract `==` erased the operand's type argument — the inner `None` renders as the bare `Option` (no payload to recover `<Int>` from), so `Some(None)` recovers as `Option<Option>`, and the dead base generic clone's slot renders `Option<Option<T>>` whose nested free `T` the top-level-only free-var check missed — so a name the structural-Eq derivation cannot lower reached it.  Both operands of an `==` share a type (checker E142 otherwise) and a monomorphized reachable clone substitutes the sibling slot to a fully concrete name, so codegen now recovers the fully-concrete name from whichever operand carries it and routes to the scalar (dead-base-clone) lowering only when NEITHER resolves.  Concrete and payload-carrying nested comparisons are unaffected, and a genuinely non-Eq concrete operand (`Box<Array<Int>>`, `Tuple<Int, Int>`) still raises the correct E613.  Pinned by `tests/test_codegen_nested_nullary_ctor_994.py` (both operand orders + `!=` + a body-position `==` compile and RUN to their sentinels, with concrete / payload-carrying controls), mutation-validated (remove the sibling recovery → the reachable clone traps on a scalar pointer compare; remove the concreteness gate → the base clone reverts to E613).
- **Reading handler state as a slot in the handled body is now a checker error, closing a check/verify-vs-compile scope divergence** ([#973](https://github.com/aallan/vera/issues/973)).  A `handle[State<T>]` reaches its state only through the typed `get(())` / `put(...)` operations — spec §7.5 scopes state to the handler *clauses*, and both backends agree (codegen routes state through host-side cells and gives the handled body no local; the verifier consumes no body-scope state reference).  The checker, though, bound handler state into the handled body's slot scope too, so a body that read the state slot (e.g. `@Int.0` under `handle[State<Int>](@Int = 0)`) passed `vera check` and `vera verify`, then crashed `vera compile` with an internal `E699` dangling-slot error (or, when an enclosing same-typed binding existed, silently read *that* instead of the state).  The checker no longer binds body-scope state; such a reference is now a natural `E130` unresolved-slot error whose fix text steers the user to `get(())`.  Handler *clause* bodies are unchanged — they keep their state slot, consistent with codegen and the captures walk.
- **A `where`-helper body reading the OUTER function's parameter slot is now a checker error, closing the same check/verify-vs-compile scope divergence for `where` blocks** ([#969](https://github.com/aallan/vera/issues/969)).  spec §5 makes a `where`-helper always local to its parent, carrying its own mandatory contract over its own parameters; both backends compile each helper as an independent param-rooted scope.  The checker, though, checked helper bodies while the parent's value scope was still live, so a helper reading an outer parameter slot (e.g. `@Int.0` inside a `helper(@Bool -> @Int)` whose parent is `outer(@Int -> @Int)`) resolved to the parent's parameter and passed `vera check` and `vera verify`, then crashed `vera compile` with an internal `E699` dangling-slot error.  The parent's value-slot scope is now popped before helper bodies are checked, so such a reference is a natural `E130` unresolved-slot error whose fix text explains that helpers are closed, param-rooted scopes and steers the user to pass the value as an explicit argument.  The parent's `forall` *type* parameters stay in scope, so a helper of a generic parent may still be written over `@T`; only value slots are isolated (an implicit outer-frame capture would move a value across a contract boundary — capture semantics already have their one canonical construct in closures).
- **`vera verify --json`'s summary is now derived from the reified obligation stream, closing a `total` off-by-one** ([#967](https://github.com/aallan/vera/issues/967)).  The `VerifySummary` counters (`tier1_verified`, `tier3_runtime`, `total`) were accumulated by ~50 hand-written increments scattered through the verifier.  One of them — the #882 call-site precondition demotion — bumped `tier3_runtime` and reified a `tier3` `call_pre` obligation but forgot the matching `total` bump, so `tier1_verified + tier3_runtime == total + 1` on the three examples that hit the path (`http.vera`, `inference.vera`, `async_http_fanout.vera`).  Diagnostics and the individual tier counts were unaffected — only the summary's own arithmetic was internally inconsistent.  The fix replaces every hand-written counter with a single `summarize(obligations)` derivation (status `verified` → `tier1_verified`; `tier3`/`timeout` → `tier3_runtime`; `violated`/`tier3_unguarded` excluded; `total = tier1_verified + tier3_runtime`), computed at report-assembly time on both the cold `verify` and the warm `VerificationSession` paths, so the counts can no longer drift from the obligations a consumer reads.  `vera verify --json` also gains an `obligations` array (per obligation: `kind`, `status`, `description`, `location`, and `error_code` when present), so a machine consumer can reproduce or refine the tier counts directly.

## [0.1.3] - 2026-07-10

### Fixed

- **The monomorphizer completeness class: generic instantiation no longer silently produces wrong programs for exotic shapes** ([#769](https://github.com/aallan/vera/issues/769)).  Three gaps, shared by codegen and the #732 verifier — both sides agreed on the *wrong* instantiation, so `vera verify` was green while the compiled program traps or does the wrong thing:
  - **The builtin return tables were a fraction of the registry.**  `_BUILTIN_PARAMETERIZED_RETURNS` covered 9 of the 41 builtins with fully-concrete parameterized returns, so `first(string_chars("abc"))` could not bind `T = String` and fell to the `Bool` phantom default — a check-green, verify-green program that failed WASM validation at run time.  Both tables are now registry-complete (41 parameterized entries incl. nested inners like `Option<Array<Json>>`; 160 simple names), enforced by a differential test against `TypeEnv._register_builtins`, with the `array_range` special cases folded in on both sides.
  - **Nested type-argument unification was one level deep.**  `Array<Array<E>>` matched against `Array<Array<Int>>` left `E` unbound (the `E521` warning literally reported `head_head<Bool>`) and the run trapped on the i64/i32 ABI mismatch.  A shared recursive unifier (`Monomorphizer._unify_type_arg_pair`) now binds variables at any depth on BOTH the discovery and WASM call-rewrite sides, and array-literal arguments expose full-depth element names (`[[1, 2]]` → `Array<Array<Int>>`, `[Some(1)]` → `Array<Option<Int>>`) so there is something to bind against.
  - **The De Bruijn collapse-reindex was parameter-only and name-truncated** — the highest-severity gap: `confuse(100, 3)` returned `3`.  A body `let`, match-arm binder, or closure parameter of a collapsing type shifts every later slot reference, which a static params-only map cannot express (one type variable suffices: `let @Int = 5;` before `@A.0` at `A = Int` silently read the `5`); and the one-level slot-name truncation collapsed `Array<Option<A>>`/`Array<Option<B>>` to the same *old* name, hiding genuine merges entirely.  The map is replaced by a scope-aware walker (`_compute_scoped_reindex`) that resolves every `SlotRef` against the full binding scope at its reference site — parameters, `let`/destructuring bindings, match-arm binders (arm-scoped), closure parameters, handler clauses, with contracts as a params-only scope, `where`-helpers as independent scopes, and full-depth canonical names from `vera/slots.py` — mirroring exactly the environments the checker, verifier, and codegen build.
- **Four sibling discovery-vs-rewrite desyncs, found probing the same class, all dropped `main` from check-green programs** (fixed in this release with [#769](https://github.com/aallan/vera/issues/769); each was an E602 dangling clone — instantiation discovery and the WASM call-site rewrite inferred different clone names): `apply_fn(closure, …)` results in generic-argument position (discovery lacked the chain's closure-return arm — added as `_closure_arg_return_te`, resolving `FnType` aliases through the shared `resolve_fn_type_alias`); `async(…)`/`await(…)` results (discovery gains the same `Future<T>` wrap/strip arms the chain has); and generic calls with i32-handle returns (`ident(pass(Some(42)))` — rewrite WAT-collapsed to `Bool`) or declared-`Nat` returns (rewrite collapsed i64 to `Int`) — the rewrite's generic branch now substitutes the callee's declared return exactly as discovery does.

- **Three regressions the adversarial review panel and PR review caught before ship, fixed in-PR** (PR [#972](https://github.com/aallan/vera/pull/972) review round): the scope-aware reindex counted a handler-state binding for `State<T>`-handled bodies that codegen never pushes (state lives in host-side cells) — a generic with a stateful handler either failed compile with a dangling-slot E699 or silently read the wrong local; the walker is now codegen-faithful, while the underlying checker/backend scope divergence stays open as [#973](https://github.com/aallan/vera/issues/973); the shared builtin-name dicts shadowed user OVERRIDES of E151-exempt prelude combinators (#815: `option_map`, the `json_*` family) — all four consult sites are now gated on `fn_ret_types` registration, the #908 show/hash gate generalised, so a real declaration's type always wins; and the reindex walker only visited `where`-helpers one level deep — `where` blocks nest, and a nested helper's collapsed indices went stale (it read its body `let` instead of the parameter).  Each fixed test-first with the panel's own repro programs, mutation-validated.  The review also revived the HISTORY row-hygiene gate: `check_doc_counts.py`'s version-row regex was pinned to the `v0.0.x` series, so no `v0.1.x` row was being inspected — it now covers every `vX.Y.N` row and codifies the current template (at most one issue link and one bold-lead-in ` — ` separator per row), mutation-checked in both directions.  The audit also filed and cataloged four checker-front-end bugs the fixes exposed but did not cause — [#969](https://github.com/aallan/vera/issues/969), [#970](https://github.com/aallan/vera/issues/970), [#971](https://github.com/aallan/vera/issues/971), [#973](https://github.com/aallan/vera/issues/973) — now in the KNOWN_ISSUES and SKILL.md bug tables (ending the "no known bugs" run; the tables reflect the tracker, not the trend).  A follow-up sweep of all 100 open issues against the same standard reclassified three more — [#758](https://github.com/aallan/vera/issues/758) and [#820](https://github.com/aallan/vera/issues/820) (verify-clean programs silently violating the `@Nat` invariant at un-obligated, un-guarded, un-disclosed positions) and [#967](https://github.com/aallan/vera/issues/967) (the `verify --json` summary arithmetic) — from `limitation` to `bug`, with their ROADMAP rows removed (bugs are burndown material, not stage work) and their guard-deferral siblings (#754, #757, #765: statically obligated, verified programs sound) staying limitations on the merits.

- **A false Tier-1-blocking E503 inside monomorphized generic clones is fixed** (PR [#972](https://github.com/aallan/vera/pull/972) review; pre-existing on `main`, but newly reachable on the very programs this release makes runnable): the span-keyed semantic-type side-table returned GENERIC types for clone nodes (clones keep their source spans for diagnostics), so `is_some_g<Nat>` matching `Some(@T)` on an `Option<Nat>` scrutinee was obligated as an `@Int` -> `@Nat` narrowing with the impossible counterexample `Some(-1)` — `vera verify` rejected a program `vera run` executes correctly.  Clone verification now substitutes the instance's concrete types into every side-table lookup (sub-pattern binds, source facts, destructures), while genuine clone-path narrowings still emit their obligation.

### Changed

- **The WASM inference chain's ~130 hand-written builtin return arms are hoisted into the shared `_BUILTIN_VERA_RETURN_TYPES` dict** ([#769](https://github.com/aallan/vera/issues/769)).  Discovery and the call-rewrite previously kept two independent copies of "what does this builtin return" — the mono-side dict and the `inference.py` if-chain — and every difference between them was a dropped-`main` bug (e.g. `json_get` was dict-only, `string_chars` was chain-only; each direction desynced).  Both consultors now read the ONE dict, so an entry added for a new builtin lands on both sides atomically; the chain keeps only its logic arms (`apply_fn`, `show`/`hash` with the #908 user-shadow gate, `async`/`await`), and values are preserved verbatim (`string_length` stays `"Int"`, `string_char_code` stays `"Nat"` — clone-name agreement is the invariant, name precision is a separate decision).

## [0.1.2] - 2026-07-09

### Added

- **A scheduled CI workflow now checks the limitation tables against live issue states** ([#852](https://github.com/aallan/vera/issues/852)).  `check_limitations_sync.py --check-states` runs Mondays 07:00 UTC and on demand: every issue a KNOWN_ISSUES.md, vera/README.md, spec, SKILL.md, or LSP_SERVER.md limitation row cites is queried against the tracker, and a closed issue still listed as a limitation fails the run — the drift class the v0.1.1 sweep caught by hand is now caught on a schedule.  Deliberately a visibility signal on the Actions tab rather than a required merge check, since issue state drifts independently of any particular PR.  External contribution by [@chethanuk](https://github.com/chethanuk).
- **Linux aarch64 gains an advisory CI lane** ([#702](https://github.com/aallan/vera/issues/702)).  The test matrix gains an `ubuntu-24.04-arm` cell (Python 3.12 — platform coverage, not version coverage), and the wheel-availability gate now checks `manylinux_2_38_aarch64` wheels for all three Python versions; README §Supported platforms lists the tested cell, with Python 3.11/3.13 on aarch64 remaining wheel-checked only.  The new cell is advisory for now — it runs on every commit but is deliberately not in the required merge checks until the arm runners prove reliable.  External contribution by [@chethanuk](https://github.com/chethanuk).

### Changed

- **Two CI workarounds are retired — their removal triggers fired** ([#537](https://github.com/aallan/vera/issues/537)).  Pygments 2.20.0 (March 2026) shipped the CVE-2026-4539 fix, so the `--ignore-vuln` flag is gone from the dependency audit (left in place it would have masked a real regression); and the `actions/setup-python` toolcache now carries pip 26.1.2 natively, so the pre-audit `pip install --upgrade pip` (CVE-2026-3219) is gone too.  Both KNOWN_ISSUES CI-workarounds rows retire with them, per the table's bridges-not-permanent-exceptions contract.

### Fixed

- **An invalid string escape no longer crashes `vera check` with a raw Python traceback** ([#966](https://github.com/aallan/vera/issues/966)).  The E009 `TransformError` is raised inside a Lark token callback, which lark wraps in `VisitError` — so the CLI's `except VeraError` never fired and a user typo like `"bad \q escape"` produced exit 1 with zero bytes of JSON and a traceback on stderr, contradicting spec §0.5 and §0.5.8.  `transform()` now unwraps a `VisitError` carrying a `VeraError` and re-raises the original, so both text and `--json` modes emit the proper diagnostic envelope.  The E009 diagnostics themselves are upgraded from description-only to the full instruction format — rationale, fix, and spec_ref for both the invalid-escape and malformed-interpolation classes — retiring a `# diag-fields-exempt` waiver whose "the grammar prevents this" premise was false (spec §1.6 defines invalid escapes as a user-facing error).  Found by the v0.1.2 pre-release sweep.
- **The pre-release sweep corrected the drift the gates don't reach.**  TESTING.md's contract-verification block was three campaigns stale (claimed 256 of 280 obligations, 91.4% static; live is 283 of 378, 74.9% — the denominator grew with the soundness campaign's auto-synthesised primitive-op obligations, and the per-example Tier-3 table is regenerated from live `vera verify --json`, which also surfaced the [#967](https://github.com/aallan/vera/issues/967) summary off-by-one, now a tracked Stage 19 limitation); the README/FAQ test counts catch up to 6,846; HISTORY's v0.1.0 by-the-numbers column is restored to its true snapshot (6,779 — a v0.1.1-cycle edit had bumped it to the then-current count) with the growth-chart figure regenerated in lockstep; the spec-block counts correct to 189; §0.5.1 acknowledges the `W001`/`W002` warning codes; TESTING.md documents the scheduled limitations-sync workflow and scopes the `# diag-fields-exempt` gate rows to the #955 semantics; and three test-code comments citing since-fixed issues (#918, #706, #635) are retired.
- **`check_diagnostic_fields.py`'s `# diag-fields-exempt` opt-out is now honoured wherever an *unresolvable* field can arise** ([#955](https://github.com/aallan/vera/issues/955)).  The field-presence pass (`check_source`) appended its non-literal-severity violation and `continue`d **before** its own opt-out lookup ran, so a marker on that exact call was never consulted even in the one pass that otherwise honours it; the `spec_ref`-validity pass never consulted the opt-out at all.  Fixed for the *unresolvable* (non-literal) sub-case only: a non-literal severity or spec_ref can't be checked statically and so is opt-out-able, same as a missing field — with the marker found anywhere across a multi-line call's span, not just the argument's own line.  A `spec_ref` that resolves but cites the wrong/nonexistent section, or an `error_code` not in `ERROR_CODES`, is a content error, not a tagging gap — the opt-out never suppresses either, marker or not; and the `error_code` pass skips non-literal codes entirely, so it has nothing to waive.  On `vera/` the exempt set is unaffected (the three existing real markers are all missing-field cases).  `main()`'s remedy text now states this precisely instead of implying the opt-out waives everything it lists.  External contribution by [@chethanuk](https://github.com/chethanuk).
- **The diagnostic-fields gate's plumbing-skip now also confirms the sole own-scope `Diagnostic` is reachable as the helper's *result*** ([#956](https://github.com/aallan/vera/issues/956)).  The `#827` narrowing (v0.1.1) requires a genuine helper method whose sole own-scope construction is ambiguity-free, but never checked that the construction was actually *used* as the helper's output — a helper that builds its one `Diagnostic` and hands it to something other than a `return` / `.append(...)` (e.g. `self.dispatch(d)`) still had that ctor elected as plumbing and skipped by all three passes, so a bogus `spec_ref` or unregistered `error_code` on it would ship silently.  A new `_ctor_is_reachable_as_result` predicate now requires the ctor be return-ed, appended, or bound to a local later return-ed/appended, gated after the existing `len(ctors) == 1` check so the `#827` ambiguity handling is untouched.  The name-based return/append match has no real data-flow, so a local rebound after the ctor-binding assign — plain reassignment (`d = Diagnostic(...)`; `d = something_else`; `return d`), a `for`/`with` loop target, a walrus, an augmented assignment, or an `except ... as` name — would otherwise still match on the name and be wrongly treated as reachable — fixed by counting binding sites *generically* rather than by enumerating statement forms (every Store-context name — assignment or unpack of any shape, `for`/`with` targets, walrus — plus `import ... as`, `except ... as`, `match` captures, the helper's own parameters, and `nonlocal` declarations in nested functions; a bare annotation carries no assignment and is not counted): a name bound more than once anywhere in own scope is treated as unreliable rather than reachable, and the return/append name-match only counts at-or-after the binding line.  Separately, `_is_append_call` treated ANY `.append(...)` call as evidence of reachability, including an append to an unrelated throwaway local (`tmp = []; tmp.append(d)`) that the helper never reads again — every real diagnostic-list sink in `vera/` has the `self.<attr>.append(...)` shape, so the check is now scoped to that.  On `vera/` the exempt set is unchanged (all five real helpers return or append their sole ctor directly), so nothing new is flagged in the live tree — the gap was latent, closed defensively.  External contribution by [@chethanuk](https://github.com/chethanuk).

## [0.1.1] - 2026-07-08

### Added

- **The documentation now carries a hand-authored SVG diagram set — thirty-three figures across the spec, the compiler README, and the top-level docs** (`assets/diagrams/`).  The second wave adds the subtyping and effect-row lattices (§2.8, §7.8), module resolution with the transitive-visibility rule (§8.6), the async model and the `vera serve` request lifecycle (§9.5), closure layout + `call_indirect` dispatch (§11.10), browser import introspection (§12.9.1), the WASI arena and server-world sequence (§13.3, §13.7), the FAQ's three verification layers, contract-driven-testing loop, and Dafny/Lean/Koka/F* comparison matrix, the CONTRIBUTING gate pipeline, the DE_BRUIJN binding-stack timeline, and the host-binding-families map (`vera/README.md`).  The headline is the overall compiler architecture diagram `vera/README.md` lacked entirely; alongside it: the spec §11.1 pipeline, the §12.5 memory layout and a new `$alloc`/mark-sweep GC flow, the §13.2 WASI component + dispatch table, the §6.8 three-tier verification decision flow, the §7.5.2 effect-handler suspend/resume sequence, the wasmtime embedding chain, the checker's three passes, Z3 proof-by-refutation, De Bruijn slot resolution (two figures, shared with `DE_BRUIJN.md`), the `Diagnostic` record, the CLI↔pipeline map (`TOOLCHAIN.md`), the three-layer/four-level testing model (`TESTING.md`), the LSP proof-delta session (`LSP_SERVER.md`), a by-the-numbers growth chart (`HISTORY.md`), and a workflow hero + the architecture in `README.md`.  Every diagram that replaced an ASCII original keeps the text in a collapsed `<details>` block tagged `text` — the docs feed `llms-full.txt` and agents reading files in a terminal, and images are invisible to both — and no diagram carries live counts (they drift outside `check_doc_counts.py`'s reach; the one exception, the growth chart, plots the historical release columns and says so).  The architecture and pipeline figures draw the check → {verify | compile} fork truthfully — `vera compile` does not consume verify results and contract guards are always emitted — where the replaced ASCII's "runtime contract insertion for Tier 3" caption echoed the spec drift tracked in [#958](https://github.com/aallan/vera/issues/958).  The landing page gains its own figure in the site's bolder design language — `docs/loop-web.svg`, the write → prove → ship loop with the diagnostics return, embedded in `docs/index.html` §Why and mirrored into the generated `index.md`.  The set's design system, conventions, and inventory are documented in `assets/diagrams/README.md`; the `README.md` project-structure tree's stale module counts (11 → 13 codegen, 9 → 19 wasm) ride along.
- **The canonical `E001` diagnostic is now guarded against drift in all five of its documentation mirrors** ([#829](https://github.com/aallan/vera/issues/829)).  `TestErrorDisplaySync` already compared `README.md`, `docs/index.html` and `spec/00-introduction.md` against the diagnostic generated from `vera/errors.py`, but two mirrors were unguarded — `AGENTS.md`'s example `--json` block, and the hardcoded example in `scripts/build_site.py` that generates `docs/index.md` — and [#826](https://github.com/aallan/vera/issues/826) had already drifted the ungated pair.  Both are now compared.  `AGENTS.md`'s `error_code` / `spec_ref` / `fix` are matched exactly (the extractor `json.loads` the block, so the escaping is resolved and every field is directly comparable) and its ellipsis-truncated `description` / `rationale` are prefix-compared; `build_site.py`'s block is extracted by anchoring on the closing code fence rather than on the `spec_ref` text, so a `spec_ref` drift yields a precise Expected/Got diff instead of an opaque "block not found".  Every guard is mutation-validated — drifting any mirror, or the canonical diagnostic, turns the corresponding test RED.  The example is still hand-duplicated across the five mirrors; single-sourcing it so nothing *can* drift is tracked in [#954](https://github.com/aallan/vera/issues/954).  External contribution by [@chethanuk](https://github.com/chethanuk).

### Changed

- **ROADMAP.md is reworked into a staged sprint plan.**  The tier/milestone mix is replaced by six themed stages continuing HISTORY.md's numbering — Stage 19 verification completeness, 20 single-source, 21 effect hardening, 22 the verified tool server, 23 agent experience, 24 browser — each with a rationale, an exit criterion, and an issue table, ordered from the design principles (verification truth first, then structural drift-proofing, then the flagship's capabilities, then the experience around them).  The Stage 17 burndown set the model: a stage is a concentrated sprint over a coherent issue class, and it moves to HISTORY.md when its table empties.  All 100 open issues are placed exactly once (staged, horizon-arc, ongoing, not-doing-now, or speculative; verified mechanically), pulling in the verification-limitation family that previously lived only in KNOWN_ISSUES.md; the browser sync-XHR fix (#355) moves from Http hardening to the browser sprint since every fix option shares the JSPI suspend machinery.  Rides along: DESIGN.md's module row claimed "explicit re-exports", which don't exist ([#127](https://github.com/aallan/vera/issues/127) is open) — it now says `public`/`private` visibility and points at #127.

### Fixed

- **A six-auditor documentation-consistency sweep reconciled every stale claim it could verify against the tree, the registry, and the tracker.**  The one behavioural-claim error: `vera/README.md` §Runtime contracts still described the pre-#957 world — codegen "classifies contracts using the verifier's tier results" and omits Tier-1 guards — contradicting the architecture diagram above it; it now states the truth (compile never consults the verifier; guards are always emitted; the §11.8 aspiration stays tracked in [#958](https://github.com/aallan/vera/issues/958)).  The rest is drift, each verified before fixing: SKILL.md's conformance count (103 → 143) and its spec table gaining the Chapter 13 row; the CLAUDE.md/AGENTS.md pipeline gaining the resolve stage; FAQ's feature bullets gaining `IO.read_char` and the `Exn<T>` effect; README's "three-tier verification" delivered-claim scoped to the two implemented tiers; DESIGN.md's effect lists gaining `HttpServer` (and `Random`/`Diverge` rows) on a list that claims to mirror `vera effects --json`, its Tier-1 coverage sentence aligned to spec §6.8, and the tiers + effect-row diagrams embedded; KNOWN_ISSUES gaining rows for the open [#439](https://github.com/aallan/vera/issues/439) and [#770](https://github.com/aallan/vera/issues/770) limitations; HISTORY's stage index gaining Stages 16–17 and the intro catching up to 94 development days; `vera/README.md`'s module-map line counts regenerated from disk (worst drift: `verifier.py` listed at 1,005 lines, actually 6,582); spec §12's Random prose no longer citing the closed #465 as a tracker and the heap-growth wording unified to "toward higher addresses"; and `build_site.py` now rewrites *image* embeds to raw URLs (a `blob/` page is not image bytes), fixing the four diagrams inlined into `llms-full.txt`.  Examples and conformance fixtures came back clean — no workaround shapes for fixed bugs survive there.
- **Five stale bug-era annotations are retired from the test suite, each verified by running the affected tests before and after** — found by the sweep's test auditors: the [#869](https://github.com/aallan/vera/issues/869) table-forcing `array_fold` is removed from the monomorphize fixture (the fixture itself is now the regression pin, sum unchanged); the [#570](https://github.com/aallan/vera/issues/570)-era "1,000 elements to stay under the bug threshold" GC graph is promoted to the true 5,000-element `Array<Box>` wide graph its docstring always intended; the [#516](https://github.com/aallan/vera/issues/516) module docstring no longer defers "Stage 2 (source mapping) and Stage 3 (`Fix:` paragraphs)" as future work — both shipped (v0.0.124/v0.0.125) and are exercised throughout the file; `ch09_decimal` now exercises `decimal_compare` through a three-arm `Ordering` match (the "not yet supported in codegen" note was disproven by running it); and the [#773](https://github.com/aallan/vera/issues/773) scalar-only-`Eq` parenthetical speaks in the past tense.
- **The verifier no longer proves false postconditions: a callee's `ensures` is now assumed only on the paths that establish it** ([#957](https://github.com/aallan/vera/issues/957)).  `_translate_call_with_info` (`vera/smt.py`) assumed each callee `ensures` — and each refined return's predicate — with a bare `self.solver.add(...)`, which lands on the solver's **base** assertion stack.  `check_valid` folds `_path_conditions` around the *goal* only, so a fact learned inside an `if` outlived the branch and became an unconditional fact about the **caller's own slots** (`_build_callee_env` binds the callee's parameters to the caller's terms).  The escaped fact is circular: `dec5 requires(@Nat.0 >= 5) ensures(@Nat.result == @Nat.0 - 5)` injects `ret == @Nat.0 - 5`, which with `@Nat`'s implicit `ret >= 0` entails `@Nat.0 >= 5` — the very precondition the branch guard existed to establish.  A caller's false `ensures(@Nat.0 >= 5)` then discharged, and `vera verify` printed `OK` / `6 verified (Tier 1)` with no diagnostic for a program that `vera run` traps on.  Worse, two calls in mutually-exclusive arms inject contradictory facts, the base solver goes **UNSAT**, and *every* obligation discharges vacuously — silently deleting real `E501` diagnostics, including the ones the `#776` fix above adds.  Each injected fact is now wrapped by `_guard_fact` in `z3.Implies(z3.And(*self._path_conditions), fact)`, matching the idiom `_translate_match` already used; the call translator was the only site that did not.  Same soundness as discarding the facts, strictly more precision.  Behaviour-neutral across all 143 conformance programs and 37 examples (`tier1` 1,464 → 1,464, `tier3` 594 → 594, no diagnostic changes) and a legitimately branch-guarded call is not over-rejected.  Note that the compiled program was never affected: `vera compile` does not consult the verifier, so the runtime `contract_fail` guard is emitted regardless of tier — the break was in `vera verify`'s claim.  That mitigation is accidental rather than designed, and `spec/11-compilation.md` §11.8 (which promises Tier-1 contracts *are* omitted) is tracked as the drift it is in [#958](https://github.com/aallan/vera/issues/958).  Found by the adversarial review of #953.
- **A call's precondition is now statically checked (`E501`) even when the call sits inside an effect-operation argument** (e.g. `IO.print(need_pos(...))`) ([#776](https://github.com/aallan/vera/issues/776)).  `translate_expr` (`vera/smt.py`) had no `QualifiedCall` branch, so an effect op (`Module.op(args)`) fell through to `return None` **without** recursing into its arguments — a precondition-bearing call passed as an argument was never visited, and no `E501` fired for a violated `requires(...)`.  A `QualifiedCall` branch now walks each argument via `translate_expr` for the `E501` precondition side effect (the `#727` span-keyed dedup keeps it duplicate-free) and returns `None` — the effect op itself is still not Z3-translated, since its result is chosen by the handler and translating an effect in a contract would be unsound (pinned by `test_effect_op_never_becomes_a_z3_term`: returning an argument's term instead makes the verifier *prove* a postcondition only the handler could satisfy, reporting `verified` where it should report `tier3`).  Translating a `FnCall` argument does more than record its `E501`: it also *assumes* the callee's `ensures`.  That assumption is scoped to the branch it was learned in by the new `_guard_fact` (see the next entry) — without it, routing effect-op arguments into the call translator would have converted programs `main` correctly rejected into programs it silently accepted.  Mirrors the `#730` statement-position (`ExprStmt`) handling, whose walk reaches the same unguarded assumption and is tracked separately.
- **The diagnostic-fields gate's plumbing-skip no longer swallows a stray `Diagnostic`** ([#827](https://github.com/aallan/vera/issues/827)).  `scripts/check_diagnostic_fields.py` exempted *every* `Diagnostic(...)` lexically inside any function **named** `_error` / `_warning`, on the premise that such a construction is the helper's own plumbing.  The name alone is not enough: a second, under-tagged ctor in the same helper (say, in an `else` branch), or a module-level function coincidentally named `_error`, escaped all three of the field-presence, `spec_ref`-validity and `error_code`-registration passes — [#826](https://github.com/aallan/vera/issues/826) had propagated the same name-based skip into the latter two.  Latent rather than live (all five real helpers in `vera/` construct exactly one `Diagnostic` each, so nothing escaped in practice), but a blind spot in the gate that exists to close exactly this class of silent under-reporting.  A ctor is now skipped only when its enclosing function is a genuine helper **method** — a class member with a `self` receiver, so a module-level or `@staticmethod` look-alike is inspected, not exempted — **and** it is that method's **sole own-scope** construction, counted without descending into nested `def` / `lambda` / `class` scopes.  A helper holding two constructions is ambiguous: neither is skipped, and both are inspected.  Counting *every* own-scope construction (rather than only the one structurally `return`ed or `.append(...)`-ed) is what makes the rule sound — a helper that binds its real diagnostic to a local first (`d = Diagnostic(...)`, then `self.errors.append(d)`) would otherwise leave a stray direct ctor as the only *recognised* construction, which would then be elected as the helper's own and skipped.  It also means an ordinary hoist-to-a-local refactor no longer trips the gate on `spec_ref is not a string literal`.  "Own scope" is the helper's **body**: a `Diagnostic(...)` written in a decorator argument, a parameter default, or a return annotation is evaluated in the *enclosing* scope, is never the helper's plumbing, and is always inspected — counting those would let one be elected as the helper's sole construction and skipped, which is the escape this gate exists to prevent.  Every exemption therefore lies inside a function span the previous rule already exempted, so the exempt set can only shrink and the change cannot open an escape; on `vera/` it resolves to the same five plumbing ctors as before.  External contribution by [@chethanuk](https://github.com/chethanuk).
- **The landing page's loop figure is rebalanced and its claims audited against the tree**.  `docs/loop-web.svg` carried 70 canvas-units of dead space above the drawing and 166 to its right (vs 30 left / 18 below), so the embed rendered top-heavy and visually left-shifted; the viewBox is recropped symmetric (12 units each side, browser-measured) and the embed's margins equalised — 48px of air above and below.  The audit that rode along, every claim verified against the registry or a live run: the effect enumeration gains `Diverge` and its count corrects to nine (`vera effects --json`; the same one-word gap in `README.md` rides along); the `Inference` provider list gains Mistral (`vera/runtime/inference.py` detects four keys, both here and in the generated `index.md`); the three-tier feature card is scoped to what shipped, mirroring README's phrasing (the Z3-guided middle tier is specified, not implemented); the WASI showcase block now shows `vera compile --target wasi-p2 --world server`'s *actual* output line and a consistent `examples/http_server.wasm` artifact path (previous text was a paraphrase; the `wasmtime serve` banner was verified against a real run); and the `safe_divide` / `classify_sentiment` samples link their `examples/` counterparts the way the fizzbuzz sample always did — with the fizzbuzz link itself backfilled into the generated `index.md`, which had never carried it.

### Security

- **The `wasmtime` runtime floor is raised to `>=46.0.1`, off a release affected by [GHSA-4ch3-9j33-3pmj](https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-4ch3-9j33-3pmj)** (medium; [#949](https://github.com/aallan/vera/pull/949)).  The advisory is a `wasmtime-wasi` `FilePerms` bypass on hard-link and rename *destinations*; its affected range includes `45.0.0`, which `uv.lock` had pinned.  The upstream core backport is `45.0.3`, but the `wasmtime-py` binding never tagged a `45.0.x` patch — `46.0.1` is the only patched binding release, so the floor moves up a major rather than sideways.  Vera's own WASI host (`vera/runtime/wasi_host.py`) is **not** exploitable: it preopens with the default `READ_WRITE` `FilePerms`, so there is no restricted permission for a link/rename to bypass.  CI already resolved the unbounded floor to `46.0.1`; this pins the lockfile and the declared floor off the affected release for minimum-version resolvers (`uv sync`, distro packagers, pre-existing venvs).

## [0.1.0] - 2026-07-04

### Fixed

- **An array literal of a zero-size element type (`[()]`, `Array<Unit>`) is now rejected cleanly at check time (`E135`) instead of silently compiling to invalid WASM** ([#945](https://github.com/aallan/vera/issues/945)).  `_element_mem_size` fell through to the 4-byte ADT default for a zero-size element, so the array-literal store (and the index load) emitted an `i32.store` / `i32.load` against an empty stack — `vera check`, `vera verify`, and even `vera compile` all succeeded (a `.wasm` was emitted), but `vera run` failed at wasmtime load with a `type mismatch`.  An `Array<T>` whose element `erases_to_unit` (`Unit`, or a `Future` transparently wrapping one; #841) is now rejected by the new **`E135`** at both the type-resolution gate (`vera/checker/resolution.py` — `Array<Unit>` params/returns/annotations) and the array-literal check (`vera/checker/expressions.py` — a bare `[()]`), so both the store and load paths are caught before codegen.  A zero-size-element array is degenerate (isomorphic to a `Nat` count); a non-zero `Array<Int>` is unaffected.  Pre-existing (reproduced on the pre-burndown baseline) and out-of-family from #900/#939/#943, found by the final pre-`v0.1.0` verification pass.
- **An annotated `Array<zero-size>` binding no longer double-reports `E135`.**  An array literal that is the RHS of an annotated `Array<Unit>` binding or return was rejected by *both* the type-resolution gate (the `@Array<Unit>` annotation) *and* the literal gate (`_check_array_lit`) — two `E135`s for one root cause.  `_resolve_type` returns the `Array<Unit>` type intact after emitting its `E135` (not `Unknown`), so the literal gate now receives that `expected` type and defers when it is already a zero-size array — stripping any refinement first via `base_type`, so a refined `{ @Array<Unit> | pred }` annotation (where `expected` is a `RefinedType`) is handled too (`vera/checker/expressions.py`); exactly one `E135` fires.  The un-annotated `[()]` literal (no `expected` type — its only gate) and the genuine `Array<Int> = [()]` element-type mismatch (`expected` not zero-size — the literal `E135` plus an `E170`) still report.  The `#945` regression tests now assert the exact `E135` *count*, not mere presence — the gap that let the duplication hide.  Found in the PR #938 review (a follow-up to #945).
- **Exact-duplicate type-checker diagnostics are now collapsed to a single entry.**  A function's signature types are `_resolve_type`'d in *both* the registration and the check pass, so any resolution-level diagnostic on a param/return — e.g. `E135` on an `Array<Unit>` **parameter** — was reported twice at one location (a pre-existing, general duplication that the new `E135` made visible, unrelated to the literal gate above).  `_error` (`vera/checker/core.py`) now dedups on `(code, file, line, column, severity, message)` — an exact match is indistinguishable to the reader — collapsing the double while keeping genuinely distinct diagnostics (a bad param *and* a bad return still report once each, not merged).  Found in the PR #938 review.
- **A generic `@T` read inside a `requires` / `ensures` contract clause, instantiated at the zero-size `Unit` type, is now rejected cleanly at check time (`E206`) instead of crashing codegen with a raw `CodegenInvariantError` traceback** ([#939](https://github.com/aallan/vera/issues/939)).  #900 rejects a `forall<T>` instantiated at `Unit` when its *body* reads a `@T.n` slot — that read lowers to a `local.get` on a slot that does not exist (`Unit` is 0 bytes, erased from the ABI) — but scoped its `E206` discriminator to the body alone, on the premise that a `@T` in a contract clause "never lowers to WASM".  That premise was false: `requires` / `ensures` clauses lower to *runtime* pre/post-condition checks (`_compile_preconditions` / `_compile_postconditions`), so a `@T.n` read there dangles identically.  A program like `forall<T> ignore_pair(@T, @T, @Int -> @Int) requires(@T.1 == @T.0) { @Int.0 }` called at `Unit` passed `vera check` **and** `vera verify`, then crashed `vera run` / `vera compile` with a raw Python traceback — violating the DESIGN.md invariant that a `check`-green program never surfaces one.  The `_forall_vars_read` discriminator (`vera/registration.py`) now also walks `requires` / `ensures` clauses (but not the verifier-only `decreases` / `invariant`, which never emit a `local.get`).  Two further gaps surfaced in adversarial review and are closed here: **(a)** the E206 zero-size test keyed on bare `Unit`, missing `Future<Unit>` — a *second* zero-size type, since codegen makes `Future<T>` transparent to `T` (#841), so a `@T` read at `T = Future<Unit>` (via `async(())`) dangled identically; the discriminator now uses a shared `erases_to_unit` predicate (`vera/types.py`) that mirrors codegen's erasure through `Future`; and **(b)** the *postcondition*-compile path lacked the `CodegenInvariantError` → `[E699]` net, so a `@T` read in an `ensures` clause at `Future<Unit>` crashed with a raw traceback despite `check` + `verify` passing.  All four contract-lowering paths — precondition, postcondition, body, closure — now degrade a `CodegenInvariantError` to a clean `[E699]`, completing the symmetry [#922](https://github.com/aallan/vera/issues/922) began.  Found during the final pre-`v0.1.0` review sweep.
- **`vera serve` now appears in the CLI help (`vera` with no arguments) and the `README.md` / `SKILL.md` / `AGENTS.md` command lists** ([#942](https://github.com/aallan/vera/issues/942); the `serve` HTTP host shipped in #305).  The `serve` command — the host for a contract-verified `handle(Request -> Response)` program — was fully dispatched but absent from the `USAGE` help text `vera` prints and from the quick-reference command lists agents scan (the module header docstring, `TOOLCHAIN.md`, `CLAUDE.md`, and the `<HttpServer>` prose already had it; the lists had drifted).  Its `Commands:` row and the `--port` / `--host` options are now listed everywhere, and a new `test_usage_lists_every_dispatched_command` guard fails if any future `cmd_<name>` handler is missing from `USAGE` — row-anchored, not a bare substring (the `--world server` row's description already contains "wasmtime serve", which a naive check would false-pass on).
- **A direct refinement parameter whose base type is zero-size but not literal `Unit` — `@{ @Future<Unit> | P }` — no longer crashes codegen with a raw `ValueError`** ([#943](https://github.com/aallan/vera/issues/943); a sibling of #939 found in the same review).  A refined parameter carries a runtime predicate guard (#746), but a zero-size base has no WASM local to check the predicate against, so the guard is skipped (`tier3_unguarded`).  That skip was keyed on the literal base name `"Unit"`, missing `Future<Unit>` — which codegen erases identically, since `Future<T>` is transparent to `T` (#841) — so a `Future<Unit>` refinement produced guard parts that then hit the `wt is None` invariant and raised a raw `ValueError` at `_compile_fn`, on a `check`-green + `verify`-green program.  The guard-parts short-circuit (`_refinement_guard_parts`, `vera/codegen/contracts.py`) now keys on codegen's own erasure — `_type_expr_to_wasm_type(base) is None` — so the guard-skip set is exactly the set of bases with no runtime local (`Unit`, `Future<Unit>`, transitively), and the `_compile_fn` invariant becomes a genuine defensive backstop rather than a reachable crash.  Found during the final pre-`v0.1.0` verification pass.
- **`compare()` / ordering on a user ADT is now rejected cleanly at check time (`E242`) instead of silently miscompiling to a wrong `Ordering` (and crashing the verifier); ordering on `String` operands, which crashed WASM translation, now works lexicographically** ([#921](https://github.com/aallan/vera/issues/921), [#927](https://github.com/aallan/vera/issues/927)).  `compare(a, b)` is the `Ord` ability spelling of the three-way if-chain `a < b ? Less : (a == b ? Equal : Greater)` (§6.4).  The type-checker accepted `compare` on operands of *any* type — its `op` parameter is a bare type variable — so `compare(Cons(1, Nil), Cons(1, Nil))` on two structurally-equal lists passed `vera check` **and** `vera verify`, then codegen lowered the `<` arm to a scalar `i32.lt_s` on the boxed heap *pointers* (allocation order), returning `Less` for equal values — a **silent wrong result**, the severest failure class.  The verifier had the parallel gap: its `<` translation was a bare Python `left < right`, which raises `TypeError: '<' not supported` on two Z3 `DatatypeRef`s — an uncaught traceback out of `vera verify`.  Ordering (`<`/`>`/`<=`/`>=`) is defined only on the orderable primitives — `Int`, `Nat`, `Float64`, `Byte`, `String` (§4.5) — so a user ADT (and `Bool`, which §4.5 excludes) is **not** Ord-comparable, unlike `Eq`/`Hash`/`Show`, whose "Satisfied by:" clauses include composite types.  The direct `MkBox(1) < MkBox(2)` form was already rejected with `E143`; `compare` now rejects on the same domain via the new **`E242`** ("Ord ability operation on non-orderable type") in `_check_ability_op_call` (`vera/checker/calls.py`) — the single gate both codegen and the verifier trust, so all three agree (checkability over silent miscompilation; DESIGN.md §"Checkability").  A bare type variable is deferred: inside a `forall<T where Ord<T>>` body `compare(@T, @T)` stays legal (the constraint promises orderability), and a non-Ord *instantiation* — a user ADT **or** `Bool` — is caught by the monomorphizer's `E613` constraint gate (`Bool` was wrongly listed among the `Ord`-satisfying types, so `Ord<Bool>` slipped past the gate and the clone lowered `compare` on two `Bool` i32s to a signed `i32.lt_s` — a silent order for an unorderable type; `Bool` is now excluded).  A defensive guard in `smt.py`'s `_translate_binary` also degrades a datatype- or Bool-sorted ordering to Tier 3 rather than crashing (the verifier monomorphizes minus the constraint filter, so a `Bool`-through-generic `compare` in a contract predicate reaches it with two Z3 `BoolRef`s), so the direct `verify()` API can never surface the `TypeError`.  **#927 (same lowering, one PR):** `String` *is* orderable, but the same codegen path lowered a `String` `<` to a scalar `i64.lt_s` on the `(ptr, len)` pair — both wrong-order and an i32/i64 type mismatch that crashed WASM translation (`vera check` / `vera verify` were green — the verifier already models `String` ordering via Z3's `StringSort`).  `String` ordering now lowers to a byte-wise three-way `$cmp_String` helper (`vera/wasm/operators.py`) — lexicographic, proper-prefix-is-less, matching Z3's `StringSort` ordering — so `vera verify` and `vera run` agree.  §9.8.1 and `SKILL.md` are corrected to state `Ord`'s satisfied set as exactly the orderable primitives (removing the erroneous `Bool`).
- **`==` / `!=` (and the `eq` ability operation) on a non-`Eq`-derivable type is now rejected cleanly at check time (`E243`) instead of silently pointer-comparing** ([#928](https://github.com/aallan/vera/issues/928); the equality sibling of #921, pre-existing on `release/v0.1.0`).  `==` / `!=` is the surface spelling of the `Eq` ability (§9.8.1), which derives **structurally** (§9.8.2): the `Eq` primitives, simple enums, and ADTs whose fields are (recursively) all `Eq`.  The type-checker's equality gate only required the two operands to *share* a type — it never asked whether that type was `Eq`-derivable — so `==` / `!=` (or `eq`) on operands with **no** structural equality passed `vera check` **and** compiled with zero diagnostics: a function-typed `==` (two structurally-identical closures) and a `State<Rec>` / composite `==` where `Rec` carries a `Map` field.  Unlike the direct-ADT path — which routes through `_translate_adt_eq` and raises a clean `E613` — these fell to a raw `i32` / pointer comparison that never reached the structural-`Eq` derivability dispatch, so `==` returned **pointer identity, not value equality** — a **silent wrong result**, the severest failure class.  A new **`E243`** ("Eq ability operation on non-Eq-derivable type") in the checker (`_check_eq_ability`, `vera/checker/expressions.py`, wired into both the `==` / `!=` binop path and the `eq` ability-op path in `vera/checker/calls.py`) rejects a non-derivable operand at the earliest, loudest stage, mirroring #921's `E242` for `Ord`.  The gate's derivability predicate (`is_eq_derivable`, `vera/checker/eq_ability.py`) mirrors codegen's structural-`Eq` dispatch (`_adt_satisfies_eq` / `_type_eq_derivable`, `vera/codegen/monomorphize.py`) exactly — accepting all `Eq` primitives, `Box`, recursive and nested-generic ADTs (`List<Int>`, `List<List<Int>>`), `Option`, `Result`, and Eq-field composites, and rejecting function types, `Array` / `Map` / `Set` / `Tuple`, and any composite carrying such a field — pinned by a checker↔codegen **cross-component differential** so the two can never desync (the #732 lockstep).  A bare type variable is deferred: inside a `forall<T where Eq<T>>` body `==` on `@T` stays legal, and a non-`Eq` instantiation is caught by the monomorphizer's `E613` gate.  The direct non-`Eq` ADT `==` (previously an `E613` at codegen) is now caught earlier, at check.
- **`show()` / `hash()` / `eq()` on a POLYMORPHICALLY-recursive (non-uniform) ADT now degrades to a clean skip instead of an uncaught Python traceback at compile** ([#933](https://github.com/aallan/vera/issues/933); a regression the #924 fix introduced for show/hash, addressed here in the same PR).  For a non-uniform generic like `data Box<T> { BNil, BCons(T, Box<Box<T>>) }`, `show(BCons(1, BNil))` passes `vera check` but its recursive-helper machinery mints a strictly deeper, DISTINCT type at every descent (`Box<Box<Int>>`, `Box<Box<Box<Int>>>`, …); #924's `_seen` / `_show_hash_pending` guards key on the full parameterized type, so they never fire, and generation recursed unboundedly into a raw `RecursionError` (exit 120 on `compile`, exit 1 on `run`) — where the base `release/v0.1.0` produced a clean `[E602]` skip.  The sibling `$eq_<type>` machinery (its `_adt_satisfies_eq` derivability gate and `_generate_adt_eq_fn`) crashed identically on the same shape, a pre-existing family-wide gap.  A shared nesting-depth cap (`DERIVED_HELPER_DEPTH_CAP`, `vera/skip.py`) now bounds the distinct-type descent in `_show_adt` / `_hash_adt` (`vera/wasm/calls_handlers.py`), `_generate_adt_eq_fn` (`vera/wasm/operators.py`), and `_adt_satisfies_eq` (`vera/codegen/monomorphize.py`): past the cap the walk yields the same "unsupported" signal a structurally-unrenderable field already produces, so `show`/`hash` fall back to a clean `[E602]` (function dropped, loud warning) and `eq`/`==` to a clean `[E613]` (a strict improvement over base, which crashed there too).  The cap fires on TYPE COMPLEXITY, independent of the interpreter's recursion limit, and a `RecursionError` catch at the codegen compile-fn / closure boundaries (`vera/codegen/functions.py`, `vera/codegen/closures.py`) backstops it — a `check`-green program can never surface a Python traceback (DESIGN.md principle 1).  Uniform shapes recur at CONSTANT nesting depth (`List<T>`, `Tree<T>`, `List<List<Int>>`, non-generic mutual `Forest`/`Rose`, two-field recursion, and the #924 deep-list GC cases) and stay far below the cap — all still render / hash / compare correctly with exactly one helper per type.  Mutation-validated: removing the bound flips the `Box<Box<T>>` tests back to `RecursionError` while the uniform controls stay green.

- **`show()` / `hash()` on a directly-recursive ADT now compiles and runs instead of being silently dropped at codegen** ([#924](https://github.com/aallan/vera/issues/924); pre-existing on both `release/v0.1.0` and `main`).  `show(Cons(1, Cons(2, Nil)))` on a `data List<T> { Nil, Cons(T, List<T>) }` passed `vera check` but died at `vera run` with `[E602]` "show() not supported for type 'List' — function skipped" → "No exported functions to call" (same for `hash`).  The [#911](https://github.com/aallan/vera/issues/911) structural traversal renders/folds composites INLINE and correctly terminated on a self-referential ADT via its full-parameterized-type `_seen` guard, but deliberately scoped out the recursive case (inline rendering cannot express unbounded depth) — cleanly skipping it.  #924 upgrades that clean-skip to an **actually-emitted, self-calling helper function** (`$show_<type>` / `$hash_<type>`, `vera/wasm/calls_handlers.py`) — one per recursive type, recursing over the FINITE value at run time — mirroring how structural `Eq` already derives one `$eq_<type>` helper for a recursive ADT ([#773](https://github.com/aallan/vera/issues/773)).  The recursion-guard branch in `_show_adt` / `_hash_adt` now requests a helper (deduped by name, re-entry-guarded by `_show_hash_pending` so exactly one helper is emitted per recursive type — codegen terminates) and emits a `call $<kind>_<type>` at the recursive field; the helper body reuses the inline emitters in a fresh local-allocation scope, wrapped by a GC prologue/epilogue that saves/restores `$gc_sp` and re-roots the returned String, so a deep (100-element) list renders under `VERA_EAGER_GC=1` with no shadow-stack overflow.  A cyclic value cannot be constructed in Vera (no mutation), so the run-time recursion is always finite.  Directly-recursive ADTs (`List<T>`, `Tree<T>`) and *non-generic* mutually-recursive ADTs (`Forest`/`Rose`) are covered; a *generic* mutually-recursive ADT whose type argument is buried in a nested generic field (`Grove(Rose<T>, Forest<T>)`) is still skipped — a pre-existing type-argument-recovery limitation shared with `Eq`.  The #911 finite-composite show/hash path is unchanged (its `TestCompositeShowHash` and conformance `ch09_show_hash_composites` stay green).  Mutation-validated: reverting the helper-emission branch to the old clean-skip flips the eight recursive tests RED while the finite cases stay green.  New run-level conformance program `ch09_recursive_show_hash`.
- **`show()` / `hash()` on a directly-recursive ADT now compiles and runs instead of being silently dropped at codegen** ([#924](https://github.com/aallan/vera/issues/924); pre-existing on both `release/v0.1.0` and `main`).  `show(Cons(1, Cons(2, Nil)))` on a `data List<T> { Nil, Cons(T, List<T>) }` passed `vera check` but died at `vera run` with `[E602]` "show() not supported for type 'List' — function skipped" → "No exported functions to call" (same for `hash`).  The [#911](https://github.com/aallan/vera/issues/911) structural traversal renders/folds composites INLINE and correctly terminated on a self-referential ADT via its full-parameterized-type `_seen` guard, but deliberately scoped out the recursive case (inline rendering cannot express unbounded depth) — cleanly skipping it.  #924 upgrades that clean-skip to an **actually-emitted, self-calling helper function** (`$show_<type>` / `$hash_<type>`, `vera/wasm/calls_handlers.py`) — one per recursive type, recursing over the FINITE value at run time — mirroring how structural `Eq` already derives one `$eq_<type>` helper for a recursive ADT ([#773](https://github.com/aallan/vera/issues/773)).  The recursion-guard branch in `_show_adt` / `_hash_adt` now requests a helper (deduped by name, re-entry-guarded by `_show_hash_pending` so exactly one helper is emitted per recursive type — codegen terminates) and emits a `call $<kind>_<type>` at the recursive field; the helper body reuses the inline emitters in a fresh local-allocation scope, wrapped by a GC prologue/epilogue that saves/restores `$gc_sp` and re-roots the returned String, so a deep (100-element) list renders under `VERA_EAGER_GC=1` with no shadow-stack overflow.  A cyclic value cannot be constructed in Vera (no mutation), so the run-time recursion is always finite.  Directly-recursive ADTs (`List<T>`, `Tree<T>`) and *non-generic* mutually-recursive ADTs (`Forest`/`Rose`) are covered; the *generic* mutually-recursive case is closed by #934 (next bullet).  The #911 finite-composite show/hash path is unchanged (its `TestCompositeShowHash` and conformance `ch09_show_hash_composites` stay green).  Mutation-validated: reverting the helper-emission branch to the old clean-skip flips the eight recursive tests RED while the finite cases stay green.  New run-level conformance program `ch09_recursive_show_hash`.

- **A generic mutually-recursive ADT whose type argument is buried in a nested generic field now recovers its full parameterized type at a `show`/`hash`/`eq` site** ([#934](https://github.com/aallan/vera/issues/934); pre-existing on both `release/v0.1.0` and `main`, and the type-argument-recovery limitation #924 scoped out).  For `data Forest<T> { Empty, Grove(Rose<T>, Forest<T>) }` / `data Rose<T> { Bloom(T, Forest<T>) }`, NEITHER field of `Grove` IS a bare `T` — they are `Rose<T>` and `Forest<T>` — so no constructor argument directly pinned `T`, type-argument recovery returned `None`, and the site fell back to the bare head `Forest`, losing `<Int>`.  The consequences differed by ability: **`eq`/`==` was a SILENT wrong result** — the composite `==` degraded to a bare-pointer `i32.eq` of two freshly-allocated, structurally-equal heap values → `0`, no diagnostic; **`show`/`hash` was silently dropped** at codegen (`[E602]`, function not exported).  Recovery now DESCENDS into the nested generic field's own argument: `Grove(Bloom(1, Empty), Empty)`'s field-0 argument `Bloom(1, …)` is a `Rose<T>` whose `Bloom(T, …)` declaration pins `T = Int` from the literal `1`, yielding `Forest<Int>`.  The descent is applied on BOTH the show/hash path (`_recover_ctor_ptype` / new `_recover_ptype_via_nested_fields`, `vera/wasm/calls_handlers.py`) and the structural-`eq` path (`_parameterize_ctor_operand` falls back to the same recovery, `vera/wasm/operators.py`), and is bounded by a `_seen` set of ADT names so the mutually-recursive descent terminates.  The recovered `Forest<Int>` now routes `eq` to the structural `$eq_Forest<Int>` helper (which cross-calls `$eq_Rose<Int>`) and lets the #924 `$show_`/`$hash_` helpers emit, so `eq` returns the correct answer and `show`/`hash` render/fold deterministically.  No silent over-accept: a generic-mutual ADT with a genuinely non-`Eq` leaf (an `Array<Int>` field) still raises a loud `E613`, and a non-`Show` leaf (a `Map` field) still drops with a loud `E602` — the recovery only restores the type argument, it does not weaken the derivability gates.  Controls unaffected (non-generic mutual and directly-recursive generic `List<T>` stay green).  Mutation-validated: reverting the nested-descent recovery flips the generic-mutual `eq`/`show`/`hash` and the non-`Eq`-leaf `E613` tests RED while the controls stay green both directions.  `ch09_recursive_show_hash` gains generic-mutual show/hash/eq cases.

- **A non-`Eq` composite `==`, or an unsupported `hash`/`show`, in a CONTRACT-PREDICATE position now degrades to a clean diagnostic instead of an uncaught Python traceback at compile** ([#922](https://github.com/aallan/vera/issues/922); pre-existing on both `release/v0.1.0` and `main`).  The [#912](https://github.com/aallan/vera/issues/912) graceful degradation (emit `E613`/`E602`, drop the function) was wired at the function-body, lifted-closure, and *postcondition* contract sites, but three sibling contract-predicate sites were left unguarded: the **precondition** (`requires`) translated its predicate with no surrounding `try/except`, so a `Tuple<Int,Int> == Tuple<Int,Int>` in a `requires(...)` raised an uncaught `AdtEqNotDerivableError`; the **refinement-type parameter guard** (`{ @T | pred }`) caught only `CodegenSkip`, so the same composite `==` in a refinement predicate escaped; and the **postcondition** backstop caught only `AdtEqNotDerivableError`, so an `ensures(hash(recursiveADT) == 0)` (or `show`) escaped as an uncaught `CodegenSkip`.  Every contract-predicate `translate_expr` call site (`vera/codegen/functions.py`, `vera/codegen/contracts.py`) is now guarded by the SAME `(AdtEqNotDerivableError, CodegenSkip)` degradation the postcondition path uses — a non-derivable `==` becomes a clean `E613`, an unsupported `hash`/`show` a clean `E602`, and the enclosing function is dropped — so a `check`-green program can never surface a Python traceback from a contract predicate (DESIGN.md principle 1; spec contract §516/§522/§589).
- **`==` / `eq` on a nested-generic recursive ADT (e.g. `List<List<Int>>`) now compiles and runs instead of being rejected with a spurious `[E613]`** ([#923](https://github.com/aallan/vera/issues/923); a check↔codegen ability-derivability divergence).  `vera check` computes `Eq`-derivability with full type-argument recursion, but the codegen direct-`==` path recovered the operand's parameterized type name only ONE level deep — a `Cons(Cons(1, Nil), Nil)` operand whose type is `List<List<Int>>` was reconstructed as `List<List>`, a bare-argument shape the derivability path treats as a lost-type-arg clone and refuses, even though every structural component is `Eq`-derivable.  `_parameterize_ctor_operand` (`vera/wasm/operators.py`) now reconstructs the operand name RECURSIVELY through nested constructor fields (`List<List<Int>>`, `List<List<List<Int>>>`, `Chain<Pair<Int>>`, …), closing the divergence so codegen accepts exactly what the checker accepts.  A genuinely non-`Eq` nested component (e.g. an `Array`/`Map`/`Set` field anywhere in the nesting) is STILL rejected with a clean `[E613]` — the fix recovers the type argument, it does not over-accept — keeping the #732 checker↔codegen derivability differential aligned.
- **The same nested-generic `Eq` value reached THROUGH an `Eq`-constrained generic call (e.g. `eq2(Cons(Cons(1, Nil), Nil), …)` on a `forall<T where Eq<T>>`) now compiles and runs instead of a spurious `[E613] 'List<List>'`** ([#932](https://github.com/aallan/vera/issues/932); the generic-call/clone sibling of #923).  #923 fixed the direct-`==` path, but the generic-call path recovers the constrained-var operand's type name via the shared clone-mangler, which recovers only ONE level of nested type argument (`List<List<Int>>` → `List<List>`) — that truncated name then fed both the `E613` constraint gate (`_check_constraints`) and the direct-`==` path inside the monomorphized clone body, spuriously rejecting a fully `Eq`-derivable type that `vera check` accepts.  The fix recurses the type-argument recovery **for the derivability DECISION only** (a new `Monomorphizer.full_arg_type_name` populates a side-map consulted by `_check_constraints` and by `_translate_binary`'s clone-body `==`), leaving the **mangled clone name untouched** — the emitted symbol stays `eq2$List<List>`, so the #772 clone-mangling contract (and its `test_772_ctor_path_nested_adt` regression guard) is unaffected.  A genuinely non-`Eq` nested leaf (`List<List<Array<Int>>>`) reached through the same generic call is STILL rejected with a clean `[E613]` — the recursion recovers the type argument, it does not over-accept.

- **A same-ADT self-nested constructor literal (`Some(Some(x))`) no longer crashes the verifier with an uncaught Z3 exception on a `vera check`-green program** ([#918](https://github.com/aallan/vera/issues/918)).  The verifier's `_find_sort_for_ctor` (`vera/smt.py`) resolved a constructor to its ADT and returned the first cached Z3 sort whose *base* name (before `<`) matched, with no discrimination by type arguments — so the outer `Some` in `Some(Some(x))` picked up whichever `Option<…>` instantiation was cached first (e.g. `Option<Int>`) rather than the needed `Option<Option<Int>>`, feeding `sort.constructor(idx)` a wrongly-sorted `DatatypeRef`.  The crash surfaced three ways: a nested-`Option`-returning body raised `z3.z3types.Z3Exception: Sort mismatch`; a nested same-ADT ctor literal in a contract (once an `Option<Int>` sort was seeded) raised `AttributeError: 'DatatypeSortRef' object has no attribute 'is_int'`; and `vera test` on such a function crashed identically (it routes through the verifier).  `_translate_ctor_call` now translates a constructor's arguments first, recovers each argument's Vera type from its Z3 sort, and unifies those against the constructor's declared (`TypeVar`-bearing) field types to pin the owning ADT's **full** instantiation — so `Some` in an `Option<Option<Int>>` context selects the `Option<Option<Int>>` sort (whose argument slot correctly expects `Option<Int>`, matching the inner `Some(x)`).  This enables genuine Tier-1 reasoning over the nested structure: a true `ensures(result == Some(Some(x)))` is now proved, and a false one (`Some(Some(x + 1))`) is still disproved (E500) — no false prove.  Pinning is gated to ADTs that already have a cached instantiation, so a top-level `Some(42)` argument in a caller whose context never materialised `Option` stays opaque and demotes at the call site exactly as before (#882), avoiding the false-E500 regression a broad on-demand materialisation would cause (#887).  The fix covers **`Nat`-instantiated** generics identically to `Int` ones at every nesting depth: a `@Nat.0` argument translates to a Z3 `IntSort` value (the shared carrier of `Int` and `Nat`), so recovering its type reads back `Int` even in an `Option<Nat>` context — and post-#884 the datatype-sort mangling is injective, making `Option<Nat>` (`Option_LNat_R`) and `Option<Int>` (`Option_LInt_R`) *distinct* Z3 sorts.  So the pin selects among *already-cached* instantiations modulo the `Nat`↔`Int` carrier ambiguity (never materialising a fresh `Int`-keyed twin of an existing `Nat` sort that would sort-mismatch it), and `Option<Nat>`, `Option<Option<Nat>>`, and a user `Box<Box<Nat>>` all verify cleanly — as do a single-level `Some(x)` and a different-ADT nesting (`Cons(Some(x), Nil)`), which were never affected.
- **A generic whose body reads a `@T`-typed slot, instantiated at the zero-size `Unit` type, is now rejected cleanly at check time (`E206`) instead of passing `vera check` and crashing in codegen** ([#900](https://github.com/aallan/vera/issues/900); surfaced by the PR #899/#878 edge-shape probe, pre-existing on both `release/v0.1.0` and `main`).  `Unit` is 0 bytes with no WASM representation (spec §11.2.2 — a `Unit`-returning function has no result type, and a `UnitLit` compiles to nothing), so a monomorphized `forall<T>` body's `@T.n` slot *read* lowers to a `local.get` with no local — the dangling-slot codegen invariant, whose exact symptom was hash-seed-dependent (`E699`, a dropped `main`, or a "file a bug report" `CodegenInvariantError`).  The checker's type-argument inference (`_check_fn_call_with_info`, `vera/checker/calls.py`) now detects when a `forall` type parameter *that the declaration's body reads* is inferred to bare `Unit` and reports the new **`E206`** ("Generic type parameter instantiated at Unit") with a concrete fix, per DESIGN.md principle 1 (checkability over correctness — a `check`-green program must never reach a codegen crash).  The check is narrowed to bodies that materialize `@T`: a `@T` parameter the body never reads erases cleanly from the ABI, so `firstInt(@T, @Int){ @Int.0 }` and `ignore(@T){ 0 }` still run at `T = Unit` and are **not** rejected (the body-read set is computed once at registration via a `@T`-`SlotRef` walk).  Keyed to *bare* `Unit` only: a boxed `Option<Unit>` (tag + pointer, non-zero-size) remains a valid type argument, and the built-in generics (`async`/`await` and the collection + prelude combinators) are excluded because their hand-written codegen leaves an EMPTY body-read set, so `async(IO.print(...))` keeps its `Future<Unit>` behaviour and `W002` warning — while a *user* `forall<T>` that shadows a prelude name (`option_map`) and reads `@T` is still caught (the exclusion keys on the empty body-read set, not the function name).  The check fires for every way `T` is pinned to `Unit` when the body reads it — a `@Unit`-returning function passed as the generic argument, a bare `()` literal, `Unit` binding the parameter through one of several arguments, and reads nested in `let` / `if` / `match` — since Vera infers all type arguments from argument types (there is no explicit-instantiation syntax) through this single call site.
- **A `Tuple` (or any constructor) with a zero-size `Unit` component now compiles and runs instead of silently dropping the function and dangling its call** ([#902](https://github.com/aallan/vera/issues/902); the by-value sibling of #900, pre-existing on both `release/v0.1.0` and `main`).  `Tuple((), 3)` (return type `Tuple<Unit, Int>`) passed `vera check` but crashed at codegen with `unknown func: failed to find name $mkt`: the constructor-call lowering (`_translate_constructor_call`, `vera/wasm/data.py`) could not infer a WASM type for the `Unit` field (spec §11.2.2 — `Unit` is 0 bytes, `UnitLit` compiles to nothing), raised a `CodegenSkip`, and omitted the whole function from the emitted WAT while its caller still called it.  A `Unit` field is now laid out as a genuine zero-size field — its argument instructions still run (for any side effect), but it occupies no bytes and stores nothing, mirroring the existing `Unit` skip in `_translate_let_destruct` — so the enclosing function is emitted and the trailing fields keep their correct offsets (a boxed `Option<Unit>` already worked; this closes the inline by-value case).  Unit detection uses `_is_void_expr` (the canonical "produces no stack value" check), so a **`Unit`-returning call** in field position — `Tuple(IO.print("x"), n)`, a user `@Unit` fn, a void effect op, a `ModuleCall` to a `@Unit` fn — is also laid out zero-size with its side effect preserved, not just a bare `()` literal.  Covers `Unit` in any tuple position, several `Unit` fields, nested tuples, a user ADT with a `Unit` field, and a side-effecting `Unit`-returning field.
- **`Eq` auto-derivation over a sparse multi-type-parameter ADT on the constructor-inferred path now type-checks a fully-determined instance and reports an accurate diagnostic for a genuinely under-determined one** ([#898](https://github.com/aallan/vera/issues/898), the sibling of #772; surfaced by the PR #897 adversarial soundness review).  For `data Res<A, B> { MkOk(A), MkErr(B) }` and an `Eq`-constrained generic, a call whose ADT argument is built from a **single** constructor pins only that constructor's type parameter (`MkErr(5)` fixes `B`, not `A`), so recovery used to collapse to the bare name `Res` and reject with a spurious `E613`.  Two fixes:

  - **Cross-argument type-argument merge (checker + monomorphizer).**  `eq2(MkErr(5), MkOk("x"))` is a fully-determined `Res<String, Int>` — the first argument fixes `B`, the second fixes `A` — but first-argument-wins binding rejected it (`E202`): the checker bound `@T` to the first argument's whole `Res<A$1, Nat>` (fresh `A$1`) and the concrete second argument did not unify.  The checker's inference (`_infer_type_args` / `_unify_for_inference`, `vera/checker/resolution.py`) now **merges** per-type-parameter information across the arguments via a shared `merge_inferred_types` primitive (`vera/types.py`): a fresh inference placeholder on either side yields to the more-determined sibling (`Res<?, Int>` ⊔ `Res<String, ?>` = `Res<String, Int>`), Nat/Int LUB to Int, and two *distinct concrete* types at the same position are a genuine **conflict** — reported as the new **`E205`** ("Conflicting type argument inference", `eq2(MkOk("x"), MkOk(5))` fixing `A` to both `String` and `Int`) rather than a misleading wrong-type `E202`.  The monomorphizer's discovery (`Monomorphizer._infer_type_args_from_args`, `vera/monomorphize.py`) and the WASM call-site rewriter (`_resolve_generic_call`, `vera/wasm/calls.py`) apply the **same** per-parameter merge across the call's constructor arguments, so codegen, the #732 verifier (which shares the discovery), and the call-rewrite all monomorphize the identical `Res<String, Int>` clone — the checker-accepted call compiles, runs by structural value comparison, and verifies Tier 1.  Soundness held: a per-parameter conflict is never silently merged (the `is_subtype` check is a backstop even if `E205` detection were disabled), and a fully-determined **non-Eq** cross-arg type (`eq2(MkErr([1]), MkOk(2))` = `Res<Int, Array<Int>>`) type-checks then rejects with a clean `E613`.

  - **`E619` diagnostic accuracy.**  When a type parameter is *genuinely* free (a single-constructor call, `id1(MkErr(5))` leaving `A` unbound), rejection is correct — structural Eq derivation checks *every* constructor's fields, so `Res<A, Int>` derives iff `A` is Eq, which the call cannot decide — but the old `E613` ("`Res` does not satisfy `Eq`") misdescribed it.  The ability gate (`_check_ability_constraints` / `_eq_type_arg_under_determined`, `vera/codegen/monomorphize.py`) now emits the clearer **`E619`** ("Cannot infer type argument for ability-constrained parameter", annotate the free parameter) **only** when the type would derive once its free parameters are bound to Eq types — i.e. every *recovered* component is itself Eq.  If a known component is already non-Eq — recovered (`id1(MkErr([1]))` → `Res<A, Array<Int>>`, `B` fixed to a non-Eq `Array`) or structural (`data W<A,B>{ K(Array<A>, B) }`, `id1(K([1], 7))` → an `Array<A>` field non-Eq for any `A`) — annotating the free parameter cannot help, so the accurate `E613` is emitted instead.  The monomorphizer materialises a free slot as a reserved `?` sentinel (`Res<?, Int>`, never an emitted clone) so the gate can probe derivability with each free slot provisionally bound to an Eq type.

  A call whose ADT type parameter is left genuinely under-determined by *every* argument — the same constructor on both sides so no argument pins the other parameter (`eq2(MkOk(3), MkOk(4))`, `Res<Nat, B>` with `B` free) — is still (correctly) rejected at the checker with `E202`: the type is not fully determined, so this is a sound false-reject, unchanged from before.  Only a fully-determined cross-argument instantiation is newly accepted.

  Mutation-validated five ways, each flipping only its discriminating tests RED: neutering `merge_inferred_types` (mergeable checker-accept tests), suppressing its conflict flag (the `E205` tests fall back to the `E202` backstop), collapsing the monomorphizer merge (the cross-arg run/`E613` tests return `E619`), skipping the gate's component-Eq probe (the two `E613`-accuracy tests return `E619`), and dropping the WASM-side merge (the cross-arg run drops `main` with `unknown func $eq2$Res`).  The #732 differential's inline and call-rewrite corpora each gain the cross-argument-merge shape, so a future regression to the checker↔codegen↔call-rewrite merge is caught by the soundness oracle (an asymmetric desync surfaces as a dangling `eq2$Res` clone; a symmetric collapse trips the vacuous-emission guard).  The E619 message renders each free slot by its declared parameter name (`Res<A, Int>`, never the internal `?` sentinel) and its fix is a compilable annotation (`let @Res<Int, Int> = ...;`).  The #772 single-type-parameter path (`Box<String>` accept, `Box<Array>` reject `E613`) and the #878 shared clone-name derivation are untouched.  New conformance program `ch09_multiparam_ctor_eq` (run level) exercises both the annotated and the cross-argument-merged fully-determined `Res` deriving Eq end-to-end.
- **Transitive module imports now compile and run — a `main → mid → base` chain no longer crashes at `vera run` with `unknown func`** ([#890](https://github.com/aallan/vera/issues/890)).  When `main` imported `mid` and `mid` imported `base`, a check-green program with **zero generics** died at WASM compilation: `mid`'s body calls `base::wrap40`, but `base` was never registered or compiled into the importer's flat module, so the emitted body was left with a dangling call and wasmtime rejected the WAT with `unknown func: failed to find name $via_mid`.  Root cause was in `vera/resolver.py`: `ModuleResolver.resolve_imports` returned only the top-level program's **direct** imports (`[mid]`), even though its recursion already parsed and cached the transitively-reached `base` — so codegen, the checker, and the verifier all consumed a module list missing every transitive dependency.  `resolve_imports` now returns the **transitive closure** of reachable modules, deduplicated (a diamond `main → {left, right} → base` yields `base` once), with each module tagged `direct` (imported by the top-level program) vs transitive-only (reached through another module's imports).  Codegen harvests and compiles every module in the closure into the flat WASM module, so an imported body's call to a deeper module resolves.  Spec §8.6.4 is preserved — a transitive module's declarations are **not** visible to the original importer: the checker and verifier inject only `direct` modules into the importer's callable namespace and qualified-call registries (so `main` can neither bare-call `wrap40` nor write `base::wrap40`), and codegen subtracts a `_transitive_only_names` set from its cross-module guard rail so a *main-program* body calling a purely transitive symbol fails loudly at compile rather than silently binding to the definition emitted for a sibling module.  Cycle-safe (the resolver's existing `_in_progress` guard catches import cycles as E011 before closure assembly).  Pre-existing on main; found by the PR #888 review.  Mutation-validated: reverting `resolve_imports` to the direct-only return flips the chain and diamond run tests RED with the original `unknown func`, and disabling the `_transitive_only_names` subtraction flips the §8.6.4 visibility test RED (no error emitted for `main`'s bare call to a transitive symbol).  A run-level conformance program (`ch08_transitive_module_import` + its `_mid`/`_base` companions) pins the end-to-end behaviour.
- **Z3 datatype sort names are now injective — distinct Vera types can no longer collide on one sort name and produce a false counterexample** ([#884](https://github.com/aallan/vera/issues/884)).  The verifier named Z3 datatype sorts with the old lossy sanitize (`key.replace("<", "_").replace(">", "").replace(", ", "_")`), so a monomorphized `Box<Int>` and a flat ADT literally named `Box_Int` both became the Z3 name `Box_Int`.  Z3's per-context datatype cache conflates same-named sorts — the last `create()` wins and earlier same-named sorts silently adopt its structure — so `Box<Int>`'s sort acquired the flat ADT's `MkBoxInt(Bool)` constructor.  The manifestation was a **spurious rejection of valid code**: a trivially-true `ensures` was rejected `[E500]` with the counterexample `@Box<Int>.0 = MkBoxInt(False)` — the flat ADT's Bool constructor witnessing a `Box<Int>` slot (the decisive control: the byte-identical program with the flat ADT renamed verifies 6-Tier-1 clean).  This was the exact lossy-encoding class [#775](https://github.com/aallan/vera/issues/775) fixed for WAT symbols, still latent on the verifier side.  Both Z3 sort-name sites (`_get_or_create_adt_sort` and `_get_or_create_tuple_sort` in `vera/smt.py`) now route through the injective `mangle_type_name` mangler #775 introduced (`Box<Int>` → `Box_LInt_R`, the flat `Box_Int` → `Box__Int`), so distinct type keys can never share a Z3 sort name; the full-type-string sort cache and the #871 nested-`Float64` `fpEQ` walk are unaffected (the cache is keyed by the type string, not the Z3-visible name).  Soundness-safe: the conflation only *freed* field constraints, so it produced conservative false negatives, never a false Tier-1 — a genuinely-false `ensures` over the colliding pair is still disproved after the fix, and now for the right reason (its counterexample witnesses `@Box<Int>.0` with its own `MkBox` constructor, never the flat ctor).  Both sort-name sites are independently pinned: the `_get_or_create_adt_sort` site by the `Box<Int>`/`Box_Int` collision test, and the `_get_or_create_tuple_sort` site by a `Tuple<Tuple<Int>, Int>` vs `Tuple<Tuple<Int, Int>>` collision that (under the lossy sanitize) both name `Tuple_Tuple_Int_Int` and crash the verifier at `sort.accessor` with `Invalid accessor index` — so reverting either site to the lossy sanitize now flips a test RED.  The rename also required inverting the mangler on the verifier's Array-element reverse lookup: `_get_element_sort_for_array`'s tier-3 fallback reconstructed the `_z3_sorts` key by string surgery (`elt_key.replace("_", "<", 1) + ">"`) that assumed the old lossy naming, so after the rename a generic-ADT element (`Array<Box<Int>>` → sort `Array_Box_LInt_R`) produced the non-matching candidate `Box<LInt_R>` and the lookup silently returned `None` — latent (tier 1's creation-time direct map fires for every live path, and no corpus program indexes an `Array<Generic<T>>` in a contract this way), but dead-on-arrival for that shape.  A new injective inverse `unmangle_type_name` (a left-to-right prefix-code decode, round-trip-tested against `mangle_type_name`) now recovers the key correctly.  Pre-existing on main; found by the PR #883 adversarial panel while auditing the mangler fix for stragglers.  Mutation-validated: reverting either sort-name site to the lossy sanitize re-collides the sorts and flips its collision test RED, and reverting the Array-element lookup to the old string surgery flips its reverse-lookup unit test RED.
- **A generic instantiated over a parameterized ADT now runs instead of trapping in its monomorphized clone** ([#891](https://github.com/aallan/vera/issues/891)).  `gid(MkBox(7))` for `gid : forall<T> fn(@T -> @T)` and `data Box<T> { MkBox(T) }` passed `vera check` and `vera verify` but trapped at `vera run` with `type mismatch: expected i64, found i32` while compiling the `gid$Box` clone.  Root cause is in `_infer_expr_wasm_type`'s `ResultRef` arm (`vera/wasm/inference.py`): once `T = Box`, the clone's postcondition `@T.result == @T.0` compares two `i32` heap pointers, but the `ResultRef` (`@Box.result`) was inferred as the scalar `i64` default — the arm handled only Int/Nat/Float64/Bool/Byte and returned `None` for everything else — so the comparison fell through to the `i64.eq` default while the operands were `i32`, an ill-typed clone body wasmtime rejects at instantiation (the `SlotRef` operand `@Box.0` was already correctly `i32`, and the clone's `param`/`result` signature was already `i32`; only the postcondition op disagreed).  The `ResultRef` and `SlotRef` arms now share one `_ref_type_name_wasm_type` helper, so an ADT-bound (and pair-/handle-bound) type variable lowers to `i32` consistently across the signature and every slot/result read.  New conformance program `ch02_generic_over_param_adt` (run level; suite now 119) plus a regression test in `tests/test_codegen_monomorphize.py` pin `gid(MkBox(42))` unwrapping to `42`.  Mutation-validated: reverting the `ResultRef` arm to the pre-fix scalar-only logic restores the `expected i64, found i32` trap, flipping the test RED.  Pre-existing on main; found by the PR #888 soundness review.
- **`Eq` auto-derivation is now type-argument-aware on the constructor-inferred path** ([#772](https://github.com/aallan/vera/issues/772)).  An `Eq`-constrained generic (`forall<T where Eq<T>>`) called with a value whose type is inferred from a **constructor** — `eq2(MkBox("a"), …)` — dropped the type argument: the monomorphizer resolves a `ConstructorCall` argument to the **bare** ADT name `Box` (correct for clone mangling, since a `Box<T>` is a uniform pointer), which left the codegen `Eq` gate (`_adt_satisfies_eq`) with no `<String>` to inspect.  Originally (pre-#773) this **false-accepted** `Box<String>` and emitted an unsound equality comparing the wrong representation; after #773 made derivation structural it flipped to an **over-reject** — a `Box<String>` that *should* derive `Eq` was refused with a spurious `E613`, even though the slot-ref call form (`let @Box<String> = …; eq2(@Box<String>.0, …)`) — which carries the type argument — was accepted and ran correctly.  Root cause: the direct type-var branch of `Monomorphizer._unify_param_arg` (`vera/monomorphize.py`) resolved a `ConstructorCall` via `_infer_vera_type_name`, which returns the bare name, whereas the parameterized (`Option<T>`) branch already recovered type args via `_get_arg_type_info`.  The fix scopes recovery to **constrained** type vars: a `ConstructorCall` bound to a `@T` that carries an ability bound now keeps its type argument (`Box<String>`, via `_get_arg_type_info`) for the clone name, the substituted slot type in the clone body, AND the `E613` gate — so the constructor path derives / rejects **exactly** as the slot-ref path does, while unconstrained generics stay bare (one `id$Box` clone for every instantiation, no needless splitting).  The parallel WASM call-site rewriter (`_unify_param_arg_wasm` in `vera/wasm/calls.py`, plus the return-type resolver in `vera/wasm/inference.py`) applies the same constrained-var recovery so the mangled call name matches the emitted clone (a new `_generic_constrained_vars` side table threads the bound set), and the **direct `==`** path (`Some(1) == Some(1)`, previously the same over-reject) recovers the operand's type argument in `vera/wasm/operators.py` so the structural helper resolves the field type.  Soundness held throughout: `Box<Array>` / `Box<Map>` (a non-`Eq` type argument) is still rejected with `E613` on the constructor path, no `E699` codegen-invariant leak, and the checker↔codegen structural-Eq differential (#732 / #773) stays symmetric.  Mutation-validated in both halves — disabling the discovery-side recovery flips the generic-path tests RED (E613 returns), disabling the direct-path recovery flips the `Some(1) == Some(1)` test RED — and the full existing `Eq` suites pass unchanged.  New conformance program `ch09_ctor_inferred_eq` (run level) exercises `Box<String>` and `Box<Inner>` derivation through the constructor path end-to-end.
- **`float_to_string` is now total over all `Float64` values, including the non-finite classes** ([#857](https://github.com/aallan/vera/issues/857)).  The built-in was documented total but its finite digit-extraction path rounds via `i64.trunc_f64_s`, which **traps** on the IEEE 754 non-finite values that the math built-ins legitimately produce (§9.6.10): a `NaN` input (`nan()`, `log(-1.0)`) died with a raw `invalid conversion to integer` WASM trap — no E-code, no rationale — and a `±inf` input (`infinity()`, or `-inf` from `log(0.0)`, which #790 made more reachable) overflowed into the E-coded **Integer overflow** diagnostic, whose Fix text steers the user to add integer `requires` preconditions — a diagnostic-as-instruction pointing in a wrong direction for a float-printing call, exactly the failure class the design principles rule out (DESIGN.md; spec §0.2(1) — loud and total over silent and partial).  A non-finite short-circuit now runs *before* the trunc path (`f64.ne(x, x)` for NaN, `f64.abs(x) == inf` for the infinities, `f64.lt 0` for the sign) and renders one canonical ASCII spelling per class — `"nan"`, `"inf"`, `"-inf"` — matching Python's `str()` and the issue's stated expectation.  Because `float_to_string` compiles to inline WASM (no host import), the Python host runtime and the browser runtime execute the *same* module and emit these bytes identically by construction — the cross-runtime parity the issue requires, pinned by a true node↔wasmtime differential (`test_non_finite_render_parity` in `tests/test_browser.py`, passed not skipped) asserting byte-identical stdout `nan,inf,-inf,-inf` across both runtimes, plus four wasmtime regression tests (`TestFloatToString` in `tests/test_codegen_string_builtins.py`) covering each class including the issue's exact `log(0.0)` → `-inf` repro.  The finite path is unchanged.  Spec §9.6.11 and §9.8.1 (`Show`) document the totality and the spellings; SKILL.md's one-liner updated.  Mutation-validated: neutering the short-circuit (`done` never set) reverts the NaN trap and the ±inf overflow, flipping the tests RED.
- **An int literal coerced to `@Byte` now lowers at `i32` at the call-argument and let-binding positions** ([#865](https://github.com/aallan/vera/issues/865)).  `byte_lt(3, 7)` for `fn byte_lt(@Byte, @Byte -> @Bool)` passed `vera check` — the checker deliberately accepts a 0..255 int literal where a `@Byte` is expected (bidirectional coercion, the same allowance that lets a `@Byte`-returning body be a bare literal) — but emitted `i64.const` arguments against the callee's `i32` `@Byte` parameters (`@Byte` is `i32`, spec §11), so the module failed wasmtime instantiation at `vera run` with `type mismatch: expected i32, found i64`.  The checker-position decision follows the #766 precedent (PR #864) and DESIGN.md §0.2 (no implicit behaviour, but codegen must emit valid WASM for everything the checker accepts): the coercion is intentional and codegen owns the width, so a literal reaching an `@Byte` parameter is now pushed as `i32.const` — mirroring the #766 binop-operand fix at the call-argument site (a per-parameter `_fn_byte_params` bitmap, harvested cross-module and onto monomorphised names alongside the `@Nat`/`@Int` guard bitmaps).  A non-literal `@Byte` argument (a Byte slot ref, a Byte-returning call) already lowered at `i32`, so only the literal needed the override; the fix covers the direct, nested-call, and match-arm argument shapes.  The same literal→Byte coercion gap at the **let-binding** site (`let @Byte = 42`, which also emitted `i64.const` into an i32 local) is fixed in the same place, and its refined-Byte sibling — `let @{ @Byte | @Byte.0 < 10 } = 5` was rejected at `vera check` with `E170` because the checker's IntLit→Byte coercion checked `isinstance(expected, PrimitiveType)` and did not see through the refinement wrapper — is closed by resolving the expected type through refinements (`base_type`) before the Byte check, with the 0..255 bound preserved (`300` bound to a refined Byte still correctly `E170`s).  New conformance program `ch01_byte_literal_coercion` (run level; suite now 109) pins the call-argument and let-binding shapes end-to-end.  Mutation-validated three ways: reverting the call-argument width dispatch flips the four call-argument tests RED, reverting the let-binding lowering flips the two let tests RED, and reverting the checker refinement-resolution flips only the refined-let acceptance test RED (the range-bound rejection stays green).  Pre-existing on main since Byte coercion landed; found by the PR #864 review's blast-radius sweep.
- **Mutually-recursive `data` declarations now build their Z3 sorts together instead of crashing `vera verify` with a raw `RecursionError`** ([#881](https://github.com/aallan/vera/issues/881)).  A mutual pair — `data A { MkA, WrapB(B) }` / `data B { MkB, WrapA(A) }` with a function over `@A` — passed `vera check` and ran under `vera run`, but `vera verify` aborted with an uncaught Python `RecursionError` deep in `_get_or_create_adt_sort`: a check-green program yielding a raw interpreter traceback, the same "check-green → pipeline crash" class as the [#861](https://github.com/aallan/vera/issues/861) refinement-predicate Z3 exception and the [#871](https://github.com/aallan/vera/issues/871) sort-equality lineage.  The *self*-recursive case was handled by a single `self_ref_key`/`self_ref_dt` pair threaded through sort creation, but that pair could name only ONE in-progress sort, so building `A` recursed into building `B`, which recursed into building `A` again, and so on until the recursion limit tripped (FP-independent — an `Int`-only pair crashed identically).  Sort construction now declares every datatype reachable through constructor fields — directly or through a `Tuple` field — together as one strongly-connected group via `z3.CreateDatatypes` (`_collect_adt_group` + a builder map in `_vera_type_to_z3_sort`, `vera/smt.py`) — the sound, complete option that keeps Tier-1 static reasoning (DESIGN.md: maximise static guarantees) — with self-recursion the singleton-group case and the `key -> z3.Datatype` name transformation hoisted to one `_z3_sort_name` choke point.  A `Tuple`-mediated cycle — `data A { MkA(Tuple<B, Int>) }` / `data B { MkB(A) }`, or a self-recursive `data C { MkC(Tuple<C, Int>) }` — is pulled into the same group as a single-constructor `Tuple` member so its back-reference stitches to the shared in-progress builder; without this the `Tuple` sort was built by a *fresh* `_get_or_create_adt_sort` call that did not share the group's builders, re-entered sort creation for the still-uncached member, and recursed into the identical `RecursionError` (an alias resolving to such a `Tuple` was affected too; a refinement-wrapped field was already unwrapped and unaffected — `Tuple` was the only structural carrier).  Because the sort now BUILDS, the [#871](https://github.com/aallan/vera/issues/871) Float64-cycle guard is reachable for mutually-recursive types: a *non*-Float64 mutual pair proves reflexivity Tier-1 (structural `=` agrees with the runtime), while a *recursive* Float64-containing mutual pair has no finite equality expansion and demotes to an honest, loud Tier-3 runtime check rather than a false proof — the same split holds for a `Tuple`-mediated cycle (non-FP proves Tier-1, recursive-FP-through-`Tuple` demotes to Tier-3).  A before/after `verify --json` census over all 37 examples shows zero tier movement (no corpus program declared mutually-recursive `data` before this).  New conformance program `ch02_adt_mutual_recursive` (run level) exercises a `Tree`/`Forest` mutual pair through check, verify, and run, and `ch02_adt_tuple_recursive` (run level) a self-recursive `Chain` through a `Tuple<Chain, Int>` field.  Mutation-validated: reverting the direct **or** `Tuple`-mediated group construction re-raises `RecursionError` on the corresponding tests.  Pre-existing on main; found by the PR #879 adversarial review.
- **Cross-module generics are now monomorphized by the importer — an imported generic instantiated only by the importer no longer crashes at run** ([#774](https://github.com/aallan/vera/issues/774)).  A `public forall<T> fn gid(@T -> @T)` imported from another module and instantiated only at the importer's call site (`gid(42)`) was monomorphized NOWHERE — the importer's Pass-1.5 discovery built from its own `program.declarations` (the imported generic isn't there) and the defining module only monomorphized its own instantiations — so `vera check` passed but `vera run` failed WASM validation at `call $gid` (the `gid$Int` clone was never emitted), for BOTH the bare and the module-qualified (`genmod::gid(42)`, an `ast.ModuleCall`) forms.  The importer now harvests imported PUBLIC generics (`_register_modules`), merges the *unshadowed* ones into its `generic_decls` so discovery + emission + the call-site rewrite (`_generic_fn_info`) all cover them, and the shared discovery walks `ModuleCall` targets too — so both call forms route to the emitted clone.  The clone is owned by the importer (emitted into its flat WASM module, since the defining module never instantiates a generic it merely exports); it carries the same Tier-3 runtime postcondition guard the defining module's uninstantiated-generic fallback emits, so it is sound at run.  The **#814 asymmetric variant** — an imported generic shadowed by a local non-generic AND module-qualified-called (`g::gen(...)`) — previously false-Tier-1'd (verify resolved the module generic's contract while codegen fell back to the local shadow, so a proved postcondition was violated at run); the shadowed generic is now monomorphized under a distinct `mod$<path>$gen$…` name reached only by the qualified call (the bare `gen` stays on the local shadow, §8.5.2), bringing codegen into agreement with the verifier.  Codegen and verifier discovery move in **lockstep** — the verifier's `_collect_instantiations` merges the same imported (unshadowed) generics and discovers the same shadowed qualified instantiations — proved by the #732 differential (`test_imported_generic_symmetric_between_codegen_and_verifier`, flipped from pinning the old symmetric "neither monomorphizes" to the new symmetric "both monomorphize", now parametrized over both call forms, plus a shadowed-variant differential).  Every clone symbol routes through the shared injective `mangle_type_name` (#775).  Two completeness gaps one level deeper in the shadowed path were closed alongside (the PR #888 review): (a) a shadowed generic whose body calls **another generic** — an unshadowed sibling (a normal clone) or a same-module shadowed sibling (its own `mod$…` clone reached by rewriting the intra-module bare call) — now gets that **transitive** clone emitted, on both codegen AND the verifier (the same worklist runs on each, keeping the differential a true equality — a missing transitive clone was an `unknown func $mod$g$outer$Int` at run and, if only one side scanned, a new false Tier-1); (b) the statement-position **result-shape predicates** (`_is_void_expr` / `_is_pair_result_expr`) now share ONE ModuleCall target resolver with the desugar, so a shadowed generic returning `@String`/`@Unit` used in statement position drops the correct number of stack values (previously they resolved via `_module_qualified_targets` only — seeing the local scalar shadow — and a String-returning `g::mkstr(5);` left a value on the stack: a WASM `type mismatch` at run on a check-green program).  Mutation-validated eight ways (disable importer discovery → the #774 tests + differential flip RED; disable the closure-body wiring for #873; desync the verifier's unshadowed merge → the differential flips; desync the verifier's shadowed discovery → the shadowed differential flips; disable the shadowed codegen emission → `g::gen(5)` returns the local shadow's `105` instead of the module generic's `5`; disable the transitive scan on codegen → `unknown func`; desync the verifier's transitive scan → the transitive differential flips; disable the shadowed-sibling call rewrite → the sibling clone's postcondition fails; desync the result-shape predicates from the desugar → the pair/scalar statement-position tests trap).  A deeper **false Tier-1** the soundness review then surfaced (PR #888 review, CR 3519156263) is also closed: an imported generic's clone is what actually RUNS in the importer, but its body is defined in another module — so the `program.declarations` verify loop never reaches it, and the defining module only Tier-3s the (uninstantiated) generic — meaning an imported generic with a **lying contract** ran its clone unverified (`verify` clean, all Tier-1, while `vera run` traps the clone's own postcondition).  Worse, a same-named **LOCAL generic** shadowing an imported one absorbed the `m::gen(...)` instantiation into the local's bare key, so the verifier proved the LOCAL body's contract in the module clone's place and the #732 differential counted the module clone as "covered".  The importer now (1) records every shadowed module clone under its `mod$…` base — never the bare name a local generic owns — so codegen's `_emitted_instances` and the verifier's `_instances` distinguish the module clone from a same-named local generic, and (2) verifies each imported generic's clone (shadowed *and* unshadowed) at the importer's own instantiations, turning a lying module contract into an honest **E500** at verify time.  An unshadowed generic whose body qualified-calls a shadowed one (`caller<T> { g::gen(@T.0) }`, CR 3519063445) has its shadowed clone discovered by scanning the emitted normal clones on both sides.  The warm incremental session runs the same imported-generic verification so `warm == cold` holds (the #732 oracle).  Mutation-validated: disable the imported-generic verify pass → the lying-contract tests stop catching E500; desync either side's `mod$…` key → the differential flips; disable either side's scan of normal clones for shadowed calls → the unshadowed-caller case flips.  Pre-existing; the #732 differential documented the symmetric behavior as a forward-compat tripwire.  Run-level conformance program `ch08_cross_module_generic`.
- **A generic called only from inside a closure body is now monomorphized** ([#873](https://github.com/aallan/vera/issues/873)).  A user generic (`are_equal<T where Eq<T>>`) whose only call site was inside a closure body — `array_any(xs, fn(...) { are_equal(@T.0, needle) })` — passed `vera check`/`verify` but failed `vera run` with `unknown func $are_equal`.  Mono discovery already walked closure bodies (the total AST walk in `collect_calls_in_node`) and the clone *was* emitted, but the LIFTED closure body was compiled in a `WasmContext` built without `generic_fn_info`, so its call stayed on the bare generic name (which has no implementation) instead of being rewritten to the mangled clone.  `_compile_lifted_closure` now threads `generic_fn_info` (and `known_fns`, for the same cross-module guard rail the per-function context uses) into the closure context — a codegen call-rewriting fix; verifier discovery already covered the closure-body instantiation (the walk is shared), so no verifier change was needed and the #732 differential stays symmetric.  Mutation-validated (dropping the closure-context `generic_fn_info` flips the #873 tests RED with the verbatim `unknown func` failure).  Pre-existing on main; found by the PR #870 blast-radius review.  Run-level conformance program `ch09_generic_in_closure`.
- **The `eq` / `compare` ability operations now lower to their canonical operator form in contract position, so a `vera check`-green contract also compiles, runs, and verifies at Tier 1** ([#874](https://github.com/aallan/vera/issues/874)).  `eq(a, b)` and `compare(a, b)` are the generic-programming spelling of `==` and the three-way `Ordering` comparison (spec §9.8) — a contract may use them directly, e.g. `ensures(eq(@Int.result, 3))`.  Codegen's Pass 1.6 (`_rewrite_ability_ops`) already rewrote `eq(a, b)` → `BinaryExpr(a, EQ, b)` and `compare(a, b)` → the `Ordering` if-chain (the one canonical form, #815), but only over function *bodies* and `where` helpers — never over `requires` / `ensures` / `decreases` clauses.  So a contract predicate written with the ability op reached the WASM contract-lowering path as a bare `FnCall` whose target is unregistered, tripping the `_translate_call` guard-rail with an **uncaught `CodegenSkip`** — a hard traceback for `vera compile` / `vera run` (the contract path is lowered *outside* `_compile_fn`'s try/except that would otherwise demote a body skip to an E602 warning + silent function drop).  The verifier had the parallel gap: `smt._translate_call` recognised none of the ability ops, so `eq` in an `ensures` returned `None` and the whole postcondition demoted to Tier 3 (E523) instead of being *proved* at Tier 1 — a check-green contract silently never statically checked (and a **false** `eq`-ensures passing verification as a Tier-3 deferral rather than failing with a counterexample).  Pass 1.6 now rewrites the FnDecl's contract clauses through the same canonicalisation it runs on bodies (`_rewrite_ops_in_contracts` in `vera/codegen/core.py`), and the verifier desugars `eq` / `compare` to the *same* canonical `BinaryExpr` / Ordering-if-chain and translates it via its existing FP-correct `==` path and `Ordering` datatype encoding (`vera/smt.py`), guarded on absence from the user-fn registry so a `where`-helper that legitimately shadows the name is not hijacked (mirroring codegen's `not in self._fn_sigs` gate).  Checker (already resolves the op → `Bool`), codegen, and verifier now agree.  Spec §6.2 documents that a contract's ability-op predicate is verified and compiled as its operator form.  Mutation-validated both halves independently — neutering the codegen contract rewrite flips exactly the four codegen (compile/run/enforce) tests RED (the `CodegenSkip` returns) while the verify tests stay green; neutering the verifier `eq` desugar flips exactly the two verify (Tier-1 / counterexample) tests RED while the codegen tests stay green.  Soundness confirmed by the ensures+run differential: a true `eq`/`compare`-ensures proves Tier 1 and runs, a false one is a static counterexample at `verify` and traps at `run`.  Pre-existing on main; found by the PR #870 blast-radius review.  The PR #887 review closed four residual gaps: (1) a `where`-helper's OWN contract using `eq`/`compare` hit the same `CodegenSkip` — `_rewrite_where_fns` rewrote only the helper body, not its `contracts`, now fixed; (2) `compare(...)` in a contract of a function whose signature never mentions `Ordering` stayed Tier 3 because the `Ordering` Z3 sort was never materialised — the desugar now forces it, scoped to the `compare` path so general ADT-call modular reasoning is untouched (a broad on-demand materialisation regressed unrelated `ensures(true)`-return ADT calls to a false E500); (3) the trap and shadowing pins were tightened — the false-ensures test asserts the specific `contract_violation` kind and canonical `@Int.result == 3` message, and a new verifier test pins the `_is_user_fn` guard (a user `where`-fn named `eq` routes to the user's semantics, not the `==` desugar — mutation-validated by disabling the guard); and (4) an `eq`/`compare` inside a `forall`/`exists` **contract** predicate hit the same uncaught `CodegenSkip` — a quantifier in a contract is runtime-lowered by `_translate_quantifier`, which compiles the predicate `AnonFn` body, but the Pass 1.6 walker `_rewrite_ops_in_expr` fell through `ForallExpr`/`ExistsExpr` to the leaf return, so the ability op inside was never canonicalised.  The walker now descends into the quantifier's `domain` and `predicate` (mutation-validated by two new codegen tests — `forall`+`eq` and `exists`+`compare` in an `ensures` — that flip RED to the `CodegenSkip` when the branch is disabled).
- **An int literal coerced to `@Byte` now lowers at `i32` at the call-argument and let-binding positions** ([#865](https://github.com/aallan/vera/issues/865)).  `byte_lt(3, 7)` for `fn byte_lt(@Byte, @Byte -> @Bool)` passed `vera check` — the checker deliberately accepts a 0..255 int literal where a `@Byte` is expected (bidirectional coercion, the same allowance that lets a `@Byte`-returning body be a bare literal) — but emitted `i64.const` arguments against the callee's `i32` `@Byte` parameters (`@Byte` is `i32`, spec §11), so the module failed wasmtime instantiation at `vera run` with `type mismatch: expected i32, found i64`.  The checker-position decision follows the #766 precedent (PR #864) and DESIGN.md §0.2 (no implicit behaviour, but codegen must emit valid WASM for everything the checker accepts): the coercion is intentional and codegen owns the width, so a literal reaching an `@Byte` parameter is now pushed as `i32.const` — mirroring the #766 binop-operand fix at the call-argument site (a per-parameter `_fn_byte_params` bitmap, harvested cross-module and onto monomorphised names alongside the `@Nat`/`@Int` guard bitmaps).  A non-literal `@Byte` argument (a Byte slot ref, a Byte-returning call) already lowered at `i32`, so only the literal needed the override; the fix covers the direct, nested-call, and match-arm argument shapes.  The same literal→Byte coercion gap at the **let-binding** site (`let @Byte = 42`, which also emitted `i64.const` into an i32 local) is fixed in the same place, and its refined-Byte sibling — `let @{ @Byte | @Byte.0 < 10 } = 5` was rejected at `vera check` with `E170` because the checker's IntLit→Byte coercion checked `isinstance(expected, PrimitiveType)` and did not see through the refinement wrapper — is closed by resolving the expected type through refinements (`base_type`) before the Byte check, with the 0..255 bound preserved (`300` bound to a refined Byte still correctly `E170`s).  New conformance program `ch01_byte_literal_coercion` (run level) pins the call-argument and let-binding shapes end-to-end.  Mutation-validated three ways: reverting the call-argument width dispatch flips the four call-argument tests RED, reverting the let-binding lowering flips the two let tests RED, and reverting the checker refinement-resolution flips only the refined-let acceptance test RED (the range-bound rejection stays green).  Pre-existing on main since Byte coercion landed; found by the PR #864 review's blast-radius sweep.
- **Call-site precondition obligations are now generated for ADT-typed arguments, closing a silent static-coverage gap** ([#882](https://github.com/aallan/vera/issues/882)).  A call to a helper whose `requires()` constrains **ADT-typed** parameters produced **no call-site obligation at all** — no `call_pre` record, no `E501`, no Tier-3 warning — so `vera verify` reported `ok: true` with a clean summary for a call it never statically examined (`data P { MkP(Int) }` + `g(@P, @P) requires(@P.1 == @P.0)` + `g(MkP(1), MkP(2))`: 4 Tier-1, zero `call_pre`), while the identical `Int`-parameter program correctly fired `E501` from the same machinery and `vera run` trapped at runtime.  The root cause was in `SmtContext._translate_call_with_info`: a constructor-call argument (`MkP(1)`) only translates once the callee's concrete ADT sort exists in the Z3 sort cache, but in a caller context that never *declared* that ADT (e.g. `main`, whose only parameter is `@Unit`) the sort was never materialised, so `_find_sort_for_ctor` returned `None`, the whole call bailed, and the obligation vanished.  The call translator now materialises each ADT-typed parameter's sort from the callee's declared types before checking the precondition (`_ensure_call_arg_sorts`), so — post-[#879](https://github.com/aallan/vera/issues/879), where ADT equality over concrete fields decomposes per-field with `fpEQ` for `Float64` — `MkP(1)` vs `MkP(2)` is now statically **refutable** (`E501` + counterexample), a satisfiable call (`MkP(5)` vs `MkP(5)`) discharges, a nested-ADT argument refutes via recursive decomposition, and a `Float64`-field ADT with `NaN` arguments refutes via `fpEQ`.  Where the argument (or the precondition) genuinely can't be modelled — an ADT field of a host-handle type such as `Map`, or an undecidable predicate like `string_length(...) > 0` — the obligation demotes to a **loud** Tier-3 warning rather than silently not existing (DESIGN.md: degrade loudly; the runtime guard still enforces the contract).  That demotion carries the dedicated code **`E532`** ("Cannot verify call-site precondition (undecidable)"), minted so the call-site-precondition class is one concept per code — distinct from `E522`, whose registered meaning is a *postcondition* demotion (body undecidable); a single program can now legitimately carry both (`async_http_fanout` reports `E521` on the callee's own precondition, `E522` on its undecidable postcondition, and `E532` on the call site).  The demotion is drained in **two** passes: once after body / `requires` translation, and once after `ensures` / refined-return / decreases translation, so a contracted call whose precondition can't be checked statically is reported **wherever it appears** — statement position, a `requires` predicate, *and* an `ensures` predicate.  A call inside an `ensures` clause records its demotion during postcondition translation, which runs after the first drain, so without the second drain the obligation vanished — the exact silent-loss this fix closes, still open for `ensures`-position calls after the initial patch.  Return-value modelling is deliberately unchanged: an ADT-argument call whose result feeds a postcondition stays opaque exactly as before, so no corpus program's postcondition tier moves — a before/after `verify --json` census over all 147 conformance + example programs shows the fix adds one new all-Tier-1 conformance fixture (`ch06_requires_adt_arg`, +4 Tier-1) and four programs (`http`, `inference`, `async_http_fanout`, `ch09_inference`) each gain one honest Tier-3 `E532` call_pre demotion for their undecidable `string_length` preconditions (+4 Tier-3), with every other program's tiers unchanged (net census 1231/495/1726 → 1235/499/1734; zero silent regressions).  Mutation-validated four ways (revert the sort materialisation → the refutable ADT tests lose their `E501`; disable the untranslatable-argument demotion → the `Map`-field test loses its warning; disable the untranslatable-precondition demotion → the `string_length` test loses its warning; delete the second (`ensures`) drain → the ensures-clause call loses its `E532` obligation while the statement-position twin keeps it — each flips exactly its discriminating tests RED).  Spec §6.4.2 documents the demotion.  Pre-existing on main; found by the PR #879 model-parity review, `ensures`-position gap and code-semantics mismatch caught by the PR #882 blast-radius review.
- **A nested (transitive) type alias to a `fn` type used as an `apply_fn` closure argument now compiles and runs** ([#867](https://github.com/aallan/vera/issues/867), found during the #843 fix).  A chain like `type Inner = fn(String -> String) effects(pure); type Mapper = Inner;` used as `apply_fn(@Mapper.0, …)` passed `vera check` and `vera verify` but failed loudly at `vera run` — the `call_indirect` closure signature carried `(result i64)` where the closure actually returned an `i32_pair` (`String`), so wasmtime rejected the module (`type mismatch: expected i32, found i64`); the fused-`async` await classifier hit the same gap one indirection out and lowered a `Future<Result<String, String>>` await to identity.  Both symptoms had one root: every closure-arg return-type consultor resolved only **one** alias hop, so a chain resolved to a `NamedType` (not a `FnType`), bailed, and fell to its default.  A single shared transitive resolver (`resolve_fn_type_alias`, hosted in the codegen-free `vera/monomorphize.py` so every consumer can import it without a cycle) now follows the alias chain to depth-N to the terminal `FnType` — unwrapping `RefinementType` layers (a peeled inline `FnType` is terminal wherever it appears, so `type Foo = { @fn(String -> String) | p };` resolves too — the PR #880 review's CodeRabbit finding; pre-fix that shape fell to the `i64` default and trapped, invisibly so for an `Int` return that coincides with the default), substituting each generic alias's type params at every hop, and carrying a `seen` cycle guard so malformed cyclic aliases terminate (returning `None` to a loud backstop) rather than spinning.  **Every** site that discovers a fn type through an alias routes through it, so depth-N behaviour cannot drift between consultors: the `call_indirect` signature builder (`_infer_apply_fn_return_type`), its Vera-type twin (`_infer_fncall_vera_type`'s `apply_fn` arm), the fused-await classifier (`_apply_fn_closure_ret_type`), the fn-typed-slot expression classifier (`_infer_expr_wasm_type`), the slot-annotation WASM-type mapper (`_slot_name_to_wasm_type` — the PR #880 review's 4th single-hop site: a `let`/param slot annotated with a refinement-wrapped fn alias (`type Foo = { @fn(...) | p };`) or one chained through a refinement was rejected with `CodegenSkip` ("has no WASM representation") and its whole function dropped, on a check/verify-green program; a plain `NamedType` chain was already collapsed by the `_resolve_base_type_name` prefix, so only the refinement-involving shapes remained), and — the PR #880 blast-radius finding, same class — the generic higher-order-fn consultors on **both** the instantiation-discovery side (`Monomorphizer._resolve_arg_fn_shape` / `_infer_fn_alias_type_args`, `vera/monomorphize.py`) and the WASM call-rewrite side (`vera/wasm/calls.py` twins), where a two-hop-alias-typed closure slot passed to a generic HOF (`my_map(@MyFn.0, 7)` with `type MyFn = InnerFn;`) failed shape resolution and a closure-bound type param fell to the phantom-var default — wrong mono suffix (`my_map$Int_JBool`), check-green → run-trap; the HOF's own fn param declared through an alias chain (`type MapFn2<X, Y> = MapFn<X, Y>;`) resolved the same way, instantiated at the alias's own param names so positional matching survives renaming hops.  New run-level conformance program `ch05_nested_fn_type_alias`; the #843 alias-of-alias await pin flips from "still fails loud" to correct execution.  Mutation-validated per site (neuter the chain-follow → every depth-2+ test flips RED while single-hop/`AnonFn` controls stay green; restore the pre-fix non-terminal peeled-`FnType` structure → exactly the refinement tests flip; un-route the slot-annotation site → exactly its two refinement-slot tests flip; un-route any one of the four HOF sites → exactly its discriminating test flips; remove the `seen` guard → the cycle-guard tests hang and time out RED).
- **A user ADT named with a single uppercase letter no longer collides with a prelude combinator's generic type parameter** ([#869](https://github.com/aallan/vera/issues/869)).  `data A` (plus a second `data B { MkB(A) }`, or any program dragging in the prelude Option combinators) made `vera run` trap at WASM instantiation with `unknown table 0` on a program that passed both `check` and `verify`.  A `forall`-generic FnDecl is a *template* — it reaches WAT only through its monomorphized clones (call sites are rewritten to mangled clone names in `vera/wasm/calls.py`), so while a type parameter stays abstract the template body cannot lower and codegen's Pass-2 attempt is correctly skipped.  The prelude ships `option_map` / `option_and_then` as `forall<A, B>` and `result_map` as `forall<A, B, E>`; a user `data A` put `A` into the ADT layout table, so codegen resolved the identically-named prelude type parameter to the *concrete* user ADT, lowered the `option_map` template cleanly, and emitted it as a bare-named function whose passed-in-closure `call_indirect` referenced a function table the module never declared (the table is emitted only when a program actually constructs a closure).  The prelude combinators' and closure-type aliases' internal type-parameter identifiers now use reserved `Vera`-prefixed names (`VeraA` / `VeraB` / `VeraE` / `VeraT` / `VeraU`) that no ordinary user ADT spells, keeping prelude internals invisible to user namespace decisions (spec §0.2 principle 4 — structural references eliminate naming-coherence errors; DESIGN.md principle 2, "explicitness — no implicit behaviour").  These names are substitution keys only and never appear in a mangled clone suffix (`Monomorphizer._mangle_fn_name` escapes the concrete type *arguments*), so the rename is orthogonal to the [#775](https://github.com/aallan/vera/issues/775) injective mangler.  Pre-existing on `main`; found 2026-07-03 during the [#773](https://github.com/aallan/vera/issues/773) structural-Eq work.  New conformance program `ch02_adt_single_letter_name` runs a single-letter ADT end-to-end; mutation-validated (reverting the rename flips the `data A` run tests and the bare-template WAT assertion RED with `unknown table 0`).
- **ADT equality over `Float64` fields is now modelled per-field with IEEE `fpEQ`, closing a false Tier-1** ([#871](https://github.com/aallan/vera/issues/871)).  A postcondition over ADT equality with a `Float64` field — `ensures(@Bool.result == true)` for `refl(@W -> @Bool) { @W.0 == @W.0 }` with `data W { MkW(Float64) }` — was proved Tier-1 by Z3's structural datatype `=` but violated at runtime by the per-field `f64.eq` the #870 structural-Eq codegen emits: the canonical ensures+run soundness differential (`vera verify`: 4 verified Tier 1; `vera run`: postcondition violation at `MkW(nan())`).  The #797 `fpEQ` special-case fired only when the *whole* operand was an FP term, so an FP value nested in a datatype fell to structural `=` — wrong in **both** directions (`NaN = NaN` holds structurally but is false under `f64.eq`; `+0.0 = -0.0` fails structurally but is true under `f64.eq`, so the verifier also emitted a false E500 against a runtime-true contract).  `==`/`!=` on a datatype sort whose fields transitively include Float64 now decompose per-field — same-constructor recognizers plus fieldwise `fpEQ`, recursing into nested Float64-containing ADTs (`_datatype_value_eq` in `vera/smt.py`) — matching the runtime's structural per-field `f64.eq` exactly; a *recursive* Float64-containing ADT (`FCons(Float64, FList)`) has no finite decomposition, so its equality returns untranslatable and the obligation demotes to an honest, loud Tier-3 runtime check instead of a false proof (soundness over completeness; DESIGN.md: "degrades gracefully where SMT is undecidable").  Non-Float64 datatype equality keeps structural `=`, where Z3 and the runtime agree — a before/after `verify --json` census over all 145 conformance + example programs shows zero tier movement (tier1 1231, tier3 495, unchanged).  Spec §6.3.1 documents the decomposition.  Mutation-validated five ways (structural-`=` fallback restored, `fpEQ` leaf degraded to `=`, FP-detection disabled, `!=` arm unrouted, per-arm recognizer guards dropped — each flips its discriminating tests RED; the last is pinned by a mixed-constructor sum type whose cross-constructor inequality must stay a Tier-1 proof, PR #879 review).  Pre-existing on main since ADTs became translatable; found by the PR #870 adversarial soundness review.
- **Monomorphized WAT symbol names are now injective — distinct generic instantiations can no longer collide on one symbol** ([#775](https://github.com/aallan/vera/issues/775)).  `Monomorphizer._mangle_fn_name` built clone names by lossy replacement (`<` and `, ` flattened to `_`, `>` dropped, components joined with `_`), so `g<Map<String, Int>>` and a user ADT literally named `Map_String_Int` both mangled to `g$Map_String_Int`, as did `g<A_B, C>` and `g<A, B_C>` to `g$A_B_C` — Pass 1.5 has no collision detection, both clones were emitted under the one name, and the module failed WAT compilation with `duplicate func identifier` on a check/verify-green program.  Type names embedded in WAT symbols now use one shared escape (`vera.monomorphize.mangle_type_name` — the same convention as the structural-`Eq` `$eq_<type>` helper names from [#773](https://github.com/aallan/vera/issues/773), which now delegate to it): `_` doubles to `__`; `<` / `>` / `,` / space become `_L` / `_R` / `_C` / `_S`; and instantiation vectors join with `_J` — a prefix code, so the encoding is provably injective (the docstring carries the argument) and deterministic.  All three name sites — clone emission, the WASM call-site rewriter (`_resolve_generic_call`, previously a hand-copied sanitizer), and the clone return-type-registry lookup in `_infer_fncall_vera_type` (previously a raw `_`-join that only agreed with emission by coincidence on simple types and silently missed every parameterized instantiation) — now delegate to the one mangler, and a nested-generic-call test pins the three-site agreement.  Monomorphized symbols are renamed throughout emitted WAT (e.g. `option_map$Int_Int` → `option_map$Int_JInt`, `are_equal$Box_String` → `are_equal$Box_LString_R`); a corpus-wide differential over all conformance programs and examples confirmed the rename is otherwise WAT-pure (134/140 byte-identical, 6 pure-rename).  Verification is untouched — the verifier keeps original names for its clones (#732).  Mutation-validated four ways (restore the lossy encoding; desync the call-site rewriter; desync the registry lookup; drop the `_`-doubling — each flips its discriminating tests RED).
- **`Eq` auto-derivation is now structural, not scalar-representation-based** ([#773](https://github.com/aallan/vera/issues/773)).  Derivation previously worked from a field's WASM rep — accepting only scalar reps (`i64`/`i32`/`f64`) and comparing each inline — which was wrong in both directions: a `String` field (an `i32_pair`) made an ADT non-derivable (`Box<String>` → E613 though `String` *is* `Eq`), and a nested concrete-ADT field (an `i32` pointer) passed the scalar check and was compared with `i32.eq` — **pointer identity, not value** — so two structurally-equal values at distinct allocations compared unequal (and a `Map`/`Array`-pointer field silently compared by identity too).  Derivation now dispatches on the field's *Vera* type: constructor layouts carry per-field declared type names, alias- and refinement-resolved like the sibling field metadata (`ConstructorLayout.field_types`), and codegen generates recursive per-instantiation `$eq_<type>` WASM functions — scalar `.eq`, `String` content comparison (a hoisted `$eq_String` helper), nested-ADT recursion into that ADT's own `$eq_` helper (real functions, so recursive types derive), and a loud E613 for a field type with no `Eq` semantics (`Array`/`Map`/host handle).  The checker-side E613 gate (`_adt_satisfies_eq`) was rewritten to the same structural basis and is held in lockstep with codegen by a differential test (a program the gate accepts never hits codegen's invariant, and vice versa).  The DIRECT `==` path consults the same gate (PR #870 review): a direct comparison on a non-derivable ADT — a `Map`/`Array`-field ADT, an empty-metadata Markdown builtin (closes [#872](https://github.com/aallan/vera/issues/872)), or a ctor-inferred bare generic — is a clean E613 instead of an E699 invariant crash; and the variadic `Tuple` placeholder (zero-field registered layout) is rejected on both paths rather than generating an always-true tag-only equality.  The #772 constructor-inferred path (`eq2(MkBox("a"), …)`, which loses its type argument to bare-name resolution) now reports a clean E613 in lockstep rather than the earlier silent wrong equality; recovering that lost type argument stays [#772](https://github.com/aallan/vera/issues/772).  Spec §9.8, SKILL.md, and the E613 message updated to the structural rule.  Mutation-validated three ways (break the String content compare, the nested-ADT recursion, or the reject gate — each flips exactly its discriminating tests RED).
- **Browser `Decimal` is now exact, matching the Python runtime op-for-op — no more silent precision loss or `decimal_compare`/`decimal_eq` self-contradiction** ([#856](https://github.com/aallan/vera/issues/856)).  Spec §9.7.2 promises `Decimal` provides *exact* decimal arithmetic, but under `--target browser` the family routed through JavaScript `Number` / `Math.round` (`vera/browser/runtime.mjs`): `0.1 + 0.2` printed `0.30000000000000004` (native: `0.3`), a 16+-significant-digit product rounded wrong, and integers past `Number.MAX_SAFE_INTEGER` lost their low digits — a **silent wrong value**, no trap.  Worse, the family contradicted itself within one runtime: `decimal_compare` converted operands via `Number()` (so `"1.0"` vs `"1"` → `Equal`) while `decimal_eq` did strict string comparison (same operands → `false`), and `decimal_eq` diverged from the Python runtime's numeric equality.  The browser runtime now uses an exact scaled-BigInt engine (`{sign, coeff: BigInt, exp}`) mirroring `decimal.Decimal`'s default context (28 significant digits, `ROUND_HALF_EVEN`) — add/sub/mul/div/round/neg/abs and the canonical string form all match Python byte-for-byte, and `decimal_compare` + `decimal_eq` share **one** exact numeric comparison so they can never disagree.  Values are stored canonically, so `decimal_to_string` also matches Python for non-canonical inputs (`"007"` → `"7"`, `"1e5"` → `"1E+5"`).  The PR #877 review panel then proved and closed four parity gaps in the first cut: `decimal_neg`/`decimal_abs` now **apply the context** (a 29-digit operand rounds to 28 significant digits, like Python's unary operators — the first cut only flipped the sign bit); `decimal_round` with `places <= -28` now targets quantum exponent `max(0, -places - 27)` (the quantum `Decimal(10)**-places` is itself context-rounded — the first cut hardcoded exponent 0, giving wrong exponents *and* wrong values), and `places` below the context's exponent floor of `-999999` now return the value unchanged in **both** runtimes — the Python host previously let the quantum computation's `decimal.Overflow` escape as a raw traceback (only `InvalidOperation` was caught) while the browser returned a value, a crash path *and* a divergence; `decimal_from_string` now accepts **exactly one spec-defined grammar in both runtimes** (ASCII finite decimals — the Python host previously inherited `decimal.Decimal`'s acceptance of `NaN`/`Infinity`/`sNaN`/`1_000`/non-ASCII digits while the browser rejected them; per DESIGN.md explicit-over-implicit, the host now pre-validates and the spec defines the domain), with the exponent token bounded to `|exp| <= 999999` in both runtimes — the browser's `parseInt` silently rounded a beyond-2^53 exponent (`"1e9007199254740993"` parsed as `1E+9007199254740991` after formatter double-rounding, while the Python host stored it exactly — a silent cross-runtime value divergence, per the follow-up CodeRabbit round), the check performed on the token *string* before any numeric conversion; the host binary ops (`decimal_add`/`sub`/`mul`/`div`) now run in an **exponent-widened context** (`Emax`/`Emin` = `decimal.MAX_EMAX`/`MIN_EMIN` ≈ `±10^18`, prec 28 and `ROUND_HALF_EVEN` unchanged), because a finite result can exceed the input-token bound (`decimal_mul(1e999999, 1e999999)` = `1E+1999998`) and under the default `Emax` of 999999 the Python host raised a raw `decimal.Overflow` traceback on a check-green program while the unbounded browser engine returned the value — a crash *and* a divergence (follow-up CodeRabbit round, finding 3518540519); the widened context returns the same exact value the browser produces; and `decimal_from_float` ports Python's `str(float)` formatting over JS's shortest round-trip digits, so `decimal_from_float(100.0)` renders `"100.0"` in both runtimes and the implied exponent propagates through arithmetic (`×2` → `"200.0"`, not `"200"`).  Non-finite floats through `decimal_from_float` remain construct/render-only in the browser (identical `NaN`/`Infinity`/`-Infinity` renderings; arithmetic on them fails loudly, documented in §9.7.2).  Spec §9.7.2's "Browser runtime precision" limitation note is rewritten to a runtime-parity statement with the grammar and the non-finite exclusion spelled out.  Proven by browser↔native differential parity tests in `tests/test_browser.py` (compile once, compare wasmtime vs Node stdout byte-exact — the self-contradiction pair, `0.1 + 0.2`, high-precision mul/div, `ROUND_HALF_EVEN`, signed-zero and negative-operand compare/eq edges, big-integer add, 29-digit neg/abs, deep negative `round` places, large-finite binary ops past the default Emax, the acceptance battery, and from_float formatting incl. the arithmetic chain), each mutation-validated (reverting any op to its old path flips its test RED), plus a randomized differential against Python's `decimal` on the engine extracted verbatim from `runtime.mjs` (extended to near-Emax exponent operands).
- **Fused-async `await` of an indirectly-called closure now lowers correctly instead of silently discarding the request outcome** ([#843](https://github.com/aallan/vera/issues/843), promoted from a documented limitation to a bug in the 2026-07-02 triage).  Under the #841 fused-async lowering, an inline `await(apply_fn(closure, …))` where the closure returns `Future<Result<String, String>>` was invisible to `await_needs_check` (its `FnCall` arm only matched named calls in the return-type registry), so the await lowered to identity: the kind-4 fused wrapper flowed into the consumer `match`'s unconditional catch-all arm, was read as the ADT, and the `Err` payload came out as a zero-length string — a **silent wrong value**, the request outcome discarded (the #842 cross-module failure shape, one call-indirection further out).  **Ceiling (shipped):** the classification now consults the closure's *declared* return type — a fn-typed slot resolved through its `FnType` alias **including generic type-arg substitution** (a `Producer<Future<Result<String, String>>>` slot classifies; the PR #868 review panel caught that an unsubstituted bare `T` return classified as unresolvable while `_infer_apply_fn_return_type` substituted and built a *valid* `call_indirect` signature — no E616, no trap, a live silent wrong value), or an inline `AnonFn` literal — the same shapes, with the same alias-map guard, that `_infer_apply_fn_return_type` resolves for the `call_indirect` signature, so a resolvable indirect await emits the fused-handle runtime check and runs correctly.  **Floor:** each unresolvable shape has its own loud backstop, mirrored from what the `apply_fn` translation does with it — a closure argument produced by a nested call is rejected with `[E616]` (function skipped); a fn-typed slot through an alias-of-alias chain trips the `call_indirect` width mismatch at WASM validation ([#867](https://github.com/aallan/vera/issues/867), found during this fix) — so no fused wrapper can silently reach an identity await.  The documented let-bind workaround (`let @Future<Result<String, String>> = …` before awaiting) is unaffected, and an Ok-path test pins the payload byte-exact through the indirect await (a wrapper mis-read would corrupt it).  Spec §9.5.4 and `KNOWN_ISSUES.md` updated; mutation-validated (disabling the `apply_fn` arm flips the slot, anon, and Ok-path tests RED; disabling the generic substitution flips exactly the generic-alias test RED).
- **Browser parse bindings no longer convert GC/rooting failures into parse errors** (PR #866 review follow-up).  The `md_parse`, `json_parse`, and `html_parse` host bindings wrapped both the parser call *and* the GC-guarded tree walk in one `try`, so a rooting bug, shadow-stack overflow, or builder invariant failure would surface as an `Err(String)` parse result instead of failing loudly.  The `try` now covers only the parser; infrastructure failures in the `gcGuard` walk trap.

- **The browser runtime's markdown tree builders now GC-root their intermediates** ([#744](https://github.com/aallan/vera/issues/744)).  `writeMdInline` / `writeInlineArray` / `writeMdBlock` / `writeBlockArray` in `vera/browser/runtime.mjs` allocated each node's body first and held it — plus every array backing buffer — only in a JS local across the child / string allocations, so a `$gc_collect` fired by a sub-alloc could sweep the block mid-build and corrupt the parsed tree (deterministic under `VERA_EAGER_GC=1`: `readMdBlock` throws `Unknown MdBlock tag` with ASCII string bytes where the tag word should be).  This was the missed sibling of the #692 / #708 `writeJson` / `writeHtml` hardening; the CLI mirror (`vera/wasm/markdown.py`) was **already fully hardened** with the `_ShadowGuard` fields-first-then-body discipline and needed no change — only the browser half of #744 remained.  The browser builders now mirror the CLI convention exactly (allocate fields first, root them via `gcShadowPush`, allocate the body last; array helpers root their backing buffer before recursing into children), and `hostMdParse` runs the whole walk under the same `gcGuard` as the `json_parse` / `html_parse` bindings.  The `gcShadowPush` / `gcShadowPop` / `gcGuard` primitives were hoisted from the import-builder closure to module scope — they only touch the module-level `wasm` binding, so behaviour is unchanged for the existing JSON / HTML / bucket-codec callers — because the markdown builders live at module scope.  A sweep of the file's other multi-alloc host builders (Result / Option / array-of-strings helpers, bucket codec, regex, JSON, HTML) confirmed the markdown family was the only one still unrooted.  Pinned by two node-run eager-GC regression tests (`TestBrowserMdBuilderRooting744` in `tests/test_browser.py`): an all-node-type parse → render round-trip asserting byte-identical stdout against the wasmtime runtime on the *same* eager-GC module bytes (which also exercises the hardened CLI walkers under eager GC), and a 60-unit volume `md_extract_code_blocks` read-back — both RED pre-fix, and mutation-validated (dropping the `writeBlockArray` backing push or the `MdText` string push flips the parity test RED).
- **Refinement predicates are now type-checked for well-formedness, at every position a refinement can be written** ([#861](https://github.com/aallan/vera/issues/861)).  A refinement type's predicate (`{ @Int | P }`) skipped type-checking entirely — registration resolved only the alias's *base*, with no refinement counterpart of the checker's `_check_contract` — so a non-`Bool` predicate (`type T = { @Byte | @Byte.0 }`, the predicate is a bare `@Byte` value) and a genuinely ill-typed one (`{ @String | @String.0 < 3 }`) both passed `vera check`.  The predicate now gets contract-grade checking: it must type as `Bool` — rejected with the new dedicated code **`E126` "Refinement predicate not Bool"**, following the registry's one-code-per-predicate-position convention (`E120` data invariant, `E123` precondition, `E124` postcondition; PR #876 review) — and its operands follow the ordinary Chapter 4 typing rules (so the `String`-vs-`Int` comparison surfaces as `E142`).  Every `type_expr` grammar position routes through the walker (the PR #876 review found the first pass wired only alias bodies, fn signatures, and constructor fields): `let` / destructure annotations, `match` binding patterns, anonymous-fn signatures, effect / ability op signatures (whose registration resolves only the base type), `forall` / `exists` binders, and the handler state / clause-param / `with`-clause annotations — plus refinements nested in type arguments (`Array<{ @Int | @Int.0 }>`) and function-type components.  The `let`-annotation escape was load-bearing: a check-green `let @{ @Int | @Int.0 } = 5;` crashed `vera verify` with an uncaught `Z3Exception` traceback (`z3.Not` on the untyped predicate in the refined-binding obligation); it is now a clean `E126` at check, before the verifier runs — pinned by a CLI-level regression test.  The CodeRabbit round hardened two rules: (1) the binder is enforced as the SOLE slot in scope (spec §2.6) — the predicate is checked in an **isolated scope stack**, so `let @{ @Int | @Int.0 > @Int.1 } = 5;` inside `fn f(@Int, @Int -> ...)` no longer resolves `@Int.1` against the enclosing parameter (now `E130`); and (2) the Byte-literal allowance is **keyed to the predicate's own resolved base**, not a blanket in-a-refinement flag — a `Byte`-typed operand inside an `@Int`-based refinement (`{ @Int | b(@Int.0) < 10 }` with `b : Int -> Byte`) is `E142`, and a predicate nested through a `forall`/`exists` binder uses its own base.  One rule is relaxed inside a predicate over a `@Byte` base: an integer literal compared against a Byte-typed operand is typed against `Byte` (literal-typing-from-context, §4.2 — not an implicit coercion, §0.2.2), because `@Byte.0 < 10` has a defined `i32` runtime-guard lowering (#766); this keeps the `ch02_byte_refinement` conformance program well-typed rather than regressing it to `E142`.  The allowance covers comparison only — `@Byte` *arithmetic* in a predicate (`@Byte.0 + 1 < 10`) is now `E140`-rejected at check like everywhere else (the unchecked predicate had been silently bypassing the documented universal Byte-arithmetic exclusion).  Spec §2.6 states the predicate-well-formedness rule and the position list.  Mutation-validated (disabling the `Bool` check, neutering the whole walker, disabling the `let`-site wiring, disabling the op-signature wiring, reverting the scope isolation, and removing the allowance's base keying each flip exactly their discriminating tests RED).  The distinct call-site gap — int-literal *arguments* to `@Byte` parameters emitting `i64.const` — is tracked separately ([#865](https://github.com/aallan/vera/issues/865)).
- **A refinement over `@Byte` now compiles to a valid runtime guard and runs** ([#766](https://github.com/aallan/vera/issues/766)).  A `{ @Byte | @Byte.0 < 10 }` refinement passed `vera check`/`verify`/`compile` but the module failed to instantiate at `vera run` — wasmtime rejected it with a WebAssembly translation error.  `@Byte` is represented as `i32`, but the boundary guard lowered the predicate at `i64` (int literals emit `i64.const` and the default arithmetic/comparison tables emit `i64` ops), so the guard compared the `i32` Byte with `i64.lt_s` — an operand-width mismatch.  A binary op with a `@Byte` operand is now lowered entirely at `i32` with unsigned comparison ops (spec §11 — Byte uses `i32.lt_u` etc.), coercing an int-literal operand to `i32.const`; the tests pin the comparison, equality, and logical-conjunction predicate shapes end-to-end.  The arithmetic shape (`@Byte.0 + 1`) shared the same width bug and its `i32` lowering is pinned at the codegen layer only — with #861's predicate checking it is `E140`-rejected at `vera check` (the universal Byte-arithmetic exclusion), so it no longer reaches codegen through the CLI.  Byte-ness of a **function-call operand** (`ident(@Byte.0) < 10`, found by the PR #864 review) comes from the callee's *declared* Vera return type — resolved through alias chains (`-> @MyByte` where `type MyByte = Byte`) — not its `i32` WASM width, which `Bool` shares and which the previous width-based inference collapsed, leaving the call result compared at `i64`.  The guard additionally conjoins the implicit `0 <= @Byte.0 <= 255` Byte range the way the `@Nat` guard conjoins `>= 0`, so a value satisfying the predicate but outside `0..255` (e.g. `300` for `@Byte.0 > 5`, which crosses the boundary as an unbounded `i32`) is rejected rather than laundered past the guard.  New conformance program `ch02_byte_refinement` (run level) pins the end-to-end path, including the fn-call predicate shape.  Mutation-validated (width dispatch disabled; range conjoin disabled; fn-call branch disabled; declared-return canonicalisation skipped — each flips exactly its discriminating tests RED).  Int-literal call *arguments* to `@Byte` parameters (`byte_lt(3, 7)`) are a distinct call-site emission gap tracked separately ([#865](https://github.com/aallan/vera/issues/865)).  Surfaced in the #746 review (deferred PR #763 range-conjoin point).
- **Spec examples corrected to the shipped API surface** (PR #863 review).  §7.5's exception-handler example no longer teaches `parse_int` as an `Exn<String>` thrower — the built-in is a pure `Result<Int, String>` function (§9), so the example now wraps it in a `parse_or_throw` helper that converts `Err` into `throw`, and the rewritten block passes the full parse+check+verify pipeline (its INCOMPLETE skip annotation is gone).  The effect-polymorphism examples (§5.9.1 and §7.6/§7.6.1 `option_map` / `with_logging`) apply stored function values with `apply_fn(...)` instead of the unsupported direct `@Fn.0(...)` call form, matching the prelude combinators and the §11.10.5 invocation rule.  A suppression reason mis-anchored onto §9's `array_sort_by` signature fence (a #606-era allowlist artifact carried through the migration verbatim) now names the block it actually covers.  The markdown/HTML block scanners also fail loudly on unterminated fences and unclosed `<pre>` blocks instead of tolerating malformed docs, and the four parse-only doc gates share one `run_parse_only_gate` implementation in `scripts/doc_annotations.py`.
- **A generic whose instantiation is inferred from a user-fn return in argument position now emits the right clone instead of dropping `main`** ([#878](https://github.com/aallan/vera/issues/878)).  `option_unwrap_or(decimal_div(d("1"), d("3")), d("0"))` — where `d` returns `@Decimal` — passed `vera check` but at `vera run` the codegen call-rewrite resolved `option_unwrap_or` to a non-existent `$Bool` clone, so `main` was skipped and dropped from the exports (`No exported functions to call`); wrapping the expression in a private helper instead left a dangling `call $show_div` that failed WAT validation.  Two compounding gaps in the WASM body-emission consultor (`vera/wasm/inference.py`), both rooted in the lossy WAT collapse `i32 → "Bool"` (a `Decimal` handle, an ADT, and an `Option`/`Result` pointer are all `i32`, the same value as the phantom-var default): `_get_arg_type_info_wasm` had **no `FnCall` branch**, so a parameterized return in `Option<VeraT>` position (`decimal_div` → `Option<Decimal>`, or a non-generic user fn declared `-> Option<Decimal>`) bound nothing and the type var fell through to the fallback argument; and a user-fn call in bare `@VeraT` position resolved through the `i32 → "Bool"` collapse rather than the callee's precise **declared** return type.  `_get_arg_type_info_wasm` now mirrors instantiation discovery's `FnCall` branch (shared `_BUILTIN_PARAMETERIZED_RETURNS` table) plus a non-generic-user-fn parameterized-return case, and `_infer_fncall_vera_type` consults the declared return TypeExpr (`_fn_ret_type_exprs`) for the ambiguous `i32` case — while **generic** calls are excluded from the user-fn branch (their declared return is parameterized over the callee's own type vars, not concrete types, so they fall through to generic-return resolution exactly as discovery does, keeping the two consultors in lockstep).  Root cause was also latent on the codegen *discovery* side (`vera/codegen/monomorphize.py`): it seeded the shared `fn_ret_types` from WAT signatures via the same `i32 → "Bool"` collapse, so a generic bound solely by a user-fn return (`pick_last(mkdec(()), mkdec(()))` where `mkdec` returns `@Decimal`) was **discovered** as `$Bool` while the #732 verifier discovered `$Decimal` — a differential desync that only failed to crash because an identity clone body masks it.  Discovery now seeds `fn_ret_types` from each function's declared return TypeExpr (precise Vera name, matching the verifier's `_simple_type_name`), with the WAT collapse kept only as a fallback.  The chosen test values are deliberately `Decimal` returns whose output (`0.3333…`) can never coincide with the `Bool` default (`true`/`false` → `1`/`0`) — the exact Bool-coincidence trap CLAUDE.md warns about, which would otherwise let a wrong instantiation pass CI looking identical to the right one.  New run-level conformance program `ch09_generic_infer_user_fn_return` (suite now 124) plus three regression tests in `tests/test_codegen_monomorphize.py` (crash repro via IO, WAT-mangle discriminator, discovery↔verifier differential).  Mutation-validated: reverting the discovery seed flips only the differential test RED; reverting the body-emission consultor flips the crash + WAT-mangle tests RED; the two are independently load-bearing, and the whole conformance program crashes with `unknown func` on pre-fix source.  #772–#775 mono-inference cluster's argument-position member; found while building the #856 parity fixtures (PR #877).  **PR #899 review round 2** closed two coupled-consultor regressions the first fix left — each the SAME check-green-then-`run`-drops-`main` class, from updating one side of a discovery↔call-rewrite pair: (1) a **non-generic user fn returning a parameterized type** (`maybe → Option<Decimal>`) in `Option<T>` position was recovered by the WASM call-rewrite (`_get_arg_type_info_wasm`) but NOT by instantiation discovery (`Monomorphizer._get_arg_type_info`), so discovery emitted `first_opt$Bool` while the call site referenced `first_opt$Decimal` — discovery now mirrors the call-rewrite's `FnCall` user-fn parameterized-return branch, threading the declared return TypeExprs through a new `MonoContext.fn_ret_type_exprs` (populated by both codegen and the #732 verifier); and (2) a user fn returning a **scalar-resolving alias/refinement** (`type Age = Int`) in bare `@T` position — discovery and the verifier key the clone on the RAW name (`pick$Age`) but the call-rewrite alias-resolved to `pick$Int`, a clone never emitted; the clone-naming path now uses a dedicated `_declared_return_clone_name` (raw, un-resolved) while general `_infer_fncall_vera_type` keeps alias-resolving scalars for its other callers (interpolation, container typing, `show`/`hash`) so the fix doesn't over-reach.  The invariant is now enforced by a corpus-driven **call-rewrite↔emitted-clone differential** (`test_call_rewrite_matches_emitted_clones` in `tests/test_monomorphize_differential.py`) that captures every mangled call target the WASM rewriter resolves and asserts each is an actually-emitted clone — the third consultor the verifier⊇codegen differential never exercised, which is why these slipped.  Both round-2 fixes mutation-validated independently (neuter discovery's user-fn branch → only the parameterized-return tests flip RED; neuter the raw clone-name override → only the alias/refinement tests flip RED).  **PR #899 review round 3** closed a third regression of the same class — this one a **net regression vs base** — and ended the whack-a-mole structurally: a non-generic user fn returning a **literal parameterized type** (`-> @Option<…>`/`@Result<…>`/`@Box<…>`, a `NamedType` carrying `type_args`) bound to a generic's **bare `@T`** desynced because the round-2 `_declared_return_clone_name` gated on `not ret_te.type_args`, so a parameterized return bailed to the `i32 → "Bool"` collapse and the call site referenced `pick_last$Bool` while discovery emitted `pick_last$Option` (base name); on base both sides consistently emit/call `$Bool` — wrong but linked — so it ran, making the PR the regressor.  Rather than patch a fourth shape, the user-fn-return clone-name key is now derived by ONE shared function, `declared_return_clone_key` (in `vera/monomorphize.py`): instantiation discovery (`_simple_return_type_name`), the #732 verifier (`_simple_type_name`), and the WASM call-rewrite (`_declared_return_clone_name`) all delegate to it, so the three consultors **cannot desync by construction** — the convention (refinement-unwrap, then base name for a parameterized return, raw name for an alias/refinement) is defined in exactly one place.  The base-name-only key (`pick_last$Option` for both `Option<Decimal>` and `Option<Int>`) is **sound** for the bare-`@T` binding it feeds: a `forall<T>` body binding a whole ADT to `@T` is representation-polymorphic — it moves an `i32` handle and cannot pattern-match or project `T` (it doesn't know `T`'s constructors) — so the colliding instantiations share one identity clone with byte-identical WAT (pinned by `test_issue3_base_name_key_collision_is_sound`).  Five run-level regression tests (`Option`/`Result`/`Box` returns into bare `@T`, the discovery↔call-rewrite discriminator, the collision-soundness check) plus three `_CALL_REWRITE_CORPUS` differential entries; mutation-validated by desyncing only the call-rewrite (re-adding the `not type_args` gate) → all eight Issue-3 tests flip RED while rounds 1–2 stay GREEN.
- **A `where`-helper inside a monomorphized generic is now emitted, instead of crashing codegen with `unknown func`** ([#904](https://github.com/aallan/vera/issues/904)).  A `forall<T>` function carrying a `where { fn helper(...) {...} }` block passed `vera check` but crashed at `vera run` with `WAT compilation failed: unknown func: failed to find name $helper`: the monomorphized clone (`outer$Int`) calls `$helper`, but the helper was never emitted.  Where-helpers used to be emitted only alongside their compilable parent (`vera/codegen/core.py` Pass 2), yet a generic parent is *skipped* there — its `@T` parameter has no concrete WASM type (`unsupported`) — so the clone's helper, carried into the clone by `monomorphize_fn`'s total-AST substitution but never separately emitted, dangled.  Codegen's monomorphizer (`vera/codegen/monomorphize.py`) now hoists each clone's `where`-helpers into standalone mono decls under a clone-aligned name (`<clone>$where$<helper>` — `$` can't appear in a source identifier and `where` is a reserved keyword, so the name collides with neither a user function nor another clone) and rewrites the clone body's (and each sibling helper's) bare call to match, so the ordinary mono-decl path registers and emits them.  Hoisting per-instantiation is uniformly correct for both helper shapes: a **T-independent** helper (`fn helper(@Int -> @Int)`) yields identical bodies under distinct names, while a **T-dependent** helper (`fn id(@T -> @T)`) reads the enclosing `@T` and so genuinely differs per instantiation (an i64 mover for `@Int`, an i32 mover for `@Bool`) — a single shared emission would be type-wrong.  Nested `where` blocks are hoisted recursively.  The shared `Monomorphizer` the #732 verifier drives is untouched, so codegen still emits exactly the verifier's instantiation set (the hoisting is codegen-local naming).  New run-level conformance program `ch09_generic_where_helper` (both shapes) plus eight regression tests in `tests/test_codegen_monomorphize.py` (both shapes, sibling-helper calls, two distinct instantiations of a T-dependent helper, a where-helper that calls another top-level generic, and the non-generic-where and generic-without-where working paths); mutation-validated by bypassing the hoist, which flips exactly the six where-in-generic tests RED with `unknown func`.
- **A non-void cross-module call used directly as a constructor argument or array-literal element now compiles and runs instead of crashing codegen** ([#905](https://github.com/aallan/vera/issues/905)).  A non-void `ModuleCall` (e.g. `vera.math::magnitude(x)`) placed *directly* in a `Tuple`/ADT constructor field (`Tuple(vera.math::magnitude(@Int.0), 9)`) or as the first element of an array literal (`[vera.math::magnitude(-5), 9]`) passed `vera check` but crashed at `vera run` with `unknown func: failed to find name $mkt` (the enclosing function was silently dropped and its call dangled).  The WASM type-inference helpers returned `None` for a `ModuleCall`: `_translate_constructor_call` (`vera/wasm/data.py`) could not size the field and raised `CodegenSkip`, while `_infer_array_element_type` → `_infer_vera_type` (`vera/wasm/inference.py`) could not type the element and dropped the array-let binding.  Both `_infer_expr_wasm_type` and `_infer_vera_type` now resolve a `ModuleCall` through the single shared `_resolve_module_call_wasm_name` (which consumes the module `path`, so no same-name-local mismatch — the earlier #597 concern) and delegate to the existing `_infer_fncall_wasm_type` / `_infer_fncall_vera_type` machinery — the same resolver the `ModuleCall` desugar and the statement-position result-shape predicates (`_is_void_expr` / `_is_pair_result_expr`) already use, so all four sites agree on which function `m::f(...)` reaches.  This is the non-void sibling of #902 (which fixed only the Unit-valued field case via `_is_void_expr`), per DESIGN.md principle 1 (a `check`-green program must never reach a codegen crash).  New run-level conformance program `ch08_ctor_field_module_call` (suite now 129) plus a `TestModuleCallInConstructorField905` class in `tests/test_codegen_modules.py` covering the Tuple field (final and non-final positions), a dotted-path `vera.math::…` import, a mixed zero-size-`Unit` field (#902 composition), a user-ADT constructor field, and both array-literal positions — with same-file-call and plain-literal controls proving the new path does not over-fire.  Mutation-validated: reverting each helper independently flips exactly its cases RED (`unknown func` for the constructor cases, `main` dropped from exports for the array-literal cases).
- **A generic called through the `|>` pipe, and a `@T` closure parameter inside a `forall<T>` body, now monomorphize correctly instead of dropping the function at run** ([#913](https://github.com/aallan/vera/issues/913)).  Two check-green-then-`run`-drops-the-function shapes that monomorphization discovery/substitution missed.  (1) **Pipe discovery**: `42 |> ident()` desugars to `ident(42)` at both the checker (`_check_pipe`) and codegen (`_translate_binary`) boundaries — the piped LHS becomes the call's first argument — but the shared instantiation-discovery walk (`Monomorphizer._collect_calls`, `vera/monomorphize.py`) only saw the RHS `FnCall`'s *own* (empty) argument list, so `T` never bound, no `ident$Int` clone was emitted, and codegen lowered the pipe to `call $ident$Int` on a non-existent function (the enclosing fn dropped with `call target 'ident$Int' not registered`).  Discovery now reconstructs the pipe-desugared argument list (`(lhs,) + rhs.args`) for a `PipeExpr` whose RHS targets a generic, inferring the type argument from the piped value exactly as a direct call does — for `FnCall` and `ModuleCall` RHS alike, and through chained pipes.  (2) **Closure `@T` substitution**: a `forall<T>` body whose parameters are compilable-looking (`Array<T>` → `i32_pair`) is body-compiled directly to draw its skip-warning surface, which lifted its `fn(@T -> @T)` closure and hit a hard `closure parameter has unsupported WASM type` invariant (`E699`) — reporting a *valid* generic as an internal compiler error, even though the monomorphized clone (`map_ident$Int`, whose closure param is the concrete type) compiles fine.  A closure parameter or return typed as an unsubstituted type variable in an uninstantiated template body is now a clean skip (`E602`, dropping the template droppably) rather than an `E699`, matching how a template's own bare `@T` *parameter* already draws `E604`; and both this closure-level `E602` and the function-level dropped-parent warning are suppressed once a clone compiles, so a *correct* generic-closure program compiles with **zero** warnings (the closure-level warning's description names the closure, not the enclosing fn, so the #604 description-prefix filter missed it and it previously leaked — the filter's new forall-origin arm, `vera/codegen/core.py`, matches a template-body `E602` by its source line falling inside a compiled-clone template's declaration span; the suppression stays gated on a clone having compiled, so an *uninstantiated* generic-closure template still keeps its honest skip-warning).  (3) A coupled call-rewrite gap the closure repro surfaced: `_get_arg_type_info_wasm` (`vera/wasm/inference.py`) had no `ArrayLit` branch, so `map_ident([1, 2, 3])` left `T` unbound on the WASM call-rewrite side and mangled the call to a never-emitted `map_ident$Bool` while discovery emitted `map_ident$Int` — a discovery↔call-rewrite desync; it now mirrors discovery's `ArrayLit` element-type inference.  The shared `Monomorphizer` the #732 verifier drives is untouched, so codegen still emits exactly the verifier's instantiation set (`test_monomorphize_differential.py` stays green).  New run-level conformance program `ch05_generic_pipe_monomorph` (suite now 131) plus twelve regression tests in `tests/test_codegen_monomorphize.py` (pipe at Int/String/Bool, chained and two-instantiation pipes, the piped-value-as-first-arg semantics, the closure `@T` body, the `Array<T>`-argument binding, the zero-warnings-on-a-correct-program and uninstantiated-template-still-warns suppression pair, and direct-call / non-generic-pipe controls) and a cross-module `x |> mod::gid()` pipe test in `tests/test_codegen_modules.py` pinning the `ModuleCall`-RHS pipe arm.  Mutation-validated: reverting the pipe-discovery branch flips only the pipe tests RED, restoring the closure `E699` raise flips only the closure tests RED, disabling the `ArrayLit` call-rewrite branch flips only the `Array<T>`-argument tests RED, and disabling the forall-origin suppression arm flips only the zero-warnings test RED (the uninstantiated-still-warns test stays green) — the four fixes are independently load-bearing.
- **A composite (user-ADT / `Option` / nested-ADT) equality in an `ensures` postcondition no longer TRAPS at runtime on a postcondition `vera verify` proved at Tier 1** ([#912](https://github.com/aallan/vera/issues/912)).  A maker `fn mk(@Int -> @Box) ensures(@Box.result == MkBox(@Int.0))` returning `MkBox(@Int.0)` passed `vera check`, verified at Tier 1, yet `vera run` exited 1 with `Postcondition violation in mk` — the postcondition is TRUE and Z3 proves it structurally, but codegen lowered the composite `==` in the *runtime* postcondition check to a raw `i32.eq` (a pointer compare), so the freshly-built return value and the freshly-built contract operand — structurally equal at different heap addresses — compared unequal and spuriously trapped.  Root cause: the `@Box.result` (`ResultRef`) operand of `==` resolved to a `None` Vera type in `_infer_vera_type` (`vera/wasm/inference.py`) — it was in that walker's "cannot occur" set — so the structural-`==` dispatch in `vera/wasm/operators.py` (guarded on the operand resolving to a known ADT name) was skipped and the code fell through to the scalar `i32.eq` default; `_infer_expr_wasm_type` already handled the `ResultRef` *width* (#891), so only the Vera-type dispatch was missing.  `_infer_vera_type` now resolves a `ResultRef` to its declared `@Type` (sharing the `SlotRef` name logic), so a composite `@T.result == ctor` postcondition routes through the SAME structural-equality codegen that `eq(a, b)` and `ctor == ctor` already use — which recurses fields and enforces the contract correctly at runtime (a genuinely-false composite postcondition is still rejected by `vera verify` at Tier 1 with `E500` AND traps at runtime — the fix makes true composite postconditions pass, never false ones; the verifier's own equality modelling is untouched).  Two coupled gaps surfaced and were closed in lockstep: the structural-`==` dispatch now recognises a monomorphized generic-ADT clone that lost its type argument (the #772 residue, `@T.result` → bare `@Box.result` for a `Box<T>` clone) and falls back to the pre-#912 scalar lowering rather than raising a spurious `E613` — genuinely non-`Eq` operands (`Map`/`Array`-field ADTs, `Tuple`) still raise the correct `E613`, keeping the checker↔codegen lockstep the #732 differential pins; and an imported generic ADT's type-parameter metadata (`_adt_tp_param_names` / `_adt_tp_counts` / `_ctor_adt_tp_indices`) is now propagated alongside its layout in `vera/codegen/modules.py`, without which the cross-module `Box<T>` clone could not be recognised.  New run-level conformance program `ch06_composite_postcondition_eq` plus `tests/test_composite_postcondition_eq_912.py` (user-ADT / `Option` / nested-ADT true-postcondition run tests in both operand orders, the `E500`+runtime-trap negative controls for false composite postconditions, and a Tier-1 verify pin); the whole `#732` verifier↔codegen differential (`tests/test_monomorphize_differential.py`) and the structural-`Eq` `E613` suite stay green.  Mutation-validated: neutering the `ResultRef` arm flips exactly the left-`@T.result` composite-run tests RED with `Postcondition violation`, while the negative controls stay rejected.  **Review round 2** closed a NEW codegen crash the round-1 fix introduced (a regression vs base): a function generic over the parameterized ADT itself with a slot-vs-slot postcondition (`fn id2(@Box<T> -> @Box<T>) ensures(@Box<T>.result == @Box<T>.0)`) resolved both operands to `Box<T>` — carrying an unresolved type variable `T` the monomorphizer did not specialize — which bypassed both lost-type-arg guards (they keyed on a *bare* name) and reached the derivability gate as an uncaught `AdtEqNotDerivableError` on a `check`-green/`verify`-green program that ran on base.  The lost-arg guard now also treats a name whose type argument is a free type VARIABLE (`Box<T>`, distinct from a concrete non-`Eq` argument like `Box<Array<Int>>`) as non-dispatchable and falls back to the pre-#912 scalar lowering.  This is SOUND because the scalar compare never runs: `Box<T>` occurs only in the BASE generic clone (`$id2`), which is DEAD CODE — never a call target, never exported (a bare generic fn cannot escape higher-order; that is a parse error, `E005`), so it can never reach a `call_indirect`/table.  Every *reachable* call dispatches to a MONOMORPHIZED clone (`$id2$Int`) whose `@Box<Int>.result == @Box<Int>.0` is lowered STRUCTURALLY (`call $eq_Box_LInt_R`; `Box<Int>` is concrete, so the free-var guard does not match it), correctly discharging the composite `==` — which IS proved at Tier 1 (the verifier substituting `T:=Int`; the free-var scalar fallback merely lets the dead base clone COMPILE as harmless dead code instead of `E613`-erroring and failing the whole compile).  A postcondition-path backstop in `vera/codegen/functions.py` also now catches the `AdtEqNotDerivableError` for a genuinely non-`Eq` contract composite (a `Tuple`, or a concrete `Array`/`Map`-field argument) and emits the same clean `E613` the body and closure paths already emit, instead of the uncaught traceback the `ensures` path produced before.  Round-2/3 tests: `@Box<T>.result == @Box<T>.0` (and the `!=` form) compile + run; a `rebox` pin where the result is a FRESHLY-constructed, structurally-equal, DIFFERENT-pointer box that is Tier-1-verified AND runs without trapping (the direct witness that the reachable path is structural — a reachable scalar pointer compare would trap); a WAT assertion that the reachable mono clone's postcondition uses `call $eq_` not `i32.eq`; and clean-`E613` pins for the `Box<Array<Int>>` and `Tuple` contract composites.  The conformance program gains the generic-parameterized-ADT case.  Mutation-validated independently: neutering the free-type-var routing flips the `Box<T>` run tests RED (dead-base-clone `E613`); neutering the `ResultRef` arm flips the `rebox` Tier-1+run pin and the structural-WAT pin RED (the mono clone becomes scalar and traps); removing the postcondition backstop flips the clean-`E613` tests to an uncaught exception.
- **`show` and `hash` now compile for composite types (ADT / `Tuple` / `Option` / `Result` / `Array` / nested) instead of dropping the enclosing function at codegen** ([#911](https://github.com/aallan/vera/issues/911)).  `show`/`hash` are registered as universal abilities (§9.8), so `show(MkFoo(5))` or `hash(Red)` passed `vera check` — but WASM codegen only handled primitives (`vera/wasm/calls_handlers.py`): every composite argument tripped `CodegenSkip` ("show()/hash() not supported for type …") and the function was silently dropped from the exports (`No exported functions to call` at `vera run`).  `_translate_show` / `_translate_hash` now render / fold a composite value *structurally*, recursing into each field by its own `show`/`hash`, mirroring the structural-`Eq` field traversal (`operators.py`): tag at offset 0, fields at concrete offsets recomputed from their concrete WASM types.  `show` renders a user ADT as `Ctor` (nullary) or `Ctor(f0, f1, …)`, a `Tuple` as `(a, b)`, `Option` as `Some(x)`/`None`, `Result` as `Ok(x)`/`Err(e)`, and an `Array` as `[e0, e1, …]` (each field/element by its own `show`; a `String` field renders as its content per the §9.8 String-identity rule); `hash` seeds with the constructor tag (or array length) and folds each field/element hash FNV-style — deterministic, and distinguishing distinct constructors.  Nesting recurses to arbitrary finite depth, including **same-base** nesting (`Option<Option<Int>>` → `Some(Some(1))`, `Tuple`-of-`Tuple` → `((1, 2), 3)`) as well as distinct-base (ADT-of-ADT, `Option`-of-`Tuple`, `Array`-of-composite): the recursion guard keys on the FULL parameterized type, so a finite composite that re-uses the same constructor at different type arguments renders correctly while a directly-recursive ADT (`List<Int>` whose field is again `List<Int>`) is still collapsed so the traversal terminates.  The parameterized argument type (`Option<Int>`, not the bare `Option` head) is recovered so inner field types resolve — including recursively for inline nested constructors (`show(Some(Tuple(1, 2)))`), from a slot's declared type, or from a non-generic user fn's declared parameterized return, which also closes the related `i32 → "Bool"` mis-typing of a composite `show`/`hash` argument returned from a helper fn (`_infer_fncall_vera_type`, `vera/wasm/inference.py`).  Primitive `show`/`hash` (and String/Unit/Decimal) are unchanged.  New run-level conformance program `ch09_show_hash_composites` plus a `TestCompositeShowHash` class in `tests/test_codegen_monomorphize.py`; mutation-validated by stubbing the composite path (composite tests flip RED, primitive pins stay GREEN) and by reverting the full-ptype recursion guard to the bare-head key (only the same-base-nesting tests flip RED, and the recursive-ADT case still terminates+skips).  A composite whose type parameters cannot be resolved at the `show`/`hash` site — a directly-recursive ADT (`List<T>`, which needs generated recursive helper functions) or an ADT with a `Map`/`Set`/host-handle field — still skips cleanly rather than mis-rendering, out of scope for #911.
- **Effect handlers (`State`/`Exn`) and effect-op results over a composite / parameterized type argument now compile and run instead of crashing codegen on a `vera check`-green program** ([#914](https://github.com/aallan/vera/issues/914)).  Seven shapes passed `vera check` but failed at codegen, across four root causes.  (A) **Effect-op result type not inferred in constructor-argument / match-scrutinee position**: a bare `get(())` (a `State<T>` op, an `ast.FnCall`) used *directly* as a `Tuple`/ADT constructor field or as a `match` scrutinee left `_infer_expr_wasm_type` returning `None` — the enclosing function was silently skipped (`could not infer constructor argument WASM type` / `could not infer match scrutinee WASM type`) and `main` dropped from exports.  The op's result WAT type (its `State<T>` parameter's type) is now recorded in a new `_effect_op_result_wt` registry at every op-injection site — the declared-effect path (`vera/codegen/functions.py`) and the handler-body path (`vera/wasm/calls_handlers.py`) — and consulted by both the `FnCall` and `QualifiedCall` inference arms (`vera/wasm/inference.py`); the old `QualifiedCall` arm's `_fn_ret_types` lookup on the dispatch target was dead for *every* value-producing op (`$vera.state_get_T` is never a `_fn_ret_types` key).  (B) **Handler WAT names not escaped for a composite type arg, plus an emit↔call desync**: a `State<Tuple<Int, Int>>` handler emitted `$vera.state_get_Tuple<Int, Int>` and an `Exn<Tuple<Int, Int>>` a `$exn_Tuple<Int, Int>` tag — raw `<`, `>`, `,`, ` ` are illegal in a WAT identifier — while a `State<Option<Int>>` handler body called `$vera.state_push_Option` (the base name `Option`) against an `(import …)` decl named `Option<Int>` (`unknown func`).  Every `state_*`/`exn_*` WAT identifier now routes its type-argument component through the injective `mangle_type_name` (#775) at all sites (`vera/codegen/assembly.py` import/tag decls, `vera/codegen/functions.py`, `vera/wasm/calls_handlers.py`, `vera/codegen/contracts.py`'s `old(State<T>)` read, and the host registration in `vera/runtime/state.py` — the `ExecuteResult.state` cell key stays the readable `State_<name>`), and the handler body uses the *full* canonical slot name (`_type_expr_to_slot_name`), so import decl and call site can never drift.  (C) **`Exn` payload `@T` binding not substituted**: an `Exn<Option<Int>>` handler pushed its caught-payload binding under the base name `Option`, so `@Option<Int>.0` in the handler body resolved to no local (dangling `@T.n`, `E699`); it now binds under the full canonical slot name.  (D) **`old(State<Composite>)` snapshot never allocated, `old`/`new` composite `==` mis-widthed, and nested type args dropped from the slot name**: `_extract_state_type_name` returned the BASE name (`Option`) for the `old(State<Option<Int>>)` snapshot lookup, missing the canonical-keyed `_state_types` entry, so no pre-execution snapshot local was allocated and the `old` read raised an uncaught `CodegenInvariantError` at run; `_infer_expr_wasm_type` had no `OldExpr`/`NewExpr` case, so a composite `new(State<Option<Int>>) == old(...)` fell through to the i64 comparison default and emitted an ill-typed `i64.eq` on two i32 pointers (`expected i64, found i32`); and every slot-name builder except the checker descended only ONE level, so `State<Option<Tuple<Int, Int>>>` and `State<Option<Tuple<Bool, Bool>>>` both collapsed to the slot name `Option<Tuple>` → the SAME mangled import/tag (a latent injectivity hole / Exn-tag type confusion, and a checker↔codegen/verifier slot-key desync since the checker already recursed).  `_extract_state_type_name` and `_infer_expr_wasm_type` now use the canonical slot name / State<T>'s WAT type; and the six duplicated one-level `_type_expr_to_slot_name` copies (`vera/codegen/core.py`, `vera/wasm/inference.py`, `vera/smt.py`, `vera/verifier.py`, `vera/tester.py`) plus the `SlotRef`-lookup and closure-capture name builders (`vera/wasm/operators.py`, `vera/smt.py`, `vera/wasm/closures.py`, `_type_expr_name`) are consolidated onto ONE shared recursive `vera.slots.type_expr_slot_name` / `slot_ref_name` (dedup) that recurses fully into nested type args, matching the checker — so a captured `@Array<Array<Int>>` and two distinct nested-composite State/Exn types resolve to distinct, correctly-matched keys.  Primitive `State<Int>` / `Exn<Int>` — the common case — are unaffected (base name equals full name).  New run-level conformance programs `ch07_state_composite`, `ch07_exn_composite`, and `ch07_state_old_composite` (`old(State<Composite>)`) plus a `TestEffectCompositeTypeArgs914` class in `tests/test_codegen_effects.py` covering all seven shapes (incl. two distinct nested-composite types → DISTINCT mangled names + values) with two primitive-effect regression pins.  Mutation-validated: reverting the result-WT inference flips exactly the two constructor-arg / match-scrutinee cases RED; reverting the full-name selection reproduces the `unknown func $vera.state_push_Option` desync; reverting the payload binding reproduces the `E699` dangling slot; reverting the `old`-state canonical-name fix reproduces the `old(State<Composite>)` crash; reverting the nested-recursion fix collides the two nested-composite names and reproduces the `unknown func $step_nested` closure-capture desync (`ch05_capture_array_index`) — the fixes are independently load-bearing.

### Changed

- **Doc code-block allowlists replaced by inline fence annotations** ([#538](https://github.com/aallan/vera/issues/538); retires the [#606](https://github.com/aallan/vera/issues/606) `fix_allowlists.py` bulk-shift bug by deleting the script).  The line-number-keyed `ALLOWLIST` dicts in the doc gates (`check_skill_examples.py`, `check_spec_examples.py`, `check_faq_examples.py`, `check_readme_examples.py`, `check_examples_doc.py`, `check_html_examples.py`, `tests/test_readme.py`, `tests/test_html.py`) are gone: a block that intentionally fails a compiler stage now carries `<!-- vera:skip-parse category="..." reason="..." -->` (or `vera:skip-check` / `vera:skip-verify`) on the line before its fence, parsed by the new shared `scripts/doc_annotations.py`.  The annotation travels with the fence through edits — no line numbers to maintain, no renumbering script, no cross-PR allowlist conflicts — and it is invisible in rendered markdown (`build_site.py` also strips it from the generated `docs/SKILL.md` / `docs/llms-full.txt`).  Stale detection got *stronger*: the gate still runs the exempted stage and fails on an annotation whose block now passes, so the skip surface shrinks as features land — the migration itself surfaced and removed 12 stale suppressions the old "does a block exist at this line" check could never see (SKILL.md's type-alias and slot-arithmetic examples parse; the spec's `async(Http.get(...))` composition, `md_parse` ADTs, `array_map`/`array_filter`/`Tuple` users, cross-module import examples, and unresolved-bare-call effect example now pass their full pipelines; the Request/Response prelude decls now parse and moved to an honest check-stage annotation for their deliberate missing visibility).  Malformed, dangling, and duplicate annotations are hard gate failures.  `scripts/fix_allowlists.py` and its pre-commit hook are deleted.
- **Mixed `Int`/`Nat` arithmetic now types as `Int`, not `Nat`** ([#755](https://github.com/aallan/vera/issues/755)).  The checker joined a mixed-numeric binary-arithmetic expression to `Nat`: the ADD/SUB/MUL/DIV/MOD join consulted the *bidirectional* `is_subtype`, and because the checker permits `Int <: Nat` as a verifier-mediated narrowing *relaxation* (spec §2.8 rule 5), `is_subtype(Int, Nat)` is `True` — so `Int <op> Nat` (Int on the left) picked the right operand's `Nat` base.  That was dishonest: only `Nat <: Int` is a *formal* subtyping rule (`Nat` is `{ @Int | @Int.0 >= 0 }`, a refinement subtype of `Int`; spec §2.2.1), so the least-upper-bound of `{Int, Nat}` is `Int`.  Typing `@Int.0 - 2` as `Nat` silently asserted non-negativity of a possibly-negative result with no verification obligation, against §0.2.2 ("no implicit behaviour"), and drove spurious `@Nat` narrowings downstream (`async(@Int.0 * 2)` inferred `Future<Nat>`; the #747 non-literal tuple-destructure `let @Tuple<Nat, Nat> = if c then Tuple(5, 5) else Tuple(0 - 1, 0 - 1)` verified `ok: true` with no narrowing obligation).  A new `numeric_join` helper (`vera/types.py`) computes the formal LUB — `Nat`/`Nat` → `Nat`, `Int`/`Int` → `Int`, any `Int`/`Nat` mix (either order) → `Int`, `Int`/`Float64` → mismatch (E141) — and the checker's arithmetic branch (`vera/checker/expressions.py`) uses it instead of the bidirectional `is_subtype`.  This aligns the checker with the behaviour the verifier and codegen already assumed (the #798 overflow classifier and #520 `@Nat`-subtraction guard both key on the operands' common type / a statically-`@Nat` result — spec §6.4.3, §11.2.1); spec §4.4 now states the mixed-arithmetic result-type rule explicitly, and `tests/conformance/ch09_async.vera` drops its now-unnecessary `requires(@Int.0 >= 0)` workaround — a cleanup, not a regression pin (that program passes either way; the fix is pinned by `tests/test_checker_int_nat.py` and the #798 overflow differential).  Mutation-validated both ways: reverting the join to the bidirectional-`is_subtype` version flips the three mixed-join type-observation tests RED, and a DIV/MOD-only bypass of `numeric_join` flips the two division/modulo pins RED (those two operators were otherwise unpinned by the entire suite).
- **`log(0.0)` / `log2(0.0)` / `log10(0.0)` now return `-Infinity` under the wasmtime runtime, matching the browser runtime and IEEE 754** ([#790](https://github.com/aallan/vera/issues/790)).  Python's `math.log(0.0)` raises `ValueError` at the zero pole — the same exception as a genuine domain error like `log(-1.0)` — and the host wrapper in `vera/runtime/math.py` folded every `ValueError` into NaN, so the two runtimes silently disagreed at the pole (native NaN vs browser `-Infinity`).  The wrapper now returns `-inf` for the log family when the input is zero (including `-0.0`, per IEEE 754); genuine domain errors (`log(-1.0)`, `asin(2.0)`, …) stay NaN in both runtimes.  Pinned by a native runtime test and a browser↔native differential (`test_log_pole_parity`) that requires identical stdout *and* the IEEE-correct answer; spec §9.6.10 now states the pole behaviour explicitly.  Mutation-validated: removing the pole branch flips both tests RED.
- **`_ShadowGuard.push` now rejects a partial shadow-stack slot** ([#791](https://github.com/aallan/vera/issues/791)).  The host-side GC shadow-stack push in `vera/runtime/heap.py` bounds-checked with `sp >= limit` before writing a 4-byte slot, so a misaligned `gc_sp` left with 1–3 bytes of headroom would have written past the window instead of raising the overflow diagnostic; the bound is now slot-complete (`sp < 0 or sp + 4 > limit`), which also rejects a negative `sp` that would have indexed host memory *before* the linear-memory base.  Defense-in-depth hardening of the GC trust root, **not a live bug**: the misaligned state is unreachable today — generated WAT always advances `gc_sp` in 4-byte steps from a 4-aligned window base — so no compiled program's behaviour changes.  New unit tests construct the state directly on a hand-rolled module and pin the three partial-headroom rejections, the exact-final-slot accept boundary, the full-window rejection, and the negative-`sp` rejection (mutation-validated: reverting the bound flips them RED).  The same file's only other window bound (`_read_string_export`) already used the slot-complete form.
- **A user-defined function named `show` or `hash` used in a constructor field or array element no longer mis-sizes the field and crashes codegen on a `vera check`-green program** ([#908](https://github.com/aallan/vera/issues/908)).  The WASM type-inference in `vera/wasm/inference.py` name-special-cased the ability operations `show` → `i32_pair` (`String`) and `hash` → `i64` (`Int`) by bare name in both `_infer_fncall_wasm_type` (field/element width) and `_infer_fncall_vera_type` (element Vera-type name).  The checker does *not* reserve the ability-op names (unlike `E151` registry builtins), so a user helper `fn show(@Int -> @Int)` or `fn hash(@Int -> @String)` — reached same-file or via `m::show(...)` — was inferred at the ability-op width, while `_translate_call` (gated on `not in known_fns`) correctly emitted a plain call to the *user* function at its declared return width; the two disagreed and produced an `expected i32, found i64` WebAssembly translation error at run.  Both inference sites now defer to the user function's registered return type (`_fn_ret_types`) when the name resolves to a user fn — mirroring the codegen dispatch gate — and fall back to the ability-op width only for a *genuine* unshadowed ability op (which has no user-fn registry entry), so `show`/`hash` on a value whose type derives `Show`/`Hash` still lowers at the correct `String`/`Int` width.  Covered by a codegen regression suite (module-qualified, dotted-path, same-file, `hash`-returning-`String`, and array-literal-element shapes, plus the load-bearing genuine-ability-dispatch-in-a-field cases) and a run-level conformance program (`ch09_user_show_hash_field`).
- **A composite `State<T>` (e.g. `State<Option<Int>>`, `State<Tuple<Float64, Int>>`) READ before any `put` now seeds the same default cell in the browser/Node runtime as it does natively, instead of throwing or diverging** ([#920](https://github.com/aallan/vera/issues/920); reachable only under the browser/Node runtime, surfaced after [#914](https://github.com/aallan/vera/issues/914) made composite `State` type arguments compile).  `vera/browser/runtime.mjs` picked each fresh state cell's default with `key.includes('Float') ? 0.0 : BigInt(0)`, keyed on the mangled type suffix.  A composite `State<T>` is an `i32` heap pointer, whose native default (`vera/runtime/state.py`) is the plain-integer null pointer `0` — but the substring test mis-seeded pointer cells: `State<Option<Int>>` (suffix `Option_LInt_R`, no `Float`) got `BigInt(0)`, which a `WebAssembly` `i32` import *cannot* accept ("Cannot convert a BigInt value to a number"), so the browser threw the moment the default was read, while native returned the null-pointer value; `State<Tuple<Float64, Int>>` (suffix `Tuple_LFloat64_CInt_R`) matched `Float` *inside* the composite name and got the JS float `0.0`, correct only by accident.  The default is now keyed on the cell's actual WASM value type — read from the module's own `state_*` import declarations (a minimal type/import-section parse of the raw bytes, since `WebAssembly.Module.imports()` omits signatures) — so `f64` → `0.0`, `i64` (bare `State<Int>` / `State<Nat>`) → `0n`, and `i32` (every composite/ADT pointer, plus `State<Bool>` / `State<Byte>`) → the plain-number null pointer `0`, matching `state.py`'s `_DEFAULT_STATE[wasm_t]` exactly.  Native `vera run` (wasmtime) was never affected.  Pinned by four browser↔native parity tests that read the default before any `put` (`State<Tuple<Float64, Int>>`, `State<Option<Int>>`, a user-ADT `State<Box>`, and a bare-`State<Float64>` regression); mutation-validated both ways — reverting to the substring heuristic flips the `State<Option<Int>>` and `State<Box>` tests RED (the pointer-typed default crash), and the naive exact-`Float64`-match alternative flips *both* composite tests RED while the bare-`State<Float64>` regression stays green.

## [0.0.196] - 2026-07-02

### Added

- **`examples/async_http_fanout.vera`** — the 37th example and the showcase for the v0.0.192 concurrent `<Async>`: two `async(Http.get(url))` requests started back-to-back so network latency overlaps, then awaited and folded into a Z3-proved `0..3` status summary (4 obligations Tier-1 verified).  `examples/async_futures.vera` and `EXAMPLES.md`'s async section were reworded to stop teaching eager-only async: eager applies to non-whitelisted shapes, direct `Http.get`/`Http.post` calls run concurrently ([#841](https://github.com/aallan/vera/issues/841)).
- **Two conformance programs pinning the `apply_fn` fix (suite now 106)**: `ch05_apply_fn_typing` (verify level — alias-typed application, variadic two-parameter application, deliberately non-commutative subtraction so a slot swap is caught) and `ch05_apply_fn_arity` (negative fixture, `expected_error: E201` — a wrong-arity `apply_fn` must fail `check`).
- **Spec completions after the server-effects sprint**: §0.7 chapter index gains the Chapter 13 row; §7.7.6 documents the `Async` marker effect alongside its peers; §9.5.3 drops the stale "(future work)" framing on async Http composition; §9.3.4 scopes the `Future<T>` zero-overhead claim to eagerly-evaluated futures; §11.10.5 documents `apply_fn`'s checker typing.
- **`wasmtime serve` line-buffering caveat** (spec §13.7 + TOOLCHAIN.md), empirically confirmed: under `--world server` a handler's `IO.print` without a trailing newline is held in the host's line buffer (flushed by the next newline, dropped at shutdown if none comes) — end log lines with `\n`; native `vera serve` does not buffer this way.
- **veralang.dev surfaces the server/WASI work**: two new Key-features cards (Verified HTTP handlers; WASI 0.2 components), a third Runs-Everywhere column with the `--target wasi-p2 --world server` → `wasmtime serve` command sequence, `HttpServer` in every effect list, and the 14-chapter/eight-effect counts — mirrored in the `build_site.py` literals that generate `index.md`/`llms.txt`/`llms-full.txt` (the hardcoded chapter list there gains Chapter 13).

### Changed

- **Documentation sweep after the server-effects sprint** (v0.0.192–v0.0.195): `HttpServer` and the WASI targets are now surfaced in README (tagline, delivered-features), FAQ, and DESIGN.md's target/Async rows; `KNOWN_ISSUES.md`'s WASI limitation row is rescoped from the closed [#237](https://github.com/aallan/vera/issues/237) to the honest remainder tracked by [#853](https://github.com/aallan/vera/issues/853) (wasi-p2 covers IO+Random only; the default target still uses ad-hoc `vera.*` imports), and the same rescope retitles vera/README's row to "Partial WASI support"; stale coverage-gap rows for the shipped [#592](https://github.com/aallan/vera/issues/592)/[#645](https://github.com/aallan/vera/issues/645) removed; vera/README's module map gains `codegen/wasi.py`, `runtime/wasi_host.py`, `runtime/server.py` (plus `tail_position.py`, `text.py`) with recounted package totals (~65,000 lines of Python); CONTRIBUTING.md's branch-protection section now records `enforce_admins` and the actual review requirement; MUTATION.md documents the stale-`.pyc` purge step for hand-run mutation kills; `ch10_float_predicates` renamed `ch09_float_predicates` (its content, manifest chapter, and spec_ref were all chapter 9); counts refreshed everywhere (37 examples, 106 conformance programs, 14 spec chapters, live test totals).  Scheduled limitation-state checking in CI is tracked by [#852](https://github.com/aallan/vera/issues/852).

### Fixed

- **`apply_fn` is now typed by the checker — no more spurious E200, and misuse is a check-time error** ([#854](https://github.com/aallan/vera/issues/854)).  The documented primitive for applying a stored function value was unknown to the checker: every use fell into the unresolved-bare-call path, drawing a `[E200] Unresolved function 'apply_fn'` warning on green programs (`vera check examples/closures.vera` showed it) and typing the call as `Unknown` — so wrong arity or argument types passed `check` with exit 0 and only surfaced at compile time (a raw WAT assembler error at line 0,0, or a silently skipped function), and applying an `effects(<IO>)`-rowed fn value inside an `effects(pure)` function passed entirely (a reachable effect-soundness hole: the fn-typed *parameter* route was open even though constructing such a closure in a pure fn already tripped E122).  `apply_fn` is variadic and effect-polymorphic — arity, argument types, result type, and effect row all come from the applied value's fn type — so it cannot be a fixed-signature registry row; it is now a checker special form typed structurally against the applied `FunctionType`: wrong arity → `[E201]`, wrong argument type / non-function first argument → `[E202]`, the applied row joins the caller's used effects and is checked against the declared row (`[E122]`/`[E125]`), and the result is the fn type's return type (a body returning an `apply_fn` result against the wrong declared return is now `[E121]`).  TypeVar-parameterised fn-type aliases (the prelude combinator shape, e.g. `@Mapper<A, B>` params) check as before.  Redefining `fn apply_fn(...)` — previously green at check time while codegen hijacked every 2+-argument call to `call_indirect`, silently skipping the caller — is now the same `[E151]` one-canonical-form error as any built-in redefinition.  Mutation-validated four ways (special form disabled, effect-row join skipped, arity check skipped, E151 reject-set entry dropped — each flips exactly its discriminating tests RED).
- **Prelude combinator skip-warnings no longer fire on every compile, and prelude diagnostics no longer misattribute to user source** ([#851](https://github.com/aallan/vera/issues/851)).  Every `vera compile` used to emit five `[E602]`/`[E604]` warnings about the generic Option/Result prelude combinators (`option_unwrap_or`, `option_map`, `option_and_then`, `result_unwrap_or`, `result_map`) being skipped — code the user didn't write and, in most programs, never calls — with locations that resolved the *prelude buffer's* line numbers against the **user's file** (a 5-line hello cited "line 61"; longer files rendered unrelated user source under the caret).  Two fixes: **(1) synthetic origin** — diagnostics about prelude-injected declarations (and their mono clones, and nodes inside their bodies) now cite the synthetic file `<prelude>` and quote the actual prelude source line, in both text and `--json` output, so a prelude span can never render user code (`inject_prelude` returns the buffer it parsed; codegen's `_warning`/`_error` resolve prelude-origin nodes against it); **(2) reachability suppression** — E602/E604/E605 skip-warnings for prelude functions are dropped unless the program actually references them, via a transitive call-target scan rooted at every non-prelude declaration (including generic user fns the mono collector never visits, and imported module fns), so a minimal program compiles with **zero warnings** while a program that references a skipped combinator without a compilable instantiation keeps exactly that combinator's warning as the honest pre-runtime signal (now correctly attributed).  A program that *successfully* calls `option_map` was already clean for the called fn via the #604 mono-compiled suppression; with the unreferenced four now silent too, it also compiles warning-free.  User-defined unsupported functions warn exactly as before, with user-file locations (pinned by test).  Mutation-validated four ways (suppression deleted, origin tag dropped, transitivity broken, body-node flag stuck false — each flips its discriminating test RED).

## [0.0.195] - 2026-07-02

### Added

- **`--world server` — verified HTTP handlers as portable wasi:http components** (Stage D of the server-effects sprint; spec §13.7).  `vera compile --target wasi-p2 --world server` packages the same contract-checked `handle(@Request -> @Response)` program `vera serve` hosts natively (#305) as a component exporting `wasi:http/incoming-handler@0.2.0` — **stock `wasmtime serve` runs it unmodified, no flags** (live-tested: routing, handler-set headers, a 1 MiB body byte-identical under GC stress, trap → host 500; imports pinned at `@0.2.0`, which wasmtime's semver-compatible lookup links — the design study also probed `@0.2.3` acceptance).  A generated adapter wrapper reads method / path-with-query / headers / body through the wasi:http interfaces into the arena, constructs the `Request` ADT in the guest heap from the compilation's own constructor layouts (WAT-level shadow-stack rooting throughout), calls `handle`, decodes the `Response`, and drives the outgoing-response resource sequence (borrow-before-transfer, child-stream-drop-before-finish, 4096-byte write chunks).  **Headers without a host:** `Map<String, String>` operations are host imports on the core target, but a Vera Map is two plain guest-heap blocks — the server world implements the String-keyed Map ops *in guest WAT* with the host's exact semantics (position-preserving update, later-insert-wins, power-of-two capacity growth), pinned by a host-vs-served differential battery (mixed-case / absent / duplicate / 41-header matrix + insertion-order agreement); non-String Map instantiations and all other collection families stay gated.  The server-world surface is honest and explicit: `IO.print`/`IO.stderr` route to the serve console; `read_line`/`read_char`/`read_file`/`write_file`/`get_env`/`args`/`exit` are **rejected with a diagnostic** (negative-probed: those imports do not link under the wasi:http proxy world); bodies are buffered (streaming is future work); request headers share the ~63 KiB arena; an out-of-range status or forbidden header answers 500 instead of trapping the server.  The cli-world emission is **byte-identical** to v0.0.194 (pinned).  `vera run` rejects server-world artifacts with a pointer to `wasmtime serve` (wasmtime-py's built-in host has no wasi:http); the native `vera serve` driver is unchanged — one handler, two deployment paths.  Design decisions live-validated before implementation (topology: MAIN owns memory, serve wrapper in the adapter, dispatch table 16→32 slots; the check-7 `$Libc` dodge and `incoming-body.finish` both superseded — recorded in `WASI.md`); five mutation kills (later-wins parity, map update-vs-append, Request-build shadow-push via an in-suite eager-GC surgery test, cli-pin leak — which first false-greened on a stale `.pyc`, purged and confirmed RED — and a family-gate line).

## [0.0.194] - 2026-07-02

### Added

- **Experimental WASI Preview 2 target — `vera compile/run --target wasi-p2`** ([#237](https://github.com/aallan/vera/issues/237)).  `vera compile --target wasi-p2` emits a **binary WebAssembly component** whose `vera.*` IO + Random host imports are implemented on top of WASI 0.2 interfaces, runnable by any stock wasip2 host — `wasmtime run` works with no flags and no Vera bindings (`wasmtime.wat2wasm` accepts component text, so no external componentizer and no new dependency).  The component wraps the **unchanged core module** (the default `--target wasm` emission is untouched, pinned by test): each `(import "vera" "op")` becomes a same-named `call_indirect` shim through a funcref dispatch table the main module defines and exports, and a compiler-generated adapter core module implements all 14 IO/Random ops (plus the `contract_fail`/`overflow_trap` channels) over canon-lowered WASI imports, planting itself into the table via active elem segments strictly before any lifted export can run — the naive main↔adapter import topology is an instantiation cycle, which the component model forbids.  `cabi_realloc` is a bump allocator over a **GC-exempt 64 KiB scratch arena** below `gc_heap_start` (host-written data needs no rooting and a collection can never move a half-written block — the #593/#695 UAF class, host-side); data crossing back into Vera is copied into GC-heap blocks under explicit shadow-stack rooting, and a 500-arg GC-pressure stress pins it.  Canonical-ABI variant discriminants are read `i32.load8_u` (they are u8; retptr-slab reuse leaves garbage in the padding — found live as an EOF misread as `last-operation-failed` with a stale list length as a "handle").  `vera run --target wasi-p2` executes the component under wasmtime-py's built-in `add_wasip2()` host (new `vera/runtime/wasi_host.py`) with the core path's `ExecuteResult` contract: stdout/stderr capture (+ the #543 live tee), `os.environ` snapshot, cwd preopen for file ops, argv, `IO.read_line` CRLF handling matching the core host's universal-newlines behavior (a lone `\r` separator stays content — spec chapter 13), and the #516 trap-kind taxonomy — a contract violation classifies as `contract_violation` with the full violation text recovered from the WASI stderr channel (structured frames do not cross the component boundary; the JSON envelope's `frames` is empty — spike check 5's documented degradation).  A program using any host family beyond IO/Random gets a **diagnostic naming the family** — never a silent fallback to the core target.  Both entry worlds are exported: `wasi:cli/run@0.2.0` (what stock `wasmtime run` invokes) and a plain lifted `main` for scalar returns (`Int`/`Nat` as `s64`, `Float64`, `Unit`); a `String`/heap-returning `main` runs via `wasi:cli/run` and reports no value.  Inherent WASI 0.2 divergences are documented in the new **spec chapter 13** and pinned by tests: `wasi:cli/exit@0.2.0` carries ok/err only, so `IO.exit(n)` degrades to exit status 0/1 under *every* wasip2 host.  Validation is live, not parse-only (the design study caught a mis-spelled filesystem `error-code` case — `quota`, not `disk-quota` — only at `add_wasip2` instantiation): the suite instantiates and executes every artifact, a **dual-target conformance differential** runs all 88 run-level conformance programs under both targets and requires byte-identical stdout/stderr (71 execute identically; the rest are family-gated, `main`-less, or wall-clock-dependent — the sweep also surfaced that `handle[Exn]` programs need the wasip2 runner to enable the exceptions proposal like the core engine does), and a stock-`wasmtime`-CLI smoke test runs where the CLI is installed.  This closes #237 as written (its listed ops are exactly the IO family) as an **experimental WASI Preview 2 target (IO + Random surface)** — not a blanket "WASI 0.2 compliant" claim; the remaining `vera.*` families (Map, Set, Decimal, Json, Html, Md, Regex, Math, Http, Inference, State, Async) stay host-bound on the core target.  Also: the doc-builtin-shadowing scanner now skips `.claude/` (session-tooling worktrees carry full repo copies that re-flagged every spec chapter).

### Fixed

- **GC use-after-free: pair-typed `let` bindings (`String` / `Array<T>`) were never shadow-rooted** ([#847](https://github.com/aallan/vera/issues/847), fixed by [#846](https://github.com/aallan/vera/pull/846)).  The plain-`let` pair branch in `translate_block` (`vera/wasm/context.py`) stored the (ptr, len) pair into WASM locals without pushing the pointer onto the GC shadow stack — the last unrooted sibling of the [#705](https://github.com/aallan/vera/issues/705) scalar-i32 let fix and the [#707](https://github.com/aallan/vera/pull/707) let-destruct pair fix (whose review comment had already named this gap class).  Unobservable for Vera-side producers — an array literal or string builtin shadow-pushes its own freshly-allocated block at the alloc site, and that push survives to the function epilogue, masking the missing let root — but a **host-import** pair (`IO.args` → `Array<String>`, `IO.read_line` → `String`) is rooted only host-side during construction (`_ShadowGuard`, popped on return), so the first Vera-side allocation after the `let` could collect the block while the locals still pointed at it: the free list overwrites the payload's first words and reads through the binding see reclaimed bytes (`IO.print` of an `IO.args` element after a `nat_to_string` call printed `2::++2@` instead of `2:aa+bb`; `string_join` over the swept backing chased overwritten element pointers).  Found stress-testing [#237](https://github.com/aallan/vera/issues/237) under `VERA_EAGER_GC=1`; no WASI code involved, and reachable under ordinary collection pressure.  Pair lets are now rooted unconditionally — static and null pointers are ignored by the conservative scan's heap range check, so the push is harmless for non-heap values.  New targeted eager-GC tests pin the fix (args / read_line reproducers) and confirm the neighbouring host-import ADT paths (`IO.read_file` → `Result<String, String>`, `IO.get_env` → `Option<String>`) were already rooted by the #705/#707-era fixes.
- **De-flaked the #841 live-interrupt test; one red combo no longer cancels the CI matrix** ([#848](https://github.com/aallan/vera/issues/848)).  `test_async_await_keyboard_interrupt_live_request` printed its progress marker *between* `async(...)` and `await`, racing the worker thread's request — whose arrival gates the test's interrupt — against the guest reaching the print: a lost race yields the correct exit 130 with empty stdout and failed the stdout assertion (twice on macos-26 runners across PR [#846](https://github.com/aallan/vera/pull/846)'s matrices, each time cancelling the other 11 fail-fast jobs).  The print now precedes the `async(...)`, so program order supplies a real happens-before (buffer write → request issuance → arrival → interrupt) and the assertion strengthens from `in` to `==`; prompt issuance at the `async` point stays pinned by the #841 request-ordering and operand-stack tests, so the reorder loses nothing.  The test matrix also sets `fail-fast: false`, so a single red combo reports alone instead of cancelling eleven healthy jobs.  **Round 2 (PR #849, closes #848):** the reorder alone proved insufficient — server arrival is produced by the executor worker thread and says nothing about the main thread's position, so `interrupt_main()`'s pending flag could materialize outside `execute()`'s protected region on slow runners (an xdist worker crash on two macOS jobs, a completed run with `exit_code=None` on ubuntu — 3/12 jobs at one head).  The interrupter now also polls `sys._current_frames()` until the main thread is verifiably parked in `host_async_await` → `Future.result` — the interruptible wait the #595-class machinery intercepts — before firing, with a bounded deadline so the test can never hang.

## [0.0.193] - 2026-07-02

### Added

- **`<HttpServer>` — verified HTTP request handling, served by `vera serve`** ([#305](https://github.com/aallan/vera/issues/305)).  A server program defines a **total, contract-checked** handler — `public fn handle(@Request -> @Response) effects(<HttpServer>)` — and `vera serve prog.vera [--port N]` hosts the accept loop: the loop lives in the host, so handlers need no `Diverge`, are termination-checked like any function, and every contract on the handler (or its helpers) is an ordinary Tier-1/Tier-3 obligation (the new `examples/http_server.vera` proves a status-range postcondition at Tier 1).  `HttpServer` is a built-in marker effect (no operations; spec §7.7.5); `Request` (method, path, headers, body) and `Response` (status, headers, body) are prelude ADTs injected when referenced (headers are `Map<String, String>`; user definitions shadow).  **Per-request isolation**: each request runs on a fresh module instance (fresh `execute()` — instantiation measured at ~0.02 ms in the Stage-0 spike), so `State<T>` cannot leak between requests (pinned by test).  A runtime **contract violation (or any trap) inside a handler answers 500** with the trap diagnostic as a JSON body (`trap_kind`, message, frames — the `vera run --json` envelope shape); the connection is always answered.  Host marshalling is layout-driven: `CompileResult.adt_layouts` exports the constructor layouts this compilation computed, `build_request_adt` shadow-roots every intermediate allocation (#570/#692 discipline), `decode_response_adt` reads the returned ADT, and both fail loudly on an unexpected layout shape; a new `InstanceCaller` adapts a (Store, Instance) pair to the caller protocol the heap helpers already use.  Handler `IO.print` goes to the server console; request handling is sequential in v1 (concurrency: #406); native-only (the browser runtime does not serve HTTP — documented divergence, spec §12.9.3).  Also fixed en route: Pass-1 function signatures that reference prelude ADTs in params/return were computed before prelude registration and recorded `unsupported` in `fn_param_types` — a post-injection pass now re-registers exactly those.  New: `tests/conformance/ch09_http_server.vera` (level `verify` — needs no network, unlike `ch09_http`), `examples/http_server.vera`, spec §9.5.6, and a `TOOLCHAIN.md` serve recipe.

## [0.0.192] - 2026-07-02

### Added

- **Concurrent `<Async>`: `async(Http.get/post(...))` now runs on a host worker thread** ([#841](https://github.com/aallan/vera/issues/841)).  The concurrency half deferred when #59 shipped the language surface: `async(Http.get(url))` / `async(Http.post(url, body))` — with call-free argument expressions — fuse into a single `vera.async_http_get` / `vera.async_http_post` host import that issues the request on a host `ThreadPoolExecutor` at the `async(...)` point (request *issuance* keeps program order) and returns the `Future` as a #578 bit-31-tagged handle wrapper (new wrap kind 4); `await` probes the value's first word for the wrapper tag and blocks on `vera.async_await` for fused handles, passing eager values through unchanged.  Two overlapping `async(Http.get)`s are pinned by a server-side request-ordering test (no wall-clock).  Every other `async` shape keeps the eager identity lowering (spec §9.5.4 now says an implementation MAY evaluate concurrently when the effect row is commutative; §7.4 records the ordering guarantee).  The fusion predicate is strictly narrower than `W002`'s whitelist, so a "evaluates eagerly" warning can never coexist with concurrent execution — and it lives in one shared module (`vera/wasm/async_fusion.py`) consumed by both import-emission passes, so the pre-scan and the translator cannot desync.  GC follows the full opaque-handle contract, mutation-validated: the wrapper is shadow-rooted at the async site (operand-stack window pinned) and an unawaited future's wrapper is reclaimed by Phase 2c `host_decref_handle(4, handle)`, cancelling the task and evicting the store entry (observable via `ExecuteResult.host_store_sizes["future"]`).  Worker threads run only the pure fetch halves (`fetch_get` / `fetch_post`, split out of the `Http` host callbacks in `vera/runtime/http.py`) and never touch guest memory; the `Result` ADT is built at await time on the guest thread.  Ctrl-C during a blocked `await` rides the `wasmtime>=45` BaseException trampoline to the exit-130 handler, and the executor is torn down (`shutdown(cancel_futures=True)`) on every exit path.  The browser runtime stays eager (spec-conformant; identical values, request fires synchronously at the `async` point, outcome buffered host-side until `await`).  A function may now *declare* a `Future<T>` return type (previously `[E605]`-skipped — `Future<T>` is transparent in the type mapper).  The `await` handle-check keys on the literal type `Future<Result<String, String>>` and covers slots, parameters, direct compositions, and calls — bare, imported, or module-qualified — whose declared return is that type (derived from the cross-module return-type registry — with module-qualified calls resolved by `(path, name)`, so a colliding local of a different future shape cannot misclassify `await(m::grab(...))` in either direction; the local-only first cut and the bare-name qualified keying were both caught in review with repros and fixed with RED-first tests).  Alias-typed shapes are rejected before codegen today (an alias-typed `let` has no WASM representation → `[E602]` skip); the one unclassifiable shape — an indirectly-called closure returning this future type — is a documented limitation in KNOWN_ISSUES.md.
- **`W002` — async concurrency-eligibility warning** ([#841](https://github.com/aallan/vera/issues/841)).  `async(e)` is only made concurrent when `e`'s effect row stays within the commutative whitelist (`{Http}` in v1; the `Async` marker itself has no operations and cannot order anything).  For any other row the checker now says so — `async(IO.print(...))` warns that it evaluates eagerly at the `async()` site — instead of letting a program imply concurrency it does not get.  The rule resolves effect-op calls to their parent effect and function calls to their declared rows, recursively, and treats anything unresolvable as conservatively non-commutative.  Mutation-validated four ways (rule deleted, whitelist over-permissive, whitelist over-firing, fn-call rows ignored — each flips exactly its discriminating tests RED).

### Changed

- **Split `tests/test_checker.py` into eight phase-focused test files** ([#420](https://github.com/aallan/vera/issues/420)).  The monolithic 6,752-line, 60-class checker test file is replaced by eight themed files — `test_checker_types.py`, `test_checker_patterns.py`, `test_checker_functions.py`, `test_checker_effects.py`, `test_checker_modules.py`, `test_checker_errors.py`, `test_checker_builtins_collections.py`, `test_checker_builtins_strings.py` — each under 1,200 lines, with the shared header (the `_check` / `_errors` / `_warnings` / `_check_ok` / `_check_clean` / `_check_err` helpers and the `EXAMPLES_DIR` / `EXAMPLE_FILES` / `CLEAN_EXAMPLES` / `WARN_EXAMPLES` constants) extracted to a new `tests/checker_helpers.py` imported by all eight (matching the repo's existing `from tests.<module> import` precedent).  Mechanical and behaviour-preserving: all 572 tests are carried over unchanged and each class moves whole; no test is added, removed, or modified.  (Companion to the [#419](https://github.com/aallan/vera/issues/419) `test_codegen.py` split.)
- **Split `tests/test_verifier.py` into nine theme-focused test files** ([#839](https://github.com/aallan/vera/issues/839)).  The 9,356-line, 38-class verifier test file — the largest remaining after the #419 split, and the verifier/SMT oracle for the mutation sweep — is replaced by nine theme files (`test_verifier_contracts.py`, `_nat_obligations`, `_primitive_ops`, `_calls_modules`, `_adt_decreases`, `_refinements`, `_shadow_audits`, `_mutation_obligations`, `_mutation_gates_smt`), each under 1,500 lines, alongside the pre-existing `test_verifier_coverage.py` (untouched).  The shared harness — the `_verify` / `_verify_ok` / `_verify_err` / `_verify_warn` / `_nat_sub_status` helpers plus the `EXAMPLES_DIR` / `ALL_EXAMPLES` corpus constants and the `_MK` source template — moves to a new `tests/verifier_helpers.py` imported per file.  Mechanical and behaviour-preserving: all 474 tests are carried over unchanged, every class / helper / constant moves byte-for-byte (46/46-block differential against the pre-split file), and the `[tool.mutmut]` oracle selection tracks the new files.  The issue's 8-file plan became nine under the 1,500-line ceiling (the #746 refinement-predicate class alone is ~1,300 lines, and the #387 hardening battery split in two).  Completes the oversized-test-oracle split program (#420, #419, #839).
- **Split `tests/test_codegen.py` into twenty-one feature-focused test files** ([#419](https://github.com/aallan/vera/issues/419)).  The monolithic 21,225-line, 161-class codegen test file — the largest file in the tree, and the codegen oracle for the mutation sweep — is replaced by twenty-one feature files (`test_codegen_expressions.py`, `_calls`, `_infrastructure`, `_interpolation`, `_effects`, `_data_types`, `_arrays`, `_refinements`, `_strings`, `_string_builtins`, `_numeric`, `_io`, `_collections`, `_json`, `_decimal`, `_host_effects`, `_nat_guards`, `_translator_fixes`, `_gc_alloc`, `_gc_rooting`, `_gc_reclamation`), each under 1,500 lines (the issue's acceptance criterion), alongside the six pre-existing `test_codegen_*` modules.  The shared harness — the eleven `_compile*` / `_run*` / WAT-and-GC assertion helpers (four of which lived interspersed between classes) plus the `_IO_PRELUDE` / `_INLINE_BUILTIN_NAMES` fixture constants — moves to a new `tests/codegen_helpers.py` imported per file.  Mechanical and behaviour-preserving: all 1,219 tests (including the 10 `stress`-marked) are carried over unchanged and every class, helper, and constant moves byte-for-byte; per-file import blocks are computed from actual usage, and the `[tool.mutmut]` oracle selection tracks the new files.  The issue's original 9-file plan was drawn when the file was 10,019 lines / 118 classes; at 21,225 / 161 the same theme boundaries yield twenty-one files.  This closes out roadmap **Tier 1** (safety net and runtime robustness).  (Companion to the [#420](https://github.com/aallan/vera/issues/420) `test_checker.py` split.)
- **Consolidated the triplicated `_resolved()` helper in `tests/test_checker_modules.py`** ([#835](https://github.com/aallan/vera/issues/835)).  `TestCrossModuleTyping`, `TestVisibilityEnforcement`, and `TestModuleCallParsed` each carried their own `@staticmethod _resolved()` that builds a `ResolvedModule` from source text, and the `TestModuleCallParsed` copy had drifted (`file_path=Path("/fake")` vs the others' `Path(f"/fake/{'/'.join(path)}.vera")`).  Replaced all three with a single module-level `_resolved_module()` called directly, unifying on the non-drifted `file_path` form.  Follow-up to the #420 split, which had deliberately preserved the duplication byte-for-byte (unifying changes the `file_path` value, which flows into diagnostic `location.file` via `vera/checker/modules.py`); the 45 module tests are unaffected.

## [0.0.191] - 2026-07-01

### Changed

- **Codegen type-check-impossible guards now raise `CodegenInvariantError` (`[E699]`)** ([#657](https://github.com/aallan/vera/issues/657), follow-up to [#626](https://github.com/aallan/vera/issues/626)).  The #626 audit classified every `return None` in `vera/codegen/**` and `vera/wasm/**`; #658 converted the 104 user-actionable SILENT_SKIP sites to `CodegenSkip` (`[E602]`).  This converts the genuine **INVARIANT_DEFENSIVE** guards — non-forwarding dispatch fall-throughs and shape guards on states the type checker has already rejected — to `raise CodegenInvariantError`, surfaced at the `_compile_fn` boundary as an `[E699]` "internal compiler error, please file a bug" (severity `error`) instead of a silent skip or a mis-attributed `[E602]`.  21 sites: 2 in `codegen/closures.py`, 19 in `wasm/operators.py`; pinned by `tests/test_codegen_invariant_e699.py`.  Behaviour-preserving — the converted paths are `# pragma: no cover` (type-check-impossible).  A closure-body invariant now *propagates* to `_compile_fn` for a single `[E699]` (rather than being caught in `_compile_lifted_closure`, which emitted `[E699]` **and** returned None so the enclosing function also got a spurious `[E602]` "closure skipped"); and the quantifier-predicate guard is tightened to require exactly one parameter (so a malformed multi-parameter predicate raises rather than silently using the first).
- **Corrected the #626 audit's PROPAGATE premise** ([#657](https://github.com/aallan/vera/issues/657)).  The audit assumed #658 made every codegen leaf *raise*, leaving the PROPAGATE `if x is None: return None` forwards dead.  That is false for the #630 `[E615]` string-interpolation channel: `_translate_interpolated_string` records failing segments and returns `None`, which propagates up and is dropped loudly as `[E615]` at the `_compile_fn` boundary.  So `translate_expr` / `translate_block` still return `None` *reachably*, and forwards of them are **load-bearing PROPAGATE, preserved** — not removed (satisfying the issue's "preserved (still reachable)" criterion).  Five `operators.py` sites the audit had tagged INVARIANT were in fact such forwards and are kept as `return None`; the `inference.py` defensive sites are kept as `Optional`-by-contract.  The invariant is now documented in `vera/skip.py` ("Reachable None via the [E615] channel") and at every preserved forward, so a future cleanup pass can't repeat the mistake (which the test suite caught as an `AssertionError` in `TestE615LoudInterpolationFallthrough630`).

## [0.0.190] - 2026-07-01

### Changed

- **Text I/O is UTF-8 regardless of the host locale, enforced by a gate** ([#645](https://github.com/aallan/vera/issues/645)).  Python's text-mode `open()` / `Path.read_text()` / `Path.write_text()` — and `subprocess.run/Popen/check_output(..., text=True)` captures — fall back to `locale.getpreferredencoding()` (cp1252 on en-US Windows) when no `encoding=` is given, so a Vera source / doc / fixture / program output containing `→`, `—`, or any non-ASCII byte failed on a locale-default Windows shell.  [#641](https://github.com/aallan/vera/issues/641) papered over CI with a `PYTHONUTF8=1` backstop; this is the durable fix.  A new `scripts/check_explicit_encoding.py` AST-audits every text-mode `open()` / `read_text()` / `write_text()` **and** every `subprocess(..., text=True)` capture under `vera/`, `scripts/`, `tests/`, requiring an explicit `encoding="utf-8"` literal (binary / bytes mode skipped; a deliberate exception opts out with `# encoding-exempt: <reason>`), wired into pre-commit and the CI `lint` job.  It found and fixed **160 bare file-I/O sites across 16 files** plus **97 subprocess text captures across 18 files** (far more than the ~30 the issue estimated — the suite has grown ~5× since #641).  The audit's scope also covers `tempfile.NamedTemporaryFile` / `TemporaryFile` / `SpooledTemporaryFile` opened in *text* mode (they default to binary, but a text one is a locale-encoded write just like `open(..., "w")` — the idiom test helpers use to stage `.vera` source containing `→` / `—`; **29 such sites across 9 files** fixed).  The `vera` CLI reconfigures **stdin** as well as stdout/stderr to UTF-8 at startup, so a Vera program's non-ASCII output *and* input (e.g. `IO.read_char` on piped UTF-8) round-trip on any locale.  Together these made text I/O locale-independent and the `PYTHONUTF8=1` CI backstop (#641) was **removed** — verified by a clean Windows matrix with the backstop gone (the tempfile and stdin gaps were surfaced by that matrix, not the audit, which is why removing the backstop is its own acceptance test).  Pinned by `tests/test_check_explicit_encoding.py` (checker logic, scope-discovery coverage, and a repo-clean assertion).  (Folds in the stdio / subprocess work that had briefly been split to [#832](https://github.com/aallan/vera/issues/832).)

## [0.0.189] - 2026-07-01

### Changed

- **The UTF-8 "safe decode" invariant now lives in one helper** ([#592](https://github.com/aallan/vera/issues/592)).  Six sites decoded WASM-memory bytes with `errors="replace"` so a corrupt String `(ptr, len)` pair from an upstream codegen bug surfaces as U+FFFD rather than a raw Python `UnicodeDecodeError` escaping wasmtime's trampoline as a "python exception" cause — the [#516](https://github.com/aallan/vera/issues/516) / [#522](https://github.com/aallan/vera/issues/522) / [#589](https://github.com/aallan/vera/issues/589) contract that a user-level program never produces a Python traceback.  The invariant was re-implemented at all six (`host_print` / `host_stderr` / `host_contract_fail` / the String-return extractor in `vera/codegen/api.py`, `_read_wasm_string` in `vera/runtime/heap.py`, and `_read_string` in `vera/wasm/markdown.py`) and guarded by six *structural source-grep* tests plus one end-to-end test covering only `host_print` — so five of the six sites had no behavioural coverage, and the greps would break under exactly the refactor the issue proposed.  The `errors="replace"` decode now lives in one place — a new `vera.runtime.text.safe_utf8_decode` (the [ROADMAP Tier-2 single-source-of-truth](ROADMAP.md) theme, same class as the [#828](https://github.com/aallan/vera/issues/828) error-code registry) — reached only through a shared `_slice_and_decode` helper (`vera/runtime/heap.py`) that the three WASM-memory string readers (`_read_wasm_string` and a new `_read_string_export` there, and markdown `_read_string`) delegate to.  The `host_print` / `host_stderr` / `host_contract_fail` host imports and the String-return extractor route through those readers instead of decoding inline, so no `bytes(...).decode(...)` call survives outside `_slice_and_decode` (a net simplification of `execute()` — the closures no longer re-implement the memory read).  The six structural greps are replaced by one helper unit test plus **three** end-to-end tests that wire the **production** readers (`_read_wasm_string`, `_read_string_export`, markdown `_read_string`) over a memory region seeded with invalid UTF-8, so a strict-decode regression surfaces as a `UnicodeDecodeError` escaping the trampoline — caught at one point per reader rather than needing a grep per call site, and transitively covering the host imports / extractor that route through them; the `host_print` end-to-end test is retained to pin the trampoline fact itself, independently of the production path.  Behaviour-preserving.  (The network-response decode sites in `http.py` / `inference.py` are a separate, already-resolved family — [#591](https://github.com/aallan/vera/issues/591), closed — with deliberate per-site handling (`errors="replace"` for `Http.get` / `Http.post`; intentionally *strict* for `Inference.complete`, so a non-UTF-8 LLM response fails as a structured `Result::Err` rather than silently gaining U+FFFD), pinned by their own `TestNetworkResponseUtf8Hygiene591` class; they are outside this WASM-memory helper's scope by design.)

## [0.0.188] - 2026-06-30

### Added

- **Diagnostic-field discipline is now enforced** ([#682](https://github.com/aallan/vera/issues/682)).  `spec/00-introduction.md` §0.5.1 requires every diagnostic to carry a `rationale`, a `fix`, and a `spec_ref`, but the `Diagnostic` dataclass defaults all three to `""` — so a partially-tagged diagnostic compiled and shipped silently, weakening the "diagnostics as instructions" guarantee (DESIGN.md §Checkability).  New `scripts/check_diagnostic_fields.py` (wired into pre-commit and the CI `lint` job, mirroring the #597 walker-coverage gate) AST-parses every `Diagnostic(...)` constructor and `self._error(...)` / `self._warning(...)` call under `vera/` and fails when one of the three fields is missing — or when a present `spec_ref` does not resolve to a real spec section/chapter with a matching title (the *present-but-wrong* case, e.g. citing §4.3 "Operators" when §4.3 is "Slot References").  The exemption surface is **explicit and reasoned**, never silently inferred (DESIGN.md §"Explicitness over convenience"): a `warning` is not required to carry a `fix` (it has no corrected-code template); the codegen `_error`/`_warning` helpers — internal-compiler (E699) and "function skipped" diagnostics with no user fix or spec section — are exempt via a documented registry in the script; and a one-off internal/defensive site carries a `# diag-fields-exempt: <reason>` marker (a dedicated token, deliberately *not* `# noqa:`-prefixed so it cannot collide with ruff's suppression namespace).  A cheap emission-side check also requires every literal `error_code` under `vera/` to be registered in `ERROR_CODES` (catching typos / unregistered codes); enforcing each code's *uniqueness* per concept and its *presence* is tracked in [#828](https://github.com/aallan/vera/issues/828).  The pre-commit hook also triggers on `spec/**/*.md` (not only `vera/**/*.py`), since the validity pass reads the spec section/chapter titles — a spec-only retitle can invalidate an otherwise-unchanged citation, and now re-runs the check locally rather than only in CI.

### Fixed

- **Corrected the pre-existing wrong `spec_ref` citations and documented typed holes** ([#682](https://github.com/aallan/vera/issues/682)).  The adversarial audit behind the new validity gate found 30-plus diagnostics across the checker, the parser-error factories, the codegen path, and the verifier whose `spec_ref` cited the wrong section number or title (e.g. all arithmetic/comparison/logical operator errors cited §4.3 "Operators", but §4.3 is "Slot References" — arithmetic is §4.4, comparison §4.5, logical §4.6) — a misleading instruction is worse than a missing one.  Every `spec_ref` under `vera/` now resolves.  Typed holes (`?`) had **no spec section at all** — `W001` (and the codegen `E614`) cited a fabricated §3.10 "Typed Holes" — so they are now documented in a new `spec/04-expressions.md` §4.17 "Typed Holes", which both diagnostics cite.  Two further citations resolved to a *real but semantically wrong* section (which the validity gate, checking only that a ref resolves with a matching title, accepts — so they needed reviewer judgment): the unresolved-qualified-call warning `E220` cited §7.4.1 "Ambiguous Operations" (the multi-*match* case) but the error is an *unresolved* op, so it now cites §7.4 "Performing Effects"; and the type-alias-cycle error `E132` cited the unrelated slot-namespace section §3.8 "Type Alias and Reference Resolution" — the rule it actually enforces ("an alias must resolve to a concrete type; the alias chain must be acyclic") had no spec home, so a sentence stating it was added to `spec/02-types.md` §2.6.3, which `E132` now cites.  Two `fix` *templates* were also corrected for machine-actionability: the unresolved-bare-call diagnostics (`E200` and the codegen cross-module backstop) replaced a literal-looking `import the.module(f)` placeholder with `import <module>(f)` plus a "replace `<module>` with that module's path" hint; and the ADT-invariant error (`E120`) — whose old `fix` suggested `invariant(@Field.0 > 0)`, a form that cannot resolve (`@Field` is not a type, and `data` invariants are NYI, [#686](https://github.com/aallan/vera/issues/686)) — now points at the documented refinement-type workaround (`type Positive = { @Int | @Int.0 > 0 };`).  Finally, the missing-contract-block error `E001` moved from §5.4 "Contract Clauses" (which states the `requires`/`ensures` rule but not the `effects` one) to §5.2 "Function Declaration Syntax" — the one section that shows the *complete* mandatory block, matching the diagnostic's own `fix` and the sibling function-structure errors (`E1xx` in the checker) that already cite it; its three rendered mirrors (`README.md`, `docs/index.html`, `spec/00-introduction.md`) moved in lockstep, pinned by the `TestErrorDisplaySync` suite.

- **Four `error_code` collisions corrected** ([#682](https://github.com/aallan/vera/issues/682) review + audit).  Each of `E130`/`E210`/`E320`/`E600` was shared by two *unrelated* diagnostics, so a `--json` consumer keying on `error_code` would mislabel one of each pair: the slot-resolution `E130` (`expressions.py`) was reused by the "Decimal takes no type arguments" type-application error (`resolution.py`), now **`E134` "Type does not take type arguments"**; the unknown-constructor `E210` (`calls.py`) by the empty-`Tuple()` error, now **`E216` "Empty tuple type"**; the unknown-constructor-in-pattern `E320` (`control.py`) by the empty-tuple-*pattern* error, now **`E323` "Empty tuple pattern"**; and the "unsupported parameter type" `E600` (`functions.py`) by codegen's "refinement base resolves to another refinement" error (`contracts.py`), now **`E618` "Nested refinement base unsupported"**.  Each stable code now maps to exactly one diagnostic concept; the new codes are registered in `ERROR_CODES`, and the three reachable ones (`E134`, `E216`, `E618`) are pinned by collision-regression tests (`E323`'s site is parser-unreachable).

- **Five further semantically-wrong `spec_ref`s corrected (exhaustive audit)** ([#682](https://github.com/aallan/vera/issues/682)).  A final multi-agent audit of *every* diagnostic — each finding adversarially verified — caught five more of the *resolves-but-wrong-section* class the validity gate cannot detect (it checks resolution, not appropriateness): `E122` (pure violation) and `E002` (missing `effects()`) cited effect-chapter sections describing *how* effects work, not the rules they enforce, and now both cite §5.5 "Effect Declaration", which states "Every function MUST declare its effects" and "A function that declares `effects(pure)` MUST NOT perform any effects" verbatim; the alias-*arity* error `E133` cited the slot-namespace §3.8 and moved to §2.6.3 alongside `E132`; the `@Int`→`@Nat` narrowing obligation `E503` cited §4.7 "Let Bindings" (it fires far beyond `let`) and now cites §2.2.1 "`Int` and `Nat` compatibility", where the obligation is stated; and the contract-testing skip warning `E701` moved from the chapter-level `Chapter 6, "Contracts"` to the dedicated §0.5.6 "Contract-Driven Testing".

- **`spec/04-expressions.md` §4.17 "Typed Holes" gains a worked example** ([#682](https://github.com/aallan/vera/issues/682)).  The section now shows `?` in context (`@Int.0 + ?`), demonstrating it is inferred to type `@Int`, type-checks with `W001`, and is rejected at compile with `E614`.  Validating it exposed that `scripts/check_spec_examples.py`'s check and verify stages treated *warning*-severity diagnostics as failures (unlike the CLI `vera check`, and unlike the verify stage's own error-only filter one line later) — both stages now filter to error severity, so a legitimately-checkable warning-emitting example is validated rather than mis-failed.

### Changed

- **Backfilled 54 under-tagged checker diagnostics** ([#682](https://github.com/aallan/vera/issues/682)).  Every `self._error(...)` site in `vera/checker/` (`calls`, `control`, `expressions`, `core`, `resolution`) — plus partial direct `Diagnostic(...)` constructions in the parser-error factories, the tester, the monomorphizer, and the verifier's obligation-fallback — now carries a concrete `rationale`, a `fix` template, and a `spec_ref`.  The canonical example from the issue, `vera check` on a function mixing `@Bool.0 + @Int.0` (E140), now emits a `Fix:` paragraph instead of stopping at the rationale; pinned by `tests/test_checker.py::TestErrorCodes::test_E140_carries_a_fix_paragraph_682`.

- **The `TestHostHandleReclamation573` GC-reclamation suite is marked `stress`** ([#738](https://github.com/aallan/vera/issues/738)).  The class compiles and runs full reclamation programs at scale (~minutes locally), so it is now deselected from the default per-PR `pytest` run and runs under `pytest -m stress` (and nightly CI), reclaiming local inner-loop time.

- **CI hardening — CodeRabbit Pro+ configuration** (no compiler or runtime change).  `.coderabbit.yaml` gains three agentic pre-merge checks — *changelog covers public-surface changes*, *spec and implementation move together*, and *diagnostics carry full metadata* — all in `warning` (advisory) mode, so they surface drift during review without blocking merge.  The deterministic floor (mypy, `scripts/check_*.py`, pre-commit, the conformance suite) remains the only hard gate; these checks complement it and target judgment-gaps it cannot cheaply cover (e.g. "the changelog *describes* the change", not merely "a bullet exists").  Also raised the GitHub-Checks ingestion timeout to 15 min so CodeRabbit waits for the full multi-OS test matrix before commenting; added `AGENTS.md` to the Code Guidelines set; linked `aallan/vera-bench` so a downstream-breaking CLI / exit-code / diagnostic-format change here is flagged during review; and extended the `tests/**` review guidance to call out the commutative-operations slot-ordering trap.  The deterministic counterpart for asserts — ruff `flake8-bandit` for assert-as-guard ([#657](https://github.com/aallan/vera/issues/657)) — is tracked as a follow-up; the diagnostic-metadata gate ([#682](https://github.com/aallan/vera/issues/682)) landed later in this same `[Unreleased]` cycle (see above), deterministically gating the rationale/fix/spec_ref fields; its interim advisory check was correspondingly narrowed to "Diagnostics carry an error code".

## [0.0.187] - 2026-06-30

### Fixed

- **Integer-overflow runtime traps now carry a precise `overflow` trap kind** ([#808](https://github.com/aallan/vera/issues/808)).  The [#798](https://github.com/aallan/vera/issues/798) `@Int` / `@Nat` arithmetic-overflow guard trapped via a bare `unreachable`, so a dynamic overflow was classified `kind="unreachable"` and surfaced the generic non-exhaustive-`match` Fix paragraph — indistinguishable from an unrelated `unreachable`.  The guard now calls a new `vera.overflow_trap` host import (mirroring `vera.contract_fail`) immediately before its `unreachable`; that signals an out-of-band channel `_classify_trap` checks *before* the `str(exc)` substring scan (which would otherwise match the trailing `unreachable` first), so the trap classifies `kind="overflow"` and carries the overflow Fix paragraph (the i64 / u64 range and the `requires(...)` remediation).  Both runtimes provide the new import — the wasmtime host in `execute()` and the browser/Node `runtime.mjs`, whose dynamic import builder now binds `vera.overflow_trap` so a `--target browser` bundle still instantiates and surfaces "Integer overflow" rather than `LinkError`-ing on any arithmetic.  The `@Nat`-subtraction underflow ([#520](https://github.com/aallan/vera/issues/520)) and `@Nat` → `@Int` widen ([#813](https://github.com/aallan/vera/issues/813)) guards are unchanged and still classify `unreachable` — `tests/test_int_overflow_codegen.py::TestOverflowTrapKind808` pins the new `overflow` classification (all five `+` / `-` / `*` sites, both `int_mul` trap branches, plus the lifted-closure and `ensures(...)`-postcondition paths) and those two unchanged controls, and `tests/test_browser.py` pins the wasmtime↔Node parity.

- **Host-import / allocation flags used only inside a `requires(...)` / `ensures(...)` contract are now declared** ([#823](https://github.com/aallan/vera/issues/823)).  A builtin or allocation reachable only through a contract predicate had its host-import / memory / GC flag set too late for module assembly, so the import / `(memory …)` / `$gc_sp` declaration was omitted and the orphaned `call`/`global.get` failed WAT compilation (`unknown func: $vera.sin`, `unknown func: $vera.regex_match`, `unknown global: $gc_sp`, …).  Surfaced while wiring #808's `vera.overflow_trap`.  Two fixes, by mechanism: (1) the per-function flag merge now runs *after* `_compile_postconditions`, covering every context-tracked family (`sin`/`cos`/`pow` math, `json_*`, `html_*`, `map_*`/`set_*`, `decimal_*`, `http_*`, `inference_*`, `random_*`) plus `$alloc`/`$gc_sp` and `vera.overflow_trap`; (2) the body host-import pre-scan now also walks the contract predicates, covering `md_*` and `regex_*`, which are registered *only* by that scan (they have no per-function set-site).  Both changes are purely additive — nothing between the old and new positions reads these flags (they are consumed only at module assembly).  Pinned by `tests/test_codegen.py::TestPostconditionHostImportPropagation823` (math, allocation, regex, and Markdown builtins each in a postcondition over a scalar body).

### Changed

- **Documentation consistency pass** (no code or behaviour change).
  - `HISTORY.md` restructured: the language-server stage (Stage 14) now ends at v0.0.170 and the verifier-soundness campaign is its own Stage 15; entries trimmed to one-liners with whole-sentence bold removed.
  - Removed the redundant defensive `nat_to_int(@Nat.0)` from three `spec/09-standard-library.md` reference examples — `@Nat <: @Int` is a first-class subtyping coercion (spec §2.2.1, "use a `@Nat` anywhere `@Int` is expected — no `nat_to_int` call"), so the explicit conversion was unnecessary.
  - Corrected stale counts and references across `vera/README.md`, `AGENTS.md`, `SKILL.md`, and `examples/README.md`: error-code range (`E001`–`E702`), ERROR_CODES count (123), test / conformance / example counts (5,442 / 103 / 35), the missing spec Chapter 8 row in SKILL's reference table, and the `examples/README.md` index completed to list all 35 examples (including the interactive `read_char` / `inference` ones, flagged for their stdin / API-key needs).
  - Synced the `KNOWN_ISSUES.md` Bugs table with the issue tracker's `bug` labels: added [#775](https://github.com/aallan/vera/issues/775), labelled [#766](https://github.com/aallan/vera/issues/766) `bug`, and relabelled [#758](https://github.com/aallan/vera/issues/758) `bug` → `limitation`.

## [0.0.186] - 2026-06-29

### Fixed

- **Widening a `@Nat` above i64.MAX to `@Int` is now sound** ([#813](https://github.com/aallan/vera/issues/813)).  `@Nat` is an unsigned i64 and `@Int` a signed i64 with `@Nat <: @Int`, so a `@Nat` in `(i64.MAX, u64.MAX]` *reinterprets* to a negative `@Int` when widened (`u64.MAX` → `-1`).  The verifier reasoned over the mathematical non-negative value and could therefore *prove* a false Tier-1 postcondition — e.g. `fn widen(@Nat -> @Int) ensures(@Int.result >= 0) { @Nat.0 }` verified, yet `vera run widen(18446744073709551615)` returned `-1`.  A completeness audit found this coercion happens at *ten* sites, not one.  The verifier now emits a `nat_to_int_coerce` obligation (**E530**) at every `@Nat → @Int` coercion: provably `<= i64.MAX` → Tier 1; provably out of range → a loud E530 error; otherwise Tier-3.  Code generation emits a runtime trap where it can statically determine the source is `@Nat` — at the **return**, **`let`**, **call-argument**, concrete **`@Int` constructor-field**, **ADT sub-pattern** extraction, **match-binding**, the explicit **`nat_to_int`** built-in, and **heterogeneous `if`/`match` arms whose alternative is a non-negative literal** sites — so those programs trap instead of silently returning the reinterpreted value (the guard fires only for a `@Nat` source, never a genuine `@Int`, which may be legitimately negative).  At the component-coercion sites code generation cannot yet guard — tuple construction/destructure, array-literal element, and a generic-instantiated `@Int` field (e.g. `Some(@Nat.0)` into `Option<Int>`) — the widening is **disclosed** as an unguarded **E531** warning rather than a silent false Tier-1.  A verifier↔codegen differential (`tests/test_int_widening_differential.py`) pins the contract — every codegen-guarded site traps on `u64.MAX`, every E531 site does not — and `tests/conformance/ch04_nat_int_widening.vera` exercises the Tier-1 discharge.  The remaining deferred sites — effect-op argument, closures, the E531-disclosed component coercions, and heterogeneous `if`/`match` arms whose alternative is a genuine `@Int` *slot* (not a literal) — share one architectural blocker: code generation has no per-component target-type metadata, the same gap that defers the `@Int → @Nat` narrowing duals ([#754](https://github.com/aallan/vera/issues/754), [#757](https://github.com/aallan/vera/issues/757), [#758](https://github.com/aallan/vera/issues/758)).  They are tracked in [#820](https://github.com/aallan/vera/issues/820).

### Added

- **Documentation examples are gated against built-in redefinition** ([#819](https://github.com/aallan/vera/issues/819)).  `scripts/check_doc_builtin_shadowing.py` (run in CI) verifies that no example in the spec, README, SKILL, FAQ, or other docs redefines an opaque verifier-modelled built-in — the E151 soundness rule shipped in v0.0.185 — so a doc snippet can never reintroduce the `verify`↔`run` divergence the rule closes.

## [0.0.185] - 2026-06-28

### Fixed

- **Redefining a built-in function is now a checker error (E151)** ([#815](https://github.com/aallan/vera/issues/815)).  A top-level, module, or `where`-block `fn` whose name matches an opaque, verifier-modelled built-in (`abs`, `min`, `max`, `clamp`, `to_string`, `string_*`, `array_*`, `parse_*`, …) is rejected at `vera check` — including a function defined in an *imported* module, surfaced in the importer.  This closes a soundness hole: `vera/smt.py` models the core numeric built-ins *by name* before consulting the user's definition, so a shadowing `fn abs` let `vera verify` reason with the built-in's idealized model (`abs(5) = 5`) while code generation ran the user's body — `verify` *proved* `ensures(@Int.result >= 0)` that `run` then *violated* at runtime, with no runtime guard.  Per DESIGN.md "one canonical form" + fail-loud, the checker disallows the redefinition rather than having the verifier silently defer to the user body; the diagnostic is instructional — it names the rule, the verifier↔runtime soundness reason, and the fix (call the built-in directly, no import needed, or choose a distinct name).  The Option/Result/Json/Html *combinators* the prelude injects (`option_map`, `option_and_then`, `option_unwrap_or`, `result_map`, `result_unwrap_or`, `json_*`, `html_attr`) are **exempt**: they are ordinary Vera functions, so a user override is sound (verifier and codegen use the same body) and remains supported.  The shipped example, conformance, and spec programs that previously shadowed `abs` / `max` / `clamp` / `to_string` were renamed to non-built-in names (`magnitude`, `larger`, `clamp_to_range`, `show_bool`).
- **`clamp_to_range` example used the wrong De Bruijn slot mapping** ([#815](https://github.com/aallan/vera/issues/815)).  The `clamp_to_range(value, lo, hi)` example clamped its *third* argument into `[second, first]`: the precondition `requires(@Int.1 <= @Int.2)` constrained `lo ≤ value` rather than `lo ≤ hi`, so a legitimate call with `value < lo` (e.g. `clamp_to_range(0, 1, 10)`) was wrongly rejected by the precondition.  The body — `max(lo, min(hi, value))` — is symmetric in the two bounds, so the bug stayed invisible for symmetric inputs.  Corrected the slots to `value = @Int.2`, `lo = @Int.1`, `hi = @Int.0` consistently across `spec/08-modules.md`, `examples/modules.vera`, `tests/conformance/ch06_ensures.vera`, `SKILL.md`, and `DE_BRUIJN.md`.
- **E151 no longer cascades into the enclosing function body** ([#815](https://github.com/aallan/vera/issues/815)).  When a `where`-helper redefining a built-in was rejected (and stripped from registration), the parent function's body could still resolve a call to that helper against the canonical built-in and emit a bogus secondary arity/type error after the E151.  The nested rejection now propagates to the enclosing declaration, so its body is skipped in the check phase and the E151 is the only diagnostic.

## [0.0.184] - 2026-06-28

### Added

- **Chapter 8 (modules) conformance programs** — the last spec chapter without `chNN_*.vera` coverage now has eight ([#679](https://github.com/aallan/vera/issues/679)): module declaration (§8.2), wildcard (§8.3.1) and selective (§8.3.2) imports, `public` / `private` visibility (§8.4), shadowing (§8.5.2), module-qualified calls (§8.5.3), and circular-import detection (§8.6.3).  Two are **negative tests** that assert a specific diagnostic is emitted, enabled by new `expected_error` support in the conformance harness (`scripts/check_conformance.py` + `tests/test_conformance.py`): a manifest entry may declare it must fail `check` with a given E-code.

### Fixed

- **Codegen soundness: a module-qualified call now bypasses a local shadow** ([#814](https://github.com/aallan/vera/issues/814)).  Per spec §8.5.3 a qualified call `m::f` resolves to the module's function even when a local `f` shadows the bare name.  Codegen desugared `ModuleCall` to a bare `FnCall`, **dropping the module path**, so `m::f` wrongly invoked the shadowing local — while the verifier resolved it correctly to the module's function via its per-module registry.  The result was an unsound verifier↔runtime desync: `vera verify` proved a postcondition against the module's contract that the runtime then violated (`verify` OK, `run` traps).  Shadowed module functions are now also emitted under a distinct, collision-free `mod$…` WASM name (the `$` separator is illegal in Vera identifiers, like the monomorphizer's `name$T`), and qualified calls resolve to it via a `(module path, name) → target` table; bare calls still resolve to the local (§8.5.2, unchanged).  A bare *intra-module* call inside a qualified-reached body is likewise redirected to the module's sibling rather than a local shadow of that name, so the fix holds one level deep.  Pinned by a verifier↔codegen differential.  Surfaced while adding the Chapter 8 conformance coverage above; `ch08_module_qualified_call` and `ch08_shadowing` are now written to the distinguishing spec behaviour.
- **Module-resolution diagnostics now carry stable E-codes** ([#679](https://github.com/aallan/vera/issues/679)).  Every Vera *error* diagnostic carries an E001–E702 code (DESIGN.md; warnings use the W-series), but the resolver/visibility errors emitted none.  Added **E011** (circular import detected), **E012** (cannot resolve import — no file found), **E013** (error parsing an imported module), and **E150** (cannot import a private declaration).  Each also carries the full diagnostic contract — `location.file`, `source_line`, `rationale`, `fix`, and a Chapter 8 `spec_ref` — so that `vera check --json` exposes module-resolution failures with the same field set as every other diagnostic (`Diagnostic.to_dict()` omits empty fields, so a bare diagnostic would otherwise drift from the documented schema).  The resolver's parse/transform error handling is also narrowed from a blanket `except Exception` to the documented resolution failures (`ParseError`, `TransformError`, `OSError`, `UnicodeDecodeError`), so an internal compiler bug propagates as a crash rather than being silently relabeled E013 "Error parsing imported module" and blamed on the user's file.  The visibility diagnostics now also cite the correct spec section: **E150** (private import), **E232** (private module-qualified call), and the missing-visibility error were repointed from the stale `Chapter 5, Section 5.8 "Function Visibility"` (which no longer exists) to `Chapter 8, Section 8.4 "Visibility"`, where the module-visibility rules now live.

## [0.0.183] - 2026-06-27

### Added

- **Tier-1 verification modeling for the modelable `@Float64` builtins `float_clamp`, `int_to_float`, and `float_to_int`** (follow-up to [#797](https://github.com/aallan/vera/issues/797), the [#392](https://github.com/aallan/vera/issues/392) `smt.py` soundness audit's `@Float64` → FloatingPoint-sort fix; the three were left as Tier-3 deferrals there).  `float_clamp(v, lo, hi)` is modeled unconditionally as the faithful WASM `f64.min(f64.max(v, lo), hi)`: Z3's own `fp.min` / `fp.max` *diverge* from WASM on `NaN` (SMT-LIB returns the non-`NaN` operand; WASM propagates `NaN`) and on signed zero, so the model builds those semantics explicitly — a naive `fpMin` / `fpMax` would unsoundly prove `!float_is_nan(float_clamp(NaN, …))`.  `int_to_float` and `float_to_int` cross the Int↔Float boundary, where Z3's *symbolic* Int↔Real↔FP reasoning is unreliable (it returns spurious counterexamples that do not satisfy their own constraints), so they are modeled at Tier 1 **only for a concrete (constant-foldable) argument** and a symbolic one defers to a sound **Tier 3** — matching the audit principle of deferring what Z3 cannot soundly model.  `float_to_int` compiles to `i64.trunc_f64_s`, which traps on `NaN` / `±Inf` / out-of-`i64`-range, so a concrete out-of-domain argument is now a loud compile error (**E529**) and a symbolic one a runtime-guarded Tier-3 trap.  Each model is confirmed by a verify-vs-run differential.  The four format/parse `@Float64` builtins (`float_to_string`, `parse_float64`, `decimal_from_float`, `decimal_to_float`) remain Tier-3 by necessity.  ([#807](https://github.com/aallan/vera/issues/807))

### Fixed

- **Out-of-range integer literals are now a clean compile error (E149) instead of an opaque codegen failure or a silent unsoundness** (found during the #807 review).  An integer literal must fit its target machine type — `@Int` is i64, `@Nat` is u64.  Previously a literal `>= 2^64` was accepted by `vera check` then failed at codegen with a raw `i64.const ... out of range` error; worse, a literal in (i64.MAX, u64.MAX] used as `@Int` (e.g. `18446744073709551615`) made `vera verify` **prove** `ensures(@Int.result == 18446744073709551615)` while the runtime returned `-1` (the i64 reinterpretation of the all-ones bit pattern) — a Tier-1 proof the runtime violates.  The checker now range-checks every integer literal against its target type's bound and emits **E149** with a clear message; the asymmetric edge `-9223372036854775808` (i64.MIN) stays valid.  ([#812](https://github.com/aallan/vera/issues/812))

## [0.0.182] - 2026-06-27

### Fixed

- **Verifier soundness: `string_length` no longer proves a false length for non-ASCII strings** (final sub-task of the [#392](https://github.com/aallan/vera/issues/392) `smt.py` soundness audit).  `vera verify` translated `string_length` to Z3's `Length`, which counts Unicode **code points**, but Vera's runtime counts **UTF-8 bytes** — so `ensures(string_length("é") == 1)` verified at Tier 1 while the runtime returns `2`.  `string_length` is now modeled at Tier 1 only for a string **literal** (whose exact byte length is known); on any non-literal argument it defers to a runtime-guarded **Tier 3** obligation (Z3's string theory has no byte-length operator), matching the numeric-cast / quantifier / decimal precedent.  The boolean predicates `string_contains` / `string_starts_with` / `string_ends_with` stay Tier 1 — UTF-8 is self-synchronizing, so a valid substring / prefix / suffix matches at the byte level exactly when it matches at the code-point level — with one further guard: a string literal containing a code point above Z3's string-sort alphabet (> U+2FFFF), or a lone surrogate (U+D800–U+DFFF) with no UTF-8 encoding, also defers to Tier 3 — the Z3 Python binding would otherwise store it as escape text and let the predicates match phantom bytes (and a surrogate has no UTF-8 byte length to take).  This was the last of the six soundness bugs ([#797](https://github.com/aallan/vera/issues/797)–[#802](https://github.com/aallan/vera/issues/802)) found by the [#392](https://github.com/aallan/vera/issues/392) audit, which is now complete.  ([#802](https://github.com/aallan/vera/issues/802))

## [0.0.181] - 2026-06-27

### Fixed

- **Verifier + codegen soundness: `@Int` / `@Nat` arithmetic overflow now traps** (batch 4 of the [#392](https://github.com/aallan/vera/issues/392) `smt.py` soundness audit).  The verifier modelled `@Int` / `@Nat` as Z3's *unbounded* integers, so `+` / `-` / `*` were total — it proved postconditions the i64 / u64 runtime then violated under two's-complement wraparound: `ensures(@Int.result > @Int.0)` for `{ @Int.0 + 1 }` verified at Tier 1, yet `inc(i64.MAX)` wraps to `i64.MIN`.  `@Int` / `@Nat` `+` / `-` / `*` are now *partial* operations that **trap** on overflow at runtime (consistent with `@Nat` underflow and signed-division `MIN / -1`), and the verifier auto-synthesises an `int_overflow` obligation that the result stays in range — a two-check mirroring array bounds: provably in range → Tier 1; provably out of range (e.g. a literal `u64.MAX + 1`, or `@Int.0 + 1` under `requires(@Int.0 == i64.MAX)`) → a compile error (**E528**); dynamic operands → a runtime-guarded Tier-3 overflow trap.  Overflow is classified at the operands' **common (coerced) type** (`@Int` if either operand is `@Int`), so a literal-left `5 + @Int.0` and an `@Int + 1` narrowed into a `@Nat` slot are both i64 adds — not the literal's `@Nat` self-type nor the narrowed result.  `@Nat` subtraction stays the existing underflow obligation (E502).  The runtime overflow trap is currently the generic `unreachable` kind; a precise `overflow` kind is tracked as [#808](https://github.com/aallan/vera/issues/808).  ([#798](https://github.com/aallan/vera/issues/798))

## [0.0.180] - 2026-06-26

### Fixed

- **Verifier soundness: `@Float64` contracts use Z3's IEEE-754 FloatingPoint sort** (batch 3 of the [#392](https://github.com/aallan/vera/issues/392) `smt.py` soundness audit).  The verifier modelled `@Float64` as Z3 `Real` — exact, unbounded, no `NaN`/`Inf` — so it proved postconditions the runtime then rejected: `ensures(@Float64.result > @Float64.0)` for `{ @Float64.0 + 1.0 }` verified at Tier 1, yet `inc(2^53)` traps (the ULP is 2, so `x + 1.0` rounds back to `x`), as do `+Inf` and `NaN`.  `@Float64` now maps to `z3.FPSort(11, 53)` (double) with round-nearest-ties-to-even, and `==`/`!=` lower to IEEE `fpEQ`/`fpNEQ` (`NaN != NaN`, `+0.0 == -0.0`) rather than structural equality.  Float64 `%` is modelled as the runtime's truncated remainder (`a - trunc(a/b)*b`, C `fmod`) rather than Z3's `fp.rem`, which had proved e.g. `5.0 % 3.0 == -1.0` where the runtime gives `2.0`.  The unsound relational / reflexive contracts above are no longer proved — Z3 finds the IEEE counterexample — while genuinely-sound contracts (e.g. guarded by `requires(!float_is_nan(...))`) still verify at Tier 1.  As a bonus, `float_is_nan` / `float_is_infinite` and the `nan()` / `infinity()` constants — uninterpreted (Tier 3) under the Real model — now translate to `fpIsNaN` / `fpIsInf` / FP literals and discharge statically.  Mixed `@Float64`/`@Int` ordering comparisons (`@Float64 < @Int`) — previously accepted via Z3's silent `Real` coercion — are now a clean `E142` type error, consistent with the arithmetic (`E141`) and equality (`E142`) arms.  FP solving returns `unknown` more often than linear real arithmetic, so some `@Float64` predicates fall (honestly) to Tier 3 ([#797](https://github.com/aallan/vera/issues/797)).

## [0.0.179] - 2026-06-26

### Fixed

- **Verifier soundness: signed division/modulo, body asserts, and contract-position divisions** (batch 1 of the [#392](https://github.com/aallan/vera/issues/392) `smt.py` soundness audit).  Three cases where `vera verify` proved a contract the runtime then rejected — each an implementation-vs-spec divergence the audit surfaced:
  - The verifier translated signed `/` and `%` to Z3's **Euclidean** `div`/`mod`, proving `(-7) / 2 == -4` and `(-7) % 2 == 1`, while the runtime (`i64.div_s` / `i64.rem_s`, truncating toward zero) computes `-3` and `-1`.  `smt.py` now uses a sign-aware truncated encoding for integer operands ([#799](https://github.com/aallan/vera/issues/799)).
  - A body `assert(P)` was ignored by the verifier — a provably-false assertion passed `vera verify` — contradicting spec §6.2.5 ("the compiler verifies this holds").  It now carries a Tier-1 obligation discharged by a two-check: prove `P` → verified; else prove `¬P` (always false) → the new loud `E507`; else Tier 3, guarded at runtime by the §11.14.1 `unreachable` trap ([#800](https://github.com/aallan/vera/issues/800)).
  - The divide-by-zero obligation ([#680](https://github.com/aallan/vera/issues/680)) covered only body divisions; a division in a `requires`/`ensures` predicate — evaluated eagerly, with no short-circuit — was silently proved.  The primitive-op walk now runs over contract predicates too, so an unguarded contract divisor that can be zero is a loud `E526` instead of a runtime trap on a "verified" program ([#801](https://github.com/aallan/vera/issues/801)).
  - Adds the `E507` diagnostic ("Assertion verified false") and the `assert` obligation kind.  The audit's three larger findings — Float64-as-Real ([#797](https://github.com/aallan/vera/issues/797)), integer overflow ([#798](https://github.com/aallan/vera/issues/798)), and `string_length` byte-vs-codepoint ([#802](https://github.com/aallan/vera/issues/802)) — remain open.
- **Verifier completeness: body `assert`/`assume` facts discharge later obligations** (batch 2 of the [#392](https://github.com/aallan/vera/issues/392) audit — the assume-half of [#800](https://github.com/aallan/vera/issues/800)).  [#800](https://github.com/aallan/vera/issues/800) made a body `assert(P)` carry a Tier-1 *proof* obligation; this adds the *assume* half of the weakest-precondition rule (spec §6.4.1: `assert(P) | P && WP(rest)`, `assume(P) | P ==> WP(rest)`).  After the statement, `P` joins the assumption context for every subsequent obligation in its block (including a later call's precondition, threaded through `SmtContext._translate_block`) — and, for a top-level `assert`/`assume`, for the postcondition and a refined return.  This moves obligations from Tier 3 to Tier 1 and removes spurious `E503` / `E500` / `E505` errors where a prior assert provably guards the site: `{ assert(@Int.0 >= 0); let @Nat = @Int.0; ... }` no longer false-errors, since the §11.14.1 runtime trap makes the negative witness unreachable.  Sound by construction — the fact is assumed only *forward* (never discharging an earlier obligation) and only *within its branch* (never leaking to a sibling), both pinned by regression guards ([#804](https://github.com/aallan/vera/issues/804)).

## [0.0.178] - 2026-06-25

### Added

- **Compiler self-introspection: `vera builtins/effects/errors --json`** ([#539](https://github.com/aallan/vera/issues/539)).  Three new CLI subcommands enumerate the compiler's own registries as a machine-readable `{schema, items}` envelope (or an aligned text table without `--json`): `builtins` (the built-in functions), `effects` (the effects *and* abilities, `kind`-tagged, including the parameterised `Exn<T>` exception effect), and `errors` (the diagnostic codes `E001`–`E702` and the `W001` warning, with their compiler phase).  Each item carries a best-effort `since` version — built-ins, effects, and abilities are git-attributed and full-coverage-gated by a test; error codes report `null` — and the published counts are now **registry differentials**: a test pins `len(items) == len(registry)`, so a hand-maintained claim like "164 built-in functions" can no longer silently drift from the compiler.  New `vera/introspect.py` (pure payload builders) + `vera/_since.py` (the version map); thin `cmd_*` glue in `cli.py`.  The toolchain is documented end-to-end in the new [`TOOLCHAIN.md`](TOOLCHAIN.md) cookbook.  Surfaced and motivated by the doc-consolidation brief ([#528](https://github.com/aallan/vera/issues/528)).
- **Characterization harness for `execute()`** ([#734](https://github.com/aallan/vera/issues/734)).  A consolidated, mutation-validated test harness (`tests/test_execute_characterization.py`) pinning the observable contract of `execute()` (`vera/codegen/api.py`) — every `ExecuteResult` field (`value` int/float/str/heap-pointer/None, `stdout`, `state`, `exit_code`, `stderr`) crossed with the three completion modes (normal return; WASM trap, which *raises* `WasmTrapError` rather than returning a result; interrupt/exit), plus the positional-constructor compatibility shape and `capture_stderr` True-vs-default.  This is the green gate for the upcoming `vera/runtime/` decomposition ([#421](https://github.com/aallan/vera/issues/421)): every host-binding extraction must keep it passing.  Each cell was confirmed to flip RED when its target return path in `api.py` is deliberately broken (9 mutations), so none is green-for-the-wrong-reason.  Test-only; no behaviour change.
- **Mutation-testing infrastructure and soundness-core baseline** ([#387](https://github.com/aallan/vera/issues/387)).  Adds `mutmut` as a `[mutation]` extra (chosen over `cosmic-ray` after a Phase-0 spike — its coverage-guided test selection is ~90× faster on this Z3/WASM suite, and its copy-based model never mutates the working tree in place), a `[tool.mutmut]` config wiring the **in-process unit-test oracle** (the subprocess suites `test_conformance` / `test_cli` / `test_browser` import the un-mutated package, so they cannot kill mutants), and a runbook (`MUTATION.md`) covering the tool decision, the oracle caveat, resume-after-hard-kill, the Z3-flakiness guardrail, and the equivalent-mutant `# pragma: no mutate` convention.  The first sweep measures the **soundness core** (`verifier.py`, `smt.py`, `checker/`, `obligations/` — 10,620 mutants): **80.8% caught**, 2,038 survivors, emitted by `scripts/mutation_report.py` as a diff-able per-module `mutation-summary.csv` and a README mutation-score badge (`mutation.json`), with the full survivor inventory and per-module chart attached to the tracking issue.  The whole-`vera/` sweep is deferred behind [#421](https://github.com/aallan/vera/issues/421): `codegen/api.py`'s ~3,500-line `execute()` inflates a 774 MB mutant file mutmut cannot index.  Tooling and tests only — no behaviour change; the issue stays open for soundness-core triage and per-module follow-ups.
- **Soundness-core test hardening (Phase 3 of [#387](https://github.com/aallan/vera/issues/387))**.  Strengthens type-checker tests in the call-, control-, and expression-checking core (`vera/checker/calls.py`, `vera/checker/control.py`, `vera/checker/core.py`, `vera/checker/expressions.py`) that stayed *green* when behaviour was deliberately broken under mutation: each hardened or added test now asserts a **specific observable** — the stable diagnostic `error_code` (call/constructor `E203`–`E204`, `E210`–`E215`, `E220`, `E230`–`E233`, `E240`–`E241`; match-arm `E302`; exhaustiveness `E310`–`E313`; pattern `E320`–`E322`; handler `E330`–`E335` plus body-type `E121`; data-invariant `E120`; expression `E146`, `E160`–`E161`, `E173`–`E175`), the diagnostic `severity`, or a type-propagation result — rather than free-text message prose.  Test-only; no behaviour change.  The tracking issue stays open for the whole-corpus sweep gated on [#421](https://github.com/aallan/vera/issues/421).
- **Soundness-core test hardening — contract-verifier discharge / report / projection layer ([#387](https://github.com/aallan/vera/issues/387))**.  Extends the Phase 3 campaign from the type checker to `verifier.py`.  `TestObligationRecordCompleteness387` + `TestProjectionHelpers387` (38 tests) pin the obligation-record and diagnostic-content surface that the coarse `_verify_err` / `_verify_ok` helpers left unasserted — every `ProofObligation` field (`fn_name`, `error_code`, `counterexample`, `expr_text`) and the `_report_*` diagnostic content (`description` / `rationale` / `fix` / `spec_ref`, plus the `"modulo"`-vs-`"division"` branch) for `div_zero` (E526), `nat_sub` (E502), `index_bounds` (E527), `nat_bind` (E503), `refine_bind` (E505), and postcondition (E500) sites — plus the `@Float64`-divisor exemption guard and the verified / tier3 `summary` counter bookkeeping (`tier1_verified` / `tier3_runtime` / `total`) via exact-count assertions.  It also pins the De Bruijn projection / term helpers (`_obligate_subpattern_narrowings`, `_obligate_destructure_narrowings`, `_check_nat_binding_obligation_term`, `_check_refined_binding_obligation_term`) via ADT sub-pattern (`Some(@Nat)` / `Some(@PosInt)` on `Option<Int>`) and tuple-destructure narrowings — including a projection-index soundness differential where a mutated De Bruijn accessor would mis-source the obligation — and adds counterexample-line assertions to the `_report_*` builders, and pins the typing-predicate soundness gates (`_is_nat_typed`, `_narrows_into_nat`, `_narrows_into_refined`, `_has_underflow_leaf`, `_meet_status`) that decide whether an obligation fires.  `TestSmtTranslation387` (26 tests) adds differential regression coverage of the `smt.py` Z3-translation layer (translate / check / counterexample, via the call-precondition and Tier-1 levers; kills 8 of its 152 survivors, with the deeper translate / sort layer tracked in [#792](https://github.com/aallan/vera/issues/792)).  Test-only; no behaviour change.  The soundness surface (obligation fields, branch guards, codes, projection indices, tier3 routing) is pinned with `==` / exact assertions; the documented residual is message-cosmetic string-wrap mutations in the pure `_report_*` builders and per-branch reporting-counter bookkeeping.  With this batch, [#387](https://github.com/aallan/vera/issues/387)'s soundness-core scope is complete — the core mutation score rises from the 80.8% baseline to **83.3% caught**; the remaining whole-`vera/` sweep, gated on making the full marathon sweep reliable (the 10,620-mutant run deadlocks on `mutmut` 3.6 / Python 3.14), moves to [#795](https://github.com/aallan/vera/issues/795), and the deep `smt.py` translate-layer / verifier-aggregation hardening plus the verifier timeout probe to [#792](https://github.com/aallan/vera/issues/792).

### Changed

- **`codegen/api.py` decomposition ([#421](https://github.com/aallan/vera/issues/421))**.  Splitting the 4,358-line public-API module into focused units, one layer per commit behind the #734 characterization harness.  So far: (1) the pure ADT memory-layout utilities (`ConstructorLayout`, `_wasm_type_size` / `_wasm_type_align` / `_align_up`) → a focused `vera/codegen/memory.py`; (2) runtime-trap classification and source-backtrace resolution (`WasmTrapError`, `TrapFrame`, `_classify_trap`, `_resolve_trap_frames`) → a new `vera/runtime/` package (`traps.py`); (3) the WASM heap-marshalling layer — memory read/write, GC shadow-rooting, the Map/Set bucket codec, the #578 wrap-handle range guard (`_validate_wrap_handle`), and Result/Option/Array allocation, all parameterised by the `wasmtime.Caller` — → `vera/runtime/heap.py`.  The public surface is unchanged — `from vera.codegen import ConstructorLayout` and `from vera.codegen.api import compile` / `execute` / `CompileResult` / `ExecuteResult` / `WasmTrapError` all still resolve.  (4) the twelve optional effect families are factored into per-effect `vera/runtime/<family>.py` modules, each a `register_<family>(linker, ...)` function (`random`, `math`, `md`, `json`, `regex`, `html`, `map`, `set`, `decimal`, `http`, `inference`, `state`; the value-type dispatch table lives in `vera/runtime/collections.py`, the LLM provider registry + HTTP call helper in `vera/runtime/inference.py`, and the shared request timeout (`_HTTP_TIMEOUT`) in `vera/runtime/http.py`, and Decimal's value store is created in `execute()` so the shared GC decref hook can close over it), and the shared collection-marshalling helpers (`_read_i32`/`_write_f64` plus the Option/Array allocation wrappers used by Map/Set/Decimal/JSON/HTML) joined `vera/runtime/heap.py`.  api.py is down from 4,358 to 1,182 lines (−73%).  The always-on IO family intentionally stays inline in `execute()` — it is execute()'s *observation channel* (its `output_buf`/`stderr_buf`/`last_violation` become `ExecuteResult` fields), not a pluggable adapter, so extracting it would relocate rather than reduce coupling; the boundary rationale is documented in `vera/README.md` ("Host-binding families").  Lifting the runtime out of the public-API module also unblocks the deferred whole-`vera/` mutation sweep ([#387](https://github.com/aallan/vera/issues/387)).  Internal refactor; no behaviour change.

### Security

- **`Http` rejects non-HTTP(S) URLs** ([#789](https://github.com/aallan/vera/pull/789)).  `Http.get` and `Http.post` now validate the URL scheme and return `Result.Err` for `file://`, `ftp://`, `data:`, and any other non-`http(s)` scheme, instead of handing it to `urllib.request.urlopen` — which would otherwise read local files or speak arbitrary protocols on behalf of a Vera program.  Surfaced by CodeRabbit's review of the #421 decomposition.

### Dependencies

- Bump `actions/checkout` from 6 to 7 ([#780](https://github.com/aallan/vera/pull/780)).
- Bump `ruff` from 0.15.17 to 0.15.18 ([#781](https://github.com/aallan/vera/pull/781)).
- Bump `pytest` from 9.1.0 to 9.1.1 ([#782](https://github.com/aallan/vera/pull/782)).

## [0.0.177] - 2026-06-21

### Added

- **Auto-synthesised safety obligations for trapping primitive operations** ([#680](https://github.com/aallan/vera/issues/680)).  The verifier now emits a Tier-1 proof obligation at every integer division and modulo (`b != 0`, `E526`) and every array index (`0 <= i < array_length(arr)`, `E527`), discharged from preconditions and path conditions exactly like `@Nat` subtraction underflow (`E502`) and `@Int` → `@Nat` narrowing (`E503`) — closing the **last item in the Tier-0 "silent failures" roadmap section**, where `vera verify` reported a division or index as proven safe and it then trapped at runtime.  Division and modulo are Tier-1-decidable: an unguarded divisor with a zero counterexample is now a compile error (a genuinely undecidable divisor would fall to Tier 3, but integer division is decidable; float division is exempt — `f64.div` by zero yields inf/NaN, not a trap).  Array bounds depend on the uninterpreted `array_length`, so the obligation is tiered honestly — proved at Tier 1 where a literal length, refinement, precondition, or path condition pins it; a compile error (`E527`) where the index provably exceeds a statically-known length (e.g. `[1, 2, 3][5]`); otherwise a runtime-guarded **Tier 3**, counted in `vera verify --json` rather than silently passed.  Lifting dynamic or closure-captured array bounds to a Tier-1 proof is part of the Tier 2 work ([#427](https://github.com/aallan/vera/issues/427)).  The `@Nat` subtraction walker generalises to a single `_walk_for_primitive_op_obligations` pass over operand-trapping sites — direct positions, array-literal elements, assert conditions, interpolated-string parts, let-destructure values, and the standard call / control-flow positions (an op inside a closure / quantifier / handler body stays runtime-guarded, tracked in [#779](https://github.com/aallan/vera/issues/779)); codegen is unchanged — the `divide_by_zero` / `out_of_bounds` runtime traps already existed.

### Changed

- **spec §6.4.3 "Primitive Operation Safety"** rewritten to state that division, modulo, and array-index obligations are now auto-synthesised (previously documented as *not* synthesised and tracked in [#680](https://github.com/aallan/vera/issues/680)); README and FAQ updated to match.  With this, the ROADMAP **Tier 0 — Close the silent failures** section is done: every known case where `vera verify` accepted a program that then did something weaker than promised is closed.

## [0.0.176] - 2026-06-21

### Fixed

- **Call-site precondition checking now covers calls in statement position** ([#730](https://github.com/aallan/vera/issues/730)). A call whose result is discarded — a bare `consume(need_pos(@Nat.0));` statement — was never checked against the callee's `requires(...)`: the SMT body translation skipped `ExprStmt` nodes, so `vera verify` reported clean while the precondition went unchecked (a Tier-0 silent failure, and a breach of DESIGN.md's "contracts are checked at every call site"). Statement-position expressions are now translated for their precondition-checking side effect; the value is discarded and an untranslatable statement (an effect op) is ignored rather than aborting the block's verification. The [#727](https://github.com/aallan/vera/issues/727) identity dedup keeps re-translation duplicate-free. Two boundaries stay out of scope: a call at or after a `let`-destructure in the same block ([#764](https://github.com/aallan/vera/issues/764), a distinct block-truncation root cause) and a call nested inside an effect-operation argument (a separate narrower gap, [#776](https://github.com/aallan/vera/issues/776)).

## [0.0.175] - 2026-06-21

### Added

- **Per-monomorphization static verification of generic functions** ([#732](https://github.com/aallan/vera/issues/732), closing [#555](https://github.com/aallan/vera/issues/555)).  A generic (`forall<T>`) body previously skipped almost all static verification — nearly every non-trivial contract fell to Tier 3 (`E520`) (a concrete refined return was the lone exception, already discharged by the [#746](https://github.com/aallan/vera/issues/746) fast path), a silent Tier-1→Tier-3 downgrade in which an `@Nat - @Nat` underflow or a false `ensures` inside a `forall<T>` function was never checked at verify time.  The verifier now monomorphizes each generic at every concrete instantiation the program uses and runs the **normal** verification pipeline on each clone, so body obligations are discharged — or caught — against real types.  Instantiation discovery is *shared with codegen* (extracted into a codegen-free `vera/monomorphize.py`), so the verifier covers every instantiation codegen emits (a superset is sound — codegen additionally prunes constraint-failing instances via `_check_constraints`/`E613`, which the verifier need not re-check); a differential test pins this coverage, because a *missed* instantiation would be a false Tier-1.  The shared discovery walks function bodies, contract clauses, `where` helpers, and imported constructors, and seeds the monomorphizer context with imported functions' return types, so a generic reachable — or whose type argument is inferable — only through any of those is found by both sides — closing three false Tier-1s and a `CodegenSkip` crash (a generic used solely in a `requires` / `ensures` or a `where` helper now compiles and runs) found in review.  Per-instance results are aggregated one-per-source-site with **meet** semantics — Tier-1 only if every instantiation verifies, otherwise the worst outcome dominates so any reachable counterexample is reported — and a failing diagnostic names the offending instantiation (`In generic function 'dec' instantiated at dec<Bool>: @Nat subtraction may underflow`).  A generic with **no** concrete instantiation in the program still falls to Tier 3 (`E520`, reworded to say so) — there are no types to check it against, and such a generic never executes in-unit — and a concrete refined *return* on an uninstantiated generic is still discharged via the [#746](https://github.com/aallan/vera/issues/746) fast path.  For instantiated generics this supersedes that fast path with the real concrete types (strictly stronger: a `@Float64` instantiation uses the Real sort rather than an integer fallback).  In the warm incremental session, a generic's discharge cache is keyed on its concrete instantiation set — a property of its *callers* — so an edit that adds or removes an instantiation re-verifies the generic instead of replaying a stale per-monomorphization result.

### Fixed

- An `Eq`-constrained generic called with a **parameterized-ADT slot reference** (e.g. `@Box<Int>.0`) is no longer spuriously rejected with `E613`, and the `Eq` auto-derivation now **validates the concrete type arguments**: it splits the parameterized name into base + args, and a type-parameter field's Eq-ness is its type argument's (recursively), not the generic boxed `i64` rep the bare layout records.  So `Box<Int>` derives `Eq` while `Box<String>` (a non-Eq `i32_pair` String field) is correctly rejected — where a naive bare-name strip would have false-accepted it.  (A constructor-inferred type — `eq2(MkBox("a"), …)` — still resolves to the bare ADT name in the monomorphizer, so the validation reaches the slot-ref / parameterized-name path only; the bare-name path's pre-existing type-arg-blindness is unchanged.)  Pre-existing; surfaced in the [#732](https://github.com/aallan/vera/issues/732) review.
- `vera compile --wat` is now **byte-stable across runs**.  The monomorphization worklist sorts each instantiation set, so the order monomorphized clones are emitted no longer varies with `PYTHONHASHSEED` (clone bodies were always identical; only their order differed) — reproducible builds matter for caching and WAT snapshots.  Pre-existing; surfaced in the [#732](https://github.com/aallan/vera/issues/732) review.

## [0.0.174] - 2026-06-19

### Added

- **Verification of refinement-type predicates — static and runtime** ([#746](https://github.com/aallan/vera/issues/746)).  A user refinement type `{ @T | P }` (e.g. `type PosInt = { @Int | @Int.0 > 0 }`) previously parsed but its predicate was never checked — a violating value verified silently (a Tier-0 silent failure).  The verifier now discharges the predicate as a Tier-1 obligation at every site where a value narrows into a refined slot — `let` bindings, call arguments (incl. piped and generic-instantiated formals), constructor fields, tuple **construction** components (a refined component of a `Tuple(...)` built in value position, recovered from the construction site's expected type — `Tuple` is not a user-registered constructor, so it is obligated separately from ADT fields), effect-operation arguments, match bindings, ADT sub-pattern binds, and literal / projected tuple-destructure components — and at a refined **return** position; a refined **parameter** is dually *assumed* to satisfy its predicate in the body (sound precisely because every call site discharges the obligation).  A provably-violating narrowing is an `E505` error with a counterexample; a refinement over a *non-primitive* base (`{ @Array<Int> | array_length(...) > 0 }`, whose collection binder the translator does not lower) or an undecidable predicate is an `E506` Tier-3 warning, never a silent pass.  This generalises the `@Nat` discharge machinery from the baked-in `>= 0` to an arbitrary translated predicate (the forward-promise in the 0.0.172 notes).  **Codegen additionally emits a runtime guard** ([#762](https://github.com/aallan/vera/issues/762)): the predicate is compiled to WebAssembly and checked at every function boundary — a refined parameter at entry, a refined return at exit (via `$vera.contract_fail`) — so even a program compiled *without* `vera verify` traps on a refinement-violating value (including at a `public`/FFI entry point, where an untrusted caller can't bypass the callee's entry guard; a call argument is covered transitively by that guard).  This covers non-primitive (collection) bases too: although a refinement over a non-primitive base such as `{ @Array<Int> | array_length(...) > 0 }` is Tier 3 *statically* (the predicate translator doesn't lower a collection binder, §2.6.4), codegen lowers the predicate directly, so an empty array into a `@NonEmptyArray` parameter traps.  A **tuple with refined / `@Nat` components** (`Tuple<PosInt, Int>`) carries no top-level refinement, so codegen decomposes it at the boundary — loading each component from the heap value and guarding it (recursively, for nested tuples) — at both a parameter (entry) and a return (exit), so an external/FFI caller passing `Tuple(-5, 3)` into a `Tuple<PosInt, Int>` boundary traps on the violating component rather than laundering it past the verifier's component-refinement assumption.  A predicate codegen cannot lower to a guard (e.g. one that calls a generic user function, which has no monomorphised instance at the guard site) now reports a clean `E617` diagnostic at the boundary rather than crashing the compiler.  Further PR-review hardening: a *generic* tuple alias (`type Box<T> = Tuple<T, Int>`) substitutes its type argument when resolving component types, so `Box<PosInt>` still guards its refined component instead of dropping the substitution; a mutually-recursive (infinite) tuple alias **fails closed** with `E617` rather than silently emitting partial guards; nested `RefinedType` components (tuple components, constructor fields) unwrap to their base Z3 *sort* so `Tuple<PosInt, Int>` gets a proper datatype sort rather than degrading to a scalar model; and a refined-ADT sub-pattern bind (`Some(@PosInt)` on `Option<PosInt>`) now carries the field's source-type predicate into the arm body — both to the narrowing walk (a downstream `@Nat` narrowing of the bound payload discharges at Tier-1 instead of a false `E503`) and, via an SMT match-translation hook, to the arm body's call **preconditions** (`Some(@PosInt) -> needs_positive(@PosInt.0)` discharges instead of a false `E501`).  The fact is the field's *source* type, so a genuine `Option<Int>` narrowing stays obligated (`E505`/`E501`), never silently assumed.  Further PR-review hardening: a refined value *returned from a match arm* (`Some(@PosInt) -> @PosInt.0`) discharges its refined-return goal via a global `arm-matched ⇒ source-fact` SMT implication (so the goal, checked after the arm path conditions pop, sees it) — on the generic fast path too; a callee's **alias-base** refined return (`{ @Age | @Age.0 >= 18 }`, `type Age = Nat`) is assumed by the caller through the predicate's binder name (`@Age.0`), not the resolved `Nat`, via a shared `ast.predicate_binder_name`; and a **refinement *over* a tuple** (`type Pair = { @Tuple<PosInt, Int> | true }`) is unwrapped so its refined components are obligated **statically** (the verifier unwraps the refined base at the construction/destructure sites — previously a refined component behind a refinement verified clean, a false Tier-1) and guarded at the boundary too (recursively through `Tuple<Pair, Int>`), with the component guards emitted *before* the enclosing top-level predicate so a refinement that reads its own components sees established values.  And a **nested** constructor sub-pattern narrowing (`Some(Some(@PosInt))` on `Option<Option<Int>>`) is now recursed and obligated **statically** — previously the inner `Int → PosInt` narrowing was neither obligated nor guarded, an *unguarded* false Tier-1; a verified program now rejects a bad nested narrowing (`E505`), with the nested-bind *runtime* guard tracked as a follow-up ([#765](https://github.com/aallan/vera/issues/765)).  A **refined ADT scrutinee** (`match` on a `{ @Option<Int> | P }`) and a **refined tuple source** (a `let`-destructure of a `{ @Tuple<PosInt, Int> | P }`) now unwrap the refined base before reading the constructor's / tuple's type args, so a sub-pattern narrowing is still obligated (no missed false Tier-1) and a component's source fact is still seeded (no false `E505` on a later re-narrowing).

### Changed

- **Internal: hardened the `@Nat` narrowing guard data structures** ([#759](https://github.com/aallan/vera/issues/759)).  Single-sourced the checker→verifier side-table span key through `ast.span_key` (previously hand-rolled at three sites), and added a `ConstructorLayout.__post_init__` assertion that `nat_fields` stays length-aligned with `field_offsets`, so a drifted built-in literal fails loudly at construction rather than as a silently mis-indexed guard.  Also pinned the [#757](https://github.com/aallan/vera/issues/757) generic-instantiated-constructor-field runtime deferral with a codegen test ([#760](https://github.com/aallan/vera/issues/760)).  No behaviour change — internal robustness follow-ups from the #756 review.

## [0.0.173] - 2026-06-17

### Added

- **ruff is now an enforced lint gate** ([#733](https://github.com/aallan/vera/issues/733)).  `ruff check` (default `F`/`E` rules) runs in pre-commit (before mypy) and the CI lint job — ruff shipped in the `[dev]` extras but nothing ran it, so ~175 findings had accumulated silently.  All cleared: 134 auto-fixed (unused imports, placeholder-less f-strings, redefinitions) and 41 hand-triaged (the three dead `skip_langs` sets, unused locals, ambiguous names, `ResolvedModule` string-annotation hoists, semicolon splits, a `type()` comparison).  Bundles the ruff `0.15.16` → `0.15.17` bump ([#741](https://github.com/aallan/vera/pull/741)).
- **The `@Nat >= 0` narrowing invariant is now obligated at every binding site** ([#747](https://github.com/aallan/vera/issues/747)).  #552 covered `let` / call-argument / effect-operation-argument / concrete-constructor-field / match-bind / literal-tuple-destructure narrowing; #747 extends the **static** obligation to the projection and instantiation sites — ADT sub-pattern binds (`match opt { Some(@Nat) -> }` on `Option<Int>`), non-literal tuple destructures (via new Z3 tuple-datatype support in the SMT layer), generic constructor / effect-operation / function formals instantiated to `@Nat`, and imported ADT constructors with `@Nat` fields — by threading the checker's instantiated expression types into the verifier, so every narrowing **binding site** now verifies statically.  The Tier-3 **runtime** guard also extends to the tuple-destructure, top-level match-bind, ADT-sub-pattern, concrete-constructor-field, call-argument, generic-function-formal-call, and builtin-`@Nat`-parameter sites (`string_repeat`, `string_pad_start`/`_end`, `string_from_char_code`, `md_has_heading`), so an unverified compile traps on a negative `@Nat` rather than storing it silently.  Two runtime backstops remain deferred (both still statically obligated — a negative is an E503/E504 compile error — so neither deferral weakens a verified program): the effect-operation-argument guard ([#754](https://github.com/aallan/vera/issues/754)) and the guard for the *generic-instantiated* constructor field — the *concrete* constructor field ships here, but a generic field instantiated to `@Nat` needs per-call monomorphisation metadata ([#757](https://github.com/aallan/vera/issues/757)).  Two narrowing **positions** remain un-obligated even statically (pre-existing, surfaced by review): a bare `@Int`→`@Nat` at a function **return** slot, and a `@Nat` component of a tuple/constructor built in value position ([#758](https://github.com/aallan/vera/issues/758)).  Folds in the [#749](https://github.com/aallan/vera/issues/749) review-debt test pins (IndexExpr / InterpolatedString walker recursion, the `_fresh_slot_var` nat-alias path, and a `_narrows_into_nat` verifier/codegen parity test).

### Changed

- **pytest `9.0.3` → `9.1.0`** (`pyproject.toml`, `uv.lock`) ([#742](https://github.com/aallan/vera/pull/742)).  Routine Dependabot dev-dependency bump; the full suite passes unchanged on 9.1.0.  Its one backward-incompatible change — autouse fixtures with `module`/`package`/`session` scope under `--doctest-modules` — doesn't affect this repo, which doesn't run `--doctest-modules`.

## [0.0.172] - 2026-06-16

### Added

- **The `@Nat >= 0` invariant is now obligation-checked at binding sites** ([#552](https://github.com/aallan/vera/issues/552)).  Generalises the #520 subtraction obligation: a value narrowing from `@Int` into a `@Nat` slot carries a Tier-1 `value >= 0` proof obligation (`E503`) at `let` bindings, call arguments, effect-operation arguments (built-in `IO.sleep` and user-declared effects), constructor fields, top-level match binds, and literal-tuple destructures, discharged from preconditions and path conditions.  The pure-literal `let @Nat = 0 - 1` idiom the #520 obligation deliberately defers is now caught.  Codegen emits a runtime guard at the `let` site so programs compiled without `vera verify` still trap rather than store a negative `@Nat`.  At a non-`let` site a narrowing the solver cannot discharge has no runtime guard, so it surfaces an `E504` warning rather than being silently counted as runtime-covered.  Narrowing through ADT sub-pattern binds, non-literal destructures, generic-instantiated or imported constructor fields, and generic effect-operation formals (the projected source / call-site / module type is not statically resolved) is deferred to [#747](https://github.com/aallan/vera/issues/747); general refinement-predicate verification is [#746](https://github.com/aallan/vera/issues/746).

### Fixed

- **Verification counterexamples now witness the violation.**  `SmtContext.check_valid` extracted the Z3 model *after* popping the assertion scope, so the counterexample described the base context — `model_completion` filled the now-unconstrained slots with arbitrary defaults (e.g. `@Int.0 = 0` for the goal `@Int.0 >= 0`) instead of the violating assignment.  Extracting the model before the pop fixes `E502` (@Nat subtraction), `E503` (@Nat binding-site narrowing), and call-site precondition diagnostics alike.

## [0.0.171] - 2026-06-15

### Added

- **`peak_heap_bytes` on `ExecuteResult`** ([#706](https://github.com/aallan/vera/issues/706)) — the exported `$heap_ptr` bump high-water mark, read after execution (no new WAT).  The Map / Set reclamation tests now assert it grows ~O(N) across a 1 000- vs 10 000-element insert chain (≈6× with reclamation working; a leak would be ~O(N²) ≈100×), replacing the deleted Python-store-size assertion.  New `TestBucketOccupancy706` pins empty-string and Int-`0` keys plus empty-string Set elements through the occupancy flag.
- **Planning-document gates** ([#736](https://github.com/aallan/vera/issues/736)).  `check_doc_counts.py` now verifies the KNOWN_ISSUES.md "Refactoring needed" line counts against the measured files (±10% tolerance — the counts convey scale, and the gate trips into a re-cite rather than taxing every PR that touches a large file) and enforces the HISTORY.md version-row template (at most one issue link, no ` — ` separator per row).  `check_limitations_sync.py` now nets SKILL.md (Known Limitations + Known Bugs tables) and LSP_SERVER.md (limitations bullets converted to the standard table) alongside KNOWN_ISSUES.md, vera/README.md, and the spec chapters — and a configured section heading that goes missing now fails loudly instead of silently shrinking coverage, which surfaced the phantom spec §9.9 reference the script had been skipping since it was written: Chapter 9 now has a real Limitations section covering the Http, Inference, and missing-domain standard-library gaps.  17 new unit tests cover both checks.
- **Release process written down, split by audience** — CONTRIBUTING.md §Releases documents what a release-prep PR must contain (contributors don't cut releases); the maintainer-side mechanics (tag-after-merge ordering, the retag-demotes-release gotcha, the fold-in release pattern, the squash-vs-merge convention) live in CLAUDE.md's release-workflow section (manual until [#481](https://github.com/aallan/vera/issues/481) automates them).

### Changed

- **Map and Set host storage moved to bucket-as-truth across the CLI and browser runtimes** ([#706](https://github.com/aallan/vera/issues/706)).  The WASM-resident bucket array is now the sole source of truth for `Map<K, V>` and `Set<T>` contents on both runtimes; the Python-side `_map_store` / `_set_store` and the browser's `mapStore` / `setStore` are deleted.  Host imports take the wrapper pointer and read/write the bucket directly (8-byte header + 20-byte slots with an explicit occupancy flag), so there is no second copy that can silently drift — the audit's top architectural risk.  The compiled WASM shares one host-import contract, so codegen and both host runtimes migrate together.  Decimal stays value-typed on its Python store and keeps the wrap-table Phase 2c destructor; Map / Set wrappers are now plain heap objects reclaimed by ordinary mark-sweep, so `host_decref_handle` is Decimal-only.  The occupancy flag closes the empty-string-key / Int-`0`-key sentinel collision the old write-only mirror left latent (PR #707 review).
- **Power-of-two bucket capacity bounds copy-on-write heap usage** ([#706](https://github.com/aallan/vera/issues/706)).  Each persistent insert builds a fresh, larger bucket; a non-coalescing free list cannot reuse a freed size-N bucket for a size-(N+1) request, so the heap frontier climbed ~O(N²) regardless of GC — a 10 000-element insert chain peaked at 2.0 GB against the 2 GB heap ceiling.  Rounding capacity up to a power of two lets same-size-class inserts reuse freed buckets, dropping that chain's high-water mark to 3.0 MB (660×) and restoring ~O(N) growth.
- **ROADMAP.md rewritten around the June 2026 repo audit.**  The near-term plan is now four tiers ordered by the project's goal — close the silent failures, build the safety net, single-source the truth, then polish — with an explicit "Not doing now" section recording declined trade-offs, an "Ongoing threads" section (VeraBench leaves Milestone 1 to live there), and all four milestones rewritten.  Per-monomorphization generic verification ([#732](https://github.com/aallan/vera/issues/732)) is the chosen verification-depth path, with Tier 2 ([#427](https://github.com/aallan/vera/issues/427)) reframed as the Milestone 4 horizon upgrade that will use per-mono results as its differential oracle; the browser seam ([#609](https://github.com/aallan/vera/issues/609)/[#610](https://github.com/aallan/vera/issues/610)) is demoted below correctness work.  All 81 issue references in the old ROADMAP were explicitly re-homed (77 open issues) or consciously dropped (4 closed ones), and priority now lives in the ROADMAP tiers and nowhere else.
- **KNOWN_ISSUES.md normalized to two sentences per row** — every row in every section now states what the issue is and then its impact and path forward, no more and no less.  The 781-character browser-seam row split into per-issue [#609](https://github.com/aallan/vera/issues/609)/[#610](https://github.com/aallan/vera/issues/610) rows, the Refactoring-needed line counts were re-measured (19,570 / 5,939 / 4,253 — the old citations had drifted up to 2×), the Bugs table is documented as 1:1 with the open `bug`-labelled issues, and the new bug row for [#606](https://github.com/aallan/vera/issues/606) landed with it.
- **HISTORY.md re-staged and normalized.**  The two oversized stages split into four along their natural seams — standard-library depth (16–23 Apr), the bug-killing campaign (26 Apr – 8 May), stabilisation and memory safety (10–29 May), and the language server (10 Jun onwards) — with a stage index up top, every version row rewritten to one sentence with at most one issue link (76 of 173 rows violated the template), the parallel "Editor and tooling support" table folded into the stages it duplicated, and "By the numbers" extended with a current snapshot column.
- **Limitation wording re-synced from the canonical KNOWN_ISSUES rows** at every site: vera/README.md gains a verification-soundness row ([#552](https://github.com/aallan/vera/issues/552)/[#555](https://github.com/aallan/vera/issues/555)/[#730](https://github.com/aallan/vera/issues/730)) and splits its browser row; SKILL.md splits its browser row the same way; project-status counts (commits, releases, coverage) were re-measured everywhere they appear.

### Fixed

- **Host-side ADT result builders now root freshly-allocated heap payloads across the enclosing struct/array allocation** ([#706](https://github.com/aallan/vera/issues/706)).  The `Option` / `Result` / `Array<String>` constructors on both runtimes allocated a payload — a string, an `Array<String>` backing, or a freshly-built `Option` / `Json` / `HtmlNode` / `Decimal` / regex-match block — then the wrapping struct, and a garbage collection triggered by the second allocation could sweep the still-host-local pointer and store a dangling reference.  Surfaced under `VERA_EAGER_GC=1` or heap pressure (e.g. `map_get` on a `Map<K, String>`, `regex_find`, or `json_parse` returning a corrupted value).  A pre-existing gap the #692 / #695 rooting work left in these simpler builders; both runtimes now root via `_ShadowGuard` / a new JS `gcRooted` helper.  Surfaced by the CodeRabbit review.
- **`Float64` `Map` keys and `Set` elements compare under SameValueZero** ([#706](https://github.com/aallan/vera/issues/706)), so a `NaN` key/element round-trips (NaN equals NaN) and deduplicates.  The CLI Python dict and the browser `decodeColumn` comparison used `==` / `===`, which treat NaN as unequal to itself, so a NaN key could never be found, removed, or deduped — and `0.0 / 0.0` verifies and runs to NaN, so this was reachable.  Surfaced by the CodeRabbit review.

## [0.0.170] - 2026-06-12

### Fixed

- **[#727](https://github.com/aallan/vera/issues/727) — duplicate E501 diagnostics.**  A violating call site could be recorded once per translation pass: the primary body translation, the `@Nat`-subtraction walker's let-RHS environment rebuild, and the walker's operand discharge each re-translate the same call (a `@Nat`-subtraction operand in a `let` RHS recorded **three** identical E501s).  The SMT layer now dedups at recording time by (call node, precondition) identity, so every topology collapses to exactly one diagnostic and one `call_pre` obligation per violating site — while sites that only the walker ever translates — a violating call inside a `@Nat`-subtraction operand in statement position, checked via the walker's operand discharge — keep their single recording rather than being suppressed.  (Bare statement-position calls with no enclosing subtraction remain unchecked either way; that pre-existing gap is tracked as [#730](https://github.com/aallan/vera/issues/730).)  Translation behaviour is unchanged and the warm/cold differential oracle is untouched.
- **[#728](https://github.com/aallan/vera/issues/728) — LSP diagnostics now carry the full instruction contract.**  The language server's diagnostic mapping put only the description into the LSP message, so editor hovers said what broke but not how to fix it.  The message now appends the rationale paragraph and the `Fix:` paragraph exactly as `--json` carries them; diagnostics without those fields map to the bare description, unchanged.
- **E501 messages now speak in call-site terms.**  The precondition is rendered with the actual arguments substituted for the callee's parameter slots (`At this call site: string_length("") > 0`), and the `Fix:` text shows concrete code — the guard with the rendered call (`if string_length("") > 0 then { classify_sentiment("") } else { ... }`) and the exact `requires(...)` to add — instead of generic advice.  Substitution honours De Bruijn most-recent-first resolution; module-qualified callees and unmappable slots keep the generic wording.

## [0.0.169] - 2026-06-11

### Added

- **[LSP_SERVER.md](LSP_SERVER.md)** — the language server's user manual: what an LSP server is, install (`pip install -e ".[lsp]"`) and editor wiring, the standard feature surface (tier-annotated diagnostics, per-function verification-tier hints, hover, slot go-to-definition, typed-hole completion), the warm incremental verification core, and full request/response shapes for the four agent-facing custom methods (`vera/speculativeEdit`, `vera/proposeEdit`, `vera/strengthenContract`, `vera/addEffect`) with the typical speculate → inspect → propose loop.
- **VS Code extension 0.2.0** — the extension now starts `vera lsp` automatically for `.vera` files (settings `vera.lsp.enabled` / `vera.lsp.path`, command *Vera: Restart Language Server*), degrading gracefully to syntax-highlighting-only when the binary or the `npm install` is absent. Binary resolution prefers an explicit `vera.lsp.path`, then a workspace-local venv (`.venv/bin/vera` — GUI-launched VS Code does not inherit a shell `PATH`, so a from-source clone works with zero configuration), then `PATH`; spawn failure surfaces one warning with an Open Settings action. Requires VS Code 1.82+.

### Changed

- **veralang.dev** — the landing page's audience-addressed sections both gain the language server: §06 *Get Started* (the VS Code row starts `vera lsp` automatically; the `[lsp]` install path) and §07 *for machines*, reframed around read vs interrogate — the markdown set is how machines *read* Vera, the server is how they *interrogate* it, with a fourth agent-card and a `speculativeEdit` proof-delta sample.  `LSP_SERVER.md` is indexed in `llms.txt` and embedded in full in `llms-full.txt`.
- Documentation sweep after [#222](https://github.com/aallan/vera/issues/222): `vera lsp` joins the command lists in README/CLAUDE/AGENTS/SKILL; README's editor-support section, feature list, install notes, and project tree now cover the language server, `vera/obligations/`, and `vera/lsp/`; AGENTS.md gains an agent-facing section on the custom proof-delta methods; KNOWN_ISSUES drops the stale "LSP server" limitation row (shipped v0.0.161–v0.0.168) and gains three real ones — the single-file editor model ([#724](https://github.com/aallan/vera/issues/724)), parameter-only slot go-to-definition ([#181](https://github.com/aallan/vera/issues/181)), and handler-unaware `vera/addEffect` propagation ([#725](https://github.com/aallan/vera/issues/725)); `llms.txt` indexes LSP_SERVER.md; the compiler README module map's `lsp/` line count is corrected (a v0.0.168 update had silently failed); the release-count figures in README and the HISTORY footer now reflect the actual tag count (the v0.0.24.1 hotfix made releases = version + 1, uncounted since then).

## [0.0.168] - 2026-06-11

### Added

- **[#222](https://github.com/aallan/vera/issues/222) Phase F3 — `vera/addEffect`**, the multi-site workflow that completes the skill layer: `{uri, fn, effect}` computes the transitive-caller closure over the Phase B call walker (plain calls only — module-qualified calls never propagate across the file boundary), rewrites every affected `effects(...)` clause by span (`pure` → `<E>`; `<A>` → `<A, E>` appending after the original source verbatim; functions already naming the effect are skipped, with identity the base name before type arguments so `State<Int>` is not added next to `State<Bool>`), and runs ONE candidate through the proposeEdit pipeline.  The response lists `rewritten` functions in declaration order; a row state that is already satisfied short-circuits to the documented no-op shape without touching the verifier.  Propagation is handler-unaware by design — a caller that handles the effect in a `handle[E]` block is still rewritten; bounding the closure at handlers is noted as a refinement.  This closes the #222 LSP arc: the three skill-layer methods (`proposeEdit`, `strengthenContract`, `addEffect`) shipped in v0.0.166–v0.0.168 on the obligation core and proof-delta machinery from v0.0.161–v0.0.165.

## [0.0.167] - 2026-06-11

### Added

- **[#222](https://github.com/aallan/vera/issues/222) Phase F2 — `vera/strengthenContract`**, the contract-change workflow with a call-site audit: `{uri, fn, kind: requires|ensures, expr}` locates the first clause of that kind on the named top-level function, splices the new expression over the clause's span in the canonical document, and runs the candidate through the proposeEdit pipeline.  The audit is the proof delta itself — a tightened precondition some caller no longer satisfies surfaces as `newly_undischarged` `call_pre` items at the call sites (Phase A keys obligations by call-site span precisely for this) and the gate refuses; a strengthened postcondition the body proves discharges and applies.  No `force` parameter — the dedicated workflow exists to make the audited path the easy one (an agent that wants to push through a breaking change can construct the full text and call `vera/proposeEdit` with `force` explicitly).  Requests that cannot name a splice target (unknown function, unopened or unparseable document, bad `kind`) refuse with JSON-RPC InvalidParams at the boundary.

## [0.0.166] - 2026-06-11

### Added

- **[#222](https://github.com/aallan/vera/issues/222) Phase F1 — `vera/proposeEdit`**, the first skill-layer workflow: the whole edit → verify → apply sequence as one LSP method, so an agent cannot apply an unverified edit — applying *is* the final step of verifying.  The proposed text runs through the Phase E speculative verify; the gate applies it iff the proof delta has no `newly_undischarged` obligations and the proposed state has no error diagnostics (`force: true` overrides both, loudly — the delta still reports the damage).  On apply: a `workspace/applyEdit` request (the client owns the buffer, so the server round-trips the edit rather than silently diverging), the canonical `DocumentStore` updates, and diagnostics republish — the client's echoed `didChange` then replays from the warm discharge cache.  On refuse, canonical state is untouched, same isolation as `vera/speculativeEdit`.  New `vera/lsp/workflows.py` with the pure decision function separated from the effectful orchestration; ROADMAP regains a Phase F row while the reopened #222 is in flight.

## [0.0.165] - 2026-06-11

### Added

- **[#222](https://github.com/aallan/vera/issues/222) Phase E — `vera/speculativeEdit`**, the one custom LSP method and the reason the obligation core was built first: apply an edit in memory, re-verify on the warm incremental session, and return a `proof_delta` — `newly_discharged` / `newly_undischarged` / `timed_out` / `removed` / `unchanged`, each item carrying the obligation's function, kind, expression, position, and before/after status.  An agent proposing an edit learns whether it keeps, breaks, or strengthens the program's proofs before committing it.  Speculative runs share the warm session and discharge cache (pre-warming by design) under the same lock, but never touch the canonical per-URI analyses or published diagnostics.  Parse/type errors in the speculative state report `ok: false` with the error count.  This completes the #222 plan: Phases A (reified obligations + warm Z3), B (incremental invalidation + discharge cache), C (stdio transport + coordinate layer), D (diagnostics/hover/goto/completion), and E shipped across v0.0.161–v0.0.165; the "No LSP server" row leaves the compiler limitation table and the ROADMAP.

## [0.0.164] - 2026-06-10

### Added

- **[#222](https://github.com/aallan/vera/issues/222) Phase D** — `vera lsp` now serves language features over the obligation core: `publishDiagnostics` on open/change (parse, type-check, and verification diagnostics, plus a synthesised per-function verification-tier Hint — "Tier 1 — all contracts proven by Z3" / "Tier 3 — N of M obligations fall back to runtime checks" — computed from the obligation stream and suppressed for functions with violated obligations); hover showing the type of the smallest expression span under the cursor; go-to-definition on `@T.n` jumping to the parameter it names (De Bruijn most-recent-first via `slots.slot_table`; references binding through `let`/`match` return no definition — signature-level scope, with full binding resolution tracked by [#181](https://github.com/aallan/vera/issues/181)); and typed-hole completion listing the in-scope bindings with their types.  All verification is serialised through one warm `VerificationSession` under a lock.  The checker gains opt-in artifact collection (`typecheck_with_artifacts`: a `Span`→type side-table recorded by a thin `_synth_expr` wrapper, and structured `HoleSite` records factored out of the W001 hole diagnostic) at zero cost to existing callers.  `Diagnostic` gains an optional `tier` field (surfaced in `--json` only when set); the verifier's six Tier-3 fallback warnings (E520–E525) now carry `tier=3`.

## [0.0.163] - 2026-06-10

### Added

- **[#222](https://github.com/aallan/vera/issues/222) Phase C** — `vera lsp` serves the Language Server Protocol over stdio: handshake with `serverInfo`, full-text document sync into an in-memory `DocumentStore` (the source of truth for open files — features never read disk), and the coordinate-conversion layer in `vera/lsp/convert.py` where the three coordinate systems meet (Vera `Span`: 1-based line + 1-based code-point column with exclusive end; `SourceLocation`: 1-based line but 0-based column; LSP: 0-based line + UTF-16 code units, with astral-plane transcoding and surrogate-pair snapping per the spec).  Deliberately featureless — Phase D wires diagnostics/hover/completion onto this transport so the advertised capability surface never promises something unimplemented.  The transport dependencies live in a new optional `[lsp]` extra (`pygls>=2.0`, `lsprotocol`; both pure-Python, mirrored into `[dev]` for CI) so the base install is unchanged; `vera lsp` without the extra prints an actionable install message.

## [0.0.162] - 2026-06-10

### Added

- **[#222](https://github.com/aallan/vera/issues/222) Phase B** — incremental verification: `VerificationSession` now caches each top-level function's verification output (obligations, diagnostics, summary deltas) under an invalidation key covering everything its verification reads, and replays unchanged functions instead of re-entering Z3.  The soundness model (documented in the new `vera/obligations/cache.py`): a callee *contract or signature* change invalidates its callers (call sites check preconditions and assume postconditions); a callee *body* change does not (bodies are never read across the call boundary); span shifts, ADT / type-alias / effect / import / timeout changes invalidate conservatively.  Functions whose output contains a solver-timeout obligation are never cached.  `SessionRunStats` (replayed vs verified counts) added for cache observability.  Pinned by the corpus-wide differential oracle (replay == re-verify == cold `verify()` across all 35 examples and every verify/run-level conformance program) plus targeted invalidation-rule tests, including a span-shift test that caught the structural hash being position-blind (`Node.span` is `repr=False`) before it shipped.

## [0.0.161] - 2026-06-10

### Added

- **[#222](https://github.com/aallan/vera/issues/222) Phase A** — proof obligations are now first-class: new `vera/obligations/` package with a `ProofObligation` record (owning function, kind, expression text, source span, stable `content_key()` digest, discharge outcome with counterexample) and a `VerificationSession` daemon that re-verifies full programs on one long-lived Z3 solver via the previously-unused `SmtContext.reset()` warm path.  `VerifyResult` gains an `obligations` field (default-empty, source-compatible); `ContractVerifier` gains a `shared_smt` hook (the cold path is unchanged: fresh context per function).  Obligation kinds cover `requires` / `ensures` / `decreases` / `@Nat`-subtraction sites / call-site preconditions (the latter recorded on violation only in Phase A — successful call-site checks discharge inside the SMT layer and are enumerated in Phase B).  Reification is observational: records are created at the existing discharge sites in discharge order, never altering solver state.  Behaviour is pinned by a 250-test differential oracle (`tests/test_obligations.py`): warm session == cold `verify()` on diagnostics, summary, and the obligation stream — plus warm-twice determinism — across all 35 examples and every verify/run-level conformance program.  This is the semantic core the #222 LSP server builds on; Phase B (incremental invalidation + discharge cache) slots in behind the same API.

### Fixed

- **`SmtContext.reset()` warm-reuse staleness** — `reset()` (previously dead code) kept `_length_fns` / `_index_fns` / `_array_element_sorts` / `_path_conditions` across solver resets.  `get_rank_fn` asserts its `ForAll rank(x) >= 0` axiom only at dict-miss, so a surviving cache entry after `solver.reset()` silently skipped re-asserting the axiom and ADT-measure `decreases` checks would diverge from a fresh context.  `reset()` now clears all four (re-seeding the `Int` length function) and re-applies the solver timeout.  Latent-only before this release — nothing called `reset()`; the new warm session is its first caller, and the differential oracle now pins the equivalence.

### Security

- **[#712](https://github.com/aallan/vera/issues/712)** — SHA-pinned `codecov/codecov-action` to a commit (`e79a696` = v6.0.1) instead of the floating `@v6` tag, as targeted supply-chain hardening following the Codecov → Harness acquisition (announced 2026-06-02).  An ownership change is the canonical scenario where a major-version tag could be repointed by the new owner and silently flow into CI; pinning to a reviewed commit closes that, while Dependabot continues to propose bumps under review.  Coverage is unaffected either way — the 80% gate is the on-runner `pytest --cov-fail-under=80` and the upload is `fail_ci_if_error: false`.  Also corrected a stale `SECURITY.md` cross-reference that attributed action SHA-pinning to #390 (a closed Python dependency-lockfile issue).

## [0.0.160] - 2026-05-29

### Changed

- **[#599](https://github.com/aallan/vera/issues/599)** — bumped the `wasmtime` floor from `>=44.0.0` to `>=45.0.0`.  45.0.0 (released 2026-05-26) is the first PyPI release whose host-import trampoline catches `BaseException` rather than `Exception` ([bytecodealliance/wasmtime-py#337](https://github.com/bytecodealliance/wasmtime-py/pull/337), merged 2026-05-07 — the 44.0.0 tag predates it).  Vera filed the upstream issue ([#336](https://github.com/bytecodealliance/wasmtime-py/issues/336)) after a Ctrl-C during `IO.sleep` in a Conway's Life animation aborted with a libmalloc SIGABRT ([#595](https://github.com/aallan/vera/issues/595)): a raw `KeyboardInterrupt` (a `BaseException`) escaped the `except Exception` trampoline into Rust with an undefined ABI return value.  45.0.0 makes the raw propagation safe — the wasm call unwinds and the original `KeyboardInterrupt` re-raises in Python at the call site.

### Removed

- **[#599](https://github.com/aallan/vera/issues/599) / [#595](https://github.com/aallan/vera/issues/595)** — removed the four per-host-import `except KeyboardInterrupt: raise _VeraExit(130)` workaround guards (one in `host_sleep`, three across `host_read_char`'s Unix-non-TTY / Unix-TTY-cbreak / Windows-getwch branches).  These laundered `KeyboardInterrupt` into `_VeraExit` (an `Exception`) so the pre-45 buggy trampoline would catch it.  With `wasmtime>=45.0.0` the launder is unnecessary: a single `except KeyboardInterrupt` handler at the `func(store, ...)` call site in `execute()` now maps a Ctrl-C in any host import to the conventional SIGINT exit code (130), preserving captured stdout/stderr/state exactly as the `IO.exit` path does.  One source of truth replaces four duplicated guards.  The Unix-TTY path's terminal restore stays correct — it lives in a `finally`, so the terminal is returned to canonical mode before the interrupt propagates.  Net behaviour is unchanged (clean exit 130, pre-interrupt output preserved); the end-to-end test that pins this contract passes identically before and after the relocation.

## [0.0.159] - 2026-05-28

### Fixed

- **[#695](https://github.com/aallan/vera/issues/695) and [#705](https://github.com/aallan/vera/issues/705)** — closed the silent use-after-free in `Map<K, T_heap>` and `Set<T_heap>` where heap-pointer values stored in Python-side `_map_store` / `_set_store` were invisible to the conservative GC scan.  Fix: every `Map` / `Set` wrapper now points (at body offset +8) to a WASM-resident bucket array that mirrors the store's (key, value) pairs as i32 slot words.  The conservative scan reaches the values via shadow stack → wrapper → bucket → val_ptr, so a `$gc_collect` between map / set construction and value access no longer reclaims the heap blocks.  Bucket population happens in three paths: the WAT-emitted `attach_bucket_to_wrapper` import (dispatching to `host_attach_bucket` for both CLI and browser targets), the host-side `_alloc_map_wrapper` (used by `write_json`'s `JObject` branch and `write_html`'s `HtmlElement` attrs), and match-arm / let-binding shadow-rooting for heap-pointer bindings in `vera/wasm/data.py` and `vera/wasm/context.py` (the orthogonal #695-root cause: parameter shadow-pushing already covered function calls, but `match` and `let` binding sites did not).  Decimal wrappers stay exempt — `PyDecimal` is value-typed and cannot contain heap pointers.  This is the **mirror** approach: the Python store remains the source of truth and the bucket array is a write-only reachability anchor.  Follow-up [#706](https://github.com/aallan/vera/issues/706) tracks the architectural "move" cleanup (single source of truth in the bucket array, deleting `_map_store` / `_set_store`, across CLI Map / CLI Set / browser runtime).
- **[#708](https://github.com/aallan/vera/issues/708)** — closed the browser-runtime parallel of the #692 silent-UAF in `vera/browser/runtime.mjs`'s `writeJson` / `writeHtml` walkers.  Surfaced by the new browser-side EAGER_GC regression tests added in this PR: `writeJson` allocates a tree of `Json` heap blocks via repeated `alloc()` and JS-local pointer holding, but never shadow-pushed intermediates (`arrPtr` for a `JArray`'s backing, recursive child results, string ptrs) — so under `VERA_EAGER_GC=1` each subsequent alloc fires `$gc_collect`, reclaims the in-progress tree, and the writes scribble freed memory.  Empirically the constructed `JArray`'s body ended up with `tag=0` (JNull) and `payload=self` (looked like `Result.Ok(self)`) — a `Result.Ok` shape allocated on the same address after `JArray` got reclaimed.  Fix: added JS-side `gcGuard(fn)` helper that mirrors the CLI `_ShadowGuard` discipline (save `$gc_sp` at entry, restore on exit), and `gcShadowPush` for each intermediate in `writeJson`'s JString / JArray / JObject branches, `writeHtml`'s comment / text / element branches, `json_parse`, and `html_parse`.  The CLI side already had this fix from v0.0.158 (#692); the browser side was missing it, exposing the latent bug at higher GC pressure.
- **[#694](https://github.com/aallan/vera/issues/694)** — bumped the `subprocess.run` timeout in `tests/test_browser.py` from 30s to 60s at both call sites.  The previous 30s budget was insufficient for cold Node startup on Windows GitHub Actions runners when combined with the `--experimental-wasm-exnref` flag's first-execution V8 codegen cost.  Symptom was an intermittent `subprocess.TimeoutExpired` on `test (windows-latest, 3.12)` only, asymmetric across the matrix — `3.11` and `3.13` running back-to-back on the same runner benefited from a warm cache.  60s gives ~2× the median budget without making real hangs painful to detect.

### Changed

- **[#691](https://github.com/aallan/vera/issues/691)** — Supported platforms are now documented explicitly in `README.md` Installation: macOS 15+ (Sequoia, Tahoe), Ubuntu x86_64 (manylinux_2_27+), Ubuntu aarch64 (manylinux_2_38+, i.e. Ubuntu 23.10+), Windows x86_64.  The macOS 15+ baseline reflects [TelemetryDeck distribution data](https://telemetrydeck.com/survey/apple/macOS/versions/) — macOS 26 (~75%) + macOS 15 (~24%) covers ~99% of the install base.  macOS 14 (Sonoma) and earlier are out of scope ([#691](https://github.com/aallan/vera/issues/691)); Ubuntu 22.04 LTS aarch64 is out of scope ([#701](https://github.com/aallan/vera/issues/701)).
- **CI matrix**: replaced `macos-latest` with explicit pins for `macos-15` and `macos-26` (12 test combinations total).  Insulates against silent `-latest` migration when GitHub flips the alias.  First repo run on macOS 26 (Tahoe).
- **`z3-solver` lower bound** tightened from `>=4.12` to `>=4.15.5`.  Expresses the macOS 15+ baseline structurally — unsupported platforms now fail at dependency resolution with a clear "no matching distribution" error instead of a cryptic source-build failure 20 minutes later.  Resolved version stays at 4.16.0.0 (no functional change).

### Added

- **`scripts/check_wheel_availability.py`** — pre-flight check that every runtime dep has prebuilt wheels for every (platform, Python-version) tuple documented in README §Supported platforms.  Runs as the new `wheel-preflight` CI job.  Structural backstop for #691-class install regressions: catches upstream platform-tag bumps before they reach users.

## [0.0.158] - 2026-05-19

### Fixed

- **[#692](https://github.com/aallan/vera/issues/692)** — `html_parse`, `json_parse`, and `md_parse` no longer trap with `Out-of-bounds memory access` on inputs large enough to pressure GC during host-side tree marshalling.  Root cause: missing-shadow-stack rooting in `vera/wasm/html_serde.py::write_html`, `vera/wasm/json_serde.py::write_json`, and `vera/wasm/markdown.py::write_md_block`/`write_md_inline` — same #570 / #515 / #593 bug class but on the host side rather than WAT-emitted user code.  The Python-held intermediate pointers (`arr_ptr` / `name_ptr` / `wrapper_ptr`) were invisible to the conservative GC scan; if a sub-walk triggered `$gc_collect`, those blocks were reclaimed and subsequent writes corrupted the free list (concrete trap signature: out-of-bounds access at `0xfffffffd` from inside `$alloc`'s free-list traversal).  Externally reported with a `summarise_urls` example that ran `html_parse` over the current `FAQ.md` body; same shape proven empirically in `write_json` (large nested JArray) and `write_md_block` (many headings).

### Added

- **`$gc_sp` and `$gc_stack_limit` are now exported** from the emitted WAT module, allowing host imports to read and advance the GC shadow stack pointer.  Inline export syntax on the globals; the existing WAT-side push helper (`gc_shadow_push` in `vera/wasm/helpers.py`) continues to work unchanged for user-code pushes.
- **`_ShadowGuard` context manager** in `vera/codegen/api.py` — exception-safe push/pop discipline for host walkers.  On `__enter__` snapshots `$gc_sp`; on `__exit__` resets it to the snapshot (atomically pops every push from the block, on both success and exception paths).  Used by `host_html_parse`, `host_html_query`, `host_json_parse`, and `host_md_parse` to root intermediate WASM heap pointers across sub-tree recursion and the final Result wrapper alloc.
- **Field-allocation-then-body-allocation convention** applied throughout `markdown.py` — every match arm now allocates its field contents first (rooting via the guard), then allocates the body last.  This eliminates the secondary bug shape where the body pointer would be held in a Python local across a subsequent string or array allocation.

### Tests

- New `tests/conformance/ch09_host_walker_gc_rooting.vera` (run-level, 4 sub-tests) pinning post-fix behaviour for `html_parse` (500 element siblings), `json_parse` (1000-element number array, 500-element string array), and `md_parse` (200 H1 + paragraph blocks).  All sizes selected to provoke real heap growth and multiple `$gc_collect` cycles during the walk while staying under Python's default recursion limit on tear-down paths.
- New `tests/test_codegen.py::TestHostWalkerGCRooting692` (6 tests) — in-process regression for the same scenarios at the codegen layer, alongside the existing host-side GC-rooting regression classes for #570 / #515 / #593.  Two extra tests added per the pr-review-toolkit pr-test-analyzer review: `test_html_query_30_matches` covers the `host_html_query` `_ShadowGuard` path (re-walks each matched subtree via `write_html` in a single guard window), and `test_json_parse_500_key_object` covers the JObject branch of `write_json` (the val_ptr-pushed-per-iteration pattern that motivated the whole fix).

### Documentation

- Structural test in `test_codegen.py::TestWorklistOverflow348` updated to match the new `(global $gc_stack_limit (export "gc_stack_limit") ...)` WAT shape.

### Fixed (post-review)

- **PR #693 CodeRabbit findings** (commit `4b8c127`): narrowed `host_md_parse`'s broad `except Exception` to wrap only the parse step — shadow-stack work and `write_md_block` now sit outside, so host-side invariant violations propagate as wasmtime traps.  Removed redundant `guard.push(arr_ptr)` calls in markdown walkers after `_write_inline_array` / `_write_block_array` / `_write_array_of_block_arrays` / `_write_table_data` (the helpers already root their backing internally; the duplicate pushes were doubling shadow-stack consumption).  TESTING.md L193 count: `435 → 440`.  Added `#694` (Windows test_browser timeout flake) to `KNOWN_ISSUES.md` per the PR-generated-new-issues-must-be-tracked rule.
- **PR #693 pr-review-toolkit findings**: same parse-only-in-try restructure applied to `host_html_parse` — the previous round had narrowed `host_md_parse` but left `host_html_parse`'s `_ShadowGuard` block inside its narrow `except (ValueError, TypeError, AttributeError)`, contradicting the in-file comment that claimed the catch matched `host_json_parse`.  Now all three host walkers structurally match: parse-only-in-try, shadow-stack work outside.  `_ShadowGuard.__init__` re-raises a missing-export `KeyError` as a clearer `RuntimeError` naming `$gc_sp` / `$gc_stack_limit` and `#692` — diagnostic-quality fix for hand-crafted-WAT scenarios.  `_ShadowGuard.__exit__` `if self._initial_sp is not None` guard tightened to an `assert` (the only way the None case is reachable is misuse of the context manager outside a `with` block; the assert pins the invariant).  Misleading `host_html_query` comment about "WASM codegen at the call site shadow-pushes via the standard mechanism" softened — the codegen does shadow-push the consuming local, but the guard's protection doesn't extend past the function boundary.  Added rooting-contract docstrings to all four markdown array helpers (`_write_inline_array`, `_write_block_array`, `_write_array_of_block_arrays`, `_write_table_data`) so future maintainers see the convention from the function signature.  Added an exception-safety note to `write_json`'s JObject branch documenting why `map_dict` partial state is safe to discard on mid-loop raise (function exits via the raise before `map_alloc` is called).  Documented the markdown helpers' `alloc-then-push` allocation order intent (a future "fix" must NOT try to push before alloc or wrap the push in try/except — both would break the trap-on-overflow invariant).

## [0.0.157] - 2026-05-19

### Added

- **[#618](https://github.com/aallan/vera/issues/618)** — new `IO.read_char` effect operation for single-character input.  Signature: `op read_char(Unit -> @Result<String, String>)`.  Returns one Unicode character from stdin, or `Err("EOF")` when the stream closes (Ctrl-D on a Unix TTY also maps to EOF).  Terminal target uses termios cbreak mode via `tty.setcbreak()` (Unix TTY — cbreak preserves ISIG so Ctrl-C still raises SIGINT, unlike raw mode which would suppress it), `msvcrt.getwch()` (Windows TTY), or buffered `sys.stdin.read(1)` (piped/redirected stdin on either platform).  Browser target returns `Result.Err` pending JSPI suspend/resume (depends on [#609](https://github.com/aallan/vera/issues/609); same primitive `IO.sleep` will use).  Unblocks real-time CLI programs (paced REPLs, terminal games) that couldn't be written before because `IO.read_line` is line-buffered.  Added per the same `IO`-effect-extension pattern as `IO.sleep` / `IO.time` / `IO.stderr` ([#463](https://github.com/aallan/vera/issues/463)): a single new op on the existing effect, not a new `<Terminal>` effect.

### Tests

- New `tests/conformance/ch07_io_read_char.vera` (verify-level — pins the type signature and effect-row wiring).  New `tests/test_cli.py::TestIOOperations::test_run_io_read_char_piped_input` / `test_run_io_read_char_eof` / `test_run_io_read_char_utf8` covering the piped-input path (no termios needed), EOF handling, and multi-byte UTF-8 round-trip (`sys.stdin.read(1)` returns one Unicode character, not one byte).

### Documentation

- `spec/07-effects.md` — added `read_char` row to the IO operation table; bumped IO operation count "ten → eleven" in the section intro.
- `spec/12-runtime.md` — added `vera.read_char` import row and the browser-runtime IO behaviour table entry.
- `SKILL.md` — added `IO.read_char` row to the IO operations table; updated the browser-runtime summary sentence to mention the pending JSPI dependency; bumped IO operation count "ten → eleven" in two places.
- `examples/read_char.vera` — new example demonstrating the read-one-char pattern with full failure-mode handling.
- `TESTING.md` — finished the 34→35 example-count sweep across six remaining call sites (validation-script comment, verification-coverage section, slot-feature row, round-trip section, validation-scripts table, pre-commit-hooks table) — caught by CodeRabbit on PR #689 after the initial header-row update.

### Fixed (post-review)

- **PR #689 CodeRabbit findings (round 1)**: wrapped `sys.stdin.fileno()` / `sys.stdin.read(1)` / `termios.tcgetattr` / `tty.setraw` / `termios.tcsetattr` in `except Exception` blocks in `host_read_char` so system errors (closed stdin, monkey-patched stream without `fileno()`, `termios.error` on weird devices) become `Result.Err` rather than propagating as wasmtime traps.  `Exception` excludes `KeyboardInterrupt` and `SystemExit` (direct `BaseException` subclasses), so Ctrl-C still terminates interactive prompts — same stance as `host_sleep`.  Added explicit `encoding="utf-8"` to the three new `subprocess.run` calls in `test_cli.py` (matches CLAUDE.md cross-platform pitfalls section: CI sets `PYTHONUTF8=1` as a backstop, but the explicit form is portable to local Windows shells without that variable).
- **PR #689 CodeRabbit findings (round 4)**: two final clean-ups.
  - **Important**: Unix TTY cbreak mode now maps `\x04` (Ctrl-D / ASCII EOT) to `Err("EOF")`.  Without this mapping, a user pressing Ctrl-D in a real-time CLI program would get `Ok("\x04")` and the program would have to know to interpret the literal byte as end-of-input — surprising and platform-specific (cbreak disables ICANON, which is what normally turns Ctrl-D-at-start-of-line into an empty read in canonical mode).  Now Ctrl-D produces the expected EOF semantics in cbreak mode.  The mapping is restricted to the Unix TTY cbreak branch only — piped `\x04` on the non-TTY shared path stays a literal byte (a pipe is a byte stream, the producer chose to include `\x04`), and the Windows `msvcrt.getwch()` branch has its own end-of-input convention (Ctrl-Z `\x1A`) which is left untouched for now.  New regression test pins the non-TTY asymmetry.
  - **Documentation**: CHANGELOG entry for #618 now says "termios cbreak mode via `tty.setcbreak()`" with the ISIG-preservation rationale, rather than the stale "termios raw-mode" wording that survived from before the round-3 fix.
- **PR #689 CodeRabbit findings (round 3)**: caught two more correctness bugs that the previous rounds (CodeRabbit ×2 + internal multi-agent review) missed.
  - **Critical**: `tty.setraw()` clears the termios `ISIG` flag, which suppresses SIGINT generation — meaning the `except KeyboardInterrupt` clause added in the previous round was actually **unreachable** in TTY mode.  Ctrl-C would arrive in the read buffer as the literal byte `\x03` instead of raising `KeyboardInterrupt`, so a Tetris-style game would receive `\x03` as input and the user could never exit the program.  Switched to `tty.setcbreak()`, which disables ICANON + ECHO (still gets one character without waiting for Enter and without echoing) but PRESERVES ISIG, so Ctrl-C → SIGINT → `KeyboardInterrupt` → `_VeraExit(130)` works as intended.  Verified empirically that `cfmakecbreak` keeps the ISIG bit set while `cfmakeraw` clears it.
  - **Important**: termios restore failure no longer silently swallowed.  The previous round wrapped the `tcsetattr(...)` call inside the inner `finally` in a bare `except Exception: pass`, which surfaced the read error correctly but lost the restore error entirely.  Now captures the restore exception into a local `restore_exc` variable: if the read itself failed, the read error wins (more actionable); if the read succeeded but restore failed, surfaces a distinct "raw-mode restore failed: ..." error; if both failed, the read error still wins.  No silent failures from this path now.
- **PR #689 pr-review findings (round 3, internal multi-agent review)**: caught two correctness bugs and three quality issues that CodeRabbit missed.
  - **Critical**: `host_read_char` no longer lets `KeyboardInterrupt` escape through the wasmtime trampoline.  Each blocking call (`sys.stdin.read(1)` in both branches, `msvcrt.getwch()` on Windows) now catches `KeyboardInterrupt` and raises `_VeraExit(130)` — the same fix `host_sleep` already applies for the same reason (#589-class WasmTrapError contract violation when a raw Python `KeyboardInterrupt` unwinds through the host import).  The prior comment claimed parity with `host_sleep`'s "let it propagate" stance, but `host_sleep` actually *catches* `KeyboardInterrupt`; the comment was factually wrong and the missing catch was a real bug.  User-facing behaviour is unchanged: Ctrl-C still terminates the program with exit code 130.
  - **Important**: termios restore failure can no longer silently mask the original read error.  The `tcsetattr` call in the inner `finally` is now wrapped in its own `try/except` — if restore fails (rare; same fd just worked) the terminal stays in raw mode but the original read error still surfaces, which is more useful for debugging.
  - **Important**: `tcgetattr` failure now reports a distinct error message ("tcgetattr failed: ...") rather than the misleading "raw-mode read failed: ..." — raw mode never started in that case.
  - **Minor**: removed a redundant local `import os` inside `host_read_char` (`os` is already imported at module level).
  - **Minor**: fixed a stale cross-reference in `tests/conformance/ch07_io_read_char.vera`'s header comment (pointed at `tests/test_codegen.py` instead of `tests/test_cli.py`).
- **PR #689 pr-review findings (test coverage)**: added 5 new `execute(stdin=...)` tests in `test_codegen.py::TestIOOperations` covering the `stdin_buf` fixture path that subprocess-based tests can't reach: single-character read, empty-buf EOF, sequential reads advance cursor, read-then-EOF, and 2-byte UTF-8 round-trip (platform-independent — no reliance on host stdin encoding).  Tightened the three existing `test_cli.py` `read_char` tests: `result.stdout.rstrip() == "<expected>"` rather than substring `in`, plus `assert result.stderr == ""` (catches a class of regression where a future host accidentally prints to stderr on the way to a clean exit).
- **PR #689 CodeRabbit findings (round 2)**: lifted the `os.isatty(fd)` check above the platform fork in `host_read_char`.  Previously the Windows branch went straight to `msvcrt.getwch()` regardless of whether stdin was a real console or a redirected pipe.  `msvcrt.getwch()` technically works on redirected stdin via Win32's `_getch` fallback but decodes raw bytes rather than honouring Python's stdin encoding, so a piped `é` would round-trip differently from the Unix path.  Now the non-TTY (pipe/redirect) branch is shared across platforms: both use `sys.stdin.read(1)` for identical encoding behaviour.  Only TTY stdin goes through `msvcrt.getwch()` (Windows) or termios raw mode (Unix).  Also wrapped `msvcrt.getwch()` in `except Exception` for symmetry with the Unix path's defensive handling, and finished a stale 86→87 sweep in TESTING.md (L92 conformance suite description, L193 parametrized test count) + ROADMAP.md (#679 row description).

## [0.0.156] - 2026-05-19

### Added

- **`TestSummary.unlisted_errors: int`** — new field on the `vera.tester.TestSummary` dataclass that counts verifier-error diagnostics whose attributable function isn't in the displayed `functions` list.  This happens when `--fn` filters to a subset, or when a private helper fails verification (private functions aren't displayed by `vera test`).  Exposed in `vera test --json` output under `summary.unlisted_errors` so downstream CI consumers can read the structured count instead of re-running regex attribution against the diagnostics array.  Introduced as part of [#674](https://github.com/aallan/vera/issues/674)'s fix to keep `vera/cli.py` purely presentational — the engine is the source of truth for attribution, `cli.py` reads structured fields.

### Fixed

- **[#675](https://github.com/aallan/vera/issues/675)** — E500 (`Postcondition does not hold`) `fix=` text now names all three repair classes neutrally, with implementation-repair first.  Pre-fix the text named only two classes (strengthen `requires(...)`, weaken `ensures(...)`) — implicitly biasing the user away from the most common repair when E500 catches a typo in the function body.  External report from @rzyns.  Tightened `tests/test_verifier.py::TestCounterexamples::test_violation_has_fix_suggestion` to pin all three classes (pre-existing assertion would have survived the rewrite without catching a regression dropping "implementation").

- **[#674](https://github.com/aallan/vera/issues/674)** — `vera test` now fails closed when the verifier reports E500/E501/E502 diagnostics instead of treating verifier-refuted contracts as successful Tier 1 results.  JSON output now sets `ok: false`, preserves the verifier diagnostics, and exits non-zero; human output displays failed functions and a diagnostics section so verifier failures cannot be hidden behind green rows.
  - `E501` call-site precondition failures are attributed to the caller rather than the callee, while `E500` postcondition failures and `E502` `@Nat` subtraction underflow diagnostics are attributed to their responsible function.
  - `--fn` keeps function rows filtered to the selected target, but whole-file verifier errors still fail closed and remain visible in diagnostics; private/non-displayed verifier failures likewise surface as unlisted verifier errors instead of disappearing.
  - Human summaries distinguish static verifier failures from Tier 3 runtime trial failures, keep E700 runtime contract violations out of verifier-error counts, and avoid double-counting multiple verifier diagnostics already represented by a displayed failed function.

### Tests

- Added regression coverage for `vera test` verifier-error handling across JSON and human output, private helper failures, `--fn` filtering, E501 caller attribution, E502 underflow attribution, mixed static/Tier 3 summaries, E700 runtime failures, multiple diagnostics on one failed function, and CLI dispatch return codes.

### Documentation

- **Docs sweep addressing 2026-05-18 compiler-review findings** ([PR #684](https://github.com/aallan/vera/pull/684)) —
  - `README.md` "Contracts the compiler proves" section reworded — the previous unqualified claim ("Division by zero is not a runtime error — it is a type error.  The compiler checks every call site to prove the divisor is non-zero.") overclaimed; the verifier checks contracts the programmer wrote, not auto-synthesised obligations on primitives.  New wording correctly describes static-vs-runtime split with forward reference to [#680](https://github.com/aallan/vera/issues/680) (auto-injection follow-up).
  - `FAQ.md` — new Q&A "Does the compiler prove division-by-zero, out-of-bounds indexing, etc. can't happen?" walking through the static/runtime split.
  - `spec/06-contracts.md` — new §6.4.3 "Primitive Operation Safety" between Call Site Verification and SMT Solver Integration.  Subsequent sections renumbered.  States explicitly that obligations on `a / b`, `a % b`, `arr[i]`, `string_at(s, i)` are not auto-synthesised; `@Nat` subtraction ([#520](https://github.com/aallan/vera/issues/520)) is the one exception today.
  - `KNOWN_ISSUES.md` — PR #684 restored the [#674](https://github.com/aallan/vera/issues/674) bug row with the external report from @rzyns and duplicate #681 provenance; this fixing PR removes that row because `vera test` now fails closed on verifier-refuted contracts.
  - `ROADMAP.md` — four new tracking items: [#679](https://github.com/aallan/vera/issues/679) (Ch 8 conformance gap), [#680](https://github.com/aallan/vera/issues/680) (auto-inject primitive obligations, cross-referenced from #427), [#682](https://github.com/aallan/vera/issues/682) (diagnostic-tagging discipline), [#683](https://github.com/aallan/vera/issues/683) (spec/Lark grammar nominal drift).

## [0.0.155] - 2026-05-13

### Fixed

- **[#578](https://github.com/aallan/vera/issues/578)** — wrapper-handle bit-31 tagging closes the last latent conservative-GC retention bug.  Pre-fix, `Map<K, V>` / `Set<T>` / `Decimal` wrapper ADTs stored their raw host-store handle as an i32 at body offset 4.  Phase 2b of `$gc_collect` does a conservative word-by-word scan of every reachable object's payload, checking each i32 against the heap-range predicate (`val >= gc_heap_start + 4 && val < heap_ptr && (val - gc_heap_start) & 7 == 4`).  For typical programs the handle counter stays well below `gc_heap_start` (~147 KiB) so the heap-range check rejects it, but a long-running program allocating >100K host-store entries in a single `execute()` call could see a handle exceed that threshold and (with the right alignment) be falsely classified as a heap pointer — silently retaining an unrelated heap object.  Retention bug, not correctness (no use-after-free, no corruption), but unbounded retention for long sessions.
  - **Fix**: store the handle ORed with `0x80000000` at body offset 4 (Option 1 from the issue body, "self-describing wrappers").  The in-heap field is now always `>= 2 GB`, structurally outside any plausible heap-range check.  Unwrap recovers the raw handle by ANDing with `0x7FFFFFFF`.  Two-instruction overhead on each wrap and unwrap; no GC-code changes.
  - **Heap-ceiling guard** in `$alloc`: traps if `heap_ptr + total >= 0x80000000`, so the disjointness invariant (`heap_ptr < 2 GB` ⇒ tagged handles `>= 2 GB` never collide with heap pointers) holds by construction.  Practical Vera programs use <100 MB; this trap fires only on egregious heap pressure.
  - **Host-side mirrors**: `vera/wasm/json_serde.py::read_json` and `vera/wasm/html_serde.py::read_html` read the wrapper's handle field directly via wasmtime memory access (bypassing the WAT `_emit_unwrap_handle` helper) when parsing `JObject` / `HtmlElement` attributes.  Both now AND with `0x7FFFFFFF` to recover the raw handle for `map_store` lookup.  `_wrap_handle` in `vera/codegen/api.py` (host-side allocator for Option<Decimal>.Some payloads etc.) writes the tagged value to match.

### Tests

- `tests/test_codegen.py::TestWrapperHandleTagging578` — 6 new tests pinning the contract: (1) wrap site emits `i32.const 0x80000000; i32.or`, (2) unwrap site emits `i32.load offset=4; i32.const 0x7FFFFFFF; i32.and`, (3) `$alloc` body contains the heap-ceiling guard (ordered 8-instruction sequence pinned by adjacent-sequence regex), (4) end-to-end wrap/unwrap round trip preserves the original handle (a Map insert + lookup), (5) `html_to_string` produces the correct length output — pinning that the host-side `read_html` mask is in place (without it the attribute dict lookup would miss and the rendered HTML would be missing the `title="..."` attribute), (6) `json_stringify(JObject(...))` produces the correct length — sibling test for the host-side `read_json` mask (which lives in `vera/wasm/json_serde.py` and bypasses the WAT unwrap helper just like `read_html` does).

### Documentation

- **`KNOWN_ISSUES.md`** — removed the #578 bug row.  The bug-tracker section is now empty for the first time since ~v0.0.80.

## [0.0.154] - 2026-05-13

### Fixed

- **[#549](https://github.com/aallan/vera/issues/549)** — GC-aware tail-call optimization for allocating functions.  Pre-fix, the post-process in `vera/codegen/functions.py::_compile_fn` reverted every `return_call` → plain `call` whenever `ctx.needs_alloc` was True, because WASM `return_call` discards the current frame and would skip the GC epilogue (`global.set $gc_sp` to restore the shadow-stack pointer), leaking shadow-stack slots once per iteration and eventually trapping on the next `$alloc` once gc_sp passed the worklist boundary.  This forced agents to restructure tail-recursive code that allocates per iteration into `array_fold` / `array_map` shapes or to hoist allocations outside the recursion.
  - **The fix** preserves TCO and the shadow-stack invariant simultaneously: instead of reverting, the post-process now PREPENDS a two-instruction `$gc_sp` restore (`local.get $gc_sp_save; global.set $gc_sp`) immediately before each `return_call` in an allocating function.  The args for the recursive call are already on the WASM operand stack at the return_call site; the restore only touches the `$gc_sp` global, so args transfer atomically to the callee.  The callee's prologue then saves a clean new `$gc_sp` baseline, so per-iteration shadow-stack usage stays bounded at `caller's entry + n_arg_roots` regardless of iteration count.
  - **Postcondition-bearing functions still revert** to plain `call` — `return_call` would skip the runtime postcondition check (`local.set $ret; <check>; trap on failure; local.get $ret`).  The dispatch is: if `post_instrs` revert; elif `needs_alloc` patch with GC-restore; else keep `return_call` as-is.  Precedence: postcondition-revert > GC-aware-TCO-patch > untouched.
  - **Local pre-allocation** — the `$gc_sp_save` local is allocated BEFORE the dispatch so both the per-`return_call` restore site AND the function's GC prologue (`global.get $gc_sp; local.set $gc_sp_save`) share the same local index.

### Tests

- `tests/test_codegen.py::TestTailCallOptimization517` — renamed `test_allocating_function_falls_back_to_plain_call` to `test_allocating_function_uses_gc_aware_tco_549` and inverted its assertions: it now verifies that an allocating tail-recursive function emits `return_call $foo` (TCO preserved) AND that every such site is preceded by `local.get <N>; global.set $gc_sp` (shadow-stack invariant preserved).  Added a new sibling test `test_allocating_function_with_postcondition_still_reverts` to pin the postcondition-revert precedence.
- `tests/test_codegen.py::TestGCShadowStackOverflow::test_shadow_stack_overflow_traps` — rewritten to use a non-tail-recursive shape (recursive call wrapped in `array_append`).  Pre-#549 the tail-recursive form would leak shadow-stack slots and trap on the overflow guard at ~1300 iterations; post-#549 the same form runs cleanly to completion.  To still exercise the overflow guard, the non-tail form stacks WASM frames whose shadow-stack roots survive across iterations.
- `tests/test_stress.py::test_deep_tail_recursion_with_allocating_arg` — body switched from string-pool literals (which don't set `needs_alloc`) to a per-iteration `let @Array<Int> = [_, _]` heap allocation, so the test now actually exercises `#549`'s GC-aware TCO path.  The pre-fix body was passing trivially.
- `tests/test_stress.py::test_tco_with_allocation_1m_iterations` — new 1M-iteration companion, parametrised over default-GC and eager-GC modes (~190ms wall-clock in both).  Pre-fix this would have been impossible: 1M plain `call`s blow the WASM call stack at ~30K frames.  Post-fix the `return_call` + `$gc_sp` restore keeps shadow-stack usage flat, so 1M iterations complete in constant memory.

### Documentation

- **`KNOWN_ISSUES.md`** — removed the #549 bug row.

## [0.0.153] - 2026-05-13

### Added

- **[#667](https://github.com/aallan/vera/issues/667)** — SMT translator coverage for `FloatLit`, `IndexExpr`, and `ArrayLit` in contract predicates.  Pre-fix all three returned `None` from `vera/smt.py::translate_expr`, dropping every affected contract to Tier 3 (runtime check).  The issue body claimed the parser would reject these shapes; reality-check against `vera check` showed the parser and type checker already accepted them — only the SMT translator was missing the cases.
  - `FloatLit` → `z3.RealVal(value)`.  Float64 already maps to Z3's `Real` sort (sound for relational properties; not a full IEEE-754 model).  One-line addition.
  - `IndexExpr` → uninterpreted `index_<sort>(arr, i)` function call, parallel to the existing `length_<sort>` pattern.  Sound (function congruence — two references to `arr[i]` with the same `i` produce the same value) but partial — the verifier can't reason about element structure beyond what explicit predicates assert.  Quantified contracts ("for all valid i, arr[i] > 0") remain Tier 2 / Tier 3 territory.
  - `ArrayLit` → fresh `Array_<elt>` constant with `length(lit) == N` and per-element `index(lit, i) == element_i` axioms asserted to the solver.  Element types that can't be sorted (e.g. function-typed) fail cleanly via `None`.
- **Array-sort infrastructure** in `vera/smt.py`: `_get_array_sort`, `_get_index_fn`, `declare_array_var`.  `Array<T>` parameters are now declared as constants of uninterpreted `Array_<T>` sorts; pre-fix they fell through to `declare_int(z3_name)` because `Array` isn't in the SMT layer's `_adt_registry`, making `Array<Int>` slots numerically-typed in Z3.  The new path routes through `_is_array_type` + `_declare_array_var` helpers in `vera/verifier.py`.

### Fixed

- **Two overstrong example contracts honestly relaxed.**  Closing the FloatLit / ArrayLit gaps in the SMT translator changed two pre-fix Tier-3-with-warning postconditions into E500 verification errors — the verifier could now fully translate the body and reach the contradiction.  The pre-fix behaviour was *not* unsound (the verifier had honestly emitted E522 warnings "Cannot statically verify postcondition…") — only more precise post-fix.  Both contracts relaxed to match what's statically provable from the helpers' existing `ensures(true)` clauses:
  - `examples/json.vera::main` — `ensures(@Int.result == 0)` → `ensures(true)` with explanatory comment.  None of `parse_current_temp` / `parse_average_temp` / `round1` carries a postcondition strong enough to let the verifier conclude `@Int.result == 0` statically.
  - `tests/conformance/ch06_quantifiers.vera::main` and `::test_has_zero` — both `ensures(@Bool.result == true)` → `ensures(true)`.  Helpers `all_positive` / `has_zero` carry `ensures(true)`; the static verifier can't conclude the postcondition from the specific array literals.

### Tests

- `tests/test_verifier_coverage.py::TestSmtCoverage667` — 9 new tests in two clusters: 5 for the core translation cases (FloatLit in pre/postconditions, IndexExpr, ArrayLit, ArrayLit-element-access), plus 4 for the call-result-typing follow-up (ADT-element-array indexing, String/Float64/Array return type propagation through `_translate_call_with_info`).  Each asserts not just "no errors" but also `tier1_verified >= N` so a regression that drops back to Tier 3 fails the test (Tier 3 is also error-free).
- Three pre-existing edge-case tests (`test_translate_expr_returns_none_for_unsupported`, `test_binary_with_none_operand`, `test_unary_with_none_operand`, `test_if_with_untranslatable_condition`) swapped their `FloatLit` sentinel (now Handled) for `UnitLit` (still intentionally-unsupported — predicates are Bool, not Unit).
- `test_overall_tier_counts` updated: 252/26/278 → 253/25/278.  The +1/-1 shift comes entirely from the `json.vera::main` relaxation (pre-fix: counted in T3 with warning; post-relaxation: counted in T1 trivially), **not** from any SMT-widening-driven T3→T1 movement.  No other example contract changed tier.

## [0.0.152] - 2026-05-13

### Added

- **[#596](https://github.com/aallan/vera/issues/596)** — stress-test harness landing as `tests/test_stress.py` with 8 logical scale-dependent regression tests covering the bug classes the standard test suite couldn't catch.  Pre-#596 the project relied on user-reported real-world programs to surface scale-dependent codegen/runtime bugs (#570 iterative-builder shadow-stack overflow at ~4000 elements, #515 GC self-fault under sustained allocation, #593 Conway's Life corruption at 12×30+).  The harness exercises each scale axis at the smallest size where the bug class historically manifested with ~2-3x safety margin: 10K `array_map`, 5K nested-array `array_map`, 1K-deep tail recursion with allocating arg, 20×20 nested array-fold-of-array-fold (#593 territory), 100K `array_fold`, 10K String allocations through interpolation, 1K `State<Int>` get/put cycles in a single handler scope, 10K `IO.print` calls with stdout-capture buffer growth.  Each test asserts on a SPECIFIC observable (closed-form sum, exact line count, etc.), not just "completed without crashing", so a future regression that silently short-circuits or skips iterations would still fail.
- **[#596](https://github.com/aallan/vera/issues/596) eager-GC lane** — six of the eight stress tests (the GC-rooting-targeted subset: #570 / #515 / #549 / #573 / #593 / captured-frame State handlers) are parametrised over `[False, True]` for the `eager_gc` flag.  The `True` mode sets `VERA_EAGER_GC=1` via `pytest.MonkeyPatch.setenv` before the compile call, so the runtime's `$alloc` function emits a `call $gc_collect` as its first instruction — forcing a full GC pass on every allocation.  This converts latent missing-shadow-root bugs from "fires occasionally at scale" to "fires on the very next allocation," so a regression that would normally need thousands of iterations to surface fails on the first or second iteration.  The pattern was used to diagnose #593 originally; the eager lane embeds that diagnostic capability as ongoing regression coverage.  Total test count: 8 logical × eager lane parametrisation on 6 of them = 14 test instances.  Wall-clock: 0.66s in-process for the full suite.
- **`stress` pytest marker** registered in `pyproject.toml` with default `addopts = "-m 'not stress'"` — stress tests are skipped from the per-PR pytest run.  Local invocation: `pytest -m stress`.
- **`.github/workflows/nightly-stress.yml`** with three triggers: (1) nightly cron at 06:00 UTC as primary safety net, (2) path-filtered PRs touching `vera/codegen/**`/`vera/wasm/**`/`tests/test_stress.py` for fail-fast on PRs likely to break stress invariants, (3) manual `workflow_dispatch` from the Actions tab.
- **Failure reporting on cron failures** — when the nightly cron fails, the workflow uses `actions/github-script@v7` to open (or comment on, if one is already open) a tracking issue labelled `stress-regression`, with the commit SHA and run URL.  Deduplicates across days: the first failure opens a fresh issue, subsequent failures comment on it.  Skipped on `pull_request` triggers (PR's own checks tab is the reporting surface) and `workflow_dispatch` (whoever triggered is already paying attention).  Converts cron failures from "visible only to whoever opens the Actions tab" to "visible in the issue feed where Vera work is already triaged."

### Documentation

- New "Stress tests" subsection in `TESTING.md` documenting the harness, the 8 initial test programs with their scale axes and target bug classes, the default-skip behaviour, the three CI triggers, and the assertion-shape convention.

## [0.0.151] - 2026-05-12

### Added

- **[#597](https://github.com/aallan/vera/issues/597)** — walker-completeness audit.  Nine `Expr`-dispatching walker functions in the compiler now carry `# WALKER_COVERAGE:` checklist comments classifying every one of the 29 `Expr` subclasses with one of four dispositions: **Handled** (explicit `isinstance` branch), **Intentionally ignored** (default fall-through is correct — e.g. literals in a sub-expression walker), **Cannot occur** (structurally impossible — e.g. `OldExpr` in body-only contexts, `HoleExpr` post-typecheck), or **MISSING** (open bug, branch should exist).  A new `scripts/check_walker_coverage.py` enforces coverage mechanically — it parses each walker's `isinstance(expr, ast.X)` calls AND its checklist text, then verifies the union covers every `Expr` subclass declared in `vera/ast.py`.  Wired into pre-commit as the `walker-coverage` hook, so a new `Expr` subclass added to `vera/ast.py` forces every walker to either handle it or document its disposition.  Closes the bug class responsible for `#588` (closure-lift), `#604` (prelude combinators), `#559` (nested aliases), and `#648` (cyclic aliases) — all five PRs from this stabilisation cycle had the same shape: a walker handled N of N+1 subclasses, missing case silently fell through.  The convention is documented in `vera/README.md` under "Walker-completeness convention".  Closes `#597`.

### Fixed

- **[#597](https://github.com/aallan/vera/issues/597) defensive adds** — 11 `isinstance` branches added across `vera/codegen/compilability.py::_scan_io_ops` (4: `IndexExpr`, `ArrayLit`, `InterpolatedString`, `AnonFn`), `vera/codegen/compilability.py::_scan_expr_for_handlers` (5: `QualifiedCall`, `IndexExpr`, `ArrayLit`, `InterpolatedString`, `AnonFn`), and `vera/wasm/inference.py::_infer_expr_wasm_type` (2: `AnonFn`, `ModuleCall`).  Plus 8 defensive branches in `_infer_vera_type` (`Block`, `MatchExpr`, `HandleExpr`, `AssertExpr`, `AssumeExpr`, `AnonFn`, `QualifiedCall`, `ModuleCall`).  No user-visible behaviour change today — every defensive add was masked by an upstream guard (type checker rejection, `[E602]` codegen-skip, closure-pipeline sibling scan, translator-side registration in `calls_math.py`/`calls_containers.py`/etc.).  Plugs the gap if any upstream mechanism is relaxed in the future, preventing the silent-skip class from reappearing.

- **[#597](https://github.com/aallan/vera/issues/597) pr-review-toolkit follow-ups** (CodeRabbit + multi-agent audit) — five additional fixes landed in the same PR after the initial commit:
  - `scripts/check_walker_coverage.py` — replaced hardcoded `WALKER_FILES` list with `_discover_walker_files()` globbing `vera/**/*.py` for the `WALKER_COVERAGE:` marker.  The hardcoded list silently skipped any new walker file added without manually updating it — replicating the exact silent-skip class this script was written to close.
  - `scripts/check_walker_coverage.py` — anchored `extract_checklist_classes` to the WALKER_COVERAGE block (marker to next `"""`).  Pre-fix the regex ran over the whole function body so a `# Foo → bar`-shaped comment outside the block could silently count as coverage.
  - `vera/wasm/inference.py::_infer_vera_type` — `AnonFn` / `QualifiedCall` / `ModuleCall` defensive branches now return `None` instead of synthesising a fake `FnCall(name, args)` (which dropped the `qualifier` / `path` field and could match a same-name local fn from a different module).  `_infer_expr_wasm_type::ModuleCall` also returns `None` for the same reason.
  - `vera/wasm/inference.py::_infer_vera_type` — removed dead `if expr.expr is not None` guards on `Block`/`HandleExpr` defensive branches.  Both fields are non-Optional in the AST schema (`vera/ast.py:470, 481`); the guards were unreachable defensive code that hid the schema invariant.
  - `vera/codegen/compilability.py` — corrected misleading "masked by closure pipeline" comments on the `AnonFn` defensive branches of `_scan_io_ops` and `_scan_expr_for_handlers`.  `_compile_lifted_closure` does NOT call these scanners on lifted bodies, so the `AnonFn` branch is the PRIMARY defence (not redundant); the comment now states this directly.

### Tests

- **[#597](https://github.com/aallan/vera/issues/597) regression coverage** — two new test files pinning the audit machinery:
  - `tests/test_walker_defensive_branches_597.py` — 21 synthetic-AST tests covering all 11 defensive `isinstance` branches plus the 5 fixed-then-pinned `_infer_vera_type` cases.  Without these, a future refactor breaking a defensive branch would land silently (no production path exercises them today).
  - `tests/test_check_walker_coverage_597.py` — 12 unit tests for the enforcement script's parsing logic (Expr subclass extraction, isinstance flattening, checklist-block anchoring including the CR-3 regression case, auto-discovery invariants, end-to-end main).

### Internal

- **ROADMAP cleanup** — removed the stale `#604` row (Stabilisation tier Order 1) that PR `#659` had closed via code fix but not deleted from the roadmap.  Stabilisation tier renumbered 1-6; Agent-integration tier renumbered 7-9.  Added `#667` ("SMT translator coverage expansion: FloatLit/ArrayLit/IndexExpr") as new Stabilisation tier Order 6 — surfaced during the walker audit as a latent gap in `smt.translate_expr`, deferred from this PR per Option A scope decision because closing it requires extending the contract grammar (parser + checker work) beyond the audit-scope brief.

## [0.0.150] - 2026-05-12

### Fixed

- **[#559](https://github.com/aallan/vera/issues/559)** — nested type aliases (alias-of-alias via `Array<…>`, e.g. `type Row = Array<Int>; type Grid = Array<Row>;`) now compile and run correctly when indexed through both layers.  Pre-fix `vera/wasm/inference.py::_alias_array_element` extracted the array element type but did not canonicalise it — for `@Grid.0`, it walked `Grid → Array<Row>` and returned `NamedType("Row")` rather than the canonical `NamedType("Array", (Int,))`.  Downstream consumers saw the opaque alias name and either fell through (chained-indexing branch in `_infer_index_element_type_expr` checks `inner_te.name == "Array"`, fails on `"Row"`) or emitted a load-as-i32 + `i64.extend_i32_u` against what is actually a heap pointer to an (`Array<Int>`) pair — producing `type mismatch: expected a type but nothing on stack` at WASM validation (or, when the bug surfaced on a private helper, the misleading `unknown func: $caller` symptom described in the issue body).  Post-fix the helper runs the extracted element through the existing `_canonical_named_type` walker (the #630 canonicalisation seam), so a `Row` element resolves to `Array<Int>` and downstream lookups see the real shape.  Falls back to the original unresolved NamedType when the canonical walk terminates at a non-NamedType (e.g. `FnType` element), preserving the pre-fix contract for the direct `Array<T>` path.  Two new regression tests in `tests/test_codegen.py::TestCompoundArrays` pin the `array_length(@Grid.0[0])` and `@Grid.0[1][0]` shapes.  Closes `#559`.

## [0.0.149] - 2026-05-12

### Fixed

- **[#648](https://github.com/aallan/vera/issues/648)** — cyclic type aliases now produce a clean `[E132]` diagnostic at `vera check` time instead of crashing `vera compile` with `RecursionError`.  Pre-fix `vera/checker/registration.py::_register_alias` resolved aliases one at a time; when `type A = B` was processed before `B` was registered, the forward-reference fallback in `_resolve_type` returned a placeholder rather than chasing the chain, so the resolved-type representation reached the post-registration state with no observable cycle.  Codegen later stored the raw AST `type_expr` and `vera/codegen/core.py::_type_expr_to_wasm_type` chased the chain through the AST, blowing the stack with `RecursionError: maximum recursion depth exceeded`.  Post-fix `_register_all` calls a new `_check_alias_cycles` pass that walks every alias's AST `type_expr` chain (following `NamedType`-of-alias references through `RefinementType` wrappers, mirroring codegen's recursion shape) and emits `[E132]` ("Cyclic type alias") with the originating decl location, the full cycle path (`A -> B -> C -> A`), and a `Fix:` paragraph pointing at `data`-declared ADTs as the alternative for self-referential types.  Defensive cycle guards on the alias-walking helpers in `vera/wasm/inference.py` (closed in #633) remain as belt-and-braces.  Closes `#648`.

## [0.0.148] - 2026-05-12

### Fixed

- **[#660](https://github.com/aallan/vera/issues/660)** — `vera check` now rejects parameterised type-alias references with wrong arity.  Pre-fix `vera/checker/resolution.py::_resolve_type` silently truncated `zip(alias.type_params, te.type_args)` on length mismatch, leaving alias-local type-vars unsubstituted; downstream codegen leaked literal alias-local names into mono suffixes (`option_map$Int_B` instead of `option_map$Int_Int`) and the call site referenced a non-existent function-table entry → `unknown table 0: table index out of bounds` at runtime.  Surfaced by the #659 multi-agent review when the #604 fix happened to surface the same bug class via a different entry point (a `SlotRef` typed as a parameterised alias with too few type-args).  Post-fix the checker emits `[E133]` ("Type alias arity mismatch") with a precise diagnostic naming the alias, expected/supplied counts, and a `Fix:` paragraph suggesting the missing or extra type arguments.  Two defensive comments in `vera/codegen/monomorphize.py::_resolve_arg_fn_shape` and `vera/wasm/calls.py::_resolve_arg_fn_shape_wasm` (left in PR #659 to document the latent gap) are now trimmed to one-line "arity enforced upstream by checker" cross-references.

### Internal

- **[#661](https://github.com/aallan/vera/issues/661)** — investigated and confirmed the `compiled_mono_bases` cross-module name-collision concern is not reachable today.  Pass 2.5 in `vera/codegen/core.py::compile_program` (lines 519-530) explicitly skips imported FnDecls whose names are already in `fn_visibility` (= local declarations), so an imported forall decl with the same name as a local one is dropped before its template warning could be emitted.  And `forall_decl_names` is built from `program.declarations` only, never from imports — only local forall decls are eligible for suppression.  Net effect: at most one template warning per base name lands in `self.diagnostics`, so a bare-name match in the suppression filter cannot cross-suppress between modules.  Added an explanatory block comment at the suppression site documenting why bare-name keying is safe AND naming the trigger conditions that would invalidate the invariant (loosened Pass 2.5 dedup, or mono pipeline starting to carry module attribution).  Added `tests/test_codegen_modules.py::TestCrossModuleNameCollision661` with two tests pinning the invariant — compiles a name-shadowing fixture and asserts no over-broad suppression.

## [0.0.147] - 2026-05-12

### Fixed

- **[#628](https://github.com/aallan/vera/issues/628)** — cross-module imports now propagate `_fn_ret_type_exprs` alongside `_fn_sigs`.  Pre-fix `vera/codegen/modules.py`'s cross-module harvest only populated `_fn_sigs` (carrying WASM type info — sufficient for call-validation), but the `_fn_ret_type_exprs` registry (added in #614, re-used by #602) was never harvested across modules.  A `String`- or `Array<T>`-returning fn defined in module A and called from module B then hit `_fn_ret_type_exprs.get(name) → None` and fell through to the silent-skip path that #602 / #614 had already closed in-module.  Two failure shapes: (1) `make_arr(())[0]` where `make_arr` is cross-module — IndexExpr element-type inference returned None, enclosing function dropped via `[E602]`; (2) `IO.print("\(make_str(()))\n")` where `make_str` is cross-module — interpolation segment fell through to the `to_string(...)` silent wrapper, tripping `expected i64, found i32` at WASM validation.  Post-fix the cross-module harvest in `vera/codegen/modules.py` populates `_fn_ret_type_exprs` with the same `setdefault` shape as `_fn_sigs`.  Closes `#628`.

## [0.0.146] - 2026-05-12

### Fixed

- **[#655](https://github.com/aallan/vera/issues/655) Shape B** — array indexing through a refinement-of-Array alias (e.g. `type NonEmptyArray = { @Array<Int> | array_length(@Array<Int>.0) > 0 }` plus `fn head(@NonEmptyArray -> @Int) { @NonEmptyArray.0[0] }`) now compiles cleanly and runs correctly.  Pre-fix `vera/wasm/inference.py::_alias_array_element` only followed `isinstance(target, ast.NamedType)` chains when resolving an alias to its underlying `Array<T>`; if the alias target was a `RefinementType` (which the user's refinement syntax produces), the helper returned `None`.  Downstream `_infer_index_element_type` then returned `None` for `@NonEmptyArray.0[0]`, the `head` function got dropped via `[E602]` with "body contains unsupported expressions — skipped", and any call site referenced a non-existent `$head` → `unknown func: $head` at WASM validation.  Post-fix the alias-target lookup peels any `RefinementType` layers before checking for a `NamedType` base, so refinement-of-Array aliases resolve their element type the same as a bare `Array<T>`.  Closes `#655` (Shape A was closed in v0.0.145; Shape B is this fix).  Allowlist in `scripts/check_e602_clean.py` shrinks from 6 to 5 entries.

## [0.0.145] - 2026-05-11

### Fixed

- **[#604](https://github.com/aallan/vera/issues/604) / [#655](https://github.com/aallan/vera/issues/655) Shape A** — generic prelude combinator mono clones now produce the correct type-arg suffix when the closure argument is a `SlotRef` typed as an FnType alias (e.g. `@Doubler.0` where `type Doubler = fn(Int -> Int)`).  Pre-fix `_unify_param_arg` in `vera/codegen/monomorphize.py` had an `AnonFn`-specific alias-resolution path; `SlotRef` args typed as FnType aliases skipped that path and left the closure's return type variable unbound.  The unbound type var fell to the `"Bool"` phantom-var fallback at result-building time, producing mono suffixes like `option_map$Int_Bool` instead of `option_map$Int_Int` and trapping at runtime with `wasm trap: indirect call type mismatch`.  Post-fix: both `AnonFn` literals and `SlotRef`-typed-as-FnType-alias args flow through a shared `_resolve_arg_fn_shape` helper, binding the closure's return type uniformly.  Same fix applied at the WASM call-site rewriting layer (`vera/wasm/calls.py::_infer_fn_alias_type_args_wasm`).  Three of the five `[E602]`/`[E604]` prelude-skip cases (`option_map`, `option_and_then`, `result_map`) close at runtime; the other two (`option_unwrap_or`, `result_unwrap_or`) were already working via mono and only emitted misleading template warnings.

- **[#604](https://github.com/aallan/vera/issues/604) / [#655](https://github.com/aallan/vera/issues/655) Shape A — template-warning suppression** — audit recommendation 2 from the #604 investigation comment: post-compile suppression pass in `vera/codegen/core.py::compile_program` drops `[E602]` / `[E604]` / `[E605]` template-only warnings on generic `forall<T>` decls whose mono clones successfully compile.  Pre-fix every program importing the prelude saw 5 spurious warnings about `option_unwrap_or` / `option_map` / `option_and_then` / `result_unwrap_or` / `result_map` even when those functions worked end-to-end via mono.  Post-fix the warnings only fire for forall decls whose generic body cannot be compiled AND has no working mono clone — preserving the "this generic can never compile and you're never using a mono clone of it" signal for genuinely-broken or unused user generics, while removing the prelude-noise.  Allowlist shrinks from 11 to 6 entries (5 user-code generics from #655 Shape A removed; the 6 remaining are the prelude generics still firing in test files that don't call them, plus the `head` real codegen gap).

- **Documentation fix in `CLAUDE.md` release-workflow section** — "Stage 9 table" reference (stale since the project moved through Stages 10, 11, 12) replaced with a stage-agnostic instruction: "the **most recent Stage table** in `HISTORY.md`" with a `grep "^## Stage" HISTORY.md | tail -1` hint for confirming the current stage before writing.  Caught during the 2026-05-11 review cycle.

### Added

- **Layer 3 of [#626](https://github.com/aallan/vera/issues/626)** — new `vera/skip.py` with two control-flow exception classes: `CodegenSkip(node, reason)` (raised when a translator hits an unsupported AST shape; caught at the `_compile_fn` / `_compile_lifted_closure` boundary and converted to a structured `[E602]` diagnostic with the unsupported-node's source span) and `CodegenInvariantError(msg, node=None)` (raised on states that type-check should have rejected; surfaced as a new `[E699]` "Internal compiler error" at severity=`error` so `vera compile` exits non-zero — compiler bugs shouldn't be maskable as soft warnings in CI logs).  An audit of all 372 `return None` sites in `vera/codegen/**` and `vera/wasm/**` classified each into SILENT_SKIP / PROPAGATE / OPTIONAL_RETURN / INVARIANT_DEFENSIVE buckets; **104 SILENT_SKIP sites converted to `raise CodegenSkip`** in this PR (55 in `calls_arrays.py` via a shared `_array_elem_triad_or_skip` helper, 24 in `data.py`, 11 in `calls_containers.py`, 9 in `calls_handlers.py`, 4 in `context.py`, 1 in `calls.py`).  Pre-conversion these all silently dropped to a generic enclosing-function-level `[E602]`; post-conversion they each emit a source-located diagnostic pointing at the specific unsupported expression.  The remaining 39 INVARIANT_DEFENSIVE sites and the 154 PROPAGATE sites that may now be unreachable are tracked in [#657](https://github.com/aallan/vera/issues/657).

- **New `_error()` API on the codegen builder** (`vera/codegen/core.py`) parallel to the existing `_warning()` method, hardcoding `severity="error"`.  Used by the `[E699]` catch handlers in `_compile_fn` (`vera/codegen/functions.py`) and `_compile_lifted_closure` (`vera/codegen/closures.py`) — both updated in this PR to route `CodegenInvariantError` through `_error()` rather than `_warning()` so internal-compiler-error diagnostics propagate to a non-zero CLI exit code rather than a swallowable warning.  Only those two internal handlers changed; no user-facing API surface or other `_warning()` call sites were modified.

- **Layer 1 of [#626](https://github.com/aallan/vera/issues/626)** — new `scripts/check_e602_clean.py` pre-commit + CI gate that fails when any compile of an example or conformance program emits `[E602]` (body unsupported) or `[E604]` (param unsupported) outside an explicit allowlist.  The `[E602]` warning channel was the project's only signal for silent translator-skip failures, and several long-standing instances of it were buried in every WASM compile — making it impossible to spot a new genuine skip without manually sifting through expected noise.  The gate makes a new silent skip a hard build failure unless explicitly allowlisted with a tracking-issue reference.
- **Allowlist of 11 currently-expected silent skips**, each tagged with a tracking issue: 5 prelude combinators (`option_unwrap_or` / `result_unwrap_or` / `option_map` / `option_and_then` / `result_map` — [#604](https://github.com/aallan/vera/issues/604)), 6 user-code cases surfaced by the new gate's first run (5 generic-decl spurious warnings + 1 real codegen gap — [#655](https://github.com/aallan/vera/issues/655)).

### Documentation

Small docs sweep — closes six aging documentation issues in one PR.  No code changes; touches `spec/02-types.md`, `spec/03-slot-references.md`, `spec/06-contracts.md`, `SKILL.md`, `README.md`, and `HISTORY.md`.

- **[#557](https://github.com/aallan/vera/issues/557)** — `spec/03-slot-references.md` Example 9 said the match-arm pattern binding "shadows the function parameter" without defining "shadow".  Two readings were equally consistent with the prose (replace vs push-on-top); the compiler implements push-on-top.  Replaced the one-liner with an explicit paragraph spelling out push-on-binding semantics, the resulting De Bruijn ordering for multi-field constructors (leftmost = deepest = highest index, rightmost = shallowest), and the non-commutative-operations caveat that exposes the rule.

- **[#561](https://github.com/aallan/vera/issues/561)** — two tier-accuracy bugs in `spec/06-contracts.md`.  (1) §6.3.1 (Tier 1) was missing pure-fn calls in `ensures` / `@T.result` / `if/then/else`, which actually verify at Tier 1 today; §6.3.2 (Tier 2 NYI) incorrectly listed them.  Moved all three from §6.3.2 to §6.3.1.  (2) §6.3.3 said "Bounded quantification is decidable for finite bounds and is handled by Z3 via finite unrolling" — that reads as Tier 1, but Tier 2 (the tier that would handle quantifier unrolling) is [#427](https://github.com/aallan/vera/issues/427) NYI.  Clarified that every `forall`/`exists` in a contract falls to Tier 3 today, both for `forall` and the symmetric `exists` text.

- **[#560](https://github.com/aallan/vera/issues/560)** — the `invariant(...)` clause on `data` declarations is documented in `spec/02-types.md` §2.4.1, `spec/06-contracts.md` §6.2.3, and `SKILL.md`, but every documented form fails with `[E130] no <DataName> bindings in scope` at v0.0.144.  Added inline NYI markers at all three sites pointing at #560, with the working alternative (refinement types).  Added the limitation to `spec/06-contracts.md`'s §6.9 Limitations table.

- **[#607](https://github.com/aallan/vera/issues/607)** — added a new `spec/02-types.md` §2.2.1 "`Int` and `Nat` compatibility" subsection covering the bidirectional subtyping (`Nat <: Int` always; `Int <: Nat` permitted with verifier-discharged obligation).  Practical-implication note tells agents not to insert `nat_to_int` defensively when calling `array_length` etc. into `@Nat` positions.  Cross-reference added to `SKILL.md`'s "Primitive types" listing.

- **[#608](https://github.com/aallan/vera/issues/608)** — added `SKILL.md` "IO model: terminal vs browser" subsection (under §Browser compilation) explaining that programs using `IO.sleep` + ANSI escapes for terminal pacing/rendering compile cleanly to `--target browser` but render escapes as literal text and busy-wait the main thread.  The recommended browser pattern is "Vera pure simulation core + JS driver via `requestAnimationFrame`".  Two runtime gaps that would make the recommended pattern more ergonomic are tracked separately ([#609](https://github.com/aallan/vera/issues/609) JSPI sleep, [#610](https://github.com/aallan/vera/issues/610) ANSI subset interpreter).  `README.md`'s "write once, run anywhere" line qualified to acknowledge the IO seam.

- **[#512](https://github.com/aallan/vera/issues/512)** — trimmed all 31 Stage 11 rows in `HISTORY.md` (v0.0.112 → v0.0.138) to match the early-stage one-sentence format established in Stage 1–8.  Per the canonical template now in long-term memory: `**X** ([#N]).` per row, no em-dash separator, no secondary clauses, no implementation detail.  Detailed mechanism descriptions for each version stay in CHANGELOG under their respective `## [0.0.X]` section.

### Changed

- **mypy 1.20.2 → 2.0.0** (`pyproject.toml`, `uv.lock`).  Mypy 2.0 enables three flags by default that were opt-in under 1.x: `--local-partial-types` (changes inference of types based on assignments in other scopes), `--strict-bytes` (per [PEP 688](https://peps.python.org/pep-0688): `bytearray` and `memoryview` no longer assignable to `bytes`), and `--allow-redefinition` behaves like 1.x's `--allow-redefinition-new` (more flexible variable redefinition across blocks).  Running mypy 2.0 against `vera/` produced **zero errors** with the existing source — no compiler-source changes needed to clear the upgrade.  Manual upgrade in favour of dependabot PR #647 (closed in favour of this change).

## [0.0.144] - 2026-05-11

### Fixed

- **[#633](https://github.com/aallan/vera/issues/633)** — `_resolve_base_type_name` (in `vera/wasm/inference.py`) now carries an explicit `_seen` cycle-detection accumulator, restoring consistency with the post-#630 `_canonical_named_type` walker that already had one.  Defence-in-depth: cyclic type aliases are user errors that should be rejected upfront by the type checker (tracked separately as [#648](https://github.com/aallan/vera/issues/648)), but a bug in the upstream rejection must not turn into a `RecursionError` inside codegen.

- **[#634](https://github.com/aallan/vera/issues/634)** — `SlotRef` and other AST nodes constructed inside `InterpolatedString.parts` now carry source spans in **original-source coordinates** instead of synthetic-wrapper coordinates.  `_parse_interp_expr` previously wrapped each interpolation expression in a dummy `private fn interpExpr(...) { <SEGMENT> }` function for parsing, with the segment placed at wrapper line 3, column 3 — and Lark-emitted spans inside the parsed body were never translated back to the original source.  The result: `[E615]` (and other) diagnostics on interpolated expressions landed on line 3 of the user's file, regardless of where the offending string literal actually was.  `_split_interpolation` now records each segment's offset within the raw string, `string_lit` computes the segment's original line/column from the outer string-literal span, and `_parse_interp_expr` walks the parsed AST and remaps every `Span` field via a new `_remap_spans_inplace` helper.  The previously-softened assertion in `TestE615LoudInterpolationFallthrough630::test_e615_fires_on_adt_in_interpolation` is tightened to pin both line **and** column of the diagnostic at the SlotRef's position, and `test_multiple_e615_in_one_interpolation` now pins per-segment column fidelity (two SlotRefs in the same string get two distinct, correct columns).

- **[#556](https://github.com/aallan/vera/issues/556)** — the user-visible bug class (calling a user-defined `@Unit`-returning function in statement position trips a WASM `type mismatch: expected a type but nothing on stack` validation error) was already fixed by #584's `_is_void_expr` work in v0.0.135.  The specific repro shape from the original #556 report — a *pure* helper (no IO effect) followed by a unit-literal final expression — wasn't pinned by the existing conformance test (which covered IO-effect variants only).  Added `TestUserUnitFnInStatementPosition556` to `tests/test_codegen.py` with two cases: the exact #556 repro plus the where-block variant from the follow-up comment.  Pinning the specific shape so it can't silently regress.

- **[#591](https://github.com/aallan/vera/issues/591)** — three network-response UTF-8 decode sites in `vera/codegen/api.py` no longer leak Python `UnicodeDecodeError` text into Vera-level `Result::Err` strings.  Two strategies, chosen per-site based on user intent:
  - **`Http.get` / `Http.post`** — now decode response bodies with `errors="replace"`.  A remote server returning non-UTF-8 bytes (rare but real with misconfigured `Content-Type`) surfaces as U+FFFD substitutions inside the Ok-branch string, preserving the data.  User intent for these calls is "fetch this URL"; preserving the body beats preserving the (already-rare) signal that bytes weren't cleanly UTF-8.
  - **`Inference.complete`** — `_call_inference_provider` now catches `UnicodeDecodeError` explicitly and re-raises as `RuntimeError("Inference provider '<name>' returned a response body that is not valid UTF-8 (invalid byte at position N).")`.  The `host_inference_complete` wrapper's existing `except Exception` catches this and writes the Vera-shaped message into the Err branch.  Non-UTF-8 from an LLM API is genuinely broken; we want loud failure with a Vera-native message, not the `codec can't decode byte 0x...` Python form.  Three structural assertions added in `tests/test_runtime_traps.py::TestNetworkResponseUtf8Hygiene591`, mirroring the #589 coverage shape.

### Documentation

- **KNOWN_ISSUES.md** — removed entries for #633, #634, and #591 (closed in this release); added entry for the newly-filed [#648](https://github.com/aallan/vera/issues/648) (cyclic-alias `RecursionError` — the upstream bug discovered while implementing #633).

## [0.0.143] - 2026-05-10

### Fixed

- **Windows compatibility** — three Windows-specific bugs surfaced when PR #639 added `windows-latest` to the CI test matrix in advisory mode (`continue-on-error`).  All three close in this release, and the matrix flips to fully strict (Windows entries are now merge gates alongside Ubuntu / macOS):

  - **[#640](https://github.com/aallan/vera/issues/640)** — Vera CLI's `/dev/stdin` path is Unix-only.  `_load_and_parse(path)` in `vera/cli.py` previously called `Path('/dev/stdin').read_text()`; on Windows the path doesn't exist as a filesystem entry and `vera <subcmd> /dev/stdin` failed with `Error: file not found: /dev/stdin`.  Now reads from `sys.stdin` directly when `path in _STDIN_PATHS` — portable across Unix and Windows, and more semantically correct (the user's intent is "read from stdin", not "read from a specific file path").  Closes 6 failing tests in `tests/test_cli.py::TestStdinInput`.

  - **[#641](https://github.com/aallan/vera/issues/641)** — default cp1252 file I/O encoding caused `UnicodeEncodeError` / `UnicodeDecodeError` on Windows for tests reading or writing files containing `→` or `—` characters.  Set `PYTHONUTF8=1` in the CI test job environment so Python's text-mode `open()` defaults to UTF-8 regardless of locale (PEP 540), and added explicit `encoding='utf-8'` to `vera/parser.py`'s grammar load (the load-bearing site that runs on every parse).  Closes ~9 failing tests across `test_codegen.py`, `test_codegen_monomorphize.py`, `test_codegen_closures.py`, `test_html.py`.  Broader audit of `open()` / `read_text()` / `write_text()` call sites for explicit `encoding='utf-8'` queued as a follow-up — for now CI is covered via `PYTHONUTF8=1`, and locally users on Windows without `PYTHONUTF8=1` may still hit the bug on individual files.

  - **[#642](https://github.com/aallan/vera/issues/642)** — `tests/test_codegen.py::TestIOOperations::test_io_read_file_success` and `test_io_read_file_roundtrip` embedded Windows tempfile paths (e.g. `C:\Users\runner\AppData\...`) into Vera string literals via f-string interpolation.  Vera's grammar correctly rejected `\U` as an invalid escape sequence, producing `[E009]` at parse time.  Fix in test fixtures: convert the path to POSIX form via `tmp_path.replace(os.sep, '/')` before embedding (Windows file APIs accept forward slashes).

### Changed

- **CI test matrix is now fully strict on Windows.**  PR #639's advisory `continue-on-error: ${{ matrix.os == 'windows-latest' }}` is removed in this release — the three Windows entries (`{3.11, 3.12, 3.13}`) now block merges alongside Ubuntu and macOS.  Total matrix coverage: 9 entries (3 OSes × 3 Python versions).

## [0.0.142] - 2026-05-08

### Fixed

- **[#630](https://github.com/aallan/vera/issues/630)** + **[#632](https://github.com/aallan/vera/issues/632)** + **[#635](https://github.com/aallan/vera/issues/635)** + **[#636](https://github.com/aallan/vera/issues/636)** — close the #602 bug class structurally across all four sites.  After ten distinct triggers across PRs #627 and #629, each fixed locally with one more `isinstance` handler or one more inference site, the discovery rate (9th and 10th triggers landing within hours of #630 being filed) outpaced reactive fixing.  This release consolidates the canonicalisation surface into a single walker, makes the silent amplifier loud at every site (interpolation, apply_fn / call_indirect), substitutes parameterised aliases in both inference and compilability paths, and propagates closure-body failures up to drop enclosing functions cleanly.

  **Tier 1 — centralised canonicalisation.** The pre-#630 codebase carried six overlapping canonicalisation helpers in `vera/wasm/inference.py` (plus an unaudited seventh in `vera/wasm/calls_arrays.py`), each handling a subset of (a) `RefinementType` unwrap, (b) alias-chain follow, (c) generic substitution, (d) `type_args` formatting.  Site by site, ad-hoc walks at the apply_fn dispatchers, FnType-return helpers, and IndexExpr branch independently re-implemented combinations of these concerns and missed the rest — accumulating triggers 1–10 of the i32_pair-into-i64 mismatch bug class.

  Replaced with two helpers — `_canonical_named_type` (the core walker: iteratively unwraps RefinementType, applies optional alias_map for generic substitution, follows NamedType alias chains, returns canonical `NamedType` or None) and `_canonical_wasm_type` (thin convenience wrapper that maps the canonical name to a WASM type).  Migrated all callers:

  - `_format_named_type_canonical` — now a 3-line delegate.
  - `_resolve_i32_pair_ret_te` — now a 2-line delegate (kept for #628 cross-module work; otherwise inline-able).
  - `_fn_type_return_wasm` — single-line delegate.
  - `_resolve_generic_fn_return` — builds `alias_map` from the generic params + concrete args, single delegate call.
  - `_infer_fncall_vera_type` apply_fn dispatcher — collapsed from a 75-line nested `isinstance` ladder over `(SlotRef, AnonFn)` × `(generic, non-generic)` × `(NamedType, RefinementType)` shapes to 18 lines: extract closure return TypeExpr + alias_map, call walker once.  Future closure-arg shapes (`FnCall` returning a closure, `IfExpr` selecting between closures, etc.) plug into the same dispatch with no new isinstance ladder.
  - `_infer_apply_fn_return_type` — same consolidation as above.
  - `_infer_index_element_type_expr` FnCall branch — now uses the canonical `NamedType` (with `type_args` preserved) directly to feed `_alias_array_element`.
  - `_infer_closure_return_vera_type` (in `calls_arrays.py`, used by `array_map`) — previously bare-`NamedType`-only; now handles refinements and alias chains.

  Deleted `_resolve_type_name_to_wasm_canonical` — functionally identical to `_resolve_base_type_name`, an unnoticed duplicate that had evolved in parallel.  All callers redirected.

  **Tier 2 — loud diagnostic on the silent amplifier.** The actual silent failure for ten triggers wasn't the inference miss itself — it was `vera/wasm/operators.py:482-486`, the `else` branch in `_translate_interpolated_string` that wrapped any unrecognised-type segment with `to_string(...)`.  `to_string` reads its argument as `i64`; an `i32_pair` (String/Array) value then tripped `expected i64, found i32` at WASM validation.  That fallthrough turned every canonicalisation gap into invalid emission rather than a clean compile-time skip.

  Added new error code [E615] "Cannot interpolate value of unknown type — type inference failed".  Converted the silent fallthrough to record the offending segment on `WasmContext._interp_inference_failures`, then return None.  `CodeGenerator._compile_fn` harvests the failures and emits [E615] for each before falling through to the existing [E602] skip — same loud-skip mechanism that any other unsupported expression triggers, but now with a specific E-code pointing at the actual inference gap rather than a generic "unsupported expressions".

  **Net effect.** Six canonicalisation helpers → two.  Seventy-five-line apply_fn dispatcher × two sites → eighteen lines × two.  Silent miscompilation on inference miss → clean compile-time skip with specific [E615] diagnostic.  All ten existing #602-class regression tests in `TestStringInterpolation` continue to pass — pure refactor + diagnostic conversion, no behavioural change for valid programs.  Seven new regression tests under `TestE615LoudInterpolationFallthrough630` cover: ADT interpolation (the canonical E615 trigger), `Result<T,E>` interpolation (parallel ADT shape), closure-body E615 (the silent-failure-hunter C1 finding — without harvest in `closures.py` the closure was silently dropped), per-function isolation of `_interp_inference_failures`, multiple-failures-per-function (UX — one [E615] per failing segment instead of N round-trips), terminal-NamedType type_args propagation (the CodeRabbit + code-reviewer flagged latent bug — alias-bound type_args propagate through the walker), and `array_map` over a refinement-returning closure (previously-unaudited `_infer_closure_return_vera_type` path now handling refinements).

  PR review pass found four additional review-pass items addressed in the same PR.  CodeRabbit + the code-reviewer agent independently identified that `_canonical_named_type`'s `outer_type_args` capture rule (always read from the *first* NamedType) lost type_args when an `alias_map` substitution bound a generic param to a parameterised type — fixed by always reading from the *terminal* NamedType.  The silent-failure-hunter agent caught the closure-body harvest gap — fixed by extracting the harvest into `CodeGenerator._harvest_interp_inference_failures` and calling it from both `_compile_fn` and `_compile_lifted_closure`.  Comment-analyzer flagged trigger-count drift in `operators.py` and a "plug in here" overstatement — both corrected.  Multiple-failures-per-function was added as a UX improvement (`had_failure` flag in `_translate_interpolated_string` so all failing segments surface in one compile pass).

  A second CodeRabbit review pass added five more findings, all addressed.  The `_canonical_named_type` walker gained: (a) a unified cycle guard via a single `seen` set covering both `alias_map` substitution and `_type_aliases` chain following, so a self-referential alias_map (`{T: NamedType("T")}`) can no longer loop forever; (b) `Future<T>` transparency in the `_canonical_wasm_type` convenience wrapper, parallel to `_slot_name_to_wasm_type`'s existing `Future<T>` strip-and-recurse handling; (c) parameterised-alias substitution via a new `_substitute_type_vars` helper, so following `type Box<T> = Array<T>` with a concrete `Box<Int>` substitutes `T → Int` in the alias body before continuing the walk.  Tests strengthened: `test_canonical_named_type_terminal_args_propagation` switched from a non-parameterised alias (`type IntList = Array<Int>`) to a parameterised one (`type Box<T> = Array<T>`) so it actually exercises the substitution path; `test_per_function_isolation_of_failures_list` added a `clean_after` function so the test catches forward leakage from `dirty` rather than only backward.

  Pragma audit closed for the canonicalisation cluster — the disproved `# pragma: no cover` on closure-return-RefinementType (PR #629) was removed during the cluster migration, plus a `# pragma: no cover — defensive` claim on `_compile_lifted_closure`'s body-instrs-None path (now provably reachable through the new E615 path).  Broader audit (verifying every prose-bearing pragma claim across the WASM codegen) is queued as a follow-up.

  Final review pass closed three more sites in the same PR rather than landing them as follow-ups.  **#635** (parameterised-alias substitution in `_type_expr_to_wasm_type` — the compilability check's parallel of the walker fix): extracted `substitute_type_vars` as a module-level free function so both `InferenceMixin` and `CodeGenerator` can use it; `type Id<T> = T; @Id<Array<Int>>` now compiles end-to-end.  **#632** (apply_fn / call_indirect E616 diagnostic): `_translate_apply_fn` now records unhandled closure-arg shapes on `_apply_fn_inference_failures`, harvested as `[E616]` before the function-skip `[E602]`, so `apply_fn(make_mapper(()), 7)` (where `make_mapper` is a FnCall returning a closure) now produces a source-located diagnostic instead of a WASM-validation trap.  **#636** (closure-body fail drops enclosing fn): `_lift_pending_closures` now reports whether any closure body failed; `_compile_fn` checks the flag and drops the enclosing top-level fn with a specific `[E602]`, so the module no longer carries a `call_indirect` to a missing function-table entry.

  **Final state.** Six canonicalisation helpers → two; ten triggers structurally closed at four sites (interpolation `[E615]`, IndexExpr-of-FnCall, FnType-alias / generic FnType return, apply_fn `[E616]`, compilability `_type_expr_to_wasm_type`, closure-body propagation).  Every silent miscompilation in the bug class is now either structurally impossible or surfaces as a source-located diagnostic + clean function skip.  Eleven regression tests in `TestE615LoudInterpolationFallthrough630` pin the closures.

  Remaining follow-ups (out of scope for this PR, smaller polish items): [#628](https://github.com/aallan/vera/issues/628) (cross-module `_fn_ret_type_exprs` propagation), [#626](https://github.com/aallan/vera/issues/626) Layer 1 (pre-commit gate on `[E602]` across the conformance suite), [#633](https://github.com/aallan/vera/issues/633) (cycle-guard alignment for `_resolve_base_type_name`), [#634](https://github.com/aallan/vera/issues/634) (SlotRef-in-interpolation source-span fidelity), and the duplicate `_type_expr_name` / `_type_expr_to_slot_name` in `inference.py`.

## [0.0.141] - 2026-05-08

### Fixed

- **Inline-refinement return types** in `_infer_fncall_vera_type` and `_infer_index_element_type_expr` — third trigger of the same bug class as #602 (i64/i32 mismatch at WASM validation) and the type-alias case fixed in v0.0.140.  Surfaced during PR #627's review (CodeRabbit duplicate-comment escalation, merged before the fix landed in #627 itself).

  When a fn declares an inline refinement return type (`@{ @String | predicate }`), `_register_fn` stores the literal `RefinementType` AST in `_fn_ret_type_exprs`.  v0.0.140's fix only handled the `NamedType` case via `isinstance` — `RefinementType` fell through to None, `_translate_interpolated_string` substituted `to_string(...)` for an `i32_pair` value, same #602 trap with a different trigger.  Same gap also lived in `_infer_index_element_type_expr`'s FnCall branch (the path #614 added) for refinement-`Array<T>` returns indexed via `f()[i]`.

  Fix: extracted the i32_pair return-type resolution into a helper `_resolve_i32_pair_ret_te` that handles both `NamedType` (with alias resolution via `_resolve_base_type_name`) and `RefinementType` (recursive unwrap of arbitrary nesting depth, then resolve).  Applied to both i32_pair branches (non-generic + generic-mono) and to the parallel IndexExpr inference path.

  PR review pass surfaced five more triggers of the same bug class.  **Nested refinements** (`@{ @{ @String | p1 } | p2 }`) are reachable per the grammar; the single-layer `if isinstance(...): unwrap` in the initial v0.0.141 fix fell through to None for nested forms.  Replaced with a `while isinstance(ret_te, ast.RefinementType): ret_te = ret_te.base_type` loop covering arbitrary nesting depth, applied to both `_resolve_i32_pair_ret_te` and the parallel IndexExpr branch.  The **`apply_fn` / `FnType`-alias** path (`apply_fn(@FnAlias.0, ())` where `FnAlias`'s return type wraps refinements) had three separate inference sites that walked `FnType.return_type` and only handled `NamedType` directly: `_infer_fncall_vera_type`'s apply_fn branch, `_resolve_generic_fn_return`, and `_fn_type_return_wasm`.  Same `while`-loop unwrap applied symmetrically at all three.  And the **`apply_fn`-over-aliased-`FnType`** path (e.g. `type Str = String; type Maker = fn(Unit -> Str) effects(pure);`) called `_format_named_type` directly on `NamedType("Str")`, returning the alias name; downstream interpolation's `vera_type == "String"` check missed and re-triggered the same trap.  Introduced `_format_named_type_canonical` (resolves `te.name` through the alias chain via `_resolve_base_type_name`, then formats with original `type_args`) and applied it to both branches of the apply_fn substitution.

  And the **inline `AnonFn` to `apply_fn`** path (`apply_fn(fn(@Unit -> @String) effects(pure) { ... }, ())`) — the SlotRef branch above was the only `apply_fn` arg shape handled; an inline anonymous closure literal fell through, `_infer_fncall_vera_type` returned None, and downstream interpolation re-triggered the same trap.  Added an `elif isinstance(closure_arg, ast.AnonFn)` branch alongside the SlotRef branch, simpler than the SlotRef path (no alias substitution — AnonFn carries `return_type` directly) but with the same RefinementType-unwrap + `_format_named_type_canonical` shape.

  And finally the **nested-refinement-`AnonFn`-on-the-WASM-side** path — same `apply_fn(fn(@Unit -> @{ @{ @String | p1 } | p2 }) ...)` shape but exercising `_infer_apply_fn_return_type` (call_indirect sig inference) rather than `_infer_fncall_vera_type` (Vera-type-name inference).  Inverse surface: `expected i32, found i64` rather than `expected i64, found i32`.  Pre-fix the AnonFn branch in `_infer_apply_fn_return_type` carried `# pragma: no cover — closure returns are not refinement types` with a single-level unwrap; the pragma was empirically disproved by the 9th and 10th triggers (an inline AnonFn *can* declare RefinementType returns per the grammar, and the type checker accepts nested forms).  Replaced single-level unwrap with the established `while`-loop shape and removed the disproven pragma.

  Eight new regression tests in `TestStringInterpolation` cover the inline-refinement String, the nested-refinement String, the refinement-over-alias String, the nested-refinement Array indexed via FnCall, the apply_fn-with-FnType-nested-refinement path, the apply_fn-over-`FnType`-aliased-String path, the apply_fn-over-inline-`AnonFn` path, and the apply_fn-over-nested-refinement-`AnonFn` (WASM-side) path.  All ten return-type shapes now verified — `f()` baseline (#602), type alias over String, inline refinement over String, nested refinement over String, refinement-over-alias, nested refinement over Array indexed via FnCall, `apply_fn` over a `FnType`-aliased nested refinement, `apply_fn` over an aliased `FnType` return, `apply_fn` over an inline `AnonFn`, and `apply_fn` over a nested-refinement inline `AnonFn` (WASM-side).

  The 9th and 10th triggers landing within hours of filing [#630](https://github.com/aallan/vera/issues/630) (the structural close-out tracking issue for this bug class) is the empirical argument for that issue: trigger discovery velocity outpaces local fix throughput.  Each new shape added to either dispatcher (Vera-type-name half or WASM-type half) is a fresh opportunity for the same bug.  The structural fix in #630 (centralised `_canonical_vera_type` + loud diagnostic on the silent fallthrough) is the queued close-out.

## [0.0.140] - 2026-05-08

### Fixed

- **[#602](https://github.com/aallan/vera/issues/602)** — `IO.print("\(make())")` (a `String`-returning function call as an interpolation segment) produced invalid WASM with `expected i64, found i32` at instantiation.  Root cause: `_infer_fncall_vera_type` in `vera/wasm/inference.py` mapped user-fn WAT return types back to Vera-type names for the `i64` / `i32` / `f64` cases but had no `i32_pair` branch; a fn returning `String` mapped to `None` here.  `_translate_interpolated_string` then fell through to the `to_string(...)` Int-conversion fallback wrapper, which reads its arg as `i64` — but the FnCall pushed `i32_pair`.  Same inference-gap shape as #614 (which was the *element-type* of an indexed FnCall result; this is the *return-type* inference half).

  Fix: extend the WAT-type → Vera-type fallback to consult `_fn_ret_type_exprs` (the registry added by #614) when the WAT type is `i32_pair`, so `String` and `Array<T>` returns are disambiguated.  Same registry, same pattern, same load-bearing infrastructure paying off twice.

  Two new tests in `TestStringInterpolation` cover the String-returning FnCall and an Array-returning FnCall indexed in interpolation — both classes of `i32_pair` return.

## [0.0.139] - 2026-05-08

### Fixed

- **[#614](https://github.com/aallan/vera/issues/614)** — `f()[i]` (indexing into a function-call result) silently dropped the enclosing function from the WAT output.  Root cause: `_infer_index_element_type_expr` in `vera/wasm/inference.py` only handled SlotRef and nested-IndexExpr collections; FnCall collections fell through to `return None`, propagating up until either `_compile_fn` skipped the function with an [E602] warning (top-level case — the same shape as #604) or `_compile_lifted_closure` returned None (closure case — silent: the registered closure_id was never added to the function table, so the call_indirect at the use site referenced a missing entry and WASM validation rejected the module with "unknown table 0: table index out of bounds at offset N").  Both manifestations close together.

  Fix: register each FnDecl's full Vera return-type expression in a new `_fn_ret_type_exprs` dict on `CodeGenerator` (alongside the WAT-type `_fn_sigs`), propagate it to the per-function and closure WasmContexts, and extend `_infer_index_element_type_expr` to look up the called fn's return type and extract the `Array<T>` element when applicable.

- **[#615](https://github.com/aallan/vera/issues/615)** — closure capture order miscompile, two failure shapes both rooted in `_collect_free_vars` returning captures unsorted and unfilled:

  1. **Non-contiguous outer slot.**  Closure body refs `@Int.k` while skipping `@Int.j` (j<k).  The lift-side env had no entry for the unreferenced outer index, so `env.resolve("Int", k)` returned None inside the closure body, body translation failed, the closure was dropped from the function table, and the call_indirect at the use site referenced a missing entry — WASM validation trap.  Concrete repro: a closure capturing `@Int.2` from outer scope while having a `@Int.1` (which the body doesn't reference) in the same scope.

  2. **Ascending walker-order silent miscompute.**  Even with contiguous captures, when source order put the lower outer_idx first (e.g. body `@Int.1 - @Int.2`), the walker added (Int,0) before (Int,1) → ascending lift-side push order → wrong stack layout under `WasmSlotEnv.resolve` (which uses `pos = len-1-index`) → body's slot refs resolved to the WRONG captured locals.  No trap, just wrong output.  This was independent of #615's original reproducer but shares the root cause.

  Fix: in `_collect_free_vars`, after walking the body for free vars, group captures by type, fill the prefix [0, max] per type with synthetic entries (their `wasm_type` matches the type's other captures since `type_name` deterministically maps to a single WAT type), and sort each group descending by outer_idx so the lift-side push lands the highest outer_idx at the deepest stack position.  No changes needed to the per-call serialisation (`_translate_anon_fn`) or the lift-side env construction (`_compile_lifted_closure`) — both already iterate the captures list in order; the fix is to make that order correct.

  New test classes `TestIndexExprOfFnCall614` (3 tests) and `TestNonContiguousCapture615` (4 tests) in `tests/test_codegen_closures.py` cover all five concrete failure shapes plus a baseline that prevents regression of the case that was previously coincidentally working.

### Added
- **`examples/life.vera`** — Conway's Game of Life as a real-world Vera program: 80×22 grid, three classic patterns (Gosper Glider Gun, R-pentomino, Pentadecathlon) interacting, recursive `run_loop` driven by `<IO>` for animation timing, ANSI cursor-control rendering.  Demonstrates the canonical iterative shape (nested `array_mapi` over `array_mapi`, capturing the whole grid into the closure so `count_neighbors` can read each cell's eight neighbours), and carries the formal Conway B3/S23 transition rule on `next_cell`'s `ensures` clause — the verifier discharges all 32 contracts at Tier 1 by symbolic substitution, so any future edit that breaks the rule fails verification before it can run.  The first agent-written Conway's Life that runs cleanly end-to-end on Vera.

### Changed
- **ROADMAP.md** — stabilisation tier reworked: added [#602](https://github.com/aallan/vera/issues/602) (String-interp WASM `i64`/`i32` mismatch) and [#604](https://github.com/aallan/vera/issues/604) (five prelude combinators silently skipped from WASM compile) at the top of the queue as the codegen residue from the life.vera campaign.  Existing items renumbered; agent-integration tier deferred behind seven stabilisation items rather than five.  Also dropped closed entries for [#595](https://github.com/aallan/vera/issues/595) (upstream [wasmtime-py#337](https://github.com/bytecodealliance/wasmtime-py/pull/337) merged 2026-05-07) and [#478](https://github.com/aallan/vera/issues/478) (closed 2026-04-16; HISTORY entry existed but ROADMAP row had not been pruned at close time).
- **HISTORY.md** — opened **Stage 12: After the Game of Life** with framing intro covering the four campaign-residue patterns (scale-only bugs, walker-completeness gaps, browser-runtime gaps, codegen-side silent feature gaps); trimmed the v0.0.135–v0.0.138 Stage 11 entries to the Stage 1/5/9 single-sentence style.

### Documentation
- **SKILL.md** — three doc fixes surfaced by an agent writing Conway's Game of Life from scratch on current main.  `array_length`'s SKILL comment updated from *"returns Int (always >= 0)"* to *"returns Nat (the array length, flows to either Nat or Int positions)"* to match user-visible behaviour (the type checker permits `Int <: Nat` via verifier-enforced refinement, so the result flows freely into either).  `array_fold` example gains a three-line comment making the closure shape explicit (`fn(@Acc, @Elem -> @Acc)` with the rightmost-is-`.0` derivation), so agents no longer have to write a probe to determine the parameter order.  New "Tuples" subsection under "Composite types" showing `Tuple(...)` construction and `match` destructuring — previously SKILL mentioned `@Tuple<Int, String>` only as a type with no construction example, leading agents to hunt for tuple-literal syntax that doesn't exist and abandon valid approaches.

### Tooling
- **`scripts/check_skill_examples.py`** allowlist re-anchored after the SKILL line offsets shifted; one stale redundant entry pruned (the Non-exhaustive Match section had three allowlist entries but only two actual code blocks); one mis-anchored entry corrected (a "bare `@Int + @Int`" allowlist entry was parked on a parseable full-function example, suppressing it).

### CI
- **Test job parallelised with `pytest-xdist`** — added `pytest-xdist>=3.6` to `[dev]` extras; CI's `pytest` invocation now uses `-n auto` to fan tests across worker processes.  Local measurement (8-core Mac): full 3,752-test suite drops from **90.7s → 15.6s** (5.8× speedup, all tests pass).  GitHub Actions 2-core runners will see roughly half that.
- **Eliminated duplicate suite run on the coverage cell.**  The `test (ubuntu-latest, 3.12) + coverage` cell previously ran `pytest -v` (full suite, ~3–4 min) followed by `pytest --cov=vera ...` (full suite again, ~5 min) — the entire suite executed twice.  Restructured to run a single `pytest -v -n auto --cov=vera ...` invocation on that cell, keeping coverage instrumentation but cutting the wall time roughly in half.  Combined with xdist, the gating cell is expected to drop from ~8 min to ~3 min.

## [0.0.138] - 2026-05-07

### Fixed
- **[#593](https://github.com/aallan/vera/issues/593)** — Conway's Life string corruption from generation 1+ at 12×30 (the residual bug acknowledged at v0.0.137 release).  Root cause: `_compile_lifted_closure` in `vera/codegen/closures.py` only emitted the closure's return-value `gc_shadow_push` when the closure body itself allocated (`ctx.needs_alloc=True`).  But `_translate_array_map` and `_translate_array_mapi` in `vera/wasm/calls_arrays.py` *always* emit a per-iteration `global.get $gc_sp; i32.const 4; i32.sub; global.set $gc_sp` after each `call_indirect` when the element type is heap-pointer-like (the `b_needs_unwind` path at lines 649-655 and 1430-1439).  That pop assumed the closure pushed.  When a closure body is non-allocating but returns a heap pointer — e.g. `fn(@Bool -> @String) { render_cell(@Bool.0) }` where `render_cell` returns String literals from the data segment — there was no push, but the loop popped anyway, dropping `$gc_sp` *below* the surrounding function's prologue baseline.  Subsequent shadow-stack pushes then overwrote slots that were holding still-live roots, so the next GC mark phase missed those roots and swept their referents.  Manifested as silent string corruption (the original Conway's Life symptom — strings render with NULL or U+FFFD bytes interleaved) or as `call_indirect` "out of bounds table access" trap at smaller scales (the nested `array_map` of String-returning closure landed differently depending on heap layout).

  Fix: lift the return-value push out of the `if ctx.needs_alloc:` branch in `_compile_lifted_closure`.  Always push the return-value root when the return type is a heap pointer (i32_pair or i32 ADT), regardless of whether the body allocated.  The non-allocating branch needs no `gc_sp` save/restore — the body has nothing to clean up — it just intercepts the return value to publish it as a root, balancing the caller's per-iteration unwind.

  New `TestClosureReturnShadowPushBalance` test class in `tests/test_codegen_closures.py` covers four shapes: positive regression (correct output at small scale), behavioural under `VERA_EAGER_GC=1` for both flat `array_map` and recursive Life-style nested rendering, and a structural assertion that the WAT for a non-allocating String-returning closure contains the `gc_shadow_push` epilogue.  Both the agent-rebuilt minimal reproducer and the user's original 12×30 `life_full_program.vera` now run all 200 generations cleanly with zero U+FFFD corruption.

### Added
- **`VERA_EAGER_GC=1`** environment variable — diagnostic build knob that prepends an unconditional `call $gc_collect` to every `$alloc` invocation, surfacing latent missing-shadow-root bugs immediately rather than only at scale.  Documented in the new top-level [`ENVIRONMENT.md`](ENVIRONMENT.md) along with the full catalogue of `VERA_*` environment variables (eight total: four Inference provider keys, two Inference selection knobs, the existing `VERA_JS_COVERAGE` browser-test knob, and the new `VERA_EAGER_GC` debug knob).  This was the diagnostic that converted the #593 investigation from "static analysis can't find a smoking gun" to "the very first iteration of the rebuilt Life crashes with a clear root-imbalance signature".  Worth keeping permanently as a debugging aid for any future GC-rooting regression.

### Documentation
- New top-level **[`ENVIRONMENT.md`](ENVIRONMENT.md)** centralising the eight `VERA_*` environment variables (previously scattered across `README.md`, `AGENTS.md`, `TESTING.md`, `CONTRIBUTING.md`, and `CLAUDE.md`).  Cross-linked from the documents that previously had only one-line mentions, so future env vars have one canonical home.

## [0.0.137] - 2026-05-07

### Fixed
- **[#588](https://github.com/aallan/vera/issues/588)** — Indexing a *captured* `Array<T>` inside a closure body no longer produces invalid WASM.  Pre-fix the `_walk_free_vars` free-variable detector in `vera/wasm/closures.py` had no `IndexExpr` branch — when the walker hit `coll[idx]` inside a closure body, the `coll` SlotRef referencing a captured outer slot was never recognised as a free variable.  The closure-lift's `captures` list came back empty, body translation failed (the SlotRef couldn't be resolved against an empty capture-only env), and `_compile_lifted_closure` returned `None` — but the call site at `_translate_apply_fn` had already emitted a `call_indirect` to the now-absent function-table entry.  Result: `unknown table 0: table index out of bounds` at WASM validation (flat case) or `undefined element: out of bounds table access` / `indirect call type mismatch` at runtime (nested case where the dispatch landed in-bounds on a wrong-typed function).  Fix adds the missing `IndexExpr` branch plus seven other AST node types that were silently falling through with the same bug class: `ArrayLit`, `InterpolatedString`, `HandleExpr` (with handler-clause param scoping), `AssertExpr` / `AssumeExpr`, `ForallExpr` / `ExistsExpr`, and `ModuleCall`.  Each silently dropped capture references inside its sub-expressions.  New conformance test `ch05_capture_array_index.vera` covers flat, nested, and combined-with-FnCall positions.

  Note: the issue body acknowledged that scaling to a full Conway's Game of Life implementation may have additional triggers beyond captured-array indexing.  Both documented `repro_min.vera` and `repro_nested.vera` reproducers pass post-fix.  The remaining full-Life corruption (string-output corruption appearing from generation 1+ at 12×30 grid scale) is a separate not-yet-isolated bug class tracked separately as a follow-up, and is masked from the user as a Python traceback by the v0.0.136 `errors="replace"` defensive layer (surfaces as U+FFFD chars in output rather than crashing).

- **`IO.sleep` no longer escapes `KeyboardInterrupt` as a raw Python traceback** when Ctrl-C arrives during the wait.  Pre-fix, `host_sleep`'s `time.sleep(ms / 1000.0)` let `KeyboardInterrupt` propagate up through wasmtime's trampoline as a "python exception" cause and the user saw a multi-line Python traceback ending in `KeyboardInterrupt`.  Same `WasmTrapError` contract violation class as #589's `UnicodeDecodeError` escape (#516 / #522 / #547 — runtime traps must surface as Vera-native errors).  Discovered when a user Ctrl-C'd a Conway's Life animation that uses `IO.sleep(120)` between frames.  Fix: `host_sleep` catches `KeyboardInterrupt` and raises `_VeraExit(130)` (conventional SIGINT exit code, 128 + signal-2) which is unwrapped at the top of `execute()` as a clean `ExecuteResult` with `exit_code=130`.  New `TestHostSleepKeyboardInterrupt` test class in `tests/test_runtime_traps.py` with one structural assertion (the guard is wired up at the source level) plus one behavioural test (synthetic `KeyboardInterrupt` → `_VeraExit(130)` conversion verified end-to-end with `unittest.mock.patch`).  A separate macOS malloc abort can still fire during wasmtime/ctypes cleanup after the clean exit; that's tracked as [#595](https://github.com/aallan/vera/issues/595) (cleanup-path issue, not data-integrity).

## [0.0.136] - 2026-05-06

### Fixed
- **[#586](https://github.com/aallan/vera/issues/586)** — `apply_fn(closure, ())` on a `(Unit -> X)` closure no longer trips a WASM type mismatch.  Pre-fix, `_translate_apply_fn` in `vera/wasm/closures.py` defaulted the value-arg's WASM type to `i64` whenever `_infer_expr_wasm_type` returned `None` — but it returns `None` for both "couldn't infer" and "Unit has no representation", conflating the two cases.  A `UnitLit` arg pushed nothing onto the stack but registered a phantom `i64` param in the call_indirect signature.  The closure-lift side correctly skips Unit params, so the two ends disagreed and validation rejected the call with `expected i64, found i32` (the `func_table_idx` landing where the phantom value-arg was expected).  Fix is three lines: change `arg_wasm_types.append(wt or "i64")` to `elif wt is not None: arg_wasm_types.append(wt)` so Unit args contribute zero entries to the sig.  Same falsy-pitfall pattern as #584's `_fn_ret_types` filter.  New conformance test `ch05_unit_arg_closure.vera` covers no-capture, Int-capture, and Array-capture variants.
- **[#589](https://github.com/aallan/vera/issues/589)** — `host_print` / `host_stderr` / `host_contract_fail` / `_read_wasm_string` / `vera/wasm/markdown.py::_read_string` no longer crash with a raw Python `UnicodeDecodeError` when an upstream codegen bug produces a corrupt String `(ptr, len)` pair pointing at non-UTF-8 bytes.  Pre-fix, the unhandled exception escaped through wasmtime's trampoline as a "python exception" cause and the user's CLI saw a 30+ line Python traceback ending in `UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc1`.  A user-level program must never produce a Python traceback regardless of what the program does — this is the WasmTrapError contract from #516 / #522 / #547 applied to the UTF-8-decode paths.  Fix is per-site `errors="replace"` so invalid bytes surface as U+FFFD replacement characters in the user's output instead.  The String-return decoder in `execute()` (added by v0.0.135) was previously try/except → pointer fallback, which silently mutated the return type from `str` to `int` when bytes weren't valid UTF-8 — that fallback was a worse silent failure than visible U+FFFD chars (downstream consumers printed an integer where a string was expected) and is now also `errors="replace"`.  Surfaced by [#588](https://github.com/aallan/vera/issues/588) (captured-Array-indexing in closure produces corrupt String pointers); fixing #588 removes the most common trigger but the defensive-coding hygiene applies regardless of source.  New `TestHostPrintInvalidUtf8589` test class in `tests/test_runtime_traps.py` covers all six affected sites with structural assertions plus an end-to-end wasmtime-trampoline contract test using a synthetic WAT module that imports `vera.print` and calls it with raw invalid UTF-8 bytes.

## [0.0.135] - 2026-05-06

### Fixed
- **`vera run` on String-returning `main` now prints the actual string** instead of a heap pointer.  Pre-fix, a public `main(@Unit -> @String)` returning `"hello"` printed e.g. `147492` (the first half of the i32_pair return — the data pointer in linear memory).  Post-fix, `execute()` decodes the UTF-8 bytes from `memory[ptr:ptr+len]` and stores the decoded `str` in `ExecuteResult.value`, which the CLI then prints directly.  Implementation: new `CompileResult.fn_string_returns: set[str]` populated by `compile_program` from each FnDecl's return type (resolving aliases via `_return_type_is_string` so `type Greeting = String` participates), checked in `execute()` to decide whether to decode.  Array<T> returns deliberately keep the bare-pointer fallback — their bytes-at-ptr aren't UTF-8 and would need element-aware formatting to render meaningfully (separate scope).  `ExecuteResult.value` type widens to `int | float | str | None`; no test breakage outside the one assertion that was checking for the pre-fix pointer value (now updated to assert the decoded str).
- **[#568](https://github.com/aallan/vera/issues/568)** — `url_parse(":foo")` now returns `Err("missing scheme")` instead of `Ok` with an empty scheme that round-tripped through `url_join` as bare `"foo"` (losing the leading colon).  Fix is RFC 3986 §3.1-aligned: rejects `colon_pos == 0` in `_translate_url_parse` (`vera/wasm/calls_encoding.py`) right after the colon-scan loop, before any further processing.  Five lines of WAT plus comment; no changes to `url_join` (the `s_len > 0` gate stays, since post-fix it can never see an empty scheme).  We don't yet enforce the full ALPHA / [ALPHA / DIGIT / "+" / "-" / "."]* scheme grammar — that's a wider RFC-conformance follow-up — but the empty-scheme case alone is the only one that lost its leading colon in the round-trip.  Existing `ch09_url_parsing.vera` extended with a `test_parse_empty_scheme` case asserting the new Err return.
- **[#584](https://github.com/aallan/vera/issues/584)** — User-defined `@Unit`-returning fn call in non-tail block-statement position no longer emits invalid WAT.  `vera/wasm/context.py::_is_void_expr` previously recognised `IO.*` qualified calls, `UnitLit`, effect-op `FnCall`s, and compound expressions as void but missed `FnCall` to user-declared `@Unit` fns — the surrounding statement-sequencer fell through to "produces a value", emitted a stray `drop`, and failed WASM validation with `expected a type but nothing on stack`.  Fix expands the codegen registry (`_fn_ret_types` filter in `vera/codegen/functions.py`) to include Unit-returning fns explicitly with `None`, then adds a clause to `_is_void_expr` that recognises them.  Recursive cases (Unit fn nested inside `if`/`match` arms in non-tail position) come for free via the existing recursion.  The natural `render(grid); IO.sleep(120); recurse(...)` shape now compiles cleanly whenever `render` is a user helper.
- **[#583](https://github.com/aallan/vera/issues/583)** — Type aliases over `Array<T>` no longer break WASM codegen.  `_is_pair_type_name` in `vera/wasm/inference.py` did string-pattern matching on unresolved alias names, so `type Row = Array<Bool>` left "Row" unrecognised as a pair type — SlotRefs to `@Row.0` only emitted the pointer (not pointer + length), and let-bindings or parameters typed `@Row` fell through to silent E602 skips.  Fix converts `_is_pair_type_name` to an instance method that resolves aliases via `_resolve_base_type_name` first; complementary alias resolution added in `_translate_array_lit` (so `[@Row.0, @Row.0]` lays out elements at the correct stride) and `_infer_index_element_type_expr` (so `@Row.0[1]` resolves the element type correctly).  Aliases in parameter, let-binding, indexing, and array-literal-element positions all work now.

### Documentation
- **SKILL.md sandbox-install affordance** — observed in the wild that a Claude.ai sandboxed instance reading the existing Installation section concluded "Vera isn't available in this sandbox" and fell back to "write code the user can run locally" without trying the install steps.  Added an explicit note in the Installation section telling agents running in sandboxes (Claude.ai, Code Interpreter, container-based execution environments) that the standard `git clone + pip install -e .` works there too — sandboxes typically have Python, `git`, `pip`, and outbound network — and to run + verify before concluding the toolchain is unavailable.  Also flagged the `pip install vera` PyPI footgun: that name resolves to an unrelated ERAV citizen-science library, not us; install from the GitHub source clone.

## [0.0.134] - 2026-05-06

### Documentation
- **Post-campaign consistency sweep** — `ROADMAP.md` near-term-priorities section reframed from the now-closed bug-killing campaign to an agent-integration push (LSP server [#222](https://github.com/aallan/vera/issues/222), `vera context` [#523](https://github.com/aallan/vera/issues/523), `Inference.complete` token/temperature controls [#370](https://github.com/aallan/vera/issues/370)).  `HISTORY.md` Stage 11 release table compacted — entries v0.0.119–v0.0.134 had drifted to 2–3× the density of Stage 5–9 entries with implementation details and parenthetical asides that didn't match the established style.  `SKILL.md` "Known Bugs and Workarounds" cleaned: dropped three closed-issue rows ([#475](https://github.com/aallan/vera/issues/475) / [#487](https://github.com/aallan/vera/issues/487) / [#535](https://github.com/aallan/vera/issues/535) all closed in v0.0.129–v0.0.131), rewrote the closure-capture subsection that was telling agents to use a now-obsolete "lift to a helper" workaround for pair-type captures (#535 closed in v0.0.130), added the still-open [#568](https://github.com/aallan/vera/issues/568) (`url_parse` leading-colon drop) for parity with `KNOWN_ISSUES.md`.  Net delta across the three commits: −108 lines, with SKILL.md no longer leading agents to write workarounds for bugs that no longer exist.

### Fixed
- **Active reclamation of host-store handles — closes [#573](https://github.com/aallan/vera/issues/573), [#575](https://github.com/aallan/vera/issues/575), [#576](https://github.com/aallan/vera/issues/576), [#579](https://github.com/aallan/vera/issues/579)**.  Pre-fix, every `map_new` / `map_insert` / `map_remove`, every `set_new` / `set_add` / `set_remove`, and every Decimal arithmetic op allocated a fresh entry in the corresponding Python-side store (`_map_store` / `_set_store` / `_decimal_store` in `vera/codegen/api.py`) and never released transient predecessors *within a single `execute()` call*.  A 10 000-iteration `array_fold` over `map_insert` left 10 001 entries in the store at `execute()` exit.  Each store is local to one `execute()`, so the leak doesn't accumulate across separate calls — but a single long-running call (a server's request loop running inside one `execute()`, an interactive session, a Game-of-Life-style program with many generations) could exhaust memory monotonically.

  Post-fix: the heap-wrap-as-ADT design from #573's body, applied to all three types in one PR.  Every `Map<K, V>` / `Set<T>` / `Decimal` value is now a pointer to an 8-byte wrapper ADT on the GC heap (tag at offset 0, raw host handle at offset 4).  Wrappers register with a new 64 KiB wrap-table region in linear memory at allocation; Phase 2c of `$gc_collect` walks the wrap table and fires a new `host_decref_handle(kind, handle)` host import for every wrapper that was unmarked, evicting the corresponding entry from the appropriate store.  Survivors are compacted in place so the table tracks live wrappers, not total allocations.

  **Infrastructure** (one-time, shared across all three types):
  - `vera/codegen/assembly.py` — new wrap-table region (gated on `_needs_wrap_table`), `$register_wrapper` helper, Phase 2c walk in `$gc_collect`, `host_decref_handle` import declaration, `register_wrapper` export so host-side JSON/HTML parsers and Decimal helpers can register wrappers from Python.
  - `vera/codegen/api.py` — `host_decref_handle(kind, handle)` Python implementation dispatching on `kind` (1=Map, 2=Set, 3=Decimal) to the appropriate `*_store.pop`.  New `_wrap_handle(caller, kind, raw)` helper for cases where the host has already obtained a raw handle (e.g. `decimal_from_string` constructing `Option<Decimal>`).
  - `vera/browser/runtime.mjs` — full mirror: `host_decref_handle` dispatcher, `wrapHandle(kind, raw)`, all three kinds.
  - `ExecuteResult.host_store_sizes` — new field exposing post-execution store population so tests can verify reclamation without linker introspection.

  **Per-type call-site migration** (`vera/wasm/calls_containers.py`):
  - **Map** (8 ops): wrap on `map_new` / `map_insert` / `map_remove`; unwrap on `map_get` / `map_contains` / `map_size` / `map_keys` / `map_values` (closes #573 phase 1).
  - **Set** (6 ops): wrap on `set_new` / `set_add` / `set_remove`; unwrap on `set_contains` / `set_size` / `set_to_array` (closes #575).
  - **Decimal** (10 ops): wrap on `decimal_from_int` / `_from_float` / `_neg` / `_add` / `_sub` / `_mul` / `_round`; unwrap on `_to_string` / `_to_float` / `_eq` / `_compare`.  `decimal_from_string` and `decimal_div` return `Option<Decimal>` constructed host-side; their inner Decimal handle is wrapped via `_wrap_handle` before being stuffed into the Some payload (closes #576).

  **GC-rooting hygiene**:
  - `vera/wasm/helpers.py` — `_HOST_HANDLE_TYPES` is now empty (was `{Map, Set, Decimal}`).  All three are real Vera-heap pointers post-#573 and MUST be shadow-stack-rooted across allocating calls; the `_is_host_handle_type` exclusion would have left them vulnerable to mid-call sweep.
  - The wrapper-allocation helper (`_emit_wrap_handle` in `calls_containers.py`) now shadow-pushes the new wrapper pointer immediately after construction, matching the existing ADT-constructor pattern in `vera/wasm/data.py`.  Without this, nested expressions like `decimal_add(decimal_from_int(a), decimal_from_int(b))` would be unsafe — the inner wrapper sits on the operand stack while the second `decimal_from_int` invokes `$alloc`, and a GC fire there would sweep the unmarked wrapper.

  **JSON / HTML internal Map wrapping** (closes a JObject / HtmlElement coupling):
  - `vera/codegen/api.py` — `_alloc_map_wrapper(caller, dict)` allocates the dict in `_map_store` AND wraps the resulting handle.  Used by `write_json` (JObject) and `write_html` (HtmlElement attrs) so the i32 stored in those ADT fields is a wrapper pointer, type-compatible with user-level `map_get` / `map_contains` calls (which now expect wrappers and unwrap with `i32.load offset=4`).
  - `vera/wasm/json_serde.py`, `vera/wasm/html_serde.py` — `write_*` use the wrapping `map_alloc(caller, dict)` signature; `read_*` unwrap before looking up the host store.

  **Tests** (`tests/test_codegen.py::TestHostHandleReclamation573`): ten regression tests covering all three types — chain-reclaims-transients (10K Map, 10K Set, 5K Decimal) plus value-correct-after-pressure (Map, Set, Decimal) plus the JObject case proving JSON/HTML internal Maps are reclaimed too plus a structural pin (`test_register_wrapper_has_compaction_slow_path`) that the slow-path WAT is wired up correctly.

  **#579 — `$register_wrapper` slow path before trap.**  Pre-fix the function trapped with `unreachable` the moment the wrap-table filled (4 096 simultaneously-live entries) — even if compaction would have freed thousands of dead entries.  Post-fix the slow path roots the in-flight wrapper on the shadow stack, calls `$gc_collect` (which runs Phase 2c compaction), pops the root, and re-checks; only if the table is still full after compaction does it trap.  Bounded in practice by heap fill rate, but the slow path covers wrapper-heavy workloads with low heap pressure (e.g. tight loops creating throwaway Map/Set/Decimal values that go dead before the next iteration).  Adds ~20 lines of WAT plus a structural test pinning the wiring.

### Updated
- `tests/test_codegen.py::TestOpaqueHandleParamRooting347` — three rooting tests flipped from "param 0 must NOT be shadow-pushed" to "param 0 MUST be shadow-pushed after #573".  All three host-handle types (Map, Set, Decimal) lower to wrapper-ADT pointers post-fix and require GC rooting.
- `tests/test_codegen.py::TestArrayFoldHandleRooting490` — `_assert_handle_not_extra_rooted` flipped to `_assert_handle_extra_rooted_after_573`.  `array_fold` and `array_map` accumulators / elements of type Decimal now MUST emit per-iteration root pushes (was: must not).

## [0.0.133] - 2026-05-05

### Fixed
- **Iterative array builders no longer leak the closure return-value root — closes [#570](https://github.com/aallan/vera/issues/570)**.  The lifted-closure epilogue (`vera/codegen/closures.py`) restores its entry `$gc_sp` and then, when the return type is a heap pointer (Vera ADT, `String`, `Array<T>`), pushes the return as a fresh root so generic stash-then-GC callers stay sound.  The iterative array builders consume the return synchronously (store into a rooted `dst[idx]`, or in-place overwrite a rooted accumulator slot), so the per-call root is redundant — and accumulating one leaked slot per iteration overflowed the 16 KiB / 4 096-entry shadow stack.  Pre-fix symptom: a 5 000-element `array_map<_, ADT>` trapped at `unreachable` inside `gc_shadow_push` around iteration 4 000.  Fix is per-callsite (`vera/wasm/calls_arrays.py`):
  - **`array_map`** and **`array_mapi`**: emit a 4-byte `$gc_sp` decrement after storing the return into `dst[idx]`.
  - **`array_fold`**: emit the same 4-byte unwind *before* the `gc_sp - 8` overwrite math that updates the rooted-accumulator slot.  Without this, the second iteration's overwrite addressed the previous-call's leaked slot instead of the accumulator's pre-call slot — a second symptom of the same bug class that this fix incidentally closes.
  - **`array_sort_by`**: same 4-byte unwind after the comparator's `Ordering` tag is read via `i32.load offset=0`.  Insertion sort issues up to `n*(n-1)/2` comparisons, so the leak surfaces at ~200 reverse-sorted elements (well past the shadow-stack budget).
  Builders that take a Bool-returning predicate (`array_filter`, `array_find`, `array_any`, `array_all`) and builders without a callback (`array_flatten`, `array_reverse`, `array_range`) are unaffected — `Bool` is excluded from the closure's `ret_is_pointer` flag, so no post-restore root push happens.  New `TestIterativeBuilderShadowStack` (4 tests in `tests/test_codegen_closures.py`) covers each fixed builder at the size that previously overflowed.

## [0.0.132] - 2026-05-05

### Fixed
- **Opaque-handle GC-rooting hygiene — closes [#347](https://github.com/aallan/vera/issues/347) and [#490](https://github.com/aallan/vera/issues/490)** (#346 closed as superseded by [#573](https://github.com/aallan/vera/issues/573) — see Note below).  Two related cleanups around how `Map`, `Set`, and `Decimal` opaque handles are treated by the codegen's GC-rooting heuristics.  Shared infrastructure: a new `_is_host_handle_type` classifier in `vera/wasm/helpers.py` distinguishes types that lower to i32 indices into Python-side host stores from real Vera-heap pointers.
  - **#347 — opaque handle parameters no longer pushed onto the GC shadow stack**.  Pre-fix `vera/codegen/functions.py` and `vera/codegen/closures.py` excluded only `Bool` / `Byte` from `gc_pointer_params`, so a `Map<K, V>` / `Set<T>` / `Decimal` parameter (i32 handle index) was treated as a heap pointer and pushed onto the shadow stack at every function entry.  Wasted shadow-stack space; a handle value that landed in the heap-pointer range with valid alignment would have caused the conservative mark phase to spuriously mark an unrelated heap object as live (memory retention, not corruption).  Post-fix the new classifier excludes opaque handles at four rooting decision sites: top-level params + return type in `functions.py`, closure params + captures + return type in `closures.py`.  New `TestOpaqueHandleParamRooting347` regression test pins the fix structurally — a function taking a `Map<K, V>` parameter no longer contains the canonical `local.get $p0; i32.store` shadow-push idiom in its WAT.
  - **#490 — `array_fold` and `array_map` no longer over-root opaque-handle accumulators**.  Pre-fix the `u_is_adt`/`t_is_adt` heuristics in `vera/wasm/calls_arrays.py` (`u_wasm == "i32" and u_type not in ("Bool", "Byte") and not u_is_pair`) classified `Map`/`Set`/`Decimal` accumulators as ADT pointers and emitted shadow-stack rooting around the loop body.  Post-fix the same `_is_host_handle_type` classifier excludes them.  New `TestArrayFoldHandleRooting490` (2 tests): a structural pin via `global.set $gc_sp` count parity between Int and Decimal accumulators, plus a functional regression that the fold still produces the right result.

### Note — `#346` superseded by `#573`
- The original issue tracker grouped `#346 (host-store leak)` with `#347` and `#490` under "opaque-handle hygiene", but `#346` is a fundamentally different problem: it requires *active reclamation* of unreachable handles from Python-side stores, while `#347` and `#490` are purely *codegen-time* decisions about which i32 values to push to the shadow stack.  An earlier draft of this PR attempted to close all three by adding a `host_gc_sweep` host import that walked the live heap + shadow stack to identify reachable handle indices, but the resulting design (six interlocking pieces — heap walk, shadow-stack scan, transitive closure, re-entrancy guard, let-binding shadow_push, JSON/HTML emission gates) had too much complexity for the practical impact.  Per-`execute()` handle leaks are bounded (Python GC reclaims at `execute()` exit), and Vera doesn't yet have long-running execution contexts where the leak would matter in practice.  `#346` was closed as superseded by [#573](https://github.com/aallan/vera/issues/573), which tracks the recommended follow-up: heap-wrap-as-ADT (return a Vera-heap `MapHandle(i32)` from each handle-creating op so the existing mark-sweep GC handles reclamation via a destructor callback) — a single mechanism that integrates with mature infrastructure rather than running parallel to it.

## [0.0.131] - 2026-05-05

### Fixed
- **GC infrastructure batch — `$alloc` multi-page grow + worklist size + overflow trap (closes [#487](https://github.com/aallan/vera/issues/487) and [#348](https://github.com/aallan/vera/issues/348))**.
  - **`$alloc` computes pages-needed for `memory.grow`** (`vera/codegen/assembly.py` `_emit_alloc`, fixes #487). Pre-fix, when `heap_ptr + total > memory.size * 65536`, `$alloc` unconditionally called `memory.grow 1` regardless of how many pages were actually needed; a single allocation request more than ~64 KB past the current memory boundary fell through to the bump-allocate and trapped on out-of-bounds memory access. Post-fix: compute `pages_needed = ceil(((heap_ptr + total) - memory.size * 65536) / 65536)` and grow by that many pages in a single call. Verified against the issue's reproducer (two 50K-element `Array<Int>`s, ~800 KB total) which now allocates cleanly. New `TestLargeAllocGrow487` (2 tests) covers the 50K case and a small-allocation regression pin.
  - **GC mark-phase worklist quadrupled to 64 KiB + trap on overflow** (`vera/codegen/assembly.py` `_emit_gc_collect` + `gc_worklist_size` constant, fixes #348). Pre-fix the worklist was 16 KiB / 4096 entries; both push branches (Phase 2 seed and Phase 2b mark scan) silently dropped pushes when full, leaving reachable objects unmarked which the sweep phase then freed — a real use-after-free hole for object graphs holding more than ~4 K reachable pointers. Post-fix: worklist increased to 64 KiB / 16 384 entries (covers reasonable program shapes), and both push branches now `unreachable` on overflow rather than silently dropping. The combined effect: most programs that previously fit have ~4× headroom, and any residual overflow is a clean WASM trap rather than silent corruption. New `TestWorklistOverflow348` (3 tests) covers a moderate-graph runtime case plus structural pins on the new GC region size and trap shape. Two existing tests (`test_heap_ptr_starts_after_strings`, `test_heap_ptr_zero_without_strings`) updated for the new heap base offset (32 768 → 81 920 bytes from the worklist resize).
- **Surfaced separately**: `array_map` (and likely sibling iterative builders) overflow the GC shadow stack at ~4 000 heap-allocating elements ([#570](https://github.com/aallan/vera/issues/570)). Found while writing the natural runtime regression test for #348 (a 5 000-element `Array<Box>`); the failure was on the shadow-stack-overflow `unreachable` inside the lifted closure, not the worklist trap. Pre-existing, separate subsystem — tracked for follow-up; the #348 runtime regression is covered by a 1 000-element graph (within the shadow-stack budget) plus structural WAT pins.

## [0.0.130] - 2026-05-05

### Fixed
- **Pair-type closure captures (`String`, `Array<T>`) preserve their length field — closes [#535](https://github.com/aallan/vera/issues/535)** (residual of [#514](https://github.com/aallan/vera/issues/514); v0.0.121 fixed nested closures and primitive/ADT captures, this release closes the residual pair-type subset).
  Pre-fix, `vera/wasm/closures.py::_walk_free_vars` resolved the wasm type of every capture via `_type_name_to_wasm`, which collapses any composite type to a single `"i32"`. `_translate_anon_fn` then allocated 4 bytes per capture and stored only the ptr half of pair-typed values; `_compile_lifted_closure` read back only the ptr and the body got the len from adjacent struct memory (typically zero). Operations on a captured `Array<T>` or `String` therefore silently saw an empty value — `array_length(@Array<Int>.0)` returned 0, `string_length(@String.0)` returned 0, and any indexed read into a captured pair worked off ptr=correct/len=0.
  Post-fix, all three sites carry an `"i32_pair"` tag for these captures: `_walk_free_vars` detects `String` / `Array<T>` and overrides the wasm type (without changing `_type_name_to_wasm`, which other callers like `handle_state` and `handle_exn` still need to return the single-slot form); `_translate_anon_fn` allocates 8 bytes per pair field (two consecutive i32 stores at `cap_offset` / `cap_offset + 4`); `_compile_lifted_closure` allocates two consecutive i32 locals (ptr, len), loads both halves, and pushes only the ptr into the slot env — matching the let-binding and parameter conventions so the closure body resolves the pair correctly. GC shadow-push was extended to root the ptr field of pair captures (the len is a byte count, not a heap pointer). New `TestPairCapture535` (5 tests) covers `Array<Int>` capture (returns 21 not 0), `String` capture (returns 15 not 0), ADT capture regression (still works), primitive capture regression (still works), and a mixed-layout test that captures both an `Int` (i64) and an `Array<Int>` (i32_pair) to exercise the field-packing order.

## [0.0.129] - 2026-05-05

### Fixed
- **WASM call translator major bugs — seven correctness fixes close out [#475](https://github.com/aallan/vera/issues/475)** (PR 2 of 2 — Major findings 4-10; Critical findings 1-3 shipped in v0.0.128). All seven fixes were CodeRabbit findings on PR #474's calls.py decomposition; with this release the issue is fully closed.
  - **`array_slice` clamps in i64 before wrapping** (`vera/wasm/calls_arrays.py` `_translate_array_slice`) — same shape as the v0.0.128 `string_slice` fix. Pre-fix, start/end indices were narrowed via `i32.wrap_i64` first and then compared with `arr_len` as i32; a huge positive i64 (e.g. 2^32 + 5) wrapped to a small in-range-looking i32 and the byte-copy read past the array. Post-fix: widen `arr_len` to i64 and use the cross-mixin `_clamp_i64_to_range_then_wrap` helper (shared via Python MRO with `CallsStringsMixin`) to clamp before narrowing. New `TestArraySliceClamp475` (4 tests) covers normal, negative-start, end-beyond-length, and the i64-overflow cases.
  - **`Map<K, Array<T>>` rejected at codegen** (`vera/wasm/calls_containers.py` `_map_wasm_tag` and 11 call sites) — pre-fix, `_map_wasm_tag` returned a placeholder string for any unknown value type, so `Map<K, Array<T>>` would compile but silently treat array values as opaque pointers. `Map<K, Array<T>>.get` returned a raw pointer i32 not a properly-tagged Array, opening a type-system hole downstream. Post-fix: return type changed to `str | None`, with `Array` detection added (`if vera_type.startswith("Array"): return None`). 11 call sites guard against `None` and surface the unsupported feature as a controlled codegen error. New `TestMapArrayValueRejected475` regression test pins the rejection.
  - **`url_parse` / `url_join` round-trip preserves URL shape** (`vera/wasm/calls_encoding.py` `_translate_url_parse` and `_translate_url_join`) — pre-fix, `url_parse` discarded the `has_auth`, `has_query`, and `has_frag` delimiter bits after using them to set component bounds; `url_join` then re-derived delimiter presence from `len > 0`. This conflated `http:path` (no authority) with `http://path` (empty authority) — both joined as `http:///path` — and dropped trailing `?` / `#` when the body was empty. Post-fix: `url_parse` packs the three flag bits plus an explicit-mode sentinel into a previously unused i32 word at struct offset 44; `url_join` reads them back and uses the bits when the sentinel is set, falling back to the legacy `len > 0` heuristic when the sentinel is clear (so direct `UrlParts(...)` data-constructor callers preserve their pre-fix behaviour). New `TestUrlParseJoinRoundTrip475` (5 tests) covers `http:path`, full URLs, query-with-body, empty `?`, and empty `#`.
  - **`base64_decode` rejects `=` outside the padding region** (`vera/wasm/calls_encoding.py` `_translate_base64_decode`) — RFC 4648 only allows `=` in the final 1-2 positions and only when total length % 4 ∈ {2, 3}. Pre-fix the decoder accepted `=` anywhere; `AB=C` decoded with the embedded `=` treated as zero bits, silently producing corrupted output. Post-fix: a position-based check verifies any `=` byte sits at index ≥ `slen - pad`, surfacing a controlled error otherwise. New `TestBase64DecodePadding475` (3 tests).
  - **`parse_nat` / `parse_int` reject embedded spaces** (`vera/wasm/calls_parsing.py` `_translate_parse_nat` and `_translate_parse_int`) — pre-fix the digit loop unconditionally skipped ASCII space bytes mid-number (a misleading "trailing space" comment hid the embedded-space accept). `"1 2"` parsed as 12, `"-1 0"` parsed as -10. Post-fix: leading whitespace is still trimmed (preserving the documented contract) and trailing whitespace is still allowed via a separate post-digit-loop tail-validation block, but a space encountered between digits breaks the digit loop and the tail-validator's "every remaining byte must be a space" check fires. New `TestParseEmbeddedSpaces475` (4 tests) covers normal, leading-space-OK, embedded-space-rejected for both nat and int.
  - **`int_to_string(INT64_MIN)` correct** (`vera/wasm/calls_strings.py` `_translate_to_string`) — pre-fix the digit-extraction loop break used signed `i64.le_s 0`. On the first iteration of negation `-INT64_MIN` overflows back to `INT64_MIN` (still `< 0`) and the loop terminated immediately, printing partial garbage. Post-fix: the loop break uses unsigned `i64.eqz` after extracting digits via `i64.div_u` / `i64.rem_u`, so the unsigned bit pattern walks down to zero correctly. New `TestToStringInt64Min475` (2 tests) covers INT64_MIN and a negative-basic sanity check.
  - **`float_to_string` handles fraction-rounding carry** (`vera/wasm/calls_strings.py` `_translate_float_to_string`) — pre-fix the integer part was emitted first, then `frac_val = round((f - floor(f)) * 1_000_000)` was computed. When the fraction rounded up to exactly 1_000_000 (e.g. `1.9999996`), the integer part `1` was already on the page so output was `1.0` instead of `2.0`. Post-fix: `frac_val` is computed first; when it equals 1_000_000 the integer is incremented and `frac_val` reset to 0 *before* any digits are emitted. New `TestFloatToStringCarry475` (3 tests) covers the carry case, normal fractions, and the full-six-decimals case.

## [0.0.128] - 2026-05-05

### Fixed
- **WASM call translator critical bugs — three safety fixes** ([#475](https://github.com/aallan/vera/issues/475), partial — Critical findings 1, 2, 3 of 10; Major findings 4-10 remain for the next release). Each was a pre-existing bug surfaced by CodeRabbit during PR #474's calls.py decomposition review and tracked since mid-April; this release ships PR 1 of 2 (Criticals) so the safety holes close immediately, with the seven Major correctness fixes following as PR 2.
  - **Memory-safety hole in `string_char_code` closed** (`vera/wasm/calls_strings.py` `_translate_char_code`) — pre-fix, no bounds check before `i32.load8_u`; out-of-range indices read arbitrary WASM linear memory at `ptr_s + (wrapped index)`. The placeholder `_ = len_s  # reserved for future bounds checking` documented the gap. Post-fix: bounds check operates in i64 (`idx < 0 || idx >= len_s_i64`) and traps with `unreachable` *before* narrowing to i32 — so a huge positive i64 value cannot wrap to a small in-range-looking i32 and bypass the check. New `TestCharCodeBoundsCheck475` (5 tests) covers the in-range, negative, at-length, huge-positive, and last-valid-index cases.
  - **`string_slice` clamp-before-narrow** (`vera/wasm/calls_strings.py` `_translate_string_slice`) — pre-fix had no clamping at all (the same `_ = len_s` placeholder pattern). Indices were narrowed via `i32.wrap_i64` first; large positive i64 values silently turned into negative i32 values, which then drove the byte-copy loop into out-of-range memory or produced garbled output. Post-fix: clamp in i64 space (via the new `_clamp_i64_to_range_then_wrap` helper that widens `len_s` to i64, clamps, then wraps) before narrowing. Negative starts clamp to 0, ends past length clamp to length, swapped indices produce empty slices, huge positive indices clamp to length cleanly. New `TestStringSliceClampBefore475` (5 tests).
  - **Expression-bodied `Exn<E>` handler result type** (`vera/wasm/calls_handlers.py` `_translate_handle_exn`) — pre-fix, catch-arm result type was inferred only when `clause.body` was an `ast.Block`; expression-bodied handlers (e.g. `throw(@String) -> None`, `throw(@Int) -> @Int.0 + 1`) left `result_wt = None` and the emitted WAT omitted the `(result T)` annotation, producing invalid WAT that failed validation when the body type was anything other than Unit. Post-fix: use `_infer_expr_wasm_type` (which handles all Expr types including `ast.Block`) for both the catch clause and the body. New `TestExpressionBodiedExnHandler475` (3 tests) covers `Option`-returning, `Int`-returning, and trap-path catch arms.

### Shared infrastructure
- New `_clamp_i64_to_range_then_wrap(max_local_i64)` helper on `CallsStringsMixin` emits the canonical "clamp i64 to [0, max] then wrap to i32" sequence. Used by both `_translate_string_slice` and `_translate_char_code`; will be promoted to a shared location when PR 2 fixes finding 4 (`array_slice` same shape, in `calls_arrays.py`).

### Documentation
- **`@Byte` arithmetic exclusion documented in spec** ([#551](https://github.com/aallan/vera/issues/551) closed as not-a-bug; [#564](https://github.com/aallan/vera/issues/564) filed speculatively) — `vera/types.py` excludes `Byte` from `NUMERIC_TYPES`, so `@Byte - @Byte` (and similar) produce E140 at type-check time. The original #551 framing assumed a runtime underflow hole; investigation showed the checker prevents the construct entirely. Spec §4.4 and §11.2.1 updated to drop the previous "Byte enforcement tracked as #551" caveat in favour of a clear note that Byte arithmetic isn't permitted; user code that needs byte-level arithmetic uses `byte_to_int` / `int_to_byte` round-trip. The forward-looking *feature* (allow Byte arithmetic with verified underflow + overflow guards) is preserved as #564 with full design analysis (pros, cons, trigger conditions, action checklist) under a new ROADMAP `## Speculative` section. New `TestByteArithmeticRejection551` regression test (5 cases) pins the checker behaviour so a future widening of `NUMERIC_TYPES` can't silently re-open the underflow hole without a corresponding extension of the verifier obligation + codegen guard from #520.

## [0.0.127] - 2026-04-29

### Fixed
- **`@Nat` subtraction silent underflow — soundness hole closed** ([#520](https://github.com/aallan/vera/issues/520), closes) — pre-fix, the type system accepted `@Nat - @Nat : @Nat` but the runtime emitted a plain `i64.sub` with no underflow check, so a negative i64 could end up in a `@Nat` slot, undermining any Tier-1-verified contract that relied on `Nat >= 0` (and turning `@Array<T>[@Nat.0]` indexing with a bad `@Nat` into a memory-safety issue). The fix is two-sided: (a) the verifier emits a Tier-1 proof obligation `lhs >= rhs` at every `@Nat - @Nat` site whose result is statically `@Nat` AND at least one operand has `@Nat` *provenance* (slot ref or function return), discharged from preconditions and path conditions via Z3; (b) the codegen emits a runtime guard (`local.set $rhs; local.tee $lhs; local.get $rhs; i64.lt_s; if unreachable end; ...; i64.sub`) on the same set of sites so programs that skip `vera verify` still trap cleanly on underflow rather than silently producing negative `@Nat` values. The two analyses share the helper logic (`_is_static_nat_typed` + `_has_nat_origin_codegen` mirror the verifier's `_is_nat_typed` + `_has_nat_origin`) so the verifier's Tier-1-discharged sites and the codegen-guarded sites agree exactly.

### Path-A scope and follow-ups
- **Pure-literal subtractions like `0 - 1` are intentionally not flagged.** The corpus uses this idiom widely (e.g. `Err(_) -> 0 - 1` and `throw(0 - 1)`) where the result is consumed at `@Int` positions and the upcast is well-defined. The verifier and codegen both require at least one operand to have `@Nat` *provenance* (a slot ref to a `@Nat` slot or a function returning `@Nat`), distinguishing real `@Nat`-flowed subtractions from pure-literal "I want -1 as a literal" idioms.
- **`@Byte` follow-up tracked as [#551](https://github.com/aallan/vera/issues/551)** — the same underflow shape applies to `@Byte - @Byte` (`0..=255` range). Mechanical follow-up once the verifier helper and codegen guard are reusable.
- **Binding-site narrowing tracked as [#552](https://github.com/aallan/vera/issues/552)** — the verifier currently checks the `@Nat >= 0` invariant only at function return positions and at subtraction sites. Narrowing from `@Int` into a `@Nat`-typed let binding or function argument (e.g. `let @Nat = 0 - 1`) is not yet obligation-checked. Architectural generalisation; ships separately so the obligation infrastructure stays focused.

### New error code
- **E502** — `@Nat subtraction underflow obligation not discharged`. Counterexample-bearing diagnostic with rationale, fix suggestion (`requires(@Nat.0 >= @Nat.1)` or guarded if-branch), and spec ref to §4.4 + §11.2.1.

### Spec
- **`spec/04-expressions.md` §4.4** — short clause mirroring the divide-by-zero language: subtraction on unsigned types is undefined behaviour when it would underflow; the compiler SHOULD verify `lhs >= rhs` and MUST insert a runtime check otherwise.
- **`spec/11-compilation.md` §11.2.1** — full treatment with the Tier-1 proof obligation, Tier-3 fallback codegen (`(if (i64.lt_s lhs rhs) (then unreachable))`), the lift-back path via `requires`, and references to #551 (@Byte) and #552 (binding-site generalisation).
- **`spec/11-compilation.md` §11.3.3** — footnote on the operator table pointing back to §11.2.1 so readers of the canonical "what does each operator compile to" reference learn that `@Nat - @Nat` is conditionally guarded.

### Tests
- New `TestNatSubtractionObligation520` in `tests/test_verifier.py` (9 tests): requires-clause discharge, if-guard / path-condition discharge (canonical recursion shape), trivial discharges (`@Nat.0 - 0`, `@Nat.0 - @Nat.0`), unguarded-subtract counterexample, `@Int - @Int` and `@Nat - @Int → @Int` exclusions, partial-requires non-discharge, pure-literal exclusion documenting Path-A scope.
- New `TestNatSubtractionRuntimeGuard520` in `tests/test_codegen.py` (6 tests): underflow traps at runtime, safe subtraction passes through, structural WAT assertions that the guard appears for `@Nat - @Nat` and is absent for `@Int - @Int` and pure-literal `0 - 1`, deep-recursion path-discharged site runs without spurious traps.
- New `tests/conformance/ch04_nat_subtraction.vera` (Section 4.4 / 11.2.1) demonstrating the canonical discharge patterns: explicit `requires`, `if`-guarded recursion, trivial discharges (`a - 0`, `a - a`), and `@Nat - @Int → @Int` exclusion. Verifies at Tier 1 with 17 contracts; corpus is now 82 conformance programs.
- Existing tier-count assertions updated where the new obligations land: `test_recursive_call_decreases_verified` and `test_mixed_tiers` (3 → 4 T1), `test_overall_tier_counts` aggregate (219/26/245 → 222/26/248), `test_mutual_recursion_example_all_t1` (8 → 10 T1).

### Migration
- **Zero corpus migration needed.** Every existing `@Nat - @Nat` site in `examples/` and `tests/conformance/` is guarded by `if @Nat.0 == 0 then base else recursive(@Nat.0 - 1)` and Z3 discharges the obligation from the path condition automatically. External programs that previously verified at Tier 1 may now require explicit `requires(lhs >= rhs)` clauses on functions doing `@Nat - @Nat`; the diagnostic (E502) names the fix and shows the counterexample inputs.

## [0.0.126] - 2026-04-28

### Fixed
- **Tail-call optimization for non-allocating tail-recursive functions** ([#517](https://github.com/aallan/vera/issues/517), closes) — pre-fix, every Vera `call` site emitted a plain WASM `call` regardless of tail-position status, so a tail-recursive function pushed one WASM frame per iteration and trapped with `call stack exhausted` at ~tens of thousands of frames. The documented "iteration is tail recursion" idiom from `SKILL.md` thus silently failed past ~5–10K iterations. The fix is a per-fn analyzer (`vera/codegen/tail_position.py`) that marks `id(FnCall)` AST nodes in syntactic tail position; `_translate_call` emits `return_call $foo` instead of `call $foo` when the call's id is in the marked set AND the callee's WASM return type matches the caller's (required for WASM `return_call` semantics — the signature must match). Non-allocating tail-recursive functions now run in **constant stack space**: the canonical `count_down(50000)` reproducer succeeds, as does a 1M-iteration stress test.

### Tail-position analysis
- **Marking rules** (recursive on the function body):
  - The body's trailing expression IS in tail position.
  - If a sub-expression is in tail position and is an `IfExpr`, both branch bodies are in tail position. The condition is NOT.
  - If a sub-expression is in tail position and is a `MatchExpr`, every arm body is in tail position. The scrutinee is NOT.
  - If a sub-expression is in tail position and is a `Block`, only the trailing expression is in tail position. Statement values (`let` initialisers, `ExprStmt` expressions) are NOT.
  - All other constructs (call arguments, quantifier bodies, `assert`/`assume`, `handle` bodies, `AnonFn`, indexing) are NOT tail-transparent — calls inside them are not in tail position regardless of the parent's status.
- **Type-safety guard at emit time:** WASM `return_call` requires the callee's signature to match the caller's, so the translator falls back to plain `call` whenever the resolved callee's WASM return type doesn't match the current function's return type. The recursive case (call to the same function) trivially matches; cross-function tail calls match when signatures align.

### Allocating-function fallback
- **Allocating functions revert `return_call` → `call`** in a post-process step at the end of `_compile_fn`. WASM `return_call` discards the current frame, which means the GC epilogue (restore `$gc_sp`, unwind shadow-stack pointer slots) never runs. For an allocating function with tail calls, that leaks shadow-stack slots once per iteration and would eventually trap on the next `$alloc` once `gc_sp` passes the worklist boundary — strictly worse than the pre-fix "stack exhausted" trap. Until full GC-aware tail-call support lands ([#549](https://github.com/aallan/vera/issues/549) tracks the follow-up), allocating functions pay the WASM frame cost in exchange for correct shadow-stack management. Non-allocating functions (the common iteration-style tail recursion case) keep the optimization.

### Tests
- New `TestTailCallOptimization517` in `tests/test_codegen.py` (9 tests): 50K-iteration behavioural test (the issue's canonical reproducer), 1M-iteration stress test (pins constant-stack-space behaviour rather than just "deeper than the broken limit"), structural assertion that `return_call $count_down` appears in WAT for the recursive call, structural assertion that a let-bound (non-tail) call emits plain `call`, allocating-function fallback assertion (allocating tail-recursive function emits plain `call` not `return_call`), plus 4 analyzer unit tests covering each tail-transparent construct (Block trailing, both branches of IfExpr, let-value NOT marked, call args NOT marked).
- Existing fixtures in `tests/test_runtime_traps.py` updated for the TCO interaction: the `_DIVIDE_BY_ZERO_USER_FN`, `_CONTRACT_VIOLATION_PROGRAM`, and `_DIVZERO_FOR_FIX` test programs originally had `main` calling the trapping function in tail position, which #517 would now optimize away — discarding `main`'s frame and shortening the backtrace assertions expect to see. The fixtures now bind the call result with `let` and produce it via slot reference, keeping the call non-tail and preserving `main`'s frame on the WASM call stack. Comments document the intentional non-tail shape so a future contributor doesn't "simplify" them back into tail position.

### Improved
- **`stack_exhausted` trap Fix paragraph rewritten** to reflect the v0.0.126 reality. Pre-rewrite: "Vera doesn't yet emit `return_call` ... wait for #517 to ship". Post-rewrite: "Vera compiles tail-position calls to WASM `return_call` ... if you're still hitting this trap the recursion isn't actually in tail position. Restructure with an accumulator parameter so the recursive call is the LAST thing the function does (no work after it, no `let`-binding of its result, no enclosing arithmetic). Allocating functions are an exception ... iterate via `array_fold` / `array_map` (which compile to WASM loops rather than recursion)."

### Documentation
- **KNOWN_ISSUES.md** — #517 row removed (closed); new row added pointing at [#549](https://github.com/aallan/vera/issues/549) (GC-aware TCO follow-up for allocating functions, with restructure/array-fold workarounds).
- **ROADMAP.md** — #517 dropped from the bug-killing campaign queue (closed by this release); intro updated to "eight remain"; priority rows renumbered (#520 promoted to position 1).

## [0.0.125] - 2026-04-28

### Improved
- **Runtime traps now carry per-`kind` `Fix:` suggestion paragraphs** ([#547](https://github.com/aallan/vera/issues/547), closes; finishes [#516](https://github.com/aallan/vera/issues/516) Stage 3) — pre-fix, runtime traps surfaced kind + description + structured backtrace (Stages 1+2), but no actionable remediation paragraph.  Compile-time errors have always carried `description` / `rationale` / `fix` / `spec_ref`; runtime traps now match that surface with a Vera-native Fix paragraph appended after the source backtrace.
- **Refactor: `_classify_trap` returns `(kind, description, fix)`** instead of `(kind, message)`.  The previous shape crammed Fix-shaped hints inline in the message (e.g. `"Out-of-bounds memory access (if the trapping frame is gc_collect, see #515; otherwise check array indexing or string slicing)"`); Stage 3 splits them into clean fields so consumers can render description and fix independently.  New per-kind table `_TRAP_FIX_PARAGRAPHS` in `vera/codegen/api.py` carries the canonical content; empty string for `contract_violation` (the contract message itself already explains what failed) and `unknown` (no specific suggestion possible).
- **`WasmTrapError` gains a `fix: str` field** alongside the existing `kind` / `frames` / `stdout` / `stderr`.  Default `""` for backward compatibility with direct `WasmTrapError(...)` constructors that don't pass the keyword.
- **CLI surface**: text mode appends a `Fix:` block after the `Source backtrace:` block, with the paragraph wrapped to ~76 columns (matching the compile-time `Diagnostic` rendering style).  JSON mode adds a `fix` key to each trap diagnostic alongside `description` / `trap_kind` / `frames` — always present for schema stability, possibly empty.  Empty-string Fix paragraphs suppress the text-mode block entirely (no empty `Fix:` header noise) but still surface as `""` in JSON for shape stability.

### Per-kind Fix paragraph content
- `divide_by_zero` — "Add a precondition `requires(divisor != 0)` on the function performing the division, or guard the division site with a non-zero check.  The Z3 verifier will then prove the division is safe at every call site at compile time."
- `out_of_bounds` — names the two most-likely user causes (array indexing, string slicing) and the runtime-helper escape hatch (file an issue if the trap is inside `gc_collect` / `alloc` / etc.).
- `stack_exhausted` — references [#517](https://github.com/aallan/vera/issues/517) (the open TCO issue) so an agent reading the Fix knows this is a known limitation rather than a bug they should report; will be rewritten to reference `return_call` as a supported feature once #517 lands.
- `unreachable` — names the most-likely cause (non-exhaustive `match`) and the resolution path (add the missing arm explicitly).
- `overflow` — names the i64 range and the canonical remediation (precondition guarded by Z3).

### Tests
- New `TestTrapFixParagraphs547` (6 tests) — text-mode Fix-block surfacing with position-ordering invariant (Fix appears after backtrace), text-mode block suppression for `contract_violation`, JSON-mode `fix` field always-present, JSON `fix` field empty-but-present for `contract_violation`, table-completeness assertion (every kind in the taxonomy has a `_TRAP_FIX_PARAGRAPHS` entry — adding a new kind without its Fix paragraph fails this test immediately), and column-wrap invariant (~76 chars max per line, two-space indent under `Fix:` heading).
- Existing `TestClassifyTrap` (7 tests) updated for the new 3-tuple return shape; per-kind assertions now also verify the Fix paragraph content matches expected substrings (`"requires(divisor != 0)"` for `divide_by_zero`, `"#517"` + `"return_call"` for `stack_exhausted`, etc.).
- `TestWasmTrapError` extended to verify the `fix` field round-trips through the constructor and defaults to `""`.

### Documentation
- **KNOWN_ISSUES.md** — #516 row removed (Stage 3 closes it; the parent #516 was closed by the v0.0.124 PR's "Closes #516 Stage 2" wording, but the doc kept the row open against #516; with #547 closed too the entire row is gone).
- **ROADMAP.md** — #516 / #547 dropped from the bug-killing campaign queue (closed by this release); intro string updated to reflect "nine remain" instead of "ten remain".

## [0.0.124] - 2026-04-27

### Improved
- **Runtime traps now carry a source backtrace** ([#516](https://github.com/aallan/vera/issues/516) Stage 2) — pre-fix, `WasmTrapError` carried only the classified `kind` and the trap message; the user got "Integer division by zero" with no indication of *which* of their functions divided by zero. Stage 2 walks `wasmtime.Trap.frames` after classification, looks each frame's WAT name up in a new `CompileResult.fn_source_map` (built during codegen by `_register_fn` and the closure-lifting pass), and produces a structured backtrace: `[{func, file, line_start, line_end, is_builtin}, …]`, outermost (leaf) frame first to match wasmtime / gdb / Python convention. Per-function granularity, not per-line — wasmtime-py doesn't expose WAT-to-WASM debug-info plumbing, so byte-offset → line mapping isn't viable; "trap inside `divide` (foo.vera:1-5)" is exactly the success criterion the issue calls out.
- **Resolution rules for the trap-frame walker** (`_resolve_trap_frames` in `vera/codegen/api.py`):
  - **Built-ins are tagged, not dropped.** WAT functions named `alloc` / `gc_collect` / `contract_fail`, plus anything starting with `exn_` / `vera.` / `closure_sig_`, are runtime infrastructure with no Vera source. They're surfaced as `<builtin>` rather than reported as missing-source-map entries (which would look like a regression).
  - **Monomorphized generics suffix-strip at the rightmost `$`.** `identity$Int` looks up `identity` because `$` cannot appear in user-written Vera identifiers, so any `$` in a WAT name was inserted by the monomorphizer (`vera/codegen/monomorphize.py::_mangle_fn_name`).
  - **Lifted closures register under `anon_N`.** The closure-lifting pass tags each `$anon_N` with the source span of the original `fn(...)` syntactic site, so a trap inside a closure points at the closure body, not at the synthetic top-level wrapper.
  - **Unknown user-named frames are surfaced with `<unknown>` location, not dropped.** Better to show the WAT name with no location than lose the frame entirely — the user still benefits from knowing which function trapped, and any future source-map gap is diagnosable from the unknown markers.
- **`cmd_run` text-mode rendering** appends a `Source backtrace:` block after the error line. Leading runtime-helper frames (the ones the user can't act on — they trapped inside `$alloc` while serving a user request) are collapsed with a `(suppressed N runtime-helper frames above first user code)` marker so the user sees their own code at the top of the trace. JSON mode adds a `frames` array to each diagnostic with the full structured backtrace (including built-ins, so machine consumers see the full picture).

### Tests
- New `TestResolveTrapFrames516` (7 tests) — unit tests for the resolver in isolation using a `_FakeFrame` shim: user-fn resolution, built-in tagging, built-in-prefix matching, monomorphized base-name fallback, unknown-name surfacing, defensive empty-frames behaviour, leaf-first ordering preservation.
- New `TestTrapSourceBacktrace516` (5 tests) — end-to-end via `cmd_run`: text-mode backtrace surfaces, leaf-first ordering preserved across stream output, JSON envelope includes `frames` array, contract violations carry the same backtrace, direct `execute()` callers also get `WasmTrapError.frames`.
- New `TestSourceMapPopulation516` (3 tests) — light-weight inspection of `CompileResult.fn_source_map`: top-level fns registered, lifted closures registered under `anon_N`, built-in helpers NOT registered (would yield bogus user-frame entries).

### Documentation
- **KNOWN_ISSUES.md** — #516 row updated to note Stage 2 shipped (Stage 3 remains).
- **ROADMAP.md** — #516 row in the bug-killing campaign queue updated; intro string unchanged (the row stays in the queue until Stage 3 ships).

### Stage 3 follow-up
- Tracked as [#547](https://github.com/aallan/vera/issues/547) — per-`kind` `Fix:` suggestion paragraphs to match the compile-time `Diagnostic` shape (description / rationale / fix / spec_ref).  Will be picked up after #517 (TCO) lands so the `stack_exhausted` Fix paragraph can reference `return_call` as shipped rather than as "see #517 for the planned fix".

## [0.0.123] - 2026-04-27

### Fixed
- **`IO.print` writes mirror live to `sys.stdout` in `vera run` text mode** ([#543](https://github.com/aallan/vera/issues/543), closes) — `IO.print` output was buffered in an in-memory `output_buf` (the v0.0.120 implementation of #522 trap preservation) and only flushed to `sys.stdout` after `execute()` returned. That was correct for trap preservation and for `--json` output (where the transcript packs into the envelope), but it had an unintended side effect: any program using ANSI escape sequences for animation (cursor home `ESC[H`, clear screen `ESC[J`), progress bars, REPLs, or any other interactive pattern was invisible until exit. The 470-line Conway implementation that surfaced #515 made it visible: ~16 seconds of `IO.sleep(80)` × 200 generations with nothing on screen, then exit fired and only the final frame was visible because the 199 preceding cursor-home + clear-screen sequences processed within microseconds and the eye couldn't resolve them.
- Fix is a tee in `host_print` (`vera/codegen/api.py`): the in-memory `output_buf` still receives every byte (so `WasmTrapError.stdout`, `ExecuteResult.stdout`, and the `--json` envelope's `stdout` field are unchanged), and *also* writes go to `sys.stdout` with an explicit per-write `flush()` when `execute(tee_stdout=True)`. New `tee_stdout: bool = False` parameter on `execute()` defaults *off* — preserves test-helper silence (`_run_io`, `_run` in `tests/test_codegen.py` rely on `ExecuteResult.stdout` and would pollute pytest's `capsys` if the default flipped). `cmd_run` text mode opts in (`tee_stdout=not as_json`); JSON mode stays off (live writes would split the envelope for downstream consumers parsing our stdout). The `cmd_run` text-mode and `WasmTrapError`-handler paths now skip re-writing `exec_result.stdout` / `exc.stdout` (those bytes already streamed live), only emitting a closing `\n` if the program's last write didn't include one — without that change every program's transcript would have double-printed.

### Tests
- New `TestStdoutTee543` class in `tests/test_runtime_traps.py` (6 tests): live streaming in text mode, write count and order preservation, JSON-mode tee suppression (envelope-corruption prevention), trap-preservation invariant still holds (#522 regression guard), per-call flush count matches per-call `IO.print` count, default `execute()` behaviour stays silent for the test suite.

### Documentation
- **SKILL.md** — IO operation table cell for `IO.print` notes "no implicit newline; flushes per call". New paragraph after the table explains output buffering: under `vera run` text mode every write is live and flushed; under `vera run --json` the transcript lives only in the envelope; in both cases the in-memory capture survives traps so `WasmTrapError.stdout` and the JSON `stdout` field are complete. Pre-v0.0.123 the whole transcript was buffered until exit — note included so anyone reading the doc with an older Vera installed isn't surprised.

## [0.0.122] - 2026-04-27

### Fixed
- **Conservative GC bounds-checked against `$heap_ptr`** ([#515](https://github.com/aallan/vera/issues/515), closes) — `$gc_collect` no longer faults under sustained allocation pressure. Root cause: the Phase 2 worklist-seeding code accepts a shadow-stack value as a heap pointer if it satisfies three guards — in heap range (`val >= gc_heap_start + 4`), aligned (`(val - gc_heap_start) % 8 == 4`), and below `$heap_ptr`. None of those guards prove the word at `val - 4` is an actual object header. A non-pointer i32 in payload data (a bit-packed `Nat` row in Conway-style code, a hash, anything else with bits in heap range) can satisfy all three; Phase 2b then reads garbage as `obj_size = header >> 1`, sets the mark bit (corrupting a random word!), and walks `obj_ptr + 0..obj_size` past `$heap_ptr` and past the linear-memory boundary, trapping with `memory fault at wasm address 0x... in linear memory of size 0x...` — `gc_collect` itself at the top of the stack. Two layers of defence emitted in `_emit_gc_collect` (`vera/codegen/assembly.py`):
  - **Layer 2 (early skip)** — before either marking or scanning a worklist entry, compute `obj_size` and verify `obj_ptr + obj_size <= heap_ptr`. If not, the entry is a Phase 2 false positive: skip it entirely (no mark store, no scan loop). This catches the bug at the cheapest possible point and prevents the mark-bit corruption that would otherwise persist across the cycle.
  - **Layer 1 (per-iter)** — inside the conservative scan loop, also check `obj_ptr + scan_ptr + 4 <= heap_ptr` before each `i32.load`. Costs a single `i32.add` + `global.get` + `i32.gt_u` per iteration relative to the load itself — negligible — and protects any future caller that reaches this loop without the Layer-2 check (e.g. a precise scan path added later, or a refactor that bypasses the early skip).
  Verified end-to-end with a 470-line Conway implementation that pre-fix reliably crashed at generation 56 (heap saturated with rendered-frame strings, bit-packed `Nat` row values matching the alignment + range guards): runs cleanly through every generation post-fix. Structural regression test in `tests/test_codegen.py::TestGarbageCollection::test_gc_collect_bounds_check_against_heap_ptr` asserts both bound checks survive in the emitted WAT — behavioural reproducers for #515 are heavily layout-sensitive (string-pool offsets, allocation order), so a structural assertion is the durable regression guard.

### Documentation
- **KNOWN_ISSUES.md** — #515 row removed from the Bugs table.
- **ROADMAP.md** — #515 dropped from the bug-killing campaign queue (closed by this release); intro updated to reflect "ten remain" instead of "eleven remain"; the "[#487](#487) likely also alleviates pressure on #515" note removed (now moot).

## [0.0.121] - 2026-04-27

### Fixed
- **Nested closures work end-to-end** ([#514](https://github.com/aallan/vera/issues/514), closes) — closures inside closure bodies (the natural 2D `array_map(rows, fn(row) { array_map(cols, fn(col) { ... }) })` shape) failed WASM validation pre-fix because only the outermost closure was lifted to a top-level function. The closure-lifting pass at `vera/codegen/closures.py:_lift_pending_closures` iterated only the outer `WasmContext`'s `_pending_closures` list; `_compile_lifted_closure` created a fresh inner ctx to translate the body, and any `fn { ... }` discovered during that translation registered on the inner ctx — never bubbled back. Result: only `$anon_0` ended up in the function table, and the inner call_indirect targeted a missing entry, surfacing as `type mismatch: expected i64, found i32` (validation) when the inner returned a pair type, or `unreachable` (runtime) otherwise. Fix is a worklist: `_lift_pending_closures` now pops closures one at a time, collects any inner pending discovered during each lift via a new `collect_pending` parameter on `_compile_lifted_closure`, and feeds them back. Inner ctx's `_closure_sigs` and `_next_closure_id` are now shared by reference with the module-level state to avoid `$closure_sig_0` / `$anon_0` name collisions across contexts. The lifter now handles arbitrary nesting depth (verified at three levels). Also fixes `_walk_free_vars` to recurse into nested `AnonFn` expressions so captures from the outer scope referenced inside an inner closure resolve correctly — pre-fix the recursion case was missing entirely; the bug was latent because nested closures didn't make it through lifting in the first place.

### Improved
- **Closure capture works for ADTs** — same fix scope. The historical [#514](https://github.com/aallan/vera/issues/514) framing claimed "all heap captures broken"; investigation showed ADT captures (`Option<T>`, `Result<T, E>`, user `data` types, `Map`/`Set`/`Decimal`/`Regex`) actually work because they're single-i32-pointer values, not pairs. Only `String` and `Array<T>` are still broken (the closure-struct serialiser drops the len field) — this residual is now scoped to its own issue [#535](https://github.com/aallan/vera/issues/535) with a pointer-only fix path. SKILL.md "Capturing outer bindings" rewritten to reflect this accurate picture; the over-broad "primitives only" claim is gone.

### Tests
- New `TestNestedClosures` class in `tests/test_codegen_closures.py` (5 tests): nested closure with primitive return, nested with pair return (the original #514 reproducer), nested with outer-param capture, three-level nesting (worklist depth), white-box check that the lifted function table contains both `$anon_0` and `$anon_1`.

### Examples + conformance
- New `examples/nested_closures.vera` — `build_grid` (3×4 multiplication-style grid via 2D `array_map`), `grid_sum` (two-layer `array_fold` that uses inner closures), `three_d_count` (3D nesting). Verifies all 6 contracts at Tier 1.
- New `tests/conformance/ch05_nested_closures.vera` (level: `run`) — covers 2D no-capture, 2D with-capture across the nesting boundary, 3D nesting, all in one program.

### Documentation
- **SKILL.md: Capturing outer bindings rewritten** — the old "primitives only" framing was inaccurate (ADTs work too). New text: primitives + ADTs work; only pair types (`String`, `Array<T>`) remain broken, scoped to [#535](https://github.com/aallan/vera/issues/535). The "Known limitation: nested closures" subsection is removed entirely (no longer broken). The Known Bugs table row for #514 is replaced with a row for #535. The #522 row is removed (closed in v0.0.120). The #516 row is updated to clarify that Stage 1 (categorisation) shipped in v0.0.120 and Stages 2-3 (source mapping + Fix paragraphs) remain.
- **KNOWN_ISSUES.md** parallel updates to the SKILL changes.
- **ROADMAP**: removed #514 row from the bug-killing campaign queue (closed by this release); inserted #535 at the bottom of the active queue (workaround exists, lower urgency); intro updated to reflect "ten remain" instead of "eleven remain".

### CI
- **Drop the CVE-2026-3219 ignore in `dependency-audit`** ([#527](https://github.com/aallan/vera/issues/527), closes) — pip 26.1 shipped on 2026-04-26 with the [pypa/pip#13870](https://github.com/pypa/pip/pull/13870) fix that addresses the concatenated-tar+ZIP archive-handling bug ([CVE-2026-3219](https://nvd.nist.gov/vuln/detail/CVE-2026-3219), [GHSA-58qw-9mgm-455v](https://github.com/advisories/GHSA-58qw-9mgm-455v)). Verified locally that `pip-audit --skip-editable` against pip 26.1 returns "No known vulnerabilities found" without the ignore. Removed the `--ignore-vuln CVE-2026-3219` flag from the workflow, the corresponding row from KNOWN_ISSUES.md's "CI ignores" table, and the per-flag annotation from TESTING.md's command example. The pygments CVE-2026-4539 ignore stays in place pending an upstream fix release.

## [0.0.120] - 2026-04-26

### Fixed
- **`IO.print` output preserved on trap** ([#522](https://github.com/aallan/vera/issues/522), closes) — the `host_print` implementation in `vera/codegen/api.py` appends to a Python `io.StringIO` that was only surfaced to the CLI on the success path. On the trap path the buffer was discarded as the exception unwound out of `execute()`, so any `IO.print` calls preceding a runtime crash were lost — exactly when an agent had inserted them to instrument the suspected crash site. Fixed by introducing `WasmTrapError` (a `RuntimeError` subclass carrying `stdout`, `stderr`, and `kind`); `execute()` now raises it on every trap path with the captured buffers attached, and `cmd_run` writes them to `sys.stdout` / `sys.stderr` (text mode) or includes them in the JSON envelope (JSON mode) before reporting the error. Order is preserved under `2>&1` redirects via an explicit `sys.stdout.flush()` after the captured-output write.
- **`IO.stderr` capture wired through to `cmd_run`** — sibling fix completing the `WasmTrapError.stderr` and JSON-envelope `stderr` contracts. `cmd_run` now passes `capture_stderr=True` to `execute()` (was always defaulting to `False`), so `IO.stderr` writes are buffered into `ExecuteResult.stderr` rather than falling through to live `sys.stderr` writes. The success path now also replays `exec_result.stderr` to `sys.stderr` (text mode) or includes it in the JSON envelope (JSON mode), parallel to the existing stdout treatment. Without this, the `WasmTrapError.stderr` field documented in the previous bullet was permanently empty in production, even though the host-side infrastructure (`host_stderr` writing to `stderr_buf`) had been in place since #463. New regression test `test_json_mode_includes_stderr_in_envelope` pins the contract.

### Improved
- **Runtime trap categorisation — Stage 1 of [#516](https://github.com/aallan/vera/issues/516)** — every WASM trap was previously relabelled `Runtime contract violation` by the CLI's catch-all, even when the actual cause was integer division by zero, out-of-bounds memory access, call stack exhaustion, or the `unreachable` instruction. The new `_classify_trap` helper in `vera/codegen/api.py` inspects the wasmtime exception message and maps it to a stable `kind` plus a Vera-native description: `divide_by_zero`, `out_of_bounds`, `stack_exhausted`, `unreachable`, `overflow`, `contract_violation`, or `unknown`. The contract-violation path remains via the existing `last_violation` host-import channel, which always wins over the wasmtime trap reason. JSON mode now includes `trap_kind` in each diagnostic so downstream consumers (LSP, agents, future tooling) can branch on the structured value instead of pattern-matching free text. Stages 2 (source mapping the trapping function) and 3 (per-`kind` `Fix:` paragraphs) remain open under #516.

### Tests
- New `tests/test_runtime_traps.py` — 16 tests covering the classifier (every documented `kind` in isolation), the `WasmTrapError` shape, the end-to-end stdout-on-trap fix in both text and JSON modes, and trap-kind reporting in both modes. Pure-helper tests use a `_FakeTrap` exception class — the classifier is stringly-typed against the wasmtime trap message format, so it can be exercised without a wasmtime runtime, and we benefit from that decoupling for tests.

### Website
- **Mobile overflow fixes in `docs/index.html`** (folded in from PR #532) — three iOS Safari overflow bugs at iPhone widths: (a) hero meerkat image overflowed because `.hero-image` had `max-width:640px` but no `width:100%` (global `img{max-width:100%}` was beaten on specificity); (b) the VeraBench `vera-bench` GitHub button was stretched into a full-width pill because the mobile `.btn{width:100%}` rule (intended for hero CTA stacking) matched every `.btn`, so the bench button got scoped to `.cta-bar .btn`; (c) shell-command code blocks in the "Runs Everywhere" section ran past the viewport edge because `<pre>` defaults to `white-space:pre`, so a mobile-only `.code-block pre{white-space:pre-wrap;word-break:break-word}` rule was added (Vera sample blocks intentionally keep `pre` to preserve syntax indentation). Verified at 375×812 viewport: all three elements fit cleanly.

### Tooling notes
- **[mcp-assert](https://github.com/blackwell-systems/mcp-assert) bookmarked as the test harness for any future Vera MCP server** ([#529](https://github.com/aallan/vera/issues/529)) — Go binary (also pip / npm / brew) that connects to MCP servers over stdio/SSE/HTTP, calls their tools, and asserts results against YAML-defined expectations. Language-agnostic on the server side, MIT-licensed, GitHub Action available. Scope is deterministic tools (data retrieval, state changes, validation) — exactly the shape Vera would expose (`vera_check`, `vera_verify`, `vera_compile`, `vera_context`). Not adopted today (no MCP server to test yet); cross-referenced from [#401](https://github.com/aallan/vera/issues/401) so whoever picks up the documentation MCP endpoint inherits the harness recommendation.

### CI
- **Ignore [CVE-2026-3219](https://nvd.nist.gov/vuln/detail/CVE-2026-3219) in `dependency-audit`** ([#527](https://github.com/aallan/vera/issues/527)) — pip 26.0.1 is flagged for an archive-handling bug (GHSA-58qw-9mgm-455v) where concatenated tar+ZIP files are parsed as ZIP regardless of the filename. Upstream fix merged in [pypa/pip#13870](https://github.com/pypa/pip/pull/13870) under the pip 26.1 milestone but not yet released; no patched version exists to upgrade to. Threat model (installing untrusted ambiguously-formatted archives) does not apply to our CI. The ignore is bridging until pip 26.1 lands on PyPI; removal is tracked as an action item on #527 and in the new "CI ignores" section of KNOWN_ISSUES.md.

### Website fixes (follow-up to PR #526 review)
- **`research_topic` homepage sample: URL-encode query** — the DuckDuckGo sample interpolated the raw `@String.0` parameter into the query string, so any input with a space, `&`, `%`, or unicode character would have been rejected by the server. Added a `let @String = url_encode(@String.0)` before the `Http.get` call; verified via `vera check`. The fix also demonstrates Vera's `url_encode` built-in in a realistic position, a small bonus for agents reading the homepage.
- **Status-paragraph fact drift** — two numbers in the status paragraph were already stale before this PR: "six algebraic effects (IO, Http, State, Exceptions, Async, Inference)" was missing `Random` (actual count: seven); "77-program conformance suite" was actually 80; HTML's "30 worked examples" was actually 32. Corrected in both `docs/index.html` and `scripts/build_site.py::build_index_md()`. The markdown generator now also uses `{n_conformance}` dynamically (previously only `{n_examples}` was dynamic). The structural fix (auto-generate or gate all homepage numbers via `check_doc_counts.py`) is tracked as [#528](https://github.com/aallan/vera/issues/528) and placed in ROADMAP Phase 3b.

### Website
- **veralang.dev homepage redesign** — full-page redesign of `docs/index.html` with an editorial-research aesthetic. Structural changes: bilingual reading-path device at the top of the page (`@reader.0 → humans` / `@reader.1 → agents` using Vera's own slot-reference syntax to acknowledge the dual audience), "Why?" thesis promoted to a weighty anchor with a serif-display callout, four-sample code showcase with commentary stacked below each block (rather than a side-by-side column that clipped long lines like `research_topic`'s URL concat), VeraBench lifted above the reference grid with a masthead stat ("Kimi K2.5 writes 100% correct Vera…") paired to the mascot in a baseline-aligned lockup, reference grid condensed from 17 features to 9 (typed-stdlib entries merged; "Full contracts" and "Contract-driven testing" merged), dark "For Agents" section framing the page as a machine-readable specification with the three agent-facing documents (SKILL.md, AGENTS.md, CLAUDE.md) as discrete cards. Visual system: three-font hierarchy committed — DM Serif Display for statements the site is making, Inter for explanations, JetBrains Mono for machine surfaces (code, eyebrows, readpath); cream/brown/orange palette aligned to Negroni's Vermouth + Campari scales (`#FFECD1` matches the hero meerkat's baked cream, `#FEEAD1` pinned to the VeraBench section to match its chart/mascot assets, `#FFE2CE` used as contrast surface). Meerkat hero, briefing-mandated font stack, and single-file HTML preserved; no frameworks, no trackers, no analytics. All load-bearing agent metadata preserved: `<link rel="alternate">` entries, `rel="llms-txt"`/`rel="llms-full-txt"` directives, schema.org JSON-LD block, and the inline `<script type="text/llms.txt">` hand-off at the bottom of `<body>`.
- **[Agent Score](https://buildwithfern.com/agent-score) improvements** — score moved from **3 failures + 2 warnings** to **2 failures + 1 warning**. Cleared: `page-size-html` (52.9K → 44.9K, under the 50K edge), `page-size-markdown` (13K, under 50K), `llms-txt-directive` (was failing), `llms-txt-freshness` (was 1/2 sitemap doc pages covered; now 2/2). Improved: `markdown-content-parity` miss rate 90% → 21% (4× reduction). `scripts/build_site.py`'s `build_llms_txt()` gained a `## Homepage` section referencing `/index.md`; `build_index_md()` rewritten to mirror the HTML section-for-section (thesis, code samples, VeraBench stat + table, runtime, install, For Agents, status) — `/index.md` grew from 4,019 to 13,491 chars. Remaining gaps (content negotiation, inline-CSS truncation budget, final 21% parity) tracked in [#525](https://github.com/aallan/vera/issues/525) and added to ROADMAP Phase 3b.

### Planned
- **`vera context` — token-budgeted project context export** ([#523](https://github.com/aallan/vera/issues/523)) — new CLI command that walks a project's dependency graph and emits a compact summary of public signatures, contracts, effects, ADTs, and imports for LLM consumption. Directly inspired by [Aver](https://averlang.dev)'s [`aver context`](https://github.com/jasisz/aver#context-export); credit retained through implementation, commit messages, and CLI help text. Placed in Milestone 3 Phase 3a alongside the LSP server and Plumbing integration.

### Documentation
- **SKILL.md documentation sweep** ([#513](https://github.com/aallan/vera/issues/513)) — eight sections added or rewritten to close agent-surfaced documentation gaps: Array literals (`[]` / `[1, 2, 3]` / type inference), Closures and captured bindings (syntax + De Bruijn shift rule + the primitives-only capture limitation + tail-recursion-with-explicit-parameters workaround), full string escape-sequence table (`\n` / `\t` / `\r` / `\0` / `\\` / `\"` / `\u{XXXX}`) with explicit unsupported list and fallback notes, Nullary vs Unit-taking function signature variants, Stored function values and `apply_fn`, Known Bugs and Workarounds section pointing at KNOWN_ISSUES.md. Additions validated against `scripts/check_skill_examples.py`; ALLOWLIST regenerated (68 unique entries, AST-verified zero duplicate keys).

### Tracked bugs
- Five compiler/runtime bugs surfaced by an agent writing Conway's Game of Life against v0.0.119, filed + added to KNOWN_ISSUES.md + reprioritised in ROADMAP.md's implementation-order table:
  - [#514](https://github.com/aallan/vera/issues/514) — closure codegen mis-emits environment when capturing heap-allocated outer bindings (Array, String, ADT). Primitive captures work; any heap capture fails at WASM validation. Root cause refined from earlier "nested closures" symptom.
  - [#515](https://github.com/aallan/vera/issues/515) — `$gc_collect` walks past `$heap_ptr` to the linear-memory bound and traps mid-sweep.
  - [#516](https://github.com/aallan/vera/issues/516) — runtime traps bubble up as raw wasmtime stack traces; CLI mis-labels every trap as "Runtime contract violation".
  - [#517](https://github.com/aallan/vera/issues/517) — no tail-call optimization; the documented tail-recursion iteration idiom blows the WASM call stack at ~tens of thousands of frames.
  - [#522](https://github.com/aallan/vera/issues/522) — `IO.print` output lost on trap. **Filed early in this cycle and fixed by this release** — see the `### Fixed` section above for the implementation (`WasmTrapError` carrying captured `stdout`/`stderr`/`kind`). Paired with #516 Stage 1 (also in `### Improved` above) — both close the "type-checks clean, runtime crashes opaque, can't even instrument" gap.

## [0.0.119] - 2026-04-23

### Added
- **JSON typed accessors** ([#366](https://github.com/aallan/vera/issues/366)) — eleven new accessor functions for the built-in `Json` ADT that eliminate the two-level pattern-match boilerplate every JSON API consumer would otherwise write (`match option ... { Some(@Json) -> match @Json.0 { JNumber(@Float64) -> ... } }`). Six Layer-1 type-coercion accessors (`Json → Option<T>`): `json_as_string`, `json_as_number`, `json_as_bool`, `json_as_int`, `json_as_array`, `json_as_object`. Five Layer-2 compound field accessors (`Json, String → Option<T>`): `json_get_string`, `json_get_number`, `json_get_bool`, `json_get_int`, `json_get_array` — each collapses `json_get` + the matching `json_as_*` into one call, so missing fields and wrong-typed fields both return `None`. `json_as_int` guards every `float_to_int` trap path — NaN, infinity, AND finite overflow (|f| ≥ 2^63) — returning `None` for all four non-representable-as-Int cases. Closes [#366](https://github.com/aallan/vera/issues/366).

### Implementation
- Unlike the v0.0.118 batch, these are **pure-Vera prelude functions**, not WASM translators. All eleven bodies live in `vera/prelude.py` `_JSON_COMBINATORS` as match expressions over `Json`; the compiler already injects the prelude into every module that references `Json` values. No new host imports, no new WASM emit, no new WASM bytes in compiled modules that don't use them.
- `vera/environment.py` registers the eleven new `FunctionInfo` entries next to the existing `json_get` / `json_type` registrations. Layer-1 accessors emitted from a tight Python loop (six identical `Json → Option<T>` shapes differ only in element type).
- `vera/prelude.py` extended in three places: `_JSON_COMBINATORS` (bodies), `_source_mentions_json` (detection names set), and the injection site's `json_fn_names` shadow-check set.

### Example rewrite
- `examples/json.vera` rewritten to showcase the new accessors — the gradebook weather-API scenario that previously used a helper-per-field pattern now uses Layer-2 accessors directly. Line count stayed roughly similar (the rewrite is a style change, not a pure reduction) but the per-field boilerplate disappeared: `json_get_array(obj, "hourly")` replaces the two-level `match json_get(obj, "hourly") { Some(@Json) -> match @Json.0 { JArray(@Array<Json>) -> ... } }` pattern.

### Tests
- 19 new unit tests in `tests/test_codegen.py::TestJsonTypedAccessors` covering every Layer-1 accessor (matching + mismatched constructors), every Layer-2 accessor (hit + missing field + wrong type), plus targeted edge cases: `json_as_int` NaN guard, `json_as_int` infinity guard, `json_as_int` toward-zero truncation for negative floats, and `json_as_coercions_are_disjoint` pinning the invariant that at most one Layer-1 accessor returns `Some` for any given Json.
- 2 new browser parity tests in `tests/test_browser.py::TestBrowserJsonAccessors` — one exhaustive Layer-1 sweep, one exhaustive Layer-2 sweep.
- New conformance program `tests/conformance/ch09_json_accessors.vera` (level: `run`) with 15 test functions.

## [0.0.118] - 2026-04-23

### Added
- **String utility built-ins** ([#470](https://github.com/aallan/vera/issues/470)) — eight pure string operations implemented entirely as inline WAT. Splits: `string_chars` (1-byte strings, the canonical bridge from `String` to `Array<String>`), `string_lines` (`\n` / `\r\n` / `\r` terminators, Python `splitlines()` semantics — trailing terminator does not add empty segment), `string_words` (whitespace-run splits, Python `split()` semantics — runs collapse, empty segments discarded). Transformations: `string_reverse`, `string_trim_start`, `string_trim_end`. Padding: `string_pad_start(s, n, fill)` / `string_pad_end(s, n, fill)` with JavaScript `padStart`/`padEnd` semantics — fill cycles left-to-right and is truncated to exactly the padding length, empty fill is a no-op, target shorter than input returns input unchanged. The three split operations return `Array<String>` whose elements are each independently `$alloc`-ed and copied — interior pointers into a shared backing buffer would fail the GC mark phase's alignment check (`(val - gc_heap_start) % 8 == 4` in `_emit_gc_collect`) and become unreachable after the function returns. Conformance: `ch09_string_char_utilities.vera`. Example: `examples/string_utilities.vera` exercises every operation in a coherent log-line-processor scenario. Closes [#470](https://github.com/aallan/vera/issues/470).
- **Character classification built-ins** ([#471](https://github.com/aallan/vera/issues/471)) — eight pure first-byte ASCII operations implemented as inline WAT (no host calls). Six classifiers: `is_digit` (`'0'..'9'`), `is_alpha` (`'A'..'Z'`, `'a'..'z'`), `is_alphanumeric` (union), `is_whitespace` (Python `str.isspace()` ASCII set — tab/LF/VT/FF/CR/space), `is_upper`, `is_lower`. Each inspects the **first byte** of the input and returns `false` for the empty string. Two case-conversion: `char_to_upper` / `char_to_lower` transform only the first byte, leaving remaining bytes untouched (useful for title-casing tokens). Eliminates the brittle hand-rolled `byte == 48 || byte == 49 || ...` patterns. ASCII-only — Unicode-aware variants tracked separately. Closes [#471](https://github.com/aallan/vera/issues/471).

### Compiler
- `vera/wasm/calls_strings.py` grew with sixteen new translators (~600 lines added). Shared helpers: `_translate_classifier(arg, env, *, body)` factors the predicate scaffolding for the six classifiers; `_translate_char_case(arg, env, *, to_upper)` factors `char_to_upper`/`char_to_lower`; `_translate_trim(arg, env, *, trim_start, trim_end)` factors both trim variants; `_translate_pad(...)` factors both pad variants; `_translate_structural_split(arg, env, *, mode)` factors `string_lines` and `string_words` via a two-pass count-then-emit shape that mirrors the existing `string_split` implementation.
- ASCII range tricks reused throughout: `(byte - 48) < 10` for digit, `((byte | 0x20) - 97) < 26` for case-folded alpha. Branchless and idiomatic across the classifiers.
- `vera/wasm/calls.py` dispatch table extended with sixteen new branches, plus `_infer_concat_elem_type` now returns `"String"` for `string_chars` / `string_lines` / `string_words` so monomorphization picks up the array element type correctly.
- `vera/wasm/inference.py` extended in both `_infer_fncall_wasm_type` and `_infer_fncall_vera_type` for the new return-type shapes (`i32_pair` for the string and array returns, `i32` for the bool returns).
- `vera/codegen/modules.py` known-names allowlist extended.
- `vera/environment.py` registers the sixteen new `FunctionInfo` entries in a tight block — the six classifiers are emitted via a Python loop because their `(STRING,) -> BOOL` signatures are identical.

### Tests
- 33 new unit tests across `TestCharClassification` and `TestStringUtilities` in `tests/test_codegen.py`.
- 9 new browser parity tests across `TestBrowserStringUtilities` and `TestBrowserCharClassification` in `tests/test_browser.py`. All sixteen built-ins produce bit-identical output under `wasmtime` and Node.js — they're inline WAT, no host imports.
- New conformance program `tests/conformance/ch09_string_char_utilities.vera` (level: `run`) with 14 test functions covering classifier, transform, pad, and split semantics including all the empty-string and edge-case shapes.

## [0.0.117] - 2026-04-22

### Added
- **Array utility built-ins (phase 1)** ([#466](https://github.com/aallan/vera/issues/466)) — seven new array combinators that complete the higher-order array operations suite without requiring ability dispatch on the polymorphic element type. `array_mapi<A, B>(arr, fn(A, Nat → B))` maps with a zero-based index — collapses the recursive-accumulator-with-index pattern that had been a leading source of De Bruijn indexing mistakes. `array_reverse<T>(arr)` reverses element order. `array_find<T>(arr, pred) → Option<T>` returns the first match, short-circuiting. `array_any<T>` and `array_all<T>` are existential / universal predicates with short-circuit evaluation and correct vacuous-truth on empty input (`any([], _) == false`, `all([], _) == true`). `array_flatten<T>(Array<Array<T>>)` concatenates one level of nesting via a two-pass length-then-copy. `array_sort_by<T>(arr, cmp)` returns a stable insertion sort using a caller-supplied `Ordering`-returning comparator. All seven are iterative WASM with O(1) shadow-stack depth, mirroring the `array_map`/`filter`/`fold` pattern from [#480](https://github.com/aallan/vera/issues/480). Conformance: `ch09_array_utilities.vera`. Example: `examples/array_utilities.vera` exercises every operation in a single coherent gradebook scenario. Phase 1 of [#466](https://github.com/aallan/vera/issues/466); `array_sort<T> where Ord<T>`, `array_contains<T> where Eq<T>`, and `array_index_of<T> where Eq<T>` are tracked separately as a phase 2 follow-up that requires reifying monomorphized `compare$T`/`eq$T` as first-class function handles.

### Compiler
- `vera/wasm/calls_arrays.py` grew from 1,214 → 2,291 lines with the seven new translators, all following the existing `_translate_array_filter` shape (call_indirect callback, GC-shadow-pushed pointers across every `$alloc`, pair-typed element handling for `String` and nested `Array`).
- `vera/wasm/inference.py` extended for the new return types (Array, Option, Bool).
- `vera/wasm/calls.py` dispatch table extended with seven new branches.
- `vera/codegen/modules.py` known-names allowlist extended.

### Bug fix during implementation
- Discovered and fixed an `Ordering`-tag dispatch bug in the initial `array_sort_by` WAT: niladic ADT variants (`Less` / `Equal` / `Greater`) are heap-allocated boxes with the tag at offset 0, not raw i32 tags. The first version of the sort compared the comparator's return value (a heap pointer) directly against the literal `2`, causing every comparison to evaluate as "not Greater" and the sort to no-op. Caught by the `test_array_sort_by_ascending_ints` unit test before the conformance suite could run; fix is a single `i32.load offset=0` to dereference the box before tag comparison.

## [0.0.116] - 2026-04-20

### Added
- **Math built-ins: log, trig, constants, numeric utilities** ([#467](https://github.com/aallan/vera/issues/467)) — fifteen new pure functions across four groups. Logarithmic: `log`, `log2`, `log10`. Trigonometric: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2` (quadrant-correct `(y, x)` argument order matching POSIX / Python / JS). Constants: `pi()` and `e()` (inlined as `f64.const 3.141592653589793` / `f64.const 2.718281828459045`, no host call). Numeric utilities: `sign(@Int)` returns `-1`/`0`/`1`; `clamp(@Int, @Int, @Int)` and `float_clamp(@Float64, @Float64, @Float64)` enforce `min(max(v, lo), hi)`. The 10 log/trig functions use host imports (wrapped Python `math.*` in the Python runtime, `Math.*` in the browser); the 5 constants/utilities are inlined as WAT for zero host-call overhead. Gated emission — modules that don't use a given op don't import it. Unlocks scientific computing, graphics, audio, physics, statistics. Conformance: `ch09_math_builtins.vera`. Closes [#467](https://github.com/aallan/vera/issues/467).

## [0.0.115] - 2026-04-18

### Added
- **`Random` effect for non-deterministic value generation** ([#465](https://github.com/aallan/vera/issues/465)) — new built-in `Random` effect with three operations: `Random.random_int(@Int, @Int) -> @Int` (inclusive range), `Random.random_float(@Unit) -> @Float64` (uniform `[0.0, 1.0)`), `Random.random_bool(@Unit) -> @Bool`. Functions drawing random values must declare `effects(<Random>)`, making non-determinism visible in the type signature. Python runtime backs onto `random.randint` / `random.random()`; browser runtime backs all three onto `Math.random()` (fast, non-cryptographic, adequate for games and simulations). No seeding API yet — `handle[Random]`-based deterministic testing is future work. Unblocks games, simulations, shuffling, Monte Carlo methods, and randomized initial states (Conway's Life soup, etc.). Conformance: `ch07_random_effect.vera`. Closes [#465](https://github.com/aallan/vera/issues/465).

## [0.0.114] - 2026-04-17

### Added
- **`IO.sleep`, `IO.time`, `IO.stderr` operations** ([#463](https://github.com/aallan/vera/issues/463)) — three new operations on the built-in `IO` effect. `IO.sleep(@Nat) -> Unit` pauses execution for N milliseconds (used for animation frame budgets and rate-limiting). `IO.time(@Unit) -> @Nat` returns the current Unix time in milliseconds (used for elapsed-time measurement and timestamps). `IO.stderr(@String) -> Unit` writes to stderr separate from stdout (used for CLI tools that pipe stdout as data). Python runtime delegates to `time.sleep`, `time.time`, and `sys.stderr`; browser runtime busy-waits on `performance.now()` (no `Atomics.wait` on the main thread), uses `Date.now()`, and captures stderr into a separate buffer exposed via `getStderr()`. `execute()` gains a `capture_stderr: bool = False` parameter; `ExecuteResult` gains a `stderr: str` field (empty string by default to preserve the pre-#463 shape). Discovered missing while writing a Conway's Game of Life program. Conformance: `ch07_io_time_stderr.vera`.

### Fixed
- **GC object header size field widened from 16-bit to 31-bit** ([#484](https://github.com/aallan/vera/issues/484)) — `$alloc` has always stored the object size as `(size << 1) | mark` (bit 0 = mark, remaining bits = size), but the GC's sweep and free-list code was masking the size readback with `0xFFFF`, silently truncating any allocation ≥ 65536 bytes. The sweeper would then interpret middle-of-payload bytes as tiny zero-size headers and link each 8-byte chunk into the free list, shredding the live object. Removed the `0xFFFF` mask at all five read sites in `vera/codegen/assembly.py` (two in `$alloc` free-list walks, three in `$gc_collect` sweep/mark). Max single allocation is now ~2 GB (bounded by WASM's 4 GB memory ceiling and the leading mark bit). Stress tests for `array_map` and `array_filter` uncapped from 8K back to 10K elements. Discovered a separate memory-grow bug while testing the fix — filed as [#487](https://github.com/aallan/vera/issues/487).

### Changed
- **Iterative WASM `array_fold`** ([#480](https://github.com/aallan/vera/issues/480) PR 3) — `array_fold<T, U>(arr, init, fn)` is now emitted as a single WAT `loop` that runs a closure `(env, U, T) -> U` over each element, updating a running accumulator in-place. Structurally different from PRs 1 and 2: no output allocation (returns a scalar `U`), closure takes two value parameters, and for pair-typed / ADT accumulators the running pointer is kept rooted via an in-place shadow-stack slot overwrite (`global.get $gc_sp; i32.const 8; i32.sub; i32.store`) — avoiding per-iteration push/pop churn. Closes [#480](https://github.com/aallan/vera/issues/480). Also decouples `_ARRAY_TYPE_ALIASES` injection from the (now empty) `array_fn_names` set in `prelude.py` so `ArrayMapFn` / `ArrayFilterFn` / `ArrayFoldFn` aliases stay available for any user code that references them in type annotations.
- **Iterative WASM `array_filter`** ([#480](https://github.com/aallan/vera/issues/480) PR 2) — `array_filter<T>(arr, pred)` is now emitted as a single WAT `loop` with a separate `write_idx`, replacing the recursive prelude (`array_filter` / `array_filter_go`). Worst-case over-allocates `len * sizeof(T)` bytes and returns `(dst, write_idx)`; the unused tail is unreachable via the returned pair and gets reclaimed by the sweeper. Single-pass by design — the predicate is invoked exactly once per element. Shadow-stack usage is now O(1). Follows the same pattern as PR 1 (`array_map`); `array_fold` to follow in PR 3.
- **Iterative WASM `array_map`** ([#480](https://github.com/aallan/vera/issues/480) PR 1) — `array_map<A, B>(arr, fn)` is now emitted as a single WAT `loop` driven by a `call_indirect` on the closure, replacing the recursive prelude implementation (`array_map_go`). Shadow-stack usage is now O(1) regardless of input length — the old recursive version hit the 16K shadow-stack ceiling (post-[#464](https://github.com/aallan/vera/issues/464)) around 4K elements. The generic higher-order signature (`forall<A, B>`) is preserved via a `FunctionInfo` built-in registration, so source code is unchanged. Also discovered and filed [#484](https://github.com/aallan/vera/issues/484) — a pre-existing 16-bit GC-header size-field limits allocations to 65535 bytes; the stress test caps at 8K Int elements (64,000 bytes) until that's fixed.

### Added
- **CHANGELOG enforcement at pre-push and CI** ([#478](https://github.com/aallan/vera/issues/478)) — new `scripts/check_changelog_updated.py` fails a PR if any substantive file (`vera/`, `spec/`, `SKILL.md`) is changed without a matching new entry in `CHANGELOG.md`. Runs at the `pre-push` hook stage locally (opt in with `pre-commit install --hook-type pre-push`) and in the CI `lint` job. Escape hatches: a `Skip-changelog: <reason>` commit trailer (Git-native) or a `skip-changelog` PR label (CI-only). Prevents the kind of missed release-prep that happened on [#474](https://github.com/aallan/vera/pull/474).

### Documentation
- **Docs consistency sweep** — retroactively tag/release v0.0.113 (the release prep PR #477 merged but the tag+release finishing steps were never run); close [#418](https://github.com/aallan/vera/issues/418) manually (PR description lacked an auto-close keyword); refresh file-size tables in `vera/README.md` after the calls.py decomposition (`calls.py` 8,332 → 572 + 8 mixin rows; `wasm/` 4,273 → 12,998 across 17 modules; `codegen/` size drift); fix stale 3,253 → 3,318 test count in `README.md` and the HISTORY by-the-numbers table; add [#424](https://github.com/aallan/vera/issues/424), [#439](https://github.com/aallan/vera/issues/439), [#480](https://github.com/aallan/vera/issues/480) (iterative WASM higher-order array ops), and [#481](https://github.com/aallan/vera/issues/481) (auto-tag + auto-release on version bump) to ROADMAP; remove closed [#416](https://github.com/aallan/vera/issues/416) and [#417](https://github.com/aallan/vera/issues/417) from SKILL.md limitations.

## [0.0.113] - 2026-04-16

### Changed
- **Decompose `vera/wasm/calls.py` into 8 subsystem mixins** ([#418](https://github.com/aallan/vera/issues/418)) — the 8,390-line `calls.py` monolith is split into a small core dispatcher (572 lines) plus 8 domain-focused mixins: `calls_math.py` (numeric + conversions), `calls_markup.py` (JSON/HTML/Markdown/Regex/async), `calls_arrays.py`, `calls_handlers.py` (Show/Hash/handle), `calls_containers.py` (Map/Set/Decimal), `calls_parsing.py`, `calls_encoding.py` (base64/URL), `calls_strings.py`. Pure code motion — `WasmContext` continues to compose all mixins via Python MRO; no behavioral changes, runtime output identical. Makes `calls.py` 93% smaller and prepares ground for Stage 11 primitives additions. Closes [#418](https://github.com/aallan/vera/issues/418).

### Known issues
- **10 pre-existing bugs in WASM call translators** surfaced during review of [#474](https://github.com/aallan/vera/pull/474) — all predate this release; tracked in [#475](https://github.com/aallan/vera/issues/475). See `KNOWN_ISSUES.md` for the summary; the issue lists each bug with severity, location, and description.

## [0.0.112] - 2026-04-16

### Fixed
- **GC shadow stack overflow causing silent array corruption** ([#464](https://github.com/aallan/vera/issues/464)) — the GC shadow stack was 4K (4096 bytes). Recursive functions with multiple `Array` parameters push ~12 bytes per frame (2 pointer params + 1 `array_append` destination), overflowing at ~341 frames into the adjacent GC worklist region. When GC triggered during deep recursion, the worklist corruption caused intermediate arrays to be incorrectly freed, silently overwriting the first bytes of Bool arrays with free-list pointers. Shadow stack increased from 4K to 16K; worklist increased from 4K to 16K; overflow guard added to `gc_shadow_push` (traps instead of silent corruption). Closes [#464](https://github.com/aallan/vera/issues/464).

## [0.0.111] - 2026-04-10

### Fixed
- **SMT translator: String/Float64 parameters declared with correct Z3 sorts** — `String` and `Float64` function parameters were being declared as Z3 integers (the `else` fallback in `verifier.py`), causing `string_contains`, `string_starts_with`, `string_ends_with`, and `string_length` to receive Int-sorted Z3 variables instead of the expected SeqSort/RealSort. String parameters are now declared via `smt.declare_string()` (Z3 `SeqSort`) and Float64 via `smt.declare_float64()` (Z3 `RealSort`).
- **SMT translator: `StringLit` now translates to `z3.StringVal()`** — string literals in contract expressions (e.g. `requires(string_starts_with(@String.0, "https://"))`) were returning `None` from `translate_expr`, silently demoting the contract to Tier 3. Fixed by adding a `StringLit` branch that emits `z3.StringVal(expr.value)`.
- **SMT translator: `string_length` uses `z3.Length()` for String sorts** — the previous uninterpreted function implementation meant Z3 could not prove `string_length("literal") > 0` at call sites (even though it's obviously true), producing spurious E501 call-site precondition errors. `string_length` on a `SeqSort` argument now uses Z3's native `z3.Length()`, giving full string-theory semantics; the uninterpreted fallback is retained for non-string sorts.
- **SMT translator: `string_contains`, `string_starts_with`, `string_ends_with` now Tier 1** — these pure Boolean predicates on String arguments are now encoded via Z3's native string theory (`z3.Contains`, `z3.PrefixOf`, `z3.SuffixOf`) rather than falling through to Tier 3.
- **`float_is_nan` / `float_is_infinite` explicitly return `None`** — previously fell through to the function-lookup path; now explicitly return `None` with a comment explaining why encoding them as `BoolVal(False)` would be unsound (Float64 is modelled as Z3 reals, which have no NaN/infinity; returning `False` would cause the compiler to skip the runtime guard).

## [0.0.110] - 2026-04-10

### Added
- **Mistral AI provider for the Inference effect** ([#413](https://github.com/aallan/vera/issues/413)) — `Inference.complete` now supports Mistral models. Set `VERA_MISTRAL_API_KEY` to use; default model is `mistral-small-latest`. Closes [#413](https://github.com/aallan/vera/issues/413).

### Changed
- **Provider registry refactor** ([#413](https://github.com/aallan/vera/issues/413)) — `_call_inference_provider()` and the auto-detection logic in `execute()` are now table-driven via a `_ProviderConfig` dataclass and `_PROVIDERS` registry dict, replacing the `elif` chain. Adding further providers (Grok, DeepSeek, Gemini) is now a one-row change. The `_call_inference_provider` signature simplified from six parameters to four (`provider`, `prompt`, `model`, `api_key`).

## [0.0.109] - 2026-04-10

### Fixed
- **Closure `i32_pair` param/return type in WAT** ([#359](https://github.com/aallan/vera/issues/359)) — `String` and `Array` parameters and return types inside closures now emit correct two-slot WAT signatures (`(param i32 i32)` / `(result i32 i32)`). Previously the closure lifting path and the `call_indirect` type descriptor each omitted the second i32 slot, causing WAT compilation failures for closures that accept or return `String`/`Array` values. Three fixes: (1) `codegen/closures.py` lifted function declarations now emit `(param $p{i}_ptr i32) (param $p{i}_len i32)` for `i32_pair` params; (2) `wasm/closures.py` `call_indirect` type descriptor emits two params per `i32_pair` argument; (3) `wasm/inference.py` `_infer_apply_fn_return_type` handles `AnonFn` literals to return `"i32_pair"` for `String`/`Array` return types, preventing `$closure_sig_N` naming collisions.
- **Host imports not registered for closures** ([#359](https://github.com/aallan/vera/issues/359)) — Map/Set/Decimal/Json/Html host-import ops used inside closure bodies were not propagated to the module-level tracker, causing `unknown func` errors at runtime. `codegen/closures.py` `_compile_lifted_closure` now propagates all resource flags from the closure's `WasmContext` back to the module codegen.
- **`_infer_fncall_vera_type` truncating parameterised accumulator types** ([#359](https://github.com/aallan/vera/issues/359)) — when inferring the return type of `apply_fn` calls for `_resolve_generic_call`, parameterised types like `Map<String, Int>` were reduced to the bare name `"Map"`, producing wrong monomorphised call targets (e.g. `array_fold_go$String_Map` instead of `array_fold_go$String_Map_String_Int`). Fixed by calling `_format_named_type` instead of returning `ta.name`.

## [0.0.108] - 2026-04-07

### Added
- **`vera check --explain-slots`** ([#445](https://github.com/aallan/vera/issues/445)) — new flag prints a slot resolution table after a successful type-check, showing which parameter position each `@T.n` index refers to. Example: `fn divide(@Int, @Int -> @Int)` produces `@Int.0 = parameter 2 (last @Int)`, `@Int.1 = parameter 1 (first @Int)`. Also available in JSON mode via `--json` (adds `slot_environments` array to output). Closes [#445](https://github.com/aallan/vera/issues/445), closes [#183](https://github.com/aallan/vera/issues/183) (won't fix — see issue for design rationale).
- **SKILL.md prescriptive improvements** — five sections reworked from descriptive to action-oriented: De Bruijn intro now leads with a bold warning; new `--explain-slots` workflow subsection; new "Workflow: writing contracts incrementally" subsection in the Contracts section; Typed Holes intro reframed imperatively; Built-in function naming section adds a directive for handling unresolved names.
- **DE_BRUIJN.md** — new §9 "Debugging with `--explain-slots`" with worked examples.
- **`uv lock --check` in CI** ([#390](https://github.com/aallan/vera/issues/390)) — lint job now verifies `uv.lock` is consistent with `pyproject.toml` on every PR. `CONTRIBUTING.md` updated to document `uv sync` as the recommended install method. Closes [#390](https://github.com/aallan/vera/issues/390).

### Fixed
- **Z3 solver timeout documented** ([#391](https://github.com/aallan/vera/issues/391)) — the 10-second per-contract timeout was already implemented in `vera/smt.py` but undocumented. `spec/06-contracts.md` §6.4.3 now explicitly documents the default. Closes [#391](https://github.com/aallan/vera/issues/391).

## [0.0.107] - 2026-04-07

### Added
- **Validate `vera run` commands in `examples/README.md`** ([#361](https://github.com/aallan/vera/issues/361)) — new `scripts/check_examples_readme.py` parses every `vera run` command in the example index tables, verifies the referenced `.vera` file exists, and verifies any `--fn <name>` target is a public function in that file. Wired into pre-commit (triggers on `examples/README.md` or `.vera` changes) and CI. Closes [#361](https://github.com/aallan/vera/issues/361).

## [0.0.106] - 2026-03-31

### Added
- **`vera test` String and Float64 input generation** ([#169](https://github.com/aallan/vera/issues/169)) — `vera test` now generates Z3 inputs for `String` and `Float64` parameters, removing the `SKIPPED (cannot generate String inputs (see #169))` limitation for those types. String uses Z3's sequence sort with a 50-character length cap; Float64 uses Z3's mathematical real sort with boundary seeding (0.0, ±1.0, ±0.5, ±10.0, ±1e10, ±1e-10). Both types route through the `raw_args` calling convention so that String's two-i32 WASM ABI is handled correctly. IEEE 754 special values (NaN, ±∞, subnormals) are not generated — use explicit test inputs for those cases. ADT input generation remains unsupported ([#440](https://github.com/aallan/vera/issues/440)). Closes [#169](https://github.com/aallan/vera/issues/169).

## [0.0.105] - 2026-03-30

### Added
- **Typed holes** ([#226](https://github.com/aallan/vera/issues/226)) — `?` is now a valid expression placeholder for partial programs. `vera check` reports each hole as a `W001` warning (not an error) with the expected type and all available De Bruijn slot bindings. Programs with holes type-check successfully (`ok: true`) but cannot compile (`E614`). The `--json` output includes hole warnings in the `warnings` array with machine-readable expected type and fix hint. New error code `W001` (typed hole warning) and `E614` (holes block compilation). Conformance test `ch03_typed_holes.vera`. Closes [#226](https://github.com/aallan/vera/issues/226).
- SKILL.md: new [Typed Holes](SKILL.md#typed-holes) section with workflow examples; best-practices tip on incremental development with holes; error code table updated with `W001` and `E614`.
- AGENTS.md: workflow section updated with typed-holes iterative pattern; error code table updated.

## [0.0.104] - 2026-03-29

### Fixed
- **Type inference for bare `None`/`Err` constructors in generic combinator calls** ([#293](https://github.com/aallan/vera/issues/293)) — `option_unwrap_or(None, 99)`, `result_unwrap_or(Err("oops"), 0)`, and `option_map(None, fn(...) {...})` now type-check and compile correctly without requiring a typed `let` binding workaround. Three-layer fix: (1) the checker's fresh-TypeVar overwrite rule in `resolution.py` — later concrete resolutions now overwrite tentative fresh-TypeVar placeholders; (2) the monomorphizer's `_get_arg_type_info` now uses `_ctor_adt_tp_indices` to correctly map sparse constructor fields to their ADT type-param positions (e.g. `Err`'s single field maps to Result's `E`, not `T`); (3) added missing `StringLit` / `InterpolatedString` / `ArrayLit` cases to the monomorphizer's `_infer_vera_type_name`. Closes [#293](https://github.com/aallan/vera/issues/293).

### Added
- Conformance test `ch09_none_err_inference.vera` (level: run) covering all four bare-constructor inference cases.

## [0.0.103] - 2026-03-29

### Added
- **`--quiet` flag for `vera check` / `vera verify`** ([#382](https://github.com/aallan/vera/issues/382)) — suppresses the `OK: ...` and `Verification: ...` success output; errors still print normally. Useful for CI scripts that only need the exit code. Closes [#382](https://github.com/aallan/vera/issues/382).
- **De Bruijn conformance tests: deep let-chains** ([#393](https://github.com/aallan/vera/issues/393)) — `ch03_slot_let_chains.vera` at `verify` level: five sequential `let @Int =` bindings where each doubles the previous; `ensures(@Int.result == 32)` is proved by Z3. Confirms slot indices resolve correctly through deep same-typed `let` chains. Closes [#393](https://github.com/aallan/vera/issues/393).
- **De Bruijn conformance tests: non-commutative operations** ([#394](https://github.com/aallan/vera/issues/394)) — `ch03_slot_noncommutative.vera` at `verify` level: subtraction, division, and comparison with `ensures` contracts that encode the exact De Bruijn ordering. Swapping `@Int.0` / `@Int.1` produces a Z3 counterexample. Closes [#394](https://github.com/aallan/vera/issues/394).
- **Known Limitations section in SKILL.md** ([#404](https://github.com/aallan/vera/issues/404)) — table listing six current implementation limits with issue links: `vera test` String/Float64 skip (#169), bare `None`/`Err` type inference (#293), effect row variable unification (#294), `map_new()`/`set_new()` type context, Inference `max_tokens`/temperature (#370), Inference user-defined handlers (#372). Closes [#404](https://github.com/aallan/vera/issues/404).
- **Conformance test: nested effect handlers** ([#395](https://github.com/aallan/vera/issues/395)) — `ch07_nested_handlers.vera` at `check` level validates correct effect rows on functions that compose multiple algebraic effects. Closes [#395](https://github.com/aallan/vera/issues/395).
- **Conformance test: cross-module contracts** ([#396](https://github.com/aallan/vera/issues/396)) — `ch07_cross_module_contracts.vera` + `ch07_cross_module_contracts_lib.vera` at `check` level validates that contract declarations are visible and enforced across module boundaries. Closes [#396](https://github.com/aallan/vera/issues/396).
- **SKILL.md served from veralang.dev** ([#398](https://github.com/aallan/vera/issues/398)) — `docs/SKILL.md` is now a generated artefact (from root `SKILL.md`, via `build_site.py`), making the language reference available at `veralang.dev/SKILL.md` — on-domain, cacheable, stable. All `href` references in `docs/index.html` updated from raw GitHub URLs to `/SKILL.md`. JSON-LD TechArticle URL updated to `https://veralang.dev/SKILL.md`. Closes [#398](https://github.com/aallan/vera/issues/398).
- **`vera version` / `--version` / `-V` CLI command** ([#381](https://github.com/aallan/vera/issues/381)) — prints `vera X.Y.Z` on stdout. Closes [#381](https://github.com/aallan/vera/issues/381).
- **CycloneDX SBOM generation in CI** ([#389](https://github.com/aallan/vera/issues/389)) — new `sbom` job runs `cyclonedx-py environment --of JSON` on every push, uploading a CycloneDX JSON SBOM as a 90-day artifact. Closes [#389](https://github.com/aallan/vera/issues/389).

### Fixed
- **`Http.post` missing `Content-Type: application/json`** ([#354](https://github.com/aallan/vera/issues/354)) — the Python host function now sets the header on the `urllib.request.Request`; the browser runtime sets `xhr.setRequestHeader('Content-Type', 'application/json')`. `Http.post` is intentionally JSON-only at the Vera level; custom `Content-Type` headers tracked in [#351](https://github.com/aallan/vera/issues/351). Closes [#354](https://github.com/aallan/vera/issues/354).
- **`vera test` skip messages now name the unsupported type** ([#383](https://github.com/aallan/vera/issues/383)) — was `SKIPPED (unsupported parameter types)`; now `SKIPPED (cannot generate String inputs (see #169))`. The specific type name is extracted via the new `_unsupported_type_names` helper. Closes [#383](https://github.com/aallan/vera/issues/383).
- **HTTP host functions missing `timeout`** — `host_http_get` and `host_http_post` now pass `timeout=_INFERENCE_TIMEOUT` to `urllib.request.urlopen`, consistent with the Inference effect.

### Security
- **`ruff check --select S` added to lint job** ([#388](https://github.com/aallan/vera/issues/388)) — 32 Bandit-equivalent S-rule findings audited and suppressed with `# noqa` comments where appropriate (S310 intentional HTTPS calls, S101 compiler invariant asserts, S105 parse-token false positives). `ruff>=0.9` added to `[dev]` dependencies. Closes [#388](https://github.com/aallan/vera/issues/388).
- **`dependency-audit` CI job** ([#384](https://github.com/aallan/vera/issues/384)) — `pip-audit --skip-editable` checks all installed packages against the OSV database on every push. `pygments 2.19.2` has `CVE-2026-4539` (transitive via pytest/rich, no fix release yet) — suppressed with `--ignore-vuln`. Closes [#384](https://github.com/aallan/vera/issues/384).
- **CI workflow hardening** ([#385](https://github.com/aallan/vera/issues/385)) — `persist-credentials: false` on all checkout steps (`artipacked`); per-job `permissions: contents: read` with `security-events: write` on the security job (`excessive-permissions`). SHA pinning of action refs deferred to [#390](https://github.com/aallan/vera/issues/390). Closes [#385](https://github.com/aallan/vera/issues/385).

## [0.0.102] - 2026-03-28

### Added
- **Type-aware CLI argument passing** ([#263](https://github.com/aallan/vera/issues/263)) — `vera run file.vera --fn f -- arg` now accepts all Vera scalar and string types, not just integers. Arguments are parsed using the function's WASM signature: `Int`/`Nat` → integer literal, `Float64` → decimal literal, `Bool` → `true`/`false` (case-insensitive), `Byte` → integer 0–255, `String` → text (allocated into WASM linear memory). Type-mismatch errors identify the bad argument and expected type. `CompileResult.fn_param_types` (new field) exposes per-function WASM-level type tags (`i64`, `f64`, `i32`, `i32_pair`) so the host runtime can handle memory allocation and invocation correctly for each parameter kind. Closes [#263](https://github.com/aallan/vera/issues/263). Closes [#403](https://github.com/aallan/vera/issues/403).
- **Multi-layer agent discovery metadata** ([#400](https://github.com/aallan/vera/issues/400)) — veralang.dev now exposes `SKILL.md` through four machine-readable layers: `<head>` link elements (`rel="alternate" type="text/markdown"`, `rel="llms-txt"`, `rel="llms-full-txt"`), an inline `<script type="text/llms.txt">` block (Vercel convention), JSON-LD `TechArticle` entries for `SKILL.md` and `AGENTS.md`, and semantic attributes on the existing CTA button. HTTP headers skipped — GitHub Pages does not support custom response headers. Closes [#400](https://github.com/aallan/vera/issues/400).

### Fixed
- **E609 false positive on `Option<T>` return types across modules** ([#360](https://github.com/aallan/vera/issues/360)) — the name-collision checker incorrectly flagged `Option`, `Result`, and other built-in ADTs as user-defined names when they appeared as return types in imported modules. Fixed by pre-seeding `builtin_adt_names` before scanning module declarations. Closes [#360](https://github.com/aallan/vera/issues/360).
- **Pipe operator into module-qualified calls** ([#326](https://github.com/aallan/vera/issues/326)) — `value |> mod::fn(args)` previously type-checked the right-hand side in isolation, ignoring the piped argument. The checker now desugars to a `ModuleCall` with the LHS prepended to the argument list. Closes [#326](https://github.com/aallan/vera/issues/326).
- **`/dev/stdin` double-read** ([#335](https://github.com/aallan/vera/issues/335)) — all CLI commands (`check`, `verify`, `compile`, `run`, `ast`, `test`) previously called `p.read_text()` for the source string and then `parse_file(path)` which re-opened the same path. For `/dev/stdin` the second open returns empty content. Fixed by extracting `_load_and_parse(path)` which reads the source once and passes it directly to `parse()`. For stdin paths, returns `Path.cwd()/"stdin.vera"` so module resolution and compile output naming use CWD. Closes [#335](https://github.com/aallan/vera/issues/335).

## [0.0.101] - 2026-03-27

### Added
- **Inference effect** ([#61](https://github.com/aallan/vera/issues/61)) — built-in `<Inference>` algebraic effect with one operation: `Inference.complete(String → Result<String, String>)`. Sends a prompt to the configured LLM provider and returns `Ok(completion)` or `Err(message)`. Provider auto-detected from whichever API key is set; override with `VERA_INFERENCE_PROVIDER`. Model configurable via `VERA_INFERENCE_MODEL`. Default models: Anthropic → `claude-haiku-4-5-20251001`, OpenAI → `gpt-4o-mini`, Moonshot → `moonshot-v1-8k`. Implemented via host imports (Python `urllib.request`). Built-in effect — no `effect Inference { ... }` declaration needed. Browser runtime returns a detailed `Err` explaining why API keys cannot be embedded in client-side JavaScript. New conformance test `ch09_inference` (64 programs, was 63). New example `inference.vera`. Closes [#61](https://github.com/aallan/vera/issues/61).

### Known limitations
- `complete` only — `embed` (returning `Array<Float64>`) deferred to a follow-up
- No streaming responses — full completion only
- No system prompt — single `complete(user_prompt)` call; structured prompting via `string_concat`
- No token limits or temperature controls — Anthropic: `max_tokens` hardcoded to 1024; OpenAI and Moonshot use provider defaults ([#370](https://github.com/aallan/vera/issues/370))
- User-defined `handle[Inference]` handlers (for mocking, local models, replay) are planned for a future release

## [0.0.100] - 2026-03-26

### Added
- **Html standard library type** ([#311](https://github.com/aallan/vera/issues/311)) — built-in `HtmlNode` ADT (`HtmlElement`, `HtmlText`, `HtmlComment`) with 5 built-in functions: `html_parse`, `html_to_string`, `html_query`, `html_text`, `html_attr`. Lenient HTML parsing (like browsers). Simple CSS selector query support (tag, class, ID, attribute, descendant combinator). Implemented via host imports (Python `html.parser`). `html_attr` is a pure Vera prelude function using `map_get`. New conformance test `ch09_html` (63 programs, was 62). New example `html.vera`.

## [0.0.99] - 2026-03-25

### Added
- **Http effect** ([#57](https://github.com/aallan/vera/issues/57)) — built-in `<Http>` algebraic effect with two operations: `Http.get(url)` and `Http.post(url, body)`, both returning `Result<String, String>`. Implemented via host imports (Python `urllib.request` / JavaScript `fetch`). Composes with `json_parse` for typed API responses. Built-in effect — no `effect Http { ... }` declaration needed. New conformance test `ch09_http` (62 programs, was 61). New example `http.vera`. Browser runtime support. Closes #57.

### Known limitations
- No custom headers ([#351](https://github.com/aallan/vera/issues/351))
- No HTTP methods beyond GET/POST ([#352](https://github.com/aallan/vera/issues/352))
- No response status code access ([#353](https://github.com/aallan/vera/issues/353))
- No request timeout configuration ([#354](https://github.com/aallan/vera/issues/354))
- No streaming responses ([#355](https://github.com/aallan/vera/issues/355))
- No cookie/session management ([#356](https://github.com/aallan/vera/issues/356))

## [0.0.98] - 2026-03-25

### Added
- **Json standard library type** ([#58](https://github.com/aallan/vera/issues/58)) — built-in `Json` ADT (`JNull`, `JBool`, `JNumber`, `JString`, `JArray`, `JObject`) with 8 built-in functions: `json_parse`, `json_stringify`, `json_get`, `json_array_get`, `json_array_length`, `json_keys`, `json_has_field`, `json_type`. Implemented via host imports (Python `json` / JavaScript `JSON`). Opaque i32 handles following the Map/Set pattern. New conformance test `ch09_json` (61 programs, was 60). New example `json.vera`. Browser runtime support.
- New conformance test `ch09_decimal_generics` (60 programs, was 59) — exercises `option_unwrap_or`, `match`, and `decimal_compare` with Decimal.
- 12 new unit tests for opaque handle monomorphization paths (Decimal, Map, Set).

### Fixed
- **Monomorphization of opaque handle types** ([#341](https://github.com/aallan/vera/issues/341)) — generic prelude functions (`option_unwrap_or`, `match`) now work with all opaque handle types: `Option<Decimal>`, `Option<Map<K,V>>`, `Option<Set<T>>`, `Ordering` from `decimal_compare`. The monomorphizer now preserves full parameterized type names (e.g. `Map<String, Int>`) through type inference, substitution, and name mangling. Previously, type arguments were dropped during inference, causing `option_unwrap_or$Map` to fail codegen. Closes #341.

## [0.0.97] - 2026-03-24

### Added
- **Decimal type for exact arithmetic** ([#333](https://github.com/aallan/vera/issues/333)) — 14 built-in operations (`decimal_from_int`, `decimal_from_float`, `decimal_from_string`, `decimal_to_string`, `decimal_to_float`, `decimal_add`, `decimal_sub`, `decimal_mul`, `decimal_div`, `decimal_neg`, `decimal_compare`, `decimal_eq`, `decimal_round`, `decimal_abs`) via host imports (Python `decimal.Decimal` / JS string-based decimal). New conformance test `ch09_decimal` (59 programs, was 58). Browser runtime support. Closes #333.

## [0.0.96] - 2026-03-24

### Changed
- **Documentation sweep for collections** ([#62](https://github.com/aallan/vera/issues/62), PR 3/3) — fix stale "future collections" language in spec, update README version and Ch 9 description, add Map/Set to SKILL.md composite types and common mistakes, update example counts across all docs (25→28). New `examples/collections.vera` showcasing word-frequency analysis with Map and Set operations. Closes #62.

## [0.0.95] - 2026-03-24

### Added
- **Set\<T\> collection type** ([#62](https://github.com/aallan/vera/issues/62), PR 2/3) — six built-in operations (`set_new`, `set_add`, `set_contains`, `set_remove`, `set_size`, `set_to_array`) with `Eq<T> + Hash<T>` ability constraints. Host-import pattern (Python sets / JS Sets behind opaque i32 handles). Pure functional semantics. New conformance test `ch09_set` (58 programs, was 57). 25 unit tests.
- **Native JS coverage issue** ([#337](https://github.com/aallan/vera/issues/337)) — tracked for future c8 integration.
- **Stdin compilation bug** ([#335](https://github.com/aallan/vera/issues/335)) — filed and added to Known Bugs.

### Changed
- **README test counts** — removed hardcoded counts, TESTING.md is now the single source of truth.

## [0.0.94] - 2026-03-23

### Added
- **Map\<K, V\> collection type** ([#62](https://github.com/aallan/vera/issues/62), PR 1/3) — eight built-in operations (`map_new`, `map_insert`, `map_get`, `map_contains`, `map_remove`, `map_size`, `map_keys`, `map_values`) with `Eq<K> + Hash<K>` ability constraints. Implemented via host imports (Python dicts / JS Maps behind opaque i32 handles). Pure functional semantics — `map_insert` and `map_remove` return new maps. New conformance test `ch09_map` (57 programs, was 56). 40 new unit tests. Browser runtime support.
- **Decimal type issue** ([#333](https://github.com/aallan/vera/issues/333)) — split from #62 as independent future work.

### Fixed
- **Type inference for zero-argument generic functions** — `map_new()` and similar zero-arg generic calls now resolve type variables from expected type context (bidirectional coercion). Fixed unification to skip ADT args whose TypeVars match the callee's own forall vars.

## [0.0.93] - 2026-03-20

### Changed
- **Standard prelude — eliminate boilerplate** ([#289](https://github.com/aallan/vera/issues/289)) — `Option<T>`, `Result<T, E>`, `Ordering`, and `UrlParts` are now unconditionally injected by the prelude, eliminating 2–6 lines of identical `data` declarations from every program that uses these types. Option/Result combinators and array operations are also always available. User-defined `data` declarations with the same name shadow the prelude versions. Non-standard variants (e.g. `data Option<T> { None, Just(T) }`) correctly suppress combinator injection. New conformance test `ch09_prelude` (conformance suite: 55→56 programs). Boilerplate removed from 8 conformance programs and 8 examples. Closes #289.

## [0.0.92] - 2026-03-19

### Changed
- **BREAKING: Built-in function naming audit** ([#288](https://github.com/aallan/vera/issues/288)) — renamed 14 inconsistently-named built-in functions to follow the dominant `domain_verb` convention. String operations gain `string_` prefix (`strip` → `string_strip`, `upper` → `string_upper`, etc.), float predicates gain `float_` prefix (`is_nan` → `float_is_nan`, `is_infinite` → `float_is_infinite`), and `to_float` becomes `int_to_float`. Closes #288.

### Added
- **Naming convention documentation** — formal naming rules added to spec §9.1.1, SKILL.md, vera/README.md, and CONTRIBUTING.md. Four patterns: `domain_verb` (dominant), `source_to_target` (conversions), `domain_is_predicate` (boolean tests), prefix-less (math universals only).

## [0.0.91] - 2026-03-19

### Added
- **Array operations** ([#133](https://github.com/aallan/vera/issues/133)) — four new built-in array functions: `array_slice` (WASM intrinsic with index clamping), `array_map` (generic, element type can change), `array_filter` (predicate-based selection), and `array_fold` (left fold with arbitrary accumulator type). Higher-order operations implemented via prelude source injection with recursive helpers. 15 unit tests. Closes #133.

### Fixed
- **De Bruijn reindexing during monomorphization** ([#316](https://github.com/aallan/vera/issues/316)) — when distinct type variables collapse to the same concrete type, slot reference indices are now correctly adjusted
- **Transitive monomorphization** ([#317](https://github.com/aallan/vera/issues/317)) — generic functions calling other generic functions now correctly generate all required specializations via worklist-based closure
- **WASM type inference gaps** ([#313](https://github.com/aallan/vera/issues/313), [#314](https://github.com/aallan/vera/issues/314), [#315](https://github.com/aallan/vera/issues/315), [#318](https://github.com/aallan/vera/issues/318)) — `_is_pair_type_name` now matches bare `Array`, `_infer_vera_type` handles `IndexExpr` and `IfExpr`, `_infer_fncall_vera_type` handles `apply_fn`, `_get_arg_type_info` handles `ArrayLit` and `FnCall`

## [0.0.90] - 2026-03-13

### Added
- **Abilities release** ([#60](https://github.com/aallan/vera/issues/60)) — four built-in abilities (`Eq`, `Ord`, `Hash`, `Show`) with full codegen support. Includes the built-in `Ordering` ADT (`Less`, `Equal`, `Greater`), ADT auto-derivation for `Eq` (structural equality for simple enums and ADTs with primitive fields), `compare` rewriting via Pass 1.6 AST transformation, `show`/`hash` dispatch at WASM level (FNV-1a for String hashing), and string equality via byte-by-byte comparison. Constraint satisfaction checked for all four abilities (E613). 20 unit tests, extended conformance test with 20 test functions. Closes #60.

## [0.0.89] - 2026-03-12

### Added
- **Option/Result combinators** ([#211](https://github.com/aallan/vera/issues/211)) — five prelude functions that eliminate common match boilerplate: `option_unwrap_or`, `option_map`, `option_and_then`, `result_unwrap_or`, `result_map`. Implemented via source injection (parsed Vera AST, injected before codegen). Includes compiler fixes for generic type alias return type inference in closures, closure signature deduplication across functions, and AnonFn type variable inference in the monomorphizer. New conformance test, 12 unit tests, spec section 9.3.7.

### Changed
- **`map_option` renamed to `option_map`** — all references across spec, examples, and tests updated to follow the `domain_verb` naming convention ([#288](https://github.com/aallan/vera/issues/288)).

## [0.0.88] - 2026-03-12

### Fixed
- **Formatter comment repositioning** ([#274](https://github.com/aallan/vera/issues/274)) — comments inside function bodies were moved to the file footer during formatting. Extended the anchor system to collect interior AST spans (statements, result expressions) and emit comments at the correct positions within blocks, if/else branches, match arms, and handler bodies. Added `_collect_interior_anchors` recursive walker. 7 new tests, `examples/string_ops.vera` reformatted to canonical form. Closes #274.

## [0.0.87] - 2026-03-11

### Added
- **FizzBuzz example** — complete runnable program demonstrating recursion as iteration with IO effects (`examples/fizzbuzz.vera`)
- **"Recursion as iteration" section** in README "What Vera Looks Like" — explains the standard Vera pattern for counted iteration
- **"Iteration" section** in SKILL.md — documents the tail-recursive loop pattern for agents
- **De Bruijn slot reference reminder** in CLAUDE.md — documents that `@T.0` = most recent (last) parameter

### Fixed
- **Verifier branch-condition bug** documented ([#283](https://github.com/aallan/vera/issues/283)) — call-site precondition checking doesn't use if-guard path conditions; added to Known Bugs with workaround

## [0.0.86] - 2026-03-11

### Added
- **Regex support** ([#231](https://github.com/aallan/vera/issues/231)):
  Four new pure functions for regular expression matching on strings:
  `regex_match`, `regex_find`, `regex_find_all`, `regex_replace`.
  All return `Result` types for safe handling of invalid patterns.
  Implemented as WASM host imports — Python's `re` module for wasmtime,
  JavaScript's `RegExp` for the browser runtime. Browser parity verified.
- New conformance test `ch09_regex` (conformance suite: 52→53 programs)
- New example `examples/regex.vera` (example suite: 23→24 programs)
- Spec §9.6.15 "Regular Expressions"
- 16 new tests (10 codegen + 6 type checker) plus browser parity coverage

## [0.0.85] - 2026-03-11

### Added
- **Browser runtime for compiled WASM** ([#273](https://github.com/aallan/vera/issues/273)):
  A self-contained JavaScript runtime (`vera/browser/runtime.mjs`, ~730 lines) that
  runs any compiled Vera `.wasm` module in the browser or Node.js. Uses
  `WebAssembly.Module.imports()` to introspect the module's imports and dynamically
  builds the host binding object — no code generation needed. The same runtime file
  works with every compiled Vera program, from hello-world (1 import) to markdown-heavy
  programs (15+ imports).
  Includes JavaScript implementations of all host bindings: IO operations (print,
  read_line, args, exit, get_env, read_file/write_file with browser-appropriate
  adaptations), State\<T\> (dynamically pattern-matched from import names),
  contract_fail, and all 5 Markdown operations (with a bundled JS Markdown parser).
- New CLI flag: `vera compile --target browser` produces a ready-to-serve browser
  bundle (module.wasm + vera-runtime.mjs + index.html) in a single command
- New `vera/browser/` package: `runtime.mjs` (JS runtime), `harness.mjs` (Node.js
  test harness), `emit.py` (browser bundle emission)
- 56 new browser parity tests ensuring identical output between Python/wasmtime and
  Node.js/JS-runtime across IO, State, contracts, Markdown, and all compilable examples
- Browser parity CI job (Node.js 22) and pre-commit hook triggered by changes to
  host binding surface (`vera/browser/`, `vera/codegen/api.py`, `vera/wasm/markdown.py`,
  `vera/markdown.py`)
- Spec §12.4.3 (Contract Violations) and §12.4.4 (Markdown Operations) — host binding
  documentation missing from v0.0.84
- Spec §12.9 (Browser Runtime) — JavaScript runtime architecture and usage

## [0.0.84] - 2026-03-11

### Added
- **Markdown standard library** ([#147](https://github.com/aallan/vera/issues/147)):
  Two new built-in ADTs for typed Markdown document representation:
  `MdInline` (6 constructors: MdText, MdCode, MdEmph, MdStrong, MdLink, MdImage)
  and `MdBlock` (8 constructors: MdParagraph, MdHeading, MdCodeBlock, MdBlockQuote,
  MdList, MdThematicBreak, MdTable, MdDocument).
  Five new pure functions: `md_parse(String → Result<MdBlock, String>)`,
  `md_render(MdBlock → String)`, `md_has_heading(MdBlock, Nat → Bool)`,
  `md_has_code_block(MdBlock, String → Bool)`,
  `md_extract_code_blocks(MdBlock, String → Array<String>)`.
  All implemented as WASM host imports with a hand-written Python parser —
  the first pure functions using the host-binding pattern.
  The WASM import interface is the portability contract: the same `.wasm`
  binary works with any host runtime (Python, JavaScript, Rust) that provides
  matching implementations.
- New conformance test `ch09_markdown` (conformance suite: 51→52 programs)
- New example `examples/markdown.vera` — parse, query, extract, and render Markdown
- 78 new tests (59 parser/renderer + 6 type checker + 8 codegen + 5 conformance)

### Changed
- **Spec §9.3 reorganization:** UrlParts (from §9.6.13), Future\<T\> (from §9.5.4),
  MdInline, and MdBlock are now documented in §9.3 (Built-in ADTs) as §9.3.3–§9.3.6.
  Function specs remain in their original sections with cross-references.

## [0.0.83] - 2026-03-11

### Added
- **Tuple type WASM codegen** ([#267](https://github.com/aallan/vera/issues/267)):
  Tuple construction (`Tuple(1, "hello", true)`) and destructuring now compile to WASM.
  Match destructuring (`Tuple(@Int, @String) -> ...`) extracts fields from heap-allocated Tuples.
  `LetDestruct` (`let Tuple<@Int, @String> = expr;`) works for Tuples and all single-constructor
  ADTs (including UrlParts, Future, and user-defined types).
  Tuples are variadic — any arity from 1 field upward is supported.
- New conformance test `ch02_tuple_basic` (conformance suite: 50→51 programs)
- Updated `examples/url_parsing.vera` with LetDestruct demonstration
- 15 new tests (5 type checker + 10 codegen)

## [0.0.82] - 2026-03-11

### Added
- **`<Async>` effect with `Future<T>` type** ([#59](https://github.com/aallan/vera/issues/59)):
  New `Async` marker effect and `Future<T>` ADT for asynchronous computation.
  New `async(@T -> @Future<T>)` — wraps a value in a future (eager evaluation).
  New `await(@Future<T> -> @T)` — unwraps a future to its result.
  `Future<T>` is WASM-transparent — same runtime representation as `T`, zero overhead.
  The reference implementation uses sequential evaluation; true concurrency will be
  available via WASI 0.3 native `future<T>` support ([#237](https://github.com/aallan/vera/issues/237)).
- New conformance test `ch09_async` (conformance suite: 49→50 programs)
- New example `examples/async_futures.vera` — async/await roundtrip and composition demo
- 15 new tests (7 type checker + 8 codegen)

## [0.0.81] - 2026-03-10

### Added
- **URL parsing and joining** ([#232](https://github.com/aallan/vera/issues/232), Phase 2):
  New `url_parse(@String -> @Result<UrlParts, String>)` — RFC 3986 URL decomposition
  into scheme, authority, path, query, and fragment components.
  New `url_join(@UrlParts -> @String)` — reassembles a `UrlParts` value into a URL string.
  New built-in `UrlParts` ADT type with five String fields.
- New conformance test `ch09_url_parsing` (conformance suite: 48→49 programs)
- New example `examples/url_parsing.vera` — parse, extract components, join, and error demo
- 29 new tests (4 type checker + 25 codegen)

### Fixed
- **ADT constructors with String/Array fields** ([#266](https://github.com/aallan/vera/issues/266)):
  Layout computation (`_wasm_type_size`, `_wasm_type_align`) now handles `i32_pair`
  (String/Array representation). User-defined ADTs with String fields compile correctly.

## [0.0.80] - 2026-03-10

### Added
- **URL percent-encoding and decoding** ([#232](https://github.com/aallan/vera/issues/232)):
  New `url_encode(@String -> @String)` — RFC 3986 percent-encoding, leaving
  unreserved characters unchanged.
  New `url_decode(@String -> @Result<String, String>)` — percent-decoding with
  error handling for invalid `%XX` sequences.
- New conformance test `ch09_url_encoding` (conformance suite: 47→48 programs)
- New example `examples/url_encoding.vera` — encode, decode, and round-trip demo
- 21 new tests (4 type checker + 17 codegen)

## [0.0.79] - 2026-03-10

### Added
- **Close #234: Base64 encoding and decoding** ([#234](https://github.com/aallan/vera/issues/234)):
  New `base64_encode(@String -> @String)` — standard Base64 (RFC 4648) encoding.
  New `base64_decode(@String -> @Result<String, String>)` — Base64 decoding with
  error handling for invalid length or characters.
- New conformance test `ch09_base64` (conformance suite: 46→47 programs)
- New example `examples/base64.vera` — encode, decode, and round-trip demo
- 20 new tests (4 type checker + 16 codegen)

## [0.0.78] - 2026-03-10

### Added
- **Close #209: Array construction builtins** ([#209](https://github.com/aallan/vera/issues/209)):
  New `array_range(@Int, @Int -> @Array<Int>)` — integer range [start, end), empty
  if start >= end. New `array_concat(forall<T> @Array<T>, @Array<T> -> @Array<T>)` —
  merge two arrays into a new array.
- New conformance test `ch04_array_construction` (conformance suite: 45→46 programs)
- 17 new tests (4 type checker + 7 codegen array_range + 6 codegen array_concat)

### Changed
- **Breaking:** `length` renamed to `array_length`. The array length builtin now
  follows the same `array_` prefix convention used by string builtins (`string_length`,
  `string_concat`, etc.). All code using `length()` on arrays must be updated.
- **Breaking:** `array_push` renamed to `array_append`. Clearer intent and consistent
  naming with the new array builtins.

## [0.0.77] - 2026-03-10

### Added
- **Close #200: Parsing completeness** ([#200](https://github.com/aallan/vera/issues/200)):
  New `parse_int` (`String → Result<Int, String>`) and `parse_bool`
  (`String → Result<Bool, String>`) built-in functions. `parse_int` handles
  optional `+`/`-` sign. `parse_bool` is strict lowercase only — only `"true"`
  and `"false"` are valid.
- New conformance test `ch04_parse_completeness` (conformance suite: 44→45 programs)
- 22 new tests (6 type checker + 8 codegen parse_int + 6 codegen parse_bool + 2 codegen parse_float64 error cases)

### Changed
- **Breaking:** `parse_float64` return type changed from `Float64` to
  `Result<Float64, String>`. Previously returned `0.0` silently on invalid input;
  now returns `Err(msg)` with a descriptive error message. All four parse functions
  (`parse_nat`, `parse_int`, `parse_float64`, `parse_bool`) now consistently
  return `Result<T, String>`.

## [0.0.76] - 2026-03-10

### Added
- **Close #230: String interpolation** ([#230](https://github.com/aallan/vera/issues/230)):
  New `\(expr)` syntax inside double-quoted strings for ergonomic string building.
  Non-String expressions of type Int, Nat, Bool, Byte, and Float64 are automatically
  converted using the appropriate `*_to_string` built-in. `InterpolatedString` is a
  first-class AST node (canonical form, survives formatting). Touches every compiler
  stage: grammar, transformer, type checker, formatter, and WASM codegen.
- New conformance test `ch04_string_interpolation` (conformance suite: 43→44 programs)
- 18 new tests (9 type checker + 9 codegen end-to-end)
- Spec Sections 1.4 "String Interpolation" and 4.13.1 "String Interpolation"

## [0.0.75] - 2026-03-10

### Added
- **Close #213: string_repeat builtin** ([#213](https://github.com/aallan/vera/issues/213)):
  New `string_repeat` function (`String, Nat → String`) that repeats a string N
  times. Uses a single-allocation loop with modulo indexing. Pure, Tier 3
  (runtime-tested).
- 7 new tests (2 type checker + 5 codegen end-to-end)

## [0.0.74] - 2026-03-09

### Added
- **Close #210: from_char_code builtin** ([#210](https://github.com/aallan/vera/issues/210)):
  New `from_char_code` function (`Nat → String`) that creates a single-character
  string from an ASCII code point. Inverse of the existing `char_code`. Pure,
  Tier 3 (runtime-tested).
- 7 new tests (2 type checker + 5 codegen end-to-end)

## [0.0.73] - 2026-03-09

### Added
- **Close #198: String search and transformation builtins** ([#198](https://github.com/aallan/vera/issues/198)):
  Nine new built-in functions for string search and transformation:
  `string_contains` (String,String→Bool), `starts_with` (String,String→Bool),
  `ends_with` (String,String→Bool), `index_of` (String,String→Option\<Nat\>),
  `to_upper` (String→String), `to_lower` (String→String),
  `replace` (String,String,String→String), `split` (String,String→Array\<String\>),
  `join` (Array\<String\>,String→String). All are pure and Tier 3 (runtime-tested).
- New conformance test `ch09_string_search` (conformance suite: 42→43 programs)
- 55 new tests (18 type checker + 37 codegen end-to-end)
- Spec Sections 9.6.6 "String Search" and 9.6.7 "String Transformation"

## [0.0.72] - 2026-03-09

### Added
- **Close #212: Float64 special value operations** ([#212](https://github.com/aallan/vera/issues/212)):
  Four new built-in functions for detecting and constructing IEEE 754 special
  Float64 values: `is_nan` (Float64→Bool), `is_infinite` (Float64→Bool),
  `nan` (→Float64), and `infinity` (→Float64). All are Tier 3 (runtime-tested).
- New conformance test `ch10_float_predicates` (conformance suite: 41→42 programs)
- 25 new tests (8 type checker + 17 codegen end-to-end)
- Spec Section 9.6.5 "Float64 Predicates" with full signatures and contracts

## [0.0.71] - 2026-03-09

### Added
- **Close #208: Numeric type conversions** ([#208](https://github.com/aallan/vera/issues/208)):
  Six new built-in functions for explicit conversion between numeric types:
  `to_float` (Int→Float64), `float_to_int` (Float64→Int, truncation toward zero),
  `nat_to_int` (Nat→Int, identity), `int_to_nat` (Int→Option\<Nat\>, checked),
  `byte_to_int` (Byte→Int, zero-extension), and `int_to_byte` (Int→Option\<Byte\>,
  checked). Widening conversions always succeed; narrowing conversions return
  `Option` for safety. `nat_to_int` and `byte_to_int` are SMT-verifiable (Tier 1).
- New conformance test `ch09_type_conversions` (conformance suite: 40→41 programs)
- 38 new tests (12 type checker + 26 codegen end-to-end)
- Spec Section 9.6.4 "Type Conversions" with full signatures and contracts

## [0.0.70] - 2026-03-09

### Added
- **Close #199: Numeric math builtins** ([#199](https://github.com/aallan/vera/issues/199)):
  Eight new built-in functions for common mathematical operations: `abs` (Int→Nat),
  `min`/`max` (Int,Int→Int), `floor`/`ceil`/`round` (Float64→Int), `sqrt`
  (Float64→Float64), and `pow` (Float64,Int→Float64). All are pure and available
  without imports. Integer builtins (`abs`, `min`, `max`) are fully verifiable by
  Z3 (Tier 1). Float builtins use native WASM instructions. `round` uses IEEE 754
  banker's rounding. `pow` takes an integer exponent with negative exponent support.
- New conformance test `ch09_numeric_builtins` (conformance suite: 39→40 programs)
- 41 new tests (13 type checker + 28 codegen end-to-end)
- Spec Section 9.6.3 "Numeric Operations" with full signatures and contracts

## [0.0.69] - 2026-03-06

### Fixed
- **Close #241: Byte literal coercion** ([#241](https://github.com/aallan/vera/issues/241)):
  Integer literals 0–255 are now accepted as `Byte` when the expected type is
  `Byte` (bidirectional type checking). The `ch01_byte_literals` conformance test
  is promoted from `check` to `run` level.
- **Close #242: `array_push` codegen** ([#242](https://github.com/aallan/vera/issues/242)):
  `array_push(array, elem)` is now a fully implemented builtin — registered in the
  type environment with a generic `(Array<T>, T) -> Array<T>` signature and compiled
  to WASM with proper allocation, element copying, and GC shadow stack integration.
  The `ch04_array_ops` conformance test now exercises `array_push`.

### Changed
- Conformance suite: `ch01_byte_literals` promoted from `check` to `run` level;
  `ch04_array_ops` now includes `array_push` in its feature coverage
- Documentation updated across TESTING.md, SKILL.md, and README.md to reflect
  resolved bugs and new builtin

## [0.0.68] - 2026-03-05

### Added
- **Close #223: Conformance test suite** ([#223](https://github.com/aallan/vera/issues/223)):
  39 small, self-contained programs in `tests/conformance/` that systematically
  validate every language feature against the spec (Chapters 1-7). Each program
  tests one feature or a small group of related features.
- `tests/conformance/manifest.json` — machine-readable metadata mapping each
  program to its spec chapter, test level (parse/check/verify/run), and feature tags
- `tests/test_conformance.py` — parametrized pytest runner generating ~195 test
  cases across all 39 conformance programs
- `scripts/check_conformance.py` — standalone validation script for CI and
  pre-commit (same pattern as `check_examples.py`)
- Pre-commit hook `conformance-suite` and CI step in lint job
- Conformance suite documentation across TESTING.md, README.md, CLAUDE.md,
  AGENTS.md, SKILL.md, and vera/README.md
- Known Bugs section in README.md listing compiler issues discovered during
  conformance suite development

### Fixed
- Opened [#241](https://github.com/aallan/vera/issues/241) (Byte literal coercion),
  [#242](https://github.com/aallan/vera/issues/242) (array_push codegen), and
  [#243](https://github.com/aallan/vera/issues/243) (nested generic constructor
  type inference) — bugs discovered while building the conformance suite

## [0.0.67] - 2026-03-05

### Added
- **Close #216: String escape sequences** ([#216](https://github.com/aallan/vera/issues/216)):
  Fix grammar regex and add escape sequence decoder in `transform.py`.
  All 7 escape sequences from spec section 1 are now supported: `\\`, `\"`, `\n`, `\t`,
  `\r`, `\0`, `\u{XXXX}`. Invalid escapes produce error code E009.
- 19 new tests (14 AST + 5 codegen end-to-end) for escape sequence handling

### Fixed
- `STRING_LIT` grammar regex had too many backslashes, causing the lexer to
  reject single-backslash escape sequences like `\n` and `\t`

## [0.0.66] - 2026-03-05

### Added
- **Close #135: IO operations** (C8.5, [#135](https://github.com/aallan/vera/issues/135)):
  Six new IO operations: `read_line`, `read_file`, `write_file`, `args`,
  `exit`, and `get_env`. All seven IO operations (including `print`) are
  now registered as built-in effect operations with full type checking.
  Programs no longer need `effect IO { op print(String -> Unit); }` preambles.
- Host function implementations for all IO operations via wasmtime, with
  WASM-to-host string passing through exported `$alloc`
- `ExecuteResult.exit_code` field for `IO.exit` support
- `execute()` accepts `stdin`, `cli_args`, and `env_vars` parameters
- Two new example programs: `io_operations.vera` and `file_io.vera`
- 26 new tests across type checker, codegen, and CLI

### Changed
- IO effect is now a built-in with all 7 operations registered in the
  type environment (no user `effect IO` declaration required)
- Replaced `_needs_io_print: bool` with `_io_ops_used: set[str]` for
  per-operation WASM import tracking
- WASM modules export `$alloc` when IO operations need host-to-WASM
  string allocation
- `_is_void_expr` now recursively handles compound expressions
  (MatchExpr, IfExpr, Block) to prevent invalid `drop` instructions
- Fixed match dispatch for ADT constructors with String fields
  (`i32_pair` type in `_sub_pattern_wasm_type` and size/align dicts)

## [0.0.65] - 2026-03-04

### Added
- **Close #51: garbage collection for WASM linear memory** (C8e, [#51](https://github.com/aallan/vera/issues/51)):
  Conservative mark-sweep garbage collector implemented entirely in WASM.
  Shadow stack root tracking, free-list reuse, and automatic `memory.grow`
  ensure allocation-heavy programs survive beyond the initial 64 KiB page.
  New `gc_pressure.vera` example demonstrates GC under repeated allocation.

### Changed
- Memory layout now reserves 8192 bytes after string constants for GC
  shadow stack (4096 bytes) and mark worklist (4096 bytes)
- `$alloc` prepends a 4-byte header to every allocation (mark bit + size)
  and checks the free list before bump-allocating
- Functions that allocate heap data receive GC prologue/epilogue
  (save/restore `$gc_sp`, push pointer parameters and return values)
- `to_string` and `strip` now allocate exact-size result buffers instead
  of returning interior pointers into temporary buffers
- Removed "No garbage collection" from limitation tables in spec, README,
  and compiler architecture docs

## [0.0.64] - 2026-03-04

### Added
- **Close #106: universal to-string conversion** (C8e, [#106](https://github.com/aallan/vera/issues/106)):
  Added 5 new string conversion builtins for all primitive types:
  `bool_to_string(Bool -> String)`, `nat_to_string(Nat -> String)`,
  `byte_to_string(Byte -> String)`, `float_to_string(Float64 -> String)`,
  and `int_to_string(Int -> String)` (alias for existing `to_string`).
  ADT/compound type Show deferred to abilities (#60).

### Changed
- Moved #56 (incremental compilation) from C8e to C8.5 in the roadmap

## [0.0.63] - 2026-03-04

### Changed
- **Close #52: dynamic string construction** (C8e, [#52](https://github.com/aallan/vera/issues/52)):
  All 8 string built-in operations (`string_concat`, `to_string`, `string_slice`,
  `strip`, `parse_nat`, `parse_float64`, `char_code`, `string_length`) were
  implemented in WASM codegen across v0.0.50 (PR #173) and v0.0.60 (#174),
  providing full dynamic string construction via the bump allocator. This
  documentation-only release updates limitation tables across the spec, README,
  and compiler README to reflect the resolved status.
  - `spec/11-compilation.md` limitation table: struck through #52 row
  - `spec/12-runtime.md` limitation table: struck through #52 row; also fixed
    stale #53 and #110 rows to match `spec/11-compilation.md`
  - `README.md` roadmap: struck through #52 with version tag
  - `vera/README.md` limitations table: marked #52 as done
  - `spec/09-standard-library.md`: updated Markdown section dependency note
  - GC for string memory remains tracked separately in [#51](https://github.com/aallan/vera/issues/51)
  - Unblocks [#106](https://github.com/aallan/vera/issues/106) (Show/Display)

## [0.0.62] - 2026-03-04

### Added
- **Exn\<E\> exception handler compilation** (C8e, [#53](https://github.com/aallan/vera/issues/53)):
  `handle[Exn<E>]` expressions now compile to WASM using the exception handling
  proposal (`try_table`/`catch`/`throw`). Enables compile-and-run for programs
  using exception-style error handling.
  - Exception tags declared per `Exn<E>` type parameter
  - `throw(value)` compiles to WASM `throw` instruction
  - Cross-function throws work via WASM stack unwinding
  - Nested `handle[Exn<E>]` expressions with unique labels
  - `examples/effect_handler.vera` extended with safe division via `Exn<Int>`
  - 10 new tests, 1,380 tests total
- **Renamed `SKILLS.md` to `SKILL.md`**: matches the Claude Code skill file naming
  convention. All references updated across README, AGENTS.md, CLAUDE.md,
  docs site, and vera/README.md. CHANGELOG historic references preserved.
- **Added installation instructions to `SKILL.md`**: agents consuming the skill
  file now get setup instructions (clone, venv, pip install, verify)

## [0.0.61] - 2026-03-04

### Added
- **Arrays of compound types** (C8e, [#132](https://github.com/aallan/vera/issues/132)):
  Arrays now support all element types, not just the five primitives (`Int`,
  `Nat`, `Float64`, `Bool`, `Byte`).  This includes ADT types
  (`Array<Option<Int>>`, `Array<Result<Int, String>>`), `String`
  (`Array<String>`), and nested arrays (`Array<Array<Int>>`).
  - ADT elements are stored as i32 heap pointers (4 bytes each)
  - String and nested-array elements are stored as (ptr, len) pairs (8 bytes each)
  - Chained indexing works for nested arrays (`@Array<Array<Int>>.0[0][1]`)
  - Constructor pair-type field storage fixed (e.g. `Err("msg")` with String field)
  - 17 new tests, 1,370 tests total

## [0.0.60] - 2026-03-03

### Changed
- **`parse_nat` returns `Result<Nat, String>`** (C8e, [#174](https://github.com/aallan/vera/issues/174)):
  `parse_nat` now returns `Ok(n)` on valid decimal input and `Err(msg)` on
  empty or invalid input, matching the spec (Section 9).  Previously it
  returned bare `Nat` and silently produced garbage for non-numeric strings.
  - Digit validation: bytes must be in ASCII 48–57; leading/trailing spaces
    are tolerated; empty/whitespace-only strings return `Err("empty string")`
  - Built-in Result and Option ADT layouts are now registered in codegen so
    match on `Ok`/`Err` works without a user `data Result` declaration
  - Match extraction supports String pair bindings inside ADT constructors
    (e.g. `Err(@String)`)
  - 6 new tests, 1,353 tests total

## [0.0.59] - 2026-03-03

### Fixed
- **Stale spec cross-reference** ([#141](https://github.com/aallan/vera/issues/141)):
  Updated `spec/09-standard-library.md` reference from "Section 11.16" to
  "Section 11.17" (section numbering shifted after chapter edits). Added the
  compound-type array limitation ([#132](https://github.com/aallan/vera/issues/132))
  to the limitations table in `spec/11-compilation.md`.

### Removed
- **Unused `hypothesis` dependency** ([#138](https://github.com/aallan/vera/issues/138)):
  Removed `hypothesis>=6.0` from `pyproject.toml` dev dependencies — it was
  declared but never imported in the test suite.

## [0.0.58] - 2026-03-03

### Fixed
- **`list_ops.vera` runtime failure — built-in name shadowing** (C8e, [#154](https://github.com/aallan/vera/issues/154)):
  User-defined functions now take priority over built-in intrinsics in WASM
  codegen. Previously, a user-defined `length(@List<Int> -> @Nat)` was
  incorrectly intercepted by the array-length built-in handler, producing
  invalid WASM. The fix guards all 9 built-in handlers (`length`,
  `string_length`, `string_concat`, `string_slice`, `char_code`, `parse_nat`,
  `parse_float64`, `to_string`, `strip`) so they only activate when no
  user-defined function with the same name exists.
  - `examples/list_ops.vera` now compiles and runs correctly
  - 3 new tests, 1,347 tests total

## [0.0.57] - 2026-03-03

### Added
- **Name collision detection for flat module compilation** (C8e, [#110](https://github.com/aallan/vera/issues/110)):
  When two imported modules define a function, data type, or constructor with
  the same name, the compiler now reports a clear error (E608/E609/E610) listing
  both conflicting modules. Previously, the first module registered silently won
  and the second was ignored, producing silently wrong code.
  - Provenance tracking in `_register_modules` detects collisions across functions,
    ADT types, and constructors
  - New error codes: E608 (function collision), E609 (ADT type collision),
    E610 (constructor collision)
  - Local definitions continue to shadow imported names without error
  - Same-module duplicates (module imported twice) are not treated as collisions
  - 7 new tests, 1,344 tests total

## [0.0.56] - 2026-03-03

### Added
- **Nested constructor pattern codegen** (C8e, [#131](https://github.com/aallan/vera/issues/131)):
  Match expressions with nested constructor patterns (e.g. `Cons(Some(@Int), _)`)
  now compile to WASM correctly. Previously, any sub-pattern that was a
  `ConstructorPattern` or `NullaryPattern` caused the entire function to be
  silently skipped.
  - `_collect_nested_tag_checks` recursively emits tag comparisons for nested
    constructors, AND-chained into the arm condition
  - `_extract_constructor_fields` recursively loads nested field pointers and
    binds their sub-patterns
  - `_sub_pattern_wasm_type` helper resolves WASM types for offset computation
  - `examples/pattern_matching.vera` `first_some` now compiles and runs
  - 5 new tests (nested Some, nested None, multi-field, different arms, fallthrough)
  - 1,337 tests total

## [0.0.55] - 2026-03-03

### Added
- **Bidirectional type checking** (C8d, [#55](https://github.com/aallan/vera/issues/55)):
  Adds local type inference via an `expected` parameter threaded through
  expression synthesis. Nullary constructors of parameterised ADTs (`None`
  for `Option<T>`, `Nil` for `List<T>`) now resolve their TypeVars from
  context — return types, let bindings, if/match branches, and function
  arguments.
  - `_synth_expr` gains optional `expected: Type | None` parameter
  - Constructors resolve unresolved TypeVars from expected type in
    `_ctor_result_type` and `_check_constructor_call`
  - Nested constructors (`Some(None)` for `Option<Option<Int>>`) resolve
    via field-type propagation
  - Function call arguments with TypeVars are re-synthesised with the
    substituted parameter type as expected
  - If/match branches with TypeVars are re-synthesised using the concrete
    branch as expected, replacing the previous `contains_typevar` guards
  - `_check_fn` passes `expected=return_type` to body synthesis, removing
    the `contains_typevar` workaround guard from v0.0.53
  - No changes to function signatures, parser, AST, or spec
  - 12 new tests (TestBidirectionalInference)
  - 1,332 tests total

## [0.0.54] - 2026-03-02

### Added
- **Effect row subtyping and call-site effect checking** (C8d, [#21](https://github.com/aallan/vera/issues/21)):
  Implements the subeffecting rules from Spec Section 7.8: a function with fewer
  effects can be used where more effects are expected.
  - `is_effect_subtype()` function in `types.py` encodes subset semantics
    for effect rows (`effects(pure) <: effects(<IO>) <: effects(<IO, State<Int>>)`)
  - `FunctionType` subtyping now includes effect covariance: a pure function
    can be passed where `fn(A -> B) effects(<IO>)` is expected
  - Call-site check in `checker/calls.py`: calling a function whose effects
    exceed the caller's context is now an error (E125)
  - Handler bodies unaffected: handlers temporarily add their effect to the
    context before checking the body, then discharge it
  - Effect row variables (`forall<E>`) are permissive pending #55
  - `effects_equal()` function added for structural effect row comparison
  - `types_equal()` updated to compare effects in `FunctionType`
  - 20 new tests (13 unit tests, 7 integration tests)
  - 1,320 tests total

## [0.0.53] - 2026-03-02

### Changed
- **TypeVar subtyping is no longer permissive** (C8d, [#20](https://github.com/aallan/vera/issues/20)):
  Removes the blanket `TypeVar <: anything` rule from `is_subtype`.
  TypeVars now only match themselves via reflexive equality.
  - Generic function bodies are properly checked: returning a `T` where a
    concrete `Int` is expected is now an error (E121)
  - Call sites unaffected: existing inference + substitution resolves TypeVars
    before subtype checks; unresolved TypeVars are skipped
  - Nullary constructors of parameterised ADTs (e.g. `None` for `Option<T>`)
    produce types with unresolved TypeVars — these are tolerated at call sites
    and in non-generic function bodies
  - `contains_typevar` utility added to `types.py`
  - 13 new tests (5 subtyping unit tests, 3 rejection tests, 5 regression tests)
  - 1,300 tests total

## [0.0.52] - 2026-03-02

### Added
- **Mutual recursion termination verification** (C8c, [#45](https://github.com/aallan/vera/issues/45)):
  Verifies `decreases` clauses across mutually recursive `where`-block function groups,
  promoting the last E525 contract in `mutual_recursion.vera` from Tier 3 to Tier 1.
  - Where-block functions now have their contracts verified (requires, ensures, decreases)
  - Cross-function measure checking: callee measure evaluated in callee parameter env
  - Verification rate: 96 T1, 3 T3, 99 total (97.0% static)
  - 4 new verifier tests, 1,287 tests total

## [0.0.51] - 2026-03-02

### Added
- **Expand SMT decidable fragment** (C4/C6, [#13](https://github.com/aallan/vera/issues/13)):
  Promotes 4 Tier 3 (runtime) contracts to Tier 1 (statically verified), bringing
  the verification rate from 91.7% to 95.8% across all 15 examples (92 T1, 4 T3, 96 total).
  - **Match expression + ADT constructor Z3 translation**: `MatchExpr`, `NullaryConstructor`,
    and `ConstructorCall` are now translated to Z3 using `z3.Datatype` sorts, with pattern
    conditions mapped to recognizers and field bindings mapped to accessors.
  - **Termination verification for `decreases` clauses**: Simple `Nat` decreases (factorial)
    and structural ADT decreases (`List<T>` length/sum) are now verified via Z3. Uses a
    rank function with universal axioms for structural subterm ordering.
  - ADT sort creation with type parameter substitution and self-reference handling
  - Per-sort `length()` uninterpreted functions (supports both Int and ADT domains)
  - Recursive call collector with Z3 path conditions and slot env tracking
  - Verifier now populates constructor info for SMT translation
  - 16 new verifier tests across 3 test classes
  - 1,283 tests total

### Remaining Tier 3 contracts (4)
- 2 x E520: Generic type parameters in `generics.vera` (unfixable — fundamental SMT limitation)
- 1 x E522: `old`/`new` state modeling in `increment.vera` (deferred — requires symbolic effect execution)
- 1 x E525: Mutual recursion in `mutual_recursion.vera` (deferred — requires cross-function measure reasoning)

## [0.0.50] - 2026-03-02

### Added
- **Eight string/conversion built-in operations** (C8e, [#134](https://github.com/aallan/vera/issues/134), [#52](https://github.com/aallan/vera/issues/52)):
  First external contribution by [@rlseaman](https://github.com/rlseaman) in PR [#173](https://github.com/aallan/vera/pull/173).
  - `string_length(@String -> @Nat)` — byte length of a string
  - `string_concat(@String, @String -> @String)` — concatenation
  - `string_slice(@String, @Nat, @Nat -> @String)` — substring extraction
  - `char_code(@String, @Int -> @Nat)` — ASCII code at index
  - `parse_nat(@String -> @Nat)` — decimal string to natural number
  - `parse_float64(@String -> @Float64)` — decimal string to float
  - `to_string(@Int -> @String)` — integer to decimal string
  - `strip(@String -> @String)` — trim leading/trailing whitespace (zero-copy)
  - New example: `examples/string_ops.vera` (15 examples total)
  - 52 new tests: 10 type checker + 42 codegen/runtime
  - Uses bump allocator (`$alloc`); GC deferred to [#51](https://github.com/aallan/vera/issues/51)
  - `parse_nat` returns bare `Nat` pending [#174](https://github.com/aallan/vera/issues/174) (spec requires `Result<Nat, String>`)
  - 1,267 tests total

## [0.0.49] - 2026-03-01

### Added
- **Register `Diverge` as built-in effect** (C8c, [#136](https://github.com/aallan/vera/issues/136)):
  `effects(<Diverge>)` now resolves to the spec-defined marker effect
  (Chapter 7, Section 7.7.3). Diverge has no operations — it signals that a
  function may not terminate. Precursor to #45 (termination verification).

## [0.0.48] - 2026-03-01

### Added
- **Improve WASM translation test coverage** (C8.5, [#156](https://github.com/aallan/vera/issues/156)):
  - New `tests/test_wasm_coverage.py` with 109 tests targeting coverage gaps in `vera/wasm/`
  - Direct unit tests for `helpers.py` pure functions (`wasm_type`, `wasm_type_or_none`, `is_compilable_type`, element helpers)
  - Full pipeline tests for `inference.py` deep branches (block result type inference, Vera type inference, expression type propagation)
  - Closure free-variable walking coverage (`closures.py`): capture in binary, if, call, let, match contexts
  - Operator edge cases: Float64 comparisons, Byte unsigned comparisons, Boolean implies, AND/OR
  - Data/match coverage: nullary constructors, wildcard patterns, Bool/Int patterns, Option<T>
  - Effect handler coverage: State<Int> init/get/put/increment patterns
  - `wasm/` coverage improved from 79% to 86% (helpers.py 62%→96%, closures.py 72%→92%, inference.py 71%→74%)
  - Overall compiler coverage improved from 87% to 88% (6,861 stmts, 819 missed)
  - 1,209 tests (up from 1,100)

### Fixed
- Fixed approximate (`~`) coverage values for `tester.py` in TESTING.md (335/51/85%, not ~350/~60/~83%)

## [0.0.47] - 2026-03-01

### Added
- **`vera test` contract-driven testing** (C8b, [#79](https://github.com/aallan/vera/issues/79)):
  - New `vera test` command generates inputs from `requires()` clauses via Z3 and executes compiled WASM to validate `ensures()` at runtime
  - Z3-based input generation for Int, Nat, Bool parameters with boundary value seeding
  - Tier classification: Tier 1 functions reported as "verified", Tier 3 functions exercised with generated inputs, unsupported/generic functions skipped
  - `--json` flag for machine-readable test results
  - `--trials N` flag to configure trial count per function (default 100)
  - `--fn name` flag to test a single function
  - New `vera/tester.py` module (~530 lines) with public `test()` API
  - E7xx error code range for testing diagnostics (E700-E702)
  - 24 new tests: 13 unit tests in `test_tester.py`, 11 CLI tests in `test_cli.py`
  - 1,100 tests (up from 1,076)

## [0.0.46] - 2026-03-01

### Changed
- **Decompose `codegen.py` into `codegen/` mixin package** (C8a, [#155](https://github.com/aallan/vera/issues/155)):
  - Split the 2,140-line `codegen.py` monolith into 11 focused modules following the mixin pattern from `checker/` and `wasm/`
  - New modules: `api.py`, `core.py`, `modules.py`, `registration.py`, `monomorphize.py`, `functions.py`, `closures.py`, `contracts.py`, `assembly.py`, `compilability.py`
  - Added 5 coverage gap tests reaching defensive error paths (E600, E601, E605, E606, unknown module calls)
  - Split the 4,834-line `test_codegen.py` into 6 focused test files mirroring the module structure
  - 1,076 tests (up from 1,071)
  - Completes C8a refactoring: all three compiler monoliths (`checker.py`, `wasm.py`, `codegen.py`) are now mixin packages

## [0.0.45] - 2026-02-28

### Added
- **`vera fmt` canonical code formatter** ([#75](https://github.com/aallan/vera/issues/75)):
  - New `vera fmt` command formats Vera source to the canonical form defined in Spec §1.8
  - `--write` flag for in-place formatting, `--check` flag for CI (exit 1 if non-canonical)
  - AST-based formatter with pre-pass comment extraction and reattachment
  - Precedence-aware parenthesization for binary expressions
  - All 10 formatting rules enforced: indentation, braces, commas, operators, semicolons, parentheses, contracts, one-per-line, no trailing whitespace, final newline
  - New `vera/formatter.py` module (1,018 lines)
  - 75 new tests (1,071 total, up from 996)
  - All 14 examples reformatted to canonical form
  - SKILLS.md and spec examples updated to canonical form ([#150](https://github.com/aallan/vera/issues/150))

## [0.0.44] - 2026-02-28

### Changed
- **Module-qualified call syntax uses `::` delimiter** (C8b, [#95](https://github.com/aallan/vera/issues/95)):
  - Module-qualified calls now use `::` between module path and function name: `vera.math::abs(42)`
  - Resolves LALR(1) grammar ambiguity where `module_path` greedily consumed the function name
  - Old dot syntax (`vera.math.abs(42)`) is rejected with a targeted "did you mean `::` ?" error (E008)
  - Added `format_expr` support for `ModuleCall` AST nodes
  - Updated spec chapters 8 and 10, examples, and all documentation
  - 12 new tests (996 total, up from 984)

## [0.0.43] - 2026-02-27

### Added
- **Stable error code taxonomy for diagnostics** (C8b, [#80](https://github.com/aallan/vera/issues/80)):
  - Every diagnostic now carries a stable error code (`E001`–`E607`)
  - Codes are grouped by compiler phase: parse (E0xx), type check (E1xx–E3xx), verification (E5xx), codegen (E6xx)
  - 80 error codes across 8 files, covering all 77+ diagnostic emission sites
  - `Diagnostic` dataclass gains `error_code: str` field
  - `format()` output shows `[Exxx]` prefix when code is present
  - `to_dict()` includes `error_code` in JSON output
  - Central `ERROR_CODES` registry in `vera/errors.py` maps codes to short descriptions
  - 13 new tests (984 total, up from 971)

## [0.0.42] - 2026-02-27

### Changed
- **Informative runtime contract violation messages** (C8b, [#112](https://github.com/aallan/vera/issues/112)):
  - Contract violations now report which function, contract kind, and expression failed
  - Before: `Runtime contract violation: error while executing at wasm backtrace: ... wasm trap: wasm unreachable instruction executed`
  - After: `Precondition violation in clamp(@Int, @Int, @Int -> @Int)\n  requires(@Int.1 <= @Int.2) failed`
  - New `vera.contract_fail` host import passes pre-interned message strings from WASM to the runtime before trapping
  - Added `format_expr()`, `format_type_expr()`, `format_fn_signature()` to `vera/ast.py` for AST-to-source reconstruction
  - 20 new tests (971 total, up from 951)

## [0.0.41] - 2026-02-27

### Changed
- **Decompose wasm.py into wasm/ package** (C8a, [#100](https://github.com/aallan/vera/issues/100)):
  - Split 2,344-line monolith into 8 focused modules using mixin-based composition
  - `context.py` (369) — composed WasmContext class, expression dispatcher, block translation
  - `helpers.py` (211) — WasmSlotEnv, StringPool, type mapping, array element helpers
  - `inference.py` (527) — type inference, slot/type utilities, operator lookup tables
  - `operators.py` (430) — binary/unary operators, if, quantifiers, assert/assume, old/new
  - `calls.py` (223) — function calls, generic resolution, effect handlers
  - `closures.py` (248) — closures, anonymous functions, free variable analysis
  - `data.py` (460) — constructors, match expressions, arrays, indexing
  - Zero test changes — public API (`WasmContext`, `WasmSlotEnv`, `StringPool`, `wasm_type`) unchanged

## [0.0.40] - 2026-02-27

### Changed
- **Decompose checker.py into checker/ package** (C8a, [#99](https://github.com/aallan/vera/issues/99)):
  - Split 2,043-line monolith into 8 focused modules using mixin-based composition
  - `core.py` (349) — TypeChecker class, orchestration, diagnostics, contracts
  - `resolution.py` (190) — AST TypeExpr → semantic Type, type inference
  - `modules.py` (153) — cross-module registration (C7b/C7c)
  - `registration.py` (138) — Pass 1 forward declarations
  - `expressions.py` (530) — expression synthesis, operators, statements
  - `calls.py` (390) — function, constructor, and module-qualified calls
  - `control.py` (439) — if/match, patterns, exhaustiveness, effect handlers
  - Zero test changes — public API (`typecheck()`) unchanged

## [0.0.39] - 2026-02-27

### Added
- **Spec Chapter 8: Modules** (C7f):
  - New specification chapter covering module declarations, imports, visibility, name resolution, module resolution algorithm, cross-module type checking, verification, and compilation
  - Formal semantics for the flattening compilation strategy, transitive resolution, circular import detection, and shadowing rules
  - Clarification that type aliases and effect declarations are module-local (not importable)
  - Complete worked example with `vera/math.vera`, `vera/collections.vera`, and `modules.vera`
  - Limitations section tracking #95 (LALR grammar), #110 (name collisions), and future extensions

### Changed
- **Roadmap restructured**: C7 collapsed as complete (v0.0.31-v0.0.39), C8 defined as the polish phase with sub-phases C8a-C8e grouping all open issues by area
- Cross-references added from spec Chapters 5, 10, 11, and 12 pointing to Chapter 8
- `SKILLS.md` module section updated with type-alias/effect locality note and spec reference
- `vera/README.md` limitations table updated: module system marked complete
- `docs/index.html` feature grid updated with "Module system" entry
- README project status: Chapter 8 status changed from "Not started" to "Draft"

## [0.0.38] - 2026-02-27

### Added
- **Multi-module codegen** (C7e — [#50](https://github.com/aallan/vera/issues/50)):
  - Imported function bodies are now compiled into the WASM module via flattening
  - `vera compile` and `vera run` work with multi-module programs (previously blocked by C7e guard rail)
  - Private helper functions called by imported public functions are compiled automatically
  - `ModuleCall` nodes are desugared to flat `FnCall` in WASM translation (including pipe operator)
  - Guard rail updated: only truly undefined functions produce errors; imported functions pass through
  - `modules.vera` example now compiles and runs end-to-end
  - Spec Chapter 11 updated with cross-module compilation section (11.16)
- 8 new cross-module codegen tests (951 total, up from 943)

### Changed
- Error messages for undefined functions no longer reference C7e; instead they report "not found in any imported module"

## [0.0.37] - 2026-02-27

### Added
- **Cross-module contract verification** (C7d — [#14](https://github.com/aallan/vera/issues/14)):
  - Imported function preconditions are now checked at call sites by the SMT solver
  - Imported function postconditions are assumed, allowing callers to rely on them
  - Chained imported calls compose correctly (e.g. `abs(max(x, y)) >= 0`)
  - Only `public` functions from imported modules are available for verification
  - Selective imports are respected — only named imports are registered
  - Refactored SMT call translation into shared `_translate_call_with_info` for both local and cross-module calls
  - Added `ModuleCall` handling in SMT translator (including pipe operator desugaring)
  - `modules.vera` example now verifies `abs_max` postcondition (`ensures(@Int.result >= 0)`) at Tier 1
- 8 new cross-module verification tests (943 total, up from 935)

## [0.0.36] - 2026-02-27

### Fixed
- **WASM export visibility gate** (C7c — [#107](https://github.com/aallan/vera/pull/107)):
  - Only `public` functions are now exported as WASM entry points; `private` functions compile but are not accessible via `vera run`
  - Both the `exports` list and the WAT-level `(export ...)` annotation are gated on visibility
  - Monomorphized generic functions inherit visibility from the original generic declaration
- **Improved "no exports" error** ([#107](https://github.com/aallan/vera/pull/107)):
  - `vera run` on a file with no public functions now lists all declared functions with their visibility, any compilation warnings, and suggests making a function public
  - `vera run --fn <name>` targeting a private function gives a specific "declared private" error with fix suggestion
  - Both plain-text and `--json` output modes supported
- **Runnable examples** ([#107](https://github.com/aallan/vera/pull/107)):
  - All 13 non-module examples now have `public` test entry points (e.g. `vera run examples/factorial.vera --fn test_factorial`)
  - Entry-point functions in examples, README, SKILLS.md, spec chapters 5-7, and docs site updated from `private` to `public`
- 3 new tests (935 total, up from 932)

### Added
- **Roadmap**: [#106](https://github.com/aallan/vera/issues/106) (universal to-string conversion / Show for all types) added to codegen gaps

## [0.0.35] - 2026-02-27

### Fixed
- **Cross-module codegen guard rail** (C7c — partial [#14](https://github.com/aallan/vera/issues/14)):
  - `vera compile` and `vera run` on programs with imported functions now produce a proper Vera diagnostic instead of a raw wasmtime error (`unknown func: failed to find name $max`)
  - Pre-compilation AST scan detects `FnCall` to undefined names and `ModuleCall` nodes before WAT generation, emitting LLM-oriented diagnostics with rationale, fix suggestion, and spec reference
  - Belt-and-braces guard in `wasm.py` `_translate_call()` and explicit `ModuleCall` handler prevent any undefined call from reaching wasmtime
  - Diagnostic directs users to `vera check` / `vera verify` for multi-module programs until C7e (multi-module codegen) is implemented
- **Bare `fn`/`data` in error messages and docs** (merged in [#103](https://github.com/aallan/vera/pull/103)):
  - Fixed remaining bare `fn` declarations in compiler error message fix suggestions (`vera/errors.py`), spec chapters, README, AGENTS.md, `vera/README.md`, and `tests/test_resolver.py`
- **`vera run` parameter mismatch diagnostic**: when a function expects arguments but none are provided, the error now names the function, lists available exports, and shows the correct `--fn ... -- <args>` syntax (previously showed a raw "Runtime contract violation: too few parameters" wasmtime error)
- 5 new tests (932 total, up from 927)

## [0.0.34] - 2026-02-27

### Added
- **Visibility enforcement** (C7c — partial [#14](https://github.com/aallan/vera/issues/14)):
  - Every top-level `fn` and `data` declaration now requires an explicit `public` or `private` annotation — no implicit default, enforcing design principle 3 ("one canonical form")
  - Cross-module access control: only `public` declarations are importable; private names produce targeted "is private" errors with fix suggestions
  - Selective imports of private names caught at import site with clear diagnostics
  - Wildcard imports (`import m;`) automatically filter to public declarations only
  - Constructor visibility derived from parent ADT — private ADT means private constructors
  - `FunctionInfo` and `AdtInfo` now carry a `visibility` field threaded through the registration pipeline
  - Updated all 14 examples, all test inline sources, spec chapters, README, SKILLS.md, and docs site — no bare `fn`/`data` declarations remain anywhere in the repo
- 13 new tests (927 total, up from 914)

## [0.0.33] - 2026-02-27

### Removed
- **`Float` type alias** (closes [#76](https://github.com/aallan/vera/issues/76)): `Float` is no longer accepted as a type name — use `Float64` exclusively
  - Enforces design principle 3 ("one canonical form") from spec §1.8
  - Removed `"Float": FLOAT64` alias from `vera/types.py` PRIMITIVES dict
  - Simplified ~12 dual-name checks in `wasm.py` and `codegen.py`
  - Updated `examples/pattern_matching.vera` to use `Float64`
  - Updated spec chapters 4 and 11 to remove `Float` references
  - Decomposition issues tracked: [#99](https://github.com/aallan/vera/issues/99) (checker.py), [#100](https://github.com/aallan/vera/issues/100) (wasm.py)
- 1 new test (914 total, up from 913)

## [0.0.32] - 2026-02-27

### Added
- **Cross-module type checking** (C7b — partial [#14](https://github.com/aallan/vera/issues/14)):
  - Imported function signatures are now registered and type-checked: arity checking, argument type checking, generic inference, and effect propagation all work across module boundaries
  - **Bare calls**: `import vera.math(abs); abs(-5)` resolves `abs` from the imported module — immediately usable from source files
  - **Module-qualified calls**: `ModuleCall` AST nodes type-checked against imported module declarations (grammar limitation [#95](https://github.com/aallan/vera/issues/95) prevents parsing `path.fn(args)` from source)
  - Selective import enforcement: `import m(f)` restricts available names to `f` only; calls to unimported names produce errors with fix suggestions
  - Wildcard imports: `import m;` makes all module declarations available
  - Local declarations shadow imported names (standard rule)
  - Imported ADT constructors available via bare calls: `import col(List); Cons(1, Nil)` works
  - Module declarations registered in isolated `TypeChecker` instances to avoid namespace pollution
  - `examples/modules.vera` now exercises bare cross-module calls (`abs`, `max` from `vera.math`)
- LALR grammar limitation for module-qualified call syntax tracked as [#95](https://github.com/aallan/vera/issues/95)
- **Project website** ([veralang.dev](https://veralang.dev)): single-page site deployed via GitHub Pages ([#81](https://github.com/aallan/vera/pull/81))
- 13 new tests (913 total, up from 900)

## [0.0.31] - 2026-02-26

### Added
- **Module resolution** (C7a — partial [#14](https://github.com/aallan/vera/issues/14), [#50](https://github.com/aallan/vera/issues/50)): `import` paths now resolve to source files on disk
  - New `vera/resolver.py`: `ModuleResolver` maps import paths (e.g., `vera.math`) to `.vera` files relative to the importing file or project root
  - `ResolvedModule` dataclass: path tuple, file path, parsed Program AST, source text
  - Parse cache: each imported module parsed at most once per compilation session
  - Circular import detection via in-progress tracking set
  - Resolver wired into all CLI commands (`check`, `verify`, `compile`, `run`)
  - `typecheck()` accepts optional `resolved_modules` parameter; improved diagnostic messages distinguish "module resolved but type merging not yet implemented (C7b)" from "module not found"
  - `verify()` accepts `resolved_modules` for forward-compatibility with C7d
  - Stub modules `examples/vera/math.vera` and `examples/vera/collections.vera` for the `modules.vera` example
- README restructured: C6/C6.5 collapsed sections moved above "What's next"; new "Longer term" section with all 19 open issues linked by category
- 20 new tests (900 total, up from 880)

## [0.0.30] - 2026-02-26

### Added
- **old()/new() state expressions in postconditions** (C6.5f — closes [#70](https://github.com/aallan/vera/issues/70)): postconditions containing `old(State<T>)` and `new(State<T>)` now compile to WASM runtime checks
  - `old(State<T>)` snapshots the state value at function entry into a temp local
  - `new(State<T>)` reads the current state value at postcondition check time via `state_get`
  - `_snapshot_old_state()` in codegen.py walks ensures clauses to detect `OldExpr` nodes and emits snapshot instructions
  - `WasmContext._translate_old_expr()` and `_translate_new_expr()` handle the AST→WAT translation
  - Snapshot is only emitted when ensures clauses actually reference `old()` (trivial contracts skip it)
  - Completes the C6.5 codegen cleanup phase
- README restructured: C7 (Module System) is now the "What's next" section with sub-phase plan; C6.5 and C6 are collapsed
- 6 new codegen tests (880 total, up from 874)

## [0.0.29] - 2026-02-26

### Added
- **String and Array types in function signatures** (C6.5e — closes [#69](https://github.com/aallan/vera/issues/69)): functions with `String` or `Array<T>` parameters and return types now compile to WASM
  - Each String/Array parameter expands to two consecutive `i32` WASM parameters (pointer and length)
  - String/Array return types use WASM multi-value return `(result i32 i32)`
  - `_type_expr_to_wasm_type()` returns `"i32_pair"` sentinel instead of `"unsupported"` for String/Array
  - Generalised `_is_pair_type_name()` helper for String and Array<T> across slot refs, let bindings, and drop logic
  - `execute()` handles multi-value (list) returns from wasmtime
  - Postcondition checks skipped for pair return types (single-local save/restore pattern incompatible with two-value results)
  - `if` and `match` blocks emit `(result i32 i32)` for pair-typed branches
  - Functions previously skipped in `examples/pattern_matching.vera` and `examples/quantifiers.vera` now compile
- 8 new codegen tests (874 total, up from 866)

## [0.0.28] - 2026-02-26

### Added
- **Float64 modulo compilation** (C6.5d — closes [#46](https://github.com/aallan/vera/issues/46)): `%` on Float64 operands now compiles to WASM via the decomposition `a % b = a - trunc(a / b) * b`
  - Uses `f64.trunc` (truncation toward zero), matching C `fmod` semantics and consistent with `i64.rem_s` for integer modulo
  - Multi-instruction WAT sequence with temporary locals (same pattern as array indexing, closures)
  - WASM has no native `f64.rem` instruction; this was previously unsupported (function silently skipped)
- 4 new codegen tests: exact division, remainder, negative operand, parameterized (866 total, up from 862)

## [0.0.27] - 2026-02-26

### Added
- **Pipe operator compilation** (C6.5c — closes [#44](https://github.com/aallan/vera/issues/44)): `a |> f(x, y)` now compiles to WASM and verifies via Z3, desugaring to `f(a, x, y)` in both backends
  - WASM codegen: intercept `BinOp.PIPE` in `_translate_binary()`, construct synthetic `FnCall`, delegate to `_translate_call()`
  - SMT verifier: same desugaring pattern in `_translate_binary()`
  - No grammar, AST, transformer, or checker changes needed (pipe already parsed and type-checked)
- 4 new tests: 3 codegen, 1 verifier (862 total, up from 858)

## [0.0.26] - 2026-02-26

### Added
- **Handler `with` clause** (C6.5b — closes [#72](https://github.com/aallan/vera/issues/72)): handler operation clauses can now update handler state via `with @T = expr` after the clause body
  - Grammar: `with_clause` rule added to `handler_clause`
  - AST: `state_update` field on `HandlerClause`
  - Type checker: validates state update type matches handler state declaration
  - No codegen changes (handler clauses remain specifications per spec 11.11.2)
- 6 new tests: 2 parser, 4 checker (858 total, up from 852)

## [0.0.25] - 2026-02-26

### Fixed
- **`resume` recognized in handler scope** (C6.5a — closes [#74](https://github.com/aallan/vera/issues/74)): the type checker now binds `resume` as a function in handler clause bodies with the correct type (takes operation return type, returns Unit), eliminating spurious "Unresolved function 'resume'" warnings
- `effect_handler.vera` example now type-checks cleanly (moved from warn to clean examples)

### Added
- `_check_clean()` test helper asserts zero errors AND zero warnings
- 3 new tests: `test_resume_wrong_arg_type`, `test_resume_wrong_arity`, `test_resume_outside_handler` (852 total, up from 849)

## [0.0.24] - 2026-02-26

### Added
- **Spec Chapter 9: Standard Library** (C6n): documents all built-in types (`Option<T>`, `Result<T, E>`), collections (`Array<T>`), effects (`IO`, `State<T>`), and functions (`length`, future `similarity`); includes future features (Http, Async, Inference effects; Json, Decimal types; Set, Map collections; Abilities) with issue cross-references
- **Spec Chapter 12: Runtime and Execution** (C6n — closes [#63](https://github.com/aallan/vera/issues/63)): documents WASM module structure, wasmtime host runtime, host function bindings (IO.print, State\<T\>), linear memory model, bump allocator, execution flow, argument passing, error handling, and runtime limitations

### Changed
- **Spec Chapter 0**: condensed Section 0.8 design notes to a cross-reference table pointing to Chapter 9 sections (previously contained full feature designs inline)

## [0.0.23] - 2026-02-26

### Added
- **Refinement type alias compilation** (C6m): type aliases like `PosInt`, `Percentage`, `Nat` that resolve through refinement types now compile to their base WASM type
  - `_type_expr_to_wasm_type()` in codegen.py now recurses on any alias type (not just FnType)
  - `_resolve_base_type_name()` helper in wasm.py follows alias chains through refinement types to the underlying primitive (e.g. `PosInt` → `Int` → `i64`)
  - Applied uniformly to parameter types, return types, let bindings, and slot references
- **Spec Section 11.15**: new "Refinement Type Alias Compilation" section
- **Codegen tests**: 8 new tests — safe_divide, to_percentage (clamp low/pass/high), refined let bindings, refined return in expr, WAT exports (849 total, up from 841)

## [0.0.22] - 2026-02-26

### Added
- **Quantifier compilation** (C6l): compile `forall`/`exists` as runtime loops with short-circuit evaluation
  - Counted loop over `[0, domain)` with predicate inlined (no closure overhead)
  - `forall` returns true if all iterations satisfy predicate, short-circuits on first false
  - `exists` returns true if any iteration satisfies predicate, short-circuits on first true
  - Empty domain: `forall` → true (vacuously), `exists` → false
- **Assert compilation** (C6l): `assert(expr)` compiles to conditional `unreachable` trap
- **Assume compilation** (C6l): `assume(expr)` compiles to no-op at runtime (verifier-only construct)
- **Spec Sections 11.13-11.14**: new "Quantifier Compilation" and "Assert and Assume Compilation" sections
- **Codegen tests**: 20 new tests — assert/assume, forall, exists, WAT inspection (841 total, up from 821)

## [0.0.21] - 2026-02-26

### Added
- **Byte type compilation** (C6k): `Byte` maps to `i32` in WASM with unsigned comparison operators (`i32.lt_u`, `i32.gt_u`, `i32.le_u`, `i32.ge_u`); `i32.wrap_i64` coercion for Byte-returning functions with integer literal bodies
- **Array compilation** (C6k — closes [#30](https://github.com/aallan/vera/issues/30)): compile `Array<T>` literals, indexing, and `length()` to WASM via linear memory
  - Array representation: `(ptr: i32, len: i32)` pairs, allocated via bump allocator
  - Element types: `Byte` (1 byte, `i32.load8_u`/`i32.store8`), `Bool` (4 bytes), `Int`/`Nat` (8 bytes, `i64`), `Float64` (8 bytes, `f64`)
  - Bounds checking: `i32.ge_u` unsigned comparison + `unreachable` trap on out-of-bounds
  - `length()` built-in: extracts len from `(ptr, len)` pair, extends to `i64`
  - Array let bindings: two WASM locals (ptr at N, len at N+1)
  - Array slot refs: emit two `local.get` ops for `(ptr, len)` pair
  - Array function params/returns unsupported (skipped with warning, same as String)
- **Spec Section 11.12**: new "Array Compilation" section covering representation, allocation, indexing, bounds checking, length, let bindings, and scope
- **Codegen tests**: 26 new tests — Byte identity/zero/max/let/comparisons, array literals/indexing/bounds-check/length, WAT inspection (821 total, up from 795)

## [0.0.20] - 2026-02-25

### Fixed
- **Spec @T notation mismatch**: fixed 30 code blocks across 5 spec files where `@T` was used in data constructor fields and effect operation signatures (value-level `@` is for binding sites only); 16 blocks now parse, 14 recategorized as fragments for unrelated syntax reasons (empty effects, handler `with` clauses, inline function types)
- **Stale README limitation rows**: removed "No closure codegen" and "No effect handler codegen" rows (closed in v0.0.18 and v0.0.19 respectively)
- **Spec limitation issue tracking**: created GitHub issues [#50](https://github.com/aallan/vera/issues/50)–[#53](https://github.com/aallan/vera/issues/53) for all unlinked limitations in spec Chapter 11; updated spec and README tables

### Added
- **Test coverage**: 104 new tests across 4 modules (795 total, up from 691)
  - `tests/test_types.py` (new): 55 tests for `is_subtype`, `types_equal`, `substitute`, `pretty_type`, `canonical_type_name`, `base_type`
  - `tests/test_wasm.py` (new): 22 tests for `StringPool`, `WasmSlotEnv`, and translation edge cases via full compilation pipeline
  - `tests/test_errors.py`: 18 new tests for `SourceLocation`, `Diagnostic.to_dict`, `diagnose_lark_error`, `unclosed_block`, `unexpected_token`, `VeraError`
  - `tests/test_cli.py`: 10 new tests for compile/run/verify error paths in both text and JSON modes

### Changed
- **Spec allowlist**: removed all 30 MISMATCH entries from `check_spec_examples.py`; added 14 FRAGMENT entries for genuine syntax fragments; parsed blocks increased from 21 to 37

## [0.0.19] - 2026-02-25

### Added
- **Effect handler compilation** (C6j — closes [#28](https://github.com/aallan/vera/issues/28)): compile `handle[State<T>]` expressions to WASM via host imports
  - State handler translation: `handle[State<T>](@T = init) { get/put clauses } in { body }` compiles by initializing state via `state_put_T`, then compiling body with get/put mapped to host imports
  - Handler clauses serve as specifications (not compiled) — `resume()` calls describe the default State semantics, validated by type checker
  - Effect discharge: pure functions containing `handle[State<T>]` are compilable — state imports registered by scanning function body for handle expressions
  - Unsupported handlers (`Exn<E>`, custom effects) cause function to be skipped with warning
- **Reworked `examples/effect_handler.vera`**: removed `safe_parse` (uses String + undefined `parse_int`), added `test_state_init` and `test_put_get` (simple compilable tests)
- **Codegen tests**: 14 new tests — state initialization, put/get, increment pattern, run_counter, let bindings, Bool state, WAT inspection, unsupported handler skip, example file round-trips (691 total, up from 677)

## [0.0.18] - 2026-02-25

### Added
- **Closure compilation** (C6h — closes [#27](https://github.com/aallan/vera/issues/27)): compile anonymous functions and closures to WASM via function tables and `call_indirect`
  - Closure representation: heap-allocated struct `[func_table_idx: i32, capture_0, ...]` as `i32` pointer, using existing bump allocator
  - Function table infrastructure: `(type $closure_sig_N ...)`, `(table N funcref)`, `(elem ...)` for indirect calls
  - Closure lifting: anonymous functions compiled as module-level WASM functions with `$env` parameter for captured variables
  - Free variable capture: walk `AnonFn` body to detect `SlotRef` nodes referencing outer-scope bindings, store captured values in heap environment
  - `apply_fn` built-in: compiler-recognized function that emits `call_indirect` with closure's `func_table_idx`
  - Function type aliases (e.g. `type IntToInt = fn(Int -> Int) effects(pure)`) resolved to `i32` closure pointers
  - Functions with function-type parameters no longer skipped (recognized as compilable)
- **Reworked `examples/closures.vera`**: removed `Array<Int>`, undefined `map`, and `forall<T>` generic `map_option`; added `make_adder` (closure capture), `apply` (closure parameter), `map_option` (closure in match arm), `test_closure` and `test_map_option` (end-to-end round-trips)
- **Codegen tests**: 17 new tests — closure creation with/without capture, `apply_fn`, closures in let bindings and match arms, function-type parameters, WAT structure verification, example file round-trips (677 total, up from 660)

## [0.0.17] - 2026-02-24 ([#42](https://github.com/aallan/vera/pull/42))

### Added
- **Generics monomorphization** (C6i — closes [#29](https://github.com/aallan/vera/issues/29)): compile `forall<T>` functions to WASM via monomorphization
  - Collection pass: walk non-generic function bodies to find calls to generic functions, infer concrete type variable bindings
  - AST substitution: create monomorphized FnDecl copies with type variables replaced by concrete types (e.g. `@T.0` → `@Int.0`)
  - Name mangling: `identity` + `(Int,)` → `identity$Int`, `const` + `(Int, Bool)` → `const$Int_Bool`
  - Call rewriting: generic function calls resolve to mangled names at WASM translation time
  - FnCall type inference: infer WASM return types and Vera type names for function call expressions (improves if-branch and chained-call handling)
  - Supports: literal args, slot ref args, constructor args, chained generic calls, arithmetic expression args
- **Codegen tests**: 17 new tests — identity/const/is_some instantiation, two-instantiation exports, ADT match, chained calls, if-branches, let bindings, example files (660 total, up from 643)

## [0.0.16] - 2026-02-24 ([#41](https://github.com/aallan/vera/pull/41))

### Added
- **Match expression codegen** (C6g — closes [#26](https://github.com/aallan/vera/issues/26)): compile `MatchExpr` AST nodes to WASM chained if-else cascades
  - ADT tag dispatch: load tag from heap pointer, compare with constructor tag, branch
  - Field extraction: load constructor fields at computed offsets into locals, bind in environment
  - Monomorphized offsets: field offsets computed from concrete binding types (same approach as C6f constructor calls)
  - Pattern types: `ConstructorPattern`, `NullaryPattern`, `WildcardPattern`, `BindingPattern`, `BoolPattern`, `IntPattern`
  - Recursive if-else cascade: each arm generates a condition check and branches, last arm emits directly
  - Environment scoping: each arm gets fresh bindings from pattern extraction, no cross-arm leakage
- **Codegen tests**: 20 new tests — ADT tag dispatch, field extraction, wildcard catch-alls, Bool/Int literal patterns, binding patterns, composability (643 total, up from 623)

## [0.0.15] - 2026-02-24 ([#40](https://github.com/aallan/vera/pull/40))

### Added
- **ADT constructor codegen** (C6f): compile `ConstructorCall` and `NullaryConstructor` AST nodes to WASM heap-allocated tagged unions
  - Nullary constructors (e.g. `Red`, `None`): alloc → store tag → return pointer
  - Constructors with fields (e.g. `Some(42)`, `Wrap(@Int.0)`): alloc → store tag → store each field at computed offset → return pointer
  - Field offsets computed from concrete argument types at translation time — handles monomorphized generic constructors (e.g. `Some(T)` with `T=Int` stores i64)
  - ADT types compile to `i32` (heap pointer) in function signatures, slot references, and type inference
  - `WasmContext` accepts `ctor_layouts` and `adt_type_names` for constructor-aware translation
  - Functions using ADT constructors now compile (no longer skipped with warning)
- **Codegen tests**: 12 new tests — nullary/tagged constructors, Int/Bool fields, Option None/Some, WAT inspection, let bindings, if-then-else branches, ADT parameters (623 total, up from 611)

## [0.0.14] - 2026-02-24 ([#39](https://github.com/aallan/vera/pull/39))

### Added
- **Bump allocator infrastructure** (C6e): heap allocation support for upcoming ADT constructor codegen
  - `$heap_ptr` mutable global: initialized to first byte after string data, exported as `"heap_ptr"`
  - `$alloc` internal function: bump-allocates with 8-byte alignment, returns pointer to allocated block
  - ADT layout metadata: `ConstructorLayout` dataclass stores tag, field offsets, and total size per constructor
  - Layout computed eagerly during registration pass — available for C6f (constructor codegen) and C6g (match codegen)
  - Allocator and heap global emitted only when user-declared ADTs are present (no overhead for pure programs)
  - `StringPool.heap_offset` property exposes first free byte after string constants
- **Codegen tests**: 26 new tests — layout helpers, WAT output inspection, ADT metadata registration, conditional emission (611 total, up from 585)

## [0.0.13] - 2026-02-24 ([#38](https://github.com/aallan/vera/pull/38))

### Added
- **State\<T\> WASM host imports** (C6d): compile `get`/`put` operations for `State<T>` effects as WASM host imports
  - `State<Int>`, `State<Nat>`, `State<Bool>`, `State<Float64>` compile to typed host import pairs
  - `get(())` → `call $vera.state_get_{T}` (returns typed value); `put(x)` → `call $vera.state_put_{T}` (consumes typed value)
  - Host runtime maintains mutable state cells per type, initialized to zero
  - `execute()` accepts optional `initial_state` parameter and returns final `state` in `ExecuteResult`
  - Mixed effects supported: `effects(<State<Int>, IO>)` compiles correctly
  - `effect_ops` dict mechanism in `WasmContext` redirects bare `get`/`put` calls to host imports
  - `_is_void_expr` recognizes `put()` as void (no `drop` emitted in ExprStmt)
- **Codegen tests**: 15 new tests — get default, put-then-get, increment pattern, example file, Bool/Float64/Nat state, String rejection, mixed effects, WAT imports, multiple types, void semantics, initial state override, pure function purity (585 total, up from 570)
- `examples/increment.vera` now compiles and runs (7 of 14 examples compilable)

## [0.0.12] - 2026-02-24 ([#37](https://github.com/aallan/vera/pull/37))

### Added
- **Match exhaustiveness checking** (C6c — closes [#18](https://github.com/aallan/vera/issues/18)): compile-time verification that match expressions cover all possible values
  - ADT exhaustiveness: all constructors must be covered or a catch-all pattern must be present
  - Bool exhaustiveness: both `true` and `false` must be covered or a catch-all present
  - Infinite type exhaustiveness: `Int`, `String`, `Float64`, `Nat` matches require a wildcard `_` or binding pattern
  - Unreachable arm warnings: arms after a wildcard or binding catch-all produce warnings (Spec Section 4.9.3)
  - Refinement types properly stripped via `base_type()` before analysis
  - Error diagnostics include missing constructor/value names and fix suggestions
- **Type checker tests**: 17 new tests — ADT exhaustive/missing/wildcard/binding, Bool exhaustive/missing/wildcard, Int/String without wildcard, unreachable arms (single/multiple/after binding), wildcard only, refined type stripping (570 total, up from 553)

## [0.0.11] - 2026-02-24 ([#36](https://github.com/aallan/vera/pull/36))

### Added
- **Callee precondition verification** (C6b — closes [#19](https://github.com/aallan/vera/issues/19)): modular call-site contract checking
  - When function `f` calls function `g`, the verifier now checks that `g`'s `requires()` clauses hold at the call site given `f`'s assumptions
  - Callee postconditions (`ensures()`) are assumed at the call site, enabling symbolic reasoning about return values
  - Fresh Z3 variables created per call, with postconditions asserted — supports chained calls, let bindings, and recursive calls
  - Recursive functions (e.g., `factorial`) now verify `ensures()` at Tier 1 instead of falling to Tier 3
  - `CallViolation` dataclass in `smt.py` records call-site violations with callee name, precondition, and counterexample
  - `_report_call_violation()` in verifier produces LLM-oriented diagnostics with fix suggestions
  - `param_type_exprs` field added to `FunctionInfo` for callee parameter slot resolution
- **Verifier tests**: 13 new tests — satisfied/violated/forwarded preconditions, assumed postconditions, recursive calls, trivial preconditions, let bindings, where-block calls, generic call fallback, multiple preconditions, sequential calls, error message quality (553 total, up from 540)

### Changed
- Tier 3 warning rationale updated: "recursive calls" replaced with "generic calls" (recursive calls now handled via modular verification)
- `SmtContext` constructor accepts optional `fn_lookup` callback for callee contract resolution
- Caller precondition assumptions now asserted into the Z3 solver before body translation

## [0.0.10] - 2026-02-24 ([#35](https://github.com/aallan/vera/pull/35))

### Added
- **Float64 WASM codegen** (C6a — closes [#25](https://github.com/aallan/vera/issues/25)): compile Float64/Float values to WebAssembly `f64` instructions
  - Type mapping: Float64/Float → `f64` in `wasm_type()`, `_type_expr_to_wasm_type()`, `_slot_name_to_wasm_type()`, `_infer_expr_wasm_type()`, `_infer_block_result_type()`
  - `FloatLit` emission: `f64.const` literals
  - Float64 arithmetic: `f64.add`, `f64.sub`, `f64.mul`, `f64.div` (MOD unsupported — WASM has no `f64.rem`)
  - Float64 comparisons: `f64.eq`, `f64.ne`, `f64.lt`, `f64.gt`, `f64.le`, `f64.ge` (result is `i32`)
  - Float64 negation: `f64.neg`
  - Float64 slot references, let bindings, if/else branches, function parameters and returns all compile
  - `ExecuteResult.value` widened to `int | float | None`; `execute()` accepts `list[int | float]` args
- **Codegen tests**: 26 new tests — Float64 literals, slot references, arithmetic, comparisons, negation, if/else, let bindings, WAT output validation (540 total, up from 514)

### Changed
- `execute()` signature updated: `args` parameter accepts `list[int | float]` for Float64 arguments
- Warning messages updated to mention Float64 as a compilable type
- CLI `fn_args` type widened to `list[int | float]` for future float argument parsing

## [0.0.9] - 2026-02-23 ([#31](https://github.com/aallan/vera/pull/31))

### Added
- **WASM code generation** (`vera/codegen.py`, `vera/wasm.py`): compile verified Vera programs to WebAssembly and execute them via wasmtime — **first light** 🌅
  - Two-pass code generator: register functions (forward references, mutual recursion), then compile bodies
  - Expression compilation: integer/Boolean literals, arithmetic, comparisons, Boolean logic, if/else, let bindings, blocks, function calls, recursion, string literals, IO operations
  - `WasmSlotEnv` — maps De Bruijn slot references (`@T.n`) to WASM local indices (mirrors `SlotEnv` in `smt.py`)
  - `WasmContext` — manages local allocation, data section, imports, accumulates WAT instructions
  - `StringPool` — deduplicated string constants in the WASM data section
  - Type mapping: Int/Nat → i64, Bool → i32, Unit → void, String → (i32 ptr, i32 len) pair
  - IO effect as host imports: `IO.print` compiles to imported host function, host reads UTF-8 from linear memory
  - Where-block functions compiled as module-level WASM functions
  - Graceful degradation: functions with unsupported types/constructs are skipped with a warning
- **Runtime contract insertion**: Tier 3 (unverified) contracts compiled as runtime assertions
  - Preconditions checked at function entry, trap on violation via `unreachable`
  - Postconditions checked after body with result stored in temp local
  - Trivial contracts (`requires(true)`, `ensures(true)`) eliminated — no runtime overhead
  - Tier 1 (proven) contracts omitted — statically guaranteed
- **CLI commands**: `vera compile` and `vera run`
  - `vera compile <file>` — full pipeline, writes `.wasm` binary; `--wat` prints human-readable WAT; `--json` for diagnostics; `-o` for output path
  - `vera run <file>` — compile and execute; `--fn` to call specific function; `--` to pass arguments; `--json` for structured output
- **Spec Chapter 11** (`spec/11-compilation.md`): compilation model documentation — type mapping, expression compilation, string pool, IO host bindings, runtime contracts, CLI commands, limitations
- **Hello World example** (`examples/hello_world.vera`): IO effect with qualified `IO.print` call (14 examples total)
- **README code sample testing** (`scripts/check_readme_examples.py`, `tests/test_readme.py`): extracts Vera code blocks from README.md and verifies they parse; pre-commit hook added
- **Codegen tests**: 76 new tests — literals, arithmetic, comparisons, Boolean logic, control flow, let bindings, function calls, recursion, strings, IO, runtime contracts, CLI commands, subprocess integration (470 total, up from 372)

### Changed
- README: updated status table (WASM codegen: Working), advanced roadmap (C5 Done, What's next → C6), added `vera compile`/`vera run` docs, updated project structure with `wasm.py`, `codegen.py`, `spec/11-compilation.md`
- SKILLS.md: added `vera compile` and `vera run` to toolchain section, added Chapter 11 to spec reference table
- CLAUDE.md: added compile/run commands, updated pipeline, updated example/test counts
- AGENTS.md: added compile/run commands, updated pipeline, added `wasm.py` and `codegen.py` to module table, updated test counts
- vera/README.md: updated pipeline diagram (6 stages), added codegen/wasm to module map, updated line counts, added codegen section, updated test suite table, updated limitations

### Fixed
- Documentation consistency: all `print(...)` calls in README.md, spec/05-functions.md, spec/07-effects.md, and SKILLS.md corrected to use qualified `IO.print(...)` syntax (matching the language's "one canonical form" design principle)

## [0.0.8] - 2026-02-23 ([#10](https://github.com/aallan/vera/pull/10))

### Added
- **Contract verifier** (`vera/verifier.py`): Z3-backed verification of `requires`/`ensures` contracts on functions
  - Three-tier verification: Tier 1 (decidable, Z3 proves automatically), Tier 3 (runtime fallback with warning)
  - Forward symbolic execution — translates function body to Z3 expression, checks postconditions directly
  - Counterexample generation — when verification fails, shows concrete input values that break the contract
  - Trivial contract fast path — `requires(true)`/`ensures(true)` counted as verified without invoking Z3
  - Graceful Tier 3 fallback for unsupported constructs (match, effects, recursion, quantifiers)
  - LLM-oriented diagnostics with counterexample values, rationale, and spec references
- **SMT translation layer** (`vera/smt.py`): bridges Vera AST expressions to Z3 formulas
  - `SlotEnv` — De Bruijn slot stacks mapped to Z3 variables
  - Expression translation: integer/Boolean literals, arithmetic, comparisons, Boolean logic, if/else, let bindings, `length()` (uninterpreted function)
  - `SmtContext.check_valid()` — refutation-based validity checking with counterexample extraction
- **CLI command**: `vera verify <file>` — type-check and verify contracts, prints verification summary
- **Convenience API**: `verify_file(path)` in `vera/parser.py`
- **Verifier tests**: 51 new tests — round-trip verification of all 13 examples, trivial contracts, ensures verification, if/else bodies, let bindings, multiple contracts, counterexample extraction, tier classification, arithmetic, summary counts, edge cases (335 total, up from 284)

### Changed
- `FunctionInfo` in `vera/environment.py` now stores contract AST nodes (for modular verification)
- `_register_fn` in `vera/checker.py` passes contracts to `FunctionInfo`
- README: updated status table (Contract verifier: Working), added `vera verify` docs, updated project structure and test count, advanced roadmap (C4 Done, What's next → C5)
- SKILLS.md: added `vera verify` to toolchain section
- LICENSE converted to Markdown format

## [0.0.7] - 2026-02-23

### Added
- **Spec code block validator** (`scripts/check_spec_examples.py`) — extracts 154 code blocks from spec Markdown, classifies them as parseable/fragment/non-Vera, and verifies parseable blocks still parse with the current grammar. Categorised allowlist tracks 30 spec/parser mismatches (spec uses `@T` in data/effect declarations, parser expects bare `T`), 4 future-syntax design proposals, and 3 fragment overrides. Stale allowlist detection catches when spec edits shift line numbers.
- **Version sync check** (`scripts/check_version_sync.py`) — verifies `pyproject.toml` and `vera/__init__.py` agree on version number
- **Dependabot** (`.github/dependabot.yml`) — weekly automated PRs for pip and GitHub Actions dependency updates
- **CODEOWNERS** (`.github/CODEOWNERS`) — automatic review requests on PRs
- **CodeQL** (`.github/workflows/codeql.yml`) — security scanning on PRs and weekly schedule
- **macOS CI** — test matrix expanded from 3 jobs (ubuntu × 3 Python) to 6 jobs (ubuntu + macOS × 3 Python)
- **README narratives** — sub-headings and explanations for each code example in "What Vera Looks Like": Hello World (effects/contracts), absolute_value (postconditions/SMT verification), safe_divide (preconditions/compile-time guarantees), increment (algebraic effects/explicit state)

### Changed
- **CONTRIBUTING.md** — added pre-commit setup instructions, validation script documentation, branch protection rules
- **CI lint job** — now runs version sync check and spec code block validator alongside example validation

## [0.0.6] - 2026-02-23

### Added
- **Hello World example** in README — first example in "What Vera Looks Like" section, demonstrates IO effect and mandatory contracts
- **Spec design notes** in Chapter 0.8 (Section 0.8):
  - **Abilities**: Roc-style restricted type constraints — auto-derivable built-in set (`Eq`, `Ord`, `Hash`, `Encode`, `Decode`, `Show`), no higher-kinded types, `forall<T where Ability<T>>` syntax
  - **LLM Inference effect**: `<Inference>` as an algebraic effect for AI runtime calls — testable via mock handlers, explicit in type signatures, contracts still apply
  - **Standard library collections**: `Set<T>`, `Map<K, V>` (depend on abilities), `Decimal` (software implementation for WASM)

## [0.0.5] - 2026-02-23 ([#2](https://github.com/aallan/vera/pull/2))

### Added
- **Type checker**: Tier 1 decidable type checking — validates expression types, slot reference resolution, effect annotations, and contract well-formedness
  - Two-pass architecture: pass 1 registers all declarations (handles forward references and mutual recursion), pass 2 checks bodies
  - Expression type synthesis for all AST node types (literals, operators, calls, constructors, match/if, handlers, anonymous functions, quantifiers)
  - De Bruijn slot reference resolution with alias opacity
  - Function call type checking with generic type argument inference
  - ADT constructor validation and pattern type checking
  - Basic effect checking (pure functions can't call effectful operations)
  - Handler effect discharge (handlers eliminate their effect from the enclosing function's requirements)
  - Contract well-formedness (predicates must be Bool, `@T.result` only in ensures, `old`/`new` only in ensures)
  - Error accumulation — all type errors collected, never stops at first error
  - Unresolved name graceful handling (warning, not failure) with `UnknownType` propagation
  - LLM-oriented `TypeError` diagnostics with description, location, rationale, fix, and spec reference
- **Internal type representation** (`vera/types.py`): semantic `Type` objects separate from syntactic AST `TypeExpr` nodes — `PrimitiveType`, `AdtType`, `FunctionType`, `RefinedType`, `TypeVar`, `UnknownType`, effect row types
- **Type environment** (`vera/environment.py`): scope stack, slot resolution, built-in type/effect/function registrations (Option, Result, State with get/put, IO, length)
- **CLI command**: `vera typecheck <file>` as explicit alias for `vera check`
- **Convenience API**: `typecheck_file(path)` in `vera/parser.py`
- **Type checker tests**: 91 new tests — round-trip tests for all 13 examples, literal types, slot references, operators, function calls, generics, constructors, patterns, control flow, effects, contracts, higher-order functions, refinement types, error accumulation, where blocks, arrays, return types (284 total, up from 193)

### Changed
- `vera check` now runs the full pipeline: parse → AST → type check (previously parse only)
- README: updated status table (Type checker: Working), added type checker to project structure, documented `vera typecheck` alias
- SKILLS.md: added `vera typecheck` to toolchain section

## [0.0.4] - 2026-02-23 ([#1](https://github.com/aallan/vera/pull/1))

### Added
- **Typed AST layer**: frozen dataclass nodes with source spans, covering all grammar constructs (~50 node classes)
- **Lark→AST transformer**: bottom-up `Transformer` with ~86 methods converting parse trees to typed AST nodes
- **CLI command**: `vera ast <file>` prints indented text AST, `vera ast --json <file>` prints JSON
- **Convenience API**: `parse_to_ast(source, file)` in `vera/parser.py`
- **AST tests**: 83 new tests — round-trip tests for all 13 examples, node-specific tests for every construct, span tests, serialisation tests (193 total, up from 110)
- **TransformError**: new error class for AST transformation failures, subclass of `VeraError`

### Changed
- README: added AST to project status table, `vera ast` CLI docs, updated project structure
- SKILLS.md: added `vera ast` and `vera ast --json` to toolchain section

## [0.0.3] - 2026-02-23

### Added
- **Parser tests**: 40 new tests covering annotation comments, anonymous functions, generics, refinement types, tuple destructuring, quantifiers, assert/assume, qualified calls, function types, float literals, nested patterns, handler variations, and implies operator (110 total, up from 70)
- **Example programs**: 8 new examples — closures, generics, refinement types, effect handlers, modules, quantifiers, pattern matching, mutual recursion (13 total, up from 5)
- **Design notes**: network access as an effect (`<Http>`), JSON as a stdlib ADT, async promises/futures as an effect (`<Async>`) documented in spec Chapter 0

### Fixed
- Grammar: annotation comments (`/* ... */`) now correctly ignored by the parser
- Grammar: `vera/__init__.py` version was `0.1.0`, corrected to match pyproject.toml
- Spec Chapter 3: removed deliberation marker about `@Fn0` approach, kept settled type alias approach
- Spec Chapter 6: rewrote counterexample reporting section (removed incorrect example, added actionable fix suggestions)
- Spec Chapter 7: cleaned up effect-contract interaction section (removed problematic `get()` in contract example, kept settled `old()`/`new()` syntax)

## [0.0.2] - 2026-02-23

### Added
- **CI**: GitHub Actions workflow running pytest on Python 3.11/3.12/3.13 with coverage on 3.12
- **Social preview**: meerkat sentinel mascot using Negroni brand colour palette
- **Custom domain**: veralang.dev with GitHub Pages and HTTPS

### Changed
- CONTRIBUTING.md: point "Questions?" section at Issues instead of Discussions
- Issue template config: remove Discussions contact link (Discussions disabled)
- README: add social preview banner image linking to veralang.dev
- pyproject.toml: update Homepage/Documentation URLs to veralang.dev

## [0.0.1] - 2026-02-23

### Added
- **Parser**: Lark LALR(1) parser that validates `.vera` source files
  - `vera check <file>` — parse and report errors
  - `vera parse <file>` — print the parse tree
- **LLM-oriented diagnostics**: error messages are natural language instructions explaining what went wrong, why, how to fix it with a code example, and a spec reference
- **SKILLS.md**: complete language reference for LLM agents, following the agent skills format
- **Example programs**: `absolute_value.vera`, `safe_divide.vera`, `increment.vera`, `factorial.vera`, `list_ops.vera`
- **Test suite**: 70 tests (54 parser, 16 error diagnostics)
- **Language specification** chapters 0-7 and 10 (draft)
  - Chapter 0: Introduction, philosophy, and diagnostics-as-instructions
  - Chapter 1: Lexical structure
  - Chapter 2: Type system with refinement types
  - Chapter 3: Slot reference system (`@T.n` typed De Bruijn indices)
  - Chapter 4: Expressions and statements
  - Chapter 5: Function declarations
  - Chapter 6: Contract system (preconditions, postconditions, verification tiers)
  - Chapter 7: Algebraic effect system
  - Chapter 10: Formal EBNF grammar (LALR(1) compatible)
- Project structure with `spec/`, `vera/`, `runtime/`, `tests/`, `examples/`
- Python project configuration (`pyproject.toml`)
- Repository documentation (README, LICENSE, CONTRIBUTING, CODE_OF_CONDUCT, CHANGELOG)
- GitHub issue and pull request templates

### Fixed
- Grammar: operator precedence chain (pipe/implies ordering)
- Grammar: `old()`/`new()` accept parameterised types (`State<Int>`)
- Grammar: function signatures use `@Type` prefix to declare binding sites
- Grammar: handler body simplified to avoid LALR reduce/reduce conflict
- `pyproject.toml`: corrected build backend, package discovery, PEP 639 compliance

[Unreleased]: https://github.com/aallan/vera/compare/v0.1.9...HEAD
[0.1.9]: https://github.com/aallan/vera/compare/v0.1.8...v0.1.9
[0.1.8]: https://github.com/aallan/vera/compare/v0.1.7...v0.1.8
[0.1.7]: https://github.com/aallan/vera/compare/v0.1.6...v0.1.7
[0.1.6]: https://github.com/aallan/vera/compare/v0.1.5...v0.1.6
[0.1.5]: https://github.com/aallan/vera/compare/v0.1.4...v0.1.5
[0.1.4]: https://github.com/aallan/vera/compare/v0.1.3...v0.1.4
[0.1.3]: https://github.com/aallan/vera/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/aallan/vera/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/aallan/vera/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/aallan/vera/compare/v0.0.196...v0.1.0
[0.0.196]: https://github.com/aallan/vera/compare/v0.0.195...v0.0.196
[0.0.195]: https://github.com/aallan/vera/compare/v0.0.194...v0.0.195
[0.0.194]: https://github.com/aallan/vera/compare/v0.0.193...v0.0.194
[0.0.193]: https://github.com/aallan/vera/compare/v0.0.192...v0.0.193
[0.0.192]: https://github.com/aallan/vera/compare/v0.0.191...v0.0.192
[0.0.191]: https://github.com/aallan/vera/compare/v0.0.190...v0.0.191
[0.0.190]: https://github.com/aallan/vera/compare/v0.0.189...v0.0.190
[0.0.189]: https://github.com/aallan/vera/compare/v0.0.188...v0.0.189
[0.0.188]: https://github.com/aallan/vera/compare/v0.0.187...v0.0.188
[0.0.187]: https://github.com/aallan/vera/compare/v0.0.186...v0.0.187
[0.0.186]: https://github.com/aallan/vera/compare/v0.0.185...v0.0.186
[0.0.185]: https://github.com/aallan/vera/compare/v0.0.184...v0.0.185
[0.0.184]: https://github.com/aallan/vera/compare/v0.0.183...v0.0.184
[0.0.183]: https://github.com/aallan/vera/compare/v0.0.182...v0.0.183
[0.0.182]: https://github.com/aallan/vera/compare/v0.0.181...v0.0.182
[0.0.181]: https://github.com/aallan/vera/compare/v0.0.180...v0.0.181
[0.0.180]: https://github.com/aallan/vera/compare/v0.0.179...v0.0.180
[0.0.179]: https://github.com/aallan/vera/compare/v0.0.178...v0.0.179
[0.0.178]: https://github.com/aallan/vera/compare/v0.0.177...v0.0.178
[0.0.177]: https://github.com/aallan/vera/compare/v0.0.176...v0.0.177
[0.0.176]: https://github.com/aallan/vera/compare/v0.0.175...v0.0.176
[0.0.175]: https://github.com/aallan/vera/compare/v0.0.174...v0.0.175
[0.0.174]: https://github.com/aallan/vera/compare/v0.0.173...v0.0.174
[0.0.173]: https://github.com/aallan/vera/compare/v0.0.172...v0.0.173
[0.0.172]: https://github.com/aallan/vera/compare/v0.0.171...v0.0.172
[0.0.171]: https://github.com/aallan/vera/compare/v0.0.170...v0.0.171
[0.0.170]: https://github.com/aallan/vera/compare/v0.0.169...v0.0.170
[0.0.169]: https://github.com/aallan/vera/compare/v0.0.168...v0.0.169
[0.0.168]: https://github.com/aallan/vera/compare/v0.0.167...v0.0.168
[0.0.167]: https://github.com/aallan/vera/compare/v0.0.166...v0.0.167
[0.0.166]: https://github.com/aallan/vera/compare/v0.0.165...v0.0.166
[0.0.165]: https://github.com/aallan/vera/compare/v0.0.164...v0.0.165
[0.0.164]: https://github.com/aallan/vera/compare/v0.0.163...v0.0.164
[0.0.163]: https://github.com/aallan/vera/compare/v0.0.162...v0.0.163
[0.0.162]: https://github.com/aallan/vera/compare/v0.0.161...v0.0.162
[0.0.161]: https://github.com/aallan/vera/compare/v0.0.160...v0.0.161
[0.0.160]: https://github.com/aallan/vera/compare/v0.0.159...v0.0.160
[0.0.159]: https://github.com/aallan/vera/compare/v0.0.158...v0.0.159
[0.0.158]: https://github.com/aallan/vera/compare/v0.0.157...v0.0.158
[0.0.157]: https://github.com/aallan/vera/compare/v0.0.156...v0.0.157
[0.0.156]: https://github.com/aallan/vera/compare/v0.0.155...v0.0.156
[0.0.155]: https://github.com/aallan/vera/compare/v0.0.154...v0.0.155
[0.0.154]: https://github.com/aallan/vera/compare/v0.0.153...v0.0.154
[0.0.153]: https://github.com/aallan/vera/compare/v0.0.152...v0.0.153
[0.0.152]: https://github.com/aallan/vera/compare/v0.0.151...v0.0.152
[0.0.151]: https://github.com/aallan/vera/compare/v0.0.150...v0.0.151
[0.0.150]: https://github.com/aallan/vera/compare/v0.0.149...v0.0.150
[0.0.149]: https://github.com/aallan/vera/compare/v0.0.148...v0.0.149
[0.0.148]: https://github.com/aallan/vera/compare/v0.0.147...v0.0.148
[0.0.147]: https://github.com/aallan/vera/compare/v0.0.146...v0.0.147
[0.0.146]: https://github.com/aallan/vera/compare/v0.0.145...v0.0.146
[0.0.145]: https://github.com/aallan/vera/compare/v0.0.144...v0.0.145
[0.0.144]: https://github.com/aallan/vera/compare/v0.0.143...v0.0.144
[0.0.143]: https://github.com/aallan/vera/compare/v0.0.142...v0.0.143
[0.0.142]: https://github.com/aallan/vera/compare/v0.0.141...v0.0.142
[0.0.141]: https://github.com/aallan/vera/compare/v0.0.140...v0.0.141
[0.0.140]: https://github.com/aallan/vera/compare/v0.0.139...v0.0.140
[0.0.139]: https://github.com/aallan/vera/compare/v0.0.138...v0.0.139
[0.0.138]: https://github.com/aallan/vera/compare/v0.0.137...v0.0.138
[0.0.137]: https://github.com/aallan/vera/compare/v0.0.136...v0.0.137
[0.0.136]: https://github.com/aallan/vera/compare/v0.0.135...v0.0.136
[0.0.135]: https://github.com/aallan/vera/compare/v0.0.134...v0.0.135
[0.0.134]: https://github.com/aallan/vera/compare/v0.0.133...v0.0.134
[0.0.133]: https://github.com/aallan/vera/compare/v0.0.132...v0.0.133
[0.0.132]: https://github.com/aallan/vera/compare/v0.0.131...v0.0.132
[0.0.131]: https://github.com/aallan/vera/compare/v0.0.130...v0.0.131
[0.0.130]: https://github.com/aallan/vera/compare/v0.0.129...v0.0.130
[0.0.129]: https://github.com/aallan/vera/compare/v0.0.128...v0.0.129
[0.0.128]: https://github.com/aallan/vera/compare/v0.0.127...v0.0.128
[0.0.127]: https://github.com/aallan/vera/compare/v0.0.126...v0.0.127
[0.0.126]: https://github.com/aallan/vera/compare/v0.0.125...v0.0.126
[0.0.125]: https://github.com/aallan/vera/compare/v0.0.124...v0.0.125
[0.0.124]: https://github.com/aallan/vera/compare/v0.0.123...v0.0.124
[0.0.123]: https://github.com/aallan/vera/compare/v0.0.122...v0.0.123
[0.0.122]: https://github.com/aallan/vera/compare/v0.0.121...v0.0.122
[0.0.121]: https://github.com/aallan/vera/compare/v0.0.120...v0.0.121
[0.0.120]: https://github.com/aallan/vera/compare/v0.0.119...v0.0.120
[0.0.119]: https://github.com/aallan/vera/compare/v0.0.118...v0.0.119
[0.0.118]: https://github.com/aallan/vera/compare/v0.0.117...v0.0.118
[0.0.117]: https://github.com/aallan/vera/compare/v0.0.116...v0.0.117
[0.0.116]: https://github.com/aallan/vera/compare/v0.0.115...v0.0.116
[0.0.115]: https://github.com/aallan/vera/compare/v0.0.114...v0.0.115
[0.0.114]: https://github.com/aallan/vera/compare/v0.0.113...v0.0.114
[0.0.113]: https://github.com/aallan/vera/compare/v0.0.112...v0.0.113
[0.0.112]: https://github.com/aallan/vera/compare/v0.0.111...v0.0.112
[0.0.111]: https://github.com/aallan/vera/compare/v0.0.110...v0.0.111
[0.0.110]: https://github.com/aallan/vera/compare/v0.0.109...v0.0.110
[0.0.109]: https://github.com/aallan/vera/compare/v0.0.108...v0.0.109
[0.0.108]: https://github.com/aallan/vera/compare/v0.0.107...v0.0.108
[0.0.107]: https://github.com/aallan/vera/compare/v0.0.106...v0.0.107
[0.0.106]: https://github.com/aallan/vera/compare/v0.0.105...v0.0.106
[0.0.105]: https://github.com/aallan/vera/compare/v0.0.104...v0.0.105
[0.0.104]: https://github.com/aallan/vera/compare/v0.0.103...v0.0.104
[0.0.103]: https://github.com/aallan/vera/compare/v0.0.102...v0.0.103
[0.0.102]: https://github.com/aallan/vera/compare/v0.0.101...v0.0.102
[0.0.101]: https://github.com/aallan/vera/compare/v0.0.100...v0.0.101
[0.0.100]: https://github.com/aallan/vera/compare/v0.0.99...v0.0.100
[0.0.99]: https://github.com/aallan/vera/compare/v0.0.98...v0.0.99
[0.0.98]: https://github.com/aallan/vera/compare/v0.0.97...v0.0.98
[0.0.97]: https://github.com/aallan/vera/compare/v0.0.96...v0.0.97
[0.0.96]: https://github.com/aallan/vera/compare/v0.0.95...v0.0.96
[0.0.95]: https://github.com/aallan/vera/compare/v0.0.94...v0.0.95
[0.0.94]: https://github.com/aallan/vera/compare/v0.0.93...v0.0.94
[0.0.93]: https://github.com/aallan/vera/compare/v0.0.92...v0.0.93
[0.0.92]: https://github.com/aallan/vera/compare/v0.0.91...v0.0.92
[0.0.91]: https://github.com/aallan/vera/compare/v0.0.90...v0.0.91
[0.0.90]: https://github.com/aallan/vera/compare/v0.0.89...v0.0.90
[0.0.89]: https://github.com/aallan/vera/compare/v0.0.88...v0.0.89
[0.0.88]: https://github.com/aallan/vera/compare/v0.0.87...v0.0.88
[0.0.87]: https://github.com/aallan/vera/compare/v0.0.86...v0.0.87
[0.0.86]: https://github.com/aallan/vera/compare/v0.0.85...v0.0.86
[0.0.85]: https://github.com/aallan/vera/compare/v0.0.84...v0.0.85
[0.0.84]: https://github.com/aallan/vera/compare/v0.0.83...v0.0.84
[0.0.83]: https://github.com/aallan/vera/compare/v0.0.82...v0.0.83
[0.0.82]: https://github.com/aallan/vera/compare/v0.0.81...v0.0.82
[0.0.81]: https://github.com/aallan/vera/compare/v0.0.80...v0.0.81
[0.0.80]: https://github.com/aallan/vera/compare/v0.0.79...v0.0.80
[0.0.79]: https://github.com/aallan/vera/compare/v0.0.78...v0.0.79
[0.0.78]: https://github.com/aallan/vera/compare/v0.0.77...v0.0.78
[0.0.77]: https://github.com/aallan/vera/compare/v0.0.76...v0.0.77
[0.0.76]: https://github.com/aallan/vera/compare/v0.0.75...v0.0.76
[0.0.75]: https://github.com/aallan/vera/compare/v0.0.74...v0.0.75
[0.0.74]: https://github.com/aallan/vera/compare/v0.0.73...v0.0.74
[0.0.73]: https://github.com/aallan/vera/compare/v0.0.72...v0.0.73
[0.0.72]: https://github.com/aallan/vera/compare/v0.0.71...v0.0.72
[0.0.71]: https://github.com/aallan/vera/compare/v0.0.70...v0.0.71
[0.0.70]: https://github.com/aallan/vera/compare/v0.0.69...v0.0.70
[0.0.69]: https://github.com/aallan/vera/compare/v0.0.68...v0.0.69
[0.0.68]: https://github.com/aallan/vera/compare/v0.0.67...v0.0.68
[0.0.67]: https://github.com/aallan/vera/compare/v0.0.66...v0.0.67
[0.0.66]: https://github.com/aallan/vera/compare/v0.0.65...v0.0.66
[0.0.65]: https://github.com/aallan/vera/compare/v0.0.64...v0.0.65
[0.0.64]: https://github.com/aallan/vera/compare/v0.0.63...v0.0.64
[0.0.63]: https://github.com/aallan/vera/compare/v0.0.62...v0.0.63
[0.0.62]: https://github.com/aallan/vera/compare/v0.0.61...v0.0.62
[0.0.61]: https://github.com/aallan/vera/compare/v0.0.60...v0.0.61
[0.0.60]: https://github.com/aallan/vera/compare/v0.0.59...v0.0.60
[0.0.59]: https://github.com/aallan/vera/compare/v0.0.58...v0.0.59
[0.0.58]: https://github.com/aallan/vera/compare/v0.0.57...v0.0.58
[0.0.57]: https://github.com/aallan/vera/compare/v0.0.56...v0.0.57
[0.0.56]: https://github.com/aallan/vera/compare/v0.0.55...v0.0.56
[0.0.55]: https://github.com/aallan/vera/compare/v0.0.54...v0.0.55
[0.0.54]: https://github.com/aallan/vera/compare/v0.0.53...v0.0.54
[0.0.53]: https://github.com/aallan/vera/compare/v0.0.52...v0.0.53
[0.0.52]: https://github.com/aallan/vera/compare/v0.0.51...v0.0.52
[0.0.51]: https://github.com/aallan/vera/compare/v0.0.50...v0.0.51
[0.0.50]: https://github.com/aallan/vera/compare/v0.0.49...v0.0.50
[0.0.49]: https://github.com/aallan/vera/compare/v0.0.48...v0.0.49
[0.0.48]: https://github.com/aallan/vera/compare/v0.0.47...v0.0.48
[0.0.47]: https://github.com/aallan/vera/compare/v0.0.46...v0.0.47
[0.0.46]: https://github.com/aallan/vera/compare/v0.0.45...v0.0.46
[0.0.45]: https://github.com/aallan/vera/compare/v0.0.44...v0.0.45
[0.0.44]: https://github.com/aallan/vera/compare/v0.0.43...v0.0.44
[0.0.43]: https://github.com/aallan/vera/compare/v0.0.42...v0.0.43
[0.0.42]: https://github.com/aallan/vera/compare/v0.0.41...v0.0.42
[0.0.41]: https://github.com/aallan/vera/compare/v0.0.40...v0.0.41
[0.0.40]: https://github.com/aallan/vera/compare/v0.0.39...v0.0.40
[0.0.39]: https://github.com/aallan/vera/compare/v0.0.38...v0.0.39
[0.0.38]: https://github.com/aallan/vera/compare/v0.0.37...v0.0.38
[0.0.37]: https://github.com/aallan/vera/compare/v0.0.36...v0.0.37
[0.0.36]: https://github.com/aallan/vera/compare/v0.0.35...v0.0.36
[0.0.35]: https://github.com/aallan/vera/compare/v0.0.34...v0.0.35
[0.0.34]: https://github.com/aallan/vera/compare/v0.0.33...v0.0.34
[0.0.33]: https://github.com/aallan/vera/compare/v0.0.32...v0.0.33
[0.0.32]: https://github.com/aallan/vera/compare/v0.0.31...v0.0.32
[0.0.31]: https://github.com/aallan/vera/compare/v0.0.30...v0.0.31
[0.0.30]: https://github.com/aallan/vera/compare/v0.0.29...v0.0.30
[0.0.29]: https://github.com/aallan/vera/compare/v0.0.28...v0.0.29
[0.0.28]: https://github.com/aallan/vera/compare/v0.0.27...v0.0.28
[0.0.27]: https://github.com/aallan/vera/compare/v0.0.26...v0.0.27
[0.0.26]: https://github.com/aallan/vera/compare/v0.0.25...v0.0.26
[0.0.25]: https://github.com/aallan/vera/compare/v0.0.24...v0.0.25
[0.0.24]: https://github.com/aallan/vera/compare/v0.0.23...v0.0.24
[0.0.23]: https://github.com/aallan/vera/compare/v0.0.22...v0.0.23
[0.0.22]: https://github.com/aallan/vera/compare/v0.0.21...v0.0.22
[0.0.21]: https://github.com/aallan/vera/compare/v0.0.20...v0.0.21
[0.0.20]: https://github.com/aallan/vera/compare/v0.0.19...v0.0.20
[0.0.19]: https://github.com/aallan/vera/compare/v0.0.18...v0.0.19
[0.0.18]: https://github.com/aallan/vera/compare/v0.0.17...v0.0.18
[0.0.17]: https://github.com/aallan/vera/compare/v0.0.16...v0.0.17
[0.0.16]: https://github.com/aallan/vera/compare/v0.0.15...v0.0.16
[0.0.15]: https://github.com/aallan/vera/compare/v0.0.14...v0.0.15
[0.0.14]: https://github.com/aallan/vera/compare/v0.0.13...v0.0.14
[0.0.13]: https://github.com/aallan/vera/compare/v0.0.12...v0.0.13
[0.0.12]: https://github.com/aallan/vera/compare/v0.0.11...v0.0.12
[0.0.11]: https://github.com/aallan/vera/compare/v0.0.10...v0.0.11
[0.0.10]: https://github.com/aallan/vera/compare/v0.0.9...v0.0.10
[0.0.9]: https://github.com/aallan/vera/compare/v0.0.8...v0.0.9
[0.0.8]: https://github.com/aallan/vera/compare/v0.0.7...v0.0.8
[0.0.7]: https://github.com/aallan/vera/compare/v0.0.6...v0.0.7
[0.0.6]: https://github.com/aallan/vera/compare/v0.0.5...v0.0.6
[0.0.5]: https://github.com/aallan/vera/compare/v0.0.4...v0.0.5
[0.0.4]: https://github.com/aallan/vera/compare/v0.0.3...v0.0.4
[0.0.3]: https://github.com/aallan/vera/compare/v0.0.2...v0.0.3
[0.0.2]: https://github.com/aallan/vera/compare/v0.0.1...v0.0.2
[0.0.1]: https://github.com/aallan/vera/releases/tag/v0.0.1
