diff --git a/src/core/arbiter/ArbiterChecks.js b/src/core/arbiter/ArbiterChecks.js index b8bad85..259b9f0 100644 --- a/src/core/arbiter/ArbiterChecks.js +++ b/src/core/arbiter/ArbiterChecks.js @@ -71,8 +71,7 @@ export class ArbiterChecks { } if (options.explain) return result; - const { reliability, ...rest } = result; - return rest; + return result; } explain(userKey, relation, objectKey, options = {}) { diff --git a/tests/rigor/rule-kind-partial-parity.test.js b/tests/rigor/rule-kind-partial-parity.test.js index 2b5538a..17983b1 100644 --- a/tests/rigor/rule-kind-partial-parity.test.js +++ b/tests/rigor/rule-kind-partial-parity.test.js @@ -38,7 +38,7 @@ function mkArbiter(opts = {}) { describe('Rule-kind × partial-graph parity (rigor)', () => { - it('FIXED MATRIX: every kind matches between persistent and partial', () => { + it('FIXED MATRIX: every kind matches between persistent and partial', async () => { // ---- direct ---- { const a = mkArbiter(); @@ -385,6 +385,63 @@ describe('Rule-kind × partial-graph parity (rigor)', () => { assert.equal(r.possibility, 1, 'challenge binary satisfied'); assert.equal(r.allow, true, 'challenge binary allow'); } + // ---- TTU multi-path fusion + reliability propagation ---- + { + const a = mkArbiter(); + a.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' }); + a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' }); + a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.8, reliability: 0.9 }); + a.addRelation('doc:0', 'owner', 'g:1', { possibility: 0.6 }); + a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7, reliability: 0.8 }); + a.addRelation('u:0', 'member_of', 'g:1', { possibility: 0.5 }); + const p = a.check('u:0', 'can_read', 'doc:0'); + assert.equal(round4(p.possibility), 0.7, 'ttu multi-path fusion persistent'); + assert.ok(typeof p.reliability === 'number' && p.reliability > 0, + `ttu reliability flows through normal check: ${p.reliability}`); + assert.ok(Math.abs(p.reliability - 0.72) < 0.01, `ttu reliability = 0.9*0.8, got ${p.reliability}`); + a.removeRelation('doc:0', 'owner', 'g:0'); + a.removeRelation('doc:0', 'owner', 'g:1'); + a.removeRelation('u:0', 'member_of', 'g:0'); + a.removeRelation('u:0', 'member_of', 'g:1'); + const r = a.check('u:0', 'can_read', 'doc:0', { + partialGraph: { relations: [ + { src: 'doc:0', relation: 'owner', dst: 'g:0', possibility: 0.8, reliability: 0.9 }, + { src: 'doc:0', relation: 'owner', dst: 'g:1', possibility: 0.6 }, + { src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.7, reliability: 0.8 }, + { src: 'u:0', relation: 'member_of', dst: 'g:1', possibility: 0.5 } + ] } + }); + assert.equal(round4(r.possibility), 0.7, 'ttu multi-path fusion partial'); + assert.ok(Math.abs(r.reliability - 0.72) < 0.01, `ttu reliability partial, got ${r.reliability}`); + } + // ---- TTU maxIntermediates circuit breaker through partial ---- + { + const a = mkArbiter(); + for (let i = 0; i < 25; i++) a.addNode('gx:' + i, 'group'); + a.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of', maxIntermediates: 5 }); + a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' }); + const rels = []; + for (let i = 0; i < 25; i++) { + rels.push({ src: 'doc:0', relation: 'owner', dst: 'gx:' + i, possibility: 0.3 + (i % 10) / 20 }); + rels.push({ src: 'u:0', relation: 'member_of', dst: 'gx:' + i, possibility: 0.5 }); + } + const r = a.check('u:0', 'can_read', 'doc:0', { partialGraph: { relations: rels } }); + assert.equal(round4(r.possibility), 0.5, 'ttu circuit breaker picks best of limited intermediates'); + } + // ---- snapshot-restored arbiter evaluates TTU ---- + { + const a = mkArbiter(); + a.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' }); + a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' }); + a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.8 }); + a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7 }); + 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 p = restored.check('u:0', 'can_read', 'doc:0'); + assert.equal(round4(p.possibility), 0.7, 'snapshot-restored TTU'); + } // ---- binary mode agrees with normal at the same threshold ---- { const a = mkArbiter();