Skip to main content

Product Shape, canonical references, and temporal history

Product Shape: the entity and relationship graph

"Product Shape" is this documentation's descriptive name for what you get when you look at Horologium's entity and relationship graph as a whole — it isn't a separate schema concept, just the useful way to think about what the graph represents. The graph is a closed set of 9 entity types and 8 relationship types (docs/adr/0003-canonical-entity-relationship-evidence-model.md). Adding a 10th entity type or a 9th relationship type requires a new ADR — this is what keeps "one truth, many lenses" enforceable: there is exactly one place to look for what a type means.

Entities — the things Product Truth refers to:

TypeMeaning
capabilityA product behavior a human can name — the semantic anchor everything else attaches to. The only entity type an accepted model proposal may create.
work_itemA tracker issue (Jira, etc.)
changeA merged commit or pull request
releaseA published version
artifactA build output, identified by content digest, not by a tag
deployment_targetAn environment (e.g. beta, production)
deploymentA release running on a target over a time interval
documentA documentation page or section
repositoryA source repository

Relationships connect them: implements (change → capability), specifies (work_item → capability), documents (document → capability), references (work_item → change), contains (release → change), deploys (deployment → release), targets (deployment → deployment_target), supersedes (entity → entity).

Every entity and relationship carries a canonical ID — either minted (<prefix>_<ULID>, e.g. cap_01JB2K7QF3N8XZ4M0YHD9WVTAR, assigned once by the Rust core) or content-addressed (<prefix>_<sha256-hex>, for evidence and finding, whose identity is their content — re-deriving the same content yields the same ID, which is what makes re-ingesting a source idempotent). The web dashboard, the HTTP API, and the MCP tools all resolve to these same IDs — no lens keeps its own naming scheme or a second copy of what a cap_… ID means (invariant 5, "One Truth, Many Lenses, Zero Duplicate Meaning").

Canonical references

A "canonical reference" here is the pairing of a canonical ID with the exact, re-fetchable evidence citation behind it — described in full in Integrity states and evidence. The point of a canonical reference is that a human or a coding agent can go from "Horologium says the refund window is 30 days" straight to the specific commit, issue field, or documentation section that backs the claim, without a second lookup or a second naming scheme.

Temporal history

Every canonical truth row — entity, relationship, claim, finding, deployment — carries four timestamps, not one updated_at:

ColumnAxisMeaning
valid_from / valid_tovalid (world) timewhen the fact was true in the world
recorded_from / recorded_totransaction timewhen Horologium believed it

Rows are never updated in place and never deleted. Superseding a fact means closing the old row (valid_to/recorded_to) and inserting a new one. This is what makes two different questions both answerable:

  • "What was true at time t?"state_at(t), exposed as ?at=<RFC3339> on every read endpoint, defaulting to now.
  • "What did Horologium believe about time t, as of some earlier moment k?" — a second axis, useful for investigating "Horologium was wrong last Tuesday" as a fact you can query, not an argument you have to reconstruct from memory.

A capability's timeline (GET /v1/capabilities/{id}/timeline?from=&to=) is the ordered set of actual transitions in that window — not samples taken at intervals. Sampling would miss a transition that happened between two sample points and invent precision the data doesn't have, so lenses are required to read transitions directly rather than reconstructing a timeline by polling state_at repeatedly.

One deliberate asymmetry: a deployment can never have two simultaneous open rows on the same target (enforced at the schema level — one target runs one release at a time), while a claim has no such restriction, because many simultaneously valid, disagreeing claims about the same subject and property is not a bug — it's the exact situation Horologium exists to surface. See docs/adr/0012-temporal-model-and-state-at.md in the repository for the full decision record.