Skip to main content

Temporal truth: what Horologium believed, and when

Horologium answers two different questions, and confusing them is the single easiest way to misread this product's history (ADR-0012). This page names them precisely, documents the current retention boundary, and states what is not promised.

The two questions​

"What was true in the world at time V?" — state_at(V), the default behavior of every read endpoint via ?at=<RFC3339> (defaulting to now). This answers "what does Horologium believe was true on 2026-05-10," using everything it has ever learned, including evidence that arrived after 2026-05-10 but described something that was already true then (retroactive facts are permitted — see below).

"What did Horologium believe at time K, about the world at time V?" — known_as_of(K, V). This is a different, narrower question: "if I had asked on 2026-06-01, before the correction landed, what would Horologium have told me was true on 2026-05-10?" The answer can legitimately differ from state_at(V) asked today — that difference is not a bug, it is the record of Horologium having been wrong and then corrected. The columns that make this answerable (recorded_from/recorded_to) exist on every canonical row from the first migration; ?snapshot=<id> resolution of a known_as_of read through the API is scoped to HORO-1399, not this page.

Both questions are answered from the same four columns on every canonical row — valid_from/valid_to (when a fact was true in the world), recorded_from/recorded_to (when Horologium learned it) — never from a second store or an event-sourcing replay engine.

Late-arriving evidence and corrections​

A source can report something today about a time in the past — a deployment record synced late, a Jira changelog entry backfilled. When that happens:

  • The new claim's valid_from reflects when the fact became true in the world, not when Horologium learned it (recorded_from).
  • The previous claim, if one existed and disagreed, is closed (valid_to set) rather than deleted or overwritten — it remains visible to a known_as_of read from before the correction.
  • state_at for that past instant, asked today, reflects the correction. state_at for that same instant, asked before the correction landed (via known_as_of), still returns what was believed then.

Nothing is ever UPDATEd in substance and nothing is DELETEd — see ADR-0012 for the full rule and the schema-level reasons (deployment's exclusion constraint vs. claim's deliberate absence of one).

valid_from_basis: not every timestamp means the same thing​

Every claim carries a valid_from_basis alongside its valid_from, because observed_at (when a source was scraped) and business-valid time (when a fact became true) are not the same thing, and conflating them is exactly the failure mode ADR-0012 warns about ("unknown valid_from is not allowed to default to now"):

BasisMeaning
source_event_timeThe source itself reported a real world-time event (a Jira changelog transition, a commit's merged_at, a deployment record's own timestamp).
declared_business_timeA caller (a proposal submitted through POST /v1/proposals) explicitly supplied valid_from, derived from the evidence it cites.
earliest_defensibleNo real event time is available; the earliest instant that can be defended is used instead of guessing "now."
unknownNo business time could be determined at all. This is the honest fallback — never silently promoted to look more certain than it is.

A proposal that omits valid_from entirely falls back to the batch's own submitted_at (so a claim isn't rejected outright), but is recorded as unknown — never as declared_business_time, which would misrepresent a guess as a source-justified fact. GET /v1/entities/{id}/claims exposes both temporal (the quad) and valid_from_basis on every claim, so a consumer can distinguish "this is when it really happened" from "this is when we happened to submit the batch."

Retention: currently unbounded, not yet a set policy​

Nothing is deleted, and no pruning runs. state_at and known_as_of can reach back to whichever row is oldest in the store — there is no retention window, no archival tier, and no configuration flag that limits how far back a historical query can see. This is the current, correct description of shipped behavior, not a placeholder for a number this page is choosing on your behalf.

How far back "time travel" is promised to reach as a product commitment is explicitly deferred to the release owner (ADR-0012's amendment) — it is not an engineering default this page, or any ticket under HORO-1387, decides. Every reconciliation run adds a new generation of rows and nothing prunes any of it, so the storage cost of this choice accrues continuously; that cost is disclosed here, not hidden, and a future retention decision (if one is made) will need a migration, not a config change, precisely because the columns already exist and rows are never deleted today.

What this is not​

  • Not a promise that every historical instant is answerable with equal precision — a source that never reported a real event time yields unknown/earliest_defensible claims, and a lens must say so rather than presenting a guess as fact.
  • Not full bitemporal query support across every endpoint — known_as_of has one endpoint in v0.0.0 (ADR-0012); broader exposure is scoped to later tickets, not invented here to look more complete than it is.
  • Not a graph database or an event-sourcing framework. Four columns on existing tables, and repository methods that always apply the temporal predicate so a raw query cannot accidentally see closed or future rows.