js-rigor: lossless persistence — validity, decay config, and TTLs survive snapshots

The persistence probe found three silent-loss defects in the snapshot
round trip: relation validity labels (finite_sample downgraded to
heuristic after save/load!), decay configs, and the value manager's
per-relation TTL settings all vanished. The restored arbiter built its
indices directly from the condensed graph, whose edge channel carries
possibility/reliability/value only.

Format version 2 now carries per-relation metadata (validity, decayConfig)
and the valueTtls table in the snapshot payload; both the snapshot-access
layer and the CondensedGraphIndices build merge the metadata back, so a
restored arbiter is lossless end to end. Version 1 buffers are rejected
with the existing clean version error.

Pinned: a persistence round-trip test asserting validity label, decay
config, TTL, and the restored check's validity label. Suites: rigor
207/0, full 809/747/0.
This commit is contained in:
John Dvorak
2026-08-02 09:51:40 -07:00
parent 86729715f1
commit faa6485e26
5 changed files with 102 additions and 6 deletions
+30
View File
@@ -213,3 +213,33 @@ describe('Security affordances (rigor)', () => {
);
});
});
describe('Persistence losslessness (rigor)', () => {
it('FIXED: validity, decay config, and TTLs survive the snapshot round trip', async () => {
const a = new Arbiter();
a.addNode('u:0', 'user');
a.addNode('d:0', 'doc');
a.setRelationConfig('can_read', { type: 'direct', relation: 'owner' });
a.addRelation('u:0', 'owner', 'd:0', {
possibility: 0.8,
reliability: 0.42,
value: 7,
validity: 'finite_sample',
decayConfig: { halfLifeMs: 60000 }
});
a.valueManager.setTTL('owner', 30000);
a.enableCondensedSnapshot();
const { serializeArbiterSnapshot } = await import('../../src/core/SnapshotBinary.js');
const { ArbiterSnapshot } = await import('../../src/core/arbiter/ArbiterSnapshot.js');
const restored = ArbiterSnapshot.fromSnapshotBinary(serializeArbiterSnapshot(a), {}, () => new Arbiter());
const rel = restored.relationManager.getDirectRelation(restored.resolveNodeId('u:0'), 'owner', restored.resolveNodeId('d:0'));
assert.equal(rel.validity, 'finite_sample', 'validity label survives persistence');
assert.deepEqual(rel.decayConfig, { halfLifeMs: 60000 }, 'decay config survives persistence');
assert.equal(restored.valueManager.getTTL('owner'), 30000, 'per-relation TTL survives persistence');
const r = restored.check('u:0', 'can_read', 'd:0', { includeMeta: true });
assert.equal(r.validity.label, 'finite_sample', 'restored check carries the persisted label');
// and a second restore of the same buffer is byte-stable
const restored2 = ArbiterSnapshot.fromSnapshotBinary(serializeArbiterSnapshot(a), {}, () => new Arbiter());
assert.equal(restored2.relationManager.getDirectRelation(restored2.resolveNodeId('u:0'), 'owner', restored2.resolveNodeId('d:0')).validity, 'finite_sample');
});
});