@credence/core
v0.6.0
Published
Credence core: the knowledge ledger — claims, evidence, sources, entities, cases.
Readme
@credence/core
The knowledge ledger: claims (with epistemic status), evidence (with locators), sources, entities (with resolution/merge), cases, and decisions. Append-only and content-addressed, so idempotency, supersession, and byte-deterministic rendering come for free. Runs on PGlite (zero-infra) or Postgres.
import { createLedger } from "@credence/core";
const ledger = await createLedger({ ontology }); // in-memory PGlite by default
const kase = await ledger.openCase({ title: "Acme diligence" });
const acme = await ledger.resolveEntity({ type: "company", label: "Acme Inc." });
await ledger.recordClaim({
caseId: kase.id,
subjectEntityId: acme.id,
key: "total_debt",
value: 410_000,
status: "verified", // 'verified' REQUIRES evidence
evidence: [{ source: { kind: "document", uri: "balance.pdf" }, locator: { page: 4 } }],
});
// "we checked, nothing there" — distinct from "never checked"
await ledger.recordAbsence({
caseId: kase.id, subjectEntityId: acme.id, key: "lawsuits",
evidence: [{ source: { kind: "query", uri: "court-registry" } }],
});
console.log(await ledger.renderCaseIndex(kase.id)); // byte-deterministicEvidence stance
An evidence row records how the source bears on what it grounds, not only that it is attached:
await ledger.recordClaim({
caseId: kase.id, subjectEntityId: acme.id, key: "total_debt", value: 410_000,
status: "verified",
evidence: [{ sourceId: doc.id, locator: { page: 4 }, stance: "supports" }],
});supports / contradicts / cited. Omit the field to leave it UNSTATED —
never defaulted to supports, and never backfilled on rows written before 0.5.0.
cited is the one that earns the enum: the agent read this and it did not bear
on the claim. Stance is inside the evidence identity, so the same passage under
two stances is two rows, not one mutated row. Credence never infers a stance.
verified still requires evidence, and now also refuses when every attached
piece explicitly declines to support. Unstated evidence satisfies the guard
exactly as it always has.
Migrations
createLedger() migrates on open. Schema changes ship as ordered migrations
recorded in _credence_migrations — not as CREATE TABLE IF NOT EXISTS, which
silently skips a table whose shape changed.
import { migrate, pendingMigrations, openDb } from "@credence/core";
const handle = await openDb({ postgres: process.env.DATABASE_URL });
await pendingMigrations(handle); // -> ids not yet applied
await migrate(handle); // idempotent; safe on every bootMigrations are embedded in TypeScript rather than read from a folder: this
library runs in-process and gets bundled downstream, where a runtime
migrations/ directory silently stops resolving. Author new ones with
pnpm db:generate (drizzle-kit) and paste the SQL into src/migrations.ts.
Relations
Entities are linked by typed, evidenced relations — append-only and content-addressed exactly like claims:
await ledger.recordRelation({
caseId, subjectEntityId: alice.id, predicate: "spouse_of", objectEntityId: bob.id,
status: "verified",
evidence: [{ source: { kind: "document", uri: "marriage.pdf" }, locator: { page: 2 } }],
});The predicate is HARD-validated against ontology.relationPredicates (an
empty list means discovery mode). This is deliberately stricter than claim keys,
which are soft-normalized through aliases: detection rules key off predicates, so
silently accepting married_to where a rule expects spouse_of would stop that
rule firing with no error anywhere. Catch UnknownPredicateError per item when
ingesting model output so one bad predicate doesn't sink a batch.
Obligations — what is still owed
Every other object records what is true. An obligation records what is still owed — a condition attached to a decision.
const o = await ledger.obligations.open({
caseId, title: "Register the lien", blocking: true, by: "committee",
});
await ledger.obligations.transition(o.id, "done", {
by: "analyst",
evidence: [{ source: { kind: "document", uri: "lien.pdf" }, locator: { page: 1 } }],
});
await ledger.obligations.blockers(caseId); // still-open blocking obligationsIt earns a place in the core because its governing rule is epistemic, not
domain-specific: done requires evidence. Marking something fulfilled with
nothing to point at is the same unsourced claim the ledger refuses everywhere
else — and it is the one that most tempts people, because closing an item feels
like progress. Waiving requires a reason, illegal transitions are rejected rather
than silently accepted, and every move appends to a transition log.
An obligation is not just stored — it is visible. It appears in the Case Index
under Owed, in next_steps, in the snapshot payload (so a pre_decision
snapshot records what was still owed, not only what was known), and it is
addressable as credence://obligation/…. Something an agent can never see is the
same as something you never recorded.
Human oversight — advisory, and never a freeze
Three primitives, no policy: a declared intervention point, a per-case hold, and an advisory autonomy limit.
await ledger.oversight.points.declare({ id: "pre-decision", required: true });
await ledger.oversight.points.list({ required: true }); // what a case must satisfy
await ledger.oversight.hold({ caseId, by: "analyst", reason: "…", pointId: "pre-decision" });
await ledger.oversight.state(caseId); // { held, holds, since, by, actorKind, reason, pointId, ref }
await ledger.oversight.release({ caseId, by: "analyst", reason: "…" });
await ledger.oversight.setAutonomyLimit({ caseId, maxIterations: 5, by: "analyst" });
await ledger.oversight.recordIteration({ caseId, by: "agent:mcp" });
await ledger.oversight.autonomy(caseId); // AutonomyReport | null — null when no ceiling was declared
await ledger.oversight.blockers(caseId); // OversightBlocker[]A hold never refuses a write. A ledger that stopped recording during a stop
would destroy the facts the human was called in to weigh, and the write that
matters most is often the one made while someone is looking. What a hold changes
is what every read says — the Case Index leads with a banner, next_steps
leads with the stop, the snapshot payload carries it — plus one machine-readable
signal, blockers(), on the same contract obligations.blockers() already has.
Credence reports; the caller enforces.
autonomy() has three states, not two, and returns null when no ceiling was
ever declared. within and exceeded are the obvious ones; unreported means
a ceiling was declared and no iteration was ever recorded against it — the loop
was never instrumented. It is a blocker, and it is rendered without a fraction,
because 0/5 reads like a comfortable zero when the truth is that nobody counted.
Four blocker reasons — held, autonomy_exceeded, autonomy_unreported,
required_point_unsatisfied — each carrying a printable detail. required on
a point is mechanism, not policy: it says a hold must cite this point before
the case reads as clear, and says nothing about who acts or under which rulebook.
Credence ships zero points, and a catalogue with no required points behaves
exactly as if the feature were absent.
A pointId is hard-validated on every write: citing a point that was never
declared throws, and nothing is written. That is deliberately unlike relation
predicates, where an unrecognised predicate is still recorded — an unusual
predicate is a true statement about the world, while a hold citing a point that
does not exist is a false statement about governance.
Holds and releases are attributed acts, so they are on the audit chain as
oversight_event. Iteration markers are not: a counter is not an act by anyone.
The audit chain — tamper-evidence and attribution
Every claim, relation, decision and obligation transition appends one entry to a
per-case, append-only hash chain: dense seq from 1, each entry hashing the one
before it. An entry stores the object's content hash, not its values — the
chain proves the sequence of writes, and never becomes a second copy of the
ledger you would then have to keep in step.
await ledger.chain.list(caseId); // the entries, in order
await ledger.verifyChain(caseId);
// { status: "ok", entries: 14, head: { seq: 14, entryHash: "…" }, witnessed: false }verifyChain recomputes every hash, re-walks the links, and checks that every
object in the case is covered. It returns one of three verdicts — ok, broken
(with the failing seq and one of nine reason codes) or indeterminate — and
persists nothing: a stored verdict is a claim about the past that the reader
has to trust, which is the thing this chain exists to avoid. There is no backfill,
so a case written before chaining existed reads indeterminate forever, never
ok.
The chain also answers who. Pass assertedBy on a write (or set the ledger's
actor) and the identity lands on the entry — and, for a claim, on the claim
itself as assertedBy/modelVersion, which is the first asserter and costs no
chain walk to read. Attribution is deliberately outside
content-addressed identity: the same fact asserted by two actors stays one claim
with two chain entries, rather than forking into two claims.
const checkpoint = await ledger.chain.checkpoint(caseId);
// { caseId, seq, headHash, at, algorithm: "sha256" } — null if the case has no entries
await ledger.verifyChain(caseId, { witness: checkpoint }); // witnessed: truecases.chain_head_seq/chain_head_hash anchor the head, but an attacker who can
write to the database can rewrite that anchor and re-derive every hash after the
edit. That is inherent to a chain living entirely inside the data it attests —
and it is precisely why checkpoint() exists: keep the exported head somewhere
Credence cannot reach and pass it back as witness, and a truncation that the
chain and its local anchor both agree on is caught (truncated_below_witness).
Credence does not notarize, sign, or timestamp the checkpoint; where it lives is
your control, not this library's.
Key ideas
- Content-addressed identity.
id = <prefix>_<hash(meaning)>. Same assertion → same id → idempotent no-op. A different value → a different claim, never a mutation. - Evidence guard.
status: "verified"without evidence throws. Evidence that is attached and explicitly and unanimously non-supporting (contradicts/cited) throws too, withEvidenceRequiredError.reasonnaming which. Unstated evidence still satisfies the guard. unknownvsabsent.recordAbsencerecords evidenced absence via theABSENTmarker (isAbsent(value)), which the matrix and diff treat distinctly.credence://refs. Every object is addressable;resolveRefpowers progressive disclosure (snapshots resolve to meta + counts only).- Evidence is polymorphic. It grounds a claim or a relation — both are
assertions, both are worthless without provenance.
listEvidence(claimId)still works; pass{ type: "relation", id }for the other side. - Merges keep their promises. Claims and relations re-point on read; findings are migrated explicitly (their identity embeds the entity id) so a merge never resurrects a dismissal a human already closed.
- Injected ontology. Claim-key catalog (with aliases), entity types, scheme — all yours. The core carries no business vocabulary.
Postgres & pgvector
The same ledger runs on PGlite (dev/test, zero-infra) and Postgres (prod) — one
code path. Opt into embeddings with vector: { dimensions } and Credence installs
the vector extension and an embeddings table on whichever driver you use.
// Zero-infra dev, with semantic search — pgvector runs inside PGlite:
const dev = await createLedger({ ontology, vector: { dimensions: 1536 } });
// Production — identical API, real Postgres (needs the optional `pg` peer dep):
const prod = await createLedger({
postgres: process.env.DATABASE_URL, // e.g. pgvector/pgvector:pg16
ontology,
vector: { dimensions: 1536 },
});
await prod.embedClaim(claim.id, { model: "text-embedding-3-small", vector });
const hits = await prod.similarClaims(queryVector, { caseId, limit: 5 });
// → [{ claim, similarity, distance }, ...] (cosine by default)Embeddings are addressed by (targetType, targetId, model), so re-embedding is an
idempotent upsert. Vectors are entirely opt-in — omit vector and nothing about the
zero-infra path changes.
Exports the Drizzle schema at @credence/core/schema.
