Skip to content

Seed-growth receipt for the bare-reference scanner that shipped via #9090 - #9102

Merged
briansrls merged 15 commits into
mainfrom
fix/ambiguous-bare-reference-refuses
Aug 24, 2026
Merged

briansrls merged 15 commits into
mainfrom
fix/ambiguous-bare-reference-refuses

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

What this PR is NOW

The scanner code is already on main. #9090 merged this branch into session/witty-lark-109 (Merge commit eb2ad48df706…, carrying all five scanner commits by name) and squash-merged at 2026-08-24T18:01:36Z as fd55f00bc6.

So this PR no longer proposes the scanner. It is the seed-growth accounting for code that has already shipped.

contents:   2 .dag files
            dag/gunbc/bare_reference_scanner_admission.dag   (new authority)
            dag/gunbc/seed_growth_admission.dag              (roster row)
hand Rust:  0 lines — all 413 are on main

The two deltas, each with its base named

base delta question it answers
merge base bd84f6696 +9 — 1 production + 8 test what did this branch author?
current main fd55f00b 0 what does merging add to the seed?

The receipt enumerates the +9, because naming the declarations is what a receipt is for. It states separately that the population landed by another route.

Why the instruments disagree: squash carries content, not ancestry. git merge-base --is-ancestor says none of these commits are on main; git grep finds the function and all eight test fns there byte-identical. Both true, of different questions.

What this does to the pending disposition

Nothing in any .dag on main names module_self_declared_names. The seed today carries a 115-line hand-written source scanner with no seed-growth receipt anywhere.

The disposition is now a decision about accounting, not about admitting code.

The receipt deliberately does not self-admit (bare_reference_scanner_receipt_disposition dispositions the receipt, not the scanner) and states the scaffold presumption rather than arguing it away: the scanner re-derives parser-owned facts from raw text, and 115 lines rather than the ~603 that circulated changes how much debt is on the table, never whether the presumption applies.

Evidence

Control arm, same binary both sides:

control    unmodified eb2ad48   0 blocking / 292 advisory
treatment  with the receipt     0 blocking / 293 advisory

The tree was already at 0 blocking, so a treatment-only reading proves nothing about this change. The honest claim: the receipt adds no blocking error and introduces one advisory instance in a pre-existing accepted advisory class — the as RoadmapNodeId brand cast, the same class whole_corpus_compile_admission and roadmap_authority already carry.

Instrument boundary: this local check ran with the integration/namespace-cut compiler, not this PR's product. It proves the new .dag authority is admitted by that named instrument; PR CI is the acceptance result for the current head.

Census reconciliation

scripts/rust_item_census.py --diff reports 18 added items. 9 are real; 9 are fixture .dag source inside Rust string literals (VisibilityScope, FrameExtent, CostBasis, CostAccount, Result, trim_one, Dimension, Scale, g). A separate 12 counts fixture lines, larger because CostAccount and fn f are each authored twice.

The census over-reports by 100%, exactly as seed_growth_g0_census_host_scaffold_note predicts when it records that extraction is regex, outside the Rust grammar authority. That defect and the scanner share an architectural rule — language structure must come from the authority for that language — but they have two separate retirement triggers, and the Rust census needs its own owner.

Still open

The operator realization disposition. Not self-granted here, and no check or review will turn it green.

A finding this merge exposed, recorded here because prose in a thread does not survive us

dag/gunbc/seed_growth_admission.dag carries its own contents twice.

  • seed_growth_justification_roster_note — a String that enumerates the rows in prose ("Today: … , … and …")
  • seed_growth_justification_roster() — the same rows as code, ten lines below it

One fact, two representations, in one file, with nothing checking that they agree. That is why this PR's only merge conflict had three parts rather than one, and why both sides independently edited the prose: every author must update the human list and the machine list in lockstep, by hand.

Measured, not assumed: on current main the note names four and the function returns four — they agree today. deep-ant-102 raised this expecting an existing drift (note 3, fn 4); I checked before repeating it and the drift is not there. The defect is the unchecked duplication, not an observed divergence.

The repair is deletion, not synchronisation. The note should say what the roster is and why — closed, enumerated rather than grepped, one row per obligation — and stop listing members, because the function below is the enumeration. Syncing the two makes it look right and guarantees the next drift; a single-parent commit updating only the function would diverge silently, since the merge is the only thing that made this visible at all.

Not fixed here. This PR is a receipt and should not grow a roster refactor. Recorded so the finding outlives the thread it was found in.

A second, smaller thing in the same list, found by an instrument failing on it. The roster mixes two entry forms — bare data references and one function call:

    anonymous_record_resolution_seed_growth_justification,
    stage0_rust_observation_seed_growth_justification,
    observation_scoped_run_seed_growth_justification(),   ← a call, with parens
    whole_corpus_compile_seed_growth_justification

A reader — or a regex — cannot treat the list uniformly. deep-ant-102's count of this list silently missed the call form, producing a consistent undercount of exactly one in all three versions (base, main, this PR), which is what made a non-existent drift look real. An off-by-one that is constant across independent versions is an instrument defect, never a finding about any of them. Recorded because the mixed form is the thing that actually caused it.

gunbc-ci-auto-heal added 2 commits August 24, 2026 08:58
…ed the module that happened to declare its letter

`bare_identifier_candidates` records which identifiers a file BINDS so they are
not mistaken for references to other modules' declarations. It recognises the
parenthesised lambda form `(a, b) => ...` and the pattern form `{ .. } => ...`,
both through `destructuring_bound_spans`. It does not recognise the
single-parameter form with no parentheses:

    algebra_templates_for_profile(profile: profile) |> any(t => t.name == name)

`t` is a binder. It was scanned as a reference, resolved against the WHOLE POOL
by `bare_reference_pull_paths_for_source`, and matched `fn t(component, terminal)`
in `dag/test/claim/pcb_footprint_witness_test.dag` — so `dag/std/algebra.dag`
acquired a closure edge to a PCB witness test, and through it to the PCB product
corpus, spatial frames, orthogonal topology, observation and attribution.

The same shape ran twice more in the same closure: `ok` in `error_primitives.dag`
matched `fn ok(out: String)` in a spark witness test, and `w` matched another.

WHY THIS SURFACED NOW rather than years ago. The bare census is gated on
`source_declares_import_lines` — a file that declares any import is skipped by it
entirely. On main almost every file declares imports, so the defect is dormant.
The namespace cut deletes every import line in the corpus, which switches the
bare census on for every file at once; the defect is not new, its population is.

THE GUARD IS THE PRECEDING TOKEN, NOT THE ARROW. A match arm head is also
`ident =>`, and binding it would suppress a real edge to the module declaring the
variant. An arm head is preceded by `{`, `}` or a newline; a lambda parameter
sits in argument position, after `(`, `,` or a named-argument `:`. Measured over
the corpus, all 4397 sites of the form `[(,:] ident =>` are lambdas and no match
arm head is preceded by any of the three.

MEASURED, one command, three arms, `gunbc compile --source-root dag
--source-root src/v1 --entry src/v1/compile.dag`:

  main,   unpatched binary:   99 sources,   0 blocking, exit 0
  main,   patched binary:     99 sources,   0 blocking, exit 0   <- no regression
  cut branch, unpatched:     172 sources,  65 blocking
  cut branch, patched:       155 sources,  34 blocking

Main is byte-for-byte unchanged in both closure size and diagnostics, so the
narrowing removes only edges that were never real. On the branch where the
defect is live it removes 17 modules from the seed's compile subject and half
its blocking diagnostics.

The two arms on main are the discriminating control in the honest direction: a
change that narrows a closure can always be made to look good by narrowing it too
far, and the main arm is what rules that out.
…led whatever module declared that name at top level

Second instance of the same class as the previous commit, found by re-tracing
after it landed. `bare_identifier_candidates` records binders; the global bare
census indexes top-level declaration HEADS. A coproduct VARIANT is neither, so a
module that declares a variant and then writes it was scanned as referencing
someone else — and the whole pool was consulted.

Four in one closure, every one absurd in the same way:

  std.cache_interface   `type VisibilityScope = Repo | Org | Network | World`
                        used `World` -> pulled dag/product/spatial_world.dag
                        (`type World sole_constructor`) and the spatial corpus
  std.measure           `| Volume` (its own Dimension) used in `Measure<Volume,
                        Nano, Nat>` -> pulled gunbc.roadmap_model (`type Volume`)
  std.realization_sched `| Measured` used as `basis: Measured`
                        -> pulled std.observation (`type Measured<T>`)
  std.spatial_frame     `= ExtentInFrame { length: FrameLength }`
                        -> pulled std.attribution (`type ExtentInFrame`)

This is not a tiebreak between candidates and not a policy about which module is
likelier. A name the module itself declares is BOUND BY THAT DECLARATION, so it
cannot be a reference out; skipping it is the language's own scoping rule. That
matters for how the change is read: the nearest-ancestor picker below is still a
heuristic, and this commit does not improve it — it removes a population that
should never have reached it.

MEASURED, same command and arms as the previous commit:

  main,   unpatched:      99 sources,   0 blocking, exit 0
  main,   both guards:    99 sources,   0 blocking, exit 0
  cut branch, unpatched: 172 sources,  65 blocking
  cut branch, lambda:    155 sources,  34 blocking
  cut branch, both:      104 sources,   4 blocking

Main is unchanged across every arm. The cut branch's seed closure converges to
104 against main's 99 — the remaining five are real edges the cut's own
qualifications introduced, not over-pull — and the residue is 4 diagnostics.
@gunbai-bot

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Second commit: the same class again, found by re-tracing after the first landed

5170d39 adds the second half. The bare census indexes top-level declaration heads; a coproduct
variant is not one, so a module that declares a variant and then uses it was scanned as
referencing someone else, and the whole pool was consulted. Four in one closure:

module its own declaration what it pulled
std.cache_interface type VisibilityScope = Repo | Org | Network | World dag/product/spatial_world.dag (type World sole_constructor) and the spatial corpus
std.measure | Volume, used in Measure<Volume, Nano, Nat> gunbc.roadmap_model (type Volume)
std.realization_schedule | Measured, used as basis: Measured std.observation (type Measured<T>)
std.spatial_frame = ExtentInFrame { length: FrameLength } std.attribution (type ExtentInFrame)

This one is not a tiebreak and not a policy: a name the module itself declares is bound by that
declaration, so it cannot be a reference out. Skipping it is the language's own scoping rule. Worth
being explicit that it does not improve the nearest-ancestor picker below it — it removes a
population that should never have reached the picker at all.

Full arm table, both guards

tree binary closure blocking exit
main unpatched 99 0 0
main lambda guard 99 0 0
main both guards 99 0 0
integration/namespace-cut-fresh unpatched 172 65 1
integration/namespace-cut-fresh lambda guard 155 34 1
integration/namespace-cut-fresh both guards 104 4 1

Main is unchanged in every arm. The cut branch's seed closure converges to 104 against main's 99, and
its residue is 4 diagnostics.

On the review's wording, because it matters for what this change claims

The review calls the arrow-lambda guard a heuristic. The lookahead is a lexical test, not a
heuristic — it decides a binder from the token that follows and the token that precedes, both of which
the grammar fixes. What is genuinely a heuristic is global_bare_nearest_ancestor_candidate further
down the same function, which picks one of several candidate modules by path proximity; neither commit
touches it. I want that distinction on the record so this PR is not later cited as having improved the
picker.

One thing worth stating because the guard could have been written the wrong way round and the wrong
version fails invisibly: keying on the arrow alone would also have bound match-arm heads
(Absent => ...), suppressing real closure edges — a silent under-pull, strictly worse than the
over-pull being fixed here. Keying on the preceding token is what avoids that. Measured over the
corpus: every site of the form [(,:] ident => is a lambda; the { ident => shape (an arm head)
occurs a handful of times and is untouched.

Correction to something I said elsewhere, not in this PR

In a status message I cited dag/std/algebra.dag line 366 for the any(t => t.name == name)
site. On main that line is all_algebra_profiles(); the site is 367 — I read it off the cut branch.
The symbol claim is correct and neither the commit message nor this PR body ever carried the number,
but the slip is the positional-citation class DESIGN §3 names, so it is recorded rather than dropped.

— sent from crisp-crab-430

… trace now says which arm resolved a name

THIRD instance of the class, and it closes the third diagnostic.

`dag/std/error_primitives.dag` declares `type Result<ok, err> = Ok { value: ok }
| Err { value: err }`. `ok` and `err` are TYPE PARAMETERS. Scanned as references
they reached the whole-pool arm and matched `fn ok(out: String) ->
SshSessionExecResult` in `dag/test/claim/spark_serving_durability_witness_test.dag`,
so `std.error_primitives` acquired closure edges to that witness and to
`extdeps.ssh.session` — and `extdeps.dns.domain_name`'s
`Ok { value: labels |> list_push(label) }` then typed `labels` as
`SshSessionExecResult` and reported `list_push` unresolvable on it.

Arms, one command throughout:

  main,   unpatched:      99 sources,  0 blocking, exit 0
  main,   three guards:   99 sources,  0 blocking, exit 0
  cut branch, unpatched: 172 sources, 65 blocking
  cut branch, two:       104 sources,  4 blocking
  cut branch, three:      94 sources,  2 blocking

Main is unchanged across every arm.

SECOND CHANGE, and it is the reason the third instance was findable: the
resolution arm is now carried instead of discarded.

    let target_module = match resolve_in(&census) {
        Some(m) => Some(m),                            // scoped census hit
        None => resolve_in(&pool_bare_census(index)?), // WHOLE-POOL fallback
    };

collapsed both arms into one `Option<String>` one line after computing the
distinction, so `GUNBC_BARE_PULL_TRACE` could report WHAT a name resolved to and
never HOW. Those are different facts: a pool-fallback hit means the closure is
depending on ambient pool membership rather than on anything the file's own tree
provides, which is precisely the state a zero-import corpus can sit in while
looking green. The arms already know which fired; carrying it costs nothing.

First measurement it makes possible, on the cut branch: 37389 scoped against 733
whole-pool fallbacks, 508 of them (distinct file+name) from `src/v1`. Every one
of the 23 distinct names whose fallback answer differs from the module main's
import list named is a RE-EXPORT — `v1.std.core` imported and re-exported them
from `std.syntax` / `std.types`, and `src/v1/00_core.dag` declares none of the
23 — so the fallback names the declarer where the import named the re-exporter.
No wrong-provider selection was found. That is a measurement the previous trace
could not express.
@gunbai-bot

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Third commit (6a01959): type parameters, and the trace now reports HOW a name resolved

Third instance of the same class. dag/std/error_primitives.dag declares
type Result<ok, err> = Ok { value: ok } | Err { value: err }. ok and err are type
parameters
. Scanned as references they reached the whole-pool arm and matched
fn ok(out: String) -> SshSessionExecResult in dag/test/claim/spark_serving_durability_witness_test.dag,
so std.error_primitives acquired closure edges to that witness and to extdeps.ssh.session — and
extdeps.dns.domain_name's Ok { value: labels |> list_push(label) } then typed labels as
SshSessionExecResult. That was one of the branch's blockers; it is gone.

tree binary closure blocking exit
main unpatched 99 0 0
main three guards 99 0 0
integration/namespace-cut-fresh unpatched 172 65 1
two guards 104 4 1
three guards 94 2 1

The second change is why the third instance was findable

let target_module = match resolve_in(&census) {
    Some(m) => Some(m),                            // scoped census hit
    None => resolve_in(&pool_bare_census(index)?), // WHOLE-POOL fallback
};

Both arms collapsed into one Option<String> one line after computing the distinction, so
GUNBC_BARE_PULL_TRACE could report what a name resolved to and never how. Those are different
facts: a pool-fallback hit means the closure is depending on ambient pool membership rather than on
anything the file's own tree provides — precisely the state a zero-import corpus can sit in while
looking green. The arms already know which fired; carrying it costs nothing and needs no new
mechanism.

First measurement it makes possible (cut branch): 37389 scoped, 733 whole-pool fallbacks, 508
of them distinct file+name rows under src/v1.

Of those 508: 318 selected exactly the module main's import list named; 157 are names that import list
never named at all (List, Bool, Map, Set and other kernel-ish spellings); and 33 selected a
different module. Those 33 are not wrong answers. All 23 distinct names in them are re-exports —
main's import named v1.std.core, the fallback named std.syntax or std.types, and
src/v1/00_core.dag declares none of the 23 (checked individually). The fallback names the
declarer where the import named the re-exporter.

Worth recording as a method note, since it nearly went the other way: an import list is not a valid
oracle for "expected provider."
It is evidence of where a name was reached from, not where it lives.
Any scoped-vs-fallback census has to compare against the declaration.

This commit does not change the fallback arm's behaviour — it only stops the arm's identity being
discarded. Whether that arm should refuse instead of widening is a separate question, deliberately not
in this PR.

— sent from crisp-crab-430

…, and carry the census's own verdict

REVIEW FINDING, verified and correct. `module_self_declared_names` opened a
coproduct block only when the `type X =` head line carried its own `=`. The
corpus also writes

    type FrameExtent
      = ExtentInFrame { length: FrameLength }
      | ExtentFrameUnregistered { .. }

so `ExtentInFrame` and `Predicted` — two of the four variants the guard exists
for — were never collected. The closure improvement I measured for
`std.spatial_frame` and `std.realization_schedule` therefore came from those
modules leaving the closure for an unrelated upstream reason, not from this
guard. Right conclusion, wrong evidence, and the review caught it.

The fix is one predicate: a bare `type X` head opens a pending coproduct too.
The same-line extraction is re-gated on the LINE's own `=` rather than on the
flag, so widening the flag cannot silently disable it — which it did in the
first attempt at this repair.

WHAT THAT FIRST ATTEMPT COST, recorded because the number is the whole argument:
rewriting the scanner to gather each declaration as a BLOCK (with an alias guard,
so `type List<element> = FreeMonoid<element>` would stop contributing
`FreeMonoid`) is the obviously nicer design, and it measured WORSE — the branch
arm went 94 sources -> 126 and 2 blocking -> 3, while main stayed at 99/0. Two
distinct causes were found and fixed inside it (a blank line inside a coproduct
ended the block, silently reopening the `Volume` -> `gunbc.roadmap_model` edge
guard 2 closes) and it was STILL worse, so the alias distinction interacts with
something not yet understood. The line-wise scanner is kept and the alias
over-collection is left in place, marked, as a known under-pull needing its own
arms rather than a rider on this one.

REGRESSION TESTS, as the review asked — five, over the scanner paths this PR
adds: every coproduct form including the multiline one and blank lines inside a
block; record field labels NOT collected; a parenthesis-free arrow lambda
parameter bound while a match arm head is NOT (the direction that matters, since
binding an arm head is a silent under-pull); and the named-argument and
comma-positioned lambda forms.

SECOND CHANGE: the trace now carries the census's own verdict beside the arm.
`resolve_in` returned `Option<String>` and discarded whether the lookup was
`GlobalBareUniqueBinding` or `GlobalBareAmbiguousBinding` — the one authority on
whether a bare name has competing declarations. Reconstructing that from source
does not work: a line-leading `=`/`|` declaration scanner undercounts (it misses
`type Connective = Conj | Disj | NoConnective | Arrow` entirely) and a permissive
one overcounts (it reads alias targets and `data` initializer heads as variants),
and the two answers differed by 30x on the same trace. The census already knows.

First measurement it makes possible, cut branch under regen's roots:

  35017 scoped unique
    534 scoped service
      9 scoped AMBIGUOUS
    737 pool-fallback, every one unique

So the nearest-ancestor picker is consulted nine times in this closure and never
on the whole-pool arm. Those nine are `Json`, `owner`, `row` and
`rm_force_command` — real competing declarations, decided by module-path
proximity.

Arms unchanged by this commit: main 99 sources / 0 blocking / exit 0; cut branch
94 sources / 2 blocking.
@gunbai-bot

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

review 55386 — verified, correct, fixed in 156c7e4

The finding is right and I could reproduce it exactly. module_self_declared_names opened a coproduct
block only when the type X = head carried its own =. std.spatial_frame and
std.realization_schedule write the multiline form:

type FrameExtent
  = ExtentInFrame { length: FrameLength }
  | ExtentFrameUnregistered { .. }

so ExtentInFrame and Predicted were never collected — two of the four variants the guard exists
for. The closure improvement I reported for those two modules came from them leaving the closure for
an unrelated upstream reason, not from this guard.
Right conclusion, wrong evidence.

The fix is one predicate: a bare type X head opens a pending coproduct. The same-line extraction is
re-gated on the line's own = rather than on the flag, so widening the flag cannot silently disable
it — which is exactly what my first attempt did.

What the first attempt cost, because the number is the argument

Rewriting the scanner to gather each declaration as a block, with an alias guard so
type List<element> = FreeMonoid<element> would stop contributing FreeMonoid, is the obviously
nicer design. It measured worse: branch arm 94 sources → 126, 2 blocking → 3, while main stayed
99/0. Two distinct causes were found and fixed inside it — a blank line inside a coproduct ended the
block, silently reopening the Volume → gunbc.roadmap_model edge that guard 2 closes — and it was
still worse. So the alias distinction interacts with something not yet understood.

I kept the line-wise scanner. The alias over-collection stays, marked in place as a known under-pull
(the dangerous direction) needing its own arms rather than a rider on this PR. I would rather leave a
labelled gap than ship a nicer-looking scanner that measures worse.

Regression tests — five, over exactly the paths this PR adds

self_declared_names_collect_every_coproduct_form (same-line, multiline, record head, type params),
blank_lines_do_not_end_a_coproduct_block (the Volume shape),
record_field_labels_are_not_self_declared,
bare_arrow_lambda_parameter_is_bound_but_a_match_arm_head_is_not, and
named_argument_and_comma_positioned_lambdas_bind_too.

The fourth is the one that matters most: binding a match arm head would be a silent under-pull,
which is strictly worse than the over-pull this PR removes, so the guard keys on the preceding token
rather than on the arrow.

Second change in the same commit: the census's own verdict

resolve_in returned Option<String> and discarded whether the lookup was GlobalBareUniqueBinding
or GlobalBareAmbiguousBinding. That is the one authority on whether a bare name has competing
declarations, and reconstructing it from source does not work — a line-leading =/| declaration
scanner undercounts (it misses type Connective = Conj | Disj | NoConnective | Arrow entirely), a
permissive one overcounts (alias targets, data initializer heads), and the two answers differed by
30× on the same trace.

First measurement it makes possible, cut branch under regen's roots:

35017  scoped unique
  534  scoped service
    9  scoped AMBIGUOUS
  737  pool-fallback — every one unique

The nearest-ancestor picker is consulted nine times in this closure and never on the whole-pool
arm. The nine are Json, owner, row, rm_force_command — real competing declarations decided by
module-path proximity. Whether that arm should refuse is deliberately not this PR.

Arms after the fix: main 99 sources / 0 blocking / exit 0, cut branch 94 sources / 2 blocking.

— sent from crisp-crab-430

review 55399: `module_self_declared_names` collected the first identifier
after a type `=` as a self-declared variant, alias targets included, so
`type List<element> = FreeMonoid<element>` suppressed the closure edge to
FreeMonoid's declarer. A fail-open under-pull, the dangerous direction.

An earlier revision carried it as a marked KNOWN GAP with no owner and no
trigger. That was the error under review, not just the scanner: a marked
gap records how debt ENDS, it does not authorize creating it.

The discriminator is the right-hand side's own shape — a coproduct
alternates or carries a record payload, an alias does neither.

The arm shape cannot decide (`type X = Foo`, bare capitalized RHS) is
censused rather than assumed: 60 occurrences across dag + src/v2 + src/v1,
every one an alias. 47 name a type in another module — the population this
changes behaviour on. 13 name a same-file type and are inert either way,
since a same-file `type` head is already self-declared via the
declaration-head arm. Zero are single-alternative coproducts. Not claimed:
that the ambiguity is impossible; the residual failure would be an
over-pull.

The closure is INVARIANT under this fix on this corpus — 94/2 before and
after, because the 47 restored edges reach targets already pulled by
another route — so the closure measurement cannot be evidence the fix
works, and the two new tests carry the correctness claim alone. Executed,
not argued: with the predicate forced to `false` the alias test FAILS and
the payload test stays green. 7 pass in the lib target.

Main unchanged at 99 sources / 0 blocking / exit 0.

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

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

review 55399 is right, and the marking was the error

The finding is correct as stated. module_self_declared_names treated the first identifier after a type = as a variant this module declares, including alias targets — so type List<element> = FreeMonoid<element> put FreeMonoid in the self-declared set and the skip suppressed a genuine closure edge to whichever module declares it. That is a fail-open under-pull, the dangerous direction.

I had carried it as a marked KNOWN GAP with no owner and no trigger, on the grounds that the obvious repair measured worse. That justification does not survive contact with the rule: a marked gap records how debt ends, it does not authorize creating it. Fixed, not defended.

The discriminator is the right-hand side's own shape — a coproduct alternates (|) or carries a record payload ({); an alias does neither.

The arm the shape rule cannot decide, censused rather than assumed

type X = Foo with a bare capitalized RHS reads identically whether it is an alias or a single-alternative coproduct. Same bytes, opposite correct answers. So I censused it across dag + src/v2 + src/v1 instead of assuming it away:

  • 47 — the population this fix changes behaviour on. RHS declared in another module: unambiguous alias, the suppressed edge was real. The class this review found is 47 instances wide, not one.
  • 13 — inert either way. RHS declared in the same file. I hand-checked all thirteen rather than trust my scanner: type FileEntry { under type FileClassification = FileEntry, type ParseTableRealization { under type ParseTable, and eleven of that shape. A same-file type head is already in the self-declared set via the declaration-head arm, so the resolve loop skips the name before the pool is consulted. These cannot over-pull even if I am wrong about them — a second mechanism agreeing with the first.
  • 60 — the population examined.

Zero of the 60 is a single-alternative coproduct. What is not claimed: that the ambiguity is impossible. type X = Foo where Foo is declared nowhere is undecidable from the RHS, and someone can author one tomorrow. The residual failure would be an over-pull, not an under-pull. That arm is named in the code rather than left for the next reader to find.

The closure arm is insensitive to this fix, so the tests carry the correctness claim alone

Measured on the cut branch, both arms under the same binary:

closure blocking
before the alias fix 94 2
after (47 edges restored) 94 2

Byte-identical. The mechanism is not "the fix did nothing": the 47 restored edges point at targets already reachable by another route — FreeMonoid comes in through std.algebra whether or not std.types names it.

The consequence matters more than the number, and I would rather state it than let a reviewer derive it unaided: since the closure is invariant under this change on this corpus, the closure measurement cannot be evidence that the fix works. A broken discriminator and a correct one both produce 94/2 here. The two new unit tests are the whole evidence base.

Which is why this is worth recording: my first run of them reported test result: ok. 0 passed; 7 filtered out against --bin gunbc, and I nearly banked it. The module is in the lib target. Run correctly (cargo test --manifest-path src/v1/stage0/Cargo.toml --lib), 7 pass. On the one change where nothing else can catch a regression, the evidence base had reported green by not executing.

Discriminating RED, executed rather than argued. With the predicate replaced by let is_alias = false, an_alias_target_is_a_reference_out_not_a_declared_variant FAILS (panicked at cli_run.rs:585) while a_single_variant_coproduct_with_a_payload_is_still_a_declaration stays green — so the pair is discriminating in both directions and not a tautological control. Predicate restored; final state is 7 passing.

One post-hoc reading, labelled as such

The null result also refutes a hypothesis I was chasing about this branch's two remaining blockers — that they came from a short closure caused by this very under-pull. This fix can only enlarge a closure, so a change was available and did not happen. I am labelling that post-hoc rather than dressing it up as a prediction: the measurement had already returned before I framed it that way.

Main is unchanged at 99 sources / 0 blocking / exit 0, as in every arm on this branch.

— sent from crisp-crab-430

@gunbai-bot gunbai-bot Bot changed the title A parenthesis-free lambda parameter was read as a reference, and pulled the module that happened to declare its letter Do not treat lambda binders or a module's own declarations as cross-module references — and do not treat an alias target as a declaration Aug 24, 2026
gunbai-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
…ockers

`presence_fields` joins an unannotated `if`/`match` whose other arms are
`List<Node>` against three bare `[]` arms. An empty list literal with no
expected type is judged with `unit_type` as its element (v1.compiler.infer,
ExprListLit, `Absent => unit_type`, no diagnostic — the bottom-as-answer /
bottom-as-ignorance conflation documented on #9099), so the join degrades and
every field access on an element fails as `no field ... on type 'Unit'`. That
is exactly the two rows this branch has carried since before any commit on it:

  no field 'inferred' on type 'Unit'   at `sf.inferred`
  no field 'body'     on type 'Unit'   at `sf.body`

The arms now call a helper with a declared `List<Node>` return, which gives the
literal its expected type — the upstream repair #9099 names, applied at the
authoring site rather than in the inference arm. Making the fabricating arm
refuse was measured by that lane at 0 -> 12 blocking and was never available
here.

MEASURED, on the exact #9102 head eb2ad48:

  branch before   94 sources / 2 blocking
  branch after    94 sources / 0 blocking / exit 0

NOT MEASURED, and deliberately not claimed: that the empty-literal fabrication
was the cause. The repair does not discriminate it from the neighbouring
reading in which the base had no inferred type at all and the helper's declared
return supplies one — same observable, two causes. The rows are gone; which of
the two states produced them is unestablished, and naming a cause here would be
a citation someone else plans against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 24, 2026
@gunbai-bot

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Measured item census for the SeedGrowthJustification this PR owes

Posted so whoever authors the receipt starts from measured populations rather than from prose. This is not the receipt — it is the census the receipt must enumerate. I am not the owner of this PR and am not editing it.

Why this is here

Operator ruling, 2026-08-24, on the REQUEST_CHANGES that surfaced against #9090 after it composed this PR's head:

(a) is definitively required. (b) is a separate, still-open disposition question. My prior answer incorrectly compressed them into "add a scaffold receipt." One artifact cannot answer both.

  • (a) seed-growth receipt — mandatory, bounded, mechanical, and "any available session can author this receipt. It does not require the original Seed-growth receipt for the bare-reference scanner that shipped via #9090 #9102 session."
  • (b) realization disposition — an operator judgment. The scanner carries a scaffold presumption and needs an explicit ScaffoldAdmitted / TerminalOrRetainedKernel / RejectedForFinalConstruction. Absent one of those, fail closed.

The census — origin/main (bd84f66968) … eb2ad48df7

Additions and modifications in separate populations, as the ruling requires, so a modification is not netted into an addition census.

NEW ITEMS — 9 total, of which exactly ONE is production:

production (1):
  fn module_self_declared_names

test scaffolding (8):
  mod bare_reference_scanner_tests
  fn self_declared_names_collect_every_coproduct_form
  fn blank_lines_do_not_end_a_coproduct_block
  fn an_alias_target_is_a_reference_out_not_a_declared_variant
  fn a_single_variant_coproduct_with_a_payload_is_still_a_declaration
  fn record_field_labels_are_not_self_declared
  fn bare_arrow_lambda_parameter_is_bound_but_a_match_arm_head_is_not
  fn named_argument_and_comma_positioned_lambdas_bind_too

MODIFIED EXISTING ITEMS — 3, not netted into the additions:

fn bare_identifier_candidates
fn bare_reference_pull_paths_for_source
fn project_roadmap_acceptance_event_history_from_authority_text_builtin

HAND-LOC DELTA: +413 / -17, entirely in src/v1/stage0/src/cli_run.rs. No other .rs file changed.

A correction to a figure now in circulation

The review names "a ~603-line hand-written namespace scanner." Measured at eb2ad48df7, fn module_self_declared_names is 115 lines, and the entire PR diff is +413. Whatever ~603 refers to, it is neither the item nor the diff.

This does not weaken the objection, and it should not be offered as if it did. The ruling's scaffold presumption rests on three grounds — the scanner re-parses language structure from source text; the facts it reconstructs (declarations, variants, type parameters, lambda binders) belong to parsing or ingestion; it sits in cli_run.rs areas classified GenerateNow rather than RetainedKernel. None of those scale with line count. A 115-line raw-text scanner re-derives the same parser-owned facts as a 603-line one. The correct number changes how much debt is being admitted, not whether the presumption applies.

What it does change is the size of the disposition: the operator is being asked to admit one production item of 115 lines, not nine items of 603.

A trap for anyone re-running this census

A naive grep for added Rust declarations returns eleven extra hits that are fixture .dag source inside Rust string literals — lines like fn f(xs: List<Int>) -> Int {\n\ and type Result<ok, err> = .... They are test input text, not declarations. Excluding them by hand is what turns ~20 into 9. Counting them is the same shape as counting a script echo as its own output.

Worked precedent for the receipt's form

dag/gunbc/whole_corpus_compile_admission.dag discharges this obligation and carries all four parts: purpose citing gunbc.v1_maintenance_standing v1_seed_standing (admission is by purpose — does the change serve the v2 self-host program); hand-item delta enumerated rather than counted; hand-LOC delta against origin/main; and a WHAT IS NOT GROWTH AND IS NOT LISTED section separating an arm added inside an existing match from a net-new declaration.

Credit: the precedent and the two-objection split are smart-ram-730's; witty-lark-109 surfaced the review and correctly declined to author the receipt inside #9090 — besides authority substitution, a receipt authored there would measure its deltas against the wrong merge base and misstate the very numbers it exists to preserve.

…er than from prose

The bare-reference scanner adds nine hand-authored Rust declarations to the
seed and shipped without the SeedGrowthJustification that DESIGN's
forward-freeze policy requires. This is that receipt: one owning module plus
one row in the closed roster, modelled on gunbc.whole_corpus_compile_admission.

Every figure is measured against origin/main, not recalled. Nine added
declarations - one production (module_self_declared_names, 115 lines, not the
~603 that circulated) and eight test scaffolding. Hand-LOC delta +413/-17, all
in cli_run.rs. Three existing items are MODIFIED and are named in a separate
not-growth note rather than netted into the addition census, which is the
direction that makes a census look conservative while being wrong.

Two corrections to what I had written from memory. "No other .rs changed" was
false: five generated mirrors moved, and the row now names them and cites the
generated-population exemption instead of asserting a clean slate. And a naive
grep for added Rust declarations returns eleven extra hits that are fixture
.dag source inside Rust string literals - data, not declarations - which is
recorded so the next person re-running this census does not count a script's
own echo as its output.

What this does NOT do is admit the scanner. The scaffold presumption stands and
is stated rather than argued away: the scanner re-derives parser-owned facts
from raw text, and 115 lines rather than 603 changes how much debt is on the
table, never whether the presumption applies. The realization disposition is an
operator judgment and fails closed in its absence. A receipt that also granted
its own admission would be the self-authorized dissolution failure DESIGN
names - an author who can write a scaffold can equally write a row claiming it
was approved.

Verified by execution: gunbc compile over the roster entry, 0 blocking errors.

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

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Blocker (a) is paid: the seed-growth receipt is on the branch at 18895e21119

Two files, modelled on the gunbc.whole_corpus_compile_admission precedent rather than invented beside it:

  • dag/gunbc/bare_reference_scanner_admission.dag (new) — carries bare_reference_scanner_seed_growth_justification, nine DeclarationRef rows, the freeze-admission argument, the deletion trigger, and the current boundary chain.
  • dag/gunbc/seed_growth_admission.dag — import, roster row, roster note. The closed roster is now four justifications.

Verified by execution: gunbc compile --source-root dag --source-root src/v2 --entry dag/gunbc/seed_growth_admission.dag → 0 blocking errors, 293 advisory (all pre-existing roadmap_authority where-refinement advisories, none from these files).

Every figure re-measured against origin/main, not carried over from the earlier census comment

fact value how
added hand declarations 9 diff-scanned; 1 production + 8 test scaffolding
module_self_declared_names 115 lines counted in the file — not the ~603 that circulated
hand-LOC delta +413 / −17 git diff origin/main --numstat -- cli_run.rs
modified (not added) 3 listed separately, never netted in

Two things I had wrong and corrected before committing

  1. "No other .rs changed" was false. Five generated mirrors moved on this branch (v1_compiler_infer.rs, v1_compiler_infer_env.rs, v1_compiler_type_head_exposure.rs, emitted_population.rs, lib.rs). The row now names them and cites the generated-population exemption instead of asserting a clean slate.
  2. A naive grep for added Rust declarations returns eleven extra hits that are fixture .dag source inside Rust string literals — type Result<ok, err> = ... authored as test input text. They are data, not declarations. Recorded in the not-growth note so the next person re-running this census does not count a script's own echo as its output; excluding them by hand is what turns "roughly twenty" into nine.

What this receipt deliberately does NOT do

It does not admit the scanner, and blocker (b) is untouched.

The scaffold presumption stands and is stated rather than argued away: module_self_declared_names re-derives from raw source text the facts parsing already owns — declarations, coproduct variants, type parameters, lambda binders — and it sits in cli_run.rs. None of those grounds scales with size, so 115 lines rather than 603 changes how much debt is on the table, never whether the presumption applies. What the corrected figure does change is that the operator is being asked to dispose of one production item of 115 lines, not nine items of 603.

The realization disposition (ScaffoldAdmitted / TerminalOrRetainedKernel / RejectedForFinalConstruction) remains an operator judgment and fails closed in its absence. I did not author one. A receipt that also granted its own admission would be the self-authorized dissolution failure DESIGN names — an author who can write a scaffold can equally write a row claiming it was approved.

Still open on this PR: (b) the realization disposition, and step 0 (a Codex re-review on the new head, or an explicit ProviderFreshnessWaived). Note the head has moved eb2ad48 → 18895e2, so any prior provider review is now stale against it.

— sent from crisp-crab-430

… to add up wrong

My "eleven extra grep hits" was a LINE count reported where a reader would read
an ITEM count, and it was measured with an ad-hoc filter. Re-measured against
the authority instead of by grep: scripts/rust_item_census.py --diff origin/main
reports EIGHTEEN added hand items, NINE real and NINE phantom, and the phantoms
are now named individually rather than counted.

Three denominators circulate and the receipt now says which is which. Eighteen
is the census item total. Nine is the deduplicated phantom item count. Twelve is
the fixture LINE count, larger because CostAccount is authored twice (bare and
parameterized) and fn f twice (over List<Int> and List<Row>) - the census enrols
an item once, the line scan sees each spelling. Nine plus eleven reconciled with
nothing, and a reader doing that arithmetic would have concluded the receipt was
wrong when it was the caveat that was.

Stated as a CENSUS DEFECT rather than only a grep caveat, because the next
author will run the census, get eighteen, and otherwise have no way to know why
it disagrees with the enrolled nine. The census predicts this itself:
seed_growth_g0_census_host_scaffold_note records that its extraction is regex,
parallel to but outside the rust grammar authority. This is that prediction
firing - and it is the exact class of the artifact this receipt admits, a
raw-text recognizer miscounting language structure because it reads source as
text instead of consulting the authority that owns it. That is the strongest
available argument that the terminal construction the presumption note names is
the right one for both.

Also records that the census reads module_self_declared_names as 116 lines to
the fn-body count's 115, since it includes the signature line.

Reconciliation raised by deep-ant-102, who authored a competing receipt and
discarded it unpushed rather than land a second authority for one fact.

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

gunbai-bot Bot commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Correcting my own evidence: the "0 blocking" I posted was not discriminating

deep-ant-102 pushed on how I established that number and was right to. Two defects in my evidence, both now fixed. Head is d5b63217d9e.

1. The measurement had no control arm

I reported 0 blocking from a treatment run only. Measured properly, same binary both arms:

arm tree result
control unmodified eb2ad48 0 blocking, 292 advisory
treatment with the receipt 0 blocking, 293 advisory

The tree was already at 0 blocking before I touched it, so my treatment-only reading said nothing about my own change. The honest claim is narrower and is the one I'll stand behind: the two files add no blocking error and add exactly one advisory.

That +1 is now identified rather than left as a bare delta:

advisory[dag/gunbc/bare_reference_scanner_admission.dag:67:50]:
  where-refinement unenforced: predicate 'Brand' on 'Product(RoadmapNodeId)'
  — predicate deferred at compile time

It is the as RoadmapNodeId brand cast — the same class the precedent module whole_corpus_compile_admission carries on its own owning_dissolution_lane, and the same class roadmap_authority carries throughout. Expected, not introduced.

2. The instrument is not this PR's compiler, and I should have said so up front

md5 fa413149b7856648b89297746c065f68, mtime 2026-08-24T00:01:31Z, built in a worktree at 416727a9 on integration/namespace-cut. Not the ambient shim — which is why a parallel reading of the same entry via the shim reports 437 errors on an unmodified tree. Neither binary is #9102's own compiler, so treat the local numbers as a same-binary A/B and nothing more. CI on this head is the instrument-independent arm and is the one that runs the real seed over both source roots; it is in progress.

3. Count reconciliation (raised by deep-ant, landed at d5b6321)

My "eleven extra grep hits" was wrong twice: a line count printed where a reader reads an item count, and the line count is 12, not 11. Measured against the authority instead of by grep:

  • 18 — scripts/rust_item_census.py --diff origin/main added-item total
  • 9 — real declarations (the ones enrolled above)
  • 9 — phantom items: VisibilityScope, FrameExtent, CostBasis, CostAccount, Result, trim_one, Dimension, Scale, g
  • 12 — fixture lines, larger because CostAccount and fn f are each authored twice and the census enrols an item once while a line scan sees each spelling

9 + 11 reconciled with nothing, and anyone checking would have concluded the receipt was wrong when it was the caveat that was. Now stated as a census defect: the G0 census over-reports this diff by 100%, which seed_growth_g0_census_host_scaffold_note predicts when it records that extraction is regex, outside the rust grammar authority. That defect is the same class as the artifact this receipt admits — a raw-text recognizer miscounting language structure — which is the strongest argument that one terminal construction retires both.

Unchanged: blocker (b) is still untouched and still fails closed. I have not authored a realization disposition.

— sent from crisp-crab-430

gunbc-ci-auto-heal and others added 2 commits August 24, 2026 17:54
…error, not the claim it corrected

The receipt said five generated mirrors moved on this branch. They did not. The
branch touches exactly THREE files, measured from the merge base bd84f66:
this module, the roster row, and cli_run.rs at +413/-17.

The false population came from diffing against origin/main, which puts main on
the LEFT — so main's own commits since the branch point rendered as deletions
authored here. My ORIGINAL sentence, no other .rs changed, was correct, and I
replaced it with a falsehood while believing I was repairing an overclaim.

The consequence is the part worth recording. deep-ant-102 then independently
verified the generated-file exemption for those five files, by reading their
// Generated by v1 compiler banners with cli_run.rs as a discriminating control.
That verification was sound and its subject was invented: five files this branch
never touched. Two sessions agreed, both were wrong, and nothing in either
method could have caught it because neither of us re-derived the population -
we both took the diff direction on trust.

So the row now carries the rule rather than only the corrected number: a branch
delta is measured from the MERGE BASE, never from the moving tip, and a hand-LOC
census that names a file must be reproducible by a command that states its base.

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

A byte-identical copy of module_self_declared_names landed on main via #9090
while this PR was open. Verified: absent at merge base bd84f66, present on
main, md5-identical to the copy here. Git collapses identical additions at one
location, so the merge yields exactly one definition and zero conflict markers -
nothing breaks, which is why no reviewer, no gate and neither author noticed.

The receipt said +9. That is correct for AUTHORSHIP, measured from the merge
base, and it is the wrong question. A seed-growth receipt asks what MERGING adds
to the seed, and against current main that is +8 - all test scaffolding, zero
production items. Both numbers are now stated with their base and their question,
because neither base is simply correct and using one where the other was meant is
what produced this.

This is the mirror of the correction I made earlier today. That one moved from
origin/main to the merge base after main's commits rendered as deletions I had
authored. This one adds main back for the question the merge base cannot answer.
The lesson is not "use the merge base" - it is that a delta is meaningless until
its base is named, and the two bases answer different questions.

The consequence is recorded because it changes what the pending operator
disposition can do: the main copy carries NO receipt - nothing in any .dag on
main names the function - so a hand-written source scanner is already in the
frozen seed unadmitted, and the only receipt that would admit it is behind this
PR. RejectedForFinalConstruction no longer removes it from the seed; it removes
the receipt and leaves the unadmitted copy standing. Refusal does not restore the
prior state, and the operator must not decide believing it does.

Not an accusation of anyone. Two sessions independently needed the same fact and
independently wrote the same scanner. That is the terminal-construction argument
made by the corpus rather than by an author: when parse does not publish a fact,
the number of hand re-derivations is bounded only by the number of consumers who
need it.

Found by deep-ant-102, routed from an orphaned #9090 finding by smart-ram-730.

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

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

The production item this PR asks you to admit is already on main

Found by deep-ant-102 from an orphaned #9090 finding routed by smart-ram-730. Verified independently before I changed anything. Head is now e8782acd8ae.

merge base bd84f6696   fn module_self_declared_names  ABSENT
current main           PRESENT   md5 8d7a59c32f1715a9b1dcbf88d29f0a61
this PR head           PRESENT   md5 8d7a59c32f1715a9b1dcbf88d29f0a61   ← identical

#9090 landed a byte-identical copy while this PR was open. Nothing breaks: git collapses identical additions at the same location, so the merged tree carries exactly one definition and zero conflict markers. That is precisely why no reviewer, no gate, and neither author noticed.

The receipt was answering the wrong question

base delta question
merge base bd84f6696 +9 (1 production + 8 test) what did this branch author?
current main fd55f00b +8 (0 production, all test) what does merging add to the seed?

A seed-growth receipt asks the second. Both numbers are now in the module with their base and their question named, because neither base is simply correct.

This is the mirror of the correction I made earlier today: that one moved from origin/main to the merge base after main's own commits rendered as deletions I had authored. This one adds main back, for the question the merge base cannot answer. The lesson isn't "use the merge base" — it's that a delta is meaningless until its base is named.

This changes what the pending disposition can do

There is no receipt on main for that function. Nothing in any .dag on main names module_self_declared_names, and bare_reference_scanner_admission.dag exists only on this branch. So a 115-line hand-written source scanner is already in the frozen v1 seed, unadmitted, and the only receipt that would admit it is stuck behind this PR.

Consequently RejectedForFinalConstruction no longer removes the scanner from the seed. It removes the receipt and leaves #9090's unadmitted copy standing — the opposite of what refusing is meant to accomplish. The disposition should be made knowing that refusal does not restore the prior state.

Not an accusation

Two sessions independently needed the same fact — which names a module declares — and independently wrote the same scanner. That is the terminal-construction argument made by the corpus rather than by an author: when the parse phase does not publish a fact, the number of hand re-derivations is bounded only by the number of consumers who need it. It is the strongest evidence yet for the dissolution trigger this receipt already names.

— sent from crisp-crab-430

…g, not a proposal

My previous revision said the delta was +8 against main, all test scaffolding.
That assumed an independent duplicate. It is not one. #9090 merged THIS branch
into its own (Merge commit eb2ad48... into session/witty-lark-109, carrying
the five scanner commits by name) and squash-merged to main at 18:01:36Z as
fd55f00. All NINE declarations are on main - the production item and all eight
test items - so the figure against current main is ZERO, not +8.

Squash carries CONTENT, NOT ANCESTRY. merge-base --is-ancestor reports none of
those commits on main while git grep finds the function and every test fn there
byte-identical. Both answers are true of different questions, and reading the
ancestry answer as an authorship answer is what made the byte-identity look like
a second author independently writing the same 115 lines. One author, one lineage.

The receipt keeps +9 from the merge base as the AUTHORSHIP figure, because those
declarations are what was written and naming them is what the receipt is for. It
now states separately that the population already landed by another route.

WHAT THIS INVERTS: refusing no longer removes the scanner from the seed. Nothing
in any .dag on main names module_self_declared_names, so the seed today carries a
hand-written source scanner with no receipt anywhere. Refusal deletes the
accounting and leaves the code standing - the outcome that keeps the seed
silently grown. Admission is what makes the seed honest about what already
landed. The disposition is a decision about accounting, not about admitting code.

This PR now contains two .dag files. The 413 lines of scanner are on main.

Found and corrected by deep-ant-102, who also retracted their own independent-
authorship reading rather than let it stand.

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

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my previous comment: this is not a duplicate, it is the same code, and #9102 is now only its receipt

Head 074f5cc892c. My last comment said main had an independently-written copy and the delta was +8. Both wrong. deep-ant-102 found the mechanism and retracted their own reading with it.

#9090 absorbed this branch. Its commit list carries Merge commit eb2ad48df706… into session/witty-lark-109 — that is this PR's head — along with all five scanner commits by name. It then squash-merged to main at 18:01:36Z as fd55f00bc6.

Squash carries content, not ancestry. So:

git merge-base --is-ancestor <my commits> main   →  NO for every one
git grep module_self_declared_names main         →  PRESENT, byte-identical
git grep an_alias_target_is_a_reference_out...   →  PRESENT
git grep record_field_labels_are_not_self_...    →  PRESENT

Both answers are true of different questions. Reading the ancestry answer as an authorship answer is exactly what made byte-identity look like a coincidence. One author, one lineage.

What this PR contains today

eb2ad48..HEAD →  2 files:  bare_reference_scanner_admission.dag
                            seed_growth_admission.dag (roster row)

All 413 lines of scanner are already on main. Against current main this PR adds zero hand-Rust items — production and all eight test items are there.

The receipt keeps +9 from merge base bd84f6696 as the authorship figure, because those declarations are what was written and naming them is what a receipt is for. It now states separately that the population landed by another route.

This inverts the pending disposition

Nothing in any .dag on main names module_self_declared_names. The seed today carries a hand-written source scanner with no seed-growth receipt anywhere.

So RejectedForFinalConstruction no longer removes the scanner from the seed. It deletes the accounting and leaves the code standing — the outcome that keeps the seed silently grown. Merging is what makes the seed honest about what #9090 already put in it.

The disposition is now a decision about accounting, not about admitting code, and should be made in those terms.

— sent from crisp-crab-430

…ied the judgment it refuses

Both corrections raised by the side-chat reviewer, and both are real.

1. "one terminal construction retires both" was WRONG. The scanner and the
   rust_item_census share a CLASS - a raw-text recognizer misreading language
   structure - but not a terminal construction. The scanner retires when the .dag
   front end publishes binder and declaration facts. The census retires when a
   Rust grammar authority publishes real Rust item identities. A .dag binder
   surface does not retire a regex Rust-item census. Writing them as one trigger
   discharges the second defect by implication instead of giving it an owner,
   which is how a known defect goes quiet. What they actually share is the
   architectural rule: language structure comes from the authority for that
   language, never from a parallel raw-text recognizer - two consumers, two
   triggers.

2. bare_reference_scanner_admission_authority_disposition was named for the exact
   judgment its own reason refuses to make. Its value is Terminal and it
   dispositions the RECEIPT, while the open question is whether the SCANNER gets a
   realization disposition. A declaration named admission_authority_disposition
   sitting beside that open question invites a reviewer to conclude the admission
   already exists. Renamed to bare_reference_scanner_receipt_disposition. Not
   cosmetic: the live blocker is precisely that admission.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot changed the title Do not treat lambda binders or a module's own declarations as cross-module references — and do not treat an alias target as a declaration Seed-growth receipt for the bare-reference scanner that shipped via #9090 Aug 24, 2026
gunbc-ci-auto-heal and others added 2 commits August 24, 2026 18:40
… both sides

# Conflicts:
#	dag/gunbc/seed_growth_admission.dag
…h its boundary

The realization disposition this receipt deliberately refused to grant itself has
been given: ScaffoldAdmitted, bound to 7bc6606, relayed by deep-ant-102. The
scanner is a bounded bootstrap realization serving the v2 self-host namespace
cut, NOT a retained kernel, and it deletes rather than surviving as a fallback
when parse publishes the binder and declaration facts it re-derives.

The receipt now carries the admitted scope and - more importantly - the SIX
things that are NOT admitted and each need a new disposition: additional
source-text recognizers, new callers, public-surface growth, the whole-pool
fallback, nearest-ancestor ambiguity selection, and a general hand-written
namespace parser. A receipt that recorded only the permission and not its edges
would be read later as broader than it is.

RECONCILED ONE DISCREPANCY rather than assuming it away: the verdict counts
THREE separately recorded modifications and its boundary block names TWO. The
third is project_roadmap_acceptance_event_history_from_authority_text_builtin.
Count agrees, enumeration does not, which reads as an omission in the boundary
rather than a narrowing - the verdict's own count includes it. Stated explicitly
because a future scope question judged against a two-item boundary reaches a
different answer than one judged against the three the verdict counted.

Also records why the head moving from 7bc6606 to the merge does not require
renewed admission: measured against main the delta is two .dag files, zero Rust,
one definition of module_self_declared_names. No scope growth, so the
next-reviewed-head clause applies rather than the renewed-admission clause.

The relay is recorded AS a relay. This session cannot read the operator thread,
so the carrier is named rather than the reading claimed first-hand.

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

gunbai-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Disposition received: ScaffoldAdmitted — recorded in the receipt at f923064a521

Relayed by deep-ant-102 (this session cannot read the operator thread, so the receipt records it as a relay rather than claiming a first-hand reading).

The scanner is admitted as a bounded bootstrap realization serving the v2 self-host namespace cut. Not a retained kernel. It deletes — rather than surviving as a fallback — when parse/ingestion publishes the binder and declaration facts it currently re-derives.

The receipt now carries the boundary, not just the permission:

Admitted: module_self_declared_names; the corrections inside bare_identifier_candidates and bare_reference_pull_paths_for_source; bare_reference_scanner_tests and its seven test fns.

NOT admitted — each requires a new disposition: additional source-text recognizers · new callers of the scanner · public-surface growth · the whole-pool fallback · nearest-ancestor ambiguity selection · a general hand-written namespace parser.

A receipt recording only the permission and not its edges would later be read as broader than it is.

One enumeration discrepancy, reconciled rather than assumed away

The verdict counts three separately recorded modifications; its boundary block names two. The third is project_roadmap_acceptance_event_history_from_authority_text_builtin. Count agrees, enumeration does not — which reads as an omission in the boundary rather than a narrowing, since the verdict's own count includes it. Stated explicitly because a future scope question judged against a two-item boundary reaches a different answer than one judged against the three the verdict counted.

Why the moved head does not need renewed admission

The disposition names 7bc6606; the branch has since merged main. Measured against current main:

delta:                     2 .dag files, +74/-2
Rust files differing:      0
module_self_declared_names in merged tree:  1

No Rust, no declaration, no scope growth — so this falls under the queue's next fully reviewed head clause rather than the renewed-admission clause, which governs scope growth. Independently confirmed by deep-ant-102.

Remaining: current-head checks and a current-head approval. No further operator decision blocks this PR.

— sent from crisp-crab-430

gunbc-ci-auto-heal and others added 2 commits August 24, 2026 21:06
…ecedent the log already sets

The required floor refuses RouteGapFreezeIntersection count=4: four identities
claim LegacyFrozenPathDeferral - admitted as having NO EXECUTING CONSUMER - while
carrying a floor_route_gap receipt that exists only because the required floor
consumed the row. A route-gap receipt is not evidence a witness is unreached; it
is evidence the floor reached for it and could not route to its subject, which is
a consumer having tried. Both claims cannot hold, so the freeze half is stale and
the freeze half goes.

THIS IS NOT A NEW JUDGMENT. The shrink log records the identical contradiction on
2026-08-19 against floor_expected_red - 38 identities, same reasoning, same
resolution. Same class, one roster over, so this follows the precedent rather
than inventing an arm.

WHAT INTRODUCED IT: #9049 added exactly these four identities to
floor_route_gap_roster when it deleted the mock arms they had routed through.
They were already frozen here, nothing joins the two rosters at authoring time,
so the contradiction landed green on both sides and surfaced only at floor time.

BOTH ENTRIES WERE ALREADY [partial] from the 2026-08-19 shrink: different
functions at the SAME entries now collide via a DIFFERENT roster. An entry going
partial twice by two rosters is the tell that this freeze roster is eroding one
join at a time rather than being retired by a plan, and the receipt says so.

COVERAGE IS UNCHANGED: all four stay in floor_route_gap_roster, so the floor
still carries them as typed, counted subjects. What is deleted is a second,
contradictory claim about the same identity. Remaining functions at both entries
stay frozen; the roster's monotonicity gate permits this direction only.

DECLARED GAP: expected_red_freeze_intersection walls this class for
floor_expected_red. Nothing walls the route-gap analogue, which is why #9049
could author the collision without a refusal. Until that wall exists the class is
mitigatable - caught at CI rather than unwritable - stated as a rung, not left
implicit.

Verified: gunbc compile over the edited carrier, 0 blocking errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Conflict in dag/gunbc/witness_deferral_freeze.dag, resolved to main's content.

WHY THAT SIDE. This branch carried its own copy of the RouteGapFreezeIntersection
repair -- the same four identities, at the same partial grain, at the same two
entries. #9133 landed that repair on main first. Both sides remove exactly the
same names, verified name by name, so the conflict is textual and the semantic
outcome is identical either way; taking main's side keeps ONE account of one
shrink rather than two, which is the rule that file's own note states about
hand-maintained counts.

WHAT THAT COSTS, named rather than silently dropped: this branch's shrink-log
entry was the richer of the two. It records the CAUSE (gunbc#9049 added these
four identities to floor_route_gap_roster when it removed the mock arms they had
been routing through, and nothing joined the two rosters at authoring time, so
the contradiction landed green on both sides); the TELL (both entries were
already [partial] from a 2026-08-19 shrink against a different roster -- an entry
going partial twice by two different rosters is the freeze roster being eroded
one join at a time rather than retired by a plan); and a DECLARED RUNG (no
construction wall refuses a future frozen row entering floor_route_gap_roster --
expected_red_freeze_intersection walls that class for floor_expected_red only,
which is why #9049 could author this collision without a refusal, so the class
sits at mitigatable). None of that is on main. It is worth salvaging into the
shrink log as a follow-up, and it is not merged here because appending prose to
another PR's conflict resolution is not resolving a conflict.

This branch's own subject is untouched: bare_reference_scanner_admission.dag
(+72) and seed_growth_admission.dag (+6/-2).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRMbwdtHZxTiMNZD5WLS3P
@briansrls
briansrls merged commit 05c627f into main Aug 24, 2026
1 check passed
@briansrls
briansrls deleted the fix/ambiguous-bare-reference-refuses branch August 24, 2026 22:27
gunbai-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
…five

The closed roster conflicted three ways -- import, roster list, and the
prose note -- because main added bare_reference_scanner (#9102) while this
branch added reference_closure_binder. Both sides carried five entries
sharing four. Taking either side whole would have silently deleted the
other side's authority obligation rather than conflicting, which is the
failure main's own note warns about; the union is six.

The prose note is a second representation of seed_growth_justification_roster()
and it has now gone stale once and conflicted once. Both receipts are kept in
the merged text -- the #9089 staleness and this merge's arithmetic -- so the
next reader can check the union rather than trust it. The standing fix is
still to derive the listing from the roster function.

Verified no new diagnostic: the same entry evaluated on clean origin/main in
a separate worktree produces a byte-identical error set apart from this
branch's own additions and a one-line offset. The two "expected item
declaration" errors on `//` blocks are an artifact of the local gunbc shim,
which is a Jun 26 build predating the §4c annotation channel -- established
by the control reproducing one of them on a file this branch never touches.
gunbai-bot Bot pushed a commit that referenced this pull request Aug 24, 2026
One conflict, in the seed_growth_justification roster. A closed roster is the
one shape where taking either side whole is a silent deletion of an authority,
so the resolution is verified by enumeration rather than by the merge exiting
clean: the merged roster carries all five justifications, including main's new
bare_reference_scanner row (#9102, landed on main while this branch was being
repaired).

Tree still carries zero import statements.

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
#9102 merged to main carrying its own import header, which would leave this
branch asserting a cut it had not made on the newest file in the tree. Same
treatment as the other 23: header removed, names qualified from the declaration
index. Tree back to zero import statements, and the three binder classes
(label, let, lambda) re-measured at zero.

Co-Authored-By: Claude Opus 5 (1M context) <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