From fc91103b0caa48f921ff03e8b22fe7f55d479c8d Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Mon, 7 Sep 2026 15:41:51 -0500 Subject: [PATCH 1/3] docs(backlog): file #1484 -- the scorecard writer's retirement path cannot empty a list (BACKLOG #1484) Found while doing BACKLOG #1004's vault half. It did not block that work and is filed because it outlives the cell it was found on. THE DEFECT. scripts/asvs/apply.py grew --allow-retirement for exactly one situation: an absence claim whose gap has CLOSED, so the record should stop asserting it. That path cannot execute when the cell holds only one claim. A payload carrying an empty absence list renders a cell with no absence key at all, because an empty array of tables emits nothing. The field-preservation invariant runs first, compares key sets, and refuses with "would LOSE field(s) ['absence']" -- so the guard that exists to catch a DROPPED KEY fires on a DECLARED RETIREMENT, before the code written to authorise that retirement is reached. The flag can shrink a list of several; it cannot empty one. MEASURED, WITH A POSITIVE CONTROL ON THE SAME RUN. Against cell 13.3.4, whose single claim had genuinely closed. Payload built by the sanctioned route, flag passed, retired_absence declared with the drop accounted for. It refused, and never printed the RETIRING: line -- the writer's own evidence that the branch did not execute. The identical payload with the claim REWRITTEN rather than emptied, cardinality 1 to 1, was accepted and left 345 cells intact. So the refusal is specific to emptying the list, not to the payload, the flag, or the declaration. WHY THE WORKAROUND IS NOT A FIX. #1004 rewrote its claim instead, and that was better on its own merits. It is not always available: a cell whose only claim has closed, with no adjacent gap worth guarding, has nowhere to re-aim, and is left choosing between asserting a closed gap and hand-editing the file. NO FIX PROPOSED, deliberately. The obvious shapes both touch an invariant that exists because a truncating repair once cut one cell 15 to 10 and another 17 to 1 with the verifier green throughout. The two checks need ordering, not trading. ONE CITATION DELIBERATELY NOT MADE BY NUMBER. The hand-edit incident this row cites is described by its measured evidence -- vault commit c117e0a2, 298 of 345 cells changed under a subject naming one -- rather than by the item number a peer gave for it. That number is allocated but does not resolve on main yet, and a citation resolving to nothing today starts resolving to unrelated work the day someone allocates it. Number allocated with scripts/coord/alloc.ps1, not grepped. backlog_status_check OK at 684 items and backlog_citation_check OK; the one remaining advisory warning belongs to #1426 and is pre-existing. Co-Authored-By: Claude Opus 5 --- docs/BACKLOG.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 6d12eccf6..b318c5455 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -24617,3 +24617,39 @@ A PAR client on the relying-party side: POST the authorization parameters to the This row was **filed, not built**. Nobody has read the pinned requirement text against `flow.py` for this row's purposes, and no provider-support survey was run. The `na` ruling above is reported as the routing fact that makes this row necessary; this row does not re-derive it and does not depend on it being correct. +## 1484. `scripts/asvs/apply.py` cannot retire a cell's LAST absence claim -- the field-preservation guard refuses before `--allow-retirement` runs + +> 🔢 **Filed 2026-09-07 -- not started.** Value **3/10** · Difficulty **4/10** -- _quick win_. **Found while doing BACKLOG #1004's vault half; it did not block that work and is filed because it outlives the cell it was found on.** The sanctioned scorecard writer grew a retirement path for exactly one situation -- an absence claim whose gap has CLOSED, so the record should stop asserting it -- and that path cannot execute when the cell holds only one claim. It is reachable only for a cell holding two or more. +> Verdict: build +> Research: none +> Closing-act: code + +**Cluster:** ASVS tooling. **Priority:** P3. **Verdict:** build. +**Severity:** low. Nothing is wrong in the engine and nothing in the record is false. The cost is that a legitimate, sanctioned edit is unavailable through the writer, so whoever needs it either leaves a closed gap asserted in the record or reaches for a hand edit -- and a hand edit of this file is the move behind the incident measured on vault commit `c117e0a2`: a subject naming ONE cell, **298 of 345 cells changed**, an owner-approved repair to an unrelated cell silently reverted, undetected for three days. Re-measured here 2026-09-07 by parsing both sides rather than reading the report. That incident has its own filed row; it is deliberately not cited by number, because the number a peer named for it does not resolve on `main` at the time of writing and a citation that resolves to nothing today starts resolving to unrelated work the day someone legitimately allocates it. + +### The defect + +`--allow-retirement` permits a cell's `evidence` or `absence` list to SHRINK when the payload DECLARES what it is retiring. The declaration is checked, and the arithmetic is checked: declare one and drop two and it still refuses. That design is right. + +It is unreachable for a 1 -> 0 drop. A payload carrying `"absence": []` renders a cell with no `absence` key at all, because an empty array of tables emits nothing. The **field-preservation invariant** runs first and compares key sets: + +``` +REFUSING: cell 13.3.4 would LOSE field(s) ['absence'] +``` + +So the guard that exists to catch a DROPPED KEY fires on a DECLARED RETIREMENT, before the code written to authorise that retirement is reached. The flag can shrink a list of several; it cannot empty one. + +### Measured 2026-09-07 + +Against cell 13.3.4, whose single absence claim had genuinely closed -- the engine half of #1004 shipped the refusal the claim recorded as missing. Payload built by the sanctioned route, `--allow-retirement` passed, `retired_absence` declared with the drop accounted for. The run refused as above and never printed the `RETIRING:` line, which is the writer's own evidence that the retirement branch did not execute. + +**Positive control on the same instrument, same run:** the identical payload with the claim REWRITTEN rather than emptied -- cardinality 1 -> 1 -- was accepted, re-rendered one cell block, and left 345 cells intact. So the refusal is specific to emptying the list, not to the payload, the flag, or the declaration. + +### Why the workaround is not a fix + +#1004 rewrote its claim instead, re-aiming it at a sibling gap that is still open. That was the better outcome **on its own merits** and would have been chosen anyway. It is not always available: a cell whose only claim has closed, with no adjacent gap worth guarding, has nowhere to re-aim. That cell's options today are an asserted-but-closed gap or a hand edit. + +### Not done here, and named rather than implied + +No fix is proposed in this row. The obvious shapes -- emit an empty `absence` key, or order the retirement check before the key-set check -- both touch an invariant that exists because a truncating repair once cut one cell 15 -> 10 and another 17 -> 1 **with the verifier green throughout**. Whoever builds this must not weaken that guard to reach the retirement path; the two need to be ordered, not traded. The writer is `scripts/asvs/apply.py` in this repository, which is the ORIGINAL -- the vault carries a mirror, and mirror drift is a separate, separately-tracked condition. + From 3bb374b38d32c3c1093a7562760c54663757cb9c Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Mon, 7 Sep 2026 15:44:22 -0500 Subject: [PATCH 2/3] docs(backlog): separate the two c117e0a2 numbers, both of which are in circulation (BACKLOG #1484) #1484 cited "298 of 345 cells changed". That is correct and it is not the number most readers want. Two figures for that incident are circulating between sessions today and they answer different questions, so the row now carries both and says which is which. Measured by parsing both sides of the commit rather than reading a report: cells whose ANY field changed : 298 of 345 cells whose VERDICT moved : 8 The eight are 12.1.5 fail -> na, which the subject announced, plus SEVEN silent pass -> partial reverts on unrelated cells: 2.3.4, 5.1.1, 6.1.3, 11.1.1, 11.5.1, 14.2.1, 15.3.2. The seven are the alarming figure; the 298 is the blast radius. A NOTE ON THE TALLY ADVICE GOING ROUND, which this row does not adopt. A peer suggested checking a verdict tally. For this commit that would have worked, because the tally did move (pass 179 -> 172). The general warning behind it is still right -- two cells swapping leaves a tally byte-identical -- so a tally is not a map. The control used on #1004's vault half is a full cell-dict comparison, which catches a verdict move, an anchor change and a residual edit alike and needs no advance judgement about which fields matter. backlog_status_check OK at 684 items, backlog_citation_check OK. Co-Authored-By: Claude Opus 5 --- docs/BACKLOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index b318c5455..cd8846b6c 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -24625,7 +24625,7 @@ This row was **filed, not built**. Nobody has read the pinned requirement text a > Closing-act: code **Cluster:** ASVS tooling. **Priority:** P3. **Verdict:** build. -**Severity:** low. Nothing is wrong in the engine and nothing in the record is false. The cost is that a legitimate, sanctioned edit is unavailable through the writer, so whoever needs it either leaves a closed gap asserted in the record or reaches for a hand edit -- and a hand edit of this file is the move behind the incident measured on vault commit `c117e0a2`: a subject naming ONE cell, **298 of 345 cells changed**, an owner-approved repair to an unrelated cell silently reverted, undetected for three days. Re-measured here 2026-09-07 by parsing both sides rather than reading the report. That incident has its own filed row; it is deliberately not cited by number, because the number a peer named for it does not resolve on `main` at the time of writing and a citation that resolves to nothing today starts resolving to unrelated work the day someone legitimately allocates it. +**Severity:** low. Nothing is wrong in the engine and nothing in the record is false. The cost is that a legitimate, sanctioned edit is unavailable through the writer, so whoever needs it either leaves a closed gap asserted in the record or reaches for a hand edit -- and a hand edit of this file is the move behind the incident measured on vault commit `c117e0a2`: a subject naming ONE cell, **298 of 345 cells changed** and **8 verdicts moved**, of which SEVEN were silent `pass` to `partial` reverts on unrelated cells (2.3.4, 5.1.1, 6.1.3, 11.1.1, 11.5.1, 14.2.1, 15.3.2), undetected for three days. **Those two numbers answer different questions and are both in circulation** -- 298 is the blast radius, 8 is the verdict movement -- so cite the one you mean. Re-measured here 2026-09-07 by parsing both sides rather than reading the report. That incident has its own filed row; it is deliberately not cited by number, because the number a peer named for it does not resolve on `main` at the time of writing and a citation that resolves to nothing today starts resolving to unrelated work the day someone legitimately allocates it. ### The defect From cc0b9976f306b5d32de92bec9706cdb87efca5a9 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Wed, 9 Sep 2026 08:08:01 -0500 Subject: [PATCH 3/3] docs(backlog): reword #1484 so its incident numbers stop matching the ASVS tally lint The row's severity paragraph wrote "298 of 345 cells changed". The ASVS tally lint reads any "N of 345" as a hard-coded whole-corpus tally (idiom AGAINST_TOTAL in scripts/docs/asvs_tally_lint.py), so the new row failed tests/test_asvs_tally_lint.py and reddened the CI gate rollup. The numbers here are not a corpus tally. They describe one incident: how much one hand edit touched, and how much of that moved a verdict. The reword keeps all three numbers and the blast-radius-versus-verdict-movement distinction the paragraph draws two sentences later, while taking 298 out of the N-of-M shape the lint matches. before: **298 of 345 cells changed** and **8 verdicts moved** after: **298 cells changed** (the record then held 345) and **8 verdicts moved** Deliberately not done, and the reason each time: - No line added to scripts/docs/asvs_tally_baseline.txt. Its header says the list is FROZEN and may only SHRINK, so an added line breaks the ratchet the file exists to be. - No carve-out added to _AGAINST_TOTAL. A claimed precedent for one is false: the lint carries no allowlist, placeholder or exemption for that idiom. The "N of 345" note at lines 87-88 is a comment explaining that the regex opens with a digit class, so a letter placeholder cannot match by construction. Widening the idiom is a lint policy change and belongs in its own item. Verified: scripts/docs/asvs_tally_lint.py over docs goes from "24 tallies total, 23 grandfathered, 1 NEW" at rc=1 to "23 tallies total, 23 grandfathered, 0 NEW" at rc=0 with nothing else changed. Co-Authored-By: Claude Opus 5 --- docs/BACKLOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 0748a5e62..56537df62 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -27576,7 +27576,7 @@ One docstring edit in `scripts/asvs/apply.py`, stating that the listed keys are **Cluster:** ASVS tooling. **Priority:** P3. **Verdict:** build. **Not a duplicate of [#1483](BACKLOG.md#1483), which was filed the same day on the same script.** #1483 is a DOCSTRING understating the code -- the module header enumerates three evidence fields while `_carried()` preserves every key it does not order, so the writer looks lossy and is not. This row is a CONTROL-FLOW gap: a sanctioned edit the writer cannot perform at all. Neither fix touches the other's code path. Filed separately by agreement between the two sessions rather than merged into one row, because a reader hitting the retirement refusal will not find it under a docstring heading. -**Severity:** low. Nothing is wrong in the engine and nothing in the record is false. The cost is that a legitimate, sanctioned edit is unavailable through the writer, so whoever needs it either leaves a closed gap asserted in the record or reaches for a hand edit -- and a hand edit of this file is the move behind the incident measured on vault commit `c117e0a2`: a subject naming ONE cell, **298 of 345 cells changed** and **8 verdicts moved**, of which SEVEN were silent `pass` to `partial` reverts on unrelated cells (2.3.4, 5.1.1, 6.1.3, 11.1.1, 11.5.1, 14.2.1, 15.3.2), undetected for three days. **Those two numbers answer different questions and are both in circulation** -- 298 is the blast radius, 8 is the verdict movement -- so cite the one you mean. Re-measured here 2026-09-07 by parsing both sides rather than reading the report. That incident has its own filed row; it is deliberately not cited by number, because the number a peer named for it does not resolve on `main` at the time of writing and a citation that resolves to nothing today starts resolving to unrelated work the day someone legitimately allocates it. +**Severity:** low. Nothing is wrong in the engine and nothing in the record is false. The cost is that a legitimate, sanctioned edit is unavailable through the writer, so whoever needs it either leaves a closed gap asserted in the record or reaches for a hand edit -- and a hand edit of this file is the move behind the incident measured on vault commit `c117e0a2`: a subject naming ONE cell, **298 cells changed** (the record then held 345) and **8 verdicts moved**, of which SEVEN were silent `pass` to `partial` reverts on unrelated cells (2.3.4, 5.1.1, 6.1.3, 11.1.1, 11.5.1, 14.2.1, 15.3.2), undetected for three days. **Those two numbers answer different questions and are both in circulation** -- 298 is the blast radius, 8 is the verdict movement -- so cite the one you mean. Re-measured here 2026-09-07 by parsing both sides rather than reading the report. That incident has its own filed row; it is deliberately not cited by number, because the number a peer named for it does not resolve on `main` at the time of writing and a citation that resolves to nothing today starts resolving to unrelated work the day someone legitimately allocates it. ### The defect