fix(ship-receipt): carry masks only package.json version on a pin-bump PR (GST-1030, GBEV-3)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-02 22:30:45 +00:00
1 parent 666efd35ad
commit 5923767db7
3 files changed
+41 -1

No files matched your search

+10
View File
@@ -675,6 +675,16 @@ function cmdCarry(rest: string[], json: boolean): number {
if (changedProduct.length >= CARRY_NAMED_PATHS) break;
const cls = changed.find(c => c.path === p)?.class ?? classifyCarryPath(p, rules.extra);
if (cls === 'metadata' && spec.ownRows.includes(p) && !movedBase.has(p)) stampOnly.push(p);
if (cls === 'metadata' && spec.demoted.includes(p) && movedBase.has(p) && /(^|\/)package\.json$/.test(p)) {
const masked = Object.values(sides).map(ref => {
const r = spawnSync('git', ['show', `${ref}:${p}`], { cwd: repoRoot, encoding: 'utf8', timeout: 30_000, maxBuffer: 16 * 1024 * 1024 });
try { const { version: _, ...rest } = JSON.parse(r.status === 0 ? r.stdout : 'null') ?? {}; return JSON.stringify(rest); } catch { return null; }
});
const [rb, rv, cb, cv] = masked;
if (masked.some(m => m === null) || rv !== cv || rb !== cb) changedProduct.push(p);
else stampOnly.push(p);
continue;
}
if (cls !== 'product') continue;
const verdict = comparePath(repoRoot, sides, p);
if (verdict === 'changed') changedProduct.push(p);
@@ -6,11 +6,12 @@
### Fixed
- **`lib/stamp-only.ts`:** `stampOnlyLine` takes the policy's placeholders. On a line the branch added itself (named by its position), a placeholder inside a `stamp_patterns` template may become that template at the new version, alongside any stamp-form version move on the same line. The built-in forms, a bare `{version}` template, base lines (including one whose text duplicates a branch line) and undeclared placeholders are still refused. Finish's `stampedFiles`, the receipt carry and the tree receipt share it.
- **`bin/gstack-ship-receipt` carry:** the carried receipt's `version` is HEAD's, read the way `write` reads it, instead of the version copied from the old receipt. A head with no version source keeps the version `write --version` recorded.
- **`bin/gstack-ship-receipt` carry, pin-bump PRs:** a `package.json` the branch changed as product (a dependency pin) that both main and a restamp moved since review is compared with only its `version` field masked, at all four sides. The receipt carries when nothing else in the file moved, and still refuses when the pin or any other field changed (GBEV-3, gbrain-evals #306).
### Tests
- `test/tree-receipt-stamp-sensitive.test.ts`, `test/gstack-ship-receipt-docs.test.ts`: the shared predicate and the carry classifier accept gbrain's `since: 'TBD'` → `since: '0.60.553.0'` row and refuse an undeclared placeholder, a base row, a duplicated base copy, a built-in form and a line no template matches.
- `test/ship-rereview-triggers.test.ts`: finish routes the declared row as metadata and stops on the same controls.
- `test/restamp-mentions.test.ts`: the tree receipt and the carry accept the declared row and refuse it undeclared or on a base row; the carried receipt records the head's version after a main merge and a restamp; a version-only restamp of an EVIDENCE.md declared in `stamp_files` carries, and an undeclared one stays product.
- `test/restamp-mentions.test.ts`: the tree receipt and the carry accept the declared row and refuse it undeclared or on a base row; the carried receipt records the head's version after a main merge and a restamp; a version-only restamp of an EVIDENCE.md declared in `stamp_files` carries, and an undeclared one stays product; a pin-bump PR's receipt carries after a main merge and a restamp, and refuses when the pin moved since review.
- `test/gstack-ship-policy.test.ts`: the key's default, floor and validation.
The EVIDENCE.md refusal (GST-1030's second point) reproduces on main and is already fixed by the GST-980 own-row carry this branch builds on, so this change adds only the test for it.
+29
View File
@@ -541,6 +541,35 @@ describe('declared placeholders and the receipt carry (GST-1026, GST-1030)', ()
expect(extractReceipt(fs.readFileSync(body, 'utf8')).receipt?.version).toBe('1.2.5.0');
});
/** package.json with its dependency pin set to `pin`, every other field kept. */
const pinned = (repo: FixtureRepo, pin: string) => {
const pkg = JSON.parse(readFile(repo, 'package.json'));
pkg.dependencies = { ...pkg.dependencies, 'left-pad': pin };
write(repo, 'package.json', JSON.stringify(pkg, null, 2) + '\n');
};
// Value: protects=a pin-bump PR's receipt carries across a main merge and a restamp when package.json's only change since
// review is its version field, and refuses when the pin itself moved (GST-1030, GBEV-3, gbrain-evals #306);
// fails_when=the carry counts the PR's package.json as product and refuses RECEIPT_CARRY_PRODUCT_CHANGED on a version-only
// move, or lets a changed dependency pin ride; seam=bin/gstack-ship-receipt cmdCarry walk
test.each([['the pin unchanged since review', false], ['the pin moved since review', true]])('the carry and a pin-bump PR after a main merge: %s (GST-1030)', (_, moved) => {
const { repo, body } = receiptThenStamp(`restamp-carry-pin-${moved}-`, policy(true), r => pinned(r, '1.3.0'));
repo.git('reset', '-q', '--hard');
makePr(repo, 'pr-other');
expect(restamp(repo, ['--next']).status).toBe(EXIT.ok);
repo.git('commit', '-qam', 'stamp other');
mergeToMain(repo, 'pr-other');
repo.git('checkout', '-q', 'pr-carry');
mergeMain(repo);
expect(restamp(repo, ['--next']).status).toBe(EXIT.ok);
if (moved) pinned(repo, '1.3.1');
repo.git('commit', '-qam', 'restamp after the lost slot');
expect(JSON.parse(readFile(repo, 'package.json')).version).toBe('1.2.5');
const carried = carry(repo, body);
if (moved) expect([carried.status, carried.err.includes('RECEIPT_CARRY_PRODUCT_CHANGED')], carried.err).toEqual([EXIT.refused, true]);
else expect(carried.out, carried.err).toContain('CARRY: carried');
});
// Value: protects=a version-only restamp of EVIDENCE.md is release metadata to the carry when the policy declares it in
// stamp_files, and product when it does not (GST-1030 point 2, GGST-1); fails_when=the carry counts the declared
// file's stamp as a product change (RECEIPT_CARRY_PRODUCT_CHANGED), or lets an undeclared file's version edit ride;