Skip to content

Commit 174b344

Browse files
author
moc
committed
fix: fail closed on rows restored into older sequence gaps
Coverage check: every source row inside the anchored range (pos <= upto) must carry a ledger link. A row imported/restored into an earlier gap was previously invisible to both the link walk and the unchained tail count — verification returned true with checked < rows. Gap rows now report firstDivergence with the row's derived identity and verified:false; regression covers both surfaces plus heal-on-restore (maintainer's independent repro shape).
1 parent 8aca0dd commit 174b344

3 files changed

Lines changed: 33 additions & 0 deletions

File tree

plugins/hetaoBackend/mcode-dynamic-workflows/checks/integrity.check.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,25 @@ test('full broken-repair-approve-finish flow keeps both chains verified and hone
176176
assert.equal(v.events.unchained,0);assert.equal(v.repair.unchained,0);
177177
}finally{await f.cleanup();}
178178
});
179+
180+
test('a row restored into an older sequence gap fails closed on both surfaces',async()=>{
181+
const f=await fixture(async s=>({output:s.id}));try{
182+
// Seed rows at seq 1 and 3 directly (legacy, pre-ledger), then anchor 1,3,4 via API writes.
183+
const ins=(seq,runId)=>f.store.db.prepare('INSERT INTO events(seq,runId,body) VALUES(?,?,?)').run(seq,runId,JSON.stringify({type:'seed',seq}));
184+
ins(1,'run-a');ins(3,'run-a');f.store.event('run-a','anchor',{});
185+
assert.equal(f.store.db.prepare('SELECT COUNT(*) n FROM events').get().n,3,'anchored set is 1,3,4');
186+
// Simulate a restore/import that fills the gap at seq 2.
187+
ins(2,'run-b');
188+
const v=f.store.verifyIntegrity().events;
189+
assert.equal(v.verified,false,'gap row inside the anchored range must fail closed');
190+
assert.deepEqual(v.firstDivergence,{key:'run-b:2',expectedHead:null,actualHead:null});
191+
f.store.db.prepare('DELETE FROM events WHERE seq=2').run();
192+
assert.equal(f.store.verifyIntegrity().events.verified,true,'restoring the anchored set heals');
193+
// Same shape on repair_cache: legacy rows at rowid 1 and 3, anchor, then fill rowid 2.
194+
const insr=(runId,id)=>f.store.db.prepare('INSERT INTO repair_cache(runId,id,body) VALUES(?,?,?)').run(runId,id,JSON.stringify({id,kind:'agent'}));
195+
insr('run-a','a');insr('run-a','c');f.store.saveRepairCandidate('run-a',{id:'d',kind:'agent'});
196+
insr('run-b','b');
197+
const r=f.store.verifyIntegrity().repair;
198+
assert.equal(r.verified,false);assert.equal(r.firstDivergence.key,'run-b/b');
199+
}finally{await f.cleanup();}
200+
});

plugins/hetaoBackend/mcode-dynamic-workflows/dist/main.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7880,6 +7880,11 @@ var Store = class {
78807880
if (!firstDivergence && (!row || key !== r.key || actual !== r.hash)) firstDivergence = { key: r.key, expectedHead: r.hash, actualHead: actual };
78817881
prev = r.hash;
78827882
}
7883+
if (!firstDivergence) {
7884+
const anchored = new Set(rows.map((r) => r.pos));
7885+
const gap = this.db.prepare(`SELECT ${posCol} AS __pos, * FROM ${table} WHERE ${posCol}<=? ORDER BY ${posCol}`).all(rec.upto).find((r) => !anchored.has(r.__pos));
7886+
if (gap) firstDivergence = { key: keyOf(gap, gap.__pos), expectedHead: null, actualHead: null };
7887+
}
78837888
const verified = !firstDivergence && prev === rec.head && unchained === 0;
78847889
return { head: rec.head, upto: rec.upto, verified, checked: rows.length, unchained, firstDivergence };
78857890
};

plugins/hetaoBackend/mcode-dynamic-workflows/src/store.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export class Store {
7171
const actual=row?this.rowHash(prev,kind,key,row.body):null;
7272
if(!firstDivergence&&(!row||key!==r.key||actual!==r.hash))firstDivergence={key:r.key,expectedHead:r.hash,actualHead:actual};
7373
prev=r.hash;}
74+
// Coverage: every source row inside the anchored range must carry a ledger
75+
// link. A row restored into an older sequence gap (pos<=upto, no link) would
76+
// otherwise be invisible to both the walk above and the unchained tail count.
77+
if(!firstDivergence){const anchored=new Set(rows.map(r=>r.pos));
78+
const gap=this.db.prepare(`SELECT ${posCol} AS __pos, * FROM ${table} WHERE ${posCol}<=? ORDER BY ${posCol}`).all(rec.upto).find(r=>!anchored.has(r.__pos));
79+
if(gap)firstDivergence={key:keyOf(gap,gap.__pos),expectedHead:null,actualHead:null};}
7480
// Verification covers the anchored prefix; any unanchored row fails closed.
7581
const verified=!firstDivergence&&prev===rec.head&&unchained===0;
7682
return {head:rec.head,upto:rec.upto,verified,checked:rows.length,unchained,firstDivergence};};

0 commit comments

Comments
 (0)