1 Commits

Author SHA1 Message Date
John Dvorak ad365a65a9 feat: direct fact checks consult registered providers
CI / test (push) Successful in 19s
CI / publish (push) Successful in 9s
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.
2026-08-03 15:25:33 -07:00
3 changed files with 31 additions and 4 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@arbiter/evidence-dsl", "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.", "description": "Evidence DSL v2 compiler: translates the natural Evidence DSL (ADR-000) into @arbiter/core relation configurations.",
"license": "ISC", "license": "ISC",
"type": "module", "type": "module",
+9 -3
View File
@@ -559,7 +559,13 @@ export class DSLRuntime {
this._checkNodeType(user, meta.params[0].type, 'subject'); 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 providers = { ...this.factProviders, ...(options.factProviders || {}) };
const maxRounds = options.maxProviderRounds ?? 3; const maxRounds = options.maxProviderRounds ?? 3;
const partialRelations = []; const partialRelations = [];
@@ -577,7 +583,7 @@ export class DSLRuntime {
// Fixed-point provider retrieval loop. // Fixed-point provider retrieval loop.
for (let round = 1; round <= maxRounds; round++) { for (let round = 1; round <= maxRounds; round++) {
let newRelationsThisRound = 0; let newRelationsThisRound = 0;
for (const fact of required) { for (const fact of requiredList) {
if (satisfied.has(fact)) continue; if (satisfied.has(fact)) continue;
const factMeta = this.relations.get(fact); const factMeta = this.relations.get(fact);
const provider = providers[fact]; const provider = providers[fact];
@@ -653,7 +659,7 @@ export class DSLRuntime {
return { return {
...result, ...result,
requiredFacts: required, requiredFacts: requiredList,
providedFacts: injectedRelations.map(r => r.relation), providedFacts: injectedRelations.map(r => r.relation),
missingFacts missingFacts
}; };
+21
View File
@@ -144,4 +144,25 @@ describe('DSLRuntime extended', () => {
rt.updateNodeData('doc:9', { created: '2026-08-03T00:00:00Z' }); rt.updateNodeData('doc:9', { created: '2026-08-03T00:00:00Z' });
assert.throws(() => rt.addNode('doc:8', 'Doc', { created: {} }), /must be timestamp/); 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' }]);
});
}); });