Time is not metadata on triples. Time is a structural axis of the database.
Every knowledge graph has temporal data. Almost none of them can query it well.
The dominant pattern is time as a qualifier: a triple like (:Napoleon :heldPosition :Emperor) gets a start date and end date as metadata hanging off it. This works for point lookups — “was Napoleon emperor in 1810?” — but it fails catastrophically for the queries that actually matter:
These queries are expensive because time is organized around triples instead of triples being organized around time.
Loka already maintains three index orderings over every triple — SPO, POS, and OSP — so that any access pattern hits an index. Ontochronology adds a fourth ordering with time as the leading key:
| Index | Key Order | Answers |
|---|---|---|
| SPO | Subject → Predicate → Object | “What do we know about Alice?” |
| POS | Predicate → Object → Subject | “Who is a Person?” |
| OSP | Object → Subject → Predicate | “What links to Tokyo?” |
| VECTOR(p) | Per-predicate HNSW graph | “What’s semantically similar?” |
| TSPO | Time → Subject → Predicate → Object | “What existed at time T?” |
With TSPO, “give me the complete world state at time T” is a single range scan on the first key component. No joins. No filter passes. No graph traversal. Every triple valid at T just falls out.
The “T” in TSPO is not necessarily a UTC timestamp. It is an ordered scalar — any value that can be sorted and range-scanned. The B-tree doesn’t care what the bytes represent. It only needs a total ordering.
| Domain | Ordering Axis | Example Values |
|---|---|---|
| Historical events | UTC timestamps (default) | "1804-05-18", "2024-03-14T10:00" |
| Screenplays | Scene numbers | 1, 2, 3.5 |
| Video production | Frame numbers | 0, 1, 2, …, 86400 |
| Film continuity | Minutes into movie | 0.0, 12.5, 90.3 |
| Novels | Page numbers | 1, 42, 300 |
| Religious texts | Book.chapter.verse | 1.1.1, 66.22.21 |
| Legal proceedings | Exhibit / transcript page | 1, 2, 47 |
The axis type is a database-wide setting configured at creation time. You do not mix frame numbers and UTC timestamps in the same TSPO index — that would make range scans meaningless.
# Database creation with non-default axis
loka create movie.sdb --temporal-axis=integer # frame/scene numbers
loka create scripture.sdb --temporal-axis=float # chapter.verse as float
loka create events.sdb # default: UTC timestamp
Every triple can carry up to three temporal signifiers. None are required — some facts are intrinsically atemporal. A triple can have any combination, and can have multiple valid time intervals.
| Signifier | Meaning | When to Use |
|---|---|---|
| Assertion time | “Known to be the case at this time” | When start/end times are unknown. The fallback — a proxy for “true as of when we recorded it.” |
| Start time | “Became true at this time” | When the onset is known. |
| End time | “Stopped being true at this time” | When the termination is known. Absent = still true. |
Assertion time is the crutch. In a perfect world, every fact would have known start and end times. But for most historical data, most extracted data, and most real-time observations, we don’t know when a state began or ended. We only know it was observed at a certain time.
Timestamps carry a precision level derived from their format:
"1977-10-29"^^loka:temporal
→ day precision
"1909"^^loka:temporal
→ year precision
Precision is not confidence. A fact with year-level precision isn’t “less certain” — the granularity is genuinely part of the truth claim. “Born in 1909” is as certain as “died October 29, 1977” — we just don’t have a more specific date.
Temporal data uses RDF-star annotations — statements about statements:
# Assertion time only (the crutch)
<< :building_42 :locatedIn :MainStreet >>
loka:assertedAt "1847"^^loka:temporal .
# Full valid-time interval
<< :napoleon :heldPosition :Emperor >>
loka:validFrom "1804-05-18"^^loka:temporal ;
loka:validTo "1814-04-11"^^loka:temporal .
# Open-ended (still true)
<< :alice :worksAt :Acme >>
loka:validFrom "2023-01-15"^^loka:temporal .
# Atemporal (no temporal annotation at all)
:water :chemicalFormula "H2O" .
New operators scope graph patterns to specific moments or intervals:
# Who was where at 10am on March 14th?
SELECT ?person ?location WHERE {
AT_TIME("2024-03-14T10:00:00"^^xsd:dateTime) {
?person :locatedIn ?location .
?person rdf:type :Suspect .
}
}
# Who was at the courthouse during the hearing?
SELECT ?person ?location WHERE {
DURING("2024-03-14T09:00", "2024-03-14T11:00") {
?person :locatedIn :Courthouse .
}
}
# Everything that was true at this moment
SELECT ?s ?p ?o WHERE {
WORLD_STATE("2024-03-14T10:00:00"^^xsd:dateTime) {
?s ?p ?o .
}
}
# What changed between scene 3 and scene 4?
SELECT ?change ?s ?p ?o WHERE {
TEMPORAL_DIFF("scene_3_end", "scene_4_start") {
?s ?p ?o .
BIND(loka:changeType AS ?change)
}
}
# Find semantically similar documents about people
# who existed at a specific time
SELECT ?doc ?entity WHERE {
AT_TIME("2024-06-01"^^xsd:dateTime) {
?entity rdf:type :Person .
?doc :mentions ?entity .
}
VECTOR_SIMILAR(?doc :hasEmbedding "..."^^loka:f32vec, 0.85)
}
Track every entity across scenes. “Who is holding the knife? What are they wearing? Was the door open?” Continuity errors become constraint violations detected by query.
Reconstruct timelines from depositions. “Where was the defendant during the relevant window? Which witness statements conflict?” Chain of custody as a temporal graph trace.
Model entities with imprecise temporal bounds. Year-level precision for births, day-level for deaths. Both stored without forcing false precision.
Track which extraction pass produced which triples, from which source, at which confidence. “What did we know at time T” vs “what do we know now.”
Ontochronological indexing is one instance of a general pattern: promote the query axis to a leading index key. The same approach extends to coordinates, provenance, and confidence:
| Dimension | Index | Data Structure | Query |
|---|---|---|---|
| Time (1D) | TSPO | B-tree | “Everything at time T” |
| Coordinates (2–3D) | XYSPO | R-tree / composite B-tree | “Everything at location L” |
| Combined | TXYSPO | Composite range scan | “Everything at L during T” |
| Embeddings (768D+) | VECTOR(p) | HNSW | “Semantically similar” |
Ontochronological features are purely additive:
Three new reserved predicates trigger temporal indexing:
| Predicate | Purpose |
|---|---|
loka:assertedAt | Point attestation — “known true at this time” |
loka:validFrom | Interval start — “became true at this time” |
loka:validTo | Interval end — “stopped being true at this time” |
The query planner treats TSPO exactly like it treats SPO, POS, OSP, and VECTOR — as an access path with cost estimates. Temporal queries that benefit from TSPO get routed there. Queries that don’t simply ignore it.