Skip to content

--diff-filter=AM lets a RENAMED changeset carry a violation past check-empty-changeset and check-adr-0087-registration #7045

Description

@os-project-manager

Found while diff-scoping check-changeset-no-major.mjs (#7005). Not fixed there — that PR closed the hole in its own file only, because each sibling's fixtures and messages are its own. Filed per Prime Directive #10. Unclaimed.

The observation

Both scripts/check-empty-changeset.mjs:229 and scripts/check-adr-0087-registration.mjs:1056 enumerate the diff with:

git diff --name-status --diff-filter=AM <from> <head> -- '.changeset/*.md'

Git's rename detection is on by default (diff.renames, since git 2.9), so a changeset that is renamed and modified in the same commit is reported with status R, which --diff-filter=AM drops. Neither gate sees the file at all.

Measured

git 2.43.0, temp repo, one changeset renamed and its frontmatter changed in one commit:

--- default (rename detection on) ---
R075	.changeset/old.md	.changeset/new.md

--- with --diff-filter=AM ---
(empty)

Same fixture shape, run against check-changeset-no-major.mjs before its own fix: 0 violations where the correct answer is 1.

The consequence in each gate

Unlike #7005 this one is live today: both gates run on every PR right now, neither is behind a pre-mode exemption.

Impact, stated honestly

  • No known instance. Changeset filenames are generated by pnpm changeset and essentially never renamed by hand, so this is not something an author trips over by accident — it is a bypass that requires doing an unusual thing.
  • It is not dormant drift either: the code path runs on every PR, and the bypass is one git mv away for anyone who wants it.
  • Both gates are advisory-shaped (they protect the release train, not a security boundary), which is the honest reason this is filed rather than hot-fixed.

The fix, already written once

#7005 / PR #7048 closes the same hole in the third member of the family, and the change is small enough to quote in full:

  1. --diff-filter=AMR instead of AM;
  2. R is R<score>\t<old path>\t<new path>, so the head path is field 3 and the branch-point path is field 2 — read the base side at the pre-rename name;
  3. everything else is unchanged, and a pure rename then compares equal and stays exempt (no false red for moving a stock file).

Both siblings already compare head-vs-base per file, so step 2 is the only real work. The fixture wants a control asserting git really reported R — a body long enough to score as a rename, otherwise the case silently degrades to add-plus-delete and the test passes for the wrong reason.

Related: #7005 (the same hole in the third parser, fixed), #7004 (the family's shared entry regex), #6129 (why all three are diff-scoped at all).

Activity

  1. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue + domain:devx.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    CollaboratorAuthor

    Claim — devx PM seat, session session_01F8q5J1MQyocgtNspb15fSn, branch claude/issue-7045-diff-filter-amr-siblings.

    Held out of dispatch until #7005 / PR #7048 landed (same gate family, and this card's prescribed fix is that PR's pattern). #7048 merged to main as 0bffdaeae, so the reference implementation now exists in-tree and this is released for dispatch.

    ⚠️ Line numbers re-verified against current origin/main before dispatch — both the body's and triage's are stale, in opposite directions:

    site body says triage says (@ 2f3e793) current main
    scripts/check-empty-changeset.mjs 229 247 247 ✅
    scripts/check-adr-0087-registration.mjs 1056 1067 1307 ⚠️

    Both sites do carry --diff-filter=AM exactly as described, so the finding itself stands unchanged — only the coordinates moved. The implementer is told to locate by content, not by line number.

    Also confirmed in passing, since it bears on dispatch rather than on the fix: .github/CODEOWNERS routes scripts/check-adr-**merge-approval**.mjs to the maintainer, not check-adr-**0087-registration**.mjs. Different scripts — this card is not a governance change and does not need code-owner review.

    If an earlier claim comment with a different session ID exists above this one, that claim wins and this one should be treated as withdrawn.


    Generated by Claude Code

  3. os-project-manager commented on Aug 9, 2026

    @os-project-manager
    CollaboratorAuthor

    Landed on main as 424316b7f via PR #7106. Card auto-closed on the Fixes line; clearing the stale pm:dispatched.

    Accepted after verifying the gates myself. Workflow-run conclusions on head 1026000d8: Lint & Type Check = success, CI = success, ADR Merge Approval = success, remainder skipped, zero non-green. Artifact re-read from origin/main after the merge: --diff-filter=AMR present in both siblings.

    The rename control the brief demanded was delivered and is the part that makes this trustworthy. Every new case asserts the real R row against raw git diff --name-status before asserting any verdict, via a helper matching /^R\d+\told\tnew$/. Standalone measurement on git 2.43.0: R077 .changeset/old.md .changeset/new.md under default rename detection, with --diff-filter=AM on the identical diff printing nothing. End-to-end in one temp repo, same commit, two script versions: origin/main's script said "✓ No empty-frontmatter changeset introduced by this diff" and exited 0; this branch's said "This PR adds an empty-frontmatter changeset … (the branch-point path is .changeset/was-declaring.md)" and exited 1. That is the bypass closing, demonstrated rather than asserted.

    One scope addition, accepted, and it is the interesting part of this PR. The implementer added an isChangesetFile(basePath) guard on the base-side read in both gates — not in the card's three prescribed steps. The justification is a measurement, not a hunch: git pairs renames by content, not by name, so under the .changeset/*.md pathspec two identically-bodied files paired as R100 .changeset/README.md .changeset/renamed-from-docs.md. Without the guard, the AMR widening would have opened a new free exemption (inherit from README, which declares nothing by definition) while closing the old one. Each gate carries a dedicated fixture that goes green when the guard is deleted. Widening a filter and accidentally granting a new exemption is exactly the failure this card was about, so catching it inside the same change was the right call.

    Also worth keeping, the report's honest note on its own reverse-verification: reverting AMR does not falsify the "read field 2" claim — under AM there is no R row to misread, so the RED cases stay green through that mistake. What actually pins field 2 is the pure-rename case in each gate, where reading the base at the head path resolves to nothing and turns a legitimate move into a fabricated violation. An ablation that only proves the thing you already believed is not a proof.

    Deviations accepted: no changeset (scripts/ belongs to the private root workspace and ships nowhere; skip-changeset applied and read back, same as PR #7048 in this family), and the PR body in English per the repo's own ruling 「issue 和 PR 必须用英文」 (#6692) — the repo rule outranks a dispatch prompt, and the implementer was right to say so.

    ⛔ Out-of-scope finding filed unassigned as #7107: the third sibling check-changeset-no-major.mjs has no isChangesetFile(basePath) guard, so a rename paired with README could subtract phantom majors. Dormant today (the real README has no frontmatter fence, so nothing is subtracted). Not graded here — triage's call.


    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

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions