Skip to content

feat(inference): add xAI Grok provider (#425) - #10

Merged
chethanuk merged 170 commits into
mainfrom
feat/issue-425-xai-grok-provider
Aug 13, 2026
Merged

feat(inference): add xAI Grok provider (#425)#10
chethanuk merged 170 commits into
mainfrom
feat/issue-425-xai-grok-provider

Conversation

@chethanuk

@chethanuk chethanuk commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Fixes aallan#425

Summary

  • xAI's chat-completions endpoint is OpenAI-compatible — bearer auth, choices[0].message.content. _call_inference_provider branches only on auth_style and response_style, so Grok needs no new dispatch code. One row in _PROVIDERS is the whole change, which is what the registry comment promises.
  • Endpoint and env var are as the issue specifies: https://api.x.ai/v1/chat/completions, VERA_XAI_API_KEY. The default model is not — see below.

The model the issue specifies is no longer documented

The issue asks for grok-3-mini-fast-beta. xAI's model list documents only grok-4.5, grok-4.3, the three grok-4.20 variants, and grok-build-0.1 — no grok-3 family at all.

Default is grok-4.3, the cheapest current general-chat tier ($1.25/$2.50 per M tokens), keeping the cheap-tier convention the registry already follows. grok-build-0.1 is nominally cheaper at $1.00/$2.00 but is a special-purpose build model, so not a sensible general default.

What I am deliberately not claiming. An earlier draft of this PR said shipping the issue's name "would make every Inference.complete call fail." I never tested that — I verified the name is undocumented, which isn't the same thing, and a vendor may 404, error, or alias a withdrawn slug.

xAI does publish a retirement page with a table mapping each withdrawn slug to a redirect target, stating "The slugs themselves continue to resolve, so you do not need to change your code to avoid breakage." That table covers plain grok-3grok-4.3 but not grok-3-mini-fast-beta, grok-3-mini-fast, or grok-3-mini, and says nothing about slugs outside it. So "it still resolves" is plausible by analogy with the sibling slug and undocumented for the name actually requested. With no api.x.ai credential I can't settle it, so nothing here asserts a call-time outcome in either direction — the default is simply a model xAI currently lists. (That page does independently support the tier choice: plain grok-3's own redirect target is grok-4.3.)

Worth noting CI could not have caught the stale name either way. Every provider test asserts sent_body["model"] == "<the literal from the registry>", so the test pins the value to itself and stays green regardless of whether the ID resolves. Same blind spot applies to the two names flagged at the bottom.

  • The row is appended last, deliberately. Insertion order is the auto-detect precedence — the loop picks the first provider whose key is present — so appending leaves the behaviour of every existing key untouched. test_multi_key_auto_detect_respects_provider_order reads next(iter(_PROVIDERS)) and would break on a prepend. There's now a comment at the append point saying so.

On considering all three siblings

You noted that registry changes should consider aallan#450 (DeepSeek) and aallan#451 (Gemini) too.

Files touched beyond the row

The provider list is enumerated in more places than the issue lists. Mirrored into the spec §9.5.5 table, ENVIRONMENT.md, SKILL.md, AGENTS.md, FAQ.md, the landing-page copy in scripts/build_site.py and docs/index.html, and the runnable-key header comments in the two inference examples, then docs/ regenerated. The issue names vera/codegen/api.py and tests/test_codegen.py; both moved in aallan#421 and are now vera/runtime/inference.py and tests/test_codegen_host_effects.py. There is no README env-var table to update.

Test plan

  • Both tests written before the row and confirmed RED on ValueError: Unknown inference provider 'xai' — so they pin the registry entry, not the dispatch code they share with Mistral.
  • GREEN after the row: 58 → 60 passed in TestInferenceProviderDispatch's file.
  • Mutation check: removed the row, both go RED again, restored.
  • test_xai_provider asserts the endpoint URL, Authorization: Bearer …, absence of X-api-key, the default model, and absence of Anthropic's max_tokens — so an OpenAI/Anthropic mix-up in either direction fails.
  • It asserts the exact turn list, [{"role": "user", "content": "prompt"}], not just that messages is present. The presence check it replaced passed with the role flipped, the content dropped, or the list emptied:
Mutation old new
role → "assistant" PASS RED
content dropped PASS RED
messages: [] PASS RED

The same presence-only check remains on anthropic, openai and mistral, and test_moonshot_provider has no messages assertion at all — four tests, all pre-existing on main and deliberately untouched here. The anthropic one looks the highest-value: its payload is the only one that differs (max_tokens: 1024) and nothing pins its turn list.

  • test_xai_auto_detect asserts the key is picked up when it is the only one set; the existing multi-key precedence test still passes unchanged.
  • Precedence is parametrized over every provider ahead of xai, not just the first. The single-provider version it replaced stayed green if the row jumped openai, moonshot or mistral — only a jump past anthropic failed it. Relocation matrix:
xai row moved to Result Counts
first RED 4 failed, 1 passed
before openai RED 3 failed, 2 passed
before moonshot RED 2 failed, 3 passed
before mistral RED 1 failed, 4 passed
restored (append-last) GREEN 64 passed

The failure count equals the number of providers jumped. The parametrize list is a literal tuple, deliberately: a list sliced from _PROVIDERS would shrink along with a relocated row and pass vacuously.

  • No API key needed — urllib.request.urlopen is patched, following test_mistral_provider.
  • mypy vera/ clean, ruff check . clean, conformance (179), examples (42), corpus canonical (227), check_site_assets.py, check_doc_counts.py all green.
  • Full pytest tests/ and pre-commit run --all-files green.

Exercised against a live server, not only a mock

Every provider test in the suite patches urllib.request.urlopen, so the bearer + openai arms this row depends on had never met a real socket. Since the whole premise is "no new dispatch code is needed", I checked that end to end rather than trusting it:

  • _call_inference_provider called with no patching at all, against two live OpenAI-compatible endpoints, using a runtime-injected row of exactly this shape. Both returned real completions.
  • vera run examples/inference.vera driven through the shipped xai row — auto-detect from VERA_XAI_API_KEY, bearer header, choices[0].message.content parsing, and the full compile → wasm → wasmtime → host-import → HTTP chain — returning a real classification.

One honest limit: I have no api.x.ai credential, so for the toolchain run the row's url was temporarily repointed at another OpenAI-compatible host and reverted immediately. That validates every part of the row and the path except that x.ai's own hostname accepts the request. Nothing in the tests or the diff touches the network — all committed tests remain mocked.

Two model IDs already on main look retired too — not touched here

Found while verifying this one; flagging rather than fixing, since neither is in this issue's scope and each deserves the same check:

  • gpt-4o-mini is absent from OpenAI's active model list; the cost-tier recommendation there is now gpt-5.6-luna.
  • kimi-k2-0905-preview is absent from Kimi's pricing page, which lists Kimi K3, K2.7 Code, K2.6, and Moonshot V1. Incidentally the spec's Moonshot docs host is stale as well — platform.moonshot.ai now 301s to platform.kimi.ai.

claude-haiku-4-5-20251001 and mistral-small-latest I didn't check. Delisting isn't always removal from the API, so I'd rather not batch-edit them on a hunch — happy to file one issue covering the whole registry, or fold the fixes in here if you'd prefer.

aallan and others added 30 commits August 6, 2026 22:37
…ruction (aallan#1208)

`TypeAliasInfo` carried only `resolved_type` — the semantic collapse
computed at registration time, which cannot be walked back to the
spelling.  The one naming renderer landing in `vera/naming.py` needs the
SOURCE-level alias body to build its `AliasEnv` from a live
`Environment`, the same map codegen already keeps in its own
`_type_aliases` side-table.

Adds `body: ast.TypeExpr | None = None` and populates it at both
construction sites (the checker's and the verifier's `_register_alias`).
Defaulted, so any other constructor stays valid; nothing reads the field
yet.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…#1208, aallan#1209)

Six subsystems independently rendered "the name of this type expression"
and disagreed about aliases; a name minted one way and looked up another
misses silently, which is the aallan#1208 / aallan#1209 bug class.  This lands the
single renderer, with the CHECKER's current rendering as the rule.  No
consumer flips yet — the module is additive, and the differential that
proves it byte-identical to the checker follows.

THE RULE, as implemented: syntactic (alias-opaque) HEAD, fully resolved
type ARGUMENTS; a refinement renders its base at top level and the
predicate-elided form in argument position; a function type renders `Fn`
at top level and its full spelling (effect row SORTED) in argument
position; every renderer is total and unresolvable types render `?`.
Argument resolution rebuilds the checker's own semantic `Type` and hands
it to the checker's own `pretty_type` / `canonical_type_name`, so
byte-identity is structural rather than a coincidence to maintain.

Alias visibility follows REGISTRATION ORDER: an alias body sees only the
aliases declared before it, exactly as `_register_alias` resolves each
body against the table as it stood.  A forward reference stays opaque and
a cycle terminates on the same placeholder the checker produces — the
ordering restriction is well-founded, so no depth bound is needed.

`substitute_named`, `resolve_alias_type_expr` and
`AliasResolutionDepthError` MOVE here from `vera/slots.py` (unchanged,
re-exported there for the existing importers).  `resolve_scalar_alias_te`
stays behind until its callers flip.

`tests/test_slot_naming.py` is the rule table: 61 tests pinning each
clause to an exact rendered string, including the two orderings the
rounds cost — arguments resolve before heads, and alias parameters
substitute before the refinement branch (both mutation-validated: each
inversion turns its own tests red).

Co-Authored-By: Claude <noreply@anthropic.invalid>
…al gate (aallan#1208)

The consolidation takes the CHECKER's rendering as the rule, so the
load-bearing claim is byte-identity with the checker — not "the module
looks right".  A unit suite cannot establish that: the divergences that
caused the aallan#1208 / aallan#1209 bug class live in alias corners nobody thought
to enumerate.

This instruments the checker's two naming entry points
(`_type_expr_to_slot_name` and `_slot_type_name`, the latter also
carrying every `_slot_ref_key` reference, so the binding side and the
reference side are both covered) to record a (checker, module) pair on
EVERY call, then sweeps the whole `.vera` corpus — 42 examples, 183
conformance programs and their module fixtures, the 283-file PR aallan#1202
probe corpus — plus a 24-program inline battery aimed at the alias /
refinement / function-type / shadowing corners, and asserts ZERO
divergence over every recorded pair.

The module-side `AliasEnv` is built AT RECORD TIME from the live
environment, so each comparison sees the alias table and `forall` scope
the checker had at that instant — and exercising the C0 `body` field on
the live path is part of what is being proved.  The checker's own result
is what is returned to it, so the instrumentation cannot change what the
checker does (pinned).  A program that fails to CHECK still contributes
every observation recorded before it, since naming runs while
diagnostics accumulate and rejected programs reach the malformed shapes
disproportionately often; only parse failures contribute nothing, and
they are counted.

Measured on this corpus: 6,109 observations, 1,438 under a non-empty
alias environment, 81 where an argument names an alias pre-resolution,
533 files swept, 5 parse-skipped (pre-existing probe fixtures), 0
divergences.  The floors asserted in `test_corpus_sweep_is_not_vacuous`
sit below those so corpus drift shows up as a failure rather than as a
quietly emptier gate.

`test_differential_gate_detects_divergence` is the live proof the gate
can go red: perturb the module renderer and the harness reports every
pair, with the checker's own results unchanged.  Mutation-validated
beyond that — dropping alias resolution, and dropping `Decimal`'s
argument-eliding branch, each turn the sweep red.

Parse + check only, one checker per program: the full sweep runs in
~1.4s, so it stays a default-CI test rather than a slow-marked one.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…lan#1208)

`_type_expr_to_slot_name` and `_slot_type_name` (the latter carrying every
`_slot_ref_key` slot reference) now call `naming.slot_name` with an
`AliasEnv` built from the live checker environment.  The in-checker
rendering composition is gone; `canonical_type_name`, `pretty_type`, and
`_resolve_type` stay, because they serve type checking broadly — only the
NAMING composition moved.

Closes the data-type corner `_resolve_named` recorded.  A user may declare
`data Float` or `data Decimal` (both check clean), and the checker's
`_resolve_named_type` reaches its data-type branch BEFORE the `Decimal` and
removed-alias branches, so `@Option<Float>` renders `Option<Float>` and a
user `Decimal` keeps its type arguments.  `AliasEnv` gains `data_types`,
populated from `env.data_types` or from the walked `DataDecl`s, and the
branch sits at the checker's precedence position.  A narrower ordering
sub-corner remains and is now stated in both the module and the unit table:
ADT visibility is not declaration-index-bounded the way alias visibility is,
so an alias body naming a special-cased ADT declared BELOW it diverges;
closing that needs the two registries merged into one index space, a data
change rather than a rendering change.

Naming is total and silent by design, so delegating would have dropped the
E133 / E134 / E135 / removed-alias diagnostics the old composition emitted
as a side effect of resolving each argument — at a slot reference
(`@Option<Box>.0` for a parameterised `Box`) that incidental report is the
only one there is.  `_check_slot_name_args` keeps them, walking exactly the
traversal the old composition took.  Proved by a corpus-wide and a targeted
diagnostic differential: byte-identical diagnostics before and after.

The naming differential is inverted so it stays non-vacuous.  With the
checker delegating, comparing it to the module compares the module to
itself; the legacy side is now a test-local rebuild of the historical
composition from still-live checker machinery, and the module side is what
the checker returns.  Same corpus, same floors, same red-proof — and the
perturbation now moves only the module side.  6,111 observations over 534
files, 1,449 under a non-empty alias env, 83 with an alias in an argument,
zero divergences.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…allan#1208)

Plumbing for the renderer swaps that follow: each subsystem now HOLDS the
`AliasEnv` it will name against.  Nothing reads it to render yet, and no
rendered output changes — proved by a WAT + codegen-diagnostic differential
over all 510 corpus programs, byte-identical before and after, and by
`vera check --explain-slots` output identical over examples + conformance.

* `CheckArtifacts` gains `alias_env` (the entry module's, always present)
  and `module_alias_envs` (per resolved module, keyed by module path and
  gated on `collect_module_artifacts`) — mirroring the aallan#987 `module_artifacts`
  wiring end to end, and for the same reason: aliases are module-scoped
  (spec §8.4.1), so an imported body must be named against ITS namespace.
* `WasmContext.set_type_aliases` / `set_type_alias_params` are REPLACED by
  one `set_alias_env`.  The pair had to be overlaid together (aallan#1184) and now
  cannot be half-updated.  Every consumer in `vera/wasm/` reads
  `_alias_env.aliases` / `.alias_params`.  One semantic seam needed care:
  `alias_params` keys every alias and maps a non-parameterised one to `None`,
  where the flat map simply omitted it, so the one MEMBERSHIP test on it
  becomes the equivalent `is None` test.
* `CodeGenerator` holds `_alias_env`, DERIVED from its flat maps rather than
  taken from `CheckArtifacts`: codegen's alias view is the prelude overlaid
  by the main file (and, inside `_module_alias_scope`, by the compiling
  module) over the transformed, monomorphized AST — not the checker's view.
  Sourcing it from the check would name against a table codegen does not
  otherwise use, which is the very split aallan#1208 closes.  `_sync_alias_env`
  re-derives it at each point the flat maps change, `_module_alias_scope`
  swaps and restores it with them.
* `ContractVerifier._alias_env` is built at the end of `register_program`;
  `SmtContext` takes an `alias_env` (threaded from both cold sites, rebound
  per function on the warm session exactly as `_fn_lookup` is); `MonoContext`
  gains the field, populated by both drivers; the tester carries it from
  artifacts down to the Z3 input generator; the LSP `Analysis` lifts both
  envs out of its artifacts; and `vera check --explain-slots` now takes the
  artifact-returning check so the table can be named against the checker's
  own aliases (only that flag pays for it).

The alias-map parameters of `resolve_alias_type_expr`, `resolve_type_alias`,
`resolve_fn_type_alias`, `resolve_scalar_alias_te`, and the async-fusion
predicates widen to `Mapping`, since what reaches them is now an `AliasEnv`
field.  Read-only either way.

Two test FIXTURES construct a `WasmContext` by poking the private alias map;
they set the env instead.  No expectation changed, and the aallan#633 cycle-guard
test still goes red when the guard is mutated — checked, because an empty
env would have made it pass vacuously.

Co-Authored-By: Claude <noreply@anthropic.invalid>
`_resolve_named` recorded a split: alias visibility followed registration
order, ADT membership was a flat set, so `type M = Decimal<Int>;` declared
ABOVE `private data Decimal { ... }` resolved against an ADT the checker's
table did not yet hold when it resolved that body.  The checker gives the
built-in `Decimal` (arguments dropped) and, for the `Float` spelling, `?`;
the module gave the ADT.  Closing it needed the two registries in ONE index
space, which is a data change rather than a rendering change.

`AdtInfo` and `TypeAliasInfo` gain `decl_index`, stamped from one shared
per-module counter (`TypeEnv.next_decl_index`) at the moment of registration
by both the checker's and the verifier's passes.  `AliasEnv.data_types`
becomes a name -> declaration-index `Mapping` in the same space as `_order`,
and `_resolve_named` applies the SAME bound to ADT membership that aliases
already got.  `-1` reads as "precedes everything", which is what the built-in
ADTs are and what any unstamped construction site conservatively gets; the
top-level bound becomes an explicit `_UNBOUNDED` sentinel, since the shared
space now runs past the alias count.

Codegen's `_sync_alias_env` is the third env builder and needed its own index
space.  `_decl_order` stamps aliases and ADTs together in three blocks
ordered as the checker sees them: built-ins, then the prelude (a NEGATIVE
block, because `inject_prelude` PREPENDS its declarations while codegen
registers the main file before injecting them -- without it a main-file alias
over a prelude alias would resolve opaquely here and fully at check), then
user declarations from 0 up, each module's absorbed in its own source order
as it is captured.  A sweep of every parameter / let / pattern / slot-ref
type expression in the 505-program corpus renders identically through
codegen's env and the checker's: 9,145 observations, 0 divergences.

The pinning test flips from documents-divergence to asserts-agreement and
gains the mirror spelling (`data Float`, whose bound leaves the removed-alias
`?` reachable) and the top-level control (the bound stops at the alias body,
so a slot naming the ADT directly renders against the whole table whatever
the order).  The differential battery gains all four shapes, each carrying
its own prelude -- the corner IS where the `data` sits relative to the `type`
-- and is mutation-validated: removing the bound turns 10 of its 24
observations red.  The corpus itself is unmoved (510 programs, `check --json`
diagnostics and probe `run` output byte-identical), as expected for a corner
that requires an ADT named after a built-in or a removed alias.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The checker already delegated its naming.  Everything downstream still
re-derived it: the monomorphizer's De Bruijn recount, codegen's bind sites
and its reference resolution, a five-method clause-scope mirror of the
checker's rendering, the verifier, the SMT layer, the tester, and
`--explain-slots`.  They agreed with each other and disagreed with the
checker, which is the aallan#1208 bug class.

This flips them all, in ONE commit, on the BIND side and the REFERENCE side
together.  Four prior attempts retreated because a partial flip produces
silent index skew — a binding minted under one rendering and looked up under
another either dangles or, worse, lands on the wrong member of the checker's
equivalence class.  Atomicity is the defence, so the two sides of every
lookup move in the same change or not at all.

DELETED, not adapted:

* the clause-scope mirror in `wasm/inference.py` — `_canonical_clause_slot_
  name`, `_checker_arg_name`, `_checker_form_slot_name`,
  `_checker_form_arg_name`, `_head_resolves_through_refinement` (184 lines).
  Its refined-argument deviation existed because the reference side could not
  spell the predicate-elided form; both sides render it now, so they meet
  there.
* BOTH class-collision `CodegenSkip` gates in `wasm/calls_handlers.py`.  They
  guarded exactly the skew this removes: one direction split a checker class
  across two codegen keys, the other merged two into one.  The shapes they
  refused now lower with the checker's semantics, and their three tests
  assert the run values instead of the refusal.
* the "retreat to opaque-only refs" comment block in `wasm/operators.py`,
  which described the pre-consolidation compromise.
* both `or type_name` fallbacks at the handler-clause bind sites.
  `type_name` was the EFFECT's type argument, not the pattern's name, so the
  fallback bound the clause parameter under a key the checker never used.
* `slots.slot_ref_name`, which has no consumers left.

Two derivations deliberately stay syntactic, and both are about a type's
REPRESENTATION rather than about naming a binding: `type_expr_slot_name`
still answers the WASM width / erasure walks and the structural-`Eq`
oracle, and the State/Exn cell FAMILY keeps its own opaque fallback, now
derived INSIDE `_family_name` / `_family_name_te` (`family_fallback_name`)
so the eight call sites cannot pass different ones.  Resolving the family
changes the emitted import surface — that is aallan#1209's — and the checker's
argument rendering is not mangle-safe: a refined argument renders
`Option<{@int | ...}>`, which `mangle_type_name` does not escape.

The refinement guard had to move with the reference side: it binds its
predicate over the base's slot name, and a predicate's `@Base.n` now
resolves through `naming.slot_ref_key`.  Both `_refinement_guard_parts` and
`naming.refinement_binder_parts` name the binder with `slot_name`, which is
also what `_check_one_refinement_predicate` binds it under.

BLAST RADIUS, measured over all 510 corpus programs (`check --json`
diagnostics for every file, `run` for every probe), before and after: SIX
files differ, all in `tests/probes/`, all one class — a program that died on
a dangling-slot [E699] now resolves and runs with the value the CHECKER's
binding rule gives.  Three of them record that value in their own header
comment, and it is what they now print.  No `check` diagnostic moved
anywhere, in code, severity, line, or message; no example and no conformance
program is affected.  `tests/test_slot_naming_blast_radius.py` pins the set
by path with its expected value, plus five slot-heavy sentinels outside it,
and is mutation-validated in both directions: reverting either the bind side
or the reference side turns it red.

`--explain-slots` is the user-facing oracle and now reports the checker's
names.  On the corpus's one signature-level rename it says `@Option<Int>.0`
for a parameter written `@Option<Cnt>`; on two constructed merges it reports
one stack where it used to report two, and the programs — which were HARD
codegen crashes on check-green input before this — return exactly the
parameter the table names.

One regression the corpus could not see was found by hand and fixed here:
`monomorphize_fn` rendered a clone's binders against the DRIVER's alias
namespace.  Aliases are module-scoped (§8.4.1), so an imported generic's
module-local alias stayed opaque, the recount missed the merge, and the
clone — which codegen emits under the DEFINING module's scope — resolved a
reference onto the wrong parameter, silently, with the right arity and type.
`monomorphize_fn` now takes the env it must name against, and each of the
four codegen clone sites supplies its origin module's through
`_clone_alias_env`.  Pinned against its single-namespace control.

E130 diagnostic wording is untouched (`vera/errors.py` unchanged) and the
SQL-provenance suite is byte-identical.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…llan#1208)

The three PRESENTATION surfaces were the last consumers still resolving a
`@T.n` by hand.  Two of them did it with the reference's bare HEAD against a
table keyed by the full rendered name, so a parameterised reference could
never match — and both failed CLOSED, which is why neither had a symptom
anyone would report:

* `verifier._pre_at_call_site` abandoned the whole substitution on the first
  parameterised slot, so E501 fell back to its generic wording ("Add a
  precondition to 'caller'…") on exactly the signatures where naming the
  concrete arguments helps most.  `@Wrap<Int>.0` in a callee's `requires`
  now renders `At this call site: 0 > 0`, and the fix shows the guard.
* `lsp.definition_at` returned nothing, so go-to-definition was silently
  dead for every container-typed parameter — `@Option<Int>.0`, `@Map<String,
  Int>.0`, an alias-spelled parameter reached from a canonical reference.

Both now key with `naming.slot_ref_key` against the same env the binding
side is rendered in.

Auditing the third consumer's env for module-correctness turned up a
divergence none of them was rendering right: a function's OWN `forall`
variables shadow same-named module aliases, and the checker binds its
parameters with them in scope (`_check_fn` step 1, before step 4).  Rendered
against the bare module env, `type T = Int` + `forall<T> fn g(@option<Int>,
@option<T>)` collapses two checker stacks into one, and `--explain-slots`
reported `@Option<Int>.0` as parameter 2 where the checker resolves it to
parameter 1 — a wrong answer, not a vague one, from the tool whose whole job
is answering that question.  `slot_table` now takes the owning function's
`forall_vars` as a REQUIRED argument, for the same reason its `env` is
required: the omission has no symptom of its own.  `slots.fn_slot_scope` is
the one narrowing, so the binding side and the reference side cannot end up
scoped differently.

The tester's threaded env is verified inert-but-correct rather than assumed:
`_get_param_types` matches a parameter's SYNTACTIC head against `PRIMITIVES`
and never resolves aliases, so an alias-typed parameter is skipped (E701)
before any Z3 variable is named.  Both halves are pinned — the renderer
canonicalizes a type-argument alias given a real env, and the gate that
currently bounds its reach — so a future `_get_param_types` that does resolve
aliases inherits correct naming instead of discovering it afterwards.

Residual-duplication sweep: every remaining `type_expr_slot_name` caller is
one of the two documented representation-only classes — the WASM width /
erasure walks (`wasm/inference.py`, `codegen/monomorphize.py`'s Eq-derivability
oracle) and the State/Exn cell FAMILY (`wasm/calls_handlers.py`,
`wasm/operators.py`, `codegen/functions.py`).  Both stay syntactic by design.
No presentation-layer file derives a slot name independently any more.

Tests, each written to fail first:

* `test_obligations.py` — a parameterised callee slot substitutes into E501's
  message and fix (was: generic wording).
* `test_lsp.py` — a parameterised reference resolves; an alias-spelled
  parameter is reachable from a canonical reference; a `forall` shadow lands
  on parameter 1 (was: None, None, parameter 2).
* `test_cli.py` — `--explain-slots` tables an alias type argument resolved,
  and keeps a `forall`-shadowed pair as two stacks.
* `test_tester_coverage.py` — the tester's env is load-bearing, and the
  primitive-head gate that bounds it.
* `test_slot_naming_blast_radius.py`, `test_obligations.py` — the two
  existing `slot_table` / `_pre_at_call_site` call sites take the new
  argument.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The checker resolves an effect instance's type arguments in full
(`_resolve_effect_ref` -> `_resolve_type`), so `State<MaybeInt>` under
`type MaybeInt = Option<Int>` and `State<Option<Int>>` are ONE
`EffectInstance`: one handler handles both spellings, and a call across
them type-checks.  Codegen named the cell FAMILY from the source spelling
for anything that did not resolve to a scalar (the aallan#1205 gate), so those
two spellings minted two host cells — one cell per checker, two per
codegen, a silent state split behind a green check.  A cross-spelling
program returned the untouched initial value; a cross-MODULE one did the
same.

`naming.family_name` is now the ONE family renderer at every site:
registration (`_check_state_type` / `_check_exn_type` / the body scan),
per-function lowering, both handler translations, and the
`old(State<T>)` snapshot.  Each renders against the DECLARING module's
`AliasEnv`, so a name means what it meant where it was written — two
modules declaring `Hid<T>` differently keep two cells.
`slots.resolve_scalar_alias_te` is deleted: its scalar gate is exactly
what aallan#1209 retires.  `family_fallback_name` stays, re-justified, for the
residue resolution cannot name.

Mangle-safety is part of the family's contract now, not an argument for
not resolving it.  A family feeds `mangle_type_name`, whose escape covers
exactly the canonical `Head<arg, arg>` grammar, so `family_name` gates on
`is_ref_spellable` and keeps the opaque spelling for a resolution that
renders outside it (`Option<fn(Int) -> Int>`, `Option<{@int | ...}>`) —
without the gate the flip emitted an import name the WAT parser rejects.
Falling back can leave a family split; it can never merge two cells the
checker keeps apart.

The family's old TypeExpr-level walk carried a 32-level depth bound whose
overflow was a loud per-function skip.  `vera.naming` resolves each alias
body against a strictly shorter prefix of the table, so it is well-founded
with no bound — the same shape the checker's own registration has, which
is why the checker accepted deep chains all along.  A 33-hop chain now
compiles and runs instead of dropping its function, and the two E607/E612
arms that reported the bound are gone with it.

Measured: the whole 510-program corpus captured before and after (`check
--json` codes, emitted `state_*`/`exn_*` symbols, `run` for every probe).
Six files differ, all in `tests/probes/state_handlers/alias_families/`:
one changes VALUE (`f1_cross_spelling`, -1 -> 7, the two spellings now
share the cell), five rename or collapse symbols (`state_*_MaybeInt` ->
`state_*_Option_LInt_R`, `exn_Msg` -> `exn_String`) at unchanged values.
No example, no conformance program, and no `check` diagnostic moved.  The
25 E533/E336 gate probes verify byte-identically — obligation tier counts
included — because those gates compare Types, not rendered names.

`tests/test_family_naming.py` pins the collapse where it is observable
(bare alias, parameterised alias, and an `Exn<Msg>`/`type Msg = String`
payload whose `i32_pair` must arrive through the same tag), one import per
collapsed family, the cross-module case, the negative (`Option<Int>` and
`Option<Bool>` stay two cells), the mangle-safety gate, byte-stable
symbols for seven alias-free corpus programs, and the whole measured
radius.  Mutation-validated: reverting the resolution, dropping the
mangle gate, and dropping type arguments each flip a distinct, correct
subset red.

Skip-changelog: the PR's [Unreleased] bullet already states the composite collapse.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…allan#1216, aallan#1217)

`vera test` resolves a parameter's type through the threaded naming
environment before asking whether Z3 can encode it (aallan#1216).  The old
derivation matched a parameter's SYNTACTIC head against `PRIMITIVES`, so
`type Cnt = Int` never reached `Int` and every alias-typed signature was
classified un-encodable and skipped (E701) before a variable was named.
Resolution is not encodability: an ADT, a function type, a type variable
and an unresolvable expression still skip — now by the resolved answer,
and the skip reason names it (`cannot generate Option<Int> inputs`).

An alias is also the only way to write a refined parameter, so aallan#1216 is
what first brings refinements to the generator.  Codegen guards them on
entry, so the predicate is seeded into Z3 through
`naming.refinement_binder_parts` — without it 96 of 100 trials on a
`type Pos = { @int | @Int.0 > 0 }` parameter are reported as refinement
violations.  Both rendering sites now name in the function's own slot
scope (its `forall` variables shadow same-named module aliases).

`vera check --explain-slots` prints a table for every `where`-block
helper too (aallan#1217), indented under its parent and qualified
`parent.helper` in the JSON.  The accumulation of enclosing type
parameters moves into `slots.fn_scopes`, shared with the language
server's `definition_at`, so the two surfaces cannot answer differently
about one helper.

`naming.resolve_alias_type_expr`, `naming.substitute_named` and
`naming.AliasResolutionDepthError` are deleted with their `vera.slots`
re-exports and their unit test: the family flip retired the last caller,
and the full suite is green with both functions raising on entry.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…re their probes

Every probe under `tests/probes/` is eventually promoted to CI or
deleted.  This dispositions the shapes owned by the two issues this PR
closes: seventeen new conformance programs carry the alias × handler
corner the PR aallan#1202 review probes were written to reach, and the
thirty-three probes that measured them are deleted.

  ch03_slot_alias_type_argument            head opaque, arguments resolved
  ch07_state_composite_alias               composite alias cell + match
  ch07_state_composite_alias_cross_spelling  two spellings, one cell
  ch07_state_scalar_alias_cross_spelling   scalar alias across a fn boundary
  ch07_state_alias_chain                   multi-hop / parameterised / alias-of
  ch07_state_nested_param_alias            twice-applied `Id<Id<Nat>>`
  ch07_state_scalar_alias_widths           Bool / Byte / Float64 cells
  ch07_state_alias_op_result_positions     get(()) as element / operand / scrutinee
  ch07_state_clause_alias_slots            alias spellings in clause slots
  ch07_exn_scalar_alias                    Exn<Code> caught as Exn<Int>
  ch07_exn_string_alias                    Exn<Name> i32_pair payload, one tag
  ch07_exn_param_alias                     Exn<Id<Int>> / Exn<Id<Id<Int>>>
  ch08_state_alias_per_module(+_lib)       same alias NAME, two modules
  ch08_state_alias_module_table(+_lib)     same name, different body
  ch02_alias_cycle_rejected                E132 negative (new coverage)

The two suite fixtures that read those probe paths — the aallan#1209 family
radius and the aallan#1208 blast radius — now read the promoted conformance
programs.  The pinned values are the MEASURED ones, not a re-baseline:
each shape's entry point is public so the probe's own expected value is
still what is asserted, and perturbing a promoted fixture takes the
blast-radius assertion red.

Differential sweep after the deletions: 6,084 observations (floor 2,000),
1,423 under a non-empty alias env (floor 200), 102 arguments naming an
alias pre-resolution (floor 40), 522 files (floor 500), 5 parse-skipped
(cap 10) — the five known parse-broken probes, now flagged as such in
the index and left for the PRs that close their issues.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The naming consolidation gave the toolchain one renderer; several consumers
were still feeding it the wrong ENVIRONMENT, and a name minted in the wrong
namespace misses silently.  Four provenance seams, each a real regression the
adversarial review exhibited:

* an IMPORTED callee's contract was rendered with the IMPORTER's alias env, so
  where two modules spell one alias differently the call-precondition
  obligation either attached to the wrong argument and VANISHED (a false
  Tier-1) or split a stack the callee merged (a spurious E501).  The verifier
  now builds a naming env per resolved module from that module's own
  registration and pins each harvested contract to it; the SMT layer scopes the
  callee-parameter stack and both contract translations to it, and the
  call-site message renders its substitution table there too.

* an imported GENERIC was monomorphized AND verified under the importer's env
  while codegen used the defining module's, so the two sides proved and emitted
  different bodies — the De Bruijn recount permutes the parameters.  A lying
  imported contract could prove clean and violate its own postcondition guard
  at run time.  Origins are recorded per discovery key and threaded into every
  `monomorphize_fn` call and into the clone's re-declaration.

* a `forall` variable SHADOWS a same-named module alias for the whole
  signature, and nothing outside the checker applied that narrowing: the
  monomorphizer's recount, the verifier's parameter declaration, and codegen's
  emission of the exported uninstantiated template all collapsed two parameter
  stacks the checker keeps apart.  The recount now renders its pre-substitution
  side in the narrowed scope (accumulating `where`-helper parameters as
  `fn_scopes` does) and its post-substitution side un-narrowed, matching what
  the consumers rebuild; the verifier and codegen narrow per function.

* the tester keyed its slot names in the narrowed scope but handed `SmtContext`
  the un-narrowed env, so a reference would have resolved against a scope the
  bind side never used.

`CheckArtifacts.module_alias_envs` is deleted rather than wired up: the two
consumers that need a per-module env each build it from a namespace they
already hold, and `vera verify` runs with module-artifact collection off — an
artifact-sourced table would have been empty exactly where these obligations
need it.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…ion (aallan#1208)

Two of the three remaining review findings; the third is characterized
rather than fixed, because closing it is a checker-semantics change.

`vera/naming.py` resolved an alias by recursive descent, one Python frame
per hop, so a legal `type A1 = A0; type A2 = A1; …` chain raised an uncaught
`RecursionError` at ~340 hops — from inside a renderer this module's own
docstring calls TOTAL, and the checker's cost is O(1) per hop (it stores
each alias's `resolved_type` at registration).  Resolution is now iterative
and dependency-first: every alias a body mentions has a strictly smaller
declaration index, so the mention graph is a DAG, and memoizing the deepest
first leaves each `_resolve` recursing no further than its own body's
syntactic nesting.  This is an evaluation ORDER, not a depth bound — a 400-
hop chain still renders `Option<Int>`, which both the checker-equivalence
rule and the 41-hop `ch07_state_alias_chain` conformance program require.
The alias branch also stops indexing `env.aliases` unconditionally, so an
environment whose `_order` names a body-less alias falls through instead of
raising.

Codegen's `_refinement_guard_parts` was a second hand-maintained copy of
`naming.refinement_binder_parts` — chase the alias chain, name the base,
conjoin the `@Nat` / `@Byte` implicit range — and the two had already
drifted at the erased-base and nested-base corners.  Codegen now consumes
the one derivation and layers its two WASM-specific decisions on top: an
erased base emits no guard, a nested refinement base is rejected loudly
(E618).  A differential pins their agreement, and because that differential
is green whether or not they share an implementation, a mutation assertion
carries the weight: perturb the shared derivation and codegen's binder has
to move with it.

The prelude-alias asymmetry is INVESTIGATED and not forced.  `inject_prelude`
runs at codegen and at the verifier's mono discovery, never at check, so the
checker has no `ArrayMapFn` and renders it as an opaque ADT while codegen
resolves it to a function type — in argument position the two therefore merge
different parameter stacks, and an exported body reads the wrong one.  Both
naming envs faithfully report their own side, so this is not a renderer bug:
closing it means registering the prelude's aliases in the CHECKER, which
changes what the checker resolves, and with it `--explain-slots`, LSP hovers,
and the binding table.  The shape joins the differential battery and the gap
is pinned by a characterization test that fails the day someone closes it.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…an#1208)

Validation, not behaviour — with two exceptions noted below.

The differential's inline battery could go inert without failing.  A blanket
`contextlib.suppress(Exception)` around the check swallowed a compiler-level
raise, so an entry that self-destructed contributed no observations at all
and read exactly like an entry that agreed; only `VeraError` is absorbed
now, and even that is recorded and floored at zero.  The cycle and
forward-reference corners were one program, so one failure took both down;
they are two entries, and the reach test asserts the exact string each must
render rather than counting.  The corpus alias-in-argument floor is
CORPUS-ONLY, which exposed that the corpus contributes 34 of the combined
count rather than the 40 the shared floor implied — growing the battery can
no longer mask corpus decay.  Two zero-covered effect-row branches gain
battery entries: a qualified effect reference (`Module.Effect`) and an
effect ROW VARIABLE, both inside a function type in argument position.

The LSP's `where`-helper narrowing was mutation-surviving: dropping the
`fn_scopes` accumulation in `definition_at` failed the CLI suite and passed
the LSP one.  Its twin now exists — one narrowing, two surfaces, both
watching.

`vera.naming.type_arg_name` had no caller while `slot_name`'s docstring
claimed a composition through it; `slot_name` now renders each argument
through it for real and restates only the `Head<a, b>` join, which the
corpus differential compares against `canonical_type_name` directly.
`alias_env_from_declarations` had no production caller either — every
consumer builds its environment from a namespace it already holds — so it
leaves the shipped module and becomes a test fixture constructor, where a
second implementation of declaration-index assignment cannot become a source
of truth.

The two per-module alias-table conformance libraries pass `verify` and were
pinned at `check`; both are raised to the deepest level they reach, which
also enrols them in the warm/cold obligation-parity corpus.
`ch02_alias_cycle_rejected`'s header claimed the diagnostic lands on the
second declaration; it lands on the first, and the comment now says why.
KNOWN_ISSUES.md regains the one-to-one invariant with the open `bug`-labelled
set (aallan#1218, aallan#1219, aallan#1220).

The naming module's docstring is made true: totality is defended rather than
asserted (the iterative resolution and the `env.aliases` membership check are
named as what defends it), the argument-rendering claim matches the code, the
representation-only sites are two rather than one, and the environment rules
— module provenance and `forall` narrowing — are stated as rules alongside
THE RULE, because one renderer is only half the contract.  TESTING.md's
naming rows describe what the suite pins rather than narrating how it got
there.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…ion at its open tracker aallan#1222

The Bugs table stays one-to-one with open bug-labeled issues; the
limitation row cited closed aallan#1172 and now cites aallan#1222, filed for it.

Skip-changelog: two-row KNOWN_ISSUES bookkeeping, no compiler change

Co-Authored-By: Claude <noreply@anthropic.invalid>
…n-keyed origins (aallan#1208)

The fixed-point adversarial review's second round found five places where the
naming consolidation was right by coincidence rather than by construction.

F1 — `_resolve_alias` still recursed once per level. Pushing a body's whole
pending list puts SIBLINGS in progress together, and the `in_progress` guard
then filters a sibling that is also a real dependency, so the body is resolved
with that sibling unmemoized and `_resolve` reaches it by recursing.  A
`type Bk = D(k-1); type Ck = Drop<Bk>; type Dk = Drop2<Bk, Ck>` graph raised
`RecursionError` at a few hundred levels, from the renderer the module
docstring calls TOTAL.  One dependency per iteration leaves only ANCESTORS in
progress, and an ancestor can never be a pending dependency, so nothing is
filtered and the nesting is constant.  The docstring now claims exactly that.

F2 — the verifier's `_fn_naming_scope` was green both ways: every call site
renders a signature and the references into it against the SAME env, so a
wrong scope is wrong consistently and invisible from inside.  Crossed against
a second component instead: the verifier's declared parameter names against
`slots.slot_table`'s, and its `where`-helper scope against `slots.fn_scopes`'
accumulation.

F3 — the De Bruijn recount narrowed only its PRE side, justified by "the clone
carries forall_vars=None".  True of the function being cloned, false one level
down: substitution clears only the top declaration's variables, so a
`forall<U>` helper keeps them in the clone and both consumers narrow by them.
The POST side now narrows by the variables that SURVIVE substitution.

F4 — the differential's file floor counted corpus plus inline battery, so
battery growth could mask corpus shrinkage.  Corpus-only now, and three
docstrings that overclaimed which floors read which population are corrected.

F5 — `_alias_env_for_generic` looked an imported nested generic's origin up
under its chain's FIRST segment, which is never a recorded key, and the
verification-time clone looked it up under the helper's bare name.  Both fall
back to the importer's namespace, and the two being wrong TOGETHER is why
nothing saw it.  The whole lexical chain is probed now, one `$where$` segment
shorter at a time, on both sides.

Plus the review's low batch: a stale `CheckArtifacts.module_alias_envs`
reference in codegen, a tester fixture with no shadowed parameter to shadow,
the `Future<Unit>` erased-base corner and a `@Nat` runtime analogue in the
binder-convergence differential, a predicate as well as a binder-name
mutation, obligation-level assertions on the imported-callee E501s, and the
KNOWN_ISSUES aallan#1222 row describing both failure directions.

Also files aallan#1223: a generic `where`-helper under a GENERIC parent never has
its own generic callees instantiated by codegen, so a check-clean,
verify-clean program compiles to no exports.  Byte-identical on this HEAD and
at the branch point, so it predates this work; tracked, not fixed.

Co-Authored-By: Claude <noreply@anthropic.invalid>
… refined returns (aallan#1208)

Three namespace defects the review probes reached, each with the failure
that exhibited it:

- Codegen's declaration-index space was shared across every absorbed
  namespace, and `_stamp_decl_order` is idempotent by name, so a name an
  imported module had stamped at Pass 0.5 kept that earlier index inside
  the main file's namespace at Pass 1 — turning a forward alias reference
  into a backward one.  `import lib;` + `type Z = X; type X = Nat;`
  resolved `Z` to `Nat` in codegen and to the imported ADT at check,
  merging two parameter stacks the checker kept apart: a check-clean,
  verify-clean program that read the wrong parameter through valid WASM
  (both erase to i32, so nothing trapped).  The space is now keyed to its
  owning namespace and swapped by `_module_alias_scope` beside the alias
  maps it describes.  `origin/main` returns 7 on the repro; this branch
  returned 137438953472 before the fix.

- `_callee_alias_env`'s unpinned fallback rendered in the ENTRY program's
  namespace.  Only public functions of directly imported modules are
  pinned, so an imported generic's own `where`-helper always reached it,
  and a helper whose parameters are two stacks in its module and one in
  the importer's discharged a violated precondition as true — a false
  Tier-1 that traps at run time, with a spurious E501 as its mirror.  The
  fallback is now the module under verification.

- A callee's refined-RETURN predicate was translated outside
  `_callee_naming_scope` while its `requires`/`ensures` were inside it.
  Latent today (the binder beside it is bare-headed, so push and lookup
  miss under both namespaces), so it is pinned by provenance.

Round-3 findings: the monomorphizer's post-substitution scope narrows by
the vars the CLONE declares rather than by those surviving substitution —
the two differ under an identity mapping, where the post side minted
`Option<Int>` against consumers' `Option<T>`; E618 reports once per
declaration rather than once per visit and per clone; the tester
translates a refinement's membership predicate against a binder-only
`SlotEnv`, matching the checker's isolated single-binder scope; and the
KNOWN_ISSUES rows for aallan#1223 and aallan#1222 now say what the code does.

Review findings: §7.5.1's cell-identity rule is hedged by the
mangle-safe-family gate (aallan#1219); TESTING.md's conformance counts are
33/26 as the manifest has them; boundary-safe WAT symbol and `local.get`
matching; recursive type-argument spellings; trap assertions on `kind`
alongside their predicate text; an invocation floor under the
constant-nesting assertion; and three cross-component docstrings now say
which axis they are independent on and which shared renderer the literal
assertions cover.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Skip-changelog: two-row bug-table bookkeeping, no compiler change

Co-Authored-By: Claude <noreply@anthropic.invalid>
…n#1208)

Codegen enters an imported module's namespace in four places.  Three paired
`_module_alias_scope` with `_module_source_scope`; the mono-clone body pass
entered the alias scope alone, so any diagnostic raised while compiling a
clone of an IMPORTED generic carried the importer's path with module-local
line/column — coordinates naming unrelated source, or a line past the
importer's end that renders an empty `source_line`.

That also swallowed a distinct module's diagnostic: the E618 nested-refinement
rejection is deduplicated on the resolved location, on the premise that a
location carries the file it belongs to.  Two imported library modules of
identical shape declare at coinciding line/column, so the pair collapsed to one
report.  The new cross-module test pins the count and the attribution together,
with the coordinate coincidence asserted first so a fixture edit cannot leave it
green for the wrong reason.

Also: TESTING.md's level-limited skip table was missing four rows — both stages
for `ch02_alias_cycle_rejected` and the `run` stage for the two per-module
alias-table libraries — and understated the documented total; measured against
the manifest, the true count is 81, not 77.  KNOWN_ISSUES gains the row for
aallan#1227 (codegen's global `_adt_layouts` versus module-scoped alias envs).

Co-Authored-By: Claude <noreply@anthropic.invalid>
 review)

Seventeen test files import it via vera.codegen.api's re-export; four
via the defining vera.runtime.traps. Join the majority convention.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…1213)

The binding table is keyed by the checker's rendering, and five documents
described a rule the compiler does not implement.

- vera/README.md and the Binding docstring in vera/environment.py called a
  canonical type name "the syntactic name". Alias opacity is a property of
  the HEAD; type ARGUMENTS resolve, so @option<Cnt> under type Cnt = Int
  binds Option<Int>.
- vera/README.md attributed the monomorphizer's De Bruijn recount to
  full-depth slot names from vera/slots.py; it 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. The decision is made on the resolved type (aallan#1216).
- DE_BRUIJN.md quoted --explain-slots output with hand-aligned columns the
  tool does not emit, and no where-helper block (aallan#1217). The sample is now
  the verbatim output of a real run.
- spec/03 3.8 asserted aliases are "not transparent for reference
  resolution" unqualified, contradicting 3.8.1 twelve lines below.

The module map also gains the environment half of the contract on the five
rows that carry it: monomorphize, smt, verifier, lsp/features and
codegen/modules.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The head-opaque / arguments-resolved / cells-resolve rule was implemented
across six subsystems and written down nowhere a reader would look.

DE_BRUIJN.md gains a section 6, "Type aliases and slot names": the
three-clause rule, aliases as the way to name a parameter without names,
merged stacks and which line of --explain-slots is authoritative, forall
shadowing, and cell identity. Sections 6 to 10 renumber to 7 to 11.
Section 4 gains the where-helper and forall-variable binding rules, the
quick reference gains Aliases and where helpers entries, the debugging
workflow gains the alias case and the qualified JSON helper names, and
further reading points at spec 3.8 / 3.8.1 and 7.5.1.

Every example in the new section was run; the tables are verbatim output.
The sweep also found two pre-existing section 5.6 closure examples that
never parsed - an inline function type in return position - now written
through a type alias as the conformance suite does.

vera/README.md gains Design Pattern 8, "One renderer for slot names":
the rule, the environment that is the other half of the contract, the two
representation derivations that stay in slots.py, and the differential
that proves the two sides agree. The two alias-opacity paragraphs gain
the argument clause, and the per-section "Files:" lines drop their line
counts, which contradicted the gated module map.

TOOLCHAIN.md, LSP_SERVER.md, FAQ.md, DESIGN.md, spec/02 and spec/08 gain
the clauses their surfaces had grown; architecture.svg and its text twin
place naming.py in the type-check stage and state the one-renderer fact.

Co-Authored-By: Claude <noreply@anthropic.invalid>
)

vera/README.md's Test Suite paragraph cited 6,821 tests across 104 files,
143 conformance programs and 37 demos against a live 9,390 / 143 / 196 /
42. Only the module map in that file was gated, so the sentence drifted
for many releases with nothing to catch it - the same class as FAQ.md's
headline line, and the same fix: read the numbers the way the oracle
reads every other citation of them, and treat a reworded sentence as an
error rather than a skip.

TESTING.md's overview row states a total and its parts. The total was
pinned; the parts were not, so refreshing only the number the gate reads
would leave an arithmetically impossible parenthetical behind. The parts
are now checked to sum to the collected total.

Both checks are unit-tested and mutation-validated: dropping any one of
the four vera/README citations, comparing the breakdown sum to nothing,
or returning silently on a reworded sentence each turns exactly its own
test red.

DE_BRUIJN.md joins the shared parse-only doc gate (now five documents).
Its two section 5.6 closure examples had stopped parsing with nothing
watching, which is the gap rather than the examples.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…an#1213)

Section 6.4 wrote `forall<T> fn split(@option<T>, @option<Int>)` and said
that without the shadow the reference would land on the wrong parameter.
It would not: in that order the shadowed and unshadowed readings both
resolve `@Option<Int>.0` to parameter 2, so the example demonstrated
nothing. With the parameters the other way round the two readings
disagree - parameter 1 shadowed, parameter 2 merged - which is the shape
the LSP and CLI regression tests pin. Table re-run.

Section 6.3's note on the report is also tightened: the signature line
re-prints the type NAMES as written, not the line as written, and the
rows are authoritative because they are the checker's binding keys.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Review of the sweep turned up claims the compiler does not implement,
and count citations no gate reads.

The absolutes about cell identity are false where the resolved type has
no mangle-safe family name — `naming.family_name` falls back to the
alias-opaque spelling there, so two spellings name two cells (aallan#1219,
spec §7.5.1).  The caveat now lives once, in `DE_BRUIJN.md` §6.5, and
§6.1, §9, `FAQ.md` and `DESIGN.md` point at it instead of each
restating "spelling never splits a cell".

`DE_BRUIJN.md` §5.6's lead-in stated two rules that are not rules: a
function type needs neither a `type` alias (`-> @fn(Int -> Int)
effects(pure)` checks) nor a `Unit` parameter to be nullary (`-> @fn(->
Int) effects(pure)` checks).  The real constraints are the `@` prefix on
a return type expression and plain inner type names inside a type-level
`fn(...)` (grammar.lark:49); the alias is the conformance suite's
convention, and the examples say so.  The same false clause is corrected
in this cycle's CHANGELOG bullet.

`SKILL.md`'s `vera test` paragraph claimed a type variable skips with
its resolved type named, where `tester.py` short-circuits a generic
function as `generic function` before parameter types are read at all;
and that `type Count = Nat; public fn twice(@count -> @nat)` is "tested",
where trials are what a Tier 3 contract gets — that signature reports
`VERIFIED (Tier 1)`.  Both now describe the classification the tester
performs.

`DE_BRUIJN.md` §6.2's `[E130]` block was hand-composed to one line; it
is now the compiler's verbatim output, as §10's sample already is.

Count and inventory fixes: `CONTRIBUTING.md`'s commit-stage hook count
(31, matching the intro and `.pre-commit-config.yaml`); `TESTING.md`'s
validation-script count (twenty-four, the rows it introduces) plus the
`check_debruijn_examples.py` row it lacked; and the CI **lint** row,
which now lists every step `ci.yml`'s lint job runs, in order.
`vera/README.md` states the W-series alongside the E-series (`W001`
typed holes, `W002` eager `async` argument) and credits `slots.py` with
the `fn_slot_scope` helper the tester and monomorphizer import.  The
root `README.md` architecture alt text matches `vera/README.md`'s.

`check_doc_counts.py` reads all four `vera/README.md` Test Suite counts
comma-tolerantly, so none of them switches its own check off by crossing
a thousand — pinned by a test that fails on the digits-only pattern.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The review of the previous commit turned up five more citations of the
same staleness class, all of them inventories that grew without their
description.

`TESTING.md`'s validation-script table gains `check_explicit_encoding.py`
(aallan#645), which the CI lint job and the `explicit-encoding` hook both run,
and `check_distribution.py`, which `ci.yml`'s `package-distribution` job
runs — twenty-six rows now, counted from the table.  That job had no row
in the CI table at all; it does now, listing the build, `twine check`,
`check_distribution.py` and the out-of-checkout wheel smoke-test it
performs.  With it the table lists all nine jobs the "nine parallel jobs"
prose promises.

`vera/README.md`'s `ERROR_CODES` entry count was nine short, and a bare
total tells a reader nothing about what moved, so it states the split:
156 entries, 154 `E` codes and the two `W` warning codes.

The `vera errors` one-liners in `README.md`, `AGENTS.md` and `SKILL.md`
described an `E`-only registry, where the command prints `W001` and
`W002` as well; all three now name the full registry, matching Design
Pattern 9's wording.

`check_vera_readme_test_counts`'s docstring is present-tense, like its
sibling `check_tests_breakdown`.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Skip-changelog: one-file orientation-doc sync with the inventory pass

Co-Authored-By: Claude <noreply@anthropic.invalid>
`_VERA_README_TESTS` spans four counts across a long sentence, so it
matches with `DOTALL` — and it searched the whole of `vera/README.md`.
That let the paragraph's head pair with digits from any LATER section:
reword the paragraph until it no longer states the counts, leave a
matching `(196 programs in \`tests/conformance/\` …)` and `(42 end-to-end
demos)` anywhere further down, and the check greens.  An adversarial
fixture confirms it — reworded section plus decoys returned no errors,
and with wrong decoys it reported an unrelated section's digits as "the
Test Suite counts".  A silent skip is precisely what this gate exists to
prevent.

The counts are now read from the `## Test Suite` section alone, sliced at
the next `## ` heading, and a renamed heading is the same loud "no longer
gated" error a reworded sentence already was.  Two regression tests pin
both, and the live file still catches a wrong count rather than passing
vacuously.

`check_tests_breakdown`'s docstring illustrated the row it parses with
the day's real totals, which go stale by construction; it uses obviously
illustrative numbers instead.

`SKILL.md`'s error-code reference listed `W001` alone, where the registry
also carries `W002` — the `async()` argument whose effects fall outside
the commutative set and is therefore evaluated eagerly.  Its line states
what the diagnostic states.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Skip-changelog: one-sentence doc-accuracy fix inside the sweep PR

Co-Authored-By: Claude <noreply@anthropic.invalid>
aallan and others added 4 commits August 13, 2026 11:16
The only rendered E130 block in the repo stopped at "...or use a lower
index.", which is where the diagnostic used to end. Nothing checks that
a pasted ```text fence still matches the compiler, so the block went
stale the moment the fix text grew a table. This one is re-pasted from
`vera check` on §6.2's own program.

What the fence now shows is worth a sentence, because on its face it
reads as a contradiction: two entries whose type is `Int` sitting under
"no `Metres` bindings in scope". They are not the same column. The left
of each entry is a slot reference you can write, alias spelling intact;
the right is what that reference resolves to. Writing `@Int.0` in that
body reports no `Int` bindings above the same two entries — §6.1's
opaque head, which is the rule §6.2 exists to demonstrate and the one
place a reader is most likely to talk themselves out of it.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The table appended to `E130` grows one row per binding in scope, and the
language server concatenates the fix into its hover message, so a wide
function turns one diagnostic into a wall of rows nobody reads: thirty
same-typed parameters rendered a 492-character fix. It now prints the
first twelve rows and then `; … and K more`, where `K` counts the rows
withheld.

Twelve is measured, not picked. 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 still renders complete; 12 of the 2,080 elide, and the p95 segment
length is 166 characters before and after. The same 30-parameter case
now renders 255. Rows are dropped off the END, so a printed `@T.n`
means what it meant uncapped — the boundary tests feed every rendered
row back as the body and check it resolves.

One rule, applied once. `_render_scope_table` is shared by the `E130`
fix and the `W001` hole hint, which is what keeps those two from
disagreeing about a scope they both describe. It is a rendering rule
and deliberately not part of `_collect_scope_bindings()`, whose third
consumer is the LSP typed-hole completion — there a dropped row is a
missing completion item, so that path keeps every binding.

Separately, SKILL.md tells the reader `@T.result` never appears in the
table, `ensures` included. That was true and unpinned. The new test
holds both halves: an out-of-range slot inside an `ensures` produces a
table with no `.result` row, and the same `ensures` with an index the
table offers still resolves `@Int.result` — so the omission is about
what a slot binding is, not about `.result` being unavailable there. A
future change to the collector now falsifies the sentence loudly.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The cap test asserted _SCOPE_TABLE_MAX_ROWS >= 12, which pins only the direction that truncates ordinary diagnostics. Every other test in the class derives its expectation from the constant, so a raised cap slid through the file unremarked: with the constant at 13 — and at 40, far past the corpus p99 — the pin still passed, while a raised cap re-widens the hover message the cap exists to bound.

Equality makes moving the cap an edit to that line, with the corpus measurement (7 rows at p95, 11 at p99 across the 2,080 slot-reference positions) kept beside it to re-derive the new value from. TESTING.md carries the file's line count, so it moves with the docstring.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Both defects predate this branch and sit outside its diff; the review
round surfaced them, so they land as a commit of their own.

FAQ: the contract-testing walkthrough read `requires(@Int.1 != 0)` as
constraining *the second* parameter. Under De Bruijn indexing `@Int.0`
is the most recent binding, so `@Int.1` is the leftmost — the first.
The slot spelling stays as written: the FAQ uses the same guard at two
other points, and `examples/safe_divide.vera` — the example that same
answer tells the reader to run three paragraphs later — guards
`@Int.1 != 0` as its divisor and documents that slot as the first
argument. Correcting the word puts all four back in agreement with
DE_BRUIJN.md; re-spelling the slot to `@Int.0` instead would have split
them.

vera/README.md: the cross-cutting summary credited `errors.py` with the
`E`-codes, where the `ERROR_CODES` description further down the same
file counts 154 `E` codes and the two `W` warning codes. Now spelled
`E`- and `W`-series diagnostic codes, matching that later wording.

`docs/llms-full.txt` regenerates from FAQ.md via scripts/build_site.py.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan
aallan force-pushed the feat/issue-425-xai-grok-provider branch from b8a8009 to 8a8fc2d Compare August 13, 2026 11:03
@codeant-ai

codeant-ai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Skipping CodeAnt AI review — this PR changes more than 100 files, which usually means a migration, codemod, or vendored drop. Line-level review on diffs this large produces duplicate findings on the same rewrite pattern and drowns out anything that actually matters.

If you still want a review, comment @codeant-ai : review. For better signal, consider splitting the PR into smaller chunks.

aallan and others added 19 commits August 13, 2026 13:13
…t-table

Put the in-scope slot table in the E130 error (aallan#558)
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.
transitive_callers takes an optional effect argument (None keeps the
old handler-unaware closure, which the existing goldens still pin), and
_unhandled_callee_names subtracts the handled sub-tree from
direct_callee_names.

Two cases deliberately still propagate, because the effect really does
escape them: a caller that reaches the callee on any unhandled path as
well, and a call in a handler clause body, which runs outside its own
handler. Both are pinned by tests, and reverting either half of the
bound turns the matching test red.

Containment is structural rather than span arithmetic - identical
answers where both apply, no special case for a node with no span, and
it reuses the existing walk_nodes walker.

The handler has to name the same effect instance, type arguments and
all. The checker discharges against EffectInstance, whose equality
includes type_args, so handle[State<Nat>] does not discharge
State<Int>: pruning that edge on a base-name match would leave the
caller pure, never write the row it actually needs, and fail the whole
candidate on E125. Only an exact match prunes, and every other outcome
keeps the edge - a surviving edge writes a row the program may not
strictly need, which still type-checks. Row identity is a separate
question and stays the base name, so State<Int> is still not appended
beside an existing State<Bool>.  Asking the same fixture for
State<Nat> is the positive control: that handler key does prune, so
the surviving State<Int> edge is attributable to the type argument
and not to a key nothing can match.

A refinement argument is one of those distinct instances, and the
narrowest way to get this wrong: format_type_expr renders
{ @int | p } as its bare base type, so a key built straight from it
spells Exn<Int> for a handler that discharges nothing of Exn<Int>, and
the pruned caller would keep pure while its call site fails E125. Any
argument holding a refinement - nested inside another type argument
included, since the renderer recurses - is spelled unmatchably
instead, so the edge survives. handle[Exn<{ @int | p }>] and
handle[Exn<Array<{ @int | p }>>] both check clean, so both are pinned,
end-to-end as well as at the closure.

The two remaining branches of the key are pinned too: an
unparameterised handle[IO] bounding an IO propagation (IO and Async are
what addEffect propagates most), whitespace-insensitive request
spelling, and handler nesting in both orders - a matching handler
inside a foreign one still bounds, and a foreign one inside a matching
one does not un-bound. handle[Mod.IO] is pinned at the key rather than
through a program: effects are only ever registered under an
unqualified name, so a qualified handler always fails E330 and no
program in which that key could prune ever type-checks.

where-block attribution is unchanged: a helper's bare call still
attributes to its containing top-level function, while a helper that
discharges the effect itself bounds the closure at its parent. Row
rewriting is also unchanged, and still top-level-only - a where helper
that needs the new effect does not get one, and the gate refuses that
candidate rather than applying it half-done.

The KNOWN_ISSUES.md and LSP_SERVER.md limitation rows are retired.
Three review fixes on the handler bound. They share lines in the
module docstring and the CHANGELOG bullet, so they land together.

The handler's STATE INITIALISER was documented as escaping the bound
for the clause rule's reason, and nothing pinned it: patching
_unhandled_callee_names to prune h.state as well left the whole suite
green. It has its own reason - the initialiser is evaluated in the
ENCLOSING scope, before the handler is installed, which is why
_check_handle synths state.init_expr before it extends
env.current_effect_row and why _translate_handle_state evaluates it
before pushing the cell. The docstring now derives the two boundaries
separately, and two tests pin them: the closure keeps an edge whose
only call site is an initialiser (with a body call under the same
handler spelling as the positive control that the key does prune),
and the checker raises E125 on that call against a pure caller while
the identical call in the handler body is clean.

The comparison was documented as instance-for-instance in three
places. It is not: format_type_expr does not resolve aliases, so the
key compares the handle[...] head's SOURCE SPELLING to the request
string, and handle[State<MyAlias>] with type MyAlias = Int does not
bound a State<Int> propagation even though the checker discharges it.
That under-prunes - the caller keeps a row it does not need, which
still type-checks - so it is the safe direction and the key is left
alone; aallan#1292 owns the swap onto resolved instances. The module
docstring, the transitive_callers docstring and LSP_SERVER.md now say
spelling, name the under-prune, and cite aallan#1292, and a test pins both
halves on one fixture: the program is error-free (only possible if
the alias-spelled handler discharges the State<Int> row its callee
declares) and the State<Int> request keeps the edge, with the
alias-spelled request as the control that does prune.

The retired LSP_SERVER.md limitation row stated two things and only
one of them is fixed, so the file boundary comes back as its own row.
It carries no issue link, being deliberate behaviour rather than
tracked work, with a note above the table saying so.

Counts and llms-full.txt are refreshed from their oracles.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…er-aware-propagation

Bound vera/addEffect propagation at handlers (aallan#725)
…#349)

The issue's premise is stale. It cites 57.79% line coverage on
`vera/browser/runtime.mjs` and asks for >80%; the actual figure at
HEAD is 81.74%, so the target was already met before this branch
existed. The parity tests added by aallan#707, aallan#744, aallan#856 and aallan#920 got it
there. What this change delivers is not a rescued metric but coverage
of the specific untested host imports the issue enumerates — several
were registered yet never invoked, so their closure bodies had zero
hits — plus two genuine parity bugs that fell out of writing them.

81.74% -> 86.86% lines, 82.99% -> 89.21% branches, 81.09% -> 88.05%
functions. Measured with the command TESTING.md documents:
`VERA_JS_COVERAGE=1 pytest tests/test_browser.py`, which is the same
`npx c8 report --src=vera/browser/` invocation the browser-parity CI
job uploads to Codecov.

33 cases in six classes. Each compiles one `.wasm` and runs it under
both wasmtime and Node, so a failure isolates `runtime.mjs` rather
than codegen.

- Map: per-value-type and per-key-type variants over `map_get`,
  `map_size`, `map_values`, `map_keys`, `map_remove`. The first three
  and `mapAllocArrayOfStrings` had no browser-side caller.
- Set: per-element-type variants over `set_to_array` and `set_remove`.
  `rebuildWithout`, 37 lines shared with `map_remove`, never ran.
- Decimal: the exact-zero sign rule in `decAdd`, `decDiv`'s
  negative-shift arm, two `decRoundPlaces` special cases,
  `pyFloatRepr`'s exponential arm, `decimalAlloc`, `decimal_to_float`.
- Json: `readJson` across all six ADT tags, plus `json_stringify`.
- Error paths: the `Result.Err` arms of `regex_find` / `regex_find_all`
  / `regex_replace` / `json_parse`, and the `read_file` / `read_char`
  browser stubs.
- Markdown: list lazy-continuation loops and the recursive descents in
  `hasHeading` / `hasCodeBlock` / `extractCodeBlocks`.

Two browser/native divergences surfaced. Neither is fixed — fixing
means editing `runtime.mjs`, which this change deliberately leaves
alone. Each is recorded by asserting the native and the browser output
as separate exact strings, not by an `xfail`. A bare strict `xfail`
accepts *any* exception as the expected failure, so a broken compile, a
dead Node harness or an unrelated `runtime.mjs` regression would all
read as the known divergence. These two cases are the only
browser-side coverage of `json_stringify` and `md_render`, so a real
regression there would have been invisible. Pinning both strings tolerates
the documented difference and nothing else:

- `json_stringify` calls bare `JSON.stringify` where the native host
  calls `json.dumps`, so separators and integral numbers differ —
  `[1,2]` against `[1.0, 2.0]`.
- `md_render` keeps a list item's lazy continuation on its own line
  where the native renderer joins it onto the item.

Neither could have been caught before: neither builtin had a
browser-side caller, and `examples/markdown.vera` is flat enough to
miss the second.

The 434 lines still uncovered are accounted for. 312 need a DOM or
`XMLHttpRequest` (the Html and Http families) and cannot run under
Node at all; 76 are defensive throws reachable only through memory
corruption; 19 are WASM import kinds Vera never emits; 16 are the
embedder host API the harness never calls; 5 are dead
(`allocResultOkUnit` has no callers anywhere in the file); 6 are
genuinely reachable. That puts the ceiling for this style of test at
87.04%.

TESTING.md also claimed "61% JavaScript" in two places. That figure
dates to v0.0.171 (be10658) and is not reproducible by any documented
command — the one TESTING.md itself documents yields 86.86% for
`runtime.mjs` and 87.06% across `vera/browser/`. Both now read 87%,
which is correct under either reading. The adjacent "91% combined"
figure is left untouched: deriving it needs a Python coverage total
this change has no measurement for, and a guessed number is worse than
a stale one. It needs its own fix.

README.md's project-status line carried the same ambiguity in shorter
form: "95% code coverage", unqualified, in the most-read file in the
repo. That 95% is the Python figure on its own, and unqualified it
reads as a whole-project total. It now says "95% Python code
coverage". The combined figure stays out for the reason above. The
`check_doc_counts.py` pattern that anchors the live test count on that
sentence was updated to take the inserted word, so the count assertion
keeps its anchor rather than silently matching nothing: perturbing the
README count by one digit still fails with "README.md project-status
tests: doc says 9071, live is 9070".

Tests and docs only — no `vera/` or `spec/` file is touched, so the
CHANGELOG gate does not apply. The count rows that
`scripts/check_doc_counts.py` gates moved in `TESTING.md`, `README.md`,
`ROADMAP.md` and `FAQ.md`, and the README pattern in the script itself
moved with them; `docs/llms-full.txt` is the regenerated
`scripts/build_site.py` output that embeds FAQ.md.
…eaks

The `md_render` divergence was recorded as "list lazy continuations".
That is the case that was found first, not the rule. Running both
runtimes over four shapes gives the actual scope: any multi-line
paragraph. The browser keeps the paragraph's internal soft line break
and does not re-apply the container prefix, where the native renderer
collapses it to a space — `hello\nworld` diverges with no list in
sight. Worse, the browser render is not a fixed point: re-rendering its
own output moves the continuation out of its container, so `> a b`
becomes `> a\nb` and then `> a\n\nb`, with `b` no longer quoted at all.
That breaks the round-trip property spec §9.7.6 states for `md_render`
itself, on top of §12.9.3's identical-results requirement. Parsing is
unaffected — the three predicates and the extractor are byte-identical
across the runtimes — so the defect is renderer-scoped. Tracked as

The nesting battery already compiles the destructive case — a
blockquote wrapping an h2 and a fenced block — but only substring-
asserts its render, so the strings were unpinned. They are pinned now,
both runtimes, both rounds:

  native   render 1 == render 2 (a fixed point)
  browser  render 1 loses the `> ` from the fence's contents
  browser  render 2 fragments the fence into three and lifts `x = 1`
           clean out of the quote

The four assertions were confirmed to bite in the direction a fix would
move them: replacing each browser expectation with the native string —
what a repaired `runtime.mjs` would produce — fails the test, once at
`browser1` and, with that reverted, again at `browser2`. Both failures
print the real Node output, so neither assertion is vacuous.

`json_stringify`'s docstring claimed a fix would fail "the browser
assertion", singular. Measuring every `_JSON_TAG_CASES` entry against
both hosts puts the blast radius at five: this test plus `jarray`,
`jobject`, `nested` and `array_of_objects`, the four whose pinned
strings carry a separator or an integral number. The other seven are
already byte-identical. Tracked as aallan#1293.

`TestBrowserDecimalExact856` carried a private `_parity_stdout` that
had been duplicated to module scope for the aallan#349 classes; its nineteen
call sites now use the module-level helper with the `dec856` filename
passed through, and the surviving docstring no longer describes itself
as a mirror of something that no longer exists.

Three TESTING.md claims were also wrong. Action refs are not uniformly
pinned to major-version tags — `pypa/gh-action-pypi-publish` and
`codecov/codecov-action` are pinned to full commit SHAs, verified
against the workflows. The nightly-stress section cited
`actions/github-script@v7` where both workflows use `@v9`. And the
"91% combined" coverage figure is not derivable: the two collectors
measure different line populations, so blending them needs a
line-weighted total neither report produces. The two real figures, 95%
Python and 87% JavaScript, stay; the invented third is gone from the
overview table and the CI section both. The test-corpus estimate moves
from ~109,000 lines to the measured ~151,000 (151,297 by
`wc -l tests/test_*.py`).

Open CI/Tooling Issues had been emptied to "No open CI/tooling issues"
when aallan#349's entry was removed, though six remain open and verified as
such: aallan#1156, aallan#1103, aallan#712, aallan#540, aallan#402 and aallan#386. The table is back, with
one-line descriptions from the issues' own titles. aallan#712's row also
carries the gap this branch's own coverage work observed: `codecov.yml`
marks both JavaScript statuses `informational: true` with
`target: auto`, and `browser-parity` uploads `lcov.info` without
printing or asserting a percentage, so nothing holds `runtime.mjs`
above a floor the way `--cov-fail-under=80` holds the Python side.

Counts refreshed from `scripts/check_doc_counts.py` rather than by
hand: the new test moves the total to 10,347 across TESTING.md,
README.md, FAQ.md, ROADMAP.md and vera/README.md, and `docs/llms-full.txt`
is the regenerated `scripts/build_site.py` output that embeds FAQ.md.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Both are open `bug`-labelled issues, so the KNOWN_ISSUES.md Bugs table
owes them a row each under the one-to-one convention its own preamble
states.

any multi-line paragraph rather than the list lazy continuation first
observed, the browser render unstable under re-render, the nested
blockquote destroyed outright, and parsing unaffected — the three
predicates and the extractor are byte-identical across the runtimes,
so the defect is scoped precisely to the renderer.

and integral-number rendering, plus the NaN asymmetry that folds in
with them: the native host traps where the browser silently emits
`null`. Neither host is individually wrong — §9.7.5 pins no output
format — but §12.9.3 requires identical results, so the divergence is
the defect, and the fix direction picks one canonical form per DESIGN
principle 3.

The CHANGELOG entry records the coverage battery and both divergences
under [Unreleased], which also restores the `check_changelog_updated`
gate: the rebase pulled `vera/README.md` into the diff, and a
substantive path with no new entry is exactly what that gate blocks.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The table's own preamble claims it matches the open `bug`-labelled
issues one-to-one, and two were missing: aallan#1288 from the v0.1.10 release
recovery and aallan#1290 from aallan#1279's review round. Neither belongs to this
branch's work, but the rows are owed whatever vehicle carries them, and
this branch is already bringing the table current. Table now holds
exactly the twelve open `bug`-labelled issues.

aallan#1288 is release.yml's `Tag and create GitHub Release` step 422ing when
the extracted CHANGELOG section exceeds GitHub's 125,000-character
release-body limit. Size measured with the workflow's own extractor
rather than taken from the issue: `scripts/release.py notes --version
0.1.10` produces 148,701 bytes / 147,919 characters, roughly 23,000
past the limit. What makes it worth a row is not the rarity but the
landing point — it fires after PyPI has accepted the immutable archives
and after the tag exists.

aallan#1290 is the Chapter 10 drift a rule-name alignment gate cannot see.
Worded against THIS branch's base rather than the issue's framing,
because aallan#1279 is still open: `scripts/check_grammar_alignment.py` does
not exist on `main` yet, so the row says the gate aallan#1279 *adds* will not
see three classes of drift, not that a gate is failing to. All eight
live instances were re-verified against the tree here rather than
copied across:

  spec/10-grammar.md:63-66,99   SOME/NONE/OK/ERR/COLON declared, each
                                appearing exactly once in the chapter
                                — no production references them
  spec/10-grammar.md:323        module_call references DOUBLE_COLON,
                                declared nowhere in the chapter
  spec/10-grammar.md:279-301    primary_expr has no "?" alternative;
                                grammar.lark:199 has had one since
                                2026-03-30
  spec/10-grammar.md:24         BLOCK_COMMENT's regex is non-nesting
                                against spec/01-lexical.md:33 ("They
                                nest") and against the parser, which
                                takes `{- a {- b -} c -}` clean

The six terminal instances are fixed on aallan#1279's branch (confirmed in
its diff) and the row says so, so the row shrinks to its two body-level
items when that merges rather than reading stale.

Tracked-limitation count 50 -> 52, taken from
`scripts/check_limitations_sync.py` rather than incremented by hand. No
document cites that figure, so nothing else moved with it.

No CHANGELOG entry: both issues stay open, so there are no release
notes to write, and this PR's [Unreleased] bullet already covers its
own work.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Four of the review round's seven findings were still valid at head.

`test_read_file_is_err_stub_in_browser` did not pin what its docstring
claimed. Against `nonexistent.txt` the native host returns `Err` too,
so `value == 0` held on either runtime and the browser stub was
indistinguishable from a genuine not-found — the assertion would have
stayed green if `hostReadFile` were fully implemented. Confirmed before
touching it: native `'err'`, browser `'err'`. It now writes a real file
under `tmp_path`, embeds it with `.as_posix()` per the Windows rule,
and pins both sides through `_both_stdouts` — native `ok`, browser
`err`. Mutating the browser pin to `ok` fails, which the old shape
could not do.

`test_nested_walks_and_list_continuations` ran Node only while its
class docstring asserted, in prose, that the three parse-side
predicates match the native runtime. That claim is not decoration: it
is what scopes aallan#1294 to the renderer rather than to the Markdown
family, and this branch put it in KNOWN_ISSUES.md. It is now a
differential — one module, both runtimes, trailing fields compared —
so a native regression in the recursive descents reds instead of
passing. Mutating the expected native field to `false` fails against
the real `['true', 'true', '1']`.

`FAQ.md` claimed the browser runtime "produces identical results to the
wasmtime runtime" and that Markdown in particular "works identically".
Both are contradicted by the two divergences this branch documents, and
the contradiction is this branch's own doing — it filed them. Both
sentences now name aallan#1293 and aallan#1294. `vera/README.md`'s parity sentence
was example-scoped and stayed true, but carried an ungated hand-count
of "56 mandatory parity tests" against a file now holding 172; the
number goes in favour of the invariant plus the three pinned
exceptions.

`vera/README.md` disagreed with itself on the runtime family count in
three places — `×14` with a fourteen-name list at line 148, "thirteen"
without `db` at 573, "twelve adapters" twice at 575. Resolved against
the codebase rather than by picking one: `vera/runtime/` defines
exactly fourteen `register_<effect>` entry points, so line 148 was
right and the other two are now fourteen, with `db` named in the list
and given the one-clause description its siblings have.

The three skipped findings, for the record: README.md:266 already reads
"95% Python code coverage" (and the "91% combined" alternative is the
figure this branch removed as underivable); the `aallan#390` reference in
TESTING.md no longer exists and the pinning claim around it has been
corrected against the workflows; and aallan#349's completion is recorded in
CHANGELOG's `[Unreleased]`, with the HISTORY.md one-liner deferred to
tag time per the release convention.

No test count movement — both test changes are rewrites, not
additions. `test_browser.py`'s line count moved and was re-derived from
`scripts/check_doc_counts.py`.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…runtime-coverage

test: cover browser runtime Map/Set/Decimal/Json host imports (aallan#349)
The grammars under editors/ enumerate the built-in effect names by hand
and nothing checked them. They drifted: HttpServer, Inference and Random
never reached the vscode and TextMate grammars, and DB (v0.1.7) reached
none of the three until aallan#1155 fixed the Vim one. The drift is silent, so
four accumulated unnoticed -- an unknown capitalised identifier falls
through to the generic type-reference rule, so DB.query(...) still
highlights as something, just not as an effect.

scripts/check_editor_grammars.py reads vera.introspect.effects_payload()
in-process and requires a word-boundary occurrence of every registered
effect name in each grammar. Absence is conclusive, presence is
optimistic, which is the right way round when the failure is omission and
keeps the check immune to the three files' formats (JSON, plist XML, Vim
regex).

Written and run before the fix: 6/10 on vscode and TextMate naming DB,
HttpServer, Inference and Random, 10/10 on Vim -- so the check
discriminates rather than failing everywhere. Both alternations now carry
all ten, with HttpServer before Http so the match does not depend on
backtracking. The two extension READMEs listed the same stale six in
prose.

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 that the list does not name
fails the gate with an instruction to add it. Otherwise the gate would
have the same omission hole as the grammars it polices -- a fourth editor
would get a green tick while knowing none of the effects.

Registered as a pre-commit hook triggered by editors/ and by the registry
files themselves, since adding an effect is what puts the grammars out of
date. Hook count 32 -> 33 in CONTRIBUTING.md and TESTING.md.

Two nearby prose defects go with it, both about this section's accuracy.
The opener said every push is checked by all 33 hooks; the 31 commit-stage
ones run at commit time, so it now states what the repository configures
rather than what a push runs. And the trigger summary listed a closed set
of paths -- .vera, vera/**/*.py, grammar.lark, the matching Markdown file,
vera/browser/* -- that this hook's editors/ pattern is not among; it now
names each hook's own files: pattern in .pre-commit-config.yaml as the
authority and gives the common ones as examples. check_doc_counts.py reads
the hook total out of the reworded sentence, so its pattern moves with it,
and still fails both files when the number drifts.

Abilities (Eq/Hash/Ord/Show) stay out of scope: whether they should
highlight distinctly from ordinary types is an open design question.

Closes aallan#1156
Review fixes for the editor-grammar gate. Four of them are about the gate
itself; the fifth is a neighbouring check with the same defect.

The registry read came from whatever `vera` the interpreter could import.
`scripts/` is not a package, so `import vera` fell through to site-packages,
and an editable install points at the main checkout -- from a git worktree
the gate held one tree's grammars against another tree's effect list. A
differential shows it: a checkout whose registry carries an eleventh effect
`Telemetry` that no grammar names reported "OK: all 3 editor grammars carry
all 10 built-in effects" and exited 0. With the in-repo bootstrap from
check_corpus_canonical.py it exits 1 naming Telemetry in all three. A test
pins it by running the gate as a subprocess against a throwaway checkout
built the same way, which is the only way to see this -- in-process, `vera`
is already imported.

The primary failure path had no test. Deleting the `if missing` branch from
`main` -- drift no longer fails the gate -- left the whole file green. It now
strips `DB` from a mirrored grammar and requires exit 1 with the name in the
report, and the deletion is caught.

The completeness guard had two evasions. Suffix matching is on the whole
filename, so `.tmlanguage` did not cover `vera.tmLanguage.json` -- the real
vscode grammar's name -- filed anywhere but a syntax directory, and the
tree-sitter editors keep their highlight queries in `queries/*.scm`, matching
neither half of the guard. Both are listed now, each with a case in the
parametrized guard test. Neither flags anything in the current tree.

The two extension READMEs repeat the effect list in prose. They carried the
same stale six, were hand-corrected with the grammars, and were left
ungated, so the documented list could fall behind again with nothing to say
so. They go through the same loop against the same registry; a mirrored
README with `Random` removed is red.

`check_doc_counts.py` read CONTRIBUTING.md's hook count behind a bare `if m:`
-- a reworded sentence would switch the check off in silence, the failure its
TESTING.md twin already reports loudly. Extracted as
`check_contributing_hook_count` in the shape the file's other checks use, a
missing sentence is now an error, and the reworded case is a test.

The abilities note in the Vim syntax file pointed at aallan#1156, which this work
closes; the open design question -- whether Eq/Hash/Ord/Show should highlight
distinctly from ordinary types -- is now tracked in aallan#1295, and the roadmap's
CI line carries it.

Counts, site assets and the extension's own CHANGELOG follow: 10,336 tests,
and a bullet for the grammar fix under an unreleased heading, since 0.2.1 is
published.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The gate was added to `.github/workflows/ci.yml`'s lint job but not to
TESTING.md's description of what that job runs, so the table enumerated 21
of the 22 scripts the job actually invokes and the one it omitted was the
one this PR introduced.

A differential over the two files is the check: before, `check_editor_grammars.py`
was in CI and undocumented and the two lists were not even in the same order;
after, both are 22 entries in identical order.

The list is prose, so nothing gates it against the workflow — the same
second-copy drift this PR's own gate exists to prevent, one layer out.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The row hand-enumerates every script the lint job runs, and nothing held the
two in step. This PR walked straight into it: 3099640 had to correct the row
by hand after d6d1eb0 added a lint step without it, leaving the table
describing 21 of the 22 scripts the job invokes with every gate green. That is
the same hand-enumerated-list drift aallan#1156's own gate exists to prevent, one
layer out, so it is closed the same way.

`check_ci_lint_scripts(testing_text, ci_text)` extracts the lint job's
`run: python scripts/<name>.py` steps and compares them against the row's
`.py` names. Three properties earn their own tests. The comparison is
job-scoped, because every other job runs scripts too and a whole-file scan
would report all of them as undocumented -- the fixture carries decoys in the
test and security jobs, and `on:`'s two-space-indented `push:`/`pull_request:`
keys, which a naive job scan would count. Order is compared, not just
membership, since the row reads as the job's sequence and a set comparison
would let a reordered row lie. And a row or job that cannot be found is an
error, not a pass: rewording either side must not switch the check off in
silence, which is the failure this whole script is written against.

Mutation-validated on the artefact rather than the checker, since a doc gate's
subject is the document. With the check wired in, dropping
`check_editor_grammars.py` from the row exits 1 naming it; with the `main()`
wiring removed, the same mutated row exits 0. That pair is the evidence, not
the unit tests -- with the wiring removed and the tree pristine all nine still
pass, because they call the function directly and cannot see whether `main()`
calls it at all.

Counts follow: 10,345 tests.

Co-Authored-By: Claude <noreply@anthropic.invalid>
…ammar-gate

ci: gate editor grammars against the effect registry (aallan#1156)
xAI's chat-completions endpoint is OpenAI-compatible: bearer auth and a
`choices[0].message.content` response. `_call_inference_provider` branches
only on `auth_style` and `response_style`, so supporting Grok needs no new
dispatch code — one row in `_PROVIDERS` is the whole change, which is what
the registry comment promises.

The default model is `grok-4.3`, not the `grok-3-mini-fast-beta` the issue
asks for. xAI no longer documents that name anywhere: the model list carries
grok-4.5, grok-4.3, the three grok-4.20 variants, and grok-build-0.1 with no
grok-3 family at all, and the retirement table that names a redirect target
for every withdrawn slug covers plain `grok-3` but not the `-mini-fast-beta`
variant. What the endpoint does when sent an undocumented name was never
tested against a live key, so nothing here claims a call-time outcome in
either direction — the default is simply a model xAI currently lists.
grok-4.3 is the cheapest of the current general-chat models ($1.25/$2.50 per
million in/out), which keeps the row on the cheap/fast tier its neighbours
use (`claude-haiku-4-5-20251001`, `gpt-4o-mini`, `mistral-small-latest`).
grok-build-0.1 is listed lower at $1.00/$2.00 but is a special-purpose build
model, not a general default.

The row is appended last. Insertion order is the auto-detect precedence
(the loop picks the first provider whose key is present), so appending
leaves the behaviour of every existing key untouched; a comment now says
so at the append point.

Six tests. Two were written before the row and confirmed failing on
"Unknown inference provider 'xai'", then re-failed when the row was
removed again, so they pin the registry entry rather than the dispatch
code they share with Mistral. The other four are one parametrized case per
provider ahead of xai — anthropic, openai, moonshot, mistral — asserting
that with that provider's key and `VERA_XAI_API_KEY` both set, the earlier
provider still wins. The names are literal rather than sliced out of
`_PROVIDERS`, because a derived list would shrink to match a relocated row
and pass vacuously; a single hardcoded 'anthropic' case would likewise stay
green while openai, moonshot, or mistral silently lost precedence. Relocating
the xai row fails exactly the providers it jumped: 4 red at the head of the
registry, then 3 before openai, 2 before moonshot, 1 before mistral. The
older `test_multi_key_auto_detect_respects_provider_order` stays green
through the same reorders because it reads its expectation out of
`_PROVIDERS`, which is why the append-last invariant needs its own coverage.

The dispatch test pins the exact request payload — `[{"role": "user",
"content": "prompt"}]` — instead of checking that a `messages` key exists.
A presence check does not discriminate: the payload builder was mutated
three ways, role flipped to `assistant`, content dropped, list emptied, and
the presence check stayed green on all three while the exact-value assertion
goes red on all three. The third argument is the model, not a system prompt,
so an empty string there selects the row's default and adds no system turn;
one user message is the whole body, which is the OpenAI-compatible shape the
endpoint expects.

Doc mirrors updated wherever the providers are enumerated: the spec §9.5.5
table, ENVIRONMENT.md, SKILL.md, AGENTS.md, FAQ.md, the landing-page copy
in scripts/build_site.py and docs/index.html, and the runnable-key header
comments in the two inference examples. ENVIRONMENT.md's local-run example
now points at the other keys instead of reading as Anthropic-only. The
landing-page feature blurb called `Inference.complete` "mockable", which
it is not — `handle[Inference]` is still open work (aallan#372) — so that word
is now "host-backed" on both the generator and the hand-written page. The
generated docs/ assets were rebuilt, and the test-count rows
check_doc_counts.py pins were bumped.

Closes aallan#425.
The `_PROVIDERS` registry defaulted each provider to its cheap/fast tier,
and aallan#425 followed that convention for the new xAI row. The trade is wrong
for a default: a program's contracts are written against what the default
model can do, so capability is what the default owes the caller, and the
cheap tier stays one `VERA_INFERENCE_MODEL` away.

Every ID verified against the vendor's own live documentation:

  anthropic  claude-opus-5
    https://platform.claude.com/docs/en/docs/about-claude/models/overview
    Claude Opus 5 -- "Claude API ID: claude-opus-5"

  openai     gpt-5.6-sol
    https://developers.openai.com/api/docs/models
    "gpt-5.6-sol" (alias "gpt-5.6"), "Frontier model for complex
    professional work"

  moonshot   kimi-k3
    https://platform.kimi.ai/docs/pricing/chat-k3
    "kimi-k3" -- "Flagship model with a 1M-token context window"

  mistral    mistral-large-latest
    https://docs.mistral.ai/api
    "mistral-large-latest" -- the alias Mistral's own chat-completion
    examples use, and the floating form the Mistral row already used

  xai        grok-4.6
    https://docs.x.ai/docs/models
    "For everything else, including code, use Grok 4.6. It is the most
    intelligent and fastest model we've built."

The xAI row is grok-4.6, not the grok-4.5 a version-number guess reaches
for: 4.5 and 4.3 are both still listed, so the ordering had to come from
xAI rather than from the numbering.

This also retires two IDs whose vendors no longer document them,
`gpt-4o-mini` and `kimi-k2-0905-preview`, both reported in aallan#1263.

Test-first: the five model assertions were flipped to the new IDs and
confirmed failing against the old ones before the registry moved. Two of
them could not have proved anything as written -- `test_openai_provider`
asserted against `_PROVIDERS["openai"].default_model`, which pins the
value to itself and would have stayed green through the flip, and
`test_anthropic_provider` asserted no model at all. Both are literals now.

Detecting an ID that rots at the vendor rather than in this repo still
needs network access, and remains aallan#1263.

Co-Authored-By: Claude <noreply@anthropic.invalid>
The provider registry's `default_model` IDs rot at the vendor, and aallan#1263
was open, unlabelled, and tracked nowhere. It is enhancement-class rather
than a bug -- nothing in the repo is wrong, the world moves underneath it
-- so it belongs on the ongoing CI/process/tooling thread rather than in
KNOWN_ISSUES.

The row states what the tests can and cannot see, since that distinction
is the whole issue: pinning each ID to a literal catches an edit to the
registry, but no offline assertion can catch a vendor delisting an ID.

Co-Authored-By: Claude <noreply@anthropic.invalid>
Rebasing onto main moved every test-count surface, and the review raised
two points that hold against the rebased tree.

Counts, all oracle-derived rather than arithmetic on the old values:
10,478 collected (10,312 passed + 26 stress, 140 skipped) across the
five documents check_doc_counts.py gates, and the per-file table row for
test_codegen_host_effects.py at 1,036 lines. docs/llms-full.txt is a
build_site.py regeneration, not a hand edit.

TESTING.md's conformance section states each non-run level twice, as a
number and as a hand-written list of program names, and neither is
gated. ch05_reserved_resume_fn_rejected reached neither list, so the
check level read thirty-eight against a manifest holding thirty-nine and
the negative subset read thirty-one against thirty-two; the E-code
sequence aligned to that subset "respectively" was short by the same one
entry. The thirty-one codes that were present did pair correctly with
the thirty-one names that were present, which is why the omission left
nothing visibly wrong. The parametrized-suite count in the same section
read 1,035 where the runner collects 1,070 -- five checks over each of
the 214 programs -- drift of seven programs, predating that fixture.
All four are now checked against the manifest as the single oracle.

test_xai_provider asserts the request's Content-Type, which its OpenAI
sibling already checked and the new row did not. Confirmed discriminating
before it was kept: flipping the header in inference.py to text/plain
fails the new assertion at its own line, and passes again on restore.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan
aallan force-pushed the feat/issue-425-xai-grok-provider branch from 8a8fc2d to 55cdab8 Compare August 13, 2026 14:49
All five verified against the branch tree before anything was written.

SKILL.md said only "auto-detected from whichever key is set", which
answers nothing when several are. The detection loop walks _PROVIDERS
in insertion order and breaks on the first key present, so the rule is
now stated with its order, and VERA_INFERENCE_PROVIDER is described as
skipping detection rather than merely overriding it.

The auto-detect tests asserted the provider NAME only. Dispatch passes
(provider, prompt, model, api_key) positionally, so the key is index 3;
both auto-detect tests now assert it. Confirmed discriminating: pinning
the key lookup to VERA_XAI_API_KEY leaves every provider name correct
and fails all four precedence cases on the key alone.

FAQ.md claimed everything but json_stringify and md_render behaves
identically in the browser. Two effects are refused outright there --
runtime.mjs returns an explanatory Err for Inference and for both DB
ops -- so the answer now separates a deliberate platform boundary from
the two unintended divergences, and notes Http really does work
(XMLHttpRequest, not a stub) so the proxy advice is actionable. DB is
named beside Inference; the finding mentioned only Inference, but the
code refuses both identically.

TESTING.md described check_doc_counts.py as verifying that counts cited
in the docs match the codebase. The conformance-list drift fixed in the
previous commit disproves that blanket claim, so the row now enumerates
what the script reads and states that an unnamed count is not read.

vera/README.md and host-families.svg both counted two stateful family
adapters. There are three: register_async takes a future_store that
execute() creates up-front, publishes it as host_store_refs["future"]
for host_store_sizes, and has entries evicted by host_decref_handle
kind 4, which also cancels an unstarted future (aallan#841). Prose, diagram
label, legend and accessible title move together; the widened pill was
checked for collisions in a renderer rather than assumed.

Line count for test_codegen_host_effects.py re-derived to 1,046.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@chethanuk
chethanuk merged commit 3099c30 into main Aug 13, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add xAI Grok provider to the Inference effect

2 participants