Skip to content

File merge_forks_one_identity_beneath_a_roster_grained_check - #10186

Closed
gunbai-bot[bot] wants to merge 5 commits into
mainfrom
session/crisp-crane-370-dupe
Closed

gunbai-bot[bot] wants to merge 5 commits into
mainfrom
session/crisp-crane-370-dupe

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

A merge created a second declaration of one identity, outside any conflict hunk, and the roster check passed because it joins at the wrong grain.

The specimen

Resolving a third successive append collision on gunbc.recurring_failure_mode, git auto-merged declared_return_disagrees_with_the_generic_it_returns into two data declarations at 5049 and 5765 bytes. The branch had taken main's copy in an earlier merge; main then revised that row; the two arrived as separate insertions rather than as a conflict.

Both sat outside every conflict hunk. The marker scan came back clean, the merge reported itself resolved, and it would have merged green — a silent second authority for one fact, created by the merge tool rather than by any author, so no diff showed a decision and no reviewer saw a choice. The copies did not agree, so the module held two different answers under one name.

Why the check missed it — the part that generalises past merges

The roster entry and the declaration are two grains, and completeness was checked at the coarser one:

  • three roster entries, three distinct names, every name present → a passing identity join over roster lines
  • the defect lived one level down: two declarations behind one roster entry

§5 already rules that completeness is an identity join, not a count equality. This is that rule failing underneath itself — the join ran over the projection of the population rather than over the population. A roster is a projection of declarations, never a substitute for them.

An identity join is only as strong as the grain it enumerates.

Recognition rule

When a check enumerates identities from a roster, list, or index, ask what happens if the underlying declarations disagree with it. If it reads only the roster, it cannot see duplication, divergence, or absence below the roster — and it reports clean.

Mechanical tell after merging an append-only authority: count declarations per identity (grep -c '^data <name>:'), not roster entries or conflict markers. A merge reporting itself fully resolved on a file where both sides appended near one anchor is the trigger to look.

Rung and ceiling

Found at mitigatable — caught by hand, by checking declarations separately from roster lines. Nothing mechanical caught it, and nothing mechanical would have.

Ceiling is structurally impossible, and genuinely reachable rather than aspirational: duplicate top-level declaration names in one module are decidable from the parsed tree, and v1.declaration_index already builds declared as a per-module set and already computes duplicate_findings. The class waits on an existing decidable check becoming load-bearing at admission — not on a new capability.

The trigger is therefore stated as the capability — duplicates refuse on the real acceptance path, with a discriminating RED fixture declaring one name twice — deliberately not "wire up duplicate_findings", because a finding that is computed and reported still admits the program, and a trigger satisfied by a report would retire this row while the invalid state stayed writable.

Note on the resolution itself

The survivor was chosen by diffing against origin/main, not by preferring the longer text: length agreement is the same class of false comfort as the count.

docs/design-failure-modes.md regenerated with a binary built from this tree and verified idempotent on a second pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx

Brian Searls and others added 4 commits September 3, 2026 03:16
A merge created a SECOND declaration of one identity, outside any conflict hunk,
and the roster check passed because it joins at the wrong grain.

THE SPECIMEN. Resolving a third successive append collision on
gunbc.recurring_failure_mode, git auto-merged
declared_return_disagrees_with_the_generic_it_returns into TWO data declarations at
5049 and 5765 bytes: the branch had taken main's copy in an earlier merge, main
then REVISED that row, and the two arrived as separate insertions rather than as a
conflict. Both sat outside every conflict hunk, the marker scan came back clean,
the merge reported itself resolved, and it would have merged green -- a silent
second authority for one fact, created by the merge tool rather than by any author,
so no diff showed a decision and no reviewer saw a choice.

WHY THE CHECK MISSED IT, which is the part that generalises past merges. The roster
entry and the declaration are TWO GRAINS, and completeness was checked at the
coarser one: three roster entries, three distinct names, every name present -- a
PASSING identity join over roster lines. The defect lived one level down, in two
declarations behind one roster entry. DESIGN section 5 already rules that
completeness is an identity join and not a count equality; this is that rule
failing underneath itself, because the join ran over the PROJECTION of the
population rather than over the population. A roster is a projection of
declarations, never a substitute for them.

Recognition rule and the mechanical tell are in the row: after merging an
append-only authority, count DECLARATIONS per identity rather than roster entries,
and treat "both sides appended near one anchor, merge reports fully resolved" as
the trigger to look.

CEILING is structurally impossible and genuinely reachable rather than aspirational:
duplicate top-level declaration names in one module are decidable from the parsed
tree, and v1.declaration_index already builds `declared` as a per-module SET and
already computes duplicate_findings. So the class waits on an existing decidable
check becoming load-bearing at admission, not on a new capability.

The trigger says so as a capability -- duplicates REFUSE on the real acceptance
path, with a discriminating RED fixture declaring one name twice -- deliberately not
"wire up duplicate_findings", because a finding that is computed and reported still
admits the program, and a trigger satisfied by a report would retire this row while
the invalid state stayed writable.

Survivor selection is recorded too: chosen by diffing against origin/main rather
than by preferring the longer text, since length agreement is the same false comfort
as the count.

docs/design-failure-modes.md regenerated with a binary built from this tree and
verified idempotent on a second pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx
…s time

Fourth append collision on this roster. main now carries
non_execution_undifferentiated_by_what_it_silenced (#10145, landed), and this
branch adds merge_forks_one_identity_beneath_a_roster_grained_check.

RESOLVED USING THIS BRANCH'S OWN MECHANICAL TELL, which is the first use of the row
it files: counted DECLARATIONS per identity rather than roster entries or conflict
markers. Swept the whole file rather than the four names I expected to see:

  69 `data` declarations, 0 duplicated identities
  68 roster entries, 0 duplicated, 0 rostered without a declaration
  (69 = 68 rows plus the roster declaration itself)

So no duplicate this time -- which is the point of running the check when it passes,
not only when it fails. The previous merge produced one silently and the marker scan
came back just as clean then.

docs/design-failure-modes.md regenerated with a binary rebuilt AFTER taking main, so
the emitter does not predate what it emits, and verified idempotent on a second pass.
Both identities render.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx
…t swept

Fifth append collision on this roster, and the first where the AUTHORITY
auto-merged cleanly and only docs/design-failure-modes.md conflicted.

That is exactly the shape this branch's own row warns about, so the sweep ran
rather than being skipped because git reported the .dag resolved:

  70 declarations, 0 duplicated
  69 roster entries, 0 duplicated
  0 rostered without a declaration, 0 declared without a roster entry
  (70 = 69 rows plus the roster declaration itself)

Clean this time. The check is worth running precisely when it passes -- the merge
that produced a silent duplicate earlier in this stack also reported itself
resolved, and its marker scan was just as clean.

Projection REGENERATED with a binary rebuilt after taking main, verified idempotent
on a second pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx
… that refused

review 59144 is correct and the cause is worse than the symptom it names. I
committed CONFLICT MARKERS into dag/gunbc/recurring_failure_mode.dag, which made the
regenerator refuse, which left the projection stale — so the .dag carried an
identity the .md did not.

THREE MISTAKES IN A ROW, each of which hid the next.

1. I read the merge output with `tail -3` and saw only the
   docs/design-failure-modes.md conflict. There was a SECOND CONFLICT line above it,
   for the .dag, scrolled off the window I chose to look at.

2. My duplicate sweep is a REGEX over `^data ` lines, and conflict markers do not
   start with `data`, so the sweep read straight past them and reported clean. It
   checks for duplicate identities and cannot see a malformed file, which is a
   different question I was treating it as answering.

3. I ran the regenerator as `... >/dev/null 2>&1` and then verified idempotency by
   an EMPTY `git diff`. The regenerator was refusing —
   `module index refused: 1 unparseable .dag source(s) ... expected expression,
   found Lt` — and it EXITS 0 while doing so. A failed run writes nothing, so "no
   diff" is exactly what both success and failure produce. I had built an instrument
   that could not tell the two apart and then trusted it twice.

That third one is the same class this branch's own row is about, and the class
`instrument_output_read_as_subject_content` names: I read the absence of a diff as
evidence of agreement when it was evidence of nothing.

FIXED: both rows kept as distinct declarations —
authority_merges_unprotected_while_its_projection_is_guarded from main and
merge_forks_one_identity_beneath_a_roster_grained_check from this branch — and the
projection regenerated from the repaired authority.

VERIFIED DIFFERENTLY THIS TIME, because the previous check was the defect:
  - markers: 0
  - declarations 69, roster entries 69, no duplicates, symmetric difference empty
  - projection index 69, and an identity-join against the declarations both ways:
    nothing declared-but-unprojected, nothing projected-but-undeclared
  - regenerator re-run with output INSPECTED for refusal lines rather than
    discarded, then idempotency confirmed on top of that

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx
@gunbai-bot

gunbai-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

review 59144 is correct, and the cause is worse than the symptom it names. Fixed in the latest push. Recording the full chain because two of the three mistakes were in my verification, not in the edit.

What you saw: authority_merges_unprotected_while_its_projection_is_guarded present in the .dag and missing from the projection — a roster-grained check disagreeing with the underlying declarations, landed in the PR that describes that class.

What actually happened: I committed conflict markers into dag/gunbc/recurring_failure_mode.dag. The regenerator then refused (module index refused: 1 unparseable .dag source(s) … expected expression, found Lt), wrote nothing, and left the projection stale.

Three mistakes, each of which concealed the next:

  1. I read the merge output with tail -3 and saw only the docs/design-failure-modes.md conflict. There was a second CONFLICT line for the .dag scrolled off the window I chose to look at.

  2. My duplicate sweep is a regex over ^data lines. Conflict markers do not start with data, so the sweep read straight past them and reported clean. It answers "are there duplicate identities" and cannot answer "is this file well-formed" — I was treating one as the other.

  3. I ran the regenerator as >/dev/null 2>&1 and verified idempotency by an empty git diff. It was refusing the whole time — and it exits 0 while refusing. A failed run writes nothing, so an empty diff is exactly what success and failure both produce. I built an instrument that could not distinguish them and then trusted it twice.

That third one is the class this very PR files, and instrument_output_read_as_subject_content besides: I read the absence of a diff as evidence of agreement when it was evidence of nothing. The row's own recognition rule — ask what the check would do if the underlying thing disagreed — applied to my check and I did not run it on myself.

Fixed: both rows kept as distinct declarations, projection regenerated from the repaired authority.

Verified differently, since the old check was the defect:

  • markers: 0
  • declarations 69, roster entries 69, no duplicates, symmetric difference empty
  • projection index 69, identity-joined against the declarations both ways: nothing declared-but-unprojected, nothing projected-but-undeclared
  • regenerator re-run with output inspected for refusal lines rather than discarded, and idempotency confirmed only on top of that

I also checked the sibling branch (#10174) for the same defect: no markers, projection matches the authority, regenerator does not refuse.

— sent from crisp-crane-370

…ses this class

codex review 59159 is correct on both counts, and the row was materially wrong.

WHAT I CHECKED, by execution rather than by reading. Planted a duplicate top-level
declaration and ran it through the real entry point:

  resolve failed ... duplicate declaration 'planted_duplicate_identity' in module
  'test.claim.dup_probe' -- a second declaration of one name silently replaced the first

`v1.compiler.resolve` `check_duplicate_declarations` folds a module's top-level items
and emits CompilerDiagnostic.DuplicateDeclaration on the second occurrence. Conflict
markers refuse the same way as an unparseable source. And `duplicate_findings` in
v1.declaration_index reports DuplicateModuleDeclaration -- duplicate MODULE PATHS,
over a BTreeSet that cannot retain duplicate names at all -- so it was never the
mechanism this row named.

FOUR CLAIMS RETRACTED, and the retraction is IN the row rather than quietly applied:
that nothing mechanical caught the class; that the evaluator would silently choose
one of the two declarations; that the duplicate would have merged green; and that
duplicate_findings needed wiring to admission. All four false. A row whose first
version asserted a missing wall gets cited onward as evidence the wall is missing,
which is the failure mode the rung_drop precedent renamed rather than reworded for
exactly this reason.

DESIGN 4b(1) requires the reported rung to equal what executed evidence establishes,
and UNDERSTATING an existing guarantee violates that as surely as overstating one --
so the rung is now mechanically preventable at the acceptance path, already, with no
work owed, and the ceiling there is REACHED with no trigger named. Naming one would
imply a gap measurement says is closed.

WHAT SURVIVES IS NARROW AND IS ABOUT INSTRUMENTS, NOT GUARANTEES. The merge creates
the state with no conflict shown, and the local checks an author reaches for cannot
see it, so detection is deferred to whatever compiles next. The harm is bounded: the
state is committable but not acceptable, and it fails loudly and cheaply.

Specimen two is now the sharper one, because the instruments failed three ways at
once -- tail -3 hiding a CONFLICT line, a regex sweep reading past markers because
they do not begin with `data`, and a regenerator run into /dev/null whose refusal
exits 0, so an empty diff is what success AND failure both produce.

The recognition rule is rewritten around that: ask at what GRAIN a check reads and
what it would do if the file were malformed rather than merely wrong; and if a tool
writes nothing on failure, checking for ABSENCE of output cannot distinguish the two.
After merging an append-only authority, the check that discriminates is COMPILING it,
not grepping it.

Projection regenerated with the regenerator's output INSPECTED for refusal lines
(0), and identity-joined against the authority both ways: 69 declarations, 69
projected, no divergence either direction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx
@gunbai-bot

gunbai-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

review 59159 is correct on both counts, and the row was materially wrong. Reconciled rather than defended — and the correction is now in the row, not just in this thread.

Verified by execution, not by reading

Planted a duplicate top-level declaration and ran it through the real entry point:

resolve failed ... duplicate declaration 'planted_duplicate_identity'
in module 'test.claim.dup_probe'
-- a second declaration of one name silently replaced the first

check_duplicate_declarations folds a module's top-level items and emits DuplicateDeclaration on the second occurrence. Conflict markers refuse the same way, as an unparseable source. And duplicate_findings reports DuplicateModuleDeclaration — duplicate module paths, over a set that cannot retain duplicate names at all — so it was never the mechanism the row named.

Four claims retracted: that nothing mechanical caught the class; that the evaluator would silently choose one declaration; that the duplicate would have merged green; and that duplicate_findings needed wiring to admission. All false. The acceptance path refuses; CI compiles the module and would have stopped it.

The retraction is written into the row rather than quietly applied, because a row whose first version asserted a missing wall gets cited onward as evidence the wall is missing — the same reason the rung_drop precedent renamed a row rather than rewording it.

On the rung specifically: §4b(1) requires the reported rung to equal what executed evidence establishes, and understating an existing guarantee violates that as surely as overstating one. The row now reports mechanically preventable at the acceptance path, already, with no work owed, and states the ceiling there is reached with no trigger named — naming one would imply a gap measurement says is closed.

What survives, and it is narrower

The merge creates the state with no conflict shown, and the local checks an author reaches for cannot see it, so detection is deferred to whatever compiles next. The harm is bounded and the row now says so: the state is committable but not acceptable, and it fails loudly, locatedly and cheaply.

The second specimen is now the load-bearing one, because the instruments failed three ways at once — tail -3 hiding a CONFLICT line; a regex sweep reading past markers because they do not begin with data; and a regenerator piped to /dev/null whose refusal exits 0, so an empty git diff is what success and failure both produce.

The recognition rule is rewritten around that: ask at what grain a check reads and what it would do if the file were malformed rather than merely wrong; and if a tool writes nothing on failure, checking for absence of output cannot distinguish the two. After merging an append-only authority, the check that discriminates is compiling it, not grepping it.

A question I would rather ask than assume

With the mechanism corrected, this row is considerably thinner than filed: no missing guarantee, bounded harm, ceiling reached. What is left is detection latency plus a set of instrument lessons whose parent class (instrument_output_read_as_subject_content) already exists.

I think it still earns its place — the merge-created-silently half is real and the "compile it, don't grep it" rule is actionable — but I would not argue hard against withdrawing it, and I would rather say that plainly than ship something padded to justify the PR. Happy to close this if you read it the other way.

Projection regenerated with the regenerator's output inspected for refusal lines (0), and identity-joined against the authority both ways: 69 declarations, 69 projected, no divergence in either direction.

— sent from crisp-crane-370

@gunbai-bot

gunbai-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Recording a structural observation about this file, since it is discoverable from here and not from a chat thread. Not a proposal, and not part of this PR — it is the mechanism behind something measurable, left for whoever owns the roster.

dag/gunbc/recurring_failure_mode.dag conflicted on six consecutive merges during this stack. That is not bad luck about timing; it is inherent to the shape.

Every lane filing a class appends in two places at once:

  • a data <identity>: RecurringFailureMode = … declaration, immediately above the roster
  • an entry in the single recurring_failure_mode_roster: List<RecurringFailureMode> literal

Both insertion points are the same anchor for every lane — the end of the declaration block and the end of the list. So any two concurrent appends collide at two points, regardless of what the rows say or how far apart their subjects are. The conflicts are a property of the container, not of the content.

DESIGN already split this ledger out of DESIGN.md, and split failure-modes from rung-drops, on the reasoning that "one lane recording a failure mode must not rewrite the file another lane is appending a drop to." That split solved the growth problem — the ledger no longer regenerates the file every session loads in full. It did not address concurrent appends within one roster, which is what these six conflicts are.

Worth stating precisely because the cost is not the conflict itself. Resolving a two-point collision by hand is where the two defects on this branch came from: a duplicate declaration created outside any conflict hunk, and conflict markers committed into the authority. Both were caught, and the second only because a reviewer noticed the projection had drifted. A container that produces a hand-resolution on every concurrent append is a container that produces those defects at some rate.

I am not proposing a fix — I do not own this file, and the obvious moves (one file per row, or an order-insensitive container) have costs I have not measured. Recording the mechanism so the next person hitting it does not have to re-derive it from six merge logs.

The operational mitigation that did work, for anyone resolving this file before the shape changes: compile it, do not grep it. A regex sweep over ^data lines cannot see conflict markers — they do not begin with data — and the acceptance path refuses both duplicates and markers with a located diagnostic. The compiler is the discriminating check here; a line scan is not.

— sent from crisp-crane-370

@gunbai-bot

gunbai-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Withdrawing this PR

Not because the row is thin after its reconciliation — because it is redundant. The surviving content is an instance of a class this repository already carries, and filing it separately would be a §3 meaning fork.

The residue already has an owner, and it is not the row I expected

After codex review 59159 correctly established that the acceptance path already refuses duplicate declarations, what survived was one specimen: a regenerator that refused while an empty git diff was read as a clean idempotent run.

I had been filing that under instrument_output_read_as_subject_content. That is the wrong parent — and that row says so itself, in the paragraph justifying its own existence:

there a value is consumed as though an observation produced it while nothing proves the observation path RAN, and the fix is to bind an execution receipt; here the observation ran and reported FAITHFULLY

Mine is the first clause. The observation never ran.

The actual owner is execution_provenance_loss, which covers it almost verbatim. Its own specimens include "a floor whose TerminalLedgerUnrenderable count is 0 reads identically whether the ledger checks passed or preparation refused before a ledger existed", and its stated root is "a masked run and a clean run rendered identically." An empty diff after a refused regeneration is that sentence with a different tool in front of it.

A forked class is a worse outcome than an absent one: a thin row gets ignored, a forked one gets cited as separate coverage by two people who each assume the other class is someone else's problem.

The other half does not carry the row either

Post-reconciliation, merge-forks-a-declaration sits at: mechanically preventable at the acceptance path, already; ceiling reached; no trigger owed; harm loud and bounded. That is the system working plus a defect in my own verification — and a verification defect is not an error class.

What is not being lost

  • The corrected mechanism — that check_duplicate_declarations refuses, and that duplicate_findings reports duplicate module paths rather than names — is recorded in this thread and was the substance of the review that found it.
  • The operational rule (compile it, do not grep it; when a tool's failure mode is silence, a check for absence is uninformative) is already the stated repair on execution_provenance_loss.
  • The roster-container observation above stays here deliberately. It is not a failure-mode class, so it has no home in that ledger, and moving it would spend an append on the very file whose append cost is its subject. It will be folded into an annotation on the roster declaration the next time something legitimately touches that file.

Closing. The reconciled row is not landing, and that is the right outcome — it was materially wrong when filed, and correcting it revealed it was redundant rather than merely overstated.

— sent from crisp-crane-370

@gunbai-bot gunbai-bot Bot closed this Sep 3, 2026
@gunbai-bot

gunbai-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Note for anyone reading this later: review 59162 approved this PR, and it arrived after the PR was closed. The approval does not reverse the withdrawal, and the two do not conflict.

The review is correct on its own terms — the diff is a well-formed roster append with no substrate or authority changes riding along, which is what a hygiene review checks. That was never the question.

This PR was withdrawn on a ground a per-diff review does not look for: §3 redundancy against a row that already exists. The surviving content is an instance of execution_provenance_loss — "a masked run and a clean run rendered identically" — and filing it separately would be a meaning fork. Establishing that required reading a different row's boundary clause, not inspecting this diff. A reviewer scoped to this changeset would not find it and should not be expected to.

So the state is: well-formed, approved, and correctly not landing. An approval means no blocking defect was found in the diff; it is not evidence that the artifact should exist.

Leaving closed. No action needed.

— sent from crisp-crane-370

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.

0 participants