Skip to content

Close the focus over prerequisites; give roadmap nodes a durable identity - #7417

Closed
briansrls wants to merge 1 commit into
mainfrom
roadmap-identity-and-focus-closure
Closed

briansrls wants to merge 1 commit into
mainfrom
roadmap-identity-and-focus-closure

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

Two roadmap-authority changes, both about replanning not destroying work. Split out of #7404 so the authority model reviews separately from the dispatch mechanism.

1. A focus must be closed under prerequisites

Owner membership alone was the wrong visibility rule. The v1 deletion lane's critical path runs through pderive-source-slot-binding, whose prerequisite namespace-canonical-binding lives in a lane the focus hid — so the page rendered the fourth link of the spine as ready, with its blocker invisible. Nothing was red; the page lied by omission.

That's worse for a dispatched agent than for a person: a worker reads a ready-looking node, starts, and stalls on work the roadmap never mentioned.

Visibility is now lane membership ∪ the transitive prerequisite closure, reusing std.graph.graph_reverse_reachable_members over the roadmap's own edge relation rather than minting a second walk — the edge is already a dependency edge, reverse reachability from a lane node is exactly its ancestors, and the graph is acyclic by witness so the walk terminates by construction.

The closure found five blockers, not the three visible by eye. namespace-canonical-binding has its own prerequisites, so the lane was blocked on a namespace sub-chain of three. Manual inspection would have discovered those one stall at a time, each costing a wasted attempt.

Pulled-in rows are named as prerequisites rather than counted as lane work, so the lane's real size stays honest at 17 (page shows 22). Focus label renamed to "the v1 deletion lane".

2. Durable node identity (stage I0)

A roadmap node's id is its authored slug (RoadmapNodeId = NonEmptyStr), so a rename is structurally a delete plus a create: edges stop resolving, 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.

The law is reused; the type deliberately is not

std.occurrence_identity already states the right law: identity may not derive from filename, span, authored name, structural equality, or content hash.

But OccurrenceId itself is unusable here:

OccurrenceIdAllocator { next_id: Int }
initial → 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 after it renumbers; two concurrent branches mint the same integer. That is positional identity wearing an allocator's clothes — correct for occurrences inside one parse, wrong for an entity that must survive edits, reordering, regeneration and independent branches. What occurrence identity deliberately does not claim is persistence scope, and that is the only thing missing.

51 collision-resistant identities are minted and persisted, one per declared node, at authoring time. Nothing mints during render, compile or regen — a value re-derivable by evaluation would reintroduce the exact property this module removes. The slug stays readable and stops being identity.

Deliberately additive

No rendering, dependency, receipt or dispatch path changes. The v1-deletion recut can be authored against durable identities immediately, without waiting on a consumer migration and without its new nodes being rewritten a second time afterwards.

Two gates declared as owed, not assumed

Permanence is unprovable from a snapshot. Every witness here reads the current tree, so an identity silently replaced between base and head satisfies all of them — the corpus is structurally incapable of catching the exact edit this module forbids. The owed row lists admitted transitions (identity retained, added, tombstoned; slug changed while identity unchanged) and refused ones (identity replaced, retired id reused, two nodes sharing an id, one slug resolving to two ids). It needs a base-vs-head registry comparison, which the floor's merge-base diff already has the shape for.

Decomposition needs an explicit completion policy. Containment keeps pointers valid but cannot say whether a parent completes on direct acceptance, on all children, or both — and inferring it would silently close a parent whose children are unfinished. Lands with containment, a later stage.

Validation

Verified on the pre-split branch (identical content, cherry-picked cleanly with zero references to binding-only symbols, so it stands alone rather than merely applying):

  • whole-tree gunbc compile --target dag — 0 blocking errors
  • ROADMAP.md regenerated from the authority
  • 109 witnesses green, zero failures across roadmap_identity (7, incl. the discriminating identity_is_persisted_not_positional), roadmap_focus (8, incl. no_visible_node_has_a_hidden_prerequisite), roadmap_authority, generated_artifact_drift, roadmap_page, doc_reachability (both homes), roadmap_emit, roadmap_document

Re-verifying against fresh binaries on this branch (main moved under #7415, which changed cli_run.rs); I'll post the re-run.

Review note

The identity half deserves the harder look. The focus closure is mechanically checkable and its witness is structural. Identity has a declared hole — identity_is_persisted_not_positional pins one literal value as a tripwire, and the real base-vs-head gate is written down as owed rather than built. If that gate should exist before the recut rather than after, that's the call to make now, since the recut is what will exercise it.

🤖 Generated with Claude Code

…tity

Two roadmap-authority changes, both about replanning not destroying work.

FOCUS CLOSURE. Owner membership alone was the wrong visibility rule. The v1
deletion lane's critical path runs through pderive-source-slot-binding, whose
prerequisite namespace-canonical-binding lives in a lane the focus hid — so the
page rendered the fourth link of the spine as ready with its blocker invisible.
Nothing was red; the page lied by omission. Worse for a dispatched agent than for
a person: a worker reads a ready-looking node, starts, and stalls on work the
roadmap never mentioned.

Visibility is now lane membership UNION the transitive prerequisite closure,
reusing std.graph.graph_reverse_reachable_members over the roadmap's own edge
relation rather than minting a second walk — the edge is already a dependency
edge, reverse reachability from a lane node is exactly its ancestors, and the
graph is acyclic by witness so the walk terminates by construction. The closure
found FIVE blockers, not the three visible by eye: namespace-canonical-binding
has its own prerequisites, so the lane was blocked on a namespace sub-chain of
three. Manual inspection would have discovered those one stall at a time.

Pulled-in rows are named as prerequisites rather than counted as lane work, so
the lane's real size stays honest at 17. The focus label is now "the v1 deletion
lane" per the operator's naming.

DURABLE IDENTITY. A roadmap node's id is its authored slug
(RoadmapNodeId = NonEmptyStr), so a rename is structurally a delete plus a
create: edges stop resolving, receipts name a node that no longer exists, and any
attempt or session joined to it is orphaned. That is 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
every node after it and two branches mint the same integer — positional identity
wearing an allocator's clothes. Correct 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.

51 collision-resistant identities are minted and persisted, one per declared
node, at AUTHORING time — nothing mints during render, compile or regen, because
a value re-derivable by evaluation would reintroduce the property the module
exists to remove. The slug stays readable and stops being identity.

Stage I0 is deliberately additive: no rendering, dependency, receipt or dispatch
path changes. The v1-deletion recut can be authored against durable identities
immediately without waiting on a consumer migration, and without its new nodes
being rewritten a second time afterwards.

TWO GATES DECLARED AS OWED, NOT ASSUMED. Permanence is a historical property and
every witness here reads the current tree, so an identity silently REPLACED
between base and head satisfies all of them — the snapshot corpus is structurally
incapable of catching the exact edit this module forbids. The owed row lists the
admitted and refused transitions for a base-versus-head registry comparison.
Separately, decomposition needs an explicit parent completion policy: containment
keeps pointers valid but cannot say whether a parent completes on direct
acceptance, on all children, or both, and inferring it would silently close a
parent whose children are unfinished.

Verified: whole-tree compile 0 blocking errors; ROADMAP.md regenerated; 109
witnesses green with zero failures across roadmap_identity (7, incl. the
discriminating identity_is_persisted_not_positional), roadmap_focus (8, incl.
no_visible_node_has_a_hidden_prerequisite), roadmap_authority,
generated_artifact_drift, roadmap_page, doc_reachability (both homes),
roadmap_emit and roadmap_document.

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 briansrls closed this Jul 29, 2026
briansrls added a commit that referenced this pull request Jul 29, 2026
…entity, bounded agent report, heal auto-remedy (#7440)

* Bind the first roadmap node's execution contract; dispatch stops refusing everywhere

Every roadmap node was ExecutionContractUnspecified, so
dispatch_environment_admit matched that arm and refused before doing anything
else. Clicking Dispatch on any of the ~46 tickets produced a typed refusal and
nothing more, which meant no part of the workflow below Env — worktree, agent,
and every receipt that depends on them — had ever executed end to end in
production. The frontier reached zero when the 2026-07-27 authority refresh
deleted the node that carried the prior binding; no claim noticed, because none
existed.

The binding is a COMBINATOR, not a constructor parameter. `sign` is the
precedent sitting beside it and already threads `execution: rn.execution`
through untouched; threading an execution argument instead would edit seven
constructors and every one of their call sites to express a fact that today
holds for exactly one node. A node without the wrapper keeps
ExecutionContractUnspecified, which stays the fail-closed refusal the frontier
note declares rather than a default anyone chose.

shell-typed-invocation is bound to the transport-clean corpus witness
(corpus_dag_compile_clean_transport_clean_holds), which is this ticket's own red
control expressed as an operation: "a new unclassified production construction
site reds the census". It reads the live tree, so it is a statement about the
repository rather than a fixture.

That validation is ALREADY GREEN, deliberately. A first binding has two jobs
that pull apart — prove the Verify path runs at all, and prove it discriminates
— and conflating them is how a green pipeline gets mistaken for a working one.
This row takes the first job only: an Agent turn that changes nothing still
produces a real admitted environment, a real derived command, and a real
validation receipt, which is a statement about the PLUMBING and not about the
provider. It proves nothing about discrimination and must not be read as
evidence that Verify can fail; the discriminating red belongs on a second node
whose validation is currently red and goes green when the work lands, and that
is the next binding, not this one.

The frontier note is corrected in the same motion. It read "Every node below is
ExecutionContractUnspecified", which was true when written and false the moment
one was bound. It now states the count is one, and that a count is not a
completion.

Three witnesses pin both halves of the frontier: the bound node yields a real
contract rather than the fail-closed variant; the platform/task capability JOIN
actually happens at admission (8 fixed dispatch mechanics + the one capability
this validation derives = 9); and the frontier is exactly one node — which fails
if a binding is silently lost AND if a second is added without updating the
roster, so the documented count cannot drift from the authority.

ROADMAP.md is unchanged: execution contracts are dispatch mechanics, not roadmap
content, and the projection correctly does not render them.

Verified: whole-tree compile 0 blocking errors; 46 witnesses green across
roadmap_authority (incl. the three new ones and the pre-existing re-entry RED
controls), generated_artifact_drift, roadmap_execution_contract, and
roadmap_dispatch_environment.

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

* Bind the discriminating second node: give Verify a validation that can actually fail

The first binding named an already-green validation, on purpose, to prove the
Verify path executes at all — admission, argv derivation, receipt production.
It cannot prove the path DISCRIMINATES, because nothing it runs is capable of
returning anything but green. A frontier with only that binding is the
coverage-by-illusion tier: machinery present, nothing gating on it.

observation-raw-print-retirement now binds a validation that FAILS today.
gunbc.observation_emit_census classifies every seed emit site as exactly one of
MigratedToObservation or CountedFrontierSite, and observation_emit_frontier_count
is the population of the second arm — 8 today (floor-drain, gate-warm-cost,
receipt, file, resolve-split, assembly-split, witness-row-cost,
witness-row-cost-drift). The witness asserts 0, which is that node's handback
stated verbatim: "the classified zero-residue progress census".

So the red is the ticket's own acceptance condition rather than a planted
fixture, the count IS the completion condition rather than a proxy for it, and
red -> green is the work being finished. The two bindings now pin both
directions, and neither claim is obtainable from the other.

Finding a genuine red took some looking, and the search is worth recording: the
transport-script wall is clean corpus-wide (every enrolled path AND every
unenrolled Exec.Run site I measured returns zero violations), so there was no red
available there; the existing known-red quarantine rows are self-host behavioral
receipts that red on cargo/shim closure refusal, which is infrastructure rather
than anything a dispatched agent could turn green. The emit census is the one
place where a small, agent-doable ticket has a countable completion condition
that is currently false.

The acceptance witness is enrolled QuarantineProbeExpectRed in
gunbc.ci_layer_roots — the cadence std.witness_admission defines for exactly
this, "known-red quarantined rows run on a probe cadence expecting red (greening
is a counted un-quarantine event)" — so a deliberately-red acceptance test does
not red the corpus. Its dissolution is forced rather than optional: when the
frontier reaches zero the roster row becomes a still-red expectation against a
green witness, so it must be deleted in the same change that lands the last
migration, promoting the witness to ordinary DiscoverySelection as a standing
regression ratchet.

It lives in its own file rather than inside observation_emit_census_witness_test
because that suite asserts the count the census HAS (== 8, which must red if the
roster drifts) while this asserts the count the work OWES (== 0). Both are true
and they disagree by construction; merging them would drag the roster's own
hygiene check into quarantine and stop it reding when it should.

The frontier note now reads TWO and says why the pair is one claim rather than
two bindings. witness_execution_contract_frontier_is_exactly_one_node becomes
_two_nodes, joined by witness_bound_pair_covers_both_verify_directions.

Verified: whole-tree compile 0 blocking errors; the discriminating validation
FAILS and the plumbing validation PASSES when each is run directly, which is the
receipt that matters; 63 witnesses green with zero failures across
roadmap_authority, generated_artifact_drift, observation_emit_census and
witness_admission.

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

* Carry the agent's report through the bounded projection, with truncation reported

The dashboard could not show what an agent actually said. The raw JSONL is
retained per attempt, but codex_provider_event_projection_filter — the jq stage
that bounds each line before the substrate reads it — emitted only
{"type":…,"item":{"type":…}} for item events, so an agent_message survived as an
activity CLASSIFICATION and its text was gone before anything could render it.
That was deliberate: the boundary exists because one item.completed carrying
~154KB of aggregated command output killed the old 64KB reader. So surfacing the
report is a change AT that boundary, not a rendering change, and the reviewable
decision is the bound.

THE FIELD NAME IS CAPTURED, NOT GUESSED. Nothing in the repository recorded the
agent_message item's text member, the shipped CLI is a stripped native binary,
and the locator extdeps.llm.cli already cites documents the mode rather than the
per-item event schema. Modeling it from memory is the guess DESIGN §3 forbids for
an extdeps surface, and #7368's own design note sets the same bar for provider
grammars: admitted only "after an actual provider receipt demonstrates its
grammar". So a receipt was produced — codex exec --json against @openai/codex
0.145.0, landed under dag/test/fixture/codex_provider_events with its version and
capture conditions. Its third line is the authority:

  {"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"ok"}}

The report gets its OWN budget rather than reusing the detail budget beside it.
They are the same type and genuinely different facts: a detail is a diagnostic,
short by nature, read to find out what broke; a report is what the provider says
it DID, and it is the text an operator reads to judge whether the work is any
good. One shared number would mean tuning either bound silently retunes the
other. 8192 is set against the incident this projection exists to prevent — an
order of magnitude under the 65536 line budget so a report can never be the
payload that reproduces it, and deliberately above the 4096 detail budget because
a 4KB error is pathological while a 6KB work summary is ordinary.

TRUNCATION IS REPORTED, NEVER SILENT. Everywhere else these bounds guard a
machine-read classification, where a cut string simply fails to match and
refuses. Here the bounded value is prose an operator reads and acts on, so a
report severed mid-sentence and shown as if complete is a wrong answer delivered
confidently — the operator would judge the work on a fragment with nothing on
screen saying so. The envelope therefore carries a message object pairing the
bounded text with a truncated flag computed from the ORIGINAL length before
slicing, so the flag describes the provider's output rather than the slice.

The anchored item.type walk is untouched by construction: the report is emitted
as a member AFTER item.type, so item.type remains item's first member and the
walk's landing position is unchanged — a report whose own text quotes an
item-type fragment still cannot reclassify the pane. The note that described the
envelope shape is corrected to say so rather than understating it.

Verified by execution, which for a jq program means running the MODELED filter
string rather than a hand copy: extracted from the substrate and run through real
jq against the real captured receipt, it yields
{"text":"ok","truncated":false}; against an 8193-character control it yields
exactly 8192 retained characters and truncated:true.

Four witnesses pin the contract the shell cannot prove downstream: the bound is
the message budget (a copy-paste of the detail budget would still emit valid jq
and still look fine until the day a long message arrived), the two budgets are
distinct, truncation is computed against the original length, and both item
envelope shapes keep item.type first.

Verified: whole-tree compile 0 blocking errors; provider-events 18/18; 50 more
green across roadmap_workflow_progress, generated_artifact_drift and
roadmap_authority; zero failures.

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

* Close the focus over prerequisites; give roadmap nodes a durable identity

Two roadmap-authority changes, both about replanning not destroying work.

FOCUS CLOSURE. Owner membership alone was the wrong visibility rule. The v1
deletion lane's critical path runs through pderive-source-slot-binding, whose
prerequisite namespace-canonical-binding lives in a lane the focus hid — so the
page rendered the fourth link of the spine as ready with its blocker invisible.
Nothing was red; the page lied by omission. Worse for a dispatched agent than for
a person: a worker reads a ready-looking node, starts, and stalls on work the
roadmap never mentioned.

Visibility is now lane membership UNION the transitive prerequisite closure,
reusing std.graph.graph_reverse_reachable_members over the roadmap's own edge
relation rather than minting a second walk — the edge is already a dependency
edge, reverse reachability from a lane node is exactly its ancestors, and the
graph is acyclic by witness so the walk terminates by construction. The closure
found FIVE blockers, not the three visible by eye: namespace-canonical-binding
has its own prerequisites, so the lane was blocked on a namespace sub-chain of
three. Manual inspection would have discovered those one stall at a time.

Pulled-in rows are named as prerequisites rather than counted as lane work, so
the lane's real size stays honest at 17. The focus label is now "the v1 deletion
lane" per the operator's naming.

DURABLE IDENTITY. A roadmap node's id is its authored slug
(RoadmapNodeId = NonEmptyStr), so a rename is structurally a delete plus a
create: edges stop resolving, receipts name a node that no longer exists, and any
attempt or session joined to it is orphaned. That is 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
every node after it and two branches mint the same integer — positional identity
wearing an allocator's clothes. Correct 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.

51 collision-resistant identities are minted and persisted, one per declared
node, at AUTHORING time — nothing mints during render, compile or regen, because
a value re-derivable by evaluation would reintroduce the property the module
exists to remove. The slug stays readable and stops being identity.

Stage I0 is deliberately additive: no rendering, dependency, receipt or dispatch
path changes. The v1-deletion recut can be authored against durable identities
immediately without waiting on a consumer migration, and without its new nodes
being rewritten a second time afterwards.

TWO GATES DECLARED AS OWED, NOT ASSUMED. Permanence is a historical property and
every witness here reads the current tree, so an identity silently REPLACED
between base and head satisfies all of them — the snapshot corpus is structurally
incapable of catching the exact edit this module forbids. The owed row lists the
admitted and refused transitions for a base-versus-head registry comparison.
Separately, decomposition needs an explicit parent completion policy: containment
keeps pointers valid but cannot say whether a parent completes on direct
acceptance, on all children, or both, and inferring it would silently close a
parent whose children are unfinished.

Verified: whole-tree compile 0 blocking errors; ROADMAP.md regenerated; 109
witnesses green with zero failures across roadmap_identity (7, incl. the
discriminating identity_is_persisted_not_positional), roadmap_focus (8, incl.
no_visible_node_has_a_hidden_prerequisite), roadmap_authority,
generated_artifact_drift, roadmap_page, doc_reachability (both homes),
roadmap_emit and roadmap_document.

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

* heal: apply the binary/source skew remedy automatically instead of printing it

The skew guard printed "merge the base branch, then re-run" and exited 1. That is
a correct refusal and a wasted round trip: the job holds contents:write, already
pushes commits to this branch, and the fix is a merge it is fully authorized to
perform. Making a human do a mechanical repair the machine can do is the toil
this pipeline exists to remove (operator ruling 2026-07-29).

This is a REPAIR, not a widen, which is what makes it admissible under DESIGN §5.
The arm does not proceed despite the skew — it REMOVES the skew by merging the
base into the branch, then re-runs the same check it just failed. Regeneration
happens only if that second check passes, so artifacts are still projected by a
compiler the tree agrees with. The invariant is re-established and re-verified,
never assumed. The remedy is attempted exactly once; a base that moves again
mid-run refuses rather than looping.

A MERGE CONFLICT CONFINED TO GENERATED ARTIFACTS IS NOT A CONFLICT. Measured on
the first real skew this guard caught (#7404): the only conflicting path was
ROADMAP.md — a registered generated artifact this job rewrites from its authority
in the very next step. Two branches that both regenerate a projection will always
collide in the projection, so refusing there would fail the auto-remedy on its
single most common case while the authorities underneath merged cleanly.

A generated artifact has no independent content to reconcile: it is a pure
function of the .dag authority, so whichever side is checked out is equally wrong
until main_wet runs and equally right afterwards. Taking either side and
regenerating is not a guess — the bytes are determined by the merged authority,
and the drift gate proves the result is that authority's fixed point. Scope is
exactly committed_generated_artifact_paths(), the same registry the staging step
uses, so a file qualifies only by being declared generated.

Both failure arms stay closed. If ANY conflicting path is outside that registry
the merge aborts and refuses, listing every conflict and naming the authored ones
— the classification is on the WHOLE set, so one authored conflict refuses the
entire remedy rather than partially resolving into a half-merged tree.

The merge commit is --no-verify: this is an unattended machine commit, and the
repository's own rulings already establish that git hooks are per-clone developer
feedback rather than enforcement (opt-in via core.hooksPath, bypassable, absent
in container worktrees). Found by execution — the local pre-commit hook failed
the remedy's commit with a cargo-not-on-PATH error, which CI would not have hit
but which should not gate a machine commit in any case.

Verified by execution against the real #7404 skew, not a fixture: skew detected,
merge attempted, conflict correctly classified as generated-only, resolved,
re-checked, EXIT=0 with the seed skew gone. The classification arm is unit-tested
across five cases — generated-only and generated x2 auto-resolve; authored-only,
seed-Rust, and MIXED all refuse, the mixed case being the one that must not
partially resolve.

Verified: whole-tree compile 0 blocking errors; ci.yml regenerated from the
model; emitted guard bash -n clean.

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

* Close the roadmap focus over prerequisites

Owner membership alone was the wrong visibility rule. The v1 deletion lane's
critical path runs through pderive-source-slot-binding, whose prerequisite
namespace-canonical-binding lives in a lane the focus hid — so the page rendered
the fourth link of the spine as READY with its blocker invisible. Nothing was
red; the page lied by omission. That is worse for a dispatched agent than for a
person: a worker reads a ready-looking node, starts, and stalls on work the
roadmap never mentioned.

Visibility is now lane membership UNION the transitive prerequisite closure,
reusing std.graph.graph_reverse_reachable_members over the roadmap's own edge
relation rather than minting a second walk — the edge is already a dependency
edge, reverse reachability from a lane node is exactly its ancestors, and the
graph is acyclic by witness so the walk terminates by construction.

The closure found FIVE blockers where three were visible by eye:
namespace-canonical-binding has its own prerequisites, so the lane was blocked on
a namespace sub-chain of three. Manual inspection would have found those one
stall at a time, each costing a wasted attempt.

Pulled-in rows are named as prerequisites in the notice rather than counted as
lane work, so the lane's real size stays honest.

THE DISCRIMINATOR IS SYNTHETIC, deliberately (review). An earlier cut asserted
count(pulled) > 0 against the live roadmap_focus_selection, which contradicts the
carrier's own promise that changing or clearing the focus is a one-row view edit:
it fails the moment the focus is NoFocus, or a lane that legitimately has no
cross-owner prerequisite — reporting a defect where none exists and making the
focus row un-editable in practice. The fixture owns its two lanes and its
transitive cross-lane chain, so it holds under any live focus, and it exercises
TRANSITIVITY (b3 pulls b2 pulls b1) which is the part manual inspection misses.
A companion asserts the no-external-prerequisite case pulls nothing, so the empty
result is a tested outcome rather than an untested branch.

The notice claim is also strengthened per review: the test named
"pulled in and named as such" now actually checks the notice contains every
pulled id, which it previously did not.

The live-tree invariant is stated conditionally beside the fixture: whatever the
current focus is, no visible node may have a hidden prerequisite. That holds
under every focus including NoFocus, so it constrains the projection without
pinning the selection.

Split out of #7417 on review: the identity half needs identity on RoadmapNode, an
active/tombstoned lifecycle, and a base-versus-head permanence gate, and is being
rebuilt separately. This half is independent of that work.

Verified: whole-tree compile 0 blocking errors; ROADMAP.md regenerated from the
authority; 104 witnesses green with zero failures across roadmap_focus (12),
roadmap_authority, generated_artifact_drift, roadmap_page, doc_reachability (both
homes), roadmap_emit and roadmap_document.

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

* Give roadmap nodes a durable identity as a required field

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>

* Enroll the known-red acceptance witness on its executing probe roster

CI caught orphaned enrollment, which is the Phase 0(b) admission invariant
working exactly as intended:

  WITNESS ADMISSION REFUSAL cause=UnexecutedDeferredWitness count=1 —
  enrolled witness row(s) excluded from discovery name zero executing consumers

The deliberately-red acceptance witness was classified QuarantineProbeExpectRed
in witness_exclusion_frontier, which removes it from per-PR discovery. That is
half the job. The exclusion says where it does NOT run; it does not say where it
DOES. Without a matching row on known_red_probe_entries — the roster that
actually executes known-red witnesses on their probe cadence expecting red —
the witness ran nowhere, which is precisely the "enrolled, zero executions"
coverage-by-illusion the invariant refuses.

Adding the probe row completes the pair. The exclusion note now states the
pairing rule and cites this failure, so the next author reads why both rows are
required rather than discovering it from a floor red.

Verified: whole-tree compile 0 blocking errors; ci.yml/ROADMAP.md regenerated.

* Restore identity pass-through in with_execution

The fixture-identity script matched the RoadmapNode literal inside
with_execution and replaced its `identity: rn.identity` pass-through with a
hardcoded literal. Since with_execution wraps both bound nodes, both collapsed
onto one identity.

identities_are_unique_across_the_registry failed on the first attempt to violate
it — which is the claim's whole purpose: it is the one that notices when identity
stops being per-node. Caught before the change could ship rather than after.

Both combinators (sign, with_execution) now thread rn.identity. Verified: 4/4
identity witnesses green including identity_survives_a_slug_rename.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
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