Skip to content

VINDEX3: executable container, and a refused expert that cannot become a token - #197

Open
chrishayuk wants to merge 45 commits into
mainfrom
worktree-vindex2
Open

VINDEX3: executable container, and a refused expert that cannot become a token#197
chrishayuk wants to merge 45 commits into
mainfrom
worktree-vindex2

Conversation

@chrishayuk

Copy link
Copy Markdown
Owner

Two things land here. They were built in one tree and verified together, so
this is the combined candidate rather than two branches merged.

1. Strict refusal survives dispatch and the engine boundary

The FfnBackend ring gave a refusal a typed channel but stopped one level
short — ffn_or_moe_layer returned a bare array, so a strict route degraded
exactly like a best-effort one. The six kv_*_via_dispatch helpers now carry
three outcomes all the way out:

Ok(Some(_))   the dispatch produced a complete result
Ok(None)      nothing to do, or the backend declined this shape
Err(refusal)  a routed operation was required and did not execute

Decode is transactional. Attention appends the token's K/V before the FFN
can refuse, so a failed step rewinds every handle via a new
KvDispatch::truncate_kv — the inverse of an append (clip_kv keeps the tail,
this keeps the head). Windowed caches are the subtle case: append-then-evict
leaves the row count unchanged while the oldest row is gone, so
rewind_is_sound asks whether every layer had room, not whether the count came
back. Where the rewind cannot be trusted, StateInvalidated wraps the cause
and the engine refuses further decode until re-prefilled.

The API said two incompatible things. is_recoverable() answered "could
this operation succeed?" while callers read it as "can I retry?" — and a
Residency refusal that invalidated the cache is recoverable in the first
sense and catastrophic in the second. Now split:

operation_is_recoverable()   could this operation ever succeed?
engine_state_is_retryable()  is this engine instance still usable?
is_recoverable()             both — what a harness may act on

2. VINDEX3 becomes an executable disk container

Every VINDEX3 parity result before this bound its operands out of a VINDEX2
file. What was proven was the runtime, not the format — nothing could write a
VINDEX3 container, and ContainerGeneration::V3 appeared only in a detection
test built from a JSON string.

Conformance fixture A now round-trips end to end — write → detect → open →
validate → resolve storage key → resolve LYRW v2 regions → bind → execute —
bit-identically to the same weights held in memory. Fused and decomposed
FC1 storage agree under one programme id, and expert count / top-K come from
the container rather than a constant.

larql show and larql verify dispatch on detect_generation; VINDEX3 is not
normalised into a VINDEX2-shaped summary. For a VINDEX3 container verify
means will this bind? — index parses, manifest validates, keys resolve,
segments parse, roles satisfy the programme, regions in bounds — with defects
carrying {layer, bank, role, segment}.

Verification

gate result
workspace tests 10,212+ passing, 0 failures
clippy --workspace --all-targets -D warnings clean (also clears the pre-existing larql-cli backlog)
coverage policies larql-vindex 94.07%, larql-kv 96.06%, larql-inference pass
Gemma ladder 10/10 — layer sweep, layer parity, free propagation, decode parity (teacher-forced + free-running), MAP-3A 4/4
VINDEX3 container region round-trip, execution parity, fused≡decomposed, not-hard-coded
strict refusal gate 24 cases (8 entry points × 3 RefusalKind)

Scope, stated honestly

  • Strict refusal covers StandardEngine. The five engines routing through
    larql-kv's layer_ffn_or_moe still swallow; that is the next ring and the
    gate suite's module doc names them.
  • VINDEX3 is proven on fixture A, not a real model. extract still writes
    VINDEX2 and the ABI is not frozen.
  • V2-0/V2-1 close for the rows fixture A can carry. Variant-selection refusal,
    shared banks (blocked on the Mini-K3 rung) and WALK/DESCRIBE parity remain
    open, and are recorded as such in the experiments programme.

Accurate summary: Gemma-conformant architecture, executable VINDEX3 container
proven on fixture A, prepared for the K3 proof ladder.

parse_layer_weights_header read the magic, skipped the version field, then
parsed the offset table as num_entries x 32 bytes unconditionally. A file
whose entry stride differs from 32 would not bounds-fail: it yields offsets
that are still inside the file, so get_layer_entry_bytes hands back a
plausible byte range from the wrong expert and the model produces plausible
wrong numbers.

Same class as the XSTRIDE and BufferCache::get_bytes bugs — a wrong stride
gives a plausible number from the wrong place, not a crash.

The single production caller (format/weights/load/q4k.rs) already treats
None as "skip this layer", so an unreadable file degrades to a clean miss
rather than a panic.

This is cheap only until the first v2 file exists anywhere, including in a
fixture. LYRW v2 files land in the following commits, so it goes first and
alone.
… programme

VINDEX2 is the successor serving container for sparse models — one
extraction, then vary what is loaded, where it resides, what precision it
uses, and whether a component is executed or queried, without rebuilding
the index.

Five new pieces over VINDEX1: per-region quantisation, multiple physical
segments per logical bank, routed/shared/dense bank kinds, a validated MoE
programme manifest, and representation variants that profiles *select*
rather than request.

Spec is at draft-2. Draft-1 was unimplementable as written; building the
reader found three binary-layout gaps, now folded in and marked inline so
the amendment trail survives freeze review:

  §6.2  bank descriptor gains region_schema_count (without it the schema
        table has no bank boundaries) and flags carrying the §15.2 browse
        mode; record is 24 B, with the two u16 counts ahead of the u32 dims
  §6.4  schema record padded 18 -> 20 B with an explicit reserved u16, so
        the two u32 dims stay 4-byte aligned
  §6.3  per-segment entry-table coverage is now normative
  §6.5  unknown role/format/packing tags are preserved at read time and
        refused at capability-check time — the mechanical form of the
        additive-extension precedent

The experiments document pre-registers five gates (V2-0..V2-4), nine
experiments (E0..E8) and seventeen falsifiable priors, with decision rules
fixed before any arm runs. Conformance envelope is Gemma MoE -> GPT-OSS ->
Kimi-Linear-48B -> Inkling-Small -> K3; K3 is extracted once, last, into
the frozen ABI.

E0's corpus is amended from draft-1's "existing production v1 indexes",
which named nothing on the rig — the experiment would have passed by having
no subject. It is now constructed and pinned: C1 a fresh Gemma v1 extract,
C2 pulled hub artifacts, C3 golden outputs captured from the pre-v2 binary
and committed. C3 is load-bearing: without pinned goldens, "zero
behavioural regression" degrades into "both binaries agree", which a shared
bug satisfies.

Also carries docs/lyrw-v2.md, the narrower predecessor programme whose
banked dec results constrain three VINDEX2 arms (fixture D superblock
alignment, E4's absent K3 fidelity axis, the streaming-writer blocker).

Registry programme: vindex2 (14 entries).
The storage half of V2-0. LYRW v2 describes storage only: banks, entries,
region schemas, offsets and formats. It carries no programme identity — the
MoE manifest binds bank_id -> programme, and keeping that in exactly one
place is why "binary says programme 4, manifest says gpt-oss-expert-v1"
cannot happen.

Layout: header, bank descriptors, segment descriptors, per-bank region
schemas, per-segment entry tables, then 64-B-aligned payload regions.

The writer streams. The v1 writer took &[LayerEntry] — every expert's
quantised bytes, fully materialised — which at a K3 routed layer is 39.4 GB
in, ~118 GB f32 intermediate and 24.3 GB out against 128 GB of RAM. That is
a property of the API signature, not the machine. This one emits the
tables, reserves the entry table, appends one region at a time and
backpatches on close; retained state is one (offset, length) pair per
region and never the payload.

The reader resolves (bank, entry, role) to a byte range without copying, so
untouched up/down pages cost nothing under mmap.

Refusals are the point. The failure this format defends against is not a
crash — it is a table read at the wrong stride landing on offsets still
inside the file, returning plausible bytes from the wrong expert. So every
check fails closed and names what was wrong: a v1 file is refused by
generation ("requires the VINDEX1 loader", never a parse error), a foreign
file by magic, a short file by which table was truncated, an out-of-range
region by bank, entry, role and the file length it was checked against.

Unknown role/format/packing tags are preserved rather than rejected —
refusal belongs at capability-check time, or a browse-only reader chokes on
a down region it never touches (spec §6.5).

Bank descriptor field order is pinned byte-by-byte against spec §6.2, since
a writer/reader divergence there produces wrong bytes rather than an error.

20 files, 151 tests, no file over 260 lines. Per-file line coverage: 13 at
100%, lowest 98.9%.

Not yet built: MoE manifest schema, profile inheritance and variant
selection, derived authority as weakest-link, programme-traversal capability
checking. All of them sit on this container.
index.json's `version` becomes the sole generation discriminator, enforced
rather than documented. `load_vindex_config` previously read index.json and
never checked it, so a VINDEX3 directory deserialised into VindexConfig on
the strength of its shared field names and the VINDEX2 loader proceeded
against a layout whose weights live somewhere else — a served model with
wrong weights, not an error.

Both generations are renamed so the number matches the discriminator:

  generation   index.json.version   LYRW format_version
  VINDEX2      2                    1
  VINDEX3      3                    2

The previous VINDEX1/VINDEX2 naming put a permanent off-by-one between the
name and the field every loader dispatches on. LYRW keeps its own sequence
and trails the container by one, permanently — the two are different
artifacts with different lifetimes, and LYRW's numbering was already right.

That offset was load-bearing and nearly shipped wrong: Lyrw2Error's
generation mapping was identity, correct only under the old naming. A LYRW
v3 file would have named "VINDEX3" when it means VINDEX4. Two tests caught
it; the mapping is now explicit and pinned across a range.

Unknown role/format/packing tags are preserved at read time and refused at
capability-check time (spec §6.5) — the mechanical form of the additive-
extension precedent, and what stops a browse-only reader choking on a down
region it never touches.
Extracting a MoE model whose experts are stored as separate tensors per
expert (`experts.{id}.w1/w2/w3` — OLMoE, Mixtral, DeepSeek) produced an
index that reported success, passed `larql verify` on every file, sliced
cleanly on all eight presets, walked cleanly, and then panicked on the
first decoded token:

  pure-MoE layer 0 has no expert weights: the vindex is missing its
  per-layer expert store (layers/layer_00.weights)

write_per_layer_moe_kquant is gated on `is_moe() && expert_format ==
PackedBF16`. The default expert_format is PerExpert and only Gemma 4
overrides it, so the entire separate-tensor family returned early and
wrote nothing. The gate had already been widened once, from
`is_hybrid_moe()`, with a comment naming this exact failure mode — the
widening did not reach far enough.

Found by extracting OLMoE-1B-7B while building the E0 preservation
corpus, which is what that corpus is for.

The new writer reuses `quantize_dense_entry`: a dense FFN layer and one
per-expert MoE expert are the same assembly, and a second implementation
was written and then deleted before it could drift from the first. That
function gains shape validation and a corrected doc comment — it claimed
gate/up were "interleaved" when they are concatenated, which matters
because reversing the order swaps every GLU's halves with no crash and no
size change. The order is now pinned against the same split the consumer
performs.

Partial layers fail closed: expert 0 present but expert 5 absent is an
error naming both counts, not a short entry list that silently drops
experts routing will later select.

The wider lesson, recorded in the E0 write-up: verify, slice and walk all
pass on an index that cannot serve, so decode stays a first-class row in
the preservation matrix rather than being inferred from verify.
E0 asserts that adding VINDEX3 support changes nothing observable on any
VINDEX2 path. Without a fixed record, that degrades into "the two binaries
agree with each other" — a condition any bug present in both satisfies,
and close to circular given both are builds of the same binary. The
baseline has to predate the v2 code and be committed.

scripts/e0-capture-goldens.sh captures, from a pre-v2 binary: verify
verdicts, index.json version fields, WALK top-K over three fixed prompts,
greedy decode over the same three, and all eight slice presets. Paths,
durations, throughputs and timestamps are normalised out, so a diff means
behaviour rather than environment. A non-zero exit is recorded as part of
the golden — a path that starts *succeeding* must be flagged too.

Two corpora, both captured from 302ae59:

  gemma4-26b-a4b   18/18 clean. Decodes ("Paris.", a Fibonacci
                   explanation, "100°C (212°F) at standard sea level").
  olmoe-1b-7b      17/18 clean; decode panics, pinning the separate-tensor
                   expert-store bug as a golden so its fix shows as a diff.

The corpora themselves are NOT committed — they are regenerable, so
PROVENANCE.txt pins the recipe instead: baseline commit, model repo,
revision hash and exact extract flags. An earlier draft claimed the
baseline expired at merge; it does not, since the checkpoint still
extracts and the commit stays checkoutable. The reason to build the corpus
early is that E0 is specified as continuous — it should be catching
regressions while later V2-0 work touches shared code.
Three amendments, all from contact with the code and all recorded in place
rather than silently applied.

E0's corpus was specified as "existing production v1 indexes". There are
none on the rig, so the experiment had no subject and every check would
have passed by having nothing to check. Replaced with a constructed,
pinned corpus: C1 a fresh Gemma extract, C2 pulled hub artifacts, C3
golden outputs from the pre-v2 binary. C1 must be `--level all` — the
`all` slice preset needs lm_head and `router` needs router weights, so a
lower level would silently reduce preset coverage to whatever the extract
reached. Also corrected: eight slice presets, not six.

E7's W0 control does not cover what E7 is about. Measured on the C1
extract, VINDEX2's searchable index holds 2,112 features per layer — the
dense FFN width — against the 90,112 the expert population would
contribute. The expert weights decode fine; they are not in the searchable
surface. W1/W2/W3/W4 are about expert regions and W0 has none, so
comparing them measures a coverage difference wearing a parity result's
clothes, and "identical top-K rankings" is unevaluable.

Resolved by restating the claim rather than rebuilding the control:
expert-feature search is a NEW CAPABILITY of VINDEX3, not parity with
VINDEX2. E7 now splits into a parity arm (W0 vs W1, dense model, genuine
like-for-like) and a capability arm (MoE, correctness against directly-
computed gate dot products, no baseline because none exists). Prior 9
splits to match. The rejected alternative — building an expert-bearing
VINDEX2 index to serve as a control — would have been a control made for
the experiment rather than the thing that shipped.

The generation rename is reflected throughout, with a §12 note on why the
LYRW sequence trails the container one and must not be "aligned".
…ects

The physical index stores tensor regions; this manifest gives them meaning.
Keeping the two apart is what makes "the binary says programme 4, the
manifest says gpt-oss-expert-v1" unrepresentable rather than discouraged —
LYRW carries no programme identity, so the manifest is the sole binding.

Programme registry (§8.4) with frozen ids, each declaring the region roles
it needs. Requirements are modelled as ALTERNATIVES rather than a flat set,
because `gate + up` and `gate_up_fused` are equally valid ways to satisfy
the same programme — which is exactly the V2-1 acceptance requirement that
fused and decomposed storage "produce identical results under one
manifest". `missing_roles` reports the closest alternative, so an index
holding gate_up_fused is told it needs `down`, not that it needs gate, up
and down.

Router vocabulary sized against the four envelope routers, which disagree
on nearly everything: softmax vs sigmoid scoring, plain vs grouped top-k,
renormalise, norm-after-top-k, gate bias, route scale, quantile balancing,
and Inkling's shared-expert sink. All four round-trip through one type.
That is the falsifiable claim: a real model needing a knob that is not here
is an ABI finding, not a post-freeze patch.

The sink is not cosmetic — with it, shared experts draw from the same
normalised weight budget as routed ones, so a reduction that ignores it
silently over-weights the routed branch. A sink declared without a shared
bank is therefore a defect, not a no-op.

Per-layer, deliberately. A global `first_k_dense_replace` covers
Kimi-Linear's leading dense layer and then fails on Inkling-Small, whose
dense MLP sits mid-stack at index 2. Dense layers are expressed by absence
from the manifest, which handles arbitrary schedules for free.

Manifest-level checks are the ones answerable without reading a weight
byte: unknown programme, latent bank without transforms, input_space
contradicting its programme, selection exceeding the bank, sink without a
shared bank, duplicate layer, unsupported schema version. Operand-absence
needs the LYRW files and lands with the capability check.

One root cause yields one defect: an unknown programme does not also report
a space contradiction, or the reader chases input_space while the real
problem is the name.
Five corrections from review of the manifest layer.

Defect suppression was scoped correctly in the code but its TEST enforced
over-suppression: an unknown programme was asserted to yield exactly one
defect in total. That would have forbidden reporting independent defects
alongside it, so a typo'd programme name could conceal a real second
corruption. Suppression is now documented and tested as dependency-scoped —
an unresolvable programme suppresses only checks whose interpretation
depends on knowing the programme (input-space compatibility), and the
assertion is by absence of that specific defect rather than by a count.

Storage-shape checks added precisely because they must survive that
suppression: empty storage reference, zero experts, zero-width dims, and
two banks naming one storage location. None is a function of programme
identity. Two banks both blank report emptiness rather than duplication —
"duplicate storage ''" would point at the wrong fix.

The sink asymmetry is now pinned in both directions. `shared_expert_sink`
requires a shared bank; a shared bank does NOT require a sink. Kimi-style
always-active shared experts outside sink normalisation are valid, and the
flag must never become shorthand for "this layer has shared experts".

Programme diagnostics gain determinism and self-description. Ties among
equidistant alternatives resolve by declaration order — the alternatives
are a fixed &'static slice and `min_by_key` keeps the first minimum, so the
chosen layout cannot drift as iteration changes. `Unsatisfied::describe`
names the layout it judged against: "closest accepted layout: gate_up_fused
+ down; missing down". A diagnostic that moves is a diagnostic nobody
trusts.

`eager` is retired in favour of `direct`, matching §6.2's normative
encoding (none | direct | strided). It was a collision between §6.2 and
§15.2 and is now gone from the enum, its serialised name, its flag mapping
and every test.

Fixture D is frozen: hidden 512, latent 256, expert intermediate 256,
112 experts, top-16, 2 shared. 448/224/192 broke Q4_K superblock alignment
on every matrix (pad 224→256 is +14%, 192→256 is +33%), making the byte
ledger a padding artefact; no proportional downscale fixes it. 112 = 7 x 16
divides the group width where 56 would bake a partial group into the
fixture, and group width dividing segment width is a §7 rule, not a
preference. Exact-K3-scale fidelity stays where it belongs: the
byte-faithful bank at 896.
…ates

Preparation for the single traversal that authority derivation, operation
admission and kernel binding will all read from. Three independent
inspectors is how permissive logic gets in: each is individually
reasonable, and the union of their leniencies is what ships.

`satisfied_alternatives` replaces `satisfied_by`. A bank can physically
satisfy BOTH `gate + up + down` and `gate_up_fused + down`, and the kernel
registry may support one at a higher maturity than the other. Returning
only the first match would let a Production fused kernel hide behind a
Reference decomposed path — correctness-preserving, silently slower, and
miserable to debug. Traversal now preserves every usable layout and leaves
ranking to kernel binding. Declaration-order tie-breaking stays where it
belongs: the *unsatisfied* diagnostic, which needs one stable answer.

OperandCapability keeps four states that must never collapse:

  Absent(kind)          hard refusal; never a reference-executor fallback
  PresentUnsupported    bytes intact, codec/packing unreadable by THIS build
  ReferenceExecutable   correct execution, optimisation maturity incomplete
  KernelExecutable      specialised path, with its rung and kernel id

The middle two are the pair §11 legislates about. If "no grouped kernel"
and "operand absent" look alike, the reference executor becomes a fallback
for corruption rather than for immaturity, and an index that should have
been refused gets served slowly instead. PresentUnsupported also carries
forward §6.5's parse-time rule: bytes this binary cannot interpret are not
missing bytes — the artifact may be intact and simply newer.

AbsenceKind distinguishes four situations with similar execution outcomes
and different fixes: absent from every segment (extract it), partial
segment coverage (complete the transfer — it exists), incompatible segment
sets between two roles (reconcile the selection, not the storage), and
omission by the active profile (not a defect at all — a browse slice has no
`down` by design, and reporting that as corruption teaches operators to
ignore diagnostics).

RegionCoordinate carries {layer, bank, role, segment} per §11. Segment
identity stays individual even for adjacent failures: presentation may
compact a run, the report must not, because a consumer re-fetching exactly
the broken segments needs the list. `Some(0)` and `None` are deliberately
distinct — segment 0 is one of several, None is an unsegmented bank.

RegionRole gains Ord by registry tag so reports sort in spec order and stay
diffable.
… type

Authority derivation (§9.2) with the invariant enforced rather than
documented: replacing a selected component with weaker fidelity, or
removing an operand, can never increase any derived authority.

The fold is three stages and deliberately boring — weakest selected region
fidelity, capped by operation completeness, capped by declared structural
omission. Its input struct contains no names, paths or profile identity, so
the fold *cannot* consult a filename or a profile name even by accident;
that is a type-level guarantee rather than a code-review one.

Kernel maturity is not an input. A Production kernel and the reference path
executing the same bytes have identical fidelity — conflating them would
let a slow-but-exact path present as approximate, or worse, a fast
approximation present as exact.

Monotonicity is tested exhaustively rather than sampled: the lattice is
five levels, so enumerating every (before, after) pair over every input
axis is cheap and total. Four properties hold across the whole space —
weakening a region, adding a region, losing execution completeness, and
declaring a structural change all move authority down or leave it alone.
Order-independence is pinned separately, because a future "first wins"
shortcut would silently break weakest-link while passing every other test.

Two floors worth stating: an empty selection derives analysis-only, not
source-exact — nothing selected cannot be exact, and defaulting high would
make the emptiest profile the most authoritative. And capping is always a
minimum, never a floor, so a structural change cannot *raise* an
already-weaker selection.

`DerivedAuthority` reports which cap bound it, so "why is this only
analysis-only" is answerable without re-deriving.

BankSelection is traversal's input — what variant-and-segment resolution
produces, not something traversal computes. Fidelity is carried per region
and never inferred from the format tag: a Q6_K container of native MXFP4
values is source-equivalent while Q6_K quantised from BF16 is
numerically-approximate, and the tag alone cannot tell those apart. It also
records roles the profile deliberately omits, separately from roles that
are absent, because a browse slice without `down` is working as designed.
…l result

Segment incompatibility is relational — it is true of no role individually
— so it does not belong in the operand model. `IncompatibleSegmentSet` is
removed from `AbsenceKind` and replaced by `SegmentCompatibility` on the
alternative evaluation.

The architectural split is now explicit:

  OperandCapability      what each selected region can provide
  SegmentCompatibility   whether those operands can participate in ONE
                         computation
  (execution derives from both)

Assigning the relational fact to each operand separately would state the
symptom twice and the cause nowhere.

Three verdicts, with the precedence they imply:

  Compatible     every required role covers the required set
  Partial        every role covers the SAME set, short of required — no
                 cross-role disagreement, the selection is consistently
                 incomplete
  Incompatible   roles cover different sets, shaped as NoCommonCoverage
                 (nowhere the computation could run) or UnequalCoverage
                 (part of the population is executable, the selection is not)

Both incompatible shapes admit identically but are different situations,
and a reader deserves to know which.

Subsumption, not suppression: `subsumed_per_role_gaps` retains the
role-local partial-coverage facts as evidence on the finding, so machines
and verbose inspection keep them while the user-facing diagnosis names one
cause. The information does not disappear; the report selects the more
explanatory parent.

Compatibility is computed as a GLOBAL intersection, not pairwise. Three
roles covering {0,1}, {1,2} and {2,0} agree pairwise and share nothing
globally; a pairwise check would have called that compatible.

`is_invalid_selection` marks incompatibility as fail-closed. It is not a
declared approximation policy and must never be laundered into
structurally-approximate authority: a deliberately omitted `down` in a
browse slice is intentional and derives analysis-only, whereas two roles
accidentally resolving to disjoint populations is a resolution error. The
authority fold must not be asked to legitimise a contradictory selection.
Traversal answers one question: are the selected bytes sufficient for this
programme? It stops at reference executability. It does not choose a kernel,
does not decide authority, and does not know what a profile is called.

Four-tier precedence, with U the required population and Cr each required
role's usable coverage:

  1  any Cr = empty     role-local failure; compatibility MOOT, not evaluated
  2  all Cr non-empty,  incompatible segment sets
     sets differ
  3  all Cr equal, != U consistently partial coverage
  4  all Cr = U         reference-executable

Tier 1 outranking tier 2 is load-bearing: asking whether two roles' segments
agree is meaningless when one of them has no segments. That is expressed in
the type — `compatibility` is Option, None meaning "moot", not "compatible".

"Usable" excludes regions this build cannot interpret, so an all-unsupported
role lands in tier 1 naming the codec rather than being mistaken for a
coverage gap.

Two conflations were removed rather than worked around:

  OperandCapability combined physical state with kernel maturity. Traversal
  stops at reference executability, so that type carried a variant traversal
  could never emit. Split into role::{OperandAvailability, ReferenceSupport}
  with maturity moved entirely to kernel.rs — a traversal that could name a
  kernel would be a traversal that had already chosen one.

  AbsenceKind::PartialSegmentCoverage stated the same fact as
  SegmentCompatibility::Partial at the wrong level. Partial coverage means
  Cr != empty, which is tier 2/3 by definition, so the role-local variant was
  a second representation inviting the two to disagree. Removed; usable
  coverage plus the compatibility verdict carry it.

Alternatives are evaluated independently and every successful one is
preserved. A bank incompatible under gate+up+down and complete under
gate_up_fused+down is executable, and the failed alternative is evidence
rather than a layer defect. A closest failure is chosen only when none
succeed, ranked by fewest unusable roles with declaration order as the
tie-break.

The decisive test is provenance: declared and undeclared absence produce the
same physical shape and the same admission result, and must differ in
diagnosis. A gate-only slice that declared its omissions reports no defect;
a gate-only index that lost its regions reports one. Nothing infers intent
from missing bytes.
Three questions kept apart, per review:

  can this operation run?        admission          (this commit)
  how faithfully would it run?   authority, folded over the regions THAT
                                 operation consumes
  which implementation runs it?  kernel binding     (later)

`OperationCapability` is admission plus an OPTIONAL authority, and the
optionality carries the load. An unavailable operation has no fidelity
because it does not run. A contradictory selection also has none — and must
not be handed analysis-only or structurally-approximate merely because those
are low lattice values. Assigning a weak-but-valid authority to an invalid
selection would launder a refusal into a downgrade, and a caller asking "is
this at least analysis-only?" would wrongly proceed.

`OperationPlan` carries the regions an operation consumes, which is what
makes per-operation authority possible: the fold's domain becomes the
operation's own regions rather than the whole index. An index with exact
gate rows and approximate down regions can then honestly report WALK as
source-exact and local decode as numerically-approximate.

Failures distinguish causes an operator triages differently — unusable
region (with exact coordinate), invalid selection, missing document input,
missing contract, no executable route — and a test requires no two of them
to render alike. `MissingContract` exists specifically so remote execution
can never be inferred from local absence: "routed operands missing" and
"routed branch deliberately remote" are the same bytes with different
meanings, and only a declaration can tell them apart.

Degradation is separate from failure. Missing query metadata reduces label
richness and never affects correctness (§15.3), and the wording says so —
an operator must not read it as "results may be wrong".

Also restores a tier-1 cause the traversal was collapsing. Regions present
only outside the required population were being reported as
AbsentEverywhere, which is the wrong repair: the bytes are real and the fix
is to reconcile the selection's segment set, not to extract a variant. Now
`PresentOutsidePopulation`, naming both what was found and what was
required.
…e split

Pins the COMPILE contract:

  COMPILE performs document-scoped canonical reconstruction from declared
  baseline variants. It is independent of the active profile and reports
  fidelity derived from those baseline regions.

Scope is structural, not documented. `CapabilityReport` splits into
`DocumentCapabilities` and `ProfileCapabilities` so callers cannot assume
every operation describes the resolved profile — because one of them does
not. This combination is now legible rather than surprising:

  profile.local_decode           unavailable
  document.reconstruct_canonical available

A browse profile selecting only gate regions fails local decode while its
parent document, holding every baseline, reconstructs perfectly. The mirror
case is equally coherent and equally tested: baselines lost, profile
variants intact — serves fine, cannot be exported.

`CanonicalSelection::from_catalogue` takes the representation catalogue and
nothing else. No profile, no placement, no browse mode. Reusing the
profile's BankSelection and overriding fields would mix two scopes and let a
deliberate serving omission leak into an export operation that has nothing
to do with serving.

Canonical is not a fidelity claim. The baseline is canonical *within the
document*; its fidelity is still measured against the source checkpoint. So
`reconstruct_canonical: available, authority: numerically-approximate` is a
valid result, and the operation is deliberately not named
ReconstructCheckpoint or ReconstructOriginal — those promise a fidelity the
baseline may not possess.

The sharpest test: a region set whose baseline is source-EQUIVALENT beside a
sibling that is source-EXACT still reconstructs from the baseline. Canonical
means declared, not best available. And an absent baseline never falls back
to a present sibling, because falling back would silently change what
COMPILE emits.

`TensorReconstruction` targets the checkpoint's tensor vocabulary rather
than the index's storage vocabulary: a fused region may split into two
checkpoint tensors, separate roles may join into one, and values/scales
recombine. The mapping comes from importer metadata, never from guessing at
role names — otherwise COMPILE reproduces storage structure and calls it a
model. Bounded to four recipes; not a graph interpreter.

Exporting exactly what a profile selected remains a *different* operation
and should not be called COMPILE or reconstruct when it lands.
An operation can run more than one way, and the ways need not be equally
faithful. WALK may read a direct gate region or stride the gate half of a
fused one — different bytes, possibly different fidelity. Attaching one
authority to the abstract operation forces a bad choice: report the stronger
and binding may later pick the weaker, making the report a lie; report the
weaker and an exact route is understated; pick during inference and binding
is constrained before it has seen the kernel registry.

So `OperationCapability` now carries `routes: Vec<QualifiedOperationRoute>`
and has no authority field at all. Authority is reachable only through a
route, and the accessors are named `best_achievable_authority` /
`worst_achievable_authority` — achievable, not achieved, because no route
has been bound. `authority_is_settled` says whether binding can still move
the answer.

The fail-closed invariant survives in a stronger form: routes are empty
exactly when the operation is unavailable, so no routes means no authority
and a contradictory selection cannot acquire a weak-but-valid fidelity by
default. `is_well_formed` states that equivalence and a test enforces it.

Plans are fixed requirements plus independent choice groups, not a Cartesian
product. Thirty layers each admitting two alternatives is 2^30 whole-model
routes; enumerating them is not a representation. Binding picks one
alternative per group, and before that the honest statement about such a
plan is a range rather than a value. A test builds thirty choice groups and
asserts the representation stays thirty.

Alternative identity is carried into the plan so kernel binding can match a
kernel to the layout it actually supports, and `best_alternative` ranks by
weakest-link rather than by "has an exact region somewhere" — a uniformly
approximate route beats one that is exact in two places and structurally
approximate in a third.
… residual

Two things pinned before inference, because both would let `infer_walk`
answer an underspecified question correctly only by accident.

WALK capability needs a target. "Is WALK available?" has no answer when one
bank has a direct gate route and another is serving-only with browse mode
`none` — both yes and no are defensible until someone says which bank they
meant. `WalkRequest` therefore carries a `WalkTarget`, and
`AllWalkableBanks` means the declared searchable surface, deliberately not
"whichever banks happen to work".

Unwalkable requested banks fail the request by default. Absent query
metadata reduces label richness and leaves rankings correct, so it degrades;
a missing searchable gate population changes the RESULT SET. Silently
dropping a bank and returning a global ranking over the remainder would be a
wrong answer wearing a successful one's clothes. `PartialPolicy` defaults to
RequireAll and partial results are opt-in, with the dropped banks reported
so a caller knows the ranking's true scope.

`WalkInput::ResidualVector` means the model's residual space, never a bank's
latent space, and width is checked against the residual dimension rather
than against the bank being searched. The trap this closes is specific: K3's
latent banks are 3584 wide, so a 3584-wide query would dot-product against
latent gate rows perfectly happily while skipping `routed_input` entirely —
succeeding at a different question. The diagnosis says so in as many words.

`TextQuery` is a separate input mode so a caller supplying a prebuilt vector
is not refused for lacking tokenizer and embedding infrastructure it never
uses.

Also records, on `PlanChoice::best_alternative`, that it is for introspection
and planning only and must never serve as a binding tie-break: when two
alternatives carry equal authority, kernel binding has to stay free to
choose on support and performance grounds, and an authority-oriented
tie-break would silently decide a question belonging to the kernel registry
using a criterion that cannot distinguish the candidates anyway.
WALK inference, built to satisfy one negative acceptance test: a WALK report
must be derivable without ever asking whether the model can perform an
expert forward pass.

The strongest form that property could take turned out to be structural
rather than behavioural. `WalkableBank` carries a browse mode, gate accesses
and an optional latent transform — and nothing else. There is no `down`
field to consult, no router, no expert traversal, no kernel maturity, no
query metadata. Unreadable expert regions cannot perturb a WALK report
because they are not reachable from the function's inputs.

Scope is enforced, not assumed. With bank 0 walkable and bank 1 serving-only:

  target bank 0      available
  target 0 and 1     unavailable, naming layer 1 and its browse mode
  target all         unavailable, naming layer 1

That third line is what stops "some walkable content exists" becoming "WALK
works". The declared surface is a list, never a filter over health — a bank
declared searchable but absent from the index is reported as a failure
rather than quietly dropped, which is the back door that would have
recreated working-subset semantics through implementation.

Partial results are opt-in and do not lower authority. Bank A's gate rows
are still source-exact when bank B is dropped; the population shrank, the
numbers did not degrade. `ResultSetCompleteness` therefore sits beside
authority rather than inside the fold, matching the distinction already
drawn for absent query metadata.

Latent banks put `routed_input` in the plan, so exact gate rows are not
enough on their own — weakening the transform weakens latent WALK and leaves
residual WALK untouched. A latent-width query vector is refused even though
it would dot-product against latent gate rows perfectly happily, because
succeeding there means having skipped the projection.

One choice group per target bank, never a Cartesian product across banks:
eight banks produce eight choice groups on one route.

`ResidualVector` needs no tokenizer and no embeddings; `TextQuery` needs
both, and folds embedding fidelity into route authority while the tokenizer
contributes availability only — it has no numerical fidelity to contribute.
Preparation for local decode, and a correction to what I proposed. I was
going to put embeddings, norms and attention weights into an environment
analogous to `TextQueryInputs`. That would have blurred three things which
must stay distinguishable:

  what the index contains     the catalogue
  what the profile selected   the resolved selection
  what the caller supplied    the request / contract

Stored weights belong to the second. An environment holding both stored
tensors and caller inputs would leave the loader unable to say whether a
missing tensor was never extracted, deliberately dropped, or simply not
passed in — which is precisely the provenance distinction the rest of this
layer works to preserve.

So `SelectedComponent` spans both addressing schemes: bank regions and
manifest-addressed tensors. `ComponentCoordinate` likewise, carrying the
weight class, optional layer and tensor key for the manifest side. Only bank
regions have a role, which the type states by returning `Option`.

`SelectedTensor` keeps omission provenance separate from readability. A
deliberately dropped LM head in an attention-only client slice is unusable
and not a defect; the same tensor missing without declaration is unusable
and a defect; an unreadable codec is unusable, not a defect, and possibly
just newer than this build. Three states, three repairs.

`OperationPlan::fixed_regions` becomes `fixed_components`, with a
`fixed_regions()` constructor retained for the WALK shape — WALK genuinely
reads only bank regions and a latent transform, and should not have to
mention a component type it never varies.

No behavioural change: the authority fold now sums fidelity over components
rather than regions, and both kinds contribute identically.
Assembly, as intended. The adapter says which tensors and banks a layer
needs; programme traversal says which bank-region arrangements execute; this
joins them and folds authority. Kernel maturity is never consulted.

The two decisive tests are inverses, and together they prove neither side
pretends to validate the other:

  experts complete, routed_input absent
    → bank traversal succeeds
    → local decode fails, naming routed_input

  fixed path complete, down unreadable
    → every tensor resolves
    → local decode fails through bank traversal, naming the bank

A complete expert bank is not a decodable layer. Traversal answers whether
the BANK COMPUTATION runs; requirements answer whether the WHOLE LAYER PATH
does. Router weights, latent transforms and routed output norms are fixed
dependencies of the layer, not part of the bank's programme.

Four things pinned in the requirement model:

Requirements never restate the programme. An adapter names participating
banks and surrounding tensors; gate/up/down alternatives stay in the
registry, so a programme change does not require synchronised edits to every
adapter.

Requirements carry alternatives, because the fused/decomposed problem recurs
outside expert regions. A tied LM head is the case: some models store a
dedicated tensor, others reuse the embedding table, and a flat mandatory
list would reject a valid model. Same rule as programme alternatives —
declaration order decides which failure is reported, never which success is
used.

"Present and readable" is not enough. `ComponentUsability` adds
ContractMismatch beside absence and unsupported encoding, because a readable
tensor of the wrong shape is neither a missing file nor a build limitation —
it is an invalid index or an adapter mismatch, and a wrong-but-compatible
shape can survive a long way down a generic buffer path or never fail at
all. Five states, five repairs.

The request is one variant. Residual-to-logits and layer-range boundaries
change which fixed components are required, so each deserves its own variant
rather than a general request whose optional-field combinations become
ambiguous. TokenIdsToLogits needs no tokenizer: the ids are already
supplied.

Failures aggregate. A missing router at layer 12 and an unreadable norm at
layer 19 are independent facts, and reporting only the first would make
fixing an index an iterative guessing game.

The dense schedule is a list of per-layer requirements, so Kimi-Linear's
leading dense layer and Inkling-Small's mid-stack one work through one
mechanism with no global field to get wrong.
…aught one

E0 is specified as step zero, green throughout, any regression blocking
merge. It had not been running. Twenty commits touched shared loading code
with VINDEX2 compatibility in an unknown state rather than a preserved one.

It now runs as a named CI step, separate from the general test run so a
failure reads as "the shipped generation regressed" rather than as one red
test among two thousand.

## Scope, stated rather than implied

The full matrix — decode, WALK, slicing, sharding, publish/pull — needs
multi-GB checkpoints and cannot run in CI. Those rows run locally against
the committed goldens via scripts/e0-capture-goldens.sh. What runs in CI is
the generation-boundary subset, which needs no weights and is precisely the
part VINDEX3 work puts at risk.

## The regression it caught, first run

`v1_config_loads_with_defaults` broke: `index.json` version 1 no longer
loaded.

My generation model assumed a bijection between container generation and
index.json.version. There isn't one below 2. Version 1 is a *legacy schema
of the same shipped generation* — such indexes exist in the wild, and the
loader has always read them by filling absent fields with defaults. Treating
the version as a generation identifier rather than a generation floor
refused them outright, breaking the exact compatibility that dual-generation
support exists to protect.

Fixed: versions 1 and 2 both resolve to VINDEX2, 3 to VINDEX3, and the
supported-versions diagnostic now reads "1-2 (VINDEX2), 3 (VINDEX3)". The
generation is named for its *current* index.json version, and the docs say
so along with why the mapping is not a bijection.

That is what a preservation matrix is for. Caught on the introducing branch
it is a one-line mapping fix; caught after another ten changes it would have
been archaeology.

2031 tests, 0 clippy warnings.
The E0 regression exposed a model error, not a forgotten branch:

  index.json schema version  !=  container generation

VINDEX2 spans schemas 1-2; VINDEX3 begins at 3. The mapping is many-to-one.
So the types no longer permit conflating them: `IndexSchemaVersion` is a
newtype, and no API accepts a bare u32 and calls it a generation. The
previous code compiled perfectly while making exactly that mistake.

Four statements are now independently true and independently testable:

  VINDEX2 writes schema   2
  VINDEX2 reads schemas   1-2
  VINDEX3 writes schema   3
  VINDEX3 reads schema    3

`generation_for_schema` maps a revision to its owning generation;
`current_schema_version` and `supported_schema_versions` answer the two
questions separately. A test asserts no two generations claim the same
schema, because overlap would make dispatch ambiguous and falsify the
sole-discriminator property.

Spec §12.1 amended to match. `index.json.version` remains the sole dispatch
input — no filename sniffing — but it is a *schema* discriminator, and the
loader maps revisions to generations. The table is written out, along with
why a generation is named for the schema it writes rather than the only one
it reads.

Unified dispatch and direct-loader refusal are separated and both tested:
schema 1 routes to the VINDEX2 loader, and the VINDEX3 loader still refuses
it by name.

## E0 now has two named gates

"E0 green" must never be reported when only part of it ran:

  E0-CI    generation/schema boundary, weight-free — required merge check
  E0-FULL  checkpoint-backed matrix against committed goldens — local,
           reported as "green at commit <sha>"

The reporting convention is written into the experiments document so a
future status line cannot claim the whole matrix on the strength of the
subset. A synthetic VINDEX2 artifact exercising load/verify/slice and one
trivial decode in CI is noted as the way to narrow the gap.

2032 tests, 0 clippy warnings.
`resolve_requirement` returned on the first satisfied candidate. A document
holding both a dedicated lm_head and a usable embedding table has two valid
routes, and returning the first settled the route — and therefore the
authority and the kernel choice — before binding had seen the registry. The
same collapse already fixed for expert alternatives, recreated one layer up.
My own commit message claimed the opposite property held; it did not.

The rule now:

  0 usable candidates  → unavailable, naming EVERY candidate's own repair
  1 usable candidate   → fixed binding, no degenerate choice group
  2+ usable candidates → independent choice group

Failed candidates survive even when another succeeds. An operator debugging
why the tied head was used needs to see that the dedicated one was absent,
and "lm_head unsatisfied" alone says nothing about which to fix.

Choice groups therefore stop being bank-only: `QualifiedAlternative` gains a
components side so a requirement's candidates can form a group beside the
expert banks, and the authority fold reads both.

Also adds the identity split this needs. Variant must NOT be part of
physical identity — folding it in would defeat the deduplication it exists
for, since two catalogue declarations may name one extent:

  RepresentationIdentity  which declaration was selected
  PhysicalStorageId       which bytes that resolves to

  same bytes, two semantic uses         → one residency allocation
  same bytes, two permitted views       → one component, two bindings
  same logical tensor, duplicated bytes → two components

Only exact extents deduplicate; partial overlap stays distinct, because a
sub-range is either legitimate or a catalogue error and neither makes it the
same component. Two declarations sharing an extent while disagreeing about
it are a catalogue defect, not two components — the bytes cannot be both.

`ComponentView` records how bytes perform a role, because
embedding-as-lm-head is a transpose, not the same access. Contracts are
checked AFTER the view: a dedicated head is [hidden, vocab] and an embedding
is [vocab, hidden], so comparing the requirement against raw storage would
reject a valid tied-weight model. No view alters values, so views never
touch authority — they affect kernel eligibility and access efficiency, and
a route needing transposed access must be exposed at Reference maturity
rather than silently repacked at decode time.

2055 tests, 0 clippy warnings.
Fixture A executes through a bound VINDEX3 operation and reproduces an
independently implemented oracle to below 1e-6, from both decomposed and
fused storage, agreeing at every internal checkpoint.

Scope, precisely: this is V2-1's first successful rung, not the gate. It
does not touch Vindex::open, on-disk profile resolution, Gemma, shared
experts or latent K3 execution.

## The boundary

BoundMoeOperation { router, transforms, banks, reduction } is the terminal
product of binding. No manifest lookup, capability traversal, authority
fold, variant resolution or kernel search is reachable from execute() —
resolution that stays reachable gets called, and per-token cost starts
tracking catalogue size rather than work size.

The router is a BoundRouter rather than a bare tensor: top-k depth,
renormalisation and per-expert scaling decide which experts run and by how
much, so leaving them for the token loop to infer would breach the same
boundary. Validation lives on the operation and is called by binding.

BoundProjection resolves fused vs decomposed once, behind an interface that
yields the same two rows either way, so activation, the gated product, the
down projection and the reduction cannot acquire a layout assumption.

## Instrumented, not final-vector-only

Checkpoints: router scores, selection margin, selected ids and order, gate
weights, per-expert outputs, reduction, residual delta. One generic
execution over a TraceSink — NoTrace compiles away, so the fixture exercises
the code Gemma will run rather than an instrumented copy.

Mutation-checked: interleaved fused reads, activation on the product rather
than the gate, and a transposed router each fail the tests that should catch
them and no others.

## Tie behaviour, pinned before Gemma

The incumbent's sort_unstable_by over partial_cmp is unspecified for ties.
This path commits to score descending, then expert id ascending, using
f32::total_cmp — a real total order, not a fallback to Equal. Non-finite
scores are refused rather than ordered, since a NaN would otherwise be
selected or dropped by sort mechanics.

top_k_with_margin reports score[k-1] - score[k] so a Gemma divergence at an
exact tie is triaged as a selection-policy difference in minutes rather than
mistaken for a weight-decoding fault. VINDEX2's production behaviour is
unchanged.

## Measurement

benches/vindex3_bound_execute — successor to vindex_storage_dispatch, gating
"has resolution leaked into decode". Its first draft asserted flat cost in
population; measuring said otherwise, and the claim was wrong — routing must
score every expert. Restated as a growth-shape budget: 64x population buys
~1.9x, which is the router term.

It caught a real defect: decode_at rebuilt its operand name per element,
allocating a String on every scalar read.

                        before        after
  population 64         2.42 ms       142 us
  decomposed/fused      43% apart     0.7% apart
  trace overhead        -             0.4%

The 43% "arrangement" gap was the allocation, not the layout —
gate_up_fused is a longer name than gate. Not a VINDEX3 speed result; the
removal of an accidental allocation catastrophe from new code.

tests/vindex3_allocation_guard — thread-local counting allocator, scoped to
that test binary. Invariant: operand lookup and scalar access must not
allocate in proportion to dimensions, population or elements read. Tightens
to zero per token when execute_into lands.

examples/vindex3_residency_probe — successor to mmap_cold_read_probe, via
mincore rather than fault counting. The bound plan predicted its resident
set exactly: 200 pages predicted, 200 resident, zero overshoot, 1.63% of a
192 MiB layer resident after one token.

Found that msync(MS_INVALIDATE) before madvise(MADV_DONTNEED) does evict on
Darwin, where MADV_DONTNEED alone leaves a freshly-written file 100%
resident. mmap_cold_read_probe works around the same limitation with
F_NOCACHE pread.

## fix: larql verify was nondeterministic

A real product defect, found by E0-FULL and unrelated to VINDEX3.

`verify_checksums` iterated a `HashMap` directly, and Rust randomises
`HashMap` iteration order per process, so `larql verify` printed its findings
in a different order on every run against an identical, intact artifact —
three invocations, three digests. That makes it unusable for goldens, CI
diffs, signed reports and operator triage, and it made E0's `verify` row
permanently unfalsifiable.

Findings are now rendered in canonical artifact order. Sorted at the
collection boundary rather than in the CLI, so every caller inherits it and
hashing stays free to run in any order — including in parallel later —
without scheduling deciding user-visible output. Five consecutive
invocations now produce one digest.

Five tests, mutation-checked. Two of them fail without the sort; the other
three do not, and one of those was actively misleading:
`repeated_verification_of_one_artifact_is_byte_identical` passed even with
the fix removed, because per-process hash randomisation means one map
iterates identically within a single test. It could never have caught the
bug it was named for. Renamed to what it actually guards (no state carried
between calls), with the limitation documented, and
`insertion_order_does_not_affect_output_order` marked as the load-bearing
guard.

## E0-FULL now has a runner

The goldens were committed and provenance-stamped, and nothing ever read them
back — an assertion no one was making. `e0-capture-goldens.sh` was
capture-only.

Added `e0-verify-goldens.sh`, which replays the record against a current
binary and diffs every row; exit status is the verdict (0 match, 1 differ, 2
goldens absent). The corpus — prompts, row set, normalisation — moved to
`scripts/lib/e0-corpus.sh` and is shared by both scripts, because a
capture-side and verify-side copy would drift and E0-FULL would degrade into
"the two scripts agree with each other", the same circularity the capture
script's own preamble warns about.

Mutation-checked against a stub binary: a changed decode path fails exactly
the three decode rows and exits 1; unchanged exits 0; missing goldens exit 2.

Two harness bugs fixed along the way, both of which produced confident false
positives that read exactly like the regression under test:

- Replay parameters were defaulted rather than read from the golden's
  PROVENANCE.txt. Replaying 24 tokens against a 16-token golden differs on
  every decode row.
- The timing normaliser ended in `\b`, a GNU extension that BSD `sed -E`
  neither supports nor complains about — it silently never matched, so no
  duration was ever stripped on macOS. Replaced with a portable form;
  verified that durations strip while file sizes, which are behaviour,
  survive.

Rows whose output is a set rather than a sequence (`verify`) are canonicalised
on both sides, so a golden captured from the still-nondeterministic baseline
binary remains usable. WALK rows are deliberately excluded — their content is
a ranking.

## E0-FULL status: not discharged

Run against a freshly extracted C1 (gemma-4-26B-A4B-it @ 7d4c97e5, the
revision the golden recipe pins):

    matched  5   verify, decode_p0, decode_p1, decode_p2, index_version
    differed 12  show, walk_p0..2, slice_* (8)

All three greedy decode rows are token-identical, and all 632 WALK ranking
lines match. That is strong evidence VINDEX2 execution has not regressed.

The 12 diffs are traced, one line each, and neither cause is a regression:

- show + 8 slice rows: `index.json 5.9 KB` vs `5.7 KB`. I extracted with the
  current binary instead of rebuilding the baseline at 6eae5ea as the recipe
  prescribes, so this conflates extractor changes with reader changes — the
  precise confound E0 exists to prevent. My procedural error.
- walk x3: the timing line only, because the goldens were captured with the
  broken normaliser and hold literal durations.

Both need the prescribed baseline reconstruction (baseline binary → baseline
extraction → current reader against that artifact), which is a separate
preservation change. Status stands as:

    E0-CI                    green
    E0-FULL decode rows      green
    E0-FULL remaining rows   not discharged

## Also

- larql-compute: f16_to_f32 made public. It is the only correct f16 decoder
  in the workspace — its subnormal branch fixes a 2x error that a from-
  scratch reimplementation reproduces almost every time.
- Fixtures are public, not cfg(test): tests, bench, demo and future
  conformance checks share one definition.
- Fixture A's population moved 4 -> 5. With population and hidden both 4 it
  could not have detected a transposed router.

2238 tests, 0 clippy warnings, no runtime file below 90% line coverage
(14 of 18 at 100%). Non-unix branch of the residency probe verified
standalone; cross-target cargo check is blocked by ring/openssl-sys needing
cross-compilers.
VINDEX3 has independently routed a real Gemma activation through real
checkpoint weights and selected exactly the same experts as production LARQL.

    layer 5, hidden 2816, 128 experts, top-8
    input: real h_post_attn, last token of "The capital of France is"

    incumbent  [17, 100, 126, 14, 120, 28, 73, 32]
    vindex3    [17, 100, 126, 14, 120, 28, 73, 32]

    gate weights  max|delta| = 6.99e-4
    boundary margin 0.000909

Same index, same Q4_K expert bytes, same f32 router, same activation. Only
the execution path differs, so a divergence would have been unambiguously
about execution rather than about re-extraction — which is why this binds
over the incumbent's own bytes instead of extracting a VINDEX3 container
first.

## Three corrections the real model forced

None was reachable synthetically. Each would have survived every shape check.

### Router input is not expert input

Gemma's `moe_router_input` applies a router-specific RMS norm, a learned
element-wise scale and a scalar on top of the vector the experts consume, so
routing scores a *different vector*. The operation fed one input to both. It
would have scored on the expert input and selected different experts — a
wrong answer with no shape error anywhere.

`MoeInputs::{shared, split}` expresses the distinction without making Gemma
special. Both vectors are supplied by the surrounding block rather than
derived here, for the same reason `execute` returns a delta: norms and scales
belong to the block, and deriving them would re-model the incumbent's routing
policy enums inside VINDEX3 with somewhere for the two to disagree.

Pinned by a test where the bank input selects [2, 0] and the router input
selects [1, 3] over identical experts.

### A shard is a subset of the routing universe

`validate` required `router.population() == bank.population()`. Wrong, and
`bank.rs` already documented the opposite: "expert 40 may be the first one a
shard carries." It would have refused every expert-server slice. The correct
invariant is that every stored expert id lies inside the router's address
space; equality is a separate question, now `holds_full_population()`.

Relaxing it created an execution requirement, so that is pinned too:

  > If routing selects an expert absent from the bound local bank, execution
  > must produce an explicit missing-expert result — not skip it, not
  > renormalise around it, not substitute another.

`SelectedExpertNotResident` is a new variant, deliberately distinct from
`ExpertOutOfRange`: one means fetch the operand, the other means the index is
wrong, and collapsing them would leave an operator unable to tell which. It
names the expert, the layer and bank coordinate, and both populations.
Remote placement will later satisfy exactly this request without touching
router semantics.

Mutation-checked: making execution `continue` past a non-resident expert
fails five of the nine placement tests.

### Physical shape is not semantic operand shape

Q4_K pads the intermediate axis. Gemma stores `gate_up` at [2x704, hidden]
unpadded and `down` at [hidden, 768] — 704 rounded to a 256-multiple — so the
two regions disagree about the intermediate width.

`ComponentView::Slice` handled it with no index rewrite, no cropped copy, no
activation padding in the generic runtime, and no lying about the stored
extent: bind the stored [hidden, 768], let the role see [hidden, 704]. The
incumbent reaches the same place by zero-padding the activation instead.

Pinned with a poison-tail test: the padding columns hold 1e9, and the logical
view never observes them. Mutation-checked by swapping the view to Direct,
which fails three tests. A guard-the-guard test confirms the poison is really
in the stored bytes, so the assertions have something to catch.

## What is not claimed

Bit-exactness, and deliberately not a tolerance argument either. Two
structural numerical differences remain:

    router   incumbent BLAS sgemv vs index-order f32 accumulation
    experts  incumbent Q4_K x Q8_K integer dot vs dequantised f32 reference

A residual-delta tolerance would blend router accumulation order, softmax and
renormalisation, activation quantisation, integer dot rounding, expert
accumulation order and reduction order. Passing it would establish nothing;
failing it would identify nothing. So the comparison stops at checkpoint
three, where it stops being meaningful.

The 0.000909 margin establishes only that this particular selection was not
decided by tie policy. It is not a robustness margin across prompts or layers,
least of all while scoring accumulation differs.

## Next

Kernel binding, in two separate rungs: bind the incumbent router kernel and
require score/selection/weight identity, then bind the Q4_K x Q8_K expert
kernel and compare per-expert outputs, reduction and residual delta — aiming
for bit identity first, and introducing a tolerance only when the exact
floating-point operation that makes it impossible can be named.

2268 tests, 0 clippy warnings.
A VINDEX3 BoundRouter, consuming real VINDEX2 storage and a real Gemma
activation, calls the exact production scoring functions and reproduces every
observable routing stage bit-for-bit.

    layer 5, hidden 2816, 128 experts, top-8
    input: real h_post_attn, last token of "The capital of France is"

    1 raw scores            BIT-IDENTICAL
    2 top-k id/score pairs  BIT-IDENTICAL
    3 boundary margin       0.00090900064
    4 pre-norm weights      BIT-IDENTICAL
    5 normalised weights    BIT-IDENTICAL

The margin is non-zero, so this selection was decided by the scores and the
result is a legitimate exact parity rather than a coincidence of tie handling.

## Binding, not reimplementing

`matmul_vec` and `softmax` are now public in larql-compute, and
`RouterKernel::Incumbent` calls those exact functions. Writing a BLAS-shaped
loop here and calling the agreement "parity" would have proved only that two
similar loops agree.

`BoundTensor::as_f32_slice()` hands over the stored bytes or refuses; there is
no path where a reconstruction quietly substitutes for the operand. A bridge
that dequantised into an incumbent-shaped temporary could have reached the
same numbers while proving nothing about the binding architecture.

`RouterKernel` is a bound choice like every other decision in the object, and
the reference kernel remains the default and the oracle.

## The ladder prevented a false diagnosis

Its first run read:

    1 raw scores            BIT-IDENTICAL
    4 pre-norm weights      BIT-IDENTICAL
    5 normalised weights    max|delta| = 6.994e-4

Scores identical, final weights not — so the fault was in post-processing and
could not have been the BLAS-vs-index-order accumulation that a single
end-to-end number would have been blamed on. The cause was a missing bound
operand: Gemma's policy is PerExpert and the harness had bound no scale, so
execution silently declined to scale.

## Tie ordering stays canonical, deliberately

The incumbent's top-k uses `sort_unstable_by` over `partial_cmp`, which is
unspecified for ties. That comparator is *not* bound: importing an unspecified
order into a path whose value is being a reproducible oracle would defeat the
purpose. VINDEX3 keeps score-descending then expert-id-ascending over a total
order, and ladder step 2 confirms the two agree whenever scores decide.

A future whole-model report should classify each layer as exact-parity
non-tied, tie-equivalent under differing tie contracts, or actual divergence,
so a legitimate canonical tie decision is never read as a kernel failure.

## Invalid scaling is now unrepresentable

    enum BoundExpertScaling { None, PerExpert { scales } }

replaces a policy enum beside an `Option<BoundTensor>`, which permitted:

    PerExpert + None      policy demands a scale that was never bound
    None + Some(scales)   an operand nothing reads

The first is what this commit's own harness built, and it executed. Coupling
the policy to its operand is the difference between a check someone can forget
to call and a mistake that does not compile.

Scale coverage is defined over the **routing population**, not the resident
experts. Expert 90's learned scale is part of what routing means whether or
not expert 90 is resident, so a shard carries the whole vector; truncating it
would make two shards of one model weight the same expert differently.
Non-finite scales are refused at bind time, before a NaN can propagate through
the reduction into the residual and surface as an inexplicable token.

## Typed kernel-binding refusal

    enum OperandUnsuitability {
        ElementFormat  -> bind another variant, or a kernel for this format
        NonDirectView  -> a view-aware kernel, or a repacked variant
        MisalignedBase -> an aligned copy
        Length         -> reject the index; only this one is a defect
    }

Four causes with four different remedies, kept apart rather than collapsed
into one message.

## Scope

Router only. Expert values still differ by kernel — incumbent Q4_K x Q8_K
integer dot against a dequantised f32 reference — and that is rung 2,
deliberately not begun here. Committing separately so that a disagreement
found during expert binding cannot require bisecting a patch containing both
systems.

larql-vindex 2272 tests, larql-compute 894, 0 clippy warnings. Acceptance run
performed with `set -euo pipefail` and an unpiped build, after an earlier
command shape was found unable to rule out executing a stale binary.
Neither roadmap mentioned VINDEX2 or VINDEX3, so three shipped milestones
were invisible to anyone reading them. The programme had been tracked only in
docs/vindex2-experiments.md.

ROADMAP.md gains a VINDEX3 section carrying:

- the thesis — VINDEX2 can observe which pages faulted, VINDEX3 can state what
  an operation will read before it runs;
- the coexistence contract, including the standing constraint that `extract`
  keeps defaulting to VINDEX2 until V2-1 acceptance passes, since a silent
  default change would evaporate E0's premise;
- the three shipped commits (f13bf38, dd2017d, f5dd256) and the rung ladder
  to the first VINDEX3-generated token;
- a "Standing method" note recording what the programme established by
  repeated failure rather than by preference: bind never reconstruct, ladders
  rather than end-to-end tolerances, mutation-check every new test, and
  suspect the instrument first — this work has produced roughly three
  measurement defects per real code defect;
- an explicit "Not discharged" list: E0-FULL's remaining rows, the OLMoE
  goldens that pin a since-fixed decode panic, the 15 CLI call sites still
  assuming VINDEX2, and why `extract --format vindex3` is deliberately not
  done yet.

ROADMAP_STATUS.md moves the active slice from the DEC funnel to VINDEX3
execution binding, with rung 2 named as next.

The DEC funnel is demoted to "previous slice" but deliberately **not** marked
closed, because it is not: VINDEX3 is the storage and capability layer DEC's
expert serving will bind to, and the two meet at `SelectedExpertNotResident`
— the seam where a valid route with a missing local operand becomes a fetch
rather than a defect.

Docs only. Every link target and commit reference verified to resolve.
A VINDEX3 BoundExpert, consuming the store's own Q4_K super-blocks and a real
Gemma activation, calls the exact production expert kernel and reproduces every
observable stage of the expert path bit-for-bit.

    layer 5, hidden 2816, 128 experts, top-8, intermediate 704
    input: real h_post_attn, last token of "The capital of France is"

    6 per-expert outputs    BIT-IDENTICAL across 8 experts   asserted
    7 weighted reduction    BIT-IDENTICAL                    asserted
    8 block output          BIT-IDENTICAL                    observed
      oracle (relative)     1.98% of 9.687e-1                banded

Rungs 1-5 (router) remain bit-identical, unchanged from f5dd256.

Read the third column. The real Gemma layer produced a bit-identical full block
output in the acceptance run; the checkpoint-free CI test enforces exact
production-kernel parity over the padded Q4_K expert path. Rung 8 is observed,
not contracted — see "Rung 8 is reported, not asserted" below — and nothing here
promises that every future full-block invocation is bit-identical, because
scheduling can make that contract unavailable.

What this closes is the whole one-layer computation, not expert binding alone:

    real activation → production router → exact top-8 and weights
    → production Q4_K × Q8_K experts → selection-order reduction
    → post-expert norm → block output

## Binding, not reimplementing

`ExpertKernel::IncumbentQ4kQ8k` calls `run_single_expert_q4k_q8k_into` over
the region's own bytes. `BoundTensor::as_blocks()` hands over the stored
super-blocks or refuses; nothing dequantises, requantises or repacks into a
kernel-shaped temporary on the way. That matters more here than at the router,
because a Q4_K bridge that dequantised and re-quantised could reach numerically
close values while proving nothing at all about block addressing.

## The oracle leg is what stops this being circular

Two calls of one function agree whatever operands they are handed, so a
bit-identical result establishes only that the handover was faithful. The same
experts are therefore also bound dequantised and run through the reference
kernel; its answer is computed independently and lands 1.98% away, which is the
low-percent figure the Q8_K activation rounding predicts.

Both legs run in CI without a checkpoint:
`the_bound_kernel_reproduces_the_incumbent_call_bit_for_bit` builds a padded
Q4_K fixture through the production quantiser and asserts bit-equality against
a direct call, and `the_two_kernels_are_not_trivially_identical` guards the
band from being vacuous.

## One binding, two kernels

Gemma's `down` is stored `[hidden, 768]` and means `[hidden, 704]`. Rung 1
resolved that with a column-prefix slice view; this rung keeps that binding and
teaches the kernel handover to read it, rather than adding a second binding of
the same bytes.

    storage_cols   768   what a block-native kernel contracts over: it decodes
                         whole super-blocks and cannot stop inside one
    role_cols      704   what the operation means

`BlockOperand` carries both, because neither alone is enough: only
`storage_cols` loses the operation's meaning, only `role_cols` misreads every
row after the first. So `as_blocks` honours a zero-start column-prefix slice —
that view is not an obstacle, it is exactly the information the kernel cannot
recover from the bytes. A row slice or a transpose is still refused, because
neither leaves a contiguous run of whole stored rows.

## Addressing is a property of the encoding

    Scalar { bytes }               one element per fixed slot, index-addressable
    Blocked { elements, bytes }    reaching one element decodes its whole block

Sizing a blocked region with a scalar stride under-counts Q4_K by ~4×, which
binds a region that looks long enough and reads past its end on the last row.
Geometry comes from `QuantFormat::packed_block_layout`, so `256 * 144` is not
spelled anywhere in vindex.

The consequence is that the refusal *moved*: a Q4_K region now binds, and it is
the per-element read that refuses. That is the right place for it — the point
of binding one is to hand its blocks to a kernel — and a codec with no
registered geometry (mxfp4, nvfp4) is still refused at bind, because a region
that cannot be sized cannot be read either.

## An activation the kernel does not implement is now refused

The incumbent's inner loop reads `match activation { GeluTanh => .., _ =>
silu(..) }`. A bank bound with `ReLU` would therefore have run SiLU, returned
finite plausible values, and signalled nothing whatsoever — the same class of
silent-wrongness as rung 1's missing per-expert scale, but with no ladder step
that would catch it, since every rung would agree.

`KernelActivationUnsupported` is a distinct variant because no rebinding of the
*bytes* repairs it: the kernel computes a different function than the recipe
specifies, and the repairs are a different kernel or a corrected recipe.

## Two more kernel-binding refusals

    BlockAlignment   rows (or the activation) are not whole super-blocks
                     -> repack, or pad the extent
    Arrangement      a decomposed gate/up pair where the kernel takes one slab
                     -> bind the fused variant, or a kernel for this arrangement

`BlockAlignment` covers the activation side too. The incumbent guards a
non-256-multiple hidden width by falling back to its f32 path; VINDEX3 refuses,
because a silent change of kernel is a silent change of answer.

Together with the shape checks these make the incumbent's own short-slab guard
— which zeroes its output and returns *successfully* — unreachable from here.
`the_bound_kernel_is_not_producing_zeros` guards the guard, since an all-zero
agreement would be two failures agreeing.

## The kernel is a bank property, and it has a session

The incumbent quantises the bank input to Q8_K once and shares it across the
bank's selected experts. That is not an optimisation to reproduce or not as
convenient — it is the shape of the call production makes, and quantising per
expert would be binding a different call. So the kernel is opened once per bank
before the expert loop, which is also why `ExpertKernel` sits on the bank rather
than on the expert: two experts of one bank on two kernels is not a binding this
runtime can express, and that restriction is correct.

## Rung 8 is reported, not asserted

`cpu_moe_forward` sums its experts through rayon or the spin pool depending on
configuration, and a tree reduction is not required to agree bit-for-bit with a
sequential one. Rung 7 therefore compares against the incumbent's own per-expert
outputs summed in *selection order*, isolating the combine from the schedule.
Rung 8 came out bit-identical on the spin-pool path — which accumulates in
selection order — and that is a measurement of one configuration, not a promise
about every one.

The oracle band is relative rather than absolute, because expert outputs are
residual-scale values whose magnitude is a property of the layer; judging them
against the router's softmax-derived 1e-3 compares the right direction on the
wrong object, and did in fact fail this run before the metric was corrected.

Its denominator is fixed and stated: `max|reference|`, never `max|found|` and
never the larger of the two. A denominator that moved with the thing being
measured would let a kernel that inflates its output report a *smaller*
percentage for a larger error, and would make two layers' figures
incomparable. Below `ORACLE_SCALE_FLOOR` the comparison switches to an absolute
one and says so, rather than dividing by a near-zero reference and emitting a
number that looks like the others but compares to nothing. Both exist so the
percentage stays meaningful when the whole-model sweep starts quoting one per
layer.

A future whole-model harness should record which of these each block comparison
is — asserted exact, observed exact, or numerically equivalent within a named
reduction-order tolerance — rather than collecting them into one "parity"
bucket, which is precisely the distinction the third column above draws.

## Test layout

`runtime/*_tests.rs` moved into a flat `runtime/tests/`, and `test_support.rs`
to `runtime/tests/support.rs`; the fixture tests likewise. Mechanical — the
runtime's own file list is what a reader scans to find the implementation, and
sixteen interleaved test files made that harder with every rung.

## Scope

One layer, one token, CPU. Not begun here: reference decoding of quantised
regions (the reference still needs a dequantised binding to serve as an oracle),
the Q6_K and Q4_0 kernels that `Addressing` now sizes but nothing binds, and
whole-model residual parity — which is the next rung, and should run as a
locked-input layer sweep first (every layer fed the incumbent's captured
`h_post_attn`, so an earlier mismatch cannot contaminate a later input) before
free propagation carries each path's own residual through the model. The first
sweep is what localises a binding defect; the second is what proves the model.

What that sweep may still find is local: a layer with a different activation or
scaling policy, a differently padded or absent expert shard, layer-specific
physical geometry, a shared rather than routed bank, alternate quantisation
metadata, or a boundary layer whose norm or reduction differs. Those are
binding defects now. The architecture is no longer the likely failure point.

larql-vindex 2044 lib tests, 0 clippy warnings, coverage policy passes at 94.05%
total with addressing.rs, expert_kernel.rs, bank.rs and region_format.rs at
100%. Acceptance run performed against the 15 GB Gemma 4 26B-A4B vindex with an
activation captured through `LARQL_CPU_DUMP_LAYERS` from a real generation.
All thirty of Gemma 4 26B-A4B's MoE layers execute through a VINDEX3-bound
route and agree with the incumbent at every checkpoint.

    lyr  resident  top-k  activation  pad       verdict
      0   128/128  top-8  GeluTanh    704→768   observed_exact
      …
     29   128/128  top-8  GeluTanh    704→768   observed_exact

    observed_exact   30 layer(s)
    oracle, layer 5  1.98% of 9.687e-1  (fidelity, not parity)

Per layer, selection / gate weights / per-expert outputs / reduction are all
`exact` — asserted and bit-identical. The block output is `observed_exact`, so
the layer's own verdict is the weakest of the five. Reading the table: every
asserted checkpoint is a contract that held; the block figure is a measurement
of this configuration.

## Locked input, because free propagation cannot localise

Every layer is fed the incumbent's own captured `h_post_attn`. If layer 7
diverged and layer 8 then read a residual that already differed, layer 8's
mismatch would say nothing about layer 8 — the first failure would be the first
*symptom*, not the first defect. Locking the input makes each layer an
independent measurement. Composition is a separate proof and comes next.

## Classification is a library concern, not a harness one

`runtime/verdict.rs` holds the vocabulary, because three harnesses — this
sweep, the free-propagation residual comparison and the decode-parity run —
must classify the same situation the same way, and three private enums would
drift within a week.

    Exact            every asserted checkpoint bit-identical — a contract
    ObservedExact    bit-identical, uncontracted — a measurement
    Equivalent       within a named tolerance
    NumericMismatch  executed and disagreed
    Refused(kind)    did not execute

`ExecutionError::refusal()` sorts refusals into three kinds by *who acts on
them*: the operator who must fetch an operand, the engineer who must write a
kernel, and whoever produced an index that is wrong.

    Residency       the route was right, the operand lives elsewhere
    Unsupported     well-formed, and no bound kernel serves it
    BindingDefect   the binding is wrong

The match is exhaustive, so a new `ExecutionError` variant does not compile
until someone has decided which it is. `nothing_but_a_non_resident_expert_
classifies_as_residency` pins the dangerous direction specifically: `Residency`
is the one kind that reads as "nothing is wrong", so an unclassified failure
must never inherit it.

## Residency is not a parity failure, and the sweep proves it

`--shard 8` binds only the first eight experts, so every layer routes to
someone absent:

    0   8/128  residency   routing selected expert 24, which is not resident in
                           layer 0 bank 0 (this bank holds 8 of 128 experts)
    …
    residency   30 layer(s)
    SWEEP: no layer indicts the execution.

Thirty refusals, and the run still exits zero. That is the point: the route was
correct, the expert exists in the model, and a shard that does not hold it is
behaving as designed. Counting these as mismatches would make the headline
number a measurement of slice coverage rather than of execution.

Which is also why the principal run binds the **full population** — free,
because Q4_K regions bind from the mapped bytes and nothing is materialised.
The single-layer harness binds only the selected experts, which is a legitimate
shard but the wrong default for a sweep.

## The oracle cannot ride along, and should not

The dequantised f32 leg costs ~24 MB per expert — ~3 GB per layer at full
population, ~90 GB across the sweep. It also answers a different question:
quantisation fidelity, which is a sample, where the sweep answers
implementation parity, which needs coverage. So it is `--oracle-layer N`, over
that layer's selected experts only, and it reports 1.98% relative on layer 5 —
unchanged from b2b94df, as it should be.

## A reporting defect caught before it shipped

The per-layer detail column first read "weakest at: block" for every layer,
because it flagged anything short of `Exact` and the block comparison is
unasserted by construction. That is a column that says the same thing for a
perfect run and a broken one. It now flags each checkpoint against its own
*strongest attainable* outcome — `Exact` for the four asserted ones,
`ObservedExact` for the block — and is empty when nothing is below ceiling.

## What the sweep did not find

None of the irregularities the single-layer result left open: every layer is
top-8 of 128, GeluTanh, 704→768 padded, one routed bank, same physical
geometry. Gemma is uniform. That is a fact about this model rather than a
property of the binding, and the next model in the conformance envelope is
where the classification actually earns its keep.

## Scope

One token, CPU, MoE layers only. Not begun here: free propagation (each path
carrying its own residual, which proves composition and needs the first
*differing* boundary reported separately from the earliest *causal* one), final
norm and logits, and multi-token decode parity.

larql-vindex 2057 lib tests, 0 clippy warnings, coverage policy passes at 94.05%
with verdict.rs at 100%.
Both paths start from the same embedding and each carries its own residual
through all thirty blocks. Every boundary agrees.

    seam        in-process via trait vs default branch  BIT-IDENTICAL
    boundaries  30 MoE layers observed on each path
                first differing boundary   none
                earliest causal operation  none
    final       free-propagated hidden     BIT-IDENTICAL

The locked-input sweep (23e7621) proved every layer independently but handed
each one the incumbent's own activation, so it could not prove they compose.
This lets each path diverge if it is going to. It does not.

## The seam is now a trait, and that is a production change

`moe_ffn_block_cpu` took `Option<&RemoteMoeBackend>` — a concrete type, so a
third route meant a second optional parameter and a rule that at most one may
be `Some`. That rule is not expressible in the type, which makes it a rule
someone eventually breaks, with a layer's expert contribution coming from a
route nobody chose.

    trait MoeExpertBackend        one route, or the in-process default
      RemoteMoeBackend            experts fetched from shards
      BoundMoeBackend             a VINDEX3 BoundMoeOperation
      InProcessMoeBackend         cpu_moe_forward, made explicit

Backends derive their own operands from `weights` and a layer. The remote route
needs the router only; the bound route needs the whole layer. Preparing the
union at the call site would make the block loop know what each route reads,
which is the coupling the trait removes — and it is why the
`build_moe_router_weights` call moved out of the loop and into the remote impl.

## The seam is falsifiable, and it is checked first

`InProcessMoeBackend` must equal running with no backend at all. If it did not,
the trait would have changed the model rather than relocating a call, and every
comparison built on it would be measuring that change. The harness asserts this
before it trusts anything else, and it comes out bit-identical.

That check is the reason to route the incumbent through the trait at all — it
also gives both paths the same observable seam, so neither has to be
reconstructed to be compared.

## Two locations, never conflated

    first_differing_boundary   where a difference is first observable
    earliest_causal_operation  the first MoE that differed on identical input

Once layer 7's contribution differs, layer 8 reads a contaminated residual and
everything downstream differs — including its post-attention state, which would
otherwise read as an attention defect. The causal test is therefore
conditional: a layer indicts its own MoE only if its input residual was still
identical when it ran. Both come out `none` here, but the distinction is the
part that will matter on the first model that fails.

## Coverage: honest debt, not compliance

`moe_bound.rs` is at 65.8% and `moe_backend.rs` at 0%. Neither meets the 90%
floor, and neither is in `larql-inference`'s coverage policy scope, so nothing
gates them.

`run_layer` was split out of `forward_moe_seq` specifically to make the
execution reachable from a `MoeLayerWeights` alone — that part is tested,
including a unit-scale bit-identity check against `cpu_moe_forward`, per-position
routing, the reference pairing refusing a Q4_K store, and the unmappable-codec
tags. What remains uncovered needs a loaded `ModelWeights`, which a unit test
cannot construct cheaply. Recording that here rather than adding a baseline at
the measured value, which would gate at a floor low enough to mean nothing.

## Scope

One token, prefill only, CPU. Not begun here: final norm, logits and greedy-token
parity; multi-token decode; and the first genuinely irregular model, where the
refusal and arrangement machinery is actually exercised — Gemma is uniform
across all thirty layers, so the classification added in 23e7621 has still only
met synthetic irregularity.

larql-inference 1431 lib tests, larql-vindex 2057, workspace clippy clean,
larql-vindex coverage policy passes at 94.05%.
    moe_backend.rs   0.0%  → 98.91%  (91/92)
    moe_bound.rs    65.8%  → 96.95%  (318/328)

Both are now in `larql-inference`'s coverage policy, so they are gated at the
90% default rather than sitting outside its scope.

## The 65.8% was partly a measurement artefact

`moe_bound.rs` reported 65.8%, then 72.6% after tests that plainly exercised
the uncovered code. The missing-lines list pointed at doc comments, which
cannot be uncovered — the profdata was stale, and `cargo fmt` had shifted every
line under it. A clean `cargo llvm-cov clean --workspace` moved the same code
from 318/438 to 318/328: identical covered count, 110 phantom lines gone.

Worth recording because the failure mode is quiet. A stale profile does not
error; it reports a plausible number against the wrong line map, and the
natural response is to write tests for code that is already covered. The
guard is the one that caught it here: if the uncovered list names a line that
cannot execute, the measurement is wrong before the code is.

## What the tests actually pin

`InProcessMoeBackend` is byte-identical to the block loop's default branch,
per position, against `cpu_moe_forward` on the synthetic Gemma fixture — the
same property the free-propagation harness checks against the real model, now
also checked without one.

`BoundMoeBackend` over a loaded `ModelWeights`: the wrapper resolves the layer,
binds its whole population and runs every position, agreeing with the
in-process route within a summation-order band. The fixture stores BF16
experts, which makes it the useful case rather than a weaker one — the
reference kernel decodes them, and the production Q4_K pairing must *refuse*
them. That refusal is asserted to arrive as `RefusalKind::Unsupported`, so a
wrapped error still carries the classification `verdict.rs` added.

Both routes contribute zeros for a layer with no expert weights. The block loop
adds the contribution unconditionally, so a backend that errored there would
change the model rather than the route.

## Not closed

`moe_remote/backend.rs` sits at 10.8%, unchanged and pre-existing — this commit
only appended a trait impl to it. It is not in the policy's scope and this
commit does not add it, because bringing it to the floor is a piece of work
about the remote path rather than about VINDEX3.

larql-inference 1439 lib tests, clippy clean on the touched files, both coverage
policies pass.
The free-propagation harness now runs to the model's own output.

    pre-final-norm hidden      BIT-IDENTICAL
    post-final-norm hidden     BIT-IDENTICAL
    top-k token ids            IDENTICAL
    top-k scores               BIT-IDENTICAL
    argmax token               " capital" vs " capital"
    argmax margin              9.483586e-1 vs 9.483586e-1

With the composition rungs above it unchanged: the seam is bit-identical to the
default branch, no boundary differs across 30 MoE layers, and no MoE differed on
an identical input.

## Production functions, not a local endpoint

`apply_norm` is the one `logits_to_predictions` itself calls, and the predictor
is the one generation calls. A harness that rolled its own final norm or lm_head
would be comparing the harness — the same reason the MoE comparison binds
`larql-compute`'s kernels rather than reimplementing them.

## The margin is recorded even though it is implied

Identical scores make an identical margin a tautology here. It is reported
anyway because the approximate and partially-resident runs that come next need
this exact report shape, and a field that only appears once something disagrees
is a field nobody can compare against.

## What the token is and is not

The harness encodes the raw prompt, where the CLI applies the chat template — so
the argmax is " capital" rather than the " Paris" a templated run produces. That
is a property of the prompt this harness feeds, not of either execution path.
Both paths receive byte-identical input, which is the only thing the comparison
claims. The token is a parity observable, not a quality claim.

## Scope

One token, prefill only, CPU. Not begun here: multi-token decode with KV reuse,
which is what "VINDEX3 runs Gemma normally" would require, and selective page
residency.

Also not done, and named rather than half-built: the stale-profile guard. The
65.8%-that-was-really-96.9% in 0465978 came from a coverage profile whose line
map predated a reformat. CI is immune — it runs on a fresh checkout — so this is
a local-development hazard, and the summary-only policy script cannot see the
signature that gives it away. Worth solving deliberately rather than by bolting
a `clean` onto a command that does not currently need one.
`MoeFfn { weights, moe: &dyn MoeExpertBackend }` — the `FfnBackend` a KvEngine
drives per MoE layer, over whichever expert route is installed.

## The seam stopped one level short

The composition rung made the *block loop's* seam a trait, and proved a VINDEX3
bound route composes through 30 layers to a bit-identical token. But that ran on
the full-recompute path. The engine's adapter — `RemoteMoeFfn`, which is what
`generate_with_engine_resident` actually drives — still named one concrete
backend in its type:

    pub struct RemoteMoeFfn<'a> {
        pub weights: &'a ModelWeights,
        pub remote:  &'a RemoteMoeBackend,   // <- concrete
    }

So the bound route was reachable from prefill and not from decode. Nothing was
wrong; the generalisation simply had not been carried up.

`RemoteMoeFfn` now delegates rather than being replaced. It keeps its
`{ weights, remote }` literal — load-bearing at a CLI call site and in two
roadmaps — and keeps reporting `"remote-moe"`, while `MoeFfn` reports whichever
route it carries.

## What the tests pin

`the_remote_adapter_still_equals_the_general_one` is the same property the block
loop's seam had to prove: same weights, same input, same output, so the
delegation relocates the call and nothing else. The name is the one deliberate
difference.

`a_bound_route_can_drive_the_engine_adapter` is the point of the change — a
VINDEX3 route producing a finite, non-zero contribution through the engine's
FFN seam, which the remote-typed adapter made impossible to express.

`a_refusing_route_still_returns_the_dense_half` pins the failure shape.
`moe_ffn_block_cpu` logs and leaves `h2` zero when a route refuses, so a refused
expert contribution is *not* the same output as one that ran. Without this a
refusal would look like a plausible layer rather than a missing one.

## Not done, and deliberately not half-done

Multi-token decode parity itself. The harness needs resident f32 dequantisation
of every layer, a built `EngineKind`, `generate_with_engine_resident`, and
`larql-kv` as a dev-dependency of the harness crate. That is a known shape
rather than an unknown one — the CLI does exactly this at
`run_cmd.rs`'s engine branch — but it is more than a careful hour, and a
half-driven engine would produce a comparison nobody should trust.

This commit removes the blocker; the run is the next rung.

`ffn_adapter.rs` is at 82.3%, below the 90% floor and not in the policy's
scope. The uncovered remainder is the pre-existing dense-fallback surface, not
the new adapter. Flagging rather than gating at a floor that would lock in the
gap.

larql-inference 1442 lib tests, clippy clean on the touched file.
Tests whether the real routed expert set is necessary and sufficient
for identical MoE execution, using the runtime's existing
ExecutionError::SelectedExpertNotResident mechanism against a real
Gemma 4 26B-A4B MoE layer and real captured routing decisions (not the
4-expert synthetic fixture in runtime/tests/placement.rs).

Four-rung ladder, all passing across 5 layers spanning early/mid/late
depth (0, 5, 10, 20, 29):
  1. full (128) vs minimal (8 routed) population -> bit-identical (sufficiency)
  2. remove one routed expert -> SelectedExpertNotResident, correct
     {expert, resident, population} metadata, no fallback (necessity)
  3. same removed expert, padded back to full byte capacity with an
     unselected substitute -> still refused (capacity is not residency)
  4. exact missing expert restored -> exact parity restored

Reframes MAP-3's original design: literal OS-level page eviction can't
cause a catchable execution failure (mmap transparently re-faults from
disk), so this tests the real, working analogue -- logical operand
presence in the bound plan, independent of physical location.

Registered as chuk-experiments map-3a (confirmed).
…pproximation

    MoeFfn::best_effort(..)   log, contribute zeros, return the dense half
    MoeFfn::strict(..)        record the refusal; the caller must consult it

## The behaviour this constrains

`moe_ffn_block_cpu` logs a route's error, leaves the expert contribution at
zero and returns the dense half. For best-effort remote inference that is
correct — a degraded continuation beats no continuation when a shard is
unreachable. For a correctness experiment it is the exact failure to avoid:

    SelectedExpertNotResident
      → swallowed and logged
      → dense half returned
      → plausible, numerically wrong continuation

A selected-but-absent expert must not silently become an FFN-skipping
approximation.

## Taken before the decode run rather than after

The ladder had this third, behind teacher-forced and free-running decode. It
moves first because it is a prerequisite for the decode rung's *diagnosis*, not
only for MAP-3A: a swallowed refusal reaches the comparison as a numeric
difference, so the divergence report would name a `NumericMismatch` where the
truth is `Residency`. That is precisely the misdiagnosis `runtime/verdict.rs`
was written to prevent, reappearing one layer up.

## Caught on the way in, not on the way out

`FfnBackend::forward_moe_full_layer` returns `Option<Array2<f32>>` — no error
channel — and widening it would reach every engine. So the refusal is
intercepted *before* `moe_ffn_block_cpu` handles it: `MoeFfn` wraps its route in
a `RefusalRecorder` that implements `MoeExpertBackend`, records, and passes the
error through unchanged. No signature moves and no engine changes.

Strictness lives in the constructor rather than a settable field, and the check
is `refusal()` / `all_experts_executed()` rather than a log line someone has to
read. The recorded refusal keeps its `RefusalKind` rather than a flattened
string, because a gate that could not tell `Residency` from `Unsupported` could
not tell a sharded deployment from a broken one.

Only the first refusal is kept. Later layers usually repeat the same cause, so
the earliest is the diagnosis — the same first-versus-causal distinction the
divergence report makes.

## What strict does *not* yet do

It does not stop the block. `FfnBackend` cannot say "no answer", so a strict
adapter still returns the dense half and relies on the caller consulting
`refusal()` before accepting logits. That is a contract, not a type-level
guarantee, and it is the weaker of the two. Making it unrepresentable means
giving `forward_moe_full_layer` an error channel, which is an engine-wide
change and deserves its own rung rather than riding on this one.

`RemoteMoeFfn` stays best-effort, pinned by a test: its callers depend on
degrading rather than stopping.

## Coverage

`ffn_adapter.rs` 82.3% → 88.2%. Still under the floor and still not gated; the
remainder is the pre-existing dense-fallback surface rather than anything added
here.

larql-inference 1447 lib tests, clippy clean on the touched file.
The real `KvEngine` decode loop, two expert routes, one model.

    == stage 1: teacher-forced ==
      prefill                exact
      step 1  fed " capital"   exact | argmax " of"     ==
      step 2  fed " of"        exact | argmax " France" ==
      step 3  fed " France"    exact | argmax " is"     ==
      step 4  fed " is"        exact | argmax " capital"==
      step 5  fed " capital"   exact | argmax " of"     ==
      step 6  fed " of"        exact | argmax " France" ==

      first differing boundary   none
      earliest causal step       none

    == stage 2: free-running greedy ==
      6/6 steps identical token and identical margin
      in-process   capital of France is capital of
      bound        capital of France is capital of

This is the condition the bound route had never met. Every prior rung ran on the
full-recompute prefill path, one token, no KV reuse — and `BoundMoeBackend` binds
per call against weights borrowed from the map, which decode is the thing that
stresses.

## Teacher-forced first, because free-running cannot localise

If step 3's hidden state differs and step 4 is then fed a different token,
step 4's difference says nothing about step 4 — the comparison has branched and
every later number is about the branch. Feeding both engines an identical token
sequence keeps each step an independent measurement, which is what the
locked-input sweep did for layers, one dimension over.

Under teacher-forcing the in-process route chooses and both engines are fed its
choice. The bound route's own preference is computed and compared but never
acted on, so a divergence is recorded without being allowed to propagate.

Stage 2 runs only because stage 1 passed, and it is what moves the claim from
"equivalent under controlled input" to "generates the same text".

## Refusal is checked before any number

Both routes are driven through `MoeFfn::strict`, and `classify` consults
`refusal()` *before* computing a difference. Without that ordering a missing
operand would arrive as a numeric mismatch and be reported as a wrong answer —
the misdiagnosis `runtime/verdict.rs` exists to prevent, and the reason
2067f5b was taken ahead of this rung rather than after it.

A step that refused is classified and stops the comparison. Only a step where
both routes executed makes a numeric verdict meaningful.

## What the tokens are and are not

The harness encodes the raw prompt where the CLI applies a chat template, so the
continuation loops — " capital of France is capital of". Both paths produce it
identically, which is the only thing the comparison claims. The margins are
0.948 to 0.9999, so no step sat near a tie: this is parity decided by the
scores, not by tie handling.

## Scope

Greedy only, CPU, `EngineKind::Standard`, one prompt. Not covered: sampling,
the windowed and boundary-KV engines, long contexts where the cache evicts, and
Metal. Nor the *lifecycle* question — the bound plan is rebuilt per call, and
whether retaining it per layer is the production shape is a performance question
this rung deliberately does not answer, having proved correctness first.

`larql-kv` is now a dev-dependency of `larql-vindex`. Dev-only: nothing in the
library depends on the engine layer.

larql-vindex 2057 lib tests, clippy clean on the new harness.
`larql-execution` — `RefusalKind`, `ExecutionRefusal`, `BoxRefusal`. Zero
larql-* dependencies, zero dependencies at all.

## A dependency cycle pointed at a missing layer

Widening `FfnBackend::forward_moe_full_layer` to a `Result` needs the error to
carry its classification. It could not:

    FfnBackend      in larql-compute   (deps: larql-models)
    RefusalKind     in larql-vindex    (deps: larql-compute, ...)

The arrow points the wrong way, and larql-compute cannot gain a dependency on
larql-vindex without a cycle. So the vocabulary belonged to neither crate — it
is a cross-runtime execution contract, and the cycle was the diagnosis rather
than an obstacle to route around.

The alternatives were worse. A boundary trait returning `kind() -> &'static str`
solves Cargo and reintroduces the semantics problem: "residency",
"not-resident", "missing-expert" all become expressible, and exhaustive matching
is gone. Moving it into `larql-models` would put runtime semantics in a crate
about model structure. Leaving it audited keeps invalid output representable.

Not speculative extraction: `larql-vindex-spec` set the precedent — a
dependency-light contract crate with four consumers — and this has three on day
one.

## The axis is the response, not the cause

Three variants because there are three distinct responses, each with a different
person on the other end:

    Residency       same operation, operand becomes available  -> may succeed
    Unsupported     same operands, another capable executor    -> may succeed
    BindingDefect   the binding or artifact itself must change

An earlier proposal split `Unsupported` into `DecomposedProjection`,
`Activation` and `Arrangement` and dropped `BindingDefect`. Both halves of that
are wrong. Those three are *causes of* unsupported-ness — they already live in
`OperandUnsuitability`, with their own detail — and promoting them puts two
abstraction levels in one enum, leaving a caller unable to switch on the thing
it must act on. And `BindingDefect` is the only kind meaning *reject the
artifact*: `ExpertOutOfRange`, `ShortRegion` and inconsistent plans are fixed by
neither fetching an operand nor choosing another kernel.

The invariant, documented on the type so a future variant has a test to meet:

  > `BindingDefect` means repeating the operation with more residency, or
  > through any other capable executor, cannot make *this* bound plan valid.

`the_two_predicates_partition_the_vocabulary` pins it — every kind must be on
exactly one side of `indicts_the_artifact` / `is_recoverable_without_rebinding`,
so a variant that is neither or both fails.

## Transport failures are deliberately not a fourth kind

A timeout, a refused connection, an expired lease are execution-*attempt*
failures, often retryable, and they belong to whatever transport owns them. One
may *conclude* in `Residency` when the semantic finding is that the operand is
unavailable to this plan. Admitting them directly would turn the enum from
execution semantics into a catalogue of operational failures.

## Concrete errors stay where they belong

`ExecutionError` implements the trait by delegating to its existing `refusal()`
rather than re-matching — two exhaustive matches over the same variants is
exactly how a classification drifts. `runtime::RefusalKind` still resolves,
re-exported, so no call site moves.

`larql-execution` 100% line coverage, 6 tests; larql-vindex 2057, unchanged.
The `FfnBackend` widening this unblocks is the next rung.
`ExecutionRefusal::kind` delegates to `ExecutionError::refusal` precisely so the
two cannot disagree — but a delegation nothing calls is a delegation nobody has
checked, and extracting the vocabulary in f5c6637 dropped `error.rs` from 100%
to 66.67% for exactly that reason. Caught by the coverage gate, not by review.

Two tests. The first walks every `ExecutionError` variant through both paths and
asserts they agree. The second boxes one as `BoxRefusal` and checks that both
levels survive the crossing the trait exists for — the response category
(`Residency`, not indicting the artifact) *and* the concrete diagnosis (expert
90, 8 of 128), rather than the detail being flattened into the category.

larql-vindex coverage policy back to passing at 94.05%.
    fn forward_moe_full_layer(..) -> Result<Option<Array2<f32>>, BoxRefusal>

Six implementations, four call sites, three crates. The classification now
crosses the boundary that most needed it — the one between a route that refused
and an engine deciding what to do about it — which is what extracting
`larql-execution` in f5c6637 was for.

## Three outcomes, never two

    Ok(Some(h_out))   the layer executed
    Ok(None)          this backend does not serve this layer; fall back
    Err(refusal)      a required operation could not execute

`Ok(None)` must never report a refusal. It means *not applicable* — the caller
runs its own dispatch and the result is still correct. `Err` means a routed
operation was required and did not happen, so anything the caller assembles is
incomplete. Conflating them puts the caller back where the error channel was
added to rescue it from, and the trait doc says so.

Pinned at the two places the distinction is easiest to lose: a sharded backend
with no shard for a layer, and a v0 backend with no MoE hook, both assert
`Ok(None)` rather than merely "not an error".

## The backend reports; the caller decides

`MoeFfn` previously chose *how failure was communicated* — best-effort swallowed,
strict recorded for later inspection. Now it reports, and policy sits at the
call site:

    Strict       Err propagates; the dense half is computed and discarded
    BestEffort   logs the degradation explicitly, returns the dense half

The discard is deliberate. `moe_ffn_block_cpu` has already logged and zeroed by
the time the recorder is consulted, so `out` is a dense half wearing the shape
of an answer; under `Strict` it must not escape. That costs one wasted dense
pass on an error path and buys the guarantee no caller can consume a partial
layer — the difference between this and the audited contract it replaces, where
the caller had to remember to ask.

`RecordedRefusal` is now an error in its own right, so it crosses as a
`BoxRefusal` carrying both levels: the category an engine switches on and the
concrete message the route produced.

## Transport failures were not promoted

The HTTP walk backend returned `None` on every transport failure, meaning "fall
back". It still does. A timeout or refused connection is an execution-*attempt*
failure, often retryable, and it is not that layer's place to decide it has
become a semantic refusal. Promoting them under cover of a signature migration
would have changed the remote walk path's behaviour silently.

## Where propagation still stops

`ffn_or_moe_layer` (both copies) and the remote-FFN recompute loop return bare
arrays, so a refusal is *named* — with its `RefusalKind` — and the caller falls
back. A strict route reaching those paths still degrades like a best-effort one.
Widening them into the engine step is the next ring; the comment at each site
says so rather than leaving it to be discovered.

So this rung delivers the channel and the policy split, not yet the end-to-end
guarantee.

## Verification

Full Gemma ladder re-run after the migration, to show that widening the error
channel changed no successful computation:

    layer sweep        no layer indicts the execution
    free propagation   composition and predictions agree
    MAP-3A             CONFIRMED, 4/4 rungs
    decode parity      identical teacher-forced and free-running
    larql run          "Paris"

Workspace 10165 tests, 0 failures, 0 clippy errors. Both coverage policies pass.
The FfnBackend ring (24a267c) gave a refusal a typed channel but stopped one
level short: `ffn_or_moe_layer` returned a bare array, so a strict route
reaching it degraded exactly like a best-effort one. This widens the six
dispatch helpers and terminates the channel at the engine.

## Three outcomes, all the way out

    DispatchOutcome<T> = Result<Option<T>, BoxRefusal>

        Ok(Some(_))   the dispatch produced a complete result
        Ok(None)      nothing to do, or the backend declined this shape
        Err(refusal)  a routed operation was required and did not execute

`Ok(None)` carries exactly what a bare `None` used to, so a declining backend
still becomes `BackendFailure`. `Err` is the channel that did not exist.

## Transactional decode

A decode step mutates before it can know whether it will finish: each layer's
attention appends the token's K/V, and only then can the FFN refuse. So a step
that does not complete rewinds every handle to its entry length, via a new
`KvDispatch::truncate_kv` — the inverse of an append, where `clip_kv` keeps the
tail this keeps the head. Its default answers `false` rather than panicking,
because "this backend cannot rewind" is a state to handle, not a bug.

Windowed caches are the subtle case: append-then-evict leaves the row *count*
unchanged while the oldest row is gone, so length cannot detect it.
`rewind_is_sound` tests whether every layer had room before the step began.

Where the rewind cannot be trusted the engine says so — `StateInvalidated`
wraps the cause, the engine refuses further decode, and `prefill` clears it.

## The API said two incompatible things

`is_recoverable()` answered "could this operation succeed?" while callers read
it as "can I retry?". A Residency refusal that invalidated the cache is
recoverable in the first sense and catastrophic in the second. Now:

    operation_is_recoverable()   could this operation ever succeed?
    engine_state_is_retryable()  is this engine instance still usable?
    is_recoverable()             both — what a harness may act on

`EngineError` loses Clone/PartialEq/Eq: it carries the refusal itself so the
classification and the concrete error reach a handler together.

`ScoreOutcome` gains three variants rather than one. A BindingDefect is not a
coverage deficit, and a dead engine is not a gap in a run.

## Scope, stated honestly

This covers `StandardEngine`. The five engines routing through larql-kv's
`layer_ffn_or_moe` still swallow refusals; widening them is the next ring and
the gate suite's module doc names them.

## Verification

24 gate cases (8 entry points x 3 RefusalKind) assert Err with the original
kind and no output; a retry after a rewound refusal is bit-identical to never
having refused. Gemma ladder 10/10. Coverage policy passes at 96.06%.

`kv_engine.rs` (1300 lines) split into kv_engine/{error,info,stages,kv,
retrieval,any,test_stubs}.rs — structure only, largest file now 343.
Every VINDEX3 parity result so far bound its operands out of a VINDEX2 file —
`index.json.version` was 2 at every step, on every model. What was proven was
the runtime:

    proven      the VINDEX3 executor, fed VINDEX2 operands, matches production
    not proven  a VINDEX3 container can be written, opened, bound and executed

Nothing could write one. `ContainerGeneration::V3` appeared in exactly one
test, constructed from a hand-written JSON string.

## The two ends

The middle of the stack was already built — LYRW v2 tables, the programme
manifest, capability and authority, the bound executor. What was missing was
the two ends, and an end nothing exercises is an end nobody has debugged.

    <root>/
    ├── index.json           schema 3 — sole root authority (§12)
    ├── moe_manifest.json    programme description (§8)
    └── routed/layer_000.lyrw  LYRW v2 bank (§6)

No transcoding in the assembler: a container writer that also converted formats
would be the silent conversion §9.1 forbids, one layer below where anyone would
look for it.

## Fixture A, and what it proves

Two arms execute identical arithmetic on identical weights; only the source
differs — in-memory buffer vs a container resolved through index.json v3 →
moe_manifest.json → LYRW v2 region table. Output is bit-identical.

Storage fidelity and semantic fidelity are asserted separately, so a failure
localises: one test says the container stored the right bytes, the other says
the binding read them right. A single assertion cannot distinguish those.

Fused and decomposed FC1 storage agree under one programme id. `gated-mlp-v1`
declares both legal, so the manifest identifies *semantics* while the region
layout supplies one valid physical implementation — the separation K3 needs
when packing, quantisation, banking and transforms all vary underneath a fixed
logical contract.

Expert count and top-K come from the container, not a constant: (8,2), (32,4)
and (5,1) run through one code path. The shared-bank axis of that row stays
open — `BoundMoeOperation::banks` documents unrouted shared banks as arriving
with the Mini-K3 rung, so there is nothing to test against yet.

## verify means something for a VINDEX3 container

A VINDEX2 container is opaque blobs, so verifying it means checksums. A
VINDEX3 container declares its own structure, so it can answer *will this
bind?* without executing: index parses, manifest validates, storage keys
resolve, segments parse, roles satisfy the programme, regions are in bounds.
Defects carry {layer, bank, role, segment}. Execution parity is deliberately
excluded — it would make routine verification cost a forward pass.

## CLI

`show` and `verify` dispatch on `detect_generation` and let each generation
describe itself. VINDEX3 is not normalised into a VINDEX2-shaped summary:
programme id, storage key, manifest validity and bindability have no VINDEX2
equivalent and are what a binding failure is diagnosed from.

Gate status: V2-0 and V2-1 close for the rows fixture A can carry. Variant
selection refusal, shared banks and WALK/DESCRIBE parity remain open, so
`extract` still writes VINDEX2 and the ABI is not frozen.
`vindex2-format-spec.md` was titled VINDEX3 and versioned 2.0-draft-2, which
put the same off-by-one between name and discriminator that `generation.rs`
already records having fixed once in code.

    index.json.version 1,2 → VINDEX2      index.json.version 3 → VINDEX3

Renamed to vindex3-format-spec.md / vindex3-experiments.md, version header to
3.0-draft-2, and two stale generation references updated (the adjacent bullet
already said "requires VINDEX3 loader").

Three things keep a 2 on purpose and now say so in the spec: the pre-registered
`V2-x` gate ids, the `vindex2` registry programme — both external keys with
results recorded against them — and `lyrw2`/`FORMAT_VERSION = 2`, which is the
bank format's own version on a different axis from the container's.

Records the container result and per-row gate status in the experiments
programme, adds the refusal/retry contract to larql-kv's state policy, and
notes container generations in the README where the vindex layout is described.

Also clears the larql-cli clippy backlog its CI comment estimated at ~82 errors
(actually 6) plus 3 in examples, so `clippy --workspace --all-targets -D
warnings` is now clean. Two unconstructed enum rungs are kept with a stated
reason rather than deleted: they are modelled vocabulary, and deleting an
unconstructed rung is how a ladder collapses back into the coarse form that
caused the wrong MXFP4 claim.
Edits made after the last fmt pass; caught by larql-vindex's format gate.
`latent_mask.rs` sat at 66.43% against the crate's 90% floor — the pure
selection logic was well covered, the accumulation and dump paths were not.
CI's coverage gate caught it; it predates this branch's other work.

Four tests for `record_stats`/`dump_stats`: the no-path early return, a
survival count reaching the dumped file, a short row growing to fit a wider
mask without losing what it had already accumulated, and the sampled costats
matrix needing its own switch.

`STATS`/`COSTATS` are process-global while `set_env_override` is
thread-local, so these assert structure — the file exists, parses, and carries
our layer with a non-zero count — rather than exact totals, which a parallel
test's accumulation would perturb.
Five items, ordered by what unblocks what, each with the condition that closes
it so "done" is not a judgement call.

The first is the one that constrains the baseline tag: strict refusal is real
for StandardEngine only, and the five engines routing through larql-kv's
`layer_ffn_or_moe` still degrade. Records that the hard half is not the ten
call sites but the per-engine rewind-or-invalidate decision — each engine holds
its own continuation state, so what can be undone differs by what it treats as
canonical.

Also records the continuation-state intervention harness as adjacent work, with
the two things PR #197 established that it should inherit rather than
rediscover: capability-on-the-trait with an unsupported default (truncate_kv),
and the sliding-window trap where a length-based checkpoint is not a checkpoint.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant