From d761a6bbae6931afc0b1e9cf3f06395c4a98470d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 13:33:49 +0000 Subject: [PATCH 1/2] Duplicate declarations in one module bound the LAST one silently: refuse them, with a fixture-boundary RED and two live specimens repaired A module declaring one name twice was ACCEPTED with zero diagnostics, and every reference bound whichever declaration was written last. `v1.compiler.env` `symbol_index_insert` is a `map_insert` keyed on the qualified name, so the second declaration REPLACED the first in the symbol index -- two authorities for one fact (DESIGN section 3) with no refusal anywhere. Names resolving is the ordinary compiler floor, so this sat BELOW it (DESIGN section 4b): silent wrongness, outside the ladder rather than a weak rung on it. MEASURED BY EXECUTION, instrument named. Binary built from source at ddb7f533e4 (main tip), sha256 1b2eb218e6725f76..., mtime 2026-08-27T13:31:07Z -- not the installed /usr/local/bin/gunbc, which is two days stale and proves nothing about current main: fn probe_dup() -> Int { 1 } fn probe_dup() -> Int { 1 } fn probe_dup() -> Int { 2 } fn probe_dup_caller() -> Int { probe_dup() } fn probe_dup_caller() ... => 2 (the LAST definition) => 1 (control) The collision is NOT fn/fn only, which the original finding did not cover: a `data` and a `fn` of one name share the same key, and a bare reference to the data evaluated to ``. LIVE EXPOSURE, denominator stated: 4163 tracked .dag files scanned, 2 modules carried it, both repaired here. * dag/gunbc/systemctl_list_units.dag -- `data systemctl_list_units_all_states: String` shadowed by `fn systemctl_list_units_all_states(..)`, so the ListUnits `state:` argument bound the FUNCTION rather than "active,inactive,failed". A production module with two importers. The data row is renamed `systemctl_all_unit_states_selector`; the function keeps its name, which is what both importers call. * dag/test/claim/spark_managed_grant_reconcile_witness_test.dag -- one `data` row declared twice with an identical value; the second is deleted. The deletion IS the census: with the dotted-name form of the scan (`service shell.Find` is one name, not `shell` repeated) zero duplicates remain. THE WALL. `v1.compiler.resolve` `check_duplicate_declarations` folds each module's OWN item list and emits a blocking `DuplicateDeclaration` on the second declaration of one name. It reads nothing outside the module it judges -- no corpus walk, no import resolution, no type information -- so the class is decidable from one module's source, which is what makes it a wall rather than a ratchet (DESIGN section 5). Rung: R2, mechanically preventable. The R4 climb is a keyed declaration roster at ingestion that cannot HOLD two entries for one name, and that is the row's next-rung trigger. THE EVIDENCE, and where it does NOT live. The first cut of this change put the RED in `v1.compiler.compiler_tests_rust`. That carrier is DEAD: its output is `#[cfg(test)] mod compiler_tests;` inside the v1-compiler crate, while the only test package that runs is the separate v1-compiler-tests, so cfg(test) is never set for it and the block is not compiled -- a mechanism `src/v1/05_emit_rust.dag` `checkpoint_table_bypasses_identity_note` already records a prior lane discovering the same way. It was reverted, and the evidence is authored where an equivalent witness demonstrably executes: dag/test/claim/duplicate_declaration_wall_witness_test.dag, `SubstrateInputsOnly`, driving the real v1 compile over inline fixtures through `compile_dag_rust_emit_check`, following `checkpoint_identity_keying_witness_test.dag`. Two REDs (fn/fn and data/fn) each paired with a control that differs from it by exactly the duplicated declaration and nothing else -- `compile_dag_rust_emit_check` answers false for ANY hard diagnostic, so the discrimination is carried by the PAIR, not by either row alone. All four measured `true` on the post-wall binary (sha256 1659e35605...), and per DESIGN 4b(4) the controls stay enrolled: they are the executing evidence that the rung is real. THE BOUND THAT MATTERS MOST IS NOT THE COMPILER'S, and it is written into both the witness and the gap-analysis row: where two definitions of one name are semantically EQUIVALENT, no behavioural witness can discriminate them whichever wins. Green-by-execution proves BEHAVIOUR and is structurally blind to duplicate AUTHORSHIP -- which this repository leans on everywhere, and which is why the live specimens above passed every witness over them. Also here: the DESIGN 4b row in docs/plans/compiler-guarantee-recovery-gap-analysis.md (invalid state, harm, distinguishing facts vs the cross-module last-import-wins row, rung, ceiling with reason, next trigger); a prose correction on dag/test/claim/duplicate_definition_binding_probe.dag, which asserted this refusal before it existed and is still `DeclinedLiveTree`, so what flipped is its SUBJECT and not its reach; and the fourth-lane receipt in `v1.std.core` `compiler_diagnostic_seed_projection_note`, which its own text obliges whenever the diagnostic coproduct grows -- cli_run.rs fn census FLAT at 895 on both sides, 2 added / 0 removed, 0 added lines declaring a fn. v1 SEED PURPOSE, since the change touches the seed: the wall is a floor repair on the name-resolution path the v2 self-host program compiles through, and the two repaired specimens are corpus modules that self-host must emit correctly. Nothing here grows the seed's capability -- the two cli_run.rs lines are the mechanical consequence of the coproduct gaining a member. Co-Authored-By: Claude Opus 5 (1M context) --- dag/gunbc/systemctl_list_units.dag | 4 +- ...uplicate_declaration_wall_witness_test.dag | 101 ++++++++++++++++++ .../duplicate_definition_binding_probe.dag | 19 ++++ ...k_managed_grant_reconcile_witness_test.dag | 1 - ...ompiler-guarantee-recovery-gap-analysis.md | 1 + src/v1/00_core.dag | 5 +- src/v1/03_resolve.dag | 35 +++++- src/v1/stage0/src/cli_run.rs | 2 + src/v1/stage0/src/v1_compiler_resolve.rs | 62 ++++++++++- src/v1/stage0/src/v1_std_core.rs | 9 +- 10 files changed, 230 insertions(+), 9 deletions(-) create mode 100644 dag/test/claim/duplicate_declaration_wall_witness_test.dag diff --git a/dag/gunbc/systemctl_list_units.dag b/dag/gunbc/systemctl_list_units.dag index 0b90ac41aea..28d33907327 100644 --- a/dag/gunbc/systemctl_list_units.dag +++ b/dag/gunbc/systemctl_list_units.dag @@ -54,10 +54,10 @@ fn systemctl_list_units_active_services(instance_glob: NonEmptyStr) -> List String? { - let result = systemd.Systemctl.ListUnits(pattern: instance_glob, state: systemctl_list_units_all_states) + let result = systemd.Systemctl.ListUnits(pattern: instance_glob, state: systemctl_all_unit_states_selector) if result.success { Present { value: result.stdout } } else { diff --git a/dag/test/claim/duplicate_declaration_wall_witness_test.dag b/dag/test/claim/duplicate_declaration_wall_witness_test.dag new file mode 100644 index 00000000000..fa49f336328 --- /dev/null +++ b/dag/test/claim/duplicate_declaration_wall_witness_test.dag @@ -0,0 +1,101 @@ +module test.claim.duplicate_declaration_wall_witness + +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import std.types { String, Bool } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE WALL THIS ROW GUARDS, and why it exists at the FIXTURE boundary rather than over the corpus. +// A module declaring one name TWICE was accepted with ZERO diagnostics and the SECOND declaration +// silently won: `v1.compiler.env` `symbol_index_insert` is a `map_insert` keyed on the qualified +// name, so the second write REPLACED the first and every reference bound the survivor. Measured by +// execution through our own binary before the wall landed: a module with `fn probe_dup() -> Int { 1 }` +// then `fn probe_dup() -> Int { 2 }` ran its caller to 2, and the one-definition control to 1 -- +// same harness, duplication the only variable. `v1.compiler.resolve` `check_duplicate_declarations` +// now refuses it with a blocking `DuplicateDeclaration`. +// +// WHY THE RED IS AUTHORABLE HERE AND NOWHERE ELSE (DESIGN 4b): no `Accepted` module may now carry +// the state, so the refusal is unreachable from the corpus -- but a fixture hands the compiler +// source text, and a compiler's regression probes are invalid programs by definition. Declining to +// author it because the accepted tree cannot hold it would be the specification-without-execution +// failure, not a saving. +// +// THE BOUND THIS ROW EXISTS TO STATE, and it is about the repository's evidence discipline rather +// than about the compiler: where two definitions of one name are semantically EQUIVALENT, no +// behavioural witness can discriminate them whichever one wins. Green-by-execution proves +// BEHAVIOUR and is structurally blind to duplicate AUTHORSHIP. That is why this row is a +// compile-seam refusal and not a runtime probe, and it is why the live specimens this wall's +// landing repaired had passed every witness over them. +// +// WHAT THIS SURFACE OBSERVES, stated so no receipt reads more coverage than it has: +// `compile_dag_rust_emit_check` answers false when the compile carries ANY hard diagnostic, so a +// red row alone does not establish WHICH judgment refused. The discrimination is carried by the +// PAIR: each red fixture and its green control differ by exactly the duplicated declaration and +// nothing else, so a red that survives its control's green is a red caused by the duplication. +// The path measured is source -> `.dag` acceptance through the v1 pipeline to the Rust render +// target, single module. NOT measured here: the interpreter's disposition of the same source, and +// cross-module name collisions, which are a different class (two keys, not one). + +fn duplicate_fn_source() -> String { + "module test.claim.duplicate_declaration_wall_duplicate_fn\nimport std.types { Int }\n\nfn which_one() -> Int { 1 }\n\nfn which_one() -> Int { 2 }\n" +} + +fn single_fn_source() -> String { + "module test.claim.duplicate_declaration_wall_single_fn\nimport std.types { Int }\n\nfn which_one() -> Int { 1 }\n" +} + +// The collision is NOT fn/fn only. A `data` and a `fn` of one name share one symbol-index key, and +// that shape had LIVE exposure on main: `dag/gunbc/systemctl_list_units.dag` declared +// `data systemctl_list_units_all_states: String` and `fn systemctl_list_units_all_states(..)`, so +// the reference intended for the string bound the FUNCTION. Repaired in the same change that +// landed the wall. +fn duplicate_data_and_fn_source() -> String { + "module test.claim.duplicate_declaration_wall_duplicate_kind\nimport std.types { Int }\n\ndata thing: Int = 1\n\nfn thing() -> Int { 2 }\n" +} + +fn distinct_data_and_fn_source() -> String { + "module test.claim.duplicate_declaration_wall_distinct_kind\nimport std.types { Int }\n\ndata thing: Int = 1\n\nfn other() -> Int { 2 }\n" +} + +// THE DISCRIMINATING REDS. + +test fn two_function_declarations_of_one_name_are_refused() -> Bool { + compile_dag_rust_emit_check( + duplicate_fn_source(), + "src/test_claim_duplicate_declaration_wall_duplicate_fn.rs", + [], + [] + ) == false +} + +test fn a_data_and_a_function_sharing_one_name_are_refused() -> Bool { + compile_dag_rust_emit_check( + duplicate_data_and_fn_source(), + "src/test_claim_duplicate_declaration_wall_duplicate_kind.rs", + [], + [] + ) == false +} + +// THE POSITIVE CONTROLS. Each is its red's fixture with the duplication removed and nothing else +// changed, so a red above cannot pass by its fixture being broken some other way. Per DESIGN 4b(4) +// these stay enrolled after the wall lands: they are the executing evidence that the rung is real, +// and deleting them would recreate specification-without-execution one rung up. + +test fn one_function_declaration_compiles_clean() -> Bool { + compile_dag_rust_emit_check( + single_fn_source(), + "src/test_claim_duplicate_declaration_wall_single_fn.rs", + ["which_one"], + [] + ) +} + +test fn a_data_and_a_function_with_distinct_names_compile_clean() -> Bool { + compile_dag_rust_emit_check( + distinct_data_and_fn_source(), + "src/test_claim_duplicate_declaration_wall_distinct_kind.rs", + ["other"], + [] + ) +} diff --git a/dag/test/claim/duplicate_definition_binding_probe.dag b/dag/test/claim/duplicate_definition_binding_probe.dag index 5b782faeaf0..9a4229f34be 100644 --- a/dag/test/claim/duplicate_definition_binding_probe.dag +++ b/dag/test/claim/duplicate_definition_binding_probe.dag @@ -59,6 +59,25 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // `test fn` collides with a plain `fn` of the same name. Each is its own row when someone // measures it; none is claimed by this one. +// THE WALL LANDED (2026-08-27, this lane). `v1.compiler.resolve` `check_duplicate_declarations` +// folds each module's own item list and emits a blocking `DuplicateDeclaration` on the second +// declaration of one name, so the two rows below assert the behaviour the compiler now has rather +// than the behaviour it lacked. THE ROW STILL DOES NOT EXECUTE HERE: the disposition above is +// unchanged and this site is still `DeclinedLiveTree`, so what flipped is the SUBJECT, not this +// row's reach -- the execution trigger recorded above is still the only thing that retires it. +// The executing evidence for the wall is `duplicate_declaration_wall_witness` in +// `v1.compiler.compiler_tests_rust`, at the fixture boundary; that harness runs LOCALLY only, +// which is stated in the gap-analysis row rather than claimed as CI coverage here. +// WHERE THE WALL SITS vs where this row said it should: at RESOLVE, over each module's item list, +// not at ingestion. It reads nothing outside the module it judges, so the "no corpus walk" +// property that made the class decidable is preserved; what is NOT yet done is the R4 climb the +// row above implies, a keyed declaration roster at ingestion that cannot HOLD two entries for one +// name. That remains this class's next-rung trigger. +// TWO SHAPES THIS ROW STILL DOES NOT MEASURE, and the second had LIVE exposure: a `data` and a +// `fn` sharing one name (the wall refuses it; measured on `dag/gunbc/systemctl_list_units.dag`, +// where a bare reference intended for a `data` String bound the function instead), and the Rust +// emission path. Both are covered by the seed witness, neither by this module. + fn census_of(source: String) -> CompileDiagnosticCensus { compile_dag_diagnostic_census(source) } diff --git a/dag/test/claim/spark_managed_grant_reconcile_witness_test.dag b/dag/test/claim/spark_managed_grant_reconcile_witness_test.dag index 2bfb91f76c5..05526a0a98f 100644 --- a/dag/test/claim/spark_managed_grant_reconcile_witness_test.dag +++ b/dag/test/claim/spark_managed_grant_reconcile_witness_test.dag @@ -249,7 +249,6 @@ test fn the_listing_banner_is_not_read_as_an_entry() -> Bool { // It is not written against #8860's variant shape, deliberately: it reads only the population's // rendered command strings, so it compiles identically before and after that PR and measures the // one fact the ordering depends on. -data measured_installed_linger_command: String = "/usr/bin/loginctl enable-linger gunbc-automation" test fn the_modeled_linger_command_is_byte_equal_to_the_line_the_hosts_hold() -> Bool { any(spark_managed_grant_population, g => diff --git a/docs/plans/compiler-guarantee-recovery-gap-analysis.md b/docs/plans/compiler-guarantee-recovery-gap-analysis.md index 1c3ff0fab84..82a7aa84638 100644 --- a/docs/plans/compiler-guarantee-recovery-gap-analysis.md +++ b/docs/plans/compiler-guarantee-recovery-gap-analysis.md @@ -175,6 +175,7 @@ unless the row says otherwise, and `Unknown` is the honest default. | Direct-call argument TYPE conformance — **applied-generic alias**, v2 corpus | **Below floor — 8 live false refusals, structurally unreachable from the comparison seam.** An alias whose right-hand side is a generic application (`type BlackConfigPatch = ConfigPatchRecord`) refuses at every call site passing it to the matching formal. Specimen, measured by execution 2026-08-22 on a fixture diffed byte-for-byte against the live `v2.std.patch` declaration, with the exemption deleted in the Rust MIRROR and `exempt=judged` on all 20352 ledger rows as the control: `formal_type = Primitive(ConfigPatchRecord)`, `actual_type = Node(BlackConfigPatch)`, `nominal=true container=false kernel=false`, `fname=ConfigPatchRecord aname=BlackConfigPatch`. Population: 9 declarations, all under `src/v2/extdeps/formatters/` (`black`, `clang_format`, `gofmt`, `google_java_format`, `ktfmt`, `lean4_format`, `prettier`, `rustfmt`, `swift_format`), of which 8 produced diagnostics in the CI arm — `lean4_format` is structurally identical and produced none in EITHER arm, so the population is 9 and the measured count is 8+k | R2, and it is decidable — the same equality already accepts the applied form when it is written directly at the formal | **THE FORMAL SIDE IS WHERE THE INFORMATION DIES, and that is the sentence that should stop the next attempt from this direction.** The formal is DECLARED `ConfigPatchRecord` and REACHES the comparison as the bare constructor, with the type argument already discarded. So the equality has one side that structurally cannot hold an argument, and the case split over any representative is EXHAUSTIVE: (a) representative = the applied form with every argument retained never equals the bare constructor, so it admits nothing and all 9 sites keep refusing; (b) representative = the bare constructor admits `ConfigPatchRecord` at `ConfigPatchRecord` for any X and Y, which is exactly the over-peel that erases the distinction. There is no third representative, and widening the carrier does not help — a node-valued map fails identically, because the missing information is not on the relation's side of the map. **Not to be confused with gunbc#8879**, the withdrawn repair that widened transparency AT RESOLVE and was refused by CI in three classes at once (20 direct-call comparisons, 18 variant projections, 3 files of regen drift). What that established is that resolve is the wrong place for a transparency JUDGMENT; preserving an applied form is resolve DISCARDING LESS, which hands the same consumers MORE structure, and is a different claim that has not been measured | executed fixture + shadow ledger with a positive control (`black.dag` must refuse; a zero there is decidably wrong). **NOT ENROLLED**: no probe pair asserts this class, so nothing reds if it changes. Three false zeros were produced while measuring it — twice from patching the `.dag` authority while the binary is built from the emitted Rust mirror, once from a run that PANICKED and reported zero diagnostics — each caught only by a positive control or by another session's raw per-line export, never by an aggregate | **RESOLVE CARRIES APPLIED FORMS AT THE FORMAL POSITION.** Explicitly NOT scoped as of 2026-08-22 (operator-lane ruling, swift-badger-524): a bounded floor-recovery lane whose completion condition has moved is closed honestly rather than extended, and nothing in that ruling says the change is wrong. Until it lands, the `v2.` exemption stays whole and gunbc#8886 stays blocked | | Producer/consumer cardinality | **UnknownUnmeasured** (typed-rejection vs silent-degeneration split unmeasured) | R3 (seam unwritable) | forgeable carrier; no signature propagation (`sole_constructor` audit pending) | §4b | Stage-3 vertical slice | | Refinement predicate (`where`) enforcement at construction | **Writable-and-unverified, one row spanning both measured SOURCE-FORM paths (declared subject grain — reconstruction is NOT in this row's grain; see the scope note at the end of this cell).** Source→v1-interpretation and source→v1-Rust-emission were independently measured and found identical — zero predicate/refinement check on either, no scope split between them. `v1_interpreter` `cast_identity_result` (the `as`-cast identity path) checks only that the underlying kernel name matches; it carries no refinement/predicate concept at all. `05_emit_rust` `render_rust_alias_rhs_type` peels a refinement node to its base type before emission (`is_where_refinement_type` true branch) and discards the predicate list — a **deliberate written branch**, not incidental loss: `04_infer`'s `is_where_refinement_type`/`where_refinement_chain`/`peel_where_refinement_base` carry the predicate live through typecheck, so the data exists at the emit boundary and is thrown away there. Where safety exists today for a refined type (e.g. `PathSegment = NonEmptyStr where brand("PathSegment")`) it comes **entirely from hand-authored per-callsite guards**, not the carrier — `gunbc.merge_admission_subject` `walk_attempt_id` and `gunbc.fleet_known_hosts_anchor` `fleet_ssh_attempt_identity` (mirrored in `claim_executor.rs`) each call `path_segment_is_safe` before casting. This is DESIGN §5's named tell, validation standing where construction was available: the law is declared once (`path_segment_safety_note`) and re-stated as a check every author must remember to write, never tied to the carrier itself. One construction form already bypasses even that discipline: `FilePathParts { segments: [...] }` struct literals (`extdeps.rust.cargo` `cargo_target_source_path`/`rust_module_candidate_paths`, `extdeps.linux.proc_self_cgroup`) coerce raw strings to `PathSegment` with no predicate call at all. A targeted search for an *existing* production site where a refined-type construction actually feeds a predicate-violating value through to a consumer (not manufactured) found none: the two real branded-`PathSegment` callers both guard, and the one `FilePathParts` caller with non-literal input has zero production callers (test-fixture only). A below-floor ("silent wrongness, demonstrated") claim was raised on this evidence and explicitly withdrawn — the class is confirmed writable and the one candidate gap is confirmed currently unexercised, not confirmed violated. **SCOPE NOTE, AND IT IS LOAD-BEARING: that withdrawal is scoped to the two SOURCE-FORM paths this row measures, and it has since been overtaken on a third path that this row does not cover.** The reconstruction seam — `recorded_fixture` `value_from_fixture_json` on replay, and `map_response_to_value_json` on live REST — builds runtime records and scalars from external JSON without passing through a source record literal or cast, so neither `cast_identity_result` nor `render_rust_alias_rhs_type` is on it and neither measurement above reaches it. On THAT path below-floor is not a withdrawn hypothesis but an EXECUTED demonstration: a predicate-violating value was silently accepted and consumed, and on the live-REST arm with no tampering of any kind. **The grain of that demonstration, stated so this cell cannot be read as more than it is:** it was executed against a PURPOSE-BUILT probe service, not against a production caller. What is established is that the MECHANISM silently admits and consumes violating values on an untampered path; what is NOT established is that any production field has actually received one. The production population sitting on that path is measured only as a declaration count (output-block fields typed as a refined alias), and none of those was executed — so read it as mechanism-demonstrated, population-unmeasured, never as demonstrated-in-production. It is intended to be carried as its own row rather than folded into this one, per §4b's one-row-per-structurally-distinct-path rule. **FORWARD REFERENCE, NOT A PRESENT FACT:** that row is authored in a separate in-flight change (gunbc#8658, reconstruction doors, findings-only) and is NOT part of this document as of this cell's landing. If this row merges first, the reconstruction row is pending rather than present, and this sentence is a statement of intent — do not cite it as evidence that the reconstruction path is documented, and do not let the absence of that row be read as the path having been cleared. **Do not read this cell as the class verdict**: per §4b a class's rung is the MINIMUM across its in-scope paths, so if the enclosing class is taken as "refinement enforcement at construction" — reconstruction being a construction path — the governing rung is the reconstruction row's, not this one's. Citing this row's writable-and-unverified while that path stayed silent is exactly the inflation §4b names | R2-shaped on the Rust target (newtype + validating constructor closes construction-time) | Bounded by what the **target** can express, not by what `.dag` knows: `.dag`'s own infer stage already carries the predicate live through typecheck, so the ceiling here is a target-capability question — the bare-alias Rust realization used today has no ceiling above writable-and-unverified regardless of source-side modeling quality; nothing on either measured path ties the declared law to the carrier, so enforcement is 100% per-callsite discipline, which has a zero-frequency-until-violated failure mode (the `FilePathParts` gap is exactly this: zero frequency because it has no caller yet, not because it is guarded) | node://adhoc-897a90b6-a9c items 1–2, 2026-08-20 (merry-tern-237/fierce-ant-91): 219 `where`-declared types found in `.dag` (`grep -rn '^type .* where ' dag/`), 49 confirmed reaching Rust as bare aliases with zero predicate residue (spot-checked `std_types.rs`); emission mechanism confirmed by reading `05_emit_rust` `render_rust_alias_rhs_type` against `04_infer` `is_where_refinement_type`; interpreter path confirmed by reading `v1_interpreter` `cast_identity_result`/`eval_cast`; violation search executed against `PathSegment` construction sites corpus-wide, empty result | a Rust-target realization for refined types (newtype + validating constructor, or equivalent) that ties the declared predicate to the emitted carrier once, replacing per-callsite discipline; the `sole_constructor` completeness audit (in flight, separate lane) is the adjacent unforgeable-construction mechanism this would likely compose with | +| Duplicate top-level declaration in ONE module | **Was BELOW FLOOR — silent, with LIVE exposure; wall landed in this change.** INVALID STATE: one module declaring one name twice. HARM: `v1.compiler.env` `symbol_index_insert` is a `map_insert` on the qualified name, so the second declaration REPLACED the first and every reference bound the last one written, with no diagnostic of any severity. MEASURED BY EXECUTION through our own binary before the wall: `fn probe_dup() -> Int { 1 }` then `fn probe_dup() -> Int { 2 }` ran the caller to `2`; the single-definition control ran to `1`; same harness, duplication the only variable. The collision is NOT fn/fn only — a `data` and a `fn` of one name share the key, and a bare reference to the data yielded ``. DISTINGUISHING FACT vs the census-ambiguous last-import-wins row above: that one is ACROSS modules, this one is INSIDE one, so it needs no corpus walk and no import resolution — it is decidable from one module's own item list. LIVE EXPOSURE, denominator stated: a scan of 4163 authored `.dag` files found 2 modules carrying it — `dag/gunbc/systemctl_list_units.dag` (`data systemctl_list_units_all_states` shadowed by `fn systemctl_list_units_all_states`, so the ListUnits `state:` argument bound the FUNCTION, not the `"active,inactive,failed"` string) and `dag/test/claim/spark_managed_grant_reconcile_witness_test.dag` (one `data` row declared twice with an identical value). Both are repaired in this change; the deletion IS the census. THE BOUND THAT MATTERS MOST is not the compiler's: where the two definitions are semantically EQUIVALENT no behavioural witness can discriminate them whichever wins, so green-by-execution — which this repository leans on everywhere — is STRUCTURALLY blind to duplicate authorship. That is why the evidence is a compile-seam refusal and not a runtime probe. | R2 (mechanically preventable) reached; R4 (unwritable) is the honest ceiling only once ingestion carries a keyed declaration roster that cannot hold two entries for one name | the refusal is a fold over the module's item list in `v1.compiler.resolve` `check_duplicate_declarations`, not a property of the carrier: the duplicate is still AUTHORABLE and is refused after it is built | wall: `v1.compiler.resolve` `check_duplicate_declarations` emitting `DuplicateDeclaration` (blocking, default arm of `is_interpreter_blocking_diagnostic`). RED + positive control: `duplicate_declaration_wall_witness` in `v1.compiler.compiler_tests_rust`, authored at the FIXTURE boundary (two REDs — fn/fn and data/fn — and a green control on distinct names). ENROLMENT, stated rather than inherited: the Rust suite was removed from CI 2026-07-11, so that witness executes LOCALLY only and guards nothing on a push; `dag/test/claim/duplicate_definition_binding_probe.dag` asserts the same refusal in `.dag` and is `ReadsLiveTree`, hence `DeclinedLiveTree` in the required floor — discovered, counted, never run. So the wall is real and its EVIDENCE is unenrolled, which is a weaker claim than R2-with-a-guard and is written that way deliberately | next trigger, two and separable: (a) an enrolled route for a compiler FIXTURE red — either the `DeclinedLiveTree` arm's deletion (which `v2.workflow.required_floor` already carries as staged) flipping the `.dag` probe into the floor, or a hermetic census that does not walk the live tree; (b) for the R4 climb, an ingestion-time keyed declaration roster (`v1_compiler.declaration_index` is the nearest existing construction) | **Domain: structural safety extensions (the differentiator)** diff --git a/src/v1/00_core.dag b/src/v1/00_core.dag index 65988c59820..3d292a7807d 100644 --- a/src/v1/00_core.dag +++ b/src/v1/00_core.dag @@ -176,6 +176,7 @@ type CompilerDiagnostic | NonExhaustiveMatch { missing: List, span: SourceSpan } | CircularDependency { modules: List, span: SourceSpan } | DuplicateModule { name: String, span: SourceSpan } + | DuplicateDeclaration { module_name: String, name: String, span: SourceSpan } | MissingAnnotation { fn_name: String, what: String, span: SourceSpan } | ParseError { message: String, span: SourceSpan } | InternalError { message: String, span: SourceSpan } @@ -327,6 +328,7 @@ fn diagnostic_to_span(d: CompilerDiagnostic) -> SourceSpan { NonExhaustiveMatch { missing: _, span: s } => s CircularDependency { modules: _, span: s } => s DuplicateModule { name: _, span: s } => s + DuplicateDeclaration { module_name: _, name: _, span: s } => s MissingAnnotation { fn_name: _, what: _, span: s } => s ParseError { message: _, span: s } => s InternalError { message: _, span: s } => s @@ -390,6 +392,7 @@ fn diagnostic_to_message(d: CompilerDiagnostic) -> String { NonExhaustiveMatch { missing: ms, span: _ } => concat("non-exhaustive match: missing variant(s) ", join(ms, separator: ", ")) CircularDependency { modules: ms, span: _ } => concat("circular dependency detected: ", join(ms, separator: " -> ")) DuplicateModule { name: n, span: _ } => concat("duplicate module declaration: '", n, "'") + DuplicateDeclaration { module_name: m, name: n, span: _ } => concat("duplicate declaration '", n, "' in module '", m, "' -- a second declaration of one name silently replaced the first") MissingAnnotation { fn_name: f, what: w, span: _ } => concat("function '", f, "' requires ", w, " annotation") ParseError { message: m, span: _ } => m InternalError { message: m, span: _ } => m @@ -468,7 +471,7 @@ fn is_where_refinement_unenforced_advisory_reason(reason: String) -> Bool { || reason == "string predicate not implemented" } -data compiler_diagnostic_seed_projection_note: String = "HAND-RUST GATE receipt for the CompilerDiagnostic seed projection (codex reviews 45469, 45481). Adding a variant to this coproduct forces arms in two TOTAL matches in the hand-maintained seed transport, cli_run.rs's compile_clean_diagnostic_histogram_key and its method-name extractor; without them the seed does not compile, so the arms are the mechanical consequence of the .dag change, not host-side capability someone chose to write in Rust. THIS IS A DIFFERENT CLASS FROM THE GATE'S USUAL SUBJECT and the distinction is the whole receipt: the gate's other explicit deferrals — the emit-surface retirement rows — are DECISION SURFACES that could live in .dag and are deferred for a stated reason, so they owe a dissolution schedule of their own. This clause used to name cli_run::selection_control_input_sources alongside them; that symbol was DELETED with affected-set selection (2026-08-15), so the deferral it recorded was discharged by deletion rather than by a schedule, and the citation was left naming nothing — the DESIGN section 3 stale-citation class, inside a receipt whose own argument is that an unverifiable figure rots silently. An exhaustiveness arm owes none, because it cannot live anywhere but the seed's projection of the coproduct, and it disappears exactly when the seed does. CHECKABLE RECEIPT for the FIVE variants this lane adds (MethodNotFound, MethodExistenceUndecided, MethodExistenceFrontierAdmitted, ReceiverTypeUnestablished, FrontierOccurrenceBudgetExceeded). It read SIX until the unjudged conformance advisory was excluded from this PR (codex review 45767); DeclaredTypeConformanceUnjudged and its arms are gone with it, which is why the figures below moved again. Two commands, and the receipt is exactly their output — EACH FIGURE NAMES THE COMMAND THAT PRODUCES IT, because a number with no command behind it cannot be checked and rots silently. (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 747 at origin/main and 747 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 23 added / 0 removed, and `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line is inside an existing fn and none declares one. The 23 lines are the five diagnostic arms plus the advisory-classification entries described next. THE NUMBERS IN A RECEIPT MUST TRACK THE DIFF THEY CERTIFY: this row read five variants and 13 lines after a sixth variant landed (codex review 45501), and then read a fn census of 742 that NO command reproduced — 501 bare `fn` and 245 `pub fn` on either side, never 742 — so the figure certifying that hand-Rust had not grown was itself unverifiable. A receipt whose number cannot be re-derived is indistinguishable from one that is wrong. Re-derive all three figures from the commands above whenever this coproduct changes, rather than carrying them forward. THE ADVISORY CLASSIFICATION IS PART OF THIS RECEIPT AND WAS THE DEFECT IT ALMOST HID: compile_clean_diagnostic_is_advisory is a CLOSED ALLOWLIST, not the complement of is_hard, so the two non-blocking variants this lane adds were counted by NEITHER predicate — rendered to the terminal while the gate reported zero of them. The residue was described as counted while nothing in the repository counted it, and the population figures quoted in review came from a grep over log text rather than from any mechanism (found by executing the gate before and after and seeing its advisory total sit unchanged at 4590 while the printed population halved). Adding a non-blocking variant to the coproduct therefore obliges a matching entry in that allowlist, or the frontier it represents is invisible to the gate that is supposed to bound it. Lane: compiler-static-failure-closure (v1-method-existence-wall / v1-declared-type-conformance-wall). Dissolves with the seed itself, ROADMAP hand-MAINTAINED to zero at v2 self-host; no separate trigger, because there is no separable work to schedule. Not migration debt and not a delete candidate. SECOND LANE, SECOND EXPANSION (review 55680, finding 2). ImportShadowedByLocalDefinition is a SIXTH variant, added by a different lane for the import-shadow wall, and it forces one arm in each of the same two total matches. The five-variant figures above were measured against THAT lane's base and do not reproduce here -- (a) alone now answers 884 rather than 747 -- which is the rot this receipt exists to make visible: a figure survives its own truth by naming a command nobody re-runs. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 884 at origin/main and 884 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 4 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 4 lines are this variant's two arms, one per total match, as rustfmt renders them. NO HOST CAPABILITY IS ADDED: both arms are the mechanical consequence of the .dag coproduct gaining a member, and they disappear exactly when the seed does. THIRD LANE, THIRD EXPANSION (codex review 56224, finding 1). DataReferenceVisibilityBudgetExceeded and ParameterDefaultFormNotAdmitted are a SEVENTH and EIGHTH variant, added by the typed-data-reference-refusal lane so that the three terminal arms of v1.compiler.emit_rust validate_workflow_param_defaults stop rendering three distinct causes as InternalError. Each forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 884 was that lane's base, this lane's base is 890, and a receipt that edited the earlier number in place would destroy the evidence that a figure moves per lane -- which is the entire argument this row makes. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 890 at origin/main and 890 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 8 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 8 lines are this lane's FOUR arms -- two variants, one arm each in both total matches -- as rustfmt renders them across braced blocks. WHY EIGHT LINES FOR FOUR ARMS AND NOT FOUR: three of the four arms exceed the line width once the variant name is spelled, so rustfmt renders them as braced blocks; the count is a property of the formatter, not of added capability, which is exactly why (c) is measured beside it rather than inferred from (b). THE THIRTEEN LINES IN v1_std_core.rs ARE NOT PART OF THIS RECEIPT AND MUST NOT BE ADDED TO IT: that file is the GENERATED mirror of this module, emitted by required-regen from the .dag authority above, so its growth is authored here in .dag and is not hand-Rust surface. Counting it would be the same category error as counting compiler_tests.rs, which the plan projection already rules out for the same reason. NEITHER NEW VARIANT IS NON-BLOCKING, so the advisory-allowlist obligation this row records does not fire: is_interpreter_blocking_diagnostic answers true by default and neither variant is listed as an exception, which is correct -- both replace a blocking InternalError at a refusal site, and the witness measures the ambiguity arm as one BLOCKING AmbiguousReference row rather than an advisory one." +data compiler_diagnostic_seed_projection_note: String = "HAND-RUST GATE receipt for the CompilerDiagnostic seed projection (codex reviews 45469, 45481). Adding a variant to this coproduct forces arms in two TOTAL matches in the hand-maintained seed transport, cli_run.rs's compile_clean_diagnostic_histogram_key and its method-name extractor; without them the seed does not compile, so the arms are the mechanical consequence of the .dag change, not host-side capability someone chose to write in Rust. THIS IS A DIFFERENT CLASS FROM THE GATE'S USUAL SUBJECT and the distinction is the whole receipt: the gate's other explicit deferrals — the emit-surface retirement rows — are DECISION SURFACES that could live in .dag and are deferred for a stated reason, so they owe a dissolution schedule of their own. This clause used to name cli_run::selection_control_input_sources alongside them; that symbol was DELETED with affected-set selection (2026-08-15), so the deferral it recorded was discharged by deletion rather than by a schedule, and the citation was left naming nothing — the DESIGN section 3 stale-citation class, inside a receipt whose own argument is that an unverifiable figure rots silently. An exhaustiveness arm owes none, because it cannot live anywhere but the seed's projection of the coproduct, and it disappears exactly when the seed does. CHECKABLE RECEIPT for the FIVE variants this lane adds (MethodNotFound, MethodExistenceUndecided, MethodExistenceFrontierAdmitted, ReceiverTypeUnestablished, FrontierOccurrenceBudgetExceeded). It read SIX until the unjudged conformance advisory was excluded from this PR (codex review 45767); DeclaredTypeConformanceUnjudged and its arms are gone with it, which is why the figures below moved again. Two commands, and the receipt is exactly their output — EACH FIGURE NAMES THE COMMAND THAT PRODUCES IT, because a number with no command behind it cannot be checked and rots silently. (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 747 at origin/main and 747 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 23 added / 0 removed, and `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line is inside an existing fn and none declares one. The 23 lines are the five diagnostic arms plus the advisory-classification entries described next. THE NUMBERS IN A RECEIPT MUST TRACK THE DIFF THEY CERTIFY: this row read five variants and 13 lines after a sixth variant landed (codex review 45501), and then read a fn census of 742 that NO command reproduced — 501 bare `fn` and 245 `pub fn` on either side, never 742 — so the figure certifying that hand-Rust had not grown was itself unverifiable. A receipt whose number cannot be re-derived is indistinguishable from one that is wrong. Re-derive all three figures from the commands above whenever this coproduct changes, rather than carrying them forward. THE ADVISORY CLASSIFICATION IS PART OF THIS RECEIPT AND WAS THE DEFECT IT ALMOST HID: compile_clean_diagnostic_is_advisory is a CLOSED ALLOWLIST, not the complement of is_hard, so the two non-blocking variants this lane adds were counted by NEITHER predicate — rendered to the terminal while the gate reported zero of them. The residue was described as counted while nothing in the repository counted it, and the population figures quoted in review came from a grep over log text rather than from any mechanism (found by executing the gate before and after and seeing its advisory total sit unchanged at 4590 while the printed population halved). Adding a non-blocking variant to the coproduct therefore obliges a matching entry in that allowlist, or the frontier it represents is invisible to the gate that is supposed to bound it. Lane: compiler-static-failure-closure (v1-method-existence-wall / v1-declared-type-conformance-wall). Dissolves with the seed itself, ROADMAP hand-MAINTAINED to zero at v2 self-host; no separate trigger, because there is no separable work to schedule. Not migration debt and not a delete candidate. SECOND LANE, SECOND EXPANSION (review 55680, finding 2). ImportShadowedByLocalDefinition is a SIXTH variant, added by a different lane for the import-shadow wall, and it forces one arm in each of the same two total matches. The five-variant figures above were measured against THAT lane's base and do not reproduce here -- (a) alone now answers 884 rather than 747 -- which is the rot this receipt exists to make visible: a figure survives its own truth by naming a command nobody re-runs. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 884 at origin/main and 884 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 4 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 4 lines are this variant's two arms, one per total match, as rustfmt renders them. NO HOST CAPABILITY IS ADDED: both arms are the mechanical consequence of the .dag coproduct gaining a member, and they disappear exactly when the seed does. THIRD LANE, THIRD EXPANSION (codex review 56224, finding 1). DataReferenceVisibilityBudgetExceeded and ParameterDefaultFormNotAdmitted are a SEVENTH and EIGHTH variant, added by the typed-data-reference-refusal lane so that the three terminal arms of v1.compiler.emit_rust validate_workflow_param_defaults stop rendering three distinct causes as InternalError. Each forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 884 was that lane's base, this lane's base is 890, and a receipt that edited the earlier number in place would destroy the evidence that a figure moves per lane -- which is the entire argument this row makes. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 890 at origin/main and 890 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 8 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 8 lines are this lane's FOUR arms -- two variants, one arm each in both total matches -- as rustfmt renders them across braced blocks. WHY EIGHT LINES FOR FOUR ARMS AND NOT FOUR: three of the four arms exceed the line width once the variant name is spelled, so rustfmt renders them as braced blocks; the count is a property of the formatter, not of added capability, which is exactly why (c) is measured beside it rather than inferred from (b). THE THIRTEEN LINES IN v1_std_core.rs ARE NOT PART OF THIS RECEIPT AND MUST NOT BE ADDED TO IT: that file is the GENERATED mirror of this module, emitted by required-regen from the .dag authority above, so its growth is authored here in .dag and is not hand-Rust surface. Counting it would be the same category error as counting compiler_tests.rs, which the plan projection already rules out for the same reason. FOURTH LANE, FOURTH EXPANSION (duplicate-declaration wall, 2026-08-27). DuplicateDeclaration is a NINTH variant, added so that v1.compiler.resolve check_duplicate_declarations can refuse a module declaring one name twice -- a state the compiler previously ACCEPTED with zero diagnostics while the symbol index silently kept the last declaration. It forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 890 was that lane's base, this lane's base is 895. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 895 at origin/main and 895 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 2 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 2 lines are this variant's two arms, one per total match, each short enough that rustfmt renders it on one line -- which is why the per-arm line count differs from the prior lane's and is measured rather than inherited. THE NEW VARIANT IS BLOCKING and carries no arm in is_error_diagnostic or is_interpreter_blocking_diagnostic: both answer true by default and it is listed as an exception in neither, which is the intended reading -- a duplicate declaration must stop the line, since every realization downstream of it is built on whichever definition happened to survive. NEITHER NEW VARIANT IS NON-BLOCKING, so the advisory-allowlist obligation this row records does not fire: is_interpreter_blocking_diagnostic answers true by default and neither variant is listed as an exception, which is correct -- both replace a blocking InternalError at a refusal site, and the witness measures the ambiguity arm as one BLOCKING AmbiguousReference row rather than an advisory one." type FrontierOccurrenceKey { method: String diff --git a/src/v1/03_resolve.dag b/src/v1/03_resolve.dag index 8610a2ae2b7..ebad833eb5b 100644 --- a/src/v1/03_resolve.dag +++ b/src/v1/03_resolve.dag @@ -5,7 +5,7 @@ import std.types { kernel_type_set } import v1.std.core { Node, Connective, Conj, Disj, NoConnective, ErrorNode, make_error_node, - UnresolvedImport, MissingExport, DuplicateModule, CircularDependency, + UnresolvedImport, MissingExport, DuplicateModule, DuplicateDeclaration, CircularDependency, no_span, module_node, import_node, import_is_all, import_specific_names_at, module_imports, module_items, NewlineIndex, authored_name_at, @@ -86,6 +86,8 @@ fn resolve_modules_with_occurrence_transport( let dup_diags = check_duplicate_modules(modules: modules, source_indices: source_indices) + let dup_decl_diags = modules |> flat_map(m => check_duplicate_declarations(module: m, source_indices: source_indices)) + let module_index = modules |> fold(init: empty_map(), f: (acc, m) => map_insert(acc, authored_name_at(source_indices: source_indices, node: m), m) ) @@ -141,7 +143,7 @@ fn resolve_modules_with_occurrence_transport( ModuleGraph { modules: sorted_resolved, - diagnostics: concat(dup_diags, import_diags, topo_diags), + diagnostics: concat(dup_diags, dup_decl_diags, import_diags, topo_diags), occurrence_transport: occurrence_transport, } } @@ -300,6 +302,35 @@ fn check_duplicate_modules(modules: List, source_indices: Map) -> List { + let module_name = authored_name_at(source_indices: source_indices, node: module) + let result = module_items(n: module) |> fold(init: DuplicateCheckState { + seen_names: empty_map(), + diagnostics: [] + }, f: fn(state, item) { + let item_name = authored_name_at(source_indices: source_indices, node: item) + if map_has(m: state.seen_names, key: item_name) { + DuplicateCheckState { + seen_names: state.seen_names, + diagnostics: concat(state.diagnostics, [make_error_node(diagnostic: DuplicateDeclaration { module_name: module_name, name: item_name, span: item.span }, module_name: module_name)]) + } + } else { + DuplicateCheckState { + seen_names: map_insert(state.seen_names, item_name, true), + diagnostics: state.diagnostics + } + } + }) + result.diagnostics +} + type TopoResult { sorted: List cycle_error: ErrorNode? diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 88509916883..015be7562e6 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -5373,6 +5373,7 @@ pub fn compile_clean_diagnostic_histogram_key(d: &Rc) -> (String, Str CompilerDiagnostic::NonExhaustiveMatch { .. } => "NonExhaustiveMatch", CompilerDiagnostic::CircularDependency { .. } => "CircularDependency", CompilerDiagnostic::DuplicateModule { .. } => "DuplicateModule", + CompilerDiagnostic::DuplicateDeclaration { .. } => "DuplicateDeclaration", CompilerDiagnostic::MissingAnnotation { .. } => "MissingAnnotation", CompilerDiagnostic::ParseError { .. } => "ParseError", CompilerDiagnostic::InternalError { .. } => "InternalError", @@ -5435,6 +5436,7 @@ pub fn compile_clean_diagnostic_histogram_key(d: &Rc) -> (String, Str CompilerDiagnostic::NonExhaustiveMatch { .. } => "(non-exhaustive)".to_string(), CompilerDiagnostic::CircularDependency { .. } => "(cycle)".to_string(), CompilerDiagnostic::DuplicateModule { name, .. } => name.clone(), + CompilerDiagnostic::DuplicateDeclaration { name, .. } => name.clone(), CompilerDiagnostic::MissingAnnotation { fn_name, .. } => fn_name.clone(), CompilerDiagnostic::ParseError { message, .. } => truncate_histogram_label(message, 80), CompilerDiagnostic::InternalError { message, .. } => { diff --git a/src/v1/stage0/src/v1_compiler_resolve.rs b/src/v1/stage0/src/v1_compiler_resolve.rs index 347c3a4defc..bdf2d3c6ce5 100644 --- a/src/v1/stage0/src/v1_compiler_resolve.rs +++ b/src/v1/stage0/src/v1_compiler_resolve.rs @@ -8,7 +8,7 @@ pub use crate::std_types::kernel_type_set; use crate::v1_rt; use crate::v1_rt::{VecCompat, VecJoin}; use crate::v1_std_core::CompilerDiagnostic::{ - CircularDependency, DuplicateModule, MissingExport, UnresolvedImport, + CircularDependency, DuplicateDeclaration, DuplicateModule, MissingExport, UnresolvedImport, }; use crate::v1_std_core::Connective::{Conj, Disj, NoConnective}; pub use crate::v1_std_core::InferredNode; @@ -105,6 +105,17 @@ pub fn resolve_modules_with_occurrence_transport( __result }); let dup_diags = check_duplicate_modules(modules.clone(), source_indices.clone()); + let dup_decl_diags = Rc::new({ + let mut __result = Vec::new(); + for m in modules.iter().cloned() { + __result.extend( + (*check_duplicate_declarations(m.clone(), source_indices.clone())) + .iter() + .cloned(), + ); + } + __result + }); let module_index = modules.iter().cloned().fold( v1_rt::rc_empty_map::>(), |acc: Rc>>, m: Rc| { @@ -227,7 +238,10 @@ pub fn resolve_modules_with_occurrence_transport( Rc::new(ModuleGraph { modules: sorted_resolved.clone(), diagnostics: v1_rt::concat( - v1_rt::concat(dup_diags.clone(), import_diags.clone()), + v1_rt::concat( + v1_rt::concat(dup_diags.clone(), dup_decl_diags.clone()), + import_diags.clone(), + ), topo_diags.clone(), ), occurrence_transport: occurrence_transport.clone(), @@ -598,6 +612,50 @@ pub fn check_duplicate_modules( } } +pub fn check_duplicate_declarations( + module: Rc, + source_indices: Rc>>, +) -> Rc>> { + { + let module_name = authored_name_at(source_indices.clone(), module.clone()); + let result = module_items(module.clone()).iter().cloned().fold( + Rc::new(DuplicateCheckState { + seen_names: v1_rt::rc_empty_map::(), + diagnostics: Rc::new(vec![]), + }), + |state: Rc, item: Rc| { + let item_name = authored_name_at(source_indices.clone(), item.clone()); + if v1_rt::map_has(&state.seen_names.clone(), item_name.clone()) { + Rc::new(DuplicateCheckState { + seen_names: state.seen_names.clone(), + diagnostics: v1_rt::concat( + state.diagnostics.clone(), + Rc::new(vec![make_error_node( + Rc::new(CompilerDiagnostic::DuplicateDeclaration { + module_name: module_name.clone(), + name: item_name.clone(), + span: item.span.clone(), + }), + module_name.clone(), + )]), + ), + }) + } else { + Rc::new(DuplicateCheckState { + seen_names: v1_rt::rc_map_insert( + state.seen_names.clone(), + item_name.clone(), + true, + ), + diagnostics: state.diagnostics.clone(), + }) + } + }, + ); + result.diagnostics.clone() + } +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TopoResult { pub sorted: Rc>, diff --git a/src/v1/stage0/src/v1_std_core.rs b/src/v1/stage0/src/v1_std_core.rs index 17620c44282..6a87904ffc1 100644 --- a/src/v1/stage0/src/v1_std_core.rs +++ b/src/v1/stage0/src/v1_std_core.rs @@ -468,6 +468,11 @@ pub enum CompilerDiagnostic { name: String, span: Rc, }, + DuplicateDeclaration { + module_name: String, + name: String, + span: Rc, + }, MissingAnnotation { fn_name: String, what: String, @@ -701,6 +706,7 @@ pub fn diagnostic_to_span(d: Rc) -> Rc { CompilerDiagnostic::NonExhaustiveMatch { span: s, .. } => s.clone(), CompilerDiagnostic::CircularDependency { span: s, .. } => s.clone(), CompilerDiagnostic::DuplicateModule { span: s, .. } => s.clone(), + CompilerDiagnostic::DuplicateDeclaration { span: s, .. } => s.clone(), CompilerDiagnostic::MissingAnnotation { span: s, .. } => s.clone(), CompilerDiagnostic::ParseError { span: s, .. } => s.clone(), CompilerDiagnostic::InternalError { span: s, .. } => s.clone(), @@ -759,6 +765,7 @@ pub fn diagnostic_to_message(d: Rc) -> String { CompilerDiagnostic::NonExhaustiveMatch { missing: ms, .. } => v1_rt::concat("non-exhaustive match: missing variant(s) ".to_string(), ms.clone().join(&", ".to_string())), CompilerDiagnostic::CircularDependency { modules: ms, .. } => v1_rt::concat("circular dependency detected: ".to_string(), ms.clone().join(&" -> ".to_string())), CompilerDiagnostic::DuplicateModule { name: n, .. } => v1_rt::concat(v1_rt::concat("duplicate module declaration: '".to_string(), n.clone()), "'".to_string()), + CompilerDiagnostic::DuplicateDeclaration { module_name: m, name: n, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("duplicate declaration '".to_string(), n.clone()), "' in module '".to_string()), m.clone()), "' -- a second declaration of one name silently replaced the first".to_string()), CompilerDiagnostic::MissingAnnotation { fn_name: f, what: w, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("function '".to_string(), f.clone()), "' requires ".to_string()), w.clone()), " annotation".to_string()), CompilerDiagnostic::ParseError { message: m, .. } => m.clone(), CompilerDiagnostic::InternalError { message: m, .. } => m.clone(), @@ -803,7 +810,7 @@ pub fn is_where_refinement_unenforced_advisory_reason(reason: String) -> bool { pub fn compiler_diagnostic_seed_projection_note() -> String { thread_local! { static CACHED: String = { - "HAND-RUST GATE receipt for the CompilerDiagnostic seed projection (codex reviews 45469, 45481). Adding a variant to this coproduct forces arms in two TOTAL matches in the hand-maintained seed transport, cli_run.rs's compile_clean_diagnostic_histogram_key and its method-name extractor; without them the seed does not compile, so the arms are the mechanical consequence of the .dag change, not host-side capability someone chose to write in Rust. THIS IS A DIFFERENT CLASS FROM THE GATE'S USUAL SUBJECT and the distinction is the whole receipt: the gate's other explicit deferrals — the emit-surface retirement rows — are DECISION SURFACES that could live in .dag and are deferred for a stated reason, so they owe a dissolution schedule of their own. This clause used to name cli_run::selection_control_input_sources alongside them; that symbol was DELETED with affected-set selection (2026-08-15), so the deferral it recorded was discharged by deletion rather than by a schedule, and the citation was left naming nothing — the DESIGN section 3 stale-citation class, inside a receipt whose own argument is that an unverifiable figure rots silently. An exhaustiveness arm owes none, because it cannot live anywhere but the seed's projection of the coproduct, and it disappears exactly when the seed does. CHECKABLE RECEIPT for the FIVE variants this lane adds (MethodNotFound, MethodExistenceUndecided, MethodExistenceFrontierAdmitted, ReceiverTypeUnestablished, FrontierOccurrenceBudgetExceeded). It read SIX until the unjudged conformance advisory was excluded from this PR (codex review 45767); DeclaredTypeConformanceUnjudged and its arms are gone with it, which is why the figures below moved again. Two commands, and the receipt is exactly their output — EACH FIGURE NAMES THE COMMAND THAT PRODUCES IT, because a number with no command behind it cannot be checked and rots silently. (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 747 at origin/main and 747 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 23 added / 0 removed, and `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line is inside an existing fn and none declares one. The 23 lines are the five diagnostic arms plus the advisory-classification entries described next. THE NUMBERS IN A RECEIPT MUST TRACK THE DIFF THEY CERTIFY: this row read five variants and 13 lines after a sixth variant landed (codex review 45501), and then read a fn census of 742 that NO command reproduced — 501 bare `fn` and 245 `pub fn` on either side, never 742 — so the figure certifying that hand-Rust had not grown was itself unverifiable. A receipt whose number cannot be re-derived is indistinguishable from one that is wrong. Re-derive all three figures from the commands above whenever this coproduct changes, rather than carrying them forward. THE ADVISORY CLASSIFICATION IS PART OF THIS RECEIPT AND WAS THE DEFECT IT ALMOST HID: compile_clean_diagnostic_is_advisory is a CLOSED ALLOWLIST, not the complement of is_hard, so the two non-blocking variants this lane adds were counted by NEITHER predicate — rendered to the terminal while the gate reported zero of them. The residue was described as counted while nothing in the repository counted it, and the population figures quoted in review came from a grep over log text rather than from any mechanism (found by executing the gate before and after and seeing its advisory total sit unchanged at 4590 while the printed population halved). Adding a non-blocking variant to the coproduct therefore obliges a matching entry in that allowlist, or the frontier it represents is invisible to the gate that is supposed to bound it. Lane: compiler-static-failure-closure (v1-method-existence-wall / v1-declared-type-conformance-wall). Dissolves with the seed itself, ROADMAP hand-MAINTAINED to zero at v2 self-host; no separate trigger, because there is no separable work to schedule. Not migration debt and not a delete candidate. SECOND LANE, SECOND EXPANSION (review 55680, finding 2). ImportShadowedByLocalDefinition is a SIXTH variant, added by a different lane for the import-shadow wall, and it forces one arm in each of the same two total matches. The five-variant figures above were measured against THAT lane's base and do not reproduce here -- (a) alone now answers 884 rather than 747 -- which is the rot this receipt exists to make visible: a figure survives its own truth by naming a command nobody re-runs. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 884 at origin/main and 884 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 4 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 4 lines are this variant's two arms, one per total match, as rustfmt renders them. NO HOST CAPABILITY IS ADDED: both arms are the mechanical consequence of the .dag coproduct gaining a member, and they disappear exactly when the seed does. THIRD LANE, THIRD EXPANSION (codex review 56224, finding 1). DataReferenceVisibilityBudgetExceeded and ParameterDefaultFormNotAdmitted are a SEVENTH and EIGHTH variant, added by the typed-data-reference-refusal lane so that the three terminal arms of v1.compiler.emit_rust validate_workflow_param_defaults stop rendering three distinct causes as InternalError. Each forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 884 was that lane's base, this lane's base is 890, and a receipt that edited the earlier number in place would destroy the evidence that a figure moves per lane -- which is the entire argument this row makes. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 890 at origin/main and 890 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 8 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 8 lines are this lane's FOUR arms -- two variants, one arm each in both total matches -- as rustfmt renders them across braced blocks. WHY EIGHT LINES FOR FOUR ARMS AND NOT FOUR: three of the four arms exceed the line width once the variant name is spelled, so rustfmt renders them as braced blocks; the count is a property of the formatter, not of added capability, which is exactly why (c) is measured beside it rather than inferred from (b). THE THIRTEEN LINES IN v1_std_core.rs ARE NOT PART OF THIS RECEIPT AND MUST NOT BE ADDED TO IT: that file is the GENERATED mirror of this module, emitted by required-regen from the .dag authority above, so its growth is authored here in .dag and is not hand-Rust surface. Counting it would be the same category error as counting compiler_tests.rs, which the plan projection already rules out for the same reason. NEITHER NEW VARIANT IS NON-BLOCKING, so the advisory-allowlist obligation this row records does not fire: is_interpreter_blocking_diagnostic answers true by default and neither variant is listed as an exception, which is correct -- both replace a blocking InternalError at a refusal site, and the witness measures the ambiguity arm as one BLOCKING AmbiguousReference row rather than an advisory one.".to_string() + "HAND-RUST GATE receipt for the CompilerDiagnostic seed projection (codex reviews 45469, 45481). Adding a variant to this coproduct forces arms in two TOTAL matches in the hand-maintained seed transport, cli_run.rs's compile_clean_diagnostic_histogram_key and its method-name extractor; without them the seed does not compile, so the arms are the mechanical consequence of the .dag change, not host-side capability someone chose to write in Rust. THIS IS A DIFFERENT CLASS FROM THE GATE'S USUAL SUBJECT and the distinction is the whole receipt: the gate's other explicit deferrals — the emit-surface retirement rows — are DECISION SURFACES that could live in .dag and are deferred for a stated reason, so they owe a dissolution schedule of their own. This clause used to name cli_run::selection_control_input_sources alongside them; that symbol was DELETED with affected-set selection (2026-08-15), so the deferral it recorded was discharged by deletion rather than by a schedule, and the citation was left naming nothing — the DESIGN section 3 stale-citation class, inside a receipt whose own argument is that an unverifiable figure rots silently. An exhaustiveness arm owes none, because it cannot live anywhere but the seed's projection of the coproduct, and it disappears exactly when the seed does. CHECKABLE RECEIPT for the FIVE variants this lane adds (MethodNotFound, MethodExistenceUndecided, MethodExistenceFrontierAdmitted, ReceiverTypeUnestablished, FrontierOccurrenceBudgetExceeded). It read SIX until the unjudged conformance advisory was excluded from this PR (codex review 45767); DeclaredTypeConformanceUnjudged and its arms are gone with it, which is why the figures below moved again. Two commands, and the receipt is exactly their output — EACH FIGURE NAMES THE COMMAND THAT PRODUCES IT, because a number with no command behind it cannot be checked and rots silently. (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 747 at origin/main and 747 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 23 added / 0 removed, and `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line is inside an existing fn and none declares one. The 23 lines are the five diagnostic arms plus the advisory-classification entries described next. THE NUMBERS IN A RECEIPT MUST TRACK THE DIFF THEY CERTIFY: this row read five variants and 13 lines after a sixth variant landed (codex review 45501), and then read a fn census of 742 that NO command reproduced — 501 bare `fn` and 245 `pub fn` on either side, never 742 — so the figure certifying that hand-Rust had not grown was itself unverifiable. A receipt whose number cannot be re-derived is indistinguishable from one that is wrong. Re-derive all three figures from the commands above whenever this coproduct changes, rather than carrying them forward. THE ADVISORY CLASSIFICATION IS PART OF THIS RECEIPT AND WAS THE DEFECT IT ALMOST HID: compile_clean_diagnostic_is_advisory is a CLOSED ALLOWLIST, not the complement of is_hard, so the two non-blocking variants this lane adds were counted by NEITHER predicate — rendered to the terminal while the gate reported zero of them. The residue was described as counted while nothing in the repository counted it, and the population figures quoted in review came from a grep over log text rather than from any mechanism (found by executing the gate before and after and seeing its advisory total sit unchanged at 4590 while the printed population halved). Adding a non-blocking variant to the coproduct therefore obliges a matching entry in that allowlist, or the frontier it represents is invisible to the gate that is supposed to bound it. Lane: compiler-static-failure-closure (v1-method-existence-wall / v1-declared-type-conformance-wall). Dissolves with the seed itself, ROADMAP hand-MAINTAINED to zero at v2 self-host; no separate trigger, because there is no separable work to schedule. Not migration debt and not a delete candidate. SECOND LANE, SECOND EXPANSION (review 55680, finding 2). ImportShadowedByLocalDefinition is a SIXTH variant, added by a different lane for the import-shadow wall, and it forces one arm in each of the same two total matches. The five-variant figures above were measured against THAT lane's base and do not reproduce here -- (a) alone now answers 884 rather than 747 -- which is the rot this receipt exists to make visible: a figure survives its own truth by naming a command nobody re-runs. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 884 at origin/main and 884 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 4 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 4 lines are this variant's two arms, one per total match, as rustfmt renders them. NO HOST CAPABILITY IS ADDED: both arms are the mechanical consequence of the .dag coproduct gaining a member, and they disappear exactly when the seed does. THIRD LANE, THIRD EXPANSION (codex review 56224, finding 1). DataReferenceVisibilityBudgetExceeded and ParameterDefaultFormNotAdmitted are a SEVENTH and EIGHTH variant, added by the typed-data-reference-refusal lane so that the three terminal arms of v1.compiler.emit_rust validate_workflow_param_defaults stop rendering three distinct causes as InternalError. Each forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 884 was that lane's base, this lane's base is 890, and a receipt that edited the earlier number in place would destroy the evidence that a figure moves per lane -- which is the entire argument this row makes. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 890 at origin/main and 890 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 8 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 8 lines are this lane's FOUR arms -- two variants, one arm each in both total matches -- as rustfmt renders them across braced blocks. WHY EIGHT LINES FOR FOUR ARMS AND NOT FOUR: three of the four arms exceed the line width once the variant name is spelled, so rustfmt renders them as braced blocks; the count is a property of the formatter, not of added capability, which is exactly why (c) is measured beside it rather than inferred from (b). THE THIRTEEN LINES IN v1_std_core.rs ARE NOT PART OF THIS RECEIPT AND MUST NOT BE ADDED TO IT: that file is the GENERATED mirror of this module, emitted by required-regen from the .dag authority above, so its growth is authored here in .dag and is not hand-Rust surface. Counting it would be the same category error as counting compiler_tests.rs, which the plan projection already rules out for the same reason. FOURTH LANE, FOURTH EXPANSION (duplicate-declaration wall, 2026-08-27). DuplicateDeclaration is a NINTH variant, added so that v1.compiler.resolve check_duplicate_declarations can refuse a module declaring one name twice -- a state the compiler previously ACCEPTED with zero diagnostics while the symbol index silently kept the last declaration. It forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 890 was that lane's base, this lane's base is 895. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 895 at origin/main and 895 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 2 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 2 lines are this variant's two arms, one per total match, each short enough that rustfmt renders it on one line -- which is why the per-arm line count differs from the prior lane's and is measured rather than inherited. THE NEW VARIANT IS BLOCKING and carries no arm in is_error_diagnostic or is_interpreter_blocking_diagnostic: both answer true by default and it is listed as an exception in neither, which is the intended reading -- a duplicate declaration must stop the line, since every realization downstream of it is built on whichever definition happened to survive. NEITHER NEW VARIANT IS NON-BLOCKING, so the advisory-allowlist obligation this row records does not fire: is_interpreter_blocking_diagnostic answers true by default and neither variant is listed as an exception, which is correct -- both replace a blocking InternalError at a refusal site, and the witness measures the ambiguity arm as one BLOCKING AmbiguousReference row rather than an advisory one.".to_string() }; } CACHED.with(|c: &String| c.clone()) From a57a9ffb34c407710f2fab47fc4cbaa60d38df01 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 14:03:44 +0000 Subject: [PATCH 2/2] Re-base the hand-Rust receipt onto the branch's own fork point: origin/main moves under other merges, so the named command reported a diff nobody authored (review 56644) The receipt in `v1.std.core` `compiler_diagnostic_seed_projection_note` names three commands and requires that each figure be re-derivable from them. All three were keyed to `origin/main`. THAT REF MOVES. Measured on this branch, hours apart, with no edit to the change being certified: git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs when authored: 2 added / 0 removed at review: 6 added / 64 removed The 64 removals are MAIN's work, not this branch's. A diff has two sides and only one of them is this branch, so a receipt keyed to the moving side reports a figure that changes without anyone touching the diff it describes -- which is exactly the rot this row exists to make visible, arriving through the REF rather than through the NUMBER. That is why it survived three prior lanes: each author re-measured honestly and the command was already wrong. REPAIR: every figure is now measured against `$(git merge-base origin/main HEAD)`, this branch's own fork point, which does not move; and the base commit is NAMED (ddb7f533e4) so the reading stays reproducible after a later merge. Re-measured against it -- fn census FLAT at 895 on both sides, 2 added / 0 removed, 0 added lines declaring a fn. THE EARLIER LANES' FIGURES ARE NOT RE-BASED OR AMENDED. They were correct against the main they named at the time, and rewriting them would destroy the evidence that a receipt keyed to a moving ref decays -- which is the finding. What changes is the command the NEXT lane runs. v1_std_core.rs is the regenerated mirror of the edited note; required-regen reported it as the only drifting surface and it is installed from the candidate tree. Co-Authored-By: Claude Opus 5 (1M context) --- src/v1/00_core.dag | 2 +- src/v1/stage0/src/v1_std_core.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/v1/00_core.dag b/src/v1/00_core.dag index 3d292a7807d..e0c175018c1 100644 --- a/src/v1/00_core.dag +++ b/src/v1/00_core.dag @@ -471,7 +471,7 @@ fn is_where_refinement_unenforced_advisory_reason(reason: String) -> Bool { || reason == "string predicate not implemented" } -data compiler_diagnostic_seed_projection_note: String = "HAND-RUST GATE receipt for the CompilerDiagnostic seed projection (codex reviews 45469, 45481). Adding a variant to this coproduct forces arms in two TOTAL matches in the hand-maintained seed transport, cli_run.rs's compile_clean_diagnostic_histogram_key and its method-name extractor; without them the seed does not compile, so the arms are the mechanical consequence of the .dag change, not host-side capability someone chose to write in Rust. THIS IS A DIFFERENT CLASS FROM THE GATE'S USUAL SUBJECT and the distinction is the whole receipt: the gate's other explicit deferrals — the emit-surface retirement rows — are DECISION SURFACES that could live in .dag and are deferred for a stated reason, so they owe a dissolution schedule of their own. This clause used to name cli_run::selection_control_input_sources alongside them; that symbol was DELETED with affected-set selection (2026-08-15), so the deferral it recorded was discharged by deletion rather than by a schedule, and the citation was left naming nothing — the DESIGN section 3 stale-citation class, inside a receipt whose own argument is that an unverifiable figure rots silently. An exhaustiveness arm owes none, because it cannot live anywhere but the seed's projection of the coproduct, and it disappears exactly when the seed does. CHECKABLE RECEIPT for the FIVE variants this lane adds (MethodNotFound, MethodExistenceUndecided, MethodExistenceFrontierAdmitted, ReceiverTypeUnestablished, FrontierOccurrenceBudgetExceeded). It read SIX until the unjudged conformance advisory was excluded from this PR (codex review 45767); DeclaredTypeConformanceUnjudged and its arms are gone with it, which is why the figures below moved again. Two commands, and the receipt is exactly their output — EACH FIGURE NAMES THE COMMAND THAT PRODUCES IT, because a number with no command behind it cannot be checked and rots silently. (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 747 at origin/main and 747 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 23 added / 0 removed, and `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line is inside an existing fn and none declares one. The 23 lines are the five diagnostic arms plus the advisory-classification entries described next. THE NUMBERS IN A RECEIPT MUST TRACK THE DIFF THEY CERTIFY: this row read five variants and 13 lines after a sixth variant landed (codex review 45501), and then read a fn census of 742 that NO command reproduced — 501 bare `fn` and 245 `pub fn` on either side, never 742 — so the figure certifying that hand-Rust had not grown was itself unverifiable. A receipt whose number cannot be re-derived is indistinguishable from one that is wrong. Re-derive all three figures from the commands above whenever this coproduct changes, rather than carrying them forward. THE ADVISORY CLASSIFICATION IS PART OF THIS RECEIPT AND WAS THE DEFECT IT ALMOST HID: compile_clean_diagnostic_is_advisory is a CLOSED ALLOWLIST, not the complement of is_hard, so the two non-blocking variants this lane adds were counted by NEITHER predicate — rendered to the terminal while the gate reported zero of them. The residue was described as counted while nothing in the repository counted it, and the population figures quoted in review came from a grep over log text rather than from any mechanism (found by executing the gate before and after and seeing its advisory total sit unchanged at 4590 while the printed population halved). Adding a non-blocking variant to the coproduct therefore obliges a matching entry in that allowlist, or the frontier it represents is invisible to the gate that is supposed to bound it. Lane: compiler-static-failure-closure (v1-method-existence-wall / v1-declared-type-conformance-wall). Dissolves with the seed itself, ROADMAP hand-MAINTAINED to zero at v2 self-host; no separate trigger, because there is no separable work to schedule. Not migration debt and not a delete candidate. SECOND LANE, SECOND EXPANSION (review 55680, finding 2). ImportShadowedByLocalDefinition is a SIXTH variant, added by a different lane for the import-shadow wall, and it forces one arm in each of the same two total matches. The five-variant figures above were measured against THAT lane's base and do not reproduce here -- (a) alone now answers 884 rather than 747 -- which is the rot this receipt exists to make visible: a figure survives its own truth by naming a command nobody re-runs. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 884 at origin/main and 884 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 4 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 4 lines are this variant's two arms, one per total match, as rustfmt renders them. NO HOST CAPABILITY IS ADDED: both arms are the mechanical consequence of the .dag coproduct gaining a member, and they disappear exactly when the seed does. THIRD LANE, THIRD EXPANSION (codex review 56224, finding 1). DataReferenceVisibilityBudgetExceeded and ParameterDefaultFormNotAdmitted are a SEVENTH and EIGHTH variant, added by the typed-data-reference-refusal lane so that the three terminal arms of v1.compiler.emit_rust validate_workflow_param_defaults stop rendering three distinct causes as InternalError. Each forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 884 was that lane's base, this lane's base is 890, and a receipt that edited the earlier number in place would destroy the evidence that a figure moves per lane -- which is the entire argument this row makes. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 890 at origin/main and 890 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 8 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 8 lines are this lane's FOUR arms -- two variants, one arm each in both total matches -- as rustfmt renders them across braced blocks. WHY EIGHT LINES FOR FOUR ARMS AND NOT FOUR: three of the four arms exceed the line width once the variant name is spelled, so rustfmt renders them as braced blocks; the count is a property of the formatter, not of added capability, which is exactly why (c) is measured beside it rather than inferred from (b). THE THIRTEEN LINES IN v1_std_core.rs ARE NOT PART OF THIS RECEIPT AND MUST NOT BE ADDED TO IT: that file is the GENERATED mirror of this module, emitted by required-regen from the .dag authority above, so its growth is authored here in .dag and is not hand-Rust surface. Counting it would be the same category error as counting compiler_tests.rs, which the plan projection already rules out for the same reason. FOURTH LANE, FOURTH EXPANSION (duplicate-declaration wall, 2026-08-27). DuplicateDeclaration is a NINTH variant, added so that v1.compiler.resolve check_duplicate_declarations can refuse a module declaring one name twice -- a state the compiler previously ACCEPTED with zero diagnostics while the symbol index silently kept the last declaration. It forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 890 was that lane's base, this lane's base is 895. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 895 at origin/main and 895 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 2 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 2 lines are this variant's two arms, one per total match, each short enough that rustfmt renders it on one line -- which is why the per-arm line count differs from the prior lane's and is measured rather than inherited. THE NEW VARIANT IS BLOCKING and carries no arm in is_error_diagnostic or is_interpreter_blocking_diagnostic: both answer true by default and it is listed as an exception in neither, which is the intended reading -- a duplicate declaration must stop the line, since every realization downstream of it is built on whichever definition happened to survive. NEITHER NEW VARIANT IS NON-BLOCKING, so the advisory-allowlist obligation this row records does not fire: is_interpreter_blocking_diagnostic answers true by default and neither variant is listed as an exception, which is correct -- both replace a blocking InternalError at a refusal site, and the witness measures the ambiguity arm as one BLOCKING AmbiguousReference row rather than an advisory one." +data compiler_diagnostic_seed_projection_note: String = "HAND-RUST GATE receipt for the CompilerDiagnostic seed projection (codex reviews 45469, 45481). Adding a variant to this coproduct forces arms in two TOTAL matches in the hand-maintained seed transport, cli_run.rs's compile_clean_diagnostic_histogram_key and its method-name extractor; without them the seed does not compile, so the arms are the mechanical consequence of the .dag change, not host-side capability someone chose to write in Rust. THIS IS A DIFFERENT CLASS FROM THE GATE'S USUAL SUBJECT and the distinction is the whole receipt: the gate's other explicit deferrals — the emit-surface retirement rows — are DECISION SURFACES that could live in .dag and are deferred for a stated reason, so they owe a dissolution schedule of their own. This clause used to name cli_run::selection_control_input_sources alongside them; that symbol was DELETED with affected-set selection (2026-08-15), so the deferral it recorded was discharged by deletion rather than by a schedule, and the citation was left naming nothing — the DESIGN section 3 stale-citation class, inside a receipt whose own argument is that an unverifiable figure rots silently. An exhaustiveness arm owes none, because it cannot live anywhere but the seed's projection of the coproduct, and it disappears exactly when the seed does. CHECKABLE RECEIPT for the FIVE variants this lane adds (MethodNotFound, MethodExistenceUndecided, MethodExistenceFrontierAdmitted, ReceiverTypeUnestablished, FrontierOccurrenceBudgetExceeded). It read SIX until the unjudged conformance advisory was excluded from this PR (codex review 45767); DeclaredTypeConformanceUnjudged and its arms are gone with it, which is why the figures below moved again. Two commands, and the receipt is exactly their output — EACH FIGURE NAMES THE COMMAND THAT PRODUCES IT, because a number with no command behind it cannot be checked and rots silently. (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 747 at origin/main and 747 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 23 added / 0 removed, and `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line is inside an existing fn and none declares one. The 23 lines are the five diagnostic arms plus the advisory-classification entries described next. THE NUMBERS IN A RECEIPT MUST TRACK THE DIFF THEY CERTIFY: this row read five variants and 13 lines after a sixth variant landed (codex review 45501), and then read a fn census of 742 that NO command reproduced — 501 bare `fn` and 245 `pub fn` on either side, never 742 — so the figure certifying that hand-Rust had not grown was itself unverifiable. A receipt whose number cannot be re-derived is indistinguishable from one that is wrong. Re-derive all three figures from the commands above whenever this coproduct changes, rather than carrying them forward. THE ADVISORY CLASSIFICATION IS PART OF THIS RECEIPT AND WAS THE DEFECT IT ALMOST HID: compile_clean_diagnostic_is_advisory is a CLOSED ALLOWLIST, not the complement of is_hard, so the two non-blocking variants this lane adds were counted by NEITHER predicate — rendered to the terminal while the gate reported zero of them. The residue was described as counted while nothing in the repository counted it, and the population figures quoted in review came from a grep over log text rather than from any mechanism (found by executing the gate before and after and seeing its advisory total sit unchanged at 4590 while the printed population halved). Adding a non-blocking variant to the coproduct therefore obliges a matching entry in that allowlist, or the frontier it represents is invisible to the gate that is supposed to bound it. Lane: compiler-static-failure-closure (v1-method-existence-wall / v1-declared-type-conformance-wall). Dissolves with the seed itself, ROADMAP hand-MAINTAINED to zero at v2 self-host; no separate trigger, because there is no separable work to schedule. Not migration debt and not a delete candidate. SECOND LANE, SECOND EXPANSION (review 55680, finding 2). ImportShadowedByLocalDefinition is a SIXTH variant, added by a different lane for the import-shadow wall, and it forces one arm in each of the same two total matches. The five-variant figures above were measured against THAT lane's base and do not reproduce here -- (a) alone now answers 884 rather than 747 -- which is the rot this receipt exists to make visible: a figure survives its own truth by naming a command nobody re-runs. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 884 at origin/main and 884 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 4 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 4 lines are this variant's two arms, one per total match, as rustfmt renders them. NO HOST CAPABILITY IS ADDED: both arms are the mechanical consequence of the .dag coproduct gaining a member, and they disappear exactly when the seed does. THIRD LANE, THIRD EXPANSION (codex review 56224, finding 1). DataReferenceVisibilityBudgetExceeded and ParameterDefaultFormNotAdmitted are a SEVENTH and EIGHTH variant, added by the typed-data-reference-refusal lane so that the three terminal arms of v1.compiler.emit_rust validate_workflow_param_defaults stop rendering three distinct causes as InternalError. Each forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 884 was that lane's base, this lane's base is 890, and a receipt that edited the earlier number in place would destroy the evidence that a figure moves per lane -- which is the entire argument this row makes. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 890 at origin/main and 890 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 8 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 8 lines are this lane's FOUR arms -- two variants, one arm each in both total matches -- as rustfmt renders them across braced blocks. WHY EIGHT LINES FOR FOUR ARMS AND NOT FOUR: three of the four arms exceed the line width once the variant name is spelled, so rustfmt renders them as braced blocks; the count is a property of the formatter, not of added capability, which is exactly why (c) is measured beside it rather than inferred from (b). THE THIRTEEN LINES IN v1_std_core.rs ARE NOT PART OF THIS RECEIPT AND MUST NOT BE ADDED TO IT: that file is the GENERATED mirror of this module, emitted by required-regen from the .dag authority above, so its growth is authored here in .dag and is not hand-Rust surface. Counting it would be the same category error as counting compiler_tests.rs, which the plan projection already rules out for the same reason. FOURTH LANE, FOURTH EXPANSION (duplicate-declaration wall, 2026-08-27). DuplicateDeclaration is a NINTH variant, added so that v1.compiler.resolve check_duplicate_declarations can refuse a module declaring one name twice -- a state the compiler previously ACCEPTED with zero diagnostics while the symbol index silently kept the last declaration. It forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 890 was that lane's base, this lane's base is 895. THIS LANE CHANGES THE RECEIPT'S NAMED COMMANDS, AND THE REASON IS THIS ROW'S OWN ARGUMENT TURNED ON ITSELF (codex review 56644). Every prior lane wrote `origin/main` as the comparison base. A DIFF HAS TWO SIDES AND ONLY ONE OF THEM IS THIS BRANCH: origin/main moves under other people's merges, so a receipt keyed to the MOVING ref reports a figure that changes without anyone touching the change it certifies. Measured here rather than argued: this lane's diff was 2 added / 0 removed when authored, and hours later the same `git diff --numstat origin/main` command answered 6 added / 64 removed -- the 64 removals being main's work, not this branch's, and the figure now describing a diff nobody authored. That is exactly the rot this row exists to make visible, arriving through the ref rather than through the number, which is why it survived three lanes: each author re-measured honestly and the command was already wrong. THE REPAIR IS A STABLE BASE: every figure below is measured against `$(git merge-base origin/main HEAD)`, which is this branch's own fork point and does not move, and the base commit is NAMED so the reading is reproducible after a later merge -- ddb7f533e4. RE-MEASURED ON THIS HEAD against that base: (a) `git show $(git merge-base origin/main HEAD):src/v1/stage0/src/cli_run.rs | grep -cE '^(pub )?fn '` is 895 and `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 895, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat $(git merge-base origin/main HEAD) -- src/v1/stage0/src/cli_run.rs` is 2 added / 0 removed. (c) `git diff $(git merge-base origin/main HEAD) -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 2 lines are this variant's two arms, one per total match, each short enough that rustfmt renders it on one line -- which is why the per-arm line count differs from the prior lane's and is measured rather than inherited. THE EARLIER LANES' FIGURES ARE NOT RE-BASED OR AMENDED: they were correct against the main they named at the time, and rewriting them would destroy the evidence that a receipt keyed to a moving ref decays, which is the whole finding here. What changes is the command the NEXT lane runs. THE NEW VARIANT IS BLOCKING and carries no arm in is_error_diagnostic or is_interpreter_blocking_diagnostic: both answer true by default and it is listed as an exception in neither, which is the intended reading -- a duplicate declaration must stop the line, since every realization downstream of it is built on whichever definition happened to survive. NEITHER NEW VARIANT IS NON-BLOCKING, so the advisory-allowlist obligation this row records does not fire: is_interpreter_blocking_diagnostic answers true by default and neither variant is listed as an exception, which is correct -- both replace a blocking InternalError at a refusal site, and the witness measures the ambiguity arm as one BLOCKING AmbiguousReference row rather than an advisory one." type FrontierOccurrenceKey { method: String diff --git a/src/v1/stage0/src/v1_std_core.rs b/src/v1/stage0/src/v1_std_core.rs index 6a87904ffc1..a1fb64e0b6c 100644 --- a/src/v1/stage0/src/v1_std_core.rs +++ b/src/v1/stage0/src/v1_std_core.rs @@ -810,7 +810,7 @@ pub fn is_where_refinement_unenforced_advisory_reason(reason: String) -> bool { pub fn compiler_diagnostic_seed_projection_note() -> String { thread_local! { static CACHED: String = { - "HAND-RUST GATE receipt for the CompilerDiagnostic seed projection (codex reviews 45469, 45481). Adding a variant to this coproduct forces arms in two TOTAL matches in the hand-maintained seed transport, cli_run.rs's compile_clean_diagnostic_histogram_key and its method-name extractor; without them the seed does not compile, so the arms are the mechanical consequence of the .dag change, not host-side capability someone chose to write in Rust. THIS IS A DIFFERENT CLASS FROM THE GATE'S USUAL SUBJECT and the distinction is the whole receipt: the gate's other explicit deferrals — the emit-surface retirement rows — are DECISION SURFACES that could live in .dag and are deferred for a stated reason, so they owe a dissolution schedule of their own. This clause used to name cli_run::selection_control_input_sources alongside them; that symbol was DELETED with affected-set selection (2026-08-15), so the deferral it recorded was discharged by deletion rather than by a schedule, and the citation was left naming nothing — the DESIGN section 3 stale-citation class, inside a receipt whose own argument is that an unverifiable figure rots silently. An exhaustiveness arm owes none, because it cannot live anywhere but the seed's projection of the coproduct, and it disappears exactly when the seed does. CHECKABLE RECEIPT for the FIVE variants this lane adds (MethodNotFound, MethodExistenceUndecided, MethodExistenceFrontierAdmitted, ReceiverTypeUnestablished, FrontierOccurrenceBudgetExceeded). It read SIX until the unjudged conformance advisory was excluded from this PR (codex review 45767); DeclaredTypeConformanceUnjudged and its arms are gone with it, which is why the figures below moved again. Two commands, and the receipt is exactly their output — EACH FIGURE NAMES THE COMMAND THAT PRODUCES IT, because a number with no command behind it cannot be checked and rots silently. (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 747 at origin/main and 747 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 23 added / 0 removed, and `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line is inside an existing fn and none declares one. The 23 lines are the five diagnostic arms plus the advisory-classification entries described next. THE NUMBERS IN A RECEIPT MUST TRACK THE DIFF THEY CERTIFY: this row read five variants and 13 lines after a sixth variant landed (codex review 45501), and then read a fn census of 742 that NO command reproduced — 501 bare `fn` and 245 `pub fn` on either side, never 742 — so the figure certifying that hand-Rust had not grown was itself unverifiable. A receipt whose number cannot be re-derived is indistinguishable from one that is wrong. Re-derive all three figures from the commands above whenever this coproduct changes, rather than carrying them forward. THE ADVISORY CLASSIFICATION IS PART OF THIS RECEIPT AND WAS THE DEFECT IT ALMOST HID: compile_clean_diagnostic_is_advisory is a CLOSED ALLOWLIST, not the complement of is_hard, so the two non-blocking variants this lane adds were counted by NEITHER predicate — rendered to the terminal while the gate reported zero of them. The residue was described as counted while nothing in the repository counted it, and the population figures quoted in review came from a grep over log text rather than from any mechanism (found by executing the gate before and after and seeing its advisory total sit unchanged at 4590 while the printed population halved). Adding a non-blocking variant to the coproduct therefore obliges a matching entry in that allowlist, or the frontier it represents is invisible to the gate that is supposed to bound it. Lane: compiler-static-failure-closure (v1-method-existence-wall / v1-declared-type-conformance-wall). Dissolves with the seed itself, ROADMAP hand-MAINTAINED to zero at v2 self-host; no separate trigger, because there is no separable work to schedule. Not migration debt and not a delete candidate. SECOND LANE, SECOND EXPANSION (review 55680, finding 2). ImportShadowedByLocalDefinition is a SIXTH variant, added by a different lane for the import-shadow wall, and it forces one arm in each of the same two total matches. The five-variant figures above were measured against THAT lane's base and do not reproduce here -- (a) alone now answers 884 rather than 747 -- which is the rot this receipt exists to make visible: a figure survives its own truth by naming a command nobody re-runs. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 884 at origin/main and 884 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 4 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 4 lines are this variant's two arms, one per total match, as rustfmt renders them. NO HOST CAPABILITY IS ADDED: both arms are the mechanical consequence of the .dag coproduct gaining a member, and they disappear exactly when the seed does. THIRD LANE, THIRD EXPANSION (codex review 56224, finding 1). DataReferenceVisibilityBudgetExceeded and ParameterDefaultFormNotAdmitted are a SEVENTH and EIGHTH variant, added by the typed-data-reference-refusal lane so that the three terminal arms of v1.compiler.emit_rust validate_workflow_param_defaults stop rendering three distinct causes as InternalError. Each forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 884 was that lane's base, this lane's base is 890, and a receipt that edited the earlier number in place would destroy the evidence that a figure moves per lane -- which is the entire argument this row makes. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 890 at origin/main and 890 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 8 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 8 lines are this lane's FOUR arms -- two variants, one arm each in both total matches -- as rustfmt renders them across braced blocks. WHY EIGHT LINES FOR FOUR ARMS AND NOT FOUR: three of the four arms exceed the line width once the variant name is spelled, so rustfmt renders them as braced blocks; the count is a property of the formatter, not of added capability, which is exactly why (c) is measured beside it rather than inferred from (b). THE THIRTEEN LINES IN v1_std_core.rs ARE NOT PART OF THIS RECEIPT AND MUST NOT BE ADDED TO IT: that file is the GENERATED mirror of this module, emitted by required-regen from the .dag authority above, so its growth is authored here in .dag and is not hand-Rust surface. Counting it would be the same category error as counting compiler_tests.rs, which the plan projection already rules out for the same reason. FOURTH LANE, FOURTH EXPANSION (duplicate-declaration wall, 2026-08-27). DuplicateDeclaration is a NINTH variant, added so that v1.compiler.resolve check_duplicate_declarations can refuse a module declaring one name twice -- a state the compiler previously ACCEPTED with zero diagnostics while the symbol index silently kept the last declaration. It forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 890 was that lane's base, this lane's base is 895. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 895 at origin/main and 895 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 2 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 2 lines are this variant's two arms, one per total match, each short enough that rustfmt renders it on one line -- which is why the per-arm line count differs from the prior lane's and is measured rather than inherited. THE NEW VARIANT IS BLOCKING and carries no arm in is_error_diagnostic or is_interpreter_blocking_diagnostic: both answer true by default and it is listed as an exception in neither, which is the intended reading -- a duplicate declaration must stop the line, since every realization downstream of it is built on whichever definition happened to survive. NEITHER NEW VARIANT IS NON-BLOCKING, so the advisory-allowlist obligation this row records does not fire: is_interpreter_blocking_diagnostic answers true by default and neither variant is listed as an exception, which is correct -- both replace a blocking InternalError at a refusal site, and the witness measures the ambiguity arm as one BLOCKING AmbiguousReference row rather than an advisory one.".to_string() + "HAND-RUST GATE receipt for the CompilerDiagnostic seed projection (codex reviews 45469, 45481). Adding a variant to this coproduct forces arms in two TOTAL matches in the hand-maintained seed transport, cli_run.rs's compile_clean_diagnostic_histogram_key and its method-name extractor; without them the seed does not compile, so the arms are the mechanical consequence of the .dag change, not host-side capability someone chose to write in Rust. THIS IS A DIFFERENT CLASS FROM THE GATE'S USUAL SUBJECT and the distinction is the whole receipt: the gate's other explicit deferrals — the emit-surface retirement rows — are DECISION SURFACES that could live in .dag and are deferred for a stated reason, so they owe a dissolution schedule of their own. This clause used to name cli_run::selection_control_input_sources alongside them; that symbol was DELETED with affected-set selection (2026-08-15), so the deferral it recorded was discharged by deletion rather than by a schedule, and the citation was left naming nothing — the DESIGN section 3 stale-citation class, inside a receipt whose own argument is that an unverifiable figure rots silently. An exhaustiveness arm owes none, because it cannot live anywhere but the seed's projection of the coproduct, and it disappears exactly when the seed does. CHECKABLE RECEIPT for the FIVE variants this lane adds (MethodNotFound, MethodExistenceUndecided, MethodExistenceFrontierAdmitted, ReceiverTypeUnestablished, FrontierOccurrenceBudgetExceeded). It read SIX until the unjudged conformance advisory was excluded from this PR (codex review 45767); DeclaredTypeConformanceUnjudged and its arms are gone with it, which is why the figures below moved again. Two commands, and the receipt is exactly their output — EACH FIGURE NAMES THE COMMAND THAT PRODUCES IT, because a number with no command behind it cannot be checked and rots silently. (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 747 at origin/main and 747 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 23 added / 0 removed, and `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line is inside an existing fn and none declares one. The 23 lines are the five diagnostic arms plus the advisory-classification entries described next. THE NUMBERS IN A RECEIPT MUST TRACK THE DIFF THEY CERTIFY: this row read five variants and 13 lines after a sixth variant landed (codex review 45501), and then read a fn census of 742 that NO command reproduced — 501 bare `fn` and 245 `pub fn` on either side, never 742 — so the figure certifying that hand-Rust had not grown was itself unverifiable. A receipt whose number cannot be re-derived is indistinguishable from one that is wrong. Re-derive all three figures from the commands above whenever this coproduct changes, rather than carrying them forward. THE ADVISORY CLASSIFICATION IS PART OF THIS RECEIPT AND WAS THE DEFECT IT ALMOST HID: compile_clean_diagnostic_is_advisory is a CLOSED ALLOWLIST, not the complement of is_hard, so the two non-blocking variants this lane adds were counted by NEITHER predicate — rendered to the terminal while the gate reported zero of them. The residue was described as counted while nothing in the repository counted it, and the population figures quoted in review came from a grep over log text rather than from any mechanism (found by executing the gate before and after and seeing its advisory total sit unchanged at 4590 while the printed population halved). Adding a non-blocking variant to the coproduct therefore obliges a matching entry in that allowlist, or the frontier it represents is invisible to the gate that is supposed to bound it. Lane: compiler-static-failure-closure (v1-method-existence-wall / v1-declared-type-conformance-wall). Dissolves with the seed itself, ROADMAP hand-MAINTAINED to zero at v2 self-host; no separate trigger, because there is no separable work to schedule. Not migration debt and not a delete candidate. SECOND LANE, SECOND EXPANSION (review 55680, finding 2). ImportShadowedByLocalDefinition is a SIXTH variant, added by a different lane for the import-shadow wall, and it forces one arm in each of the same two total matches. The five-variant figures above were measured against THAT lane's base and do not reproduce here -- (a) alone now answers 884 rather than 747 -- which is the rot this receipt exists to make visible: a figure survives its own truth by naming a command nobody re-runs. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 884 at origin/main and 884 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 4 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 4 lines are this variant's two arms, one per total match, as rustfmt renders them. NO HOST CAPABILITY IS ADDED: both arms are the mechanical consequence of the .dag coproduct gaining a member, and they disappear exactly when the seed does. THIRD LANE, THIRD EXPANSION (codex review 56224, finding 1). DataReferenceVisibilityBudgetExceeded and ParameterDefaultFormNotAdmitted are a SEVENTH and EIGHTH variant, added by the typed-data-reference-refusal lane so that the three terminal arms of v1.compiler.emit_rust validate_workflow_param_defaults stop rendering three distinct causes as InternalError. Each forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 884 was that lane's base, this lane's base is 890, and a receipt that edited the earlier number in place would destroy the evidence that a figure moves per lane -- which is the entire argument this row makes. RE-MEASURED ON THIS HEAD, same three commands: (a) `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 890 at origin/main and 890 on this branch, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs` is 8 added / 0 removed. (c) `git diff origin/main -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 8 lines are this lane's FOUR arms -- two variants, one arm each in both total matches -- as rustfmt renders them across braced blocks. WHY EIGHT LINES FOR FOUR ARMS AND NOT FOUR: three of the four arms exceed the line width once the variant name is spelled, so rustfmt renders them as braced blocks; the count is a property of the formatter, not of added capability, which is exactly why (c) is measured beside it rather than inferred from (b). THE THIRTEEN LINES IN v1_std_core.rs ARE NOT PART OF THIS RECEIPT AND MUST NOT BE ADDED TO IT: that file is the GENERATED mirror of this module, emitted by required-regen from the .dag authority above, so its growth is authored here in .dag and is not hand-Rust surface. Counting it would be the same category error as counting compiler_tests.rs, which the plan projection already rules out for the same reason. FOURTH LANE, FOURTH EXPANSION (duplicate-declaration wall, 2026-08-27). DuplicateDeclaration is a NINTH variant, added so that v1.compiler.resolve check_duplicate_declarations can refuse a module declaring one name twice -- a state the compiler previously ACCEPTED with zero diagnostics while the symbol index silently kept the last declaration. It forces one arm in each of the same two total matches. THE PRIOR LANE'S FIGURES DO NOT REPRODUCE HERE AND ARE NOT AMENDED: 890 was that lane's base, this lane's base is 895. THIS LANE CHANGES THE RECEIPT'S NAMED COMMANDS, AND THE REASON IS THIS ROW'S OWN ARGUMENT TURNED ON ITSELF (codex review 56644). Every prior lane wrote `origin/main` as the comparison base. A DIFF HAS TWO SIDES AND ONLY ONE OF THEM IS THIS BRANCH: origin/main moves under other people's merges, so a receipt keyed to the MOVING ref reports a figure that changes without anyone touching the change it certifies. Measured here rather than argued: this lane's diff was 2 added / 0 removed when authored, and hours later the same `git diff --numstat origin/main` command answered 6 added / 64 removed -- the 64 removals being main's work, not this branch's, and the figure now describing a diff nobody authored. That is exactly the rot this row exists to make visible, arriving through the ref rather than through the number, which is why it survived three lanes: each author re-measured honestly and the command was already wrong. THE REPAIR IS A STABLE BASE: every figure below is measured against `$(git merge-base origin/main HEAD)`, which is this branch's own fork point and does not move, and the base commit is NAMED so the reading is reproducible after a later merge -- ddb7f533e4. RE-MEASURED ON THIS HEAD against that base: (a) `git show $(git merge-base origin/main HEAD):src/v1/stage0/src/cli_run.rs | grep -cE '^(pub )?fn '` is 895 and `grep -cE '^(pub )?fn ' src/v1/stage0/src/cli_run.rs` is 895, so the hand-Rust CARRIER census is FLAT. (b) `git diff --numstat $(git merge-base origin/main HEAD) -- src/v1/stage0/src/cli_run.rs` is 2 added / 0 removed. (c) `git diff $(git merge-base origin/main HEAD) -- src/v1/stage0/src/cli_run.rs | grep -cE '^\\+.*\\bfn '` is 0, so every added line sits inside an existing fn and none declares one. The 2 lines are this variant's two arms, one per total match, each short enough that rustfmt renders it on one line -- which is why the per-arm line count differs from the prior lane's and is measured rather than inherited. THE EARLIER LANES' FIGURES ARE NOT RE-BASED OR AMENDED: they were correct against the main they named at the time, and rewriting them would destroy the evidence that a receipt keyed to a moving ref decays, which is the whole finding here. What changes is the command the NEXT lane runs. THE NEW VARIANT IS BLOCKING and carries no arm in is_error_diagnostic or is_interpreter_blocking_diagnostic: both answer true by default and it is listed as an exception in neither, which is the intended reading -- a duplicate declaration must stop the line, since every realization downstream of it is built on whichever definition happened to survive. NEITHER NEW VARIANT IS NON-BLOCKING, so the advisory-allowlist obligation this row records does not fire: is_interpreter_blocking_diagnostic answers true by default and neither variant is listed as an exception, which is correct -- both replace a blocking InternalError at a refusal site, and the witness measures the ambiguity arm as one BLOCKING AmbiguousReference row rather than an advisory one.".to_string() }; } CACHED.with(|c: &String| c.clone())