Skip to content

Every graph checksum embeds its construction timestamp, so no two identical plans agree #690

Closed

Description

@David-Noble-at-work

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

  1. pkg/op/graph.go:152 — NewGraph stamps timestamp: time.Now() into graphMetadata
  2. pkg/op/graph.go:807 — CanonicalContent's canonicalGraph carries Timestamp string \yaml:"timestamp"``
  3. 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:

  1. Drop Timestamp from CanonicalContent and keep the field on the graph and in the serialized document.
    Provenance survives; identity stops depending on it. One-line change to canonicalGraph.
  2. Keep it in canonical content but zero it for checksum purposes — same effect, more machinery.
  3. 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

  1. David-Noble-at-work commented on Aug 26, 2026

    @David-Noble-at-work
    ContributorAuthor

    Fixed. Timestamp is out of canonicalGraph, 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 still
    carries it. Only the canonical content — what the checksum is taken over — stops including it.

    The reasoning, recorded at the site

    canonicalGraph has always omitted Signature, because a signature is taken over the content and cannot be
    in 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_IdenticalAcrossConstructionTimes builds the same graph twice, a second apart, asserts the
    timestamps differ (so identity cannot pass by coincidence), and asserts the checksums match.

    Reverting the fix makes it fail:

    --- FAIL: TestGraphChecksum_IdenticalAcrossConstructionTimes
        checksums differ across construction times
    

    It sits beside TestGraphChecksum_IdenticalAcrossJSONAndYAMLDocuments, which already pinned the other axis: the
    same 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.AssembleDefinition still cannot claim +devlore:claim=deterministic. The graph's identity is now
    deterministic, but the method still reaches time.Now() through NewGraph — the field is still stamped, just no
    longer hashed — and #677's propagation check reports that correctly.

    Closing it properly means NewGraph not reading the clock at all: if the timestamp is provenance, whoever records
    provenance 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Area:ArchitectureSystem architecture and designEpic:ExecutionGraphThe execution graph epicPriority:P1This cycleSeverity:HighA supported path is broken with no workaroundbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions