diff --git a/src/core/Arbiter.js b/src/core/Arbiter.js index d08a466..6e7c100 100644 --- a/src/core/Arbiter.js +++ b/src/core/Arbiter.js @@ -130,7 +130,13 @@ export class Arbiter { || 'deterministic', reducers: { ...(options?.partialGraphPolicy?.reducers || {}) - } + }, + // Size/reserved limits must survive construction: the partial-graph + // context reads them from the policy, and dropping them silently + // disabled configured DoS guards (defaults 1000/2000 applied). + maxNodes: options?.partialGraphPolicy?.maxNodes, + maxRelations: options?.partialGraphPolicy?.maxRelations, + reservedRelations: options?.partialGraphPolicy?.reservedRelations }; this.snapshotEnabled = false; diff --git a/src/core/arbiter/ArbiterConfig.js b/src/core/arbiter/ArbiterConfig.js index 9e349ed..e2e5208 100644 --- a/src/core/arbiter/ArbiterConfig.js +++ b/src/core/arbiter/ArbiterConfig.js @@ -62,7 +62,10 @@ export class ArbiterConfig { this.arbiter.partialGraphPolicy = { conflict_mode: conflictMode, - reducers + reducers, + maxNodes: next.maxNodes !== undefined ? next.maxNodes : current.maxNodes, + maxRelations: next.maxRelations !== undefined ? next.maxRelations : current.maxRelations, + reservedRelations: next.reservedRelations !== undefined ? next.reservedRelations : current.reservedRelations }; return this.arbiter.partialGraphPolicy; } diff --git a/tests/rigor/partial-graph-parity.test.js b/tests/rigor/partial-graph-parity.test.js index 5431639..0d587ab 100644 --- a/tests/rigor/partial-graph-parity.test.js +++ b/tests/rigor/partial-graph-parity.test.js @@ -274,6 +274,46 @@ describe('Partial-graph overlay semantics (rigor)', () => { } }); + it('POLICY LIMITS: maxNodes/maxRelations enforce and reservedRelations customizes', () => { + const four = [ + { src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.9 }, + { src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.8 }, + { src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.7 }, + { src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.6 } + ]; + const mk = (policy) => { + const a = new Arbiter(policy ? { partialGraphPolicy: policy } : {}); + a.addNode('u:0', 'user'); + a.addNode('doc:0', 'doc'); + a.setRelationConfig('can_read', { type: 'direct', relation: 'owner' }); + return a; + }; + assert.throws( + () => mk({ maxRelations: 3 }).check('u:0', 'can_read', 'doc:0', { partialGraph: { relations: four } }), + /exceeds max relations/, + 'constructor maxRelations enforces' + ); + assert.throws( + () => mk({ maxNodes: 1 }).check('u:0', 'can_read', 'doc:0', { + partialGraph: { nodes: [{ key: 'g:a' }, { key: 'g:b' }], relations: four } + }), + /exceeds max nodes/, + 'constructor maxNodes enforces' + ); + const viaSetter = mk(); + viaSetter.setPartialGraphPolicy({ maxRelations: 2 }); + assert.throws( + () => viaSetter.check('u:0', 'can_read', 'doc:0', { partialGraph: { relations: four } }), + /exceeds max relations/, + 'setter maxRelations enforces' + ); + const customReserved = mk({ reservedRelations: ['owner'] }); + const r = customReserved.check('u:0', 'can_read', 'doc:0', { + partialGraph: { relations: [{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.9 }] } + }); + assert.equal(r.possibility, 0, 'custom reservedRelations skips owner'); + }); + it('PROPERTY CAMPAIGN: direct/chain overlay parity under persistent+partial mutations', async () => { const result = await rigor.campaign( [rigor.object('graph', makeWrapper, [