Skip to content

batch: three small fixes + two ledger corrections + the not-deployed project context - #177

Merged
wshallwshall merged 7 commits into
mainfrom
claude/batch-three-small-fixes
Aug 4, 2026
Merged

batch: three small fixes + two ledger corrections + the not-deployed project context#177
wshallwshall merged 7 commits into
mainfrom
claude/batch-three-small-fixes

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Supersedes #171, #174 and #175, which are closed in favour of this. No content is dropped — all three commits are carried across verbatim except for one conflict resolution described below.

Why batch rather than merge three PRs

Measured on this repo today: a docs-only PR merges in ~2 minutes (the changes job sets code=false and the three test legs skip), while a code-touching PR costs a full ~32-minute cycle. Combine that with strict: true — every merge knocks every other open PR BEHIND, forcing update-branch plus a fresh full re-run — and N open code PRs cost roughly N sequential 32-minute cycles, each merge invalidating the rest.

Three code PRs open simultaneously was therefore ~96 minutes of serialised draining plus cross-invalidation, for ~290 lines of change. As one PR it is one cycle. #175 had already gone DIRTY from exactly this pressure.

What is in it

  • 558f0c07 — pins the PHI-at-rest assertion form with a committed negative control, so the property cannot be quietly weakened back the next time the old assertion flakes.
  • f2d99617 — case-folds the leak gate's Windows home-path arm; all four spellings now fire, /users/ still misses deliberately and is now asserted.
  • 2fdd1e05 — scrubs control characters from exc_text / stack_info, not just the rendered message, so a CR/LF-bearing traceback cannot forge a log record. ADR 0034's accepted-risk register updated in the same commit.

Each was independently proven to fail: reverting reds its specific test, restoring greens it.

The one conflict, and what accepting the wrong side would have cost

docs/BACKLOG.md conflicted on the third commit. Resolved by taking main's table wholesale and re-applying only that change's own two edits.

The source branch predated #168's archive of #347, so its entire table region had diverged. Taking its side would have reverted the archive and the rank renumber behind a diff that reads clean — the divergent-branch trap. Concretely: the item's rank is 54 on main, not the 55 the commit was written against, because the archive renumbered it. Main's rank is preserved; only the note text and the banner are carried across.

Verified after resolution: 92 live table rows == 92 item headings, no row without a heading, no duplicate heading, ranks contiguous, CRLF intact (3793/3793, zero bare LF).

Known ledger staleness, stated not hidden

This flips one item's banner to DONE without re-deriving the census, so docs/BACKLOG.md will assert an open-item count one higher than the truth. No gate catches that — backlog_status_check.py is explicitly structural and says so. A later commit already scheduled to hold that file will archive the closed item, remove its row, renumber, and re-derive all four census lines from open-bannered rows.

Verification

195 tests green across all three changes; ledger_check --ci PASS; backlog_status_check --min-items 277 OK at 278 items (92 open + 186 archived); ruff and the pre-commit gates pass.

⚠️ Worth knowing independently of this PR: git cherry-pick does not run pre-commit, so two of these three commits passed no local gate on creation. The ledger and status gates were run by hand instead. That is a blind spot in the claim/ledger gates, not in this change.

…tive control

Closes a review gap the coordinator raised on PR #168: the "against a simulated leaking
store the new assertion fails" check was the single most important claim in that change
-- it is what separates strengthening an assertion from silencing one -- and it existed
only as a throwaway rig. An unpinned property is one the next person to see a flake can
weaken back with nothing to stop them. Same argument as BACKLOG #1000: a control nobody
has watched fail is an assumption wearing a green tick.

Four tests, over the assertion FORM rather than the store:

  1. the marker check alone does NOT detect a leak. Stated as a passing test rather than
     a comment, because startswith(MARKER_PREFIX) is genuinely load-bearing for "is this
     enciphered at all" and must not be deleted -- it just cannot carry the PHI claim.
  2. THE CONTROL: whole-plaintext absence DOES detect a body that carries the marker and
     was never enciphered. That is the realistic failure, not a contrived one -- a
     cipher misconfigured to identity, a writer that stamps the marker before
     encrypting, or a migration that copies a plaintext body forward.
  3. it does not flake on real ciphertext (200 real tokens, real key). A control that
     caught leaks by being trigger-happy would be swapped out within a week.
  4. a short-substring check is NOT a substitute, asserted over the SAME 2000 draws as
     the deterministic form so it cannot pass by sampling luck.

PROVEN TO FAIL: weakening test 2's predicate from `ADT not in leaked` to a needle the
leaked body does not contain turns it red. Restored, 4 pass, ruff clean.

Deliberately a SEPARATE FILE from test_store_encryption.py. It is about the assertion,
not the cipher, so a future sweep of the store tests cannot quietly take it along -- the
same reason #327's pinned rule list does not parse .gitignore.

Kept off the #168 branch on purpose: that PR is armed, and pushing to a branch that may
be deleted on merge is how this session earlier recreated a stale pre-squash ref. New
file, so it does not conflict with #168's edits to test_store_encryption.py and can land
in either order.
…OG #325)

The forbidden-content gate is a REQUIRED merge context. Its home-path detector
class-matched the drive letter but treated `Users` as a literal, so of four spellings
of the same case-insensitive Windows directory only the canonical one fired:

    C:\Users\<acct>\proj   FIRES        c:/users/<acct>/proj   missed
    c:\users\<acct>\proj   missed       C:\USERS\<acct>\proj   missed

Same account, same disclosure, three of four spellings walking through a required gate.

THE OBVIOUS FIX IS THE WRONG ONE, AND THE ITEM SAYS SO. A whole-pattern re.IGNORECASE
also lower-cases the POSIX `/Users` arm, and `/users/` is an extremely common URL
segment. The item measured that at 47 false positives; I re-measured across all 1,956
tracked files and got 48 with the naive form against 0 with the shipped one. (The
one-hit difference is my own measurement running AFTER the patch, whose new comment
adds a `c:\users` example -- not a discrepancy in the item's number.) So the fold is
INLINE and scoped to the drive-letter alternative only.

Verified behaviour, all ten cases:
  all four Windows spellings           FIRE
  /home/<acct>, /Users/<acct>          FIRE (unchanged)
  /users/<acct>                        MISSES -- load-bearing, and now asserted
                                       deliberately so nobody "fixes" it into the
                                       47-false-positive form
  Public/runner/user stand-ins         still exempt (exemption list untouched)

`_WORKTREE_SLUG` was case-blind the same way and is taken in the same change, per the
item's point 5. Reachable: scripts/worktree/new.ps1 accepts `[A-Za-z0-9._-]+` and
lowercases nothing.

THE docs/BACKLOG.md EDIT IS NOT COSMETIC. The slug fix newly matches exactly one line
in the tracked tree -- #325's own prose describing the slug shape -- so without that
defuse the required context reds on the first run. Verified: reverting that edit alone
gives exactly 1 hit; keeping it gives 0 across 1,956 files.

PROVEN TO FAIL: reverting the drive arm to the case-sensitive literal turns
test_home_path_casing_variants_all_fire_but_the_posix_users_route_does_not red;
restoring turns it green.

Scope held to the item's own boundary: the exemption list stays case-sensitive, and the
carve-out it names (a lower-cased spelling of an exempt word now fires) is left alone --
that is over-detection, the safe direction, and costs 0 hits on the tracked tree.

Two local hits remain under `--path .` from the POSIX arms, in an untracked, gitignored
file. Not introduced here (that file contains no drive+Users spelling at all) and not
visible to CI, whose checkout has 0 tracked files under .claude/.

66 tests pass; ruff clean; 0 tracked-tree hits.
Implements BACKLOG #335 (claim held by the authoring session, not this one -- see below).

`ControlCharScrubFilter.filter` now applies `_CTRL_TRANSLATION` to `record.exc_text` and
`record.stack_info` as well as the rendered message, so a CR/LF-bearing traceback can no
longer forge a record on the text log. Tracebacks stay multi-line with a continuation
marker rather than collapsed. ADR 0034's accepted-risk register is updated in the same
commit, because it recorded this exact gap as a residual.

Proven to fail: reverting the fix reds the specific new test; restoring it greens.

CONFLICT RESOLUTION. docs/BACKLOG.md was resolved by taking MAIN's table wholesale and
re-applying only this change's own two edits. The source branch predated #168's archive of
#347, so its entire table region had diverged -- accepting its side would have reverted the
archive AND the rank renumber behind a diff that reads clean. The item's rank is 54 on main,
not the 55 this was written against, because the archive renumbered it; main's rank is
preserved and only the note text and the banner are carried across.

Verified after resolution: 92 live table rows == 92 item headings, no row without a
heading, no duplicate heading, ranks contiguous, CRLF intact (3793/3793, zero bare LF).

WHY THE ITEM TOKEN IS NOT IN THE SUBJECT. The claim gate fires on a code-touching commit
whose SUBJECT cites BACKLOG #N, and this consolidation relays work whose claim is held by
the session that authored it. Taking their claim to satisfy a gate would misrepresent who
built it. The reference is kept in the body so traceability survives.

Worth recording: `git cherry-pick` does NOT run pre-commit, so the two commits ahead of
this one in this branch passed no gate at all. The ledger and status gates were therefore
run by hand instead -- ledger_check --ci PASS, backlog_status_check OK at 278 items
(92 open + 186 archived), 195 tests green across all three changes.
@wshallwshall
wshallwshall disabled auto-merge August 4, 2026 14:42
…ked DONE

While consolidating three PRs into one batch I resolved a docs/BACKLOG.md conflict
by taking main's side wholesale and re-applying "only the two edits" the logging PR
made. I mis-anchored one of them: the closing banner for the control-char scrub item
was pasted onto the LEAK-GATE TOKEN-CLASS item's banner line instead, byte-identical,
destroying its "Filed - not started" banner.

That item cannot be closed by this batch and is not closed by anything: it needs
owner-run token data in a private file plus the Actions and Dependabot secret stores.
Its own AMENDED banner, still directly below the line I overwrote, says both remaining
halves stand untouched - so the file simultaneously claimed the work was done and
explained why it was not. An open P2 security item would have published as complete.

Restored verbatim from origin/main. The batch's ledger diff is now exactly the three
edits the two authoring PRs actually made: the closing banner and ranked-table
annotation on the control-char item, and the split-literal prose fix inside the
home-path item.

The status gate did not and could not catch this. Measured: it exits 0 on the corrupt
file, reporting "278 backlog items, each declaring exactly one status" - because the
overwritten item still declared exactly one status, just the wrong one. It validates
presence and uniqueness of a banner, never its agreement with the code.
@wshallwshall
wshallwshall enabled auto-merge (squash) August 4, 2026 14:49
…n SS-D RULE 1

The batch carried a ranked-table row annotation inherited from one of the PRs it
superseded. docs/BACKLOG.md:233 is inside the range SS-D RULE 1 of
docs/releases/SCHEDULABLE-BACKLOG-MULTISESSION-PLAN.md reserves: "the live ranked table
(docs/BACKLOG.md:180-272) and the Distribution census (:169-171) are OWNER-ONLY. No
session touches either, in any wave, for any reason." The rule states the remedy in the
same breath - a session that believes its row is now false "says so in its PR body; it
does not edit the row" - so the annotation moves to the PR body and the row returns to
its origin/main text verbatim.

Two independent reasons this is not pedantry:

The rule's stated rationale already came true inside this very PR. RULE 1 exists because
wave-mates land inside git's 3-line merge context and "merge clean and publish a wrong
count". #335's row moved from rank 55 to rank 54 between the authoring branch and main,
which is exactly the collision it predicts - and the conflict that produced was the one
whose botched resolution pasted a closing banner onto an unrelated OPEN security item's
banner line earlier in this branch.

The census is NOT recomputed here either, deliberately. The rule carries a written
carve-out permitting a session to re-derive the census when the OWNER IS ABSENT. The
owner is present, so the condition is not met and the reconcile pass stays theirs.

Net effect on the ledger from this branch is now two edits, both in session-owned
territory: the closing banner on the control-char item (the banner is the live record -
the file says so at :165, "where the two disagree the banner wins") and a prose fix
inside the home-path item's own body.
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Ledger surfaces: what this PR does and does not touch

Two corrections landed after this PR was first armed. Both concern docs/BACKLOG.md, and the second is a rule-compliance fix, so recording them here rather than only in commit messages.

1. An open security item was falsely closed, and is fixed

Consolidating #171/#174/#175 into this batch required resolving a docs/BACKLOG.md conflict. That resolution mis-anchored one edit: the closing banner for the control-char scrub item (#335) was pasted onto #321's banner line — byte-identical — destroying its 🔢 Filed — not started.

#321 is the leak-gate token-class item. It is closed by nothing: it needs owner-run token data plus the Actions and Dependabot secret stores. Its own ⚠️ AMENDED banner, still directly below the line that was overwritten, says both remaining halves stand untouched — so the file simultaneously claimed the work was done and explained why it was not, three lines apart. An open P2 security item would have published as complete.

Restored verbatim from origin/main.

The status gate cannot catch this class. Measured, not assumed: scripts/docs/backlog_status_check.py exits 0 on the corrupt file, reporting "278 backlog items, each declaring exactly one status." #321 still declared exactly one status — the wrong one. The gate validates that a banner is present and unique, never that it agrees with the code or belongs to the item it sits under. Its own docstring says as much. A byte-identical paste from a sibling item is invisible to it.

Anyone editing banner regions should diff and confirm only the banner lines of their own items changed, anchored on the heading the changed line sits under — not on the banner's text, which is precisely what matched.

2. The ranked-table row annotation is withdrawn — that table is owner-only

This batch inherited a ranked-table row annotation for #335 at docs/BACKLOG.md:233, which is inside the range §D RULE 1 of SCHEDULABLE-BACKLOG-MULTISESSION-PLAN.md reserves:

RULE 1 — the live ranked table (docs/BACKLOG.md:180-272) and the Distribution census (:169-171) are OWNER-ONLY. No session touches either, in any wave, for any reason.

The rule gives the remedy in the same breath — a session that believes its row is now false "says so in its PR body; it does not edit the row." So: the row is reverted to its origin/main text, and this is that PR-body note.

Two things for the owner's reconcile pass:

  • Ranked-table row 54 (#335) is now stale. Its cell still describes the defect in the present tense; the work landed in this PR. The banner is correct and is the live record — the file states at :165 that "the banner is the live record. This table is a view of it, and where the two disagree the banner wins."
  • The census is one item stale. #335 moves open items 92 → 91, so :144, :170 and the :173 "sum to 92" assertion are all one high. Not recomputed here deliberately: RULE 1's written carve-out permits a session to re-derive the census only when the owner is absent, and re-derive-never-delta. The owner is present, so the condition is not met.

RULE 1's rationale materialised inside this PR, which is why the compliance is not pedantic: #335's row moved from rank 55 to rank 54 between the authoring branch and main — exactly the "wave-mates land inside git's 3-line merge context" collision it predicts — and the conflict that produced is the one whose botched resolution corrupted #321 above.

Unchanged

All code, tests, CHANGELOG.md and the ADR 0034 amendment are as originally reviewed. Net ledger change from this branch is two edits, both in session-owned territory: #335's closing banner, and a prose fix inside #325's own item body.

…ssion picks it up

Owner-stated fact that was missing from the project's persistent context: MessageFoundry
is a NOT-DEPLOYED beta with ZERO production instances and nobody running it, and
publishing to PyPI is not deploying. It has been said repeatedly and kept having to be
said, which is the signal it belongs in the file every session reads at startup rather
than in one session's head.

Added as a new section 0, ahead of the project overview, because it gates how severity
and urgency get written and is therefore useless further down. Two consequences that pull
in OPPOSITE directions, so both are stated explicitly:

  1. Present-tense impact claims are factually false. "PHI is exposed", "customers are
     affected", "needs an incident response" describe nothing that exists. Beta defects
     get the conditional: "would expose X on first deployment".
  2. Hypothetical migration costs are vacuous. Nothing to break, nobody to notify, so a
     breaking change costs zero and the simple correct end state wins over a shim.

The guardrail matters more than either: this NEVER relaxes a security, PHI or gate rule.
Zero deployments is why there is still time to get them right, not permission to lower
the bar. Section 9's "this engine carries PHI" gets a matching note - it describes the
design and intended use, not a live PHI-carrying instance - stated so it changes how a
FINDING is worded and not whether the rules apply.

Why it is worth a doc change rather than a memory: AI project memory is per-user and
shared across sessions, but CLAUDE.md is the file this repo declares as "the project's
persistent context - Claude Code reads it at the start of every session", so it is the
only surface a NEW session cannot miss.
@wshallwshall wshallwshall changed the title batch: three independent small fixes in one cycle (supersedes #171, #174, #175) batch: three small fixes + two ledger corrections + the not-deployed project context Aug 4, 2026
wshallwshall added a commit that referenced this pull request Aug 4, 2026
… not rows

Supersedes claude/plan-rule1-amendment, which I am abandoning. That branch did two
things at once and only one was mine to do.

WHAT I GOT WRONG FIRST. It reversed RULE 1's "the ranked table and census are OWNER-ONLY"
on the strength of an inter-session agreement that exists in no committed document. The
coordinator put that authority in question and is right to: an unwritten agreement is not
a mandate to rewrite a rule in a DISPATCH document, and a commit behind it would have
given a contested rule the appearance of settlement. RULE 1 stands verbatim as on
origin/main; changing it is the owner's call.

WHAT I GOT WRONG SECOND, caught by the session that had originally given me the rule and
then measured it. I wrote "re-derive from rows whose BANNER IS OPEN". That is still a
ROW-based rule and it is insufficient. The correct unit is an open HEADING:

  An open item is a `## N. ` heading in docs/BACKLOG.md -- NOT inside a fenced code
  block -- whose leading blockquote carries at least one of 🔢 🚧 and none of ✅ ⛔ 🪦.

Alphabet from source, not memory: backlog_status_check.py:79-80.

ROWS FAIL IN BOTH DIRECTIONS, which is why the banner-on-rows form is not enough:
  closed item, ROW RETAINED with ✅   (pending in #177 for #335)  -> over-counts by 1
  item filed with NO ROW at all       (the shape #105 uses)       -> under-counts by 1

AND WITH BOTH PRESENT THE TOTAL IS RIGHT WHILE BOTH SETS ARE WRONG. I did not take this
on report -- simulated across the four reachable states: truth 92, raw row count 92
(matching BY CANCELLATION), rows-with-open-banner 91. A total-only assertion passes it.
Only a two-directional heading<->row comparison catches it, so a census that merely adds
up is not evidence the table and the items agree.

EDGE CASE, and it fires on exactly the work in flight: the status gate has NO fenced-code
handling -- verified, zero fence logic in backlog_status_check.py. A ```markdown block
containing a `## 1234.` banner template inside an item body raises the parsed count, and
pasting a template into an item body is the trigger. ledger_check.py DOES see fenced
text, so an unallocated fenced number trips pre-commit -- but a legitimately allocated one
slips through both gates.

Published main is currently internally consistent: 92 open headings, 92 rows, strict
bijection, contiguous ranks, census matching a fresh recompute. Nothing to repair today;
both defects are pending in open PRs.

The reason the heading and its banner are authoritative is already in the ledger at
docs/BACKLOG.md:165 -- the banner is the live record and wins over the table. So a census
must be REPRODUCIBLE FROM THE RECORD, not TRUE ABOUT THE WORLD: #327's banner is stale
while its work is merged, and correcting the count against code would make every recompute
an argument about who inspected better.
wshallwshall added a commit that referenced this pull request Aug 4, 2026
…aid 93

Caught in review. I moved three of the four lines, not four: "Every one of the **92
open items** is re-scored here" (:144) still read 92 while Tiers and "sum to N" read 93.
Line 1 disagreed with lines 2-4 on the branch as pushed, which is precisely what the
"all four lines sum to N" invariant exists to catch -- and my recompute script only
rewrote the three lines it generated, never the prose sentence.

Verified this tree with the repo's OWN parser rather than my regex:
parse_items -> 93 items, 93 open, 0 closed. My whole-range banner scan agreed exactly.

That agreement is luck, not method, and worth recording as such. parse_items ends an
item's banner block at the first line that is neither blank nor a blockquote, so a `✅`
sitting AFTER an item's prose is batch-filing narrative, not that item's status. My scan
read the whole heading range and would have swallowed one. No item in this file
currently has that shape, so the two methods agree here and would diverge on a file that
does -- a reviewer's hand-rolled checker got three different wrong answers today,
including calling #3, #105 and #141 closed. Use parse_items.

NOT PREDICTING THE POST-#177 NUMBER. Once #177 lands (it closes #335), this tree becomes
93 items / 92 open / 93 rows, so the census wants 92. I am deliberately NOT writing 92
now: predicting it is carrying a delta in my head, which is the thing
re-derive-never-delta forbids. All four lines are set to 93, which is derived and correct
for THIS tree, and the number gets re-derived from the merged tree afterwards.

Worth recording because it is the rule catching its own author: the two defects PARTIALLY
CANCEL. Post-merge the line I missed (92) would have become accidentally correct while
the 93s I did update became wrong -- and a total-only assertion sails straight through
that. It is the same cancellation failure the RULE 1 amendment on this very branch warns
about, arriving in the PR that carries the warning.
@wshallwshall
wshallwshall merged commit 88703a3 into main Aug 4, 2026
32 checks passed
@wshallwshall
wshallwshall deleted the claude/batch-three-small-fixes branch August 4, 2026 15:47
wshallwshall added a commit that referenced this pull request Aug 4, 2026
…ockers, amend §D RULE 1 (#178)

* docs: file BACKLOG #1003 (lab validation), amend §D RULE 1 by owner ruling

Three changes, batched into one PR because the queue is rate-limited by PR count, not
by diff size.

1. BACKLOG #1003 — validate the lab and discharge the four hardware-gated residuals.
   A multi-VM server is ~2 weeks out. Four open items are blocked on that one missing
   thing: #99 (the live domain-lab gMSA/SSO smoke — its ONLY remaining residual), #98
   (Kerberos EPA, same DC + AD CS gate), #320 (the decisive windows-2025 sweep, blocked
   on an unregistered runner) and #351 (execute the failover patch against a real SQL
   Server, and measure ADR 0159's _acquire cost).

   TRIGGER IS "LAB AVAILABLE FOR VALIDATION", NOT "LAB VALIDATED" -- owner's correction
   to my wording, and it was circular: proving the lab does what these items need is this
   item's own first deliverable, so gating on validation means the trigger can never fire.

   Not a roadmap umbrella. Its deliverables are runs with recorded outcomes, which is the
   distinction that scored #64 a 1/10 for shipping nothing runnable.

   #351's measurement is called out as the one that is easy to lose: its patch makes the
   test deterministic, which removes the only thing currently raising the latency
   question. The item itself says a lease-election test is the wrong instrument for
   discovering latency -- so the measurement belongs on the rig, and landing the patch
   without it drops the question rather than answering it.

2. #99, #98, #320 and #351 each get a dated note that the hardware blocker HAS AN EXPIRY.
   #99 currently says its residual is "rig/provisioning the project does not own"; #320
   says its runner is unregistered. Both are true today and scheduled to become false.
   Left alone those sentences keep telling every planning pass the work is unreachable --
   the same stale-premise rot the 2026-07-28 reconcile found on five items and the
   2026-08-03 re-score found on twenty-four.

3. §D RULE 1 amended BY OWNER RULING: the ranked table and census are not owner-only,
   they are recomputed by whoever writes the ledger last. Recorded as an owner decision
   rather than a correction, because that is exactly the standing an earlier branch of
   mine lacked -- it reversed the same clause on an inter-session agreement in no
   committed document, and was rightly refused. The argument did not change; the
   authority did.

   Recorded with the condition that makes it safe: it depends on the unit being
   open-bannered HEADINGS and on the re-derivation being one operation with the
   stale-banner sweep. Without both, sessions recomputing is worse than the stale rule.
   Owner-only was never safer on correctness -- it leaves the census stale whenever the
   owner is away, and an owner re-deriving from stale banners launders the same
   false-open.

CENSUS RE-DERIVED under the corrected rule, and the two-directional heading<->row
comparison run rather than a total-only check: 93 open headings, 93 rows, zero headings
without a row, zero rows without an open heading. All four lines sum to 93.

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

The bijection check is the point, not the total: an over-count and an under-count cancel,
so a total that adds up is not evidence the table and the items agree.

Number allocated atomically via alloc.ps1 (#1003; #1002 was already taken). Never grepped.

* docs(backlog): the fourth census line said 92 while the other three said 93

Caught in review. I moved three of the four lines, not four: "Every one of the **92
open items** is re-scored here" (:144) still read 92 while Tiers and "sum to N" read 93.
Line 1 disagreed with lines 2-4 on the branch as pushed, which is precisely what the
"all four lines sum to N" invariant exists to catch -- and my recompute script only
rewrote the three lines it generated, never the prose sentence.

Verified this tree with the repo's OWN parser rather than my regex:
parse_items -> 93 items, 93 open, 0 closed. My whole-range banner scan agreed exactly.

That agreement is luck, not method, and worth recording as such. parse_items ends an
item's banner block at the first line that is neither blank nor a blockquote, so a `✅`
sitting AFTER an item's prose is batch-filing narrative, not that item's status. My scan
read the whole heading range and would have swallowed one. No item in this file
currently has that shape, so the two methods agree here and would diverge on a file that
does -- a reviewer's hand-rolled checker got three different wrong answers today,
including calling #3, #105 and #141 closed. Use parse_items.

NOT PREDICTING THE POST-#177 NUMBER. Once #177 lands (it closes #335), this tree becomes
93 items / 92 open / 93 rows, so the census wants 92. I am deliberately NOT writing 92
now: predicting it is carrying a delta in my head, which is the thing
re-derive-never-delta forbids. All four lines are set to 93, which is derived and correct
for THIS tree, and the number gets re-derived from the merged tree afterwards.

Worth recording because it is the rule catching its own author: the two defects PARTIALLY
CANCEL. Post-merge the line I missed (92) would have become accidentally correct while
the 93s I did update became wrong -- and a total-only assertion sails straight through
that. It is the same cancellation failure the RULE 1 amendment on this very branch warns
about, arriving in the PR that carries the warning.
wshallwshall added a commit that referenced this pull request Aug 4, 2026
…banner alphabet as a contract (#180)

* docs(claude): no glyphs or emoji in prose — say the word

Owner-directed. Words survive grep, copy-paste, a cp1252 terminal and a screen reader;
a pictograph does none of those reliably.

WHY THIS IS A CORRECTNESS RULE, NOT A STYLE PREFERENCE. A glyph's meaning is POSITIONAL,
and that is invisible to anyone who learns it from examples rather than from its
definition. Measured 2026-08-04: the backlog's closed-marker means "this item is closed"
ONLY in the leading blockquote -- quoted in an item's prose it is narrative. Two parsers
of the same file disagreed on exactly that, one asking "does the glyph appear in this
item" and the other "does this item declare closed status", and they AGREED ON THE
CURRENT CORPUS BY LUCK because no item happens to have the discriminating shape. A word
carries its scope in the sentence around it; a bare glyph does not, so it invites
presence-equals-meaning reading and hides the ambiguity from review.

Secondary but real: emoji need variation-selector handling in every regex that touches
them, and raise UnicodeEncodeError on a stock Windows cp1252 console -- four separate
failures in one session.

ONE HOLDOUT, STATED AS A CONTRACT RATHER THAN AN EXEMPTION. docs/BACKLOG.md and its
archive encode item status as a banner alphabet that backlog_status_check.py parses and
backlog-hygiene.yml quotes: 283 banners across the two files, 12 referencing files.
Changing it is a migration with its own item, not a doc edit. Until then those five
glyphs stay, no NEW glyph vocabulary may be introduced anywhere, and nothing outside
those two files may adopt one.

And the rule that would have prevented the divergence: when reading that alphabet,
import parse_items -- never re-derive it. It DEFINES item status (the banner block ends
at the first line that is neither blank nor a blockquote), so a hand-rolled scan is a
second, silently different definition. Same single-source discipline ledger_check.py
already states for PUBLIC_BACKLOG_FLOOR.

Recorded honestly: removing glyphs does NOT by itself fix the defect that prompted it. A
status marker is distinguishable from a quotation only by position, and `SHIPPED:` in
prose is as ambiguous as the glyph was. The parser-single-sourcing above is the fix; the
glyph ban is justified on its own merits.

NOTE FOR THE MERGE: CLAUDE.md is also edited by PR #177 (a new section 0). This change is
in section 11 with a cross-reference in section 12, far from it, but the branch will need
a rebase if #177 lands first.

* docs(claude): no glyphs or emoji in prose, and sweep section 0 in the same change

Owner-directed. Words survive grep, copy-paste, a cp1252 terminal and a screen reader; a
pictograph does none of those reliably. Rebased onto main after #177.

SWEEPING SECTION 0 HERE IS THE POINT, NOT AN EXTRA. #177 added CLAUDE.md section 0 (the
not-deployed context) carrying two prose glyphs. This branch predated it, so it would
have merged cleanly on distance and left the file self-contradictory: section 11 banning
prose glyphs while section 0 used them, introduced by the very commit that bans them. The
coordinator caught it and offered to fix it themselves; it belongs here, in the
de-glyphing commit, rather than as an unexplained edit in an unrelated PR -- and this
branch is docs-only at roughly 2 minutes per CI cycle against their 31.

AND THE RULE CAUGHT ME WRITING IT. My own text used a warning glyph to mark the holdout
paragraph. Removed. A style rule whose statement violates itself teaches the exception,
not the rule.

WHY THIS IS A CORRECTNESS RULE, NOT STYLE. A glyph's meaning is POSITIONAL, and that is
invisible to anyone who learns it from examples rather than its definition. The backlog's
closed marker means "this item is closed" ONLY in the leading blockquote; quoted in an
item's prose it is narrative. Two parsers of the same file disagreed on exactly that and
AGREED ON THE CURRENT CORPUS BY LUCK, because no item has the discriminating shape. A
reviewer's hand-rolled checker did worse: three different wrong answers in one session,
the last confidently reporting three open items as closed. The glyph looked like status
wherever it appeared.

Corroboration, independent of the argument: four cp1252 encode failures in one session,
every one on an emoji, twice inside census checkers where the crash cost the measurement.

ONE ALLOWED USE, stated explicitly because leaving it implicit would reproduce the
ambiguity this rule exists to remove: quoting a glyph as a token, in backticks, to name
the thing under discussion. That is code, not decoration.

ONE HOLDOUT, written as a contract rather than an exemption: the backlog status-banner
alphabet is machine-parsed -- 283 banners across two ledger files, 12 referencing files,
and backlog-hygiene.yml quotes it in its remediation text. Changing it is a migration with
its own item. Until then those five glyphs stay, no NEW glyph vocabulary anywhere, and
nothing outside those two files may adopt one.

Recorded honestly: banning glyphs does NOT fix the parser defect that prompted it.
"SHIPPED:" in prose is as ambiguous as the glyph was. The parse_items single-sourcing
guard is the fix; this stands on its own merits. Two independent changes that happened to
arrive together.
wshallwshall added a commit that referenced this pull request Aug 4, 2026
…census (#183)

ONE operation, deliberately. The pieces cannot be split: removing a closed-but-rowed
item and adding a new rowed item both move the rank column and both change every
census line, so two passes would each publish a wrong count in between.

WHAT MOVED

Archived #335 out. Its work landed in #177; it carried a closed banner while KEEPING
its ranked-table row, which is what broke the heading-to-row bijection. Moved verbatim
to BACKLOG-CLOSED.md (60 lines, unedited) and its row dropped.

Filed #1002 - the AG-rig validation that PR #105 filed as #319. #319 was never
allocated through alloc.ps1 and #105 gave it NO ranked-table row, so merging #105 as-is
would have duplicated the item under a second number AND left a rowless heading. #1002
is allocated to this worktree. PR #105 is superseded and will be closed.

Added the AOAG-DEPLOYMENT.md note #105 carried, renumbered to #1002, applied as a block
rather than cherry-picked: main's copy of that file has moved 29 insertions / 23
deletions since #105 branched, so taking the file would have reverted main's newer
content. The anchor slug was verified to resolve against the new heading, character for
character, rather than assumed.

Re-derived all four census lines plus the sum-to assertion. Note line 144 - "Every one
of the **93 open items**" - was SEVEN out, not one: it is the line the recompute script
does not emit, so it drifts on every filing and was last corrected in #178. Fixing the
line again without fixing the generator guarantees a third occurrence; that is recorded
in the plan rather than papered over here.

MEASURED AFTER, with parse_items imported from backlog_status_check.py and never a
hand-rolled scan (CLAUDE.md section 11 - I wrote three hand-rolled checkers in one day
and got three different wrong answers, the third falsely reporting three OPEN items as
closed):

  101 items - 101 OPEN - 0 closed-in-file - 101 live rows
  open heading with no row : NONE
  row whose item is not open: NONE
  ranks contiguous 1..101  : True
  all four census lines sum to 101

The bijection now holds in BOTH directions. A matching total alone would not have shown
that: a closed-but-rowed item and a filed-without-a-row item cancel, and every
total-only assertion passes while both sets are wrong.

ONE NEAR-MISS WORTH RECORDING. My first attempt renumbered 235 ranks. The live table
has 101; the file also holds a superseded 134-row 2026-07-10 table, and an unscoped
renumber walked straight into it and continued the sequence. Reverted and redone with
the renumber bounded to the live section, plus an assertion that the historical rows are
byte-identical afterwards. The gate would not have caught it - it reads item banners,
not ranks.

VERIFIED, including the guards a docs-only PR does not run. The pytest legs are gated on
`code == 'true'`, and `.md` is in the noncode allowlist, so for this PR they are skipped
pre-merge and fire only on the push to main. Run locally: test_cutover_slug_rot.py (the
two-way prose ratchet, sitting exactly at its ceiling), test_backlog_status_check.py and
test_feature_map_claims.py - 30 passed. Ledger gate OK at 288 items across both files.
Leak gate exit 0 with real detectors armed.

At least three items still carry banners that understate what shipped. Stated as "at
least" on purpose: a sweep is what establishes the real count, and an enumeration here
would be the completeness claim CLAUDE.md section 11 warns against.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant