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:
| Type | Meaning |
|---|---|
capability | A product behavior a human can name — the semantic anchor everything else attaches to. The only entity type an accepted model proposal may create. |
work_item | A tracker issue (Jira, etc.) |
change | A merged commit or pull request |
release | A published version |
artifact | A build output, identified by content digest, not by a tag |
deployment_target | An environment (e.g. beta, production) |
deployment | A release running on a target over a time interval |
document | A documentation page or section |
repository | A 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:
| Column | Axis | Meaning |
|---|---|---|
valid_from / valid_to | valid (world) time | when the fact was true in the world |
recorded_from / recorded_to | transaction time | when 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 momentk?" — 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.