Skip to content

The self-host step is an instrument target, not a flag on the unmodeled binary - #11467

Closed
briansrls wants to merge 6 commits into
mainfrom
graft-declaration-survives-spine
Closed

briansrls wants to merge 6 commits into
mainfrom
graft-declaration-survives-spine

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

gunbc test //gunbc/instruments:self-host runs the generation this repository can perform today — the seed emits v2's compiler closure, assembles it as a crate, and builds it. Measured here at 553s, exit_status=0 warning_count=0 under RUSTFLAGS="-D warnings".

Stacked on #11456 (in the merge queue), which introduced run_self_host. This PR moves it to the modeled route and withdraws the flag.

Why the route moved

The step first landed as claim_executor --self-host. That binary is the wrong home by construction: ~20 flags hand-parsed in a 2783-line main over 44,451 lines of cli_run.rs with no .dag authority at all — while gunbc's own surface is modeled (gunbc.cli_dispatch_surface → gunbc_cli_dispatch_generated.rs).

The seam that already exists for this is gunbc.target_binding TargetProducer bound to a gunbc.instrument_targets label, and its host note states the extension shape outright:

adding one is a row in instrument_registry and an arm here; it is not a new route

The flag is withdrawn in this change rather than left beside the target — two routes to one fact is the §3 fork the seam exists to prevent.

What the seam buys

The label carries its own refusal vocabulary, so an unknown or pattern operand refuses rather than reporting a pass, and the three terminations stay distinct — 0 held, 1 observation did not hold, 2 no observation — for every instrument at once rather than per flag.

That split is load-bearing here: a refusal from prepare_emitted_compiler (emit refused, crate wouldn't write, cargo couldn't run) is SubjectUnreached, not an observation that the seed cannot build v2. Collapsing them would report a broken bench as a broken compiler.

run_self_host now returns SelfHostHeld rather than printing and returning unit, so the seam decides the termination once instead of every caller re-deriving it from stderr.

Where it stops is the status

No roster accompanies it, deliberately. A list of known-good entries would be a second representation of what the instrument demonstrates by running — and it drifts exactly when nobody updates it: required_v2_emission_entries has changed once in repository history, and that change was a directory move.

A green is emission and compilation, not behavioural equivalence (that half lives in the three behavioral-receipt-* instruments beside it). It is not the second generation either: the built binary emitting the same closure is the v2 → v2 boundary, and it refuses today —

REFUSED: unknown mode compile -- expected adjudicate or census

— because v2.compiler.compile declares SourceRootEvalDriver, whose rendered main has no compile mode. That refusal is the honest next position and belongs in this instrument when a driver can reach it.

The roster documents itself

An unknown-target refusal now lists every rostered instrument, read from the same instrument_registry rows the lookup just failed against:

$ gunbc test //gunbc/instruments:what-was-it-again
gunbc test: no such target: //gunbc/instruments:what-was-it-again
  available targets:
    //gunbc/instruments:heads-reading-differential
    //gunbc/instruments:behavioral-receipt-plan
    //gunbc/instruments:behavioral-receipt-census
    //gunbc/instruments:behavioral-receipt-selftest
    //gunbc/instruments:compile-clean-diagnostic-census
    //gunbc/instruments:self-host
exit: 2

A listing therefore cannot disagree with what is invocable. DESIGN's Building & checks carries the rule (the seam; a new instrument is a row rather than a flag); the roster itself is never restated there, because a prose page naming them goes stale the first time someone adds a row and doesn't edit prose.

Witness, with the red measured

dag/test/claim/self_host_instrument_binding_witness_test.dag. Removing self_host_binding() while keeping its label — the half-added row a registry actually suffers:

claim binding present binding removed
routes through the live registry true false
every rostered label carries a binding true false
subject is the instrument's own fact true true

The routing claim asserts the producer arm, not merely the absence of a refusal — a binding naming the wrong producer routes successfully and then measures something else while reporting this target's standing.

🤖 Generated with Claude Code

gunbc-ci-auto-heal and others added 6 commits September 15, 2026 22:48
…ng, keep the name through the graft

A locally-declared record type was unresolvable anywhere in v2. Two defects in
series, each measured rather than inferred, and neither of them the generics
story the symptom first suggested.

FIRST, LOWERING NEVER NAMED IT. body_lower_type_decl gated the construction of
its Named edge on body_lower_find_where_host_conj -- a search for a `where`
refinement clause. dag_grammar_type_decl_expr admits `field_decl_block | "="
type_alias_rhs` and the where clause lives inside the alias rhs, so a record
CANNOT carry one: the guard was unsatisfiable rather than merely narrow, and
every record returned the unlowered shell. The body is reachable by emitted
identity, which is how this file already finds param lists and fn decls
(body_lower_param_binding_symbols_from_fn_captured), so the Absent arm now
looks there before giving up.

SECOND, THE GRAFT DISSOLVED THE NAME IT WAS HANDED.
namespace_graft_collect_body_edge descended through a Named edge whose target
is a "transparent sequence spine" -- a Conj whose children are all projections
-- and the label was lost in the descent. A record's field block is raw parse
structure, so it is transparent; a fn body is an arrow, so it is opaque. That
asymmetry, not the where clause, is what made alias-with-where survive while
records did not. The descent stays on the Positional arm, which is where an
anonymous spine belongs; a Named edge that is not metadata, not a projection
and not a module header is a declaration, and keeping it is what
namespace_graft_body_edges_survive_holds already specifies by asserting that
NgPilotType survives with its name.

MEASURED ON THE FULL NATIVE FOLD, 723 modules, against the same fold before the
change:

  accepted         139 -> 159   (+20)
  resolve_refused  540 -> 519   (-21)
  context_refused   43 ->  43   (unchanged, as predicted: neither fix touches
                                 the parse or body-lowering walls)
  infer_refused      1 ->   2   (+1)

Twenty-one modules changed outcome; twenty reached accepted and one
(v2.test.claim.local_binding_shadow) now passes resolve and fails later at
infer, which is forward motion rather than displacement. Nothing regressed from
accepted to refused.

Unit evidence, each with a control that must not move: RecordP goes absent ->
PRESENT as a named edge before the graft while control_fn stays PRESENT; the
symbol index goes RecordP:MISS -> HIT while AliasW:HIT and control_fn:HIT hold.
namespace_graft_body_edges_survive_holds and
coproduct_declaration_interior_is_not_retained_holds both stay true -- the
second constrains the collector in the opposite direction, so this is not a
blanket loosening. The emitted crate builds with zero warnings under
RUSTFLAGS="-D warnings", and v2 still typechecks itself (reconcile 84s against
an 82s baseline).

WHAT THIS DOES NOT CLOSE: 519 modules still refuse at resolve, so this is one
cause among several rather than the whole wall.

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

v2 hardcoded ONE pairing for the contextual test marker --
dag_grammar_test_fn_decl_expr is `ident + fn_decl` -- so `test data` matched the
ident, demanded `fn`, found `data`, and failed. No other top-level alternative
starts with an identifier, so the item failed inside the module's `repeat`, the
repeat simply ended, and the module surfaced as `parse_g0_tokens_remain`
pointing at leftover tokens rather than at a form the grammar has no rule for.

v1 does not hardcode the pairing: drop_leading_test_marker strips a leading
`test` ident before ANY keyword. So `test data` is corpus syntax v2 never caught
up to rather than a form being invented here, and the corpus uses exactly two
spellings -- `test fn` (19839) and `test data` (40).

The lowering side already handles it. body_lower_test_fn_decl_to_member is
marker-driven, not shape-driven: it checks the left child is `test` and returns
the right child whatever it is, and still refuses a non-test marker with
body_lowering_reason_test_marker_not_test. Only the grammar was narrow, so this
widens the production's right side to `fn_decl | data_decl` with fn_decl first,
leaving every one of the 19839 `test fn` declarations on the identical path.

MEASURED ON THE FULL NATIVE FOLD, against the same fold without this change:

  accepted          159 -> 161
  context_refused    43 ->  34   (-9)
  resolve_refused   519 -> 526
  infer_refused       2 ->   2
  file_refusals     224 -> 215   (-9)

  parse_g0_tokens_remain  397 -> 355  (-42 identities)

Nine modules left context_refused, all of them `test data` files: two reached
accepted and seven progressed to resolve_refused, where they meet the separate
resolve wall. Zero modules regressed from accepted to refused, and
context_refused FELL rather than rose, so the added alternative introduced no
grammar ambiguity for declarations that previously parsed. The emitted crate
still builds with zero warnings under RUSTFLAGS="-D warnings", and v2 typechecks
itself in 82s against an 82s baseline.

WHAT THIS DOES NOT CLOSE, and it is a real incompleteness rather than a caveat:
a new reason appears, normalize_reason_post_normalize_not_well_formed, at 33
identities where there were none. Those sit in modules that were already failing
at parse and now fail slightly later, so nothing regressed -- but it means
`test data` is admitted by the grammar without its lowering always producing a
well-formed tree. The form parses; it is not yet fully modeled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ype fix withdrawn for shipping without one

THE STEP. `claim_executor --self-host` runs the generation this repository can
perform today: the seed emits v2's compiler closure, assembles it as a crate,
and builds it. Pass or fail is the whole verdict and where it stops is the
status, so there is no roster of known-good entries to widen -- a roster is a
second representation of what this process demonstrates by running (DESIGN
sections 2 and 3), and it drifts exactly when nobody updates it.
`required_v2_emission_entries` has changed once in repository history, and that
change was a directory move.

It is a PREFIX of --v2-native-route rather than a second path: it calls the same
prepare_emitted_compiler and returns on its verdict, so the two modes cannot
disagree about whether the seed can build v2. The native route then spends
roughly nine further minutes executing the v2.test.* universe, which is a
different claim -- what the emitted compiler ANSWERS -- and bundling it would
price the self-host question at the cost of a question nobody asked.

Measured on this tree: emit 4 minutes, write 182 files, cargo build, exit_status=0
warning_count=0 under RUSTFLAGS="-D warnings". The interpreted alternative --
running v2.workflow.product_receipt_stage's seven boundaries through `gunbc run`
-- was killed at twenty minutes still inside boundary 3, so the modelled
boundaries are the right shape on the wrong engine.

It is a flag and not a --required-ci phase deliberately. A phase is a standing
claim on a paid runner for every push and every pull request, and that enrolment
is the operator's to make after the wall time has been watched on real heads.

THE WITNESS, which is the other half of what this commit is for.
v2.test.claim.parse.test_data_decl_parse carries the treatment (a `test data`
declaration parses), the control (a `test fn` declaration still parses, so the
widened choice did not disturb the path 19,839 declarations already take), and a
standing red (the marker followed by a form the top level does not admit still
refuses, so the choice was not widened into an absorbing accept). Discriminating
by execution: with the grammar fix the treatment returns true and without it
false, while both controls hold either way.

THE WITHDRAWAL, and the reason is the point rather than the diff. b6373b9
landed two changes -- naming a record type at lowering, and keeping that name
through the namespace graft -- with NO witness of their own. The evidence was a
probe under target/ and the fold's aggregate counts. The graft half then broke
all four v2.test.native_decl_selection claims, including the plain positive
`the_requested_declaration_is_selected_and_evaluated`: removing the
transparent-sequence-spine descent let graft SPINE SEGMENTS be collected as body
edges, and that fixture's module exists precisely because its only
collision-named edge is a spine segment. The branch I deleted was load-bearing
and I had inferred its purpose rather than established it.

The lowering half is withdrawn with it, because measurement says it does not
stand alone: with only that change RecordP becomes a named edge before the graft
and is still absent from the grafted tree, so it names a declaration without
reaching the index. Landing it would leave a half-repair whose green means less
than it appears to.

What the withdrawn work established stays on the record and is not re-derived by
the next reader: record types never reach the symbol index while functions do;
the declaration is dropped rather than merely unnamed; and the `where` clause
that appeared to discriminate is not causal -- it incidentally puts a
non-projection named child under the host, which is what makes the host opaque to
a structural test three stages later.

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

`gunbc test //gunbc/instruments:self-host` runs the generation this repository can
perform today: the seed emits v2's compiler closure, assembles it as a crate, and
builds it. Measured here at 553s with exit_status=0 warning_count=0 under
RUSTFLAGS="-D warnings".

WHY THE ROUTE MOVED. The step first landed as `claim_executor --self-host`, and
that binary is the wrong home by construction: its ~20 flags are hand-parsed in a
2783-line main over 44,451 lines of cli_run.rs with NO .dag authority at all,
while `gunbc`'s own surface IS modeled (gunbc.cli_dispatch_surface ->
gunbc_cli_dispatch_generated.rs). The seam that already exists for exactly this
is gunbc.target_binding TargetProducer bound to a gunbc.instrument_targets label,
and its host note states the extension shape outright: adding one is a row in
instrument_registry and an arm here; it is not a new route. The flag is WITHDRAWN
in this change rather than left beside the target, because two routes to one fact
is the section 3 fork this seam exists to prevent.

WHAT THE SEAM BUYS, and it is not tidiness. The label carries its own refusal
vocabulary, so an unknown or pattern operand REFUSES rather than reporting a pass,
and the three terminations stay distinct -- 0 held, 1 an observation that did not
hold, 2 no observation -- for every instrument at once rather than per flag. That
split is load-bearing here: a refusal from prepare_emitted_compiler is the subject
never having been reached (the emit refused, the crate would not write, cargo could
not run) and NOT an observation that the seed cannot build v2. Collapsing them
would report a broken bench as a broken compiler, which is the absorbing answer
DESIGN section 5 forbids.

run_self_host returns SelfHostHeld rather than printing and returning unit, so the
instrument seam decides the termination once. A producer that only printed would
make every caller re-derive the verdict from stderr, which is the second
representation section 3 forbids.

WHERE IT STOPS IS THE STATUS, which is why no roster accompanies it. A list of
known-good entries beside this instrument would be a second representation of what
it demonstrates by running, and it drifts exactly when nobody updates it:
required_v2_emission_entries has changed once in repository history and that change
was a directory move. A green here is EMISSION AND COMPILATION and not behavioural
equivalence -- that half lives in the three behavioral-receipt instruments beside
it -- and it is not the second generation: the built binary emitting the same
closure is the v2 -> v2 boundary, and it refuses today because v2.compiler.compile
declares SourceRootEvalDriver, whose rendered main answers `census` and `adjudicate`
and has no compile mode. That refusal is the honest next position and belongs in
this instrument when a driver can reach it.

THE ROSTER DOCUMENTS ITSELF. An unknown-target refusal now lists every rostered
instrument, read from the same instrument_registry rows the lookup just failed
against -- so a reader who has forgotten the label is handed it by the refusal, and
a listing cannot disagree with what is invocable. A prose page naming them would be
the second representation sections 2 and 3 price, stale the first time someone adds
a row and does not think to edit prose. DESIGN's Building & checks carries the RULE
(the seam, and that a new instrument is a row rather than a flag); the roster itself
is never restated there.

THE WITNESS, with its red measured rather than asserted. Removing self_host_binding
while keeping its label -- the half-added row a registry actually suffers -- turns
the_self_host_operand_routes_through_the_live_registry and
every_rostered_label_carries_a_binding red while the subject invariant stays green,
because that row asserts a different property. The routing claim asserts the
PRODUCER ARM and not merely the absence of a refusal: a binding naming the wrong
producer routes successfully and then measures something else while reporting this
target's standing.

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