Engineering specification for GapTime, bi-temporal knowledge-graph memory. This document is the binding contract for the public surface, the temporal semantics, the SemVer policy, and the threat model. Where this document and the code disagree, the code is the bug.
GapTime never talks to any provider network API. It stores facts the host already has, answers time-travel and retrieval queries over them, and accounts for every version. The host stays in full control of its own API calls, credentials, and transport.
Consequences, by design:
- Zero runtime dependencies. No provider SDK, no database driver, no model enters the dependency graph; the package is a pure-logic store.
- Zero credentials. GapTime never sees an API key, so it can never leak one.
- Runs everywhere. Node, browsers, edge runtimes, and workers run the same engine; nothing touches sockets or platform globals (the optional file state backend and the CLI are the only Node-bound surfaces, isolated in their own entries).
- Determinism. GapTime never calls the global random generator. Entity, fact, and episode ids derive from a seeded counter (
options.seed, default 7), and the wall clock is read only where a timestamp is part of the public record, always injectable throughoptions.clock.
One engine (createGapTime) wires the stateful core. Each module is pure or self-contained; the core decides every interaction.
| Component | Module | Responsibility |
|---|---|---|
| Intervals | src/temporal/intervals.ts |
Allen interval algebra over half-open valid-time intervals; overlap and containment predicates |
| Hashing | src/stats/hash.ts |
Deterministic FNV-1a 64 statement keys, combined keys, seeded short ids |
| Retrieval | src/retrieval/bm25.ts |
In-memory Okapi BM25 over an inverted postings index |
| Engine | src/core/createGapTime.ts |
The bi-temporal log, entity store, contradiction detection, time-travel queries, traversal, hybrid search, audit trail, telemetry, persistence |
| State | src/state/{memory,file,kv}.ts |
Snapshot persistence behind the StateBackend interface |
| Store | src/store/index.ts |
Structural write-through adapters for external Neo4j, Postgres, and SQLite |
The core owns the append-only version log, the current-belief indexes (by subject-predicate, by subject, by object, by statement hash), the supersession ledger (for heal-on-retract), the classification sets (superseded, retracted), the audit log, telemetry fan-out (synchronous, listener exceptions swallowed), and defensive snapshot restore (a corrupt snapshot degrades to a fresh engine, never to a crash).
assert(input) executes, in order: validate input, resolve the subject and object entities (create on inline declaration), resolve object kind, default the valid interval, derive the statement key (FNV-1a 64 of subject, predicate, object token), consolidate same-object overlapping current facts into the union interval, detect contradictions against different-object current facts for single-cardinality predicates, write the immutable fact version, register it in the current-belief indexes and BM25, push the fact.asserted audit event, fire external hooks defensively, and emit telemetry. The same input, options, and seed always produce the same ids and statement keys.
Every fact is a statement subject predicate object annotated with two half-open intervals and immutable once written.
| Field | Axis | Meaning |
|---|---|---|
validFrom |
valid time | Inclusive start; when the fact became true in the world |
validTo |
valid time | Exclusive end, or null for still-valid (open end) |
txFrom |
transaction time | Inclusive start; when GapTime recorded the fact |
txTo |
transaction time | Exclusive end, or null for current belief; closing it ends a belief without erasing it |
Half-open semantics: [from, to) includes the start and excludes the end, so a fact ending exactly where another begins does not overlap (the meets Allen relation). Open ends normalize to positive infinity for interval math. The two axes are queried independently or jointly; asOf(t) filters valid time, reconstructAsOf(t) filters transaction time, query takes both coordinates, history filters neither.
Predicate cardinality drives detection. single (the default): at most one object is true per subject and predicate at any instant; a different object over an overlapping valid interval is a contradiction. multi: many objects coexist; distinct objects never contradict. Cardinality comes from a registered PredicateSchema, else options.defaultCardinality, else single.
On a contradiction, the engine closes the older fact's belief on the transaction axis, writes clipped current-belief versions for any surviving prefix [olderFrom, newerFrom) and suffix [newerTo, olderTo), records the Contradiction with the Allen relation between the two valid intervals, and emits fact.invalidated plus contradiction.detected. A fact contained inside the older one (the during relation) therefore yields both a prefix and a suffix clip; the surviving tail is never silently destroyed.
The thirteen Allen relations are exposed as a typed enum (AllenRelation): before, after, meets, metBy, overlaps, overlappedBy, starts, startedBy, during, contains, finishes, finishedBy, equals. Classification is pure endpoint comparison, O(1) per pair, a tractable point-based subalgebra; full constraint propagation over arbitrary Allen networks is NP-complete and GapTime never needs it.
Same-object handling: asserting the same statement over an overlapping or adjacent valid interval is a restatement, merged into the union interval so a subject never carries two current versions of the same object at one instant. Disjoint same-object spans stay separate. Object identity is one canonical token (objectKind, runtime type, string value) used by both the statement key and the contradiction comparator, so a number 41 and a string "41" are never conflated by one path and distinguished by the other.
Hashing is FNV-1a 64-bit over UTF-16 code units (two bytes per unit, low byte first), reference offset basis and prime per Fowler, Noll, and Vo, rendered as 16 lowercase hex characters. combineKeys(parts) joins parts with the ASCII unit separator U+001F, which cannot occur in hex output, so part boundaries always contribute to the digest and concatenation ambiguity is impossible.
The statement hash fuses subject id, predicate, and the canonical object token. Two assertions of the same triple share the hash regardless of time or actor, which drives deduplication, same-object merge, and contradiction grouping. Entity ids are e-prefixed, fact ids f-prefixed, episode ids ep-prefixed, each a base36 digest of the seed and a monotonic counter, never the platform UUID.
Non-cryptographic by declaration: FNV-1a is trivially invertible and collision-constructible by an adversary. Keys exist solely so byte-equal inputs get equal bookkeeping keys; they must never gate authentication, authorization, or integrity. Non-adversarial collision odds follow the birthday bound, roughly n^2 / 2^65, about 1 in 37 million at one million distinct statements; a collision costs at worst one misattributed deduplication, never a wrong answer to a temporal query.
GapTime never deletes. Two operations close intervals, and both stay reconstructable.
supersession closes the older fact's VALID interval over the contradicted span,
writes clipped current-belief versions for the surviving prefix and
suffix, and ends the older belief on the transaction axis. The
superseding fact and the clips it produced are recorded in a ledger.
retraction closes a fact's TRANSACTION interval ("recorded in error") and heals
the supersession it caused: each surviving clip is closed and the
overridden originals are reopened over their full valid intervals as
fresh current versions. Past reconstructions are unaffected because
every prior version remains in history.
A reopened or clipped version is a new immutable fact with its own id and a fresh txFrom; the originals stay in history with their belief closed. reconstructAsOf(t) returns every version whose transaction interval contains t, which is exactly the belief set held at t, regardless of any later supersession or retraction.
Reads default to "valid now, believe now". selectFacts filters the version log on two axes: transaction (when asOfTransactionTime is set, keep versions whose [txFrom, txTo) contains it, else keep current belief txTo === null) and valid (when not including invalidated, keep versions whose [validFrom, validTo) contains the valid coordinate, defaulting to the clock).
asOf(t, pattern?): facts valid att, believed now.reconstructAsOf(t, pattern?): facts believed at transaction-timet, including invalidated, the compliance reconstruction.query(pattern?, options?): both axes independently, with limit.history(pattern?): every version, unfiltered.neighbors(entityId, options?): breadth-first graph traversal up to a hop depth, honoring valid time and an optional predicate and direction filter.
Every query emits a query.executed telemetry event carrying the matched count and the mode.
search(text, options?) fuses three signals by reciprocal rank fusion (score += 1 / (60 + rank) per signal), restricted to facts eligible at the requested time coordinates:
- Lexical. In-memory Okapi BM25 (
k1 = 1.5,b = 0.75) over an inverted postings index, so a query scores only documents containing a query term, not the whole corpus. Documents are facts rendered as subject name, predicate words, object text. - Graph. N-hop traversal around the lexical and vector seeds, honoring valid time.
- Vector (optional). A host-supplied
VectorIndexkeyed by statement hash; GapTime fuses its hits and never holds an embedding model. Absent a hook, the lexical and graph signals stand alone.
Hits carry the fused score and the set of contributing signals.
retract(factId, at?) closes a fact's transaction interval at at (or the clock), classifies it retracted, and heals any supersession it caused (section 6). Retracting an already-closed fact is a no-op. Use retraction for "this was recorded in error", and supersession (assert the successor) for "this stopped being true".
prune(beforeTransactionTime) is a retention policy: it drops fully-closed fact versions whose belief ended at or before the cutoff, and audit events recorded before it, while keeping every current belief. It bounds the in-memory footprint of a long-running store at the cost of reconstructability before the cutoff. Choose a cutoff that respects retention obligations (EU AI Act Article 26 requires deployers to keep logs at least six months). It returns the number of versions removed.
Provenance is mandatory on every fact: source, ingestion timestamp, content hash, and optional actor and episode id. It is required at once by the runtime ingestion conventions (MCP source, OpenTelemetry span identity), by EU AI Act Article 12 paragraph 3 (who, what, when), and by ISO/IEC 42001 A.6.2.8 event logging, and it is what lets a host retract every fact from a later-distrusted source.
auditTrail(options?) projects the transaction-time axis into a typed event log (fact.asserted, fact.invalidated, fact.retracted, episode.ingested), each event frozen on push so the returned references cannot be tampered with. reconstructAsOf(t) answers "reconstruct what the system knew at time T". GapTime provides the record-keeping primitive these regimes build on; conformance remains the deploying organization's responsibility, and prune lets the host apply a retention cutoff. The store is not a certification.
createGapTime(options?: GapTimeOptions): GapTime, the single entry point. Zero-config default: in-memory engine, seed 7, wall clock, single-cardinality default, 100000-fact ceiling.GapTime:assert,ingest,retract,query,asOf,reconstructAsOf,history,neighbors,search,contradictions,entity,entities,resolveEntity,registerPredicate,facts,prune,stats,auditTrail,on,flush,close. The write and read methods are synchronous;ingest,search,flush, andcloseare async (extractor, vector hook, and persistence may be).GapTimeOptions:clock,seed,predicates,defaultCardinality,extractor,vector,store,state,maxFacts.- State backends:
memoryState(),fileState(options)(Node-only),kvState(kv, key?)over anyKvLike. - Building blocks:
Bm25Index,tokenize,fnv1a64,combineKeys,allenRelation,overlaps,contains,normalizeEnd,ALLEN_RELATIONS. - Errors:
GapTimeErrorwith stablecode(ERR_INVALID_INPUT,ERR_NOT_FOUND,ERR_LIMIT_REACHED,ERR_STATE_LOAD,ERR_STATE_VERSION); branch oncode, never onmessage. - Types:
Fact,FactInput,Entity,EntityRef,EntityDeclaration,Episode,EpisodeInput,IngestResult,Provenance,ProvenanceInput,PredicateSchema,PredicateCardinality,Contradiction,AllenRelation,ValidInterval,FactPattern,QueryOptions,Neighbor,NeighborOptions,SearchHit,SearchOptions,VectorIndex,Extractor,GraphStore,EntityFilter,GraphStats,AuditEvent,AuditOptions,TelemetryEvent,TelemetryListener,KvLike,StateBackend,StateSnapshot, opaqueEntityId,FactId,EpisodeId,LiteralValue,ObjectKind.
mcpEpisode(toolName, result, options?)andingestMcpResult(memory, toolName, result, options?). Structural ingestion of Model Context Protocol tool results: sourcemcp:tool:<name>, real-world instant from the provenance hint timestamp, optionalfactsmapper. Errored tool results are skipped.
gaptimeOnStepFinish(memory, options?),gaptimeOnFinish(memory, options?),ingestStep(memory, step, options?). Structural ingestion of Vercel AI SDK steps: one episode per tool result, response timestamp to valid time, ingestion to transaction time, optionalfactsmapper, optionalincludeText. Noaipackage import.
episodeFromSpan(span, options?),ingestSpan(memory, span, options?). Structural ingestion of OpenTelemetry GenAI spans: source fromgen_ai.tool.nameorgen_ai.operation.name, start time (HrTime tuple, epoch, or Date) to valid time, optionalfactsmapper. No OpenTelemetry SDK import.
memoryGraphStore(),cypherGraphStore(run),sqlGraphStore(exec, options?). Structural write-through mirror adapters for Neo4j or Memgraph (Cypher), Postgres, and SQLite. The host passes one runner or executor; GapTime imports no driver. Mirrors are write-through, not query backends, in 1.0.0.
behavioralaiBridge(memory, behavioralLike, options?): contradiction and invalidation events become behavioral observations.keymeshBridge(memory, keymeshLike, options?): retracts facts from a source onkey.rotatedandcircuit.open.racsBridge(memory, racsLike, options?)andmemorySegment(memory, options?): invalidate a racs prefix cache on belief change; render an as-of snapshot as a cacheable segment.noeticosBridge(noeticosLike, options?): returns tuned search options for the memory-search task.modelchainExtractor(modelchainLike, generate, options?): builds anExtractorwhose model is routed by modelchain and whose generation is a host function.- All shapes are local structural interfaces; no sibling package is imported at runtime or at the type level, and the siblings stay optional peer dependencies.
The full core surface minus fileState, so browser and edge bundles never advertise a backend they cannot run. No node: imports anywhere in these graphs.
Commands: help, version, assert, ingest, query, asof, reconstruct, contradictions, audit, stats, search, demo, inspect --state <path> [--watch], serve [--port] [--host] [--token] [--state] [--seed] [--cors-origin] [--insecure-no-token]. Exit codes: 0 success, 1 runtime failure, 2 usage errors. The first line of gaptime help is the tested CI contract: exactly gaptime 1.0.0.
serve endpoints: GET /healthz (no bearer required), POST /assert, POST /ingest, POST /query, POST /asof, POST /reconstruct, GET /stats, GET /contradicts, GET /audit. Posture: loopback bind by default; non-loopback refused without --token unless --insecure-no-token is passed with a loud warning; in tokenless mode every request whose Host header hostname is not loopback is answered 403 forbidden host, the DNS-rebinding defense, /healthz included; bearer compared in constant time; POST bodies must be application/json (415) and at most 1 MB (413); CORS headers emitted only when --token and --cors-origin are both set; SIGINT and SIGTERM flush state and exit 0.
The package follows SemVer 2.0.0. The public surface is everything exported from the eight entry points plus the CLI flag and exit-code contract and the serve endpoint contract.
Minor-extensible unions, declared: AllenRelation, TelemetryEvent, and the AuditEvent kind union may gain new members in minor versions. Consumers must tolerate unknown members: treat these unions as open when switching on them, and carry a default arm. The same applies to GapTimeError.code values and to new discriminants on discriminated unions.
- Patch: bug fixes, internal refactors, documentation, dependency-free tooling.
- Minor: new optional exports, new optional fields, new union members per the rule above, new CLI flags with defaults preserving current behavior, new optional snapshot fields.
- Major: renaming or removing an export, changing a signature, changing the meaning of an existing field or relation, changing the
StateSnapshotlayout incompatibly (the snapshot carriesversion: 1and the loader rejects unknown versions withERR_STATE_VERSION), removing a CLI flag, changing an exit code.
The TeleologHI provider, a managed conversational extractor, in-process default vectors, branching and multi-agent namespacing, a contradiction resolver, and an MCP server surface are reserved for later versions.
tests/integration/scenario-benchmark.test.ts is the permanent correctness contract: ten labeled bi-temporal scenarios with hard bounds pinning behavior in CI forever. A regression on any bound must fail the suite. Every scenario runs on an injected clock and fixed seed.
| ID | Contract | Bound |
|---|---|---|
| S1 | Supersession | A different object for a single-valued predicate over an overlapping interval closes the older fact and records the Allen relation |
| S2 | Suffix preservation | A contained fact (during) keeps the surviving prefix and suffix of the older fact; the tail does not vanish |
| S3 | Valid-time travel | asOf returns the object valid at the instant, before and after a supersession |
| S4 | Transaction-time travel | reconstructAsOf before a write returns the prior belief with its open interval |
| S5 | Heal on retract | Retracting the superseding fact restores the overridden belief and closes the clips |
| S6 | Same-object merge | Overlapping same-object assertions merge to one current version; disjoint spans stay separate |
| S7 | Object identity | A number and a same-spelled string are distinct objects and contradict |
| S8 | Audit immutability | auditTrail events are frozen; classification and ledger round-trip a reload |
| S9 | Determinism | Same seed and call sequence on two engines: deeply identical ids and statement keys |
| S10 | Hybrid retrieval | search fuses lexical, graph, and optional vector signals, filtered to the time coordinates |
The full test baseline is 127 tests across 16 suites on Node 22 and Node 24; coverage about 92 percent statements, 95 percent lines.
- In-memory working set. The graph lives in the process heap (roughly 2 kB per fact version) and nothing is freed automatically.
asOfandreconstructAsOfare linear scans;searchuses an inverted index but grows with the corpus. Applyprunefor retention and mirror to a database via the store adapters beyond a bounded working set. maxFactsis a hard ceiling, not eviction. Past the limit,assertthrowsERR_LIMIT_REACHED. A single-cardinality predicate writes about two versions per supersession.- No provider API calls, by design. GapTime stores what it is told; it does not verify facts against the world. The invariant is the feature.
- Extraction is the caller's. Free-text to facts is a host-supplied
Extractor; the deterministic write path takes pre-structured facts only. - Per-process state. One engine holds one store. KV persistence shares state across restarts and replicas as last-writer-wins snapshots; it is not coordination.
- Retract heals the direct supersession. Retracting a fact restores the beliefs it directly overrode; chains involving facts later superseded by a third fact are reconstructable through
reconstructAsOfbut not auto-replayed into current belief.
The full policy is in SECURITY.md.
- No credentials, ever. GapTime handles no API keys, so credential theft through this package is structurally impossible. The KV, store, vector, and sibling bridges receive ready objects; connection secrets never transit GapTime.
- Untrusted ingestion is the operator trust boundary.
assertandingesttrust the caller. Anything that can reach an authenticated serve bridge can write fabricated facts and skew the store. The host owns input validation; thekeymeshBridgeexists to retract facts from a source whose credential is later distrusted. - Host hooks are non-fatal. A throwing or rejecting
Extractor,VectorIndex, orGraphStorenever crashes the host or aborts a write; failures are swallowed and the fact is still recorded and audited. - Serve posture. Loopback by default, explicit opt-in for network exposure, tokenless Host-header validation against DNS rebinding (403
forbidden hoston non-loopback names,/healthzincluded), constant-time bearer comparison, content-type and body-size gates, CORS off unless doubly opted in, version-validated snapshot loading (ERR_STATE_VERSION), corrupt-snapshot quarantine. - Statement keys are non-cryptographic. FNV-1a 64 keys are predictable and collision-constructible; they are bookkeeping labels and are never used for authentication, authorization, or integrity.