The geo KB treats provenance as a first-class object: a geometry carries its source, CRS, and boundary vintage, and a conversion that loses them is broken. Time deserves the same treatment, and this is where temporal interoperability stops being calendaring and becomes a distributed-systems and AI-pipeline problem.
A value like 2026-07-12T14:31:02Z looks complete. It is not. It hides which
clock produced it, how accurate that clock was, what timescale it is on,
and which stage of a pipeline the instant refers to.
A timestamp is a lifecycle, not a moment
The same datum passes through many times, and any of them can be mistaken for the canonical event time:
- requestedTime
- What a caller asked for.
- observedTime
- When the phenomenon happened — the sensor's world.
- capturedTime
- When the device recorded it.
- receivedTime
- When the ingestion endpoint got it.
- ingestedTime
- When it entered the pipeline.
- processedTime
- When it was transformed.
- storedTime
- When it was persisted.
- reportedTime
- When it was surfaced in a report.
Two systems can pick different stages as canonical from the same record and land
in different hour-of-week slots. So the choice of canonical stage must travel with
the value, and the gap between observedTime and storedTime is the pipeline
latency — a real quantity, not a rounding error.
The clock is part of the value
- source
- gps ≈ 20 ns · ptp ≈ 1 µs · ntp ≈ 1–10 ms · cellular ≈ 100 ms · manual ≈ minutes · monotonic · inferred · synthetic
- accuracyMs
- 1-sigma accuracy. It sets how wide the slot assignment's error bar is (see time uncertainty).
- model
- utc · tai · gps · smeared · monotonic. A smeared clock (Google/AWS leap-second smear) disagrees with UTC by up to ~0.5 s.
- synchronized
- NTP/PTP sync state. An unsynchronized clock can be minutes off while emitting valid-looking timestamps.
performance.now() and CLOCK_MONOTONIC measure elapsed time from an arbitrary
origin. They have no fixed epoch, so they cannot be converted to UTC or a slot at
all. Mixing a monotonic reading into a wall-clock column silently corrupts every
latency and ordering computation downstream.
The model, made checkable
The KB ships this as an executable type, not just prose. A TemporalProvenance
record is validated: monotonic values are rejected as unmappable, a backwards
lifecycle step is flagged (a VM snapshot restore, an offline replay, an NTP jump,
or a corrected timestamp), the canonical stage must be present, and sub-second
digits from a coarse clock are flagged as false precision.
import {
provenanceIssues,
canonicalInstant,
pipelineLatencyMs,
type TemporalProvenance,
} from "@/lib/time";
const p: TemporalProvenance = {
observedTime: "2026-07-12T14:31:02.000Z",
capturedTime: "2026-07-12T14:31:02.200Z",
ingestedTime: "2026-07-12T14:35:00.000Z",
storedTime: "2026-07-12T14:35:01.000Z",
canonicalStage: "observedTime",
clock: { source: "gps", accuracyMs: 0.00002, model: "utc", synchronized: true },
conversion: { tzdbVersion: "2026a", disambiguation: "none", lossless: true },
};
canonicalInstant(p); // epoch ms of observedTime, or null if monotonic/unmappable
pipelineLatencyMs(p); // storedTime − observedTime = 239_000 ms
provenanceIssues(p); // [] — clean; else out-of-order / monotonic / false-precision
The Python parity uses the same lifecycle fields on a dataclass plus
zoneinfo for the conversion metadata; the tested reference implementation is the
TypeScript in lib/time/temporal-provenance.ts.
Why it matters now
AI pipelines make this urgent. An AI-inferred timestamp is a model output, not an observation; a synthetic event time must never masquerade as measured; and when multiple clock authorities disagree during an outage, the record must say which one won. None of that fits in a single ISO-8601 string.
Carrying temporal provenance is what turns this knowledge base from a time-conversion reference into a temporal-interoperability standard for event streams, distributed systems, and AI agents.
