fix(fhir): anchor path-segment patterns, catch InvalidURL, and screen operator config at construction (#1240, #1241) - #379
Merged
Conversation
…refused (BACKLOG #1240)
Python's `$` also matches immediately before a final newline, so `^[A-Za-z]+$` accepted
"Patient\n" and the gate did not enforce the grammar it advertises.
Fixed on the PATTERNS, not the call sites. `match` versus `fullmatch` is a property of
the CALL and there are three call sites (fhir.py:189, :698, :704), so a per-call fix
covers whichever two you happen to notice and leaves the third to re-introduce the hole.
Anchoring the pattern fixes all three at once and cannot be re-broken by a future caller.
The item as filed prescribed "two one-line changes: match to fullmatch on both regexes".
That is not executable -- it is three call-site edits, not two. `$` to `\Z` is genuinely
two lines and strictly stronger. Re-verified against the code before building; the
amendment content is with the dispatcher.
NOT DONE, deliberately: the item's read-path `_reject_control_chars` limb. Once the gates
are strict it is redundant -- both charsets exclude every C0 and DEL character, and every
character of the query reaches a gate ('?' refused at :713, more than two segments raised
at :696). Adding it would also re-introduce the second control-char treatment that #1239
records as retired by #1243.
TEST SHAPE IS LOAD-BEARING. A trailing LF on the whole query ("Patient/123\n") is
normalised away upstream and builds a URL byte-identical to the clean input -- measured
both before and after this change -- so the obvious test passes either way and proves
nothing. Only an LF ending a segment followed by more path ("Patient\n/123") reaches a
gate carrying the newline. Both shapes are pinned: the discriminating one asserts the
refusal, and a second test pins the normalisation so that if it ever starts raising, the
first test is known to need re-deriving rather than deleting.
Red-first: both parametrized cases failed with "DID NOT RAISE ValueError" against the
unfixed patterns, and the normalisation pin passed before and after, as it should.
Verified, with scope stated: ruff format --check and ruff check clean on both changed
files; mypy strict clean on transports/fhir.py; pytest over tests/test_fhir_lookup.py,
tests/test_egress_allowlist.py and tests/test_transports.py = 165 passed, 1 skipped, in
the lane venv built against constraints.lock (ruff 0.15.22, matching the pin). The FULL
suite was NOT run and neither test path was collected in full.
wshallwshall
enabled auto-merge (squash)
August 13, 2026 19:25
…nstead of escaping (BACKLOG #1241)
InvalidURL is not a ValueError and not an OSError. Its MRO is
InvalidURL -> HTTPException -> Exception
so it matched NONE of _post's except arms: not HTTPError (:616), not URLError (:634),
not (TimeoutError, OSError) (:647), and not the ValueError backstop at :638 -- whose own
comment says it exists for "a CRLF in a header/URL that slipped past the control-char
guard", which is precisely the condition urllib raises InvalidURL for.
So the arm written for this exception could not catch it. On first deployment the URL
limb would surface as an unhandled internal error out of send() rather than the
classified permanent dead-letter the file intends, which is a different disposition and
a different operator experience: an escaping exception instead of a dead-lettered
message with a reason.
This is a PARTIAL fix for #1241 and I am not claiming otherwise. The item's filed claim
-- that operator-config values reach the URL and header sinks with no construction-time
screen -- still HOLDS for both sinks and is NOT addressed here. This commit closes the
narrower defect found while re-verifying the item: that when the URL sink does fail, it
fails in the wrong class.
Scope note carried from the re-verification, because it bounds how far this goes: the
URL limb has two incidental neutralisations the header limb does not -- urllib.parse.unwrap
strips a trailing CRLF, and Request.full_url splits at '#' client-side. The header sink
has neither, which is why the construction-time screen is still needed and why a fix
cannot stop here.
Red-first: the test failed with a raw `http.client.InvalidURL: URL can't contain control
characters` escaping _post, which is the defect itself rather than a proxy for it.
NEGATIVE CONTROL SHIPPED ALONGSIDE. A second test asserts a URLError still raises a
retryable DeliveryError and NOT a NegativeAckError, so this cannot pass by the method
having been widened to swallow everything into the permanent class. It passed before this
change and after it.
Verified, with scope stated: ruff format --check and ruff check clean on both changed
files; mypy strict clean on transports/fhir.py; pytest over test_fhir_transport.py,
test_fhir_lookup.py, test_egress_allowlist.py, test_transports.py and test_smart_backend.py
= 259 passed, 1 skipped, in the lane venv built against constraints.lock (ruff 0.15.22,
matching the pin). THE FULL SUITE WAS NOT RUN and neither test path was collected in full.
No ledger edit: the banner flip is withheld deliberately and the disposition routes to the
dispatcher.
wshallwshall
disabled auto-merge
August 13, 2026 19:28
wshallwshall
enabled auto-merge (squash)
August 13, 2026 19:29
…nstruction (BACKLOG #1241)
This is the item's FILED defect, which the previous commit did not touch: operator-config
values reached the URL and header sinks with no construction-time screen.
conditional_query was taken verbatim from settings and reached TWO sinks:
- an unencoded URL interpolation, f"{base}/{type_seg}?{self.conditional_query}"
- the If-None-Exist HEADER value
The header sink is why this could not be left to the send path. The URL limb has two
incidental neutralisations it does not: urllib.parse.unwrap strips a trailing CRLF, and
Request.full_url splits at '#' client-side. Neither touches a header value, so a CRLF in
conditional_query is a header injection with nothing in front of it.
SCREENED AT CONSTRUCTION, NOT PER MESSAGE, AND THE DISPOSITION IS THE REASON.
_reject_config_control_chars raises ValueError and is deliberately distinct from the
existing _reject_control_chars, which screens message-derived values and raises a
permanent NegativeAckError. A bad MESSAGE dead-letters one message. A bad SETTING is
wrong for every message the connection will ever send, so it must fail the connection at
load rather than dead-letter an unbounded stream of messages that were never at fault.
Applied to both `url` and `conditional_query`.
Red-first: all five new cases failed with "DID NOT RAISE ValueError". One of them first
failed with a TypeError instead -- the test passed url= through a helper that already
supplies it -- and a test failing for the wrong reason is not a red-first proof, so it was
rebuilt to construct the Destination directly and re-confirmed.
POSITIVE CONTROL SHIPPED: a clean conditional_query carrying '|' and ':' and '/' still
constructs and is preserved verbatim, so the screen cannot pass by rejecting everything.
STILL NOT COMPLETE, and #1241 must not be closed on this either. Not addressed here:
- transports/dicomweb.py, which the item also names. Untouched.
- FhirLookupExecutor has a SECOND url construction site in this same file with the same
unscreened shape. Found only because an edit matched two locations rather than one.
Not fixed here because it is outside what was dispatched; reported as content.
Verified, with scope stated: ruff format --check and ruff check clean on both changed
files; mypy strict clean on transports/fhir.py; pytest over test_fhir_transport,
test_fhir_lookup, test_egress_allowlist, test_transports, test_smart_backend and
test_connection_api = 302 passed, 1 skipped, in the lane venv built against
constraints.lock (ruff 0.15.22, matching the pin). THE FULL SUITE WAS NOT RUN and the
webconsole suite was not collected at all.
No ledger edit; the banner flip is withheld and disposition routes to the dispatcher.
wshallwshall
disabled auto-merge
August 13, 2026 20:05
wshallwshall
enabled auto-merge (squash)
August 13, 2026 20:06
wshallwshall
disabled auto-merge
August 13, 2026 22:45
wshallwshall
enabled auto-merge (squash)
August 13, 2026 22:46
wshallwshall
disabled auto-merge
August 13, 2026 22:46
cannot make itself PR #379 is red on a required check that says a PR implementing BACKLOG #N must update BACKLOG.md. The owner's 2026-08-13 ruling says a builder may resolve merge conflicts but may not author ledger content. Those two are mutually unsatisfiable for a compliant builder PR, so the builder correctly withheld the banner and the PR correctly went red. Authoring is dispatcher and lander only; this supplies the edit. Neither a bug nor anyone's error -- two correct rules meeting. #1240 CLOSED. Verified before signing by printing the operands on both refs rather than counting them, after a count instrument returned 0 on a string the printed lines visibly contained: origin/main _FHIR_TYPE_RE = re.compile(r"^[A-Za-z]+$") PR #379 head _FHIR_TYPE_RE = re.compile(r"^[A-Za-z]+\Z") $ -> \Z on the two pattern definitions, call sites unchanged. That is the durable form: it covers all three call sites at once and cannot be re-broken by a future caller, where converting the calls to .fullmatch would fix three and leave a fourth free to reintroduce it. The read-path _reject_control_chars limb was deliberately not added -- redundant once the gates are strict, and it would reintroduce duplication that #1239 records as retired. The item also records that the obvious regression test cannot discriminate: _resolve_read_url strips, so "Patient/123\n" yields an identical URL before and after the fix and only "Patient\n/123" flips. Measured by executing the shipped and patched sources, not argued. #1241 STAYS OPEN, amended to record partial progress. #379 fixed construction-time screening plus a wrong-exception-class defect worse than the filed finding -- http.client.InvalidURL derives from HTTPException, not ValueError and not OSError, so it escaped every except arm in _post including the backstop written for that case. Still outstanding: transports/dicomweb.py, which the item names, and a second unscreened url-construction site in FhirLookupExecutor in the same file. The item's subject is the ASYMMETRY, so one sink screened while a sibling is not reproduces the very defect being reported. A partial close would be wrong. Two corrections to #1241's filed text, neither reducing severity: its comparison clause INVERTS rather than going stale, because the neighbouring path it called "weaker but at least screening" was removed outright, leaving :431 the only unencoded interpolation in the file; and its enum rationale is right advice for the wrong reason, since containment comes from the !r conversion rather than the enum's closedness. Controls: parse_items 281 items / 206 open / 75 closed before, 281 / 205 / 76 after -- 0 / -1 / +1, the expected delta for exactly one close and one amendment. backlog_status_check green, every item declaring exactly one status. Banner invariant checked per item: #1240 one closed-alphabet character and zero open, #1241 zero closed and one open.
wshallwshall
enabled auto-merge (squash)
August 13, 2026 22:49
wshallwshall
disabled auto-merge
August 13, 2026 23:59
…ruction (BACKLOG #1241) Completes #1241's second named file. dicomweb.py already had the right helper and the right contract -- _reject_url_control_chars raises ValueError at construction -- and applied it to exactly ONE operator setting, study_uid. Three others reached the same wire unscreened: url scheme-checked only headers merged into the request headers verbatim, NAMES as well as values bearer_token interpolated into Authorization verbatim Header NAMES are screened as well as values because both halves land on the wire, so a CRLF in either splits the request. The header sink is the one that needs this most: a URL has incidental neutralisation downstream (urllib.parse.unwrap strips a trailing CRLF, Request.full_url splits at '#' client-side) and a header value has none -- nothing strips or re-encodes it. Screened at CONSTRUCTION, matching the existing study_uid treatment and the fhir.py sibling in this same item: a bad MESSAGE dead-letters one message, a bad SETTING is wrong for every message the connection will ever send, so it fails the connection at load rather than dead-lettering an unbounded stream of messages that were never at fault. The inconsistency is the interesting part and worth recording: the file was not missing the concept, the helper, or the contract. It had all three and applied them to one of four settings. A reader auditing "does dicomweb screen its config?" finds study_uid screened and can reasonably stop. Red-first: all six new cases failed with "DID NOT RAISE ValueError". POSITIVE CONTROL SHIPPED: clean operator headers still construct and are preserved verbatim on the destination, so the screen cannot pass by rejecting everything. Verified, with scope stated: ruff format and ruff check clean on both changed files; mypy strict clean on transports/dicomweb.py; pytest over test_dicomweb, test_dicom_wiring, test_fhir_transport, test_fhir_lookup and test_transports = 271 passed, 1 skipped, in the lane venv built against constraints.lock (ruff 0.15.22, matching the pin). THE FULL SUITE WAS NOT RUN and the webconsole suite was not collected at all. Still open on #1241 and NOT closed by this: FhirLookupExecutor has a second unscreened url construction site in fhir.py, reported to the dispatcher as content rather than fixed here because it is outside what was dispatched. No ledger edit; banner flip withheld, disposition routes to the dispatcher.
wshallwshall
enabled auto-merge (squash)
August 14, 2026 00:00
wshallwshall
disabled auto-merge
August 14, 2026 00:25
…CKLOG #1241) The SECOND url construction site in this module. FhirDestination screens its own url and conditional_query; FhirLookupExecutor took a base url from the same operator config and checked only that it was a non-empty string with an http(s) scheme. THE ASYMMETRY IS THE ITEM'S SUBJECT, which is why this is not a separate concern. #1241 reports operator-config values reaching sinks with no construction-time screen. Screening one sink and leaving its sibling unscreened reproduces the defect being reported, in the same file, on the same setting name. Found only because an earlier edit to the destination matched TWO locations instead of one. It was reported to the dispatcher as content rather than fixed at the time, because it was outside what had been dispatched. The helper gained a `where` parameter so the message names WHICH construction site raised. It defaults to "destination", so the two existing call sites are unchanged in behaviour and the tests that match on "control character" are unaffected. That parameter exists because there are two sites and the reader of a load-time failure needs to know which one. Red-first: all three control-char cases failed with DID NOT RAISE ValueError against the unscreened constructor. POSITIVE CONTROL SHIPPED: a clean https url still constructs and the connection is registered, so the screen cannot pass by rejecting everything. Verified, with scope stated: ruff format --check and ruff check clean on both changed files; mypy strict clean on transports/fhir.py; pytest over test_fhir_lookup, test_fhir_transport, test_dicomweb, test_egress_allowlist and test_transports = 270 passed, 1 skipped, in the lane venv built against constraints.lock (ruff 0.15.22, matching the pin). THE FULL SUITE WAS NOT RUN and the webconsole suite was not collected. WHAT REMAINS OPEN ON #1241, so this commit is not read as closing it: nothing in this module that I have found. dicomweb.py was screened in 45293154, which is committed and anchored but did NOT reach PR #379 -- content-tested against the PR head, not inferred. Whether the item closes depends on that commit landing alongside these. No ledger edit; the banner flip is withheld and disposition routes to the dispatcher.
wshallwshall
enabled auto-merge (squash)
August 14, 2026 00:26
wshallwshall
added a commit
that referenced
this pull request
Aug 14, 2026
…n that are DONE (#386) * backlog: correct #1114's Severity line, which contradicted its own body The Severity line read "could submit unbounded messages". Four bounds ship ON and were re-verified at origin/main 96c9a86: DEFAULT_MAX_FRAME_BYTES 16 MiB (transports/mllp.py:105), DEFAULT_MAX_CONNECTIONS 256 (:106), DEFAULT_RECEIVE_TIMEOUT 60.0s (:107), and max_file_bytes (transports/file.py:384, remotefile.py:808). They bound SIZE and CONCURRENCY, not RATE. The item's own "What holds it short today" paragraph already said exactly that, two paragraphs above, so the Severity line was contradicting its own item rather than describing the engine. Corrected to "at an unbounded RATE"; the finding is unchanged and the item stays open. The correction runs in the direction that makes the engine look better, which is why the amendment states it explicitly: a Severity line is the sentence most often quoted onward without its body. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 277 items / 203 open, unchanged. * backlog: flip #1237 to shipped -- its fix landed without a ledger edit PR #372 merged the code as 96c9a86 and did not touch docs/BACKLOG.md, so the item read "not started" while its fix was on main. Banner repair, not a change of plan. Both stated limbs verified at origin/main 96c9a86 rather than inferred from the PR title: Signature -- an AST probe located all three functions, so it was not blind: gzip_decompress :101, deflate_decompress :138, zip_decompress :195 each carry max_output_bytes keyword-only with NO DEFAULT. That is the construct the item asked for, a gate that refuses when the precondition is absent, rather than a changed default value, which the parent item #1129 explicitly rules out. Tests -- tests/test_compression.py pins it: "Calling a decompressor without max_output_bytes is a TypeError, not an unbounded read", with a pytest.raises(TypeError) assertion. The re-exported public surface still resolves in messagefoundry/__init__.py and parsing/__init__.py. This closes NO ASVS cell and the amendment says so in the item. The verdict is the assessor's and the vault scorecard is the record of record; the "before uncompressing" reading question is unresolved without the pre-pass #1237 deliberately excluded, which remains unfiled and is an owner call. Controls: parse_items 277 items / 203 open / 74 closed before, 277 / 202 / 75 after -- 0 / -1 / +1, the expected delta for one close. backlog_status_check green, every item declaring exactly one status. Banner invariant checked on the item body: one closed-alphabet character, zero open-alphabet characters. * backlog: strike #1245's false delete clause, and narrow two overclaims #1245's SCOPE paragraph said the bootstrap account "cannot be renamed (update_user does not rename) or deleted, so it persists as a permanently disabled row". The rename half is correct. The delete half is FALSE. Reproduced end to end by a second session: DELETE /users/<bootstrap admin> returns 200 {'detail': 'deleted'}. Confirmed here independently from the code -- BOOTSTRAP_USERNAME appears 0 times in messagefoundry/api/auth_routes.py, with a positive control of 7 occurrences in auth/service.py so the probe discriminates. No delete-time guard names the bootstrap account. The only guard on that route is is_last_enabled_admin (auth_routes.py:714), which skips disabled users, and retirement is what disables this one. Why the correction matters more than the fact: "persists as a permanently disabled row" is what makes this read as an availability defect with no exit. There is an exit and it is destructive. A fix must not rest on the row being undeletable, and a reader checking this paragraph would otherwise re-conclude a related defect is unreachable and close it as impossible. Two narrower corrections in the same pass, both REDUCING what the item claims: "Silent at both ends" is half wrong. The 201 response body does carry disabled:true (auth_routes.py:656-658 re-reads after retirement, _user_summary sets it at :195). It is silent at the login end only, via the generic 401 that is deliberately indistinguishable from a wrong password. Login is not the sole retirement trigger: auth/service.py:518 fires on every service start and :2551 on create, so a regression test assuming :650 is the only path is narrower than the defect. Struck rather than deleted, so the wrong version stays visible to the next reader. The item stays open and its severity is unchanged. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 280 items / 205 open / 75 closed, unchanged. * backlog: retract my own #1245 narrowing -- it was measured on the wrong route Forty minutes ago I amended #1245 to say "silent at both ends" was half wrong, on the ground that a 201 response body carries disabled:true. That measurement is TRUE and it is about a DIFFERENT ROUTE. auth_routes.py:656-658 and _user_summary:195 are POST /users -- a create, and the stacked-admin-name defect's route. #1245 is a RESET defect. The reset route at auth_routes.py:753 ends at :776 with return PasswordResetResponse(temp_password=temp) no re-read, no _user_summary, no disabled field. And the stronger reason, which makes "silent at both ends" true in principle rather than by omission: admin_reset_password (auth/service.py:2717) does not call _retire_superseded_bootstrap at all. Its only three call sites are :518, :651 and :2551 -- positive control, the probe resolves real sites. So the re-arm is LATENT: at the moment the reset returns nothing has happened yet, there is no disabled state to report, and a re-read there would correctly say disabled:false. "Silent at both ends" therefore STANDS for this item. The create-path fact is real and belongs in the stacked-name item instead. Caught by the builder holding #1245, which re-measured rather than accepting a correction from the dispatcher. That is the second time today the two of us have hit the same shape in opposite directions: an instrument answering truthfully about the neighbouring question. Kept struck rather than deleted, because the two routes are adjacent in one file and the wrong version is what a later reader would re-derive. One correction from that pass DOES stand and is retained: login is not the sole retirement trigger (:518 on service start, :2551 on create), so a test assuming :651 is the only path is narrower than the defect. Recorded as already handled. SECOND DEFECT IN THIS SAME EDIT, caught by the before/after control and fixed before commit: the retraction was first written with a closed-alphabet character opening a blockquote in the item body. parse_items read it as a status banner and #1245 flipped to CLOSED -- 280/204/76 against an expected 280/205/75. A live item under active build, removed from the queue by a prose edit. The rule is absolute for exactly this reason: no banner-alphabet character in an item body, any position. Say the word. Controls after the fix: 280 items / 205 open / 75 closed, #1245 is_open True, zero closed-alphabet characters in the body, backlog_status_check green with every item declaring exactly one status. * backlog: close #1240, record #1241 as partial -- the ledger edit PR #379 cannot make itself PR #379 is red on a required check that says a PR implementing BACKLOG #N must update BACKLOG.md. The owner's 2026-08-13 ruling says a builder may resolve merge conflicts but may not author ledger content. Those two are mutually unsatisfiable for a compliant builder PR, so the builder correctly withheld the banner and the PR correctly went red. Authoring is dispatcher and lander only; this supplies the edit. Neither a bug nor anyone's error -- two correct rules meeting. #1240 CLOSED. Verified before signing by printing the operands on both refs rather than counting them, after a count instrument returned 0 on a string the printed lines visibly contained: origin/main _FHIR_TYPE_RE = re.compile(r"^[A-Za-z]+$") PR #379 head _FHIR_TYPE_RE = re.compile(r"^[A-Za-z]+\Z") $ -> \Z on the two pattern definitions, call sites unchanged. That is the durable form: it covers all three call sites at once and cannot be re-broken by a future caller, where converting the calls to .fullmatch would fix three and leave a fourth free to reintroduce it. The read-path _reject_control_chars limb was deliberately not added -- redundant once the gates are strict, and it would reintroduce duplication that #1239 records as retired. The item also records that the obvious regression test cannot discriminate: _resolve_read_url strips, so "Patient/123\n" yields an identical URL before and after the fix and only "Patient\n/123" flips. Measured by executing the shipped and patched sources, not argued. #1241 STAYS OPEN, amended to record partial progress. #379 fixed construction-time screening plus a wrong-exception-class defect worse than the filed finding -- http.client.InvalidURL derives from HTTPException, not ValueError and not OSError, so it escaped every except arm in _post including the backstop written for that case. Still outstanding: transports/dicomweb.py, which the item names, and a second unscreened url-construction site in FhirLookupExecutor in the same file. The item's subject is the ASYMMETRY, so one sink screened while a sibling is not reproduces the very defect being reported. A partial close would be wrong. Two corrections to #1241's filed text, neither reducing severity: its comparison clause INVERTS rather than going stale, because the neighbouring path it called "weaker but at least screening" was removed outright, leaving :431 the only unencoded interpolation in the file; and its enum rationale is right advice for the wrong reason, since containment comes from the !r conversion rather than the enum's closedness. Controls: parse_items 281 items / 206 open / 75 closed before, 281 / 205 / 76 after -- 0 / -1 / +1, the expected delta for exactly one close and one amendment. backlog_status_check green, every item declaring exactly one status. Banner invariant checked per item: #1240 one closed-alphabet character and zero open, #1241 zero closed and one open. * backlog: flip #1204 to shipped, and record that #1203 is abandoned rather than free #1204's banner read OPEN while its own body said "FIXED in the same change" and its Verdict line said "build (done)". Banner repair, not a change of plan. Verified at origin/main before signing, with a discriminating control. All four artifacts ship: scripts/docs/asvs_tally_lint.py, scripts/docs/asvs_tally_baseline.txt, .github/workflows/asvs-tally-lint.yml, tests/test_asvs_tally_lint.py. A deliberately impossible path under the same probe returned ABSENT, so the four PRESENTs are evidence rather than a probe that answers yes to everything. One defect found while verifying, and it is not what it first looks like. asvs-tally-lint.yml:3 cites BACKLOG #1203; the item it implements is #1204. The obvious reading is a typo pointing at an unissued number, and that reading is wrong in the direction that causes harm: it would send someone to file #1203 as free. Measured: "## 1203." appears in neither docs/BACKLOG.md nor docs/archive/backlog/BACKLOG-CLOSED.md, with "## 1204." resolving in the live ledger as the positive control. But the allocator record mefor-coord/alloc/backlog/1203.json EXISTS, titled "Decide how the public engine repo obtains the private ASVS scorecard for --prove-absences". So #1203 is ALLOCATED AND NEVER FILED -- abandoned, not free. Numbers are never reclaimed and holes are free, so the hole costs nothing. What costs something is that nothing reports an allocated-but-unfiled number, and a live citation makes it look issued. The :3 correction to #1204 is a one-word fix and rides with whatever next touches that workflow. Controls: parse_items 281 items / 205 open / 76 closed before, 281 / 204 / 77 after -- 0 / -1 / +1, the expected delta for one close. backlog_status_check green, every item declaring exactly one status. Banner invariant on the item body: one closed-alphabet character, zero open-alphabet. * docs(adr): ADR 0165 -- a builder PR satisfies the ledger gate with a paired commit Records a coordination decision that until now existed only in session messages and a queue file, which is the exact shape this project keeps being bitten by -- a ruling with no artifact behind it. THE COLLISION. The required check "a PR that implements BACKLOG #N must update BACKLOG.md" demands a ledger edit in the PR's own diff. The owner's 2026-08-13 authoring ruling forbids a BUILDER to author ledger content, on the property that a mechanical union cannot invent a disposition but authoring a banner can, and a seat that can author its own item's banner can turn its own PR green. Composed, a compliant builder PR cannot pass a required check. Measured live: PR #379 went red for obeying the ruling. THE DECISION. The Dispatcher or Lander authors the disposition and the commit rides on the PR branch. Owner-ruled a1. THE PART THAT INVERTED ON MEASUREMENT. Reading the gate rather than reasoning about it: backlog-hygiene.yml:64-98 computes a three-dot diff and passes if the changed set touches docs/BACKLOG.md or docs/archive/backlog/. It never inspects authorship. Evaluated against the real cherry-picked head for #379 -- touches_code 1, ledger 1, PASS. So the pattern was in force before it was named, no gate change was required, and none is pending. The ledger gate permits the cherry-pick for a non-obvious reason: it iterates headings added relative to base, and a banner flip or amendment on an item already on main adds no "## N." heading, so ownership is never consulted and the committing seat is irrelevant. Holds only for landed items; a PR that FILES an item is a different shape. REJECTED, with reasons rather than preferences. (a2) separately-landed plus cross-branch correlation would undo a deliberate control -- the gate uses three-dot on purpose and its own comment says two-dot "would pass while enforcing nothing". (b) a builder carve-out to flip its own banner reopens the self-approval hazard. (c) as a distinct interim dissolved: it is the same mechanism, so there is no transition. RECORDED NEAR-MISS, kept rather than deleted because the wrong version is what a later reader re-derives: the ruling was briefly written as "(c) is fine until (a) lands" -- an expiry whose trigger had ALREADY FIRED. It looks like the safe construction and behaves like the unsafe one, becoming permanent by default while appearing bounded. Provenance is split three ways in the ADR because each half is only checkable if attributed: the collision found by the Lander on #379's red check, the self-approval property by Builder 2, the gate measurement and the no-build finding by the Dispatcher, the ruling by the owner. ADR number allocated atomically to this worktree; index row added in the same commit, as the ledger gate requires. No engine behaviour changes. Note for whoever integrates: docs/adr/README.md is an APPEND/APPEND conflict with claude/builder-seat-playbook-bf2ead, which appends ADR 0164's row at the same tail. Both rows are additive and disjoint -- take both sides. * backlog: measure one limb of #1143's research question, and fix a blank-line citation #1143 asks what identification keyed on the IdP-namespaced subject would actually require across all three store backends. One limb of that is now measured rather than left to be re-derived by whoever picks the research up. MEASURED at origin/main, with a discriminating control: UNIQUE index or index naming oidc_issuer / oidc_subject: store.py 0 postgres.py 0 sqlserver.py 0 positive control: "UNIQUE" appears 13 times in store.py, so the probe discriminates and the three zeroes are real absences column types today: postgres.py:531-532 oidc_issuer TEXT, oidc_subject TEXT sqlserver.py:1357 oidc_issuer NVARCHAR(MAX) NULL, oidc_subject NVARCHAR(MAX) NULL So the federated columns exist and carry no uniqueness constraint of any kind. An (issuer, subject) identity key is therefore not a code-only change: it needs a unique index on all three backends, and on SQL Server NVARCHAR(MAX) cannot be an index key column at all, so both columns must first be re-typed to a bounded NVARCHAR(n). That is a second migration on that backend. That cost is an INPUT to choosing between the candidate designs rather than a consequence of having chosen one, which is why it belongs in the item before the research runs rather than after. CITATION FIX in the same pass: the banner cites store.py:1590 for users.username. :1590 is a BLANK LINE; the declaration "username TEXT NOT NULL UNIQUE" is at :1593. Found independently by two seats, so it is recorded rather than quietly patched. Explicitly NOT settled, and stated in the item: the ceremony for the first federated login of an account that predates federation. That remains the item's hard question and nothing above touches it. A migration cost informs that decision; it does not answer it. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged. * backlog: audit #1020's directory path -- it widens, and it invalidates one candidate fix #1020 said "the AD/OIDC-provisioned path in auth/reconcile.py was not audited, so the finding may narrow to local accounts". Audited now. Wrong file, and it widens rather than narrows. Every link opened individually, because a chain of separately-verified links is not a verified chain: authenticate_oidc auth/service.py:942 -> returns _complete_ad_login :1056 _complete_ad_login auth/service.py:1081 -> calls _upsert_ad_user :1107 _upsert_ad_user auth/service.py:1209 -> update_user_profile(email=principal.email) update_user_profile store/store.py:7742 -> UPDATE users SET display_name=?, email=?, ... Both the AD and the OIDC login paths provision through the SAME function, _upsert_ad_user -- not auth/reconcile.py, which the struck sentence names. authenticate_oidc returns _complete_ad_login directly, so one provisioning path serves two providers. The sharp end is the unconditional write. update_user_profile issues UPDATE users SET display_name=?, email=? with no conditional and no coalesce, and _upsert_ad_user calls it on every directory login with whatever the directory asserted. So a directory-sourced account cannot retain a hand-set address: an operator who sets one via PATCH /users/{id} has it overwritten at the account holder's next login. LDAP mail is optional at every layer, so where the directory asserts nothing the address returns to NULL. That invalidates one of the item's three candidate fixes. "Add a self-service email field" does not reach directory-provisioned accounts at all -- whatever the user sets is overwritten on their next login by the same unconditional write. Any fix gating on "a privileged account must have a deliverable address" needs a separate answer for the directory-sourced population, which moves it into the owner's decision rather than leaving it an implementation detail underneath. Deliberately NOT re-litigated: "Difficulty 3, no schema change, no migration cost" may still hold for the local-account half, and nobody has measured it for the directory half. The amendment says so rather than quietly widening the estimate. Also removed a pre-existing closed-alphabet character from this item's BODY at the old :4111. It was not flipping the item -- the counts were identical before and after -- but the rule is absolute for exactly that reason: position decides whether it parses as a status banner, and four items were mis-parsed by this class today. Replaced with the word. Provenance: the widening was measured by the Builder 2 seat during a blind re-verification pass; the chain above was re-read link by link here before being written into the ledger. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged. * backlog: record #1020's owner ruling, and correct the fix location it points at Owner ruled option (b): gate startup on a deliverable channel. Recorded in the item because until now the ruling existed only in session messages, which is the failure mode this session has been correcting all evening -- a decision with no artifact behind it. The deciding argument is recorded as the reason rather than only the choice: (b) is the only option that does not rest on an operator action. That is decisive because update_user_profile issues UPDATE users SET display_name=?, email=? with no conditional and no coalesce, on every directory login (store.py:7742), so any address a human sets on an AD or OIDC account is overwritten at the account holder's next login. A fix depending on someone setting an address cannot cover that population. The item's stated fix location is wrong and a builder would walk into it. The text points at __main__.py:2259, but _serve is synchronous and opens no store -- probing its full range for open_store|AuthService|list_users|count_users returns one hit and it is a comment, and uvicorn.run is at :2827 so the lifespan bootstrap has not run. The only place the store and the fresh bootstrap admin are both in hand is the ASGI lifespan at api/app.py:~5852. Option (c) is recorded as population-limited, NOT defective, and the amendment says the ruling must not be cited as a finding that it was broken. A self-service email field works for local accounts and is silently overwritten for directory ones. The incompleteness was invisible to the operator, which is more useful than "it was wrong" -- an earlier framing of mine that I withdrew. Recorded as unpriced rather than carried forward: whether "Difficulty 3, no schema change, no migration cost" still holds for the directory half. It may hold for the local half; nobody has measured the directory half. A stale difficulty estimate silently sets a lane's expectations. The build is NOT dispatched. The pool is at HOLD NEW WORK / PROTECT AND WRAP, and a builder taking this would be starting a new item and a new claim, which that state prohibits. Recording the ruling is work in hand; building it is not. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged. * backlog: record #1217 half 1 as built, half 2 outstanding -- the item stays open PR #383 is red on "a PR that implements BACKLOG #N must update BACKLOG.md". The builder withheld the banner correctly under the owner's authoring ruling, so the ledger edit is the dispatcher's. This supplies it. Same shape as #1241 on #379, and ADR 0165 records why that is the standing pattern. Half 1, the >=1 floor, is BUILT. Verified on the branch against origin/main rather than taken from the report: origin/main retry_max_attempts: int | None = 100 PR #383 retry_max_attempts: int | None = Field(default=100, ge=1) A configured 0 or negative is now refused at load rather than loading clean and dead-lettering on the FIRST failure -- the delivery check is item.attempts >= max_attempts against a post-increment count (pipeline/wiring_runner.py:5040), so 0 meant give-up-now while reading like "no limit". The floor is on the OPERATOR-FACING setting only, and that is deliberate. RetryPolicy(max_attempts=0) remains a live internal idiom for a permanent no-retry failure: measured across 5 files, including store.mark_failed call sites and asserted by tests at tests/test_batch_completion.py:206-208 and tests/test_postgres_store.py:3109. Constraining the dataclass instead would have deleted a used mechanism while claiming to add a guard -- the reads-as-hardening-but-removes-a-control shape. The item's stated reason for deferring the floor is answered rather than ignored. It said the floor was documented and not fixed "because a floor changes the accepted-configuration set". Under section 0 there are zero deployments, so there is no accepted configuration to break and no migration cost to protect. STILL OPEN, and it is why the item does not close: whether the retry-forever posture needs a TOML or env spelling. "", none and null all raise ValidationError, so that posture is reachable in code-first configuration only. It is a product question, it was handed back rather than decided, and the item itself says it should be decided alongside the floor. A closure on #383 would answer it by omission. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged, and #1217 verified still OPEN after the edit. * backlog: #1242's loss is irreversible, not merely expensive -- and a relay's framing corrected The ASVS Tracker surfaced this defect via the Liaison as a candidate for a NEW item. It is not new: it is #1242, filed 2026-08-13, and Builder 2 is building it. No number was allocated. A duplicate ledger row is not a harmless extra line -- it splits the work and the second number looks unbuilt forever, because the fix lands under the first. Two things in the relay were genuinely new. One is recorded here; the other is deliberately not. RECORDED: the loss is not recoverable by re-running the derivation. The anchors most at risk are the D3 backfill's, and their warrant was that two independent derivations AGREED, measured at two different refs. Those refs have moved. So a fresh derivation reproduces values without reproducing the agreement that justified writing them, and that agreement is the whole evidentiary content. The loss is therefore irreversible rather than expensive, which is why this item outranks other writer defects rather than being one among them. Nothing in the item said this. NOT RECORDED, deliberately: the supporting anchor counts. docs/BACKLOG.md is public, and a tally over a closed public requirement set is the shape that hands out coverage by subtraction. The mechanism is fully stated without them -- a reader with vault access can price it, and a reader without one still knows what to fix and why. Verified my added lines carry no such figure, with a positive control proving the scan discriminates. AND THE RELAY'S FRAMING IS CORRECTED BY THE ITEM'S OWN TEXT. It described the defect as dropping sym/ctx. #1242 already forbids fixing it that way: the defect is the handling of UNKNOWN keys, and a fix special-casing those two by name rebuilds the same trap for the next field added. The item was ahead of the relay and a builder must follow the item. The Tracker's core claim was verified here rather than taken: sym and ctx each return 0 occurrences in scripts/asvs/apply.py at origin/main, positive control expect returns 2. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged, and #1242 verified still OPEN. * backlog: close #1238 and #1239 on the code, and file #1253 so the closure does not retire the hazard Builder 2 reported four banners owed. Two of the four -- #1237 and #1204 -- were already authored on this branch and are invisible to that lane because ledger authorship and push are held by different seats. That is recorded as a finding in the episode note; it is not fixed here. The two genuinely owed are written now. Both closures verified against origin/main rather than against the build report, because a banner that closes on a report inherits the report's errors. #1238: _is_contained_name is defined at transports/remotefile.py:97, wired at :954, and asserted in BOTH polarities at tests/test_remotefile_transport.py:1185 and :1195. The one-polarity case is called out because such a test passes against a function that refuses everything. posixpath.basename() was not used, per the owner ruling. #1239: _has_control_char returns 0 occurrences across messagefoundry/ at origin/main, so the pair it named is a single. The item's own condition is met. #1253 exists because closing #1239 there would have been true of the item as written and false of the hazard it describes. The reporting lane amended its own closure recommendation to say so -- the predicate is copied more widely today than when #1239 was filed, partly by the work that resolved it. A repo-wide re-measure widened that further: the amendment scanned transports/ and found five sites across four files; across messagefoundry/ it is seven across six, the two extra being config/codeset_edit.py:305 and config/impact.py:631. Two exclusions are recorded in the item so a later scan does not re-add them: rest.py:109 matches a naive grep but is prose in a docstring, and sniff.py:179 tests the same code points through a genuinely different byte-wise predicate that subtracts an allowlist, so folding it in would change its behaviour. rest.py:111 strips where the others reject. That is recorded as defensible and NOT as a second instance of the pattern the owner ruled against in #1238, so the next reader does not inherit a false lead: stripping CR/LF from a header value cannot redirect a request, whereas basename() mutates a path into a real and different target. #1253 allocated with alloc.ps1, never grepped. parse_items before and after: 281/204/77 -> 282/203/79, matching the predicted delta for two closures plus one filing, with a control confirming no item carries a stray banner. * backlog: record that #1234 is not startable from main -- its subject has not landed Builder 2 refused a restock offer of #1234 by correcting its OWN earlier recommendation, and verified before claiming rather than after. Re-measured here rather than taken: at origin/main, require_least_privilege returns 0 hits in any .py and appears only in this ledger's own prose. Positive control on the same instrument, require_managed_identity, returns hits across four .py files, so the scan sees Python fine -- the zero is a fact about the tree, not a broken needle. The probe this item reports a defect in exists only on the w3-store-privilege-preflight branch, dormant at that reading. The amendment is careful not to overrule the item's own "independent of #1008" paragraph, because that paragraph is right about a different thing. Both halves hold: the item is not hostage to #1008's POLICY ruling, and it is nonetheless unbuildable by any lane working from main until that BRANCH lands. Collapsing the two would either re-gate a code defect behind a demand gate or keep offering work whose subject does not exist. This is the same wasted-claim cost as #1253's provenance, one layer deeper: there, an item was unstartable because the FIX had already landed; here, because the SUBJECT has not. A banner-driven queue cannot distinguish either case from startable work, which is why both are now written down where the next dispatcher reads rather than left in session mail. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 282 items / 203 open / 79 closed, unchanged, and #1234 verified still OPEN. * backlog: file #1254 -- a required check is named for its subject, not its assertion Handed to me by the Liaison to number if I judged it worth one, in the Lander's framing. It is, because the naive fix is dangerous and nothing currently records that. MEASURED INSTANCE: the Windows leg went red under the label "test (windows-2025, py3.14)", which reads as "the tests failed on Windows". The tests passed. What failed was the wall-clock gate "Step margin -- both gated steps" (ci.yml:675). The check answered its own question truthfully and the NAME described a different one -- the reverse of the shape this project keeps hitting, where the label is honest and the instrument is not. The job name is built at ci.yml:42 from the matrix, so all three legs are named for WHERE they ran and never for WHAT they assert, while holding at least three independent assertions. Stated as at least three rather than enumerated. WHY THIS IS NOT A ONE-LINE RENAME, which is the whole reason it needed writing down: those three strings ARE required contexts. They are listed in .github/required-contexts.txt, asserted against branch protection by tests/test_required_contexts.py, and matched BY NAME on the GitHub side. A required-but-absent context blocks every PR forever, so a rename is one atomic change across the workflow, the contexts file, that test's pinned count, and the branch-protection setting, in the order that file's header prescribes. So the item deliberately does NOT recommend the rename. It prices three options and names the cheapest first: make the margin gate's failure output say in its first line that the suite passed and a timing gate fired. That costs nothing and cannot wedge the repo. The rename is listed third. Severity carries no deployment axis, but the near-miss is recorded: the misreading pointed at the wall-clock cap, and #1096's banner already says the actual fix is #320 and that re-deriving the caps is itself the failure mode. #1254 allocated with alloc.ps1, never grepped. parse_items before and after: 282/203/79 -> 283/204/79, matching the predicted delta for one filing, with a control confirming no item carries a stray banner. * backlog: amend #1235 -- its two named instances are inert, and I dispatched the opposite Builder 2 refused the starting fact I gave it and measured instead. It was right and I was backwards. I told it to start #1235 from #1203 as a CONFIRMED LIVE TRAP, reasoning that an allocation record with no ledger entry meant the number was free. The record is what makes the number permanently UNAVAILABLE. Verified here rather than taken: 1203.json and 1231.json both exist in .git/mefor-coord/alloc/backlog/ (claimed 2026-08-09 and 2026-08-12), and alloc.ps1 has no release at all -- :41 "a one-way door -- claims are never released", :24 "numbers are never reclaimed ... holes are free, collisions are not". So the item's own text is wrong where it says #1231 was "allocated and released without being filed", and wrong that the pair is "defused only by an accident of timing". They are defused by construction. ONE CORRECTION AGAINST THE REPORT AS WELL, because its reason is weaker than its conclusion. The argument as relayed rests on the allocation RECORDS existing. Those live under .git: uncommittable, machine-local, losable without trace. The reason that survives their loss is structural -- alloc.ps1 issues $observed + 1 (:392, and :389 under the public floor clamp) and NEVER fills a hole, so a number below the floor is unreachable whether or not its record still exists. Recording the registry as the protection would make a sound property look fragile and invite a guard nothing needs. The live shape is the other one and the item now says so: a citation to a number NEVER allocated sits above the floor and will be issued in the normal course. A detector reading only the ledgers rates the two states identically, which over docs/ in this repo mis-scores 26 reserved citations as live; of the 6 genuinely never-allocated tokens there, all six are foreign references, so this repo holds zero genuine instances. The private-repo population the item was filed against is not re-measured here and is stated as separate. The remedy is unchanged and still correct. Only the account of WHY the two named instances are harmless is corrected. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 283 items / 204 open / 79 closed, unchanged, and #1235 verified still OPEN. * backlog: correct #1235 and #1254 -- two of my own committed claims, falsified under adversarial check I ran six independent skeptics over every claim I committed tonight, against the SIMULATED POST-MERGE TREE rather than my branch, because my branch was six behind origin/main and the merge is CLEAN -- git conflicts on concurrent edits, never on invalidated claims. Four claims held. Two did not. Both failures are my authoring, not merge drift: all cited files are byte-identical across the merge tree, origin/main and HEAD. #1235. The conclusion survives, the reason did not. I had written that the allocation registry is irrelevant because the mechanism is structural. That is FALSE for the newest number: the high-water ratchet persists $floor, the maximum of the OBSERVED set (:205, :214, :215), NOT the number being issued, so after issuing N it holds N-1. alloc.ps1 only PRINTS the heading, so until it is committed the sole durable record of N is its own untracked, never-pushed <n>.json. Lose that and the next run re-issues N. So the registry is exactly what protects a just-allocated-but-unfiled number -- the state #1203 and #1231 were both in when allocated. What actually makes those two unreachable is a CONJUNCTION, now stated as one: the loop never searches downward (:392, :389, :394), AND the floor is computed from COMMITTED LEDGER HEADINGS. Measured non-destructively with -ShowFloor: floor 1254, swept from docs/BACKLOG.md and the closed archive. Those are tracked content on refs, they survive a fresh clone, and both numbers sit far below them. Recorded the public-floor clamp as a THIRD, separate guarantee about the output range, with the caveat that its own anti-lowering ratchet lives in the same untracked directory and is disarmed on a registry-absent clone. #1254 cited the margin gate as ci.yml:675. That is a clock MARK (step_margin.py --mark between, :677). The gate is :765, if: always(), invoking at :778 and :781. The error is worth recording rather than silently fixing: I opened :675, found a step whose name NEARLY matched, and adopted it instead of treating the near-match as the signal the line was wrong. A near-miss terminates the search; no match would have continued it. Also corrected in #1254: tests/test_required_contexts.py does NOT call the GitHub API. It pins the count at :101 and resolves contexts against real workflow job names at :107; the branch-protection comparison is a HUMAN step in the comment at :100. The item's central argument is unaffected -- the strings are still required contexts and a rename still resolves them to no job -- but the evidence now says what the test does. The four that held: #1253's seven-sites-across-six-files with both exclusions and every line number, #1239's zero occurrences, #1238's defined-wired-and-both-test- polarities, and #1234's zero .py hits with its positive control. Amendments only, no heading added, so ledger ownership is not consulted. parse_items unchanged at 283 items / 204 open / 79 closed. * backlog: file #1255 -- two testpaths ship a top-level conftest each Diagnosed by the lane whose own commit tripped it, and filed here because the collision outlives that commit. Seven tests in one file failed in a full run and passed in isolation, twice. Cause: pyproject sets two testpaths, both directories contain a conftest.py, neither contains an __init__.py, so both claim the top-level module name and a bare `import conftest` binds to whichever loaded first. Verified at origin/main rather than taken from the report: both conftest.py files present, both __init__.py absent, and a scan for `import conftest` / `from conftest import` across both trees returns ZERO hits. That zero is why this is filed as LATENT rather than live -- the collision is real and currently untripped, so nothing is failing today and the item must not be cited as a current gap. The signature is recorded because it mis-attributes itself: the mis-bound import surfaces as an AttributeError naming a module path from the WRONG package, not as an ImportError, so it reads as a missing attribute rather than a bad import. Two things the item forbids, both because a plausible fix is worse than the defect. Do not import conftest BY PATH -- its body claims a per-process test slot and registers an atexit unlink, so a second import under another name has side effects. And do not prove a fix in isolation: isolation is precisely the condition under which this defect reports success. The proof has to run both testpaths together and then restore the bare import to confirm the same command fails again. The house idiom already solves it -- tests/_workflow_contexts.py is imported package-qualified at tests/_negative_controls.py:35 -- so the scope is to make the name unambiguous, not to invent a mechanism. #1255 allocated with alloc.ps1, never grepped. parse_items before and after: 283/204/79 -> 284/205/79, matching the predicted delta for one filing, with a control confirming no item carries a stray banner. * backlog: record that PR #382 closed ONE of #1242's four limbs -- the item stays open I told a builder its engine fix closed this item's mechanism and to claim it. That was wrong, and this records the correction where the next reader will hit it rather than in session mail. #382 merged 2026-08-13 with this item's number in its title and a title that faithfully describes what it fixed: the payload-only TOP-LEVEL key limb. Verified at origin/main -- apply.py:97 now walks {**(live or {}), **cell}.items() rather than the live cell alone. That limb is genuinely closed. The limb carrying the item's severity is untouched, and the same revision shows why the union cannot reach it: :98 skips _ORDERED and _SUBTABLES BEFORE the union at :97 is consulted for them, and evidence entries are re-emitted at :101-105 by enumerating exactly path, line and expect (absence at :106-110 by exactly pattern, positive_control, mutation). A key inside a [[cell.evidence]] entry is still dropped -- which is exactly where the backfill put the affected keys, on evidence ENTRIES and not on top-level keys. The top-level table-mangling limb also appears untouched. I had additionally told that builder to stop measuring the two affected key names because the item forbids fixing by naming them. The prohibition is real but I applied it to the wrong activity: the item forbids naming them in a FIX, not measuring their absence as a SYMPTOM, and their absence from the writer is exactly the evidence that the sub-table limb still bites. Recorded as the PARTIAL-MOVE shape, which is the reusable part: a merged PR bearing an item's number, whose title truthfully describes what it fixed, is the strongest available signal the item is done. Verify-before-closing is not enough on its own here -- the verification has to ask WHICH HALF. The item's own proof condition is the discriminator and is unchanged: put an unknown key INSIDE an evidence entry, re-render, assert both that it survives and that the guard refuses when it is deliberately dropped. #382 does not satisfy it, and no test asserting only top-level carry-through will. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 284 items / 205 open / 79 closed, unchanged, and #1242 verified still OPEN. * backlog: record #1242's BASE REQUIREMENT -- the obvious base for limb 4 reverts limb 3 Builder 2 confirmed limb 4 as I described it, then refused to build it and handed it back with a reason better than the instruction I gave. Recording the reason, because it is not discoverable from the item and git will not raise it. Its branch still carried scripts/asvs/apply.py:88 as if key in _ORDERED or key in _SUBTABLES or key in cell: Verified here against that ref rather than taken: the clause is present there and ABSENT at origin/main. `or key in cell` is precisely what #382 deleted to fix limb 3. So a limb-4 fix authored on that base and merged would carry limb 3's REVERSAL in the same diff -- no conflict, no marker, every check green, and the item's own landed fix undone by the commit claiming to extend it. Git raises nothing here because git conflicts on concurrent edits to the same lines, never on a stale base re-asserting a clause that was deleted elsewhere. That is the same family as the clean-merge hazard this session has been working under all night, arriving from the direction nobody watches: not a doc invalidated by a merge, but a FIX reverted by an extension of itself. The item now states the base requirement and a pre-PR check that discriminates: branch fresh from current origin/main, and confirm `or key in cell` returns zero hits in the diff's own version of the file before opening a PR. Also corrected upstream of this, in my own dispatch rather than the ledger: I had told that lane to stop measuring the two affected key names. Measuring their absence as a SYMPTOM was always legitimate; only fixing by naming them is forbidden. It withdrew its acceptance of my earlier "bound lifted" on the grounds that it had taken it from me without measuring -- correctly. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 284 items / 205 open / 79 closed, unchanged, and #1242 verified still OPEN.
wshallwshall
added a commit
that referenced
this pull request
Aug 14, 2026
…30 on its ruled half (#388) * backlog: correct #1114's Severity line, which contradicted its own body The Severity line read "could submit unbounded messages". Four bounds ship ON and were re-verified at origin/main 96c9a860: DEFAULT_MAX_FRAME_BYTES 16 MiB (transports/mllp.py:105), DEFAULT_MAX_CONNECTIONS 256 (:106), DEFAULT_RECEIVE_TIMEOUT 60.0s (:107), and max_file_bytes (transports/file.py:384, remotefile.py:808). They bound SIZE and CONCURRENCY, not RATE. The item's own "What holds it short today" paragraph already said exactly that, two paragraphs above, so the Severity line was contradicting its own item rather than describing the engine. Corrected to "at an unbounded RATE"; the finding is unchanged and the item stays open. The correction runs in the direction that makes the engine look better, which is why the amendment states it explicitly: a Severity line is the sentence most often quoted onward without its body. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 277 items / 203 open, unchanged. * backlog: flip #1237 to shipped -- its fix landed without a ledger edit PR #372 merged the code as 96c9a860 and did not touch docs/BACKLOG.md, so the item read "not started" while its fix was on main. Banner repair, not a change of plan. Both stated limbs verified at origin/main 96c9a860 rather than inferred from the PR title: Signature -- an AST probe located all three functions, so it was not blind: gzip_decompress :101, deflate_decompress :138, zip_decompress :195 each carry max_output_bytes keyword-only with NO DEFAULT. That is the construct the item asked for, a gate that refuses when the precondition is absent, rather than a changed default value, which the parent item #1129 explicitly rules out. Tests -- tests/test_compression.py pins it: "Calling a decompressor without max_output_bytes is a TypeError, not an unbounded read", with a pytest.raises(TypeError) assertion. The re-exported public surface still resolves in messagefoundry/__init__.py and parsing/__init__.py. This closes NO ASVS cell and the amendment says so in the item. The verdict is the assessor's and the vault scorecard is the record of record; the "before uncompressing" reading question is unresolved without the pre-pass #1237 deliberately excluded, which remains unfiled and is an owner call. Controls: parse_items 277 items / 203 open / 74 closed before, 277 / 202 / 75 after -- 0 / -1 / +1, the expected delta for one close. backlog_status_check green, every item declaring exactly one status. Banner invariant checked on the item body: one closed-alphabet character, zero open-alphabet characters. * backlog: strike #1245's false delete clause, and narrow two overclaims #1245's SCOPE paragraph said the bootstrap account "cannot be renamed (update_user does not rename) or deleted, so it persists as a permanently disabled row". The rename half is correct. The delete half is FALSE. Reproduced end to end by a second session: DELETE /users/<bootstrap admin> returns 200 {'detail': 'deleted'}. Confirmed here independently from the code -- BOOTSTRAP_USERNAME appears 0 times in messagefoundry/api/auth_routes.py, with a positive control of 7 occurrences in auth/service.py so the probe discriminates. No delete-time guard names the bootstrap account. The only guard on that route is is_last_enabled_admin (auth_routes.py:714), which skips disabled users, and retirement is what disables this one. Why the correction matters more than the fact: "persists as a permanently disabled row" is what makes this read as an availability defect with no exit. There is an exit and it is destructive. A fix must not rest on the row being undeletable, and a reader checking this paragraph would otherwise re-conclude a related defect is unreachable and close it as impossible. Two narrower corrections in the same pass, both REDUCING what the item claims: "Silent at both ends" is half wrong. The 201 response body does carry disabled:true (auth_routes.py:656-658 re-reads after retirement, _user_summary sets it at :195). It is silent at the login end only, via the generic 401 that is deliberately indistinguishable from a wrong password. Login is not the sole retirement trigger: auth/service.py:518 fires on every service start and :2551 on create, so a regression test assuming :650 is the only path is narrower than the defect. Struck rather than deleted, so the wrong version stays visible to the next reader. The item stays open and its severity is unchanged. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 280 items / 205 open / 75 closed, unchanged. * backlog: retract my own #1245 narrowing -- it was measured on the wrong route Forty minutes ago I amended #1245 to say "silent at both ends" was half wrong, on the ground that a 201 response body carries disabled:true. That measurement is TRUE and it is about a DIFFERENT ROUTE. auth_routes.py:656-658 and _user_summary:195 are POST /users -- a create, and the stacked-admin-name defect's route. #1245 is a RESET defect. The reset route at auth_routes.py:753 ends at :776 with return PasswordResetResponse(temp_password=temp) no re-read, no _user_summary, no disabled field. And the stronger reason, which makes "silent at both ends" true in principle rather than by omission: admin_reset_password (auth/service.py:2717) does not call _retire_superseded_bootstrap at all. Its only three call sites are :518, :651 and :2551 -- positive control, the probe resolves real sites. So the re-arm is LATENT: at the moment the reset returns nothing has happened yet, there is no disabled state to report, and a re-read there would correctly say disabled:false. "Silent at both ends" therefore STANDS for this item. The create-path fact is real and belongs in the stacked-name item instead. Caught by the builder holding #1245, which re-measured rather than accepting a correction from the dispatcher. That is the second time today the two of us have hit the same shape in opposite directions: an instrument answering truthfully about the neighbouring question. Kept struck rather than deleted, because the two routes are adjacent in one file and the wrong version is what a later reader would re-derive. One correction from that pass DOES stand and is retained: login is not the sole retirement trigger (:518 on service start, :2551 on create), so a test assuming :651 is the only path is narrower than the defect. Recorded as already handled. SECOND DEFECT IN THIS SAME EDIT, caught by the before/after control and fixed before commit: the retraction was first written with a closed-alphabet character opening a blockquote in the item body. parse_items read it as a status banner and #1245 flipped to CLOSED -- 280/204/76 against an expected 280/205/75. A live item under active build, removed from the queue by a prose edit. The rule is absolute for exactly this reason: no banner-alphabet character in an item body, any position. Say the word. Controls after the fix: 280 items / 205 open / 75 closed, #1245 is_open True, zero closed-alphabet characters in the body, backlog_status_check green with every item declaring exactly one status. * backlog: close #1240, record #1241 as partial -- the ledger edit PR #379 cannot make itself PR #379 is red on a required check that says a PR implementing BACKLOG #N must update BACKLOG.md. The owner's 2026-08-13 ruling says a builder may resolve merge conflicts but may not author ledger content. Those two are mutually unsatisfiable for a compliant builder PR, so the builder correctly withheld the banner and the PR correctly went red. Authoring is dispatcher and lander only; this supplies the edit. Neither a bug nor anyone's error -- two correct rules meeting. #1240 CLOSED. Verified before signing by printing the operands on both refs rather than counting them, after a count instrument returned 0 on a string the printed lines visibly contained: origin/main _FHIR_TYPE_RE = re.compile(r"^[A-Za-z]+$") PR #379 head _FHIR_TYPE_RE = re.compile(r"^[A-Za-z]+\Z") $ -> \Z on the two pattern definitions, call sites unchanged. That is the durable form: it covers all three call sites at once and cannot be re-broken by a future caller, where converting the calls to .fullmatch would fix three and leave a fourth free to reintroduce it. The read-path _reject_control_chars limb was deliberately not added -- redundant once the gates are strict, and it would reintroduce duplication that #1239 records as retired. The item also records that the obvious regression test cannot discriminate: _resolve_read_url strips, so "Patient/123\n" yields an identical URL before and after the fix and only "Patient\n/123" flips. Measured by executing the shipped and patched sources, not argued. #1241 STAYS OPEN, amended to record partial progress. #379 fixed construction-time screening plus a wrong-exception-class defect worse than the filed finding -- http.client.InvalidURL derives from HTTPException, not ValueError and not OSError, so it escaped every except arm in _post including the backstop written for that case. Still outstanding: transports/dicomweb.py, which the item names, and a second unscreened url-construction site in FhirLookupExecutor in the same file. The item's subject is the ASYMMETRY, so one sink screened while a sibling is not reproduces the very defect being reported. A partial close would be wrong. Two corrections to #1241's filed text, neither reducing severity: its comparison clause INVERTS rather than going stale, because the neighbouring path it called "weaker but at least screening" was removed outright, leaving :431 the only unencoded interpolation in the file; and its enum rationale is right advice for the wrong reason, since containment comes from the !r conversion rather than the enum's closedness. Controls: parse_items 281 items / 206 open / 75 closed before, 281 / 205 / 76 after -- 0 / -1 / +1, the expected delta for exactly one close and one amendment. backlog_status_check green, every item declaring exactly one status. Banner invariant checked per item: #1240 one closed-alphabet character and zero open, #1241 zero closed and one open. * backlog: flip #1204 to shipped, and record that #1203 is abandoned rather than free #1204's banner read OPEN while its own body said "FIXED in the same change" and its Verdict line said "build (done)". Banner repair, not a change of plan. Verified at origin/main before signing, with a discriminating control. All four artifacts ship: scripts/docs/asvs_tally_lint.py, scripts/docs/asvs_tally_baseline.txt, .github/workflows/asvs-tally-lint.yml, tests/test_asvs_tally_lint.py. A deliberately impossible path under the same probe returned ABSENT, so the four PRESENTs are evidence rather than a probe that answers yes to everything. One defect found while verifying, and it is not what it first looks like. asvs-tally-lint.yml:3 cites BACKLOG #1203; the item it implements is #1204. The obvious reading is a typo pointing at an unissued number, and that reading is wrong in the direction that causes harm: it would send someone to file #1203 as free. Measured: "## 1203." appears in neither docs/BACKLOG.md nor docs/archive/backlog/BACKLOG-CLOSED.md, with "## 1204." resolving in the live ledger as the positive control. But the allocator record mefor-coord/alloc/backlog/1203.json EXISTS, titled "Decide how the public engine repo obtains the private ASVS scorecard for --prove-absences". So #1203 is ALLOCATED AND NEVER FILED -- abandoned, not free. Numbers are never reclaimed and holes are free, so the hole costs nothing. What costs something is that nothing reports an allocated-but-unfiled number, and a live citation makes it look issued. The :3 correction to #1204 is a one-word fix and rides with whatever next touches that workflow. Controls: parse_items 281 items / 205 open / 76 closed before, 281 / 204 / 77 after -- 0 / -1 / +1, the expected delta for one close. backlog_status_check green, every item declaring exactly one status. Banner invariant on the item body: one closed-alphabet character, zero open-alphabet. * docs(adr): ADR 0165 -- a builder PR satisfies the ledger gate with a paired commit Records a coordination decision that until now existed only in session messages and a queue file, which is the exact shape this project keeps being bitten by -- a ruling with no artifact behind it. THE COLLISION. The required check "a PR that implements BACKLOG #N must update BACKLOG.md" demands a ledger edit in the PR's own diff. The owner's 2026-08-13 authoring ruling forbids a BUILDER to author ledger content, on the property that a mechanical union cannot invent a disposition but authoring a banner can, and a seat that can author its own item's banner can turn its own PR green. Composed, a compliant builder PR cannot pass a required check. Measured live: PR #379 went red for obeying the ruling. THE DECISION. The Dispatcher or Lander authors the disposition and the commit rides on the PR branch. Owner-ruled a1. THE PART THAT INVERTED ON MEASUREMENT. Reading the gate rather than reasoning about it: backlog-hygiene.yml:64-98 computes a three-dot diff and passes if the changed set touches docs/BACKLOG.md or docs/archive/backlog/. It never inspects authorship. Evaluated against the real cherry-picked head for #379 -- touches_code 1, ledger 1, PASS. So the pattern was in force before it was named, no gate change was required, and none is pending. The ledger gate permits the cherry-pick for a non-obvious reason: it iterates headings added relative to base, and a banner flip or amendment on an item already on main adds no "## N." heading, so ownership is never consulted and the committing seat is irrelevant. Holds only for landed items; a PR that FILES an item is a different shape. REJECTED, with reasons rather than preferences. (a2) separately-landed plus cross-branch correlation would undo a deliberate control -- the gate uses three-dot on purpose and its own comment says two-dot "would pass while enforcing nothing". (b) a builder carve-out to flip its own banner reopens the self-approval hazard. (c) as a distinct interim dissolved: it is the same mechanism, so there is no transition. RECORDED NEAR-MISS, kept rather than deleted because the wrong version is what a later reader re-derives: the ruling was briefly written as "(c) is fine until (a) lands" -- an expiry whose trigger had ALREADY FIRED. It looks like the safe construction and behaves like the unsafe one, becoming permanent by default while appearing bounded. Provenance is split three ways in the ADR because each half is only checkable if attributed: the collision found by the Lander on #379's red check, the self-approval property by Builder 2, the gate measurement and the no-build finding by the Dispatcher, the ruling by the owner. ADR number allocated atomically to this worktree; index row added in the same commit, as the ledger gate requires. No engine behaviour changes. Note for whoever integrates: docs/adr/README.md is an APPEND/APPEND conflict with claude/builder-seat-playbook-bf2ead, which appends ADR 0164's row at the same tail. Both rows are additive and disjoint -- take both sides. * backlog: measure one limb of #1143's research question, and fix a blank-line citation #1143 asks what identification keyed on the IdP-namespaced subject would actually require across all three store backends. One limb of that is now measured rather than left to be re-derived by whoever picks the research up. MEASURED at origin/main, with a discriminating control: UNIQUE index or index naming oidc_issuer / oidc_subject: store.py 0 postgres.py 0 sqlserver.py 0 positive control: "UNIQUE" appears 13 times in store.py, so the probe discriminates and the three zeroes are real absences column types today: postgres.py:531-532 oidc_issuer TEXT, oidc_subject TEXT sqlserver.py:1357 oidc_issuer NVARCHAR(MAX) NULL, oidc_subject NVARCHAR(MAX) NULL So the federated columns exist and carry no uniqueness constraint of any kind. An (issuer, subject) identity key is therefore not a code-only change: it needs a unique index on all three backends, and on SQL Server NVARCHAR(MAX) cannot be an index key column at all, so both columns must first be re-typed to a bounded NVARCHAR(n). That is a second migration on that backend. That cost is an INPUT to choosing between the candidate designs rather than a consequence of having chosen one, which is why it belongs in the item before the research runs rather than after. CITATION FIX in the same pass: the banner cites store.py:1590 for users.username. :1590 is a BLANK LINE; the declaration "username TEXT NOT NULL UNIQUE" is at :1593. Found independently by two seats, so it is recorded rather than quietly patched. Explicitly NOT settled, and stated in the item: the ceremony for the first federated login of an account that predates federation. That remains the item's hard question and nothing above touches it. A migration cost informs that decision; it does not answer it. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged. * backlog: audit #1020's directory path -- it widens, and it invalidates one candidate fix #1020 said "the AD/OIDC-provisioned path in auth/reconcile.py was not audited, so the finding may narrow to local accounts". Audited now. Wrong file, and it widens rather than narrows. Every link opened individually, because a chain of separately-verified links is not a verified chain: authenticate_oidc auth/service.py:942 -> returns _complete_ad_login :1056 _complete_ad_login auth/service.py:1081 -> calls _upsert_ad_user :1107 _upsert_ad_user auth/service.py:1209 -> update_user_profile(email=principal.email) update_user_profile store/store.py:7742 -> UPDATE users SET display_name=?, email=?, ... Both the AD and the OIDC login paths provision through the SAME function, _upsert_ad_user -- not auth/reconcile.py, which the struck sentence names. authenticate_oidc returns _complete_ad_login directly, so one provisioning path serves two providers. The sharp end is the unconditional write. update_user_profile issues UPDATE users SET display_name=?, email=? with no conditional and no coalesce, and _upsert_ad_user calls it on every directory login with whatever the directory asserted. So a directory-sourced account cannot retain a hand-set address: an operator who sets one via PATCH /users/{id} has it overwritten at the account holder's next login. LDAP mail is optional at every layer, so where the directory asserts nothing the address returns to NULL. That invalidates one of the item's three candidate fixes. "Add a self-service email field" does not reach directory-provisioned accounts at all -- whatever the user sets is overwritten on their next login by the same unconditional write. Any fix gating on "a privileged account must have a deliverable address" needs a separate answer for the directory-sourced population, which moves it into the owner's decision rather than leaving it an implementation detail underneath. Deliberately NOT re-litigated: "Difficulty 3, no schema change, no migration cost" may still hold for the local-account half, and nobody has measured it for the directory half. The amendment says so rather than quietly widening the estimate. Also removed a pre-existing closed-alphabet character from this item's BODY at the old :4111. It was not flipping the item -- the counts were identical before and after -- but the rule is absolute for exactly that reason: position decides whether it parses as a status banner, and four items were mis-parsed by this class today. Replaced with the word. Provenance: the widening was measured by the Builder 2 seat during a blind re-verification pass; the chain above was re-read link by link here before being written into the ledger. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged. * backlog: record #1020's owner ruling, and correct the fix location it points at Owner ruled option (b): gate startup on a deliverable channel. Recorded in the item because until now the ruling existed only in session messages, which is the failure mode this session has been correcting all evening -- a decision with no artifact behind it. The deciding argument is recorded as the reason rather than only the choice: (b) is the only option that does not rest on an operator action. That is decisive because update_user_profile issues UPDATE users SET display_name=?, email=? with no conditional and no coalesce, on every directory login (store.py:7742), so any address a human sets on an AD or OIDC account is overwritten at the account holder's next login. A fix depending on someone setting an address cannot cover that population. The item's stated fix location is wrong and a builder would walk into it. The text points at __main__.py:2259, but _serve is synchronous and opens no store -- probing its full range for open_store|AuthService|list_users|count_users returns one hit and it is a comment, and uvicorn.run is at :2827 so the lifespan bootstrap has not run. The only place the store and the fresh bootstrap admin are both in hand is the ASGI lifespan at api/app.py:~5852. Option (c) is recorded as population-limited, NOT defective, and the amendment says the ruling must not be cited as a finding that it was broken. A self-service email field works for local accounts and is silently overwritten for directory ones. The incompleteness was invisible to the operator, which is more useful than "it was wrong" -- an earlier framing of mine that I withdrew. Recorded as unpriced rather than carried forward: whether "Difficulty 3, no schema change, no migration cost" still holds for the directory half. It may hold for the local half; nobody has measured the directory half. A stale difficulty estimate silently sets a lane's expectations. The build is NOT dispatched. The pool is at HOLD NEW WORK / PROTECT AND WRAP, and a builder taking this would be starting a new item and a new claim, which that state prohibits. Recording the ruling is work in hand; building it is not. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged. * backlog: record #1217 half 1 as built, half 2 outstanding -- the item stays open PR #383 is red on "a PR that implements BACKLOG #N must update BACKLOG.md". The builder withheld the banner correctly under the owner's authoring ruling, so the ledger edit is the dispatcher's. This supplies it. Same shape as #1241 on #379, and ADR 0165 records why that is the standing pattern. Half 1, the >=1 floor, is BUILT. Verified on the branch against origin/main rather than taken from the report: origin/main retry_max_attempts: int | None = 100 PR #383 retry_max_attempts: int | None = Field(default=100, ge=1) A configured 0 or negative is now refused at load rather than loading clean and dead-lettering on the FIRST failure -- the delivery check is item.attempts >= max_attempts against a post-increment count (pipeline/wiring_runner.py:5040), so 0 meant give-up-now while reading like "no limit". The floor is on the OPERATOR-FACING setting only, and that is deliberate. RetryPolicy(max_attempts=0) remains a live internal idiom for a permanent no-retry failure: measured across 5 files, including store.mark_failed call sites and asserted by tests at tests/test_batch_completion.py:206-208 and tests/test_postgres_store.py:3109. Constraining the dataclass instead would have deleted a used mechanism while claiming to add a guard -- the reads-as-hardening-but-removes-a-control shape. The item's stated reason for deferring the floor is answered rather than ignored. It said the floor was documented and not fixed "because a floor changes the accepted-configuration set". Under section 0 there are zero deployments, so there is no accepted configuration to break and no migration cost to protect. STILL OPEN, and it is why the item does not close: whether the retry-forever posture needs a TOML or env spelling. "", none and null all raise ValidationError, so that posture is reachable in code-first configuration only. It is a product question, it was handed back rather than decided, and the item itself says it should be decided alongside the floor. A closure on #383 would answer it by omission. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged, and #1217 verified still OPEN after the edit. * backlog: #1242's loss is irreversible, not merely expensive -- and a relay's framing corrected The ASVS Tracker surfaced this defect via the Liaison as a candidate for a NEW item. It is not new: it is #1242, filed 2026-08-13, and Builder 2 is building it. No number was allocated. A duplicate ledger row is not a harmless extra line -- it splits the work and the second number looks unbuilt forever, because the fix lands under the first. Two things in the relay were genuinely new. One is recorded here; the other is deliberately not. RECORDED: the loss is not recoverable by re-running the derivation. The anchors most at risk are the D3 backfill's, and their warrant was that two independent derivations AGREED, measured at two different refs. Those refs have moved. So a fresh derivation reproduces values without reproducing the agreement that justified writing them, and that agreement is the whole evidentiary content. The loss is therefore irreversible rather than expensive, which is why this item outranks other writer defects rather than being one among them. Nothing in the item said this. NOT RECORDED, deliberately: the supporting anchor counts. docs/BACKLOG.md is public, and a tally over a closed public requirement set is the shape that hands out coverage by subtraction. The mechanism is fully stated without them -- a reader with vault access can price it, and a reader without one still knows what to fix and why. Verified my added lines carry no such figure, with a positive control proving the scan discriminates. AND THE RELAY'S FRAMING IS CORRECTED BY THE ITEM'S OWN TEXT. It described the defect as dropping sym/ctx. #1242 already forbids fixing it that way: the defect is the handling of UNKNOWN keys, and a fix special-casing those two by name rebuilds the same trap for the next field added. The item was ahead of the relay and a builder must follow the item. The Tracker's core claim was verified here rather than taken: sym and ctx each return 0 occurrences in scripts/asvs/apply.py at origin/main, positive control expect returns 2. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged, and #1242 verified still OPEN. * backlog: close #1238 and #1239 on the code, and file #1253 so the closure does not retire the hazard Builder 2 reported four banners owed. Two of the four -- #1237 and #1204 -- were already authored on this branch and are invisible to that lane because ledger authorship and push are held by different seats. That is recorded as a finding in the episode note; it is not fixed here. The two genuinely owed are written now. Both closures verified against origin/main rather than against the build report, because a banner that closes on a report inherits the report's errors. #1238: _is_contained_name is defined at transports/remotefile.py:97, wired at :954, and asserted in BOTH polarities at tests/test_remotefile_transport.py:1185 and :1195. The one-polarity case is called out because such a test passes against a function that refuses everything. posixpath.basename() was not used, per the owner ruling. #1239: _has_control_char returns 0 occurrences across messagefoundry/ at origin/main, so the pair it named is a single. The item's own condition is met. #1253 exists because closing #1239 there would have been true of the item as written and false of the hazard it describes. The reporting lane amended its own closure recommendation to say so -- the predicate is copied more widely today than when #1239 was filed, partly by the work that resolved it. A repo-wide re-measure widened that further: the amendment scanned transports/ and found five sites across four files; across messagefoundry/ it is seven across six, the two extra being config/codeset_edit.py:305 and config/impact.py:631. Two exclusions are recorded in the item so a later scan does not re-add them: rest.py:109 matches a naive grep but is prose in a docstring, and sniff.py:179 tests the same code points through a genuinely different byte-wise predicate that subtracts an allowlist, so folding it in would change its behaviour. rest.py:111 strips where the others reject. That is recorded as defensible and NOT as a second instance of the pattern the owner ruled against in #1238, so the next reader does not inherit a false lead: stripping CR/LF from a header value cannot redirect a request, whereas basename() mutates a path into a real and different target. #1253 allocated with alloc.ps1, never grepped. parse_items before and after: 281/204/77 -> 282/203/79, matching the predicted delta for two closures plus one filing, with a control confirming no item carries a stray banner. * backlog: record that #1234 is not startable from main -- its subject has not landed Builder 2 refused a restock offer of #1234 by correcting its OWN earlier recommendation, and verified before claiming rather than after. Re-measured here rather than taken: at origin/main, require_least_privilege returns 0 hits in any .py and appears only in this ledger's own prose. Positive control on the same instrument, require_managed_identity, returns hits across four .py files, so the scan sees Python fine -- the zero is a fact about the tree, not a broken needle. The probe this item reports a defect in exists only on the w3-store-privilege-preflight branch, dormant at that reading. The amendment is careful not to overrule the item's own "independent of #1008" paragraph, because that paragraph is right about a different thing. Both halves hold: the item is not hostage to #1008's POLICY ruling, and it is nonetheless unbuildable by any lane working from main until that BRANCH lands. Collapsing the two would either re-gate a code defect behind a demand gate or keep offering work whose subject does not exist. This is the same wasted-claim cost as #1253's provenance, one layer deeper: there, an item was unstartable because the FIX had already landed; here, because the SUBJECT has not. A banner-driven queue cannot distinguish either case from startable work, which is why both are now written down where the next dispatcher reads rather than left in session mail. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 282 items / 203 open / 79 closed, unchanged, and #1234 verified still OPEN. * backlog: file #1254 -- a required check is named for its subject, not its assertion Handed to me by the Liaison to number if I judged it worth one, in the Lander's framing. It is, because the naive fix is dangerous and nothing currently records that. MEASURED INSTANCE: the Windows leg went red under the label "test (windows-2025, py3.14)", which reads as "the tests failed on Windows". The tests passed. What failed was the wall-clock gate "Step margin -- both gated steps" (ci.yml:675). The check answered its own question truthfully and the NAME described a different one -- the reverse of the shape this project keeps hitting, where the label is honest and the instrument is not. The job name is built at ci.yml:42 from the matrix, so all three legs are named for WHERE they ran and never for WHAT they assert, while holding at least three independent assertions. Stated as at least three rather than enumerated. WHY THIS IS NOT A ONE-LINE RENAME, which is the whole reason it needed writing down: those three strings ARE required contexts. They are listed in .github/required-contexts.txt, asserted against branch protection by tests/test_required_contexts.py, and matched BY NAME on the GitHub side. A required-but-absent context blocks every PR forever, so a rename is one atomic change across the workflow, the contexts file, that test's pinned count, and the branch-protection setting, in the order that file's header prescribes. So the item deliberately does NOT recommend the rename. It prices three options and names the cheapest first: make the margin gate's failure output say in its first line that the suite passed and a timing gate fired. That costs nothing and cannot wedge the repo. The rename is listed third. Severity carries no deployment axis, but the near-miss is recorded: the misreading pointed at the wall-clock cap, and #1096's banner already says the actual fix is #320 and that re-deriving the caps is itself the failure mode. #1254 allocated with alloc.ps1, never grepped. parse_items before and after: 282/203/79 -> 283/204/79, matching the predicted delta for one filing, with a control confirming no item carries a stray banner. * backlog: amend #1235 -- its two named instances are inert, and I dispatched the opposite Builder 2 refused the starting fact I gave it and measured instead. It was right and I was backwards. I told it to start #1235 from #1203 as a CONFIRMED LIVE TRAP, reasoning that an allocation record with no ledger entry meant the number was free. The record is what makes the number permanently UNAVAILABLE. Verified here rather than taken: 1203.json and 1231.json both exist in .git/mefor-coord/alloc/backlog/ (claimed 2026-08-09 and 2026-08-12), and alloc.ps1 has no release at all -- :41 "a one-way door -- claims are never released", :24 "numbers are never reclaimed ... holes are free, collisions are not". So the item's own text is wrong where it says #1231 was "allocated and released without being filed", and wrong that the pair is "defused only by an accident of timing". They are defused by construction. ONE CORRECTION AGAINST THE REPORT AS WELL, because its reason is weaker than its conclusion. The argument as relayed rests on the allocation RECORDS existing. Those live under .git: uncommittable, machine-local, losable without trace. The reason that survives their loss is structural -- alloc.ps1 issues $observed + 1 (:392, and :389 under the public floor clamp) and NEVER fills a hole, so a number below the floor is unreachable whether or not its record still exists. Recording the registry as the protection would make a sound property look fragile and invite a guard nothing needs. The live shape is the other one and the item now says so: a citation to a number NEVER allocated sits above the floor and will be issued in the normal course. A detector reading only the ledgers rates the two states identically, which over docs/ in this repo mis-scores 26 reserved citations as live; of the 6 genuinely never-allocated tokens there, all six are foreign references, so this repo holds zero genuine instances. The private-repo population the item was filed against is not re-measured here and is stated as separate. The remedy is unchanged and still correct. Only the account of WHY the two named instances are harmless is corrected. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 283 items / 204 open / 79 closed, unchanged, and #1235 verified still OPEN. * backlog: correct #1235 and #1254 -- two of my own committed claims, falsified under adversarial check I ran six independent skeptics over every claim I committed tonight, against the SIMULATED POST-MERGE TREE rather than my branch, because my branch was six behind origin/main and the merge is CLEAN -- git conflicts on concurrent edits, never on invalidated claims. Four claims held. Two did not. Both failures are my authoring, not merge drift: all cited files are byte-identical across the merge tree, origin/main and HEAD. #1235. The conclusion survives, the reason did not. I had written that the allocation registry is irrelevant because the mechanism is structural. That is FALSE for the newest number: the high-water ratchet persists $floor, the maximum of the OBSERVED set (:205, :214, :215), NOT the number being issued, so after issuing N it holds N-1. alloc.ps1 only PRINTS the heading, so until it is committed the sole durable record of N is its own untracked, never-pushed <n>.json. Lose that and the next run re-issues N. So the registry is exactly what protects a just-allocated-but-unfiled number -- the state #1203 and #1231 were both in when allocated. What actually makes those two unreachable is a CONJUNCTION, now stated as one: the loop never searches downward (:392, :389, :394), AND the floor is computed from COMMITTED LEDGER HEADINGS. Measured non-destructively with -ShowFloor: floor 1254, swept from docs/BACKLOG.md and the closed archive. Those are tracked content on refs, they survive a fresh clone, and both numbers sit far below them. Recorded the public-floor clamp as a THIRD, separate guarantee about the output range, with the caveat that its own anti-lowering ratchet lives in the same untracked directory and is disarmed on a registry-absent clone. #1254 cited the margin gate as ci.yml:675. That is a clock MARK (step_margin.py --mark between, :677). The gate is :765, if: always(), invoking at :778 and :781. The error is worth recording rather than silently fixing: I opened :675, found a step whose name NEARLY matched, and adopted it instead of treating the near-match as the signal the line was wrong. A near-miss terminates the search; no match would have continued it. Also corrected in #1254: tests/test_required_contexts.py does NOT call the GitHub API. It pins the count at :101 and resolves contexts against real workflow job names at :107; the branch-protection comparison is a HUMAN step in the comment at :100. The item's central argument is unaffected -- the strings are still required contexts and a rename still resolves them to no job -- but the evidence now says what the test does. The four that held: #1253's seven-sites-across-six-files with both exclusions and every line number, #1239's zero occurrences, #1238's defined-wired-and-both-test- polarities, and #1234's zero .py hits with its positive control. Amendments only, no heading added, so ledger ownership is not consulted. parse_items unchanged at 283 items / 204 open / 79 closed. * backlog: file #1255 -- two testpaths ship a top-level conftest each Diagnosed by the lane whose own commit tripped it, and filed here because the collision outlives that commit. Seven tests in one file failed in a full run and passed in isolation, twice. Cause: pyproject sets two testpaths, both directories contain a conftest.py, neither contains an __init__.py, so both claim the top-level module name and a bare `import conftest` binds to whichever loaded first. Verified at origin/main rather than taken from the report: both conftest.py files present, both __init__.py absent, and a scan for `import conftest` / `from conftest import` across both trees returns ZERO hits. That zero is why this is filed as LATENT rather than live -- the collision is real and currently untripped, so nothing is failing today and the item must not be cited as a current gap. The signature is recorded because it mis-attributes itself: the mis-bound import surfaces as an AttributeError naming a module path from the WRONG package, not as an ImportError, so it reads as a missing attribute rather than a bad import. Two things the item forbids, both because a plausible fix is worse than the defect. Do not import conftest BY PATH -- its body claims a per-process test slot and registers an atexit unlink, so a second import under another name has side effects. And do not prove a fix in isolation: isolation is precisely the condition under which this defect reports success. The proof has to run both testpaths together and then restore the bare import to confirm the same command fails again. The house idiom already solves it -- tests/_workflow_contexts.py is imported package-qualified at tests/_negative_controls.py:35 -- so the scope is to make the name unambiguous, not to invent a mechanism. #1255 allocated with alloc.ps1, never grepped. parse_items before and after: 283/204/79 -> 284/205/79, matching the predicted delta for one filing, with a control confirming no item carries a stray banner. * backlog: record that PR #382 closed ONE of #1242's four limbs -- the item stays open I told a builder its engine fix closed this item's mechanism and to claim it. That was wrong, and this records the correction where the next reader will hit it rather than in session mail. #382 merged 2026-08-13 with this item's number in its title and a title that faithfully describes what it fixed: the payload-only TOP-LEVEL key limb. Verified at origin/main -- apply.py:97 now walks {**(live or {}), **cell}.items() rather than the live cell alone. That limb is genuinely closed. The limb carrying the item's severity is untouched, and the same revision shows why the union cannot reach it: :98 skips _ORDERED and _SUBTABLES BEFORE the union at :97 is consulted for them, and evidence entries are re-emitted at :101-105 by enumerating exactly path, line and expect (absence at :106-110 by exactly pattern, positive_control, mutation). A key inside a [[cell.evidence]] entry is still dropped -- which is exactly where the backfill put the affected keys, on evidence ENTRIES and not on top-level keys. The top-level table-mangling limb also appears untouched. I had additionally told that builder to stop measuring the two affected key names because the item forbids fixing by naming them. The prohibition is real but I applied it to the wrong activity: the item forbids naming them in a FIX, not measuring their absence as a SYMPTOM, and their absence from the writer is exactly the evidence that the sub-table limb still bites. Recorded as the PARTIAL-MOVE shape, which is the reusable part: a merged PR bearing an item's number, whose title truthfully describes what it fixed, is the strongest available signal the item is done. Verify-before-closing is not enough on its own here -- the verification has to ask WHICH HALF. The item's own proof condition is the discriminator and is unchanged: put an unknown key INSIDE an evidence entry, re-render, assert both that it survives and that the guard refuses when it is deliberately dropped. #382 does not satisfy it, and no test asserting only top-level carry-through will. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 284 items / 205 open / 79 closed, unchanged, and #1242 verified still OPEN. * backlog: record #1242's BASE REQUIREMENT -- the obvious base for limb 4 reverts limb 3 Builder 2 confirmed limb 4 as I described it, then refused to build it and handed it back with a reason better than the instruction I gave. Recording the reason, because it is not discoverable from the item and git will not raise it. Its branch still carried scripts/asvs/apply.py:88 as if key in _ORDERED or key in _SUBTABLES or key in cell: Verified here against that ref rather than taken: the clause is present there and ABSENT at origin/main. `or key in cell` is precisely what #382 deleted to fix limb 3. So a limb-4 fix authored on that base and merged would carry limb 3's REVERSAL in the same diff -- no conflict, no marker, every check green, and the item's own landed fix undone by the commit claiming to extend it. Git raises nothing here because git conflicts on concurrent edits to the same lines, never on a stale base re-asserting a clause that was deleted elsewhere. That is the same family as the clean-merge hazard this session has been working under all night, arriving from the direction nobody watches: not a doc invalidated by a merge, but a FIX reverted by an extension of itself. The item now states the base requirement and a pre-PR check that discriminates: branch fresh from current origin/main, and confirm `or key in cell` returns zero hits in the diff's own version of the file before opening a PR. Also corrected upstream of this, in my own dispatch rather than the ledger: I had told that lane to stop measuring the two affected key names. Measuring their absence as a SYMPTOM was always legitimate; only fixing by naming them is forbidden. It withdrew its acceptance of my earlier "bound lifted" on the grounds that it had taken it from me without measuring -- correctly. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 284 items / 205 open / 79 closed, unchanged, and #1242 verified still OPEN. * backlog: file #1256 -- the federated binding never checks subject exclusivity Builder 1 concluded #1143's research and handed over the finding rather than a commit: the defensible-ceremony question dissolves, because every candidate collapses to trust-on-first-use when no out-of-band proof exists at first federated login. What it surfaced instead is a separable gap, and that gap is this item. Content theirs, number mine, per the authoring split. Verified at origin/main rather than relayed. auth/service.py:1109-1114 compares user.oidc_subject against the presented subject and refuses on mismatch, then binds at :1119-1120. The comparison is keyed on the USER, so it is structurally incapable of noticing a second account carrying the same (issuer, subject). A scan for a UNIQUE constraint naming the federated columns returns 0 on ALL THREE backends, so nothing below it closes the gap either. The item credits the three shipped controls rather than implying an absence: the hybrid-only refusal, the subject-continuity guard, and the UPN suffix allow-list. All three constrain which subject may bind to a GIVEN account. None constrains how many accounts one SUBJECT may bind to. Stating that explicitly is what should stop this being re-closed as a duplicate of #1015 or #1143. The difficulty is recorded where it actually lives. SQL Server types the federated columns NVARCHAR(MAX), and a MAX column cannot be an index key, so a unique index there needs a RE-TYPE and not merely a constraint -- a cost SQLite and Postgres do not share. The proof condition requires demonstrating the refusal on every backend in CI, because the two server store suites SKIP in a local run and a green on SQLite alone would certify nothing. #1143 is NOT closed by this and the item says so: whether TOFU is the defensible ceremony is separable and still open. #1256 allocated with alloc.ps1, never grepped. parse_items: 285 items / 206 open / 79 closed after, matching the predicted +1/+1/0 for a single filing, with a control confirming no item carries a stray banner. * backlog: record that #1020's refusal path hangs in-harness and is unverified under uvicorn Reported by the lane that built the gate, against its own work, and recorded here because it changes what "built" means for this item. The gate refuses by raising during ASGI lifespan STARTUP, and in-harness that HANGS rather than exiting. Their control is what makes it a measurement rather than an impression: the sibling non-raising lifespan test passes in 1.13s, raising from the lifespan BODY exits cleanly, and raising during STARTUP hangs with zero output. It sits after engine.start() and before the task handles teardown expects, so the condition is PRE-EXISTING -- the gate is simply the first thing to raise in that window, which is why this is not filed as a defect in their fix. What was NOT measured is what uvicorn does there, and uvicorn is the runner that ships. They said so explicitly rather than letting the harness result stand for the product, which is the reason this is worth recording at all. The consequence is ordered plainly in the item: a startup refusal that hangs a service is STRICTLY WORSE than the mis-report #1020 exists to correct. An operator can see a wrong readiness answer; they cannot see a process that never finishes starting. So the banner now carries an explicit bar on closing this on the gate landing until the refusal is shown to TERMINATE under uvicorn rather than under the test harness. Amendment only, no heading added, so ledger ownership is not consulted. parse_items unchanged at 285 items / 206 open / 79 closed, and #1020 verified still OPEN. * backlog: #1235 is PARTIAL, not closed -- the detector shipped, the rule did not I was about to close this on PR #385 landing. An adversarial pass over my own proposed banner refuted it, and the refutation is right. What I would have relied on -- do the two files exist at origin/main -- passes identically whether or not the rule landed and whether or not anything invokes the detector. It measures PRESENCE, NOT ENFORCEMENT, and it is the same cannot-fail shape as running a CLI's --help and reading exit 0, which I had already caught once today in my own #1030 check. Three measurements, each independently sufficient to block closure: the RULE is unwritten. #1235's Scope names its deliverable as "a rule, not a sweep", and that guidance appears at origin/main only in the item's own prose -- zero hits for "unallocated" across CLAUDE.md, docs/LEDGER-GATE.md, CONTRIBUTING, scripts/, .github/ and .claude/. the DETECTOR is wired into nothing. Repo-wide it is referenced by exactly two lines, both inside its own unit test. Not in any workflow, not in .pre-commit-config.yaml, not in .mefor-hooks/pre-commit, not in pyproject.toml. it EXITS 0 even when it fires. main() ends `return 1 if args.fail else 0` and --fail is opt-in and passed by nothing. A planted live-shape citation was reported correctly and the process still exited 0. No test runs unresolved_citations over the real docs/ tree, so a new dangling citation turns nothing red. The controls are what make those zeros trustworthy: the same wiring grep DOES resolve three sibling scripts/docs tools that are wired into CI, and the detector itself discriminates correctly when --fail is supplied. So the absences are facts about the tree, not a broken pattern or a blind scan. Banner moves to in-progress rather than closed, and the item now records what shipped, the two residual limbs (write the rule where authors read it; wire the detector with --fail or test it against the real tree), and a coverage bound that survives both -- by its own docstring the detector cannot see the private companion repository, which is where this item's filed instances live. Closing on the detector's existence would have recorded an enforced rule where nothing enforces it. The builder declined to close it for the same reason and left the ledger half to me. Amendment only, no heading added, so ledger ownership is not consulted. Both glyphs are OPEN, so parse_items is unchanged at 285 items / 206 open / 79 closed, with #1235 verified still OPEN and carrying exactly one banner. * backlog: close #1230 on the owner-ruled half, with the wiring's own regression gap named Four proposed banner verdicts went through an adversarial pass before any was written. Three survived; one did not and was corrected separately (#1235). This is the survivor, and it survived on evidence stronger than I would have gathered. #1230 CLOSES because the loud-omission half is built AND WIRED, not merely present. tests/_extras_probe.py supplies the emitters and tests/conftest.py binds them to pytest's own hooks at :366 and :370. Verified by RUNNING rather than reading, in both directions on two real interpreters: extras-complete venv, banner ABSENT; the venv scripts/worktree/new.ps1 actually builds, banner FIRES in both surfaces -- including on a bare full-suite run collecting 13,187 tests. The silent direction was observed rather than assumed, which is what makes the loud direction mean something. Scope is exactly the half the owner ruled. new.ps1:232 is unchanged, so options (a) and (b) -- installing the five extras into every worktree venv -- were correctly not taken. CLOSED WITH A NAMED RESIDUAL, because folding it into the closure would reproduce the defect. Deleting BOTH hook functions from tests/conftest.py leaves tests/test_incomplete_run_banner.py reporting 7 passed, while a real run goes from banner-present to banner-gone. The tests drive the probe against a fake reporter; nothing asserts that pytest invokes it. Repo-wide, pytest_report_header and pytest_terminal_summary appear ONLY at conftest.py:366 and :370, guarded by no test, lint or gate. So a conftest refactor can silently delete the mechanism with every check green -- which is this item's own failure shape, one layer up, inside the fix for it. The item closes on the owner-ruled half working. The residual is recorded in the banner rather than discovered later by whoever the silence next costs. Amendment plus banner flip, no heading added, so ledger ownership is not consulted. parse_items 285 items / 206 open / 79 closed -> 285 / 205 / 80, matching the predicted 0/-1/+1 for a single closure, with a control confirming no item carries a stray banner and #1235, #1249 and #1030 all verified still OPEN.
wshallwshall
added a commit
that referenced
this pull request
Aug 14, 2026
…1250 (#389) * backlog: correct #1114's Severity line, which contradicted its own body The Severity line read "could submit unbounded messages". Four bounds ship ON and were re-verified at origin/main 96c9a860: DEFAULT_MAX_FRAME_BYTES 16 MiB (transports/mllp.py:105), DEFAULT_MAX_CONNECTIONS 256 (:106), DEFAULT_RECEIVE_TIMEOUT 60.0s (:107), and max_file_bytes (transports/file.py:384, remotefile.py:808). They bound SIZE and CONCURRENCY, not RATE. The item's own "What holds it short today" paragraph already said exactly that, two paragraphs above, so the Severity line was contradicting its own item rather than describing the engine. Corrected to "at an unbounded RATE"; the finding is unchanged and the item stays open. The correction runs in the direction that makes the engine look better, which is why the amendment states it explicitly: a Severity line is the sentence most often quoted onward without its body. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 277 items / 203 open, unchanged. * backlog: flip #1237 to shipped -- its fix landed without a ledger edit PR #372 merged the code as 96c9a860 and did not touch docs/BACKLOG.md, so the item read "not started" while its fix was on main. Banner repair, not a change of plan. Both stated limbs verified at origin/main 96c9a860 rather than inferred from the PR title: Signature -- an AST probe located all three functions, so it was not blind: gzip_decompress :101, deflate_decompress :138, zip_decompress :195 each carry max_output_bytes keyword-only with NO DEFAULT. That is the construct the item asked for, a gate that refuses when the precondition is absent, rather than a changed default value, which the parent item #1129 explicitly rules out. Tests -- tests/test_compression.py pins it: "Calling a decompressor without max_output_bytes is a TypeError, not an unbounded read", with a pytest.raises(TypeError) assertion. The re-exported public surface still resolves in messagefoundry/__init__.py and parsing/__init__.py. This closes NO ASVS cell and the amendment says so in the item. The verdict is the assessor's and the vault scorecard is the record of record; the "before uncompressing" reading question is unresolved without the pre-pass #1237 deliberately excluded, which remains unfiled and is an owner call. Controls: parse_items 277 items / 203 open / 74 closed before, 277 / 202 / 75 after -- 0 / -1 / +1, the expected delta for one close. backlog_status_check green, every item declaring exactly one status. Banner invariant checked on the item body: one closed-alphabet character, zero open-alphabet characters. * backlog: strike #1245's false delete clause, and narrow two overclaims #1245's SCOPE paragraph said the bootstrap account "cannot be renamed (update_user does not rename) or deleted, so it persists as a permanently disabled row". The rename half is correct. The delete half is FALSE. Reproduced end to end by a second session: DELETE /users/<bootstrap admin> returns 200 {'detail': 'deleted'}. Confirmed here independently from the code -- BOOTSTRAP_USERNAME appears 0 times in messagefoundry/api/auth_routes.py, with a positive control of 7 occurrences in auth/service.py so the probe discriminates. No delete-time guard names the bootstrap account. The only guard on that route is is_last_enabled_admin (auth_routes.py:714), which skips disabled users, and retirement is what disables this one. Why the correction matters more than the fact: "persists as a permanently disabled row" is what makes this read as an availability defect with no exit. There is an exit and it is destructive. A fix must not rest on the row being undeletable, and a reader checking this paragraph would otherwise re-conclude a related defect is unreachable and close it as impossible. Two narrower corrections in the same pass, both REDUCING what the item claims: "Silent at both ends" is half wrong. The 201 response body does carry disabled:true (auth_routes.py:656-658 re-reads after retirement, _user_summary sets it at :195). It is silent at the login end only, via the generic 401 that is deliberately indistinguishable from a wrong password. Login is not the sole retirement trigger: auth/service.py:518 fires on every service start and :2551 on create, so a regression test assuming :650 is the only path is narrower than the defect. Struck rather than deleted, so the wrong version stays visible to the next reader. The item stays open and its severity is unchanged. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 280 items / 205 open / 75 closed, unchanged. * backlog: retract my own #1245 narrowing -- it was measured on the wrong route Forty minutes ago I amended #1245 to say "silent at both ends" was half wrong, on the ground that a 201 response body carries disabled:true. That measurement is TRUE and it is about a DIFFERENT ROUTE. auth_routes.py:656-658 and _user_summary:195 are POST /users -- a create, and the stacked-admin-name defect's route. #1245 is a RESET defect. The reset route at auth_routes.py:753 ends at :776 with return PasswordResetResponse(temp_password=temp) no re-read, no _user_summary, no disabled field. And the stronger reason, which makes "silent at both ends" true in principle rather than by omission: admin_reset_password (auth/service.py:2717) does not call _retire_superseded_bootstrap at all. Its only three call sites are :518, :651 and :2551 -- positive control, the probe resolves real sites. So the re-arm is LATENT: at the moment the reset returns nothing has happened yet, there is no disabled state to report, and a re-read there would correctly say disabled:false. "Silent at both ends" therefore STANDS for this item. The create-path fact is real and belongs in the stacked-name item instead. Caught by the builder holding #1245, which re-measured rather than accepting a correction from the dispatcher. That is the second time today the two of us have hit the same shape in opposite directions: an instrument answering truthfully about the neighbouring question. Kept struck rather than deleted, because the two routes are adjacent in one file and the wrong version is what a later reader would re-derive. One correction from that pass DOES stand and is retained: login is not the sole retirement trigger (:518 on service start, :2551 on create), so a test assuming :651 is the only path is narrower than the defect. Recorded as already handled. SECOND DEFECT IN THIS SAME EDIT, caught by the before/after control and fixed before commit: the retraction was first written with a closed-alphabet character opening a blockquote in the item body. parse_items read it as a status banner and #1245 flipped to CLOSED -- 280/204/76 against an expected 280/205/75. A live item under active build, removed from the queue by a prose edit. The rule is absolute for exactly this reason: no banner-alphabet character in an item body, any position. Say the word. Controls after the fix: 280 items / 205 open / 75 closed, #1245 is_open True, zero closed-alphabet characters in the body, backlog_status_check green with every item declaring exactly one status. * backlog: close #1240, record #1241 as partial -- the ledger edit PR #379 cannot make itself PR #379 is red on a required check that says a PR implementing BACKLOG #N must update BACKLOG.md. The owner's 2026-08-13 ruling says a builder may resolve merge conflicts but may not author ledger content. Those two are mutually unsatisfiable for a compliant builder PR, so the builder correctly withheld the banner and the PR correctly went red. Authoring is dispatcher and lander only; this supplies the edit. Neither a bug nor anyone's error -- two correct rules meeting. #1240 CLOSED. Verified before signing by printing the operands on both refs rather than counting them, after a count instrument returned 0 on a string the printed lines visibly contained: origin/main _FHIR_TYPE_RE = re.compile(r"^[A-Za-z]+$") PR #379 head _FHIR_TYPE_RE = re.compile(r"^[A-Za-z]+\Z") $ -> \Z on the two pattern definitions, call sites unchanged. That is the durable form: it covers all three call sites at once and cannot be re-broken by a future caller, where converting the calls to .fullmatch would fix three and leave a fourth free to reintroduce it. The read-path _reject_control_chars limb was deliberately not added -- redundant once the gates are strict, and it would reintroduce duplication that #1239 records as retired. The item also records that the obvious regression test cannot discriminate: _resolve_read_url strips, so "Patient/123\n" yields an identical URL before and after the fix and only "Patient\n/123" flips. Measured by executing the shipped and patched sources, not argued. #1241 STAYS OPEN, amended to record partial progress. #379 fixed construction-time screening plus a wrong-exception-class defect worse than the filed finding -- http.client.InvalidURL derives from HTTPException, not ValueError and not OSError, so it escaped every except arm in _post including the backstop written for that case. Still outstanding: transports/dicomweb.py, which the item names, and a second unscreened url-construction site in FhirLookupExecutor in the same file. The item's subject is the ASYMMETRY, so one sink screened while a sibling is not reproduces the very defect being reported. A partial close would be wrong. Two corrections to #1241's filed text, neither reducing severity: its comparison clause INVERTS rather than going stale, because the neighbouring path it called "weaker but at least screening" was removed outright, leaving :431 the only unencoded interpolation in the file; and its enum rationale is right advice for the wrong reason, since containment comes from the !r conversion rather than the enum's closedness. Controls: parse_items 281 items / 206 open / 75 closed before, 281 / 205 / 76 after -- 0 / -1 / +1, the expected delta for exactly one close and one amendment. backlog_status_check green, every item declaring exactly one status. Banner invariant checked per item: #1240 one closed-alphabet character and zero open, #1241 zero closed and one open. * backlog: flip #1204 to shipped, and record that #1203 is abandoned rather than free #1204's banner read OPEN while its own body said "FIXED in the same change" and its Verdict line said "build (done)". Banner repair, not a change of plan. Verified at origin/main before signing, with a discriminating control. All four artifacts ship: scripts/docs/asvs_tally_lint.py, scripts/docs/asvs_tally_baseline.txt, .github/workflows/asvs-tally-lint.yml, tests/test_asvs_tally_lint.py. A deliberately impossible path under the same probe returned ABSENT, so the four PRESENTs are evidence rather than a probe that answers yes to everything. One defect found while verifying, and it is not what it first looks like. asvs-tally-lint.yml:3 cites BACKLOG #1203; the item it implements is #1204. The obvious reading is a typo pointing at an unissued number, and that reading is wrong in the direction that causes harm: it would send someone to file #1203 as free. Measured: "## 1203." appears in neither docs/BACKLOG.md nor docs/archive/backlog/BACKLOG-CLOSED.md, with "## 1204." resolving in the live ledger as the positive control. But the allocator record mefor-coord/alloc/backlog/1203.json EXISTS, titled "Decide how the public engine repo obtains the private ASVS scorecard for --prove-absences". So #1203 is ALLOCATED AND NEVER FILED -- abandoned, not free. Numbers are never reclaimed and holes are free, so the hole costs nothing. What costs something is that nothing reports an allocated-but-unfiled number, and a live citation makes it look issued. The :3 correction to #1204 is a one-word fix and rides with whatever next touches that workflow. Controls: parse_items 281 items / 205 open / 76 closed before, 281 / 204 / 77 after -- 0 / -1 / +1, the expected delta for one close. backlog_status_check green, every item declaring exactly one status. Banner invariant on the item body: one closed-alphabet character, zero open-alphabet. * docs(adr): ADR 0165 -- a builder PR satisfies the ledger gate with a paired commit Records a coordination decision that until now existed only in session messages and a queue file, which is the exact shape this project keeps being bitten by -- a ruling with no artifact behind it. THE COLLISION. The required check "a PR that implements BACKLOG #N must update BACKLOG.md" demands a ledger edit in the PR's own diff. The owner's 2026-08-13 authoring ruling forbids a BUILDER to author ledger content, on the property that a mechanical union cannot invent a disposition but authoring a banner can, and a seat that can author its own item's banner can turn its own PR green. Composed, a compliant builder PR cannot pass a required check. Measured live: PR #379 went red for obeying the ruling. THE DECISION. The Dispatcher or Lander authors the disposition and the commit rides on the PR branch. Owner-ruled a1. THE PART THAT INVERTED ON MEASUREMENT. Reading the gate rather than reasoning about it: backlog-hygiene.yml:64-98 computes a three-dot diff and passes if the changed set touches docs/BACKLOG.md or docs/archive/backlog/. It never inspects authorship. Evaluated against the real cherry-picked head for #379 -- touches_code 1, ledger 1, PASS. So the pattern was in force before it was named, no gate change was required, and none is pending. The ledger gate permits the cherry-pick for a non-obvious reason: it iterates headings added relative to base, and a banner flip or amendment on an item already on main adds no "## N." heading, so ownership is never consulted and the committing seat is irrelevant. Holds only for landed items; a PR that FILES an item is a different shape. REJECTED, with reasons rather than preferences. (a2) separately-landed plus cross-branch correlation would undo a deliberate control -- the gate uses three-dot on purpose and its own comment says two-dot "would pass while enforcing nothing". (b) a builder carve-out to flip its own banner reopens the self-approval hazard. (c) as a distinct interim dissolved: it is the same mechanism, so there is no transition. RECORDED NEAR-MISS, kept rather than deleted because the wrong version is what a later reader re-derives: the ruling was briefly written as "(c) is fine until (a) lands" -- an expiry whose trigger had ALREADY FIRED. It looks like the safe construction and behaves like the unsafe one, becoming permanent by default while appearing bounded. Provenance is split three ways in the ADR because each half is only checkable if attributed: the collision found by the Lander on #379's red check, the self-approval property by Builder 2, the gate measurement and the no-build finding by the Dispatcher, the ruling by the owner. ADR number allocated atomically to this worktree; index row added in the same commit, as the ledger gate requires. No engine behaviour changes. Note for whoever integrates: docs/adr/README.md is an APPEND/APPEND conflict with claude/builder-seat-playbook-bf2ead, which appends ADR 0164's row at the same tail. Both rows are additive and disjoint -- take both sides. * backlog: measure one limb of #1143's research question, and fix a blank-line citation #1143 asks what identification keyed on the IdP-namespaced subject would actually require across all three store backends. One limb of that is now measured rather than left to be re-derived by whoever picks the research up. MEASURED at origin/main, with a discriminating control: UNIQUE index or index naming oidc_issuer / oidc_subject: store.py 0 postgres.py 0 sqlserver.py 0 positive control: "UNIQUE" appears 13 times in store.py, so the probe discriminates and the three zeroes are real absences column types today: postgres.py:531-532 oidc_issuer TEXT, oidc_subject TEXT sqlserver.py:1357 oidc_issuer NVARCHAR(MAX) NULL, oidc_subject NVARCHAR(MAX) NULL So the federated columns exist and carry no uniqueness constraint of any kind. An (issuer, subject) identity key is therefore not a code-only change: it needs a unique index on all three backends, and on SQL Server NVARCHAR(MAX) cannot be an index key column at all, so both columns must first be re-typed to a bounded NVARCHAR(n). That is a second migration on that backend. That cost is an INPUT to choosing between the candidate designs rather than a consequence of having chosen one, which is why it belongs in the item before the research runs rather than after. CITATION FIX in the same pass: the banner cites store.py:1590 for users.username. :1590 is a BLANK LINE; the declaration "username TEXT NOT NULL UNIQUE" is at :1593. Found independently by two seats, so it is recorded rather than quietly patched. Explicitly NOT settled, and stated in the item: the ceremony for the first federated login of an account that predates federation. That remains the item's hard question and nothing above touches it. A migration cost informs that decision; it does not answer it. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged. * backlog: audit #1020's directory path -- it widens, and it invalidates one candidate fix #1020 said "the AD/OIDC-provisioned path in auth/reconcile.py was not audited, so the finding may narrow to local accounts". Audited now. Wrong file, and it widens rather than narrows. Every link opened individually, because a chain of separately-verified links is not a verified chain: authenticate_oidc auth/service.py:942 -> returns _complete_ad_login :1056 _complete_ad_login auth/service.py:1081 -> calls _upsert_ad_user :1107 _upsert_ad_user auth/service.py:1209 -> update_user_profile(email=principal.email) update_user_profile store/store.py:7742 -> UPDATE users SET display_name=?, email=?, ... Both the AD and the OIDC login paths provision through the SAME function, _upsert_ad_user -- not auth/reconcile.py, which the struck sentence names. authenticate_oidc returns _complete_ad_login directly, so one provisioning path serves two providers. The sharp end is the unconditional write. update_user_profile issues UPDATE users SET display_name=?, email=? with no conditional and no coalesce, and _upsert_ad_user calls it on every directory login with whatever the directory asserted. So a directory-sourced account cannot retain a hand-set address: an operator who sets one via PATCH /users/{id} has it overwritten at the account holder's next login. LDAP mail is optional at every layer, so where the directory asserts nothing the address returns to NULL. That invalidates one of the item's three candidate fixes. "Add a self-service email field" does not reach directory-provisioned accounts at all -- whatever the user sets is overwritten on their next login by the same unconditional write. Any fix gating on "a privileged account must have a deliverable address" needs a separate answer for the directory-sourced population, which moves it into the owner's decision rather than leaving it an implementation detail underneath. Deliberately NOT re-litigated: "Difficulty 3, no schema change, no migration cost" may still hold for the local-account half, and nobody has measured it for the directory half. The amendment says so rather than quietly widening the estimate. Also removed a pre-existing closed-alphabet character from this item's BODY at the old :4111. It was not flipping the item -- the counts were identical before and after -- but the rule is absolute for exactly that reason: position decides whether it parses as a status banner, and four items were mis-parsed by this class today. Replaced with the word. Provenance: the widening was measured by the Builder 2 seat during a blind re-verification pass; the chain above was re-read link by link here before being written into the ledger. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged. * backlog: record #1020's owner ruling, and correct the fix location it points at Owner ruled option (b): gate startup on a deliverable channel. Recorded in the item because until now the ruling existed only in session messages, which is the failure mode this session has been correcting all evening -- a decision with no artifact behind it. The deciding argument is recorded as the reason rather than only the choice: (b) is the only option that does not rest on an operator action. That is decisive because update_user_profile issues UPDATE users SET display_name=?, email=? with no conditional and no coalesce, on every directory login (store.py:7742), so any address a human sets on an AD or OIDC account is overwritten at the account holder's next login. A fix depending on someone setting an address cannot cover that population. The item's stated fix location is wrong and a builder would walk into it. The text points at __main__.py:2259, but _serve is synchronous and opens no store -- probing its full range for open_store|AuthService|list_users|count_users returns one hit and it is a comment, and uvicorn.run is at :2827 so the lifespan bootstrap has not run. The only place the store and the fresh bootstrap admin are both in hand is the ASGI lifespan at api/app.py:~5852. Option (c) is recorded as population-limited, NOT defective, and the amendment says the ruling must not be cited as a finding that it was broken. A self-service email field works for local accounts and is silently overwritten for directory ones. The incompleteness was invisible to the operator, which is more useful than "it was wrong" -- an earlier framing of mine that I withdrew. Recorded as unpriced rather than carried forward: whether "Difficulty 3, no schema change, no migration cost" still holds for the directory half. It may hold for the local half; nobody has measured the directory half. A stale difficulty estimate silently sets a lane's expectations. The build is NOT dispatched. The pool is at HOLD NEW WORK / PROTECT AND WRAP, and a builder taking this would be starting a new item and a new claim, which that state prohibits. Recording the ruling is work in hand; building it is not. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged. * backlog: record #1217 half 1 as built, half 2 outstanding -- the item stays open PR #383 is red on "a PR that implements BACKLOG #N must update BACKLOG.md". The builder withheld the banner correctly under the owner's authoring ruling, so the ledger edit is the dispatcher's. This supplies it. Same shape as #1241 on #379, and ADR 0165 records why that is the standing pattern. Half 1, the >=1 floor, is BUILT. Verified on the branch against origin/main rather than taken from the report: origin/main retry_max_attempts: int | None = 100 PR #383 retry_max_attempts: int | None = Field(default=100, ge=1) A configured 0 or negative is now refused at load rather than loading clean and dead-lettering on the FIRST failure -- the delivery check is item.attempts >= max_attempts against a post-increment count (pipeline/wiring_runner.py:5040), so 0 meant give-up-now while reading like "no limit". The floor is on the OPERATOR-FACING setting only, and that is deliberate. RetryPolicy(max_attempts=0) remains a live internal idiom for a permanent no-retry failure: measured across 5 files, including store.mark_failed call sites and asserted by tests at tests/test_batch_completion.py:206-208 and tests/test_postgres_store.py:3109. Constraining the dataclass instead would have deleted a used mechanism while claiming to add a guard -- the reads-as-hardening-but-removes-a-control shape. The item's stated reason for deferring the floor is answered rather than ignored. It said the floor was documented and not fixed "because a floor changes the accepted-configuration set". Under section 0 there are zero deployments, so there is no accepted configuration to break and no migration cost to protect. STILL OPEN, and it is why the item does not close: whether the retry-forever posture needs a TOML or env spelling. "", none and null all raise ValidationError, so that posture is reachable in code-first configuration only. It is a product question, it was handed back rather than decided, and the item itself says it should be decided alongside the floor. A closure on #383 would answer it by omission. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged, and #1217 verified still OPEN after the edit. * backlog: #1242's loss is irreversible, not merely expensive -- and a relay's framing corrected The ASVS Tracker surfaced this defect via the Liaison as a candidate for a NEW item. It is not new: it is #1242, filed 2026-08-13, and Builder 2 is building it. No number was allocated. A duplicate ledger row is not a harmless extra line -- it splits the work and the second number looks unbuilt forever, because the fix lands under the first. Two things in the relay were genuinely new. One is recorded here; the other is deliberately not. RECORDED: the loss is not recoverable by re-running the derivation. The anchors most at risk are the D3 backfill's, and their warrant was that two independent derivations AGREED, measured at two different refs. Those refs have moved. So a fresh derivation reproduces values without reproducing the agreement that justified writing them, and that agreement is the whole evidentiary content. The loss is therefore irreversible rather than expensive, which is why this item outranks other writer defects rather than being one among them. Nothing in the item said this. NOT RECORDED, deliberately: the supporting anchor counts. docs/BACKLOG.md is public, and a tally over a closed public requirement set is the shape that hands out coverage by subtraction. The mechanism is fully stated without them -- a reader with vault access can price it, and a reader without one still knows what to fix and why. Verified my added lines carry no such figure, with a positive control proving the scan discriminates. AND THE RELAY'S FRAMING IS CORRECTED BY THE ITEM'S OWN TEXT. It described the defect as dropping sym/ctx. #1242 already forbids fixing it that way: the defect is the handling of UNKNOWN keys, and a fix special-casing those two by name rebuilds the same trap for the next field added. The item was ahead of the relay and a builder must follow the item. The Tracker's core claim was verified here rather than taken: sym and ctx each return 0 occurrences in scripts/asvs/apply.py at origin/main, positive control expect returns 2. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 281 items / 204 open / 77 closed, unchanged, and #1242 verified still OPEN. * backlog: close #1238 and #1239 on the code, and file #1253 so the closure does not retire the hazard Builder 2 reported four banners owed. Two of the four -- #1237 and #1204 -- were already authored on this branch and are invisible to that lane because ledger authorship and push are held by different seats. That is recorded as a finding in the episode note; it is not fixed here. The two genuinely owed are written now. Both closures verified against origin/main rather than against the build report, because a banner that closes on a report inherits the report's errors. #1238: _is_contained_name is defined at transports/remotefile.py:97, wired at :954, and asserted in BOTH polarities at tests/test_remotefile_transport.py:1185 and :1195. The one-polarity case is called out because such a test passes against a function that refuses everything. posixpath.basename() was not used, per the owner ruling. #1239: _has_control_char returns 0 occurrences across messagefoundry/ at origin/main, so the pair it named is a single. The item's own condition is met. #1253 exists because closing #1239 there would have been true of the item as written and false of the hazard it describes. The reporting lane amended its own closure recommendation to say so -- the predicate is copied more widely today than when #1239 was filed, partly by the work that resolved it. A repo-wide re-measure widened that further: the amendment scanned transports/ and found five sites across four files; across messagefoundry/ it is seven across six, the two extra being config/codeset_edit.py:305 and config/impact.py:631. Two exclusions are recorded in the item so a later scan does not re-add them: rest.py:109 matches a naive grep but is prose in a docstring, and sniff.py:179 tests the same code points through a genuinely different byte-wise predicate that subtracts an allowlist, so folding it in would change its behaviour. rest.py:111 strips where the others reject. That is recorded as defensible and NOT as a second instance of the pattern the owner ruled against in #1238, so the next reader does not inherit a false lead: stripping CR/LF from a header value cannot redirect a request, whereas basename() mutates a path into a real and different target. #1253 allocated with alloc.ps1, never grepped. parse_items before and after: 281/204/77 -> 282/203/79, matching the predicted delta for two closures plus one filing, with a control confirming no item carries a stray banner. * backlog: record that #1234 is not startable from main -- its subject has not landed Builder 2 refused a restock offer of #1234 by correcting its OWN earlier recommendation, and verified before claiming rather than after. Re-measured here rather than taken: at origin/main, require_least_privilege returns 0 hits in any .py and appears only in this ledger's own prose. Positive control on the same instrument, require_managed_identity, returns hits across four .py files, so the scan sees Python fine -- the zero is a fact about the tree, not a broken needle. The probe this item reports a defect in exists only on the w3-store-privilege-preflight branch, dormant at that reading. The amendment is careful not to overrule the item's own "independent of #1008" paragraph, because that paragraph is right about a different thing. Both halves hold: the item is not hostage to #1008's POLICY ruling, and it is nonetheless unbuildable by any lane working from main until that BRANCH lands. Collapsing the two would either re-gate a code defect behind a demand gate or keep offering work whose subject does not exist. This is the same wasted-claim cost as #1253's provenance, one layer deeper: there, an item was unstartable because the FIX had already landed; here, because the SUBJECT has not. A banner-driven queue cannot distinguish either case from startable work, which is why both are now written down where the next dispatcher reads rather than left in session mail. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 282 items / 203 open / 79 closed, unchanged, and #1234 verified still OPEN. * backlog: file #1254 -- a required check is named for its subject, not its assertion Handed to me by the Liaison to number if I judged it worth one, in the Lander's framing. It is, because the naive fix is dangerous and nothing currently records that. MEASURED INSTANCE: the Windows leg went red under the label "test (windows-2025, py3.14)", which reads as "the tests failed on Windows". The tests passed. What failed was the wall-clock gate "Step margin -- both gated steps" (ci.yml:675). The check answered its own question truthfully and the NAME described a different one -- the reverse of the shape this project keeps hitting, where the label is honest and the instrument is not. The job name is built at ci.yml:42 from the matrix, so all three legs are named for WHERE they ran and never for WHAT they assert, while holding at least three independent assertions. Stated as at least three rather than enumerated. WHY THIS IS NOT A ONE-LINE RENAME, which is the whole reason it needed writing down: those three strings ARE required contexts. They are listed in .github/required-contexts.txt, asserted against branch protection by tests/test_required_contexts.py, and matched BY NAME on the GitHub side. A required-but-absent context blocks every PR forever, so a rename is one atomic change across the workflow, the contexts file, that test's pinned count, and the branch-protection setting, in the order that file's header prescribes. So the item deliberately does NOT recommend the rename. It prices three options and names the cheapest first: make the margin gate's failure output say in its first line that the suite passed and a timing gate fired. That costs nothing and cannot wedge the repo. The rename is listed third. Severity carries no deployment axis, but the near-miss is recorded: the misreading pointed at the wall-clock cap, and #1096's banner already says the actual fix is #320 and that re-deriving the caps is itself the failure mode. #1254 allocated with alloc.ps1, never grepped. parse_items before and after: 282/203/79 -> 283/204/79, matching the predicted delta for one filing, with a control confirming no item carries a stray banner. * backlog: amend #1235 -- its two named instances are inert, and I dispatched the opposite Builder 2 refused the starting fact I gave it and measured instead. It was right and I was backwards. I told it to start #1235 from #1203 as a CONFIRMED LIVE TRAP, reasoning that an allocation record with no ledger entry meant the number was free. The record is what makes the number permanently UNAVAILABLE. Verified here rather than taken: 1203.json and 1231.json both exist in .git/mefor-coord/alloc/backlog/ (claimed 2026-08-09 and 2026-08-12), and alloc.ps1 has no release at all -- :41 "a one-way door -- claims are never released", :24 "numbers are never reclaimed ... holes are free, collisions are not". So the item's own text is wrong where it says #1231 was "allocated and released without being filed", and wrong that the pair is "defused only by an accident of timing". They are defused by construction. ONE CORRECTION AGAINST THE REPORT AS WELL, because its reason is weaker than its conclusion. The argument as relayed rests on the allocation RECORDS existing. Those live under .git: uncommittable, machine-local, losable without trace. The reason that survives their loss is structural -- alloc.ps1 issues $observed + 1 (:392, and :389 under the public floor clamp) and NEVER fills a hole, so a number below the floor is unreachable whether or not its record still exists. Recording the registry as the protection would make a sound property look fragile and invite a guard nothing needs. The live shape is the other one and the item now says so: a citation to a number NEVER allocated sits above the floor and will be issued in the normal course. A detector reading only the ledgers rates the two states identically, which over docs/ in this repo mis-scores 26 reserved citations as live; of the 6 genuinely never-allocated tokens there, all six are foreign references, so this repo holds zero genuine instances. The private-repo population the item was filed against is not re-measured here and is stated as separate. The remedy is unchanged and still correct. Only the account of WHY the two named instances are harmless is corrected. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 283 items / 204 open / 79 closed, unchanged, and #1235 verified still OPEN. * backlog: correct #1235 and #1254 -- two of my own committed claims, falsified under adversarial check I ran six independent skeptics over every claim I committed tonight, against the SIMULATED POST-MERGE TREE rather than my branch, because my branch was six behind origin/main and the merge is CLEAN -- git conflicts on concurrent edits, never on invalidated claims. Four claims held. Two did not. Both failures are my authoring, not merge drift: all cited files are byte-identical across the merge tree, origin/main and HEAD. #1235. The conclusion survives, the reason did not. I had written that the allocation registry is irrelevant because the mechanism is structural. That is FALSE for the newest number: the high-water ratchet persists $floor, the maximum of the OBSERVED set (:205, :214, :215), NOT the number being issued, so after issuing N it holds N-1. alloc.ps1 only PRINTS the heading, so until it is committed the sole durable record of N is its own untracked, never-pushed <n>.json. Lose that and the next run re-issues N. So the registry is exactly what protects a just-allocated-but-unfiled number -- the state #1203 and #1231 were both in when allocated. What actually makes those two unreachable is a CONJUNCTION, now stated as one: the loop never searches downward (:392, :389, :394), AND the floor is computed from COMMITTED LEDGER HEADINGS. Measured non-destructively with -ShowFloor: floor 1254, swept from docs/BACKLOG.md and the closed archive. Those are tracked content on refs, they survive a fresh clone, and both numbers sit far below them. Recorded the public-floor clamp as a THIRD, separate guarantee about the output range, with the caveat that its own anti-lowering ratchet lives in the same untracked directory and is disarmed on a registry-absent clone. #1254 cited the margin gate as ci.yml:675. That is a clock MARK (step_margin.py --mark between, :677). The gate is :765, if: always(), invoking at :778 and :781. The error is worth recording rather than silently fixing: I opened :675, found a step whose name NEARLY matched, and adopted it instead of treating the near-match as the signal the line was wrong. A near-miss terminates the search; no match would have continued it. Also corrected in #1254: tests/test_required_contexts.py does NOT call the GitHub API. It pins the count at :101 and resolves contexts against real workflow job names at :107; the branch-protection comparison is a HUMAN step in the comment at :100. The item's central argument is unaffected -- the strings are still required contexts and a rename still resolves them to no job -- but the evidence now says what the test does. The four that held: #1253's seven-sites-across-six-files with both exclusions and every line number, #1239's zero occurrences, #1238's defined-wired-and-both-test- polarities, and #1234's zero .py hits with its positive control. Amendments only, no heading added, so ledger ownership is not consulted. parse_items unchanged at 283 items / 204 open / 79 closed. * backlog: file #1255 -- two testpaths ship a top-level conftest each Diagnosed by the lane whose own commit tripped it, and filed here because the collision outlives that commit. Seven tests in one file failed in a full run and passed in isolation, twice. Cause: pyproject sets two testpaths, both directories contain a conftest.py, neither contains an __init__.py, so both claim the top-level module name and a bare `import conftest` binds to whichever loaded first. Verified at origin/main rather than taken from the report: both conftest.py files present, both __init__.py absent, and a scan for `import conftest` / `from conftest import` across both trees returns ZERO hits. That zero is why this is filed as LATENT rather than live -- the collision is real and currently untripped, so nothing is failing today and the item must not be cited as a current gap. The signature is recorded because it mis-attributes itself: the mis-bound import surfaces as an AttributeError naming a module path from the WRONG package, not as an ImportError, so it reads as a missing attribute rather than a bad import. Two things the item forbids, both because a plausible fix is worse than the defect. Do not import conftest BY PATH -- its body claims a per-process test slot and registers an atexit unlink, so a second import under another name has side effects. And do not prove a fix in isolation: isolation is precisely the condition under which this defect reports success. The proof has to run both testpaths together and then restore the bare import to confirm the same command fails again. The house idiom already solves it -- tests/_workflow_contexts.py is imported package-qualified at tests/_negative_controls.py:35 -- so the scope is to make the name unambiguous, not to invent a mechanism. #1255 allocated with alloc.ps1, never grepped. parse_items before and after: 283/204/79 -> 284/205/79, matching the predicted delta for one filing, with a control confirming no item carries a stray banner. * backlog: record that PR #382 closed ONE of #1242's four limbs -- the item stays open I told a builder its engine fix closed this item's mechanism and to claim it. That was wrong, and this records the correction where the next reader will hit it rather than in session mail. #382 merged 2026-08-13 with this item's number in its title and a title that faithfully describes what it fixed: the payload-only TOP-LEVEL key limb. Verified at origin/main -- apply.py:97 now walks {**(live or {}), **cell}.items() rather than the live cell alone. That limb is genuinely closed. The limb carrying the item's severity is untouched, and the same revision shows why the union cannot reach it: :98 skips _ORDERED and _SUBTABLES BEFORE the union at :97 is consulted for them, and evidence entries are re-emitted at :101-105 by enumerating exactly path, line and expect (absence at :106-110 by exactly pattern, positive_control, mutation). A key inside a [[cell.evidence]] entry is still dropped -- which is exactly where the backfill put the affected keys, on evidence ENTRIES and not on top-level keys. The top-level table-mangling limb also appears untouched. I had additionally told that builder to stop measuring the two affected key names because the item forbids fixing by naming them. The prohibition is real but I applied it to the wrong activity: the item forbids naming them in a FIX, not measuring their absence as a SYMPTOM, and their absence from the writer is exactly the evidence that the sub-table limb still bites. Recorded as the PARTIAL-MOVE shape, which is the reusable part: a merged PR bearing an item's number, whose title truthfully describes what it fixed, is the strongest available signal the item is done. Verify-before-closing is not enough on its own here -- the verification has to ask WHICH HALF. The item's own proof condition is the discriminator and is unchanged: put an unknown key INSIDE an evidence entry, re-render, assert both that it survives and that the guard refuses when it is deliberately dropped. #382 does not satisfy it, and no test asserting only top-level carry-through will. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 284 items / 205 open / 79 closed, unchanged, and #1242 verified still OPEN. * backlog: record #1242's BASE REQUIREMENT -- the obvious base for limb 4 reverts limb 3 Builder 2 confirmed limb 4 as I described it, then refused to build it and handed it back with a reason better than the instruction I gave. Recording the reason, because it is not discoverable from the item and git will not raise it. Its branch still carried scripts/asvs/apply.py:88 as if key in _ORDERED or key in _SUBTABLES or key in cell: Verified here against that ref rather than taken: the clause is present there and ABSENT at origin/main. `or key in cell` is precisely what #382 deleted to fix limb 3. So a limb-4 fix authored on that base and merged would carry limb 3's REVERSAL in the same diff -- no conflict, no marker, every check green, and the item's own landed fix undone by the commit claiming to extend it. Git raises nothing here because git conflicts on concurrent edits to the same lines, never on a stale base re-asserting a clause that was deleted elsewhere. That is the same family as the clean-merge hazard this session has been working under all night, arriving from the direction nobody watches: not a doc invalidated by a merge, but a FIX reverted by an extension of itself. The item now states the base requirement and a pre-PR check that discriminates: branch fresh from current origin/main, and confirm `or key in cell` returns zero hits in the diff's own version of the file before opening a PR. Also corrected upstream of this, in my own dispatch rather than the ledger: I had told that lane to stop measuring the two affected key names. Measuring their absence as a SYMPTOM was always legitimate; only fixing by naming them is forbidden. It withdrew its acceptance of my earlier "bound lifted" on the grounds that it had taken it from me without measuring -- correctly. Amendment only, no heading added, so ledger ownership is not consulted. parse_items before and after: 284 items / 205 open / 79 closed, unchanged, and #1242 verified still OPEN. * backlog: file #1256 -- the federated binding never checks subject exclusivity Builder 1 concluded #1143's research and handed over the finding rather than a commit: the defensible-ceremony question dissolves, because every candidate collapses to trust-on-first-use when no out-of-band proof exists at first federated login. What it surfaced instead is a separable gap, and that gap is this item. Content theirs, number mine, per the authoring split. Verified at origin/main rather than relayed. auth/service.py:1109-1114 compares user.oidc_subject against the presented subject and refuses on mismatch, then binds at :1119-1120. The comparison is keyed on the USER, so it is structurally incapable of noticing a second account carrying the same (issuer, subject). A scan for a UNIQUE constraint naming the federated columns returns 0 on ALL THREE backends, so nothing below it closes the gap either. The item credits the three shipped controls rather than implying an absence: the hybrid-only refusal, the subject-continuity guard, and the UPN suffix allow-list. All three constrain which subject may bind to a GIVEN account. None constrains how many accounts one SUBJECT may bind to. Stating that explicitly is what should stop this being re-closed as a duplicate of #1015 or #1143. The difficulty is recorded where it actually lives. SQL Server types the federated columns NVARCHAR(MAX), and a MAX column cannot be an index key, so a unique index there needs a RE-TYPE and not merely a constraint -- a cost SQLite and Postgres do not share. The proof condition requires demonstrating the refusal on every backend in CI, because the two server store suites SKIP in a local run and a green on SQLite alone would certify nothing. #1143 is NOT closed by this and the item says so: whether TOFU is the defensible ceremony is separable and still open. #1256 allocated with alloc.ps1, never grepped. parse_items: 285 items / 206 open / 79 closed after, matching the predicted +1/+1/0 for a single filing, with a control confirming no item carries a stray banner. * backlog: record that #1020's refusal path hangs in-harness and is unverified under uvicorn Reported by the lane that built the gate, against its own work, and recorded here because it changes what "built" means for this item. The gate refuses by raising during ASGI lifespan STARTUP, and in-harness that HANGS rather than exiting. Their control is what makes it a measurement rather than an impression: the sibling non-raising lifespan test passes in 1.13s, raising from the lifespan BODY exits cleanly, and raising during STARTUP hangs with zero output. It sits after engine.start() and before the task handles teardown expects, so the condition is PRE-EXISTING -- the gate is simply the first thing to raise in that window, which is why this is not filed as a defect in their fix. What was NOT measured is what uvicorn does there, and uvicorn is the runner that ships. They said so explicitly rather than letting the harness result stand for the product, which is the reason this is worth recording at all. The consequence is ordered plainly in the item: a startup refusal that hangs a service is STRICTLY WORSE than the mis-report #1020 exists to correct. An operator can see a wrong readiness answer; they cannot see a process that never finishes starting. So the banner now carries an explicit bar on closing this on the gate landing until the refusal is shown to TERMINATE under uvicorn rather than under the test harness. Amendment only, no heading added, so ledger ownership is not consulted. parse_items unchanged at 285 items / 206 open / 79 closed, and #1020 verified still OPEN. * backlog: #1235 is PARTIAL, not closed -- the detector shipped, the rule did not I was about to close this on PR #385 landing. An adversarial pass over my own proposed banner refuted it, and the refutation is right. What I would have relied on -- do the two files exist at origin/main -- passes identically whether or not the rule landed and whether or not anything invokes the detector. It measures PRESENCE, NOT ENFORCEMENT, and it is the same cannot-fail shape as running a CLI's --help and reading exit 0, which I had already caught once today in my own #1030 check. Three measurements, each independently sufficient to block closure: the RULE is unwritten. #1235's Scope names its deliverable as "a rule, not a sweep", and that guidance appears at origin/main only in the item's own prose -- zero hits for "unallocated" across CLAUDE.md, docs/LEDGER-GATE.md, CONTRIBUTING, scripts/, .github/ and .claude/. the DETECTOR is wired into nothing. Repo-wide it is referenced by exactly two lines, both inside its own unit test. Not in any workflow, not in .pre-commit-config.yaml, not in .mefor-hooks/pre-commit, not in pyproject.toml. it EXITS 0 even when it fires. main() ends `return 1 if args.fail else 0` and --fail is opt-in and passed by nothing. A planted live-shape citation was reported correctly and the process still exited 0. No test runs unresolved_citations over the real docs/ tree, so a new dangling citation turns nothing red. The controls are what make those zeros trustworthy: the same wiring grep DOES resolve three sibling scripts/docs tools that are wired into CI, and the detector itself discriminates correctly when --fail is supplied. So the absences are facts about the tree, not a broken pattern or a blind scan. Banner moves to in-progress rather than closed, and the item now records what shipped, the two residual limbs (write the rule where authors read it; wire the detector with --fail or test it against the real tree), and a coverage bound that survives both -- by its own docstring the detector cannot see the private companion repository, which is where this item's filed instances live. Closing on the detector's existence would have recorded an enforced rule where nothing enforces it. The builder declined to close it for the same reason and left the ledger half to me. Amendment only, no heading added, so ledger ownership is not consulted. Both glyphs are OPEN, so parse_items is unchanged at 285 items / 206 open / 79 closed, with #1235 verified still OPEN and carrying exactly one banner. * backlog: close #1230 on the owner-ruled half, with the wiring's own regression gap named Four proposed banner verdicts went through an adversarial pass before any was written. Three survived; one did not and was corrected separately (#1235). This is the survivor, and it survived on evidence stronger than I would have gathered. #1230 CLOSES because the loud-omission half is built AND WIRED, not merely present. tests/_extras_probe.py supplies the emitters and tests/conftest.py binds them to pytest's own hooks at :366 and :370. Verified by RUNNING rather than reading, in both directions on two real interpreters: extras-complete venv, banner ABSENT; the venv scripts/worktree/new.ps1 actually builds, banner FIRES in both surfaces -- including on a bare full-suite run collecting 13,187 tests. The silent direction was observed rather than assumed, which is what makes the loud direction mean something. Scope is exactly the half the owner ruled. new.ps1:232 is unchanged, so options (a) and (b) -- installing the five extras into every worktree venv -- were correctly not taken. CLOSED WITH A NAMED RESIDUAL, because folding it into the closure would reproduce the defect. Deleting BOTH hook functions from tests/conftest.py leaves tests/test_incomplete_run_banner.py reporting 7 passed, while a real run goes from banner-present to banner-gone. The tests drive the probe against a fake reporter; nothing asserts that pytest invokes it. Repo-wide, pytest_report_header and pytest_terminal_summary appear ONLY at conftest.py:366 and :370, guarded by no test, lint or gate. So a conftest refactor can silently delete the mechanism with every check green -- which is this item's own failure shape, one layer up, inside the fix for it. The item closes on the owner-ruled half working. The residual is recorded in the banner rather than discovered later by whoever the silence next costs. Amendment plus banner flip, no heading added, so ledger ownership is not consulted. parse_items 285 items / 206 open / 79 closed -> 285 / 205 / 80, matching the predicted 0/-1/+1 for a single closure, with a control confirming no item carries a stray banner and #1235, #1249 and #1030 all verified still OPEN. * backlog: file #1257 -- a startup refusal after engine.start() hangs instead of exiting Found by the lane building #1020, against its own work, when it closed that item's own open question: does the refusal terminate under uvicorn rather than under the harness. It does not, and the answer blocks #1020. The distinction is the finding. uvicorn behaves correctly -- full error printed, "Application startup failed. Exiting.", SystemExit(3), no socket bound. The PROCESS then never exits; measured alive 90 seconds later and killed. So this is a HUNG refusal, not a silent one: an operator at a console sees exactly the right error, and a SUPERVISOR sees a process that started and never stopped. The engine ships under NSSM, and systemd and container runtimes decide the same way -- by process liveness. Running-and-dead is the one state a restart policy cannot detect, which is why this outranks the mis-report #1020 exists to fix. Structural half re-verified here at HEAD rather than taken. Every pre-engine step carries its own unwinding: api/app.py:5608 for startup attestation and :5628 for the trust-anchor preflight each wrap in try/except BaseException, close the notifier and store, and re-raise. From await engine.start() at :5731 to the yield there is no try, no except and no finally at all, and auth_settings handling sits at :5816-5818, inside that unprotected span. Filed separately from #1020 deliberately. The cause is pre-existing and the gate is merely the first thing that has ever raised in that window; a lifespan-teardown change has its own blast radius in code every deployment runs and should not ride in on an auth item. The rejected option is recorded with its reason so it is not re-proposed: moving the check before engine.start() fails on measurement, because the bootstrap admin does not exist until auth.initialize(), which runs later. The proof condition is written to exclude the checks that already pass: raise deliberately in the post-engine span and assert the PROCESS EXITS -- not that the error is printed and not that SystemExit is raised, both of which are true today and neither of which discriminates. #1257 allocated with alloc.ps1, never grepped. parse_items 285/206/79 -> 286/206/80 ... note the closed count moves because the prior commit closed #1230; this filing itself is +1 item / +1 open. Control confirms no item carries a stray banner. * backlog: record that #1250 is now load-bearing, and keep the retroactive question separate Owner ruled 2026-08-14 that an item whose substance is a weakness in the coordination tooling or the seat topology is a DEFICIT and may not go in the public ledger. With no private ledger existing, such an item PARKS pending #1250. That changes what #1250 is. It was a proposal; it is now the thing a real, measured, reproduced defect is waiting on, and the ruling applies to every future item of this class rather than to the one instance. The cost of not having it is therefore per-item and accumulating: each such finding lands in a coordination handoff file, discoverable only by whoever thinks to look there. I did NOT re-score the priority. The P2 line predates the ruling and is left untouched deliberately -- re-scoring is the owner's call, and recording why an item now matters is not the same act as deciding it matters more. Someone reading this should be able to see the new fact and make that decision, rather than inherit a number I moved quietly. The parked item itself is not described here, only that one exists. Describing it in this file is precisely what the ruling forbids, so the amendment records the GATING relationship and nothing about the gated content. The retroactive question is recorded as explicitly NOT settled by this. The ruling answered where a NEW item goes; several items already in this ledger appear to be the same class, and they have a different cost structure because unfiling from a public repository does not unpublish anything. A boundary never applied retroactively is not a boundary ignored, and merging the two questions would let the cheaper answer decide the harder one. Amendment only, no heading added, so ledger ownership is not consulted. parse_items unchanged at 286 items / 206 open / 80 closed, #1250 verified still OPEN, no stray banners. * backlog: file #1258 -- classify existing ledger items against the deficits ruling Owner-authorised. I raised this observation earlier and deliberately did not act on it, on the grounds that reclassifying existing public items is not a dispatcher's unilateral call. It was escalated, the owner said file it, so it is filed. FILED AS BLOCKED BEHIND #1250, and that is a judgment I made before dispatching rather than one discovered when the result lands. The sweep's product is a list of public items that disclose tooling weaknesses. That list is an index to exactly what the rule protects -- the same aggregation argument that kept the 2026-08-14 item out of this file. So the output cannot be written here, and with no private ledger existing it has nowhere to go. Running the sweep first would manufacture a finding that must then be parked in a handoff file, adding to the cost #1250 exists to end. WORDING CONSTRAINTS ARE THE SUBSTANCE OF THIS ITEM, not decoration on it. The item names the RULE and the SCOPE and no candidates. It records no count either, because a count over a bounded set is the same disclosure one subtraction later. Verified before committing: the only backlog number appearing in the item's body is #1250, its blocker, with a positive control showing the same pattern finds 543 references across the file -- so the single hit is a true reading rather than a broken scan. An item that enumerated its candidates would perform the defect it was filed to find. The remedy is explicitly left open. Unfiling from a public repository does not unpublish anything, so what to do about anything found is a separate question with a different cost structure, and this item must not answer it by implication. A reclassification that moves text without reducing exposure is churn wearing the shape of a fix. The framing is a constraint too. The ruling is dated 2026-08-13 and the items most likely to match were filed around the same time. A boundary never applied retroactively is not a boundary ignored. This is hygiene, not an audit of anyone's judgement, and if the output reads as an accusation it has been written wrongly. #1258 allocated with alloc.ps1, never grepped. parse_items 286/206/80 -> 287/207/80, matching the predicted +1/+1/0, no stray banners. * docs(ledger): write the citation rule #1235 asked for, in both places a reader looks #1235's stated deliverable was "a rule, not a sweep", and an adversarial pass established that the rule existed nowhere at origin/main except the item's own prose. A builder shipped the detector and correctly declined this half as authorship rather than build work. This writes it. TWO PLACES, because "where authors read it" is the whole point and they are different audiences. CLAUDE.md section 5 gets it as a bullet directly beneath the never-grep-for-a-number rule it mirrors -- an author reading the allocation rule now meets the citation rule in the same breath. docs/LEDGER-GATE.md gets the full treatment under its own section, next to the machinery. THE RULE: either allocate the number before citing it, or write a reference that CANNOT resolve. Naming the subject instead of a number costs nothing and cannot arm. WHAT THE LONG FORM ADDS, because a bare rule invites the wrong remediation: - Only a citation ABOVE the allocation floor can ever arm. A reserved-but-never- filed number is inert PERMANENTLY -- alloc.ps1 issues $observed + 1, never fills a hole, and computes its floor from committed ledger headings that survive a fresh clone. Without this, a reader would treat every unresolved citation as a live trap and sweep the harmless majority. - Foreign #N references are not citations of this ledger at all and are not the rule's subject. - Enforcement does not replace the rule. A checker finds only what has already been written; the rule is what stops it being written. THE CLAUDE.MD EDIT IS APPEND-ONLY inside an existing bullet list, and I verified section numbering is unchanged -- 13 numbered headings before and after -- because renumbering that file silently breaks citations across the tree and nothing validates one. The new cross-reference was checked to resolve: the LEDGER-GATE.md anchor it names exists. #1235 stays PARTIAL. Limb (i) is discharged and recorded as such; the wiring and fail-closed halves are a builder's work on an unlanded branch, so the item does not close on this. parse_items unchanged at 287 items / 207 open / 80 closed, #1235 verified still OPEN and carrying exactly one banner. * backlog: file #1259 -- parse_items censuses a conflicted ledger without error The landing seat ran parse_items on a merge-tree output BEFORE checking the merge's exit code and got a census matching my incoming batch's prediction exactly: right item count, right open count, both new numbers present, no duplicates. The exit code was rc=1 with six conflict markers in the blob. It reported the near-miss against its own process rather than quietly reordering its checks, which is why there is an item. Reproduced here with a control rather than taken: the live ledger and a copy poisoned with conflict markers BOTH parse to 287 items / 207 open, and nothing raises. The counts being IDENTICAL is the finding -- the census cannot be used to detect the condition, because the number is right and the file is unusable. I nearly recorded that a gate already covers this. A grep for "conflict" across the ledger gates returned a hit in ledger_check.py, and reading the context showed both matches are PROSE -- a comment about clean merges and an error string reading "not as a conflict". Neither detects a marker. backlog_status_check.py and backlog_citation_check.py contain no detection either, and .pre-commit-config.yaml runs nothing that inspects Markdown for markers. So a conflicted docs/BACKLOG.md can be committed and every ledger gate passes over it. Scope prices two options and says which one matters. The standard check-merge-conflict pre-commit hook is the cheap general fix and covers every file. Making the READER refuse a conflicted source is the one that protects PROGRAMMATIC callers, and that is where this bit -- a hook does not help a gate handed a tree in memory. The proof condition excludes the check that cannot fail: poison a copy of the real ledger and assert the reader FAILS. Asserting it parses a clean file proves nothing; it already does that, and did so throughout the incident. The general rule is recorded with the item because it outlives the fix: read the EXIT CODE before the CONTENT, because a content check on a conflicted tree certifies nothing. #1259 allocated with alloc.ps1, never grepped. parse_items 287/207/80 -> 288/208/80, matching the predicted +1/+1/0, no stray banners. * backlog: reframe #1242 limb 4 as the unbuilt half of an existing spec The ASVS Tracker supplied this from the design record and it is a better brief than anything in the item so far. The promotion of the writer was SPECIFIED to carry through the union of live and payload keys, so the schema could grow without hand-editing the record. What shipped delivers that for top-level scalars and drops it for sub-table entries. So limb 4 is not a new requirement. It is the unbuilt half of one, and "restore the specified behaviour" is a stronger instruction to a builder than "also handle sym and ctx" -- the latter invites a fix keyed to the two names that happen to exist today, which would satisfy the symptom and still drop the next field anyone adds. The Tracker made that same point about its own earlier framing and I am recording its version rather than mine. The evidenc…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Anchors the two FHIR path-segment patterns with
\Zinstead of$(BACKLOG #1240).Python's
$also matches immediately before a final newline, so^[A-Za-z]+$accepted"Patient\n"and the gate did not enforce the grammar it advertises.Fixed on the patterns, not the call sites -- deliberately
The item as filed prescribed "two one-line changes:
matchtofullmatchon both regexes". That isnot executable:
matchversusfullmatchis a property of the call, and there are three callsites (
:189,:698,:704), so it would be three edits and a future caller could re-introduce it.$to\Zis genuinely two lines, covers all three call sites, and cannot be re-broken by a new caller.Deliberately not done
The item's read-path
_reject_control_charslimb. Redundant once the gates are strict -- both charsetsexclude every C0 and DEL character, and every character of the query reaches a gate -- and adding it
would re-introduce the duplication that #1239 records as retired by #1243.
Test shape, because the obvious test proves nothing
"Patient/123\n"is normalised away upstream and builds a URL byte-identical to the clean input,measured before and after the fix -- so the obvious test passes either way. Only
"Patient\n/123", anLF ending a segment followed by more path, reaches a gate carrying the newline.
Both shapes are pinned: the discriminating case asserts the refusal, and a second test pins the
normalisation, so if that ever starts raising, the first test is known to need re-deriving rather than
deleting.
Red-first: both parametrized cases failed with
DID NOT RAISE ValueErroragainst the unfixedpatterns; the normalisation pin passed before and after, as it must.
No banner flip, and that is compliance rather than an oversight
This branch touches exactly two files and nothing under
docs/. The ledger disposition is withhelddeliberately and routes to the Dispatcher.
Flagging it explicitly because the opposite cause produces the same visible shape: earlier today PR #372
landed a fix with no ledger edit, and its item sat reading not-started while the fix was already on main.
Verification scope -- stated, not implied
Re-run against the rebased commit, not the originally tested one:
The full suite was NOT run locally and neither test path was collected in full. CI is the gate for
that. All commit hooks passed, including ruff and bandit.
ALSO CARRIES #1241 (added after this PR was opened)
http.client.InvalidURLis not a ValueError and not an OSError -- its MRO isInvalidURL -> HTTPException -> Exception-- so it matched none of_post's arms, including theValueError backstop at
:638whose own comment says it exists for "a CRLF in a header/URL that slippedpast the control-char guard". The arm written for this exception could not catch it.
Now caught and named explicitly, with the MRO in the comment so nobody folds it back into a bare
ValueError.#1241 IS A PARTIAL FIX -- DO NOT CLOSE THE ITEM ON IT
The item's filed claim -- operator-config values reaching the URL and header sinks with no
construction-time screen -- still HOLDS for both sinks and is not addressed here. What is fixed is
the narrower defect found while re-verifying: when the URL sink fails, it failed in the wrong class.
Scope bound worth carrying: the URL limb has two incidental neutralisations the header limb does not
--
urllib.parse.unwrapstrips a trailing CRLF, andRequest.full_urlsplits at#client-side.The header sink has neither, which is why the construction-time screen is still needed.
Red-first, with a shipped negative control
Red was the defect itself, not a proxy: the test failed with a raw
http.client.InvalidURLescaping_post.A second test ships as the negative control -- a
URLErrormust STILL raise a retryableDeliveryErrorand NOT aNegativeAckError-- so this cannot pass by the method having been widened tosweep everything into the permanent class. It passed before and after.
Why both fixes are on one branch
Both write
transports/fhir.py, and the write serialisation on that file was explicit. Splitting theminto concurrent branches is the thing that was ruled out, and stacking a second PR on this one would
create a pre-squash base the moment either merged.
Verification for the combined branch
The full suite was NOT run locally, neither test path was collected in full, and the webconsole suite
was not run at all. CI is the gate for that.
Both diff instruments agree on the tip: three-dot and two-dot are byte-identical at 3 files, 77
insertions, 3 deletions, and
docs/is untouched -- so no banner flip in either commit, deliberately.AND #1241's ACTUAL FILED DEFECT (third commit, f7bd300)
The earlier commit fixed the wrong-exception-class defect. This one fixes the claim as filed.
conditional_querywas taken verbatim from operator settings and reached two sinks with no screen --an unencoded URL interpolation and the
If-None-Existheader value.urlwas equally unscreened.Screened at construction, and the disposition is the point
_reject_config_control_charsraisesValueError, deliberately distinct from the existing_reject_control_chars, which screens message-derived values and raises a permanentNegativeAckError.So a bad setting fails the connection at load rather than dead-lettering an unbounded stream of
messages that were never at fault.
The header sink is why the send path was not enough. The URL limb has two incidental neutralisations
it does not --
unwrapstrips a trailing CRLF,full_urlsplits at#client-side -- and neithertouches a header value.
Still incomplete -- #1241 must NOT be closed on this either
transports/dicomweb.py, which the item also names, is untouched.FhirLookupExecutorhas a SECOND unscreened url construction site in the same file, found onlybecause an edit matched two locations instead of one. Outside what was dispatched, so reported rather
than fixed.
Red-first, including one that had to be rebuilt
All five new cases red with
DID NOT RAISE ValueError. One first failed with aTypeError-- thetest passed
url=through a helper that already supplies it -- and a test failing for the wrong reasonis not a red-first proof, so it was rebuilt to construct the Destination directly and re-confirmed red.
Positive control shipped: a clean
conditional_querycarrying|,:and/still constructs andis preserved verbatim, so the screen cannot pass by rejecting everything.
Verification
The full suite was NOT run and the webconsole suite was not collected at all. CI is the gate.
A diff disagreement that was chased, not assumed
Two-dot and three-dot disagreed after this commit (3 files vs 5, 131 deletions) -- the same signature as a
genuine squash-revert caught earlier today. The discriminator was run rather than assumed:
So this branch is merely behind, not reverting: main gained #376 during the build, and two-dot renders
main's newer work as deletions. Intersection test, the decisive one: files changed here
(
fhir.py+ tests) versus files main changed since the merge-base (remotefile.py+ its test) --empty intersection, so a merge cannot lose anything. Re-verified independently before this push.