Skip to content

Give roadmap nodes a durable identity as a required field - #7430

Closed
briansrls wants to merge 1 commit into
mainfrom
roadmap-durable-identity
Closed

briansrls wants to merge 1 commit into
mainfrom
roadmap-durable-identity

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

Rebuilt from the review of #7417. Identity is now a required field on RoadmapNode, not a side registry.

Why the side registry was wrong

My first cut carried a hand-written (identity, slug) list beside the authority. It compiled cleanly and was wrong twice: two authorities that must be kept in step, and a node still authorable without an identity — detectable only by a later witness. Validation standing where construction was available.

The change measured its own necessity. Adding the field produced 50 hard failures: 28 struct literals the compiler rejected outright, then 22 constructor call sites failing with undefined variable: identity. Every one was a site where a node could previously come into existence without durable identity — and the side registry would have compiled past all fifty.

The index is now derived from declared_roadmap_nodes(), so the two cannot disagree.

The law is reused; the type deliberately is not

std.occurrence_identity states it correctly: identity may not derive from filename, span, authored name, structural equality or content hash. But OccurrenceId itself is unusable here —

OccurrenceIdAllocator { next_id: 0 }
alloc → OccurrenceId { value: next_id }, next_id + 1

a per-graph counter from zero. Insert a node at the front and every id renumbers; two branches mint the same integer. Positional identity wearing an allocator's clothes — right for occurrences inside one parse, wrong for an entity that must survive edits, reordering, regeneration and independent branches. What occurrence identity does not claim is persistence scope, and that's the only thing missing.

62 collision-resistant identities, minted and persisted at authoring time. Nothing mints during render, compile or regen.

Tombstones — required for the gate to work at all

A base-vs-head diff alone cannot enforce non-reuse:

  1. PR A removes node X
  2. a diff observes the removal once
  3. after merge, X is simply absent from base
  4. PR B reuses X's identity
  5. the next comparison sees present-in-head / absent-in-base — indistinguishable from a legitimately new identity

The reuse passes every check. So retirement is a state: TombstonedIdentity keeps the identity forever, carries the slug it last wore so historical links resolve, and names its successor.

The roster is empty today, stated honestly. The witnesses are arithmetic (registry == active + tombstoned) rather than pinning zero, so the first real retirement won't red them.

Resolution is exactly-one

filter → map → first answers a duplicate registry with a plausible first row — and fails exactly when the registry is corrupt and a confident wrong answer is worst. Now IdentityResolved | IdentityUnknown | IdentityAmbiguous{matches}, because the three remedies differ.

The discriminating witness

identity_survives_a_slug_rename. Every other claim here — uniqueness, totality, resolution — is equally true of a broken registry where identity is the slug. Only this one fails if the separation collapses, which is the entire property the module exists for.

Still owed

The permanence gate. Every claim reads the current tree, so an identity silently replaced between base and head satisfies all of them. The carrier lists admitted transitions (retained across rename, added, tombstoned) and refused ones (replaced, tombstone reactivated, vanished rather than tombstoned, two records sharing an id, one slug → two ids). The tombstone carrier above is what makes that gate implementable rather than illusory.

Validation

  • whole-tree gunbc compile --target dag — EXIT=0, 0 blocking errors
  • ROADMAP.md regenerated and unchanged — identity is machinery, slug remains display
  • 241 witnesses green across 15 roadmap suites

Seven wet witnesses fail identically on plain origin/main (hermetic mode "refusing to fabricate Unit" for a host Check) — confirmed by running them on main before attributing, so they're unrelated to this change.

🤖 Generated with Claude Code

A roadmap node's id was its authored slug, so a rename was structurally a delete
plus a create: edges stop resolving, acceptance receipts name a node that no
longer exists, and any attempt or session joined to it is orphaned. Not
hypothetical — the 2026-07-27 authority refresh deleted the node carrying the
only execution contract and no claim noticed.

std.occurrence_identity already states the right law: identity may not derive
from filename, span, authored name, structural equality or content hash. The LAW
is reused; the TYPE deliberately is not. OccurrenceId is an ordinal from a
per-graph allocator starting at zero, so inserting a node at the front renumbers
everything after it and two branches mint the same integer — positional identity
wearing an allocator's clothes. Right for occurrences inside one parse, wrong for
an entity that must survive edits, reordering, regeneration and independent
branches. What occurrence identity does not claim is PERSISTENCE SCOPE, and that
is the only thing missing.

IDENTITY IS A REQUIRED FIELD, NOT A SIDE REGISTRY. The first cut carried a
hand-written (identity, slug) list beside the authority. It compiled cleanly and
was wrong twice over: two authorities that must be kept in step (DESIGN 3), and a
node still authorable WITHOUT an identity, detectable only by a later witness —
validation standing where construction was available (DESIGN 5). As a field it is
unwritable instead, and the index is DERIVED from declared_roadmap_nodes() so the
two cannot disagree.

The change measured its own necessity: adding the field produced 50 hard failures
— 28 struct literals the compiler rejected outright, then 22 constructor call
sites failing with `undefined variable: identity`. Every one was a site where a
node could previously come into existence without durable identity, and the side
registry would have compiled past all fifty.

62 collision-resistant identities are minted and persisted at AUTHORING time.
Nothing mints during render, compile or regen; a value re-derivable by evaluation
would reintroduce the property this exists to remove.

TOMBSTONES, because a base-versus-head diff alone cannot enforce non-reuse.
Without persisted retirement: PR A removes node X, a diff observes the removal
once, after merge X is simply absent from base, PR B reuses X's identity, and the
next comparison sees present-in-head/absent-in-base — indistinguishable from a
legitimately new identity. The reuse passes every check. So retirement is a STATE:
TombstonedIdentity keeps the identity forever, carries the slug it last wore so
historical links resolve, and names its successor. The roster is empty today and
the witnesses are arithmetic (registry == active + tombstoned) rather than
pinning zero, so the first real retirement will not red them.

RESOLUTION IS EXACTLY-ONE. filter-map-first answers a duplicate registry with a
plausible first row — the fabricated-plausible-output arm DESIGN 5 forbids, and
it fails exactly when the registry is corrupt and a confident wrong answer is
worst. IdentityResolved | IdentityUnknown | IdentityAmbiguous{matches} keeps the
three remedies distinct.

The discriminating witness is identity_survives_a_slug_rename. Every other claim
here — uniqueness, totality, resolution — is equally true of a broken registry
where identity IS the slug; only this one fails if the separation collapses.

STILL OWED: the permanence gate. Every claim reads the current tree, so an
identity silently REPLACED between base and head satisfies all of them. The
carrier lists the admitted and refused transitions concretely; the tombstone
above is what makes that gate implementable at all.

ROADMAP.md is unchanged: identity is machinery, the slug remains display.

Verified: whole-tree compile EXIT=0, 0 blocking errors; ROADMAP.md regenerated;
241 witnesses green across 15 roadmap suites. Seven wet witnesses fail
identically on plain main (hermetic mode refusing to fabricate a host Check) and
are unrelated — confirmed by running them on origin/main before attributing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@briansrls

Copy link
Copy Markdown
Contributor Author

Superseded by #7440, which consolidates this with the other roadmap-belt PRs. Four branches racing a moving main produced a ROADMAP.md conflict on every advance; one branch regenerating the artifact ends that. All content from this PR is carried forward and re-verified on the consolidated branch (199 witnesses green).

@briansrls briansrls closed this Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant