fix(#2914): per-PR ack fragments instead of one shared mutable file (#2923)

* fix(#2914): never persist a spent emitted-drift ack on next

tests/emitted-drift-ack.json held 34 spent #2834 entries merged via #2900.
Every entry is scoped to the diff that introduced it (#2789), so once merged
to next it is at the base by definition -- spent and inert. Its presence is
still load-bearing though: each PR rewrites the paths map wholesale, making a
persistent base copy a shared cell. Five of six conflicting PRs in the open
queue collided on this file and nothing else.

Deletes the stale document and adds a push-to-next guard asserting it stays
absent. The guard is deliberately NOT wired into lint:ci -- a PR-lane check
against the base is the #2768 shape #2789 exists to end.

Closes #2914

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(#2914): backfill changeset pr number

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(#2914): per-PR ack fragments instead of one shared mutable file

The emitted-drift acknowledgment lived in a single tests/emitted-drift-ack.json
whose paths map every PR rewrote wholesale. That is a shared mutable cell: any
two PRs needing an ack edit the same lines and conflict. Five of six conflicting
PRs in the open queue collided on this file and nothing else.

Acks now live as per-PR fragments under tests/emitted-drift-acks/, the same
shape .changeset/ already uses to solve this exact problem. Two PRs pick
different filenames, so they cannot collide, and fragments lingering on next
are harmless rather than toxic.

The legacy file's 35 entries are MIGRATED into a fragment, not deleted. An
earlier delete-only attempt failed verification twice: the ratchet lost the
spec-phase.md acknowledgment from #2779 and reported a 10-byte growth with no
ack. Relocating preserves every acknowledgment.

The legacy single file is still READ (unioned with the fragments) because five
open PRs carry it; dropping support would break all of them. A duplicate path
key across sources is a hard error, never last-wins.

The push-to-next guard is retargeted accordingly: it now asserts only that the
legacy SHARED file never reappears on next. Fragments may persist harmlessly.

Closes #2914

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-31 13:15:29 -04:00
committed by GitHub
parent d2d2f7c088
commit c043f2946c
9 changed files with 1287 additions and 62 deletions

View File

@@ -773,23 +773,50 @@ what your PR changed against `next` and requires every emitted-artifact hash tha
to be attributable to your diff. If it is not, the check fails and names the paths.
Legitimate cases where emitted bytes move for a reason your diff cannot show directly —
a converter change, for example — go through `tests/emitted-drift-ack.json` (name the
path, say why); see `CONTEXT.md`'s `### Emitted Artifact Provenance` entry for the full
model. Growth in a `gsd-core/workflows/*.md` or `agents/gsd-*.md` file is reported with
its exact byte delta and needs the same acknowledgment; the outer tier hard caps in
a converter change, for example — go through a **per-PR fragment** under
`tests/emitted-drift-acks/` (#2914; name the path, say why); see `CONTEXT.md`'s
`### Emitted Artifact Provenance` entry for the full model. Growth in a
`gsd-core/workflows/*.md` or `agents/gsd-*.md` file is reported with its exact byte delta
and needs the same acknowledgment; the outer tier hard caps in
`tests/workflow-size-budget.test.cjs` / `tests/agent-size-budget.test.cjs` are unaffected
and still apply.
and still apply. The legacy single `tests/emitted-drift-ack.json` is still read and
unioned in for any branch that still carries it, but new acknowledgments go in a NEW
fragment, never that file.
You do not need to memorize any of this. **The failure output names its own remedy** — it
tells you the file to create, that it does not exist yet, which key to use, and prints a
minimal valid document you can paste. Note the two key spaces, because the message says
which one applies: an unattributable **hash** ripple is keyed on the emitted path
tells you to create a new fragment under `tests/emitted-drift-acks/` (with a name nobody
else is using — include your issue or PR number), which key to use, and prints a minimal
valid document you can paste. Note the two key spaces, because the message says which one
applies: an unattributable **hash** ripple is keyed on the emitted path
(`skills/gsd-add-tests/SKILL.md`), while **growth** is keyed on the bare filename as it
appears under `gsd-core/workflows/` or `agents/` (`explore.md`). When you remove the last
entry from `tests/emitted-drift-ack.json`, delete the file too — its presence is the
alarm, so an empty one signals nothing. Nothing here is regenerated: if you find yourself
looking for a baseline file to re-run a generator over, that file was deleted by #2724 and
is not coming back.
entry from your fragment, delete the fragment file too — its presence is the alarm, so an
empty one signals nothing. Nothing here is regenerated: if you find yourself looking for a
baseline file to re-run a generator over, that file was deleted by #2724 and is not coming
back.
**Why fragments, not one file (#2914):** every PR needing an acknowledgment used to
rewrite `tests/emitted-drift-ack.json`'s `paths` map wholesale — a single shared mutable
file every such PR touches guarantees a merge conflict between any two of them (5 of 6
conflicting PRs in one open queue collided on this file and nothing else), and it means
spent, already-merged entries pile up on `next`. A fragment per PR — the same shape
`.changeset/` already uses for the identical problem — means two PRs can never conflict on
this seam again, and a fragment left on `next` after merge is inert rather than a shared
cell. Two ack sources (two fragments, or a fragment and the legacy file) may **never** name
the same path; that is a hard, loudly-reported error, not a silent last-wins.
`tests/emitted-drift-ack.json` (the legacy single file, specifically — NOT the fragment
directory) must never persist on `next` (#2914): every entry is scoped to the diff that
introduced it, so once merged it is, by definition, already at the base — spent and inert,
regardless of shape, and its persistence is what makes it a shared merge-conflict cell. A
fragment persisting on `next` is harmless, since fragments are independently named and
cannot conflict with anything, so this guard is deliberately scoped to the legacy file
alone. This is enforced only on `next` itself, by the `guard-no-ack-on-next` job in
`.github/workflows/test.yml` (push-to-`next` trigger,
`scripts/lint-emitted-drift-ack.cjs --guard-next`), never as a PR-lane check — a PR-lane
"base ack must be absent" check would red every open PR the moment one landed (the #2768
shape #2789 exists to prevent). If you ever see the legacy file present on `next`, delete
it; do not try to make it well-formed.
`npm run regen:derived` still exists for the artifacts that ARE committed and derived —
`sync-manifest-versions`, the ADR index, the capability matrix, the inventory manifest,
@@ -929,8 +956,9 @@ gsd-core/
Per-file growth is caught by the differential
attribution check (tests/emitted-attribution.test.cjs,
ADR-2719) — it reports the exact byte delta and
requires an entry in tests/emitted-drift-ack.json,
no committed snapshot to regenerate. Loose tier
requires a per-PR fragment in
tests/emitted-drift-acks/ (#2914), no committed
snapshot to regenerate. Loose tier
hard caps remain in tests/workflow-size-budget.test.cjs.
The same applies to agent files (agents/gsd-*.md,
tests/agent-size-budget.test.cjs). Full how-to +