Uh oh!
There was an error while loading. Please reload this page.
fix(pm-tooling): refuse windowed history questions a shallow clone cannot see all of - #10506
Merged
Merged
Conversation
…ee all of Three seat-run tools asked `--since` questions over a branch with no shallow guard. A shallow clone answers those from whatever part of the window is present, exits 0 and prints no warning, so each returned a confidently wrong smaller number. Agent containers clone shallow and all three run from a seat; every workflow that reads history already checks out `fetch-depth: 0`, which is why the exposure survived. - git-history.mjs: add `historyHorizon()`, the read-only half of the #9878 helper — one predicate for all adopters, never fetching, and printing a deepen command computed so it can only ADD history. - check-governed-merges.mjs: a repo whose history stops inside the window is UNAUDITED, like an absent checkout. It rendered `clean window` over ~40 governed merges. - check-engine-split-ratio.mjs: refuse rather than print an ADR trigger metric over a short denominator; annotate the horizon when the window is whole. - collect-release-notes.sh: withhold the cloud section rather than print it short, and exit 2. - lint.yml: run the three self-tests, including git-history's, which nothing in this repo ran until now. Part of #9902 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
… checked Two defects found by ablating the guards rather than by reading them. - check-engine-split-ratio: the horizon line spelled "(predates the window)" unconditionally, so removing the refusal above it produced a false statement beside a wrong number instead of no statement. - collect-release-notes --self-test: the wiring pin grepped the script for a literal that also occurs in the assertion itself, so it matched its own source line and printed a tick with section 4 unwired. The needle is now assembled from two adjacent literals, and the ablation reddens it. Part of #9902 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
This was referenced Aug 21, 2026
os-zhuang
marked this pull request as ready for review
August 21, 2026 02:09
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#9902
Three seat-run tools asked windowed history questions (
--sinceover a branch) with no shallow guard. A shallow clone answers those from whatever part of the window happens to be present, exits 0, and prints no warning — so each returned a confidently wrong smaller number. Agent containers clone shallow, and all three run from a seat: every workflow that reads history already checks out withfetch-depth: 0, which is exactly why the exposure survived. CI is not where these run.The census, re-derived (not taken from the card)
A mechanical sweep of every windowed history question in
scripts/**+.github/workflows/**, classified by whether its file carries any horizon guard, run againstorigin/main(a718ee3) and against this branch:origin/mainThe three it names are the three the card named, at the same
file:line— independently re-derived rather than assumed:scripts/pm/check-governed-merges.mjs:472scripts/check-engine-split-ratio.mjs:63scripts/collect-release-notes.sh:120Positive control for the zero. A fourth, synthetic, unguarded windowed query injected into
scripts/check-role-word.mjs(a file with none) is found: 4/6. So the post-fix zero is a real zero and not a blind detector. The detector also had to be widened once mid-census: written to require a git verb on the same line it missedcollect-release-notes.sh:120, where the window is passed to a shell helper (print_log_split "$CLOUD_ROOT" --since=…) and thelogsits in the function. That is how a hand census loses a call site.Classes deliberately left alone. Range/
merge-basequestions (A..B) fail loudly in a shallow clone — a missing endpoint isfatal: bad object. Unwindowed path history (git log -- <path>) is the other silent class, and the two live instances are already guarded:check-adr-0087-registration.mjsrefuses its ledger-touch signal when--is-shallow-repository(its header records 91 of 92 rows wrong before that), andrelease-rehearsal-clone.mjsexists for that shape.qa-rollup.mjsis three-valued for the same reason.dispatch-gates.mjsis NOT in the affected set (asked explicitly). ItschangedPathsFromGitreadsgit merge-base, and its docblock records the measurement: on a shallow checkout whose base is below the graft,merge-baseexits 1 with empty output and refuses the two-dot fallback with a deepen remedy. It cannot lie here, only refuse.The harm, reproduced in this shallow container (floor
e7c0d5ef7d@ 2026-06-02, tip 2026-08-21)1.
check-engine-split-ratio --days 90— the workflow's own invocation. Its window reaches 2026-05-23, ten days below the floor:The truth for that window is 282. GitHub lists 18 distinct commits touching
engine.ts/registry.tsbetween 2026-05-23 and the floor; the clone sees exactly one commit dated in that slice, and it is the graft boundary commit itself. A graft commit carries the whole tree, so it "touches" both engine-core files and everything outsidepackages/objectql/— a truncated window does not merely lose data points, it fabricates one, and that one lands in the cross-package numerator.2.
check-governed-mergesover a window crossing the same floor, rendered by the tool's ownrenderReport:GitHub lists 38 commits touching
docs/adr/**and 13 touchingAGENTS.mdin that same window. The green tick printed over ~40 governed merges, under a line claiming the repo was✓ audited.3.
collect-release-notes.shsection 4, on a depth-5 fixture whose true answer is 21 commits: the shallow clone printed 5, exit 0, under the same### feat / fixheading. Sixteen release-note items missing, nothing to indicate it.The shape: refuse when truncated, annotate when not — argued from each consumer
Not one shape for all three; the predicate is shared, the verdict is not.
check-governed-merges→ UNAUDITED, via the exit-2 channel it already has. Under-enumeration here reads as compliance, the one direction this list must never be wrong in. The tool already holds that "incomplete must never read as clean" (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690) for an absent checkout; a history that stops inside the window is the same fact wearing different clothes — nothing was looked at — so it reuses that channel and adds no new exit code. A warning was rejected outright: a caveat printed beside✅ clean windowis still a green tick.check-engine-split-ratio→ refuse, exit 2 (cannot compute), never exit 1. It is an ADR trigger metric read to decide whether a repo split may proceed. A ratio printed beside a caveat is still a ratio and gets quoted without the caveat; a refusal cannot be quoted at all.2and not1, so "I could not measure" is never confusable with "the engine is not separable". Itstotal === 0 ? 0case is now loud for the same reason (check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690).collect-release-notes.sh→ withhold the section, and exit 2. The consumer is a human transcribing a list; a short list is indistinguishable from a quiet release, while an absent one cannot be transcribed by accident. The exit code exists because… > material.mdsucceeding is otherwise the only signal a pipeline gets, and it lies.None of the three deepens.
check-governed-mergessweeps four sibling checkouts andcollect-release-notesreads the release engineer'scloudtree; a tool must not move ground it does not own, least of all mid-audit. The remedy is printed as a command instead — and computed bychooseDeepenSince(), so the--shallow-sinceit prints can never be newer than the floor already present. The naive spelling is measured ingit-history.mjs's header shortening a clone by 1380 commits at exit 0, which would make the remedy a second instance of the defect.Shared helper vs per-call-site guard — the ruling, and its boundary
Shared predicate, per-call-site verdict.
The drift objection ("a helper is a second source of truth") bites when a helper duplicates knowledge the call sites also hold. Here they hold none: they import
historyHorizon()and render their own words, so there is exactly one implementation of the predicate and one self-test over real git fixtures. Three hand-rolled guards would be three implementations of a predicate that is measurably easy to get wrong in two independent ways, both already recorded ingit-history.mjs's header from #9878:--is-shallow-repositoryis the wrong predicate — a shallow clone whose floor predates the window answers exactly, and a guard refusing that refuses correct answers and trains bypass. The question is whether the floor predates the window.fetch --shallow-sinceis not monotonic; it shortens too.A copy that misses either is green and wrong, which is the same failure the card is about. The helper also already existed and was merged (#9878) — writing three fresh guards beside it would have been the second source of truth, not the first.
Boundary of the ruling: the refusal wording and exit-code mapping stay per call site, because what a refusal means is consumer-specific — an UNAUDITED repo, an unmeasurable metric, an incomplete release-notes section are three different facts. Sharing the predicate; not sharing the verdict.
collect-release-notes.shis bash and reaches the same predicate through the helper'sensure --no-fetchCLI — a process boundary, not a copy.Ablations — and the two defects they found
Every guard was removed, exercised, and restored, with the mutation confirmed on disk (marker counts before/after) rather than by an editor's exit code.
check-engine-split-ratio(real container,--days 90)265/98.1%, exit 0 — the pre-change tool, exactlycheck-governed-merges(shallow fixture; truth 19)✓ audited, exit 0collect-release-noteswiring pinTwo things the ablations caught that reading the code did not:
collect-release-noteswiring pin could not fail. It grepped the script for a literal that also occurs in the assertion itself, so it matched its own source line and passed with section 4 unwired. The needle is now assembled from two adjacent literals so it cannot match its own line, and the ablation reddens it. A pin that cannot fail is worse than no pin, because it is believed.(predates the window)was spelled unconditionally, so removing the refusal above it produced a false statement beside the wrong number rather than no statement. It is now conditional oncovered.git-history.mjswas wired to nothingGrep for it across the repo before this PR: zero references — no
package.jsonscript, no workflow, no skill. The remedy #9878 shipped had a self-test that nothing in this repo ever ran, which is the@ts-expect-error-in-an-uncompiled-file shape from AGENTS.md wearing a different hat.lint.ymlnow runs it, plus the two new adopter self-tests (check-governed-merges's cases run in the existingpnpm check:pm-governed-mergesstep). Invoked asnode/bash, not apnpm check:*alias: that alias lives in rootpackage.json, fenced by #9465 — same precedent as the release-rehearsal step directly above it.Gates
Derived with
node scripts/pm/dispatch-gates.mjs(no path arguments) on the final commit2d137b1abf; 12 families matched, all run at that sha:check:node-versionNode 22 is in maintenance; supported until 2027-04-30check:cross-package-test-inputsAll 60 self-test cases passed./OK: 12 package(s) read outside themselves, all declaredcheck:pm-governed-merges✓ check-governed-merges --self-test: 90 assertionscheck:required-contexts✓ check-required-contexts --self-test: 124 assertions+ live run OKcheck:shard-attestation✓ …: 92 assertions/OK: 2 aggregate gate(s) count 3 declared leg(s)check:type-check-coverageOK — 64/77 workspace packages type-checkedcheck:workflow-status-functions✓ …: 34 assertions/OK (scanned 26 workflow file(s))node scripts/check-engine-split-ratio.mjs--self-test: all cases passed(this is whatlint.ymlruns)check:nul-bytesOK (scanned 6146 text file(s) … no raw ASCII control bytes)Two declared narrowings, both stated rather than skipped:
check:type-check-debtrefuses locally —--re-measure cannot run: 55 workspace dependenc(ies) … have no built type entry point on disk, its own documented prerequisite.lint.ymlbuilds the closure before this step; this diff contains zero TypeScript, so no reading it produces can move. Not run locally.node scripts/check-engine-split-ratio.mjs --days 90asengine-split-metric.ymlruns it exits 2 in this container by design — it is a shallow clone and that is the whole point of the change. The CI shape (a checkout that can see the window) is pinned by the self-test's full-clone leg:a complete clone computes the metric, exit 0/the ratio is the fixture's real 50.0% over all 40 commits.Not done here, deliberately
scripts/**+.github/workflows/lint.yml— no published package, nothing to release.skip-changesetapplied by publish surface, not by default.git-history.mjsis still undiscoverable from the instruction files: a seat asking "how many commits landed this month" has no reason to find it. The natural homes — AGENTS.md's guard index and.claude/skills/pm-dispatch/SKILL.md's tool table (which already listscheck-governed-merges.mjs) — are governed surfaces, human-merge-only, and never a rider on a code PR. Filed for a maintainer to route.Generated by Claude Code