From ad365a65a98a8976280611f5d933f5c9dc87a7d5 Mon Sep 17 00:00:00 2001 From: John Dvorak Date: Mon, 3 Aug 2026 15:25:33 -0700 Subject: [PATCH] feat: direct fact checks consult registered providers Checking a FACT relation directly (rt.check('u:1', 'owns', 'doc:9')) now runs the provider-retrieval pipeline instead of returning 0 without ever consulting the registered provider. The retrieval set for a direct fact check is the fact itself (plus, for evidence checks, the injectable deps as before). The result reports requiredFacts/providedFacts/missingFacts for the fact, and missingFacts surfaces 'no_provider' when neither a provider nor an edge can satisfy the check. --- package.json | 2 +- src/runtime/DSLRuntime.js | 12 +++++++++--- tests/DSLRuntimeExt.test.js | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 3ba7c63..1f04eb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@arbiter/evidence-dsl", - "version": "1.7.0", + "version": "1.8.0", "description": "Evidence DSL v2 compiler: translates the natural Evidence DSL (ADR-000) into @arbiter/core relation configurations.", "license": "ISC", "type": "module", diff --git a/src/runtime/DSLRuntime.js b/src/runtime/DSLRuntime.js index 9053092..d93f984 100644 --- a/src/runtime/DSLRuntime.js +++ b/src/runtime/DSLRuntime.js @@ -559,7 +559,13 @@ export class DSLRuntime { this._checkNodeType(user, meta.params[0].type, 'subject'); } - const required = this.requiredFacts(relation); + // Retrieval set: for an evidence, the injectable facts it depends on; for + // a direct FACT check, the fact itself is the retrieval target (its + // provider, if registered, supplies the edge — checking `owns` directly + // must consult the `owns` provider, not only evidence-mediated checks). + const required = new Set(this.requiredFacts(relation)); + if (meta && meta.kind === 'fact') required.add(relation); + const requiredList = [...required]; const providers = { ...this.factProviders, ...(options.factProviders || {}) }; const maxRounds = options.maxProviderRounds ?? 3; const partialRelations = []; @@ -577,7 +583,7 @@ export class DSLRuntime { // Fixed-point provider retrieval loop. for (let round = 1; round <= maxRounds; round++) { let newRelationsThisRound = 0; - for (const fact of required) { + for (const fact of requiredList) { if (satisfied.has(fact)) continue; const factMeta = this.relations.get(fact); const provider = providers[fact]; @@ -653,7 +659,7 @@ export class DSLRuntime { return { ...result, - requiredFacts: required, + requiredFacts: requiredList, providedFacts: injectedRelations.map(r => r.relation), missingFacts }; diff --git a/tests/DSLRuntimeExt.test.js b/tests/DSLRuntimeExt.test.js index f4aad6e..260338f 100644 --- a/tests/DSLRuntimeExt.test.js +++ b/tests/DSLRuntimeExt.test.js @@ -144,4 +144,25 @@ describe('DSLRuntime extended', () => { rt.updateNodeData('doc:9', { created: '2026-08-03T00:00:00Z' }); assert.throws(() => rt.addNode('doc:8', 'Doc', { created: {} }), /must be timestamp/); }); + + it('direct FACT checks consult the registered provider', async () => { + const rt = makeRuntime(); + rt.addNode('u:1', 'Employee', {}); + rt.addNode('doc:9', 'Doc', {}); + let calls = 0; + rt.registerFact('owns', async () => { calls++; return 0.9; }); + // Checking the fact directly (not via an evidence) must retrieve it. + const res = await rt.check('u:1', 'owns', 'doc:9'); + assert.equal(res.possibility, 0.9); + assert.equal(calls, 1); + assert.deepEqual(res.requiredFacts, ['owns']); + assert.deepEqual(res.providedFacts, ['owns']); + // Without a provider and without an edge, it reports the missing fact. + const rt2 = new DSLRuntime(new Arbiter()).compile(BASE_DSL, 'rt-fact-miss'); + rt2.addNode('u:1', 'Employee', {}); + rt2.addNode('doc:9', 'Doc', {}); + const missed = await rt2.check('u:1', 'owns', 'doc:9'); + assert.equal(missed.possibility, 0); + assert.deepEqual(missed.missingFacts, [{ relation: 'owns', reason: 'no_provider' }]); + }); });