
Bitemporal time-travel + truth-maintenance-style provenance retraction on Postgres/SQLite (open-source TS graph library)
I just shipped bitemporal provenance for TypeGraph, my open-source graphs-on-SQL library. Three pieces, usable independently but most powerful together:
- Valid time: when a fact was true in the world (an invoice's effective date, a role grant's window).
- Recorded/system time: when the system captured that fact (what you knew, as of a commit instant; the SQL:2011
FOR SYSTEM_TIME/ Datomic system-time axis). - Provenance: why the system still believes a derived fact, and what happens downstream when a source it depended on turns out to be wrong.
Derived facts are the annoying case that surfaces the issue(s) these primitives solve. For example, a Vulnerability node exists because a scanner and a vendor advisory both pointed at it. The graph concluded it; nobody asserted it directly.
ScannerSource ──┐
├──▶ Vulnerability (CVE-2026-1234, libvector)
VendorSource ──┘
So when the scanner turns out to be garbage, you can't treat retracting it as a delete. The vendor might still back that vulnerability. The scanner might have been the only thing propping up a bunch of other facts. You want the graph to sort out which.
What you want: retract a source and it recomputes which derived facts still have grounded support. Retract the vendor too and the vulnerability finally goes non-current, and a "block the deploy" decision sitting on top of it goes with it.
The behavior, then the theory
A fact stays believed while it has at least one justification whose premises are all still supported. Premises bottom out at sources. Retract a source and every justification that leaned on it stops counting; a fact loses currency only once it runs out of surviving justifications.
const provenance = createRetractionCapability(store, {
source: { kinds: ["ScannerSource", "VendorSource"] },
justification: { kind: "Justification" },
fact: { kinds: ["Vulnerability", "DeployDecision"] },
premiseOf: { kind: "premiseOf" },
derives: { kind: "derives" },
});
const report = await provenance.retract({ kind: "VendorSource", id: vendorId });
// report.died: facts that lost all grounded support
// report.survivedVia: facts that still have an alternate justification
This is modeled on truth-maintenance systems. The storage follows the JTMS shape (Doyle 1979, "A Truth Maintenance System"): AND-justifications over premises, sources at the bottom, a fact in the well-founded support set only if some justification has all its premises supported. I use the monotonic, inlist-only fragment, so this is the easy part of Doyle's system; the hard part, non-monotonic belief revision, isn't here. The question retract actually answers, "which facts survive because an alternate justification still holds," is the ATMS question (de Kleer 1986): which combinations of sources hold each fact up. So it's JTMS-shaped storage with an ATMS-flavored query.
Retraction is a normal write, so you get replay for free
Retraction doesn't hard-delete. It recomputes support and flips unsupported facts to non-current, leaving the justification edges in place so you can still see why something used to be believed. Because that write lands on TypeGraph's recorded-time (system-time) substrate, you can replay the belief transition:
const before = await store.recordedNow();
await provenance.retract(badSource);
const after = await store.recordedNow();
await store.asOfRecorded(before).nodes.Vulnerability.getById(id); // believed
await store.asOfRecorded(after).nodes.Vulnerability.getById(id); // not current
TypeGraph tracks both temporal axes as explicit read lenses, valid time ("when true in the world") and recorded time ("when the database learned it"), and because they're lenses they compose:
store.asOf(validTime).asOfRecorded(recordedTime)
Architecture
No engine-native temporal tables. Postgres needs an extension for system-versioning and SQLite has nothing, so TypeGraph stores history explicitly and reconstructs point-in-time views in the query compiler. That's why one implementation runs on both backends.
Limits
- Only TypeGraph-managed writes are captured. Raw SQL bypasses it; this isn't a database-level CDC/audit layer.
- No backfill. Enable history on a fresh graph.
- Point-in-time reads reconstruct from history relations, so they're slower than current-state reads. It's an audit tool, keep it off hot paths.
- Per-write overhead runs ~2.5–6x unless you batch writes in one transaction, where it drops to ~1–1.5x.
A naming note
My asOf is valid time, the reverse of SQL:2011 FOR SYSTEM_TIME AS OF and Datomic (d/as-of db t), where a bare as-of is system time. Valid-time reads are the common case here so they took the short name; system time is asOfRecorded.
I'd love to compare with other systems that handle provenance retraction, or truth maintenance generally, modeled directly on ordinary SQL tables instead of a dedicated reasoning engine. There's plenty of JTMS/ATMS literature but not much on mapping it onto relational storage. Pointers welcome.
GitHub: https://github.com/nicia-ai/typegraph Docs: https://typegraph.dev/provenance
Examples: https://typegraph.dev/examples/provenance-retraction/ https://typegraph.dev/examples/bitemporal-time-travel/