Skip to content

Complete G2 live-read selection and restore affected-set falsifier narrowing - #7997

Merged
briansrls merged 20 commits into
mainfrom
claude/falsifier-g2-runtime-read-closure
Aug 8, 2026
Merged

briansrls merged 20 commits into
mainfrom
claude/falsifier-g2-runtime-read-closure

Conversation

@briansrls

@briansrls briansrls commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

DRAFT — DO NOT MERGE. Steps 1–4 of a 7-step sequence. The seed still decides from the G1-only interim until the manifest lands, so merging this state leaves the .dag authority disagreeing with its only consumer, the falsifier red, and its corpus not executing.

Retitled from a simplification framing: this is a safety repair, and the previous description claiming downstream classification was maintained is not accurate.

What is broken

The affected-set falsifier's falsifier-build job fails, and while it fails the corpus never runs — so the falsifier provides zero coverage, not merely a red badge.

Clean-main axis trace for the unrelated entry B (affected_set_floor_runner_test.dag) isolates one firing axis:

reads_live_tree=false  import_closure=false  runtime_carrier=TRUE
declared_src=Absent    effect_reach=false    stop_line=false
touched_n=1            stopline_n=1

runtime_data_dependency_touched_via_carrier_closure consults touched_paths only as a nonempty guard, then answers from carrier-home reachability alone. An entry with a 474-module closure is therefore unskippable on any nonempty .dag diff. This is the documented G1-only interim (live-read-witness-classification-design.md P2) — not a regression from #7933, whose stop-line channel the trace rules out.

Landed so far

  • G1 union removed from completed G2. It made the answer a function of import reachability rather than reachable invocations. Wiring G2 unchanged would have preserved the failure verbatim.
  • LiveReadSelectionProjection (Classified | Refused) in v2.std.live_read. Without a refusal arm, an unavailable G2 producer silently answers LocalRead — under-selection, which presents as a faster green floor rather than a failure, and is strictly worse than the over-selection being repaired.
  • Declaration-grain root. call_reachable_decls seeded every function in the entry file; call_reachable_decls_from_root seeds the exact enrolled declaration. A manifest keyed per declaration with per-file values would imply precision it lacks.
  • Three completeness controls replacing g2_union_retains_g1_import_carriers_with_g2_literal_paths.
  • Mutation proof: restoring the union makes control 1 red.

Two controls that tested nothing

Recorded because the pattern is the finding, not either instance.

The two CI failures on this branch were not collateral damage — they were the union's cover being removed. Both asserted a conclusion their facts could not support: their subject's closure does not contain the carrier homes their fixture facts name, so fn_arrow_decl_facts_live() returns no declarations for them and no reachability evidence exists in that context. G1 supplied the carriers unconditionally, so they passed without the evidence ever being available.

Separately, planted control 3 emitted its callee fully qualified (v2.compiler.self_host.carrier_fn) while decls_matching_callee compares the bare decl.name. The planted carrier was never call-reachable; the control typechecked and would have reported success while exercising no reachability edge.

Both passed ordinary runs. Only execution and mutation exposed them.

Remaining before ready

  • consume dependency_resolution_facts_live → ModuleDependencyEdge (+ ModuleDeclarationFact, FnArrowDecl) as the live boundary; ModuleImportFact stays fixture-only
  • replace the "one declaration per carrier home" completeness heuristic with CompleteForSubject{index/tree identity} vs IncompleteOrUnbound{cause}
  • one compact declaration-grain manifest per MultiEntryIndex, subject-mismatch refusing
  • seed predicate swap + precompute parity
  • mutation-prove both incident shapes: the nonempty-diff carrier-home boolean, and the restored G1∪G2 union — each must recreate 2 selected / 1 expected
  • complete affected-set falsifier on the exact candidate SHA

Infrastructure changes are deliberately excluded from this PR.


🤖 Generated with Claude Code

https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs

INCOMPLETE BY CONSTRUCTION — this is step 2 of a 7-step sequence and must not
land alone. The operator ruling is explicit: "Do not land an intermediate state
where the .dag law changes but the seed still uses G1." That is exactly this
state. No PR is opened for this branch.

WHAT THIS CHANGES. live_read_classification_g2_from_facts unconditionally merged
the G1 carrier-home result back into G2 via merge_g1_and_g2_carriers, so a
"completed" G2 classification inherited every carrier home reachable through the
import graph. Wiring that into the seed unchanged would have preserved the
falsifier failure verbatim: completed G2 + broad G1 reachability = RuntimeRead on
every nonempty diff. A completed G2 result now contains only the carriers of
reachable invocations; G1 is fallback evidence for the case where G2 cannot be
established, and that case refuses rather than classifying.

WHY, measured. The clean-main axis trace isolates one firing axis for the
unrelated entry B (affected_set_floor_runner_test.dag):

  reads_live_tree=false import_closure=false runtime_carrier=TRUE
  declared_src=Absent effect_reach=false stop_line=false
  touched_n=1 stopline_n=1

runtime_data_dependency_touched_via_carrier_closure consults touched_paths only
as a nonempty guard and then decides from carrier-home reachability alone, so an
entry with a 474-module closure is unskippable on any nonempty .dag diff. That is
the documented G1-only interim (live-read-witness-classification-design.md P2),
not a regression from #7933.

REMAINING BEFORE THIS MAY LAND:
  - delete the now-dead merge_g1_and_g2_carriers / g1_carriers_from_classification
  - resolve the now-unused carrier_homes parameter (remove, or state its G1
    fallback role)
  - add the LiveReadSelectionProjection classified/refused seam
  - replace the pinning test g2_union_retains_g1_import_carriers_with_g2_literal_paths
    with the three completeness controls
  - produce the manifest from the whole-tree fn_arrow_decl_facts_live context,
    memoized at declaration grain on the exact MultiEntryIndex subject
  - replace the seed predicate; wire precompute parity
  - mutation-prove BOTH shapes red (nonempty-diff guard; G1 union restored)
  - dispatch the falsifier on the exact recovery SHA

Already present and reusable, so not rebuilt: runtime_read_touched_by_path_g2 +
filesystem_path_carrier_touched already implement the classified-case fold
(LiteralPath -> path match, ParamRef/UnknownPath -> conservative true).

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

cursor Bot commented Aug 7, 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.

Copy link
Copy Markdown
Contributor Author

⚠️ DO NOT MERGE — this is step 2 of 7 and is incomplete by construction

Flagging prominently because this branch was pushed deliberately without a PR, and merging it in its current state is specifically forbidden by the governing ruling:

"Do not land an intermediate state where the .dag law changes but the seed still uses G1."

That is exactly this state.

What merging this alone would do

It removes the G1 union from the completed G2 classification, but the seed still decides from runtime_data_dependency_touched_via_carrier_closure, which consults touched_paths only as a nonempty guard and answers from carrier-home reachability. So merging this now:

  • does not fix the falsifier — falsifier-build stays red, and while it is red the corpus never executes, so the affected-set falsifier keeps providing zero coverage;
  • leaves the .dag authority disagreeing with its only consumer, which is worse than either state alone.

Remaining before this may land (steps 3–7)

  • delete the now-dead merge_g1_and_g2_carriers / g1_carriers_from_classification
  • resolve the now-unused carrier_homes parameter of live_read_classification_g2_from_facts (remove, or state its G1-fallback role)
  • add the LiveReadSelectionProjection classified/refused seam
  • replace the pinning test g2_union_retains_g1_import_carriers_with_g2_literal_paths with the three G2-completeness controls
  • produce the classification from the whole-tree fn_arrow_decl_facts_live context; memoize a compact declaration-grain manifest bound to the exact MultiEntryIndex subject, refusing on subject mismatch
  • replace the seed predicate with manifest lookup + the existing carrier fold; wire precompute parity
  • mutation-prove both failure shapes red: restoring !touched_paths.is_empty() && reaches_any_carrier_home, and restoring the G1∪G2 union — each must recreate the 2 selected / 1 expected incident
  • dispatch the complete affected-set falsifier on the exact recovery SHA

Root cause, validated by execution

Clean-main axis trace for the unrelated entry B (affected_set_floor_runner_test.dag):

reads_live_tree=false  import_closure=false  runtime_carrier=TRUE
declared_src=Absent    effect_reach=false    stop_line=false
touched_n=1            stopline_n=1

Exactly one axis fires. This rules out the #7933 stop-line channel, the ordinary import closure, and the path-literal hypothesis. An entry with a 474-module closure is unskippable on any nonempty .dag diff — the documented G1-only interim (live-read-witness-classification-design.md P2), not a regression introduced by #7933.

Already present and reusable, so not rebuilt: runtime_read_touched_by_path_g2 + filesystem_path_carrier_touched already implement the classified-case fold (LiteralPath → path match, ParamRef/UnknownPath → conservative true).


Generated by Claude Code

claude added 3 commits August 7, 2026 16:53
…r_homes' role

Still step 2 of 7 — see the DO-NOT-MERGE notice on the parent commit and on
PR #7997. The seed still decides from the G1-only interim, so this must not land
alone.

Deletes merge_g1_and_g2_carriers and g1_carriers_from_classification, which the
union removal left with no callers. merge_carriers survives — six other users.

carrier_homes is RETAINED on live_read_classification_g2_from_facts rather than
deleted, with its role stated in g2_carrier_homes_parameter_role_note. A completed
G2 answer deliberately does not read it; the classified/refused projection still
to land needs it on the other path, where G2 facts are unavailable and carrier-home
reachability decides between refusing loudly and refusing at all. Deleting it now
and reintroducing it with that seam would be churn; leaving it silently unread
would read as an oversight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…eness controls

Steps 3-4 of 7. Still must not land alone — the seed continues to decide from
the G1-only interim until the manifest lands.

THE PROJECTION. LiveReadClassification answers what an entry reads and has no arm
for "we could not establish what this entry reads". That third state is not
cosmetic: an unavailable G2 producer that silently answered LocalRead would be
observationally identical to the defect this lane repairs, except failing in the
opposite and more dangerous direction — under-selection, which presents as a
faster green floor rather than as a failure. LiveReadSelectionProjection
(Classified | Refused) lives in v2.std.live_read beside the classification it
wraps, so no second classification type exists (DESIGN section 3).

WHAT COMPLETE MEANS. G2 concludes LocalRead by finding no reachable carrier
invocation. That is sound only when the facts contain the bodies where such an
invocation would appear. If the import closure reaches a carrier home whose
declarations are absent from the supplied fn-arrow facts, "no call found" and
"nothing to look at" are indistinguishable, and LocalRead asserts the stronger.
So carrier-home reachability decides COVERAGE, never the answer — G1 as evidence,
which is the role the completed-G2 recut left it.

WHY THE TWO PINNED TESTS WERE REPLACED RATHER THAN REPAIRED, measured. CI on this
branch failed exactly 2 of 7563 witnesses, both in this file, both asserting the
union. Their subject is entry-closure scoped: this witness does not import the
carrier homes its fixture facts name, so fn_arrow_decl_facts_live() returns no
declarations for them and no reachable-invocation evidence exists in that context
at all. The union was supplying the carriers, so the assertions passed without the
evidence ever being available. Remove it and the same subject answers LocalRead —
under-selection. Those tests were asserting a conclusion their facts could not
support, so they are replaced, not fixed.

THE THREE CONTROLS separate what the union test conflated:
  evidence present + negative   -> LocalRead
  evidence absent               -> Refused
  evidence present + imprecise  -> RuntimeRead (unknown argument)
Planted FnArrowDecls supply the coverage the live entry-closure cannot, reusing
the production_qualification_origin_probe planting idiom, so the complete-facts
cases are reachable now rather than blocked behind the whole-tree manifest.

Whole-tree compile-clean over the projection alone: exit 0, zero diagnostics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…ted nothing

Two operator corrections. Still step 4/7 — the seed decides from the G1-only
interim until the manifest lands, so this must not merge.

ROOT GRAIN. call_reachable_decls seeds decls_in_module(entry_module) — every
function in the entry FILE. One sibling calling a live-read carrier makes every
witness in that file inherit it, so their classifications become
indistinguishable. Left in place, the manifest this lane is building would have
been keyed per declaration while its VALUES were per file: worse than an honestly
coarse manifest, because the key implies a precision the value does not have.
call_reachable_decls_from_root seeds the exact enrolled function instead. Entry
skip is then DERIVED by conjunction over the entry's rows rather than assumed.

A CONTROL THAT TESTED NOTHING. decls_matching_callee compares decl.name — the
BARE name — against callee atom text. The planted control 3 emitted the callee as
"v2.compiler.self_host.carrier_fn" against name "carrier_fn", so string_eq never
matched and the planted carrier was not call-reachable at all. The control
typechecked and would have reported success while exercising no reachability
edge. Spelling corrected to the bare name.

That is the second self-certifying control this session — the first asked its
question with an all-zero SHA that rev-parse rejected one guard earlier. Both
passed the ordinary run and both were exposed only by execution or mutation, not
by review or typecheck. Recorded because the pattern, not either instance, is the
finding: a control's green says nothing until something proves it can go red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
@briansrls briansrls changed the title Simplify live read classification by removing g1 computation Complete G2 live-read selection and restore affected-set falsifier narrowing Aug 7, 2026
@briansrls
briansrls marked this pull request as draft August 7, 2026 18:35
claude added 16 commits August 7, 2026 18:45
…g a match failure

Step 4/7. Still must not merge — the seed decides from the G1-only interim until
the manifest lands.

EXECUTED RATHER THAN TYPECHECKED, which is the whole point:

  complete facts, reachable home, no carrier call -> LocalRead      true
  reachable filesystem_read with unknown argument -> RuntimeRead    true
  carrier-home facts missing -> Refused                            match failure

Control 3 only became meaningful with this commit's predecessor: its planted
callee was fully qualified while decls_matching_callee compares the BARE
decl.name, so the planted carrier was never call-reachable and the control
exercised no reachability edge while reporting success.

CONTROL 2's MECHANISM IS CORRECT; THE TEST HELPER IS NOT. The projection returned
LiveReadSelectionRefused with exactly the authored cause for exactly the right
subject, then the helper's match failed to select an arm that syntactically
exists for that variant. Both variants are imported and both sibling helpers
match the same type successfully — but they only ever take the Classified arm.
Control 2 is the only case that reaches Refused at runtime.

A value that IS the variant, against a match that HAS an arm for it, failing to
match, points at constructor identity across a module boundary: the lens
constructs it by bare reference while the test imports it explicitly. That is the
ensure_is_converged pool-resolution class one layer over, and it matters beyond
this test — the seed manifest decodes these same variants, where the same failure
would present as a memo bug rather than a resolution one. Probing before wiring.

Two invocation errors of mine are recorded so the receipts are not read as
stronger than they are: I first ran controls 2 and 3 under names I had
misremembered, and then, seeing the runner refuse, grepped for the invoked names
rather than reading the file and wrongly concluded the controls were never
written. Both wrong, both mine, both caught by looking at the source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…ing a wildcard match failure

Step 4/7. Must not merge — the seed still decides from the G1-only interim.

THE ANOMALY THAT MADE THIS DIAGNOSABLE. Control 2's helper failed as a
non-exhaustive match on a LiveReadSelectionRefused value. Inverting arm order and
adding a `_` wildcard did not fix it — and a wildcard matches anything, so arm
selection never ran. The matcher did not accept the scrutinee as an inhabitant of
the type being matched. Exactly one declaration of LiveReadSelectionProjection
exists in the corpus, so a duplicate type is ruled out; the lens's constructed
value and the test's annotated type are nonetheless not one identity.

MECHANISM, and it is this repository's documented one. v2.lens.live_read_classification
carries ZERO imports and reaches every type by bare cross-module reference. Those
resolve by pool-membership coincidence: LiveReadClassification and LocalRead
happen to be dragged in by unrelated importers, while a newly added type has no
such incidental importer. That is the ensure_is_converged class from earlier
today — a bare reference does not select an identity, it hopes one is present —
and it is the reason DESIGN blocks further import-stripping.

WHY THIS MATTERS BEYOND A TEST HELPER. The seed manifest decodes these same
variants. Had the import gone in silently with the manifest, the identical
failure would have surfaced inside the memo decode and read as a caching bug
rather than a resolution one. Finding it against a three-line helper is the
cheap version.

Explicit import added; retest running. If control 2 does not green, a
single-declaration type failing a wildcard match is a substrate defect rather
than this lane's business, and it goes to the operator instead of being worked
around.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…te construction from match

Step 4/7. Must not merge.

RETRACTING 56c8ad2, both halves.

Its diagnosis was wrong. It claimed the lens's bare cross-module reference failed
to resolve LiveReadSelectionRefused, by analogy to the morning's
ensure_is_converged homonym. But controls 1 and 3 pass, and they construct
LiveReadSelectionClassified in that same import-less lens and match it in the
test. Both constructors resolve. So this is not module resolution, and the
analogy was pattern-matching on the day's earlier incident rather than on this
evidence.

Its edit was harmful. Adding one import to an import-less file switches that file
from pool-coincidence resolution to import-scoped visibility, so every remaining
bare reference stops resolving — `path_matches_touched` first. Recorded as a
finding in its own right: an import-less .dag file cannot be given "just one"
import, and any future migration of such a file must import everything it
references in the same motion.

A near-miss worth recording. `git checkout --` on the lens restored the COMMITTED
state, which contained the bad import, so a probe launched right after "reverting"
was measuring a tree with an unresolved symbol. Caught by re-counting imports
instead of trusting the word revert. Its output would have been noise readable as
signal.

WHAT IS ACTUALLY ESTABLISHED: two controls green; the refusal mechanism fires with
the correct cause and subject; a `_` wildcard fails to match a Refused value;
exactly one declaration of the type exists. What is NOT established is why Refused
specifically fails to match. The asymmetry between the arms — one carries a
concat-built String — is the next candidate, and the running probe constructs
Refused inside the test module to separate construction site from match site
before any further hypothesis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…e, not the caller

Step 4/7. Must not merge.

FOUR HYPOTHESES, ALL REFUTED BY EXECUTION:

  cross-module constructor identity  refuted — construction AND match in the same
                                     module fail identically
  arm ordering / missing arm         refuted — `_ => true` is present and the match
                                     is still reported non-exhaustive
  duplicate type declaration         refuted — exactly one declaration corpus-wide
  concat-built cause string          refuted — a plain literal "locally constructed"
                                     fails the same way

A `_` wildcard matches anything, so a non-exhaustive report against a wildcard
means arm selection never ran: the matcher does not accept the value as an
inhabitant of the type being matched. That is not a property a caller can get
wrong, so this is a substrate question rather than a test-helper one. The probe
added here checks whether the FIRST variant matches where the second does not; if
Classified matches locally and Refused does not, variant registration for a newly
declared coproduct is implicated.

WHAT WILL NOT BE DONE. Control 2 will not be rewritten around the failure. Its job
is to prove that missing G2 facts REFUSE rather than silently answering LocalRead
— the under-selection guard, and the most dangerous direction in this lane. A
version that dodges the matcher would be the fourth self-certifying control of the
session; a red control with a diagnosis is worth more than a green one with a
workaround.

The refusal MECHANISM is unaffected and demonstrably correct: it fires with the
authored cause for the right subject. Only the ability to pattern-match the result
is broken, so the lane is blocked on this alone, not on its design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…ariant constructors

Step 4/7. Must not merge.

A FIFTH HYPOTHESIS, and the first with a controlled contrast already in the file.

Previously refuted, all by execution: cross-module construction, arm ordering,
duplicate declaration, concat-built payload, declaration formatting (the corpus
idiom uses the same `= Variant { .. }` form on the `=` line, and LiveReadCarrier's
first arm matches fine), and stale disk cache (the tier is opt-in via
GUNBC_RESOLVED_GRAPH_CACHE_DIR and is unset).

The "first arm never matches" rule inferred one commit ago was an ARTIFACT of
which values happened to be tested, not a rule. Both arms fail for this type; the
sibling helpers only ever exercised the arm belonging to the other one.

THE CONTRAST. Two coproducts declared in the same module, matched in the same test
by the same interpreter:

  LiveReadClassification         test imports the TYPE only;
                                 LocalRead / RuntimeRead resolve BARE   -> matches
  LiveReadSelectionProjection    test imports the type AND both variant
                                 constructors explicitly                -> fails

Everything else is held fixed. This commit drops the two constructors from the
import list, leaving the type, so they resolve bare like LocalRead does.

If the probes pass, the finding is that explicitly importing a variant constructor
breaks pattern matching on it — a substrate defect with reach well past this lane,
because the more precise-looking import silently yields a match that can never
succeed. It would also be the third time today that explicitness and
resolve-by-coincidence diverge; in the previous two, explicitness was the repair,
and here it may be the fault.

If they still fail, the import is not implicated and there is no working
hypothesis — which is what will be reported, rather than a sixth guess.

Control 2 stays red until it passes for the right reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…ng and reduce

Step 4/7. Must not merge.

SEVEN HYPOTHESES REFUTED BY EXECUTION, so this stops guessing and reduces instead:

  cross-module constructor identity   construction AND match in one module fails
  arm ordering / missing arm          `_ => true` present, still non-exhaustive
  duplicate type declaration          exactly one declaration corpus-wide
  concat-built payload                plain literal fails identically
  declaration formatting              matches the corpus idiom; LiveReadCarrier
                                      uses the same form and matches fine
  stale disk cache                    tier opt-in via unset
                                      GUNBC_RESOLVED_GRAPH_CACHE_DIR
  explicit variant imports            dropping them from the import list changes
                                      nothing

THE FACT THAT BREAKS EVERY TIDY STORY: projection_classification_is_local matches
this SAME type successfully — controls 1 and 3 depend on it — while three sibling
functions matching the same type fail. "First arm never matches" was inferred
twice and refuted twice by the next data point.

variant_match_minimal_probe_test.dag is the reduction: ten lines, one two-arm
coproduct, matched in both directions, no imports, no lens, no fixtures. It splits
the space decisively. If it fails, a two-arm coproduct match is broken in the
substrate and reproducible in ten lines, which is its own lane and not #7997's
business. If it passes, the fault is specific to LiveReadSelectionProjection or to
that test file, and the repro is the baseline to bisect against.

A ten-line reproduction is worth more than an eighth theory.

Control 2 remains red. It will not be rewritten around the matcher: it is the
under-selection guard — the control proving missing G2 facts REFUSE rather than
silently answering LocalRead — and a green obtained by dodging the failure would
be worth less than nothing.

Steps 5-7 are not blocked by this. The refusal mechanism is proven correct, so
bridge option 3 and the subject-bound completeness receipt can proceed in
parallel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…card-payload

Step 4/7. Must not merge.

THE REDUCTION ELIMINATED THE SUBSTRATE. variant_match_minimal_probe_test declares
a two-arm coproduct locally and matches it in both directions with wildcard
payloads: both true. So coproduct matching is not broken, and the fault is
specific to this type or this file.

THE FIRST HYPOTHESIS THAT FITS EVERY DATA POINT, not most of them:

  type declared   payload binding        result
  local           { a: _ }               works   (minimal repro)
  foreign         { classification: c }  works   (controls 1 and 3)
  foreign         { cause: _ }           fails
  foreign         { classification: _ }  fails

Foreign-declared coproduct plus a wildcard payload binding does not match. This
accounts for the fact that defeated every earlier theory: projection_classification_is_local
matches the SAME foreign type successfully, because it binds the payload by name
rather than with `_`. "First arm never matches" fit two points and broke on the
third; "explicit variant imports" fit a surface difference and not the outcome.

variant_match_foreign_probe.dag plus the two probes here are the clean 2x2: same
foreign type, same test module, only the payload binding differs.

If wildcard fails and named passes, the defect is pinned to a one-line difference,
and control 2 gets a fix that is not a dodge — bind the payload by name instead of
`_`, because `_` provably does not work across a module boundary, with the defect
documented separately for whoever owns the compiler. If both pass, something
remains specific to LiveReadSelectionProjection, and the last unexplored axis is
what distinguishes it from LiveReadClassification in the same module.

Eighth hypothesis, which is more than anyone would like. The justification is that
each round has been cheap and strictly eliminative, and the blocked control is the
under-selection guard — the one whose absence lets the floor silently skip
witnesses it should run.

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

Step 4/7. Must not merge.

The 2x2 refuted hypothesis 8: a FOREIGN-declared coproduct with WILDCARD payloads
matches fine (both probes true). So neither the declaration site nor the wildcard
binding is the fault, and the elimination table is now:

  substrate coproduct matching     local repro passes both arms
  foreign declaration site         foreign probe passes
  wildcard payload binding         foreign probe passes with { a: _ }
  payload type                     Refused carries String and still fails
  constructor imports              removing them changed nothing
  arm order / duplicate decls /
  concat payload / formatting /
  disk cache                       all refuted earlier

ONE VARIABLE REMAINS, and it is the one only half-tested before: the TYPE NAME
being explicitly imported. ForeignTwoArm resolves bare in its probe and matches;
LiveReadSelectionProjection is imported by name and does not. The earlier round
removed only the CONSTRUCTORS from the import list and left the type, so this axis
was never cleared. This commit drops the type too, leaving the file to resolve it
bare exactly as it already resolves LocalRead and RuntimeRead.

If confirmed, control 2's fix is one line and not a dodge — reference the type the
same way the working sibling helpers reference theirs — and the defect is written
up as its own lane, because "an explicit type import silently yields an unmatchable
value" would bite every file the namespace migration touches.

If refuted, this stops. Nine eliminative rounds is enough; the repro artifacts are
committed and the table goes to whoever owns the compiler rather than a tenth
guess.

Either outcome, step 5 starts next: bridge option 3 and the subject-bound
completeness receipt. The matcher question is a side lane; the falsifier stays
dark until the manifest ships, and that is the critical path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
… result produced

Step 4/7. Must not merge.

Hypothesis 9 (explicit type import) refuted: dropping LiveReadSelectionProjection
from the import list, leaving the file to resolve it bare exactly as it resolves
LocalRead, changed nothing. Both Refused cases still fail; control 1 still passes.

STOPPING AT NINE WAS THE STATED PLAN AND IS BEING BROKEN DELIBERATELY. The ninth
result did not merely refute — it produced a pattern out of data already in hand:

  function                          arm order              value      result
  projection_classification_is_local  Refused, CLASSIFIED    Classified  pass
  projection_is_refused v1            REFUSED, Classified    Refused     fail
  projection_is_refused v2            CLASSIFIED, wildcard   Classified  fail

Every failure is a value matching the FIRST arm. Every success is a value matching
the SECOND. That fits every observation in the investigation, costs one test, and
yields a one-line fix if true. Stopping on principle there would have been
stubbornness, not discipline.

This commit puts Refused in SECOND position with both payloads bound by name and
no wildcard, then matches a Refused value.

IF CONFIRMED the finding is serious and belongs in its own lane: the first arm of a
match over a foreign coproduct never matches, which does not error — it MISROUTES
control flow, silently, anywhere it occurs. The local minimal repro passing is
consistent with that if locally declared types take a different path, which is
exactly what would let such a defect survive undetected.

IF REFUTED this stops for real and the elimination table goes to whoever owns the
compiler.

Either way step 5 begins next: bridge option 3 (dependency_resolution_facts_live ->
ModuleDependencyEdge, ModuleImportFact fixture-only) and the subject-bound
completeness receipt. The falsifier stays dark until the manifest ships; control 2
can green later once the matcher question has an owner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
Step 4/7. Must not merge.

Hypothesis 10 (arm position) refuted: LiveReadSelectionRefused fails in SECOND
position too, with both payloads bound by name and no wildcard present. Stopping
here as committed.

TEN ROUNDS, ALL REFUTED BY EXECUTION:

  substrate coproduct matching   local minimal repro passes both arms
  foreign declaration site       foreign probe passes
  wildcard payload binding       foreign probe passes with { a: _ }
  payload type                   Refused carries String and still fails
  constructor imports            removing them changed nothing
  explicit type import           removing it changed nothing
  arm position                   Refused fails first AND second
  arm order / duplicate decls /
  concat payload / formatting /
  disk cache                     refuted earlier

WHAT REMAINS TRUE AND UNEXPLAINED: LiveReadSelectionClassified matches (controls 1
and 3 depend on it); LiveReadSelectionRefused never matches, in any position, with
any binding, imported or bare. Both are arms of one coproduct with a single
declaration, in a module whose OTHER coproduct (LiveReadClassification) matches
both of its arms fine — RuntimeRead is likewise a last variant with a record
payload and it matches.

Two committed artifacts for whoever owns the compiler:
  src/v2/test/claim/variant_match_minimal_probe_test.dag   local type, passes
  src/v2/test/fixture/variant_match_foreign_probe.dag      foreign type, passes
Both are green, so they bound the defect from the working side rather than
reproducing it — the reproduction is control 2 itself.

Diagnostic scaffolding removed. The helper is a clean two-arm match and control 2
stays RED, which is the honest state: its mechanism is proven correct (it refuses
with the authored cause for the right subject) and only the ability to
pattern-match the result fails. It is not rewritten around the matcher because it
is the under-selection guard, and a green bought by dodging would be worth less
than nothing.

Step 5 next: bridge option 3 and the subject-bound completeness receipt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…cy edges

Step 5 of 7 begins. Must not merge: the seed still decides from the G1-only
interim until the manifest and predicate swap land.

live_read_classification_g2_from_edges consumes ModuleDependencyEdge +
ModuleDeclarationFact + FnArrowDecl, keyed by entry PATH, entry module, and the
exact enrolled FUNCTION — so it uses step 4's declaration-grain root rather than
seeding every declaration in the entry file.

WHY ModuleDependencyEdge AND NOT ModuleImportFact (operator ruling). module_graph
declares dependency_resolution_facts_live the single swappable edge producer and
forbids a second one, because the namespace terminal step replaces its BODY when
the import grammar is deleted. Calling import_resolution_facts_live instead would
omit the strict-tier reference-derived edges that producer already unions in, so
the G2 closure would be NARROWER than the selection closure deciding the same
question one axis over — an under-selection source. It would also bind live
classification to a grammar scheduled to disappear, so the namespace swap would
silently narrow G2 rather than leaving it untouched, which is the exact failure
the edge-source-agnostic-by-construction note exists to prevent.

ModuleImportFact stays fixture-facing. It is a hand-authorable {module, imports}
view with NO live producer in the corpus — which is what makes it good for planted
tests and unfit for production: a shape nothing derives cannot be kept honest
against the tree. The seed does not mint it either; asking the host to fabricate
the fact shape would put fact production back in the layer this lane repairs.

TWO MECHANICS WORTH RECORDING. ModuleDeclarationFact {module, path} is the
path/module bridge: dependency_closure works in PATH space while G2 reachability
works in MODULE space, so modules_of_closure_paths translates using the same fact
rows the closure was built from rather than a second mapping. And FreeMonoid<T> IS
the list type — fold_list takes it directly — so no conversion is needed; a
free_monoid_to_list call was written and removed before building.

Next in step 5: the CompleteForSubject{index/tree identity} vs
IncompleteOrUnbound{cause} receipt replacing the one-declaration-per-carrier-home
heuristic, then the declaration-grain manifest and its MultiEntryIndex-bound memo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…d on a partial one

Step 5 of 7. Must not merge — the seed still decides from the G1-only interim.

5a typechecked clean (whole tree, GATE_EXIT=0).

COMPLETENESS IS NOT INFERABLE FROM THE FACT LIST, which is why the previous
heuristic was wrong in KIND rather than merely weak. It asked whether every
reachable carrier home contributed at least one declaration — that is, it asked
the facts to vouch for their own coverage, which is the question it was trying to
answer. A List<FnArrowDecl> holding one planted declaration is indistinguishable
from a whole-tree projection with one match.

G2FactSubject is therefore CARRIED from the producer:
  WholeTreeFactSubject { subject_id }   the exact index/tree the facts came from
  PartialFactSubject   { cause }        a fixture, planted list, or narrower context

THE GATE IS DELIBERATELY ASYMMETRIC, and the asymmetry is the entire content of
the type:
  partial + RuntimeRead -> classified   seeing a call proves a call exists; positive
                                        evidence survives a narrow view
  partial + LocalRead   -> REFUSED      absence of evidence in a partial view is not
                                        evidence of absence

Only the verdict that can license a skip requires the whole-tree subject, so
under-selection is unreachable through this path rather than merely discouraged.

It also makes the memo CHECKABLE rather than only fast: a manifest carries the
subject it was built against, and a lookup whose current index differs refuses
instead of serving a classification derived from another tree. Evidence that
cannot name its subject cannot be checked against it — the same defect class as
this morning's baseline that did not name the commit it compared.

Next: 5c, the declaration-grain manifest and its MultiEntryIndex-bound memo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…uses on mismatch

Step 5 of 7. Must not merge — the seed predicate is not swapped yet.

5a and 5b both typecheck clean on the whole tree (GATE_EXIT=0), so the .dag half
of step 5 is complete: production G2 consumes the canonical ModuleDependencyEdge
boundary, and the carried G2FactSubject refuses LocalRead on a partial subject
while still classifying RuntimeRead.

THE MANIFEST NAMES ITS SUBJECT. G2 classification is expensive — it needs fn-arrow
facts reflected from a whole-tree resolved context, which is why the design pairs
path-intersection with memoization rather than treating the memo as an
optimisation. But a memo whose validity is implicit is a cache with no
invalidation story: it serves whatever it holds and cannot say which tree the
answer came from. subject_id is the content identity of the source population the
classifications were derived from, and a lookup whose current index differs
REFUSES. No invalidation heuristic is needed because the subject identity IS the
validity condition. Same discipline the freeze-gate baseline needed this morning,
with a worse failure mode: a stale classification that looks fresh would license a
SKIP.

BOTH FAILURE PATHS REFUSE RATHER THAN RETURN ABSENCE. Subject mismatch refuses; a
missing declaration row refuses. Neither yields a None a caller could read as
"nothing known, proceed" — an absent row is a gap in the classification, not
evidence of no runtime read. Refused is deliberately not an error channel: it is
the state that FORBIDS a skip, so it must survive into the seed rather than being
flattened to a bool.

ROWS ARE DECLARATION GRAIN. One verdict per entry would make a single live-read
witness render every sibling in its file unskippable — the same precision loss the
G1 union caused one layer down, and the reason step 4 moved the root off the file.

Remaining in 5c: the producer that evaluates the projection against a whole-tree
context and fills the manifest, plus the memo field on MultiEntryIndex. Then 5d
swaps the seed predicate and 6 gives precompute the same result.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…e context

The manifest is built by a single evaluation folding over every request,
not one evaluation per declaration: the edge/node/decl closure is derived
once and shared. Per-request evaluation would recompute the whole-tree
projection for each row, which is the affordability problem the G2 design
names when it pairs classification with carrier memoization.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…ng consumer

Builds the whole-tree manifest in ONE evaluation over the full request set
and decodes each row. Every failure arm refuses: a partial manifest would
convert the producer's own failure into a skip licence for the rows it
dropped, which is the under-selection direction this lane exists to close.

Three executing tests: the producer discriminates a carrier-reaching
declaration from a pure one against the real tree, and the lookup refuses
on both subject mismatch and absent declaration.

Also escapes a literal brace in a lens doc string that the module-binding
parser read as interpolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
…ind the root, mint the subject

Addresses the review on 351e4d2. Six changes, each closing a hole that made
the manifest unable to carry what the selector needs:

FULL MIRROR. LiveReadClassification, LiveReadCarrier and PathPattern are
mirrored arm for arm instead of collapsing to runtime_read: bool +
Vec<String>. That collapse discarded literal-path identity, param-vs-unknown,
and scan-vs-reflection — leaving a consumer only 'some runtime read happens',
whose one safe reading is 'every nonempty diff touches it', which is the G1
over-selection rebuilt a layer up. touched_by is one total fold over the
mirror: literals compare, param/unknown/structural-scan are touched by any
nonempty diff as the model's precision frontier, LocalRead never, Refused
always.

DECODER. Carriers arrive as Value::Variant, and the previous decoder handled
only Value::Str and Value::Record with a wildcard empty-vector fallthrough — so every
real carrier decoded to an EMPTY carrier list, which reads as 'runtime read,
intersecting nothing' and licenses the skip the runtime read forbids. That
absorbing arm sat directly beneath a doc-comment claiming fail-closed
behaviour, and the old discriminating test passed through it because it
compared only the bool. Nine controls now execute the five carrier arms plus
the non-variant and unmodelled-variant refusals.

ROOT BINDING. entry_path/entry_module/fn_name were three independently
writable strings: the closure walked from the path while seeding used the
separately supplied module and name. Zero matches seeded an empty walk and
classified LocalRead — the strongest answer, licensing a skip; many were
silently unioned. bind_g2_root derives the module from the path and refuses
zero-or-many at both transitions.

SUBJECT. Minted by MultiEntryIndex as an opaque generation, never a caller
string; WholeTreeFactSubject is nullary and constructible only where the live
whole-tree producers are called. The mismatch control now uses a real second
index over identical source roots, which a caller-authored label could not
have distinguished.

PARTIAL NEVER DECIDES. The asymmetric gate accepted partial RuntimeRead on
the reasoning that positive evidence survives a narrow view. Existence
survives; the PATH SET does not, and that set is what selection consumes — a
partial view of a two-literal closure still says RuntimeRead while exposing
one path, so a diff touching the other licenses a skip. Every partial subject
refuses, retaining the classification as diagnostic payload.

MEMO + DUPLICATE KEYS. One manifest per index, refusals memoized too; a
duplicate declaration key refuses rather than letting HashMap::insert discard
a classification without saying which.

Probe files deleted.

Green by execution: 9/9 decoder and selection-fold controls. Producer
controls still to re-run serially — four whole-tree indices concurrently
OOM-killed the run.

STILL OPEN from the review: whole-tree fn-arrow coverage is a construction
wall around an unproven premise (recorded in whole_tree_subject_mint_site_law,
needs the outside-closure control); the G1 production predicate is still in
place; precompute parity, CI enrolment of these controls, and both mutation
proofs remain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012DrzU3z7PJzhN5mxvZAfAs
@briansrls
briansrls force-pushed the claude/falsifier-g2-runtime-read-closure branch from 3412fe2 to 7b3b9a9 Compare August 8, 2026 05:26
@briansrls
briansrls marked this pull request as ready for review August 8, 2026 15:45
@briansrls
briansrls merged commit 51c95f1 into main Aug 8, 2026
6 checks passed
@briansrls
briansrls deleted the claude/falsifier-g2-runtime-read-closure branch August 8, 2026 15:46
briansrls pushed a commit that referenced this pull request Aug 8, 2026
…tion, and a retracted claim reproduced

TWO SELF-INFLICTED, both found by reading `gunbc_ci_selection_control_step_note`
rather than by anything executing.

(1) `stop_line_population_does_not_widen_incident_subject` built its injected
name-status with `\u{0}` — an actual NUL byte. The separator is the literal
four-character sequence `\000`: `floor_diff_observe` splits the injected value on the
STRING "\\000", which is also what the suite's own `injected_name_status_modify`
emits. A NUL would have decoded as one unsplit path, so the control would have
exercised a population it does not name — passing or failing for a reason unrelated
to the stop line. That is the shape of the two controls #7997's body recorded as
testing nothing, reproduced here.

(2) The previous commit's correction to `falsifier_rehomed_bin_wet_rows` said the
suite is "caught within one cadence window rather than at merge". That exact phrasing
was ALREADY RETRACTED in the tree as too strong (`gunbc.ci_workflow`
`gunbc_ci_selection_control_step_note`, review correction 2026-07-30): the falsifier
is queue-not-cancel and the step sits after the prelude and release build, so a
regression landing just after a run passed the control waits for that run's
remainder, plus queued-run startup, plus the next run's prelude and build — a sum
that can exceed one cadence period. The honest statement is that it reds when the
NEXT COMPLETED control runs.

So a commit whose whole subject was correcting a stale citation introduced a claim
the tree had explicitly rejected, into a load-bearing roster, one commit later. It now
uses the words the retraction settled on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Aug 10, 2026
…roup_by and execute the interpreter-parity controls CI only compiled (#8055)

* Consume the G2 live-read manifest at every selection decision; retire the G1 carrier-home predicate

#7997 landed the declaration-grain G2 classification, the canonical dependency-edge
production, the subject-carrying manifest and its per-index memo — but left the old
answer in charge. Every production decision still called
`runtime_data_dependency_touched_via_carrier_closure`, whose only use of
`touched_paths` was `is_empty()`; it then answered from broad carrier-home
reachability with no path intersection at all. So an entry with a wide import
closure stayed unskippable on any nonempty `.dag` diff, which is the
`2 selected / 1 expected` incident and the `SelectionSuperset` the floor reports.

ONE CONTEXT, ONE DECISION. `LiveReadSelectionContext` is built once per index from
the COMPLETE discovery roster and answers axis (iv) through
`runtime_dependency_touched_for_entry`. An entry is untouched on this axis only when
every enrolled declaration is present, classified, and untouched. Producer failure,
subject mismatch, absent row, absent entry, and an entry with no enrolled
declaration all REFUSE: none falls back to the retired predicate, silently widens to
a whole-corpus run, or licenses a skip. A missing classification is not a missing
runtime read.

FIVE CONSUMERS, NOT FOUR. Beside the four named decisions,
`discovery_rows_runtime_dependency_touched_count` fed the precompute's
`touched_runtime_dependency_entry_count` through a SECOND carrier fold. That is how
a retired predicate survives its replacement — the precompute keeps a copy — so it
now counts by calling the same function the per-row path decides with. Parity is
construction, not two implementations a later edit can separate.

THE COMPLETE-ROSTER REQUIREMENT IS ENFORCED, NOT DOCUMENTED. The memo is
index-scoped and request-set insensitive, so a first call carrying a chunk would
memoize a partial manifest and every declaration outside it would afterwards refuse.
`live_read_manifest` now refuses a later call whose requests its rows do not cover,
naming the priming rather than the lookup: `row_for` would also have refused, but
its cause reads as a classification gap when the defect is where the manifest was
built.

Two shapes the code forced, against the plan:

  - A prebuilt context cannot be handed to a width>1 worker. Each worker calls
    `build_multi_entry_index` in its own closure and therefore carries its own
    generation, so the main thread's manifest would refuse on subject mismatch at
    every lookup — correctly. `run_discovery_rows` takes the complete roster beside
    its chunk and each worker builds its own context.
  - The context is built whenever selection is on at all, PredictOnly included. The
    falsifier runs PredictOnly and counts divergences; predicting from a different
    predicate than the applied one would make it measure agreement with a retired
    answer.

RETIRED, with zero remaining consumers: the predicate, `LIVE_READ_CARRIER_HOME_MODULES_V0`,
`import_closure_module_reaches_carrier_home`, and the const's drift gate. The stale
comment citing the removed `merge_g1_and_g2_carriers` as the seed's soundness reason
goes with them, as do two further stale citations of the deleted const (in
`v2.lens.enforcement.gate_test` prose and the design doc's status line, which still
described the G1 interim as the production axis).

`lying_substrate_inputs_only_stamp_census` was a real semantic consumer, so rather
than delete a merge gate it is re-expressed on the authority that now decides:
a SubstrateInputsOnly-stamped declaration classifying RuntimeRead — or refusing —
is a lying stamp. Same intent, one authority, no mirrored roster.

A COST SHAPE THIS CHANGE ACTIVATES. `live_read_selection_manifest_over` accumulated
with `list_snoc_item`, which is `list_append(acc, single)` and rebuilds the whole
left list per row — quadratic in the request count. The realized n stopped being
small the moment the producer gained a production consumer folding over the complete
corpus roster, so it is fixed here per DESIGN section 6's bare-minimum-cost rule.
Prepending via `fold_list_right` is O(1) per row and preserves the emitted order
(the idiom is copied from `list_append` itself); order is in any case not
load-bearing, since the seed decodes rows into a key-addressed map that refuses
duplicates.

EXECUTING CONTROLS live in `floor_skip_discovery_witness` — CI compiles
`v1-compiler-tests --no-run` and does not execute `cli_run.rs` unit tests, so a
control placed there would prove nothing. The incident-subject control asserts
`selected_entry_groups == 1` of 2, which is the grain the incident was measured in:
row totals can read 2/1/1 while both entry groups were selected and resolved. A
stop-line-population control separates that axis from axis (iv), and two refusal
controls pin `LiveReadEntryAbsent` and `LiveReadContextSubjectMismatch`, each with a
positive control beside it so the refusal is a fact about the missing subject rather
than about a broken context.

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

* Restore the stamp census to the floor's declared scan dirs, and state the subject change

The rewrite in the previous commit re-expressed `lying_substrate_inputs_only_stamp_census`
on the G2 manifest but discovered its roster from `dag/test/claim` alone, which is
narrower than the floor's own declared population — a silent scope narrowing inside
a change whose whole subject is selection breadth. It now discovers over
`gunbc.ci_layer_roots` `witness_discovery_scan_dirs`.

The grain change that remains is real and is now stated in the test rather than left
to be found: the retired form walked every `*_test.dag` with a parseable disposition
because an entry-grain import closure needs no declaration names, while this form is
declaration-grain and its subject is the enrolled roster. A `*_test.dag` outside
discovery is not covered — it is also not floor-selectable, so it sits outside the
population the stamp governs, but the two subjects are not identical.

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

* Correct a stale justification: the rehomed selection-control row cites per-PR coverage that #7446 removed

`falsifier_rehomed_bin_wet_rows` gave two grounds for keeping
`floor_skip_discovery_keystone_holds` off the per-PR lane: a measured 289s wall, and
that the row was "ALSO a per-PR duplicate: the ci job's own Affected-set selection
control step already runs on every PR".

The second ground is false. #7446 (Demote the affected-set selection control to the
falsifier cadence) moved that step out of `ci.yml` into `falsifier.yml`; `ci.yml`
builds, verifies and tars the binary but never invokes it, and `falsifier.yml` runs
on `schedule`/`workflow_dispatch`, where the step's own `GITHUB_EVENT_NAME =
pull_request` skip branch cannot fire. So the roster row justifies an exclusion by
pointing at coverage that stopped existing — a citation that rotted without anyone
touching it or the thing it named.

The rehome itself stands: the measured wall is its own sufficient ground, and this
commit changes no admission decision. What changes is that the row now says what is
true, and states the coverage grain plainly — the binary executes on the falsifier
cadence only, so this suite (including the affected-set incident controls this branch
adds) is caught within one cadence window rather than at merge.

Found while establishing where the new controls actually execute, which is a question
the stale sentence would have answered wrongly.

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

* Assert the unrelated entry skips BEFORE resolving, not merely without running

`selected_entry_groups == 1` proves the unrelated entry ran no witness. It does not
prove its cold entry resolve was elided: a row skipped as assumed-green AFTER its
entry resolved records no `witness_outcome` either, so both states produce the same
counter. Eliding that resolve is the entire cost win axis (iv) exists to buy — a
resolve-then-skip pays the 474-module closure and reports identically.

The control now also asserts the skipped row's provenance is
`skip-before-resolve-fast-path`, which is the fact that distinguishes them.

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

* Fix two defects in this branch's own work: a malformed control population, and a retracted claim reproduced

TWO SELF-INFLICTED, both found by reading `gunbc_ci_selection_control_step_note`
rather than by anything executing.

(1) `stop_line_population_does_not_widen_incident_subject` built its injected
name-status with `\u{0}` — an actual NUL byte. The separator is the literal
four-character sequence `\000`: `floor_diff_observe` splits the injected value on the
STRING "\\000", which is also what the suite's own `injected_name_status_modify`
emits. A NUL would have decoded as one unsplit path, so the control would have
exercised a population it does not name — passing or failing for a reason unrelated
to the stop line. That is the shape of the two controls #7997's body recorded as
testing nothing, reproduced here.

(2) The previous commit's correction to `falsifier_rehomed_bin_wet_rows` said the
suite is "caught within one cadence window rather than at merge". That exact phrasing
was ALREADY RETRACTED in the tree as too strong (`gunbc.ci_workflow`
`gunbc_ci_selection_control_step_note`, review correction 2026-07-30): the falsifier
is queue-not-cancel and the step sits after the prelude and release build, so a
regression landing just after a run passed the control waits for that run's
remainder, plus queued-run startup, plus the next run's prelude and build — a sum
that can exceed one cadence period. The honest statement is that it reds when the
NEXT COMPLETED control runs.

So a commit whose whole subject was correcting a stale citation introduced a claim
the tree had explicitly rejected, into a load-bearing roster, one commit later. It now
uses the words the retraction settled on.

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

* Repair the producer's fact universe, propagate the modelled refusal, and retract the P3 claim

The previous head consumed a manifest whose fact universe was the classification
lens's OWN import closure, which makes this branch a widening rather than the repair
it was named for. Three coupled fixes, none of which lands alone.

FACT UNIVERSE. `fn_arrow_decl_facts_live()` reflects the eval context's modules
(`for_each_live_registry_item` iterates `ctx.modules`), and the producer resolved
`LIVE_READ_CLASSIFICATION_ENTRY` alone. So every request root outside that closure —
nearly every enrolled witness — could not bind: `bind_g2_root` finds the module fact
(whole-pool scan) but no fn-arrow declaration, and answers `G2RootUnbound`. The
manifest carried a modelled refusal for essentially every declaration. The tree
already said this in `production_qualification_origin_probe`: "fn_arrow_decl_facts_live()
scope is entry-closure today", and `whole_tree_subject_mint_site_law` flagged the
coverage premise as unproven. It is not merely unproven; it is false.

The context is now resolved from the INDEX'S OWN sources, so universe identity is
structural rather than asserted — the facts cannot describe a different tree than the
one the consuming index holds, because they are derived from exactly its files.
Deliberately NOT `whole_tree_resolved_ctx`: that helper applies the witness and
strict-resolve exclusion sets first, so its universe can omit the very test
declarations this manifest must classify, and its name would have read like a receipt
while excluding the subject. Sources are sorted so the build is not
host-iteration-order dependent.

Coverage is checked at identity grain, not by count. `fn_arrow_decl_substrate_is_whole_tree()`
projects only `ctx.modules.len()` into a census comparison, and equal counts do not
prove equal identities: a context with the right number of modules and a substituted
one answers that check and still cannot classify the declaration that went missing. So
a resolved-module shortfall refuses by number AND the primary wall is per-root — any
request root that fails to bind produces a modelled refusal, which the consumer now
propagates.

MODELLED REFUSAL PROPAGATES. `touched_by` answers `true` for a `Refused` row, which is
right for a path-intersection question and wrong for a consumer to act on: routed
through the boolean, an undecidable classification is indistinguishable from a decided
"this diff touches a runtime read", so an incomplete producer presents as
`SelectionSuperset` — everything ran, slow but apparently valid — instead of "the
manifest cannot decide". `runtime_dependency_touched_for_entry` now matches the arm
explicitly and returns a typed, located `Err`. The entry still does not skip; it does
not skip because the line STOPPED, not because a widened answer looked like a decision.

TWO CONTROLS, for the two arms the existing ones did not reach.
`live_read_classifies_a_declaration_outside_the_lens_closure` pins the defect
directly: a real enrolled witness entry that `v2.lens.live_read_classification` does
not import must CLASSIFY, and mutating the producer back to a lens-entry context makes
it red. `live_read_present_but_refused_row_propagates_refusal` covers the arm the
missing-entry control cannot: a row that is PRESENT and refused must stop the line,
where the existing control only exercises a roster-lookup miss.

P3 RETRACTED to candidate in the design doc. "P3 landed" was written against the
consumption half alone and was wrong for exactly the reason above; producer universe
completeness and production consumption are ONE atomic change.

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

* Bound the fact universe to the request roots' closures, not the whole index

The previous commit resolved the index's ENTIRE source population to guarantee every
request root could bind. Sound, but it pays a whole-corpus resolve to answer any
question — including the two-entry rosters the selection-control suite runs. A
producer that costs a corpus resolve to decide a two-entry question is the cost shape
this lane exists to remove, reintroduced one layer up, and it would have been measured
as a regression in step 6 rather than caught by reading.

The universe is now the union of the request roots' own closures from this index's
pool, plus the classification lens's closure. Containment is by construction rather
than by hope: classifying (E, f) needs E's module and the modules
`call_reachable_decls_from_root` walks from it, and both are exactly E's import
closure. For the full floor roster this approaches the corpus anyway — correctly, that
IS the population being decided — while a scoped roster now pays only for what it
asks about.

Deduplicated and ordered through a path-keyed BTreeMap, so overlapping closures cost
one entry each and the resolve is not a function of host hash-iteration order. A root
whose closure cannot be loaded refuses by name rather than resolving a population
that silently lacks it.

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

* Instrument the manifest build: requests, universe size, wall, CPU, RSS

Step 6 of the acceptance sequence needs a measurement, not an argument. This producer
resolves the union of the request roots' closures once per index, and that has to be
paid back by the entry resolves selection elides; two cost readings on this lane have
already been wrong, so the numbers are printed where any run that builds a manifest
carries them and a reader can divide them by the resolves saved.

CPU is reported beside wall via `getrusage(RUSAGE_SELF)` because the two answer
different questions on a shared runner: wall includes time the process spent
descheduled behind co-resident jobs, so a wall figure alone cannot separate "this
producer is expensive" from "this host was busy", and a cost claim that cannot tell
those apart is not a measurement.

RSS is labelled as whole-process and therefore an upper bound rather than this
producer's attribution — other work shares the process, and quoting it as the
producer's own would be the same overclaiming this lane keeps producing.

Emitted unconditionally rather than behind a verbose flag: a measurement that only
appears when someone thinks to ask for it is not available at the moment a regression
lands.

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

* Repair the producer/decoder representation boundary; measure the union producer

The decoder required Value::List while every representation the producer can
return is the modeled FreeMonoid chain, so a well-formed manifest reported as a
foreign shape. Decoding now accepts the native list and the Empty/Cons chain
through the interpreter's own free_monoid_to_vec, inside the active context
(the Empty/Cons/head/tail symbols resolve through active_ctx, so a decode
outside it reports every variant as foreign). A non-collection value refuses by
name rather than decoding to zero rows.

The fold was never the functional defect. Both folds return the same modeled
collection; execution disproved the earlier attribution, and the note recording
that mistake is corrected rather than deleted.

Instrumentation added, and it measures the union producer out of the design:
one request over a 545-module union costs 179125 ms wall / 178751 ms CPU at
9.4 GiB whole-process RSS. That specimen no longer reproduces the prior SIGKILL
-- which is not the same as the OOM class being closed, and the full roster,
wider unions and width-N workers remain unproven.

fn_arrow_decl_facts_from_sources lands as the replacement's core: the same
FnArrowDecl population derived from a parse rather than from the eval context's
resolved modules, reusing build_item_info and fn_arrow_decl_record verbatim so
it is not a second definition of the fact. Not yet consumed.

The foreign-shape control was vacuous -- it rebuilt a well-formed roster and
asserted it decoded, which the control above it already proves. It now calls
the decoder with a non-collection value and asserts the refusal by cause.

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

* Merge main; land the index-scoped G2 fact substrate with a bidirectional identity join

Merges current main (7bcd31d's decl-facts work included) BEFORE binding a
declaration key, so the bundle binds to the landed ResolvedDeclarationLocator
surface rather than to a key invented against a stale one.

That merge changed the plan for the better. decl_facts_corpus_walk already IS a
parse-level declaration walk over pool roots with no eval context, so the
hand-rolled walk written before the merge was a second definition of a
declaration fact. It is deleted. What remains is one projection,
push_decl_facts_from_parsed, shared by the corpus walk and the new
index-scoped walk; a caller chooses only which modules to visit.

Two divergences the live-read consumer requires, each stated where it lives:
the corpus walk skips is_test_dag, and enrolled witness declarations are
exactly those files, so an unfiltered population is needed -- an empty one
would read as "no runtime reads", a silent narrow. And parse_dag_file drops
unparseable files to None, which is fine for a census and not for a consumer
deciding skips: a dropped module's declarations report as unbound, conflating
"no such declaration" with "its module did not parse". parse_dag_source is
extracted as the content-taking core (parse_dag_file becomes its filesystem
instantiation) and names its refusal.

G2FactBundle carries the exact index pool with TWO levels of failure, because
collapsing them recreates the union's defect one layer down: the bundle refuses
only when the index-wide population itself cannot be observed, while a module
that will not parse is a local fact under its own identity. bad A -> A refuses,
never bad A -> bundle refuses -> B disappears.

Completeness is an identity join in both directions, never a count: a
population with one identity missing and one extra has the same size and is a
different tree. Foreign facts refuse; unvisited index modules refuse. Visits
are reported by the walk rather than inferred from absence, because a module
may legitimately parse and declare nothing -- an earlier revision of this join
computed that check and then discarded it under a comment claiming it caught a
defect it could not catch.

Facts are cached RAW. A marshalled Value carries symbols interned in the
evaluating context, so caching marshalled rows on the index would hand one
context's symbols to another's evaluation.

Not yet wired: the union resolver, complete-roster memo guard and stale fold
row are still present and still in charge; this commit adds the substrate they
will be replaced by.

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

* Index-owned fact cell, declaration-keyed row cache, and zero/one/many request binding

Replaces the one-manifest memo's SHAPE, ahead of removing its last consumer.

g2_facts is index-owned and request-set INSENSITIVE: the population is the
index's, so any roster reads the same facts and no roster can prime it
partially. live_read_rows fills incrementally, keyed by
ResolvedDeclarationLocator rather than a rendered entry::name string, so two
same-named declarations in different modules cannot share a verdict and a miss
classifies one declaration instead of poisoning the index. The suite already
disproved "complete on first access" as a property an index can have -- the
same immutable index is legitimately reused across scenarios with different
rosters -- so this is not a convention made easier, it is the convention
deleted.

Binding is separated from classification. A request names a SPELLING, and a
spelling is not an identity: over a whole-tree population it may match nothing
or more than one. Folding that into classification would force a verdict out of
an ambiguous subject and let "many" become whichever the walk reached first --
a heuristic standing where an identity was owed. All three cardinalities are
answered, only exactly-one proceeds, and zero and many refuse with different
causes because their remedies differ. An entry whose own module failed to parse
refuses under its own key, locally, leaving unrelated entries classifiable.

ResolvedDeclarationLocator derives Hash beside its existing Eq so the hash and
the equality cannot disagree about which fields decide identity.

Not yet wired: nothing consumes these yet. The union resolver and the
complete-roster guard are still in charge on the decision path.

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

* Cut over to the index-bound producer; delete the union resolver and the complete-roster convention

ONE transition, per the ruling: the bridge and its consumer land together, and the
machinery they replace is deleted in the same change rather than left beside them.

THE PRODUCER. fn_arrow_decl_facts_index_live is a transport, not a second semantic
authority: it reads the exact MultiEntryIndex bound to the classification operation,
delegates to the shared push_decl_facts_from_parsed projection, reads no files,
resolves nothing, walks no closure and decides no classification. It marshals into
the CURRENT evaluator context because a marshalled value carries context-interned
symbols; what is cached is the parse. With no index bound it REFUSES -- it does not
fall back to reflecting the eval context, because the two producers answer about
different subjects and substituting one for the other is never a safe default, and
an empty population would read as "no runtime reads" and license a skip.

It arrives the supported way: .dag declaration, authored roster row, regenerated
dispatch (a 3-line diff, generated from the roster rather than hand-edited),
interpreter arm. dependency_resolution_facts_live remains the sole edge authority;
only the missing fn-arrow projection is supplied.

THE LAW CHANGED BECAUSE ITS PREMISE DID. whole_tree_subject_mint_site_law admitted
it had not proven the producer covered the consuming index, and it could not: that
producer reflects the eval context, so coverage meant evaluating a union of every
request root's closure. Measured at 179125 ms wall / 178751 ms CPU for one request,
and then it failed outright -- bare names that bind uniquely inside each entry's own
import scope stop binding across a union, so Holds, Finding, Exact, Refuse, Blocking
and FailClosed went unresolved and ONE shared refusal denied classification to all
133 enrolled entries. That is a property of composition, not a repairable module.
Coverage is now established by a bidirectional identity join before any subject is
minted, and completeness is per bound request: an unrelated module that fails to
extract refuses only the requests whose closure contains it. Reading the subject as
"the whole index parsed" would recreate the union's failure in another form. The
variant is still spelled WholeTreeFactSubject; the honest name and the deferred
rename are stated in the law rather than left to make the prose quietly false.

DELETED, not deprecated: the union resolve and its RequestUniverseUnresolved,
RequestUniverseIncomplete and RequestRootClosureUnloadable causes; the one-manifest
memo, its complete-roster requirement and MemoRequestPopulationIncomplete; the old
live_read_selection_manifest_live mint site; and the fold essay whose premise the
complete roster was. Only the classification lens's OWN closure is resolved now --
one entry, so a refusal there is about the lens and cannot be caused by an unrelated
module.

The context becomes a roster-scoped VIEW over the index-owned cache, and re-checks
the subject at lookup rather than trusting construction.

The memo test asserted Rc::ptr_eq on one manifest -- precisely the property being
deleted, since it could only hold if a single object served every request. It is
replaced by the incrementality and cache-hit controls: disjoint rosters classify in
any order, a repeat ask does not grow cache occupancy, and a distinct index inherits
nothing. Occupancy is exposed so a hit is proven by execution rather than inferred
from two calls agreeing.

STILL OWED, stated rather than implied: the lying_substrate_inputs_only_stamp_census
denominator is still narrowed, and the remaining ladder gates (bad-A/good-B
isolation, exact-identity refusal, zero/one/many binding, no-union execution proof,
the 1-selected/1-skipped incident, both G1 mutations, falsifier_red_control_holds)
have not been executed.

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

* Fix build: reattach the producer's doc comment to the function, not to a macro

CI builds with RUSTFLAGS=-D warnings; my local checks did not, so an
unused-doc-comment error reached the branch that no local run could have caught.
The thread_local! block for the bound fact substrate was inserted between
build_live_read_selection_manifest's doc comment and the function, leaving the
comment attached to a macro invocation.

The comment was also stale in the same motion, and rewriting it was the fix
rather than moving it: it described a whole-tree manifest built once for the
entire request set and a producer whose job was to be handed the full roster.
Both premises were deleted by the cutover. It now says what the batching is
actually for -- the caller passes exactly its cache misses, one lens resolve and
one marshal serve them, and the index keeps the rows -- while retaining the arm
that matters, that no failure returns a partial batch, because widening the
SKIPPABLE set is the under-selection direction and reads as a faster green.

Verified against CI's own flag and bin set rather than my previous weaker local
command. Two -D warnings errors remain in p1_cohort_probe.rs; they are
pre-existing, untouched by this branch, and that bin is not in ci.yml.

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

* Fix two source-identity defects the first execution found

Nothing had ever executed this path; running it found two real bugs, both of
the same family -- an identity compared in the wrong spelling.

PATH KEYS. The bundle keyed declarations by MultiEntryIndex.source_files' map
key, which is the dotted MODULE PATH (std.machine_shape), not a file path --
`source_files.contains_key(module_path)` elsewhere is the tell I misread. The
declaration population must be identified by FILE, because that is what a diff
touches and what a discovery row names, so identity now comes from sf.path.
Both sides additionally go through the index's own `module_index_path_key`,
because a raw string compare against the pool is the source-identity fork
DESIGN names on this very index: a caller arriving with an absolute path while
the pool holds a relative one gets "no such declaration" about a declaration
that is plainly present -- a refusal whose cause is the spelling, not the
subject.

MODULE PATH ON THE FACT. DeclFactRaw carried only the qualified name, which
passes through `decl_logical_qualified_name` and DROPS the `v2.` layer prefix
for decl_index consumers. fn-arrow reflection keeps the full path, so
reconstructing a module by un-stripping that name produced facts under a name
no root can bind. The fact now carries its authored module_path unstripped.

MEASURED, first execution of the cutover:
  requests=1 rows=1 fact_modules=3404 fact_decls=61705 parse_refusals=0
  lens_closure_sources=579 lens_modules=579 wall_ms=101506 process_rss_mib=12386
The pool is every .dag file including all 1290 tests, and nothing failed to
parse, so the population is complete and no union is formed. But removing the
union fixed the SEMANTICS, not the price: 101s for one request, because the
classification lens's own closure is 579 modules -- nearly the size of the
union deleted. That is a finding, not a fix.

STILL RED, stated exactly: the subject declaration still reaches the lens as
G2RootUnbound. The population contains its file and its module name is right,
so the remaining defect is in the projection rather than the walk. It has not
been located, and each observation costs a ten-minute run.

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

* Correct the record: c8161f32b4e's module_path claim did not land

c8161f32b4e's message states "The fact now carries its authored module_path
unstripped." That claim is FALSE. The field was never added, the projection
never changed, and `git log -S"pub module_path: String,"` over
coproduct_reflection.rs returns nothing for any commit. The edit reported
success and the build passed, and I recorded a landed fix from those two
signals without re-reading the file -- but the ORIGINAL code compiles fine, so
a green build was never evidence the edit applied. That is
specification-without-execution with the author as the consumer: the artifact
looked finished because nothing ran that could tell the difference.

It also means run 4's failure was never evidence against the v2.-stripping
hypothesis. The fix was absent, so the run re-tested the unmodified code and
the hypothesis stayed untested for two cycles.

Applying it for real, and proving it by execution rather than by inspection:

- DeclFactRaw carries `module_path`, the module's own AUTHORED path, populated
  at the single construction site in push_decl_facts_from_parsed.
- fn_arrow_decl_rows_from_facts reads that field instead of reconstructing the
  module by stripping the declaration's name off `qualified_name`. That
  un-strip could not be correct: `qualified_name` passes through
  decl_logical_qualified_name, which drops the `v2.` layer for decl_index
  consumers, so the reconstruction filed every v2-authored fn-arrow row under
  `test.workflow.x` while the root asks for `v2.test.workflow.x`. Every such
  root reported G2RootUnbound -- not missing, misfiled.

Two probes, both green, both fast (36s against the floor's ten minutes):

- the_fact_bundle_contains_an_enrolled_witness_declaration proves the bundle
  holds the exact declaration CI named and binds it to exactly one identity.
  This is what rules the host side OUT: the population and the binding were
  never the defect.
- v2_layer_facts_keep_the_authored_module_path_and_drop_it_from_the_qualified_name
  asserts BOTH directions together, because either alone is satisfied by the
  defect -- with the un-strip in place `qualified_name` lacked `v2.` exactly as
  it does now, so only the presence of `v2.` on module_path beside its absence
  on qualified_name discriminates. It also refuses to pass vacuously: it fails
  if no v2.-authored fact exists at all.

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

* Decode the produced manifest inside the active context

CarrierListAbsent was never a carrier defect. `free_monoid_to_vec` resolves
`Empty`/`Cons`/`head`/`tail` against `active_ctx()` and answers None when there
is no active context -- so run outside that scope it cannot tell a cons chain
from a foreign shape, and reports the foreign one. The decode ran after the
producer call returned, i.e. outside the wrapper.

The outer row collection concealed this for three rounds because it arrives as
a native `Value::List`, which returns before the symbols are ever consulted.
The nested carrier lists are real cons chains and are not so forgiving. So the
active context is a property of DECODING a produced value, not of the one call
that happened to need it first, and it is now scoped to the whole walk
(decode_live_read_manifest_rows).

Two diagnostic defects made those three rounds cost what they did, and both are
fixed here rather than left as the price of the next one:

- The refusal named a cause and no SUBJECT. It saw one projection and could not
  say which declaration produced it, so analysis could not begin without
  re-deriving the culprit by hand -- a line that stops without locating is half
  of what §5 asks for. The key is in scope at the row loop, so attribution
  belongs there. It named
  `src/v2/lens/module_graph.dag::module_declaration_facts_live` immediately.
- The refusal reported what it EXPECTED and never what it RECEIVED, and fused
  "field absent" with "field present, wrong shape" into one arm -- two states
  whose repairs share nothing. Split, with the received shape named: the answer
  came back `Variant::Cons["head","tail"]`, which is a well-formed cons chain
  and refuted the representation hypothesis I had just spent a round on. An
  expectation is not an observation.

Green by execution, and the receipts are the evidence rather than a counter:
three asks over two indexes print exactly TWO [live-read-rows] lines. The
repeat roster prints none, so the per-declaration cache hit is observed, not
asserted; the distinct index prints its own, so it classified for itself.

Cost, stated plainly and unresolved: 275s per cold classification in an
unoptimized build (101s release), lens closure 579 modules. The floor is over
budget on CI at 3,300,000 ms for this reason, and this commit does not address
it.

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

* Replace the quadratic manifest accumulator, and add the missing O(1) cons

STEP 1 of the operator's ordered plan, on the exact head rather than in a PR
description. The body claimed the manifest accumulator had been changed to a
right fold with constant-time prepend. It had not: at b1976f7
`live_read_selection_manifest_over` still left-folded with `list_snoc_item`,
whose body is `list_append(left: xs, right: singleton)` and therefore traverses
the accumulated prefix on every row.

Why the earlier evidence could not see it: the probe that certified this path
used ONE request, where the quadratic term does not exist. The ordinary floor
builds from a roster of thousands, and the completion receipt is emitted only
after the fold and decode -- so the failure presents as zero receipts and 6
executed discovery rows against main's 4728 in the same window, which reads
like a route never entered rather than an accumulator that cannot finish.

THE ROOT CAUSE IS A GAP IN THE VOCABULARY, not fifteen careless call sites.
`list_snoc_item` existed with NO O(1) counterpart. Writing the cheap form by
hand means a `Cons` literal, which typechecks as Coproduct(FreeMonoid) and does
NOT unify with a branch typed Container(List, T) through the `List` alias --
proven here by execution, since that is exactly the type error my first attempt
produced. So the expensive spelling compiled and the cheap one did not, and the
census found sixteen snoc-in-fold sites in ONE lens module. `std.algebra` now
carries `list_cons_item` beside `list_snoc_item`, named as an item-level
operation so the alias asymmetry cannot resurface per call site.

Converted in this pass (prepend + one reverse, order-preserving): the manifest
fold, `decls_in_modules` (genuinely quadratic in output, on the hot path),
`module_facts_for_path`, `decls_with_qualified_name`, `g1_carriers_from_homes`,
`merge_carriers` (seeded on a reversed left so the left side keeps its order),
`param_names_of`, `positional_child_targets`, `arg_symbols_from_nodes`.

NOT converted, and deliberately so: the `fold_node`-composed pair
(`atom_collect_step` / `atom_identities_in_node`) and the remaining dedup
accumulators. Prepend-plus-reverse is not obviously order-preserving through a
node-fold composition, they are not hot-path quadratic (atoms per node are
few), and rewriting them on a guess is how a load-bearing lens acquires a
silent semantic change.

I therefore did NOT enroll this file in the accumulator-copy roster gate. I
wrote that enrolment, ran it, and it FAILED at ceiling 0 -- because the sites
above remain. Landing an enrolment I cannot pass would be either a red gate or
a false claim, so it is withdrawn until the residue is closed. The lens already
decides this exact shape (`red_list_snoc_left_is_suspect`), so the control is
available the moment the file is clean; it is owed, not invented.

Green by execution: all 20 witnesses in live_read_classification_test pass, so
the rewrites preserve classification semantics.

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

* Locate the cost (steps 2-3), isolate local refusals, and drop the eager roster

STEP 2 -- start and phase receipts. The terminal receipt could not distinguish
a route never entered, a route entered and stalled, and a route that returned;
it reported only the third. That is why the floor's stall showed NO receipt and
I read it as "never ran". Every line now carries one invocation identity
(subject + pid + request count, so concurrent workers are separable and a
per-process counter cannot collide across them), with an unconditional
[live-read-start] before any expensive work and a phase end for each of: fact
bundle build, lens closure source load, lens resolve/typecheck, eval context,
request marshalling, classification production, decode.

IT LOCATED THE COST ON ITS FIRST RUN, and not where step 5 expected:

  lens_closure_resolve_typecheck  ms=40468   modules=579
  classification_production       ms=207790  <- ONE request
  decode                          ms=0

The 579-module closure is 40s. The modeled fold is 208s for a SINGLE request.
So the dominant term is fact production plus per-request classification -- the
batch-grain indexing work -- not the fixed closure. Recorded here because two
cost readings on this lane were already wrong from argument rather than
measurement.

STEP 3 -- stop classifying rows whose verdict G2 cannot change. Selection is a
disjunction of independent do-not-skip axes and every other axis is a fact
lookup. A row already forced to run cannot have its verdict changed by a
live-read classification, so classifying it is cost paid before the first
discovery row executes. Three call sites now consult G2 LAST (semantics
identical -- `||` is commutative, ordering only decides what is evaluated), and
the context is built over candidates only, counted in a [live-read-candidates]
receipt rather than narrowed silently. `entry_blocked_by_cheap_axes` is the one
definition of "already decided"; a second copy could drift into classifying
needlessly or withholding a classification a consumer then asks for.

LOCAL FAILURE ISOLATION. `live_read_rows_for` returned Err for the whole batch
on one unbindable request, beneath a comment asserting the refusal was local --
the two disagreed for as long as nothing executed the case. An unbindable
request is now a Refused row: the batch keeps classifying, and the floor still
refuses to skip, because the consumer already reads Refused as do-not-skip. So
isolation costs no fail-closed strength. Not cached -- the durable cache is
keyed by resolved declaration identity and an unbindable request has none.

The control asserts BOTH halves, since either alone is satisfied by a defect:
answering for A would be fail-open, and refusing for A while dropping B is the
abort being repaired. Green by execution.

Also fixes a second site of the module_path defect: bind_live_read_request
still derived the module by un-stripping the qualified name. Same fork as the
fn-arrow projection, one call away.

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

* Apply the candidate filter where the run actually builds the context

The phase receipts from the previous commit did their job on their first CI
run, and what they showed is that my step-3 filter was on a path this run does
not take:

  [live-read-start] requests=8538
  phase=fact_bundle_build              ms=0     modules=3450 decls=62767
  phase=lens_closure_source_load       ms=25    sources=587
  phase=lens_closure_resolve_typecheck ms=7367  modules=587
  phase=eval_context_construction      ms=59
  phase=request_marshalling            ms=5     requests=8538
  <no classification_production end>

Three things are now located rather than inferred. The stall is in
classification_production. The fixed closure is NOT it -- source load plus
resolve is 7.4 seconds here, against a 55-minute budget. And 8538 requests
entered that fold, so the candidate filter never ran: no
[live-read-candidates] line appears anywhere in the log.

The site that builds it is `run_discovery_corpus_with_options_inner`, which
ran before `run_discovery_rows` and built from the COMPLETE roster under a
comment declaring that to be the design: "First consumer of the manifest in the
run, and it is handed the COMPLETE roster -- the index-scoped memo it fills is
what every later decision reads."

That convention outlived its mechanism. It was true of an immutable
whole-roster manifest built once; it has been false since the cache became
per-declaration and incremental, which disjoint-roster use forced. Nothing
downstream needs priming -- a later caller asking about a row this one skipped
classifies it then. So the comment was not merely stale prose: it was the live
justification for putting every enrolled row into one classification.

`live_read_candidate_rows` is now the single definition, used at all three
build sites, and the inline copy in `run_discovery_rows` is deleted rather than
left to drift from it. The struct doc that chartered the convention says what
replaced it and why, and the `LiveReadEntryAbsent` refusal no longer instructs
the reader to build from the complete roster -- callers ask about their own
demand, so an absent entry means it was not among THIS view's rows, not that a
roster was incomplete.

This does not fix the quadratic in the fold. It stops feeding it rows whose
verdict it cannot change.

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

* The short-circuit has to hold at every consumer, not only the predicates

The filter worked, and the measurement is unambiguous:

  [live-read-candidates] roster_rows=8545 candidate_rows=91 excluded=8454
  phase=classification_production ms=287414   <- COMPLETED, 91 requests
  phase=decode rows=91

A 94x reduction in requests, and the classification RETURNS instead of never
finishing. Two further facts fall out of the same receipt: the fixed closure is
6639 ms of a 55-minute budget, so step 5 was never the target; and 287 seconds
for 91 requests is still ~3.2 s/request, so the quadratic in the fold is
untouched and remains the next thing to fix.

The run then failed differently, and correctly:

  AFFECTED-SET REFUSAL cause=LiveReadEntryAbsent
    entry=dag/test/claim/accelerator_demo_execution_witness_test.dag

That is the exact risk excluding rows creates, and it fired at a consumer I did
not convert: the first site built the view from candidates and then handed the
COMPLETE roster to discovery_rows_runtime_dependency_touched_count beside it,
which asks the view about each row. So it asked about rows the view was
deliberately not built for. Fixed by passing the candidates there too.

The count's meaning narrows, and that is worth stating rather than burying: it
is now "among entries whose verdict a live-read answer can still change, how
many have a touched runtime dependency". Excluded entries are not uncounted --
they are already forced to run by the axis that excluded them, and counting
them here would count one entry under two axes.

Every remaining production consumer is guarded: the two entry predicates and
the fast-skip predicate all consult G2 last, after the cheap axes have returned
early. The one unguarded `row_for` loop is a test that builds its own context
from the rows it asks about.

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

* Recut the candidate filter as one typed selection plan

Findings 2, 3 and 4 from the review. Finding 1 (precompute iterating the full
roster) was already fixed at 36be85d9cbc, before the review landed.

THE ROOT DEFECT WAS THE REPRESENTATION, exactly as called. A sparse context
gives one representation to two states with opposite remedies: "absent because
another axis already decided" is correct and must be silent, while "absent
because the classification is missing" must refuse. With only a context every
consumer sees LiveReadEntryAbsent for both -- which is why filtering the
context alone moved the failure from the stall to a refusal instead of fixing
it, and why chasing it consumer-by-consumer would never have terminated.

`LiveReadSelectionPlan` carries `AlreadySelected { cause } | NeedsClassification`
per entry beside the classifications for the candidates only. `entry_must_run`
answers an AlreadySelected entry from its own cause WITHOUT consulting G2, so
an excluded row is unqueryable rather than a lookup that happens to refuse. An
entry the plan never saw is a real gap and refuses with a distinct cause
(LiveReadEntryNotPlanned) -- the plan must not become a way to make every
absence silent, which would be the same conflation inverted.

FINDING 2 -- post-resolve accounting. resolve_discovery_entry_for_corpus_row
short-circuited only on the declared/effect half, so a row excluded for any
OTHER cheap reason (ReadsLiveTree, direct touch, edited test fn, import
closure, stop line, data-item overlap) still reached G2 after resolve. It now
goes through the plan. The runtime axis is not faked false for excluded rows;
AlreadySelected answers true, which is the honest do-not-skip verdict rather
than a claim about an axis nothing evaluated.

FINDING 3 -- the categorization site. eprintln_affected_set_categorization
still built directly from the full roster and could independently recreate the
8538-request classification the execution sites had just stopped making, moving
the stall to another consumer. It consumes the plan now.
measure_selected_entry_closure_overlap is classified deliberately rather than
missed: it is an OFFLINE whole-population measurement whose subject IS the full
roster, so filtering would change what it measures, and it says so in place.

FINDING 4 -- the duplicated chain. entry_qualifies_for_skip_without_resolve and
entry_eligible_for_discovery_skip_before_resolve each repeated the disjunction
inline; both now call entry_blocked_by_cheap_axes. Two independently writable
copies of one rule only have to disagree once to produce either a refusal or
the restored cost. The authority returns a typed CheapAxisCause rather than a
bool, so the plan can preserve WHY G2 was omitted instead of consumers
re-deriving it.

Controls, green by execution (465s, serial):
- every_cheap_axis_excludes_without_the_classification_being_consulted: one arm
  per exclusion reason, asserting BOTH the expected cause AND entry_must_run ==
  true. Either alone is satisfied by a defect -- an arm checking only the cause
  would pass while the entry silently became skippable.
- an_unplanned_entry_refuses_rather_than_reading_as_already_selected.
- the local-isolation control from the previous commit still passes.

The fixture constructor for plans is cfg(test): minting demands without
deriving them from the cheap axes is precisely the forgery the plan prevents,
and it must not exist on a production route.

Not addressed here, and still the dominant term: the per-callee 579 x ~62,000
scan inside every candidate.

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

* Prove zero candidates cost zero classification; group_by is blocked upstream

ZERO-CANDIDATE CONTROL (merge bar item). Candidate filtering reduces the number
of REQUESTS through the fold; it does not by itself remove the ~7s fixed
startup, because a context built unconditionally resolves the ~587-module lens
whether it has one candidate or none. "The roster filter appears to work" and
"the run stopped paying for the classifier" are different claims and only the
second is asserted here.

Asserted on a COUNTER, not on stderr: a test that greps its own output proves a
line was printed, not that work was skipped.
`LIVE_READ_CLASSIFICATION_INVOCATIONS` counts the expensive unit -- one lens
resolve plus one modeled fold per `build_live_read_selection_manifest`. It also
supplies the width-1/width-2 invocation count the merge bar asks for, which is
otherwise unfalsifiable from outside the process.

The control also asserts the plan stays USABLE at zero classifications: an
excluded entry still answers `entry_must_run == true` from its own cause. Zero
classification must not mean zero answers, which is the fail-open direction the
filter could have introduced.

GROUP_BY: BUILT, MEASURED AGAINST THE SUBSTRATE, REVERTED. The native
multiplicity-preserving arm compiles and the roster/regeneration path accepts
it, but it is NOT REACHABLE FROM AUTHORED .dag. The witness refuses at
typecheck in both spellings:

  method 'group_by' not found on receiver type 'Container(List,...)'
  function 'group_by' not found in scope

and -- the decisive part -- `index_by` refuses IDENTICALLY, though it is an
established interpreter arm. So this is not a missing registration for my new
primitive; the whole family is unreachable from authored source and exists only
for emitted-Rust/internal paths, which is why no .dag module calls it.

Making it usable needs primitive REGISTRATION, and DESIGN already names why
that is not a one-line addition: the primitive subject universe is forked
across infer_method's builtin registry, std.primitives PrimitiveContract rows,
std.algebra templates, the native interpreter arms, and per-target emit
handlers (the determinism thread's closed-denominator gap). That is substrate
work with its own sequencing, not a lens patch.

Landing the arm anyway would be machinery no consumer can reach -- the inert
mechanism DESIGN calls coverage by illusion -- so it is reverted rather than
left in the tree with a note. The dispatch table is regenerated back to
`group_by`-free through the supported path, not hand-edited.

The per-callee 579 x ~62,000 scan is therefore still unfixed, and the route to
fixing it now has a named upstream blocker rather than an unknown.

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

* Hoist the closure restriction out of the per-callee scan

decls_matching_callee called decls_in_modules itself, filtering ~62000
declarations against 587 closure modules once per callee of every reached
declaration. Nothing in that filter depends on the callee. The caller now
restricts once and threads the restricted list.

Same set by construction: filter(filter(xs, in_closure), named) ==
filter(filter(xs, named), in_closure), so no classification changes.
20 lens witnesses green in 8259ms.

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

* Land group_by as a real primitive, and normalize index_by beside it

The lens needed a name-keyed declaration index and the substrate had no
operation that could build one. index_by is a native HAMT but last-wins,
so it drops homonyms; the modeled Map is a closure chain, measured 33x
slower than the linear scan it would replace. Neither was reachable from
authored .dag in either spelling.

One atomic cut, on the shape empty_map already established: a typed public
group_by<T,K>(List<T>, fn(T)->K) -> Map<K,List<T>> backed by a private
group_by_primitive_delegate intercepted at the collection-native grounding
seam, plus the same normalization for index_by. Multiplicity-preserving:
every item reaches its key's bucket, buckets keep input order.

Surfaces, all in this commit: std.primitives contract (work n x (key_fn_work
+ expected HAMT insertion), certainty Expected), std.primitive_identity
definition/alias/traversal/cost rows, the interpreter roster row and its
regenerated dispatch table, the native handler, rc_group_by in runtime_rust.dag
and v1_rt.rs, and the rust emit registry row.

The grounding seam now threads env, because both grouping delegates apply a
caller closure; eval_builtin has no env and the builtin-backed arms keep their
own path. A missing native binding refuses rather than falling back - the
delegate's body is its own name, so a fallback would be an unbounded self-call.

Witnesses green by execution: 6 in test.claim.grouping_primitive_witness
(empty, unique key, three equal keys in one bucket, input order, absent key,
and index_by keeping the last item). The two multiplicity claims are the
discriminating pair a last-wins mutation reds.

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

* Regenerate the stage0 seed for the group_by emit surface

The rust emit registry row and the runtime function both live in .dag
authorities that generate seed files; hand-editing v1_rt.rs left the
generated emitter and the registry stale, and regen refused with a
three-file fixed-point mismatch.

Two regen rounds were needed and the reason is the bootstrap order: the
first round emits v1_rt.rs with the OLD compiled-in v1_compiler_runtime_rust
while writing the new one, so rc_group_by is dropped from v1_rt.rs and added
to its emitter in the same pass. Rebuilding and regenerating converges.

Also adds a stopped-line audit hook to the live-read producer: with the
forensics env var set it dumps per-.dag-fn self time after classification
production. It reports and never decides. It is there because the grouped
index did NOT move the measured number - classification production is 216427 ms
for 2 requests against 207790 ms before - so the dominant term is something
other than the per-callee scan and needs a name rather than another guess.

Population measured at the seed and now printed with the fact-bundle phase:
61977 declarations, 59218 distinct names, largest bucket 1018.

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

* Stop materializing strings and lists to hash them

Forensics on the live-read producer named the dominant term: 73315126
free_monoid_to_vec calls materializing 3251118183 items, every one of them
from the single site inside value_hash. Hashing a Value::Str allocated one
Value::Int per character and hashing a Value::List cloned every element,
walked the clone, and threw it away - and every HAMT key in the interpreter
goes through that path.

Both arms now hash in place. The result is identical by construction: same
0xF0 tag, same length, same per-element value_hash in the same order, which
is exactly what the walk over the materialized vector did. A Cons chain still
materializes, because walking it needs the monoid symbols resolved and there
is no borrowed view of it.

Controls, green by execution: the replaced implementation is kept in the test
as the oracle and asserted equal on empty/one-char/path/non-BMP strings and on
empty and mixed lists, paired with a discriminating control that distinct
strings and permutations still hash apart, so a constant would fail.

Measured on the two-request producer: classification production 216427 ms ->
167213 ms. Real, and not the whole term - the remaining cost is still inside
the edge producer's two folds and gets measured next rather than guessed at.

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

* Name the remaining term: ExprCall, at 389 microseconds per evaluation

Three forensics-gated counters, each of which answered a question that was
otherwise a hypothesis, and one of which REFUTED one of mine.

The stack-segment probe: 1089374 probes, 0 under the red zone. stacker never
grows a segment on this workload, so the 8 MiB-per-call theory is dead. The
zero is kept as the receipt.

The node-evaluation counter: 4423731 evaluations for a 173-second phase, which
prices ONE evaluation at ~39 microseconds. That is a per-node cost, not a
node-count problem - so no amount of work on WHAT the lens computes was ever
going to move it.

The per-variant profile, using the interpreter's existing GUNBC_INTERP_PROFILE
instrument rather than a fourth counter, locates it exactly:

  ExprCall        434702 evals   169227ms self   389295ns/eval   97.8%
  ExprRecordLit   793655 evals     1515ms self     1909ns/eval
  ExprVar        2610850 evals      643ms self      246ns/eval

Self time excludes the callee body and the argument nodes, both of which are
child evaluations. So 389 microseconds is spent per call in the call machinery
itself, and everything this lane touched - the grouped index, the hoist, the
accumulator - was optimizing terms three orders of magnitude below it.

Reports only; no production behavior changes here.

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

* Make the hash oracle independent, execute it in CI, and delete the probes

Three repairs from review, in the order they depend on each other.

THE ORACLE WAS NOT INDEPENDENT. reference_materializing_hash recursed through
the PRODUCTION value_hash, so it checked only the outermost string/list layer:
a drift appearing solely inside a nested list would have been shared by both
sides and stayed green. Since value_hash decides HAMT key identity, that drift
changes grouping, lookup and cache behaviour with no type error anywhere. The
oracle is now a complete recursive copy of the replaced implementation --
reference_materializing_hash and reference_hash_fields_commutative are mutually
recursive and call no production code -- over 24 specimens: empty and ASCII and
multi-byte strings, empty list, list of primitives, list containing a list,
list of strings, record and variant carrying nested lists, a Map, a Set, and
the i64 boundaries. The discriminating half now also asserts the ORACLE
separates the same pairs, so a degenerate oracle fails too.

THE CONTROLS WERE COMPILED, NOT EXECUTED. The build job runs cargo test
--no-run, which proves compilation and runs nothing, so every #[cfg(test)]
control in the v1-compiler lib had never executed -- specification without
execution. A second build-job step now runs exactly the two named modules:
-p v1-compiler --release --lib, two filters, --test-threads=1 because the
parity control reads process-global interpreter state. Deliberately NOT the
whole lib suite: the heavyweight producer module resolves the live tree and has
been observed to exhaust memory as a group, so enrolling everything would trade
an unexecuted control for an unreliable job. Two witnesses keep the two claims
apart, and the RED half fails if the execution step ever acquires --no-run.

THE PROBES ARE DELETED, NOT KEPT. record_node_eval ran on every expression
evaluation and record_stack_probe on every dag-fn call and closure application.
Both were investigation instruments and both have delivered: 1089374 stack
guards with ZERO below the red zone (segment growth refuted), and 4423731 node
evaluations pricing one eval at ~39 microseconds. Leaving a call and a branch on
the hottest path in the compiler would be permanent residue charged to the cost
they were built to measure, so the receipts stay in the commits that took them
and the code goes.

Green by execution: 5 interpreter controls, 2 step witnesses; ci.yml regenerated.

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

* Record the measured value_hash A/B receipt (Cut 1 step 5)

Both arms unprofiled, same exact classification subject, same binary shape:

  arm        classification_production   live-read wall   cpu        RSS
  control    218738 ms                   313266 ms        312468 ms  7195 MiB
  candidate  182038 ms                   274597 ms        273853 ms  7183 MiB

Streaming the hash is 36.7 s (16.8%) cheaper on the classified term and
38.7 s on the whole read, at identical resident cost and identical rows
(requests=2 rows=2 in both arms). The fixed prelude reproduced across arms
(17.4/17.7 s fact bundle, 36.7/35.7 s source load, 40.4/39.0 s resolve+
typecheck), which is what makes the pair comparable rather than two samples
of host noise.

The receipt also RETRACTS the earlier 216 -> 167 s reading: those were single
runs on a host whose run-to-run variance on this phase is tens of seconds,
so only the paired arms above are quotable.

classification_production stays far above the fixed lens resolve/typecheck
term, so the operator's stop rule does not fire: this repair removed one
measured term, it did not move the dominant cost.

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

* Mark the interpreter-parity CI shell transport as a scaffold with a dissolution trigger

The compile gate carries ci_v1_compiler_tests_compile_gate_shell_emit_scaffold
plus a named dissolve-on trigger, rendered as a comment on its emitted GitHub
Actions run:. The interpreter-parity step added a second foreign-executor run:
path over the same seam with neither, so a new shell transport landed unmarked
and uncounted.

Add the sibling pair. The trigger states what the compile gate's does not: this
is the SECOND consumer of one seam rather than an independent transport class,
so it dissolves with it under shell-to-intent Phase 2, and it joins the srvN
STEP 2 population already tracked for the shared EAGAIN retry arm.

The marker is emitted, not merely modeled. ci_interpreter_parity_controls_run_script
renders the trigger the same way the compile gate does, and two witnesses execute:
the carrier is a Scaffold, and the emitted run script actually carries its trigger
text. The second is what keeps the disposition from being an inert registration -
a row nobody emits would be registration mistaken for enforcement.

ci.yml regenerated: one added comment line, no other artifact churn.

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

* Build the live-read selection plan once per index, not once per entry group

The floor refused at its 55-minute population budget on 1eb10b48f29 with no
witness failing. The log names the cost: [live-read-candidates] printed 719
times, every one reporting the identical roster_rows=8563 candidate_rows=91
excluded_by_cheap_axes=8472. Summed gap preceding those lines is 17.6 minutes
of a 70.3-minute job, median 1.41s each.

run_discovery_rows is called once per queued entry group inside each worker's
loop, and it built LiveReadSelectionPlan over the COMPLETE roster on every
call. Roster, index, diff and changed paths are all invariant across the groups
one worker processes, so 718 of those 719 builds recomputed an answer that
could not differ.

The code had already caught this class one call site over: the narration site
carries a comment saying "THROUGH THE PLAN, not a second full-roster build --
it could independently recreate the 8538-request classification the execution
sites had just stopped making, moving the stall from one consumer to another."
The worker-group site is that same mistake, unfixed.

The plan is per-INDEX, not per-run: a manifest carries the subject of the index
that produced it and each width>1 worker builds its own MultiEntryIndex, so the
main thread's plan would refuse on subject mismatch at every lookup, correctly.
Per-index is therefore the strongest sharing the subject identity permits.
live_read_selection_plan_for_index is the single build site, called where each
index is built, and run_discovery_rows now takes the prebuilt plan instead of
the roster -- so the per-group rebuild is not merely removed, it is unwritable
from there: the roster it would need is out of scope.

The rationale comments that documented the removed build move onto the helper
rather than being deleted with it.

NOT CLAIMED: that this greens the floor. 70.3 - 17.6 leaves ~52.7 minutes
against a 55-min…
briansrls pushed a commit that referenced this pull request Aug 16, 2026
… getting from the pool (#8326)

* Unbreak the whole-tree compile: name the import three call sites were getting from the pool

v2.lens.live_read_classification carries no import block and calls
witness_import_closure at three sites. Those resolved only when some unrelated
module elsewhere in the assembled closure had already pulled
v2.lens.witness_cost_locality in - the pool-membership coincidence DESIGN
records as the import-strip Class B failure. Under a whole-tree compile no
module does that, and all three refuse.

MEASURED, both directions, at origin/main 87513a53e8:

  without this change   gunbc compile --target dag  exit 1, 3 blocking errors
  with this change      gunbc compile --target dag  exit 0, 0 blocking errors

The call sites are unchanged; this names where the function comes from, from
the definer, exactly as v2.lens.effect_reach already does for the same
function.

Why this stood since 2026-08-08 (#7997): per-PR compile-clean is scoped to the
affected import closure, and this module is outside most PRs' closures. The
whole-tree baseline is what any PR touching a non-selectable path would have
hit.

* Import the module_graph functions too: one import block governs the whole module

The first commit added one import and broke a green witness. That is the Class B
failure inverting, and it is worth stating because it makes the class harder to
fix than it looks: while live_read_classification carried NO import block, every
bare cross-module reference resolved from the pool. Adding one import made the
module import-governed, so path_matches_touched - which had been resolving the
same accidental way - stopped resolving, and
g2_roster_gate_literal_path_touched_selects failed at runtime.

MEASURED, and the split matters: the whole-tree compile reported only
witness_import_closure. path_matches_touched resolved at COMPILE time and failed
at RUNTIME, so the compile gate could not have caught this and did not. Only
executing the witness did.

The consequence for the namespace-cut lane: a module with bare references
cannot be repaired one import at a time. The first import is the breaking
change, and every reference the pool was supplying has to be named in the same
commit.

All four names this module takes from v2.lens.module_graph are now imported from
the definer. live_read_classification_test: 20 of 20 green by execution,
including the witness that regressed.

---------

Co-authored-by: Brian Searls <briansearls1@gmail.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.

2 participants