Skip to content

docs: land the stranded ADR 0074 capacity corrections, and unstale two trackers - #95

Merged
wshallwshall merged 1 commit into
mainfrom
doc-defects
Jul 31, 2026
Merged

docs: land the stranded ADR 0074 capacity corrections, and unstale two trackers#95
wshallwshall merged 1 commit into
mainfrom
doc-defects

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Three independent documentation defects, each verified against origin/main before editing, each with an adversarial second pass. No code changes — ruff correctly skipped this commit.

1. BACKLOG #96 — stranded capacity corrections, finally landed

ADR 0074's 2026-07-14 validity re-check found three pieces of #96's adopter-facing capacity guidance measured-false and unsafe. The correction was written the same day and never landed — it survives only on bd969b0e, in a ref namespace whose remote no longer exists. B14 named four downstream artifacts; three were amended on 2026-07-14, and #96 was the last one outstanding, so required-change 13 still read as fully open.

Corrected Why
sum-across-interfaces aggregate measured-false, ~11× — 16 lanes summed predicts 960/s against a measured 87/s
"only trustworthy success gate" over-reports 3–5.5× alone; needs the non-filling companion #96 itself already named
poller-zero remedy circular/stats zeroes the field the gate reads, so the failure mode satisfies the gate

Deviations from bd969b0e, each deliberate:

Every figure traces to a repo line — THROUGHPUT-STATUS-2026-07-10.md §4, ADR 0074 B2/B4, THROUGHPUT.md §7. Nothing invented.

2. Three stale demand-gate banners — AMENDED, not closed

Verification refuted a full close on all three, so each gets an AMENDED banner recording exactly what shipped and what did not:

3. Demand-gate tracker — 11 lanes wrongly "Not started"

All eleven shipped. Each verified individually by ADR status plus code symbol. No merge SHA can be cited for any of them — the public history is snapshot-collapsed across the 2026-07-17..24 window, so the evidence is necessarily ADR-plus-symbol. S7a is marked In progress rather than merged (ADR 0137, branch dg-s7a); if you prefer a strictly binary table, that cell is the one to argue with.

Deliberately NOT in this PR

docs/AOAG-DEPLOYMENT.md §4.5. It was on the same worklist — it mandates a planned cluster-resource outage for the RegisterAllProvidersIP=0 workaround, which [store].multi_subnet_failover should make unnecessary. Verification returned PARTIAL and I am not shipping it, because there is a configuration where the outage is still required: a brownfield listener already at RegisterAllProvidersIP = 0. Two further gaps — the setting has an unstated engine-version floor, and the db_lookup connector builds its own ODBC string with no MultiSubnetFailover at all. Telling a DBA they can skip an outage when they cannot is worse than leaving stale text in place, so this needs an owner decision rather than a guess.

Verification

tests/test_backlog_status_check.py — 15 passed, including test_the_real_backlog_satisfies_the_invariant, which the form fails. Plus ledger, runbook, doc-ref and doc-drift suites: 100 passed, 89 skipped. Pre-commit: ledger gate passed, PHI leak guard passed, ruff skipped (no Python).

🤖 Generated with Claude Code

…o trackers

Three independent doc defects, all verified against origin/main before editing.

1. BACKLOG #96 carried three pieces of capacity guidance that ADR 0074's
   2026-07-14 validity re-check found measured-false and unsafe, and the
   correction never landed - it sat only on a stranded commit (bd969b0e) in a
   ref namespace whose remote no longer exists. B14 named four downstream
   artifacts; three were amended on 2026-07-14 and #96 was the last one left,
   so ADR 0074 required-change 13 still read as fully outstanding.

   Corrected here: the sum-across-interfaces aggregate rule (measured-false,
   ~11x over-report - 16 lanes summed predicts 960/s against a measured 87/s);
   the "only trustworthy success gate", which over-reports 3-5.5x on its own and
   needs the non-filling companion #96 already named; and the poller-zero
   remedy, which is circular. Adds the BUILD GATED (measurement layer) advisory
   and marks the quick-win/Difficulty-5 score as no longer trustworthy.
   ADR 0074 B14 and required-change 13 are closed out to match.

   Deviations from bd969b0e, each deliberate: it used a leading ⛔ banner, which
   the backlog checker reads as a CLOSED status against #96's OPEN 🔢 banner and
   fails "item contradicts itself" - ⚠️ is the sanctioned non-status advisory
   glyph, per the ⛔+⚠️ stacking that landed in #92. Its "11 open decisions"
   count is unsourced (the Amendment lists 14) and is dropped rather than
   restated. Two of its quotations were inexact and are corrected against the
   file. "zero code" became "only TOML comments, zero code" - the grep returns
   10 TOML hits and no Python.

2. BACKLOG #171, #131 and #127 carried only their pre-build demand-gate banners
   although work has shipped. Each gets an AMENDED banner rather than a close,
   because verification refuted a full close in all three cases: #171's API half
   is built but its console half is dead code (app.js registers the handlers, no
   page emits the attributes, and the /ui routes do not exist); #131 built the
   connection flag and the Flagged-only filter, but "every configuration object"
   is a scope fork ADR 0007 ratified deliberately; #127 built Basic, and Digest
   for http destinations, while NTLM and Windows are refused at construction and
   recorded as deferred - so this is explicitly not four-scheme parity.

3. The demand-gate tracker claimed 11 lanes "Not started" that have all shipped.
   Each lane was verified individually by ADR status plus code symbol; the
   public history is snapshot-collapsed across the 2026-07-17..24 window, so no
   merge SHA can be cited for any of them.

Verified: tests/test_backlog_status_check.py (15 passed, including
test_the_real_backlog_satisfies_the_invariant, which the ⛔ form fails), plus
the ledger, runbook, doc-ref and doc-drift suites - 100 passed, 89 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wshallwshall
wshallwshall merged commit 304a93b into main Jul 31, 2026
32 checks passed
@wshallwshall
wshallwshall deleted the doc-defects branch July 31, 2026 13:09
wshallwshall added a commit that referenced this pull request Aug 4, 2026
The 2026-08-03 re-score flagged 24 open items as misdescribing what the code does.
Re-verified each against the tree as it stands -- after the archive move and after
PR #162, both of which post-date the findings -- and 10 survived. The other 14 did
not, and are recorded here rather than silently dropped:

  #84 #95 #99 #105 #114 #124 #125 #127 #133 #137 #167 #169 #214 #228

Most of those already carry an amendment that covers the stale sentence (#95, #99,
#105, #114, #124, #125, #127, #133, #228), and stacking a second ruling saying the
same thing is noise. The rest did not survive verification: the finding was itself
wrong or overstated, and a wrong correction in a ledger is worse than a stale one.

CORRECTIONS ARE ADDED AS DATED AMENDMENTS, NOT PROSE REWRITES. This file's convention
is to leave the original claim standing and rule against it, so the record shows what
was believed and what replaced it. Silently editing the stale sentence would destroy
the evidence that makes the correction checkable.

Applied to #62 #64 #131 #166 #179 #182 #237 #321 #329 #336. Representative:

  - #329 "Five MEFOR_ALLOW_INSECURE_TLS cells": the census is FOUR. #323 landed and
    routed transports/direct.py through the clamp; it now holds no call to the raw
    predicate at all (:63, :197, :215).
  - #321 "no test asserts the detectors can see a site code": false --
    tests/test_scan_forbidden.py has per-class hit tests for at least the site code
    (:126), a customer name (:83), a case-sensitive code (:91) and a routable IP
    (:107). The detector-coverage half of its Proposed 2 is already in the tree.
  - #62 plans a dual-read over "existing mfenc:v1 rows", but cell-bound mfenc:v2 is
    the default writer (settings.py:383 -> base.py:1841; crypto.py:36), and v2 folds
    (table, column, pk) into the GCM tag -- so a body landing under a different column
    must be RE-ENCRYPTED, not merely re-encoded. That tightens the catch.
  - #64's ordered plan still reads live ("Nothing builds before it"), but the
    measure-first phase completed 2026-07-12 (ADR 0051) and its step-2 lever is
    refused outright (ADR 0055 withdrawn; ADR 0107 "Do not build F2 or F3").

The refuters removed two overclaims before they landed: #62's draft asserted a live
store holds both mfenc markers (a fresh store under the shipped default holds only
v2 -- the defensible claim is that a MIGRATION must expect both), and #64's asserted
the multi-DB log split still remains, which could not be verified against ADR 0098 and
would have been a fresh false claim.

No item closes here: in every case the correction narrows the remainder rather than
discharging it, and the 2026-08-03 scores already price the remainder.

backlog_status_check.py --min-items 277: OK, 277 items, one status banner each.
wshallwshall added a commit that referenced this pull request Aug 4, 2026
…ones (#163)

* docs(backlog): close BACKLOG #226 — the estate Hybrid-layout sweep is done, off-repo

The per-feed Hybrid split (connections.toml / <INBOUND>_router.py /
<INBOUND>_handler.py / _<feed>_transforms.py) landed across the ported estate in
the maintainer-internal migration repository. Owner-attested; nothing in this
repository changes, which is also why leaving the item open could never have
closed it.

Both "Also" clauses are recorded as NOT delivered, with the reason each is not a
residual of this item:

  - "align the IDE Corepoint-import / scaffold path to emit the Hybrid layout" —
    there is no Corepoint-import path in ide/ to align. That tooling is #105,
    still open, so the clause is a constraint on #105's design rather than work
    #226 can perform. The scaffold half is misaddressed too: Insert Element (#48)
    drops per-file idioms into the current buffer (ide/src/insertElement.ts:1-5)
    and emits no multi-file feed layout.

  - "consider a recursive-glob / folder-per-feed loader enhancement" — filed as a
    consider, and not taken: load_config still globs *.py non-recursively
    (config/wiring.py:4162), the flat-merge behaviour the Hybrid layout is built
    around.

Follows the #227 precedent: close the primary, state the off-repo/misaddressed
residuals explicitly so the item is not re-opened for them.

backlog_status_check.py: OK — 277 items, each declaring exactly one status.

* fix(ledger): teach the number-space gates to span an archive, and fix two holes found proving it

Prerequisite for moving the 185 closed BACKLOG items into docs/archive/backlog/.
No item has moved yet; this only makes the guards able to see one when it does.

The item namespace will span two paths, so every guard now reads their UNION:

  - backlog_status_check.py: scan() takes (label, text) pairs and parses them as ONE
    namespace. A number re-used across BACKLOG.md and the archive was structurally
    undetectable before -- `seen` was per-parse -- which is the erratum's own shape.
  - ledger_check.py: triggers on any backlog-bearing path, not the one literal, and
    builds head/base as the union. Reading the union on both sides also removes a
    false positive: the move relocates 185 items, so head-union == base-union and
    `head - base` stays empty, where a per-file view would report 185 vanished
    numbers with a remedy that renumbers cited items.
  - alloc.ps1: sweeps both paths in the all-refs term and the working-tree term.
  - backlog-hygiene.yml: accepts a banner updated in either location.

Two pre-existing defects surfaced only because the gates were made to fail on
purpose first, neither of which is about the archive:

  1. alloc.ps1's working-tree term has NEVER worked. `[regex]'^...'` anchors at the
     start of the STRING; the term feeds it `Get-Content -Raw`, one string starting
     "# Backlog". Measured: 0 of 277 headings matched without Multiline, 277 with.
     The all-refs term hid it by covering every number committed somewhere -- i.e.
     every case except the uncommitted one this term exists for.
  2. backlog-hygiene.yml diffed BASE_SHA..HEAD_SHA (two-dot), which credits a PR for
     main-side changes to paths it never touched. One main-side edit to BACKLOG.md
     -- the move being a large one -- would let every PR with an older base pass the
     "must update BACKLOG.md" required check while enforcing nothing. Now three-dot,
     matching ci.yml's form for the same question.

Anti-narrowing, because a green gate over a shrunken corpus is the failure mode:
  - `--min-items N` fails when fewer items are found than required, and CI pins 277.
    Without it, 277 -> 92 fails nothing.
  - The scanned files are always printed with the count; a bare integer cannot
    distinguish "items closed" from "a file stopped being read".
  - A liveness receipt in the test suite asserts the same floor.
  - An explicitly-named --backlog path that does not exist is an error, not a skip.

alloc.ps1 gains `-ShowFloor`: print the floor and the swept paths, allocate nothing.
Allocation is a one-way door, so before this the only way to ask what the floor could
see was to spend a number on the question -- which is how it ran a whole release
reading two refs while its header promised all of them. Get-Floor takes -Peek so the
inspection cannot advance the high-water ratchet; the first -ShowFloor run against a
planted number moved this clone's watermark 316 -> 990 before that was fixed.

Proofs run, each observed failing BEFORE the fix:
  - archive-only unallocated #1007 staged: old gate rc=0, new gate BLOCKED.
  - #990 planted in the archive: old sweep floor 353 (blind), new sweep 990.
  - cross-file duplicate #118: detected, naming the other file.
  - banner violations inside the archive only: detected.
  - --min-items over a narrowed corpus: rc=1 with the scanned-file list.
  - -ShowFloor twice against a plant: watermark unchanged at 316.

ruff + mypy --strict clean; 43 gate tests pass.

* docs(backlog): move the 185 closed items into docs/archive/backlog/BACKLOG-CLOSED.md

docs/BACKLOG.md becomes the ~92 items someone can act on: 8,742 -> 3,648 lines.
The closed items are not deleted, summarised, or rewritten -- they are relocated
verbatim, so the file that gets opened, grepped and edited daily is the open set.

MOVED, NOT REWRITTEN. Every relocated block is byte-identical to the one that left
BACKLOG.md, headings included. Verified mechanically against a pre-move copy:

  - 277 items before = 92 after + 185 archived, no overlap, union identical
  - every OPEN block byte-identical to its source
  - every ARCHIVED block byte-identical to its source
  - all non-item prose in BACKLOG.md preserved verbatim

Byte-identical headings are load-bearing, not tidiness: GitHub derives anchor slugs
from heading text, so all 64 archived->archived cross-references keep resolving with
no edit at all. That is the whole argument for one archive file rather than a split
by status, year, or cluster -- #52 alone receives 99 of the 110 in-file anchors, and
its citers span #65 to #184, so no cut isolates them.

Cutting item blocks at the next '## ' heading of EITHER kind, not the next numbered
item: 4 blocks in this file are followed by a section header, which a naive cut would
have dragged into the archive along with the prose beneath it.

Anchors, all 127 re-resolved against real headings after the edit:
  - 44 rewritten in BACKLOG.md   -> archive/backlog/BACKLOG-CLOSED.md#<same-slug>
  -  1 rewritten in the archive  -> ../../BACKLOG.md#<same-slug>  (#226 -> #105)
  -  3 cross-file links repointed: AOAG-DEPLOYMENT.md (#100, #101), ADR 0026 (#30)
  - 64 archived->archived untouched, by design

13 anchors still do not resolve, and ALL 13 WERE ALREADY DEAD BEFORE THIS COMMIT --
confirmed by running the same check over the pre-move file, which returns the
identical multiset (11 bare-number self-anchors: #40 x4, #323 x3, #28, #29, #329,
#333; plus 2 links to #13 in COUNSEL-ENGAGEMENT-BRIEF.md, a number this sequence
never had). They are left dead and documented in the archive header rather than
repointed at a plausible neighbour: a citation resolving to the WRONG item is the
erratum's failure mode, and unlike a dead link it looks like success.

The archive carries its retirement banner inline rather than in a sibling README --
docs/archive/throughput/ needs a README because it indexes five documents; one file
does not, and two documents that must agree is a drift surface. It states the rules
that keep the namespace honest: never renumber, re-open by moving the block back
(never by copying, which creates the cross-file duplicate the status check now
fails), and add any future archive file to alloc.ps1's $backlogPaths AND
backlog_status_check.py's DEFAULT_SOURCES in the same commit -- a file named in
neither is policed by nothing.

Gates verified post-move:
  - backlog_status_check.py --min-items 277: OK, 277 items, and it now PRINTS
    "scanned: docs/BACKLOG.md (92), docs/archive/backlog/BACKLOG-CLOSED.md (185)"
  - ledger_check.py on the staged move: rc=0 (relocation adds no numbers, because
    head-union == base-union -- the exact false positive the union view removes)
  - alloc.ps1 -ShowFloor: floor 353 across both paths, next 1000
  - 43 gate tests pass

Note the floor is unchanged at 353 because the highest item (#353) is open and stays
in BACKLOG.md. The archive-sweep fix is therefore PROSPECTIVE, not a save: it starts
mattering the first time a top-of-range item closes and moves.

* docs(backlog): re-score all 92 open items on the ten-level scale (2026-08-03)

Every open item now carries a current value x difficulty score. Before this, 23 had
none at all and the other 69 were from the frozen 2026-07-10 pass, which predates the
2026-07-28 reconcile that closed 31 items -- and a stale score reads exactly like a
fresh one.

Method, unchanged from the pass it supersedes: scored from each item's own Scope /
Why / Trigger / Nearest-existing-mechanism text rather than rescaled from the old
number, then adversarially verified against the code -- a second reader per batch
attacking build state first, then verdict/tier, then value and difficulty. 26 of 92
scores were overturned by that pass and carry the refuter's number.

The banner is the live record and the table is a view of it; both are written here and
a mechanical check confirms 92 banners and 92 rows agree on every triple.

THE RATIONALE IS REPLACED, NOT JUST THE NUMBERS. Carrying an old justification under a
new score is how a banner comes to argue against itself:
  - #114's surviving "clean workaround via the on-demand test probe" is a claim PR #162
    explicitly retracted -- both destinations' test_connection CREATE the target dir, so
    the probe cannot answer the question the toggle asks. That is what lifts it off the
    parity-with-a-workaround band to 6/3. Its replacement rationale was ALSO stale (it
    described the silent-ignore #162 had just fixed) and is hand-corrected.
  - #105's "large greenfield 71-action mapper needing its own ADR" describes an importer
    that has since shipped under ADR 0086.

Scheduling barely moved, which is the reassuring result: only TWO tiers changed --
#64 DEMAND-GATE -> P3 (an index over levers that live in #62/#63/#47/#34, so it ships
nothing runnable of its own) and #105 P3 -> DEMAND-GATE. Neither contradicts an
explicit demand-gate/on-trigger ruling in its own body; that was checked for all 51
items carrying a prior tier.

Distribution is RECOMPUTED with the table rather than carried forward, and all four
lines sum to 92. The superseded table keeps its own frozen lines and now says so.

  Tiers: P1 4, P2 19, P3 17, DEMAND-GATE 52
  Quadrants: quick win 22, big bet 5, fill-in 56, money pit 9

The four P1s: #341 (9/3, a handler returning a tuple/set of Sends delivers nothing
silently -- an accept-and-drop CLAUDE.md §12 forbids), #324 (7/2), #325 (6/2), #327 (6/2).

NOT in this commit: 24 items were found to misdescribe their own build state -- prose
asserting a gap that has since shipped, or citing messagefoundry/console/, a package
retired with #103. Those are banner corrections and land separately; the scores here
already price the remainder rather than the original scope.

Two mechanical faults were caught by reading the output rather than trusting the run:
the quadrant regex omitted the hyphen in "fill-in", so 57 of 69 items took the fallback
branch and got a SECOND score inserted beside the first; and the synthesizer's own
distribution lines did not follow from its own table (11 quadrant mismatches, 8
ordering violations, difficulty summing to 95 of 92). The script now refuses to write
when any line carries two score spans or the scored count is not 92.

backlog_status_check.py --min-items 277: OK, 277 items across both files.

* docs(backlog): correct 10 items whose own prose misdescribed build state

The 2026-08-03 re-score flagged 24 open items as misdescribing what the code does.
Re-verified each against the tree as it stands -- after the archive move and after
PR #162, both of which post-date the findings -- and 10 survived. The other 14 did
not, and are recorded here rather than silently dropped:

  #84 #95 #99 #105 #114 #124 #125 #127 #133 #137 #167 #169 #214 #228

Most of those already carry an amendment that covers the stale sentence (#95, #99,
#105, #114, #124, #125, #127, #133, #228), and stacking a second ruling saying the
same thing is noise. The rest did not survive verification: the finding was itself
wrong or overstated, and a wrong correction in a ledger is worse than a stale one.

CORRECTIONS ARE ADDED AS DATED AMENDMENTS, NOT PROSE REWRITES. This file's convention
is to leave the original claim standing and rule against it, so the record shows what
was believed and what replaced it. Silently editing the stale sentence would destroy
the evidence that makes the correction checkable.

Applied to #62 #64 #131 #166 #179 #182 #237 #321 #329 #336. Representative:

  - #329 "Five MEFOR_ALLOW_INSECURE_TLS cells": the census is FOUR. #323 landed and
    routed transports/direct.py through the clamp; it now holds no call to the raw
    predicate at all (:63, :197, :215).
  - #321 "no test asserts the detectors can see a site code": false --
    tests/test_scan_forbidden.py has per-class hit tests for at least the site code
    (:126), a customer name (:83), a case-sensitive code (:91) and a routable IP
    (:107). The detector-coverage half of its Proposed 2 is already in the tree.
  - #62 plans a dual-read over "existing mfenc:v1 rows", but cell-bound mfenc:v2 is
    the default writer (settings.py:383 -> base.py:1841; crypto.py:36), and v2 folds
    (table, column, pk) into the GCM tag -- so a body landing under a different column
    must be RE-ENCRYPTED, not merely re-encoded. That tightens the catch.
  - #64's ordered plan still reads live ("Nothing builds before it"), but the
    measure-first phase completed 2026-07-12 (ADR 0051) and its step-2 lever is
    refused outright (ADR 0055 withdrawn; ADR 0107 "Do not build F2 or F3").

The refuters removed two overclaims before they landed: #62's draft asserted a live
store holds both mfenc markers (a fresh store under the shipped default holds only
v2 -- the defensible claim is that a MIGRATION must expect both), and #64's asserted
the multi-DB log split still remains, which could not be verified against ADR 0098 and
would have been a fresh false claim.

No item closes here: in every case the correction narrows the remainder rather than
discharging it, and the 2026-08-03 scores already price the remainder.

backlog_status_check.py --min-items 277: OK, 277 items, one status banner each.

* docs(backlog): file BACKLOG #1000 — prove each required merge context can fail

Escalated by the coordinator on the ground that it outlives the PR that fixed it.
Deliberately NOT filed as "fix the two-dot diff": that instance already landed in
39b62bf, and filing shipped work is the rot the hygiene gate exists to prevent.

The item is the CLASS. `.github/required-contexts.txt` names 13 contexts that block
merge, and not one of them is proven able to go red. The deliverable is a negative
control per context -- a fixture carrying the exact violation that context exists to
catch -- plus a CI job that fails when a required context has none, so the coverage
cannot silently decay as contexts are added.

Scoped narrower than "test the gates" on purpose: it does not re-test what each gate
checks, since the gates' own suites do that. It asserts one property per context --
this gate is capable of failing.

The argument is that the class has now fired at least four times here, each found by
hand and none by CI:

  #334  semgrep, required and blocking, scans a two-directory allow-list
  #327  six .gitignore rules are the sole control over maintainer-internal docs, and
        nothing asserts they still match anything
  #321  the forbidden-content gate exited 0 on a real site code and partner product
  #325  the same gate's home-path detector misses 1 of 4 spellings of a Windows path

Each is correctly filed as its own defect. None of them establishes the property that
would have caught all four before they shipped, and that property is a different
artifact from any of the individual fixes.

Value 7 / Difficulty 3, quick win, P1 -- not demand-gated; the trigger fired four
times. Ranked table and all four distribution lines recomputed to 93 open items; a
mechanical check confirms 93 banners and 93 rows agree on every triple.

Number allocated atomically via scripts/coord/alloc.ps1 (#1000 -- the first in the
post-partition public sequence, clamped to >= PUBLIC_BACKLOG_FLOOR), never grepped.

backlog_status_check.py --min-items 277: OK, 278 items across both files. The floor is
a floor, so growth passes it; it is there to catch shrinkage.
wshallwshall added a commit that referenced this pull request Aug 12, 2026
…surface (BACKLOG #1220) (#346)

* feat(seam): discover the webconsole seam surface instead of enumerating it (BACKLOG #1220)

Layer 1 of #1220, and it is INERT: nothing consumes this yet. The generator still reads its
five hand-maintained tuples and ENGINE_UI_SEAM is still the hand-chosen integer 20. Separating
coverage from derivation means a bisect can tell which one broke something.

WHY THIS AND NOT A HASH OVER THE EXISTING LISTS. #1220's ruling is that the hash input must be
DISCOVERED, never enumerated, because a content hash over a hand-maintained list stops colliding
while still failing to move when an unlisted model changes -- and a hash LOOKS like it covers
content, so the next reader trusts it more than the list it replaced. Measured at ebf4882, three
of the five tuples had already drifted:

  _API_MODELS_DTOS      30 curated, 55 reachable. 7 DTOs the console imports directly are absent
                        (UploadedFileList among them); the other 18 are reachable only by NESTING,
                        and _dto_fields records field names ONE level deep with no recursion, so a
                        nested model's field set is absent from the contract entirely.
  _API_SECURITY_SYMBOLS missing client_ip and enforce_phi_read_pacing, carrying five names the
                        console no longer imports. That is why _ui_seam.py asserts the console
                        imports six symbols directly -- false for five of six.
  _AUTH_SERVICE_METHODS AuthService.has_action_step_up is called at _auth.py:620 and absent. Six of
                        the seven additions are PROPERTIES, on which inspect.signature raises, so
                        the curated list held methods only because the renderer could not do more --
                        the instrument's limitation had silently defined what counted as contract.

The proof the defect is real rather than theoretical is in the history: 40a4d5d added a REQUIRED
scope field to UploadedFileList, which pages/uploaded_logs.py renders unconditionally, and touched
no seam file across 17 changed files. The seam moved later, by hand, in review.

CALIBRATION. _APP_STATE_ATTRS was the one tuple with zero drift in either direction, and the
two-sided rule (console reads intersected with engine writes, plus console writes) reproduces its
eight names EXACTLY. A walk that merely returned "more" would be consistent with measuring the
wrong thing; reproducing the un-drifted list is the evidence that it measures the contract.

The AuthService rule is annotation-driven -- attribute access on a receiver annotated AuthService or
assigned from get_auth() -- rather than a bare name intersection. The bare rule yields 38 names, ~11
of which are present only because they collide with AdminHandlers field names, and a gate that moves
for methods the console never calls is one people stop reading. Annotation-driven yields 34: the 27
curated plus 7 verified real, nothing dropped.

FAIL LOUD, never skip. A star import, an alias escaping attribute position, a dynamic getattr on a
DTO module, or an unresolvable model raises SeamDiscoveryError. A silent skip would recreate the
enumeration blind spot inside the walk's control flow, which is strictly worse than the tuple it
replaces -- a 30-line tuple was at least reviewable. Every loud case measures zero occurrences
today, so the guard costs nothing now and its first firing is a genuine new idiom.

The closure deliberately crosses module boundaries: a rendered DTO that exposes a model defined
elsewhere still ships that model's field set across the seam. Discovery runs in the generator and
the test only -- messagefoundry/ must not import the console or scripts/, and a seam computed at
import time would make every proof condition pass vacuously.

Verified: ruff check + format clean, mypy strict clean, 21 new tests plus the 3 existing seam tests
pass. Full-suite baseline at this merge-base was 11882 passed / 861 skipped / 0 failed.
NOT verified: no CI leg run; the SQL Server and Postgres legs skip locally.

* feat(seam): feed discovery into the snapshot and delete the curated tuples (BACKLOG #1220)

Layer 2 of #1220. The generator now reads the DISCOVERED surface; the five hand-maintained tuples
are gone. ENGINE_UI_SEAM is STILL the hand-chosen 20 -- deriving the value is layer 3. Isolating
coverage from derivation keeps a bisect able to say which one broke something.

WHAT THE GOLDEN GAINS. It went from 196 lines to 237, and the additions are the measured blind spot:

  UploadedFileList        appeared ZERO times in the old golden. It now carries its field set AND
                          its Literal values. This is the DTO whose required `scope` field landed in
                          40a4d5d without moving any seam file -- the gate stayed green through
                          exactly the change it exists to catch.
  25 more DTOs            7 the console imports directly, 18 reachable only by nesting. The closure
                          matters because _dto_fields records names one level deep with no
                          recursion, so a nested model's fields were absent entirely.
  6 AuthService properties action_step_up_required, ad_enabled, kerberos_available, oidc_enabled,
                          oidc_flow_ttl_seconds, store -- all read by the console across the seam,
                          none recorded. inspect.signature RAISES on a property, which is why the
                          retired tuple held methods only: the renderer's capability had silently
                          defined what counted as the contract. _member() renders them now.
  has_action_step_up      called at _auth.py:620, absent from the golden, and on the ADR 0077
                          per-action step-up path.
  client_ip,              imported by the console and never hashed. The same correction drops five
  enforce_phi_read_pacing stale names the console no longer imports -- which is why _ui_seam.py
                          asserted the console imports six api.security symbols directly, false for
                          five of six. That assertion is deleted with the tuple it described.
  enum + Literal sets     Field NAMES are not the contract. pages/uploaded_logs.py renders
                          UploadedFileList.scope as _SCOPE_NOTES[data.scope], so renaming a literal
                          from "own" to "owner" would KeyError at runtime while a field-name-only
                          snapshot stayed byte-identical.

NO SEAM BUMP, DELIBERATELY. The snapshot changed because the GENERATOR changed, not because the
engine's contract did. A console built against seam 20 is no less compatible than it was an hour
ago -- recording more of an unchanged contract cannot make an older console incompatible. Bumping
here would spend a hard console-wheel refusal (SUPPORTED_ENGINE_SEAMS holds exactly one value,
BACKLOG #279) on a bookkeeping change.

The calibration tests now assert against the retired tuples FROZEN AS LITERALS rather than reading
them from the generator. Reading them would make the calibration vacuous the moment the generator
stopped carrying them, which is this commit. Frozen, they keep proving the two properties that
matter: app.state is reproduced EXACTLY (the one tuple with zero drift), and no previously-curated
name is ever dropped.

Verified: ruff check + format clean, mypy strict clean on both scripts, 24 seam tests pass, plus
tests/test_ui_oidc_interstitial_route.py and the full packaging/messagefoundry-webconsole suite --
those are the only three test files in the tree that reference the seam, the golden, or the
generator (grep over tests/ and packaging/.../tests/ for ENGINE_UI_SEAM|webconsole_seam|
SUPPORTED_ENGINE_SEAMS|engine_seam).
NOT verified: no CI leg run; full suite re-run deferred to the end of the layer stack.

* feat(seam): derive ENGINE_UI_SEAM from the contract surface (BACKLOG #1220)

Layer 3, and the one that closes #1220. ENGINE_UI_SEAM is no longer a number anyone picks: it is a
16-hex SHA-256 of the DISCOVERED contract surface, produced by
`python scripts/webconsole_seam_snapshot.py --write`. Value: 494a51230dce5730.

WHY THIS FIXES THE COLLISION AND GATING THE INTEGER WOULD NOT. Two branches bumping to 19 for two
independent contract changes produced a COSMETIC conflict in a comment block while the golden
snapshot auto-merged CLEAN carrying both changes under one seam -- so resolving the visible conflict
correctly still shipped the fault. A digest has no such failure mode: two branches changing different
surfaces derive different values, and their MERGED surface derives a THIRD matching neither, so the
merge reds. Uniqueness stops being something a human maintains.

str, NOT a truncated int, and the old skew test is why. It asserted `ENGINE_UI_SEAM - 1` is refused,
under a docstring claiming "one seam older AND one newer". Under an int digest that arithmetic still
evaluates, the assertion still PASSES, and the docstring silently becomes false -- a passing test
making a claim the value no longer supports, which is the same defect class #1220 was filed against.
A str makes it a TypeError instead. It also removes the hand-typable affordance: someone can write
21 and it looks legitimate; nobody types a hex digest by hand and believes it.

ANTI-CIRCULARITY IS BY CONSTRUCTION. contract_sections() never reads ENGINE_UI_SEAM, so the seam
cannot feed its own digest -- there is no "strip the seam line" filter that could quietly stop
matching. The test asserts it by MOVING the constant and requiring the digest to hold still, not by
checking the seam's text is absent from the input: a substring check is defeated by a short or
coincidental value (the header already contains the literal 0065). Mutation-proved before trusting
it -- a generator that folds the seam into its own input makes that test FAIL, as it must.

Four tests, three of them new: the stored value equals the derived digest (the gate); the digest
holds still when the seam moves (anti-circularity); the digest MOVES when a rendered DTO gains a
field, with 40a4d5d as the historical control where it did not; and the skew test rewritten to
refuse the empty string, a truncation, an extension and an arbitrary digest, preserving the
both-directions intent BACKLOG #279 put in the original.

The failure message is rewritten and is now pure ASCII. The old one carried U+2014 (a replacement
character on a cp1252 console, the developer-facing half of #1221) and told the reader to "bump
ENGINE_UI_SEAM" -- an action that no longer exists. A gate whose message names the wrong repair
teaches the wrong repair.

--write rewrites the constant and the golden, and REFUSES rather than guesses if the constant line
does not match exactly once. It deliberately does NOT write the console's SUPPORTED_ENGINE_SEAMS:
that is the independent half of a two-wheel handshake, and a tool writing both sides turns it into a
self-consistent tautology. The message spells out the one-line manual edit instead.

Also fixes tests/test_ui_oidc_interstitial_route.py, which passed engine_seam=0 into a field now
typed str. Nothing automated would have caught it -- tests/ is outside mypy's configured scope and a
dataclass does not validate at runtime -- so it was found by grepping every consumer deliberately.

Verified: ruff check + format clean; mypy strict clean on messagefoundry_webconsole; 40 tests pass
across the three seam-touching test files. mypy on messagefoundry reports 21 errors in 4 files
(auth/webauthn.py, parsing/dicom/_deps.py, parsing/fhir/_deps.py, transports/dicom.py) -- all
pre-existing and all downstream of four optional extras absent from this local venv (pydicom,
pynetdicom, webauthn, fhir), which CI installs. None of the four is in this diff and none references
the seam.
NOT verified in this commit: the two-branch merge proof and the full suite; both run next.

* fix(seam): refuse an unresolved ForwardRef instead of walking past it (BACKLOG #1220)

Found by running #1220's own acceptance proof, which is the entire reason for running it rather than
reasoning about it. Proof (c2) plants a DTO reachable ONLY as a field of one the console imports,
renames that nested DTO's field, and requires the gate to red. It stayed GREEN.

MECHANISM. Pydantic resolves string annotations at class-build time, but NOT when the referenced
class is defined later in the module and nobody calls model_rebuild(). The field's annotation then
stays a ForwardRef; typing.get_args() returns () on it; and the closure walked straight past a nested
DTO whose field set belongs in the contract. Measured directly:

    SystemStatus.model_fields['proof'].annotation
      -> ForwardRef("'ProofNested | None'", is_class=True)
    typing.get_args(...) -> []

That is a SILENT SKIP inside the walk -- precisely what this module's own docstring forbids, and the
defect it was written to remove, reproduced one level down. A skip here is indistinguishable from
full coverage at every downstream vantage point: the digest is computed, the gate is green, and the
uncovered model looks exactly like a covered one.

The fix raises SeamDiscoveryError naming the class and the ref, and says how to resolve it (define
the referenced model first, or call model_rebuild). Zero occurrences in the tree today, so this costs
nothing now; its first firing is a real hole.

With the guard in place the corrected proof (c2) discriminates as it should -- renaming the nested
field reds the gate on this tree and leaves origin/main GREEN, which is the defect the closure exists
to close. Same edit, opposite outcomes.

Verified: ruff check + format clean, mypy strict clean, 28 tests in tests/test_seam_discovery.py
pass including a new one that drives the ForwardRef case directly.

* docs(seam): record the seam digest in the crypto inventory (BACKLOG #1220)

The full suite caught this, not review: three tests failed on the #1220 stack, all one cause --
scripts/webconsole_seam_snapshot.py now calls hashlib.sha256 and the crypto-inventory gate refuses an
undocumented crypto call site. The gate is correct and this is the entry it asked for.

Recorded in all three places the convention requires, because they are three INDEPENDENT
enumerations of the same fact and a fix that updates one leaves the other two lying:

  scripts/security/crypto_inventory_check.py  INVENTORY -- what the gate enforces
  tests/test_security_static.py               _CRYPTO_SITES_OUTSIDE_THE_PACKAGE -- the test-side pin
                                              that covers roots the gate does not yet walk (#282)
  docs/ASVS-L2-PHASE0-CHANGES.md section 4    the human inventory the gate calls itself the
                                              machine-readable companion to

WHAT THE RATIONALE HAS TO SAY, because "we hash something" is not a justification: it is a CHANGE
DETECTOR, not a security control. No secret, no key, no message authentication, and nothing user- or
PHI-derived is hashed -- the input is a serialization of public type signatures, field names, enum
members and Literal values. The property required is accidental-collision avoidance across the
contract surfaces this project will ever produce, which at 64 bits is 2.7e-12 for 10,000 distinct
surfaces (about 500x the ~20 seam moves to date). Preimage resistance buys nothing: anyone able to
craft a colliding surface already has commit access to _ui_seam.py, where writing the constant
directly is strictly easier. SHA-256 rather than BLAKE2 or a truncated non-approved digest only
because the engine renders a fips_mode attestation, and a non-approved hash in the shipped surface
invites a FIPS question for zero benefit.

Verified: tests/test_security_static.py + tests/test_crypto_inventory_scanner.py, 64 passed, 0
failed -- the three that failed on the previous full run. ruff check + format clean.

* feat(ai): refuse an unserviceable [ai].provider at config time (BACKLOG #95)

Validator ONLY, per the owner's ruling -- no per-provider wire shapes.

THE DEMAND GATE ON #95 WAS LIFTED BY THE OWNER IN SESSION on 2026-08-12, in response to a
pause-and-ask that named the item, its scope and its cost. Recording that here because the ledger row
cannot cite a source otherwise, and this project has already resolved one lane-vs-lane dispute over
an unsourced claim that a gate had been lifted.

WHAT WAS WRONG. settings.py declared `provider: str = "claude"` with no validation, so any string
loaded clean -- azure_openai, gpt-4o-gateway, '', even '../../etc/passwd'. Verified through the real
config path (load_settings), not just the constructor.

WHY IT MATTERS EVEN WHEN THE BROKER IS NEVER BUILT, which is the part that decided the design: the
value is recorded in the PER-USE AUDIT regardless of mode (api/app.py). A config naming a provider
the engine cannot service therefore made the audit trail assert something untrue -- so the validator
is field-level and refuses in every mode, rather than a model_validator gated on managed_endpoint.
The narrower form was the defensible alternative; this is the deliberate choice, not the default.

THE ALLOWLIST IS ONE ENTRY, AND ITS SOURCE OF TRUTH IS A HARD-CODED REQUEST BODY. AiBroker.chat
builds one wire shape unconditionally -- an Anthropic Messages body with x-api-key and
anthropic-version, and an _extract_text that assumes Anthropic's content-block list. There is no
provider registry, no dispatch, and AiBroker.provider has ZERO readers, so nothing can derive the
serviceable set. That makes the list hand-maintained, which is a real cost and is written down as
one. A test pins it to chat()'s body so widening the list without teaching chat() a second shape
fails loudly.

THE INVERSE FAILURE IS THE TEMPTING ONE. Listing aspirational names (azure_openai, bedrock, ollama)
would ACCEPT configurations the broker still cannot service, turning a clean config-time refusal back
into the opaque runtime failure this item exists to remove. The list describes what chat() can send,
never what the roadmap intends.

DOCS. The item names docs/AI.md:22. That line is stale, but docs/CONFIGURATION.md:637 carried a
STRONGER false claim the item does not mention -- that provider selects "the broker's request shape",
which is not true in the code and directly contradicted AI.md. Fixing only the named line would have
left the more actively misleading one in place. Also corrected: AI.md asserted "No model-provider or
engine broker integration exists yet" while managed_endpoint has been BUILT since ADR 0135, and the
mode tables omitted managed_endpoint entirely (the string appears zero times in that file). The
ai_broker module docstring said the Anthropic shape "is used when the provider is claude", implying a
branch that does not exist.

Verified: ruff check + format clean, mypy strict clean on both changed modules, 94 tests in
tests/test_ai_policy.py + tests/test_ai_broker.py pass, and 916 passed / 0 failed across every
doc-drift, link and config test in the tree.
NOT verified: no CI leg run.

* fix(uploads): attribute the upload.prune audit row to the system (BACKLOG #1224)

Both sites, because closing one leaves the same false attribution reachable by the other path: the
save-time opportunistic sweep in the upload handler, and the background UploadRetentionRunner's audit
closure in the API lifespan. Located by the literal "upload.prune", not by line number -- the item's
own cited lines had already moved 87 and 121 lines when #1152 landed above them.

THE DEFECT IS CROSS-OWNER, AND IT IS STRUCTURAL RATHER THAN INCIDENTAL. prune_expired() is
deliberately UNSCOPED -- it has to be, since the per-uploader quota and the sweep both need to see
every file -- so the operator whose upload triggers a pass is in general NOT the owner of what it
prunes. The row named the pruned file's OWNER as actor while stamping the TRIGGERING operator's
address as client, asserting that X deleted their own file from Y's host. That is true of neither of
them.

The fix applies a decision this repo already made rather than inventing one. actor="system" matches
pipeline/retention.py's retention_purge (plus dr, dr_backup, reference_sync and the auth bootstrap).
Dropping client is not tidying: _record_reload_audit's own contract is that client is the address OF
THE ACTOR NAMED IN THE ROW, and once the actor is the system principal no address is in scope --
ADR 0150 decision 4 rejects exactly this pairing for dual-control config reload, calling a stamped
approver address "worse than NULL". Note the engine-internal-writes rule does NOT justify it here,
because site 1 genuinely runs inside a request; the actor-address correspondence is what does.

The uploader is not lost -- it stays in detail.uploader, which is where a reader can still see whose
file went without the row claiming they went and deleted it.

RED-FIRST, TWICE, because one mutant was not enough. Reverting the actor reds the test with
actor == 'op' while op2 triggered the sweep -- the cross-owner defect in one assertion. And a
PARTIAL-FIX mutant (actor corrected, client=client_ip(request) left behind) reds on
`assert not '127.0.0.1'`. That second mutant is the point: a test asserting only the actor string
would have passed the half-fix, which is the likeliest way this gets "fixed" wrong.

Zero tests referenced "upload.prune" before this one, so the defect had no guard at all.

Verified: 97 tests across tests/test_upload_api.py, tests/test_uploads.py and
tests/test_audit_integrity.py pass -- the last because dropping client changes the ADR 0150
hash-chain payload shape for these rows. ruff check + format clean, mypy strict clean on app.py.
NOT verified: no CI leg; SQL Server and Postgres record_audit implementations were read, not executed
-- the audit-integrity suite covers the SQLite path only in a local run.

* fix(search): key saved presets on the immutable user_id, not the username (BACKLOG #1225)

Four sites in api/app.py, and the WRITE went first because it is the setter the other three read --
re-keying only the readers would have made every newly created preset invisible to its own creator.
Located by store-method name, not line number: the item's cited lines had all moved +122 when #1152
landed above them.

THE ENABLING HALF THE ITEM DOES NOT CITE. A username being reassignable is only half the defect. The
other half is that delete_user (store/store.py) removes user_roles, sessions and webauthn_credentials
and NEVER preset rows -- so a departed operator's presets outlive the account under a name that is
now free to reissue, and a recreated account of that name inherited them, including the PHI-shaped
`criteria` that the layered compose returns. Both halves are asserted in the new test.

WHY THE EXISTING SUITE WAS NOT EVIDENCE. test_preset_crud_and_owner_scoping compares two operators
who are BOTH LIVE, and it passes on the defective code. The defect needs an account to go away and
its name to come back; nothing exercised that. The new test does, through the list route, the delete
route and the layered compose.

RED-FIRST, AND THE FIRST RED WAS THE WRONG ONE. Restoring username-keying at all four sites initially
failed on my own PRECONDITION -- which queried by user_id and so could not survive a username-keyed
build -- masking the assertion that matters. Made key-agnostic, the mutant now reds exactly where it
should: the recycled account sees {'name': 'mine'} where it must see []. A test that goes red for the
wrong reason proves nothing, and it looks identical to one that does not.

I ALSO TRIED TO CLEAN UP THE STRANDED ROWS AND BACKED IT OUT. The re-key leaves pre-existing rows
matching no user_id: unreachable by every route, with their encrypted criteria lingering. I added a
one-shot DELETE keyed on `owner NOT IN (SELECT id FROM users)` -- exact, idempotent, and it would have
collected delete_user's orphans too. tests/test_retention.py caught it:
test_reopening_a_pre_306_db_migrates_last_used_at_in asserts that migration PRESERVES rows, and my
statement ran on every open and destroyed data. That is a behaviour change well outside this item, so
it is reverted. CONSEQUENCE, stated rather than hidden: a developer with an existing dev store will
find their old presets unreachable and should recreate the store. There is no correct
username-to-user_id backfill -- a freed name may since have been reissued, which IS the defect.

THIS IS A NARROWING, NOT A CLOSURE, and the ADR now says so: _upsert_ad_user re-binds a surviving
mirror row's user_id when a directory-side sAMAccountName is recycled without a MessageFoundry
delete_user, so a user_id-keyed check is still defeated on that path. BACKLOG #1143 is the real close.

Also corrected: the schema comment that read "the owning username" and would have become a lie, the
field_authz seed that wrote presets under a username (a row no route can now reach), and ADR 0136,
which asserted `owner = identity.username` as the design.

NOT swept: identity.username appears 60 times in app.py and 46 are audit actor= fields, where
recording a NAME is correct. Only the four ACCESS KEYS moved.

Verified: ruff check + format clean, mypy strict clean on both changed modules; 212 passed across
every preset/retention/field-authz/schema/PHI-inventory/doc-drift selection, and 447 passed across
every doc, ADR and link test.
NOT verified: no CI leg. The Postgres and SQL Server preset paths were read, not executed -- they
skip locally, and per project experience a blind DB-leg change fails only in CI. Their DDL is
unchanged by this commit (the column still holds a string) but their tests seed owners by name.

* fix(lint): harden the ASVS tally gate's own stdout against a cp1252 console (BACKLOG #1221)

The gate prints DOCUMENT CONTENT, and the corpus it scans is documentation -- so a hit whose line
carried a glyph killed the run with UnicodeEncodeError before the finding could be read.

THE DEFECT IS INVISIBLE UNTIL THE GATE HAS SOMETHING TO SAY. Against the real corpus it exits 0 and
looks perfectly healthy; it fails exactly when it is needed. Reproduced deterministically here before
fixing: a one-line doc with a new tally and a U+2192 dies at the findings loop, and the same run is
green on the shipped corpus.

STREAM-LEVEL, NOT A TARGETED SAFE-PRINT AT THE OBVIOUS SITE -- and that is a correctness choice, not
a style one. Python's \d is Unicode-aware, so _AGAINST_TOTAL and _ARITHMETIC take m.group(0) VERBATIM
into the token: idioms_for_line() over Arabic-Indic digits yields a token carrying non-ASCII. That
token flows into the --print-keys output, the stale-baseline report AND the baseline file, so
hardening only the findings loop leaves the same class open in three other places. The two fix shapes
are not equally defensible, which is what the item asked to have decided explicitly.

Copied rather than imported: this file is on MIRRORED_TOOLS and is contractually stdlib-only, so it
cannot reach for a shared helper. The guard mirrors messagefoundry/__main__.py's shape including its
reason for being guarded -- some stream wrappers lack reconfigure or reject it, and hardening must
never itself crash the tool.

THE TEST IS A SUBPROCESS, DELIBERATELY. The existing 45 tests never exercised the print path at all.
Forcing the encoding in-process would NOT be the same instrument: pytest's capture wrapper is one of
the very objects the hardening guards against, so an in-process test can pass having measured
nothing. The child runs under PYTHONIOENCODING=cp1252 with no error handler, so it starts at
errors='strict' and only the fix can save it -- a test of the fix rather than of the environment.

Mutation-proved: removing the _harden_stdout() call reds the new test with the child's own
UnicodeEncodeError traceback in the failure message.

Verified: 45 tests in tests/test_asvs_tally_lint.py plus the vault mirror contract pass; ruff check +
format clean. The real-corpus run still exits 0.
NOT verified: no CI leg. The lint's CI job runs on ubuntu (utf-8), which is why the test forces the
encoding rather than relying on the platform -- the defect is not reproducible there by default.

* test(seam): classify the seam digest as non-key material (BACKLOG #1220)

The full suite caught this, and it is the SECOND gate to fire on the same commit -- the crypto
inventory wanted the call site recorded, and this one wants every ASVS section 4 row classified as
key material or explicitly not. Both are correct, and between them they are why "we hash something"
cannot be waved through here.

Classified as NOT key material, with the reason the gate asks for: it is a keyless content hash over
PUBLIC type signatures, field names, enum members and Literal values -- a change detector for the
engine/console handshake, not a key, a secret, or a message authenticator. Nothing user- or
PHI-derived is hashed, and the digest is committed in source on both sides of the seam, so there is
no scope to state and nothing to rotate.

That places it with the Config fingerprint, the ASVS corpus pin and the engine wheel attestation --
the three existing rows of exactly this shape.

Verified: 69 tests across tests/test_key_usage_scope_inventory.py, tests/test_security_static.py and
tests/test_crypto_inventory_scanner.py pass. ruff clean. This was the ONLY failure in the full-suite
run over the whole lane (1 failed, 11914 passed, 861 skipped).

* feat(uploads): record the immutable owner key on the prune audit row (BACKLOG #1224)

Owner-approved follow-on to #1224, raised rather than folded in silently because it is beyond that
item's stated scope -- #1224 was about the ACTOR field, this is the DETAIL.

WHY IT IS WORTH THE KEYSTROKES. The prune row is a permanent record of a deletion whose subject
cannot be recovered afterwards: the file is gone, so `detail` is the only surviving description of
whose it was. It recorded `uploader`, a username, and a username is reassignable -- which is the
entire finding of #1225, shipped two commits ago. A row read a year from now could name a different
person than it meant, and nothing would flag it.

UploadedFileMeta already carries both fields and its docstring says the split is deliberate
(uploads.py:116: "uploader_id is the account's IMMUTABLE..."), so the data was in hand at both sites.
This records both: the username because that is what a human reads, the id because that is what still
identifies the account after the name moves.

FIRST-OF-KIND, AND SAID OUT LOUD. No other audit row in api/app.py carries an id in its detail, so
this sets a convention rather than following one. That was the honest argument against doing it, and
the counter-argument that won: you fix things where you touch them, and the alternative -- an item
proposing the convention for every owner-bearing audit row -- improves nothing today and may sit.
If that broader sweep is ever wanted, these two rows are the precedent to point at.

No schema change: `detail` is a free-form JSON string, so existing rows still verify against the
ADR 0150 hash chain. Ran the audit-integrity suite rather than assuming that.

The test asserts uploader_id is present AND that it differs from the username, so a fix that
populated it with the name would still red.

Verified: 68 tests across tests/test_upload_api.py and tests/test_audit_integrity.py pass; ruff check
+ format clean; mypy strict clean.

* backlog: close BACKLOG #1220 -- the derived seam, and the numbers this item published are now void

The fix is the building lane's (claude/instruments-config-handoff-e64adc). This commit
adds only the banner, which is the coordinator's to write: allocation and the commit
that files it cannot be split across worktrees, and the claim gate requires the banner
in the SAME PR as code citing BACKLOG #N. I will not buy that check's green by removing
the citation from the PR body -- a green there is evidence nothing looked.

The banner records three things beyond "shipped":

1. It shipped the option THIS ITEM PREFERRED. The Scope says derive the seam from a hash
   so collisions are impossible by construction, and warns the gate-an-integer
   alternative is "one more thing to remember". The built form is the derived one, and
   the surface is DISCOVERED rather than enumerated, which was the stated condition.

2. THE ITEM'S OWN NUMBERS ARE VOID, kept rather than deleted. It publishes
   origin/main = 18 and "the second branch to land re-bumps to 20". Measured 2026-08-12:
   main reached 20 when #1152 landed, so BOTH held branches sat at 19 -- BELOW main --
   and that rule had already inverted before anyone applied it.

3. WHAT IT BREAKS. The seam becomes a str digest, so the two held branches no longer
   carry a stale integer, they carry a mypy-strict TYPE error. Their bumps are a rewrite,
   not a renumber. Sequence #1227's class fix after this, never concurrently.

Attribution is split in the banner: what the lane reported (the three acceptance
criteria, the 25-DTO coverage figure) is labelled as reported, and what was measured
here (main at 20, both branches at 19) is labelled as measured.
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