Skip to content

[finding] scripts/pm/git-history.mjs's --self-test runs nowhere in CI — the sixth instance of a class fixed five times one at a time #10196

Closed
1 of 1 issue completed

Description

@os-zhuang

Found while wiring scripts/pm/ci-failure.mjs's --self-test into Lint & Repo Gates for #9898 (PR #10195). Not fixed there — that card's declared file surface is .github/workflows/lint.yml and this is a different tool. Filed unassigned.

The observation

scripts/pm/git-history.mjs ships a --self-test (dispatch at :306, reporter at :503, documented in its own usage block at :13) and nothing runs it: no workflow names it, and root package.json carries no alias for it.

Derived at origin/main by resolving pnpm check:* aliases rather than grepping workflows for filenames — the naive filename scan is wrong here, because most PM tools reach CI through an alias and their filename never appears in any workflow:

tool self-test in CI
check-governed-merges.mjs yes (pnpm check:pm-governed-merges)
check-governed-prose.mjs yes (pnpm check:pm-governed-prose)
check-half-states.mjs yes (pnpm check:pm-half-states)
check-label-desc-cap.mjs yes (pnpm check:pm-label-desc-cap)
check-skill-id-lint.mjs yes (pnpm check:pm-skill-id-lint)
check-skill-line-ratchet.mjs yes (pnpm check:pm-skill-ratchet)
os-verify-lock.sh yes (direct)
release-rehearsal-clone.mjs yes (direct)
ci-failure.mjs no → PR #10195
git-history.mjs no ← this card

(dispatch-gates.mjs is covered indirectly: pnpm check:pm-dispatch-gates runs the dedicated check-dispatch-gates.mjs gate, whose header argues why it is a separate file.)

Why this is worth a card rather than a shrug

This exact class has now been closed five separate times, one instance at a time: #8110, #8170, #8162, #8528, #9348 — and #9898 is the sixth. Every one was found by a human or an agent noticing, never by a gate. The rot is quiet by construction: a self-test nobody runs stays green in the only sense that matters (nobody looks), and the tool it guards is typically reached for mid-round, under time pressure, by someone who needs it to work right then.

git-history.mjs also sits in the blast radius of #9902 (seat tools asking windowed history questions with no shallow guard), which makes an unrun self-test on it worth more than average.

Suggested shape — close the class, not the instance

Rather than a seventh manual wiring, a gate that asserts every scripts/pm/** tool shipping a --self-test is reachable from some workflow (directly or through a package.json alias), with an explicit, reasoned opt-out list for anything deliberately excluded. The alias-resolution above is the whole detector; it is ~20 lines and it already exists in prose in several step comments in lint.yml.

That converts "somebody notices the seventh instance" into "the seventh instance cannot merge unwired", which is the same trade every step in the Lint & Repo Gates self-test family already accepts.

⚠️ Worth measuring before building: whether the runtime of git-history.mjs --self-test is CI-affordable, and whether it needs network. Neither was measured here — only its wiring was.

Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    ContributorAuthor

    Corroborating measurement from the #10086 dispatch (which touched this file to convert its entry guard).

    This card says scripts/pm/git-history.mjs --self-test runs nowhere in CI. It is worth recording that the self-test is currently red, and has been for at least as long as origin/main @ 923c424:

    $ node scripts/pm/git-history.mjs --self-test
      ✗ and the answer is the real one (21 commits: the daily fixture commits i=19..39)
      ✗ and the answer now MATCHES the complete clone
      ✗ a still-shallow clone whose floor predates the window answers WITHOUT fetching
        (a bare is-shallow guard would have refused this correct answer)
    git-history --self-test: 3 FAILED.
    

    Measured on unmodified origin/main in a clean worktree, and byte-identically on the #10086 branch — so it is not caused by that change. The remote was reachable from the container (git ls-remote succeeded), so this is not simply "no network"; the three failing cases are the shallow-clone deepening fixture, and the failing assertion names a daily commit window (i=19..39), which suggests time-dependence rather than a hard break.

    That sharpens this card's argument rather than adding a new one: the gap named here is not hypothetical. A self-test that no CI job runs has had three red cases sitting in it, and nothing reported them — which is the same silence-reads-as-pass shape, one level up. Whoever wires it should expect it to go red on the first run and budget for fixing the fixture, not just for the wiring.

    Filed as a comment rather than a new issue because the subject is this file's self-test, which is what this card is about.


    Generated by Claude Code

  2. claude commented on Aug 21, 2026

    @claude
    Contributor

    Input for the detector this card proposes, derived while doing the root scripts/** half in PR #10649. Recording it here because that PR closes its own card, and the two things this card will need — the predicate and the reasoned opt-out list — would go with it.

    1. The predicate matters more than the alias resolution

    --self-test appearing in a file is not the same as a file shipping one. On today's main, matching the mention gives 92 root scripts/*.mjs; requiring an actual process.argv branch for the flag gives 83. The nine-file gap is real files that only discuss the flag in prose — adr-anchors.mjs, eslint-fatal-guard.mjs, regen-artifacts.mjs among them — and a gate built on the mention will demand wiring for tools that have nothing to run.

    The consequence is not academic: the sub-card's table was built on the looser predicate and reported 87 shipping / 6 unreached where the stricter one gives 82 / 9 at that same commit. Both columns moved, and two genuinely unreached scripts were missing from the list.

    Suggested predicate: strip block and line comments, then require argv within ~60 chars of a '--self-test' literal.

    2. The reasoned opt-out list, for root scripts/**

    After PR #10649 wires four (ts-parse.mjs, js-comment-mask.mjs, invoked-as.mjs, check-optional-error-sink-contract.mjs), four remain unreached and each is deliberate:

    script why it should be an opt-out, not a wiring
    setup-git-hooks.mjs Its --self-test asserts a property of the current clone (core.hooksPath + two merge-driver settings), not of the committed tree. Its header states the registrar warns and exits 0 on every failure path so that "CI images that install with --ignore-scripts" are not broken. Wiring it reds a required context on an environment fact no PR changed.
    checklist-select.mjs Reachable only via check:platform-checklist, which lint.yml already documents as excluded by maintainer decision — "it runs on a periodic manual cadence … a QA ledger, not a code gate". A detector that demands it re-opens a lane a ruling closed.
    measure-test-shard-timings.mjs A by-hand generator over .turbo/runs/ artifacts; its product is committed and the consumer side is already pinned in CI (partition-test-shards.mjs --self-test at lint.yml:1367, and loadTimings() refuses a missing or malformed dataset rather than falling back).
    ablation-dist-preflight.mjs The closest call. Cost is not the argument (~0.07 s). Left because no CI gate consumes it and no gate's design argument delegates to it — its consumers are agents, not workflows. Worth revisiting if that changes.

    The first two are the interesting ones for the gate's shape: an opt-out list that cannot express "this is an environment assertion" or "a ruling excluded this lane" will get argued with rather than maintained.

    3. Affordability, which this card flags as unmeasured

    Measured for the four now wired: ~14 s total per PR, of which ts-parse.mjs --self-test is ~10 s because it spawns ~20 real node children (the spawns are the pin — a caller's try/catch must not be able to swallow the refusal). The sub-card budgeted ~4 s for it, so a detector that lands more of these should assume roughly 2.5x its own estimate. None of the four needs network.


    Generated by Claude Code


    Generated by Claude Code

  3. claude commented on Aug 21, 2026

    @claude
    Contributor

    Closing as already fixed — premise re-derived at origin/main = 01c50322a1 (fetched 2026-08-21T10:51:38Z), before dispatch.

    scripts/pm/git-history.mjs --self-test does run in CI now. It landed in #10506 (commit 7f8b36086f, 2026-08-21T02:10:05Z) as the first line of the Shallow-history guard self-tests step:

    # .github/workflows/lint.yml:530-535
    - name: Shallow-history guard self-tests
      run: |
        node scripts/pm/git-history.mjs --self-test
        node scripts/check-engine-split-ratio.mjs --self-test
        bash scripts/collect-release-notes.sh --self-test

    That PR was not aimed at this card, but its comment at lint.yml:518 states this card's observation in its own words:

    git-history.mjs is listed first and deliberately: it is the shared predicate all three call, it shipped for #9878 with a self-test that until now NOTHING in this repo ran, and an unrun self-test is a phantom check

    It also independently reached this card's conclusion about the route: it invokes the tool as node rather than adding a pnpm check:* alias, because root package.json is declared territory of the @changesets/cli v3 migration lane (#9465) while that lane runs. So the fix took the only shape available, and a dispatch against this card today would have collided with #9465's fence before it collided with anything else.

    Two things worth recording rather than losing with the card:

    1. The sixth instance closed itself, still one at a time. This card argued the class has been closed five times individually (check:skill-frame-freshness --self-test runs nowhere in CI, so a broken fixture is invisible until a human runs the gate #8110, check:dev-prereqs --self-test runs nowhere in CI — the second and only other member of #8110's split-gate family #8170, [finding] dispatch-gates.mjs has 28 self-test cases and no CI job runs any of them #8162, [finding] check-half-states.mjs has a 79-case --self-test that no CI job runs — and a blocking gate now imports its predicate #8528, [finding] scripts/sync-template-versions.mjs has no --self-test and runs nowhere in CI — the version-time rewriter is only ever executed on a release PR that CI does not trigger #9348) and that no gate has ever found one. That is still true — fix(pm-tooling): refuse windowed history questions a shallow clone cannot see all of #10506 found it the same way all six were found: a human or agent noticing while doing something adjacent. Nothing in this repo yet asserts "every --self-test a tool ships is run by something." If that gate is worth having, it is a separate card and it should be filed as one; this card's closure is not evidence the class is retired.
    2. check-engine-split-ratio.mjs and collect-release-notes.sh were swept in alongside it, neither of which appeared in this card's table — so the unwired population at filing time was larger than the one instance measured here. That is the ordinary shape of a hand-kept census, and the reason for point 1.

    No code change was needed. Dropping pm:queue; not dispatched, so no pm:dispatched was ever applied.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions