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.
- Duration literals now accept s/m/h/d/w (was m/h/d/w): 'BEHAVES { ttl 30s }'
is 30s, and 'within 30s' temporal expressions parse.
- Definition fields are REQUIRED by default ('field: type'); 'field: type?'
marks a field optional. addNode enforces presence on insert, getSchema
exposes per-field requiredness, and updateNodeData still validates the
provided fields' types.
- Provider-returned edges are validated against the fact's declared typing:
a value-carrying fact must return { value, possibility } with a value of the
declared type (bare-number shorthand is rejected); a non-value fact must not
carry a value; and every possibility must lie in [0, 1]. Violations throw a
clear provider-authoring error instead of silently injecting malformed edges.
Tests: DSLRuntimeTyping (duration units, required-field enforcement, schema
requiredness, value-type + shape + possibility validation).
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.
The higher-order DSL+Core wrapper now covers the full contract the DSL
informs, beyond the typed mutations already present:
- getSchema(): serializable introspection of the compiled type system —
entity types/fields, facts (params + injectable flag), evidence (with
transitive dependsOn), and registered providers. relationNames() lists all
declared relations. (The DSL's type system was always present; this exposes
it programmatically.)
- registerFact(relation, fn) / unregisterFact / registeredFacts: per-relation
async providers that retrieve missing partial-graph edges; per-check
factProviders merge OVER registered ones.
- Bounded fixed-point provider retrieval loop (maxProviderRounds): each round
invokes providers for required facts whose edges are not yet injected. A
provider may return edges for relations other than its own — those satisfy
the other required facts and can unblock later rounds.
- check() now type-validates FACT relations too (not just evidence); edge
normalization preserves a provider edge's own relation name.
- removeNode / removeRelation passthroughs; require() throws on denial for
middleware.
- Field typing extended to the DSL's full value-type universe
(timestamp/duration accept number or string; object/any accept anything).
Tests: DSLRuntimeExt (schema, registration, merge, fixed-point, require,
removal, fact-check validation, timestamp typing).