# 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] ### Added - **`examples/ephemeris.vera` — the first floating-point example** ([#143](https://github.com/aallan/vera/issues/143)). Nine stages from a calendar date to a formatted right ascension: the apparent positions of the Sun and Mars for 2027 February 20.0 TT, from JPL's low-precision Keplerian elements. It is also the first example whose contracts do not all discharge at Tier 1, and that split is the point — 47 of 49 obligations prove, and the two that do not are `vec_norm` and `declination`, both `ensures` clauses standing on the far side of a `sqrt` or an `asin`, where no solver budget reaches them. `right_ascension` asserts the same shape of range claim and proves, because its value returns through `wrap_deg` whose own range is proved; nothing about the difficulty of the mathematics separates them, only whether a transcendental intervenes. The eccentricity bound is established where the value is built — `earth_elements` and `mars_elements` guard it with exactly the `Ecc` predicate, so the then-branch discharges by path condition and the else-branch by constant folding. An earlier draft inherited that bound from refined `Julian` / `Century` types and carried it through a division chain instead: correct, but it cost 9–11 s against the 10 s default budget, which made the reported tier a property of machine load. Three comparisons replaced the chain, the date types went back to plain aliases, and the file now verifies in under a second. The slot discipline is the other half: dense numerical code is where De Bruijn indexing hurts most, so every physical quantity gets its own type alias and therefore its own index space — `rot_x` takes five parameters and reaches all five at `.0`. `tests/test_examples_ephemeris.py` pins the rendered stdout byte-exact and both geocentric distances, then re-derives the angles from that output and checks them against an INDEPENDENT reference, ERFA's analytic model via astropy, agreeing to 19 arcsec for the Sun and 5 for Mars — a byte-exact pin alone would accept a wrong model rendered consistently. - **The Z3 budget is configurable, so a verification tier stops depending on the machine** ([#1350](https://github.com/aallan/vera/issues/1350)). The per-query budget was hardcoded at 10 s, which quietly made the TIER of a borderline obligation a property of host speed: `ephemeris.vera` originally had a bound proving in roughly 9–11 s, so it discharged at Tier 1 on a cold fast run and fell to Tier 3 after a single prior verification in the same process — the corpus-wide pin measured 413 statically one way and 412 the other. (That example was subsequently reshaped to bound the value at construction, so it no longer sits near the budget; the knob is what made the problem legible in the first place.) `vera verify --timeout-ms N` sets it for a run and `VERA_Z3_TIMEOUT_MS` for an environment, with an explicit `timeout_ms=` argument taking precedence over both; resolution happens at the seams that construct a solver (`verify()`, `ContractVerifier`, and the LSP's `VerificationSession`), so the variable reaches `vera verify`, `vera test` and the language server, while `vera compile` and `vera run` never build one and are unaffected. A malformed budget is a loud error rather than a silent fall back to the default, which would be indistinguishable from the sensitivity the knob removes. `vera verify --json` now reports the effective `timeout_ms` in its `verification` summary, so a measurement says which budget produced it. The practical use is diagnostic: raise the budget and a Tier 3 that becomes Tier 1 only needed more time, while one that stays is standing behind something the solver cannot see through at all. - **A generated implementation-status appendix, and one vocabulary for obligation outcomes** ([#1341](https://github.com/aallan/vera/issues/1341)). The specification already marked every gap between what it describes and what the reference compiler does with a `Status:` callout, but those callouts were scattered across chapters and the boundary they describe had never been readable in one place. `docs/implementation-status.md` is now that place — generated by `build_impl_status()` in `scripts/build_site.py`, indexed from `llms.txt`, and gated by `scripts/check_site_assets.py`. Generating it rather than writing it was the whole point: a hand-maintained index of what is not implemented yet is the first page to go stale, and stale here reads as "no gap". A callout added to a chapter reaches the page without anyone remembering to copy it, and one removed disappears. The scanner matches both spellings the spec actually uses — the blockquote form and the bare paragraph in Chapter 13 — because matching only the first would have silently dropped the WASI chapter; a test asserts count parity against an independent scan of `spec/` so that failure mode cannot return. Alongside it, new §6.8.1 maps the words Chapter 6 uses for obligation outcomes — proved, runtime-guarded, unguarded, refuted, assumed, tested, specified-but-not-implemented — onto the `status` values `vera verify --json` reports, and states the partition `total == tier1_verified + tier3_runtime` explicitly, so a summary counting two obligations beside a three-entry array reads as the partition it is rather than a disagreement. ### Fixed - **A `Tuple` nested inside a constructor no longer crashes the verifier, and `verify --json` always emits an envelope** ([#1360](https://github.com/aallan/vera/issues/1360)). `let @Option> = Some(Tuple(@Nat.0, 1234));` is `vera check`-green and killed `vera verify` with a raw `z3.z3types.Z3Exception: Sort mismatch` — a Python traceback, exit 1 — while `vera compile` and `vera run` handled it fine; under `--json` the process emitted **no envelope at all**, so a machine consumer got empty stdout where it expects a diagnostic object. The two sorts are derived by different routes and disagreed. A nested `Tuple` argument is built by the variadic-tuple branch of `_translate_ctor_call`, which keys its synthesised sort on the arguments' Z3 sorts — and `Nat` reads back as `Int`, both being one `IntSort` — so it always spells `Int`; the enclosing constructor's sort comes from `_resolve_pinned_sort`, which prefers a cached instantiation equal to the pin *modulo* `Nat`/`Int`. That preference is sound at a SCALAR position, where the two spellings share a Z3 sort, and unsound at a position that is itself a datatype: since #884 `Tuple` and `Tuple` are distinct injective sorts, so the constructor's domain named one and the argument term was built as the other. The `Nat` appears even in an all-`Int` program, because a positive literal types as `Nat` on the declared side — which is why the trigger is a `Tuple` NESTED in a constructor rather than anything about `Nat`, and why same-ADT nesting (`Some(Some(...))`) was unaffected. Applying a resolved constructor now asks first whether it can take the arguments that were actually built, preferring the instantiation those arguments pin and otherwise declining to translate — the Tier-3 demotion every other refusal on that path already means. Independently of that repair, `vera check --json`, `vera verify --json` and `vera test --json` now emit a parseable envelope on every exit path: an exception escaping any of the three becomes an `E699` internal-compiler-error diagnostic instead of empty stdout, so a crash is distinguishable from a clean run and the next translator bug is a diagnostic rather than a traceback. The envelope is a real `Diagnostic` built through the error registry, so it carries the same fields spec §0.5.1 requires of every other diagnostic — including the `spec_ref` and the file on its location — and formats identically on the text path. Two routes that bypassed it are closed with it: `verify`'s function-scope imports moved inside the guarded region, since a broken `z3` wheel raises there and produced exactly the empty stdout the envelope exists to eliminate, and the `FileNotFoundError` / `VeraError` handlers are now themselves inside its reach, a handler failing mid-report being the same silence as no handler at all. No obligation status, summary count, diagnostic or warning moves anywhere in the corpus — by construction, since a program that crashed `vera verify` could not have been in it. - **A `@Nat` component narrowed at construction is obligated, not assumed** ([#1332](https://github.com/aallan/vera/issues/1332)). `let @Tuple = Tuple(@Int.0, 5);` followed by a destructuring `match` verified as **proved** — Tier 1, zero diagnostics — while the compiled program trapped on `-1`, the #392-class false Tier-1 whose guard fires; the identical narrowing in return position reported it as `violated`/E503 all along. The obligation discharged itself. Translating the body's `match` emits, for each `@Nat` sub-pattern binder, the fact "this component is `>= 0`" from the scrutinee's DECLARED type; a single irrefutable arm has no preceding conditions, so `_translate_match` asserted that fact unconditionally at the solver's base level, visible to every later obligation including ones at program points BEFORE the match. The let-bound scrutinee's Z3 term is literally `Tuple(@Int.0, 5)`, so the datatype accessor axiom reduced the fact to `@Int.0 >= 0` — exactly the goal the construction site had to prove. That fact holds at run time only because codegen plants a trapping guard at the destructure, so assuming it to prove the guard unreachable assumes the conclusion. The anti-circularity test already existed — no facts for a literal-constructor scrutinee, "concrete args, not accessors" — but it was SYNTACTIC, asking whether the scrutinee's AST is a `ConstructorCall`, where a `let`-bound tuple arrives as a slot reference whose TERM is one; it now asks the question of the term, and of either arm of a branch. The test is about PROVENANCE rather than spelling, which is what a syntactic reading kept getting wrong: a value produced by a call or handed in as a parameter carries facts something other than the obligation under proof established — its own construction obligation was discharged in the callee's context — while a value constructed in this body does not. An `if` producing the tuple laundered past a term-level test that asked only "is this literally `C(args)`", verifying both narrowings while the compiled program trapped, and a `match`-produced spelling did the same; the suite is now parametrised over how the scrutinee is PRODUCED — bare constructor, either branch form, `match`-produced, let-of-let — in both families, beside call-produced and opaque-parameter controls that must keep their facts. Nothing is lost: whatever establishes the construction establishes the component. The same change repairs the REFINED sibling, which was worse in kind — a refined tuple component carries no runtime guard at all, so `type PosInt = { @Int | @Int.0 > 0 };` with the same shape verified clean and then RETURNED `-7`, a wrong answer rather than a trap; it is now E505. Measured across the whole corpus at the default budget, no obligation status, summary count, diagnostic or warning moves on any of the 244 conformance programs or 43 examples, while the new guard fires 13 times in three of them — the corpus exercises the change and no verdict moves. - **`check_limitations_sync.py --check-states` reads a table's Issue column, not its prose** ([#1337](https://github.com/aallan/vera/issues/1337)). The scan had been failing the nightly on eight issues closed in v0.1.12 and cited only in row PROSE — "the general disease behind #1315", "fixed in #1305" — because it collected every issue link in a table row where the one-to-one contract binds the row's **Issue** column. A context citation was therefore read as a claim the issue is still open. The state scan now reads the Issue column; the presence checks keep the wide reading, because there an issue named anywhere in a row genuinely is tracked by that row, and narrowing both would have silently dropped cross-reference coverage. Default-mode output is byte-identical. All eight citations stay in the file — they were never the problem. - **Public claims narrowed to the assurance the compiler actually delivers** ([#1341](https://github.com/aallan/vera/issues/1341)). An external review checked what the project says about itself against what is implemented; five claims did not survive. The landing page and its generator described the loop as proving *every* contract, where the compiler type-checks every program, proves supported contract obligations via Z3, guards most of the rest at runtime, and discloses what it can neither prove nor guard. The third arm is not padding: `tier3_unguarded` obligations genuinely exist — an `E531` widening site compiles to no guard at all, and `vera run` on `u64.MAX` returns `-1` rather than trapping — so a two-state "proved or guarded" partition is false, and the sentence now matches the accounting §6.8.1 tabulates. Three narrower repairs follow the same rule: `invariant` left the "full contracts" list, which had advertised it as mandatory on every function; the single-binary claim became what `vera compile --target` actually emits: one core module that both wasmtime and the browser runtime execute — the parity suite runs identical bytes under each — and a separate WASI 0.2 component; and DESIGN.md stopped calling `Inference` handlers "mockable" for a capability still open as [#372](https://github.com/aallan/vera/issues/372). The largest was Design Goal 3. "Every construct has exactly one textual representation" is refuted by any construct carrying a documented alternative, so the goal is restated as what `vera fmt` genuinely guarantees — one preferred surface spelling per construct, and formatting that is deterministic and idempotent for a given parse (`fmt(fmt(p)) == fmt(p)`) — a claim the formatter's own tests already establish, rather than one the language cannot meet. Finally, Chapter 6's `safe_divide` prose said the compiler verifies that the *second* argument is non-zero, where `@Int.1` under most-recent-first indexing is the *first* parameter: a De Bruijn error in a chapter that teaches De Bruijn. A sweep of every other argument-position claim in `spec/` found no siblings. The same sentence turned up in three places and went the same way: "Division by zero is not a runtime error — it is a type error" had already been retired from `README.md` for overclaiming and survived in the generator's copy and in `EXAMPLES.md`, and the `requires` clause was credited with turning a crash into a compile error when in fact the compile error (`E526`, with a counterexample) is what happens *without* it — the clause is what discharges the obligation, and only a divisor the verifier can neither prove non-zero nor witness a zero for falls to a runtime guard. - **A module generic instantiated from an effect-operation result now registers its clone** ([#1310](https://github.com/aallan/vera/issues/1310)). `idg(get(()))` inside `handle[State]`, where `idg` is a `forall` generic declared (public or private) in an imported module, checked and verified clean and then compiled with `[E602]`/`[E620]` compilation notes and no `main` in the emitted module: the WASM call-rewrite (`CallsMixin._resolve_generic_call`), which runs with the real handler context, correctly named `mod$mlib$idg$Int`, but instantiation discovery for a module's QUALIFIED-ONLY ("shadowed") generics (`_collect_shadowed_qualified_calls` in codegen and its mirror `walk_seed` in the verifier's `_collect_shadowed_qualified_instances`, #732) had no `handle[State]` op-result registry at all, unlike the unshadowed discovery walk #1207 already gave one. `get(())`'s type therefore fell through to the phantom-type-variable `Bool` default, so `mod$mlib$idg$Bool` was the clone actually emitted and verified: a wasted, unreachable instantiation standing in for the one the call site needed. Both walks now thread the same merge-over-the-enclosing-scope `HandleExpr` handling `Monomorphizer._collect_calls` uses, so a nested handler's own cell still wins for its own body while an outer one still answers outside it. `tests/test_module_shadowed_generic_effect_op_1310.py` pins the issue's own repro end to end (no E602/E620, `mod$mlib5$idg$Int` in the emitted WAT, and the checker's own run value) plus a nested-distinct-state cell that would still pass a fix that stopped defaulting to `Bool` without preserving merge, not replace, semantics. ## [0.1.13] - 2026-08-21 ### Fixed - **`Inference.complete` reads a provider's response by shape, not by position** ([#1333](https://github.com/aallan/vera/issues/1333)). `examples/inference.vera` printed `'text'` and exited 1 against the Anthropic flagship. The Messages API returns `content` as a list of TYPED blocks and a reasoning-capable model leads with a `thinking` block, so `_call_inference_provider`'s `data["content"][0]["text"]` landed on a block with no `"text"` key; the resulting `KeyError('text')` reached the host boundary's blanket `except Exception`, and `str(exc)` published the bare missing key as the entire `Result::Err` payload — a message naming neither the operation, the provider, nor the model. The maintainer's six-provider sweep found five healthy (`openai`, `moonshot`, `mistral`, `xai` and `deepseek` all take the OpenAI-style branch, which carries the file's only other response parse) and initially read the failure as a SECOND affected provider: with `VERA_INFERENCE_PROVIDER` unset, auto-detect takes the first key set to a non-empty value in registry insertion order, so a still-exported `VERA_ANTHROPIC_API_KEY` won the "xAI run" and the Anthropic parse failed under another provider's name. Three repairs, one per defect. **Selection by type**: the Anthropic branch collects every block whose `type` is `"text"`, in order, and joins them, so a leading `thinking` or `tool_use` block is skipped rather than mis-read. The OpenAI-compatible branch accepts `output_text` beside `text` as a part discriminator, in PREFERENCE order rather than as a union — a gateway that mirrors the same reply under both spellings had every fragment counted twice and returned `PositivePositive`, a wrong answer delivered as a success, so the first discriminator that yields TEXT wins and the other is not consulted. A hit is a non-empty result, not a non-empty list: treating `[""]` as one let an empty `text` part shadow a real `output_text` part and return `Ok("")` in either order, while a list whose every part is empty still returns the empty completion it is. Spec 9.5.5 states the preference; the sentence there described joining both, which a review pass caught — Responses-API-shaped gateways spell it that way, and they worked on v0.1.12 because the old code read `content` positionally and never looked at `type` at all, so selecting by type alone regressed them — and surfaces `message.refusal` when the model declined, which is the answer where the shape of the empty `content` beside it is only the symptom — including when that content is an empty or whitespace-only string, where `Ok("")` told the caller the model said nothing when it had said why it would not. The rule is symmetric across the branches and covers both signals each spells: an empty or whitespace-only completion is an error when the provider marked the turn a refusal (`stop_reason: refusal`, or `message.refusal` present) or a truncation (`stop_reason: max_tokens`, `finish_reason: length`) — the latter being #1333's own species, a thinking block exhausting the budget before any text was emitted. The reason is matched case-insensitively — every registered provider emits these lowercase today, but an exact match sent a normalising gateway's `MAX_TOKENS` straight to `Ok("")`, losing the answer over a spelling difference — while the diagnostic carries the token exactly as received, so `stop_reason=refusal` and `finish_reason=length` are greppable and a consumer is never told the provider said something it did not. An empty completion under any other reason, or none, stays `Ok("")` exactly as in v0.1.12: a model may legitimately answer with nothing, and the review's wider proposal to treat every empty completion as missing is declined as a behaviour change unrelated to #1333. A non-empty reply is returned unchanged whatever the reason. The blank test is `.strip()` throughout: the list path tested truthiness while the check downstream tested `.strip()`, so a whitespace-only `text` part short-circuited the loop — shadowing a real `output_text` part and skipping the refusal check that the string form applied, which is why round 9's "string and list forms" held for the list form only when the fragment was exactly `""`. A selected block's `text` must itself be a string — the PR review found the same silent-wrong-answer class one level deeper, where `{"type": "text", "text": null}` was coerced by `str()` into the successful completion `"None"`, a number into its digits, and an object into a Python repr; a non-string `text` now names its own type, as does a selected block carrying NO `text` field. Both refuse, on both branches. (Before this release the key-less block was instead skipped, so `[{"type": "text"}, {"type": "text", "text": "Positive"}]` returned `Ok("Positive")` while the same pair with `null` refused — two malformed shapes treated differently for no reason a caller could see. Blocks of a type other than the selected one, such as `thinking` or `tool_use`, are still skipped, by design — but if that leaves no block or part of the selected type at all, the response is an `Err` naming the types that WERE present and, when the provider sent one, its reason, which spec 9.5.5 now states beside the skip rule rather than leaving to be inferred from it.) Either way the refusal wins over any salvageable block after it, because joining the remainder would return a completion the provider never sent as a whole with no way for the caller to tell it was short. The OpenAI-style branch gets the same treatment, because `message.content` is a string on an ordinary turn, a list of typed parts on some multimodal ones, and `null` on a reasoning or tool-call turn — where the old `str(...)` returned the literal completion `"None"`, a silent wrong answer rather than merely a bad message. **Named failures**: every shape failure reports the provider, the model that answered, and the block or part types it actually saw — and, for the Anthropic no-text case, the response's `stop_reason` (`content block types: thinking; stop_reason=max_tokens`), which separates a reply truncated at the request's token budget from a model that simply said nothing — all of which is what makes the sweep's misattribution class impossible to repeat; a `urllib` `HTTPError` — which escaped as the status line `HTTP Error 401: Unauthorized`, naming neither the provider nor the reason it gave — now reads the error body and quotes the provider's own `error.message`, falling back to the raw text truncated so a proxy's HTML page cannot become a Vera value. EVERY provider-supplied fragment an `Err` quotes is redacted before it is surfaced — the rejection body, a 200 body that is not JSON, a `stop_reason` or `finish_reason`, a refusal, and the key and type names a shape report lists. A review pass found the non-JSON body building its message with no redaction at all; sweeping every message-building site for provider text that bypassed the rule found six more, so the redaction and the 200-character bound now share one helper and the rule is a property of the module rather than of seven call sites. The configured key is matched literally and unconditionally, with no minimum length: a pathologically short key therefore also redacts incidental text (with the key `a`, `stop_reason=max_tokens` renders `stop_reason=m[redacted]x_tokens`). That is deliberate: a length floor would stop redacting short REAL tokens, which gateways and proxies do issue, and redaction does not trade coverage for tidiness. The configured API key and any credential-shaped token (`sk-`, `sk_`, `key-`, `key_`, `token-`, `token_`, `xai-`, `xai_` followed by eight or more `[A-Za-z0-9_-]`) become `[redacted]`. The `xai` prefix was uncovered until a review pass caught it, so an `xai-…` token echoed by a gateway — one that is NOT the configured key, and so invisible to the exact-match rule — reached the `Err` intact. The pattern cannot cover every provider and is not meant to: Mistral issues a bare alphanumeric key with no prefix, which only the configured-key rule can catch, and the two rules are complements rather than alternatives. because providers quote the key they rejected (`Incorrect API key provided: sk-…`) and an `Err` is a value the program prints, logs, or ships onward — the fix that surfaced the provider's message is what created the exposure. Bounding is not allowed to become deleting: the truncation window is taken from the first non-space character rather than from position 0, since a window anchored at the front was an implicit bet on how much leading whitespace a body would carry, and 900 spaces before real text filled it entirely and rendered the value as the empty string. A provider-supplied value that is present but renders to nothing now says `(blank)` rather than trailing off — a whitespace-only `stop_reason` produced the dangling `; stop_reason=).` and a whitespace-only body left the `is not JSON:` clause with nothing after the colon. `"(no keys)"` likewise now means there were none, rather than being derived from a rendered string — an object whose only key was 900 spaces was reported as having no keys at all, which is not a truncated statement but a false one. Every other interpolated field is bounded too: `stop_reason`, `finish_reason`, the block types and the response keys all pass through the 200-character limit, where a 64 MB `stop_reason` previously produced a 67 MB `Err`. Shape messages no longer contradict themselves — absent and present-but-wrong-typed read differently, so `{"content": "Positive"}` reports `'content' is str, not a list` instead of naming, as a key it had, the key it had just called missing; `stop_reason` is reported on every Anthropic failure branch rather than one; and a choice with no `message` says so instead of describing the keys of nothing. That read is bounded at 64 KiB rather than unbounded — the 200-character message limit bounded what was printed, never what was held, so a hostile or misconfigured endpoint answering a rejection with megabytes cost that much resident memory to produce a 200-character string. One byte past the cap is requested so an overrun stays distinguishable from an exact fit, and an overrun body is reported as marked raw text rather than parsed: `json.loads` rejects most cut bodies by itself, but a short envelope padded past the cap with whitespace parses cleanly, and having read part of a body we do not claim to have parsed it. The read is also guarded — a socket already closed raises rather than returning bytes, and losing the detail is a far smaller loss than losing the provider name and status code to a second, unrelated exception; and a 200 whose body is not JSON is named the same way. **A boundary that labels what it did not write**: an `InferenceError` — this module's own class, raised at every site that has something to say — passes through verbatim, and every other exception, a plain `RuntimeError` included, becomes `Inference provider '' () failed: : `, so a future shape surprise can never again surface as a bare quoted key. The verbatim channel belongs to a dedicated `InferenceError` and nothing else. The rule was first written as "a plain `RuntimeError` or `ValueError`, by exact type", which the PR review refuted: those are the types an unforeseen failure raises too, so a `RuntimeError("boom")` from anywhere below — the transport, a dependency, a later edit — claimed the channel and reached the user as the single word `boom`, which is this defect one level up. A private class cannot be raised by accident, so every deliberate site in the module now raises it, including the registry's unknown-provider refusal (previously a `ValueError`, and the one test pinning that type moved with it). `isinstance` is safe again as a result — the hazard that forced exactness was the standard library subclassing the types being checked for, and nothing outside this module subclasses `InferenceError`. It extends `RuntimeError` and deliberately not also `ValueError`, because the module wraps `json.loads` in `except ValueError` twice and a subclass of both could be swallowed by its own handler after a later edit. `tests/test_inference_response_shapes_1333.py` pins all three across 164 cells, the headline ones END TO END through a compiled program over a mocked transport — the path the report came from — with the six-provider sweep parametrized in registry order and its rows checked equal to the provider registry, so a new provider cannot silently leave the sweep. Mutation-checked one edit at a time, every count re-measured on the tree as it now stands rather than carried forward — a review pass found two of the published figures stale and one of them self-contradicting, the same sentence quoting both 5 and 15 for one mutation. Restoring the by-position read fails 45 cells; the boundary's plain-type rule 18; removing redaction from the shared helper 28; dropping the boundary label 8, reproducing the reported `'text'`; restoring the `str()` coercion 8, every one with "DID NOT RAISE" — that is, by returning a completion; restoring the skip of a `text`-typed block with no `text` field 7; disabling the reason clause 21, the same figure whether its function body is emptied or all four call sites are neutralised (an earlier note claimed the two forms differed; they do not); collapsing the `output_text` preference to a single discriminator 6; removing the empty-completion rule from both branches 10; reverting the blank test to truthiness 4; dropping the reason's case fold 3; removing `xai` from the credential pattern 3; and restoring the strict `.decode("utf-8")` 3. The narrower guards fail their own cell or two: the redact-before-truncate order, the exact-key rule beside the pattern, the `output_text` preference, the truncation window's starting point, the "(no keys)" honesty rule, the credential pattern's eight-character floor — dropping it to one redacts `token-based`, `key-holder` and `xai-ish` alike — and each of the three bounded-read guards. That plain-type figure had read 5, and the explanation offered for the drop — that the cells added since exercise the parse rather than the boundary — was wrong. The real cause was that threading the model into the boundary label made the label a PREFIX of itself: under the mutation the Err reads `Inference provider 'anthropic' (claude-opus-5) failed: InferenceError: Inference provider 'anthropic' (claude-opus-5) returned no text block (…)`, which satisfies `startswith("Inference provider 'anthropic' (claude-opus-5)")` while being exactly the regression the cell exists to catch. Eleven cells lost their discrimination and nothing went red. They now assert the label's ABSENCE beside the prefix, through one shared helper so the next cell cannot forget it, and the figure went back to 16 — rising with each `_assert_deliberate` cell added since, which is why the count above is higher. The browser runtime is unaffected: `Inference.complete` there returns a deliberate, explanatory `Err` and never calls a provider. ## [0.1.12] - 2026-08-15 ### Added - **The examples are now RUN in CI, not only checked, verified and compiled** (`scripts/check_examples_run.py`). `check_examples.py` type-checks and verifies all 42, and `check_e602_clean.py` compiles all 42 as a side effect of policing silent translator skips — but nothing executed them as a set, and an audit of every referencing test found **seventeen examples that no test ran at all**: `array_utilities`, `async_http_fanout`, `collections`, `database`, `fizzbuzz`, `html`, `http`, `inference`, `io_operations`, `json`, `life`, `maximum_syntax`, `modules`, `nested_closures`, `read_char`, `scoreboard` and `string_utilities`, plus `file_io`, which ran only under the browser runtime, where the file IO it demonstrates is a deliberate `Err` stub. Between them they demonstrate `Map`/`Set`, the `` effect, JSON and HTML parsing, module imports and the whole string-utility family, so a runtime regression in any of it could reach a release with every gate green. The gate now runs 34 of the 42 under the native runtime and asserts a trap-free exit; the other 8 carry a documented skip property (`network`, `api-key`, `stdin`, `non-scalar-entry`, `long-running`) which the report prints with its reason on every run. Output pinning deliberately stays in the dedicated tests that already do it, so the gate does not go red on a cosmetic edit to an example. The load-bearing part is not the runs but the **coverage rule**: the script enumerates `examples/*.vera` from disk and requires every name to be in exactly one of its two tables, so an unclassified example is an error and adding one forces the author to decide whether the harness can drive it — and a table key whose file is gone is an error too, so a suppression cannot outlive its example and mask a later program of the same name. The classification is cross-checked against a new execution-coverage table in `TESTING.md` on the `check_doc_counts.py` model, the codebase being the oracle and the documentation having to match it, so the execution model stops living in maintainers' heads. Trap-freedom is asserted on two signals, the discipline `check_examples.py` already applies: the exit code, and an output signal. Either alone accepts a measured failure. Every spec names its entry point rather than relying on `vera run`'s first-export fallback — with `main` privatised, `array_utilities.vera` ran a different function and the gate passed; it now exits 1 on the name. And the three examples that reach outside the process (`sqlitedb.vera` for its committed fixture, `database.vera` for an in-memory database, `file_io.vera` for the filesystem) answer a failure by printing a message and completing normally, so each pins a substring only its success path prints — deleting `examples/sqlitedb.sqlite` left the gate green on the graceful in-memory arm, and now fails on the sentinel. Runs are hermetic: an ambient `VERA_DB_URL` or inference-provider key is stripped from the environment, so a gate run cannot be pointed at a real database or turned into a billed API request, and each example gets a scratch working directory so `file_io.vera` stops dropping `hello.txt` beside the sources. `TESTING.md`'s round-trip section is corrected with them — it claimed all 42 examples were tested through "every pipeline stage ... WASM compilation, and execution", where the directory-globbing parametrised tests in fact stop at verification and canonical form. - **The grammar-alignment gate now compares terminals and production bodies, not only rule names** ([#1290](https://github.com/aallan/vera/issues/1290)). `scripts/check_grammar_alignment.py` held rule-name headers together and was blind to three drift classes, each demonstrated green on a live file during #1279's review: a fabricated terminal added to spec 10.2 (the header pattern requires a lowercase lead, so no terminal was seen at all), a rule reference restored to a right-hand side, and a production body edited on one side only — the class most grammar edits actually fall into. Three checks close them. A **terminal audit in both directions, within each file**: a terminal declared and never referenced, or referenced and never declared, is now an error — the shapes `SOME`/`NONE`/`OK`/`ERR`/`COLON` and `DOUBLE_COLON` had between them, found by hand and fixed in #1279 with the gate itself unable to see either. A **cross-file terminal-pattern check**: every terminal the chapter publishes as a bare regex must have that pattern in `vera/grammar.lark`, as a named terminal or an `%ignore`, after a semantics-preserving normalisation of Lark's `\\/` and `\\"` escapes — which is the whole of the difference between how the two files spell `STRING_LIT` and `ANNOTATION_COMMENT`, and which `BLOCK_COMMENT` failed. And a **production-body comparison** over the 80 rules both files declare, of the rules and the terminals each right-hand side refers to, with Lark's quoted literals mapped through the chapter's own terminal table rather than a hand-written one. Two notational differences are folded rather than reported: a rule's reference to itself, since Lark spells repetition with left recursion where the chapter uses a Kleene star, and a waived spec-only production, which the existing `ALLOWLIST` already pins to the Lark rule that inlines it. The body comparison needs no waivers of its own, and the six-entry rule-name allowlist is unchanged. - **`KNOWN_ISSUES.md`'s Bugs table is gated one row per open `bug` issue.** The structural half is pure text and always on: each row's Issue column must hold exactly one `[#N](…/issues/N)` link whose number matches its URL, no two rows may claim one issue, and an empty section must be written `No known bugs.` rather than left as a bare table. The parity half needs the tracker, and a pre-commit hook must not depend on a network call, so it is opt-in through `scripts/check_doc_counts.py --check-bug-issues` for the release PR — mid-burndown the two legitimately disagree, a bug filed against an open PR's branch having an issue before it has a row. - **TESTING.md's dual-target conformance row is gated against the manifest and a live run.** The row states a run-level total, a tested/skipped split and three category counts, and claims the excluded set "stays accurate as programs are added" — a claim nothing measured. The total now comes from the conformance manifest and the rest from a three-second `-rs` run of the differential itself, with two arithmetic checks the individual figures cannot make: tested plus skipped must be the run-level total, and the three categories must be the skip total. A skip whose reason matches none of the three documented properties fails rather than being folded into one of them. - **`check_examples_run.py` derives which examples need an output sentinel instead of naming them.** The rule was a hard-coded triple — `database`, `file_io`, `sqlitedb` — so a fourth example reaching outside the process could be added with nothing but an exit code asserted, exactly the gap the sentinel exists to close. The set now comes from each program's own declarations: a resource effect in a function's effect row, or a call to a resource operation, read off the parsed AST rather than the source text so a header comment mentioning `` is prose. Both halves are needed, and the measurement said so: `FileIO` and `Time` are not effects in this language — file and clock operations live under `IO` — so `file_io.vera` declares exactly the bare `` that `hello_world.vera` does, and only the operation it calls separates them. What stays hand-written is a short list of registry *names*, and those are validated against the live effect registry, so a renamed or deleted effect or operation fails loudly rather than silently matching no example. The derived set must equal the specs carrying a sentinel in both directions, so a sentinel on an example with no resource signal is an error too. - **The corpus differential and the grammar gate are hardened against the platform they run on and the patterns they read** (PR #1329 review). `_first_error` stripped the compiled file's path from a diagnostic by matching `str(path)` alone, which ties the strip to the host's separator: on Windows a diagnostic carrying the POSIX spelling went unstripped and its absolute path pushed the message past the truncation. Both spellings are stripped now, and the parameter is a `PurePath` so a test can render a Windows path on any host rather than waiting for the Windows CI cell. The grammar gate's comment scanner had the same shape of defect with worse consequences: a `/` inside a regex character class was read as the closing delimiter, so the chapter's `ANNOTATION_COMMENT` — which spells the class `[^/*]` where the Lark grammar escapes it `[^\/*]` — was truncated, and a truncated body is not a bare regex, so the terminal was **skipped from the pattern comparison entirely**. That gate was green on it by never looking. Both now have cells that fail on any host. Alongside them: the differential rejects a non-positive `--timeout` (which would fail every compile and report "no movers" over a corpus that never compiled), decodes compiler output leniently (a stray byte otherwise raised out of `subprocess.run` and aborted the whole run), checks the base revision out repository-locally rather than under a predictable shared temporary path whose contents it puts on `PYTHONPATH`, and prints a reproduction command that names the same input it actually compared. `check_doc_counts.py` reads a pytest summary that omits a zero-count category, and its two external calls — the dual-target run and the tracker query — join the script's own error convention instead of ending the run on a traceback. The chapter's `BLOCK_COMMENT` production excludes both delimiters from its character alternative, so `{- {- -}` is no longer derivable from a rule describing a construct the implementation rejects as unterminated. - **`scripts/check_corpus_differential.py`** promotes the burndown's ad-hoc corpus differential to a first-class instrument: it compiles every corpus program at two revisions and reports the movers, including the programs that compile on one side only. It is deliberately not a pre-commit hook — it compiles the whole corpus twice — and is documented as a CI-optional burndown instrument. ### Fixed - **Spec §1.4's reserved-keyword MUST is now enforced, for twenty-one names that nothing held** ([#1296](https://github.com/aallan/vera/issues/1296)). `§1.4` says its keywords must not be used as function names; `E153` held that for eleven of them, and `private fn with(@Int -> @Int)` — with `then`, `else`, `data`, `type`, `module`, `import`, `public`, `private`, `requires`, `ensures`, `invariant`, `decreases`, `effect`, `in`, `where` and `pure` — declared, type-checked, verified, compiled, ran and round-tripped `vera fmt`. They were not traps: a bare `with(1)` resolved to the declaration and returned its value, and stayed working inside a contract clause, inside an `if`/`then`/`else`, in a function carrying its own `where { }` block, and after a `let`. The comment above `_KEYWORD_FN_NAMES` gave the opposite as the reason they were absent from the set — that the contextual lexer "does not admit them as a function name, so no declaration reaches this checker at all" — so the omission rested on a premise the tree refuted, and the divergence was between the specification and the implementation rather than in any program's behaviour: a model trusting §1.4 and a model trusting the compiler derived different programs from one source of truth, with no tool contradicting either. DESIGN principle 1 (checkability) makes an unenforced MUST a defect whatever the program does at runtime, principle 6 (fewer valid programs) chooses enforcement over narrowing §1.4, and principle 3 supplies the precedent — `E152` rejects even a *faithful* re-declaration of a built-in effect, because a second textual spelling is itself the problem. The reserved set is now **derived from `vera/grammar.lark`** rather than hand-listed, the shape `builtin_effect_names()` already uses for `E152`, so a keyword added to the grammar is reserved the moment it is added; the hand-list this replaces had fallen twenty-one names behind the grammar with no gate able to see the drift. The derivation is what found the other four: `ability`, `effects`, `op` and `result` are grammar keywords §1.4 never listed and were accepted as function names on the same footing, and §1.4's list is reconciled to the grammar (gaining those four plus `old` and `new`, which `E153` already reserved). They join `E153` as a **fourth** branch with its own rationale: the existing keyword wording asserts that no call site can reach the declaration, which is false for every one of these names, so reusing it would have told authors a falsehood about their own program — the new branch argues from the reservation instead, and carries a per-name rename suggestion because the generic `_fn` template produces `in_fn` / `type_fn` / `pure_fn`. `handle` stays legal, carved out as the host-invoked `vera serve` / `wasi:http` entry point; the reservation remains on the whole identifier, so `older`, `with_it` and `then_value` are ordinary names. §1.4's "type names" half is corrected rather than enforced: every type-namespace binder in the grammar is an `UPPER_IDENT` and every keyword is lowercase, so that half was never violable. New conformance negative `ch05_reserved_contextual_keyword_fn_rejected` plus 110 tests in `tests/test_checker_modules.py` — five parametrized batteries over all 21 (declaration, visibility, `where`-helper, rationale-free-of-the-false-claim, and a usable fix suggestion) with `handle` and fifteen keyword-containing names as controls; mutation-validated by dropping one keyword from the derivation, which flips that name's five cells and both set pins red while the other twenty stay green. Corpus differential: zero movers, no program in `examples/` or `tests/conformance/` having used such a name. - **Two imports supplying one bare name are refused, in every namespace** ([#1304](https://github.com/aallan/vera/issues/1304)). Spec §8.5 ordered a local declaration against an import (§8.5.2) and gave the module-qualified form for reaching what a clash hides (§8.5.3), but defined no order between two *imports* that both supply one name. Neither did the implementation, and the gap was reachable: a module importing two dependencies that each export `forall fn gen` — one returning `@Int`, one `@Bool` — bound its bare call to whichever supplier a set of module paths happened to yield first, so one unchanged file was `vera check`-green on one run and `[E121] body has type Bool` on the next. Measured at the branch point across eight consecutive runs and eight hash seeds: accepted on seeds 0, 2 and 3, rejected on 1, 4, 5, 6 and 7, with the winner tracking module-name hash order rather than which import is written first. Codegen's E608 rail caught the *entry-visible* pair before it could matter there; the flap lived in the shapes the rail only reached at compile, from inside a module the entry program merely imports. Spec §8.5.2.2 now states the rule — a program **MUST NOT** leave a namespace with two imports supplying one bare function name — and the checker enforces it as **E155**, a check-phase code for a scope question that had been enforced by a codegen rail at the wrong layer. Refusing is what removes the flap rather than merely labelling it: with no pick to make, there is no iteration order left to expose, which a deterministic first-wins order would not have achieved (it would make the resolved declaration implicit in import sequence, §0.2.2, and let a library *adding* an export silently rebind a downstream bare call). The refusal is **definition-gated**, matching the rail it generalises: it fires because the import pair exists, not because a body names it, so an entry program importing two suppliers and never calling either is refused exactly as E608 already refused it, and rewriting a bare call in module-qualified form does not lift it. Two shapes clear it, both exercised through to their runtime value: a **local declaration** of the name (§8.5.2 — every bare call is then the local one, and each import stays reachable through `dep::name(...)`), or a **selective import** narrowing the other module's list. The ambiguity predicate is the one `namespace_fn_names` already derived for #1281 and #1299, now exposed per namespace as well as unioned, so the layer that refuses early and the layer that backstops it cannot disagree about which shape is ambiguous; the E608 condition keeps its cell, driven through a door that bypasses the checker. An ambiguous name is not injected into the type environment at all — reporting the clash while binding one supplier would leave the follow-on diagnostics keyed to whichever module the injection loop reached first, which is the nondeterminism the refusal exists to remove — so a bare call to it misses with an ordinary `E200` instead — an E-coded diagnostic emitted at **warning** severity, which the `--json` envelope reports in `warnings` rather than `diagnostics` and which does not fail the check on its own (measured: a program whose only diagnostic is `E200` reports `ok: true` and exits 0). The W-series is the separate `W001`/`W002` code namespace, and this is not one of them. **The data namespaces flapped the same way and are folded in.** Spec §8.5.4 gives constructor names the same shadowing rules as function names, which a function-only refusal would have made false: two modules each exporting a `public data Shape` with different constructor field types type-checked on some hash seeds and reported `[E213]` on others (accepted on seeds 2, 8, 9, 10 and 11; rejected on 0, 1, 3, 4, 5, 6 and 7), and the accepting seeds were the worse half — `check` **and** `verify` both passed, and the program died at `run` with an `E609` located at line 0 of the entry file, naming two modules the entry never imported. Data types are now **E156** and constructors **E157**, one code per declaration namespace exactly as codegen splits E608/E609/E610, and reported independently because they come apart: two modules exporting differently-named types that share a constructor name clash on the constructor alone. Their remedy differs from the function one and says so — E609/E610 refuse two modules' same-named data declarations by DECLARATION, consulting neither visibility nor the importer's filter nor local shadowing (the relaxation E608 received in [#1281](https://github.com/aallan/vera/issues/1281) has no data-side twin), so narrowing an import or shadowing the name locally leaves the program `E609` at compile. Both were measured against the fixture and both fail — as does marking one declaration `private` — so the two diagnostics prescribe renaming, and a cell pins that measurement so the fix text cannot drift into offering remedies that do not work. That rail over-breadth is now tracked as [#1317](https://github.com/aallan/vera/issues/1317). **A name the built-in registry already owns is not a clash** — the injection loops are `setdefault` over a `TypeEnv` the built-ins populate first, so a dependency exporting its own `option_map` never wins the bare name (measured as `E201` against the *prelude's* two-argument signature). The first cut of this refusal did not pass the built-in snapshot and reported two such dependencies as a clash, which was a new rejection rather than an earlier one; `namespace_fn_names`' claim that its ambiguity half is identical with or without the prelude argument was wrong for the same reason and is corrected, with the codegen call ordering it depends on now pinned by a cell. - **A `throw` payload is runtime-guarded, not only obligated** ([#1268](https://github.com/aallan/vera/issues/1268)). `throw(v)` narrows `v` into the `Exn` payload, and since the static half of this issue the narrowing carries the same obligation every other binding site does — but codegen emitted no guard, so the obligation's Tier-3 leg promised a runtime check that did not exist and an unverified `vera compile`/`run` delivered the violating value anyway. `throw(0 - 5)` under `effects(>)` ran to completion and returned **-5** through the `@Nat` payload; the refined spelling (`type Pos = { @Int | @Int.0 > 0 }`) did the same. Worse than a wrong answer: a handler clause binds the payload at its declared type, so the verifier hands every downstream consumer the invariant the payload just broke — a `@Nat`-taking function discharging `ensures(@Bool.result)` at Tier 1 from its parameter's type alone reported a **postcondition violation at run time on a postcondition `vera verify` had proved**. `throw` now takes the write boundary's guards at its op-call site, beside `put`'s ([#1203](https://github.com/aallan/vera/issues/1203)): the `@Int` -> `@Nat` sign guard, the `@Nat` -> `@Int` widening guard, and — refined FIRST, as at every other narrowing site — the §2.6.5 predicate guard for a refined payload, which traps through `$vera.contract_fail` naming the predicate that failed (`Refinement violation in throw(@Pos) / payload: @Int.0 > 0 failed`). The three arms mirror the verifier's own obligation triple one-for-one, so the obligation stream and the emitted guards stay in lock-step: the payload obligation is now `guarded` at all three arms and its Tier-3 leg is counted in `tier3_runtime` rather than disclosed as `tier3_unguarded`, and the refined arm's `guarded` claim is intersected with the same `_refined_boundary_codegen_guardable` test every other refined site uses, so an erased `@Unit` base or a nested refinement — which codegen emits no guard for — stays honestly unguarded. That mirror needed one repair to be true: it answered "guarded" for a refinement OVER a refinement, which `_refinement_guard_parts` refuses outright with a loud `E618` because the outer predicate alone would silently drop the inner membership — so `vera verify` exited 0 recording a Tier-3 runtime check for a program `vera compile` then refuses, a promise about a run that can never happen. It now bails on a refinement base, and the obligation discloses `tier3_unguarded` while `E618` still refuses. The same audit found the **qualified spelling recording something different from the bare one**: the `QualifiedCall` arm hardcoded `guarded=False` behind a comment stale since [#1203](https://github.com/aallan/vera/issues/1203), so `Exn.throw(v)` — which codegen lowers by synthesizing a bare node and delegating to the very dispatcher that emits the guards — disclosed `E504`/`E506` for a boundary that traps, and `State.put(v)` had been doing the same since #1203. Both now take the bare arm's rule on the same key (`op.parent_effect`), so the two spellings of one operation record identical statuses. The review of that fix found the arm had been hand-written as a refined-then-`@Nat` chain with **no widening branch at all**, so `State.put(@Nat.0)` / `Exn.throw(@Nat.0)` into an `@Int` cell recorded no obligation whatever while codegen emitted the `@Nat` -> `@Int` widening guard on both spellings — a guard the obligation stream never mentioned, the mirror image of the claim-without-a-guard this issue started from. It now routes through the shared `_obligate_binding_triple`, so the three arms cannot drift apart again by omission. The triple itself then turned out to be missing the [#820](https://github.com/aallan/vera/issues/820) INTERSECTION at these boundaries: its three arms are an `elif` chain, so a refinement OVER `@Int` claimed the value and the widening check never ran — and codegen mirrored that exactly, so both sides agreed to skip a check the UNREFINED spelling performs. A refinement predicate does not imply fit-in-i64, and `{ @Int | true }` is satisfied by the negative a `@Nat` above i64.MAX reinterprets to, so adding a refinement WEAKENED the boundary: `Exn` fed u64.MAX trapped on the widening guard while `Exn<{ @Int | true }>` fed the same value returned **-1**. The widening obligation and its guard now ride beside the refined pair rather than being replaced by it, and the two spellings trap alike; a user-declared effect's operation and `IO.sleep`'s `@Nat` formal stay the honest [#754](https://github.com/aallan/vera/issues/754) unguarded class. Two diagnostic rationales (`E504`, `E531`) that listed the `throw` payload among the unguarded sites — false once the guard landed, and contradicting the spec sentences this change amends — no longer do. Reaching the predicate needed the payload's TYPE, which neither of a cell's two names carries: `family` renders the predicate and `base` strips it, so `CellNames` now carries the type expression its producer already held rather than parsing one back out of a mangled family name. The predicate lowering itself is injected into the translation context (`set_refinement_guard_emitter`), because the two halves of a §2.6.5 guard sit on opposite sides of that seam — which local at what width is the context's question, while the trap message, the contract-fail import and the E617/E618 diagnostics are the generator's. An unrefined payload's WAT is byte-identical to before: a differential over all 278 pre-existing corpus programs — every `examples/` and `tests/conformance/` program, compiled and verified on both trees — moves nothing, in emitted WAT or in the obligation and diagnostic streams. - **Spec §6.4.3 and `KNOWN_ISSUES.md` now name every unguarded `@Nat` narrowing site** (release-PR review). §6.4.3 said two sites stay unguarded — a user-declared effect operation's argument and the generic-instantiated constructor field — and the `#754` row said the value-position tuple component was "runtime-guarded at the function boundary". Measured, a **tuple component at construction** is a third: `Tuple(float_to_int(x), 5)` narrowing into a `@Tuple` records `tier3_unguarded` with an E504 that names the site (`@Int value narrowing into a @Nat tuple component`), and the emitted function carries no guard — at construction, in return position, or at a call argument alike. The only guard is the one the *consumer* emits when it destructures, so a tuple that is only returned or passed on is never checked. Both documents now say so; the behaviour is unchanged and the residual stays disclosed statically. - **`json_parse` accepts one domain, and both runtimes accept it** ([#1306](https://github.com/aallan/vera/issues/1306)). The reference host parsed with `json.loads`, whose default `parse_constant` admits `NaN`, `Infinity` and `-Infinity`; the browser gated with `JSON.parse`, which refuses them as RFC 8259 requires. So the two hosts disagreed about *which call* rejects a non-finite value: the browser at the parse with a handled `Err`, the reference host at `json_stringify` — and there as a raw Python traceback rather than a Vera error ([#1302](https://github.com/aallan/vera/issues/1302) below). Spec §9.7.1 now states the accepted domain instead of leaving each host to inherit its parser's: RFC 8259-valid text that decodes to finite numbers and strings of Unicode scalar values, everything else `Err` at the parse with the same message on every runtime. **A non-finite number has two entry routes and the domain closes both.** The constants are one; the other is a syntactically valid number that overflows — `1e999`, `-1e999`, `[1e999]`, `{"a":1e309}` — which both host parsers accept, decoding to an infinite `JNumber` that then died at `json_stringify`, the same divergent-refusal-point defect one syntax over. RFC 8259 §6 sets no limit on a number's range and says an implementation may set one; Vera's is the finite `Float64` values, which is exactly what `json_stringify` can write back. **The refusal covers the integer spelling too**, and that half was reference-host-only: `json.loads` returns a Python `int` for a digit string with no fraction and no exponent, so `1` followed by 309 zeros never met a float range check — and then had to become an f64 at the WASM boundary, where `float()` raises. It died with `int too large to convert to float` where `JSON.parse`, which has no int/float split and sees an `Infinity` either way, returned the shared sentence. The integer bound is the double **rounding** boundary (`2**1024 - 2**970`) rather than `sys.float_info.max`, and compared in integer arithmetic: an integer *larger* than the largest finite double still rounds to it and both hosts accept it, so the obvious bound would have traded this divergence for its mirror image, and a bound implemented as `float(value)` would be the very overflow it is looking for. Underflow is not the same question and is not refused: `1e-999` decodes to `0`, finite and in the domain, pinned as a control beside `1e308` and the largest representable double so a refusal cannot generalise from "unrepresentable magnitude" to "large". Both value-level exclusions — overflow and lone surrogate — are found by ONE document-order walk returning the sentence itself, so "whichever comes first names the refusal" is the rule rather than a precedence table the two hosts could implement differently. The domain is the parse-side counterpart of the canonical output form [#1293](https://github.com/aallan/vera/issues/1293) pinned — a non-finite number has no JSON representation in either direction — and with no entry route through `json_parse`, the output-side refusal is now reachable only from a `JNumber` a program *constructed* from `nan()` or `infinity()`. The reference host's `parse_constant` hook **records rather than raises**, and the refusal is decided after the parse completes. Raising on sight would have made it answer a different question from the browser's: Python's scanner calls the hook the moment it sees the token, so `[Infinity_x]` — malformed for a reason that has nothing to do with the constant — would have reported the non-finite sentence natively while the browser reported a syntax error. Recording and continuing asks what the browser asks, by substituting `0` for each bare constant and re-parsing: *would this text be valid JSON if the constants were admitted?* Only then is the constant the whole story, and only then do both hosts say the same sentence; text malformed for any other reason keeps its host-native syntax message, as every syntax error always has. The browser's scan also only considers a token where a *value* may begin — the start of the text, or after `[`, `,` or `:`. Without that it found `NaN` at offset 1 of `-NaN`, substituted, re-parsed `-0` successfully and reported the shared sentence, where the reference host's parser never reaches the token at all and gives a syntax error. `-NaN`, `[-NaN]`, `+Infinity`, `infinity`, `nan`, `NaNx` and `-Infinityx` are all pinned as host-native on both hosts. The parity battery pins all four probe inputs from the issue's table plus the container and multi-constant shapes, compares the whole `Err` message across hosts rather than which arm was taken, and runs beside controls the refusal must not disturb — `"NaN"` as an ordinary string value among them. - **A lone-surrogate escape is refused at the parse, on both runtimes** ([#1308](https://github.com/aallan/vera/issues/1308)). `{"k":"a\ud800b"}` is grammatically legal RFC 8259 whose decoded value is not a sequence of Unicode scalar values, and a Vera `String` is — so the value has no UTF-8 encoding and cannot cross the WASM boundary at all. Both host parsers accepted the text and the *memory boundary* decided what happened next, differently and by accident: the browser's `TextEncoder` substituted U+FFFD, so `json_stringify` printed `{"k":"a�b"}` with nothing to tell the caller the value had changed, while the reference host died inside `_alloc_string` with a raw `UnicodeEncodeError`. Neither is a value the program can handle. The refusal now happens where the decoded value is known and before anything is marshalled, with one sentence naming the code point in canonical `\uXXXX` form so both escape casings produce the same message. Keys are covered as well as values, at any nesting depth — the key position is the one the issue's own reproduction used. The check does not overshoot: a *matched* high-then-low pair denotes one astral scalar value and still parses, which the batteries pin with matched pairs in every position, two pairs adjacent, and a pair at the end of a string. The two hosts' scans differ in a way worth recording, because the same rule reads differently against the two representations of a decoded value — `json.loads` has already combined a well-formed escape pair into one astral code point, so a plain D800–DFFF range test is complete on the reference host, while a JS string is UTF-16 and its scan must consume pairs before judging anything lone. With this and [#1306](https://github.com/aallan/vera/issues/1306), `md_parse` is the only operation on the shared surface still diverging (§12.9.3). - **A host callback's failure is a Vera error, not a Python traceback** ([#1302](https://github.com/aallan/vera/issues/1302)). `execute()` converted an escaping exception into `WasmTrapError` only when its type name was `Trap` or `WasmtimeError`. A host import raising an ordinary Python exception — `json_stringify` refusing a non-finite `JNumber`, the case that surfaced it — is re-raised through wasmtime's trampoline and arrives as, say, a `ValueError`, so the branch was skipped entirely: no classification, no source-map resolution, and the captured stdout/stderr dropped as the exception unwound. Measured on a program printing `"before"` and then `json_stringify(JNumber(nan()))`: **63 lines** of stderr across 17 Python stack frames, none of them naming the user's `.vera` file — and in `--json` mode no envelope at all, so a machine consumer got nothing parseable. It is now one line: `Error: json_stringify: NaN is not representable in JSON — RFC 8259 has no NaN or Infinity. Guard with float_is_nan / float_is_infinite before serialising.` The refusal itself was always right and is an instruction (DESIGN principle 1); only its presentation was wrong. The conversion is keyed on the **boundary** rather than on the exception's type, which is what makes the fix general: the guarded region is the guest invocation and nothing else, so everything arriving there is either a wasmtime trap or a host callback that raised, and every compiler phase has already finished. The taxonomy gains a `host_error` kind, carried in the JSON envelope's `trap_kind` beside the captured `stdout` (per [#522](https://github.com/aallan/vera/issues/522)); its `Fix` paragraph is empty for the same reason `contract_violation`'s is — the description already carries the specific instruction, and a canned paragraph beneath it would be noise. The original exception stays reachable as `__cause__` for anyone debugging the binding itself, and `VERA_DEBUG_HOST_ERRORS=1` re-raises it untouched so the Python frames are still one environment variable away (ENVIRONMENT.md). This closes the gap against the invariant already written on `host_print` in `vera/codegen/api.py`: *a user-level program must never produce a Python traceback regardless of what it does.* - **A `type` alias sharing a prelude ADT's name emits the alias target's width** ([#1309](https://github.com/aallan/vera/issues/1309)). Spec §8.4.1 makes the prelude's data types ordinary declarations a program names *and shadows*, and the checker resolves such a name the way `vera/naming.py`'s `_resolve_named` documents — type parameter, primitive, alias, declared ADT. Codegen's `_type_expr_to_wasm_type` tested `_adt_layouts` (and `Array` / `Map` / `Set` / `Decimal`, none of which are primitives) *before* the alias table, so `type Option = Int;` emitted its parameter as the ADT's i32 pointer where the checker and verifier had both agreed it was an i64 — check-green, verify-green, dead at load with `type mismatch: expected i64, found i32`. The alias branch now sits where the checker puts it: after the primitives, ahead of every ADT and container branch. The issue predicted the disagreement would be *silently* wrong wherever the two widths coincide; measured across every built-in ADT name against every representation, that is not where the silence is. A matching width (`Bool`, `Byte`, `Map`, `Set`, `Decimal`, all i32) emits WAT byte-identical to the same program under a fresh alias name — inert. The silent cases are the **pair** types, whose widths differ: an `i32_pair` is two words, the ADT branch's single i32 dropped the length, and nothing trapped — `type Option = String;` returned two junk bytes for `string_concat("ab", "ab")` and `type Option = Array;` reported length 0 for a three-element array, both at exit 0. Of the 16 built-in ADT and container names, 14 now compile and run correctly where 1 did before. `Json` and `HtmlNode` still fail, in a *prelude* body (`json_get`, `html_attr`) rather than the user's, because those bodies render their own parameters against the flat alias map a main-file shadow pollutes — an alias-env scoping defect ([#1316](https://github.com/aallan/vera/issues/1316)) this branch-order fix does not reach. It does, however, MOVE that failure: the reorder flips 17 prelude `json_*` signatures from `(param $p0 i32)` to `(param $p0 i64)`, reverses the mismatch the loader reports (`expected i64, found i32` becomes `expected i32, found i64`), shifts its offset, and costs `html_attr` one shadow-stack push. Same defect, same frame, later point — not, as an earlier draft of this entry claimed, an identical failure. A generated battery pins every name in the live built-in ADT registry against every representation class, comparing the emitted `twice` signature to a fresh-name control, so a reintroduced ordering cannot hide behind matching widths. - **A `match` on a `String` or `Array` scrutinee compiles** ([#1305](https://github.com/aallan/vera/issues/1305)). `_translate_match` saved the scrutinee into one local at its inferred WAT type, and a pair-represented scrutinee infers `i32_pair` — the internal two-word spelling, not a value type — so the module carried `(local $l1 i32_pair)` and never assembled. The scrutinee and any binding pattern over it now take two consecutive i32 locals, the same (ptr, len) convention parameters and constructor fields already use. Programs as ordinary as `match @String.0 { @String -> string_length(@String.0) }` and `match @Array.0 { @Array -> array_length(@Array.0) }` were check-green and failed `vera compile` at the branch point; both run now. The issue reached this through `json_keys` and framed it as an `Option>` payload binder, which measurement does not support: `json_keys` returns `Array` (see `vera/environment.py` and the prelude body), so its result was never a binder problem and `array_length(json_keys(j))` compiled and ran throughout — the trigger is the scrutinee's representation, with nothing JSON-specific about it. The issue's own repro additionally matches `Some` / `None` against that array; a pair carries no constructor tag, so codegen now refuses that arm with an `E602` naming it, at the pattern's own location, instead of emitting a local that stops the whole module from assembling. That the checker accepts such a match over a constructor-less container ADT at all is filed separately as [#1315](https://github.com/aallan/vera/issues/1315). - **A `Future`-named type alias no longer makes an array return print as text.** `_return_type_is_string` — which decides whether `vera run` decodes a function's (ptr, len) result as UTF-8 — tested the representation-transparent `Future` strip *before* the alias table, and `Future` is an ADT name rather than one of `vera.types.PRIMITIVES`, so an alias of that name shadows it. Under `type Future = Array;` a `@Future` return was therefore classified a string while the width derivation resolved the alias and lowered an `Array`, and the array's backing bytes were decoded as text: two NULs where the same program under a non-ADT alias name printed the pointer. This is the third consumer of the branch-order defect [#1309](https://github.com/aallan/vera/issues/1309) fixed in `_type_expr_to_wasm_type`, found by review on the PR rather than by the original issue, and it misclassifies identically at that PR's branch point — pre-existing, not introduced by the reorder. `String` keeps its place ahead of the alias branch, being the one primitive involved, and the #841/#1047 transparent-`Future` decode and PR #1041's alias-to-`Future` shape are held by over-correction controls. - **The `ch09_json` conformance entry cites the section that exists.** Its manifest `spec_ref` read `Section 9.4.4`, a section Chapter 9 does not have — the chapter's `9.4` runs to `9.4.3 Map` and `Json` is `9.7.1`. - **The Vera-level type namers join over a conditional instead of reading one branch** ([#1286](https://github.com/aallan/vera/issues/1286)). [#1276](https://github.com/aallan/vera/issues/1276) fixed the WAT result-type deciders to take the first branch that yields a type; their Vera-level siblings kept the one-branch read — `InferenceMixin._infer_vera_type` (the WASM call-rewrite consultor) read `then_branch` only and `arms[0]` only, and `Monomorphizer._infer_vera_type_name` (the instantiation-discovery consultor) read `then_branch` only and had no `MatchExpr` arm at all. A branch whose every path `throw`s names no type, so reading only that branch answered "unknown" for the whole expression, and the issue's latency estimate was wrong in the program's favour: the shape is constructible, and it is loud in two different ways from check-green source. As an **array-literal element** (`[if false then { throw(true) } else { 42 }, 7]`, and the `match` and `String` spellings of the same position) the unknown element type raised `CodegenSkip`, so a declared `public fn main` — `vera check`-green and `vera verify`-green at 2 Tier 1 — was absent from the compiled exports behind an `[E602]` note. As a **generic argument** (`idg(if false then { throw(true) } else { 42 })`, verify-green at 4 Tier 1) the type variable bound nothing and the clone fell to the phantom-var default: the module carried `idg$Bool`, an i32 clone, reached with an i64 argument, and failed to load with `Invalid input WebAssembly code at offset 73: type mismatch: expected i32, found i64`. The same reading through a constructor **field** mis-instantiated the unboxing clone the other way round (`expected i64, found i32`). The repair lands on both consultors together because the `match` case was broken in both directions: with every arm completing and nothing diverging, the rewrite named `idg$Int` from arm 0 while discovery, having no arm for `MatchExpr`, named the phantom default, and the caller was dropped on a dangling target — the clone-name agreement contract ([#772](https://github.com/aallan/vera/issues/772)) makes the pair, not either function, the unit of repair. All 291 pre-existing corpus programs (`examples/` plus `tests/conformance/`, recursive) emit byte-identical WAT, since the join only changes an answer that was previously unknown; `ch02_generic_arg_branch_join` promotes the witness into the conformance suite at level `run`. The review round closed the same divergence in two further shapes, both of them the one gap — the discovery consultor must stay structurally parallel to the rewrite one, arm for arm. It had no `Block` arm, and the transformer leaves a braced match-arm body AS a `Block`, so `idg(match … { Some(@Int) -> { let @Int = @Int.0 + 1; @Int.0 }, None -> throw(true) })` named nothing on the discovery side and `idg$Int` on the rewrite side: a dangling target that dropped `main` from check-green source. A braced `if` branch whose tail is itself braced does the same, and so does a `handle` in argument position, which likewise had no arm. An `IndexExpr` argument is measured to dangle identically and is deliberately left for its own change ([#1327](https://github.com/aallan/vera/issues/1327)): the rewrite's arm resolves chained indexing, aliases and `Future` payloads against codegen tables the monomorphizer does not have, so a partial mirror would replace a shape where both consultors answer "unknown" with one where they disagree. - **A GitHub Release body that would exceed the 125,000-character limit is condensed instead of failing** ([#1288](https://github.com/aallan/vera/issues/1288)). `release.yml`'s `Tag and create GitHub Release` step 422'd on v0.1.10, whose CHANGELOG section extracts to 147,918 characters, and it failed **after** PyPI had accepted the immutable archives and **after** the tag was cut — the one point in the pipeline where a step must not fail. `scripts/release.py notes` is now total: within budget it publishes the section verbatim, and past it, it regenerates the shape the v0.1.10 release was completed by hand with — the section's `###` subsection headers, one condensed line per bullet carrying its lead-in and its last issue or pull-request reference, and a link to the canonical section in the CHANGELOG at the tag. Run against v0.1.10's section the generated index reproduces the released body's 73 index lines byte for byte. In the pathological case where even the index overflows it is truncated and says so, so the builder cannot be the thing that fails. - **Four production-level divergences between spec Chapter 10 and the parser are closed** ([#1290](https://github.com/aallan/vera/issues/1290)). Typed holes have been in `grammar.lark` since 2026-03-30 and appeared nowhere in the chapter: 10.2 now declares `HOLE` and `primary_expr` carries the alternative, so the chapter's expression grammar is the parser's. 10.2's `BLOCK_COMMENT` published a non-nesting regex, contradicting 1.3 ("They nest") and the implementation, which counts depth in `vera/lexical.py` because a regular expression cannot; it is now a nesting production with that fact recorded beside it. The other two the new body comparison found: `slot_ref` and `result_ref` admitted an arbitrary `type_expr`, where the parser accepts only `UPPER_IDENT type_args?` — a refinement-typed slot reference is a syntax error, and the published grammar said it was legal; and `effect_list` carried a second alternative ambiguous with the one beside it, `effect_ref` already admitting a bare `UPPER_IDENT`, the same redundancy #1279 removed from `statement`. - **README's project-status line has every count gated, not just its test count.** The `check_readme` helper returned silently when a pattern matched nothing, and four of its five patterns matched no README text at all — so the conformance count sitting beside the gated test count drifted through two rebases unseen. The line's four countable figures — tests, conformance programs, examples and spec chapters — are now read from that line alone, and a figure that has gone missing is an error rather than a skip. - **A user-defined `fn get` / `fn put` is no longer hijacked by an enclosing handler** ([#1284](https://github.com/aallan/vera/issues/1284)). Three sites answered "does this `get` mean the user's declaration or the effect operation?" independently. The checker answers user-fn-first — `_check_call_with_args` looks a bare name up as a function before it looks it up as an operation, so a declaration named `get` owns every bare `get(...)` in its scope, which an arity or argument-type error at such a call site proves by reporting the *user's* signature (E201/E202). Codegen answered twice more: the declared-effect row in `vera/codegen/functions.py` withheld the intrinsic when `_fn_sigs` already owned the name, and the handler expression in `vera/wasm/calls_handlers.py` installed `get`/`put` unconditionally. From `vera check`-green source that produced, depending on the nesting shape, a **silently wrong value** (`nat_to_int(get(3))` under `handle[State](@Int = 5)` returned the cell's 5 for the function's 4, and the argument was not even emitted), a **module WASM validation rejects** (a `@Bool`-returning user `get` took `state_get_Int`'s i64 into an `i32` position; different-family nesting took the *enclosing* cell's getter at the wrong width), or a **spurious `[E602]`** in which the #1233 unaddressable-cell gate refused `main` outright, naming "a bare or qualified State operation `get`" the program never contained. The repair is one predicate, `vera.slots.bare_call_denotes_user_fn`, stating the checker's rule once and consumed by the bare-call dispatch in `vera/wasm/calls.py` (which now gates the clause-inline registry, the host-cell intrinsics and the addressability gate together), by the three bare-`FnCall` result-type inference sites, and by the monomorphizer's discovery walk — each passing its own name table, so the sites cannot answer differently about the table they share. Gating the *dispatch* rather than the *registries* is what makes it correct rather than merely consistent: the registries record which cell an op name reaches, which is true whatever the program's declarations are called, and withholding an entry answered both questions with one table. That is why the gate-only fix measured during PR #1283's review turned the loud skip into a differently-broken module, and why it also cost the qualified spelling its cell — `State.put(5)` in a function that also declares `fn put` compiled to `call $vera.put` and failed to link, which now lowers to the intrinsic the checker always meant. Discovery's `MonoContext.fn_names` moves to the same lookup-time question, so a `get(())` fixing a generic's type variable under a handler names the clone the rewrite emits. All 256 conformance and example programs emit byte-identical WAT. **The `W002` async-commutativity warning was the same defect in the checker's own file** and is corrected with them: `_collect_expr_effects` asked `lookup_effect_op` before the scoped function lookup — the last op-first consumer — so a user function named after an operation contributed the *operation's* parent effect to the commutativity analysis instead of its own declared row, wrong in both directions. A **pure** `fn get`, in a program containing no `State` at all, drew `async argument performs State effects`; a `fn get` that performs `IO`, under a row naming `Http` first, drew **no warning**, because the walk bound the name to `Http.get`, which is inside the commutative whitelist, and silently withheld the eager-evaluation warning the program is owed. Both are pinned with rename controls — the byte-identical program with the helper called `gett` / `fetch` was correct throughout — beside a control that an unshadowed bare `get(())` under a `State` row still warns, so the fix cannot degenerate into never reporting `State`. The walk's comment claiming it resolves "like the call checker above" is now true. One caveat the predicate did not close on its own: codegen's name table was not scope-accurate, so a name the call site cannot see still answered "user-owned" there — a property of the table rather than of the rule, closed by [#1299](https://github.com/aallan/vera/issues/1299) below. - **A bare call is lowered against the names its call site can see** ([#1299](https://github.com/aallan/vera/issues/1299)). The [#1284](https://github.com/aallan/vera/issues/1284) ownership predicate is one rule read over two tables, and only one of them was a scope. The checker's is a lexical walk; codegen passed `set(_fn_sigs.keys())` — a flat mirror of every symbol the whole compilation absorbed — so a bare `get(())` the checker had resolved to a `State` operation was lowered as a call to a declaration the body cannot name. Four source shapes reach it, all `vera check`-green and all one defect: an imported module's **private** `fn get` (invisible, but still compiled in because the module's own bodies call it), a **public** one a selective import excludes, a `where` helper of a **`forall` parent** (which keeps a bare `_fn_sigs` key beside its clone-qualified one where a non-generic parent's helper does not), and the ability operation `show`, which `E151` does not reserve and which reaches the same table through the *intrinsic* gate rather than the operation one. How it lands is a property of the widths, not of the route: where the invisible declaration and the cell share a WAT type the module loads and returns the wrong value (7007 where the cell holds 42007), where they differ it fails to load (`type mismatch: expected i64, found i32`), and the generic-`where` route is always loud — the bare key exists in the signature table while no bare *symbol* is emitted, so the call dies at WAT assembly on `unknown func: failed to find name $get` with no E-code. The repair splits the two questions the one set was answering. `_known_fns` keeps the flat registry for `_translate_call`'s guard rail, which asks whether a *resolved* target — already mono-mangled, already `mod$…` rerouted — has an implementation, and is flat by nature. A new `_scoped_fns` carries the names visible in the compiling declaration's **lexical** scope, and that is what the ownership predicate reads: its namespace's own declarations plus the public, in-filter names of the imports *that namespace* makes (spec §8.6.4 — imports are never inherited, so a transitively-reached module contributes nothing to the entry program), the prelude, and the `where` helpers of every enclosing function. Module scope alone would not have closed the third route: a generic's helper *is* in the module and still is not in a sibling's scope. The narrowing is a strict subset of the registry by construction — every `$`-bearing key is admitted unconditionally, since `$` cannot occur in a Vera identifier and a mangled name is never what a bare source call spells — so it can only withdraw a name the flat table wrongly claimed. Three consumers read that question, not one, and the third is reached by wrapping the same call in a generic. **Instantiation discovery** (`MonoContext.fn_names`) types a bare call to NAME the clone, and its table is program-wide by nature — the guard rail needs every symbol in it — so `idg(get(()))` beside an invisible `fn get(@Unit -> @Bool)` discovered `idg` where the checker had typed the `State` cell. Discovery now enters the namespace of the declaration it is walking (`Monomorphizer.namespace_scope`, accumulating each function's own `where` helpers as it descends, exactly as it already accumulates `forall` binders), and **both sides enter it at all ten walks between them** — six on codegen's side, four on the verifier's — from the same shared derivation, so narrowing one and not the other would leave a clone verified that nobody emits. The tenth is the one worth naming: `collect_generic_helper_instances`, the leaf under a generic's `where` family, is driven *directly* by both, so leaving it unscoped left them agreeing while both read the flat table — and two sides being wrong together is exactly what a differential cannot see. It is pinned against the **checker's** answer instead, and an instrumented audit over the corpus reports zero entries into the scoped region with no scope entered. Behind them the WASM call-rewrite's clone-naming override (`_declared_return_clone_name`, which beats the general inference for #899's benefit) read the same flat return-type registry and is gated on the same predicate. Depending on the widths, the shapes landed as a load failure (`expected i32, found i64`), a live clone of the wrong signedness reached by a negative cell, or — with discovery corrected and the override not — an `[E602]` drop of the caller. All 258 pre-existing conformance and example programs emit byte-identical WAT, and all three gates now have conformance coverage: reverting any one of them turns the suite red. - **E608 no longer refuses two modules' provably distinct generics** ([#1281](https://github.com/aallan/vera/issues/1281)). The flat-namespace collision rail exists because Pass 2.5 emits every imported function under one WASM name — but a generic emits nothing under its bare name, and since [#1274](https://github.com/aallan/vera/issues/1274) its clones live in a namespace chosen per *owner*: `gen$Bool` for a generic that owns the importer's bare name, `mod$$gen$Bool` for one that does not. A diamond where `base` declares a public `forall fn gen` and `mid1` a private one occupies two different namespaces and was refused outright with `Function 'gen' is defined in both imported module 'mid1' and 'base'`, while `vera verify` returned rc=0 on the same program — a loud verify-vs-compile disagreement. The rail now reads the same ownership classification the clone namespace does, and fires only when the pair really can collide: when either declaration is not a top-level generic (a non-generic *is* emitted under the bare `$name`), when both own the bare name, or when some namespace can name both — a module importing two dependencies that each export `gen` would resolve its own bare call to one of them, and spec §8.5 now refuses the name outright rather than ordering the two imports, so that shape keeps its refusal here as the backstop behind the check-phase refusal described below ([#1304](https://github.com/aallan/vera/issues/1304)). The registration moved with the message, as defence in depth: a qualified-only generic no longer injects a bare `_fn_sigs` or `_fn_ret_type_exprs` entry at all. Those two registries are read *per name* by consumers the clone classification says nothing about — `MonoContext.fn_names`, the [#1207](https://github.com/aallan/vera/issues/1207) shadow guard, and the WASM call-rewrite's return-type lookup — where first-module-wins would make the answer depend on registration order. What actually closes that shape is the #1299 scope narrowing above, which reaches the same consultors through the call site: reverting both withholdings leaves every suite and all 224 conformance programs green. They are kept, and pinned structurally on the tables they act on with one cell each, because nothing but those four consumers' current internals stops any of them from picking a winner. - **A module's data type no longer empties a prelude one out of every other namespace, and the shape that silently dropped functions is now an error** ([#1277](https://github.com/aallan/vera/issues/1277)). Codegen keeps ONE flat `_adt_layouts` map while the checker gives every namespace the prelude's data types from the start, and the two halves of that gap failed differently. **Membership**: `_adt_members_in_scope` recovered global infrastructure by SUBTRACTING what the namespaces declare from the registered layouts, which is sound only while "declared by a namespace" and "global infrastructure" are disjoint — and §8.4.1 makes them overlap on purpose, since the prelude's data types are ordinary public declarations a program may name and shadow. So one file's `data Json` removed `Json` from the member set of every OTHER namespace, including the entry program's, while the checker's `TypeEnv` carried it in all of them; measured as a straight disagreement, `Json` a data type in a module's namespace for the checker and not for codegen. The Pass-0.5 built-in snapshot unioned in as a floor could not protect the four demand-injected prelude ADTs, because it is taken before Pass 1.2 injects them — the same asymmetry [#1253](https://github.com/aallan/vera/issues/1253) fixed, one layer down. The floor is now stated positively rather than recovered by elimination: `vera.prelude.prelude_adt_names()` parses the prelude's own data blocks with the same parser `inject_prelude` uses, so a new prelude ADT joins the set by being written, and a differential holds the two against each other; the cached `prelude_data_decls()` behind it hands back a read-only mapping, since one cached object is shared by every caller in the process. Scoping the subtraction per namespace instead — the other direction the issue left open — is refuted by `tests/test_adt_membership_scope_1253.py`, which it re-opens: a sibling module's ADT would become infrastructure for every namespace but its own. **Contention**: where a MODULE declares one of the prelude's data type names with a DIFFERENT SHAPE and the prelude is also compiling its own, the two contend for the one layout slot and the module's wins. The prelude's ADT was then never registered, its own combinators hit `unknown constructor` (an `[E602]` inside ``), and every user function touching the type was dropped behind an `[E620]` cascade — all of it reported as WARNINGS, so a `vera check`-green program compiled with **exit 0** to a module with a function silently missing from its exports, and nothing named the declaration that caused it. That is now **E621**, an error located at the module's declaration in the module's own file, refused by the same Pass-1.9 severity gate `E608`/`E609`/`E610` use. It covers all eight of the prelude's data types, which required reading the DECLARATIONS rather than the registered layouts: the layout harvest skips a built-in name outright, because the throwaway registrar holds `Option`, `Result`, `Ordering` and `UrlParts` for every module whether it declares them or not, so a layout-keyed rail saw `data Json` and never `data Option`. The two halves differ only in when the prelude is present — the demand-injected four not until the entry program uses them, the always-injected four in every program — so a differently-shaped module `data Ordering` contends unconditionally, which upgrades that shape from an `[E602]`/`[E620]` cascade to one instruction. What decides contention is the two declarations' SHAPES: the same constructors, in the same order (the tag is the position), with the same field types, type parameters compared positionally — and each declaration's field types are resolved through the alias maps of the namespace it was WRITTEN in, the module's own for the module's declaration and none at all for the prelude's. Both halves of that are load-bearing. A module that restates the prelude's type through its own alias (`type Payload = String;`) is still a restatement, and comparing raw spellings refused it. Resolving the prelude's spelling through a module's aliases would go wrong the other way: `type Array = Int;` in a module makes its `JArray(Array)` an `Int` field, and with both sides resolved the two keys collapse — which is how that program compiles today, with the module's layout in the slot, the entry's `json_array_length` reading it, and no diagnostic at all. A module that restates the prelude's type shares the one layout — measured legal for all eight, and kept legal, which is what stops the rail from becoming the reservation §8.4.1 forbids. That is not a hypothetical: `examples/vera/collections.vera` declares `public data Option { None, Some(T) }` and `examples/modules.vera` imports it, so a rail that fired on the name alone refuses a shipped example — `vera compile` on `examples/modules.vera` returns E621 under that mutation, which `scripts/check_e602_clean.py` catches as a `COMPILE_ERROR` (measured; `check_examples.py` does not, running only `check` and `verify`). The rail's first form refused four of the synthetic restatements. Every declaring module is asked, not the first: a library that restates the prelude's `Ordering` otherwise answered for a sibling declaring a different one, so with the restating module imported first the sibling's contention went unseen — check-green, exit 0, the caller silently dropped — and the reverse import order caught it. An order-dependent rail is not a rail, and the battery now carries both orders. The module-versus-module pair for a name the prelude does NOT provide stays E609's, which a control pins. The acceptance battery is a parameterized test over all eight names in both shapes plus the restatement control, asserting that no cell reports `[E602]`/`[E620]` and that no zero-exit compile is missing a function, so the four-of-eight coverage the rail started with cannot return silently; `ch08_module_prelude_adt_contention_rejected` pins the same refusal at conformance level, paired with the positive that imports the same module and never names the type. The conformance manifest gained an `expected_error_stage` key for it (`"check"`, the default, or `"compile"`): a compile-stage negative asserts that the program type-checks CLEANLY and is then refused by `vera compile` with the declared code, which is the property a codegen-phase diagnostic exists for and which the check-only negative path could not express. Reserving the prelude's names is not the fix and is not done: §8.4.1 forbids it. - **The prelude's declaration-index block no longer depends on what the main file declares** ([#1287](https://github.com/aallan/vera/issues/1287)). `_stamp_decl_order` guarded its PRELUDE write on `_decl_order`, the ACTIVE (main-file) namespace — but `_prelude_decl_order` is not a namespace: `_module_alias_scope` builds every module's index space as `{**prelude, **module_own}`, so it is the base layer under all of them and its contents are a fact about what `inject_prelude` laid down. A main-file `type Option = Int` is accepted (§8.4.1 again) and, being an alias rather than a `data`, does not suppress the prelude's own `data Option`, so the guard fired on the prelude stamp: `Option` was left out of the block entirely, and every later prelude declaration shifted one place earlier because the skipped stamp never advanced the counter. Inside a module namespace the prelude's `Option` then reached `AliasEnv.data_types` at `_BUILTIN_DECL_INDEX` — below `_PRELUDE_DECL_BASE`, so ordered ahead of every other prelude declaration rather than among them — which is exactly the cross-namespace leak `_decl_order` and `_module_decl_order` were split apart to prevent. Latent at emission: that map changes a rendering only for `Decimal` and the single `REMOVED_ALIASES` entry `Float`, and no prelude ADT is either, so no WAT moves; the defect is the wrong value reaching the consumer. The prelude write is now unconditional and the ACTIVE space still takes the main file's stamp, so the shadow keeps winning its own namespace. Pinned as an invariance — the same program with and without the shadowing alias must stamp an identical prelude block — with the main-namespace control that a fix stamping `_decl_order` unconditionally would fail. - **`new(State)` reads the cell its contract names** ([#1285](https://github.com/aallan/vera/issues/1285)). `old(State)` has been keyed on the resolved cell family since [#1205](https://github.com/aallan/vera/issues/1205)/[#1209](https://github.com/aallan/vera/issues/1209); `new(State)` read the name-keyed `_effect_ops["get"]`, which holds whichever `State` the effect row registered first. Under a single-`State` row the two keyings coincide, which is why the corpus agreed; under a multi-`State` row the two sides of one `ensures` clause read different cells. `effects(, State>)` with `ensures(new(State) == false)` was check-green *and* verify-green, emitted `state_get_Int`'s i64 into the Bool comparison's `i32.eq`, and died at load with wasmtime's raw `type mismatch: expected i32, found i64`. The type mismatch is the symptom rather than the defect: where both cells share a machine width — `State` beside `State` — the module loaded and answered about the other cell, so `ensures(new(State) == old(State))` on a function that writes neither was refuted at runtime on a contract the verifier had discharged. Codegen now carries a family→getter registry populated at the declared-row registration site from the per-family `CellNames` it already computes, and `_translate_new_expr` keys on `_state_effect_family` exactly as `_translate_old_expr` does. A bare `get(())` names no family and so is right to keep reading the name-keyed registry, source-order-first-wins; a contract names one and must not. - **`decimal_from_string` ignores one stated whitespace set, and a leading byte-order mark survives the browser boundary** ([#856](https://github.com/aallan/vera/issues/856) review). §9.7.2 said the grammar is applied "after ignoring surrounding whitespace" and that the accepted domain is defined by the grammar "rather than inherited from whatever the host library parses" — but the whitespace half *was* inherited, from `str.strip` on the reference host and `String.prototype.trim` in the browser, and those two sets differ in both directions. Measured through identical module bytes: `U+001C`–`U+001F` and `U+0085` around a decimal were `Some` natively and `None` in the browser, `U+FEFF` was `None` natively and `Some` in the browser, and `U+00A0` with the Unicode space separators were accepted by both for reasons neither specification names. §9.7.2 now states the set, and it is the one the language already had: the six code points `is_whitespace` names. **The `U+FEFF` half turned out not to be about trimming at all** — `new TextDecoder('utf-8')` defaults to `ignoreBOM: false`, whose meaning is the reverse of its name, so the browser's `readString` **removed** a byte-order mark from the front of every string crossing into a host binding. `IO.print("\u{FEFF}x")` printed `x`, `json_parse` accepted a BOM-prefixed document the reference host refuses, and `md_parse` dropped the character from its text. The decoder now passes it through, matching `safe_utf8_decode`, which never stripped one. The exponent bound `|exp| <= 999999` is unchanged in force and in value; only its keyword is, from a lowercase "must" to the RFC 2119 MUST it was always enforced as. - **Three constructed `MdBlock` values the renderer could not write back** ([#1294](https://github.com/aallan/vera/issues/1294) review). All three are reachable only from a value a program *builds*, which is why a round-trip corpus could not find them: the parser never produces the shape that breaks. **A code span whose content starts and ends with a space** was eaten by the parser's own strip — `_parse_inlines` removes one such pair whenever the fenced text is two characters or longer, so `MdCode(" x ")` rendered `` ` x ` `` and read back as `MdCode("x")`, and `MdCode(" `x` ")` rendered to the same bytes as ``MdCode("`x`")``, which made the loss unrecoverable even by guessing. The renderer now pads those spans the same way it already padded backtick-bounded ones, so the strip removes the pad instead of the content; spec §9.7.3 loses one of its three documented round-trip losses as a result. **A list item with no blocks** was dropped outright, though `- ` is exactly what the parser reads back as one — and in an ordered list dropping it silently renumbered every item after it. It now renders as its marker plus the space both item patterns require (a bare `-` is a paragraph). **A container that renders to nothing** — a list with no items, a table with no rows — still drew the document's blank-line separator, so `MdDocument([MdList([]), p])` rendered `"\nafter"`: a blank line standing for an absent block, which the next parse cannot attribute to anything and which cost the render its fixed-point property. A zero-line child now contributes no separator, in `MdBlockQuote` as well as `MdDocument`. Both runtimes move together and the two spec rules are restated; the change is held to a zero-regression bar over the 6,078-case Markdown corpus — ADT agreement, render agreement and each host's fixed-point property all show no ok→broken transition. §9.7.3's two normative sentences are stated at the strength the batteries enforce while the section is open: the fixed point is unconditional, so it reads MUST with no carve-out, and the round-trip property reads MUST with an exception clause that now names *both* families outside it — the two unwriteable code spans, and a container with nothing in it to write, since an `MdList` with no items renders to no lines and so re-parses to no block. The earlier clause named only the code spans, which made the property false for the empty containers it did not mention. Each rule cluster also gains a compiled, contract-verified example, because every one of them is reachable only from a constructed value and the section had no executable form of that. - **A block quote separates and keeps its children, on both runtimes** ([#1294](https://github.com/aallan/vera/issues/1294) review). The **reference** renderer emitted no separator between a blockquote's children, so `MdBlockQuote([Para, Para])` — the shape `md_parse` builds from `> a\n>\n> b` — rendered as two adjacent quoted lines and read back as **one** paragraph. Structure lost silently, on both hosts, and the round-trip property spec §9.7.3 states did not hold for it. A quote with no children was the same defect one size down: it rendered as no lines at all, so a `>` in a document vanished on the round trip and left the enclosing separator dangling (`---\n>` came back as `---`). Both arms now mirror `MdDocument`'s: a bare `>` between children, and `>` for an empty quote. Two more mirrors land with them. The **browser parser**'s blockquote reader required `> ` with the space where the reference accepts `^>\s?`, and had no lazy-continuation branch at all, so `>no space` parsed as literal text and `> a\nb` pushed the second line out of the quote — both inside #1294's stated scope and neither closed by the renderer fix. And a **code span** is now fenced with one backtick more than its longest internal run rather than a fixed two, padded only when the content starts or ends with a backtick: the old rule was right for one backtick and wrong for two, since ``` `` a``b `` ``` closes on the run *inside* the content. The browser's inline parser scanned for the next single backtick rather than a run of equal length, so it could not read that back either; it now counts runs like the reference. Spec §9.7.3 states all four rules. Measured on a 1,471-input adversarial corpus: `md_render` is a fixed point on **every** input on both hosts (from 65 and 54 unstable), and cross-host render divergence falls from 489 inputs to 34; on 4,850 sections of the project's own documentation, render divergence falls from 40 to 11 and the reference round trip is a fixed point everywhere. The remaining `md_parse` divergences are a separate tracked bug, listed in `KNOWN_ISSUES.md` with a measured class-by-class breakdown. Alongside them: the browser's `json_stringify` no longer falls back to `"null"` when `JSON.stringify` returns `undefined` — unreachable from `readJson`, but the same silent substitution [#1293](https://github.com/aallan/vera/issues/1293) removed one layer up, and an unreachable branch is where a silent wrong answer survives; and the non-finite parity test now asserts the *whole* shared sentence, taken from the reference implementation so the browser's hand-copied duplicate is held against the original, plus that neither host printed anything before failing. - **The browser runtime's `md_render` mirrors the reference renderer, holds the round-trip property, and is a fixed point** ([#1294](https://github.com/aallan/vera/issues/1294)). It preserved a paragraph's internal soft line breaks and did not re-apply a container's prefix (`> `, list-item indent) on output, so it broke the round-trip property spec §9.7.3 states for `md_render` and, with it, §12.9.3's identical-results requirement. The scope was any multi-line paragraph, not the list lazy continuation first observed, and the render was not stable: re-rendering its own output moved content out of its container (`> a b` → `> a\nb` → `> a\n\nb`, where `b` is no longer quoted), and on a blockquote wrapping a heading and a fenced block the second render fragmented the fence into three and lifted the code clean out of the quote, past recovery by any subsequent parse. Two defects underlay it, in two different phases, and both are fixed because neither alone closes the issue. The **parser** joined a paragraph's lines with `\n` where the reference parser joins with a space: §9.7.3's design note excludes hard and soft line breaks from the ADT — "collapsed into paragraph text" — so a break that survives into `MdText` is one no renderer can tell from text the author wrote, and `md_parse` itself therefore returned different ADTs on the two hosts. The **renderer** returned one string and threaded the container prefix down as an argument, which a container could only apply to the *first* line of each child; it is now line-based, mirroring `_render_block` in `vera/markdown.py`, so every caller re-applies its own prefix to every line it receives — the property that makes the render a fixed point. A third mirror lands with them: a code span containing a backtick now renders with the longer `` `` … `` `` fence, which the reference renderer has always done and neither parser can produce, so it was reachable only from a constructed ADT. Spec §9.7.3 states both rules rather than leaving them as an implementation detail two hosts had to rediscover. `tests/test_browser.py`'s two pinned-divergence assertions collapse into parity assertions, and the battery around them is now three-layered — cross-host equality, the expected string, and stability under re-render — because equality alone passes two hosts that agree on a wrong answer and a single render passes a renderer that drifts on the second pass. The §9.7.3 round-trip property is exercised over a nineteen-case corpus: the eight the reference renderer is already held to in `tests/test_markdown.py`, every one of which is single-line or fence-only and therefore blind to exactly this defect, plus the container and multi-line shapes the bug was about. Three further cases render ADTs a Vera program *built* rather than parsed, since the parser only reaches the shapes it happens to produce. - **`json_stringify` has one canonical output form, and both runtimes produce it** ([#1293](https://github.com/aallan/vera/issues/1293)). The two hosts disagreed: the reference runtime called `json.dumps(value, ensure_ascii=False, allow_nan=False)` — `", "` / `": "` separators, and since `read_json` hands it Python `float`s, a `JNumber` parsed from `1` re-rendered as `1.0` — where the browser called bare `JSON.stringify`. Spec §12.9.3 requires every non-IO operation to produce identical results in both runtimes, so the divergence itself was the defect, and §9.7.1 now states the resolution: the compact form, `,` and `:` with no padding, object members in insertion order, strings escaped with non-ASCII emitted literally, and numbers rendered by ECMAScript's `Number::toString`. The browser already emitted that form; the reference host moved to it. Measuring the gap first showed it was wider than the issue's two axes — `json.dumps` renders floats with `repr`, hard-wired inside `json.encoder` and not reachable through any separator setting, and `repr` disagrees with `Number::toString` on **four** independent boundaries, not one: the fractional part of an integral value (`1.0` vs `1`), the threshold for exponential notation at each end of the range (`1e+16` vs `10000000000000000`, `1e-06` vs `0.000001`), and the spelling of the exponent itself (`1e-07` vs `1e-7`), plus negative zero (`-0.0` vs `0`). Fixing only the reported symptom would have left the other three diverging, so `vera/wasm/json_serde.py` gains `format_json_number`, an implementation of ECMA-262 §6.1.6.1.20 that takes its shortest-round-trip digits from `repr` and recomputes only their placement; string escaping is still delegated to `json.dumps`, which already agrees with `JSON.stringify` byte for byte. The claim "matches ECMAScript" is checked differentially against the real `JSON.stringify` over 2,000 doubles drawn from raw bit patterns, not only against a hand-written boundary table, since a table proves the cases its author thought of and those are the cases the code was written to handle. Alongside the formatting, the third asymmetry the issue folds in is closed in the other direction: a `JNumber` holding `NaN` or an infinity now **fails on both hosts** with the same sentence, where the browser used to emit `null` — swapping a value RFC 8259 cannot carry for a different, perfectly valid one that no consumer could tell from a genuine `JNull`. The eleven Node-only tag assertions in `tests/test_browser.py` become full parity assertions, the two pinned-divergence strings collapse into single-truth ones, and the battery gains the number boundaries, a three-pass idempotence check on every case, and the two-sided failure assertion for non-finite values (it must raise *and* print nothing, so a host that emitted `null` before failing cannot read as a pass). Review of the change found the browser host breaking the *insertion order* clause of that same canonical form, which no test covered because every JSON object in the suite had alphabetically-ordered, non-numeric keys: `vera/browser/runtime.mjs` reached the WASM-side `Map` through ordinary JS objects on both sides of the boundary — `JSON.parse` returns one, `writeJson` enumerated it with `Object.entries`, `readJson` rebuilt one key by key — and an ordinary object cannot carry insertion order, because ES `OrdinaryOwnPropertyKeys` lists array-index keys first in ascending numeric order. `{"2":1,"1":2}` round-tripped to `{"2":1,"1":2}` natively and `{"1":2,"2":1}` in the browser. The same intermediate lost a field named `__proto__` outright — assigning it runs `Object.prototype`'s setter and creates no own property — so `{"__proto__":{"a":1}}` came back `{}`. Both are now carried in a JS `Map` from parse through serialization: `json_parse` keeps `JSON.parse` as the accept/reject decision (so the `Err` domain and its message are unchanged) and rebuilds the tree with an order-preserving re-scan that hands every leaf back to `JSON.parse` on its own slice, and `json_stringify` walks the result with a canonical emitter mirroring `dumps_canonical` rather than calling `JSON.stringify`, which does not know about `Map`. Both losses were silent, and both sat inside the property this entry claims to establish. ## [0.1.11] - 2026-08-13 ### Added - **`[E130]` lists the bindings in scope at the error position** ([#558](https://github.com/aallan/vera/issues/558)). An unresolved slot reference reported only how many same-typed bindings existed, so recovering the right index meant tracing pattern pushes and `let`s by hand, or writing a typed hole and re-running — `vera check --explain-slots` stops at the signature and is no help several levels into a `match` arm. The fix text now ends with the same `Available bindings: @T.n: Type; …` table the `W001` typed-hole warning already emits, rendered from the scope at the reference itself, so the read-time diagnostic carries what the write-time one always did — the same set from the same helper, zero-size bindings included, because the index range in the description counts them and hiding them would make one diagnostic describe two different scopes (`@Unit.1` against `(@Unit, @Int)` reporting "valid indices: 0..0" above a table with no `Unit` row). A zero-size read stays `E182`'s to explain. Nothing is appended when no binding is in scope. The table renders at most twelve rows and then `; … and K more`, because the scope it reports is unbounded and the language server concatenates the fix into the hover message, so a wide function would turn one diagnostic into a wall of rows nobody reads — thirty same-typed parameters rendered a 492-character fix. Twelve sits above the measured corpus: across the 2,080 slot-reference positions in `tests/**/*.vera` and `examples/` the table is 7 rows at the 95th percentile and 11 at the 99th, so every position through the 99th renders complete and 12 of the 2,080 elide. `W001` renders through the same capped helper, so the two diagnostics still agree row for row; the LSP's typed-hole completion consumes the binding list itself and keeps every row, since there a dropped row is a missing completion item. This is the issue's option (a); the positional query (option (b), `--explain-slots-at :`) stays open on the roadmap. - **The browser runtime's untested host imports are now exercised, and the two divergences that fell out are pinned rather than papered over.** `tests/test_browser.py` gains a battery covering the Map, Set, `Decimal`, `readJson`/`json_stringify` and `Result.Err` bindings that `vera/browser/runtime.mjs` registered but nothing ever invoked — several closure bodies had zero hits — taking the file from 81.74% to 86.86% lines under the `VERA_JS_COVERAGE=1 pytest tests/test_browser.py` command TESTING.md documents ([#349](https://github.com/aallan/vera/issues/349)). Each case compiles one `.wasm` and runs it under both wasmtime and Node, so a failure isolates the host import rather than codegen. Writing them surfaced two browser↔native divergences of the kind spec §12.9.3 forbids: `json_stringify` differs on separator padding and on integral-number rendering, and traps natively on a NaN number where the browser silently emits `null` ([#1293](https://github.com/aallan/vera/issues/1293)); and `md_render` breaks the round-trip property §9.7.6 states for it on **any** multi-line paragraph — not just the list lazy continuation first observed — is not stable under re-render, and destroys a nested blockquote outright the second time round ([#1294](https://github.com/aallan/vera/issues/1294)). Neither is fixed here, because fixing means editing `runtime.mjs` and this change is tests and documentation only; instead both runtimes' exact current strings are asserted separately, including the destructive blockquote case, so a fix on either side goes red until the pins are updated deliberately. Alongside them, `TESTING.md` drops a "91% combined" coverage figure that no documented command produces (the two collectors measure different line populations, so the blend needs a line-weighted total neither report emits), corrects a blanket claim that no action ref is pinned to a commit SHA — `pypa/gh-action-pypi-publish` and `codecov/codecov-action` both are — refreshes its `~109,000`-line test-corpus estimate to the measured `~152,000`, and repopulates its Open CI/Tooling Issues table, which had been emptied to "No open CI/tooling issues" while six remained open. `vera/README.md` and `FAQ.md` lose an unqualified "identical results" parity claim that the two divergences above contradict, along with a stale hand-count of parity tests and a runtime-family count that disagreed with itself in three places against the fourteen `register_` entry points `vera/runtime/` actually defines. - **The editor grammars are gated against the effect registry.** The three grammars under `editors/` (vscode, TextMate, Vim) enumerate the built-in effect names by hand and nothing checked them, so they drifted: `HttpServer`, `Inference` and `Random` never reached the vscode and TextMate grammars, and `DB` reached none of the three. The drift is silent — an unknown capitalised identifier falls through to the generic type-reference rule, so `DB.query(...)` still highlights as *something*, just not as an effect — so four accumulated unnoticed. `scripts/check_editor_grammars.py` reads `vera.introspect.effects_payload()` from the checkout it is checking and requires a word-boundary occurrence of every registered name in each grammar, and in the two extension READMEs that repeat the list in prose and had gone stale in exactly the same way. Absence is conclusive, presence is optimistic, which is the right way round when the failure is omission and keeps the check immune to the formats involved (JSON, plist XML, Vim regex, Markdown). The checked list is explicit, since not every file under `editors/` is a grammar, so it is paired with a completeness guard: a file in a syntax directory, or carrying a grammar extension — the tree-sitter `.scm` query sets and a `.tmLanguage.json` filed anywhere but `syntaxes/` among them — that the list does not name fails the gate rather than passing unchecked. It runs as a pre-commit hook and a CI step, triggered by `editors/` and by the registry files themselves, since adding an effect is what puts the grammars out of date. Registering that CI step exposed the same defect one layer out — TESTING.md's CI-pipeline table hand-enumerates the scripts the lint job runs and nothing held the two in step, so the new step left the table describing 22 of the 23 scripts the job invokes; `scripts/check_doc_counts.py` now compares that row against the workflow as an ordered list, treating a reworded row or a renamed job as an error rather than a skip. Abilities (`Eq`/`Hash`/`Ord`/`Show`) stay out of scope: whether they should highlight distinctly from ordinary types is an open design question, tracked separately as [#1295](https://github.com/aallan/vera/issues/1295). - **xAI (Grok) provider for the Inference effect, and one flagship model per provider** ([#425](https://github.com/aallan/vera/issues/425)) — `Inference.complete` now supports Grok models. Set `VERA_XAI_API_KEY` to use. xAI's endpoint is OpenAI-compatible (bearer auth, `choices[0].message.content`), so this is one row in the `_PROVIDERS` registry with no new dispatch code. Appended last in the registry, which leaves the existing auto-detect precedence unchanged. (The issue asks for `grok-3-mini-fast-beta`, which xAI documents nowhere any more — neither in its model list nor in the retirement table that names the redirect target for each slug it withdrew — so the default is a currently-listed model instead.) Alongside it, every row's `default_model` is now that provider's **flagship** general-chat model rather than its cheap/fast tier: `claude-opus-5`, `gpt-5.6-sol`, `kimi-k3`, `mistral-large-latest`, and `grok-4.6` for the new row. Each was verified against the vendor's own live documentation at the time of the change, which also retired two IDs their vendors no longer list (`gpt-4o-mini`, `kimi-k2-0905-preview`). A program's contracts are written against what the default model can do, so capability — not price — is what the default owes the caller; the cheap tier stays one `VERA_INFERENCE_MODEL` away. **This raises the per-call cost of any program that relies on the default**, on every provider. The provider tests now pin each model ID as a literal: `test_openai_provider` asserted against `_PROVIDERS["openai"].default_model`, which pinned the value to itself and would have stayed green through the flip, and `test_anthropic_provider` asserted no model at all. `test_xai_provider` also asserts the request's `Content-Type`, which its OpenAI sibling checked and it did not, so the new row's headers are pinned as completely as the row it was modelled on. The auto-detect tests assert the API key that reaches `_call_inference_provider`, not only the provider name it selected: with two keys set, naming the right row while reading another row's environment variable satisfies a name-only assertion, and the four-case precedence test now fails on exactly that. `SKILL.md` also states the detection rule the tests pin — the registry is walked in insertion order (`anthropic`, `openai`, `moonshot`, `mistral`, `xai`) and the first provider whose key is set wins — where it previously said only "auto-detected from whichever key is set", which answers nothing when several are. Detecting IDs that rot *at the vendor* still needs network access and is tracked in [#1263](https://github.com/aallan/vera/issues/1263). Closes [#425](https://github.com/aallan/vera/issues/425). ### Changed - **`resume` is reserved as a function name, as Chapter 1, Section 1.4 already required.** The MUST was unenforced: `resume` is not a keyword token — `vera/grammar.lark` has no `RESUME` terminal and lexes the name as an ordinary `LOWER_IDENT` everywhere — so `private fn resume(@Int -> @Int)` parsed, type-checked, and ran, and a bare `resume(7)` outside a handler resolved to it. The declaration is not merely dead weight: inside every handler clause body the checker binds `resume` to the effect-resumption operator, and with a top-level declaration present the clause bodies resolved against *its* signature instead — an otherwise valid `handle[State]` was rejected with `[E202]` on `put(@Int) -> { resume(()) }` ("has type Unit, expected Int"), and deleting the declaration made the identical handler check clean. Declaring the name broke working code elsewhere in the file. Such a declaration is now rejected at `vera check` with **E153**, on the same rail as `old`/`new` (#1181) and the grammar keywords (#1187), in a fourth named piece (`_HANDLER_OPERATOR_FN_NAMES`) with its own rationale — the keyword branch's reason, that no unqualified call site can reach the declaration, is false here and would have misinformed the reader. The rejection is also the only diagnostic the program gets. A bare call resolves lexically — the enclosing where-helpers, then the top-level function of that name, then the flat registry — and the clause binding lives in that last tier, so a declared `resume` shadowed it and drew a *second* error out of clause bodies that were correct, at both top level and where-helper depth. `resume` now resolves against the flat registry alone, which is the only place its binding can live; in a valid program nothing changes, because the name is reserved and no declaration of it exists to be found. The reservation is on *declarations* only: `resume(...)` inside a handler clause is bound by the handler rather than declared and is untouched, as is `resumed` or any other longer name. A `where`-helper named `resume` is rejected one scope deeper, like every other reserved name. Covered by `tests/conformance/ch05_reserved_resume_fn_rejected.vera`. The neighbouring statement in Chapter 10 — that `resume` is a `LOWER_IDENT` and `resume(expr)` parses as an ordinary `fn_call` — remains true, and now says which phase enforces the reservation instead. - **`vera/addEffect` bounds its propagation at handlers** ([#725](https://github.com/aallan/vera/issues/725)). The transitive-caller closure was handler-unaware: a caller that wrapped its call in `handle[E]` had `E` appended to its own `effects(...)` row even though the handler discharges it there, so the workflow wrote rows the program does not need and dragged that caller's own callers in behind it. A call site inside a `handle[E]` body now contributes no edge, so propagation stops at the function that discharges the effect. Three cases deliberately still propagate, because the effect really does escape them: a caller that reaches the callee on *any* unhandled path as well (the row is needed for that path), a call in a handler *clause* body, which runs outside its own handler, and a call in the handler's *state initialiser*, which escapes for a different reason — the initialiser is evaluated in the enclosing scope, before the handler is installed, so an effectful call there is an `E125` against the caller's own row. A handler bounds the propagation only when its `handle[...]` head is spelled the way the request is, type arguments included and compared as written rather than as resolved. That is required in one direction: the checker discharges against `EffectInstance` equality, so `handle[State]` leaves `State` escaping and a caller around it still needs the row — pruning that edge on a base-name match would leave the caller `pure` and fail the whole candidate on `E125`. In the other direction it under-prunes: `handle[State]` with `type MyAlias = Int` *is* discharged by the checker but is not spelled the way a `State` request is, so that caller keeps a row it does not need — documented behaviour until [#1292](https://github.com/aallan/vera/issues/1292) keys the bound on the resolved instance. Every non-match keeps the edge, which is the safe direction: a row the program does not strictly need still type-checks. (Row identity, a separate question, remains the base name — `State` is not appended beside an existing `State`.) Containment is structural (the handled sub-tree), not span arithmetic, so the handler bound applies inside a `where`-helper body exactly as it does at the top level, and a helper's bare call still attributes to its containing top-level function as before. Row *rewriting* is unchanged and still top-level-only: a `where` helper that needs the new effect does not get it, and the gate refuses the resulting candidate rather than applying a broken one. The `KNOWN_ISSUES.md` limitation row is retired. The `LSP_SERVER.md` row stated two things, only one of which is fixed, so it is replaced by a by-design row for the other: propagation still stops at the file boundary. - **DeepSeek provider for the Inference effect** ([#450](https://github.com/aallan/vera/issues/450)) — `Inference.complete` now supports DeepSeek. Set `VERA_DEEPSEEK_API_KEY` to use. DeepSeek's endpoint is OpenAI-compatible (bearer auth, `choices[0].message.content`), so this is one row in the `_PROVIDERS` registry with no new dispatch code — the issue's proposed env-var fetch in `host_inference_complete` would have been dead code, since that function already iterates the registry. Appended last, after `xai`, leaving the existing auto-detect precedence unchanged. The default is `deepseek-v4-pro`, following the one-flagship-model-per-provider convention the other five rows adopted in [#425](https://github.com/aallan/vera/issues/425): DeepSeek's own API reference lists exactly two current `model` values, `deepseek-v4-flash` and `deepseek-v4-pro`, and positions the latter as the flagship — its change log records "The GA release of DeepSeek-V4-Pro has been rolled out on the APP, Web, and API" (2026-08-13) against `deepseek-v4-flash`'s public beta (2026-07-31), and every code sample in the documentation's first API call sends `deepseek-v4-pro`. Capability, not price, is what a default owes the caller, because a program's contracts are written against what the default can do; `deepseek-v4-flash` is one `VERA_INFERENCE_MODEL` away. (The issue asks for `deepseek-chat` with `deepseek-reasoner` for the reasoning model. DeepSeek's change log announced both discontinued as of 2026-07-24, a date now past, and documents neither; the default is a currently-documented model instead. There is also no reasoning *model* to point `VERA_INFERENCE_MODEL` at any more: on v4, thinking mode is a request-body field, which the registry does not send, so no model override reaches it.) The provider tests meet the [#425](https://github.com/aallan/vera/issues/425) bar — the model ID and the endpoint as literals rather than registry-derived expressions that would pin each value to itself, the `Authorization` and `Content-Type` headers, the absence of Anthropic's `x-api-key` and `max_tokens`, the exact turn list, and the API key that reaches `_call_inference_provider` rather than only the provider name it selected — and the precedence battery runs one case per provider ahead of `deepseek`, all five, so relocating the row fails exactly the providers it jumped. Detecting IDs that rot *at the vendor* still needs network access and is tracked in [#1263](https://github.com/aallan/vera/issues/1263). Closes [#450](https://github.com/aallan/vera/issues/450). ### Fixed - **Chapter 10's EBNF names the rules the parser actually has** ([#683](https://github.com/aallan/vera/issues/683)). The published grammar still called the assertion forms `assert_stmt` / `assume_stmt` after they became expressions (`assert_expr` / `assume_expr` in `vera/grammar.lark`), and three Lark rules appeared in no EBNF block at all: `pure_effect` and `effect_set` were inlined into `effect_row`, and `with_clause` was missing entirely, leaving the `with` form of a handler clause undocumented as a production. `scripts/check_grammar_alignment.py` now holds the two files together in pre-commit and CI — every rule header in one must exist in the other, with a six-entry allowlist for the pairs that differ on purpose (`start` / `program`, and the four spec headers Lark expresses as `-> alias` names or folds into a more general rule). Rule *names* only; bodies are not compared. Each allowlist entry records the side its name lives on and, where its reason rests on a Lark `-> alias`, both that alias and the production it must be an alternative of; those facts are checked rather than asserted, against comment-stripped text, so neither deleting the alternative nor commenting it out nor moving it to another rule can hold a waiver up, and deleting the waived spec production fails too instead of passing forever. What the alias premise does not establish is that the Lark alternative still spells the same construct as the spec production — a body-level fact a header-only gate cannot see; `tuple_literal` and `tuple_type` rest on `constructor_call` and `named_type`, general forms that would outlive tuples leaving the language, and the script says so at both the entry and the module docstring rather than claiming more. A name gone from *both* files is reported as a broken premise rather than as "the sides now agree" (the old symmetric-difference arithmetic pointed the reader at the deletion that turns the gate green with the construct documented nowhere), and each name yields at most one report, so a spent waiver whose premise also broke no longer asks for the waiver's deletion and its restoration in the same run. The header extractor also tolerates Lark's template parameters and rule priorities (`sep{item}.2:`), which previously dropped a rule from the Lark set entirely. Two further Chapter 10 defects are fixed: `statement` carried `assert_expr SEMICOLON | assume_expr SEMICOLON` alongside `expr SEMICOLON`, so `assert(p);` derived two ways in the published grammar while the reference parser has one production (`expr ";"`); and a `RESUME: "resume"` terminal was declared but referenced by no production, contradicting the neighbouring note that `resume(expr)` is an ordinary `fn_call` — under the spec's own lexer a keyword terminal shadows `LOWER_IDENT`, and `vera/grammar.lark` has no such terminal. That criterion, applied to the rest of §10.2 rather than to `RESUME` alone, clears five more phantom declarations and one omission. `SOME` / `NONE` / `OK` / `ERR` were declared as keyword terminals and referenced by no production: `Some`, `None`, `Ok` and `Err` are ordinary prelude ADT constructors (`data Option { None, Some(T) }`, `data Result { Ok(T), Err(E) }`), so under the spec's own lexer those four declarations would shadow `UPPER_IDENT` and stop `Some(1)` parsing as a constructor call at all — the identical defect `RESUME` had against `LOWER_IDENT`. `COLON` was declared for a `:` that no production in either file uses. All five are deleted. The converse case is fixed with them: `module_call` referenced a `DOUBLE_COLON` that §10.2 never declared, now added as `DOUBLE_COLON: "::"`, the spelling `vera/grammar.lark` carries as an inline literal. The whitespace and comment terminals stay — they are labelled "(skipped)" and `%ignore`d, so no production referencing them is correct. This fixes the six instances; auditing declared-versus-referenced terminals as a *gate* is [#1290](https://github.com/aallan/vera/issues/1290)'s scope, and the rule-name gate here does not compare terminals. The issue also asked for a `qualified_call` → `module_call` rename in the Lark grammar, which is not done and should not be: those are two different constructs (`Effect.op()` and `mod::fn()`), both already present under both names, and merging them would be a regression. The gate has a test pinning that it never reports them. - **Two documentation statements corrected in review.** The FAQ's contract-testing walkthrough read `requires(@Int.1 != 0)` as constraining *the second* parameter, inverting the De Bruijn rule the same document links out to: `@Int.0` is the most recent binding, so `@Int.1` is the leftmost — which is exactly what `examples/safe_divide.vera`, the example that answer tells the reader to run, guards as its divisor. Separately, the compiler-architecture README's cross-cutting summary credited `errors.py` with the `E`-codes alone, where the registry described further down the same file also holds the `W001`/`W002` warning codes. - **TESTING.md's conformance section counts the programs the manifest actually holds.** The section states each non-`run` level twice — as a number in prose and as a hand-written list of program names — and `ch05_reserved_resume_fn_rejected` reached neither, so the `check` level read thirty-eight against a manifest holding thirty-nine and the negative-test subset read thirty-one against thirty-two. The parenthesised E-code sequence is aligned to that subset "respectively", so it was short by the same one entry — the thirty-one codes present did pair correctly with the thirty-one names present, which is why the omission left nothing visibly wrong to notice. The same section's parametrized-suite count read 1,035 where the runner collects 1,070 — five checks over each of the 214 programs — a number that had drifted seven programs back, before the fixture above existed. None of the four is gated: `scripts/check_doc_counts.py` pins the suite total, the per-file table and the skipped-stage rows, but reads neither prose list nor the count in the run instructions, which is why a fixture could land with its skipped-stage rows added and its two list entries missed. All four are corrected against the manifest, which is the oracle for every one of them. - **Three more documentation statements corrected in review.** The FAQ's browser-parity answer claimed every operation but `json_stringify` and `md_render` behaves identically across the two hosts, which is not true of two whole effects: `Inference` and `DB` return an explanatory `Err` from every operation in the browser, because the API key or database credential they need would be readable from page source. That refusal is now stated as what it is — a deliberate platform boundary, distinct from the two divergences, which are unintended and tracked — alongside the fact that `Http` genuinely does work there, backed by `XMLHttpRequest` rather than a stub, so a server-side proxy is a real escape route rather than advice. Separately, TESTING.md described `check_doc_counts.py` as verifying that "counts cited in the docs match the live codebase", a blanket claim the entry above disproves; it now enumerates the citations the script actually reads, and says plainly that a count it does not name is not read at all and that hand-written program lists are outside its scope. Finally, the compiler-architecture README and its `host-families` diagram both counted **two** stateful family adapters, Decimal and State, where there are three: `async_http.py`'s `register_async` takes a `future_store` that `execute()` creates up-front, publishes it as `host_store_refs["future"]` so its size reaches `ExecuteResult.host_store_sizes`, and has entries evicted by the same `host_decref_handle` — kind 4, which also cancels a future that never started ([#841](https://github.com/aallan/vera/issues/841)). Prose, diagram label, legend and accessible title are corrected together. - **Spec §1.4's reserved-keyword list now matches the checker**: `exists` was missing and `handle`'s host-invoked carve-out was unstated. ## [0.1.10] - 2026-08-12 ### Added - **Two more count citations and one more document are gated.** `scripts/check_doc_counts.py` pinned TESTING.md's total test count but not the passed/stress/skipped breakdown beside it, so a release that refreshed the number the gate reads could leave an arithmetically impossible parenthetical behind; the parts are now checked to sum to the collected total. It also gained the four counts in `vera/README.md`'s "Test Suite" paragraph — ungated in a file where only the module map was watched, and stale by 2,569 tests, 39 test files, 53 conformance programs and 5 examples when it was finally read. All four of those counts are read with thousands separators, so none of them switches its own check off by crossing a thousand, and they are read from the `## Test Suite` section alone: the pattern spans several sentences and therefore matches with `DOTALL`, which against the whole file let a reworded paragraph pair its head with digits from any later section and green off the decoy. Both checks treat a *reworded* sentence as an error rather than a skip, because a silent skip is the failure mode the gate exists to prevent. Separately, `scripts/check_debruijn_examples.py` puts `DE_BRUIJN.md` on the shared parse-only doc gate (now five documents), which is what had been missing when its two closure examples stopped parsing. - **TESTING.md's conformance-stage skip total is gated against its own table.** The "Skipped tests" section states a total and then enumerates every skipped stage one row each — two statements of one number, and they had drifted apart: three new check-level programs added six rows while the total stayed at 85 against a table of 91. Nothing read either number. The total is now checked against the row count, which is what a reader is counting and costs nothing to measure, and a reworded sentence is an error rather than a silent skip. - **The corpus-count gate reads every document that cites the count.** `check_corpus_count` gated TESTING.md alone, so CLAUDE.md and AGENTS.md — which cite the same number in the comment on the command that runs `check_corpus_canonical.py` — went stale unnoticed, and a fix driven by the gate's own output could not see them. All three are read now, each keyed on the script name rather than on the prose or a bare numeral: a document-wide numeric substitution is not safe, `E207` being a live example of a token those digits sit inside. A row reworded away is an error rather than a silent skip, as elsewhere in this script. - **The release count is gated against the tags, not just against itself.** README's status line and HISTORY's "By the numbers" total state one hand-maintained number in two places, and cross-checking them against EACH OTHER — all `check_doc_counts.py` did — catches a half-applied bump and nothing else. Both had read 206 since v0.1.8 while the repository held 207 tags, agreeing with each other the whole way down: two documents can be consistently wrong, so `git tag` is the oracle now. The one permitted gap is the release being cut, because `release.yml` creates `vX.Y.Z` only after the merge — on the PR that bumps `version` to an untagged release the documented count is one ahead of the tags by convention, and exactly one; once that version is tagged the counts must equal them. A checkout whose tags were never fetched prints a skip rather than reading every count as wrong, since no tags means no evidence, not zero releases. The reader clears git's repository-selecting environment variables before asking, because this script also runs as a pre-commit hook and inside one `git -C ` answers for the repository being committed to rather than for the path — an ambient `GIT_DIR` made a directory that is not a repository at all report another repository's tags. Both documents now say 208. ### Changed - **Spec §4.2 says where each end of the integer range is checked.** The sentence read as though a literal itself could fall below `Int`'s minimum, but an integer literal is always non-negative — a leading `-` is negation over the magnitude, so `-9223372036854775809` parses as a negation of `9223372036854775809`. The upper bounds are checked on the literal against its target (`2^63 - 1`, `2^64 - 1`, `255`); the lower bound is checked on the negation, whose operand may reach `2^63` and no further. Both were already enforced — the text described only one of them, and described it in the wrong place (PR [#1282](https://github.com/aallan/vera/pull/1282) review). - **Spec §7.5.1 says where each unnameable cell type is actually refused.** The paragraph read as though a bare function type and a non-resolving type expression were both "refused by the compilability gate before they reach a cell" and then, in the same sentence, named a cell each by their alias-opaque spelling — two claims that cannot both hold, and the first of which is wrong for half the pair. Family naming is total and runs first, so both are named; the refusals happen downstream and at different gates. The unresolvable one is refused at the compilability gate before any cell is declared (E607/E612). The bare function type maps to `i32`, so it passes that gate, its cell is declared, and it is refused only when the function reading it is dropped (E616 at the closure read, then E602/E620) — which `tests/test_family_naming.py` already pins by asserting the four `state_*_F` imports exist beside those diagnostics. The conservative-direction guarantee is unchanged: the fallback can leave split a cell the resolution would have merged, never merge two the checker keeps apart. `DE_BRUIJN.md`, which cites this paragraph, and the `vera/naming.py` docstring that repeated the same overclaim are corrected with it (PR [#1283](https://github.com/aallan/vera/pull/1283) review). - **`CompileResult.state_types` carries a cell-name pair, not a bare string.** Its element type is now `tuple[CellNames, str]` where it was `tuple[str, str]`: a State cell's IDENTITY (the family, which since this release carries a refinement predicate) and its REPRESENTATION are two names, and every in-tree consumer — the host `register_state`, both WASI gates, `execute()`, and `vera serve` through it — reads them as such. The motivating changes are described above; this records the result-struct shape they moved, because a downstream consumer unpacking the old pair gets a `CellNames` where it expected a string, with nothing in the release notes to find (PR [#1283](https://github.com/aallan/vera/pull/1283) review). - **The prelude's reserved namespace is reserved in every declaration namespace, not just the type one** ([#1260](https://github.com/aallan/vera/issues/1260)). `E154` closed the type namespace as a trio — a reserved name could not be declared, bound as a type parameter, or referenced in a type position — and stopped there, so `effect VeraZed`, `ability VeraZed` and `data Other = VeraZed(Int)` all checked clean. That is half a reservation: DESIGN.md principle 6 counts the removed programs as error surface rather than as a cost (an effect named in the prelude's namespace has no future but collision with toolchain internals or confusion with them), principle 2 objects to a boundary discoverable only by tripping over it in one namespace and not the next, and principle 3 asks for one reservation rule rather than one per namespace — each unenforced namespace being where a future internalization would recreate the teach-the-checker-or-break-user-spellings dilemma [#1221](https://github.com/aallan/vera/issues/1221) resolved. Effect, ability and constructor names now run through the same anchored regex as the existing three rails, so none of them can drift, and the fix text is right per namespace: a type position can be answered by writing the type out or declaring an alias, and nothing outside it can, so those three say rename and only rename. `Veranda`, `Vera_thing` and a bare `Vera` remain ordinary names everywhere. Spec §8.4.1 states the full scope; the corpus was swept and nothing in it moved. Three conformance negatives complete the family (suite now 210). ### Fixed - **A module generic that does not own the importer's bare name is reached under its own module's name.** An imported PUBLIC generic whose name the importer also declares collided in the clone-name space: both files' `gen2` mangled to one `gen2$Bool`, one overwrote the other, and the module's own body ran the IMPORTER's — 999 where the declaring module standalone answers 111, with `check`, `verify` and `compile` all clean. That is a **false Tier-1**: the module's `ensures(@Int.result == 111)` is proved and then violated at run. The type-discriminating variant is worse in a different direction, collapsing two clones with different WAT result types onto one symbol. Non-generic module functions have never had this problem — `_register_shadowed_import` renames a shadowed one to `mod$$name` — and PRIVATE module generics escaped it only because [#1000](https://github.com/aallan/vera/issues/1000) reroutes them separately, which left the two other qualified-only cases silently wrong: public-but-shadowed (the false Tier-1 above) and public-but-outside-the-import-filter, which was registered in no clone namespace at all and assembled to `unknown func: failed to find name $gen2`. Generic and non-generic now share ONE predicate — a module function keeps the bare name exactly when that name in the importer's flat namespace denotes this very declaration: public, in-filter, and unshadowed — expressed once as `monomorphize.module_qualified_generic_names` and driven by codegen's registration and reroute and by all four of the verifier's mirror sites, replacing four hand-copied classifications with one. That is necessary but not sufficient for the #732 differential, because the two sides feed the predicate from different program shapes: codegen reads the importer's occupied bare names AFTER Pass 0's generic-helper qualification (#1014) and non-generic hoist (#991), the verifier from the pre-transform AST, so a non-generic `where`-helper named `gen2` made the same imported generic bare-name-owning on one side and qualified-only on the other — codegen emitting `gen2$Bool` while the verifier verified `mod$lib$gen2$Bool`, each side's clone uncovered by the other. That input is now a shared derivation too (`importer_occupied_bare_names`), stated over the source shape so it is idempotent across the transforms and both sides reach it from whichever AST they hold; the agreement is pinned by a differential rather than assumed. The reroute also reaches ACROSS modules: a module's bare call can name a generic it imported rather than one it declares — `mid` declares no generics at all and calls `deep`'s — so the per-module set was empty and nothing was rerouted, leaving the entry program's same-named generic to capture the call (verify clean, `mid`'s proved postcondition violated at run, or a silent wrong number where the contracts are loose enough to admit both). Each module's imports are now classified under the same predicate and keyed by OWNER, because `mod$$name` is per-owner. Two conflations surfaced by that reroute are closed with it: monomorphization seeded from the pre-reroute module AST (recording an instantiation for a call that no longer exists), and a `ModuleCall` was discovered by BARE name regardless of its target, so a rerouted `deep::gen(...)` instantiated whichever generic owned the bare name — both emitting a clone nothing calls and the verifier never sees. The proof is the full visibility matrix with the verify verdict and the runtime value asserted together in each cell (a clean verify beside a violated postcondition is the bug, so splitting them across sibling tests would let it hide), against the standalone library as oracle; the per-module both-sides differential covers each qualified-only family and its complement — an unshadowed in-filter generic must NOT be renamed, or [#774](https://github.com/aallan/vera/issues/774)'s bare-call routing would break in silence. The visibility dimension is completed by the same predicate: a module reached only TRANSITIVELY contributes nothing to the entry program's namespace (spec §8.6.4), so all of its declarations are qualified-only there — the classification had been asking `import_names.get(path)`, which answers `None` for such a module, the same spelling that means "wildcard import", and read every one of its public generics as a bare-name owner. Latent rather than live (the checker refuses a bare call to it from the entry, and two transitive namesakes are refused by the E608 collision rail), and corrected for parity, because the predicate is meant to BE §8.6.4 rather than to happen to agree with it wherever another rail is watching. Spec §8.5.2.1 and §8.9.1 now state these rules — which declarations own the importing program's bare name, that the rest are qualified-only, and that a bare call inside an imported body is compiled against ITS module's namespace including across an import of its own. All 258 corpus programs emit byte-identical WAT, before and after the promotion of three new conformance programs ([#1274](https://github.com/aallan/vera/issues/1274)). - **Discovery no longer instantiates a callee at a type VARIABLE.** Inside `forall fn helper`, the call `pick(@U.1, @U.0)` binds `pick`'s type variable to the *name* `U` — a fact about the scope, not a type. Discovery recorded that vector anyway, so a `pick$U` clone was emitted whose parameter has no WASM type and which the compilability pass then skipped with a loud `[E604]` on every generic-under-generic program. The answer was always right, but the noise is what kept [#1223](https://github.com/aallan/vera/issues/1223)'s regression shapes in pytest instead of the conformance suite, since `scripts/check_e602_clean.py` rejects any conformance program that emits one. A name counts as a variable when some `forall` in play binds it — the walk's own enclosing binders, the callee's, and those of every generic discovery is keyed on, which is what also catches a variable arriving through a callee's declared RETURN type (`b(leaf(…), …)` binding `b`'s variable to `leaf`'s `W`) — minus every name that denotes a TYPE here. That subtrahend is the built-in primitives and the removed aliases as well as this namespace's ADTs and type aliases: a binder may legally be spelled `Int`, the language reserving no type name against one, and reading that spelling as a variable poisoned every genuine instantiation at `Int` anywhere in the program — `forall fn idg` beside an unrelated `idw(5)` dropped `main` from the exports of a program that ran before the filter existed. Erring toward "it is a real type" costs only the pre-existing `[E604]` on the pathological template itself. Whether the checker should reject a primitive-spelled binder outright is a language question, deliberately left open. A program whose `data T` shares a binder's spelling instantiates normally by the same subtraction. Components are matched by identifier token, never by substring, so `Option` is phantom while `Unit` is not. The filter lives in the shared `Monomorphizer` walk both codegen and the verifier drive, so it lands once. Its cost is measured rather than assumed: over the corpus, exactly three programs move, each losing only phantom clones (`pick$U`, `pick$V`, `parent$Int$where$inner$U`, `option_unwrap_or$U`) and the E604 warnings they carried, with no clone added and no real instantiation lost — and the emitted WAT is byte-identical everywhere, the phantoms having been skipped rather than emitted. With the noise gone, three of the #1223 shapes are promoted from pytest to conformance programs, which is where the acceptance for this fix was written ([#1271](https://github.com/aallan/vera/issues/1271)). - **A `handle[Exn]` that never completes is lowered as one that never completes.** A handler whose clause body AND handled body both diverge — `handle[Exn] { throw(@Bool) -> { 1000 } } in { handle[Exn] { throw(@Int) -> { throw(true) } } in { throw(5) } }` — inferred no result type from either half and emitted a result-LESS `block` into a context expecting a value: check-green, verify-green, and rejected at load with `type mismatch: expected i64 but nothing on stack`. The fix turns on what `result_wt is None` actually means, which is two different things wanting opposite lowerings: **Unit**, where the block completes carrying no value and a result-less block is exactly right, and **divergence**, where it never completes at all. Only the second may be followed by `unreachable` — doing it for the first would trap a program that runs — so the divergent case is recognized structurally (a block's tail, both arms of an `if`, every arm of a `match`, a nested always-throwing handler, and the `throw` at the leaf in both its bare and `Exn.throw` spellings), conservatively: an unrecognized shape keeps the previous lowering, because the answer authorizes an `unreachable` and a wrong yes is a trap. Whether a bare `throw` IS the operation is read the same way the lowering reads it — unconditional inside a `handle[Exn]`'s handled body, and from the enclosing `_effect_ops` in a clause body, so a program declaring its own `fn throw` is not misread. Each divergent shape is pinned with its Unit twin, which must keep running and must carry no `unreachable` at all; the observable for a both-diverge handler is the OUTER handler's clause value. The MIRROR shape is fixed in the same place and is the one that shows the deciders were reading a branch rather than a join: a clause that throws on one arm and COMPLETES on the other is not divergent, but the WAT result-type inference read only the `then` branch (and only arm 0 of a `match`), saw the throw, answered `None`, and emitted a result-less block that the completing arm then left a value in — `values remaining on stack at end of block`, again from check-green source. Both deciders now take the first branch that yields a type, which is a join over the arms that actually carry one ([#1276](https://github.com/aallan/vera/issues/1276)). - **The divergence predicate asks whether the handler it is answering for is an `Exn` one.** `_handle_exn_always_throws` never inspected the handler's effect, and the structural walk routes EVERY nested handler to it — so a `handle[State]` was analysed as an `Exn` handler and its body evaluated under the claim that a bare `throw` there is this handler's operation, which a handler installing `get`/`put` does not make true. The predicate's answer authorises an `unreachable`, and its docstring states the governing rule: an unrecognised shape answers no, because a wrong yes traps a program that runs. Nothing reached a wrong yes — a lowerable `State` clause body carries a tail `resume(...)`, which the walk reads as non-divergent, so the all-clauses leg failed first — but that made the safety a property of the clause-lowering shape rule rather than of the predicate. The guard is one line in the conservative direction, and all 255 corpus programs emit byte-identical WAT either side of it (PR [#1283](https://github.com/aallan/vera/pull/1283) review). - **`old()` over a non-`State` effect is a diagnostic, not a traceback.** `ensures(old(IO))` is `check`-green — the checker types `old(E)` as an unknown type, which satisfies a `Bool` postcondition — so it reached codegen, where `state_type_arg` raises a codegen-invariant error for an effect reference that is not `State`. The `_compile_fn` boundary that wraps the old-state snapshot caught the skip exception alone, while the precondition and `decreases` boundaries either side of it catch both, so this one raise escaped as a raw Python traceback from `vera compile` on a program `vera check` accepts. That is the [#939](https://github.com/aallan/vera/issues/939) gap one boundary over, and it is closed the same way: one loud `[E699]` naming the function and attributing the fault to the compiler. The three sibling nets have to be driven by monkeypatching an unreachable guard; this one has a source-level reproduction, which is why it went unnoticed — the raise it fires is marked unreachable-by-construction and is not (PR [#1283](https://github.com/aallan/vera/pull/1283) review). - **An out-of-range literal keeps exactly one verdict — never zero.** A contextual range verdict SUPERSEDES the unconstrained guess for the same literal, withdrawing the earlier diagnostic while deliberately keeping its duplicate-collapse key so the withdrawn message cannot reappear from a third synthesis. Those two mechanisms compose into a hole: a contextual verdict rendering a byte-identical message is collapsed as a duplicate and appends nothing, and the withdrawal has by then removed the only verdict the literal had — the [#812](https://github.com/aallan/vera/issues/812) gate opening silently on the value it exists to catch. No `.vera` input reaches it today, because the one provisional-to-contextual transition the checker can currently take re-renders the message (`` `@Byte` ``'s 0..255 superseding `` `@Nat` ``'s u64 bound), and a 255-program corpus differential confirms the emitted diagnostics are byte-identical either side of the fix. It is repaired rather than left latent because the next contextual verdict is what would open it, and the invariant — one verdict per literal, never zero — is the thing that has to hold then (PR [#1283](https://github.com/aallan/vera/pull/1283) review). - **One builder for multi-module test fixtures, and it does not leak.** Six test files across the checker, codegen, monomorphization, naming and refinement suites carried independent copies of the same temp-file `ResolvedModule` pattern — eleven definitions in all, since four were re-declared inside individual test bodies. They had drifted: one wrote its temp file and never unlinked it, leaking one per fixture it built, and the cleanup added for that had to be sequenced as well as present: `delete=False` means the file outlives its `with`, so a failed write must still remove it, yet Windows cannot delete a handle that is still open — an unlink in an `except` inside the block is `WinError 32` there. One cleanup site, after the block closes the handle, satisfies both. `tests/module_fixture_helpers.py` now holds the two shapes that were being conflated — `resolved_module` parses a real temporary file and deletes it before returning, so what the module keeps is on-disk parse provenance under a realistic absolute path, while `fake_resolved_module` parses in memory under a deliberately synthetic `/fake/` path. Neither leaves a file behind, and neither `file_path` can be opened afterwards; nothing downstream reopens it. TESTING.md points at both from the Windows-portability rules they implement ([#1228](https://github.com/aallan/vera/issues/1228)). - **The temp-file leak check names the file it is about.** `test_a_failed_write_leaves_no_temp_file` snapshotted `gettempdir()` for `*.vera` either side of the call it was measuring — a directory that under CI's `pytest -n auto` several xdist workers are writing their own fixtures into, so a sibling worker's `tmp*.vera`, alive for the microseconds between the snapshot and the assertion, counted as litter this test had left. It did: an `AssertionError` over a `C:\...\tmp*.vera` on worker gw1 in the v0.1.10 release push, an hour after the identical tree passed on the same commits, and reproducible on macOS at roughly one run in three under a second writer. The evidence is now the ONE path the call created, captured from `NamedTemporaryFile` the way the sibling ordering test captures handles, so no shared directory is read at all — the race is removed rather than relocated to a private `TMPDIR`, which would still assert over a directory and would additionally have to reset `tempfile`'s cached `tempdir` for the environment override to take effect. A `len(created) == 1` guard keeps the check from passing over an empty list should the builder ever stop creating its file that way, and the failure message names the stranded path. Test-only: the cleanup contract it pins held throughout, as a stranded file of this test's own would have raised from the builder's `finally` and displaced the `TypeError` the test asserts. - **`examples/file_io.vera`'s run artifact is ignored where it actually lands.** The example writes the relative path `hello.txt` and the host resolves it against the process CWD, so the documented `vera run examples/file_io.vera` from the repository root drops it in the ROOT — which is where a `git add -A` twice picked it up — and only a run from inside `examples/` puts it there. The ignore rule named the second path alone; both are covered now, and the comment states the mechanism rather than a fixed location. A sweep of the corpus found no other example writing a relative path, and `pytest tests/` from the repository root leaves the tree clean ([#1240](https://github.com/aallan/vera/issues/1240)). - **A relative-path document keeps its imports.** The path-less isolation rule keyed on "the parent directory is `.`", which is true of an `untitled:` buffer and equally true of `entry.vera` — a real file whose directory happens to be the process working directory. So the rule that stopped a path-less document borrowing the CWD's modules also took the siblings away from a genuine relative-path one. `is_file()` separates the two: a document that exists on disk resolves against the directory it actually lives in, whatever spelling names it ([#1246](https://github.com/aallan/vera/issues/1246)). - **The module-aware check's errors reach the editor.** `analyze` type-checks module-blind and then calls `verify_source`, which type-checks module-aware — and only the second sees the resolver's errors and anything that needs an imported signature to detect. Those came back as `check_diagnostics` and were discarded, so `glib::takes_int("nope")` published a warning, produced no obligations, and gave no sign that an `E202` had stopped verification; a missing import lost its `E012` the same way. They are appended now, minus what the blind check already reported — the module-aware pass re-derives those, and a straight append shows each twice ([#1246](https://github.com/aallan/vera/issues/1246)). - **`uri_to_path` survives a malformed URI.** `urlsplit` raises `ValueError` on a bad authority (`file://[` is "Invalid IPv6 URL"), and the raise happened in the split itself — before the guards that make the conversion total, on the same didOpen/didChange path the `URLError` escaped from. A URI that malformed names no path, so it takes the same opaque route as the other four cases ([#1246](https://github.com/aallan/vera/issues/1246)). - **A document with no path on disk no longer resolves its imports against the server's working directory.** The warm session rooted a `ModuleResolver` at `Path(file).parent` whenever it was given a `file` at all — and a document that names no location gives `.`, the process CWD. So an `untitled:` buffer, a non-`file:` URI, a degenerate `file:` or `file://`, or an empty label searched for imports wherever the language server happened to be started, and a measured probe pulled an unrelated `glib.vera` out of that directory into a document that had never referred to it. Pre-existing rather than a regression, and never silent — the imports it did NOT find still warned (`E230`) — but what it did find, it used. The resolver is now built only when `file` names a directory that exists and is not `.`; anything else is analysed alone, and its relative imports, which cannot meaningfully resolve, say so through the same `E230`. The rule lives at the session so both `verify_source` callers get it, rather than one call site getting a special case. Returning the URI unchanged from `uri_to_path` does not achieve this on its own and the earlier note claiming it did was wrong: `Path("file:")` is exactly as directory-less as `Path("")` ([#1246](https://github.com/aallan/vera/issues/1246)). - **`vera/speculativeEdit` compares two streams keyed the same way.** The proof delta diffs a speculative run against the baseline in `server.analyses[uri]`, which `analyze` produced — and `analyze` was moved onto the document's PATH earlier in this cycle while `speculative_edit` kept passing the raw URI. An obligation's identity includes its file, so the two sides shared no keys: an identical-text edit reported `unchanged: 0` and every obligation `removed`, which is the delta saying an unedited document lost all its proofs. Both callers now convert. The suite did not catch it because its baseline was built by a second `verify_source` call spelling the document the same way the speculative side did — agreeing with each other and with nothing in production; the fixture now goes through `analyze`, as the server does ([#1246](https://github.com/aallan/vera/issues/1246)). - **The LSP analyses the document the URI names, not a directory called `file:`.** The server hands `analyze` the LSP document URI, and the pipeline uses its `file=` as a path — the module resolver reads imports from `Path(file).parent`, which for `file:///a/b.vera` is the literal directory `file:`. So a document with imports resolved none of them, and said nothing: `verify_source` returns its resolver errors as `check_diagnostics`, which the analyser does not collect, so the editor showed no error, no obligations and no tier hints for any multi-module program — silently unverified, and contradicting LSP_SERVER.md's "module imports resolve from disk". `uri_to_path` is now the fourth conversion at that boundary, beside the three coordinate ones, and the `Analysis` carries both spellings because they answer different questions: the URI is what the client is told (a `textDocument/definition` Location must carry one), the path is what the compiler was told, so it is what an obligation's `file` is comparable against. The conversion is total, because every caller is on the didOpen/didChange path where a raise escapes the request handler rather than becoming a diagnostic: a URI naming no path this process can open comes back unchanged, as the opaque label the pipeline carries but never opens. That covers a non-`file:` scheme (`untitled:`, a virtual filesystem — what an unsaved buffer needs), an authority naming another host, a degenerate URI decoding to the empty string, and anything `url2pathname` rejects. The authority case is decided here rather than left to the standard library so the answer does not depend on the interpreter: Python 3.14 validates it and raises `URLError` for anything but localhost, where 3.13 returned a `//host/...` string that on POSIX is a stray local path rather than a UNC mount — wrong on both, and on 3.14 it took the editor request with it. Scheme matching is case-insensitive per RFC 3986 §3.1 ([#1246](https://github.com/aallan/vera/issues/1246)). - **The LSP's tier hints stop at this document's edge.** Verifying an entry program verifies the generics it imports, so the obligation stream carries the imported module's functions beside the entry's own — and the hint synthesis placed every one of them at its obligation's line number IN THE CURRENT DOCUMENT. Those line numbers index the module that declared the function, so an editor showed `glib::pick: Tier 1` sitting on whatever line of the open file shared the number, or past its end. Nothing could tell them apart until [#1239](https://github.com/aallan/vera/issues/1239) put the file on `ProofObligation`; `publishDiagnostics` is per-URI, so a foreign obligation is not this document's to publish under any line and its hint is left to the document that owns it. An obligation with no file at all is a record built outside a verifier run, not a foreign one, so it keeps its hint ([#1246](https://github.com/aallan/vera/issues/1246)). - **One integer literal gets one range verdict.** A refinement predicate's operands are synthesised twice — once with no expected type, once against the refined base — and the two passes answer with different bounds, so `{ @Byte | @Byte.0 < 18446744073709551616 }` drew TWO `E149`s for one literal, naming `@Nat (u64)` and `@Byte`. The existing duplicate collapse could not see it: the messages differ, which is the problem rather than the exception. The unconstrained pass is a guess — it reports u64 because nothing told it the target, not because u64 is the target — so a contextual verdict now supersedes it and the guess is withdrawn. The reverse never happens, and where the unconstrained verdict is the only one it stands, which is the [#812](https://github.com/aallan/vera/issues/812) gate itself. Superseding is keyed on the literal's OCCURRENCE, so two `999`s in one call still report two errors (PR [#1282](https://github.com/aallan/vera/pull/1282) review). - **An out-of-range `@Byte` literal names the range instead of the mismatch it caused.** `@Byte` is a target machine type like `@Int` and `@Nat`, but the literal range check knew only the i64 and u64 bounds: a `@Byte` context accepted 0..255 and let everything else fall through to `@Nat`, so the literal's own mistake was described by whichever downstream mismatch the `@Nat` then produced. `let @Byte = if c then { 999 } else { 3 }` reported E301 "then-branch is Nat, else-branch is Byte" — a true statement about a consequence, in a program whose actual error is that 999 is not a `@Byte` (the expectation IS pushed into both branches, which is why `3` typed as one). A differential against the previous behaviour measured the radius: eight shapes reroute to `E149` — a function body (E121), a `let` (E170), an `if` join (E301) and a `match` join (E302), a call argument and a `where`-helper argument (E202), an effect-operation argument (E204), a handler-state initialiser (E331), a refined `@Byte` base and a `@Byte` alias — and each names 0..255 at the literal. Where a join has MORE than one out-of-range literal the diagnostic count rises rather than falls: `if c then { 999 } else { 888 }` was one message about the branches disagreeing and is now one error per offending literal, which is the number of mistakes the program actually contains. The count also rises outside joins, where the literal check now precedes another gate that used to short-circuit it: `g(999)` against a two-parameter `g` reported the arity mismatch alone and now reports the range violation beside it, two true statements neither of which implies the other. Where nothing supplies a target type at all — an unresolved callee — there is no `@Byte` verdict to add and the diagnostics are unchanged. The literal keeps the type its context asked for, so no cascade follows it. Positions that do not push the `@Byte` expectation into the literal at all — an array-literal element, a constructor field, a `Map` value, a generic instantiation — are unchanged and still report their own mismatch ([#1252](https://github.com/aallan/vera/issues/1252)). - **Codegen's ADT membership is the module's, not the whole program's.** `_adt_layouts` is one map across every absorbed namespace, and the naming environment's `data_types` set was derived from all of it — so inside `_module_alias_scope(blib)` a sibling module's ADTs were still members of `blib`'s namespace, while the checker registers each module in isolation and never sees them. For a `blib` signature `fn bcount(@Array, @Array -> @Int)` where `Float` is an ADT `alib` declares and `blib` never imports, the two sides render the same declaration differently — checker `['Array', 'Array']`, codegen `['Array', 'Array']` — which is the [#1213](https://github.com/aallan/vera/issues/1213) disease as a membership question: a name minted one way and looked up another misses silently. Membership now derives from the owning namespace plus that namespace's OWN imports — public only, and only the names an explicit import list mentions, which is exactly the checker's view — so an unimported sibling ADT is as opaque to codegen as it is to the checker, and so is a PRIVATE one (the visibility dimension appended to the issue: codegen registers a module's private ADTs (#1008) where the checker registers only its public ones). Imports are read per namespace and never inherited, because §8.6.4 visibility is a property of the importer: a module reached transitively from the entry program is a DIRECT import of whichever module names it, and holds what THAT module's import list allows. The proving check is a differential over the two slot tables rather than an assertion on either, since both sides agreeing on the wrong name would satisfy a one-sided check; each case additionally pins the value the checker derives. The positive control — an ADT the module does import, which renders `Array` on both sides before and after — is what separates scoping the membership from erasing cross-module ADTs. Global INFRASTRUCTURE — the names no namespace declares — is derived by subtraction rather than snapshotted, because the built-in ADTs registered before module registration are only half of it: `Json`, `HtmlNode`, `Request` and `Response` are registered by the prelude injection two passes later, and a snapshot taken at module-registration time necessarily missed them while the checker's `TypeEnv` had carried them from the start. That miss was inert at the one consumer `AliasEnv.data_types` has (`naming._resolve_named` changes its answer only for `Decimal` and the `REMOVED_ALIASES` entry `Float`), and the whole corpus compiles to byte-identical WAT with and without the correction — what it fixes is the set being factually right for whatever reads it next, which is asserted directly rather than through a rendering that cannot distinguish it. The E609 rail is unchanged and still refuses two modules declaring one ADT name outright, so the owner-slot ordering half of the visibility case stays masked behind it ([#1253](https://github.com/aallan/vera/issues/1253)). - **`vera check` gives one verdict, whatever file it was given.** `mid.vera` imports only `cap` from `deep` and its body also calls `other`. Checked directly, that was an E200 — correct, spec §8.5.1: a module's bodies resolve in the namespace ITS file declares and imports. Checked as a dependency of `main`, the same program was accepted in silence, because the importer only REGISTERED each module — harvesting what it declares — and never checked its bodies at all. The lenient verdict is the dangerous one: it lets a module use names it never imported, and codegen then resolves them out of the importer's flat namespace, so the program runs on a binding the module was never entitled to. The verifier has honoured the module-local rule regardless of entry point since [#1225](https://github.com/aallan/vera/issues/1225); this is the checker catching up. Each resolved module's bodies are now checked under that module's own import filter, in a fresh checker given the module's own imports (with `direct` re-derived against it, since §8.6.4 visibility is a property of the importer), memoised by path so a module reached from two importers is checked once and an import cycle terminates. The blast radius was measured before implementing and is ZERO: all 210 conformance programs and 42 examples are unchanged, and only 7 corpus programs import a non-stdlib module at all. One existing expectation moves, and toward the stricter reading — a module redeclaring the built-in `IO` effect and calling `IO.print(a, b)` now reports E152 *and* the E203 arity error, exactly what `vera check` on that module reports standalone; E203 names the CANONICAL built-in's arity, so the property that case was written for (the rejected block is not registered) is now asserted directly instead of by absence. The same change closes the issue's second shape — a module body binding an `@Int`-returning call to a `@Bool` slot, which checked clean through an importer, verified Tier-1, and failed at compile — and it removes a duplicate: `_collect_module_artifacts` surfaced imported-body ERRORS on the codegen paths only, and now collects artifacts alone while the diagnostics come from the one pass that runs everywhere ([#1244](https://github.com/aallan/vera/issues/1244)). - **An imported clone's body calls its OWN module's functions.** A public generic in `glib` whose body calls `glib`'s private `need` is cloned by the importer and compiled into the importer's flat module — and both consumers of that clone body then resolved the bare name `need` in the IMPORTER's namespace. The verifier's `_scoped_fn_lookup` fell through to this program's registry, because `_declaring_module_scope` swapped the naming env, the source buffer and the file name but not the function registry; codegen's clone-emission door was the one door that did not thread the module's intra-rename map, so a bare sibling call landed on the importer's same-named function instead of the module's own `mod$…` emission. The checker's answer is the module's own (spec §8.5.1), proven by a type-discriminating probe: `glib`'s `need(@Int -> @Int)` beside an importer's `need(@Int -> @Bool)` checks clean, which only types if `glib`'s is meant. The measured cost was a module that verifies clean and runs correctly on its own being REFUSED through an importer (its honest `ensures(@Int.result == 111)` refuted against the importer's function, which returns 999) and, once compiled, trapping on that same postcondition — with the type-discriminating pair emitting invalid WASM (`expected i64, found i32`) from check-green source. The two halves are one routing rule and had to land together: fixing the verifier alone converts today's aligned-but-both-wrong state into a false Tier-1, measured directly — with only the registry pin applied, `vera verify` reports clean and the compiled program raises `Postcondition violation in gen$Int … ensures(@Int.result == 111) failed`. Every case in `tests/test_clone_body_declaring_module_1241_1243.py` therefore asserts the verify verdict and the runtime value in one test, against an oracle taken from the module verified and run STANDALONE, so neither half passes on its own; the unshadowed-callee control, which was correct before and after, is what pins the defect to the shadowed name rather than to cross-module calls in general ([#1241](https://github.com/aallan/vera/issues/1241), [#1243](https://github.com/aallan/vera/issues/1243)). - **A generic `where`-helper under a generic parent instantiates its own generic callees.** `forall fn parent` carrying a `forall fn helper` is the [#1002](https://github.com/aallan/vera/issues/1002) shape, and both sides already instantiated the HELPER per its concrete call sites — but neither then walked the helper CLONE's body. A top-level generic called from inside the helper was therefore discovered only in its still-generic spelling, where the argument's type is the enclosing type VARIABLE, so what got emitted was `pick$U` (a clone whose `@U` parameter is not a WASM type) while the call-rewrite asked for the concrete `pick$Bool`. On a check-clean, verify-clean program that is an E602 skip, an E620 drop of the parent, another of `main`, and `vera compile` ending with "No exported functions" — the whole program compiled to nothing. The same helper under a NON-generic parent has always worked, because the ordinary worklist rescans every clone it emits; the generic ancestor was the one path that produced clones outside it. Hoisting and the top-level worklist are now a FIXPOINT rather than two phases: each hoisting round re-seeds the worklist from the bodies it just produced, and any clones that yields go back through hoisting for their own where-trees. The review of that fix surfaced the SIBLING case in the same walk and it is fixed here too — a helper's concrete clone can call a sibling helper at a type only that clone knows, so the helper family is now discovered as a family over a GROWING body set (each clone fed back in) instead of one helper at a time against its still-generic siblings. Both sides drive one leaf for it (`Monomorphizer.collect_generic_helper_instances`), which is what keeps the verifier's recorded set and codegen's registered set from parting: the codegen half alone makes `pick` an instantiation codegen emits and the verifier never checks — a false Tier-1 — so the shape is in `tests/test_monomorphize_differential.py`'s inline corpus, where that check goes red until the verifier half lands with it. `tests/test_generic_under_generic_callees_1223.py` carries the user-generic, prelude-generic and two-level-nesting shapes with a registered-vs-resolved differential, plus the non-generic-parent control that was green before and after. One residual is left as-is and reported: the still-generic spelling's `pick$U` clone is still emitted and still loudly skipped (E604), which is noise on an otherwise clean compile rather than a wrong answer ([#1223](https://github.com/aallan/vera/issues/1223)). - **An effect operation that fixes a generic's type argument names ONE clone.** `pick([get(()), 4], 9)` inside `handle[State]` compiled to nothing: monomorphization discovery had no effect-operation registry at all, so the `get(())` driving the instantiation fell through to the literal-driven `Int` default and emitted `pick$Int`, while the WASM call-rewrite read the cell's type from `_effect_op_result_vera` and called `pick$Nat`. The clone dangled, the caller was skipped with E602, `main` was dropped with E620, and `vera compile` ended with "No exported functions" on a check-green, verify-clean program — reachable from a `State` handler with no unusual syntax anywhere. The two consultors now read ONE table: `vera.slots.effect_op_result_names` derives op name → Vera result-type name from an effect reference, and all three sites that need that answer take it from there — codegen's per-function registry built from the declared `effects(>)` row, the handler-expression registry inside `_translate_handle_state`, and mono discovery's new scoped walk. Scope is mirrored rather than approximated, because codegen's two injection sites are scoped differently and a registry that ignored the difference would move the desync rather than close it: a `handle` installs its table over its BODY only — the state-init expression and the clause bodies belong to the enclosing context (#1211), so an operation there still names the outer cell — and an operation the DECLARED row provides is not injected at all when a user function already owns the name, which discovery now decides from the same function table (`_fn_sigs`, mirrored into `MonoContext.fn_names`) that codegen's `_effect_ops` guard consults. The name is the alias-OPAQUE source spelling on both sides, so `handle[State]` with `type Count = Nat` instantiates `pick$Count`: resolving the alias on one side only would have dangled exactly as before. The proving check is a differential over the two consultors, not a unit test on either — the compiler's own E602 IS the two sides disagreeing — and each case additionally pins WHICH name they agreed on, so an alignment on the wrong one fails. New conformance program `ch07_state_op_generic_instantiation` (suite now 207) carries the array-element, direct-argument and declared-effect-row forms end to end, with the clone-name differential and the shadowed-name control in `tests/test_mono_effect_op_naming_1207.py`; the `tests/probes/` `p15*` trio it promotes is retired ([#1207](https://github.com/aallan/vera/issues/1207)). - **A parameterised type alias APPLIED gets the return guards its plain spelling already had.** `type Ident = T; type Count = Ident;` used as a return type got NO runtime guard at either return boundary, and `f(0 - 5)` returned **-5** through the `` `@Nat` `` slot — the same silent negative #983 closed for `type Count = Nat`, reached one spelling over. Silently WRONG rather than merely unguarded: called directly at the export (`vera run --fn f -- -5`) the negative is the returned value, with no trap and no diagnostic, so any caller-side guard that happens to sit in front of it is masking rather than preventing. Both codegen gates chased the alias by NAME (`_resolve_base_type_name` over the slot name), which drops an application's type arguments and answers the bare head `Ident`; neither `` `Nat` `` nor `` `Int` `` matched, so both gates stayed shut while the verifier's alias-resolving 7c/7d gates went on obligating the narrowing. **This changes when a program traps**: an alias-of-`` `@Nat` `` return whose body can be negative, and an alias-of-`` `@Int` `` return whose body is `` `@Nat` ``, now carry the guards the unparameterised spellings carry — the guard fires only on values that actually violate, so a correct program's results are unchanged. Both gates now ask the same representation-base derivation as the `throw` payload and the `apply_fn` signature, which resolves the type EXPRESSION rather than chasing its head. The refinement exclusion beside the narrowing gate is a separate conjunct and is untouched: an alias to a refinement still takes its single §2.6.5 boundary guard rather than a second one. One conformance program's base spelling moves (`ch02_refinement_base_param_alias`, `T` to `Nat`) and is inert for exactly that reason; no corpus program gains or loses a guard, and all 254 emit byte-identical WAT ([#1256](https://github.com/aallan/vera/issues/1256)). - **A `` `@Byte` `` payload can be thrown.** `throw(5)` under `effects(>)` with `type Small = { @Byte | @Byte.0 < 10 }` emitted a module WebAssembly rejects at load — `type mismatch: expected i32, found i64`. Root-caused rather than assumed: the `Exn` TAG was already declared at the payload's resolved representation width, i32, so the width derivation was never wrong; the thrown literal simply defaulted to `i64.const`, because `throw`'s payload had never been registered as one of the `` `@Byte` `` write boundaries. It is one now, through the same marking every other boundary drives, which means the branch spelling (`throw(if c then { 5 } else { 3 })`) is covered by the same descent rather than by a second test. What the payload boundary needed to ask that question was the cell's REPRESENTATION name, so `throw` now carries a `CellNames` beside its dispatch target exactly as the `State` ops do — from the declaration's effect row and from the `handle[Exn]` injection alike, the two registration paths a `throw` can reach. The qualified `Exn.throw(v)` delegates to the bare dispatcher for the same reason `State.put(v)` does: the marking lives there, and a spelling that skipped it emitted the i64 while the bare one did not. The addressability gate that refuses a shadowed `State` op now asks whether the op IS a State op rather than whether it has a cell recorded — the same question only while `State` was the sole cell-carrying effect, and `throw` under `Exn` inside a `State` clause body would otherwise have compared equal to a pushed cell family and been refused. The #1268 runtime GUARD on this payload is a separate obligation and is still open; this change threads the metadata it will want. No conformance or example program moves — all 254 corpus programs emit byte-identical WAT, measured by differential ([#1269](https://github.com/aallan/vera/issues/1269)). - **An `apply_fn` call site takes its signature from the closure's type, not from its argument.** `apply_fn(fn(@Byte -> @Int) effects(pure) { byte_to_int(@Byte.0) }, 200)` type-checks, verifies, compiles, and then traps: `wasm trap: indirect call type mismatch`. Both sides of an indirect call must name ONE type, and they were deriving it from two different things — the lifted closure's own signature from the declared formals, the `call_indirect` type from each argument's inferred width — so an int literal (i64) into a `` `@Byte` `` formal (i32) registered two incompatible `$closure_sig` types for a module holding one closure and one call. The RESULT has come from the closure type since #630; the parameters now come from the same place, through the same alias-resolving walker, so a formal spelled through a refinement or an alias resolves as its base does. The argument then coerces at the boundary rather than dictating it — the declared formal is a `` `@Byte` `` write boundary like any other, marked before translation, which covers the literal and the `if`/`match` join spelling together. The argument-inferred width survives only where the formal is genuinely unrecoverable (a slot reference whose alias chain reaches no function type). All the #1212 boundaries that hold a type expression now resolve their representation base through ONE derivation rather than each composing its own two hops. No corpus movement — byte-identical WAT across all 254 programs ([#1256](https://github.com/aallan/vera/issues/1256)). - **Pointer-ness resolves through the alias chain.** A closure returning `` `@SmallByte` `` under `type SmallByte = { @Byte | … }` pushed the Byte VALUE onto the GC shadow stack as a root, because the classification received the SYNTACTIC type head and `SmallByte` is in neither the scalar set nor the host-handle set. The same head-string test stood at the closure parameter, return and capture, and at the two named-function twins. The issue counted eleven sites; six of the eleven, all in `vera/wasm/`, turn out to receive a name their own producers have ALREADY resolved — measured, not argued, by recording what each classification is handed for a refined-Byte program, where the array-element deciders are passed `Byte` and never the alias — so there is nothing at those six to fix. The five that were genuinely syntactic now ask `is_gc_pointer_base`, which states the rule once and requires a representation base, over the same resolution the width decision at each site already performs. The reliance the inertness rested on is closed rather than documented away: the GC heap's start address is guarded at build against the inline i32 scalar range, and the two constants that create that margin — a 16 KiB shadow stack and a 64 KiB mark worklist, both fixed — are named so the argument can be read where the layout is computed. The exposure the issue named, a build with an empty string pool, is now a test. No corpus movement — byte-identical WAT across all 254 programs, against a differential shown to move 23 of them when the rule is mutated ([#1255](https://github.com/aallan/vera/issues/1255)). - **`throw`'s payload is obligated like every other narrowing site.** `throw(0 - 5)` under `effects(>)` verified at 4/4 Tier 1 with zero obligations, and `vera run` returned **-5** out of the `` `@Nat` `` payload: check-green, verify-green, silently wrong. The contrast that localises it is that a *declared* effect's op argument in the same position was already loud — `op emit(Pos -> Unit)` with `Log.emit(0 - 5)` is an E505 — so the shared machinery covered effect-operation arguments generally and `throw` alone bypassed it. Not `` `Never` ``, as first supposed: `throw` is a bare call with no function-registry entry, so the loop that obligates a call's arguments never saw it, and the table-driven fallback added for the same hole at the State `put` was keyed on that one name. Keyed on the STRUCTURE instead — does this name resolve to an effect operation — because a name list is a claim about every name not on it, and listing the two built-ins left a bare user-effect op spelled `put` loud while the identical narrowing through one spelled `emit` stayed silent, contradicting both spec §2.6.4's site list and the `KNOWN_ISSUES` row this change edits. With the gate structural, `throw`'s payload now takes the ordinary refined/`` `@Nat` ``/widen triple — which means the concrete-literal gate above reaches it for free, so `throw(200)` into an `Exn<{ @Byte | @Byte.0 < 10 }>` is a rejection naming the value. Guardedness is COMPUTED from the operation's parent effect rather than assumed, and `throw` comes out unguarded: it lowers straight to `throw $exn_` with the payload on the stack, and nothing on that path checks it — measured by run, not inferred. So an undischargeable payload is disclosed `tier3_unguarded` and excluded from the totals rather than claiming a runtime check that is not emitted, which puts it in the same disclosed class as the user-effect operation argument (#754) — whose enumeration in the E504 and E531 rationales, stale at "the sole unguarded case" while already naming one of two, is re-derived from which callers can pass `guarded=False` rather than from which one motivated the code. Emitting that guard is the residual and is NOT in this change ([#1268](https://github.com/aallan/vera/issues/1268)). No conformance or example program moves: the new branch is entered 12 times across the corpus and correctly produces no obligation at any of them, every corpus `throw` passing a value already of its payload's type. - **A refinement over an unmodelled base decides a literal instead of disclosing it.** `handle[State](@Small = 200)` with `type Small = { @Byte | @Byte.0 < 10 }` ran to completion and returned 200 — a value the cell's refinement forbids — because `` `Byte` `` is not one of the five bases the verifier models, so `200 < 10` was never asked. Naming that cause honestly was one half of the issue; this is the other half, which asks the question. **It changes when Vera rejects a program**: a literal narrowing whose predicate evaluates to false is now an `E505` error naming the value, at every binding site the obligation covers — `let`, call argument, constructor field, tuple component, effect-operation argument, match binding, array element, handler state init and update, `State`-op resume — and a literal the predicate admits is a Tier-1 proof where it used to be a Tier-3 disclosure. The gate is the VALUE, not the base: widening the base itself was measured to turn `ch02_byte_refinement`'s four boundary narrowings — symbolic `` `@Byte` `` values that codegen runtime-guards — into false rejections, so anything that does not reduce to a Z3 literal keeps exactly today's runtime-guarded disclosure, and so does a predicate whose operands the verifier models by something other than evaluation (`{ @Byte | ident(@Byte.0) < 10 }` does not fold even for a literal). Assumptions are not consulted for the PREDICATE, because nothing a `requires` can assert changes the value of a literal; what they do decide is whether the site runs at all. A binding obligation is conditional — if control reaches here, the value satisfies the predicate — so premises no state satisfies discharge it vacuously, which is how a base the verifier DOES model has always treated `let @Pos = 0 - 5` inside `if @Int.0 < 0` beneath `requires(@Int.0 > 0)`. A false fold therefore asks the one remaining question, whether the site is reachable at all: unreachable discharges, reachable is the E505, and a solver that cannot settle reachability gets a disclosure rather than a refutation nothing backs — one that names the value, so the reader knows the predicate WAS decided, and names the reachability question as what was not, rather than reporting no decision "on the obligation" when the obligation is the one thing that got an answer. The satisfied direction needs none of that — a predicate true of the value is true under any premises — and the asymmetry is the point. The diagnostic follows: it states the value, says the predicate was evaluated on it directly, and asks for a value the predicate admits or a wider refinement, rather than repeating the precondition advice that cannot help here. The refined RETURN position is a separate code path and is unchanged; it is runtime-guarded, so a literal there traps rather than running on. No conformance or example program moves — verdicts, tier counts, per-obligation statuses and diagnostic text are identical across the whole corpus, which contains no concrete narrowing over an unmodelled base; the gate is reached three times there and correctly declines all three. Spec §2.6.4 records that a concrete narrowing is decided whatever the base, and §2.6.5 no longer offers a literal as an example of something the runtime guard catches ([#1251](https://github.com/aallan/vera/issues/1251)). - **`E531` and `E504` say why they demoted too.** Both rationales read "outside Z3's decidable fragment (untranslatable or the solver timed out)" for every demotion — the same conflated sentence that came out of `E506`, in two obligation families that had no reason plumbing at all. Neither half of it describes the site `E531` is actually reached from: `Some(@Nat.0)` into an `Option` field translates fine and the solver answers promptly, twice; the value is simply unconstrained, so `<= i64.MAX` and `> i64.MAX` both keep countermodels, which is a fact about the program (bound the value) and not about the solver. `E504`'s reachable site is the other cause outright — a value that never translated, so no solver call was made to time out. Each family now threads its own cause into its disclosure, and the two draw the non-verdict split from the SAME derivation rather than a parallel copy: it is renamed `_undecided_reason` and worded for the obligation instead of for one family's predicate, since three families share it. The reason is required on the leg that emits a disclosure and refused by a raise when missing — the guarded leg emits nothing, so text there would be dead — and both structural pins now range over all three recorders: no demotion may hardcode a solver-outcome reason, and no call that is not literally `guarded=True` may omit one. No corpus diagnostic changes text, neither code occurring in any conformance or example program (recorded on [PR #1247](https://github.com/aallan/vera/pull/1247) review; [#1251](https://github.com/aallan/vera/issues/1251)). - **An E506 says why it demoted, and is right about it.** Both Tier-3 refinement emitters carried one fixed rationale — "The refinement predicate is outside Z3's decidable fragment (a non-primitive base such as Array, an undecidable construct, or a solver timeout)" — for causes that are not the same thing and do not call for the same response. On `handle[State](@Small = 200)` with `type Small = { @Byte | @Byte.0 < 10 }` that sentence is simply false: `200 < 10` is decidable, and decidably FALSE. What actually happened is that `Byte` is not one of the five bases the verifier models as a refinement base, so the predicate was never handed a value to reason about. A reader following that rationale would rewrite a predicate that was never the problem, which spec §0.3 forbids a diagnostic to cause. Each of the thirteen demotion sites now states its own cause, drawn from ten distinct texts: the value did not translate, the base is unmodelled (named, e.g. `` `Byte` `` or `` `Array` ``), the predicate is outside the fragment, the closure body is opaque, the scrutinee or the destructure source could not be projected, and the two non-verdicts. The two return-position sites, which folded "the body did not translate" and "the predicate did not translate" into one bail, now distinguish them; and three sites reported the `opaque` outcome — a countermodel ranging over an effect-operation stand-in, which the solver reaches promptly — as a timeout that never happened, so the two non-verdicts are now told apart at the one derivation every site draws from. That derivation handles each outcome by name and raises on anything else, rather than letting an unnamed status inherit the no-decision text: the point of a single derivation is that nothing is absorbed silently, and `SmtResult`'s annotation is corrected to the four outcomes `check_valid` actually produces (it listed an `unsupported` that no code path constructs). All six E506s in the corpus change text and five change meaning; no verdict, tier or obligation count moves. Attempting the decidable check where the base is unmodelled is the other half of the issue, and lands in the same release behind a concrete-value gate of its own: a symbolic `@Byte` narrowing must keep its runtime-guarded disclosure rather than become a false rejection ([#1251](https://github.com/aallan/vera/issues/1251)). - **A Unit literal renders as `()` wherever an expression is quoted back.** `format_expr` had no `UnitLit` arm, so every Unit literal fell to the `` catch-all and a call spelled `mk(())` described itself as `mk()`. Two calls differing only in a Unit argument were then indistinguishable by description (their spans still differed). That renderer feeds four surfaces, not one: `verify --json` obligation descriptions, E505 message text, the `vera test` skip reason and its E701 warning, and the codegen trap strings baked into the data section (`codegen/contracts.py`, `codegen/closures.py`) — so the same `` was compiled into shipped binaries. A missing arm there never fails; it silently degrades every mention of the construct, which is why the fallback is the wrong place to learn about it. Across the corpus this moves 167 obligation descriptions in 37 programs from the catch-all to the real text, and 28 of those programs are left with no `` at all. The emitted code is unaffected: all 217 corpus programs that compile produce byte-identical WAT with and without the arm, measured by differential ([#1248](https://github.com/aallan/vera/issues/1248)). - **A zero-size call argument no longer collapses the verifier's call summary.** `mk(1)` proved a violating refined destructure of the result — a loud E505 — while `mk(())`, the same function with a `@Unit` parameter, demoted the SAME obligation to an unguarded Tier-3 E506. `@Unit` has no Z3 sort, so the argument translated to `None` and the call translator read that as "this call cannot be modelled at all", dropping the summary entirely. A zero-size type has exactly one value: an argument in that position tells the summary nothing the signature did not already say, and can never be a reason to know *less*. Such formals are dropped from the argument list and from the callee's parameter stack together — one mask, computed once in the callee's own namespace and threaded to both consumers, since the two sides have to drop the same POSITIONS or argument *i* pairs with formal *i+1*. Dropping a formal cannot disturb another parameter's De Bruijn index, a slot stack being keyed by its parameter's type name. The argument expression is still translated and only its result discarded, so a nested call inside a `Unit`-typed argument keeps recording its own precondition obligation. The corpus is where the size of this shows: 40 programs move, all 40 still accepted once the 18 stale value pins were repaired in the same change, and no obligation appears or disappears on any of them — the count and the kinds are identical, only the tier moves, 30 toward Tier 1 (`ch09_json` alone goes 30/71 to 57/44), 7 the other way and 3 level. On 39 of the 40 the obligations keep their line numbers too; the exception is `examples/array_utilities.vera`, whose lines shifted because this change also edits its source. The 40 split 22 / 18. The 18 are the programs whose `main` carried a value pin — `ensures(@Int.result == 3)` over helpers whose own `ensures` was `true` — discharged as a Tier-3 runtime check only because a `(())` argument made the helper's result unknowable, and refuted outright the moment the call is modelled. That is the ordinary modular-verification outcome for the `f(1)` spelling and always has been, so those helpers now STATE what they return; the pin proves at Tier 1 from the contract instead of being deferred, and the guarantee each handler test was asserting in a comment is machine-checked at run time. The 7 that end LOWER are the price of that repair rather than a loss: each gained a real postcondition over a handler body — honestly Tier 3 — where a trivial `ensures(true)` had been counting Tier 1 for free, and each bought a genuine Tier-1 proof on its caller in exchange. `examples/array_utilities.vera`'s `first_above` takes its cutoff from a binding rather than a parameter for the same reason — a caller reasons about a helper only through its `ensures`, and a per-threshold answer is not something one postcondition can name ([#1214](https://github.com/aallan/vera/issues/1214)). - **`vera test` skips a function whose precondition it cannot model, instead of reporting the trap as a broken contract.** `requires(string_length(@String.0) > 0)` is deliberately untranslatable — Vera's `string_length` counts UTF-8 bytes, Z3's `Length` counts code points, and Z3's string theory has no byte-length operator ([#802](https://github.com/aallan/vera/issues/802)) — so the conjunct reached the input generator's solver as nothing at all, `_seed_boundaries` offered `""`, the compiled function trapped on its own entry guard, and the trial loop scored that trap as a contract FAILURE. A correct program reported `19/20 passed, 1 failed` with an E700 naming its own `requires`: a limit of the generator presented as a falsified contract, which spec §0.3's diagnostics-must-not-mislead rule forbids. The function is SKIPPED now, with the blocking conjunct NAMED — `cannot generate inputs satisfying `` `string_length(@String.0) > 0` `` — mirroring the existing `cannot generate inputs` taxonomy, and disclosed as an E701 warning so a consumer reading only `diagnostics` still learns why nothing ran. Detection is the mechanism rather than a list of built-ins: each conjunct is handed to the SMT layer and the answer believed. That is what makes it complete — the tester's `SmtContext` is deliberately bare, with no function lookup and no ADT registry, so a quantifier, a call to a user function and most built-ins defer there too, and any hand-written list would have been incomplete the day it was written. A refined PARAMETER's predicate is checked the same way, being the same defect one door over: it is part of the type rather than of the contract, so no clause states it and codegen emits it as an entry guard regardless. Conjunct granularity is what keeps the message actionable — a clause is untranslatable as a whole the moment one conjunct is, so `requires(@Int.0 > 0 && string_length(@String.0) > 0)` names the second and not the first. The check is asked only of Tier-3 functions, the only ones trials are run for; demoting a Tier-1 proof to "skipped" would throw away the proof. The question is asked against the very declarations the generator will translate against — `_declare_param_vars` is shared — so the two cannot answer differently. Two conformance programs were exhibiting the bug in the corpus: `ch09_http` reported three failing functions and `ch09_inference` one, ten spurious trial failures between them, and all four now skip cleanly ([#1229](https://github.com/aallan/vera/issues/1229)). - **A parameterised type alias APPLIED is substituted, in the verifier's resolver too.** `type Box = T;` used as `@Box` resolved to `AdtType(name='T')` inside `ContractVerifier` — the alias's own binder handed back wearing an ADT's name, because that resolver ignored the application's type arguments entirely. The checker has substituted since [#660](https://github.com/aallan/vera/issues/660), so such a program type-checks; only the verifier's parallel resolver did not, and everything downstream that asks what a type IS got the phantom. The reported symptom was a refinement's BASE: `{ @Box | @Box.0 >= 18 }` resolved to that ADT, failed the modelled-primitive gate in the call summary, and the refined-return fact was dropped — so a valid program was rejected with a spurious E501 (and its producer's own return obligation demoted to an E506 runtime guard) while `vera run` returned the right answer. Two halves had to move together and both did: a parameterised alias's own type parameters are now bound as TYPE VARIABLES while its body is resolved, mirroring what `_register_data` already did for an ADT's, because `substitute` maps type variables and an ADT-named binder is unsubstitutable however carefully the application side is written; and the application substitutes its resolved arguments into the body. Arguments resolve at the use site before they are substituted, so an argument that is itself an application (`Box>`) and a body that applies another alias (`type Wrap = Box;`) both land fully resolved. The gate itself is untouched, which is the point of the `@Byte` case in the new suite: a base outside the five statically-modelled primitives still degrades to a disclosed Tier-3 runtime check and still refuses to grant its predicate to a caller. `tests/conformance/ch02_refinement_base_param_alias.vera` carries the shape end to end (suite now 202), and the [#1226](https://github.com/aallan/vera/issues/1226) binder fixtures drop the `type Box = Nat;` they had been written around — the parameter is exercised now rather than avoided ([#1237](https://github.com/aallan/vera/issues/1237)). - **`verify --json`'s `obligations` array is a partition of the summary, and the documentation now says so.** `verification.total = 2` beside a THREE-entry `obligations` array on `tests/conformance/ch08_transitive_module_import_base.vera` read as the summary disagreeing with the stream it is documented to be derived from. It does not: the third entry is a refuted `ensures`, and a refutation discharges to no tier — counted by no summary field, surfaced as the E500 error instead, exactly as a `tier3_unguarded` obligation is counted nowhere and surfaced as a warning. The counts were already derived from the stream by a single `summarize()`, so the code was right and the documentation under-specified; the documentation moved. `CLAUDE.md` and `AGENTS.md` now give the `status` → counter mapping as a table and state the full accounting — `len(obligations) == total + violated + tier3_unguarded` — with the rule that a consumer reproduces the counts by FILTERING on `status`, never by taking the array's length. The contract is machine-checked from here: `tests/test_obligations.py` asserts both directions on every program `vera verify` runs on — the counts are reproducible from the stream, and the stream is exhaustively bucketed by the counts — over a corpus widened to include the `check`-level base modules the warm/cold differential skips, which is why the one corpus program exhibiting the shape had never been asserted. The status vocabulary is read from the `ObligationStatus` Literal itself, so a sixth status added without a decision about which bucket it belongs in fails there rather than silently vanishing from the counts, and `tests/test_cli.py` pins that the array is emitted unfiltered and that the uncounted entry still joins its diagnostic on `(line, column)` ([#1242](https://github.com/aallan/vera/issues/1242)). - **A refined State/Exn cell is its own cell.** Spec §7.5 makes two cells distinct exactly when their resolved types are, and a refinement is part of that type — `EffectInstance` holds the `RefinedType`, and E125 refuses to pass one where another is required. Codegen collapsed the refinement away, so under `type Pos = {@Int | @Int.0 > 0}` and `type Neg = {@Int | @Int.0 < 0}` all of `State`, `State` and `State` shared one host cell: a `Pos` handler wrapping a `Neg` handler, with a callee declaring `effects(>)` called inside the inner one, sent that callee's `put(111)` to the `Neg` cell and returned `1`. Check-green, verify-green, silently wrong. The family now carries the predicate — rendered through `vera fmt`'s own single-line expression form, which is a left inverse of parsing (it re-derives parentheses from precedence so the text re-parses to the same expression) and therefore discriminates, while staying LINEAR in the predicate a user wrote. The key asks for that renderer in a *structural* mode, because the two jobs disagree in exactly two places: canonical form CHOOSES between spellings of one construct — it drops a match arm's redundant block wrapper and supplies a handler clause's braces either way — while the key has to tell those spellings apart, `RefinedType` equality being dataclass equality over the predicate. Rendering the key canonically merged them, and a merged family made the nested-cell addressability gate refuse a check-green program outright. Structural mode follows the AST at both sites and still re-parses, so the left-inverse argument holds in either mode; `vera fmt`'s output is untouched. So each of the three routes to its own cell and the repro returns 111; the mirrored nesting and `State` inside `State` are correct for the same reason. Two consequences follow. The PR #1232 addressability gate now sees `Pos` and `Neg` as different families, so a nest it previously refused outright — a three-deep `Pos`/`Neg`/`Pos` whose middle clause body routes a bare `put` outward per §7.5.2 — compiles and routes correctly. And every decision that was keyed on the family's TEXT (i32/i64/f64, pointer-ness for the GC shadow stack, pair-ness for a `String` payload, which #1203 write guard applies) now takes the cell's separately-derived base name, because a family that discriminates the predicate matches nothing in those tables: sharing one name for identity and representation would have switched all of them off silently while the verifier went on recording the guards as `tier3_runtime`. The base and the family are derived side by side from one type expression and carried together, which also retires the last place a cell family was recovered by slicing it back out of a mangled import name — and it is what lets the WASI target's unsupported-family error say `state (Byte)` rather than print a predicate. Behind the renderer, `MAX_CELL_FAMILY_SYMBOL` refuses a family whose mangled symbol passes 4,096 characters with an E607/E612 naming the length and the cap: a State family becomes the module and field string of four imports, the binary format caps a name string at 100,000 bytes, and an emitted module no host can parse is the one outcome that has to be impossible rather than merely unlikely ([#1218](https://github.com/aallan/vera/issues/1218)). - **A cell type carrying a function type is named by its resolution, like every other cell.** Spec §7.5.1 says two `State`/`Exn` cells are one exactly when their resolved types are, and `naming.family_name` carved out an exception: a resolution outside the canonical `Head` grammar kept the alias-opaque spelling instead, because the mangler could not escape it. `State` under `type Handler = Option Int) effects(pure)>` and `State Int) effects(pure)>>` were therefore two cells behind a check that typed them as one — a helper's `put` landing where nothing reads it. With the mangler total the gate is gone, and both spellings are the one `Option Int) effects(pure)>` family. Removing the gate on its own would have merged rather than split, because `pretty_type` — the renderer behind it — elides a refinement's predicate to `...` and strips a type variable's built-in marker, so `Option` and `Option` render identically; the family now renders through `types.structural_type_key`, the same discriminating key the checker orders effect rows by, so family identity and checker identity are one derivation rather than two that happened to agree. That key renders a refinement's predicate through `vera fmt`'s expression form, so both readings of it — the row ordering and the cell name — discriminate on the text `vera fmt` prints. An open effect-row variable is `'`-prefixed there, matching how a type variable is marked: an open `effects()` and a closed row over a declared `effect E` rendered alike, which is the same tie one level further out. What still falls back is a type expression with no resolution to name — one resolving to a bare function type, one that does not resolve at all — and both are shapes the compilability gate refuses before a cell exists, so the split is free where a merge would not be. `is_ref_spellable` was the gate's only caller and is removed with it ([#1219](https://github.com/aallan/vera/issues/1219)). - **The type-name mangler is total over canonical type renderings.** `mangle_type_name`'s escape covered exactly the `Head` grammar — `_`, `<`, `>`, the `", "` separator and a bare space — so any other character in a canonical rendering reached a WAT symbol unescaped: a function type's parentheses and arrow, a refinement's braces and predicate. Those produce an import name the WAT parser rejects, which is why `naming.family_name` gated on `is_ref_spellable` and kept such a cell's alias-opaque spelling instead. The escape now covers every remaining character through one variable-length `_U_` code, so the output alphabet is `[A-Za-z0-9_]` — inside the WAT `idchar` set, inside the SMT-LIB simple-symbol set, and returned unchanged by the browser runtime's `state_get_` split. The five pre-existing codes keep their exact meanings, so no symbol the corpus already emits moves. One collapse is repaired rather than preserved: a comma NOT followed by a space used to share the `", "` separator's `_C`, which no `Head` rendering can produce but a string literal inside a refinement predicate can — `'a,b'` and `'a, b'` are two names again rather than one. `unmangle_type_name` inverts the widened code, and the mangler's deliberate NON-idempotence is now pinned along with the reason it cannot be otherwise: `Option_LInt_R` is itself a legal flat ADT name, so a mangler that left an already-mangled string alone would collide it with `Option` ([#1219](https://github.com/aallan/vera/issues/1219)). - **The compiler documentation states the naming rule the compiler implements.** Five claims actively misinformed a reader. `vera/README.md` called a binding's canonical type name "the syntactic name used for slot reference matching" and the `Binding` docstring in `vera/environment.py` said the same — alias opacity applies to that name's HEAD, while its type ARGUMENTS resolve, so `@Option` under `type Cnt = Int` binds `Option`. `vera/README.md` also attributed the monomorphizer's De Bruijn recount to "full-depth slot names from `vera/slots.py`", where the recount renders through `naming.slot_name` against the clone's ORIGIN module `AliasEnv`. `SKILL.md` read as though any non-primitive parameter is skipped by `vera test`, steering agents away from alias-typed testable signatures — the decision is made on each parameter's resolved type ([#1216](https://github.com/aallan/vera/issues/1216)). `DE_BRUIJN.md`'s `--explain-slots` sample had hand-aligned columns the tool does not emit and no `where`-helper block ([#1217](https://github.com/aallan/vera/issues/1217)); the sample is now the tool's verbatim output. And `spec/03-slot-references.md` §3.8 asserted that type aliases are "not transparent for reference resolution" without the head/argument scoping §3.8.1 supplies twelve lines below. Alongside the corrections, the architecture story gains a home: `DE_BRUIJN.md` a §6 "Type aliases and slot names" (the three-clause rule, aliases as parameter names, merged stacks, `forall` shadowing, cell identity) with §4, §9, §10 and §11 additions, `vera/README.md` a Design Pattern 8 "One renderer for slot names", `DESIGN.md` a technical-decisions row, `FAQ.md` its first entry on aliases and `@T.n`, and `TOOLCHAIN.md`/`LSP_SERVER.md`/`spec/02`/`spec/08` the clauses their surfaces had grown. `assets/diagrams/architecture.svg` and its text twin place `naming.py` in the type-check stage. Sweeping `DE_BRUIJN.md`'s code blocks through `vera check` while adding §6 also turned up two that never parsed: §5.6's closure examples wrote a function type inline in return position (`-> fn(-> @Int) effects(pure)`), where the grammar requires a `@`-prefixed return type expression and plain inner type names inside a type-level `fn(...)` — both now carry the `type IntToInt = fn(Int -> Int) effects(pure);` form the conformance suite uses. - **The testing and CLI docs list what the toolchain actually runs.** `TESTING.md`'s validation-script table omitted `check_debruijn_examples.py`, `check_explicit_encoding.py` and `check_distribution.py`, and its introductory count did not match the rows it introduces; its CI table omitted the `package-distribution` job, and the **lint** row four of the steps that job runs. `CONTRIBUTING.md`'s commit-stage hook count disagreed with the total two paragraphs above it. The `vera errors` one-liners in `README.md`, `SKILL.md` and `AGENTS.md` named only the `E` series, where the registry also carries `W001` and `W002` — as `vera/README.md`'s diagnostics pattern and its `ERROR_CODES` entry count now state as well. - **The compiler's pipeline documentation numbers seven stages, because there are seven.** `vera/README.md` opened with "a seven-stage pipeline" and then numbered only six — Resolve appeared as "2b", a sub-step of Transform — and `assets/diagrams/architecture.svg` carried the same half-numbering. The resolver is a full stage by every measure the file itself uses: it has one public entry point (`ModuleResolver.resolve_imports`), `cli.py` runs it as its own step between transform and check, it accumulates its own diagnostics, and the module map already gives it its own `Resolve` stage column. Prose, ASCII diagram, SVG and the SVG's `naming.py` footnote now all number Parse 1 · Transform 2 · Resolve 3 · Type Check 4 · Verify 5 · Compile 6 · Execute 7. - **Unified slot/family naming** (`vera/naming.py`): one total renderer for slot names, slot-reference keys, and State/Exn family names, replacing six independent per-subsystem renderings; the checker's rendering is now the rule everywhere. Composite type aliases collapse to one State/Exn cell family exactly as scalar aliases already did ([#1208](https://github.com/aallan/vera/issues/1208), [#1209](https://github.com/aallan/vera/issues/1209)). The alias × handler corner these two bugs lived in is now covered by the conformance suite: seventeen new programs — the head-opaque/arguments-resolved slot-naming rule, composite and scalar cell-family collapse across function and module boundaries, multi-hop / parameterised / twice-applied alias cells, `Bool`/`Byte`/`Float64` widths, the `Exn` payload twins including the `i32_pair` String payload, alias spellings inside handler clause slots, and a cyclic-alias negative (E132) — carry the shapes the PR #1202 review probes were written to reach, and those probes are retired with them (suite now 196). Every consumer is also handed the env the checker rendered under: an imported callee's contract and an imported generic's clone render in their DEFINING module's alias namespace — including a callee the origin registry never pinned, such as an imported generic's own `where`-helper, which renders in the module under verification rather than the entry program — a callee's refined-RETURN predicate is translated in that namespace alongside its `requires`/`ensures`, codegen's declaration-index space (the bound deciding which declarations an alias body can see) is keyed per namespace rather than shared across every absorbed module, and a `forall` variable shadows a same-named module alias wherever a generic signature is rendered — each of which was otherwise a silently mis-resolved slot (a lost call-precondition obligation, a postcondition proved from collapsed premises, or a body reading the wrong parameter through valid WASM). Alias resolution is iterative and resolves ONE dependency at a time, so neither a long-but-legal alias chain (400 hops) nor an alias graph whose bodies mention siblings as well as ancestors raises `RecursionError` from inside a renderer documented as total — both render their real resolution. Codegen's refinement boundary guard derives its binder from the same one derivation rather than a second hand-maintained copy of the walk. The monomorphizer's De Bruijn recount narrows its post-substitution side by the `forall` variables that SURVIVE substitution, so a `where` helper's own `forall` — which the clone keeps, and which both consumers narrow by — no longer resolves through a same-named module alias; and a generic nested under an imported non-generic function is looked up in the origin registry by its whole lexical chain, so the discovery-time recount and the verification-time clone both run in the module that declared it instead of the importer's namespace. `vera.naming` no longer exports `alias_env_from_declarations` — no production consumer walked declarations to build an environment, and the fixture constructor that did now lives in the test suite. - **A monomorphized clone of an imported generic reports diagnostics in its own module's file**, completing the [#1186](https://github.com/aallan/vera/issues/1186)/[#1189](https://github.com/aallan/vera/issues/1189) attribution work (PR #1224 review). Codegen works on an imported module's declarations in four places; three entered that module's source scope alongside its alias scope, and the clone-body pass entered the alias scope alone — so any diagnostic raised while compiling the clone paired the IMPORTER's path with module-local line/column, naming unrelated source in the importer or a line past its end (which renders an empty `source_line`). It also silently merged two modules' [E618] nested-refinement rejections into one, because that diagnostic is deduplicated by resolved location on the premise that a location carries the file it belongs to. - **State/Exn host-import registration covers the whole handler** ([#1210](https://github.com/aallan/vera/issues/1210)). Codegen decides which host-cell imports and exception tags a module declares by walking each function for `handle` expressions, and the walk descended a handler's BODY alone — not its clause bodies, not a clause's `with` state update, and not its own state-init expression. A cell family or tag reached only through one of those went unregistered while the lowering emitted its calls regardless, so a check-green, verify-clean program died at whole-module WAT compilation with `unknown func $vera.state_push_Nat` / `unknown tag $exn_Int`. The walk now covers all four sub-expression positions, in both the handler scan and the IO/Markdown/Regex host-import scan (a host builtin in a clause body had the same orphaned-import shape). The walk also covers every CONTRACT predicate, which is lowered code: a `handle[State]` written in a `requires`, an `ensures`, an `assert`, or a `decreases` measure emitted `state_push_Nat` against an import the body-only walk never declared, and the walker's own case split had declared those positions structurally handler-free. The `decreases` measure needed its own enumeration (`contract_exprs`, now shared by both pre-scans) because it carries `exprs`, not `expr` — the attribute-name shortcut the host-import scan used skipped it entirely. The `i32_pair` cell type the walk used to skip in SILENCE — `handle[State]` inside a `pure` function, where the declared-effect gate never runs — is now the same loud `E607` that gate emits, with the offending cell's own location; the two paths share one registration derivation rather than two hand-kept copies, so they cannot come to accept different cell types. Its Exn twin is fixed with it: the handler walk called the shared tag registration and DISCARDED the verdict, so `handle[Exn]` in a `pure` function registered no tag and compiled anyway (`unknown tag $exn_Unit` at WAT) where the declared-row spelling of the same payload had always been a clean `E612` function drop — both arms now record what they could not register and drop the function. A permanent registration-completeness differential asserts the cross-component invariant directly: over every `examples/` and `tests/conformance/` program the toolchain compiles, every `state_*`/`exn_*` symbol the emitted WAT references has a matching import or tag declaration, **and every handler-bearing module validates** — the name comparison alone reports a symbol declared at the WRONG TYPE as perfectly balanced, so those modules are also handed to `wasmtime.Module` through the exceptions-enabled engine `execute()` itself uses (shared from `tests/codegen_helpers.py`; 10 of the 30 handler-bearing modules fail to load with `wasm_exceptions` off, which is a supported wasmtime configuration even though the current runner defaults it on), with a planted retyped-import fixture proving that leg can go red. The conformance suite's deliberate negatives are filtered out of the sweep rather than swept and ignored: a program written to fail `vera check` never reaches codegen, so its emitted WAT is not a thing the invariant is about. Three further positions were closed after that, none of which the corpus contained — so the corpus-anchored differential could not have found any of them. `LetDestruct.value`: a destructuring `let` was absent from the `Block` dispatch of BOTH walkers, so `let Tuple<@Nat, @Nat> = f(handle[State] …)` emitted `state_push_Nat` against no import, and an `Exn` payload written there bypassed the new E612 gate outright (an unwalked position does not merely miss a registration, it disarms the gate driven by the same walk). `ModuleCall.args`: the "tracked by the imported module's own scan" disposition was true of the CALLEE and false of the ARGUMENTS, which are this module's own lowered expressions. And a SIGNATURE REFINEMENT predicate: a `{ @Base | P }` parameter or return type has `P` emitted as a boundary guard in the function's prologue/epilogue and is reached through the ALIAS table rather than structurally, so no walk from `decl.body` can find it — `_signature_refinement_predicates` is the signature's `contract_exprs`, mirroring `_refinement_guard_parts`'s two bails so registration equals what is emitted rather than exceeding it. The two walkers now also carry a schema-driven FIELD-coverage gate: it reads the dataclass fields of `vera/ast.py` and fails on any node class with an expression-carrying field that neither walker dispatches on, unless the class is in an explicit ten-entry justified-ignore table naming the route its expressions ARE reached by. It is deliberately stronger than `scripts/check_walker_coverage.py`, whose canonical set is the `Expr` subclasses (`LetDestruct` is a `Stmt`) and whose verdict is "the class is NAMED" (`ModuleCall` was named, with a disposition that covered half the node)). `contract_exprs` dispatches on the real contract types instead of probing for an `expr` attribute — an attribute probe reports an unrecognised contract kind as carrying no predicates, which is the silent skip the helper exists to fix (it is how `decreases` was dropped), and it hides the field accesses from mypy; a new `ast.Contract` subclass now raises. The Exn half of the walk's unregistrable-type record is declared beside its State sibling in `CodeGenerator.__init__`, so neither depends on the walk having run for the attribute to exist. The signature enumeration reached the boundary-guard emitters by only ONE of their four routes, because it was written as a copy of what one of them does: the guard layer is also entered by decomposing a TUPLE parameter into its components, by decomposing a tuple RETURN, and from a closure's own refined formals and return, and each lowered predicates nothing registered — a `Tuple` parameter, a `Tuple` return, and `fn(@Big -> @Int)` / `fn(@Int -> @Big)` behind an `apply_fn` each died at whole-module WAT with `unknown func $vera.state_push_Nat` from a check-green, verify-clean program. There is now ONE derivation of what the guards check (`_signature_refinement_predicates`), living beside the emitters in `vera/codegen/contracts.py` and consumed by both sides: the emitter, the return-epilogue gate and the pre-scan all read the same tuple decomposition (`_tuple_component_guard_sites`), which owns the component classification, the `@Unit`-component skip and the fail-closed depth limit, so a component the layer guards cannot be one the pre-scan never registered. Registration equals emission in BOTH directions — component decomposition stays a named-function leg because the closure path emits no component guards, and enumerating it for a closure would declare a host import nothing calls. Both walkers now descend an `AnonFn`'s SIGNATURE as well as its body, cycle-guarded: `type R = { @Int | … fn(@R -> @Int) … }` type-checks, so expanding a refinement that contains a closure refined by itself is a real cycle. The pre-scans' field-coverage gate gained FIELD granularity to match its claim — it compares each dispatched class's descendable fields against the names the walker's source actually reads, so a new field on an already-dispatched class is loud, where before only a new CLASS was — and a callgraph gate asserts the structure that keeps the derivation single: every consumer reads the shared decomposition, and none classifies behind its back. `_error_once` replaces the bespoke E618 site set, since the depth-limit E617 is now reachable from three consumers of one derivation. - **A handler clause body's bare `get`/`put` targets the ENCLOSING cell** ([#1211](https://github.com/aallan/vera/issues/1211)). A clause is not part of the body it refines, so an operation written in a clause body belongs to the handler's declaration scope — the same rule that already gave clause-body slot references the declaration scope (#1202), and the rule the checker has always applied (clauses are checked before the handled effect joins the effect row). Codegen disagreed: inlining a clause body cleared the clause registry but left the op registries pointing at the handler's OWN host-cell imports, so with two nested handlers over different cell families a bare `put` in the inner clause body wrote the INNER cell while the checker had typed it against the outer one — check-green, verify-clean, valid WASM, silently wrong value. The clause registry now carries the whole declaration-time scope in one record (`StateClauseEntry`) rather than the slot environment alone, and the inline restores all four registries around the clause body and its `with` expression; the op's result-type mirrors travel with them, so a bare `get(())` in match-scrutinee or array-element position inside a clause body is typed from the enclosing cell instead of emitting invalid WASM for a check-green program. Termination is now a property of the data: the restored registry is the one from strictly outside the handler, so re-entry from a clause body walks outwards through a finite nesting. Because the outward-routed operation is an operation SITE of the enclosing handled body, the enclosing handler's own clause runs on it — a transforming `with` one level out applies, which is the half of §7.5.2 that used to be stated twice and incompatibly (the old "clauses are lexical" bullet said such an operation performs the bare intrinsic). That bullet is now scoped to what is actually true of it — clause transforms do not cross a CALL boundary, and a clause never re-enters ITSELF — so the two rules compose. Nesting depth is bounded: each outward re-entry re-expands another clause, so the emitted code is exponential in the nesting (an 18-deep nest of 106 source lines reached ~2M lines of WAT), and past `STATE_CLAUSE_INLINE_DEPTH_CAP` (8, `vera/skip.py`, following the `DERIVED_HELPER_DEPTH_CAP` precedent) the function is a loud `E602` skip naming the cap. SAME-family nesting is refused rather than lowered: the clause TRANSFORM routes outward correctly but the host intrinsics address only the innermost cell of a family, so `handle[State]` inside `handle[State]` (or inside a function declaring `effects(>)`) would write the inner cell where the rule says the outer one — that shape is now an `E602` citing [#1233](https://github.com/aallan/vera/issues/1233), which tracks depth-indexed host addressing, and §7.5.2 states the limitation. The gate keys on the whole pushed-cell stack, not just the adjacent handler, so an `Int`/`Nat`/`Int`/`Nat` nest — whose third level routes to the second while its `state_put_Nat` addresses the fourth's cell — is caught too. `resume` keeps the clause's own family (it types the op's result). The handler lowering now saves and restores its op registries the way the clause inline already did — whole-dict replacement inside a `try`/`finally`, rather than in-place mutation with the restore outside it, so a `CodegenSkip` out of a handled body cannot leak the handler's `get`/`put` into the enclosing scope. The verifier is unchanged — a differential over the shapes confirms its obligation stream is identical before and after — and across the `examples/` + `tests/conformance/` + `tests/probes/` corpus exactly two programs moved, both since promoted: the cross-family shape is `tests/conformance/ch07_clause_body_op_enclosing.vera`, and the one that previously emitted invalid WASM is the `match_scrutinee_in_clause` case of `tests/test_nested_handler_clause_ops.py`. The depth cap checks before it mutates: its `CodegenSkip` used to fire after the six clause registries had been replaced, the one exit from the clause inline that did not restore them. The same-family refusal names both spellings, because `State.put(x)` routes through the same dispatcher as the bare `put(x)` and one gate covers both. The refusal also compares family names in ONE representation: `_pushed_cell_families` and a clause registry entry carry the canonical family (`Option`) while an import name carries the mangled one (`Option_LInt_R`), and mangling is not idempotent — so re-mangling the already-mangled side made every COMPOSITE family compare unequal to itself and the gate returned instead of refusing. `handle[State>]` nested in `handle[State>]`, with the outer handler declaring no `put` clause (the branch that reads the import name), compiled and ran: 5100 where the enclosing-context rule says 5042 — the silent wrong value the gate exists to prevent, on the exact shape its scalar `Int` twin was refusing correctly the whole time. Spec §7.5.2 states the rule for both halves, and its same-family lead-in states the precise claim: same-family nesting is supported, and it is a clause-body operation routing outward that is refused. - **Bare effect-op resolution is deterministic and source-ordered** ([#1215](https://github.com/aallan/vera/issues/1215)). When two effects in one function's row declare the same op name — no user `effect` declaration needed, since the built-in `State` and `Http` both declare `get` — `lookup_effect_op` picked by iterating the row's `frozenset`, so which signature bound flipped with `PYTHONHASHSEED`: `effects(, Http>)` compiled and ran on some interpreter starts and failed `E217` on others, from identical source. The row now travels with the declaration order the `ast.EffectSet` was written in (one resolution produces both views, so they cannot describe different effects), and resolution walks ordered candidates only: innermost enclosing handler first, then each enclosing handler outwards, then the declared row in SOURCE order, then the registered effects in registration order. Spec §7.3's lead-in stops calling a row an "unordered set" — containment compares rows by set equality, and the written order is meaningful — §7.4 gains the rule (naming the final step honestly — the REGISTERED effects in registration order, built-ins first then user effects in source order, which is what the implementation walks); §7.3.2 drops its claim of an alphabetical canonical form, because written order now carries meaning and `vera fmt` does not reorder a row — set equality governs containment and nothing else; and §7.4.1 says what the compiler does with an unqualified ambiguous call instead of claiming it is rejected, while noting that "not rejected for ambiguity" is not "always accepted" (the binding it picks must still be routable and well-typed). `ordered_effect_row()`'s fallback for a row member the order tuple never mentions sorts STRUCTURALLY — effect name plus rendered type arguments — because a name-only key ties `State` against `State` and a stable sort then hands back the frozenset's own order, reintroducing the seed dependence inside the method that exists to remove it. That rendering is a dedicated structural one, NOT `pretty_type`: the human-readable renderer elides a refinement's predicate (`{@Int | ...}`) and strips a type variable's built-in marker (`T#b` → `T`), so `effects(, State>)` over two refinement aliases of one base tied on the key exactly as the name-only version tied `State` against `State` — the same bug one level down, inside its own fix. That structural rendering now goes all the way down: a type argument may itself be a function type, and a function type carries its own effect ROW, whose leg was still rendered by `pretty_effect` — so two outer instances differing only inside a nested row, by a refinement's predicate or by a type variable's built-in marker, tied for a third time and fell back to `frozenset` order. The row rendering is structural too now, sorting its members on the same key rather than on a presentation string, and the two mutually recurse. The **type-argument** leg of effect resolution is fixed with it ([#1231](https://github.com/aallan/vera/issues/1231)), a sibling the fix's own hash-seed sweep surfaced: spec §7.3.3 permits one effect twice with different type arguments (`effects(, State>)` is two independent cells), and `_effect_type_mapping`'s row fallback iterated the same frozenset — the identical program checked clean on some interpreter starts and failed `E121` (`body has type Bool, expected Int`) on others. It now walks the ordered row too, and codegen's own per-row loop is aligned to the checker's rule: the FIRST instantiation written wins, where it previously let the last overwrite the first and emitted `state_get_Bool` (i32) for a call the checker had typed `Int` (i64). - **An E501 for an imported callee quotes the clause that callee actually wrote** ([#1220](https://github.com/aallan/vera/issues/1220)). A contract clause's span numbers lines in the file that DECLARED it, and the renderer indexed the IMPORTER's buffer with it, so the `Precondition:` line of a cross-module call violation showed whatever text sat on that line here. Where the importer happened to have a `requires` of its own there, the message read as a perfectly well-formed clause belonging to another function — nothing marked it as the wrong one — and where the callee's file was the longer one the line fell off the end and vanished from the message entirely (an imported generic's `where`-helper, whose contract sits past the end of a short importer, quoted nothing at all). Both halves of "which file is this?" now ride one scope: `_declaring_module_scope` swaps the naming env and the source buffer together, so a clone named in one module cannot quote another's text. Every clause a module declares is additionally pinned to that module's source at registration — walked off the module AST rather than off the flat, last-wins function registry, so a file spelling one helper name twice pins both — and a clause reached through the harvested registry, through a `mod::fn` qualified call, or through a monomorphized clone all quote the same file. - **An imported callee's contract is read in the module that wrote it — its FUNCTIONS as well as its aliases** ([#1225](https://github.com/aallan/vera/issues/1225)). A `requires` / `ensures` calls by bare name, and the SMT layer resolved those names through the IMPORTER's registry: a module whose exported function is guarded by `requires(@Int.0 < cap(0))` had its precondition interpreted using whatever `cap` the importing file happened to declare — a private helper the callee cannot even see. `vera verify` reported all-Tier-1 clean and `vera run` trapped on the precondition the callee actually has: a false Tier 1, with the mirror (a valid call rejected with a spurious E501) reachable by moving the same literal the other way, and the `ensures` path failing the same way one step later (a postcondition read too strongly proves the CALLER's contract, so the caller is what traps). The naming env and the function registry are now one `CalleeScope`, applied and restored as a pair by one context manager, because a scope that swapped one of them and not the other is precisely the defect — the contract would be read half in its own module and half in the importer's. The scope pinned at registration covers every function a module declares, private helpers included and its builtins behind them, since that is the namespace a bare name in one of its contracts resolves in; and the fallback for a callee nothing pins is the scope *in force at the call*, not the entry program's, so a helper reached from inside another callee's contract is read where it was declared. The E501 renderer reads the same pin through the same accessor. Three shapes that used to demote to a loud Tier-3 (E532) because the callee's contract named a function absent from the importer entirely now get a static verdict, each of which the runtime agrees with. - **A refined return over a parameterised base keeps its fact** ([#1226](https://github.com/aallan/vera/issues/1226)). The binder a refinement predicate is translated under came from the predicate's HEAD identifier, and a head is only the binding-table key when the binder has no type arguments: `type Grown = { @Box | @Box.0 >= 18 }` pushed the value under `Box` while `@Box.0` resolves `Box`. The predicate therefore failed to translate and the refined-return fact was dropped in silence — at the caller, where it left a valid program rejected with a spurious E501 (`vera run` returns from the same program without a trap), and at the producing function, where a provable refined return demoted to a Tier-3 runtime guard (E506). The binder is now `naming.predicate_binder_key`, the whole reference rendered against the environment the predicate is being translated in, so the push side and the lookup side are one derivation over one environment — and the two ways to ask for a binder (from the refinement's type expression for codegen's runtime guard, from the predicate's own reference for the verifier and SMT layers) both render through `slot_name` and meet. Being env-dependent makes the callee-namespace wrap around the refined-return translation load-bearing rather than defensive: with the derivation moved outside that scope, the same predicate binds `Box` in the defining module and `Box` under an importer's `type Cnt = Int`, and the cross-module case goes red while the single-module one stays green — which is exactly the prediction `TestRefinedReturnTranslatesInTheCalleeNamespace` recorded by provenance when the bare-headed binder still masked it. - **A module's pinned registry holds the names its own file can call, and its diagnostics point at its own file.** Two follow-ups to the cross-module contract scope above, both found by adversarial review of the change itself. The registry each module's contracts resolve in was built by walking that module's DECLARATIONS, so a bare name the module's own file *imports* resolved to nothing: a `requires` calling it demoted an honest Tier-1 to E532 while the same call spelled `deep::cap(0)` stayed Tier-1, and an `ensures` calling it rejected a valid program outright ([#1225](https://github.com/aallan/vera/issues/1225) regression). Each module now takes its imports' public surface the way the entry program does — same import-name filter, same `setdefault` — while what it *exports* and what its scope is pinned to stay its own declarations, so filling the registry cannot re-export a name it merely imports (§8.6.4) nor attribute another module's contract to this one's namespace. A name outside the module's own import filter still misses, loudly, which is §8.5.1's rule. Separately, the rendering half of [#1220](https://github.com/aallan/vera/issues/1220) is finished: the excerpt under the caret and the file a location names now come from the module under verification, not the entry program (a clone's diagnostic pointed at the importer's line N for another file's line N, and produced an empty excerpt where the importer was the shorter file), and a contract clause broken across lines is quoted whole instead of truncated at the first newline into an unbalanced-parenthesis fragment naming a condition the program does not have — with its comments blanked first, through the lexer's own scanner, since joining the lines otherwise puts a trailing `--` comment in front of the rest of the clause (a `--` inside a string literal survives, which is what using the scanner rather than a split buys; all three comment forms are blanked, the annotation `/* */` included). The aggregate diagnostic a generic instantiation synthesises when no per-instance one matches carries the obligation's file too, since that path runs after the declaring-module scope has been left. **Both halves of `verify --json` move together**: a `ProofObligation` now carries the file its line number belongs to, and the CLI emits that instead of stamping the entry path on every entry, so the documented `(file, line, column)` join between the `diagnostics` and `obligations` arrays holds for a module-located obligation — it had begun producing non-matches and line numbers past the entry file's end. An obligation from the entry program is unchanged, and the field is additive (the warm session and the language server read the same records). - **A generic callee's call-site precondition demotes loudly instead of vanishing** ([#1236](https://github.com/aallan/vera/issues/1236)). A contract written over type parameters has no Z3 sort to build a call summary from, so the SMT layer bailed on any callee with `forall_vars` — and it bailed *silently*, recording nothing. Unlike every other arm of the same taxonomy (an untranslatable argument, an untranslatable precondition), the call-site obligation did not exist: an importer calling `forall fn pick(@Array, @Int -> @Int) requires(@Int.0 > 10)` as `pick([1, 2], 3)` was reported all-Tier-1 clean while the run trapped on `pick$Int`'s entry guard — a false Tier 1. It now records the same E532 Tier-3 disclosure the untranslatable-argument path records, naming the generic callee, under the same gate (a callee with only `requires(true)` has no obligation to lose) and through both drains, so a generic call written in the caller's `ensures` is disclosed as well as one in its body — with one qualifier: a call in BOTH positions is disclosed once, from the body, because a body that did not translate leaves no term to check the postcondition against and the clause never reaches translation (it demotes as E522 instead). The demotion is unconditional on whether the precondition would in fact HOLD — this is a "cannot check", not a "does not hold" — so a satisfied generic call demotes beside a violating one; discharging either statically means translating the contract at each monomorphized instance, which is [#732](https://github.com/aallan/vera/issues/732)'s per-instantiation machinery rather than this call-summary path. The postcondition half needs no record of its own: an unassumed fact is conservative and already surfaces where it mattered (the caller's own clause demotes, E520/E522, or a later call's argument does), and reifying it as a Tier-3 obligation would claim a runtime check the *caller* never performs. Three `tests/conformance/` programs gain one honest demotion each — no `examples/` program gains an obligation, no Tier-1 count changes, no verdict flips — and `E532`'s rationale now names the generic cause beside the host-handle one, which re-renders that warning's text in four further programs (three of them examples) for seven whose `verify --json` bytes move in total. - **The prelude's type aliases are the prelude's, and the reserved namespace is unwritable** ([#1221](https://github.com/aallan/vera/issues/1221)). `inject_prelude` runs at code generation and at the verifier's monomorphization discovery, never at the checker, so every alias name it injects is a name codegen resolves and the checker leaves opaque. Under the six user-facing spellings that asymmetry was writable: `fn f(@Array>, @Array Bool) effects(pure)> -> @Int)` checked, verified and compiled clean while the checker kept TWO parameter stacks and codegen merged them into ONE, so the emitted export read parameter 2 where the binding table says parameter 1 — valid WASM, unreachable from Vera source (the checker rejects every argument for the opaque head) and reachable by any host calling the export. `OptionMapFn`, `OptionBindFn`, `ResultMapFn`, `ArrayMapFn`, `ArrayFilterFn` and `ArrayFoldFn` are retired into the prelude's reserved `Vera` namespace, where the combinators' own closure-parameter aliases already lived (#869/#1184): the prelude now injects nothing outside that namespace, the six spellings are ordinary unknown names both sides treat identically, and the checker's ignorance is correct by construction rather than reconciled. Renaming alone would only have moved the bug — the reserved twins stay in codegen's table, and the identical wrong-parameter export was reproducible verbatim as `@Array>` on the parent commit — so **E154 now covers every way a program can spell a name in that namespace**: mentioning one in a type is refused where it is written, and so is BINDING one — a `forall` variable, or a `data` / `type` / `effect` / `ability` type parameter — because type-parameter scope is consulted ahead of both other rails, so `forall` made every mention inside that declaration resolve to the type variable and the reservation held at neither end (adversarial review of this PR; one in-corpus conformance program was using `forall` and is renamed). All three rails read the same anchored regex, so they cannot drift (`Veranda` is still an ordinary name). A function type keeps its one canonical spelling, `fn(A -> B) effects(pure)`, and a program wanting a short name for one declares its own alias — visibly, module-scoped, by its own choice (DESIGN.md principles 2, 3 and 6). Spec §8.4.1 states the rule — and now states it accurately: its previous wording claimed the prelude declares no type a program can name, which is false of the prelude's own ADTs (`Option`, `Result`, `Ordering`, `UrlParts` are ordinary public declarations a program names and shadows); the reservation covers the closure-parameter aliases only. Two new conformance negatives pin the reference and binder halves beside the existing declaration one (suite now 203). - **An imported ADT is ordered where its own module declared it** ([#1227](https://github.com/aallan/vera/issues/1227)). Codegen's `_adt_layouts` is one map across every absorbed namespace — layouts are structural, and a module's own bodies compile against the importer's generator — while the alias maps and the declaration-index space that bounds what an alias body can see are per namespace (§8.4.1, PR #1224). An imported ADT therefore reached the importer's naming environment with no index of its own and took the before-everything floor every built-in takes, so a main-file `type M = Float;` over an imported `data Float` resolved THROUGH the ADT while the checker — which carries the index the ADT's own module gave it — left the alias body opaque: the checker partitioned `['Array', 'Array']` where codegen rendered `['Array', 'Array']`, merging two parameter stacks the binding table keeps apart, and `@Array.0` compiled to a read of the second array. The index now comes from the owning module's own space, which codegen already records, so the two sides partition alike; a locally declared ADT still takes the main file's index, and a module's own alias bodies still resolve through it inside that module's scope. The same change pairs the module SOURCE scope at the fifth door codegen enters a module's namespace through — the mono-clone construction path, which entered the alias scope alone — so that door cannot become the next `_diag_location` misattribution (#1186/#1189) when something under it starts reporting. - **`vera test` now exercises alias-typed parameters**: the tester resolves type aliases before deciding Z3-encodability instead of matching syntactic heads, so `type Cnt = Int; fn f(@Cnt -> ...)` is trialed rather than skipped ([#1216](https://github.com/aallan/vera/issues/1216)). A refined alias reaches the generator with its predicate as a Z3 constraint, so the generated arguments satisfy the entry guard codegen emits for it, and a parameter whose resolved type Z3 still cannot encode is skipped with that type named (`cannot generate Option inputs`) rather than a placeholder. - **`--explain-slots` covers `where`-block helpers**: helper functions get their own slot tables, with enclosing `forall` type parameters correctly shadowing module aliases ([#1217](https://github.com/aallan/vera/issues/1217)). The helper is printed indented under its parent and appears in the `--json` `slot_environments` array qualified as `parent.helper`; the accumulation of enclosing type parameters is now one walk shared with the language server's go-to-definition. - **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` 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` / `Exn` 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)` 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` cell type is now a loud E336** ([#1206](https://github.com/aallan/vera/issues/1206)) — `(@Int = ...)` on `handle[State]` 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` at `State>`, an alias of `Id`, and `Exn>` 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` 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]` 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` 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` 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>`-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` (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` 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. - **A closure created while lowering a RETURN-position predicate is lifted** ([#1245](https://github.com/aallan/vera/issues/1245)). `_compile_fn` lifted the pending closures after the body and before `_compile_postconditions` — which is where the refined-RETURN guard, a tuple return's per-component guards and every `ensures(...)` predicate are lowered. A closure written in any of those registered on the context after the one lift pass had run, so it was constructed and never emitted: the module's function table stayed empty, the `call_indirect` its construction emits was orphaned, and the [#1185](https://github.com/aallan/vera/issues/1185) propagation dropped the function and every caller — a check-green, verify-clean program compiling to ZERO exports. The lift now runs twice, over a pending list that is CONSUMED and a closure-id counter that is handed back to the context, so the second pass sees exactly the closures the postcondition phase added and each one's stored `func_table_idx` still equals its `_closure_table` position. Neither pass is a special case: both go through one degradation net (`_lift_closures_or_drop`), so a failed lift drops the function identically whichever phase created the closure. The root cause was an ORDERING one rather than a limit on closures in predicates — the same predicate in PARAMETER position, lowered before the lift, always worked, and is carried as the control that says so. The `ensures(...)` twin (a closure in a postcondition, no refinement in sight) was reached by the same defect and is fixed with it. - **A cyclic refinement in a signature terminates** ([#1234](https://github.com/aallan/vera/issues/1234)). `type SelfRef = { @Int | ... apply_fn(fn(@SelfRef -> @Int) ..., 3) > 0 }` type-checks; used as a parameter type, each lifted closure's refined-formal guard lowered a predicate containing that same closure, which queued another one — `vera compile` and `vera run` never returned, on a check-green program, with no diagnostic and no bound. The lift worklist now carries the ancestry of closure identities that led to each entry and refuses an entry already on its own lift chain with a loud [E602] naming the closure it refused, dropping the enclosing function through the existing [#636](https://github.com/aallan/vera/issues/636) path — mirroring the cycle guard the registration pre-scan has carried since [#1210](https://github.com/aallan/vera/issues/1210) round 7. Keyed on the CHAIN rather than a seen-set of everything already lifted, which matters in both directions: the chain catches a cycle of any length — a mutual `A -> B -> A` and a three-type cycle hang identically without it, and neither is a self-reference — while a seen-set would refuse the SECOND legitimate lift of one predicate's closure, which `fn f(@R, @R -> @Int)` (two refined formals of one type) and a diamond both do. A finite nested refinement chain still lifts and runs. - **A closure's tuple-COMPONENT refinements are guarded at its boundary** ([#1235](https://github.com/aallan/vera/issues/1235)). A named function with a `Tuple` formal gets per-component [#746](https://github.com/aallan/vera/issues/746) boundary guards; a closure with the same formal crossed unguarded, so a violating component reaching an `AnonFn` through `apply_fn` or a collection combinator was never checked where the named path checks it — the verifier's projection fact assumes each component holds, and at an untrusted boundary only that guard backs the assumption. `_compile_lifted_closure` now consumes the same `_tuple_component_guard_sites` decomposition the named path consumes, on the formal and the return alike, with the components established before their boundary's top-level guard exactly as `_compile_fn` and `_compile_postconditions` order them. The emitter takes the rendered signature the message names rather than a `FnDecl`, since an `AnonFn` has none. Registration flips with it: `_signature_refinement_predicates` drops its `FnDecl`-only condition on component decomposition, which was there for exactly as long as the closure path emitted nothing for those components — so the co-extensiveness the derivation exists to hold still holds in both directions, and the fixture that pinned "a closure tuple formal registers nothing" now pins "it registers what it lowers". - **A `@Byte` literal inside a value-position join reaches the boundary at i32** ([#1212](https://github.com/aallan/vera/issues/1212)). `@Byte` is i32 (spec §11) while an int literal defaults to `i64.const`, so every write into a Byte boundary coerces the literal — and the [#865](https://github.com/aallan/vera/issues/865) / [#1092](https://github.com/aallan/vera/issues/1092) coercions each tested for a top-level `IntLit`, which the checker's bidirectional coercion is equally happy to type inside an `if` or `match` BRANCH. `let @Byte = if c then { 200 } else { 3 }`, the handler state-init / `put` / `resume` / `with` twins (the `resume` form is verbatim what the E602 clause-lowerability skip message recommends), a branch literal in a `@Byte` call argument, a generic constructor field at `Box`, and a `match` arm whose sibling arm is an i32 `@Byte` slot were therefore check-green programs that failed WASM validation with `type mismatch: expected i32, found i64` — loud at run, never silent corruption, but ten arms of one defect. Two of them are not missing MARKS. The ninth is a lifted closure's own RETURN, a missing MIRROR: a named function's `@Byte` return has been coerced at the return boundary since #865 (`i32.wrap_i64` when the body infers i64 into an i32 result) and `_compile_lifted_closure` simply had no such step, so `fn(@Bool -> @Byte) { 207 }` behind an `apply_fn` emitted `i64.const` into an `(result i32)` and the lifted `$anon_0` failed validation while its named twin ran; the same gate now sits in the same place on both paths. The tenth is a HETEROGENEOUS join at a `@Byte` return — the return boundary marked no leaves at all, and `_infer_block_result_type` reads the then-branch / first arm only, so a join whose read arm was already i32 and whose sibling was a bare literal took its annotation from one arm while the arms lowered at their own widths, with ARM ORDER deciding which way the module failed to validate (`expected i32, found i64` one way round, `expected i64, found i32` the other), on the named path and the closure path alike. The return is now a MARKING boundary on both, which makes every arm i32 and the join agree with itself whichever arm inference reads; teaching the decider to read every arm would not have done it, since the literal arm would still have emitted `i64.const`. The count is measured coverage rather than a closed class: the checker has exactly one Byte coercion, so the true enumeration is every position that propagates a Byte expectation, and nothing enumerates those in one place. There is now ONE branch descent (`_mark_byte_literal_leaves`, driven through `_mark_byte_write_value`) that marks a join's literal LEAVES — through `Block` tails, both `if` branches and every `match` arm — and every arm calls it BEFORE translating the written value, so the `IntLit` lowering and both join result-type deciders read the same marks and the `(result i32)` annotation agrees with its arms. Marking rather than overwriting an already-translated `i64` lowering also means each written value is translated exactly once, where the state-cell arms used to discard a whole join's translation (and the locals and pending closures it registered) to replace it. Nothing widens by accident: an ordinary `@Int` join still lowers at i64 (pinned on a value above 2^32, which an i32 store cannot represent), a Byte join with no literal arm is untouched, and an out-of-range literal never reaches the marking — the checker rejects it first (E170 directly, E301 through a branch). ## [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 ()`; 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)`, spec §7.9.2 — so a model reaching for Dafny's `old()` 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()` and rejecting it in the checker is not available: `old_expr: "old" "(" expr ")"` stops `old(State)` 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 > 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`, `Exn`, `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` becomes a real entry in the effect registry as part of this. It was recognised only by code generation (`handle[Exn]`), so `handle` and `throw` needed the very `effect Exn { op throw(E -> Never); }` block the new rule forbids; it is now in scope with no declaration, like `IO` and `State`. `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 {}` 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` 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` 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 (...)` 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, @OptionMapFn)` — 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_` 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> = [Some(@String.0)]; DB.query("... a = ? AND b = ?", @Array>.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>` 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>` 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 :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 (`` + `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>` 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` 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 `` 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>>`: 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>` 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 `` 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>; 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>` 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` with `type R = Result`) — 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` 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>` 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 `` 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 | ... }` — 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(>)`; `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`) 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;`, the mutual `type A = Future; type B = Future;`, and `type L = Array;` 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`). 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 = Int; type C = Wrap;`) is rejected on the same rule, and the edge set covers arbitrarily nested type arguments (`type A = Future>; 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; type B = Future;`) 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>;`) 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 = array_concat(...); @Array.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>` 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;` (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 = array_reverse(...); @Array.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_flatten(@Array>.0)`) compiled. `_translate_array_flatten` recovered the inner element type `T` only from a `SlotRef` `@Array>` 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` 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 `` layer of a `SlotRef` `@Array>` (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_flatten(@Grid.0)`, `type Row = Array; 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` 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>)`) 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 = array_reverse(array_reverse(x)); @Array.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;` 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; type Grid = Array;`, `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>`) 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.0)` compile instead of dropping), and all five shapes plus the sort compute correct values. The direct spellings (`@Array`, `@Array>`) 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 = Map; fn mk(-> @MyMap)` 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` resolves to `Map` 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;`) 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` — 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` element case its E135 gate (#945) already rejects with this exact rationale — not the boxed-ADT-field case (`Box` / `Option` fields live inside a heap layout and stay legal, as does `Map>`, pinned). Rejection also keeps one canonical form (DESIGN.md principle 3): `Map` is informationally a `Set`, which the fix text names. The type-resolution gate now covers `Map` (both positions, reported per position) and `Set`, keyed on erasure (`Future` 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` directly or behind `type FU = Future` / `type Task = Future`, 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`) 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` 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` component in a `let`-destructure or match binding erases like bare `Unit`** ([#1031](https://github.com/aallan/vera/issues/1031)). A zero-size `Future` 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` field needs `async(())`) — compared the bare string `Unit`, so the transparent `Future` 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` 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;`, 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`) 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;`) 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` 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` compiles in match-scrutinee position** ([#1038](https://github.com/aallan/vera/issues/1038)). `match await(@Future.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`, a user ADT, and a scalar `Int` alike — because the SlotRef/ResultRef WASM-type mapper did not treat `Future` as transparent the way the let-binding path already does. It now unwraps `Future` 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` 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` 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``'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` payload binds two locals** ([#1039](https://github.com/aallan/vera/issues/1039)). `let @Future = async("hello");` — and any `Future` whose payload is pair-represented (`String`, `Array`) — was check/verify-green but skipped the enclosing function (E602, "has no WASM representation"). `Future` is representation-transparent ([#841](https://github.com/aallan/vera/issues/841)), so `Future` 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`) or pointer (`Future`) payload stays a single local, and a zero-size payload (`Future`) still skips loudly. - **A closure that captures a `Future` free variable serialises it at the payload's width** ([#1044](https://github.com/aallan/vera/issues/1044)). Capturing a `Future` returned 0 and capturing a `Future` 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` 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>` elements are sized, loaded, and stored at the payload's representation** ([#1045](https://github.com/aallan/vera/issues/1045)). `Array>` trapped at WASM validation (`expected i32, found i64`) and `Array>` 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` 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` 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;`) 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;`, #1037) is unchanged. - **`vera run` displays a bare `Future` return as its string** ([#1047](https://github.com/aallan/vera/issues/1047)). A function returning `Future` 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` is representation-transparent ([#841](https://github.com/aallan/vera/issues/841)), so a `Future` 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>` 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>` 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>` returned wrong values (a 4-byte stride copied over the 8-byte i64 payloads), `Array>` 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` array element is stored at the payload's representation** ([#1058](https://github.com/aallan/vera/issues/1058)). `type FI = Future; let @Array = [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`) 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` (`type FI = Future`) 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;`, `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` — 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` 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>` (`type FlagA = Bool`) — or hidden behind another alias, `type X = Future; Array` — 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` 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` → `Future`) 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`, 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` 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` field `T`) registers as the generic 4-byte `i32` placeholder, but construction lays each instantiation out concretely — `Box` *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` 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` read the `String` header at the wrong offset, and `En(_, MkBox(@Int))` on `Entry` 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` — 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`, `Box>`, 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` (`type U = Unit;`), `Box>`, `Box` (`type FU = Future;`), 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` (`type MyInt = Int;`), `Box`, `Box`, `Box>`, 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` field compares its payload), and the width function (so the #1060 wildcard walks size a `MyInt`/`Future` 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` (`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>` — 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>`, `Array>`) 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;` 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` spelling compared correctly. This is the whole-OPERAND sibling of #1076, which grounded type ARGUMENTS (`Box`) 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` 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 | 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>` 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` (`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`) alike. A genuinely non-Eq alias (`type BadArr = Array;`) 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;`) and `show(@Future.0)` loud-skipped the enclosing function ([E602]): the argument's inferred type reached the top-level show/hash dispatch as `FI` / `Future`, 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>;`, 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` 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 = 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` via `type MB = Byte;` — the spelling the #1086 fix newly admits under `forall>` — 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` — 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 `` 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`) 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` `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 fn wrap(@T -> @P2)` — 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`), 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>` 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` 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`), and aliased-payload (`Future`, `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>` 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` 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>` 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>` to the target spelling and extracts the element — but returned a COMPOUND element (`Future`) verbatim, skipping the payload canonicalization the sibling bare-element branch performs (`type FF = Future; type Rows = Array` 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` → `Future`); 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$$` — 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` values** ([#1097](https://github.com/aallan/vera/issues/1097)). A `Map>` value round-trip (`map_insert` / `map_get` / `map_values`) was check- and verify-green but compiled exit-0 to an INVALID module. `Future` is representation-transparent ([#841](https://github.com/aallan/vera/issues/841)) — a `Future` IS an i64, a `Future` 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` 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;`) and strips `Future<…>` wrappers to the payload before tagging — so a `Future` value still takes the zero-size loud [E602] skip and a `Future>` value the existing Array-reject skip, never an invalid module — and the same fix covers `Set>` 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>` 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` 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` 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.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 = 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.0` where `Option` 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 (`` 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.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>` 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` 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]`'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 = 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]` 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` 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`, `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`/`Tuple` 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`/`Tuple` widening inside a `forall` 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` `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` 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`/`Tuple` 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` 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`), 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`, 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` repro was masked by a name coincidence (the unsubstituted parameter `@Array` 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>.0)` under a user `forall` was rejected with a spurious `E202`, and the issue's own suggested workaround (`forall`) 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 fn nothing(@Unit -> @Option)`, 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`), 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` and a `forall` 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` 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` return type `@Option`, a `let @Option = 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, expected Option`; 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` now threads the declared forall var through the nested-constructor field path, so `Some(None)` returned as `@Option>` 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`) 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>, expected Option>`, 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>>`) descends every level, and the ill-typed direction is unchanged (`Some(Some(5))` stays `Option>`, 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.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.result == None` was rejected `E142` `Cannot compare Option with Option` — in BOTH operand orders, in `requires` as well as `ensures`, and (the defect was wider than the `forall` case) even at a concrete `@Option`, which minted `Option` 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.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` shapes, plus non-`Option` and cross-ADT rejection pins). - **A NESTED payload-less constructor result (`Some(None) : Option>`) 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` and `Option>` 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>`; 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` `==` / `!=` contract (`ensures(Some(None) == @Option>.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
` 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` 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` 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`) 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` 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` 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` 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` binding no longer double-reports `E135`.** An array literal that is the RHS of an annotated `Array` binding or return was rejected by *both* the type-resolution gate (the `@Array` annotation) *and* the literal gate (`_check_array_lit`) — two `E135`s for one root cause. `_resolve_type` returns the `Array` 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 | 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 = [()]` 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` **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` 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 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` — a *second* zero-size type, since codegen makes `Future` transparent to `T` (#841), so a `@T` read at `T = Future` (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` 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 `` 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_` 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 | 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` — which codegen erases identically, since `Future` is transparent to `T` (#841) — so a `Future` 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`, 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>` 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` 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` / 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`, `List>`), `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>` 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 { BNil, BCons(T, Box>) }`, `show(BCons(1, BNil))` passes `vera check` but its recursive-helper machinery mints a strictly deeper, DISTINCT type at every descent (`Box>`, `Box>>`, …); #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_` 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`, `Tree`, `List>`, 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>` 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 { Nil, Cons(T, List) }` 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_` / `$hash_`, `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_` 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 $_` 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`, `Tree`) 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, Forest)`) 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 { Nil, Cons(T, List) }` 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_` / `$hash_`, `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_` 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 $_` 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`, `Tree`) 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 { Empty, Grove(Rose, Forest) }` / `data Rose { Bloom(T, Forest) }`, NEITHER field of `Grove` IS a bare `T` — they are `Rose` and `Forest` — so no constructor argument directly pinned `T`, type-argument recovery returned `None`, and the site fell back to the bare head `Forest`, losing ``. 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` whose `Bloom(T, …)` declaration pins `T = Int` from the literal `1`, yielding `Forest`. 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` now routes `eq` to the structural `$eq_Forest` helper (which cross-calls `$eq_Rose`) 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` 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` 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 == Tuple` 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>`) 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>` was reconstructed as `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>>`, `Chain>`, …), 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>`) now compiles and runs instead of a spurious `[E613] '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`) — 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`, 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>>`) 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`) rather than the needed `Option>`, 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` 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>` context selects the `Option>` sort (whose argument slot correctly expects `Option`, 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` context — and post-#884 the datatype-sort mangling is injective, making `Option` (`Option_LNat_R`) and `Option` (`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`, `Option>`, and a user `Box>` 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` 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` (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` behaviour and `W002` warning — while a *user* `forall` 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`) 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` 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 { 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` — 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` (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` ⊔ `Res` = `Res`), 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` 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>`) 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` 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>`, `B` fixed to a non-Eq `Array`) or structural (`data W{ K(Array, B) }`, `id1(K([1], 7))` → an `Array` 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`, 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` 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`, never the internal `?` sentinel) and its fix is a compilable annotation (`let @Res = ...;`). The #772 single-type-parameter path (`Box` accept, `Box` 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` 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`'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.0 = MkBoxInt(False)` — the flat ADT's Bool constructor witnessing a `Box` 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` → `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.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`/`Box_Int` collision test, and the `_get_or_create_tuple_sort` site by a `Tuple, Int>` vs `Tuple>` 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>` → sort `Array_Box_LInt_R`) produced the non-matching candidate `Box` 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>` 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 fn(@T -> @T)` and `data Box { 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>`) 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` is a uniform pointer), which left the codegen `Eq` gate (`_adt_satisfies_eq`) with no `` to inspect. Originally (pre-#773) this **false-accepted** `Box` and emitted an unsound equality comparing the wrong representation; after #773 made derivation structural it flipped to an **over-reject** — a `Box` that *should* derive `Eq` was refused with a spurious `E613`, even though the slot-ref call form (`let @Box = …; eq2(@Box.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`) 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`, 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` / `Box` (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` and `Box` 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) }` / `data B { MkB(A) }`, or a self-recursive `data C { MkC(Tuple) }` — 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` 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 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$$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 { 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>`) 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>` 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 = MapFn;`) 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` and `result_map` as `forall`; 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>` and a user ADT literally named `Map_String_Int` both mangled to `g$Map_String_Int`, as did `g` and `g` 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_` 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` → 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_` 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>` 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>>` 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> = …` 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` thrower — the built-in is a pure `Result` 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 `
` 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` position (`decimal_div` → `Option`, or a non-generic user fn declared `-> Option`) 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`) in `Option` 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` and `Option`) is **sound** for the bare-`@T` binding it feeds: a `forall` 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` 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 (`$where$` — `$` 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` 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` body whose parameters are compilable-looking (`Array` → `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`-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`-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` 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` 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 -> @Box) ensures(@Box.result == @Box.0)`) resolved both operands to `Box` — 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`, distinct from a concrete non-`Eq` argument like `Box>`) as non-dispatchable and falls back to the pre-#912 scalar lowering.  This is SOUND because the scalar compare never runs: `Box` 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.result == @Box.0` is lowered STRUCTURALLY (`call $eq_Box_LInt_R`; `Box` 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.result == @Box.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>` 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` 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>` → `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` whose field is again `List`) is still collapsed so the traversal terminates.  The parameterized argument type (`Option`, 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`, 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` 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` 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>` handler emitted `$vera.state_get_Tuple` and an `Exn>` a `$exn_Tuple` tag — raw `<`, `>`, `,`, ` ` are illegal in a WAT identifier — while a `State>` handler body called `$vera.state_push_Option` (the base name `Option`) against an `(import …)` decl named `Option` (`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)` read, and the host registration in `vera/runtime/state.py` — the `ExecuteResult.state` cell key stays the readable `State_`), 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>` handler pushed its caught-payload binding under the base name `Option`, so `@Option.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)` 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>)` 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>) == 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>>` and `State>>` both collapsed to the slot name `Option` → 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'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>` and two distinct nested-composite State/Exn types resolve to distinct, correctly-matched keys.  Primitive `State` / `Exn` — 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)`) 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)` 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 `` (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  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`; the #747 non-literal tuple-destructure `let @Tuple = 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` (e.g. `State>`, `State>`) 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` 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>` (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>` (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` / `State`) → `0n`, and `i32` (every composite/ADT pointer, plus `State` / `State`) → 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>`, `State>`, a user-ADT `State`, and a bare-`State` regression); mutation-validated both ways — reverting to the substring heuristic flips the `State>` and `State` tests RED (the pointer-typed default crash), and the naive exact-`Float64`-match alternative flips *both* composite tests RED while the bare-`State` 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 ``: 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` 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()`-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` 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 `` 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` 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`) 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`, `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`, `IO.get_env` → `Option`) 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

- **`` — 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()` — 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`; 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` 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(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` return type (previously `[E605]`-skipped — `Future` is transparent in the type mapper).  The `await` handle-check keys on the literal type `Future>` 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. 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: `), 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: ` 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 (f)` plus a "replace `` 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`) — 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` 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`) 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/.py` modules, each a `register_(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`) 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` 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: @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.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` derives `Eq` while `Box` (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 | 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 | 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`) 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` 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 = Tuple`) substitutes its type argument when resolving component types, so `Box` 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` gets a proper datatype sort rather than degrading to a scalar model; and a refined-ADT sub-pattern bind (`Some(@PosInt)` on `Option`) 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` 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 | 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`), 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>`) 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 | P }`) and a **refined tuple source** (a `let`-destructure of a `{ @Tuple | 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`), 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` and `Set` 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` constructors on both runtimes allocated a payload — a string, an `Array` 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`, `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` → ``; `` → `` appending after the original source verbatim; functions already naming the effect are skipped, with identity the base name before type arguments so `State` is not added next to `State`), 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` and `Set` 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)`.  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 `` 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() == ""` 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` / `Set` / `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.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; ; 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 ; 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 = [_, _]` 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_(arr, i)` function call, parallel to the existing `length_` 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_` 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` parameters are now declared as constants of uninterpreted `Array_` sorts; pre-fix they fell through to `declare_int(z3_name)` because `Array` isn't in the SMT layer's `_adt_registry`, making `Array` 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` 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; type Grid = Array;`) 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` 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`) 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` 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` 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`-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 | array_length(@Array.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`; 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`.  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` 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  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(...) {  }` 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 '' 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  /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` 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` transparency in the `_canonical_wasm_type` convenience wrapper, parallel to `_slot_name_to_wasm_type`'s existing `Future` strip-and-recurse handling; (c) parameterised-alias substitution via a new `_substitute_type_vars` helper, so following `type Box = Array` with a concrete `Box` 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`) to a parameterised one (`type Box = Array`) 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; @Id>` 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` 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` 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` 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` 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` 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` 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 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` 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` 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` / `Set` / `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`).
  - `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` 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`), 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` / `Set` / `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` 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`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`); 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`) 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` or `String` therefore silently saw an empty value — `array_length(@Array.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` 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` 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` (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>` 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>` would compile but silently treat array values as opaque pointers. `Map>.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` 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[@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 `` 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 `` 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`, `Result`, user `data` types, `Map`/`Set`/`Decimal`/`Regex`) actually work because they're single-i32-pointer values, not pairs. Only `String` and `Array` 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`) 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 `
` 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: `` entries, `rel="llms-txt"`/`rel="llms-full-txt"` directives, schema.org JSON-LD block, and the inline `