fix(stamp): name branch lines by position; carry and finish accept a kept VERSION claim (GST-979, GST-980)

BranchSide carries the positions of the branch-added lines and stampOnlyLine takes the removed line position, so a base line whose text duplicates a branch line keeps the old-version floor. At equal versions only the branch own lines may move. stampOnlyText and stampedFiles accept a kept claim for those lines, and the carry leaves a stamp file out of the product comparison when its only change since the reviewed head is a stamp on the branch own rows and the base did not touch it. Failing-first: all five new tests fail on main fc292e22f.

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 17:29:44 +00:00
1 parent 40a3170335
commit fbf83f11ef
12 files changed
+178 -35

No files matched your search

+18 -1
View File
@@ -225,12 +225,29 @@ describe('carry follow-ups: git floor, named refusal, branch-touched walk, digit
const mine = "export const MINE_KEY_VERSION = '1.2.3.0';";
const before = `${mine}\nexport const BASE_KEY_VERSION = '1.2.3.0';\n`;
const moved = (text: string) => ({ before, after: text, oldVersion: '1.2.4.0', newVersion: '1.2.5.0', templates });
const side = { fork: '1.2.3.0', added: new Set([mine]) };
const side = { fork: '1.2.3.0', added: new Set([mine]), at: new Set([1]) };
expect(stampOnlyText({ ...moved(before.replace(mine, mine.replace('1.2.3.0', '1.2.5.0'))), side })).toBe(true);
expect(stampOnlyText(moved(before.replace(mine, mine.replace('1.2.3.0', '1.2.5.0'))))).toBe(false);
expect(stampOnlyText({ ...moved(before.replace("BASE_KEY_VERSION = '1.2.3.0'", "BASE_KEY_VERSION = '1.2.5.0'")), side })).toBe(false);
});
// Value: protects=the carry reads the branch's own line by position and, with a kept VERSION claim, accepts only the
// branch's own row moved from the fork version (GST-979, GST-980); fails_when=stampOnlyText refuses every change at
// equal versions or takes a duplicated base line as the branch's; seam=lib/ship-receipt.ts stampOnlyText
test('stampOnlyText reads the branch line by position, and with a kept VERSION moves only the branch\'s own row (GST-979, GST-980)', () => {
const templates = ["_KEY_VERSION = '{version}'"];
const key = (v: string) => `export const MINE_KEY_VERSION = '${v}';`;
const before = `${key('1.2.3.0')}\nexport const BASE_KEY_VERSION = '1.2.3.0';\n${key('1.2.3.0')}\n`;
const side = { fork: '1.2.3.0', added: new Set([key('1.2.3.0')]), at: new Set([3]) };
const lines = (a: string, c: string) => `${a}\nexport const BASE_KEY_VERSION = '1.2.3.0';\n${c}\n`;
const change = (after: string, oldVersion: string, newVersion: string) => stampOnlyText({ before, after, oldVersion, newVersion, templates, side });
expect(change(lines(key('1.2.3.0'), key('1.2.5.0')), '1.2.4.0', '1.2.5.0')).toBe(true);
expect(change(lines(key('1.2.5.0'), key('1.2.3.0')), '1.2.4.0', '1.2.5.0')).toBe(false);
expect(change(lines(key('1.2.3.0'), key('1.2.4.0')), '1.2.4.0', '1.2.4.0')).toBe(true);
expect(change(lines(key('1.2.4.0'), key('1.2.3.0')), '1.2.4.0', '1.2.4.0')).toBe(false);
expect(stampOnlyText({ before, after: lines(key('1.2.3.0'), key('1.2.4.0')), oldVersion: '1.2.4.0', newVersion: '1.2.4.0', templates })).toBe(false);
});
test('planCarry names at most CARRY_NAMED_PATHS paths', () => {
const many = Array.from({ length: CARRY_NAMED_PATHS + 3 }, (_, i) => `src/f${i}.ts`);
const plan = planCarry({ from: 'a'.repeat(40), head: 'b'.repeat(40), changed: many.map(p => ({ path: p, class: 'product' as const })), productPatchId: { from: 'x', head: 'y' }, testPatchId: { from: '', head: '' }, changedProduct: many, mergeCommits: 0, rangeDiff: '' });
+54
View File
@@ -329,4 +329,58 @@ describe('a branch-added row left at the fork version after the base moved (GST-
expect(receipt.out).toContain("id: 'base_row'");
expect(receipt.out).not.toContain("id: 'mine'");
});
// Value: protects=a base row whose text a branch row duplicates keeps the old-version floor in the tree receipt
// (GST-979, the #3862 outside voice); fails_when=the side matches by text, so a hand move of the base copy reads as
// same-modulo-stamps; seam=lib/stamp-only.ts branchSides positions
test('a base row whose text the branch duplicates still refuses a hand move; the branch copy\'s own move passes (GST-979)', () => {
const repo = makeFixtureRepo({ prefix: 'restamp-dup-row-', policy: receiptPolicy(true) }); repos.push(repo);
write(repo, NOTICE, notice([row('1.2.3.0', 'base_row')]));
repo.git('add', '-A'); repo.git('commit', '-qm', 'notice rows'); repo.git('push', '-q', 'origin', 'main');
makePr(repo, 'pr-dup');
expect(restamp(repo, ['--version', '1.2.4.0']).status).toBe(EXIT.ok);
repo.git('commit', '-qam', 'stamp 1.2.4.0');
write(repo, NOTICE, notice([row('1.2.3.0', 'base_row'), '', '', row('1.2.3.0', 'base_row')]));
repo.git('commit', '-qam', 'dup: the same row text at the fork version');
const gated = repo.git('rev-parse', 'HEAD');
expect(restamp(repo, ['--version', '1.2.5.0']).status).toBe(EXIT.ok);
expect(readFile(repo, NOTICE)).toBe(notice([row('1.2.3.0', 'base_row'), '', '', row('1.2.5.0', 'base_row')]));
repo.git('commit', '-qam', 'stamp dup');
const own = runBin(repo, 'gstack-tree-receipt', ['--gated', gated, '--base', 'main']);
expect(own.out, own.err).toContain('TREE: same-modulo-stamps');
write(repo, NOTICE, notice([row('1.2.5.0', 'base_row'), '', '', row('1.2.5.0', 'base_row')]));
repo.git('commit', '-qam', 'move the base copy by hand');
const hand = runBin(repo, 'gstack-tree-receipt', ['--gated', gated, '--base', 'main']);
expect(hand.out, hand.err).toContain('TREE: changed');
});
// Value: protects=the receipt carry follows a stamp that moves the branch's own fork-version row, the move the writer
// makes since GST-963 (GST-980); fails_when=the carry keeps the branch-added row's bytes in both product patch-ids and
// refuses RECEIPT_CARRY_PRODUCT_CHANGED; seam=bin/gstack-ship-receipt carry walk
test('the carry follows a stamp that moves the branch\'s own fork-version row, and still refuses a product edit beside it (GST-980)', () => {
const repo = makeFixtureRepo({ prefix: 'restamp-carry-row-', policy: policy(true) }); repos.push(repo);
write(repo, NOTICE, notice([row('1.2.3.0', 'base_row')]));
fs.rmSync(path.join(repo.work, 'package-lock.json'));
repo.git('add', '-A'); repo.git('commit', '-qm', 'notice rows, no lockfile'); repo.git('push', '-q', 'origin', 'main');
makePr(repo, 'pr-carry');
expect(restamp(repo, ['--version', '1.2.4.0']).status).toBe(EXIT.ok);
repo.git('commit', '-qam', 'stamp 1.2.4.0');
write(repo, NOTICE, notice([row('1.2.3.0', 'base_row'), '', row('1.2.3.0', 'mine')]));
repo.git('commit', '-qam', 'mine: claims 1.2.4.0 with a row at the fork version');
const body = path.join(repo.root, 'body.md');
fs.writeFileSync(body, '## Summary\n');
const w = runBin(repo, 'gstack-ship-receipt', ['write', '--pr', '3', '--base', 'main', '--session-kind', 'x', '--body', body]);
expect(w.status, w.err).toBe(EXIT.ok);
expect(restamp(repo, ['--version', '1.2.5.0']).status).toBe(EXIT.ok);
expect(readFile(repo, NOTICE)).toBe(notice([row('1.2.3.0', 'base_row'), '', row('1.2.5.0', 'mine')]));
repo.git('commit', '-qam', 'stamp 1.2.5.0');
const carried = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
expect(carried.err).not.toContain('RECEIPT_CARRY_PRODUCT_CHANGED');
expect(carried.out, carried.err).toContain('CARRY: carried');
write(repo, 'src.ts', 'export const x = 1;\n');
repo.git('add', '-A'); repo.git('commit', '-qm', 'a product change after the stamp');
const changed = runBin(repo, 'gstack-ship-receipt', ['carry', '--body', body, '--base', 'main']);
expect(changed.status).toBe(EXIT.refused);
expect(changed.err).toContain('RECEIPT_CARRY_PRODUCT_CHANGED');
});
});
+18
View File
@@ -248,6 +248,24 @@ describe('a restamp after a lost slot (GST-895, GBRA-218, gbrain #8134)', () =>
const e = finish(edited.f, edited.reviewed, edited.post, ['--reviewed-base', edited.reviewedBase]);
expect([e.status, lineOf(e.stdout, 'STOP')]).toEqual([3, behaviorStop]);
});
// Value: protects=with a kept VERSION claim, the restamp's move of the branch's own fork-version row routes as metadata,
// and a base row moved the same way still stops (GST-980); fails_when=stampedFiles returns nothing at equal versions
// and finish routes the row as behavior; seam=lib/ship-bin-common.ts stampedFiles
test('a kept VERSION claim: the moved fork-version row is metadata; a moved base row stops (GST-980)', () => {
const kept = (rows: [string, string][]) => {
const f = fx({ '.gstack/ship-policy.json': policy, VERSION: '1.0.0.0\n', 'src/notice.ts': notice(['0.9.0.0', 'old'], ['1.0.0.0', 'base']) },
{ VERSION: '1.0.1.0\n', 'src/notice.ts': notice(['0.9.0.0', 'old'], ['1.0.0.0', 'base'], ['1.0.0.0', 'mine']) });
const reviewed = wtree(f);
const post = snapshot(f);
f.write({ 'src/notice.ts': notice(...rows) }); f.commit('stamp, VERSION kept');
return finish(f, reviewed, post, ['--reviewed-base', mergeBase(f)]);
};
const own = kept([['0.9.0.0', 'old'], ['1.0.0.0', 'base'], ['1.0.1.0', 'mine']]);
expect(lineOf(own.stdout, 'ROUTE')).toEqual(expect.stringMatching(/ behavior=0 docs=0 metadata=1 tests=0 /));
const base = kept([['0.9.0.0', 'old'], ['1.0.1.0', 'base'], ['1.0.0.0', 'mine']]);
expect([base.status, lineOf(base.stdout, 'STOP')]).toEqual([3, behaviorStop]);
});
});
describe('adjacent base edits (GST-384, #3631, founderstar#441)', () => {
+16
View File
@@ -105,6 +105,22 @@ describe('tree receipt: stamps, stamp-sensitive gates and negative fixtures', ()
expect(width.out).toMatch(/^TREE: changed \(lib\/policy\.ts\)$/m);
});
// Value: protects=the fork-version bound reads the branch's own line by its position, so a base copy of the same text
// is still floored at the old version, and when the slot keeps its VERSION only the branch's own lines may move
// (GST-979); fails_when=stampOnlyLine matches the branch's lines by text alone or takes the old-version floor at equal
// versions; seam=lib/stamp-only.ts stampOnlyLine
test('the fork-version bound holds only at the branch line\'s own position, and a kept VERSION moves only the branch\'s own lines (GST-979)', () => {
const tpl = ["since: '{version}'"];
const row = " { since: '1.2.3.0', id: 'x' },";
const moved = row.replace('1.2.3.0', '1.2.5.0');
const side = { fork: '1.2.3.0', added: new Set([row]), at: new Set([10]) };
expect(stampOnlyLine(row, moved, '1.2.4.0', '1.2.5.0', tpl, side, 10)).toBe(true);
expect(stampOnlyLine(row, moved, '1.2.4.0', '1.2.5.0', tpl, side, 2)).toBe(false);
const mine = { fork: '1.2.3.0', added: new Set(["mine since: '1.2.3.0'"]), at: new Set([5]) };
expect(stampOnlyLine("base since: '1.3.0.0'", "base since: '1.2.4.0'", '1.2.4.0', '1.2.4.0', tpl, mine, 1)).toBe(false);
expect(stampOnlyLine("mine since: '1.2.3.0'", "mine since: '1.2.4.0'", '1.2.4.0', '1.2.4.0', tpl, mine, 5)).toBe(true);
});
test('negative fixture: a dependency pin equal to the old version is `changed`, not a stamp, even inside stamp_paths', () => {
const repo = fixture();
const g = gated(repo);