Skip to content

Commit 32bafc4

Browse files
committed
Preserve historical owners when retrying ledger writes
1 parent b339d97 commit 32bafc4

3 files changed

Lines changed: 15 additions & 1 deletion

File tree

‎docs/implementation/persistent-review-store.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,7 @@ Build the review screen under #3 next. The real-issue assignment, experiment pro
3333
## Review round 1
3434

3535
Copilot's summary identified two findings (no inline threads). Both were reproduced with failing regressions, then fixed: immutable ledger comparisons now compare the typed fields rather than JSON property order, and a checkpoint's item must be the last item in its completed prefix. No findings were declined. Final suite: 151 tests.
36+
37+
## Review round 2
38+
39+
Reproduced a historical-owner retry failure after an amendment removed the owning item. Normal history writes now apply current-item validation only to new ledger entries; existing SHAs still pass the immutable field comparison. The regression also checks that new commits cannot claim the removed owner and that rebases retain historical ownership. No findings declined. Final suite: 152 tests.

‎runner/store.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ export class Store {
176176
this.#expect(key, expected);
177177
const plan = this.getPlan(identity);
178178
for (const entry of entries) {
179-
if (entry.owner !== null && !plan.items.some(item => item.id === entry.owner)) throw new Error('Unknown ledger owner.');
179+
const existing = this.#get('SELECT sha FROM ledger WHERE key=? AND sha=?', key, entry.sha);
180+
if (!existing && entry.owner !== null && !plan.items.some(item => item.id === entry.owner)) throw new Error('Unknown ledger owner.');
180181
this.#entry(key, entry);
181182
}
182183
return this.#snapshot(key, base, head);

‎test/store.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,12 @@ it('requires the checkpoint item to be the last item in the completed prefix', (
176176
expect(() => store.recordCheckpoint(identity, state(store), { item: 'P1', completedItems: ['P1', 'P2'], outOfScopePaths: ['outside'], baseEntries: context.baseEntries })).toThrow(/prefix/);
177177
expect(store.recordCheckpoint(identity, state(store), { item: 'P2', completedItems: ['P1', 'P2'], outOfScopePaths: ['outside'], baseEntries: context.baseEntries }).item).toBe('P2');
178178
});
179+
it('allows historical ledger retries after the owning item is removed, but rejects new entries for it', () => {
180+
const { store } = fixture(); const entry = { sha: oid(2), owner: 'P1', origin: 'owned' as const, sourceSha: null };
181+
store.recordHistory(identity, state(store), oid(1), oid(2), [entry]);
182+
const next = plan(); next.items[0]!.id = 'P2'; store.importRevision(JSON.stringify(next), 'json', context, 1);
183+
expect(() => store.recordHistory(identity, state(store), oid(1), oid(2), [entry])).not.toThrow();
184+
expect(() => store.recordHistory(identity, state(store), oid(1), oid(3), [{ ...entry, sha: oid(3) }])).toThrow(/Unknown ledger owner/);
185+
store.recordRebase(identity, state(store), oid(4), oid(5), [{ oldSha: oid(2), newSha: oid(5) }]);
186+
expect(store.ownership(identity).get(oid(5))).toBe('P1');
187+
});

0 commit comments

Comments
 (0)