Skip to content

The systemctl state filter on two host-hygiene observers binds a FUNCTION instead of a string, because the compiler silently accepts duplicate declarations: the wall, the repair, and a fixture-boundary RED - #9452

Merged
briansrls merged 2 commits into
mainfrom
session/sleek-gull-113
Aug 27, 2026

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

The production specimen, and it is what this PR leads with

dag/gunbc/systemctl_list_units.dag declared one name twice, and the state: filter on a
systemd.Systemctl.ListUnits call bound the function instead of the string it was written to
bind:

57  data systemctl_list_units_all_states: String = "active,inactive,failed"
60  ... ListUnits(pattern: instance_glob, state: systemctl_list_units_all_states)   <- the USE
68  fn   systemctl_list_units_all_states(instance_glob: NonEmptyStr) -> List<NonEmptyStr>?

The use at line 60 sits BETWEEN the two declarations. A reader going top-to-bottom has seen only
the data row when they reach line 60, and is right about everything they have read. That is why
review did not catch it, and it is the argument for a wall rather than diligence.

Blast radius, measured rather than inferred — and narrower than first reported. Two modules
import the collided name: gunbc.host_hygiene_liveness_observe and
gunbc.host_hygiene_reaper_observe, both host hygiene observers, both using it to ask systemd which
units exist in a non-default state. gunbc.host_converge_slice1 imports this module too, but it
imports systemctl_list_units_active_services, a different declaration that reaches
systemctl_list_units_local and passes no state: argument at all — so host convergence is
not on this path. An earlier relay of this finding said three consumers including host
convergence; that is corrected here at the source rather than left to travel.

What is NOT claimed: the emitted argv is not measured. What is established is that the reference
bound the function rather than the string, by the mechanism below and by direct execution of the
same shape.

The compiler defect underneath it

v1.compiler.env symbol_index_insert is a map_insert keyed on the qualified name, so a second
declaration of one name replaced the first and every reference bound whichever was written last
— with zero diagnostics at any severity. Two authorities for one fact (DESIGN §3), and since
names resolving is the ordinary compiler floor, this sat below it (DESIGN §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:

fixture result
fn probe_dup() -> Int { 1 } then fn probe_dup() -> Int { 2 }, caller calls probe_dup() 2 — the last definition
same with one definition (control) 1
data thing: String + fn thing(), bare reference to thing <fn thing>

The third row is the shape the production specimen has, and it is not fn/fn — so a
per-declaration-kind wall would have missed it.

Live exposure, denominator stated

4163 tracked .dag files scanned; 2 modules carried it, both repaired here:

  • dag/gunbc/systemctl_list_units.dag — above. 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.

A first pass also flagged 9 extdeps files. That was my own regex truncating service shell.Find
to shell; the real service names are distinct. With full dotted names, 0 remain beyond the two.

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 membership is
decidable from one module's source, which is what makes this a wall rather than a ratchet (DESIGN §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.

Post-wall binary (sha256 1659e35605…, mtime 2026-08-27T13:33:11Z) refuses both shapes with a typed,
located diagnostic; the single-definition control is unchanged at 1.

The evidence, and where it deliberately does NOT live

The first cut 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.

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 differing 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,
and per DESIGN §4b(4) the controls stay enrolled: they are the executing evidence that the rung is real.

The bound that matters most, and it is not the compiler's

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 — which this repository leans on everywhere, and which is why the
live specimens above passed every witness over them. That bound is written into both the witness and
the gap-analysis row.

Also here

  • The DESIGN §4b row in docs/plans/compiler-guarantee-recovery-gap-analysis.md: invalid state, harm,
    distinguishing facts against 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, not
    its reach.
  • 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.

Two --required-regen passes ran the full seed emit with the wall armed and completed, so the seed
closure carries no duplicate declarations either.

v1 seed purpose

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.

🤖 Generated with Claude Code

…use 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 ddb7f53
(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 `<fn thing>`.

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) <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot changed the title Duplicate declarations in one module bound the LAST one silently: refuse them, with a fixture-boundary RED and two live specimens repaired The systemctl state filter on two host-hygiene observers binds a FUNCTION instead of a string, because the compiler silently accepts duplicate declarations: the wall, the repair, and a fixture-boundary RED Aug 27, 2026
…n/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 (ddb7f53)
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) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in a57a9ff. Review 56644 is correct: git diff --numstat origin/main -- src/v1/stage0/src/cli_run.rs answers 6 64, not the 2 0 the receipt claimed.

Both readings are true, and the difference is which side moved. Measured just now:

base numstat
origin/main (now dabe4c5804) 6 64
$(git merge-base origin/main HEAD) = ddb7f533e4 2 0

The 64 removals are main's work — main deleted them from cli_run.rs after this branch forked. 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 change it certifies.

The finding is bigger than this PR, which is why the repair is in the note and not just in the numbers. Every prior lane wrote origin/main; each author re-measured honestly; the command was already wrong. The rot arrived through the ref rather than through the number, which is precisely why three lanes' worth of diligence did not catch it — this row's own argument, turned on itself.

Repair: all three figures now use $(git merge-base origin/main HEAD), which does not move, with the base commit named (ddb7f533e4) so the reading stays reproducible after a later merge. Re-measured against it — fn census flat at 895 both sides, 2 added / 0 removed, 0 added lines declaring a fn.

Deliberately not done: 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. 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, installed from the candidate tree.

— sent from sleek-gull-113

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant