From 9f16231fee33fc719c824775ff90451a7faf53b4 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 3 Sep 2026 17:31:17 -0500 Subject: [PATCH 1/7] fix(coord): give the durability hook's rescue ref provenance (BACKLOG #1349) The post-commit hook force-pushed a bare ref recording nothing, so the ref could only be graded against a branch that still existed -- the population a rescue ref was never needed for. It now builds an annotated tag object carrying the same mefor-rescue-v1 message rescue.ps1 -Anchor writes, and pushes it by object id. No local ref is created. A local annotated tag reachable from a branch tip would be swept up by git push --follow-tags to whatever remote a hand reaches for, and the default one is public. Every new failure mode degrades to a warning. git hash-object fsck-validates the tag and git var GIT_COMMITTER_IDENT can fail; either way the push falls back to the bare form, one warning goes to stderr, and the commit stands. Measured cost: 0.19s of CPU per commit over five paired runs. Detached HEAD omits was-tip rather than guessing it, which is what makes -Check report SELF-DESCRIBING instead of a claim about a branch that does not exist. Co-Authored-By: Claude Opus 5 --- scripts/coord/install-git-hooks.ps1 | 2 +- scripts/hooks/durability_push.sh | 95 ++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/scripts/coord/install-git-hooks.ps1 b/scripts/coord/install-git-hooks.ps1 index c6d17d5c3..1358ab267 100644 --- a/scripts/coord/install-git-hooks.ps1 +++ b/scripts/coord/install-git-hooks.ps1 @@ -474,7 +474,7 @@ if ($armedRemote) { # remote; the bare shape this line used to print still resolves, to that contested fossil. So # an operator who built a query from the old wording got a CONFIDENT HIT at a commit belonging # to neither repository and concluded their work was backed up. Keep this in step with - # scripts/hooks/durability_push.sh:93 and :97. + # scripts/hooks/durability_push.sh:122 and :126 -- the two lines that assign $TAG. Write-Host " Every commit now also lands as refs/tags/rescue/auto//" Write-Host " there, or refs/tags/rescue/auto//detached/ off a branch." } else { diff --git a/scripts/hooks/durability_push.sh b/scripts/hooks/durability_push.sh index f643a929e..da6372f09 100644 --- a/scripts/hooks/durability_push.sh +++ b/scripts/hooks/durability_push.sh @@ -43,9 +43,35 @@ # # NEVER FAILS A COMMIT. Always exits 0 and pushes in the background. A durability mechanism that can # block or slow a commit gets disabled by the first person it inconveniences, and then protects -# nobody. Failure here is silent by design: the reporting job belongs to +# nobody. PUSH failure here is silent by design: the reporting job belongs to # scripts/coord/unbacked_check.ps1, which measures the true state rather than trusting this ran. # +# THE REF IT WRITES RECORDS WHAT IT CAPTURED (BACKLOG #1349), and the reason is the whole item. A +# rescue ref is consulted ONCE, in the moment the original is already gone -- so a ref that records +# nothing can only be graded against a branch that still exists, which is exactly the population it +# was never needed for. Measured 2026-09-03 in this checkout: `rescue.ps1 -Check` examined 1671 refs +# and returned UNVERIFIABLE for all 1671, because every one of them was written by a bare push. +# +# So this pushes an ANNOTATED TAG OBJECT carrying the same `mefor-rescue-v1` message +# scripts/coord/rescue.ps1 -Anchor writes, and `-Check` reads it back without needing the branch. +# The object is built with `git hash-object -t tag -w` and pushed BY ID -- no local ref is created. +# That is deliberate: a local annotated tag reachable from a branch tip would be swept up by +# `git push --follow-tags` or `git push --tags` to whatever remote a hand reaches for, and the +# default one is PUBLIC. Provenance must not open the publication path this hook exists to avoid. +# +# IT COSTS THREE MORE GIT SPAWNS IN THE FOREGROUND, MEASURED RATHER THAN GUESSED. Five runs of each +# form on 2026-09-03: 2.615s of user+sys for this version against 1.688s for the bare-push one, so +# about 0.19s of CPU per commit. Wall clock is NOT quoted, because the box was running six +# concurrent test processes at the time and the figure would be about the load, not the hook. It +# stays in the foreground rather than joining the background push so that a failure is reported to +# the terminal that caused it instead of arriving after the prompt returns. +# +# AND EVERY NEW FAILURE MODE HERE DEGRADES TO A WARNING. `git hash-object` fsck-validates the object +# and exits non-zero on a malformed tagger line, and `git var GIT_COMMITTER_IDENT` can be empty in a +# repository with no identity configured. Either way $TAGOBJ comes back empty, the push falls back to +# the bare `HEAD:` form it used before, one warning goes to stderr, and THE COMMIT STANDS. Durability +# is the property that must never regress; provenance is the property that improves it. +# # WHAT IT DOES NOT COVER, stated because a control trusted past its reach is worse than none: # * Uncommitted work. Nothing here helps; a lost working tree is lost. # * Rebases. The tag force-moves to follow the branch, so commits only reachable from a discarded @@ -53,6 +79,9 @@ # not close it. # * Concentration. Every tag lands on ONE nominated remote. That is one account away from total # loss, and tags are mutable and unprotected. +# * The refs already pushed. Provenance cannot be retrofitted -- the information was never +# captured. Unlike the dated tag namespace, though, THIS one heals: the ref force-moves on the +# next commit to the same branch, so it carries provenance from then on. # # Re-install after changing this file: pwsh -NoProfile -File scripts/coord/install-git-hooks.ps1 @@ -97,8 +126,70 @@ else TAG="refs/tags/rescue/auto/$REPO/detached/$(git rev-parse --short HEAD 2>/dev/null)" fi +# --- provenance (BACKLOG #1349) --------------------------------------------------------------- +# SRC is what gets pushed. It stays HEAD unless a provenance tag object can be built, so the +# durability guarantee is unconditional and the provenance rides on top of it. +SRC=HEAD +TAGOBJ= + +COMMIT=$(git rev-parse --verify --quiet "HEAD^{commit}" 2>/dev/null) +IDENT=$(git var GIT_COMMITTER_IDENT 2>/dev/null) + +if [ -n "$COMMIT" ] && [ -n "$IDENT" ]; then + if [ -n "$BRANCH" ]; then + # WAS IT THE TIP? Verified rather than assumed. A post-commit hook runs with HEAD on the commit + # it just made, so the answer is True by construction -- and "true by construction" is the exact + # shape of claim this item exists to distrust, so it costs one rev-parse to actually check. + TIP=$(git rev-parse --verify --quiet "refs/heads/$BRANCH^{commit}" 2>/dev/null) + if [ "$TIP" = "$COMMIT" ]; then + WASTIP="was-tip: True" + else + WASTIP="was-tip: False" + fi + LABEL="$BRANCH" + else + # NO BRANCH, SO THE LINE IS OMITTED RATHER THAN GUESSED. `was-tip: True` and `was-tip: False` + # are both claims about a branch that does not exist. Leaving the line out is what makes + # `rescue.ps1 -Check` report SELF-DESCRIBING -- "intact, and whether it held a tip cannot be + # told" -- which is the true statement about a detached capture. + WASTIP= + LABEL="(detached)" + fi + + CAPTURED=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null) + + MSG="mefor-rescue-v1 +commit: $COMMIT +branch: $LABEL" + if [ -n "$WASTIP" ]; then + MSG="$MSG +$WASTIP" + fi + if [ -n "$CAPTURED" ]; then + MSG="$MSG +captured: $CAPTURED" + fi + MSG="$MSG +writer: durability_push.sh" + + # `-w` writes the object and prints its id; nothing references it until the push lands, and an + # unreferenced loose object is collected on the usual schedule if the push never does. + TAGOBJ=$(printf 'object %s\ntype commit\ntag %s\ntagger %s\n\n%s\n' \ + "$COMMIT" "${TAG#refs/tags/}" "$IDENT" "$MSG" \ + | git hash-object -t tag -w --stdin 2>/dev/null) +fi + +if [ -n "$TAGOBJ" ]; then + SRC="$TAGOBJ" +else + echo "durability_push: WARNING -- could not build a provenance tag object for $TAG." >&2 + echo " Pushing the bare commit instead, so DURABILITY IS UNAFFECTED and this commit stands." >&2 + echo " The ref will read UNVERIFIABLE under scripts/coord/rescue.ps1 -Check, which is the" >&2 + echo " honest verdict for a ref that records nothing about what it captured." >&2 +fi + # Backgrounded and detached so the commit returns immediately. --force because the tag tracks a # moving tip. Output is discarded: see "NEVER FAILS A COMMIT" above. -( git push --quiet --force "$REMOTE" "HEAD:$TAG" >/dev/null 2>&1 & ) >/dev/null 2>&1 +( git push --quiet --force "$REMOTE" "$SRC:$TAG" >/dev/null 2>&1 & ) >/dev/null 2>&1 exit 0 From dc91908bddf51e3c158f808e8bc47c6e407449b0 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 3 Sep 2026 17:32:31 -0500 Subject: [PATCH 2/7] fix(coord): point unbacked_check's printed remedy at rescue.ps1 -Anchor (BACKLOG #1349) The remedy block printed a bare git push --force :refs/tags/rescue/branch/ so an operator following the tool's own advice wrote a ref that records nothing about what it captured. Measured 2026-09-03 in this checkout: rescue.ps1 -Check examined 1671 refs and returned UNVERIFIABLE for all 1671, every one of them written by a bare push of that shape. The block now runs -Anchor first and pushes the annotated tag it wrote, and says why the extra step is the remedy rather than decoration. Co-Authored-By: Claude Opus 5 --- scripts/coord/unbacked_check.ps1 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/coord/unbacked_check.ps1 b/scripts/coord/unbacked_check.ps1 index cc7cffec9..af8b687ed 100644 --- a/scripts/coord/unbacked_check.ps1 +++ b/scripts/coord/unbacked_check.ps1 @@ -456,6 +456,21 @@ Write-Host "result : $totalCommits commits on $($where -join ' and ') exist on Write-Host "" Write-Host "Remedy, which buys durability without publication or review:" Write-Host " git config mefor.durabilityRemote # once, per repository" -Write-Host " git push --force :refs/tags/rescue/branch/" +Write-Host " pwsh -NoProfile -File scripts\coord\rescue.ps1 -Anchor # IN the checkout holding the work" +Write-Host " git push --force refs/tags/rescue/" Write-Host "Verify the remote is private BEFORE nominating it: gh repo view / --json visibility" +# THE -Anchor STEP IS THE REMEDY, NOT DECORATION (BACKLOG #1349). This block used to print a bare +# `git push --force :refs/tags/rescue/branch/`, so an operator who followed +# the tool's own advice wrote a ref recording NOTHING about what it captured. That ref can then only +# be graded against a branch that still exists -- which is precisely the population a rescue ref was +# never needed for. Measured 2026-09-03 in this checkout: rescue.ps1 -Check examined 1671 refs and +# returned UNVERIFIABLE for all 1671, every one of them written by a bare push like the one this +# script was printing. -Anchor writes an annotated tag whose message carries the commit, the branch +# and whether it was that branch's head at the instant of capture, and -Check reads that back after +# the branch is gone. +Write-Host "" +Write-Host "-Anchor is what makes the ref readable LATER: it records the commit, the branch and" +Write-Host "whether the capture was that branch's head. A bare push records none of that, and a" +Write-Host "rescue ref is read once -- after the branch it names is already gone. Audit what you" +Write-Host "have with: pwsh -NoProfile -File scripts\coord\rescue.ps1 -Check" exit 1 From a21678f4e563588172143d47a28c03a9f9c30bfd Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 3 Sep 2026 17:33:47 -0500 Subject: [PATCH 3/7] fix(coord): widen the rescue audit to the push-updated namespace (BACKLOG #1349) -Check read refs/rescue and refs/tags/rescue only, so it graded 1671 refs while 1505 more sat under refs/remotes/private/rescuetags unexamined. That namespace exists because remote.private.fetch remaps refs/tags/rescue/* into it, so one server-side tag arrives under a second local name that git tag -l cannot see. Remote prefixes are derived from git remote rather than hardcoded, since the remote's name is an operator's choice. Namespaces matching nothing are still listed at zero, because a silently dropped namespace is how a reader loses the ability to tell what was examined. The two mechanisms are kept apart. A snapshot behind its branch is behind it forever; a push-updated ref behind a LIVE branch may merely be lagging, and the next commit fixes it. Mechanism changes the wording of BEHIND and DIVERGED and deliberately changes no verdict: once the branch is gone nothing pushes again, so the branch-gone arms are equally final either way. Counts are not deduplicated -- the two names disagreeing is the finding. EXAMINED goes from 1671 to 3187 in this checkout. Co-Authored-By: Claude Opus 5 --- scripts/coord/rescue.ps1 | 172 ++++++++++++++++++++++++++++++++++----- 1 file changed, 153 insertions(+), 19 deletions(-) diff --git a/scripts/coord/rescue.ps1 b/scripts/coord/rescue.ps1 index 26356391d..42dcffbed 100644 --- a/scripts/coord/rescue.ps1 +++ b/scripts/coord/rescue.ps1 @@ -55,6 +55,29 @@ for a reason that has nothing to do with staleness. Every commit id here comes from `%(*objectname)` where it exists and `%(objectname)` otherwise. + AND THE AUDIT READS THREE KINDS OF NAMESPACE, NOT TWO, BECAUSE THE LARGEST ONE WAS MISSING. + Measured 2026-09-03: this checkout holds 266 refs under `refs/rescue`, 1405 under + `refs/tags/rescue` -- and 1505 under `refs/remotes/private/rescuetags`, which the first version + of this script never looked at. That namespace exists because `remote.private.fetch` carries + `+refs/tags/rescue/*:refs/remotes/private/rescuetags/*`, so ONE server-side tag arrives under a + SECOND local name. `git tag -l 'rescue/*'` cannot see it, which is how two readers each verified + a real object with an instrument structurally blind to the other's. + + THE TWO KINDS ARE DIFFERENT MECHANISMS AND ARE REPORTED AS SUCH. A `refs/tags/rescue/*` ref is a + SNAPSHOT: nothing re-takes it, so a ref behind its branch is behind it forever. A + `refs/remotes//rescuetags/*` ref is PUSH-UPDATED: the durability hook force-moves it on + every commit, so a ref behind its branch may merely be LAGGING and the next commit fixes it. One + reflog on such a ref shows six updates. Grading those two at one severity is the naming collapse + this whole item is about, so `mechanism` is a first-class field here and the report splits on it. + + THE SOFTENING APPLIES ONLY WHILE THE BRANCH IS ALIVE. A push-updated ref whose branch is GONE + gets no further pushes, so its staleness is as permanent as a tag's. Mechanism therefore changes + the wording of BEHIND and DIVERGED and deliberately changes nothing about the branch-gone arms. + + THE COUNTS OVERLAP ON PURPOSE. A tag and its remote-tracking mirror are two names for what may or + may not be one object, and 15 percent of the pairs measured for this item DISAGREED. Deduplicating + them would delete the finding. + .EXAMPLE pwsh -NoProfile -File scripts\coord\rescue.ps1 -Anchor my-lane-before-rebase pwsh -NoProfile -File scripts\coord\rescue.ps1 -Check @@ -87,6 +110,38 @@ $PROVENANCE = 'mefor-rescue-v1' $FS = [char]0x1F $RS = [char]0x1E +#: The two mechanisms a rescue ref can be written by, and the difference decides how loudly a +#: behind-its-branch ref should read. Named once here so no call site spells them differently. +$SNAPSHOT = 'snapshot' +$PUSH_UPDATED = 'push-updated' + +function Get-RescueNamespaces { + <# + EVERY NAMESPACE A RESCUE REF LANDS IN, WITH THE MECHANISM THAT WRITES IT. + + The remote-tracking prefixes are derived from `git remote` rather than hardcoded, because the + remote's NAME is an operator's choice: this checkout calls it `private`, and nothing makes that + universal. Enumerating remotes costs one spawn and removes the guess. + + A prefix that matches nothing is kept and reported as zero. That is the coverage rule this + directory already sets: a run that examined nothing and a run that examined everything must not + print the same reassuring line, and a namespace silently dropped for being empty is exactly how + a reader loses the ability to tell. + #> + $ns = [ordered]@{} + $ns['refs/rescue'] = $SNAPSHOT + $ns['refs/tags/rescue'] = $SNAPSHOT + foreach ($remote in (& git -C $repo remote)) { + $r = "$remote".Trim() + if (-not $r) { continue } + # `rescuetags` is what remote..fetch remaps refs/tags/rescue/* into here; `rescue` covers + # a mirror of the refs/rescue subtree under the same remote. + $ns["refs/remotes/$r/rescuetags"] = $PUSH_UPDATED + $ns["refs/remotes/$r/rescue"] = $PUSH_UPDATED + } + return $ns +} + function Get-RescueRecords { <# ONE `for-each-ref` FOR THE WHOLE AUDIT, AND THE REASON IS MEASURED. The first version of this @@ -107,17 +162,30 @@ function Get-RescueRecords { `%(objectname)` is already the commit. Taking both and picking is why an annotated tag does not silently compare its TAG OBJECT against a branch tip. #> + param([System.Collections.Specialized.OrderedDictionary]$Namespaces) + + $prefixes = @($Namespaces.Keys) $fmt = "%(refname)$FS%(objectname)$FS%(*objectname)$FS%(contents)$RS" - $raw = (& git -C $repo for-each-ref --format=$fmt refs/rescue refs/tags/rescue) -join "`n" + $raw = (& git -C $repo for-each-ref --format=$fmt $prefixes) -join "`n" foreach ($rec in ($raw -split [regex]::Escape($RS))) { if (-not $rec.Trim()) { continue } $f = $rec -split [regex]::Escape($FS) if ($f.Count -lt 3) { continue } $commit = if ($f[2].Trim()) { $f[2].Trim() } else { $f[1].Trim() } + $ref = $f[0].Trim() + # Longest match is not needed: no prefix here is a prefix of another. `refs/rescue` and + # `refs/remotes//rescue` are disjoint subtrees, and git's own prefix rule stops at a + # slash, so `refs/rescue` never claims `refs/rescuetags`. + $namespace = $null + foreach ($p in $prefixes) { + if ($ref -eq $p -or $ref.StartsWith("$p/")) { $namespace = $p; break } + } [pscustomobject]@{ - ref = $f[0].Trim() - commit = $commit - contents = if ($f.Count -ge 4) { $f[3] } else { '' } + ref = $ref + commit = $commit + contents = if ($f.Count -ge 4) { $f[3] } else { '' } + namespace = $namespace + mechanism = if ($namespace) { $Namespaces[$namespace] } else { 'unknown' } } } } @@ -180,7 +248,8 @@ if ($PSCmdlet.ParameterSetName -eq 'Anchor') { # --------------------------------------------------------------------------------------------- # -Check # --------------------------------------------------------------------------------------------- -$records = @(Get-RescueRecords) +$namespaces = Get-RescueNamespaces +$records = @(Get-RescueRecords -Namespaces $namespaces) $tips = Get-LocalBranchTips $rows = @() @@ -188,6 +257,7 @@ foreach ($rec in $records) { $r = $rec.ref $commit = $rec.commit $body = $rec.contents + $mechanism = $rec.mechanism $selfDescribing = $body -match [regex]::Escape($PROVENANCE) $recordedBranch = $null @@ -256,11 +326,25 @@ foreach ($rec in $records) { } elseif ($ahead -eq '0') { $verdict = 'BEHIND' - $detail = "$behind commit(s) short of $branchName -- a snapshot older than its branch, NOT a defect" + # MECHANISM CHANGES THE SENTENCE, NEVER THE VERDICT. A snapshot behind its branch is + # behind it forever, because nothing re-takes it. A push-updated ref behind its + # branch is a different fact -- the hook force-moves it on the next commit -- and + # reporting the two in identical words is the collapse this item exists to name. + $detail = if ($mechanism -eq $PUSH_UPDATED) { + "$behind commit(s) short of $branchName -- re-pushed as the branch moves, so this may merely be LAGGING" + } + else { + "$behind commit(s) short of $branchName -- a snapshot older than its branch, NOT a defect" + } } else { $verdict = 'DIVERGED' - $detail = "$behind behind / $ahead ahead of $branchName" + $detail = if ($mechanism -eq $PUSH_UPDATED) { + "$behind behind / $ahead ahead of $branchName -- re-pushed, so the behind half may merely be LAGGING" + } + else { + "$behind behind / $ahead ahead of $branchName" + } } } } @@ -284,15 +368,28 @@ foreach ($rec in $records) { $rows += [pscustomobject]@{ ref = $r; commit = $commit; verdict = $verdict + namespace = $rec.namespace; mechanism = $mechanism selfDescribing = [bool]$selfDescribing; wasTipAtCapture = $recordedWasTip; detail = $detail } } +#: Coverage, per namespace, including the ones that matched nothing. Built before the JSON branch so +#: both outputs report the same thing. +$coverage = @() +foreach ($p in $namespaces.Keys) { + $coverage += [pscustomobject]@{ + namespace = $p + mechanism = $namespaces[$p] + count = @($rows | Where-Object namespace -eq $p).Count + } +} + if ($Json) { [pscustomobject]@{ - examined = $rows.Count - repo = $repo - rows = $rows + examined = $rows.Count + repo = $repo + namespaces = $coverage + rows = $rows } | ConvertTo-Json -Depth 5 exit 0 } @@ -301,11 +398,22 @@ $byVerdict = $rows | Group-Object verdict | Sort-Object Name Write-Host "" Write-Host "RESCUE REF AUDIT -- $repo" -Write-Host "EXAMINED $($rows.Count) ref(s) across refs/rescue/ and refs/tags/rescue/." +Write-Host "EXAMINED $($rows.Count) ref(s) across $($coverage.Count) namespace(s):" +foreach ($c in $coverage) { + Write-Host (" {0,6} {1,-13} {2}" -f $c.count, $c.mechanism, $c.namespace) +} +Write-Host " $SNAPSHOT = a one-time capture. Nothing re-takes it, so staleness here is PERMANENT." +Write-Host " $PUSH_UPDATED = force-moved by the durability hook, so a ref behind a LIVE branch may" +Write-Host " merely be lagging. Once that branch is gone, nothing pushes again and" +Write-Host " the two mechanisms are equally final." +Write-Host " The counts OVERLAP by construction: remote..fetch remaps refs/tags/rescue/*" +Write-Host " into refs/remotes//rescuetags/*, so one server-side object arrives under a" +Write-Host " second local name. They are not deduplicated -- the two names disagreeing is the" +Write-Host " finding, and folding them together would delete it." if ($rows.Count -eq 0) { Write-Host " Zero refs examined. That is a fact about this repository, not a clean bill." -ForegroundColor Yellow } -Write-Host "" +Write-Host ("{0,-16} {1,10} {2,13}" -f 'verdict', $SNAPSHOT, $PUSH_UPDATED) foreach ($g in $byVerdict) { # SHORT-AT-CAPTURE is Gray with BEHIND, not Green with TIP: both are snapshots older than their # branch and neither is a defect, but neither is the outcome a reader hopes for either. Bare @@ -318,7 +426,26 @@ foreach ($g in $byVerdict) { 'ALTERED' { 'Red' } default { 'Yellow' } } - Write-Host ("{0,-16} {1}" -f $g.Name, $g.Count) -ForegroundColor $colour + $snap = @($g.Group | Where-Object mechanism -eq $SNAPSHOT).Count + $pushed = @($g.Group | Where-Object mechanism -eq $PUSH_UPDATED).Count + Write-Host ("{0,-16} {1,10} {2,13}" -f $g.Name, $snap, $pushed) -ForegroundColor $colour +} + +$unknown = @($rows | Where-Object mechanism -eq 'unknown').Count +if ($unknown -gt 0) { + # Cannot happen while every ref comes from an enumerated prefix, and is printed anyway: a row + # whose mechanism is unclassified would otherwise render as 0 and 0 and read as nothing at all. + Write-Host "" + Write-Host "$unknown ref(s) matched no known namespace and are MISSING from the split above." -ForegroundColor Red +} + +$laggingBehind = @($rows | Where-Object { $_.verdict -eq 'BEHIND' -and $_.mechanism -eq $PUSH_UPDATED }).Count +if ($laggingBehind -gt 0) { + Write-Host "" + Write-Host "$laggingBehind push-updated ref(s) sit behind a branch that still exists." -ForegroundColor Gray + Write-Host " Read this one QUIETLY. The durability hook force-moves these on the next commit, so" -ForegroundColor Gray + Write-Host " behind here is usually lag, not loss -- one such ref's reflog shows six updates. It is" -ForegroundColor Gray + Write-Host " NOT the same finding as a snapshot behind its branch, which never catches up." -ForegroundColor Gray } $short = @($rows | Where-Object verdict -eq 'SHORT-AT-CAPTURE').Count @@ -330,13 +457,20 @@ if ($short -gt 0) { Write-Host " Nothing can compare them against anything now, so the recorded answer is the only one." -ForegroundColor Yellow } -$unverifiable = @($rows | Where-Object verdict -eq 'UNVERIFIABLE').Count -if ($unverifiable -gt 0) { +$unverifiable = @($rows | Where-Object verdict -eq 'UNVERIFIABLE') +if ($unverifiable.Count -gt 0) { + $uSnap = @($unverifiable | Where-Object mechanism -eq $SNAPSHOT).Count + $uPush = @($unverifiable | Where-Object mechanism -eq $PUSH_UPDATED).Count Write-Host "" - Write-Host "$unverifiable ref(s) are UNVERIFIABLE, which is NOT the same as healthy." -ForegroundColor Yellow + Write-Host "$($unverifiable.Count) ref(s) are UNVERIFIABLE, which is NOT the same as healthy." -ForegroundColor Yellow Write-Host " They carry no recorded provenance and name no branch that still exists, so nothing" -ForegroundColor Yellow - Write-Host " here can say what they hold. They predate -Anchor and cannot be retrofitted: the" -ForegroundColor Yellow - Write-Host " information was never captured. Refs written by -Anchor stay verifiable after their" -ForegroundColor Yellow - Write-Host " branch is deleted, which is the case a rescue ref exists for." -ForegroundColor Yellow + Write-Host " here can say what they hold." -ForegroundColor Yellow + Write-Host " $uSnap of them are $SNAPSHOT refs. Those CANNOT be retrofitted -- the information was" -ForegroundColor Yellow + Write-Host " never captured and nothing re-takes a snapshot. Write new ones with -Anchor." -ForegroundColor Yellow + Write-Host " $uPush of them are $PUSH_UPDATED refs, and this half HEALS ITSELF. The durability hook" -ForegroundColor Yellow + Write-Host " now pushes an annotated tag object carrying the same provenance, so each of these" -ForegroundColor Yellow + Write-Host " becomes readable on the next commit to the branch it tracks. A branch with no" -ForegroundColor Yellow + Write-Host " further commits keeps its unverifiable ref, so the count falls with activity, not" -ForegroundColor Yellow + Write-Host " with time." -ForegroundColor Yellow } exit 0 From 3694818a5436cbe693252669a85fc48a58cb7915 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 3 Sep 2026 17:56:28 -0500 Subject: [PATCH 4/7] test(coord): pin the three rescue-ref writer surfaces (BACKLOG #1349) Eight new arms over the durability hook in tests/test_durability_hook_provenance.py, three over the widened audit, one over the printed remedy. The hook arms are ordered durability first, because that is the guarantee the provenance layer must not move: a broken committer identity degrades to a bare push with a warning and exit 0, an unreachable remote does not fail the commit, and an unarmed hook pushes nothing. The sabotage arm disarms the hook while its fixture commit is made, so the verdict cannot be decided by which of two racing pushes lands second. The push is detached, so the remote is polled rather than read once. Nothing asserts on elapsed time. The audit arms drive ONE object under TWO names and require the details to differ: a snapshot behind its branch is behind it forever, a push-updated mirror may merely be lagging. An empty namespace is still required to appear in the coverage, since a silently dropped one reads exactly like a clean bill. Co-Authored-By: Claude Opus 5 --- tests/test_coord_unbacked_check.py | 32 +++ tests/test_durability_hook_provenance.py | 341 +++++++++++++++++++++++ tests/test_rescue_ref_provenance.py | 108 +++++++ 3 files changed, 481 insertions(+) create mode 100644 tests/test_durability_hook_provenance.py diff --git a/tests/test_coord_unbacked_check.py b/tests/test_coord_unbacked_check.py index d1853c56b..ed745adc7 100644 --- a/tests/test_coord_unbacked_check.py +++ b/tests/test_coord_unbacked_check.py @@ -328,3 +328,35 @@ def test_a_merge_carrying_a_HAND_RESOLUTION_still_trips(repo: Path) -> None: r = check(repo) assert r.returncode == 1, "a hand-resolved merge is NOT re-derivable\n" + r.stdout + r.stderr assert "exist on no remote" in r.stdout + + +# --- BACKLOG #1349: the printed remedy must not write an unverifiable ref ----------------------- + + +def test_the_printed_remedy_writes_a_ref_that_can_be_VERIFIED_LATER(repo: Path) -> None: + """The tool's own advice used to produce the defect its sibling audit reports. + + The remedy block printed a bare + ``git push --force :refs/tags/rescue/branch/``, which writes a ref + recording nothing about what it captured. Such a ref can only be graded against a branch that + still exists -- and a rescue ref is read once, after the branch it names is already gone. + + Measured 2026-09-03 in the live checkout: ``rescue.ps1 -Check`` examined 1671 refs and returned + UNVERIFIABLE for all 1671, every one of them written by a push of exactly that shape. So this + asserts BOTH halves: the bare form is gone, and ``-Anchor`` is what replaced it. Asserting only + that ``-Anchor`` appears would pass a block that printed both and left the reader to choose. + """ + git(repo, "checkout", "-q", "-b", "side") + (repo / "g.txt").write_text("this exists on exactly one disk", encoding="utf-8") + git(repo, "add", "-A") + git(repo, "commit", "-qm", "real work") + + r = check(repo) + assert r.returncode == 1, r.stdout + r.stderr + assert "Remedy" in r.stdout + assert "rescue.ps1 -Anchor" in r.stdout + assert ":refs/tags/rescue/branch/" not in r.stdout, ( + "still printing the bare push that writes an unverifiable ref" + ) + # The reason has to travel with the command, or the extra step reads as ceremony and gets cut. + assert "A bare push records none of that" in r.stdout diff --git a/tests/test_durability_hook_provenance.py b/tests/test_durability_hook_provenance.py new file mode 100644 index 000000000..9b861c406 --- /dev/null +++ b/tests/test_durability_hook_provenance.py @@ -0,0 +1,341 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 MessageFoundry Organization and contributors +"""The durability hook must write a ref that can be read after the branch is gone (BACKLOG #1349). + +``scripts/hooks/durability_push.sh`` is the highest-volume writer of rescue refs in this project -- +it fires on every commit in every armed checkout -- and it used to force-push a bare ref recording +nothing. Measured 2026-09-03 in the live checkout: ``rescue.ps1 -Check`` examined 1671 refs and +returned UNVERIFIABLE for all 1671, because a bare ref can only be graded against a branch that +still exists, and a rescue ref is read once, in the moment the original is already gone. + +**DURABILITY OUTRANKS PROVENANCE, AND THE TESTS ARE ORDERED THAT WAY.** This is a POST-COMMIT hook +whose first contract is that it never fails a commit. Provenance is an improvement layered on top of +a guarantee that must not move, so the degradation arms below are not edge cases -- they are the +acceptance. Each one drives a real failure and requires the commit to stand and the push to happen +anyway. + +**THE PUSH IS BACKGROUNDED, SO THE REMOTE IS POLLED RATHER THAN READ ONCE.** The hook detaches the +push so the commit returns immediately, which is the property that keeps it from being disabled by +the first person it inconveniences. A single read straight after ``git commit`` is therefore a race, +and asserting on elapsed time would be a load measurement rather than a behaviour one. + +**THE TAG OBJECT IS NOT THE COMMIT.** ``git rev-parse`` on an annotated tag returns the TAG OBJECT. +``diff``, ``merge-base`` and ``rev-list`` all dereference silently, so ancestry looks right while a +published sha does not resolve -- which happened once and was corrected. The commit assertions here +go through ``%(*objectname)`` or ``^{commit}`` for that reason. + +**NO LOCAL REF IS CREATED, AND THAT IS ASSERTED.** A local annotated tag reachable from a branch tip +would be swept up by ``git push --follow-tags`` to whatever remote a hand reaches for, and the +default one in this project is PUBLIC. A provenance mechanism that opens the publication path the +hook exists to avoid would be a worse defect than the one it fixes. + +The fixtures build throwaway repositories under ``tmp_path`` and never touch the real +``.git/hooks``: the suite runs under ``pytest-xdist``, so four workers would race any shared state. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import time +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +HOOK = ROOT / "scripts" / "hooks" / "durability_push.sh" +RESCUE = ROOT / "scripts" / "coord" / "rescue.ps1" +TIMEOUT = 180 +#: The push is detached, so the remote is polled. Generous because the box this runs on is routinely +#: saturated; a short budget would turn load into a red test. +PUSH_WAIT = 90.0 + +pytestmark = pytest.mark.skipif( + shutil.which("sh") is None, reason="durability_push.sh is a /bin/sh hook and needs sh on PATH" +) + + +def git(repo: Path, *args: str) -> str: + return subprocess.run( + ["git", "-C", str(repo), *args], capture_output=True, text=True, timeout=TIMEOUT, check=True + ).stdout.strip() + + +@pytest.fixture +def armed(tmp_path: Path) -> tuple[Path, Path]: + """A checkout with the hook installed and armed, plus the bare remote it pushes to. + + The repository directory is named ``r`` on purpose: the hook derives the ```` path segment + from the git COMMON DIR's parent, so the ref it writes is ``refs/tags/rescue/auto/r/``. + That segment is load-bearing -- two repositories pushing to one remote collided on + ``refs/tags/rescue/auto/main`` before it existed -- so the tests spell the full refname out. + """ + bare = tmp_path / "priv.git" + subprocess.run( + ["git", "init", "-q", "--bare", "-b", "main", str(bare)], check=True, capture_output=True + ) + repo = tmp_path / "r" + repo.mkdir() + subprocess.run(["git", "init", "-q", "-b", "main", str(repo)], check=True, capture_output=True) + git(repo, "config", "user.email", "t@example.invalid") + git(repo, "config", "user.name", "t") + hook = repo / ".git" / "hooks" / "post-commit" + shutil.copy2(HOOK, hook) + hook.chmod(0o755) + git(repo, "remote", "add", "priv", str(bare)) + git(repo, "config", "mefor.durabilityRemote", "priv") + return repo, bare + + +def commit(repo: Path, text: str) -> str: + (repo / "a.txt").write_text(text, encoding="utf-8") + subprocess.run( + ["git", "-C", str(repo), "add", "-A"], check=True, capture_output=True, timeout=TIMEOUT + ) + proc = subprocess.run( + ["git", "-C", str(repo), "commit", "-qm", text.strip()], + capture_output=True, + text=True, + timeout=TIMEOUT, + ) + assert proc.returncode == 0, "THE HOOK FAILED A COMMIT\n" + proc.stdout + proc.stderr + return git(repo, "rev-parse", "HEAD") + + +def wait_for_ref(bare: Path, ref: str) -> bool: + deadline = time.monotonic() + PUSH_WAIT + while time.monotonic() < deadline: + got = subprocess.run( + ["git", "-C", str(bare), "rev-parse", "--verify", "--quiet", ref], + capture_output=True, + text=True, + timeout=TIMEOUT, + ) + if got.returncode == 0 and got.stdout.strip(): + return True + time.sleep(0.25) + return False + + +def tag_body(bare: Path, ref: str) -> str: + return git(bare, "for-each-ref", "--format=%(contents)", ref) + + +def test_the_pushed_ref_carries_the_provenance_the_audit_reads(armed: tuple[Path, Path]) -> None: + """THE POSITIVE CONTROL, and every degradation arm below is meaningless without it. + + The message shape is not this file's invention: it is the same ``mefor-rescue-v1`` block + ``rescue.ps1 -Anchor`` writes and ``-Check`` parses, so a hook-written ref and an operator-written + one grade identically. + """ + repo, bare = armed + sha = commit(repo, "one\n") + ref = "refs/tags/rescue/auto/r/main" + + assert wait_for_ref(bare, ref), "the durability push never landed" + assert git(bare, "for-each-ref", "--format=%(objecttype)", ref) == "tag", ( + "a bare ref again -- nothing can be read back from it" + ) + body = tag_body(bare, ref) + assert "mefor-rescue-v1" in body + assert f"commit: {sha}" in body + assert "branch: main" in body + assert "was-tip: True" in body + assert "writer: durability_push.sh" in body + + +def test_the_ref_reports_its_COMMIT_and_not_the_tag_object(armed: tuple[Path, Path]) -> None: + """The trap the item flags by name, pinned where the sha is actually published. + + ``rev-parse`` on this ref returns the TAG OBJECT. Ancestry and diffstat dereference silently, so + a wrong sha survives every check a reader is likely to run and only fails when someone pastes it + into a log. One such sha was published and corrected while #1349 was being written. + """ + repo, bare = armed + sha = commit(repo, "one\n") + ref = "refs/tags/rescue/auto/r/main" + assert wait_for_ref(bare, ref) + + tag_object = git(bare, "rev-parse", ref) + assert tag_object != sha, "fixture is not annotated -- this test would prove nothing" + assert git(bare, "rev-parse", f"{ref}^{{commit}}") == sha + assert git(bare, "for-each-ref", "--format=%(*objectname)", ref) == sha + # The recorded sha must be the COMMIT too, or -Check compares a tag object against a branch tip + # and reports ALTERED about a ref that is perfectly sound. + assert f"commit: {sha}" in tag_body(bare, ref) + + +def test_no_LOCAL_ref_is_created_so_provenance_cannot_leak_to_a_public_remote( + armed: tuple[Path, Path], +) -> None: + """The failure mode the design avoids, asserted rather than asserted-in-a-comment. + + Building the tag object with ``hash-object`` and pushing it BY ID leaves nothing locally for + ``git push --follow-tags`` or ``git push --tags`` to carry to ``origin``. Writing the same + annotated tag under ``refs/tags/`` first would have been simpler and would have made the + durability hook a publication channel, which is the one thing it must never become. + """ + repo, bare = armed + commit(repo, "one\n") + assert wait_for_ref(bare, "refs/tags/rescue/auto/r/main") + + local = git(repo, "for-each-ref", "--format=%(refname)", "refs/tags") + assert local == "", f"the hook left a local tag behind: {local}" + + +def test_a_DETACHED_capture_omits_was_tip_rather_than_guessing(armed: tuple[Path, Path]) -> None: + """A missing answer and a negative answer must not collapse into one. + + With no branch there is no tip to have been, so both ``True`` and ``False`` would be claims about + a branch that does not exist. Leaving the line out is what makes ``-Check`` say SELF-DESCRIBING: + intact, and whether it held a tip cannot be told. That is the true statement here. + """ + repo, bare = armed + commit(repo, "one\n") + git(repo, "checkout", "-q", "--detach") + sha = commit(repo, "two\n") + short = git(repo, "rev-parse", "--short", "HEAD") + ref = f"refs/tags/rescue/auto/r/detached/{short}" + + assert wait_for_ref(bare, ref), "a detached HEAD is the state most likely to lose work" + body = tag_body(bare, ref) + assert "was-tip:" not in body, "guessed an answer about a branch that does not exist" + assert "branch: (detached)" in body + assert f"commit: {sha}" in body + + +def test_a_BROKEN_IDENTITY_degrades_to_a_bare_push_and_the_hook_still_exits_0( + armed: tuple[Path, Path], +) -> None: + """THE DEGRADATION ARM. A post-commit hook must never fail a commit, whatever else goes wrong. + + ``git hash-object`` fsck-validates the tag object and refuses a malformed tagger line, and + ``git var GIT_COMMITTER_IDENT`` fails outright on an empty name -- measured, not assumed: + ``fatal: empty ident name (for ) not allowed``, exit 128. That is the whole class of new + failure this change introduced, driven here through its most reachable trigger. + + The requirement is not that provenance survives. It is that DURABILITY does: the ref still lands, + the exit status is still 0, and the operator is told once rather than left to discover a silent + downgrade. The hook is invoked directly because the sabotage would otherwise break the commit + itself, and the question is about the hook. + + THE HOOK IS DISARMED WHILE THE FIXTURE COMMIT IS MADE, and that is not tidiness. Leaving it armed + would fire a healthy backgrounded push for the same ref, racing the sabotaged one -- and whichever + landed second would decide the assertion. A test whose verdict depends on which of two pushes wins + reports scheduling, not behaviour. + """ + repo, bare = armed + git(repo, "config", "--unset", "mefor.durabilityRemote") + git(repo, "checkout", "-q", "-b", "sabotaged") + sha = commit(repo, "two\n") + git(repo, "config", "mefor.durabilityRemote", "priv") + assert git(bare, "for-each-ref", "--format=%(refname)") == "", "the fixture commit pushed" + + env = dict(os.environ, GIT_COMMITTER_NAME="") + proc = subprocess.run( + ["sh", str(repo / ".git" / "hooks" / "post-commit")], + cwd=str(repo), + env=env, + capture_output=True, + text=True, + timeout=TIMEOUT, + ) + + assert proc.returncode == 0, "a post-commit hook that exits non-zero is a broken commit" + assert "WARNING" in proc.stderr, "a silent downgrade is how a control stops protecting anything" + assert "DURABILITY IS UNAFFECTED" in proc.stderr + + ref = "refs/tags/rescue/auto/r/sabotaged" + assert wait_for_ref(bare, ref), "provenance failed and took durability down with it" + assert git(bare, "for-each-ref", "--format=%(objecttype)", ref) == "commit" + assert git(bare, "rev-parse", ref) == sha + + +def test_an_UNREACHABLE_remote_does_not_fail_the_commit(armed: tuple[Path, Path]) -> None: + """The ordinary field failure: the nominated remote is gone, offline, or misspelled. + + Nothing here can report that, and it deliberately does not try -- the reporting job belongs to + ``unbacked_check.ps1``, which measures the true state rather than trusting this hook ran. What + must hold is that the commit is unaffected. + """ + repo, bare = armed + commit(repo, "one\n") + git(repo, "remote", "set-url", "priv", str(bare.parent / "not-a-repo.git")) + + commit(repo, "two\n") # asserts returncode 0 internally + assert git(repo, "rev-list", "--count", "HEAD") == "2" + + +def test_an_UNARMED_hook_pushes_nothing_and_the_commit_stands(armed: tuple[Path, Path]) -> None: + """Fail-safe by absence. A fresh clone, a CI checkout or a contributor's fork must push nowhere. + + This is the negative control for the whole file: without it, a hook that pushed unconditionally + would satisfy every other test here. + """ + repo, bare = armed + git(repo, "config", "--unset", "mefor.durabilityRemote") + commit(repo, "one\n") + + assert git(bare, "for-each-ref", "--format=%(refname)") == "", "pushed without being armed" + + +@pytest.mark.skipif(shutil.which("pwsh") is None, reason="pwsh (PowerShell 7) not on PATH") +def test_a_hook_written_ref_is_still_readable_AFTER_the_branch_is_deleted( + armed: tuple[Path, Path], tmp_path: Path +) -> None: + """THE PAYOFF, and the only state that matters: the branch is gone and the ref still speaks. + + This is the population #1349 is about -- a rescue ref is consulted once, after the original is + already gone, and 436 of one measured namespace's 730 refs name a branch that no longer exists. + Before this change the hook's refs came back UNVERIFIABLE there, which is the honest verdict for + a ref recording nothing and a useless one for somebody deciding what to recover. + + The audit is run in a CONSUMER of the remote rather than in the writing repository, because that + is the real shape: the refs arrive under ``refs/remotes//rescuetags/*`` through the fetch + refspec, which is the namespace the audit was widened to read in the same change. + """ + repo, bare = armed + git(repo, "checkout", "-q", "-b", "doomed") + sha = commit(repo, "work that outlives its branch\n") + assert wait_for_ref(bare, "refs/tags/rescue/auto/r/doomed") + + consumer = tmp_path / "consumer" + subprocess.run( + ["git", "init", "-q", "-b", "main", str(consumer)], check=True, capture_output=True + ) + (consumer / "scripts" / "coord").mkdir(parents=True) + shutil.copy2(RESCUE, consumer / "scripts" / "coord" / "rescue.ps1") + git(consumer, "remote", "add", "priv", str(bare)) + git( + consumer, + "config", + "--add", + "remote.priv.fetch", + "+refs/tags/rescue/*:refs/remotes/priv/rescuetags/*", + ) + git(consumer, "fetch", "-q", "priv") + # The branch never existed here, which is exactly the state a rescue ref is read in. + assert git(consumer, "for-each-ref", "--format=%(refname)", "refs/heads") == "" + + proc = subprocess.run( + [ + "pwsh", + "-NoProfile", + "-NonInteractive", + "-File", + str(consumer / "scripts" / "coord" / "rescue.ps1"), + "-Check", + ], + cwd=str(consumer), + capture_output=True, + text=True, + timeout=TIMEOUT * 2, + ) + assert proc.returncode == 0, proc.stderr or proc.stdout + assert "HELD-THE-TIP" in proc.stdout, proc.stdout + assert "UNVERIFIABLE" not in proc.stdout, proc.stdout + # And the recorded sha is the COMMIT, so a reader who acts on it reaches the work. + assert sha in git( + consumer, "for-each-ref", "--format=%(contents)", "refs/remotes/priv/rescuetags" + ) diff --git a/tests/test_rescue_ref_provenance.py b/tests/test_rescue_ref_provenance.py index 67f166c8c..2118db66b 100644 --- a/tests/test_rescue_ref_provenance.py +++ b/tests/test_rescue_ref_provenance.py @@ -422,6 +422,114 @@ def test_the_suite_OBSERVES_every_verdict_the_script_can_emit(tmp_path: Path) -> ) +def _repo_with_a_remote(path: Path, bare: Path) -> Path: + """A checkout plus a bare remote carrying the rescuetags refspec this repository really uses. + + ``git config --get-all remote.private.fetch`` in the live checkout returns + ``+refs/tags/rescue/*:refs/remotes/private/rescuetags/*`` beside the ordinary branch line, so one + server-side tag arrives under a SECOND local name. That is not a quirk of one machine -- it is + why ``git tag -l 'rescue/*'`` cannot see the larger of the two populations. + """ + repo = _repo(path) + subprocess.run( + ["git", "init", "-q", "--bare", "-b", "main", str(bare)], check=True, capture_output=True + ) + _git("remote", "add", "priv", str(bare), cwd=repo) + _git( + "config", + "--add", + "remote.priv.fetch", + "+refs/tags/rescue/*:refs/remotes/priv/rescuetags/*", + cwd=repo, + ) + return repo + + +def test_the_audit_reads_the_PUSH_UPDATED_namespace_it_used_to_skip(tmp_path: Path) -> None: + """The largest population was never examined, and nothing said so. + + Measured 2026-09-03 in the live checkout: 266 refs under ``refs/rescue`` and 1405 under + ``refs/tags/rescue`` were graded, while 1505 under ``refs/remotes/private/rescuetags`` were not + read at all. A ref under a name the audit does not enumerate is not reported as unexamined -- it + is absent, which reads exactly like a clean bill. + """ + repo = _repo_with_a_remote(tmp_path / "r", tmp_path / "priv.git") + _git("checkout", "-b", "work", cwd=repo) + _advance(repo, "two\n") + assert _run(repo, "-Anchor", "mirrored").returncode == 0 + _git("push", "-q", "priv", "refs/tags/rescue/mirrored", cwd=repo) + _git("fetch", "-q", "priv", cwd=repo) + + verdicts = _verdicts(repo) + assert "refs/remotes/priv/rescuetags/mirrored" in verdicts, ( + "the push-updated mirror is invisible to the audit" + ) + # The positive control on the fixture: both names must exist, or 'the audit reads both' is + # satisfied by a repository that only has one. + assert "refs/tags/rescue/mirrored" in verdicts + + +def test_a_LAGGING_push_updated_ref_does_not_read_like_a_PERMANENTLY_stale_tag( + tmp_path: Path, +) -> None: + """ONE OBJECT, TWO NAMES, TWO MECHANISMS -- and the item's whole point is that they differ. + + A ``refs/tags/rescue/*`` ref is a snapshot: nothing re-takes it, so behind its branch means + behind it forever. A ``refs/remotes//rescuetags/*`` ref is force-moved by the durability + hook on the next commit, so behind its branch may merely be lag -- one such ref's reflog shows + six updates. Reporting the two at one severity is the naming collapse #1349 exists to name, and + a reader who checks "the rescue tag" gets opposite answers depending only on which name they + reach for. + + The verdict is deliberately the SAME on both. It is the DETAIL, the thing a recovery decision is + actually made from, that has to differ. + """ + repo = _repo_with_a_remote(tmp_path / "r", tmp_path / "priv.git") + _git("checkout", "-b", "work", cwd=repo) + _advance(repo, "two\n") + _run(repo, "-Anchor", "mirrored") + _git("push", "-q", "priv", "refs/tags/rescue/mirrored", cwd=repo) + _git("fetch", "-q", "priv", cwd=repo) + _advance(repo, "three\n") + _advance(repo, "four\n") + + rows = _rows(repo) + tag = rows["refs/tags/rescue/mirrored"] + mirror = rows["refs/remotes/priv/rescuetags/mirrored"] + + assert tag["commit"] == mirror["commit"], "fixture broken -- these must be the same object" + assert tag["verdict"] == mirror["verdict"] == "BEHIND" + assert tag["mechanism"] == "snapshot" + assert mirror["mechanism"] == "push-updated" + assert tag["detail"] != mirror["detail"], "same severity for two different mechanisms" + assert "NOT a defect" in tag["detail"] + assert "LAGGING" in mirror["detail"] + + out = _run(repo, "-Check").stdout + assert "push-updated" in out and "snapshot" in out + assert "merely be lagging" in out + + +def test_a_namespace_that_matched_NOTHING_is_still_named_in_the_coverage(tmp_path: Path) -> None: + """A namespace silently dropped for being empty is how a reader loses the ability to tell. + + This is the standard the sibling ``unbacked_check.ps1`` already sets: a run that examined + nothing and a run that examined everything must not print the same reassuring line. An empty + namespace is the case where that is easiest to get wrong, because omitting it costs nothing and + looks tidier. + """ + repo = _repo_with_a_remote(tmp_path / "r", tmp_path / "priv.git") + proc = _run(repo, "-Check", "-Json") + assert proc.returncode == 0, proc.stderr or proc.stdout + listed = {n["namespace"]: n for n in json.loads(proc.stdout)["namespaces"]} + + assert listed["refs/remotes/priv/rescue"]["count"] == 0 + assert listed["refs/remotes/priv/rescuetags"]["count"] == 0 + assert listed["refs/rescue"]["mechanism"] == "snapshot" + assert listed["refs/remotes/priv/rescuetags"]["mechanism"] == "push-updated" + assert "refs/remotes/priv/rescuetags" in _run(repo, "-Check").stdout + + def test_an_annotated_tag_reports_its_COMMIT_not_the_tag_object(tmp_path: Path) -> None: """``rev-parse`` on an annotated tag returns the TAG OBJECT, and the item flags it by name. From 2c18ac7706b16c5c2d332b49baf2dc09ad29730b Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 3 Sep 2026 18:04:31 -0500 Subject: [PATCH 5/7] refactor(coord): simplify pass over the widened rescue audit (BACKLOG #1349) Coverage counts are grouped in one pass instead of filtering the row set once per namespace, which walked 3202 rows six times in the checkout measured. The mechanism legend is padded by the format string rather than by hand-counted spaces, so renaming a constant cannot shear the legend away from its table. The push-updated test is hoisted to one named boolean shared by the BEHIND and DIVERGED arms. Co-Authored-By: Claude Opus 5 --- scripts/coord/rescue.ps1 | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/scripts/coord/rescue.ps1 b/scripts/coord/rescue.ps1 index 42dcffbed..e84f7a28f 100644 --- a/scripts/coord/rescue.ps1 +++ b/scripts/coord/rescue.ps1 @@ -320,17 +320,19 @@ foreach ($rec in $records) { $behindOk = ($LASTEXITCODE -eq 0) $ahead = (& git -C $repo rev-list --count "$tip..$commit" 2>$null) $aheadOk = ($LASTEXITCODE -eq 0) + # MECHANISM CHANGES THE SENTENCE, NEVER THE VERDICT, and only where the branch is ALIVE + # -- which is the only place this arm runs. A snapshot behind its branch is behind it + # forever, because nothing re-takes it. A push-updated ref behind its branch is a + # different fact: the hook force-moves it on the next commit. Reporting the two in + # identical words is the collapse this item exists to name. + $lagging = ($mechanism -eq $PUSH_UPDATED) if (-not $behindOk -or -not $aheadOk) { $verdict = 'UNVERIFIABLE' $detail = "git could not compare against $branchName in this clone -- a statement about THIS CLONE, not about the ref" } elseif ($ahead -eq '0') { $verdict = 'BEHIND' - # MECHANISM CHANGES THE SENTENCE, NEVER THE VERDICT. A snapshot behind its branch is - # behind it forever, because nothing re-takes it. A push-updated ref behind its - # branch is a different fact -- the hook force-moves it on the next commit -- and - # reporting the two in identical words is the collapse this item exists to name. - $detail = if ($mechanism -eq $PUSH_UPDATED) { + $detail = if ($lagging) { "$behind commit(s) short of $branchName -- re-pushed as the branch moves, so this may merely be LAGGING" } else { @@ -339,7 +341,7 @@ foreach ($rec in $records) { } else { $verdict = 'DIVERGED' - $detail = if ($mechanism -eq $PUSH_UPDATED) { + $detail = if ($lagging) { "$behind behind / $ahead ahead of $branchName -- re-pushed, so the behind half may merely be LAGGING" } else { @@ -374,13 +376,16 @@ foreach ($rec in $records) { } #: Coverage, per namespace, including the ones that matched nothing. Built before the JSON branch so -#: both outputs report the same thing. +#: both outputs report the same thing. Grouped in ONE pass rather than filtering $rows per namespace, +#: which walked 3187 rows six times in the checkout this was measured on. +$byNamespace = @{} +foreach ($g in ($rows | Group-Object namespace)) { $byNamespace[[string]$g.Name] = $g.Count } $coverage = @() foreach ($p in $namespaces.Keys) { $coverage += [pscustomobject]@{ namespace = $p mechanism = $namespaces[$p] - count = @($rows | Where-Object namespace -eq $p).Count + count = if ($byNamespace.ContainsKey($p)) { $byNamespace[$p] } else { 0 } } } @@ -402,8 +407,10 @@ Write-Host "EXAMINED $($rows.Count) ref(s) across $($coverage.Count) namespace(s foreach ($c in $coverage) { Write-Host (" {0,6} {1,-13} {2}" -f $c.count, $c.mechanism, $c.namespace) } -Write-Host " $SNAPSHOT = a one-time capture. Nothing re-takes it, so staleness here is PERMANENT." -Write-Host " $PUSH_UPDATED = force-moved by the durability hook, so a ref behind a LIVE branch may" +# The mechanism names are padded by the format string, not by hand-counted spaces: renaming a +# constant must not silently shear the legend away from the table above it. +Write-Host (" {0,-13}= a one-time capture. Nothing re-takes it, so staleness here is PERMANENT." -f $SNAPSHOT) +Write-Host (" {0,-13}= force-moved by the durability hook, so a ref behind a LIVE branch may" -f $PUSH_UPDATED) Write-Host " merely be lagging. Once that branch is gone, nothing pushes again and" Write-Host " the two mechanisms are equally final." Write-Host " The counts OVERLAP by construction: remote..fetch remaps refs/tags/rescue/*" From 47e45f9a9598bfe682405f3822f75c7c25d8f95d Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 3 Sep 2026 18:06:12 -0500 Subject: [PATCH 6/7] docs(backlog): record what #1349's remainder landed and what is not work All three writer surfaces the 2026-09-03 score named now carry provenance and the audit reads the namespace it was missing. EXAMINED went from 1671 to 3202 and UNVERIFIABLE with it: widening the audit found more unverifiable refs, it did not repair any, and the two counts moving together is the correct result. Recorded as NOT work so nobody schedules it: the snapshot refs cannot be retrofitted because the information was never captured, and the push-updated ones heal on the next commit to the branch they track. The banner stays open. A builder does not flip it -- the closing act is code, and the LANDER flips the banner on merge. Co-Authored-By: Claude Opus 5 --- docs/BACKLOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index a19e6b606..24574702c 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -16633,6 +16633,7 @@ removed with their sessions. > 🔢 **Filed 2026-08-23 - not started.** ***A rescue ref whose NAME contains a branch name can dereference to an ANCESTOR of that branch rather than its tip, with nothing reporting it.*** **Confirmed instance, re-derived independently by two seats to the file and the insertion: one dated rescue tag is a STRICT ANCESTOR of the branch its name contains -- 75 commits short, `167 files changed, 23410 insertions(+), 1578 deletions(-)`, holding ZERO commits that branch lacks.** ***THE WORK IS NOT AT RISK AND THE FIRST FILING SAID IT WAS.*** *That branch is gone from every WORKTREE, which was read as gone. **It exists as a ref, and the push-updated namespace holds its tip exactly.** The hazard is a reader reaching for the wrong ref and concluding work is lost, or recovering 75 commits short.* > > **Scored 2026-09-03 -> P2.** Value **6/10** · Difficulty **4/10** · _quick win_. Partly shipped, and I re-ran the audit rather than trust the claim. The write-time control is real -- scripts/coord/rescue.ps1:166 writes an annotated tag recording branch, commit, was-tip and instant, scripts/coord/rescue.ps1:276 reads it back after the branch is gone, and tests/test_rescue_ref_provenance.py holds 12 tests over it. What is left is the population it grades: `rescue.ps1 -Check` run in this checkout reports EXAMINED 1671, UNVERIFIABLE 1671, because zero refs under refs/rescue or refs/tags/rescue carry the mefor-rescue-v1 marker defined at scripts/coord/rescue.ps1:83. Three writer surfaces still bypass it -- the post-commit hook force-pushes a bare lightweight tag at scripts/hooks/durability_push.sh:102, scripts/coord/unbacked_check.ps1:459 prints that same un-provenanced push as its remediation, and the audit reads only two namespaces at scripts/coord/rescue.ps1:111 while 1455 refs sit under refs/remotes/private/rescuetags. Landing the remainder means a provenance design for an sh hook that must never fail a commit, pointing the printed remedy at -Anchor, widening the audit, and Windows-gated tests for each. +> 🚧 **The remainder is built and open in a PR, 2026-09-03. All three writer surfaces the score named now carry provenance, and the audit reads the namespace it was missing.** Landed: (1) `scripts/hooks/durability_push.sh` builds an annotated tag object carrying the same `mefor-rescue-v1` message `-Anchor` writes and pushes it BY OBJECT ID, so no local ref exists for `git push --follow-tags` to carry to the public remote; (2) `unbacked_check.ps1`'s printed remedy runs `-Anchor` and pushes what it wrote, instead of the bare form that produced the unverifiable population; (3) `rescue.ps1 -Check` enumerates remote-tracking prefixes from `git remote` and grades them as a separate MECHANISM -- a snapshot behind its branch is behind it forever, a push-updated ref behind a LIVE branch may merely be lagging, and mechanism changes the wording of BEHIND and DIVERGED while changing no verdict; (4) 12 new tests across `tests/test_durability_hook_provenance.py`, `tests/test_rescue_ref_provenance.py` and `tests/test_coord_unbacked_check.py`, the hook arms ordered durability-first because a post-commit hook that fails a commit is a worse defect than one that records nothing. **Re-measured: EXAMINED went from 1671 to 3202, and UNVERIFIABLE with it** -- widening the audit found more unverifiable refs, it did not fix any, and the two counts moving together is the correct result. **NOT work, and recorded so nobody schedules it:** the 1671 snapshot refs cannot be retrofitted, because the information was never captured; the 1531 push-updated ones heal on the next commit to the branch they track, so that count falls with activity rather than with time. **A grep of `scripts/`, `.github/` and `ide/` finds no fourth writer surface.** ***Left open deliberately: the banner flip is the LANDER's on merge, not the builder's.*** > Verdict: build > Research: none > Closing-act: code From bd67c670d9f13cc5e23146dea6c6057b22b5d012 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Fri, 4 Sep 2026 03:48:01 -0500 Subject: [PATCH 7/7] test(tooling): classify the new harness test in the manifest tests/test_tooling_partition.py::test_every_non_engine_test_is_classified reds all three required test legs when a test that does not import the engine is absent from tests/tooling_manifest.txt. This PR adds such a test, so CI could not go green as it stood. The manifest is read as a set, so this is a single inserted line at its alphabetical slot; no existing line moves. --- tests/tooling_manifest.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/tooling_manifest.txt b/tests/tooling_manifest.txt index bb5029a6f..f91584d4e 100644 --- a/tests/tooling_manifest.txt +++ b/tests/tooling_manifest.txt @@ -50,6 +50,7 @@ tests/test_ci_tooling_gate.py tests/test_ci_venv_pinning.py tests/test_citation_line_check.py tests/test_claim_check.py +tests/test_durability_hook_provenance.py tests/test_gate_ci_mirror_parity.py tests/test_hook_prose_folding.py tests/test_hook_prose_folding_push_ledger.py