js-rigor: the partial graph carries the temporal context
The partial graph is the caller's self-contained view of the world — so the time belongs in it. partialGraph.now becomes the evaluation clock for that check (TTL gates, proof expiry, decay all honor it), with an explicit options.now taking precedence. The explain rerun replays the SAME partial graph object and reproduces the original decision; the serializer records request.temporal.now for the record. Pins: partialGraph.now driving a challenge decision both sides of the window, the explicit-override precedence, and the explain rerun with the same object.
This commit is contained in:
@@ -4,6 +4,13 @@ import { validateReducerConfig, getAllowedRelationsForLayer } from './partial-gr
|
|||||||
export class PartialGraphContext {
|
export class PartialGraphContext {
|
||||||
constructor(arbiter, partialGraph) {
|
constructor(arbiter, partialGraph) {
|
||||||
this.arbiter = arbiter;
|
this.arbiter = arbiter;
|
||||||
|
// The caller's view of the world at a moment in time: the partial graph
|
||||||
|
// may carry that time. When present it becomes the evaluation clock for
|
||||||
|
// this check, so a rerun that replays the same partial graph reproduces
|
||||||
|
// the same decision (TTL gates, proof expiry, decay all honor it).
|
||||||
|
this.now = (partialGraph && partialGraph.now !== undefined && partialGraph.now !== null)
|
||||||
|
? partialGraph.now
|
||||||
|
: null;
|
||||||
const policy = arbiter?.partialGraphPolicy || null;
|
const policy = arbiter?.partialGraphPolicy || null;
|
||||||
this.nodeIdByKey = new Map();
|
this.nodeIdByKey = new Map();
|
||||||
this.keyByNodeId = new Map();
|
this.keyByNodeId = new Map();
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ export class ArbiterChecks {
|
|||||||
throw new Error(`Partial graph exceeds max nodes: ${nodes.length} > ${maxNodes}`);
|
throw new Error(`Partial graph exceeds max nodes: ${nodes.length} > ${maxNodes}`);
|
||||||
}
|
}
|
||||||
options.partialGraphContext = this.arbiter._createPartialGraphContext(options.partialGraph);
|
options.partialGraphContext = this.arbiter._createPartialGraphContext(options.partialGraph);
|
||||||
|
if (options.now === undefined && options.partialGraphContext.now !== null) {
|
||||||
|
options.now = options.partialGraphContext.now;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { returnHints = false, _visited = new Set(), _currentRelation = null, binary = false, epsilon, delta, clientStateId } = options;
|
const { returnHints = false, _visited = new Set(), _currentRelation = null, binary = false, epsilon, delta, clientStateId } = options;
|
||||||
@@ -115,6 +118,9 @@ export class ArbiterChecks {
|
|||||||
|
|
||||||
if (options.partialGraph && !options.partialGraphContext) {
|
if (options.partialGraph && !options.partialGraphContext) {
|
||||||
options.partialGraphContext = this.arbiter._createPartialGraphContext(options.partialGraph);
|
options.partialGraphContext = this.arbiter._createPartialGraphContext(options.partialGraph);
|
||||||
|
if (options.now === undefined && options.partialGraphContext.now !== null) {
|
||||||
|
options.now = options.partialGraphContext.now;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const explainOptions = {
|
const explainOptions = {
|
||||||
|
|||||||
@@ -289,3 +289,25 @@ describe('Temporal replay (re-entrant diagnostics) (rigor)', () => {
|
|||||||
assert.deepEqual(e.request.temporal, { now: issued + 301000 }, 'challenge temporal recorded');
|
assert.deepEqual(e.request.temporal, { now: issued + 301000 }, 'challenge temporal recorded');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Partial graph carries the temporal context (rigor)', () => {
|
||||||
|
it('FIXED: partialGraph.now drives the decision, and the rerun replays the same object', () => {
|
||||||
|
const a = new Arbiter();
|
||||||
|
a.addNode('u:0', 'user');
|
||||||
|
a.addNode('d:0', 'doc');
|
||||||
|
a.setRelationConfig('can_download', { type: 'challenge', challenge: 'mfa', subject: 'user', withinMinutes: 5 });
|
||||||
|
const issued = 500000;
|
||||||
|
// One self-contained request object: evidence + the caller's time.
|
||||||
|
const pg = { challenges: [{ name: 'mfa', subject: 'u:0', issuedAt: issued, expiresAt: null }], now: issued + 299000 };
|
||||||
|
assert.equal(a.check('u:0', 'can_download', 'd:0', { partialGraph: pg }).possibility, 1, 'within window via partialGraph.now');
|
||||||
|
pg.now = issued + 301000;
|
||||||
|
assert.equal(a.check('u:0', 'can_download', 'd:0', { partialGraph: pg }).possibility, 0, 'past window via partialGraph.now');
|
||||||
|
// an explicit options.now overrides the partial graph's time
|
||||||
|
assert.equal(a.check('u:0', 'can_download', 'd:0', { partialGraph: pg, now: issued + 299000 }).possibility, 1, 'explicit now overrides');
|
||||||
|
// the rerun replays the same object and reproduces the decision
|
||||||
|
pg.now = issued + 299000;
|
||||||
|
const e = a.explain('u:0', 'can_download', 'd:0', { partialGraph: pg });
|
||||||
|
assert.equal(e.decision.possibility, 1, 'explain rerun with the same partial graph reproduces');
|
||||||
|
assert.deepEqual(e.request.temporal, { now: issued + 299000 }, 'temporal recorded from the partial graph');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user