js-rigor: TTU operand values flow; modify honors changed_last_at override
Two issues found by the extended probe sweep: - A relational_comparator operand backed by a tuple_to_userset rule always denied: the TTU rule collected only the intermediate KEY, which the operand extraction skips as non-numeric, so no value was ever available. The TTU rule now emits a value-carrying collected entry when the tupleset edge carries a numeric value (entityKey = tuple src, relation = tupleset relation), keeping the bare intermediate key when there is no value. Comparator-with-TTU-operand now allows/denies on the tuple value through both persistent and partial contexts. - _modifyRelation ignored the changed_last_at override that the add path honors: value-changing modifies stamped fresh Date.now() regardless of the pin, so replay/restore tools pinning timestamps got different semantics via modify vs add. The override now applies to refresh events (value/reliability/possibility change) and is ignored for value-unchanged writes, preserving the TTL parity contract that identical replays never un-expire old values. Verified clean: intersection through partial, defeasible with logical when, challenge subject object/session with sessionKey, non-binary minAllowPossibility threshold, batch value updates, explain agreement under partial.
This commit is contained in:
@@ -442,6 +442,53 @@ describe('Rule-kind × partial-graph parity (rigor)', () => {
|
||||
const p = restored.check('u:0', 'can_read', 'doc:0');
|
||||
assert.equal(round4(p.possibility), 0.7, 'snapshot-restored TTU');
|
||||
}
|
||||
// ---- comparator operand as TTU (value flows via the tupleset edge) ----
|
||||
{
|
||||
const a = mkArbiter();
|
||||
a.setRelationConfig('can_access', {
|
||||
type: 'relational_comparator', comparator: '>=',
|
||||
left: { rule: { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' }, extractValue: true },
|
||||
right: { rule: { type: 'direct', relation: 'requested_level' }, extractValue: true, evaluateFrom: 'object' }
|
||||
});
|
||||
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
|
||||
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.9, value: 7 });
|
||||
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
|
||||
a.addRelation('doc:0', 'requested_level', 'doc:0', { possibility: 1.0, value: 4 });
|
||||
assert.equal(a.check('u:0', 'can_access', 'doc:0').possibility, 1, 'ttu operand persistent allow');
|
||||
a.removeRelation('doc:0', 'owner', 'g:0');
|
||||
a.removeRelation('u:0', 'member_of', 'g:0');
|
||||
a.removeRelation('doc:0', 'requested_level', 'doc:0');
|
||||
const r = a.check('u:0', 'can_access', 'doc:0', {
|
||||
partialGraph: { relations: [
|
||||
{ src: 'doc:0', relation: 'owner', dst: 'g:0', possibility: 0.9, value: 7 },
|
||||
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
|
||||
{ src: 'doc:0', relation: 'requested_level', dst: 'doc:0', possibility: 1.0, value: 4 }
|
||||
] }
|
||||
});
|
||||
assert.equal(r.possibility, 1, 'ttu operand partial allow');
|
||||
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.9, value: 3 });
|
||||
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
|
||||
a.addRelation('doc:0', 'requested_level', 'doc:0', { possibility: 1.0, value: 4 });
|
||||
assert.equal(a.check('u:0', 'can_access', 'doc:0').possibility, 0, 'ttu operand below threshold deny');
|
||||
a.removeRelation('doc:0', 'owner', 'g:0');
|
||||
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.9 });
|
||||
assert.equal(a.check('u:0', 'can_access', 'doc:0').possibility, 0, 'ttu operand without value denies');
|
||||
}
|
||||
// ---- modify changed_last_at override (value-changing modify honors the pin) ----
|
||||
{
|
||||
const a = mkArbiter();
|
||||
a.addRelation('u:0', 'balance', 'doc:0', { value: 10, changed_last_at: 1000 });
|
||||
a.relationManager.updateRelationsBatch([
|
||||
{ operation: 'modify', srcKey: 'u:0', relation: 'balance', dstKey: 'doc:0', options: { value: 20, changed_last_at: 5000 } }
|
||||
]);
|
||||
const after = a.relationManager.getDirectRelation(a.resolveNodeId('u:0'), 'balance', a.resolveNodeId('doc:0'));
|
||||
assert.equal(after.changed_last_at, 5000, 'value-changing modify honors changed_last_at override');
|
||||
a.relationManager.updateRelationsBatch([
|
||||
{ operation: 'modify', srcKey: 'u:0', relation: 'balance', dstKey: 'doc:0', options: { value: 20, changed_last_at: 9999 } }
|
||||
]);
|
||||
const unchanged = a.relationManager.getDirectRelation(a.resolveNodeId('u:0'), 'balance', a.resolveNodeId('doc:0'));
|
||||
assert.equal(unchanged.changed_last_at, 5000, 'value-unchanged modify keeps old timestamp (override not a refresh)');
|
||||
}
|
||||
// ---- binary mode agrees with normal at the same threshold ----
|
||||
{
|
||||
const a = mkArbiter();
|
||||
|
||||
Reference in New Issue
Block a user