How Semantica's Bi-Temporal Graph Model Manages Valid Time vs. Recorded Time
Semantica stores every knowledge-graph relationship as a bi-temporal fact with four timestamps: valid_from/valid_until for real-world truth intervals and recorded_at/superseded_at for system-ingestion history.
The Semantica AGI framework implements a robust bi-temporal data model that independently tracks when facts are true in reality versus when they were recorded in the system. This architecture enables complete historical reconstruction of both domain state and system knowledge evolution.
What Is Bi-Temporal Modeling?
Bi-temporal modeling separates two distinct temporal dimensions for every fact in the knowledge graph:
| Timeline | Purpose | Fields |
|---|---|---|
| Valid time | When the fact is true in the real world | valid_from, valid_until |
| Transaction time (recorded time) | When the fact was entered or modified in the system | recorded_at, superseded_at |
Each relationship in Semantica carries both timelines, allowing queries to reconstruct the graph as it was known at any system time, or as it truly existed at any world time.
Core Implementation in temporal_model.py
The BiTemporalFact dataclass in semantica/kg/temporal_model.py encapsulates both timelines with automatic defaults and conversion utilities.
from semantica.kg import BiTemporalFact, TemporalBound
rel = {
"source": "alice",
"target": "acme_corp",
"type": "ceo_of",
"valid_from": "2018-01-01",
"valid_until": "2022-06-01",
# recorded_at omitted → auto-stamped at ingestion
}
fact = BiTemporalFact.from_relationship(rel)
print(fact.valid_from) # 2018-01-01 00:00:00+00:00
print(fact.recorded_at) # e.g. 2024-03-05 12:34:56+00:00 (auto-stamp)
Key implementation details from the source code:
TemporalBound.OPENis the sentinel for unbounded intervals (ongoing facts with novalid_untilorsuperseded_at)_default_recorded_atprovides fallback logic: usesvalid_fromif present, otherwise current UTC time- Factory method
from_relationshiphandles ingestion-time stamping and boundary parsing to_relationship_fieldsserializes back to storage-compatible format with ISO8601 strings
The file also contains helper functions parse_temporal_value, parse_temporal_bound, serialize_temporal_value, and serialize_temporal_bound for consistent temporal handling across the codebase.
Querying by Valid Time vs. Recorded Time
Semantica provides distinct query mechanisms for each timeline, implemented across temporal_query.py and provenance/manager.py.
Valid-Time Queries: "What Was True Then?"
from semantica.kg import TemporalGraphQuery
query = TemporalGraphQuery(temporal_granularity="day")
snapshot = query.reconstruct_at_time(kg, at_time="2020-05-01")
# Returns edges where valid_from ≤ 2020-05-01 < valid_until
The TemporalGraphQuery.query_at_time method filters on valid_from and valid_until to return the domain state as of a specific real-world date. This supports historical reasoning with Allen interval algebra (implemented in semantica/kg/temporal_reasoning.py).
Transaction-Time Queries: "What Did We Know Then?"
from semantica.provenance.manager import ProvenanceManager
mgr = ProvenanceManager(storage=my_storage)
history = mgr.revision_history(fact_id="rel-123")
# Each entry: version, valid_from, valid_until, recorded_at, author
ProvenanceManager.query_recorded_between and TemporalVersionManager.revision_history filter on recorded_at and superseded_at to reconstruct the system's knowledge state at prior ingestion moments. This enables audit trails and reproducibility.
How the Timelines Evolve Independently
A single relationship can undergo multiple changes that affect only one timeline:
-
Valid-time extension: A contract's end date is extended from 2022-06-01 to 2024-12-31
- New
valid_untilvalue - New
recorded_attimestamp for the amendment - Prior version gets
superseded_atstamped
- New
-
Correction without valid-time change: Fixing a typo in a relationship type
valid_from/valid_untilunchanged- New
recorded_atfor the correction - Full provenance preserved
This separation ensures that domain semantics and system audit trails remain orthogonal concerns.
Key Files and Their Roles
| File | Role |
|---|---|
semantica/kg/temporal_model.py |
Core bi-temporal data model (BiTemporalFact, parsing/serialization) |
semantica/kg/temporal_reasoning.py |
Allen interval algebra operations on valid-time fields |
semantica/kg/temporal_query.py |
Point-in-time and range queries across both timelines |
semantica/provenance/manager.py |
Transaction-time provenance tracking |
docs/reference/temporal.md |
Official documentation and API reference |
Summary
- Four timestamps per fact:
valid_from,valid_until,recorded_at,superseded_at - Independent axes: Valid time tracks real-world truth; transaction time tracks system knowledge
- Automatic defaults:
recorded_atauto-stamps at ingestion if omitted - Open-ended support:
TemporalBound.OPENrepresents unbounded intervals - Query flexibility: Reconstruct domain state or system state at any historical point
- Full provenance: Every amendment preserves prior versions with complete temporal metadata
Frequently Asked Questions
What happens if recorded_at is not provided when creating a relationship?
The BiTemporalFact.from_relationship factory falls back to the valid_from timestamp if present, otherwise uses datetime.now(utc) via the _default_recorded_at helper. This ensures every fact has a defined transaction time without requiring explicit user input.
How does Semantica represent facts that are currently true or currently known?
Open-ended intervals use the TemporalBound.OPEN sentinel. For valid time, this means no known end date to the fact's truth. For transaction time, it means the fact has not yet been superseded by a newer version.
Can I query for facts that were valid in the real world before the system recorded them?
Yes. Valid-time queries operate purely on valid_from/valid_until, independent of recorded_at. This allows retrospective modeling where domain history predates system ingestion—common when migrating legacy data into Semantica.
What's the difference between superseded_at and valid_until?
valid_until marks when a fact ceased being true in reality (domain time). superseded_at marks when a newer version replaced it in the system (recorded time). A fact can be superseded with a correction while its valid time remains unchanged, or its valid time can extend without any system-level superseding event.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →