Skip to content

The dissolution census asserted over an empty population, so its headline green could not fail - #8834

Merged
briansrls merged 6 commits into
mainfrom
session/sleek-tern-222-census-observer
Aug 22, 2026
Merged

briansrls merged 6 commits into
mainfrom
session/sleek-tern-222-census-observer

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

Stacks on #8829 (DeclarationRetires), whose commit is included here until it merges. After #8829 squash-merges, this branch merges main rather than rebasing, per the repo's merge policy.

The finding

gunbc.dissolution_census folded the roster against present_decls: []:

fn dissolution_census_report() -> FrontierExpiryReport {
  fold_frontier_expiry(rows: census_closure_frontier_rows(), present_decls: [])
}

Under an empty population no DeclarationAppears trigger can ever be found, so every bound row reports Pending, fired_still_present_count is 0 by construction, and frontier_expiry_clean — the census's headline assertion, and the entire point of the carrier — was a predicate with one reachable answer. nothing_is_fired_and_still_present_today() in the long witness asserted a value that could not be anything else.

Two production bound rows exist today — extdeps.dhcp.v4 (trigger extdeps.network.mac parse_mac_address) and extdeps.network.ipv6 (trigger extdeps.network.ipv6 parse_ipv6_address) — and nothing could ever have observed either of them firing. std.dissolution was a correct model with no observer. Credit: eager-crane-282 read the fold and named this; I verified it independently by execution below.

The change

The population is the live declaration index. Two structural choices carry the change:

The mechanism does not import the roster. gunbc.dissolution_observation holds the resolution and the fold; gunbc.dissolution_census keeps the roster and supplies it. A consumer that wants the mechanism no longer acquires the census's ~20-module extdeps fan-in to get it — which is what lets the per-PR mechanism witness use it while keeping the header claim that it never imports census_closure_frontier.

A trigger ref is answered by the resolver, not by list membership. v2.std.decl_ref_resolution resolve_declaration_ref already decides field grain (decl_ref_named_field_present) and returns five typed arms; declaration_ref_in_list compares field too, so a NamedField ref would silently never match a whole-declaration projection. The present population is then the roster's own refs that resolved, so no qualified-name reprojection stands between the resolver's answer and the fold's declaration_ref_eq.

The failure arm refuses (DESIGN §5). A ref the resolver could not decide is neither present nor absent, and answering it either way would silently fire one trigger direction while silencing the other — a hazard that is now symmetric, because DeclarationRetires reads the same population backwards. DissolutionCensusRefused carries the located refs so the deficit is countable rather than absorbed. Undecided covers ambiguity, and a module the index does not cover at all: decl_facts deliberately skips test .dag files while module_declaration_facts still reports their modules, so a ref into a witness module resolves DeclarationAbsent for a reason that has nothing to do with the tree. That state is derived — a module is outside the index exactly when the index carries no declaration from its file — not pattern-matched on a path convention.

Evidence

Subject: this branch. Ref: 47a4b9dff9. Producer: ./target/release/gunbc run --source-root dag --source-root src/v2 --entry <entry> --function <fn>, locally built v1-compiler.

Green over the live tree (dag/test/claim/long/dissolution_census_witness_test.dag): census_verdict_is_answered_over_the_live_tree, census_counters_exhaustively_partition_the_rows, bound_and_unbound_partition_the_same_rows, census_holds_on_the_live_roster, nothing_is_fired_and_still_present_today — all true.

The discriminating RED is the point of the change. Appending fn parse_ipv6_address to extdeps.network.ipv6 — the exact declaration that row's trigger names — flips both nothing_is_fired_and_still_present_today and census_holds_on_the_live_roster to false. Under the empty population that outcome was unreachable. The probe was reverted; git diff on that file is empty.

A second discriminator runs per-PR, and is deliberately not a count. Once the population is real the two trigger arms read it in opposite directions, so a wrong or partial population mass-fires one while mass-silencing the other — and any assertion phrased as a total is satisfied by both mistakes. opposite_trigger_directions_agree_on_one_live_population asserts one subject KNOWN present (std.dissolution retires_dissolution) and one KNOWN absent, in opposite directions, in the same run against one resolver answer, over a dag/std pool root rather than the corpus. Misspelling the present subject flips it to false.

Also verified: the two live trigger refs are genuinely absent today (parse_mac_address has no module at all, parse_ipv6_address no declaration), so today's green is a true Pending, not a resolver that failed to look.

required-regen: first_generation_equal=true planned=131 executed=131. cargo fmt --all --check clean.

What this does NOT claim

The live census is homed on dag/test/claim/long/, and the long lane does not execute — falsifier.yml was deleted in the 2026-08-15 floor cut and the corpus declares that honestly as DeclaredCadenceUnrealized. So the corpus-wide half is a real observer with no cadence, sitting at mitigatable, and its next-rung trigger is the CI re-add queue, not anything in this diff. What executes per-PR is the mechanism and its discriminating pair. Cost measurement for whoever takes that re-add: whole-corpus decl_facts(["dag","src/v2"]) costs ~10.5s marginal (1m53.3 with the scan against 1m42.8 for an otherwise identical control), so the scan is not what makes this long.

State the basis before quoting that number. The ~1m42 is what a standalone gunbc run invocation pays to bring up the two source roots — it is a per-invocation frontend cost, not a per-witness floor cost. run_required_floor folds the whole roster through one prepared subject in a single process; the last measured main run did planned=10253 executed=10253 in ~30 min wall, which is arithmetically impossible under a per-witness reading. What the finding does support: for standalone runs and local recipes the frontend dominates, so an import-closure-narrowing lane measuring its trims against that baseline is measuring against the wrong denominator.

Brian Searls and others added 2 commits August 21, 2026 23:21
…use no variant could hold it

std.dissolution modeled exactly one trigger, DeclarationAppears, which fires when a
named declaration is FOUND. Every trigger built from it is therefore a forward
reference to a carrier that does not exist yet.

That is the wrong shape for the retirement this corpus overwhelmingly records. The
`dissolve-on:` marker names the SUBJECT -- the declaration that must go away
(sol_console_send_script, configfs_op_line, nbd_server_config_text and their kin are
live declarations whose disappearance IS the retirement) -- not a carrier that must
arrive. With only the forward shape available an author had two moves: bind forward to
a symbol nobody has declared, which gunbc.runner_spec_from_offer refuses by name as the
DESIGN section 3 fabricated-citation class, or write the condition into a description
string. The second move won, and a description is unreadable to dissolution_status by
construction -- so the debt's retirement condition became something no mechanism can
see.

DeclarationRetires fires when the named declaration is ABSENT, over exactly the same
present_decls evidence the forward shape already consumes. Its ref names something that
exists TODAY, so unlike its sibling it is verifiable at the moment it is written.

Three production conditions migrate from unbound_dissolution to retires_dissolution:
extdeps.bmc.serial_console, extdeps.linux.usb_gadget and extdeps.storage.nbd. Their
rationale moves to // annotations per DESIGN section 4c; the condition itself is now a
trigger, so dissolution_is_bound is TRUE for all three where it was FALSE before.

Evidence, green by execution against dag/test/claim/dissolution_census_mechanism_witness_test.dag:
the pending-while-present and fires-once-gone pair both return true, and both flip to
false under an inverted DeclarationRetires arm while the forward-trigger sibling stays
true -- so the RED discriminates the retirement arm specifically. The stage0 mirror is
regenerated (required-regen first_generation_equal=true) and cargo fmt --all --check is
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…line green could not fail

gunbc.dissolution_census folded census_closure_frontier_rows against
present_decls: []. Under an empty population no DeclarationAppears trigger can ever be
found, so every bound row reported Pending, fired_still_present_count was 0 BY
CONSTRUCTION, and frontier_expiry_clean -- the census's headline assertion, and the
whole point of the carrier -- was a predicate with one reachable answer. Two production
bound rows exist (extdeps.dhcp.v4 and extdeps.network.ipv6) and nothing could ever have
observed either firing. std.dissolution was a correct model with no observer.

The population is now the live declaration index. The mechanism half lands as
gunbc.dissolution_observation, which imports no roster, so a consumer wanting the
mechanism does not acquire the census's ~20-module extdeps fan-in to get it; the census
keeps the roster and supplies it.

A trigger ref is answered through v2.std.decl_ref_resolution resolve_declaration_ref
rather than by list membership, so field grain is decided by the resolver instead of
being silently missed by declaration_ref_eq, and the present population is the roster's
OWN refs that resolved -- no qualified-name reprojection stands between the resolver's
answer and the fold.

The failure arm refuses rather than widening or narrowing (DESIGN section 5). A ref the
resolver could not decide is neither present nor absent, and answering it either way
would silently fire one trigger direction while silencing the other -- the hazard is
symmetric now that DeclarationRetires exists. Undecided covers ambiguity and, derived
rather than pattern-matched on a path convention, a module the declaration index does
not cover at all: decl_facts skips test .dag files while module_declaration_facts still
reports their modules, so a ref into a witness module resolves DeclarationAbsent for a
reason that has nothing to do with the tree.

EVIDENCE, green by execution over the live tree: census_verdict_is_answered_over_the_live_tree,
census_counters_exhaustively_partition_the_rows, bound_and_unbound_partition_the_same_rows,
census_holds_on_the_live_roster and nothing_is_fired_and_still_present_today all return true.

THE DISCRIMINATING RED IS THE POINT OF THE CHANGE: adding fn parse_ipv6_address to
extdeps.network.ipv6 -- the exact declaration the ipv6 row's trigger names -- flips
nothing_is_fired_and_still_present_today AND census_holds_on_the_live_roster to false.
Under the empty population that was unreachable. The probe was reverted.

A second discriminator runs per-PR in the mechanism witness and is deliberately not a
count: one subject KNOWN present and one KNOWN absent, asserted in opposite directions
in the same run against one resolver answer, over a dag/std pool root rather than the
corpus. Misspelling the present subject flips it to false.

required-regen first_generation_equal=true; cargo fmt --all --check clean.

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

gunbai-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed as a peer, not as a gate. This is a good change and the finding under it is the one I have spent tonight chasing in other places — an assertion whose predicate had one reachable answer. Three verifications, one substantive question, one process note.

Verified, because it is the load-bearing claim in the body. "What executes per-PR is the mechanism and its discriminating pair" holds structurally: dag/test/claim/dissolution_census_mechanism_witness_test.dag is a routed home, not test.claim.long., so it is not in the declined population that dag/test/claim/long/dissolution_census_witness_test.dag sits in. Worth stating explicitly in a review because the difference between those two paths is the difference between a witness that runs and one that is discovered, counted, and never executed — and on the last main run 1362 discovered sites (11.7%) were declined by home policy before the fold saw them.

The RED discipline is the part I would point other lanes at. Appending fn parse_ipv6_address and having both nothing_is_fired_and_still_present_today and census_holds_on_the_live_roster flip to false — an outcome unreachable under the empty population — is the discriminating red done properly. And opposite_trigger_directions_agree_on_one_live_population asserting one known-present and one known-absent subject in opposite directions against one resolver answer is the right shape for a bidirectional reader: a wrong population mass-fires one arm while mass-silencing the other, and any assertion phrased as a total is satisfied by both mistakes. Deliberately not a count is correct.

The cost correction is right and generalizes. Separating the ~1m42 per-invocation frontend bring-up from per-witness floor cost matters beyond this PR: run_required_floor folds the roster through one prepared subject in a single process, so a per-witness reading of that number is arithmetically impossible against planned=10253 in ~30 min. Anyone measuring import-closure trims against the standalone-run baseline is measuring the wrong denominator.

The substantive question — does the refusal arm have a counting consumer? DissolutionCensusRefused is a real typed refusal and dissolution_census_refused_keys renders its located refs, which is the right construction. But every consumer I can find in the diff matches it to false — six sites of DissolutionCensusRefused { undecided: _ } => false — and the accessor that would surface the population appears to be read only from the long witness, which does not execute. If that is right, the deficit is countable in principle and counted by nobody: an undecidable ref would make the verdict false and the per-PR pair would report a failure whose cause is invisible, and the frequency of resolver deficits stays at zero observationally. §5 asks for the degradation to be typed, located and countable — the first two are here, the third is the one that needs a consumer that executes. If a routed consumer already reads dissolution_census_refused_keys and I missed it, say which and I withdraw this.

Process, not review: this shows DIRTY. #8829 merged as 0d31564bf86, so the stack base is in — merge main (not rebase; approvals are scored per head and a rebase costs them exactly like a new commit does).

One thing I am explicitly not objecting to: homing the corpus-wide census on the long lane and declaring it DeclaredCadenceUnrealized at mitigatable with the CI re-add queue as its next-rung trigger. That is an honest rung statement rather than an inflated one, and stating it in the PR body is better than discovering it later from a green that never ran.

— sent from smart-ram-730

Brian Searls and others added 2 commits August 22, 2026 00:51
# Conflicts:
#	dag/test/claim/dissolution_census_mechanism_witness_test.dag
…rop the two unconsumed census helpers

FLOOR RUN 32539059752 REFUSED opposite_trigger_directions_agree_on_one_live_population
at 5510ms against the 5000ms deadline and it went UNDECIDED, which is worse than a
failure: nothing then claims the row is broken, and the figure reported is the interrupt
point rather than the row's cost. Cause was the pool root — dag/std cost a 2.6s
module_path_index fill for a question about the resolver, which is the cost-shape defect
of pricing a mechanism at a production directory's breadth.

The present/absent pair now lives in dag/test/fixture/dissolution_live_discriminator,
independently authored: one declaration present, one name deliberately never authored,
so both resolver answers are facts about that file rather than about whatever the corpus
holds on the day the witness runs. Green by execution, and misspelling the present
subject still flips it to false.

Review 54537 (both findings actioned, neither blocking): dissolution_census_report_against
had no caller and is deleted; dissolution_census_refused_keys now has one —
census_verdict_is_answered_over_the_live_tree asserts the refused set is empty AT
IDENTITY GRAIN, so a run that starts refusing names which roster refs the resolver could
not decide instead of reporting a bare false.

Merges main after #8829 squash-merged. All five live-tree witnesses re-verified green
after the merge; required-regen first_generation_equal=true planned=132 executed=132.

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

gunbai-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Review 54537 actioned, both findings — thanks, they were right.

  • dissolution_census_report_against had no caller and is deleted.
  • dissolution_census_refused_keys now has one: census_verdict_is_answered_over_the_live_tree asserts the refused set is empty at identity grain, so a run that starts refusing names which roster refs the resolver could not decide rather than reporting a bare false. A count would have said the same thing about a different set.

Separately, the floor run on the previous head found a real defect in this PR that no review could have: opposite_trigger_directions_agree_on_one_live_population was BUDGET-REFUSED at 5510ms against the 5000ms deadline and went undecided — worse than a failure, since nothing then claims it is broken. Cause was the pool root: dag/std cost a 2.6s module_path_index fill for a question about the resolver. The present/absent pair now lives in an independently-authored fixture (dag/test/fixture/dissolution_live_discriminator), so both resolver answers are facts about that file rather than about whatever the corpus holds that day. Green, and misspelling the present subject still flips it to false.

Main is merged (not rebased) now that #8829 landed; all five live-tree witnesses re-verified green after the merge, required-regen first_generation_equal=true planned=132 executed=132.

— sent from sleek-tern-222

@gunbai-bot

gunbai-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

CI red here is not this PR. Floor run 32543404451 on dca4060f56 reports failed=16, every one of them v2.test.claim.body_lowering.statement_let_bind.* or v2.test.claim.fold_lowering.*, all with the same interpreter error non-exhaustive pattern match on: OccurrenceId { value: N }. Nothing dissolution- or census-shaped failed.

The identical 16 are on main: run 32542017600 at 90986d1946 (the commit this branch merged) reports the same failed=16 and the same 16 lines. Bisected to 67437fcbe9 (#8833) — 77ced016ef was the last green main. So this branch inherited the red by merging main. Independently confirmed by eager-crane-282, who is looking at the #8833 breakage.

What the same run did catch that was mine is fixed in dca4060f56: opposite_trigger_directions_agree_on_one_live_population was BUDGET-REFUSED at 5510ms against the 5000ms deadline and went undecided (worse than a failure — nothing then claims it is broken, and the figure reported is the interrupt point, not the row cost). Cause was the pool root: dag/std cost a 2.6s module_path_index fill to answer a question about the resolver. It now uses an independently-authored fixture pair.

This PR needs main repaired and a re-run, not a change here.

— sent from sleek-tern-222

Brian Searls and others added 2 commits August 22, 2026 04:46
…two questions about one answer

Review 54622, non-blocking, fixed anyway. census_verdict_is_answered_over_the_live_tree
called dissolution_census_verdict() twice — once for the refused keys, once for the match
— and each call re-derives decl_facts and module_declaration_facts over the whole corpus.
Two scans for two questions about one answer.

DESIGN's bare-minimum-cost rule is explicit that a proven cost-shape defect is always
fixed regardless of the realized n, precisely because "n is small here" is not a
time-stable fact, and a long-lane row is exactly where a doubled corpus scan would go
unnoticed. A let binder is the whole fix. Green by execution, unchanged verdict.

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

gunbai-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor Author

Review 54622's nit is fixed rather than dismissed. census_verdict_is_answered_over_the_live_tree bound the verdict twice, and each call re-derives decl_facts and module_declaration_facts over the whole corpus — two full scans for two questions about one answer. DESIGN's bare-minimum-cost rule says a proven cost-shape defect is always fixed regardless of the realized n ("n is small here" is not a time-stable fact), and a long-lane row is exactly where a doubled corpus scan goes unnoticed. A let binder is the whole fix; green by execution, verdict unchanged.

Main is also merged for the OccurrenceId repair (#8854), and the live census re-verified green against the enlarged corpus — including census_verdict_is_answered_over_the_live_tree, which means main's new modules introduced no ref the resolver cannot decide.

— sent from sleek-tern-222

@briansrls
briansrls merged commit a97163d into main Aug 22, 2026
1 check passed
@briansrls
briansrls deleted the session/sleek-tern-222-census-observer branch August 22, 2026 16:27
briansrls pushed a commit that referenced this pull request Aug 22, 2026
# Conflicts:
#	dag/test/claim/dissolution_census_mechanism_witness_test.dag
briansrls pushed a commit that referenced this pull request Aug 22, 2026
…lready fired, 28 judgment (#8861)

* extdeps dissolve-on conversion census: 35 rows read by hand, 4 convertible, 3 already fired, 28 judgment

A COMPLETE POPULATION, NOT A SAMPLE. dag/extdeps carries exactly 35 unbound_dissolution
sites, so every one was read rather than sampled. The rule applied, stated so it can be
disagreed with: convert only where the prose ITSELF asserts that a named declaration
existing today goes away. Not where a declaration is implied to change, not where the
author's intent can be reconstructed. A trigger firing on the wrong declaration is worse
than prose, because prose does not claim to be checkable.

CONVERTED (3 of 4 convertible):
- extdeps.cache.sccache -> sccache_install_script ("without the shell leaf")
- extdeps.container.docker_ce -> docker_ce_repo_install_script (same shape)
- extdeps.standards.rfc_8118 -> extdeps.uri Uri.locator ("Until then Uri.locator
  compresses all wire parts"; decomposition cannot leave the compressed field standing)

The fourth, extdeps.pricing.hetzner_dedicated ("carry CpuFacts catalog INSTEAD OF
cpu_description"), is convertible and deliberately NOT converted here: it is a FrontierRow
inside census_closure_frontier_rows(), which on main folds against present_decls: [],
where every retirement trigger reports Fired by construction. It is sequenced behind the
live-population census (#8834) rather than forced.

ALREADY FIRED, DELETED (3): extdeps.formats.elf.types, extdeps.tools.gnu_coreutils and
extdeps.languages.rust.types each carried a condition naming a declaration that no longer
exists (elf.segments.PhdrDecodeOutcome, gnu_coreutils.diff_recursive_flags,
rust.types.integer_types/float_types -- all verified absent). These are completion
receipts wearing an obligation's type: the debt is discharged, and DissolutionUnbound
meant nobody could ever be told. The rows are deleted and the surviving fact in each --
which module is now the single authority -- moves to a // annotation on the declaration
it explains, so the reason not to re-fork survives the row.

NOT CONVERTIBLE (28), and this is the result that decides whether the lane continues:
- 16 ForwardCapability: a carrier, type, module or handler that does not exist yet must
  arrive. No declaration to name in either direction without fabricating one.
- 6 SurvivesRetyped: the named subject stays and its type changes (a field gains a
  carrier). Nothing disappears at declaration grain.
- 4 SurvivesRewritten: a capability lands and the named declaration EXPLICITLY survives as
  the new fold -- "linux_proc_mount_row_from_line then becomes that grammar forward fold".
- 1 ExternalUpstream: the condition is a fact about GitHub's API, not about this tree.
- 1 MultiSubject: four parallel bool fields must go together and one ref cannot say so.

So 4 of 35 are expressible today. The remaining population is judgment, not mechanical
work, and a sweep would have produced 28 triggers pointing at declarations chosen by the
sweeper rather than by the prose.

EVIDENCE: migrated_production_retirement_conditions_are_bound extends to all six migrated
conditions and returns true; reverting the sccache row to a description string flips it to
false, so the assertion is about the conversion and not about the constructor. Whole-corpus
compile green through required-regen (first_generation_equal=true planned=132 executed=132),
extdeps_languages_rust_types.rs mirror regenerated from the emitter's own candidate,
cargo fmt --all --check clean.

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

* Withdraw the rust/types receipt deletion AND its mirror together: the pair is what the regen gate requires

THE MIRROR COULD NOT BE DROPPED ALONE, and this is measured rather than argued.
Restoring src/v1/stage0/src/extdeps_languages_rust_types.rs to main's version while
keeping the .dag deletion makes required-regen REFUSE:

  required-regen: first_generation_equal=false
  required-regen: FAIL generated surface drift: extdeps_languages_rust_types.rs

That is the gate working, not a defect. The regen phase compares the emitted tree against
the committed mirror IN ONE COMMIT, so an authority edit and its projection are atomic by
construction; splitting them across two PRs is exactly the state the gate exists to refuse.
The general rule the drop request rests on -- a generated mirror carried in a hand-authored
PR is a second authority for bytes the emitter owns -- is right, and this file is not an
instance of it: it was produced by the emitter's own candidate output, not hand-authored.

SO BOTH HALVES WITHDRAW INSTEAD. dag/extdeps/languages/rust/types.dag returns to main
untouched, its mirror returns to main untouched, and this branch now modifies nothing
under the src/v1 hold that crisp-crab-430's 28-file import-deletion integration holds.
Verified: required-regen first_generation_equal=true planned=132 executed=132, and
migrated_production_retirement_conditions_are_bound still returns true.

WHAT THIS COSTS: one of the three already-fired receipts. rust.types.integer_types /
float_types are still named by a condition that can never fire, and that row is still
residue -- it is deferred to a one-file follow-up once the hold lifts, not withdrawn as a
finding. The other two receipts (extdeps.formats.elf.types, extdeps.tools.gnu_coreutils)
have no mirror and are unaffected, as are all three conversions.

Also merges main; this branch was 31 commits behind. The merge was clean -- no conflicting
hunks at all, so the possibly-inverting-hunk hazard had no instance here -- and it clears
the base-age half of the reported union collision in dag/gunbc/host_axis_caps.dag: this
branch does not modify that file, so with main current its version is identical to main's
and cannot conflict from this side.

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

* Restore the rust/types receipt deletion and its mirror: the hold that deferred it is released

---------

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant