@credence/rules
v0.6.0
Published
Credence findings engine: gap | conflict | risk detection with persisted lifecycle.
Readme
@credence/rules
The findings engine. Detection is derived; lifecycle is persisted. Declarative
rule specs (policy in data) plus registered detectors (mechanism in code) recompute
what's true now; reconcile moves each finding through open → resolved → dismissed.
import { createRulesEngine } from "@credence/rules";
const rules = createRulesEngine(ledger, {
rules: [
{ id: "need-cnpj", kind: "gap", keys: ["cnpj"], scope: "entity", entityType: "company" },
{ id: "conflicts", kind: "conflict" },
{ id: "high-debt", kind: "risk", key: "total_debt", op: "gt", value: 1_000_000,
severity: "blocking", title: "Debt over 1M" },
],
});
await rules.recompute(caseId); // or safeRecompute (anti-cascade)
const open = await rules.listFindings(caseId, { status: "open" });
await rules.dismiss(finding.id, { by: "analyst", reason: "n/a" });
// reopens ONLY if the finding's evidenceHash changesRule kinds
- gap — a required key is missing (
scope: "case" | "entity", optionalminStatus). - conflict — two live claims for the same subject+key disagree.
- risk — a value trips a declarative predicate (
exists,gt,in, …). - relation — an expected link between entities is missing, conditional on the
subject's own claims:
{ id: "married-needs-spouse", kind: "relation", predicate: "spouse_of", entityType: "person", requiredWhen: { key: "marital_status", op: "eq", value: "married" } } - custom — your own registered detector.
Phase gating (advisory)
An ordered phase catalogue makes minPhase mean something — without it, that
field is inert metadata since nothing can compare "diligence" to "closing".
const engine = createRulesEngine(ledger, {
phases: [
{ id: "intake", label: "Intake", order: 0 },
{ id: "diligence", label: "Diligence", order: 1 },
{ id: "closing", label: "Closing", order: 2 },
],
rules,
});
const { dueNow, later } = await engine.nextSteps(caseId);Nothing is ever filtered out — a gap gated behind a later phase is still a gap, just grouped under "later". An unknown or missing phase fails open (due now): quietly deferring real work because of a typo in a rule is worse than showing an item early.
Coverage guard
Before paying for a lookup, ask whether the ledger already answers it:
const report = await checkCoverage(ledger, {
caseId, keys: ["cnpj"], minStatus: "verified",
});
if (report.covered) console.log(report.advice);It returns a report and nothing else — no throwing, no blocking, no mutation. A
guard that blocks becomes a guard people route around, and a false "we already
know this" would stop an agent from checking something it should. minStatus
exists so a lookup whose purpose is verification isn't suppressed by the
hearsay it was meant to verify.
A contested key is not a covered one. If an open conflict finding sits on the
key, or a claim is explicitly disputed, the ledger holds more than one answer —
and the lookup under consideration is very often exactly what would settle it.
Those keys come back in report.contested, are not counted as covered, and the
advice points toward running the query rather than away from it. Reporting
"you already know this" there would talk the agent out of the one call worth
making.
Findings owned by someone else
Some findings belong to an asynchronous owner — a semantic judge, a nightly job, a human workflow. Declare those rule ids so this synchronous engine leaves them alone:
createRulesEngine(ledger, { rules, externallyManagedRuleIds: ["semantic-judge"] });Without this, reconcile resolves every finding it didn't itself detect — deleting the async owner's work on the very next write.
Why it matters
- Stable identity. A finding is
hash(caseId, ruleId, subjectKey), so its dismissal survives claim supersession. - Dismissal with memory. A dismissed finding stays dismissed across recomputes and only reopens when its evidence actually moves.
- Anti-cascade.
safeRecomputenever throws into the write that triggered it. - Policy in data.
persistRules()/loadRules()— edit detection at runtime.
Expectations (addExpectation) are known-unknowns with matchers that auto-resolve.
