# #159 — registry + doc draft (authored 2026-08-22 UTC, NOT yet landed)

Registry-first per AGENTS.md rule 3. This is the paste-ready FIRST commit of lane `fix/159-…`,
once #213 is gated and the lane branches off its tip.

**Status: mint, id, (3)-clause shape, int-deferral, and all three doc amendments APPROVED by
doyle (2026-08-22).** Section 3b is RESOLVED and already landed elsewhere. Dates in the registry
are UTC (doyle 2026-08-22) — the earlier 08-21 stamps are the same evening in PDT, kept because
they date real rulings and rewriting them would move a ruling off its instant.

**Everything in sections 1–3 lands in ONE commit with the parameter threading** — doyle's
disposition on 3.3, and it generalises to all of it: those sentences are TRUE today and go false
at the exact instant the parameter lands, not one commit sooner. Together there is no false
window; separated, every way they could be separated is a way someone would create one.

---

## 1. MINT — new requirement

```toml
[[requirements]]
id = "REQ-ADAPTER-FLOOR-VS-STAGED-CORE"
title = "IN A COMPOSITE UPDATE, AN ADAPTER'S CORE-VERSION FLOOR IS JUDGED AGAINST THE CORE THAT RUN WILL ACTIVATE, NOT THE ONE CURRENTLY RUNNING (releases#159, doyle ruling 2026-08-21). THIS DELIVERS A CONTRACT ALREADY RULED AND NEVER SATISFIED, which is what the lane is FOR: CONTEXT.md's adapter-manifest-header entry has said since the glossary was written that `when spt-core self-updates, re-verify adapters still satisfy (coordinate core + adapter updates when needed)` — the code never did. Same shape as F-5 itself (REQ-ADAPTER-FLOOR-ENFORCE), where the floor was PARSED and REQUIRED and never compared: a dead rule, not a missing one. THE DEFECT: `spt update` plans FetchApply -> Adapters, and `spt update --restart` plans Fetch -> Adapters -> Finish; in BOTH the adapters leg runs while the NEW core is staged and the invoking CLI is still the OLD image, so a floor comparison against `env!(CARGO_PKG_VERSION)` asks a question about a core that is on its way out. An adapter whose floor the INCOMING core satisfies is therefore refused in-cycle, and the run then activates that very core and leaves the OLD adapter sitting on it — the refusal protected nothing, it deferred the pairing it was meant to enforce, ungated, into a state nobody checks again. FIX: `cmd_adapter_update` takes the comparison version as a PARAMETER instead of baking `env!`; the composite leg passes the STAGED release's `product_version`, read from the VERIFIED signed metadata via `ReleaseCache::staged_update` (`UpdateSetMetadata` / `ReleaseMetadata`). SINGLE-PASS, NOT A RE-SWEEP (doyle): a post-finish second sweep can itself fail half-rolled, so the one pass judges against the truth it already holds — and the CONTEXT.md clause above is amended to state that cadence, because a glossary sentence whose cadence a future implementer could build against would authorize the refused re-sweep with the glossary's own authority behind it. BOTH COMPARISON SITES INSIDE THAT FUNCTION TAKE THE PARAMETER (doyle confirmation 2026-08-22, dated to the confirmation and not to the lane: the correction postdates the ruled shape it corrects) — (a) the pre-swap peek `staged_floor_ok`, and (b) the post-swap re-register, which moves from `registry::register` (the `env!`-baking wrapper) to the already-public `registry::register_with_core` seam beneath it. (b) IS LOAD-BEARING, NOT TIDINESS: fixing only (a) admits the adapter at the peek, crc-swaps the new bits into the live pointer-mode home, and THEN refuses at the record against the old running core — live files new, registry record old, reported as FAILED. That is the record-and-reality disagreement REQ-ADAPTER-FLOOR-ENFORCE's own pre-swap-peek bind exists to prevent, reached from the other side, and it is strictly worse than the bug this requirement fixes (today's refusal is clean and byte-untouched). One value, two consumers, SO THE PEEK AND THE RECORD CANNOT DISAGREE ABOUT WHICH CORE THEY JUDGED AGAINST — that property is what makes this a fix rather than a second gate to keep in sync, and it is why the parameter is THREADED rather than each site reading the staged metadata for itself. THE BARE VERB IS UNCHANGED: `spt adapter update` (cli.rs AdapterCmd::Update) and its `spt update adapters` alias keep passing the running CLI's version — with no update in flight the installed core IS the right question — and both call sites are pinned as unchanged. `spt adapter add` is untouched: no staged core exists on that path. FAIL OPEN, ON THE NEW INPUT ONLY (doyle): an unreadable staged version degrades LOUDLY to the running-CLI comparison — an absent answer is not a zero, and an empty string is never compared against a floor — while `staged_floor_ok`'s deliberate FAIL-CLOSED arm for an unverifiable STAGED MANIFEST floor (REQ-ADAPTER-FLOOR-ENFORCE) is NOT touched. ONE ARM, TWO DIAGNOSTICS (doyle Q1 ruling): an empty `product_version` (`#[serde(default)]` on both metadata types — pre-v0.3.2 releases really do parse to an empty string) and an absent stage take the SAME degrade but must NOT share a message; the first says the staged release predates the field that carries the semver, the second says there is nothing staged. READING `product_version` HERE DOES NOT VIOLATE ITS OWN WARNING, stated in this title so the next reader meets the warning before they meet the use: that field's doc forbids using it to decide WHICH RELEASE IS NEWER — the monotonic `version` counter is the ordering authority and this requirement does not touch ordering — it does not forbid reading it as the semver a declared floor is compared against, provided it comes from the verified signed metadata, which is the only place this reads it from. Gate: doc — the CONTEXT.md adapter-manifest-header clause restated as the ruled cadence (the sentence this requirement delivers), the CONTEXT.md update-composite entry's floor-gate phrase, and MANIFEST.md's enforcement note, each naming WHICH core the floor is judged against; impl — the comparison-version parameter on `cmd_adapter_update` threaded to both the peek and `register_with_core`, the composite leg's staged-version read off the verified metadata, and the two-diagnostic loud degrade; unit — an adapter flooring on the FETCHED core is ADMITTED by the composite AND its record registers at the new version, the bare verb and its alias still compare against the running CLI, and the empty-`product_version` and absent-stage degrades each render their own diagnostic and fall back rather than refusing."
required_stages = ["doc", "impl", "unit"]  # ACTIVATED TURNKEY W3 lane 2 (todlando build 2026-08-22, releases#159). doc ACTIVATED on doyle's 2026-08-22 catch: the amended CONTEXT.md clause IS the authoritative statement of the ruled cadence, so this requirement depends on a sentence the lane itself writes — landing it untagged would leave that dependency pointing at nothing. int DELIBERATELY NOT ACTIVATED (same ruling): the owed proof is FIELD ACCEPTANCE — one real composite roll on a real box landing new-core AND new-adapter together — owed at the milestone's field verification and gated by no lane, since no rig can simulate it. The same single leg REQ-UPDATE-REFUSAL-EXIT-DISTINCT defers: ONE owed proof, not two.
```

## 2. AMEND BY REPLACEMENT — `REQ-ADAPTER-FLOOR-ENFORCE` (traceable-reqs.toml:2151)

Clause **(2)** stays TRUE as written: `register()` itself is unchanged and still delegates with
`env!(CARGO_PKG_VERSION)`; only the composite's CALL SITE moves to the seam beneath it. Do not
touch it.

Clause **(3)** is UNDER-SPECIFIED, not false — it says the peek "parses its manifest floor" and
never states what that floor is compared against. Keep its fail-closed sentence INTACT and
append:

> **THE COMPARISON BASIS IS THE CALLER'S, NOT THIS FUNCTION'S (releases#159): the bare verb
> judges against the running CLI, and a COMPOSITE update judges against the core that run will
> activate — see REQ-ADAPTER-FLOOR-VS-STAGED-CORE. The FAIL-CLOSED discipline in this clause is
> about an unverifiable STAGED MANIFEST and is unrelated to that input.**

doyle 2026-08-22: keeping the fail-closed sentence intact and appending is what stops the next
reader from "simplifying" one discipline into the other, which is how this class of drift starts.

## 3. Doc surfaces — all three amended BY REPLACEMENT, in the same commit

Census rule as doyle widened it 2026-08-22 after this lane broke his earlier one: **census who
ENFORCES the invariant AND who STATES it.** A false sentence on a CI-drift-gated surface outlives
a wrong line of code, because nobody re-derives prose.

### 3.1 `CONTEXT.md` adapter-manifest-header entry (~line 76) — the sentence this lane delivers

Untagged glossary prose today, but authoritative for MEANING. Replace its closing clause:

> ~~If installed spt-core < the adapter's `min_spt_core_version`, surface the incompatibility
> rather than silently breaking; when spt-core self-updates, re-verify adapters still satisfy
> (coordinate core + adapter updates when needed).~~

with:

> If spt-core is below the adapter's `min_spt_core_version`, surface the incompatibility rather
> than silently breaking. **WHICH spt-core the floor is judged against is the caller's question,
> not the manifest's:** a standalone `spt adapter add` / `spt adapter update` judges against the
> RUNNING core, and a COMPOSITE `spt update` — which stages a new core and updates adapters in
> the same run — judges against **the core that run will activate**. The decision is SINGLE-PASS,
> taken before anything is applied; it is *not* a re-verification swept after the core lands,
> which can itself fail half-rolled and leave the pairing ungated in a state nothing checks
> again. Coordinating core + adapter updates is therefore one decision inside one run, not two
> passes.
>
> `<!-- [doc->REQ-ADAPTER-FLOOR-VS-STAGED-CORE] -->`

**The tag is the whole of doyle's catch:** this clause is the authoritative statement the
requirement rests on, so the requirement carries `doc` and this is what satisfies it. Writing an
authoritative sentence a requirement depends on and leaving nothing pointing at it is the same
omission the lane is fixing, one level up.

### 3.2 `docs/MANIFEST.md:43-46` — carries `[doc->REQ-ADAPTER-FLOOR-ENFORCE]`

Leaving it would land a lane whose own doc-stage evidence contradicts its impl. Replace:

> ~~Both acquisition verbs — `spt adapter add` and `spt adapter update` — REFUSE when the
> installed spt-core is below this version, naming the installed core, the floor, and the next
> action (update spt-core first).~~

with:

> Both acquisition verbs — `spt adapter add` and `spt adapter update` — REFUSE when the spt-core
> being judged is below this version, naming that core, the floor, and the next action (update
> spt-core first). **The core being judged is the RUNNING one for a standalone add or update, and
> the core the run will ACTIVATE under a composite `spt update`** (which stages a new core and
> updates adapters in the same run) — so an adapter whose floor the incoming core satisfies
> installs in that run instead of being refused against a core on its way out.
>
> `<!-- [doc->REQ-ADAPTER-FLOOR-VS-STAGED-CORE] -->`

The existing `[doc->REQ-ADAPTER-FLOOR-ENFORCE]` tag stays: the numeric-compare and
nothing-written-on-refuse sentences it covers are untouched.

### 3.3 `CONTEXT.md:682` update-composite entry — **#213's own doc-stage evidence**

Tagged `[doc->REQ-UPDATE-REFUSAL-EXIT-DISTINCT]`. True today; false the instant the parameter
lands. Replace the phrase in place:

> ~~the floor gate declining an adapter whose `min_spt_core_version` exceeds this core~~

with:

> the floor gate declining an adapter whose `min_spt_core_version` exceeds **the core it is
> judged against** — the running core for the adapters leg alone, the core the run will activate
> under the composite (REQ-ADAPTER-FLOOR-VS-STAGED-CORE)

and add `<!-- [doc->REQ-ADAPTER-FLOOR-VS-STAGED-CORE] -->` beside the existing tags.

Assembly order is unchanged (#213 first, #159 after) and doyle does not need to change the batch:
the sentence is true for exactly as long as the composite compares against the running core.

### Clean — leave alone

docs-site `manifest.md` (60/67/77), `quickstart/adapter.md`, `integration-checklist.md`,
`install-on-demand.md`, `shells/getting-started.md`, and PRD.md R-UPD-5 all describe the floor as
a "compat gate checked before install/update" **without naming what it is compared against**.
Under-specified, not false.

## 3b. RESOLVED — corrected on #213 itself, as a fourth commit (doyle 2026-08-22)

**Landed: `fix/153-update-refusal-exit` @ `f7d26dd5`**, comment-only, one line of
`traceable-reqs.toml`, `traceable-reqs check` 796/796 clean unpiped. NOT this lane's to carry.

doyle's discriminant, kept because the two cases look identical: **ask what made the sentence
false.**

- `CONTEXT.md:682` is TRUE today and goes false at the instant #159's parameter lands. **The lane
  falsifies it, so the lane carries the amendment** — same commit, no window.
- The stage note was falsified by **a RULING already made**. It is false NOW, on a row in the gate
  tree, and stays false whether or not #159 ever branches. Correcting it downstream would make a
  landed row's truth conditional on a lane that is currently cold and unbuilt.

**Where a RULING falsifies a record, the correction belongs at the record, now.**

Second half of the ruling: **the new sentence names no lane at all.** Repointing "#159" at another
issue number would reproduce the defect with a fresher dependency. What was written survives #159
landing, slipping, or being cut.

## 4. Owed at ship, both lanes

Name the deferred field-acceptance leg in the FULFILLMENT for #213 AND #159, so it reaches
deployah in the head's never-executed-cells list. A claim nobody has executed must arrive with the
head, not be discovered at field time.
