feat: sources wired as recency-gated injectables; transitive closure, NOT scoping, recompile-scope uninstall; targeted parse errors
CI / test (push) Successful in 20s
CI / publish (push) Has been skipped

BEHAVES AS transitive now emits bounded multi_hop configs (direct checks and
evidence references), fixing a silent no-op. NOT builds keep _subjectAsObject
scoping so unary predicates negate the right node, and value-typed evidence
objects gate by exact edge value. Recompiling a scope uninstalls its stale
relation configs (compileMultiple coexistence preserved). Sources become
injectable relations honored by requiredFacts with a within-X recency gate.
Duplicate definition fields and three common declaration mistakes (within on a
fact, two BEHAVES clauses, limit on a non-pattern body) now produce targeted
errors. Provider edges referencing unknown nodes are warned and dropped.
This commit is contained in:
John Dvorak
2026-08-03 20:27:13 -07:00
parent 4d498b07e8
commit 2a7f4c315b
10 changed files with 556 additions and 28 deletions
+29
View File
@@ -165,4 +165,33 @@ describe('DSLRuntime extended', () => {
assert.equal(missed.possibility, 0);
assert.deepEqual(missed.missingFacts, [{ relation: 'owns', reason: 'no_provider' }]);
});
it('recompiling a scope uninstalls its stale relation configs', async () => {
const rt = new DSLRuntime(new Arbiter());
rt.compile(`
definition Employee { id: string? }
definition Doc { id: string? }
fact owns(user: Employee, doc: Doc)
evidence can_read(user: Employee, doc: Doc) { owns(user, doc) }
`, 'rt-stale');
rt.addNode('u:1', 'Employee', {});
rt.addNode('doc:9', 'Doc', {});
rt.addRelation('u:1', 'owns', 'doc:9', { possibility: 1.0 });
assert.equal(rt.arbiter.check('u:1', 'can_read', 'doc:9').possibility, 1.0);
// Recompile the SAME scope with a different program: can_read/owns are no
// longer declared and must not keep granting. A second scope's relations
// would be unaffected (compileMultiple coexistence).
rt.compile(`
definition Employee { id: string? }
definition Doc { id: string? }
fact shares(user: Employee, doc: Doc)
evidence can_share(user: Employee, doc: Doc) { shares(user, doc) }
`, 'rt-stale');
const stale = rt.arbiter.check('u:1', 'can_read', 'doc:9');
assert.equal(stale.possibility, 0, 'revoked can_read must not keep granting');
assert.ok(!rt.arbiter.relationConfigs.has('can_read'));
assert.ok(!rt.arbiter.relationConfigs.has('owns'));
assert.ok(rt.arbiter.relationConfigs.has('can_share'));
});
});