Temporal Version History
GestaltDB can append immutable node and edge versions alongside its existing mutable graph. Temporal versions use two distinct time dimensions:
valid time is the half-open interval
[start, end)in which a fact is true in the modeled domain;system time is the monotonically increasing UTC instant at which GestaltDB committed that version.
The explicit version APIs do not update records returned by get_node or
get_edge and do not alter Cypher or sampling behavior. Temporal history has
separate indexes for exact versions, logical entities, valid starts, system
times, and typed outgoing/incoming traversal.
Assertions, Corrections, and Retractions
from gestaltdb.graphdb import Edge, Node
from gestaltdb.temporal import TemporalInterval
alice = graph.put_node_version(
Node(node_id="alice", labels=["Person"], properties={"name": "Alice"}),
valid=TemporalInterval.parse("2020-01-01T00:00:00Z"),
metadata={"source": "people-import"},
)
corrected = graph.correct_node_version(
Node(node_id="alice", labels=["Person"], properties={"name": "Alicia"}),
supersedes_version_id=alice.version_id,
)
graph.retract_node_version(
"alice",
valid_from="2030-01-01T00:00:00Z",
supersedes_version_id=corrected.version_id,
reason="source withdrew the assertion",
)
Corrections and retractions append records. They never modify or physically
delete the superseded payload. Omitting valid from a correction inherits the
superseded version’s interval.
Atomic Logical Batches
Use write descriptors from gestaltdb.versioning to publish several
versions under one commit ID:
from gestaltdb.versioning import EdgeVersionWrite, NodeVersionWrite
commit = graph.commit_versions([
NodeVersionWrite.assertion(Node(node_id="acme"), (0, None)),
EdgeVersionWrite.assertion(
Edge(
edge_id="employment-1",
source="alice",
target="acme",
properties={"type": "WORKS_FOR"},
),
(0, None),
),
], metadata={"source": "hr-import"})
Transactional LMDB and transactional PyRex publish the complete batch in one backend transaction. All filesystem-backed stores reserve commit IDs through a durable sidecar sequence while holding a database-wide writer lock. LevelDB and default PyRex then write the descriptor and records and publish a visibility marker last. Records without a valid marker remain invisible. Reservations are never reused, including when an outer backend transaction rolls back, so visible commit IDs may contain gaps.
list_temporal_orphans() reports empty reservation gaps and validated
incomplete publications. reclaim_temporal_orphans(through_commit=...)
removes only unmarked artifacts through an explicit inclusive horizon, rebuilds
derived indexes, and never reuses reclaimed commit IDs. Use the recovery
procedure in Compatibility, Migration, and Recovery; malformed unpublished data fails closed.
As-Of Resolution and Traversal
get_node_version and get_edge_version retrieve visible versions by UUID.
iter_node_versions and iter_edge_versions return append order and accept
an optional system-time or commit horizon. iter_temporal_commits accepts a
commit horizon.
get_node_as_of and get_edge_as_of select a logical entity at one valid
instant and optional inclusive system horizon. Eligible versions are ordered by
commit and in-commit ordinal. The newest assertion or correction supplies the
payload; a newest retraction returns None.
historical = graph.get_node_as_of(
"alice",
valid_time="2025-01-01T00:00:00Z",
system_time=corrected.system_time,
)
edges = list(graph.iter_edges_as_of(
source="alice",
edge_type="WORKS_FOR",
valid_time="2025-01-01T00:00:00Z",
))
Temporal traversal resolves each logical edge before checking its winning source, target, and type, so interval-local topology corrections and retractions do not leak historical adjacency.
Consistent Read Views
GraphDB.read_view captures the contiguous prefix of fully visible commits
and a default valid-time instant. It stops before the first incomplete commit,
so a later marker cannot change the view. Every history and as-of call through
the view uses that horizon, even if newer commits are published through the
graph handle.
with graph.read_view(valid_time="2025-01-01T00:00:00Z") as view:
alice = view.get_node_as_of("alice")
snapshot = view.build_sampler_snapshot("snapshots/2025-01-01")
token = view.provenance.token
Pass system_time=... to capture the visible prefix at a historical system
instant, or through_commit=... to select a commit directly; these options
are mutually exclusive.
The immutable ReadViewProvenance records the stable database UUID, visible
commit marker and full visibility-prefix digest, valid instant, backend layout,
serializer format, and snapshot mechanism. Its token is SHA-256 over canonical
JSON. Snapshot builds materialize nodes and edges from immutable temporal
history at that exact point,
publish through a staging directory, and store the authenticated provenance.
SamplerSnapshot.verify_source and provenance-aware node/edge hydration fail
if the source identity, serializer, backend, commit marker, or token differs.
Legacy snapshots without source provenance retain their existing unverified
behavior.
through_commit may recreate an older visible view. Database paths are not
part of provenance, so moving a managed database does not change its identity.
End-to-End Temporal Knowledge Workflow
A production workflow can keep one graph fact aligned across each read layer:
from gestaltdb import ClaimStatus, SamplerEngine, TemporalContext, TemporalInstant
# Append node and edge versions, then query the same valid instant through Cypher.
graph.put_edge_version(employment_edge, valid=(0, None))
rows = graph.query(
"MATCH (a {id: 'alice'})-[:WORKS_FOR]->(b) "
"FOR VALID_TIME AS OF datetime($at) RETURN b.id AS company",
{"at": "2024-06-01T00:00:00Z"},
)
# Export one authenticated history horizon and filter before seeded fanout.
snapshot = graph.build_sampler_snapshot(
"snapshots/knowledge", temporal=True, time_bucket="day"
)
engine = SamplerEngine(snapshot, seed=7)
sampled = engine.sample_neighbors(
[alice_compact_id], fanout=10,
temporal=TemporalContext.as_of(TemporalInstant.parse("2024-06-01T00:00:00Z")),
)
# Claims remain sourced; rules derive claims; modal evaluation returns evidence.
graph.assert_claim(
subject="alice", predicate="WORKS_FOR", object="acme",
polarity="positive", agent="source:hr", world="verified", valid=(0, None),
)
graph.create_rule(
"employment-implies-affiliation",
when=[("?p", "WORKS_FOR", "?c")], then=("?p", "AFFILIATED_WITH", "?c"),
)
graph.run_rules(as_of=0, world="verified")
graph.assert_world_accessibility(
agent="auditor", from_world="actual", to_world="verified",
kind="knowledge", valid=(0, None),
)
result = graph.entails(
"auditor",
{"subject": "alice", "predicate": "AFFILIATED_WITH", "object": "acme"},
"KNOWS", world="actual", valid_time=0, max_depth=4, max_states=100,
)
assert result.status is ClaimStatus.SUPPORTED
Temporal snapshots contain graph history, not claims. Claims, rule conclusions, truth maintenance, and modal evidence remain in the source database and share the read-view commit horizon. Use time-aware hard-negative policies described in Typed Traversal and Sampling to prevent future positives from leaking into training.
Index Maintenance
Temporal writes maintain indexes by default. Pass
index_mode=IndexMaintenanceMode.DEFER for bulk append workflows; indexed
reads then fail closed until rebuild_temporal_indexes or
rebuild_deferred_indexes succeeds. DEFER_REBUILD rebuilds before the
write returns. Rebuilds are additive and idempotent because canonical history is
append-only. Temporal node scans and untyped endpoint traversal use derived
catalog indexes in addition to logical, system, and typed-adjacency indexes. A
database predating any current temporal index format is automatically reported
with the temporal stale family and can be migrated with
rebuild_deferred_indexes.
Point-in-time candidates are intersected through valid-start and valid-end indexes before entity payloads are decoded. This preserves half-open interval semantics while avoiding hydration of expired historical versions.
Index rebuilds use copy-on-build generations. The active generation records its visible-marker checksum and commit horizon; activation is one metadata update, and prior-generation entries are reclaimed only after activation succeeds.
Migrating TimeIndexedEdge
The deprecated TimeIndexedEdge class encoded a datetime in a mutable edge
key; it is not an immutable temporal version. Use
migrate_time_indexed_edges once, with concurrent writers stopped. The method
maps successive timestamps for each logical edge ID to half-open intervals,
publishes all new versions in one temporal commit, and can optionally delete the
legacy records after publication. Validate first with delete_legacy=False;
cleanup is independently retryable.
Migration fails closed for undecodable payloads, payload/key mismatches, duplicate timestamps, or changed legacy input after a previous migration. It does not rewrite existing temporal history or snapshots. See Compatibility, Migration, and Recovery for the backup, retry, serializer, snapshot, and recovery contract.
Epistemic Claims and Provenance
Epistemic claims are immutable, sourced subject-predicate-object assertions or
denials. They use the same valid/system-time commits and read views as node and
edge versions, but remain separate from graph edges and current-state Cypher.
Entity objects are stable graph IDs; pass object_kind="literal" for a
JSON-compatible literal value.
from gestaltdb import ClaimStatus
reported = graph.assert_claim(
subject="alice", predicate="WORKS_FOR", object="acme",
polarity="positive", agent="source:hr-feed", source="hr-2024.csv",
confidence=0.97, world="reported", provenance={"row": 42},
valid_from="2024-01-01T00:00:00Z",
)
graph.assert_claim(
subject="alice", predicate="WORKS_FOR", object="acme",
polarity="negative", agent="source:investigator", source="interview-7",
confidence=0.6, world="reported",
valid_from="2024-01-01T00:00:00Z",
)
assert graph.claim_status(
"alice", "WORKS_FOR", "acme",
valid_time="2024-06-01T00:00:00Z", world="reported",
) is ClaimStatus.BOTH
Claim.statement_id is a deterministic SHA-256 identity for the proposition
only. Claim.claim_id additionally includes polarity, agent, source, and
world. Confidence and provenance are not identity fields, so
correct_claim can revise them while preserving the sourced claim identity.
Confidence is optional or a finite number from 0 through 1; it never determines
polarity. Claim payloads and provenance are canonical JSON and therefore do not
depend on the graph’s configured entity serializer.
iter_claims_as_of filters by statement, subject, predicate, object kind,
agent, source, world, and polarity. get_claim_as_of resolves one claim ID.
Both accept system_time or through_commit and use half-open valid-time
intervals. Corrections and retractions append history:
reviewed = graph.correct_claim(
reported.logical_id,
supersedes_version_id=reported.version_id,
confidence=0.99,
provenance={"row": 42, "reviewed": True},
)
graph.retract_claim(
reported.logical_id,
supersedes_version_id=reviewed.version_id,
valid_from="2025-01-01T00:00:00Z",
reason="source withdrew the assertion",
)
claim_status uses open-world four-valued semantics: SUPPORTED when at
least one matching positive claim is visible, REFUTED for negative only,
BOTH when both polarities coexist, and UNKNOWN when neither is visible.
A GraphReadView exposes the same claim lookup, iteration, and status methods
at its pinned system horizon and default valid time.
Positive Horn Rules
create_rule validates and appends immutable system-time versions of named
positive Horn rules. Predicates are constants, and every variable in the head
must occur in the body. Invalid or unsafe definitions are rejected before the
catalog changes. Reusing a rule name creates a new version; get_rule and
iter_rule_versions accept a system-time horizon.
rule = graph.create_rule(
"employment-implies-affiliation",
when=[("?p", "WORKS_FOR", "?c"), ("?c", "MEMBER_OF", "?g")],
then=("?p", "AFFILIATED_WITH", "?g"),
)
result = graph.run_rules(
as_of="2024-06-01T00:00:00Z",
max_iterations=100,
max_derivations=10_000,
max_justifications=100_000,
)
run_rules evaluates the latest version of each rule with semi-naive deltas.
Only positive entity-object claims participate. All premises in a match must
share a world, and each justification contributes the intersection of its
premises’ half-open valid-time intervals. Overlapping supports for an equivalent
conclusion are consolidated into one validity interval. Derived conclusions use the
reserved gestaltdb:rules agent identity and retain canonical justification
records containing the rule version ID and ordered premise version IDs.
Recursive finite rule sets stop at a fixpoint, and repeating an unchanged run
is idempotent.
The three positive resource limits are mandatory safeguards. Exceeding one
raises RuleEvaluationLimitError before any staged conclusion is persisted.
Successful conclusions are published together in one temporal commit and
remain separate from mutable graph edges and unqualified Cypher. Rule
evaluation does not consume negative claims, literal-object claims, or claims
from different worlds in one match.
Incremental Truth Maintenance and Explanations
maintain_truth reconciles rule-derived claims after claims or rule versions
change. It recomputes a bounded fixpoint from non-derived positive entity
claims, indexes premise/rule dependents and independent supports, and appends
only changed derivations. Removing one support corrects the conclusion’s
justifications; the conclusion is retracted only after its final support
disappears. Derived claims are never authoritative seeds, so unsupported rule
cycles terminate instead of sustaining themselves.
graph.retract_claim(
reported.logical_id,
supersedes_version_id=reviewed.version_id,
reason="source correction",
)
maintained = graph.maintain_truth(
as_of="2024-06-01T00:00:00Z",
max_iterations=100,
max_derivations=10_000,
max_justifications=100_000,
)
The first pass requires as_of. Later calls may omit it to resume the valid
time recorded by the latest completed rule run or maintenance pass. Conclusions
are published in one temporal commit before the rebuildable dependency index;
retrying after interruption is therefore idempotent. A bound failure publishes
no maintenance commit.
explain_claim resolves the claim at both valid and system time and returns a
ClaimExplanation graph. Claim nodes contain exact immutable
ClaimVersion values, rule nodes contain exact RuleVersion values, and
derived_by/premise edges retain premise order. Historical horizons keep
earlier explanations reproducible after later corrections or retractions.
Traversal tracks visited version IDs and accepts max_depth and max_nodes
bounds; truncated reports a reached bound.
explanation = graph.explain_claim(
derived.logical_id,
valid_time="2024-06-01T00:00:00Z",
system_time=derived.system_time,
)
assert explanation.root_version_id == derived.version_id
Current Limits
Read views cover immutable temporal history on every backend. Bitemporal Cypher
MATCH reads use these views. Unqualified Cypher automatically uses a unified
mutable-state read snapshot on capable backends, while explicit snapshot requests
fail closed elsewhere. Mutable and temporal provenance remain distinct. Temporal
properties persist through Cypher and configured exact/range indexes. Temporal
sampler snapshots support point/window traversal, causal paths, node
availability, and time-aware hard-negative rejection. Claim, rule, and
explanation Cypher syntax is not implemented. Marker-backed data that fails hash or
envelope validation raises
TemporalCorruptionError rather than returning partial history.
Version dataclasses contain immutable metadata and detached entity payloads.
Node and Edge payload objects remain mutable for compatibility; mutating
a returned payload never changes persisted history, and a later read returns a
fresh decoded payload whose hash still matches storage. Temporal history must be
reopened with the same configured serializer used to write it.
There is no temporal-history pruning or TTL API; canonical commits are retained
indefinitely. Derived temporal indexes and sampler snapshots are rebuildable,
but corrupted canonical history requires restoring a complete backup.