Skip to content

CALLABLE-LOOKUP-UNIQUE: collect every admissible callable candidate before deciding, and stop erasing the ambiguity arm - #8952

Merged
briansrls merged 28 commits into
mainfrom
session/tidy-pike-614
Aug 25, 2026
Merged

briansrls merged 28 commits into
mainfrom
session/tidy-pike-614

Conversation

@briansrls

@briansrls briansrls commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Two findings that came out of executing this, stated first

1. The wall could not fire for its own specimen, because of a guard I wrote

builtin_callable_candidates excluded every algebra method template name. I wrote that guard and justified it in a paragraph: receiver-dispatched names (filter, any, contains) belong to the known-method ExprCall arm, and admitting them would make every module declaring a collection-shaped name a collision.

Measured, the guard was redundant for the names it named and fatal for the ones it did not. Of ~50 algebra template names only 19 carry a builtin_function_registry row, and filter/any/contains/fold/map are not among them — infer_builtin_call_type already answered Absent, so they were never candidates and the guard changed nothing about them. What it actually excluded was the 19 names that do have a free-call surface, map_get first among them — the exact name row 30 is about.

So the wall was inert on the collision it was built for, and the witness that should have shown that returned false in a way indistinguishable from a broken fixture (which it also was). The admission test is now the registry alone, which is precisely the question being asked: a registry row means "a bare call to this name types against a primitive."

Worth naming beyond the fix: a justification is not evidence, and the paragraph made the guard more durable rather than less — a defended exclusion is harder to question than an undefended one.

The wall does not add a diagnostic — it replaces three false ones with one true one

Measured on the RED source, before and after, through compile_dag_diagnostic_census with the mirrors regenerated and installed:

rows produced
before VariantNotFound/Present, VariantNotFound/Absent, NonExhaustiveMatch/(non-exhaustive)
after AmbiguousReference/BLOCK/map_get

The three "before" rows are all downstream, and all describe the damage rather than the cause: each one points the author at a match arm that was never the problem. The single "after" row sits at the call and names both authorities. This is not a stricter compiler — it is a compiler that stops lying about where the defect is, which is the difference between a check and a diagnosis.

It also closes row 30's own text, which recorded the class as latent and observable only as downstream non-exhaustive matches. The same source now produces the refusal instead of those matches, on the real acceptance path.

2. Row 30 is now reproducible on demand, not just historically observed

Two sources, identical call, import closure the only difference — measured through compile_dag_diagnostic_census:

source imports census
std.types only [] — completely clean; bare map_get is the builtin and Present/Absent is exhaustive
extdeps.posix.identity VariantNotFound/Present, VariantNotFound/Absent, NonExhaustiveMatch

The second module names no authority for map_get; posix.identity's own closure supplies the Outcome-returning declaration. That is the incident, on demand, under a controlled fixture — converting the specification from a historical observation into a discriminating RED.


What this is

Two independent things, deliberately in one PR and named separately so neither hides inside the other.

  1. The wall. lookup_func_sig stops asking the environment first and the builtin surface second. It now collects every admissible callable candidate and then decides: zero → typed FuncSigUnresolved; exactly one → resolve; two or more → typed FuncSigAmbiguous naming every exact candidate. And the projection that erased that third arm (func_sig_if_resolved, which mapped FuncSigAmbiguous { candidates: _ } to Absent for ten consumers) is deleted, replaced by a total func_sig_for_derivation whose NoDerivableSig carries a typed reason.

  2. An independent repair, in its own commit: the emit_map_has builtin registry row is deleted. It certified a call that nothing at any tier could run.

Specification: row 30 of docs/plans/compiler-guarantee-recovery-gap-analysis.md.


The wall

Mechanism (a) — the candidate set is now assembled. callable_lookup_over_candidates reads four levels, and the level structure is the whole point, because the failure mode this lane must not become is "the leaf name appears twice, therefore refuse":

  • level 0 — the caller's own declaration. A lexical relation, not a candidate. It wins outright and is never entered into the decision. Body-scope binders shadow one seam further up, in body_shadow_aware_func_sig. Legitimate shadowing stays legal.
  • level 1a — names the author explicitly brought into scope (type_env.source_visible_names: the module's own declarations, the kernel surface, selectively imported names, all-module import exports). import v2.std.collection { map_get } names one exact declaration, and every later bare call in that module is a reference to the thing the author named — the same authorial act as a qualified reference, one line higher. The builtin surface is not a co-candidate here.
  • level 1b — the transitive closure and the builtin surface, together, as peers. This is the change, and this is the incident. Both are visible without the author naming either anywhere, so two answers here are two answers at one level and nothing ranks them.
  • level 2 — the corpus-wide census (func_sig_from_global_bare), reached only when nothing above produced a candidate. Preserved: a bare call to a declaration outside the closure still resolves exactly as before.

Why 1a exists, and why it is not the precedence this cut removes. The row-30 incident is transitive: dag/extdeps/git/object_store.dag at ac9e010a83~1 names v2.std.collection zero times — the declaration reached it through somebody else's edge. Meanwhile six modules bare-call map_get while naming it in a listed import — 03_ingest, target_model, compilation_unit, cli_surface, affected_set, v2_effect_io_pure — and zero reach it through an is_all import. Without 1a the wall refuses all six, which includes the v2 compiler's ingest and target-model stages. (An earlier revision of this section said sixteen: that was modules importing v2.std.collection at all, only six of which bare-call map_get.)

1a ranks nothing this cut forbids: not import order (a listed import is set membership, not a position, and two listed imports of one name are still ambiguous), not "declared beats builtin" (an unlisted declaration still does not beat the builtin — that is exactly the case that refuses). It distinguishes what the author named from what a closure happened to supply, which is the definition of the defect rather than a tiebreak between kinds of callable.

It reuses source_visible_names rather than minting a visibility notion beside it: that map is already the corpus authority for "the author named this", and v1.compiler.infer_env's closure_independent_bare_free_call_note already refuses global-bare and pool-coincidence resolution for registered symbols absent from it — same question, same authority, one seam over. Empty is ignorance, not an answer: source_visible_names is built at resolve time and is not persisted onto TypedModule.type_env, so the emit-side consumer reads an empty map and cannot distinguish listed from transitive. An empty map therefore takes the 1a arm (today's behaviour), never "nothing was named", which would make the wall strictest exactly where it knows least.

Rung, per path (§4b), minimum governing: refuses on the source→resolve path; silent on the emit path. Next-rung trigger for the emit row: persisting source_visible_names onto TypedModule.type_env. Citing only the resolve path would be inflation.

The decision itself is not new logic. It routes through module_path_owner_binding_decide, the existing single 0/1/many cardinality authority, so the wall does not mint a second answer to "how many owners does this name have" (§3).

Order-independence falls out of the shape rather than being asserted: the candidate list is decided by cardinality, not scanned for a first hit. There is no "first imported wins", no "declared beats builtin", no import-order arm to be sensitive to.

One deliberate asymmetry, stated because it looks like a hole. A lone builtin candidate answers FuncSigUnresolved rather than resolving. The registry maps a name to a return type and carries no parameters, so resolving from it would assert a call shape the registry does not know — a fabricated plausible signature (§5). Unresolved is the correct answer and routes the call to the known-builtin bridge exactly as today. The builtin's presence is load-bearing only for counting, which is the half row 30 needed.

Diagnostic. Candidates carry exact identities — DeclaredCallable { owner_module_path, decl_name } / BuiltinCallable { primitive_name } — not bare strings, so the measured specimen reads:

callable 'map_get' is ambiguous: <builtin:map_get> / v2.std.collection.map_get

Evidence — three sources, all through the real acceptance path (compile_dag_diagnostic_census runs the production resolve/typecheck over the live source roots), in dag/test/claim/callable_candidate_ambiguity_witness_test.dag, enrolled in the required floor (no live_tree_disposition, so it executes rather than being declined):

source asserts
RED a module naming no authority, reaching a declarer only transitively (import extdeps.posix.identity, whose own closure contains v2.std.collection) refuses
GREEN 1 the same spelling with the authority named (import v2.std.collection { map_get }) resolves
GREEN 2 the same spelling declared by the caller itself (lexical shadowing) resolves

The counter returns 0 - 1 on CensusNotRunnable, so a broken harness fails in both directions rather than passing the RED by silence.

The RED had to be rebuilt, and that is worth recording rather than quietly fixing. Its first version used the listed-import source as its RED and asserted a refusal — which is not the incident's shape at all. A witness that certifies a wall while testing a shape the defect never had is worse than no witness, because it goes green and reads as coverage: this one would have certified refusing the v2 compiler core as correct behaviour. It was found by checking the specimen against the incident, which is the only way it could have been found.


What the wall's 1215-site output IS

Turning the wall on exposes 1215 sites. That number is not a defect count and must not be read as one. It is the first-ever measurement of the surface-versus-primitive fork: how much of this corpus calls a name that exists both as a registered primitive and as a .dag declaration.

name sites modules verdict
length 615 211 one authority
map_insert 505 59 one authority
decl_facts 27 16 one authority
map_get 12 6 two authorities
data_decl_type_facts 2 one authority
src/v1: to_string 31, emit_map_has 20, map_has 3 54 see below

1147 of the 1161 dag+src/v2 sites are not ambiguity bugs. They are a nickname census (§3): one concept wearing one spelling across two carriers, where the second carrier adds no meaning. The dissolution is the PrimitiveDefinition/PrimitiveIdentity join (dag/std/primitive_identity.dag), dispatched as its own lane; this PR is stacked behind it and stays draft until it lands. It is explicitly not dissolved by generalizing the _host_binding marker: that marker links a contract to its fn by name, through a module-level data row — the same nickname class the wall exists to remove.

Per-name verdicts, each established by execution

Asked for per name, no bulk judgment, and each carries its own evidence class:

  • map_get — TWO AUTHORITIES. control 14 diagnostics, call 17: +3, including one NonExhaustiveMatch — row 30's signature exactly. The builtin returns Optional<V>; v2.std.collection.map_get returns Outcome<Optional<V>>. Different shape, so a Present/Absent match over the declared one is non-exhaustive.
  • length — ONE AUTHORITY. control 14, call 14: no drift. The free-call builtin arm intercepts ahead of user-fn dispatch.
  • map_insert — ONE AUTHORITY. control 14, call 14: no drift. The module declares map_insert_host_binding: PrimitiveContract, and the interpreter carries try_v2_std_collection_map_primitive_grounding → MapGroundingMapInsert — grounded, not forked.
  • decl_facts, data_decl_type_facts, export_signature_facts, concept_decl_facts — ONE AUTHORITY, and by a different and stronger instrument than the probe: an already-green floor witness (test.claim.record_construction_census_witness_test) imports and bare-calls decl_facts, whose declared body is fn decl_facts(x) { decl_facts(x: x) } — an infinite self-call. A green run is only possible if the builtin served it. The existing corpus was the oracle; no new probe needed.
  • emit_map_has — PHANTOM. Registry-typed Bool, no arm at any tier, bare call → NoSuchFunction. Deleted in this PR (below).

The control construction — the reusable part

The first pass of this probe reported map_get +17, length +14, map_insert +14, decl_facts +8 and read as all four names drifting. Three of those four were false.

The bug: the probe counted every diagnostic a source produced, and an importing source inherits diagnostics from the imported closure, not only from the call site. So the instrument was measuring "did I import a big module", not "did this call resolve differently".

The fix, and the thing worth keeping:

For each name, author two sources: a CALL source that imports the declarer and calls the name, and a CONTROL source that imports the same declarer and never calls it. Drift is CALL − CONTROL, not CALL.

Under that control only map_get moved. This is written down because the next person measuring a closure-sensitive property will otherwise rebuild the instrument wrong the same way — an uncontrolled count over an import closure looks like a measurement and is a measurement of the closure.


The map_get residue: there is none, and that is a positive result

I initially reported "12 sites to repair". Classifying by the match arms rather than the spelling, over current main:

  • 12 sites reach v2.std.collection and match Accepted/Rejected — Outcome-shaped, i.e. correct consumers of the declared fn. These are the twelve. Repairing them would have rewritten twelve correct call sites away from the authority they were deliberately written against — a regression, in a lane whose entire subject is that one name can mean two things.
  • 85 sites are builtin-shaped (Present/Absent), and every one has reaches_declarer = false — they resolve to the builtin and typecheck, as written.
  • The intersection of builtin-shaped and reaches the declarer — the only actually mis-resolved shape — is ZERO.

Row 30 asserted that none of the 43 is currently mis-resolved, from the absence of diagnostics. This establishes the same claim constructively, from a direction the row could not reach: absence of a symptom and emptiness of the intersection are different facts, and the second is the one that holds. The class is latent, not live — now on evidence rather than on silence.

gunbc#8944 (merged, ac9e010a83) repaired four sites — dag/extdeps/git/object_store.dag ×2, src/v2/workflow/floor_preparation.dag ×2 — using the Map primitive method X.lookup(key) rather than switching to map_lookup, because map_lookup lives in v2.std.collection, the very module whose closure presence is the unstable fact. Those four are in main and in this branch via a merge; they are absent from every count above. No exclusion set was needed, and no repair set exists.

This PR therefore edits no map_get call site.


The emit_map_has deletion (independent repair, own commit)

Row 30 with the halves swapped: there, a consumer's import closure changes a name's meaning; here, the closure is the only thing supplying one.

A registry row types a bare call; an interpreter arm runs it. emit_map_has had the first and not the second — no free-call arm, no method arm, no bridge — so the call typechecked clean and answered NoSuchFunction at evaluation. Measured by execution, not by reading the dispatch table: a probe calling it bare through gunbc run refused with NoSuchFunction, while the same probe's map_get and length returned 7 and 3.

20 of 20 live call sites reach the declared v1.compiler.infer_types emit_map_has through their own closure, so none loses resolution. That is exactly what kept the row invisible: nothing was ever served by it. (An earlier message of mine said 21. That figure counted the declaration in 04_types.dag as one of its own callers — the measure-the-name-rather-than-the-binding error this PR exists to close, committed inside the receipt for it. git grep puts the occurrence count at 21 on every commit involved, so nothing moved under me; I simply miscounted, and the correction is recorded in the source note rather than quietly applied.)

Deleting the row costs no call site and makes the typecheck honest: a future caller outside that closure now gets an unresolved-name refusal at compile time instead of a runtime NoSuchFunction. Rung: runtime failure → compile-time refusal for that name; mechanically preventable, not structural — nothing yet prevents authoring the next registry row with no runtime behind it. Next-rung trigger: the PrimitiveIdentity join, which is precisely the registry-row-to-runtime join this row was missing.


src/v1's 54 sites — named, not deferred silently

to_string 31, emit_map_has 20, map_has 3. They fall in the gap between the two arms above: the witness roots are ["dag", "src/v2"], so no floor witness can import a src/v1 module, and the census instruments above do not reach them. They are in scope for this program and are recorded here rather than dropped:

  • emit_map_has's 20 are resolved by this PR — all reach the declaration, and the phantom row that could have shadowed them is gone.
  • to_string (31) and map_has (3) remain unmeasured by execution. Not asserted clean: no control probe was run against them, because the instrument that would run it cannot reach src/v1. They ride the same PrimitiveIdentity dissolution as the dag/src-v2 census, and the honest status is unmeasured, not one authority.

The length question is answered, by the join lane rather than here

I had recorded this as open: the length verdict shows the free-call builtin arm intercepting ahead of user-fn dispatch, so the declared v2.std.algebra.length body cannot run for a bare call — does that generalize to the other projection declarations?

It does, and gunbc#8964 (the PrimitiveIdentity join, gentle-koi-563) establishes it by reading the declarations rather than by probing them, and splits the five names into three fidelities:

  • HostRealizedSeam — body is a self-call, so the declaration could not survive being reached. decl_facts, export_signature_facts, data_decl_type_facts, concept_decl_facts, concept_decl_facts_live, empty_map_primitive_delegate.
  • ModeledProjection — a real body the builtin arm intercepts ahead of. length, map_insert, empty_map. This is the answer to my question: dead code with a live spelling.
  • DivergentProjection — map_get, Outcome<Optional<V>> against the primitive's Optional<V>. Genuinely two authorities, and it must keep refusing at this wall.

That carrier reproduced the emit_map_has finding below from the model (registry_no_runtime=4: emit_map_has, Some, to_int, with) rather than from my probe — which is the receipt that the mechanism works, not that one specimen happened to be found.

What this wall will consume when the join merges, agreed across the two lanes and stated here so a future reader can check my seam against their type rather than against a message thread: my suppression consumes DeclarationProjectsPrimitive and nothing else. Every other arm is either a refusal (NotAProjection, DivergentProjection) or a fall-through (SymbolUndisposed). Concretely — seam → suppress silently; modeled → suppress plus an advisory naming the intercepting primitive; divergent → refuse with the divergence text beside both candidate identities; not-a-projection → refuse; undisposed → do not refuse, fall through to today's behaviour and count.

DeclaredCallable { owner_module_path, decl_name } becomes std.decl_ref DeclarationRef at that point rather than staying a structural twin of it; it is minted locally today only because v1's infer_sigs cannot import a dag/ module.

What remains for the join, measured under 1a

Counting bare calls only — a qualified v2.std.collection.map_get(...) is an exact reference naming one exact declaration, not a bare call, and my first pass wrongly counted three of those as bare — and splitting by whether the author named the authority and whether the closure reaches the declarer:

name bare sites 1a: named, resolves 1b: refuses modules
length 797 373 244 76
map_insert 620 434 71 4
empty_map 294 186 23 5
decl_facts 35 24 3 3
map_get 97 12 0 0

341 refusals across ~88 modules, and every one of the four names carrying them is already disposed in gunbc#8964 with a suppress verdict (length/map_insert ModeledProjection, decl_facts HostRealizedSeam). So the stacking is now measured rather than assumed: 341 ≠ 0, this PR cannot merge first, and nothing further is needed from that lane once it does.

There is no live map_get collision left in dag/ or src/v2. After gunbc#8944 the 1b count for it is zero: 12 sites name the authority, 85 never reach the declarer. The wall's RED for it is therefore fixture-authored, not corpus-present — which §4b makes the correct posture rather than a gap, since reachability is judged against what a fixture may author, never against what the accepted corpus contains. What the wall buys is that the next such collision refuses instead of silently re-typechecking three untouched files.

The resolver consumes the join by ordinary import. I claimed src/v1 could not import a dag/ module and built a two-branch integration question on it; that was false, and gentle-koi-563 refuted it by reading the file. src/v1/04_lookup.dag — the module this wall lives in — opens with import std.induction (line 3) and import std.algebra (line 23), both dag/std/*; 31 of 47 src/v1 .dag modules import a std./extdeps./gunbc. module today, and dag/std/primitive_identity.dag imports nothing from src/v1, so the edge cannot close a cycle. The error was reading the witness roots (["dag", "src/v2"], which govern what the floor discovers) as a rule about the import graph — where §3 says acyclicity is the only structural law and folder prefixes are browsing conventions.

Unowned scope, named so it is not silently dropped: to_string (31), map_has (3) and the deleted emit_map_has (20) are src/v1 declarations. A projection row needs a declaration to point at, and whether src/v1 declarations get rows at all is a scope question rather than three rows. It belongs to neither this lane nor the join lane today.

The SymbolUndisposed arm is not bookkeeping — it is where this join could have manufactured 196 refusals

196 of 209 census symbols are undisposed today; 13 are disposed. If "nobody has classified this yet" reaches my seam as the same answer as "classified as not a projection", the wall refuses on the first one — and that is absence of a classification rendered as a classification of not. Two states, opposite repairs: the first is closed by disposing a symbol in the join's carrier, the second by qualifying a reference at a call site.

What makes it worse than an ordinary state-space conflation is the direction it fails in: it converts an incomplete census into a corpus-wide refusal, so the wall's strictness would scale inversely with how much of the census was done. A join whose entire purpose is to remove refusals would have produced more than it removed — and it would have looked correct from both sides, because both halves check out and only the arrow between them is invented: the carrier answers truthfully that no projection is declared, and this resolver reasons soundly that an undeclared projection means two authorities.

It is being fixed structurally rather than by agreement: the two arms are distinct arms of one total answer, so this seam cannot silently merge them and any future consumer must name both. An agreement between two lanes evaporates when either session is archived; a coproduct does not.

The eleventh consumer, and why a .dag census could not see it

The specification names ten consumers of func_sig_if_resolved — nine in v1.compiler.infer, one in v1.compiler.emit. There is an eleventh: src/v1/stage0/src/cli_run.rs, which is hand-written periphery, not emitted code. The modules compiled clean as .dag and emitted fine; the build then failed as Rust with E0432: unresolved import crate::v1_compiler_infer_lookup::func_sig_if_resolved.

That is this PR's own subject turned on the PR: a census over the .dag corpus answers "who consumes this declaration" for the emitted world only, and the hand-written seed periphery is a second population no .dag query reaches — the same shape as the src/v1 residue that no dag/-rooted witness can import.

All three of its uses are inside tests that deliberately pin the legacy ImportScoped policy, and their own comment already records that the default policy yields "a typed AmbiguousReference instead of a first-hit pick". So this is the one place the collapse is legitimate — under ImportScoped no ambiguity can arise, and the other assertion is a genuine miss. They now route through a #[cfg(test)] helper that performs the collapse locally and states why, rather than through a production projection that erases the arm for all eleven.

// annotations must sit at module-item grain

Three of this branch's required-regen refusals were mine and all had one cause: ten annotation lines written inside declaration bodies, which the src/v1 parse sweep refuses because only module-item grain is modeled (DESIGN §4c). Hoisted to module scope, each attached to the declaration it describes and naming the arm inside it. Recorded because §4c is behaving exactly as written and this is the second lane to hit it. The #8691 second-pass regen red on main is a different failure, downstream of where mine were stopping — mine are closed and that one is inherited.

The lane's real output

Three times on this lane I was wrong in the same way: I measured a name rather than a binding.

  1. The drift probe counted the diagnostics a source produced rather than the ones its call produced — so an import closure's own diagnostics read as drift. Three of four first-pass results were false. Fixed by a control source.

  2. The residue count matched a spelling rather than the authority each site was written against — so twelve correct consumers read as twelve repair targets. Fixed by reading the match arms.

  3. The emit_map_has receipt counted the declaration as one of its own callers — 21 occurrences reported as 21 call sites, when there are 20 and one definition. Fixed by excluding the definition. This one was committed to source, inside the note explaining that a name's occurrences are not its bindings.

  4. I claimed src/v1 cannot import a dag/ module, and built a two-branch integration question on top of it. src/v1/04_lookup.dag — the file the wall lives in — opens with two such imports. I had that import block on screen an hour earlier while looking for something else.

The third is the instructive one, because I already knew the rule when I made it. "Pair the measurement with something that discriminates" is true and turned out to be insufficient as a defence: the discriminator has to be applied to the thing being counted, not held as a principle. The operational form is narrower and checkable:

When the unit is a binding, the denominator must exclude the declaration. A grep for a name returns definitions and uses in one undifferentiated number.

The fourth needed a different control, and gentle-koi-563 supplied the general one after making the same class of error independently (misreading ps -o etime as HH:MM when it is MM:SS, and publishing the wrong figure twice before a 75-second sleep settled it):

Any claim of the form "X cannot Y" or "nothing does Z" names a set it asserts is empty. The control is to go find one element. One grep, and 04_lookup.dag was the first hit.

All four of mine and all three of theirs were claims of that shape, and every one had a cheap counterexample sitting in the tree.

That generalization is worth more than the wall, and it is the thing that otherwise gets rediscovered in six months by someone who greps.



The architectural constraint nobody has stated

Consuming the PrimitiveIdentity join from the wall means the seed imports std.primitive_identity, and that is where this branch met a boundary that is enforced nowhere:

The v1 seed closure has never contained a v2.* module. Zero of 47 src/v1 modules import v2.*, and neither do the closures of the two dag/ modules the seed already imports (std.algebra closure = 2 modules, 0 of them v2.*; std.induction = 9, 0). std.primitive_identity's closure is 33 modules, 11 of them v2.*, all reached through a single edge: gunbc.v1_interpreter_primitive_surface imports v2.std.qualified_name.

Nothing declares this constraint. It is enforced only accidentally, by regen refusing an unresolved import — a symptom-level refusal that names a missing module rather than the rule it is protecting. A contributor adding one import to a dag/ module the seed consumes has no way to learn the rule exists until a regen red names a file they did not touch. That is worth more than this PR's mechanism, and it is recorded here rather than fixed, because widening the seed's module index is an architectural decision several orders above this brief.

What the 29 → 0 collapse proves

Wiring the join dissolved 29 seed collisions to 0 (to_string ×26, map_has ×3). None of them was a ModeledProjection I had qualified: every one answers DeclarationPrimitiveUndisposed, so the builtin is not admitted as a rival and the refusal never fires.

That is a receipt for the join's third arm rather than for this wall. DeclarationPrimitiveUndisposed was authored as the honest third state — this symbol is on a primitive surface, but nothing has disposed of the relationship — and had no consumer at the moment it landed. The first real population it met was these 29, and it classified all of them correctly. A three-arm answer whose middle arm has never been exercised is indistinguishable from a two-arm answer with a dead branch; this is the measurement that separates them.

Four seed import repairs authored before the join was wired are reverted in this branch (b6b547c522). Their justification evaporated when the collisions did, and four edits whose stated reason no longer exists are exactly the residue that gets rediscovered six months later.

Which direction the dependency runs: PrimitiveIdentity sits below callable resolution

Stated explicitly because the opposite wiring is available and would be a layer inversion: callable resolution consumes the projection answer; it never asks whether a declaration happens to equal a primitive.

declared_candidate_rivals_the_builtin calls primitive_projection_for_declaration(decl_ref(owner, decl)) and matches its three arms. That is the whole of the interaction. It performs no taxonomy work of its own — it never inspects a PrimitiveIdentity (the identity field is discarded at the binding), never compares authored symbols, never consults a surface roster, and never repairs or supplements a projection row. It asks one question and maps the total answer onto one local question: is the builtin a second authority for this name, or the same one?

That is the boundary that keeps the two concerns separable. If the wall instead reconstructed the primitive relationship itself, callable resolution would become responsible for repairing primitive taxonomy — every gap in the roster would surface as a resolution defect owned by the wrong lane, and the wall's correctness would be coupled to the completeness of a population it has no standing to fix.

One consumer-side policy choice is worth naming so it isn't misread as taxonomy work: DeclarationPrimitiveUndisposed maps to not a rival, i.e. suppress. The wall does not decide anything about the primitive; it decides what this consumer does under ignorance, and it decides it fail-closed in the direction that matters here — refusing on an unclassified symbol would refuse the 196 undisposed symbols corpus-wide, which is the false-refusal storm rather than a wall. The projection answer keeps absence and ignorance as separate arms precisely so the consumer can make that choice explicitly instead of inheriting it from a collapsed boolean.


Import-safe and emit-safe are two properties, and only the first was verified

This is the finding worth more than anything else in the PR, and it was found by being the first consumer of someone else's module.

std.primitive_projection (#9060) was landed as the seed-safe leaf this wall needs. Its acceptance criteria verified that its import closure stops at std.decl_ref/std.types and reaches no v2.* module. That check was correct and it passed.

Nobody checked that the seed can emit it — and the seed must, because a seed-consumed module becomes a committed Rust mirror. Attempting the consumption produced nine compile_error!("unsupported mock expression") rows in src/v1/stage0/src/std_primitive_projection.rs, one per data row in the leaf. The module could not be consumed by its intended consumer.

Generalised, because it outlives this module: for any module intended for seed consumption, closure-stops-here is necessary and not sufficient. The sufficient check is an emit. The two properties are independent, and only one of them has a mechanism today.

The defect underneath it

05_emit_rust.dag data_value_has_cross_refs answers does this initializer reference another declaration, and emit_data_def uses it to choose between the JSON serializer (literals only) and emit_typed_expr (anything):

ExprVar { binding_kind: _ } => true      // a bare name is a reference — correct
ExprListLit                => descends
ExprRecordLit              => descends
_                          => false      // ExprCall lands here

A call is that same reference with an argument list attached. It fell through _, so data d: R = mk(..) went to a serializer that cannot represent a call, which then refused — the failure surfaced one layer below the judgment that caused it.

Total at the level examined, blind one level down. Exhaustive over expr_data, no arm missing, nothing for a reviewer or a wildcard-free lens to catch, because the _ carried the payload. Wrong by the function's own definition rather than incomplete.

The census that authorised the one-arm repair, with three instruments

instrument scope count
line regex (wrong) initializer is a call 1259
multi-line, string-stripped initializer is or contains a call 3181 across 971 files
emitted mirrors (ground truth) rows actually refused 9, all in the new leaf

The line regex was wrong by 2.5×: blind to multi-line initializers, and blind to the class that matters most — the predicate descends into record and list literals, so Foo { a: bar() } flips too.

The third instrument decides, because only the seed closure is lowered to Rust and the JSON branch is guarded by record-shaped type AND no-cross-refs. Every row on that path today emitted successfully, which by construction means it holds no call. So nothing that works today can move. DissolutionCondition (6 seed rows) is a coproduct and String (2) is not a record — which is why PrimitiveIdentity { slug: NonEmptyStr } supplies the first rows in the seed closure's history to satisfy both conditions at once, and why the defect stood this long unasked.

Verified after: JSON-path rows 34 before, 34 after — zero existing rows changed route. The emitter mirror moved by exactly one line.

The discriminating red, demonstrated rather than claimed

The nine compile_error! rows are evidence that self-destructs — they exist only while the defect does. The regen fixed point survives the fix:

arm verdict
present first_generation_equal=true
removed FAIL generated surface drift: v1_compiler_emit_rust.rs

Run both ways rather than argued. Its RED is authorable where the check runs (one arm in a .dag file regen reads every run), so this is not the permanently-green decoration case.

The storm, measured at full scale

The pre-join wall produced 608 refusals across 79 files on four names — length ×454, map_insert ×154, to_string ×26, map_has ×6 — and zero were genuine two-authority collisions. That is the case for the join reaching the seed, stated in the only currency that matters.

With the join wired and the transient bootstrap imports removed: zero ambiguity refusals, first_generation_equal=true, and all six witness assertions returning true against a binary built after install.

Two corrections to my own record

The revert commit message was incomplete. I removed four seed import edits saying their justification had evaporated. The stated one had — the join makes them unnecessary. They carried a second, unstated role I had not identified: they satisfy the old mirror's level 1a, which is what lets regen run at all during the bootstrap. Removing them broke the build for a reason the commit message gave no way to recover.

I quoted a corpus-wide count as a blast radius before reading the guard. 1259, then 3181, when the number that answers the question is 9. The number you can get quickly is not the number that answers the question, and it is always the scarier one. Read the thing that decides — the guard, the scope, the visible set — before counting the thing that is easy to count.


Status: ready for review. The closure question that parked this PR is resolved — #9060 landed std.primitive_projection, the seed-safe leaf, and this branch is its first consumer. Consuming it surfaced an emitter defect that blocked the consumption entirely; that repair is authorised, scoped to one arm, measured against the actual guard (34 JSON-path rows before and after — zero existing rows moved), and carries a discriminating red demonstrated in both directions.

Verified on the runner, not just locally: the required CI run — parse, regen, v2-emission, witness floor — is green, with required-regen: first_generation_equal=true. Zero ambiguity refusals corpus-wide, against 608 before the join reached the seed.

The inherited cargo check --all-targets break this branch also repaired was split out as #9142 and then closed as superseded — the identical fix landed independently as #9125. Three sessions found that break in one day, which is the gap its body names: no test-target compilation in CI since 2026-07-11, so the cost is paid in repeated rediscovery rather than once at the break.

Brian Searls and others added 5 commits August 22, 2026 23:38
…and the ambiguity arm stops being erased

`map_get` names two callables — the registered builtin (Optional<V>) and
v2.std.collection.map_get (Outcome<Optional<V>>) — and which one a bare call
meant was decided by whether the consumer's transitive import closure happened
to reach the declaration. One import edge added to extdeps.git moved two
unrelated modules' closures and silently re-typechecked three untouched files
(gap-analysis row 30; 46 files carry the same bare spelling).

The three-state outcome this needs already existed and could not describe the
collision, for two independent reasons:

(a) The candidate set was never assembled. lookup_func_sig asked the resolved
    function environment first and consulted the builtin/global path only on
    the Unresolved arm, so a declared map_get and the builtin map_get were
    never co-candidates — the declared one won by short-circuit.

(b) Where ambiguity was constructed, func_sig_if_resolved mapped it onto the
    same Absent that means "no such signature", and ten call sites read the
    seed's inference through it.

Both are closed here. Candidates are collected per admissible level and then
decided (0/1/many through the existing module_path_owner_binding_decide), with
lexical scope applying first: a module's own declaration wins, the import
closure and the builtin surface are ONE level where nothing ranks two answers,
and the corpus census stays strictly outside both. A lone builtin still answers
Unresolved so the call routes to the known-builtin bridge — the builtin is a
candidate in the decision, never an answer. func_sig_if_resolved is deleted and
replaced by a total projection whose two arms every site now names.

Candidates carry identities (module path + declaration name, or the primitive)
rather than pasted strings, because a builtin has no qualified name to paste.

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

An independent repair, landed beside the callable-candidate wall rather than
folded into it, because it is gap-analysis row 30 with the two halves swapped.

A registry row types a bare call; an interpreter arm runs it. `emit_map_has`
had the first and not the second, so the call typechecked clean and answered
`NoSuchFunction` when evaluated. Measured by execution rather than by reading
the dispatch table: a probe calling it bare through `gunbc run` refused with
`NoSuchFunction`, while the same probe's `map_get` and `length` returned 7
and 3.

Row 30 is a consumer's import closure CHANGING a name's meaning; this is the
closure being the only thing SUPPLYING one. All 21 live call sites reach the
declared `v1.compiler.infer_types` `emit_map_has` through their own closure,
which is exactly what kept the row invisible: nothing was ever served by it.
Deleting it costs no call site and converts a runtime `NoSuchFunction` for
any future caller outside that closure into a compile-time unresolved-name
refusal.

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

Only module-item grain is modeled (DESIGN 4c), and the src/v1 parse sweep
refuses a block inside a declaration body. The note now attaches to
builtin_function_registry and names where the row sat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The src/v1 parse sweep refused ten annotation lines across 04_lookup and
04_infer for the same reason as the emit_map_has note: only module-item
grain is modeled. Each block now attaches to the declaration it describes
and names the arm inside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brian Searls and others added 16 commits August 23, 2026 00:56
The earlier figure counted the declaration in 04_types.dag as one of its
own callers -- the measure-the-name-rather-than-the-binding error this
change exists to close, committed inside the receipt for it. The note
records the correction rather than applying it silently.

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

The row-30 incident is TRANSITIVE: dag/extdeps/git/object_store.dag names
v2.std.collection zero times and reached the declaration through somebody
else's edge. Collecting the builtin as a co-candidate against every visible
declaration therefore refused sixteen modules that name their authority
outright -- 02_parse, 03_ingest, 03_resolve, target_model among them.

So the level structure gains 1a: names in type_env.source_visible_names
(locals, kernel, selective imports, all-module exports) are what the author
named, and the builtin surface is not a co-candidate against them. 1b keeps
the incident's case, where nothing was named and the closure supplied one.
This ranks what-the-author-named against what-a-closure-supplied, never one
kind of callable over another: two listed imports of a name are still
ambiguous, and an unlisted declaration still does not beat the builtin.

source_visible_names is reused rather than a fresh visibility notion minted
beside it -- infer_env's closure_independent_bare_free_call_note already
gates global-bare resolution on the same map. It is resolve-time only, so an
EMPTY map is ignorance rather than an answer and takes the 1a arm; the rung
is per path -- refuses on resolve, silent on emit -- with the emit row's
trigger being persistence onto TypedModule.type_env.

The witness is rebuilt as a triple. Its RED was the listed-import shape,
which is the storm rather than the defect: it would have certified refusing
the v2 compiler core as correct. The RED is now transitive reach, and the
listed import becomes a green control.

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

The brief named ten consumers of func_sig_if_resolved -- nine in
v1.compiler.infer, one in v1.compiler.emit. There is an eleventh, in
hand-written periphery rather than in emitted code: cli_run.rs imported it,
which is why the emitted mirrors compiled as .dag and then failed to build
as Rust.

All three of its uses are inside tests that PIN the legacy ImportScoped
policy and assert its first-hit behaviour -- the one place the collapse is
legitimate, since an ambiguity cannot arise under that policy and the other
assertion is a genuine miss. They now go through a cfg(test) helper that
does the collapse locally and says why, rather than through a production
projection that would erase the arm for everyone else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit's message claimed this change and its diff deleted it.
The regen candidate tree under target/stage0-regen-candidate/src is a whole
crate source tree -- it carries hand-written files like cli_run.rs through
unchanged, not only the emitted mirrors -- so 'cp candidate/*.rs' restored
the original over the edit. git status then showed six changed files, which
is exactly what the six drifted mirrors would show, so the reverted edit was
invisible in the count.

Read what changed, not how many changed: the two are the same number here
and they are not the same fact.

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

The RED imported PosixUserName, which extdeps.posix.identity does not
export. The source failed for an unrelated reason, produced zero
AmbiguousReference rows, and the assertion read false -- indistinguishable
from 'the wall did not fire'. Executed, that is exactly what it looked like.

Corrected to PosixUserId, and joined by two assertions the pair could not
make on its own: the RED's only blocking diagnostics are the ambiguity
itself, and both GREEN sources carry no blocking diagnostic at all. A
fixture that breaks for any other reason now fails loudly on those instead
of returning a verdict about the wall it never reached.

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

builtin_callable_candidates excluded every algebra method template name,
reasoning that receiver-dispatched names belong to the known-method ExprCall
arm. Measured, that guard was redundant for the names it named and fatal for
the ones it did not: of ~50 algebra template names only 19 carry a
builtin_function_registry row, and filter/any/contains/fold/map are not among
them, so infer_builtin_call_type already answered Absent and they were never
candidates. What the guard actually excluded was the 19 names that DO have a
free-call surface -- map_get first among them, the exact name row 30 is about.

Executed evidence of the effect: the RED source produced VariantNotFound and
NonExhaustiveMatch -- row 30's ORIGINAL symptom, the declared Outcome-returning
map_get winning outright -- and no AmbiguousReference at all.

The admission test is now the registry alone, which is precisely the question
being asked: a registry row means a bare call to this name types against a
primitive. A name without one produces no candidate anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured through compile_dag_diagnostic_census on the real acceptance path,
with the mirrors regenerated and installed so the binary carries the change:

  RED    (imports extdeps.posix.identity, names no authority for map_get)
         -> AmbiguousReference/BLOCK/map_get
            and the VariantNotFound/NonExhaustiveMatch that row 30 describes
            are GONE -- replaced by the refusal, which is the point
  GREEN1 (import v2.std.collection { map_get }, then a bare call)
         -> no AmbiguousReference; the named authority resolves
  GREEN2 (declares its own map_has, no path to the declarer)
         -> census empty

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

source_visible_names folds BOTH import arms into one map -- is_all contributes
every name in the imported module's interface AND every name that module
acquired through ancestry_str_bindings. So 'import v2.std.collection' would put
map_get in it without the author writing the word, and the ancestry half
contributes transitively: the closure-supply case row 30 is about, arriving
through a set whose name suggests the opposite. Membership there is NECESSARY
for 'the author named this declaration' and not SUFFICIENT.

The sufficient half cannot be recovered by filtering, because the union already
happened at construction. So authored_import_names becomes its own field on
TypeEnv, built from the specific_names arm alone, threaded through every literal
site. Type parameters are deliberately not in it: authored, but not imports.

Measured before cutting: of the modules that bare-call map_get and import
v2.std.collection, all six name it in a listed import and none uses is_all, so
the narrowed rule still covers the population 1a was introduced for.

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

The wall refuses 29 sites in the v1 seed: to_string (26, across compile.dag,
dag_collect.dag, dag_collect_support.dag) against
v1.compiler.emit_core_support.to_string vs the builtin, and map_has (3, in
04_infer.dag) against v1.compiler.resolve.map_has vs the builtin. These are the
src/v1 residue that no dag/-rooted witness can reach and that #8964 does not
cover -- now measured rather than deferred, because the wall found them.

Repaired the way the diagnostic asks and at the grain the defect has: four
listed imports, not twenty-nine qualified call sites. Naming the authority once
per module is what the wall is asking the author to do, and it preserves
behaviour exactly -- the declared function is what these sites resolve to today.

The call shape settles intent for to_string rather than my judgement doing it:
the sites call to_string(value: x), and  is the DECLARED function's
parameter name (fn to_string(value: Int) -> String). A labelled call names its
callee. map_has is behaviourally identical under either authority and the
declared one is what runs today, so naming it changes nothing but the silence.

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

THE JOIN (#8964, on main at 642604b) now decides whether a same-named
builtin is a rival authority or the same one seen through a declaration: seam
and modeled projections drop the builtin, divergent keeps both and refuses,
no-projection keeps both, and UNDISPOSED drops it -- 196 of 209 census symbols
sit in that last arm, and treating ignorance as an answer would scale the
wall's strictness inversely with how much of the census is done. The builtin is
retained only when a declared candidate positively says the two differ.

PROSE CORRECTION, the load-bearing half. Level 1a was described as "a listed
import names one exact declaration". It does not: authored_import_names is a
Map<String, Bool>, so it carries NAME MEMBERSHIP and cannot say WHICH
declaration the author meant. What the mechanism does is remove the implicit
builtin co-candidate; the remaining DECLARED population still undergoes exact
cardinality admission, so one visible name plus several declarations still
refuses. Conservative, never silently selects the wrong declared function, and
strictly weaker than the sentence I had written. An inflated sentence in a
merged PR is what the next lane builds against.

A boundary control pins it: a source naming to_string in a listed import AND
reaching a second to_string through the closure must still refuse. If anyone
later reads 1a as "the imported declaration excludes every transitive
homonym", that control goes red -- the stronger rule needs an exact-binding
carrier that does not exist.

VisibilityUnobservable is documented as a COMPATIBILITY FALLBACK that may never
serve as a resolution authority downstream: it reports that this seam cannot
see source visibility, not that the author named nothing.

Two over-strict assertions replaced with per-class ones. "No blocking
diagnostic at all" read false for both GREEN sources, because the census
compiles against live witness roots and an unrelated UnresolvedType lands in
the count -- a check whose red is produced by something it does not measure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hority for the probe corpus

A debugging harness I used to read the census while iterating. Right thing to
have while iterating; experimental residue the moment the PR is proposed. It
has no test fn, no assertion, and nothing that can go red -- and the 'tmp_'
in a committed path is a statement that the author knew.

The worse half is that it carried red_src / green1_src / green2_src as data
rows duplicating the witness file's three sources: two authorities for the
probe corpus, in adjacent files, in the PR whose subject is one name having
two authorities. Whichever someone edited later, the other would silently
disagree, and the scaffold has no assertion to notice.

Reading a census as a string is a reasonable capability to want; if it is
wanted it is a separate proposal with its own consumer, not a leftover.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Its first version imported v1.compiler.emit_core_support -- a SEED module.
compile_dag_diagnostic_census resolves against the witness roots
[dag, src/v2], so that import could never resolve, no to_string candidate
existed, and the assertion read false for a reason with nothing to do with
the boundary. Third fixture broken this way, and this one was inside the
control built to prevent a different mistake.

Rebuilt on v2.std.spine int_max, whose own closure reaches
std.realization_width, which declares int_max too -- so ONE listed import
puts both declarations in the closure at once. No builtin is involved,
because int_max has no registry row, which is what makes the row ISOLATE
its claim rather than restate the RED: the declared population survives a
listed import and still undergoes exact cardinality admission.

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

The 29 seed collisions these repaired now answer DeclarationPrimitiveUndisposed,
so the builtin is not admitted as a rival and the refusals do not fire. The
edits' stated reason no longer exists; leaving them would be residue whose
justification a later reader could not reconstruct.

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

The projection query's Undisposed arm is defined over five surfaces, one of
which (InterpreterDispatch) is the only one whose authority reaches v2.*.
A consumer restricted to the other four answers identically today, but only
as an occupancy fact. This checks the equality rather than assuming it, and
goes red naming the first interpreter-only symbol anyone adds.

Rung: mechanically preventable, not structural -- the two censuses remain
capable of diverging and this check is what catches it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Measured 31 interpreter-only symbols, so the four-surface substitution the
guard was written to license does not hold and there is nothing to guard.
Keeping it would either assert something false or be re-pointed at the
current count, which is a tree-copied census pin rather than an oracle.

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

gunbai-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Why this is red, and why it stays red while parked

cited-symbol refuses three sites with ambiguous reference 'length': 2 candidates: v2.std.algebra.length, <builtin:length>.

That is this PR's wall firing from a stale mirror, not a defect in the wall as specified. Measured on the committed src/v1/stage0/src/v1_compiler_infer_lookup.rs:

  • the candidate-collection wall is installed (9 references to the ambiguity path), but
  • authored_import_names — the 1a narrowing — appears in no mirror, and
  • primitive_projection_for_declaration — the join consultation — appears in the lookup mirror not at all.

So CI is executing the pre-1a, pre-join wall: every declared homonym rivals its builtin unconditionally. v2.std.algebra.length is a ModeledProjection in primitive_projection_roster(), so with the join wired the builtin is not admitted as a rival and these three sites resolve. This is precisely the corpus-wide false-refusal storm the join was built to prevent, observed on the population that still runs without it.

Closing it means regenerating and installing the mirrors. That pulls std.primitive_identity into the seed closure, and regen then refuses unresolved import: module 'v2.std.qualified_name' not found (imported by 'gunbc.v1_interpreter_primitive_surface') — the closure constraint recorded above. The red and the parked architectural question are the same fact, so it is not independently fixable here and I am not routing around it.

Status: parked pending an operator ruling on whether the seed's module index may reach src/v2. The branch is deliberately left with the wall consulting the join — the state that is correct if the answer is yes, and mechanical to revert if it is no.

It is a consumer-side policy choice, not taxonomy work, and an unexplained
arm reads as an oversight. Refusing on an unclassified symbol would refuse
196 symbols corpus-wide: a wall whose strictness rises with how much census
work remains undone is not strict, it is broken.

Also records why the answer has three arms rather than a boolean -- the
argument is about where the decision lives, not what the states mean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
…lared, not implied

Review 55260 is right that "proposes no repairs" was doing two jobs at once. Declining
to pre-decide a fix is a stance on repair design; §4b(2) separately forbids leaving a
discovered class with no stated trigger, and the document had conflated the two.

Each of the six now carries a disposition and a trigger. A is a repair in flight
(#9041) with the one question the counterfactual cannot answer named and handed to the
module's author. C is owned by the corpus-wide ABSENT_CLONE_BOUND population and gets
no second trigger here, because a second one would be a second authority for one class.
D's lane is #8952. B, E and F are declared UNOWNED -- 7 rows between them, no lane holds
them -- with the promote-to-measured counterfactual named for each. Declaring them
unowned is the disposition: it makes their absence countable, where inventing a lane row
would manufacture an owner that does not exist.

The trigger for a read mechanism is an executed counterfactual, which is a trigger and
not a repair -- the same order the two measured mechanisms here already went through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Aug 24, 2026
… each with its evidence status (#9068)

* Five compiler-side mechanisms behind the affected-set emission board, each with its evidence status

Partitions the 24 rows landing in v2_lens_application.rs, std_change.rs and
v2_lens_affected_set.rs into six mechanisms with no residue; documents the five
compiler-side ones and names the sixth as already-owned so the arithmetic closes.
Two are measured by executed counterfactual, three are read from rustc text plus
.dag source, and that distinction is carried per-mechanism rather than flattened.

Proposes no repairs, deliberately.

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

* Pin the reproduction and script the blocker-lift; drop the positional citations

Review 55234, two findings, both real.

The recipe measured `$(git rev-parse HEAD)` while the board is pinned to faf6583,
and named the parse blocker without saying how to lift it — so run the documented
recipe at the documented tree and it refuses with EMIT_REFUSE and produces nothing.
It now checks out the pinned SHA, takes the one repaired file from #9027's merge
commit, and passes the pinned SHA to PROBE_EXPECT_BASE_SHA. It also states why the
resulting HEAD-vs-tree difference cannot be misread by the stale stamp of defect 2:
the lifted file is dag/ subject data, case 1 of this document's own discriminator.

Six positional `.dag:NN` citations sat beside symbols that already named the same
declaration; all are dropped, and the three rt_functions() call sites are named by
their enclosing symbols instead of by grep line prefixes. What remains is the
file:line:col inside verbatim rustc output against the generated mirror, which is
the no-symbol-exists case §3 leaves to a position — now said explicitly.

Also: the count above the defect list said two while listing three.

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

* Give every mechanism a disposition and a next trigger; unowned is declared, not implied

Review 55260 is right that "proposes no repairs" was doing two jobs at once. Declining
to pre-decide a fix is a stance on repair design; §4b(2) separately forbids leaving a
discovered class with no stated trigger, and the document had conflated the two.

Each of the six now carries a disposition and a trigger. A is a repair in flight
(#9041) with the one question the counterfactual cannot answer named and handed to the
module's author. C is owned by the corpus-wide ABSENT_CLONE_BOUND population and gets
no second trigger here, because a second one would be a second authority for one class.
D's lane is #8952. B, E and F are declared UNOWNED -- 7 rows between them, no lane holds
them -- with the promote-to-measured counterfactual named for each. Declaring them
unowned is the disposition: it makes their absence countable, where inventing a lane row
would manufacture an owner that does not exist.

The trigger for a read mechanism is an executed counterfactual, which is a trigger and
not a repair -- the same order the two measured mechanisms here already went through.

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

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
…des of resolution

The disposition row landed one commit ago cited #8952 as D's open lane. Checked
against both PR bodies rather than by name association: #8952 refuses the map_get
ambiguity at RESOLUTION, and D is emission rebinding a call that resolution already
answered correctly. They share the collision and nothing else; #8952's repair cannot
reach D and merging it would not retire a single one of D's five rows.

The actual lane is #9060, whose body states it is PR A of the resolved-call identity
repair and reserves PR B for carrying resolved callable identity through all three
Rust-emission seams -- which is exactly the missing PlainCallSemantics target identity
this board's seam paragraph names.

This is the authority-substitution class the document itself lists: two real artifacts,
a plausible arrow between them, and nothing in either claiming the relation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
…des of resolution

The disposition row landed one commit ago cited #8952 as D's open lane. Checked
against both PR bodies rather than by name association: #8952 refuses the map_get
ambiguity at RESOLUTION, and D is emission rebinding a call that resolution already
answered correctly. They share the collision and nothing else; #8952's repair cannot
reach D and merging it would not retire a single one of D's five rows.

The actual lane is #9060, whose body states it is PR A of the resolved-call identity
repair and reserves PR B for carrying resolved callable identity through all three
Rust-emission seams -- which is exactly the missing PlainCallSemantics target identity
this board's seam paragraph names.

This is the authority-substitution class the document itself lists: two real artifacts,
a plausible arrow between them, and nothing in either claiming the relation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Aug 24, 2026
…des of resolution (#9082)

The disposition row landed one commit ago cited #8952 as D's open lane. Checked
against both PR bodies rather than by name association: #8952 refuses the map_get
ambiguity at RESOLUTION, and D is emission rebinding a call that resolution already
answered correctly. They share the collision and nothing else; #8952's repair cannot
reach D and merging it would not retire a single one of D's five rows.

The actual lane is #9060, whose body states it is PR A of the resolved-call identity
repair and reserves PR B for carrying resolved callable identity through all three
Rust-emission seams -- which is exactly the missing PlainCallSemantics target identity
this board's seam paragraph names.

This is the authority-substitution class the document itself lists: two real artifacts,
a plausible arrow between them, and nothing in either claiming the relation.

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Aug 24, 2026
… transitive, measured (#9084)

* WIP measure: derive + 'static from callable-value-wrap captures, not the return connective

* Filter the bound's capture set twice, and stop claiming the return arm is capture-derived

Two precise findings from the side thread, both correct against the branch source.

1. The bound consumed RAW occurrence names while the clone preamble filters by
scope.body_locals, so the two were related-but-different computations. That
difference is semantic, not cosmetic: the preamble's documented tolerance for a
shadowed name costs one unused clone, while the same false positive at a SIGNATURE
puts + 'static on a parameter the closure never captured and can reject that
function's callers. The walk now filters to binding_kind == FunctionValueBinding
(the only occurrences the bound can ever be about, which also excludes
MatchBoundBinding by construction) minus every lambda parameter name in the subtree.
Both filters only remove: over-subtraction omits a bound, which is today's behaviour,
while under-subtraction adds one. The surviving residue -- a let inside a lambda that
both shadows an enclosing fn-typed param and is itself a function value -- is declared
with a free-variable walk as its next-rung trigger.

2. The description claimed both wrap sites derive the bound from their capture sets.
Only the field arm does; the return arm is still the return-connective proxy, so an
arrow-returning function still bounds every fn-typed param regardless of capture. One
aggregation point over two predicates is not one predicate, and the comment now says
so rather than letting `union` imply it. Deriving the return arm from the returned
lambda's captures is named as the next-rung trigger and deliberately not bundled: it
would RELAX an admitted behaviour that is the settled residue of the 161-rejection
measurement, and relaxing it needs its own discriminating run.

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

* The FunctionValueBinding filter erased the repair; subtract shadows instead of selecting kinds

Measured on the full chain (regen -> install candidate -> rebuild -> re-emit), which is
the only reason this was caught before review: the pre-filter walk emitted
  key_eq: impl Fn(K, K) -> bool + Clone + 'static
and the filtered walk emitted the same signature with no bound at all, 'static back to
zero occurrences in the module. Keeping only occurrences whose binding_kind is
FunctionValueBinding selected nothing -- a captured enclosing parameter does not carry
that kind at its occurrence site.

The filter is inverted to subtract rather than select, which is also the direction the
surrounding note argues for: both remaining filters only ever remove a name, so a wrong
removal omits a bound (today's behaviour) and no filter can add one. Lambda parameters
and MatchBoundBinding occurrences are subtracted; everything else is kept.

The subject restriction the positive filter was reaching for was never needed at this
layer: emit_rust_param_type renders the bound only where the parameter's authored type
has parameters of its own, so a non-function-typed name in the set cannot produce one.

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

* Revert the local repair: the measurement it produced is the deliverable, the code is not

The implementation relocated obligations rather than retiring them, so it does not
belong in the emitter. Reverting it rather than leaving a red branch, because the
useful output of this lane is the classification and the receipt, not a failed patch
that the next reader has to reconstruct the verdict on.

Reverting also removes the regen drift: no .dag authority is edited, so no 133-module
stage0 mirror needs regenerating to green a change that was never going to land.

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

* B was the first symptom, not the mechanism: supersede it with the measured, transitive form

The board recorded B as "fn-typed params captured into an Rc closure demanding
'static", which names the symptom the emitter happens to hit first. Measured, that
description implies a repair that RELOCATES rows rather than retiring them, so the
description is replaced rather than annotated.

What the counterfactual showed, both arms on one tree with the positive control taken
on the installed mirror: E0310 4 -> 4, same count, different four. Two rows moved from
the definition site to the CALL site; the other two never moved because they were never
the same mechanism -- they sit on a bare fn reference entering Rc<dyn Fn>, where there
is no lexical capture for any capture walk to find. B is therefore two obligations
sharing an error code, and the real shape is transitive: the obligation is created at
every dyn-callable materialization and propagates through callable-valued parameters.

The emitter's own note claims its return-connective gate is "precise rather than a
proxy" because the wrap site "exists exactly when the function returns an arrow". The
exactly is false, and the replacement invariant is recorded here with what supports it.

Deriving it is a call-graph fixpoint -- a lifetime-propagation engine. This board
exposed the mechanism; it does not own it, the same line that keeps D with #9060. B's
disposition says so instead of carrying a trigger nobody can act on.

Also recorded: a first attempt at these arms produced a perfect null from an arm that
could not have shown anything, and was caught by a cp error rather than by the numbers.

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

* D's lane is #9060 PR B, not #8952 -- same name collision, opposite sides of resolution

The disposition row landed one commit ago cited #8952 as D's open lane. Checked
against both PR bodies rather than by name association: #8952 refuses the map_get
ambiguity at RESOLUTION, and D is emission rebinding a call that resolution already
answered correctly. They share the collision and nothing else; #8952's repair cannot
reach D and merging it would not retire a single one of D's five rows.

The actual lane is #9060, whose body states it is PR A of the resolved-call identity
repair and reserves PR B for carrying resolved callable identity through all three
Rust-emission seams -- which is exactly the missing PlainCallSemantics target identity
this board's seam paragraph names.

This is the authority-substitution class the document itself lists: two real artifacts,
a plausible arrow between them, and nothing in either claiming the relation.

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

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brian Searls and others added 5 commits August 24, 2026 18:18
# Conflicts:
#	src/v1/stage0/src/cli_run.rs
…lIndex's new field

Inherited, not introduced by the merge: on origin/main SymbolIndex declares five
fields including type_head_exposures, and the hand-written peel-fixpoint probe in
cli_run.rs initializes four, so cargo check --all-targets and the documented local
cargo test --workspace both fail there. Neither the struct nor that probe is touched
by this branch.

It is the eleventh-consumer class again: the field was added to the emitted mirror,
and the hand-written seed periphery that constructs the same struct is a population
no .dag census reaches. The Rust suite left CI on 2026-07-11, so nothing observed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng to the JSON serializer

data_value_has_cross_refs answers one question -- does this initializer reference
another declaration -- and ExprVar was already true for exactly the right reason.
ExprCall is that same reference with an argument list, and it fell through `_` to
false, routing `data d: R = mk(..)` to a serializer that can only render literals.
The serializer then refused with "unsupported mock expression", so the failure
surfaced one layer below the judgment that caused it.

TOTAL AT THE LEVEL EXAMINED, BLIND ONE LEVEL DOWN: exhaustive over expr_data, no
arm missing, nothing a reviewer or a wildcard-free lens could catch -- the `_`
carried the payload. Wrong by the function's own definition, not incomplete.

MEASURED BEFORE EDITING, with two instruments that disagreed:
  line regex        1259 rows  (WRONG -- blind to multi-line and to nested calls)
  multi-line census 3181 rows across 971 files, corpus-wide
  emitted mirrors      9 rows refused, all in std_primitive_projection.rs
The third decides: only the seed closure is lowered to Rust, and the JSON branch
is guarded by record-shaped-type AND no-cross-refs. Every row on that path today
emitted successfully, which by construction means it holds no call. So nothing
that works today can move.

VERIFIED AFTER: JSON-path rows 34 before and 34 after -- zero existing rows
changed route. The emitter mirror moved by exactly one line. required-regen
reports first_generation_equal=true.

Authorized as a scoped one-arm repair; the surrounding emitter is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fourth instance of this class on this lane, and the reason I missed it is the
finding: my local sweep checked src/v1/*.dag, because that is where the previous
three appeared. The required-ci parse phase covers dag/ as well, and required-regen
-- the only phase I can run locally in a tight loop -- does not run that parse at
all. So the local verification loop cannot see this class in dag/, and I scoped
the sweep to where the error had last appeared rather than to where the rule applies.

Swept every .dag this branch touches, not just the file CI named: all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 25, 2026 01:31
@briansrls
briansrls merged commit ffb0170 into main Aug 25, 2026
2 checks passed
@briansrls
briansrls deleted the session/tidy-pike-614 branch August 25, 2026 03:16
gunbai-bot Bot pushed a commit that referenced this pull request Aug 25, 2026
witnesses.yml builds 2 of the 16 bin targets v1-compiler declares, so 14 are absent from the
build selection and nothing required compiles them. Combined with the Rust suite removed from CI
2026-07-11, clippy 2026-07-08, and the compile-clean gate deleted in the floor cut, a bin can go
stale silently -- and one had: #8952 added authored_import_names to TypeEnv without updating
infer_semantics_witness, leaving main red at 'cargo check -p v1-compiler'. That specimen is being
repaired separately in #9205; this is the standing that stops the next one.

Two populations were answered by one roster:
  RuntimeArtifactPopulation -- the executables this job RUNS (claim_executor, gunbc)
  BinaryCompilePopulation   -- every bin target the manifest DECLARES
witness_floor_required_bins is correct as the first and is left alone.

Derived, not a second roster: the step calls repo_self_build_command(bins: []), whose no-selector
form gunbc.repo_self_build already documents as cargo's meaning for 'build every target'. A new
[[bin]] joins the standing with no edit anywhere, and no new argv word is minted.

Build rather than check, decided by measurement: from a cold target dir with the two-bin build
already paid, building every target cost 12s against 52s for 'cargo check --release --bins', and
it additionally links.

Placed LAST, after the fold and the roster uploads, guarded by !cancelled(): ahead of the fold it
would be a preparation mask, and an auxiliary compile error would take the floor's ledger with it.
briansrls pushed a commit that referenced this pull request Aug 25, 2026
… stale TypeEnv initializers blocking the local suite

The test asserted nothing until it built. Two separate obstacles, both found by
running it rather than reading it:

compile_sources takes an im::Vector, so the fixture's Vec needed .into().

infer_semantics_witness.rs constructs TypeEnv literally at six sites and has
been missing authored_import_names since that field landed in #8952, so ANY
cargo test in this crate failed to build -- cargo builds bins for a --test
target too. Pre-existing on main and invisible there because the Rust suite is
a local check, removed from CI 2026-07-11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 25, 2026
…census

A workflow_dispatch job with ZERO runs in its entire existence, its .dag emitter
(286 lines), its GeneratedArtifact registration, the claim_executor mode, and the
266-line cli_run census that had no other consumer.

WHY THE WHOLE CHAIN AND NOT JUST THE FLAG. The mode was the census's only caller
and the workflow was the mode's only caller, so deleting any one link would have
left the other two as an authority for a fact nothing asks. The fail-closed
census did the finding: removing the GeneratedArtifact variant surfaced five more
sites (the roster list, the commit-policy arm, the equality arm, the emit import
and the yml-parse arm) that a name-grep of the workflow path alone would have
missed.

THIS IS A DECLARED CAPABILITY DROP, not a dead-code sweep, and saying so is the
point of the entry. WHAT IS GONE: the only route to a located corpus-wide
type-judgment population -- the blocking/advisory/unclassified partition over the
required run's own subject, with its planted control. DESIGN 4b names exactly
this gap in the other direction: the advisory residue is computed on every
required run and counted by nothing, and a frontier whose deficit frequency is
unobservable never ranks for climbing. That argument is why the mode was built.

WHY IT GOES ANYWAY: it was never executed once. An instrument nobody has ever
run is specification-without-execution, not coverage, and a workflow_dispatch job
nobody dispatches cannot be the thing that makes a frequency observable. Keeping
the flag while deleting the workflow would be worse -- a surviving mode no
workflow invokes guards nothing and would be cited as though it did.

POPULATION: one capability, the corpus type-judgment measurement. PREVIOUS
STATE: reachable by manual dispatch, never reached. TEMPORARY STATE: no route.
RESTORATION TRIGGER: the measurement returns as a QUERY over the build/test
target graph -- the population is a property of what the required run compiled,
which is exactly what a target-graph query answers -- rather than as a mode on a
binary this program is deleting. It does not return as a flag.

NOT CLAIMED: this does not repair src/v1/stage0/src/bin/infer_semantics_witness.rs,
which #8952 (ffb0170) broke by adding TypeEnv.authored_import_names without
updating six initializers there. That binary is in fleet-converge.yml's build
list and does not compile on main today; it is untouched here and is not this
cut's to fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 25, 2026
… cli_run tests review 55847 caught

Review 55847 found four tests in cli_run.rs still referencing corpus_judgment_*
symbols the census deletion removed. It was right, and it had found one end of a
larger population: 542 test functions across the two files tested machinery this
branch deleted. claim_executor 11,380 -> 6,270.

WHY THE REVIEWER SAW FOUR AND NOT 546. Two masks, and the second is the one worth
recording. My own verification ran `cargo check --bin claim_executor` WITHOUT
`--tests`, so a test module referencing a deleted symbol produced no diagnostic
at all -- the deletion was verified against a target that does not compile tests.
Then, when I did run --tests, the seed's lib failed FIRST on main's unrelated
TypeEnv breakage, and 526 downstream errors were masked behind that one. A masked
run and a clean run rendered identically: 526 errors and 1 error look like
progress rather than a different question being answered. DESIGN's
execution-provenance row names exactly this, and it cost two wasted sweeps here.

DELETED SURGICALLY, NOT WHOLESALE, and the distinction is load-bearing. The
obvious cut was `mod tests` entire -- 5,514 lines, 518 of the 526 errors. It
would have been wrong: `verify_build_artifacts_reds_on_zero_byte` lives in that
module and is the discriminating RED for a mode fleet-converge.yml invokes twice.
So the cut deletes only test functions that FAIL TO COMPILE because their subject
is gone, iterated to a fixed point against the compiler. 38 tests survive,
including all four verify_build_artifacts controls (accepts / zero-byte / missing
/ empty-arglist) and the five attempt_identity refusals.

That is the enumerate-before-deleting rule applied to a test module: a module is
deleted for one reason and takes everything in it unless its contents are
enumerated first. The compiler did the enumeration.

WHAT IS NOT CLAIMED. src/v1/stage0/src/bin/infer_semantics_witness.rs is still
broken by #8952 (six TypeEnv initializers) and is untouched here; it is in
fleet-converge.yml's build list and does not compile on main. The one-line lib
fix at cli_run.rs is present because without it nothing on this branch can
compile tests at all -- deep-ram-742 ships the same repair in #9222 and the
duplicate is deliberate, not a fork: identical text, and whichever lands second
merges clean or drops out.

.gitattributes loses its corpus-type-judgment merge row, which review 55847 also
flagged. That row is DERIVED from the artifact roster this branch already
trimmed, so the committed file was stale against its own authority rather than
carrying an independent fact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Aug 25, 2026
…66 -> 11,508) (#9228)

* Delete the plan/walk surface nothing could reach, and the 27 cli_run symbols it was importing to do it

claim_executor: 21,366 -> 11,508 lines. Zero dead-code warnings, zero errors,
fmt clean, and the two live modes verified by execution.

WHY THIS IS A REACHABILITY CUT AND NOT AN OCCUPANCY ONE. run() required
--plan-entry after every --required-* arm returned, and nothing supplies
--plan-entry: not a workflow, not a hook, not an emitted yml. The plan function
it defaulted to named src/v2/workflow/ci_floor_plan.dag, which the 2026-08-15
floor cut deleted. So the plan walk, the batch executor, the coordinator/worker
protocol, the scoped-request machinery, the perturb re-walk, the falsifier
failure-class helpers and their terminal reporting were not quiet guards that
happened to be empty -- their governing MECHANISM was removed, and no input any
caller can author reaches them. DESIGN's reachability-read-as-occupancy row asks
three questions; this population answers no to the first two, not merely to the
third.

The coordinator is the sharpest case: maybe_run_floor_coordinator is the first
thing main() does, and it returns None immediately unless --plan-function names
a plan entry that does not exist. It spawned this binary as its own child with
--floor-worker-role/--scoped-batch-id, from an arm that never armed.

WHAT THE CENSUS SURFACED, which is the point of cutting at the root rather than
the leaves. Removing the walk left 251 items unreferenced; deleting those left
27 cli_run imports unused -- active_workset_admit, the heartbeat feed, the
discovery roster snapshot, the histogram/percentile projections,
install_floor_compile_clean_receipt and the rest. That is a measurement about
cli_run.rs, not about this file: a quarter of the seam between the two existed
only to feed machinery with no caller.

WHAT REMAINS AND IS PROVEN BY EXECUTION (release binary, four cases):
  --required-ci                          the one mode witnesses.yml invokes
  --verify-build-artifacts               fleet-converge.yml, both jobs
  no mode                     -> exit 2, typed refusal naming the live modes
  --plan-entry                -> exit 2, unknown argument
  --verify-build-artifacts on a present binary   -> exit 0
  --verify-build-artifacts on an absent one      -> exit 1, fail-closed
The last pair is the discriminating red: the mode's whole purpose is refusing a
'successful' build that produced a missing or zero-byte artifact, so a green
without its red would establish nothing.

The no-mode arm REFUSES rather than falling through to a default. An argv this
binary no longer understands must stop the line; a silent success would be the
absorbing fallback one level up from the machinery just deleted.

WHAT THIS DOES NOT CLAIM. The .dag residue is NOT repaired here and is named
rather than left to be rediscovered: gunbc.cli_invoke still builds
--plan-entry/--plan-function/--notice-title argv (dead transport -- no workflow
contains those words), PlanFunction survives in ci_spec/cli_services with its
witness, and src/v2/test/fixture/walk_plan_stage/ is a fixture family whose only
execution route was the recipes this commit deletes. That family has eight
external touchpoints including a Rust integration test, so it is its own census
and its own cut, not a tail this one can sweep. Nothing in CI executed any of it
before this commit or after it.

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

* Delete the five empty impl stubs the method deletion left behind

Review on #9228 named two (FloorBatchClampAuthority, ResolvedFloorBatchClamp) as
the ones it spot-checked. There were five: ParsedRunnableProfile,
ProcessTermination and ScopedExecutionRequest carry the same shape. Swept by
pattern rather than by the two cited, because a cosmetic residue found by
inspection is a population, not a list -- fixing only the named two would leave
three identical stubs behind and read as though the class had been handled.

Each is the shell of an impl whose every method was deleted as unreachable. The
types themselves are still constructed and are unaffected.

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

* Delete corpus-type-judgment at the root: mode, workflow, emitter and census

A workflow_dispatch job with ZERO runs in its entire existence, its .dag emitter
(286 lines), its GeneratedArtifact registration, the claim_executor mode, and the
266-line cli_run census that had no other consumer.

WHY THE WHOLE CHAIN AND NOT JUST THE FLAG. The mode was the census's only caller
and the workflow was the mode's only caller, so deleting any one link would have
left the other two as an authority for a fact nothing asks. The fail-closed
census did the finding: removing the GeneratedArtifact variant surfaced five more
sites (the roster list, the commit-policy arm, the equality arm, the emit import
and the yml-parse arm) that a name-grep of the workflow path alone would have
missed.

THIS IS A DECLARED CAPABILITY DROP, not a dead-code sweep, and saying so is the
point of the entry. WHAT IS GONE: the only route to a located corpus-wide
type-judgment population -- the blocking/advisory/unclassified partition over the
required run's own subject, with its planted control. DESIGN 4b names exactly
this gap in the other direction: the advisory residue is computed on every
required run and counted by nothing, and a frontier whose deficit frequency is
unobservable never ranks for climbing. That argument is why the mode was built.

WHY IT GOES ANYWAY: it was never executed once. An instrument nobody has ever
run is specification-without-execution, not coverage, and a workflow_dispatch job
nobody dispatches cannot be the thing that makes a frequency observable. Keeping
the flag while deleting the workflow would be worse -- a surviving mode no
workflow invokes guards nothing and would be cited as though it did.

POPULATION: one capability, the corpus type-judgment measurement. PREVIOUS
STATE: reachable by manual dispatch, never reached. TEMPORARY STATE: no route.
RESTORATION TRIGGER: the measurement returns as a QUERY over the build/test
target graph -- the population is a property of what the required run compiled,
which is exactly what a target-graph query answers -- rather than as a mode on a
binary this program is deleting. It does not return as a flag.

NOT CLAIMED: this does not repair src/v1/stage0/src/bin/infer_semantics_witness.rs,
which #8952 (ffb0170) broke by adding TypeEnv.authored_import_names without
updating six initializers there. That binary is in fleet-converge.yml's build
list and does not compile on main today; it is untouched here and is not this
cut's to fix.

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

* Delete the 542 tests whose subjects this branch deleted, and the four cli_run tests review 55847 caught

Review 55847 found four tests in cli_run.rs still referencing corpus_judgment_*
symbols the census deletion removed. It was right, and it had found one end of a
larger population: 542 test functions across the two files tested machinery this
branch deleted. claim_executor 11,380 -> 6,270.

WHY THE REVIEWER SAW FOUR AND NOT 546. Two masks, and the second is the one worth
recording. My own verification ran `cargo check --bin claim_executor` WITHOUT
`--tests`, so a test module referencing a deleted symbol produced no diagnostic
at all -- the deletion was verified against a target that does not compile tests.
Then, when I did run --tests, the seed's lib failed FIRST on main's unrelated
TypeEnv breakage, and 526 downstream errors were masked behind that one. A masked
run and a clean run rendered identically: 526 errors and 1 error look like
progress rather than a different question being answered. DESIGN's
execution-provenance row names exactly this, and it cost two wasted sweeps here.

DELETED SURGICALLY, NOT WHOLESALE, and the distinction is load-bearing. The
obvious cut was `mod tests` entire -- 5,514 lines, 518 of the 526 errors. It
would have been wrong: `verify_build_artifacts_reds_on_zero_byte` lives in that
module and is the discriminating RED for a mode fleet-converge.yml invokes twice.
So the cut deletes only test functions that FAIL TO COMPILE because their subject
is gone, iterated to a fixed point against the compiler. 38 tests survive,
including all four verify_build_artifacts controls (accepts / zero-byte / missing
/ empty-arglist) and the five attempt_identity refusals.

That is the enumerate-before-deleting rule applied to a test module: a module is
deleted for one reason and takes everything in it unless its contents are
enumerated first. The compiler did the enumeration.

WHAT IS NOT CLAIMED. src/v1/stage0/src/bin/infer_semantics_witness.rs is still
broken by #8952 (six TypeEnv initializers) and is untouched here; it is in
fleet-converge.yml's build list and does not compile on main. The one-line lib
fix at cli_run.rs is present because without it nothing on this branch can
compile tests at all -- deep-ram-742 ships the same repair in #9222 and the
duplicate is deliberate, not a fork: identical text, and whichever lands second
merges clean or drops out.

.gitattributes loses its corpus-type-judgment merge row, which review 55847 also
flagged. That row is DERIVED from the artifact roster this branch already
trimmed, so the committed file was stale against its own authority rather than
carrying an independent fact.

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

* Delete the second argv reader: the coordinator was reachable the whole time

Review 55875 is correct and the finding is the important kind -- not a leftover,
a REACHABLE MUTATING PATH behind a deletion I had claimed was complete.

WHAT I GOT WRONG. main() read std::env::args() ITSELF and dispatched
maybe_run_floor_coordinator before run() parsed anything. So deleting
--plan-function from run()'s parser did nothing to the coordinator's
reachability. My earlier claim that the coordinator "returns None immediately
unless --plan-function names a plan entry that does not exist" described the
guard correctly and the DISPATCH not at all: the guard tests argv, and argv still
carried the flag.

WHY IT WAS WORSE THAN BEFORE THIS BRANCH, which is what makes it a defect rather
than an incomplete cut. With --plan-function deleted from one parser and live in
the other, the flag answered `unknown argument` for every value EXCEPT
gunbc_ci_floor_plan -- the one value that ran the entire coordinator. And that
path is not inert: it create_dir_all's a receipt directory, remove_file's the
worker-observation receipt, the scoped-execution requests and the phase journal,
arms a scoped receipt and spawns workers, all before any refusal could fire. A
deletion that leaves the single most destructive entry point as the only reachable
one is the opposite of fail-closed.

THE LESSON IS THE CUT'S, NOT THE COORDINATOR'S: a flag is not deleted when one of
two parsers stops reading it. The repair is at the root -- main() no longer reads
argv at all, run() is the only thing that does -- and the census then took the
worker/scoped-request machinery with it: 6,275 -> 5,069 lines, 57 further test
functions whose subjects went, iterated to a joint fixed point over errors and
dead code.

PROVEN BY EXECUTION, on the exact invocation the review named:
  claim_executor --plan-function gunbc_ci_floor_plan --source-root dag
    -> "unknown argument: --plan-function", EXIT=2, and no receipt file created
  --verify-build-artifacts on a present binary -> 0
  --verify-build-artifacts on an absent one    -> 1
The negative control matters here specifically: the bug was that a mutating path
ran before the refusal, so "it refuses" is only half the claim -- the other half
is that nothing was written on the way to refusing.

claim_executor is now 5,069 lines against 21,366 on main.

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

* Drop the four imports the coordinator cut orphaned; CI builds with -D warnings and my check did not

CI red at bda1331, all three jobs, one root cause: ExitStatus, std::time::Instant,
build_floor_discovery_request and verify_floor_discovery_terminal_for_coordinator
lost their last users when the coordinator and its worker machinery went, and the
required build compiles with -D warnings, so an unused import is an ERROR there.
`floor` failed identically; `witnesses` is only the gate that reports both.

THIS IS THE THIRD TIME ON THIS BRANCH THAT A CHECK WAS GREEN AND THE CLAIM WAS
WRONG, and it is the same defect each time: verifying a deletion against a target
that cannot observe its dependents. --bin without --tests could not see 526
orphaned test references. A broken lib masked those behind one error. And a bare
cargo check cannot see an unused import, because unused-imports is a WARNING
until -D warnings makes it fatal -- so the instrument I was steering by was
strictly weaker than the one that gates merge.

The repair is to use the gating instrument, and to PROVE it is the one running
rather than assume the flag arrived. Planted control, executed: with
RUSTFLAGS="-D warnings" forwarded (ctrl-build prints `forwarding env: RUSTFLAGS`),
an added `use std::collections::BTreeSet;` produces `error: unused import`, not a
warning. The clean result on the real tree is therefore load-bearing rather than
a flag that silently never reached rustc.

No behavior change: four import names, zero call sites.

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

---------

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Aug 26, 2026
…9196)

witnesses.yml builds 2 of the 16 bin targets v1-compiler declares, so 14 are absent from the
build selection and nothing required compiles them. Combined with the Rust suite removed from CI
2026-07-11, clippy 2026-07-08, and the compile-clean gate deleted in the floor cut, a bin can go
stale silently -- and one had: #8952 added authored_import_names to TypeEnv without updating
infer_semantics_witness, leaving main red at 'cargo check -p v1-compiler'. That specimen is being
repaired separately in #9205; this is the standing that stops the next one.

Two populations were answered by one roster:
  RuntimeArtifactPopulation -- the executables this job RUNS (claim_executor, gunbc)
  BinaryCompilePopulation   -- every bin target the manifest DECLARES
witness_floor_required_bins is correct as the first and is left alone.

Derived, not a second roster: the step calls repo_self_build_command(bins: []), whose no-selector
form gunbc.repo_self_build already documents as cargo's meaning for 'build every target'. A new
[[bin]] joins the standing with no edit anywhere, and no new argv word is minted.

Build rather than check, decided by measurement: from a cold target dir with the two-bin build
already paid, building every target cost 12s against 52s for 'cargo check --release --bins', and
it additionally links.

Placed LAST, after the fold and the roster uploads, guarded by !cancelled(): ahead of the fold it
would be a preparation mask, and an auxiliary compile error would take the floor's ledger with it.

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
briansrls pushed a commit that referenced this pull request Aug 26, 2026
#9209)

* Resolve a qualified callee in pipe position as that callee, not as a method named by its root segment

parse_pipe_rhs consumed exactly one identifier after the pipe arrow, so
`xs |> a.b.f()` lowered as a method call named `a` on the receiver, with the
remaining segments folding on top as field access. The refusal an author saw
therefore named the root segment as a missing method on the receiver's type,
while the same callee in ordinary call position resolved and still does.

The parse now consumes the whole dotted path and hands the leading segments to
the qualifier spine that 04_infer's qualified_direct_call arm already reads, so
both positions reach one resolution path. A bare callee is unchanged.

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

* Give the piped receiver arg the same span convention as other positional args

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

* Decide a dotted pipe callee by its application, not by the shape of its path

An unconditional dotted-path consume re-denoted a live, correct site --
v1.compiler.emit_rust's `field_names |> first.value`, pipe into the kernel
method `first` then take `.value` off the Optional -- as a call into a module
named `first`. The parser cannot know that a leading path names a module, so
the pipe production decides syntactically: an APPLIED dotted callee is a
qualified callee applied to the receiver, an unapplied one stays
method-then-field. Regression case added to the test.

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

* Regenerate the stage0 parse mirror for the pipe-callee change

Emitted by claim_executor --required-regen from the current .dag; installed
as the candidate it produced. Before the install, regen refuses naming exactly
this file (first_generation_equal=false, 'generated surface drift:
v1_compiler_parse.rs'), which is what makes the install the closing move
rather than a hand edit.

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

* Make the evidence compile and execute: im::Vector conversion, and the stale TypeEnv initializers blocking the local suite

The test asserted nothing until it built. Two separate obstacles, both found by
running it rather than reading it:

compile_sources takes an im::Vector, so the fixture's Vec needed .into().

infer_semantics_witness.rs constructs TypeEnv literally at six sites and has
been missing authored_import_names since that field landed in #8952, so ANY
cargo test in this crate failed to build -- cargo builds bins for a --test
target too. Pre-existing on main and invisible there because the Rust suite is
a local check, removed from CI 2026-07-11.

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

* Name the six ExistingSeedItemModified declarations the receipt excludes

Review 55814: the receipt claimed a +7 delta confined to the new test file
while the change also touched six declarations in infer_semantics_witness.rs.
They are modifications, not additions -- each gained one already-required
field initializer inside an existing body -- so they stay out of
hand_authored_declarations per the disposition the sibling receipts use, and
the receipt now SAYS so instead of omitting them.

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

* Say four cases where the trigger said three: the regression case made it four

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

* Drop the duplicate authored_import_names the main merge text-merged in

Same repair as on the stacked branch, and the same cause: main added the field
after module_path, this branch added it after source_visible_names, and git's
text merge kept BOTH in six TypeEnv literals (E0062 x6). The witness lane
passed; the failing step was 'Every v1-compiler binary target compiles'.

Main's placement is kept at all six sites -- the values are identical, so the
choice minimizes divergence from main rather than changing semantics.

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

* Correct the hand-Rust census receipt: it described base-branch work as this change's

The row excluded six declarations in infer_semantics_witness.rs as
ExistingSeedItemModified. That was true when written and is now false: the file
has no diff in this change, and origin/main already carries all six
authored_import_names initializers.

Main repaired them independently. This branch's copy of the same repair was
dropped when the merge produced the field TWICE per literal (E0062 at six
sites), and keeping main's placement removed this change's delta in that file
entirely -- so the exclusion paragraph was left describing work the base branch
owns as part of this change's census. That inflates what the change is
answerable for, which is the one direction a census receipt must not be wrong in.

The removal is recorded in the row rather than made silently, and the live fact
the deleted paragraph carried is kept: the enumerated evidence could not execute
while those initializers were missing, and the repair that made it executable is
main's, not this change's.

Found by review 56100.

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

---------

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 28, 2026
…n census, tapping nothing

The exact consumer relation is what makes a replacement cut's population exact
and what decides whether residue goes loud or silent when X is deleted. The
ruled construction is to observe decisions the compiler ALREADY makes, keyed by
the parser-minted occurrence identity -- reconstructing resolution from outside
would answer with what a reimplementation believes rather than what the compiler
selected. But a tap cannot be placed where the information is already gone, and
which sites have lost it is not knowable from a type name. So the first output
is not the relation: it is where exact occurrence identity and exact target
identity COEXIST.

THE PRICING RESULT: zero of five callable-resolution sites are tappable, because
not one receives the occurrence identity -- every one is keyed by a bare
name: String. Threading that seam is not a repair for stragglers, it is the whole
precondition, and 0B.1b cannot begin on this family until it lands.

The target axis discriminates, which is what makes this a measurement rather than
a decoration: four sites carry the exact selected declaration in the outcome, one
(borrowed_census_decl) carries the owning module while the declaration NAME does
not survive. Two remedies, not one -- an erased target is repaired by widening an
outcome, an absent occurrence by threading an input.

THE ROSTER WAS RE-DERIVED AGAINST CURRENT MAIN BEFORE LANDING, and that is the
more useful half. First read four days of main movement ago, it recorded all
three FuncSigLookup sites as computed-then-erased. Between the readings #8952 and
#9436 landed and FuncSigResolved gained its `declared` field. Publishing the first reading
would have named a defect that no longer existed and priced work already done, in
the file it was measuring. The revision field is not bookkeeping: these two files
moved 440 lines while the claim was being written.

Counts are DERIVED from the two axes, never stored, so a transcribed verdict
cannot disagree with the facts beside it. Four unsurveyed decision surfaces are
counted refusals rather than absent rows. No tap is placed, no relation produced.

13/13 green, including the four-corner control -- tappable is a conjunction at one
site, and a conjunction written as a disjunction passes every single-axis test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Aug 28, 2026
…n census, tapping nothing (#9600)

* Price the resolver tap before building it: the decision-site retention census, tapping nothing

The exact consumer relation is what makes a replacement cut's population exact
and what decides whether residue goes loud or silent when X is deleted. The
ruled construction is to observe decisions the compiler ALREADY makes, keyed by
the parser-minted occurrence identity -- reconstructing resolution from outside
would answer with what a reimplementation believes rather than what the compiler
selected. But a tap cannot be placed where the information is already gone, and
which sites have lost it is not knowable from a type name. So the first output
is not the relation: it is where exact occurrence identity and exact target
identity COEXIST.

THE PRICING RESULT: zero of five callable-resolution sites are tappable, because
not one receives the occurrence identity -- every one is keyed by a bare
name: String. Threading that seam is not a repair for stragglers, it is the whole
precondition, and 0B.1b cannot begin on this family until it lands.

The target axis discriminates, which is what makes this a measurement rather than
a decoration: four sites carry the exact selected declaration in the outcome, one
(borrowed_census_decl) carries the owning module while the declaration NAME does
not survive. Two remedies, not one -- an erased target is repaired by widening an
outcome, an absent occurrence by threading an input.

THE ROSTER WAS RE-DERIVED AGAINST CURRENT MAIN BEFORE LANDING, and that is the
more useful half. First read four days of main movement ago, it recorded all
three FuncSigLookup sites as computed-then-erased. Between the readings #8952 and
#9436 landed and FuncSigResolved gained its `declared` field. Publishing the first reading
would have named a defect that no longer existed and priced work already done, in
the file it was measuring. The revision field is not bookkeeping: these two files
moved 440 lines while the claim was being written.

Counts are DERIVED from the two axes, never stored, so a transcribed verdict
cannot disagree with the facts beside it. Four unsurveyed decision surfaces are
counted refusals rather than absent rows. No tap is placed, no relation produced.

13/13 green, including the four-corner control -- tappable is a conjunction at one
site, and a conjunction written as a disjunction passes every single-axis test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Split the remedy count in two: the census was collapsing the distinction it exists to make

The standing derived ONE not-tappable count, defined as answers-a-reference AND
NOT tappable. Because tappable is a conjunction over two independent axes, that
reported the same number for a site missing its occurrence identity and a site
whose target was erased -- and those need OPPOSITE repairs: one threads an INPUT
into the decision site, the other widens an OUTCOME to retain what the site
already selected.

This module's own note says exactly that, in as many words, and then the derived
count collapsed it. The census contradicted itself in the one place a reader
takes the number from rather than the prose, which is worse than a note that had
never made the distinction.

The witness had permanently RATIFIED the collapse: the four-corner control
asserted a single threading count of three, so the occurrence-exact/target-erased
corner was encoded as owing occurrence threading, a repair it does not need.

Now two overlapping derived counts. The axes are independent, so they do NOT
partition the population: a site deficient on both inhabits both, because it owes
both repairs, and forcing a partition would be the same collapse in a tidier
shape. The four corners read 1 tappable / 2 occurrence-threading / 2
target-retention, and the production roster reads 5 surveyed / 0 tappable / 5
occurrence-threading / 1 target-retention -- which is what the measurement always
found: threading is the family-wide prerequisite and borrowed_census_decl
additionally needs its outcome widened.

Found in side-chat review, not by the blocking review on the PR.

13/13 green by execution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant