Configuring Bi-Temporal Fact Tracking in Semantica: Valid-Time vs Recorded-At
Semantica implements bi-temporal fact tracking through the BiTemporalFact wrapper class, which captures both valid-time (when a fact was true in reality) and recorded-time (when the system learned the fact) to enable precise temporal reasoning and audit trails.
The Semantica framework stores graph relationships as plain dictionaries augmented with bi-temporal metadata, allowing developers to track when facts were true in the real world separately from when they entered the knowledge graph. This dual-axis approach, implemented in semantica/kg/temporal_model.py, powers time-travel queries, historical audits, and compliance reporting without duplicating data structures.
Understanding Valid-Time vs Recorded-Time
Semantica's bi-temporal model maintains two independent time axes on every relationship edge.
Valid-time represents when a fact was true in the real world, typically expressed through valid_from and valid_until fields. This axis drives business logic, temporal reasoning, and point-in-time snapshots. You can use the TemporalBound.OPEN sentinel to indicate an ongoing interval with no defined end date.
Recorded-time (also called recorded-at) captures when the system learned about the fact, stored in the recorded_at and superseded_at fields. This axis drives provenance tracking, audit trails, and data lineage, showing when information entered the graph regardless of its real-world validity period.
Both axes coexist on the same edge, enabling queries against either timeline without data duplication.
Core Architecture and Components
The bi-temporal implementation centers on the BiTemporalFact class defined in semantica/kg/temporal_model.py (lines 27-41). This wrapper normalizes raw dictionary values into proper datetime objects and provides serialization methods for graph storage.
Key architectural components include:
BiTemporalFact– Normalizes temporal fields and converts between relationship dictionaries and structured objects.TemporalGraphQuery(semantica/kg/temporal_query.py) – Executes time-range queries against the knowledge graph.TemporalReasoning(semantica/kg/temporal_reasoning.py) – Applies reasoning operators that respect both temporal axes.ContextGraph(semantica/context.py) – High-level API exposingstate_at()for valid-time snapshots andrecord_decision()for explicit recorded-time overrides.
The system defaults recorded_at to datetime.now(timezone.utc) via the internal _default_recorded_at() function, but supports explicit backdating for data migration scenarios.
Configuring Bi-Temporal Relationships
Defining Valid-Time Intervals
To create a relationship with explicit valid-time bounds, pass valid_from and valid_until parameters when calling graph.add_edge(). For open-ended intervals, use the string "OPEN" or the TemporalBound.OPEN sentinel.
from datetime import datetime, timezone
from semantica.context import ContextGraph
graph = ContextGraph(advanced_analytics=True)
# Valid-time: employment from 2022-01-01 to 2024-06-30
graph.add_edge(
"alice_chen",
"acme_corp",
edge_type="works_for",
valid_from="2022-01-01T00:00:00Z",
valid_until="2024-06-30T00:00:00Z",
recorded_at="2024-07-05T12:34:56Z",
)
For manual construction, use the BiTemporalFact.from_relationship() method (lines 44-58 of temporal_model.py) to parse temporal fields, then convert back to a dictionary using to_relationship_fields():
from semantica.kg.temporal_model import BiTemporalFact
fact = BiTemporalFact.from_relationship({
"valid_from": datetime(2025, 1, 1, tzinfo=timezone.utc),
"valid_until": "OPEN", # open-ended valid-time
"recorded_at": datetime(2025, 1, 2, tzinfo=timezone.utc),
})
edge_dict = fact.to_relationship_fields()
graph.add_edge("bob_smith", "acme_corp", edge_type="works_for", **edge_dict)
Managing Recorded-Time for Audit Trails
The recorded_at timestamp defaults to the current UTC time upon insertion, but you can override this for back-dating historical imports or replaying ingestion logs. According to the source code, passing recorded_at in the relationship dictionary overrides the default behavior of _default_recorded_at().
This capability is essential for compliance pipelines that must reflect when documents were signed or when external systems generated data, distinct from when Semantica processed them.
Querying Across Temporal Axes
For valid-time point-in-time snapshots, use ContextGraph.state_at(), which returns the graph as it existed at a specific world-time:
snapshot_2023 = graph.state_at("2023-03-15")
print("Edges active on 2023-03-15:", snapshot_2023.get_edges())
For arbitrary interval queries across the valid-time axis, instantiate TemporalGraphQuery and call query_time_range():
from semantica.kg.temporal_query import TemporalGraphQuery
tq = TemporalGraphQuery()
facts = tq.query_time_range(
kg=graph.to_kg_dict(),
query="valid_facts",
start_time="2023-01-01",
end_time="2023-12-31",
)
print("Facts valid in 2023:", facts)
Serializing for Compliance and Export
When exporting relationships for downstream audit systems, use dumps_relationship_json() to convert bi-temporal fields into JSON-compatible strings. The serialize_temporal_bound() function (lines 30-33 of temporal_model.py) ensures open bounds are omitted (None) rather than exposing internal sentinel values.
from semantica.kg.temporal_model import dumps_relationship_json
rel = {
"source": "alice_chen",
"target": "acme_corp",
"edge_type": "works_for",
"valid_from": "2022-01-01T00:00:00Z",
"valid_until": "2024-06-30T00:00:00Z",
"recorded_at": "2024-07-05T12:34:56Z",
}
print(dumps_relationship_json(rel))
Summary
- Bi-temporal fact tracking in Semantica uses the
BiTemporalFactwrapper to maintain separate valid-time and recorded-time axes on every relationship. - Valid-time (
valid_from,valid_until) drives business logic and historical snapshots viaContextGraph.state_at(). - Recorded-time (
recorded_at,superseded_at) tracks system provenance and defaults todatetime.now(timezone.utc)unless explicitly overridden. - The
TemporalBound.OPENsentinel represents ongoing intervals without end dates, serialized asNonefor JSON compatibility. - Query capabilities include point-in-time snapshots (
state_at()) and range queries (TemporalGraphQuery.query_time_range()). - All temporal utilities reside in
semantica/kg/temporal_model.py,temporal_query.py, andtemporal_reasoning.py.
Frequently Asked Questions
What is the difference between valid-time and recorded-time in Semantica?
Valid-time indicates when a fact was true in the real world using valid_from and valid_until fields, while recorded-time indicates when the system learned about the fact via the recorded_at timestamp. Valid-time drives temporal reasoning and business logic, whereas recorded-time supports audit trails and data provenance tracking.
How do I represent an ongoing fact with no end date in Semantica?
Use the TemporalBound.OPEN sentinel (or the string "OPEN") in the valid_until field to indicate an open-ended interval. When serialized via serialize_temporal_bound() in temporal_model.py, these open bounds convert to None for JSON compatibility while maintaining internal consistency for temporal queries.
Can I backdate the recorded_at timestamp when importing historical data?
Yes, explicitly pass the recorded_at parameter when calling graph.add_edge() or include it in the relationship dictionary processed by BiTemporalFact.from_relationship(). This overrides the default _default_recorded_at() behavior and allows you to preserve the original ingestion time from legacy systems or document signatures.
Which method should I use for point-in-time graph snapshots?
Use ContextGraph.state_at() to retrieve a snapshot of the entire graph as it existed at a specific valid-time instant. For querying specific fact intervals rather than full graph states, use TemporalGraphQuery.query_time_range() with the appropriate start and end time parameters.
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 →