Skip to content

fix(coord): a branch that predates docs/BACKLOG.md is not a ledger violation - #10

Merged
wshallwshall merged 1 commit into
mainfrom
ledger-head-absent
Jul 27, 2026
Merged

fix(coord): a branch that predates docs/BACKLOG.md is not a ledger violation#10
wshallwshall merged 1 commit into
mainfrom
ledger-head-absent

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Publishing docs/BACKLOG.md (#6) broke the ledger gate on every branch cut before that merge — including PR #7, whose diff touched release.yml and leak-gate docs and nothing near a ledger:

OSError: git show HEAD:docs/BACKLOG.md failed (128):
fatal: path 'docs/BACKLOG.md' does not exist in 'HEAD'

Why

CI's change set is diff base HEAD. Once origin/main gained the file, a branch that predates it lists docs/BACKLOG.md as changed — as a deletion relative to base — despite never touching it. check_backlog then read HEAD for a copy that was never there.

base_has (added when the file was imported) covers the opposite end: absent on base, being added. This is the mirror case — present on base, absent on HEAD — and it's the one that fires repeatedly, because it hits every open branch at once rather than the single commit that publishes the file.

The fix

Both probes now share _obj_exists, which asks git cat-file -e directly rather than inferring absence from an exception. git() must keep raising on real failures — an error swallowed as "empty ledger" reads as "no numbers taken", the false-clean this gate exists to prevent. "The path is not on that ref" is the one genuine non-failure, so it gets its own probe instead of a broad except.

head_has mirrors head_text's ref exactly (HEAD in CI, the index locally), so the guard can't disagree with the read it guards.

Mutation-verified: neutralise the guard and the new test fails with the same exit-128 shape seen in CI.

A note on how this was found

It surfaced as an unrelated-looking failure on someone else's PR, and the first re-run "fixed" nothing — the attempt before it had failed for a genuinely different, genuinely random reason (a 3-char needle colliding with base64 ciphertext, fixed in #8). Two different faults on consecutive attempts of the same leg is exactly the shape that gets misread as one flaky test and re-run until it goes green.

🤖 Generated with Claude Code

…olation

Publishing docs/BACKLOG.md (#6) broke the ledger gate on every branch cut before that merge, including
PR #7 -- whose diff touched release.yml and leak-gate docs and nothing near a ledger:

    OSError: git show HEAD:docs/BACKLOG.md failed (128): fatal: path 'docs/BACKLOG.md' does not exist
    in 'HEAD'

CI's change set is `diff base HEAD`. Once origin/main gained the file, a branch that predates it lists
docs/BACKLOG.md as CHANGED -- as a deletion relative to base -- despite never touching it. check_backlog
then read HEAD for a copy that was never there.

`base_has` (added when the file was imported) covers the opposite end: absent on BASE, being added.
This is the mirror case -- present on base, absent on HEAD -- and it is the one that fires repeatedly,
because it hits every open branch at once rather than the single commit that publishes the file. Both
probes now share `_obj_exists`, which asks `git cat-file -e` directly rather than inferring absence
from an exception: `git()` must keep raising on real failures, since an error swallowed as "empty
ledger" reads as "no numbers taken" -- the false-clean this gate exists to prevent. "The path is not on
that ref" is the one non-failure, so it gets its own probe instead of a broad `except`.

`head_has` mirrors `head_text`'s ref exactly (HEAD in CI, the INDEX locally), so the guard cannot
disagree with the read it guards.

Mutation-verified: neutralise the guard and the new test fails with the same exit-128 shape seen in CI.

Worth noting for whoever meets this next: the failure looked like an unrelated flake on somebody else's
PR, and the first re-run "fixed" nothing -- the run before it had failed for a genuinely different,
genuinely random reason (a 3-char needle colliding with base64 ciphertext, fixed separately). Two
different faults on consecutive attempts of the same leg is exactly the shape that gets misread as one
flaky test.
@wshallwshall
wshallwshall merged commit bee2ea6 into main Jul 27, 2026
31 checks passed
@wshallwshall
wshallwshall deleted the ledger-head-absent branch July 27, 2026 15:07
wshallwshall added a commit that referenced this pull request Aug 5, 2026
…sambiguate PR citations that already mis-resolve (#209)

* fix(quality): the two advisory gates wrote a status glyph into their own summaries

liveness.py and c901_delta.py each emitted a check mark on their clean-result
line, and c901_delta rendered its complexity table with a non-ASCII arrow.

Both write via sys.stderr.write when no summary file is given, and stderr
defaults to backslashreplace, so a stock Windows cp1252 console silently
mangled the text at exit 0 rather than raising. Verified by running the
pre-edit scripts from HEAD under PYTHONIOENCODING=cp1252: neither crashed,
both corrupted. That is the worse failure -- a crash is loud and self-
reporting; silent corruption at exit 0 is not. CI was never affected, because
the step summary is opened utf-8, so this was a local-run defect only.

Say the word instead (CLAUDE.md section 11). PASS trails each sentence because
that is where the glyph sat -- the minimal edit -- and it leaves the substring
tests/test_c901_delta.py:291 asserts ("No function was introduced over the
threshold") byte-identical. That assertion is a containment check rather than
a prefix check, so it does not by itself forbid a leading token; nothing in
the repo pins the placement either way.

The three arrows become ASCII ->, matching lines 227 and 313 of the same file,
which already wrote the identical relation that way.

Both files now carry zero characters cp1252 cannot encode.

* docs(quality): document /simplify, and take the status glyphs out of the rubric

Code_Quality_Standards.md carried 40 check marks and one red circle as status
markers, which CLAUDE.md section 11 forbids outside the two machine-parsed
backlog files. All 41 are gone. Most sat beside the word they decorated and
were simply deleted, that word carrying the meaning on its own; only two were
genuine rewrites -- the red circle in Appendix A.2 became Failing, and the
Appendix A.3 legend, where the glyph was the subject, became prose.

Adds section 5.1 as the single home for /simplify: a local, human-invoked
review that APPLIES its fixes rather than reporting them, which is why it runs
before the local quartet rather than after -- running it after would mutate a
tree the quartet just certified. It is not one of the five measurement gates,
sits outside the AI companion section 6.5 local gate, and carries no Built
status, because it ships with Claude Code rather than with this project and so
leaves no artifact here to score. CLAUDE.md gains a "Before you verify"
heading so the instruction is not governed by a pass-gate it cannot satisfy:
/simplify applies edits and returns no verdict.

Also corrects a PRE-EXISTING Appendix A.3 error that removing the glyphs
surfaced rather than introduced: the legend glossed its status marker as
advisory under two PR numbers, across a five-item list whose fifth item is
blocking and shipped under a third. That legend is byte-identical in every
commit this file has existed in.

HANDOFF-mutation-coverage.md prescribed typing a check mark back into the
rubric, which would have undone the pass -- so the removal was not durable.
Its instruction is corrected, along with its stale signal numbering and column
name, and it now says to write the word.

* docs(quality): the handoff cited the PR that built the gates, not the one that restatused them

The parenthetical said signals 7 and 8 were "restatused in v0.8 (#1040)". Two
different pull requests, verified by subject:

  7540f260  ci(quality): mutation (#7) + diff-coverage (#8) advisory gates (#1040)
  46714159  docs(rubric): v0.8 -- restatus signals 7 + 8 to Built (#1044)

PR #1040 built the gates; PR #1044 did the v0.8 restatus the sentence is
about. Caught by the session sweeping citation ambiguity in these docs.

Written in the settled "PR #NNN" form rather than a bare number, which is what
that sweep is standardising. Bare and bolded #N stays for backlog items, and
the distinction is load-bearing in code: backlog_status_check.py defines
_CL_EXPLICIT as BACKLOG\s+#(\d+) to pick out item citations, so writing
"BACKLOG #1040" for a pull request would make that regex misread it as an item.

* docs(quality): the rubric cited pull requests as bare "#N", which already resolves to the wrong item

#1020, #1028, #1040 and #1047 are PULL REQUEST numbers (97b79fef, 42756bdb,
7540f260, 3d6c8adf -- subject-anchored search). A bare "#N" in this corpus
reads as a backlog item, so these send a reader to the wrong document.

#1020 is not a future risk. Backlog item 1020 exists on main, filed
2026-08-04, about a first-run bootstrap Administrator with no email address --
while the rubric uses #1020 four times to mean the PyPI sdist private-doc leak
fix. Those four citations already land on the wrong item. 1028 is next; 1040
and 1047 are still unallocated.

"PR #N" is the settled repo form rather than a new one, and the repo encodes
the distinction in code: backlog_status_check.py defines _CL_EXPLICIT to match
the word BACKLOG followed by whitespace and a number, under the comment
"Unambiguous CHANGELOG citations of a *backlog item* (not a PR number)". That
regex is also why the inverse rewrite is unavailable -- prefixing a pull
request number with that word would make the parser read it as an item.

39 markers inserted: 38 in the rubric, 1 in the handoff. The rubric's 40
four-digit citations carry 38 markers because one slash-joined run takes a
single "PRs " across its three tokens.

Deliberately left alone: the 11 rubric-signal citations (#3, #6, #7, #8, #9,
#10, #11). Six of those numbers are also real backlog items, so the ambiguity
is genuine, but resolving it is prose surgery ("signal 7", not "PR #7") and
sits with the owner as a separate decision.

Analysis and the marking script are the work of the session on
claude/sleepy-villani-df328d. Verified here as markers-only: stripping every
marker from both the committed and the working text yields identical files.

* backlog: file 1029 -- the /simplify placement decision had no number to cite

Filed closed: the documentation is the whole deliverable, and it shipped in the
three commits below this one.

REWRITTEN BEFORE FILING. The draft item, written when the change was first
made, described a structure that the remediation then reverted -- it claimed a
sixth row in the section 5 gate table, a Built status, and a CLAUDE.md bullet.
None of those is what shipped: section 5's table is unchanged at five rows,
Built is a claim the document explicitly declines to make, and CLAUDE.md
carries a "Before you verify" heading placed ahead of the verification list
rather than a bullet inside it. Every claim in the filed item was read from the
working tree at 17c5212 rather than recalled.

APPENDED, NOT INSERTED, and the instruction to insert was checked rather than
followed. It was handed over on the grounds that 1028, 1030 and 1031 are being
filed concurrently. No such rule exists and the file does not follow one:

  108 items carry 10 descending adjacent pairs in file order, the tail running
  1019, 1018, 1024, 1026, 1025, 1027;
  backlog_status_check.py enforces no ordering -- its only sort is a citation
  report at line 190;
  this file's own header states only that the numbered items are intentionally
  deferred, and docs/README.md calls them "ranked", which is not numeric order.

So nothing states an ordering rule for the numbered items, and imposing numeric
order would discard whatever the existing arrangement encodes. Appending is
where every recent item sits. The only real interaction with the concurrent
items is a textual end-of-file conflict that resolves by keeping both.

Validated with the canonical parser rather than a hand-rolled scan
(CLAUDE.md section 11): parse_items reports item 1029 with closed=['<check>'],
open=[], is_open=False -- exactly one banner, no OPEN/CLOSED contradiction. The
hygiene gate at its ci.yml invocation reports OK, 304 items, each declaring
exactly one status.

Number allocated via scripts/coord/alloc.ps1, never by grepping for the next
free one.

* docs: correct the CRLF rationale in this branch's earlier merge commit

The merge commit 95cd856 states that docs/BACKLOG.md "is 100 percent CRLF" and
that "a resolver that normalises to LF produces a clean-looking merge that
churns every line". That is FALSE about the stored file, and this commit exists
so the correction travels with the claim -- this repository composes its squash
body from the concatenated commit messages, so both land on main together.

Every committed revision of that file is pure LF. Measured on the blobs rather
than the working tree:

    origin/main   CRLF=0  bareLF=5045
    453c95f      CRLF=0  bareLF=4994
    95cd856      CRLF=0  bareLF=5065
    working tree  CRLF=5065  bareLF=0
    core.autocrlf = true

CRLF exists only as the checkout materialisation. The original measurement read
bytes on disk and reported them as the stored form -- the working tree answered
a question about the object store.

THE RESOLUTION ITSELF WAS CORRECT; only the stated reason was wrong. The
resolver read and wrote with newline="", so it preserved the on-disk form
byte-for-byte and let autocrlf normalise on the way in -- the same outcome a
resolver that ignored line endings entirely would have produced.

THE INSTRUMENT THAT SETTLES THIS IS CHURN, NOT A LINE-ENDING COUNT:

    git diff --numstat 453c95f 95cd856 -- docs/BACKLOG.md
    71      0      docs/BACKLOG.md

71 added, zero removed -- items 1030, 1031 and 1032 plus one seam blank line.
A resolver that had normalised would show thousands of lines on BOTH sides.
That reconciles to the line against the independent resolution of the same
collision on another branch, which came back 70/0 and needed no seam line.

Anyone resolving the next end-of-file collision in this file should run the
numstat check and should not chase line endings that are not there.
wshallwshall added a commit that referenced this pull request Aug 5, 2026
… surfaced

1033. The rubric cites its own eleven signals as bare #N, and six of those
numbers are real backlog items -- #3 is OPEN today, and #6/#7/#8/#10/#11 are
closed items. Ten citations on four lines, re-measured against 780ee1d. Owner
ruled on 2026-08-05 that they get disambiguated. The four-digit PR citations in
the same file were fixed in PR #209; this is the short-number half that was
deliberately left out of that scope.

Two traps are recorded because each has already caught a reader. The #3 at L120
is a markdown ANCHOR FRAGMENT inside a link target, not a citation -- converting
it silently breaks the link, and a prior census listed it as a signal because it
counted tokens without printing context. And L299/L319 use backslash-escaped
forms: a grep attempt during this triage returned ZERO matches on a file that
demonstrably contains them, and the empty result was believed until a
self-tested pattern contradicted it. The item says to prove the pattern fires
before trusting a count from it.

1034. The pre-push shim exits 0 with "THE PUSH GUARD IS OFF for this push" when
python is not on PATH. With enforce_admins OFF, push_guard.py is the only thing
refusing an admin's direct push to main, and since the cutover that push is
publication -- so the one control has a silent off switch that depends on an
environment variable. As of today the shim switches off three guards rather
than one, the two added alongside it being the namespace allowlist and the
tip-tree check.

Filed with the adjacent gaps in the same class rather than separately: a fresh
clone or new worktree has no hook at all until install-git-hooks.ps1 runs, and
--no-verify and MEFOR_ALLOW_DIRECT_PUSH=1 skip everything by design. The item
states plainly that a client-side hook cannot be the sole control and that the
durable answer is server-side, with the shim as defence in depth.

Numbers allocated via scripts/coord/alloc.ps1, never by grepping for the next
free one. Validated with parse_items rather than a hand-rolled scan: 114 items,
zero duplicate numbers, 1033 and 1034 each carrying exactly one open banner.
Hygiene gate OK at 309 across both ledger files.
wshallwshall added a commit that referenced this pull request Aug 5, 2026
…d what it CARRIES (#213)

* fix(hooks): the push guard asked where a push LANDS, and nothing asked what it CARRIES

Two guards, both for paths the existing PROTECTED check waves through.

GUARD A -- namespace allowlist. Refuse any push whose remote ref is outside
refs/heads/ or refs/tags/. That is the shape of git push --mirror, which offers
every ref in the clone including remote-tracking namespaces. A mirror push was
refused before only INCIDENTALLY: it also offers local main as an update of
refs/heads/main, so PROTECTED happened to fire. That is a property of one
branch's state, not a rule, and it evaporates the moment main is up to date.

GUARD B -- content check. Refuse a push whose ref's tip tree carries
docs/security. That directory is gitignored, and an ignore rule governs only
UNTRACKED paths, so it does nothing about a ref whose history already tracks
those files. The path this closes is the likeliest of the set and is not a
mirror at all: branch off a ref of that lineage and push it as an ordinary
branch, which every other check here permits by design.

PROVEN, not assumed. Both guards exercised via crafted pre-push stdin against a
throwaway repo, with the fixture self-checked in both directions first (a
fixture whose add -f lost to the ignore rule would make every assertion pass
vacuously):

  case                                        new  old(HEAD)
  remote-tracking ref, mirror shape             1      0
  ordinary branch, clean tip                    0      0
  ordinary branch, tip carries docs/security    1      0
  tag push, clean tip                           0      0
  delete an unprotected branch                  0      0
  direct push to main                           1      1

The old-guard column is the negative control: it returned 0 for exactly the two
cases these guards add, so this is new coverage rather than restated behaviour.
Refusals were checked to name the right reason, not merely to exit 1.

WHAT THESE ARE NOT, stated in the code because the difference decides what a
green run entitles anyone to conclude. Guard B reads the TIP TREE only -- a
branch that added and then removed the files passes with a dirty history, so it
is not a history check. It matches paths, not content. Every check here is
skipped by --no-verify, by MEFOR_ALLOW_DIRECT_PUSH=1, and by the installed
shim's own fail-open, which prints "THE PUSH GUARD IS OFF for this push" and
exits 0 when python does not resolve. A fresh clone or new worktree has no hook
at all until install-git-hooks.ps1 runs. A client-side hook cannot be the sole
control and the docstring says so.

Also fixes a false docstring in the test file, which asserted that git push
--all sends every ref. It does not -- --all is refs/heads only, while bundle
create --all and rev-list --all mean every ref. That belief is what makes
someone treat --all and --mirror as interchangeable.

109 tests pass; ruff and mypy clean.

* docs(ledger): record the vault-ref cleanup, and retire a warning that was true when written

489 refs carrying docs/security content were deleted from this clone on
2026-08-05 with git update-ref -d, across THREE namespaces: refs/remotes/vault
(20), refs/remotes/vaultall (466), and refs/vault (3). That third sits outside
refs/remotes entirely and held the newest, densest content, so a cleanup scoped
to refs/remotes would have missed it.

THE STANDING WARNING AGAINST THIS IS NOW STALE, NOT WRONG. LEDGER-GATE.md and
alloc.ps1 both named "deleting its refs" as the hazard the allocator ratchet
defends against. Re-measured directly: BACKLOG max is 1032 and sub-floor max
353 both with and without the refs, ADR max 0161 either way, the allocator
emits max+1 and never fills gaps, and the ratchets already persist 1031 / 1000
/ 160. The warning was accurate when written, in the era when the floor did
depend on the ref sweep; the ratchet and the public-boundary split made it
independent since. It is updated rather than deleted, because the principle it
teaches still holds.

THE MULTISESSION PLAN GAVE A COMMAND THAT NO LONGER WORKS, and its description
of the ref was wrong when written. It called vault/main a remote-tracking ref;
git rev-parse --symbolic-full-name resolved it to refs/vault/main, and
refs/remotes/vault/main never existed. Nor was a remote named vault ever
configured -- only origin. The refs were orphaned namespaces from two
direct-URL fetches on 2026-07-28, 45 seconds apart. Sessions should read the
vault ledger from the separate MessageFoundry-vault clone instead.

REVERSIBILITY, since deleting refs is only safe if it is undoable. A manifest
of 489 refname/SHA pairs (464 unique commits -- 25 refs share a tip) is held
outside this repo, durably, inside the vault clone's own .git. The objects
remain addressable here, and every tip is REACHABLE from the vault clone's own
refs, so they are gc-safe there rather than merely undeleted. gc.auto is set to
0 in this clone: it was unset with 7060 loose objects against a default
threshold of 6700, already over, so a routine command could have fired an
auto-gc and converted a reversible ref deletion into permanent loss.

NOTHING WAS EVER PUBLISHED FROM THESE REFS. origin/main, all 30 origin refs and
all 195 local branches carry zero docs/security files at tip and in history,
confirmed three independent ways; the two graphs share no merge base.

* backlog: file 1033 and 1034 -- the two follow-ups the push-guard work surfaced

1033. The rubric cites its own eleven signals as bare #N, and six of those
numbers are real backlog items -- #3 is OPEN today, and #6/#7/#8/#10/#11 are
closed items. Ten citations on four lines, re-measured against 780ee1d. Owner
ruled on 2026-08-05 that they get disambiguated. The four-digit PR citations in
the same file were fixed in PR #209; this is the short-number half that was
deliberately left out of that scope.

Two traps are recorded because each has already caught a reader. The #3 at L120
is a markdown ANCHOR FRAGMENT inside a link target, not a citation -- converting
it silently breaks the link, and a prior census listed it as a signal because it
counted tokens without printing context. And L299/L319 use backslash-escaped
forms: a grep attempt during this triage returned ZERO matches on a file that
demonstrably contains them, and the empty result was believed until a
self-tested pattern contradicted it. The item says to prove the pattern fires
before trusting a count from it.

1034. The pre-push shim exits 0 with "THE PUSH GUARD IS OFF for this push" when
python is not on PATH. With enforce_admins OFF, push_guard.py is the only thing
refusing an admin's direct push to main, and since the cutover that push is
publication -- so the one control has a silent off switch that depends on an
environment variable. As of today the shim switches off three guards rather
than one, the two added alongside it being the namespace allowlist and the
tip-tree check.

Filed with the adjacent gaps in the same class rather than separately: a fresh
clone or new worktree has no hook at all until install-git-hooks.ps1 runs, and
--no-verify and MEFOR_ALLOW_DIRECT_PUSH=1 skip everything by design. The item
states plainly that a client-side hook cannot be the sole control and that the
durable answer is server-side, with the shim as defence in depth.

Numbers allocated via scripts/coord/alloc.ps1, never by grepping for the next
free one. Validated with parse_items rather than a hand-rolled scan: 114 items,
zero duplicate numbers, 1033 and 1034 each carrying exactly one open banner.
Hygiene gate OK at 309 across both ledger files.
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