Repository navigation
Every graph checksum embeds its construction timestamp, so no two identical plans agree #690
Description
Epic: #530
Found by: #677's claim propagation, 2026-08-26
2.4-hermeticity-guarantees.md states the guarantee as "given the same inputs, it produces the same graph, on
any machine, every time." It does not. Every graph's checksum embeds the wall-clock time it was built.
The chain
pkg/op/graph.go:152—NewGraphstampstimestamp: time.Now()intographMetadatapkg/op/graph.go:807—CanonicalContent'scanonicalGraphcarriesTimestamp string \yaml:"timestamp"``pkg/op/graph.go:229—g.checksum = GitStyleChecksum("graph", canonical)
So the checksum is taken over content that includes the construction time. Plan the same thing twice and the two
graphs differ, by construction, with no input having changed.
Why it went unnoticed
Nothing asserted determinism, so nothing could contradict it. plan.AssembleDefinition carried
+devlore:claim=deterministic for a few hours, and #677's propagation pass reported it immediately:
method plan.AssembleDefinition: claims deterministic but reaches time.Now at graph.go:152,
through op.NewGraph
The claim is withdrawn — the method genuinely is not deterministic while this stands.
What it costs
- Graph checksums cannot answer "is this the same plan?" They answer "was this built at the same instant",
which is never true twice. - Any cache keyed on a graph checksum misses every time.
- Drift detection comparing a stored graph to a freshly planned one sees a difference on every run, whether or
not anything changed — which makes the comparison useless exactly where it matters (Epic: Reconciliation and drift detection #450).
The shape of a fix
The timestamp is legitimate provenance — when this graph was built is worth recording. It is not content.
Options, in rough order of preference:
- Drop
TimestampfromCanonicalContentand keep the field on the graph and in the serialized document.
Provenance survives; identity stops depending on it. One-line change tocanonicalGraph. - Keep it in canonical content but zero it for checksum purposes — same effect, more machinery.
- Accept non-determinism and stop claiming hermeticity — recorded only for completeness; it forfeits 2.4.
Option 1 also matches how Signature is already handled: canonicalGraph omits it, because a signature is
over the content and cannot be in it. A timestamp has the same relationship to identity.
Verification
Once fixed, plan.AssembleDefinition can carry +devlore:claim=deterministic again and #677's propagation pass
will hold it — the same check that found this.
Activity
David-Noble-at-work commentedon Aug 26, 2026
Fixed.
Timestampis out ofcanonicalGraph, so a graph's identity no longer depends on when it was built.The field is unchanged everywhere else:
Graph.Timestamp()still reports it, and the serialized document stillcarries it. Only the canonical content — what the checksum is taken over — stops including it.
The reasoning, recorded at the site
canonicalGraphhas always omittedSignature, because a signature is taken over the content and cannot bein it. A build time has the identical relationship: it says when this graph was made, not what it is. Both are
facts about a graph rather than of it, so this applies an existing rule rather than adding one.
That framing also avoids the alternative, which was to call the timestamp an acceptable deviation from
hermeticity. An exception has no natural boundary — if wall-clock is acceptable, so is hostname, so is the
planning user — and "hermetic except X" cannot be checked mechanically, where "hermetic" can.
Verified in both directions
TestGraphChecksum_IdenticalAcrossConstructionTimesbuilds the same graph twice, a second apart, asserts thetimestamps differ (so identity cannot pass by coincidence), and asserts the checksums match.
Reverting the fix makes it fail:
It sits beside
TestGraphChecksum_IdenticalAcrossJSONAndYAMLDocuments, which already pinned the other axis: thesame graph saved as JSON or YAML checksums the same. Together they say identity depends on neither when a graph
was built nor how it was written down.
One thing this does not fix
plan.AssembleDefinitionstill cannot claim+devlore:claim=deterministic. The graph's identity is nowdeterministic, but the method still reaches
time.Now()throughNewGraph— the field is still stamped, just nolonger hashed — and #677's propagation check reports that correctly.
Closing it properly means
NewGraphnot reading the clock at all: if the timestamp is provenance, whoever recordsprovenance should supply it, through the spec or at save time. That is wider than this issue asked for, so it is
left. Filed as a note here rather than a new issue, since it only matters if someone wants the claim back.
feat(op): claim propagation follows a method into the helpers it calls
fix(op): a graph is conceived once, and its identity never mentions when
Providers claim what they guarantee, and a type-checked pass holds th…