From fb969877a57d41e3de32e8cbe6073de800268a29 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 11 Sep 2026 11:35:31 +0000 Subject: [PATCH 1/4] Scope the jointly-incompatible-open-PR mitigation: a claim-channel x surface join over the declaration index, advisory-first Three 2026-09-10 reds on main were pairs of individually-green, textually disjoint PRs whose union does not compile. This records the scope of a mitigation and builds nothing, answering the four questions the brief asked before any shape: THE CUT. All three public instances are one finding kind, ImportMemberAbsent, and only one was a deletion: #10865 was a rename, and #10923 dropped a RE-EXPORT with no declaration deleted anywhere. The cut is therefore the surface predicate the declarations rider already applies, import_surface_has (declared | variants | reexported) as a base-minus-head DELTA, not the kind of edit. The general case is every claim channel the index carries -- imports, citations, rostered rows (#10769, #10718 are the same shape through the other two) -- and the residuals the index cannot see are named: field rename, arity/signature, and pure freshness (private #49 x #50). THE COST. namespace_wave_admission already reconstructs a base index beside the head index from only the diff-touched files, per run, in the witnesses lane; the join is A(I) & R(D) minus the claims D itself retires, a set intersection, not a pairwise compile. Measured retrospectively over 374 first-parent commits since 2026-08-28: the raw join is a 207-hit candidate list (the superset trap); with the one exclusion it is 5 hits, 4 of them the three brief instances, the fifth an ordering artefact an open-PR-set join excludes by construction. Recall 3/3 on the window's ImportMemberAbsent reds. THE PLACEMENT. No new job (roster closed): a rider on the parse phase, like rostered_row_join, advisory, reporting and annotating without pushing a phase failure; the one emitted-workflow change is a pull-requests: read permission row. The freshness caveat of a push-time verdict is stated as the reason this sits beneath the ceiling. THE REFUSAL. One finding per removed entry x claiming site: the (module, name) and which surface it left, the sibling PR and head, the claiming module, in_declaration and SourceLocation; NotEvaluated when the open-PR set is unobservable, never an empty hit set. gunbc-private: no parse phase, the overlay is outside DAG_PARSE_SWEEP_ROOTS, so the join is public-only today; 113/114 private modules import from public, a cross-repo exposure no public PR is ever compiled against, named and left to warm-badger-62's lane. Ceiling stays with gunbc.plans.ci_merge_freshness and the landed, deferred receipt_is_admissible; this record retires with it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QzQcQRu3WKxJuWV2zaiHSW --- dag/gunbc/plan_registry_batch_g.dag | 4 +- .../joint_incompatibility_claim_join.dag | 143 ++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 dag/gunbc/plans/joint_incompatibility_claim_join.dag diff --git a/dag/gunbc/plan_registry_batch_g.dag b/dag/gunbc/plan_registry_batch_g.dag index 085156e946d..d4aa4f0fac6 100644 --- a/dag/gunbc/plan_registry_batch_g.dag +++ b/dag/gunbc/plan_registry_batch_g.dag @@ -22,6 +22,7 @@ import gunbc.plans.extdeps_modeling_preflight { extdeps_modeling_preflight_plan import gunbc.plans.discrete_cost_derivation { discrete_cost_derivation_plan } import gunbc.plans.floor_claim_cost_distribution { floor_claim_cost_distribution_plan } import gunbc.plans.v2_complexity_capability_parity { v2_complexity_capability_parity_plan } +import gunbc.plans.joint_incompatibility_claim_join { joint_incompatibility_claim_join_plan } data batch_g_plans: List = [ ci_humming_plan, @@ -44,5 +45,6 @@ data batch_g_plans: List = [ deploy_convergence_observed_side_plan, v2_corpus_self_host_plan, import_namespace_program_plan, - floor_claim_cost_distribution_plan + floor_claim_cost_distribution_plan, + joint_incompatibility_claim_join_plan ] diff --git a/dag/gunbc/plans/joint_incompatibility_claim_join.dag b/dag/gunbc/plans/joint_incompatibility_claim_join.dag new file mode 100644 index 00000000000..52b725c25a5 --- /dev/null +++ b/dag/gunbc/plans/joint_incompatibility_claim_join.dag @@ -0,0 +1,143 @@ +module gunbc.plans.joint_incompatibility_claim_join + +import std.dissolution { unbound_dissolution } +import gunbc.plan { Plan, PlanRetirement, PlanRetiresWhen, PlanIsAuthorityOnly } +import std.markdown { MarkdownBlock, TableBlock, AlignNone } +import gunbc.plans.md_helpers { h2, h3, p, li, ul, quote, cell, row } + +fn status_block() -> MarkdownBlock { + quote(blocks: [ + p(text: "**Status: SCOPE RECORD (checkpoint 0) — nothing is built.** Answers the four questions the 2026-09-11 brief asked before any mechanism may be proposed: the cut, the cost, the placement, the refusal. The operator picks a checkpoint in §7. This record is deliberately NOT a rung drop and NOT a restatement of `gunbc.plans.ci_merge_freshness`: that record owns the class's ceiling and already carries instance 1 as its §7; this one scopes a mitigation beneath that ceiling and cites the ceiling rather than re-arguing it."), + ]) +} + +fn section_1() -> List { + [ + h2(text: "1. The class, and where it already lives"), + p(text: "Two individually-correct PRs, both approved, textually disjoint, `merge-tree` clean — jointly incompatible. Each PR's CI compiles ITS head against `main` at the moment its run started, never against its siblings, and squash-merge composes the two trees for the first time at the second merge. The first evidence is a third party's inherited red, and that party's first hypothesis is that the defect is theirs. Three occurrences on 2026-09-10 in gunb-ai/gunbc cost about an hour each."), + p(text: "**This is the stale-green class, not a new one.** `gunbc.plans.ci_merge_freshness` §2 names the hole (freshness, not coverage), §3 names the fix (a merge-time freshness requirement), and §7 already records instance 1 (#10933 × #10934). `gunbc.merge_admission` `receipt_is_admissible` is the landed predicate — a receipt is admissible only when its base tree hash equals the merge target's and its gate-roster hash is current — and it is held at `GatingComputedDeferred` behind the `gunbc.ci_failure_class` prerequisites recorded in that plan's §6. Under that predicate the class is unwritable by construction and everything scoped here is redundant. So the honest frame is: **the ceiling exists, is landed, and is deferred by ruling; this record scopes a cheaper partial mitigation for the interval, at a lower rung, and says which subclass it covers.**"), + p(text: "The third occurrence in the brief (gunbc-private #49 × #50, an hour-long run computed against a base #49 had already moved) is the freshness class with NO name-level signature at all — no join over any index sees it. It is listed here so that nobody cites this mitigation as covering it; it belongs entirely to the ceiling."), + ] +} + +fn section_2() -> List { + [ + h2(text: "2. Q1 — the cut: not deletion-versus-import, but claim-channel-versus-surface"), + p(text: "The brief asked whether the deletion/import asymmetry is the right cut or whether renames, narrowed variants and signature changes make it a partial wall that will be cited as coverage. Read from the evidence rather than the proposal, all three public instances are ONE finding kind — `DeclarationIntegrityKind::ImportMemberAbsent` from the declarations rider of the parse phase — and only one of the three was a deletion:"), + TableBlock { + header: row(cells: [cell(text: "Instance"), cell(text: "Removing side"), cell(text: "Claiming side"), cell(text: "What shrank")]), + alignments: [AlignNone, AlignNone, AlignNone, AlignNone], + rows: [ + row(cells: [cell(text: "#10933 × #10934"), cell(text: "#10934 deletes `DurableOriginRead`, `DisposableCacheRead` from `gunbc.fabric_m0_commit`"), cell(text: "#10933 adds `test.claim.fabric_m0_publication_witness` importing both"), cell(text: "`variants` — a DELETION")]), + row(cells: [cell(text: "#10865 × colo NJ census"), cell(text: "#10865 renames `grain_admits_single_cabinet` to `grain_offers_single_cabinet`"), cell(text: "`test.claim.colo_nj_central_south_census` imports the old name"), cell(text: "`declared` — a RENAME, whose removal half is what fired")]), + row(cells: [cell(text: "#10925 × #10923"), cell(text: "#10923 rewrites `gunbc.secret_provision_actuator` and drops its OWN import of `mutation_status_is_commit_ambiguous` from `extdeps.transports.rest`"), cell(text: "#10925 imports that name FROM `gunbc.secret_provision_actuator`, which only re-exported it"), cell(text: "`reexported` — NO declaration was deleted or renamed anywhere")]), + ], + }, + p(text: "So the right cut is the predicate the declarations rider already applies, `declaration_index` `import_surface_has` — `declared ∪ variants ∪ reexported` at `(module, name)` grain — taken as a DELTA: a PR's **surface removals** are the pairs in `import_surface_has` at its base and not at its head, whatever the edit that produced them (deletion, rename, re-export drop, a variant narrowed away). Keying on the edit kind would have caught one instance of three. Keying on the surface catches all three with one rule, and it is the same rule that reports the red on `main` afterwards, so the advisory and the eventual failure name the same fact."), + h3(text: "2.1 The general case is every claim channel the index carries, not only imports"), + p(text: "The window's other `main` repairs are the same shape through a different consumer channel: #10769 followed a RENAMED WITNESS in the cost-debt roster (a rostered row naming a `test fn`), and #10718 repaired planted-control absences (citations naming declarations). `ModuleDeclarationRecord` already carries three cross-module name-claim channels — `imports` (member claims), `cited` (`DeclarationRef` citations, checked by `cited_symbol_findings`), and the rostered rows joined by `rostered_row_join` — and each is already an identity join against the same surface in the parse phase. The join scoped here is therefore ONE rule applied per channel: **claims one open PR ADDS in channel C, against surface entries another open PR REMOVES, minus the claims the removing PR itself retires.** Imports are the first channel because they are the channel all three instances used and the one with a located member-grain claim; citations and rostered rows are the same join with the record's other two fields and are named here so the first landing does not read as the whole."), + h3(text: "2.2 What the index cannot see, named as residual rather than left to be found"), + ul(items: [ + li(text: "**Field removal or rename** (the `observed_digest` → `reading` half of instance 1, which the declarations run never reached because it stopped on the imports). The removed side IS derivable — `decl_fields` gives each declaration's one-level field names, base minus head. The claiming side is NOT: the record's `referenced` set is an over-collecting name-occurrence walk (binders and labels land beside references), so a field-grain join yields a candidate list, not a finding — the superset-check trap of §5. Next-rung trigger: a resolved field-access channel in the record, which is the resolver's job and out of this scope."), + li(text: "**Arity and signature change.** The index records no parameter lists, so a caller in PR A of a function PR B re-signed is invisible to any join over it. Not derivable at this rung; this is the type wall's subject (`gunbc.rung_drop` records the application-argument typecheck as off), and naming it here keeps this record from being cited as covering it."), + li(text: "**Freshness without a name signature** (private #49 × #50) — outside every join; ceiling only."), + ]), + p(text: "**Rung honesty.** With the import channel landed this is a *mechanically preventable* (rung 2) partial over the `ImportMemberAbsent` subclass, and it must be reported as exactly that — the minimum across the class's paths stays where the ceiling record puts it until `receipt_is_admissible` gates. A wall over a declared subclass is admissible under §4b; the same wall cited as coverage of the class is the inflation §4b(1) forbids."), + ] +} + +fn section_3() -> List { + [ + h2(text: "3. Q2 — the cost: a set intersection over deltas that already exist, not a pairwise compile"), + p(text: "Three facts, each established from the code or by measurement, decide this before any shape is proposed."), + ul(items: [ + li(text: "**(a) The per-PR delta is already computed, per run, in the witnesses lane.** `namespace_wave_admission` `evaluate` reconstructs a BASE `DeclarationIndex` beside the head index by carrying every head record the `git diff --name-status` did not touch and re-parsing only the touched paths at the merge base (with absence established from `ls-tree`, never inferred from a read failure). A PR's surface removals and added import claims are two set differences over records that function already holds. No new parse, no new ingestion."), + li(text: "**(b) The join is an intersection, not a product.** For a PR D with removals R(D) and each open sibling I with added claims A(I), the hit set is `A(I) ∩ R(D)` minus the claims D's own diff retires. Cost is linear in the summed delta sizes. Nothing is compiled pairwise; nothing is compiled at all beyond what the parse phase already does. The O(N²)-compile shape the brief feared is simply not the shape the index affords."), + li(text: "**(c) What is NOT free is obtaining the siblings' deltas**, because no run publishes its delta and no cross-run state exists. Two routes: RECOMPUTE — fetch each open PR's head (`refs/pull/N/head`, one `git fetch`), diff each against its merge base, parse the touched `.dag` files at both sides with `base_records`, and take the two differences; 30 open PRs today, tens of touched files each, parse-only, seconds. Or READ — each run publishes its own delta as a small artifact and a later run joins the published set; cheaper per run, but cross-run state with staleness semantics of its own, and no producer for it exists. Route RECOMPUTE is the honest first cut: it reads only facts git already holds and needs no new store."), + ]), + h3(text: "3.1 Measured on the last fourteen days of `main` (the scoping instrument)"), + p(text: "A retrospective join was run over the 374 first-parent commits on `origin/main` since 2026-08-28, computing per commit the surface removals and added import claims from the touched `.dag` files under the three sweep roots, and joining every pair within three days of each other. The instrument is a text-level approximation of `import_surface_has` written for this scope (declaration keywords, coproduct variants, import members); its intended home is a `gunbc` entry over the real index, which checkpoint 1 supplies so the number below is re-derivable rather than transcribed."), + TableBlock { + header: row(cells: [cell(text: "Join"), cell(text: "Pair hits"), cell(text: "Reading")]), + alignments: [AlignNone, AlignNone, AlignNone], + rows: [ + row(cells: [cell(text: "Raw: `A(I) ∩ R(D)` over all pairs"), cell(text: "207"), cell(text: "the superset-check trap: a later deletion that also deletes or edits the importer in the SAME diff is not incompatibility — the remover saw the claim on `main` and handled it. A candidate list, not a detector.")]), + row(cells: [cell(text: "Minus claims the removing commit itself retires"), cell(text: "5"), cell(text: "4 of 5 are exactly the three brief instances (instance 1 is two names). The fifth is an ordering artefact: a THIRD commit retired the claim between the pair, which a join over the OPEN-PR set excludes by construction because a merged PR is not open. Recall over the window's `ImportMemberAbsent` reds on `main`: 3 of 3. No false positive among co-open pairs.")]), + ], + }, + p(text: "So the predicate is discriminating at the one exclusion, and the exclusion is not a tuning knob: it is the statement that the join asks about the MERGED tree, and a claim the remover's own head already retired is not in that tree."), + ] +} + +fn section_4() -> List { + [ + h2(text: "4. Q3 — where it runs: no new job; a rider on the parse phase, advisory"), + p(text: "The required job roster is closed to growth without operator sign-off (DESIGN, Building & checks; `gunbc.rung_drop` `required_gate_bankruptcy`). The established shape for a check that needs no job is the RIDER: `rostered_row_join` runs inside the parse phase because it reads a membership fact only that phase's ingestion carries, and `required_ci_phase_roster` records that a rider owns no lane of its own. This join reads the same index, and its base-side reconstruction is the wave wall's, so it rides where they ride — the witnesses lane, `ParsePhase` — and adds no `RequiredCiPhase` variant."), + TableBlock { + header: row(cells: [cell(text: "Option"), cell(text: "Where the verdict is read"), cell(text: "What it needs"), cell(text: "Verdict")]), + alignments: [AlignNone, AlignNone, AlignNone, AlignNone], + rows: [ + row(cells: [cell(text: "A — parse-phase rider, ADVISORY"), cell(text: "the run log, plus a `::warning::` annotation on the PR's checks tab (the one surface `contents: read` can write)"), cell(text: "the open-PR set: `pull-requests: read` on the emitted witnesses job (`gunbc.witness_floor_workflow` carries `contents: read` only today — a permission row, not a job) and a fetch of `refs/pull/N/head` for each"), cell(text: "**recommended first move**")]), + row(cells: [cell(text: "B — dashboard-side advisory at merge readiness"), cell(text: "where the merge decision is actually made"), cell(text: "the same join, computed outside this repository; no CI cost at all"), cell(text: "the better READER, but out of this repo's authority; named so the operator can choose it")]), + row(cells: [cell(text: "C — required, blocking"), cell(text: "the merge"), cell(text: "a measured hit rate first"), cell(text: "**refused as a first move** (brief); the ceiling gates here instead once its prerequisites clear")]), + ], + }, + p(text: "**The freshness caveat, stated so it is not discovered.** A rider runs at push time, so its verdict is exactly as fresh as the green beside it: it sees the siblings open at that run and cannot see one opened after the PR's last push. Each pair gets two chances (either side's later run) — every instance here had both PRs open at the later side's run — but a PR that sits for hours after its last run, which is the stale-green situation itself, is not re-joined. That residual is the reason this is beneath the ceiling and not a substitute for it."), + p(text: "**Hit-rate instrument.** Advisory-first is honest only if the rate is measured: the retrospective of §3.1, re-expressed over the real index as a `gunbc` entry, is that instrument — run over `main` history it answers precision and recall for any window, without log mining. A checkpoint that flips anything to blocking cites that entry's output, never a count in prose."), + ] +} + +fn section_5() -> List { + [ + h2(text: "5. Q4 — the refusal: typed, located, and never `conflicts with an open PR`"), + p(text: "Per DESIGN §5 the arm names what would break and where. One finding per (removed surface entry × claiming site), never per pair:"), + ul(items: [ + li(text: "**`removed`** — the `(module, name)` this PR's head no longer exports, and WHICH surface it left (`declared`, `variants`, or `reexported`), so a re-export drop reads as what it is."), + li(text: "**`claimed_by`** — the sibling PR number and head sha, the claiming module, the `in_declaration` the claim sits under, and the claim's `SourceLocation` (rel_path, offset) — the same located shape `DeclarationIntegrityFinding` renders today, so the advisory and the eventual `ImportMemberAbsent` on `main` name one site."), + li(text: "**`channel`** — `ImportMember` first; `CitedSymbol` and `RosteredRow` are the same record with the claim taken from the other two fields (§2.1)."), + ]), + p(text: "Three arms, and only the first is a hit. **`NotEvaluated`** when the open-PR set or a sibling head is unobservable — an API failure, a missing ref, a base that does not resolve — reported as ignorance with its cause, never as an empty hit set (the wave wall's lesson: absence is established from a listing, a read failure is not news). **`Clear`** names the denominators — how many open PRs, how many claims joined — so a green is legible as a scope, not silence. Under option A a hit reports and does not push a phase failure; the phase's verdict is unchanged by it, which is what advisory means here and is written on the arm rather than left to a flag."), + ] +} + +fn section_6() -> List { + [ + h2(text: "6. gunbc-private: the same exposure, and the join is public-only today"), + ul(items: [ + li(text: "The private witness workflow (`strategy.private_witness_workflow`) builds the PUBLIC seed at `main`, copies the private `dag/` to `public/private_overlay/dag`, and runs `gunbc run` entries over the composed roots. It has no parse phase and no `claim_executor --required-ci`, so no `DeclarationIndex` is ever built over the private corpus; and the overlay path is outside `DAG_PARSE_SWEEP_ROOTS` (`src/v1`, `dag`, `src/v2`), so even a parse sweep there would not see it. The overlay's own annotation names the dissolution — an extra `--source-root` outside the process workspace root — and that trigger is what makes the same records derivable for private."), + li(text: "Private merges with MERGE COMMITS (`Merge pull request #62`), not squash; the class is the same, since a merge commit composes the trees at the same moment a squash does."), + li(text: "**The larger private exposure is CROSS-REPO, and no join in this record reaches it.** 113 of the 114 private modules import from 77 distinct public modules. A public PR that shrinks a surface is never compiled against private at all — not before merge, not after — and private learns of it only when its own next run resolves against a `main` that has moved. That is the join of §2 with the private corpus standing as one more open sibling whose claims are the whole overlay's import set; it is derivable the day the sweep roots admit the overlay, and it is warm-badger-62's territory (#46 against the workflow, a compiler-hole scope against the public roster), so it is named here and not touched."), + ]), + ] +} + +fn section_7() -> List { + [ + h2(text: "7. Sequencing — the operator picks a checkpoint; none is built by this record"), + ul(items: [ + li(text: "**Checkpoint 1 — the instrument.** A `gunbc` entry over the real `DeclarationIndex` that computes a commit's or a PR's surface-removal and added-claim deltas and joins a set of them, with the §5 finding shape and the retrospective mode over `main` history. Positive control: the three 2026-09-10 pairs as fixtures; discriminating RED: the exclusion arm on a pair whose remover retires the claim. This is the hit-rate instrument of §4 and the only artifact that makes any later number re-derivable."), + li(text: "**Checkpoint 2 — option A, advisory.** The rider in the parse phase joining against the open-PR set via route RECOMPUTE; the one emitted-workflow change is the `pull-requests: read` permission row. Reports, annotates, never pushes a phase failure. Its declared population is the `ImportMember` channel; `CitedSymbol` and `RosteredRow` are the same rider with the other two fields and land as their own checkpoints so each carries its own positive control."), + li(text: "**Checkpoint 3 — blocking, IF EVER.** Only from the instrument's measured precision over a stated window, and only if the ceiling is still deferred; if `receipt_is_admissible` gates first, checkpoint 3 is never built and checkpoint 2 dissolves with it."), + ]), + p(text: "What is refused as a first move, so it is not re-proposed: a new required job (roster closed); blocking on the join (rate unmeasured); a pairwise compile of open PRs (the shape §3 shows is unnecessary); and any framing of this join as the class's wall — the wall is the merge-time freshness requirement, landed and deferred by ruling, and this record retires with it."), + ] +} + +fn joint_incompatibility_claim_join_body() -> List { + concat([status_block()], + concat(section_1(), + concat(section_2(), + concat(section_3(), + concat(section_4(), + concat(section_5(), + concat(section_6(), section_7()))))))) +} + +data joint_incompatibility_claim_join_plan: Plan = Plan { + projection: PlanIsAuthorityOnly { ruling: "gunbc.plans.joint_incompatibility_claim_join — no markdown for this slug has ever been committed at docs/plans/joint-incompatibility-claim-join.md; the .dag carrier is the authority, as a295e17415 ruled for its siblings." }, + slug: "joint-incompatibility-claim-join", + title: "[SCOPE] Jointly-incompatible open PRs — a claim-channel × surface join over the declaration index, advisory-first", + body: joint_incompatibility_claim_join_body(), + retirement: PlanRetiresWhen { condition: unbound_dissolution(description: "Delete this record when either holds: the merge-time freshness requirement of gunbc.plans.ci_merge_freshness §3 is in force as a witnessed property (receipt_is_admissible gating), at which point every join scoped here is redundant and any landed rider is deleted with this record; or checkpoint 2 has landed and its instrument has measured a window, at which point this scope is superseded by the rider's own carrier and the plan is redundant.") + } +} From 3bda40377ee0662f5c061ae69458abf4734fbfff Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 11 Sep 2026 11:57:46 +0000 Subject: [PATCH 2/4] chore: regenerate drifted generated artifacts (ci auto-heal) Ledger-Repair-Judged: docs/design-rung-drops.md --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a656728c455..d0584c4f325 100644 --- a/.gitignore +++ b/.gitignore @@ -213,3 +213,4 @@ src/v2/test/claim/manual/sg2_scratch_probe.dag /docs/plans/v2-complexity-capability-parity.md /docs/plans/v2-corpus-self-host.md /docs/plans/import-namespace-program.md +/docs/plans/joint-incompatibility-claim-join.md From 1ea557ff083af2ad255e859b32f1d8489bfb4fbf Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 11 Sep 2026 12:26:25 +0000 Subject: [PATCH 3/4] Checkpoint 1: joint_claim_join, the instrument behind the scope's figures; the private gap as a typed stall; the within-tree half re-homed Answers review 63772 / 63786 (a transcribed measurement with no producer) and the manager's 2026-09-11 rulings, by landing the producer rather than softening the claim. THE INSTRUMENT. v1_compiler.bin.joint_claim_join computes, per change, the export-surface entries removed (declared | variants | reexported, base minus head) and the import claims added and retired, from only the diff-touched files on both sides through namespace_wave_admission base_records and diff_sides, and joins every admitted pair. A claim is joined only while LIVE when the entry leaves: not retired by the removing change itself, and in retrospective mode not retired by any change that landed between the pair. raw_candidates is reported beside findings so the width the exclusion removes stays a number. Seven fixture-boundary controls, real parser, real join: one positive control per instance shape (variant deleted, name renamed, re-export dropped), one RED per exclusion, one for the pair predicate, one for a claim already at the base. WHAT IT ANSWERS, cited by invocation in the plan and not transcribed: `joint_claim_join 6a54695f6c7^..f078c59b0b4` (first-parent main since 2026-08-28, default 3-day window) reports subjects=373 raw_candidates=236 findings=4 -- exactly the three 2026-09-10 instances (instance 1 is two names). The two candidates the first-exclusion-only run reported as findings were both a third commit retiring the claim between the pair (#10617 for the harness one; the DensityMarketedMax one likewise); two artefacts with one cause was a defect in the retrospective's pair predicate, built in as the second exclusion with its own control rather than described. TWO CORRECTIONS THE INSTRUMENT MADE TO THE SCOPE. Instance 2 (#10923 x #10925) was a MOVE, not a dropped re-export: the base declared mutation_status_is_commit_ambiguous in secret_provision_actuator and #10923 re-homed it; the instrument reports `(declared)` and the plan's table now says so. The dropped re-export stays covered by the surface and keeps its control. THE RULINGS. Checkpoint 2 stops at the pull-requests:read permission row on a required job (the operator's trade); the dashboard reader is costed in section 4.1 and preferred on every axis but authority. The private gap is a typed GuaranteeStall row (joint_claim_join_private_corpus_unindexed_stall) with the overlay's extra-source-root trigger as its grounding, not prose. The within-tree field-set case is NOT a checkpoint here: its home is witness_that_fails_to_compile_is_absent_rather_than_red, and this change appends a receipt there widening the trigger from claim-root files to the ruled capability -- every construction site of a type whose field set changed, regardless of gate closure -- per section 4b(3), rather than minting a second authority. Hand Rust is enumerated in gunbc.joint_claim_join_seed_growth (33 items, no impl block, every one citable) and rostered in seed_growth_admission. Verified remotely: clippy -D warnings clean on the bin, 7/7 tests, the retrospective above, and v1_src_dag_parse over the corpus with every new .dag present: 5461 files parse-clean, no findings. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QzQcQRu3WKxJuWV2zaiHSW --- ...im_join_private_corpus_unindexed_stall.dag | 39 ++ dag/gunbc/guarantee_stall/roster.dag | 2 + dag/gunbc/joint_claim_join_seed_growth.dag | 222 +++++++ .../joint_incompatibility_claim_join.dag | 59 +- ...s_to_compile_is_absent_rather_than_red.dag | 2 + dag/gunbc/seed_growth_admission.dag | 4 +- src/v1/stage0/Cargo.toml | 6 + src/v1/stage0/src/bin/joint_claim_join.rs | 594 ++++++++++++++++++ 8 files changed, 906 insertions(+), 22 deletions(-) create mode 100644 dag/gunbc/guarantee_stall/joint_claim_join_private_corpus_unindexed_stall.dag create mode 100644 dag/gunbc/joint_claim_join_seed_growth.dag create mode 100644 src/v1/stage0/src/bin/joint_claim_join.rs diff --git a/dag/gunbc/guarantee_stall/joint_claim_join_private_corpus_unindexed_stall.dag b/dag/gunbc/guarantee_stall/joint_claim_join_private_corpus_unindexed_stall.dag new file mode 100644 index 00000000000..0c1de95b7ed --- /dev/null +++ b/dag/gunbc/guarantee_stall/joint_claim_join_private_corpus_unindexed_stall.dag @@ -0,0 +1,39 @@ +module gunbc.guarantee_stall.joint_claim_join_private_corpus_unindexed_stall + +import gunbc.guarantee_rung { Mitigatable, MechanicallyPreventable } +import gunbc.guarantee_stall { GuaranteeStall, AwaitsOneGrounding, UncountedNotEnumerable, climbs_when } + +// FILED AS A STALL AND NOT AS PROSE IN THE PLAN THAT FOUND IT (gunbc.plans.joint_incompatibility_claim_join +// section 6), on the manager's 2026-09-11 ruling: an instrument that is blind to almost every module in +// the other repository, and does not say so in a typed way, will later be cited as coverage it never had. +// +// THE SUBJECT. The joint-claim join (v1_compiler.bin.joint_claim_join) reads ModuleDeclarationRecords, +// and a record exists only for a module the .dag parse sweep reaches. gunbc-private's witness workflow +// (strategy.private_witness_workflow) builds the public seed at main and runs gunbc run entries over a +// copied overlay at public/private_overlay/dag -- no parse phase, no claim_executor --required-ci, and the +// overlay path is outside DAG_PARSE_SWEEP_ROOTS (src/v1, dag, src/v2). So no DeclarationIndex covers the +// private corpus, on any run, anywhere. +// +// WHY THE EXPOSURE IS LARGER THAN THE INTRA-REPO ONE. Measured on the private tree at #62 with a text +// scan of import lines against its own module list: 113 of 114 private modules import from 77 distinct +// public modules. A public PR that shrinks a surface is never compiled against private at all -- not +// before merge, not after -- and private learns of it only when its next run resolves against a main that +// has moved. That is exactly the join with the private corpus standing as one more subject whose claims +// are the whole overlay's import set; the join cannot run because the subject has no records. +// +// THE BLOCKER IS ONE GROUNDING AND THE OVERLAY ALREADY NAMES IT. The overlay's own annotation states its +// dissolution: gunbc admits an extra --source-root outside process_workspace_root with module-scope +// annotations attaching as they do for an in-workspace root. The day the sweep admits that root, private +// records exist and this row's trigger is met by the same change. +data joint_claim_join_private_corpus_unindexed_stall: GuaranteeStall = GuaranteeStall { + subject: "the joint-claim join is blind to the gunbc-private corpus: no DeclarationIndex is built over private modules (no parse phase there; the overlay is outside DAG_PARSE_SWEEP_ROOTS), so a public change shrinking a surface a private module claims is joined by nothing, before or after merge", + current: Mitigatable, + ceiling: MechanicallyPreventable, + blocker: AwaitsOneGrounding { + grounding: "the private overlay is not a source root the .dag parse sweep can read: DAG_PARSE_SWEEP_ROOTS is a closed triple of in-workspace roots, and the private workflow copies its dag/ to public/private_overlay/dag precisely because gunbc does not yet admit an extra --source-root outside the process workspace root with module-scope annotations attaching as for an in-workspace root -- the overlay's own declared dissolution trigger", + }, + population: UncountedNotEnumerable { + reason: "every private module that imports a public module, on every future public change that shrinks a surface; open rather than a list, and listing the 113 importing modules here would be a second representation of the private corpus's import edges that rots on its next commit. IT IS MEASURABLE AND THE PREDICATE IS THE INSTRUMENT: joint_claim_join with the private corpus as one subject whose added claims are its whole import set answers it the moment private records exist; until then the count is a text scan of import lines against the private module list (113 of 114 at gunbc-private #62), a scoping figure and not a producer", + }, + next_rung_trigger: climbs_when(capability: "THE PRIVATE OVERLAY IS AN ADMITTED SOURCE ROOT OF THE .dag PARSE SWEEP, so a ModuleDeclarationRecord exists for every private module on the same ingestion the public corpus gets -- sufficient for the private corpus to stand as one subject of joint_claim_join against every public change's removed surface, at which point a public change is joined against private BEFORE it merges rather than discovered by private's next run after it, and this row's class is mechanically preventable on the same instrument as the public one"), +} diff --git a/dag/gunbc/guarantee_stall/roster.dag b/dag/gunbc/guarantee_stall/roster.dag index 1427300c2ca..5c006cff2a3 100644 --- a/dag/gunbc/guarantee_stall/roster.dag +++ b/dag/gunbc/guarantee_stall/roster.dag @@ -68,6 +68,7 @@ import gunbc.guarantee_stall.generic_lambda_parameter_receiver_evidence_stall { import gunbc.guarantee_stall.argument_shape_undecided_at_a_declared_position_stall { argument_shape_undecided_at_a_declared_position_stall } import gunbc.guarantee_stall.information_retrieval_transport_cannot_report_status_stall { information_retrieval_transport_cannot_report_status_stall } import gunbc.guarantee_stall.module_cited_only_in_annotation_prose_stall { module_cited_only_in_annotation_prose_stall } +import gunbc.guarantee_stall.joint_claim_join_private_corpus_unindexed_stall { joint_claim_join_private_corpus_unindexed_stall } // THE COHORT PROVENANCE NOTES BELOW WERE AUTHORED WHEN EVERY ROW SAT IN ONE FILE, and they are // carried here verbatim rather than split across the row modules they describe. Their membership @@ -231,6 +232,7 @@ data all_guarantee_stalls: List = [ argument_shape_undecided_at_a_declared_position_stall, information_retrieval_transport_cannot_report_status_stall, module_cited_only_in_annotation_prose_stall, + joint_claim_join_private_corpus_unindexed_stall, ] // THE FOUR SUBJECTS RESTORED TO THE ROSTER, PINNED BY IDENTITY SO THEIR LOSS REFUSES. diff --git a/dag/gunbc/joint_claim_join_seed_growth.dag b/dag/gunbc/joint_claim_join_seed_growth.dag new file mode 100644 index 00000000000..0b3bee419df --- /dev/null +++ b/dag/gunbc/joint_claim_join_seed_growth.dag @@ -0,0 +1,222 @@ +module gunbc.joint_claim_join_seed_growth + +import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.types { String } +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.seed_growth { SeedGrowthJustification } + +// Seed-growth obligation for the joint-claim join instrument, checkpoint 1 of +// gunbc.plans.joint_incompatibility_claim_join. Homed here, beside the obligation it declares, +// rather than in gunbc.seed_growth_admission, which owns the roster and the join. +// +// WHAT THE CHANGE IS. One cargo bin, v1_compiler.bin.joint_claim_join, that computes for each change +// the two set differences a joint-incompatibility question needs -- export-surface entries removed +// (declared, variants, reexported; base minus head) and import-member claims added or retired -- and +// intersects them across a set of changes, excluding a claim no longer live when the entry left. It +// reuses the parse the wave wall already performs (namespace_wave_admission base_records over +// declaration_index record_from_module) and the diff listing it already trusts (diff_sides); it adds +// no parse, no phase, no job and no permission. Retrospective mode walks a first-parent commit range. +// +// THE EXHIBITED COST, which is what admits this under the v1 PURPOSE test rather than either DESIGN +// trigger: three main reds on 2026-09-10, about an hour each, of a class no check compiles -- two +// individually green, textually disjoint PRs whose union does not compile. The manager's ruling on +// the scope (2026-09-11) was that the raw-candidates-to-findings figure that decides the mitigation is worth acting on +// only if it has an entry point behind it (DESIGN section 6: name the instrument, never transcribe +// its output). This bin is that entry point. It is an instrument that serves the self-host program +// the same way v1_src_dag_parse does: it measures the corpus the program compiles, and its output is +// read by nothing that gates. +// +// CLASSIFIED AGAINST THE FIVE REFUSED CLASSES. NewLanguageBehavior: no -- the compile pipeline is +// byte-untouched. NewCompatibilityObligation: no. NewEscapeHatchOrAdmissionRow: no -- nothing skips +// anything; the bin is not on the required path. SeedFeatureCompletion: no -- the seed compiler gains +// no capability. PublicSurfaceGrowth: no -- a bin contributes no pub mod line to the emitted lib.rs; +// what grows is the SeedRetained roster, enumerated here in full. +// +// NO UNCITABLE ITEM. The one method the first draft carried (a key() on ImportClaimSite) was made a +// free function before this row was written, so every item is nameable as a WholeDeclaration +// DeclarationRef; test-module items are enumerated under the tests submodule path. +// +// RUNG, HONESTLY. The class this instrument measures stays at its ceiling record's rung +// (gunbc.plans.ci_merge_freshness; gunbc.merge_admission receipt_is_admissible, deferred). This bin +// establishes no rung for it: it is executed by nobody on the merge path, and its own evidence is +// the fixture-boundary controls in its tests module -- one positive control per instance shape and +// one discriminating RED for the exclusion -- which run under cargo test, itself off the merge path. + +data joint_claim_join_seed_growth_justification: SeedGrowthJustification = SeedGrowthJustification { + hand_authored_declarations: [ + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "SurfaceKind", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "surface_kind_label", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "ImportClaimSite", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "ClaimKey", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "claim_key", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "ClaimDelta", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "surface_of", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "claim_sites", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "claim_delta", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "JointClaimSubject", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "JointClaimFinding", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "JointClaimJoin", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "joint_claim_join", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "render_finding", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "CommitSubject", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "commit_delta", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "usage", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "main", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join", + decl_name: "tests", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "records", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "subject", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "CARRIER_BOTH", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "CARRIER_ONE", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "WITNESS_CLAIMS", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "WITNESS_SILENT", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "all", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "never_between", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "a_claim_retired_by_a_third_change_between_the_pair_is_a_candidate_and_not_a_finding", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "variant_deleted_by_one_change_and_imported_by_another_is_one_located_finding", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "a_remover_that_retires_the_claim_itself_is_a_candidate_and_not_a_finding", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "a_dropped_reexport_is_a_finding_that_names_the_reexported_surface", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "a_rename_fires_on_the_old_name_and_not_on_the_new_one", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "a_pair_outside_the_admitted_window_is_not_joined", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.bin.joint_claim_join.tests", + decl_name: "a_claim_already_present_at_the_claimer_base_is_not_added", + field: WholeDeclaration + } + ], + reason: "Three 2026-09-10 main reds were pairs of individually green, textually disjoint PRs whose union does not compile, and the join that would have named them -- surface entries one change removes against import claims another adds -- is a set intersection over records the parse phase already builds. The scope record that priced the mitigation rests on the width between the raw candidate count and the finding count; a figure worth acting on needs a producer, and this bin is it. Rust rather than .dag because the two inputs it joins -- a base-side record reconstructed from a git blob and a diff listing -- are host facts the seed already owns in namespace_wave_admission, and a .dag realization would re-derive both through a host effect that does not exist yet.", + owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, + trigger: "Delete every row when EITHER holds: the merge-time freshness requirement of gunbc.plans.ci_merge_freshness section 3 is in force as a witnessed property (receipt_is_admissible gating), at which point the class is unwritable by construction and the instrument measures nothing that can occur; OR the per-change delta (removed surface, added and retired claims) is a modeled operation over a modeled base-side read, at which point the join is a .dag fold over two declared inputs and this host has nothing left to do. What does NOT dissolve them: a rider consuming this join in the parse phase (that is checkpoint 2, a consumer, not a replacement), or the figure being cited in the plan (a citation is not a producer).", + current_boundary: "src/v1/stage0/src/bin/joint_claim_join.rs; src/v1/stage0/Cargo.toml [[bin]] joint_claim_join; dag/gunbc/joint_claim_join_seed_growth.dag" +} diff --git a/dag/gunbc/plans/joint_incompatibility_claim_join.dag b/dag/gunbc/plans/joint_incompatibility_claim_join.dag index 52b725c25a5..f79d41ccc81 100644 --- a/dag/gunbc/plans/joint_incompatibility_claim_join.dag +++ b/dag/gunbc/plans/joint_incompatibility_claim_join.dag @@ -7,7 +7,7 @@ import gunbc.plans.md_helpers { h2, h3, p, li, ul, quote, cell, row } fn status_block() -> MarkdownBlock { quote(blocks: [ - p(text: "**Status: SCOPE RECORD (checkpoint 0) — nothing is built.** Answers the four questions the 2026-09-11 brief asked before any mechanism may be proposed: the cut, the cost, the placement, the refusal. The operator picks a checkpoint in §7. This record is deliberately NOT a rung drop and NOT a restatement of `gunbc.plans.ci_merge_freshness`: that record owns the class's ceiling and already carries instance 1 as its §7; this one scopes a mitigation beneath that ceiling and cites the ceiling rather than re-arguing it."), + p(text: "**Status: SCOPE RECORD, with checkpoint 1 (the instrument) LANDED beside it and nothing else built.** Answers the four questions the 2026-09-11 brief asked before any mechanism may be proposed: the cut, the cost, the placement, the refusal. The manager ruled 2026-09-11: checkpoint 1 yes; checkpoint 2 stops at the permission row and prefers the dashboard reader; the within-tree field case is an already-filed row's trigger, not a checkpoint; the operator picks the rest in §7. This record is deliberately NOT a rung drop and NOT a restatement of `gunbc.plans.ci_merge_freshness`: that record owns the class's ceiling and already carries instance 1 as its §7; this one scopes a mitigation beneath that ceiling and cites the ceiling rather than re-arguing it."), ]) } @@ -23,26 +23,30 @@ fn section_1() -> List { fn section_2() -> List { [ h2(text: "2. Q1 — the cut: not deletion-versus-import, but claim-channel-versus-surface"), - p(text: "The brief asked whether the deletion/import asymmetry is the right cut or whether renames, narrowed variants and signature changes make it a partial wall that will be cited as coverage. Read from the evidence rather than the proposal, all three public instances are ONE finding kind — `DeclarationIntegrityKind::ImportMemberAbsent` from the declarations rider of the parse phase — and only one of the three was a deletion:"), + p(text: "The brief asked whether the deletion/import asymmetry is the right cut or whether renames, narrowed variants and signature changes make it a partial wall that will be cited as coverage. Read from the evidence rather than the proposal, all three public instances are ONE finding kind — `DeclarationIntegrityKind::ImportMemberAbsent` from the declarations rider of the parse phase — and only one of the three was a deletion; the others were a rename and a move:"), TableBlock { header: row(cells: [cell(text: "Instance"), cell(text: "Removing side"), cell(text: "Claiming side"), cell(text: "What shrank")]), alignments: [AlignNone, AlignNone, AlignNone, AlignNone], rows: [ row(cells: [cell(text: "#10933 × #10934"), cell(text: "#10934 deletes `DurableOriginRead`, `DisposableCacheRead` from `gunbc.fabric_m0_commit`"), cell(text: "#10933 adds `test.claim.fabric_m0_publication_witness` importing both"), cell(text: "`variants` — a DELETION")]), row(cells: [cell(text: "#10865 × colo NJ census"), cell(text: "#10865 renames `grain_admits_single_cabinet` to `grain_offers_single_cabinet`"), cell(text: "`test.claim.colo_nj_central_south_census` imports the old name"), cell(text: "`declared` — a RENAME, whose removal half is what fired")]), - row(cells: [cell(text: "#10925 × #10923"), cell(text: "#10923 rewrites `gunbc.secret_provision_actuator` and drops its OWN import of `mutation_status_is_commit_ambiguous` from `extdeps.transports.rest`"), cell(text: "#10925 imports that name FROM `gunbc.secret_provision_actuator`, which only re-exported it"), cell(text: "`reexported` — NO declaration was deleted or renamed anywhere")]), + row(cells: [cell(text: "#10925 × #10923"), cell(text: "#10923 MOVES `mutation_status_is_commit_ambiguous` out of `gunbc.secret_provision_actuator` (its base declared it at line 242) into `extdeps.transports.rest`"), cell(text: "#10925 imports that name from its OLD home"), cell(text: "`declared` of the old home — a MOVE: the declaration survived, its home stopped exporting it (the first draft of this table called this a dropped re-export; the instrument's `(declared)` corrected it)")]), ], }, - p(text: "So the right cut is the predicate the declarations rider already applies, `declaration_index` `import_surface_has` — `declared ∪ variants ∪ reexported` at `(module, name)` grain — taken as a DELTA: a PR's **surface removals** are the pairs in `import_surface_has` at its base and not at its head, whatever the edit that produced them (deletion, rename, re-export drop, a variant narrowed away). Keying on the edit kind would have caught one instance of three. Keying on the surface catches all three with one rule, and it is the same rule that reports the red on `main` afterwards, so the advisory and the eventual failure name the same fact."), + p(text: "So the right cut is the predicate the declarations rider already applies, `declaration_index` `import_surface_has` — `declared ∪ variants ∪ reexported` at `(module, name)` grain — taken as a DELTA: a PR's **surface removals** are the pairs in `import_surface_has` at its base and not at its head, whatever the edit that produced them (deletion, rename, move, a dropped re-export, a variant narrowed away). Keying on the edit kind would have caught one instance of three; the dropped re-export had no instance and has its control in the instrument. Keying on the surface catches all three with one rule, and it is the same rule that reports the red on `main` afterwards, so the advisory and the eventual failure name the same fact."), h3(text: "2.1 The general case is every claim channel the index carries, not only imports"), p(text: "The window's other `main` repairs are the same shape through a different consumer channel: #10769 followed a RENAMED WITNESS in the cost-debt roster (a rostered row naming a `test fn`), and #10718 repaired planted-control absences (citations naming declarations). `ModuleDeclarationRecord` already carries three cross-module name-claim channels — `imports` (member claims), `cited` (`DeclarationRef` citations, checked by `cited_symbol_findings`), and the rostered rows joined by `rostered_row_join` — and each is already an identity join against the same surface in the parse phase. The join scoped here is therefore ONE rule applied per channel: **claims one open PR ADDS in channel C, against surface entries another open PR REMOVES, minus the claims the removing PR itself retires.** Imports are the first channel because they are the channel all three instances used and the one with a located member-grain claim; citations and rostered rows are the same join with the record's other two fields and are named here so the first landing does not read as the whole."), h3(text: "2.2 What the index cannot see, named as residual rather than left to be found"), ul(items: [ - li(text: "**Field removal or rename** (the `observed_digest` → `reading` half of instance 1, which the declarations run never reached because it stopped on the imports). The removed side IS derivable — `decl_fields` gives each declaration's one-level field names, base minus head. The claiming side is NOT: the record's `referenced` set is an over-collecting name-occurrence walk (binders and labels land beside references), so a field-grain join yields a candidate list, not a finding — the superset-check trap of §5. Next-rung trigger: a resolved field-access channel in the record, which is the resolver's job and out of this scope."), + li(text: "**Field removal or rename** (the `observed_digest` → `reading` half of instance 1, which the declarations run never reached because it stopped on the imports). The removed side IS derivable — `decl_fields` gives each declaration's one-level field names, base minus head. The claiming side is NOT: the record's `referenced` set is an over-collecting name-occurrence walk (binders and labels land beside references), so a field-grain join yields a candidate list, not a finding — the superset-check trap of §5. That is the CROSS-PR field case. The WITHIN-TREE field case is different and cheaper, and is answered in §2.3 rather than left as a residual."), li(text: "**Arity and signature change.** The index records no parameter lists, so a caller in PR A of a function PR B re-signed is invisible to any join over it. Not derivable at this rung; this is the type wall's subject (`gunbc.rung_drop` records the application-argument typecheck as off), and naming it here keeps this record from being cited as covering it."), li(text: "**Freshness without a name signature** (private #49 × #50) — outside every join; ceiling only."), ]), p(text: "**Rung honesty.** With the import channel landed this is a *mechanically preventable* (rung 2) partial over the `ImportMemberAbsent` subclass, and it must be reported as exactly that — the minimum across the class's paths stays where the ceiling record puts it until `receipt_is_admissible` gates. A wall over a declared subclass is admissible under §4b; the same wall cited as coverage of the class is the inflation §4b(1) forbids."), + h3(text: "2.3 The within-tree field-set case — NOT subsumed by the surface cut, and ALREADY FILED as another row's trigger"), + p(text: "The manager asked whether the export-surface cut subsumes the rule *a change altering a type's required fields must compile every construction site of that type*. It does not: a field is not a surface entry, so a change that keeps `ReadbackConfirmed` exported while replacing its `observed_digest` field with `reading` removes nothing from any surface and this join is silent on it. Within ONE tree the oracle for that case already exists — the compiler's construction field-set check (whose zero-field escape #10979 closed) refuses a construction naming a field the type no longer declares. What is missing is not a check but REACH: the floor compiles the closures of its discovered entries, so a construction site in a module no entry reaches is compiled by nobody — the field-grain twin of the orphan-module import class `gunbc.declaration_index_seed_growth` records (`an ORPHAN module's import claims are checked by nothing`)."), + p(text: "So the within-tree rule needs no open-PR set, no permission and no cross-PR anything; it is a SELECTION problem the index already answers. The removed side is `decl_fields` base-minus-head per declaration — derivable from the same base reconstruction §3 reuses. The claiming side does not need a precise construction-site channel, because the compile is the oracle and the index only chooses what to compile: every module whose `referenced` or `authored_type_references` names the changed declaration is compiled, and over-collection there costs compile time, never a false finding. It is strictly cheaper than the cross-PR join and within the parse phase's existing reach — and it is NOT a checkpoint of this plan. The class is already filed: `gunbc.recurring_failure_mode` `witness_that_fails_to_compile_is_absent_rather_than_red` (rung 1, ceiling 3), whose second specimen (#11050: two `WorkflowRun` construction sites, a diff that touched neither) is exactly this shape, and whose next-rung trigger is this selection. The scope's one contribution there is a receipt on that row (landed with this change) recording that the trigger text was NARROWER than the ruled capability — claim-root files only, while a construction site can live in a module that is no claim root — and widening it to the ruled width, per §4b(3). Minting a checkpoint here would have been a second authority for a filed class."), + ] } @@ -55,17 +59,18 @@ fn section_3() -> List { li(text: "**(b) The join is an intersection, not a product.** For a PR D with removals R(D) and each open sibling I with added claims A(I), the hit set is `A(I) ∩ R(D)` minus the claims D's own diff retires. Cost is linear in the summed delta sizes. Nothing is compiled pairwise; nothing is compiled at all beyond what the parse phase already does. The O(N²)-compile shape the brief feared is simply not the shape the index affords."), li(text: "**(c) What is NOT free is obtaining the siblings' deltas**, because no run publishes its delta and no cross-run state exists. Two routes: RECOMPUTE — fetch each open PR's head (`refs/pull/N/head`, one `git fetch`), diff each against its merge base, parse the touched `.dag` files at both sides with `base_records`, and take the two differences; 30 open PRs today, tens of touched files each, parse-only, seconds. Or READ — each run publishes its own delta as a small artifact and a later run joins the published set; cheaper per run, but cross-run state with staleness semantics of its own, and no producer for it exists. Route RECOMPUTE is the honest first cut: it reads only facts git already holds and needs no new store."), ]), - h3(text: "3.1 Measured on the last fourteen days of `main` (the scoping instrument)"), - p(text: "A retrospective join was run over the 374 first-parent commits on `origin/main` since 2026-08-28, computing per commit the surface removals and added import claims from the touched `.dag` files under the three sweep roots, and joining every pair within three days of each other. The instrument is a text-level approximation of `import_surface_has` written for this scope (declaration keywords, coproduct variants, import members); its intended home is a `gunbc` entry over the real index, which checkpoint 1 supplies so the number below is re-derivable rather than transcribed."), + h3(text: "3.1 Measured by `joint_claim_join`, the checkpoint-1 instrument"), + p(text: "The producer is `v1_compiler.bin.joint_claim_join` (cargo bin `joint_claim_join`, seed-growth row `gunbc.joint_claim_join_seed_growth`): `joint_claim_join .. [--window-seconds N]` walks the first-parent commits of a range, takes each commit against its first parent as one subject, computes its delta from the touched `.dag` files on both sides through `namespace_wave_admission` `base_records` and `diff_sides`, and joins every ordered pair within the window. A claim is joined only while it is LIVE when the entry leaves: not retired by the removing change itself, and — in retrospective mode, where the subjects are a history — not retired by any change that landed between the pair. Its summary line carries the denominators (`subjects`, `with_removals`, `with_added_claims`, `window_seconds`) and BOTH counts: `raw_candidates`, the hit set before those exclusions, and `findings`, after them. The figures below are what that entry answers over `6a54695f6c7^..f078c59b0b4` (first-parent `main`, 2026-08-28 to 2026-09-11) with the default three-day window; they are cited by that invocation, and a reader who wants them re-derived runs it rather than trusting this paragraph."), TableBlock { - header: row(cells: [cell(text: "Join"), cell(text: "Pair hits"), cell(text: "Reading")]), - alignments: [AlignNone, AlignNone, AlignNone], + header: row(cells: [cell(text: "Count the instrument reports"), cell(text: "Reading")]), + alignments: [AlignNone, AlignNone], rows: [ - row(cells: [cell(text: "Raw: `A(I) ∩ R(D)` over all pairs"), cell(text: "207"), cell(text: "the superset-check trap: a later deletion that also deletes or edits the importer in the SAME diff is not incompatibility — the remover saw the claim on `main` and handled it. A candidate list, not a detector.")]), - row(cells: [cell(text: "Minus claims the removing commit itself retires"), cell(text: "5"), cell(text: "4 of 5 are exactly the three brief instances (instance 1 is two names). The fifth is an ordering artefact: a THIRD commit retired the claim between the pair, which a join over the OPEN-PR set excludes by construction because a merged PR is not open. Recall over the window's `ImportMemberAbsent` reds on `main`: 3 of 3. No false positive among co-open pairs.")]), + row(cells: [cell(text: "`raw_candidates`"), cell(text: "the join before the exclusion. A later deletion that also deletes or edits the importer in the SAME diff is not incompatibility — the remover saw the claim on `main` and handled it. This is the superset-check trap: a candidate list, never wrong and never useful, DESIGN §5's absorbing fallback read from the other side. It is reported so the width the exclusion removes stays a number.")]), + row(cells: [cell(text: "`findings`"), cell(text: "the join after excluding claims no longer live at the removal. Over the cited range every finding is a 2026-09-10 instance from §2 (instance 1 is two names, so two findings). Recall over the range's `ImportMemberAbsent` reds on `main`: all of them.")]), ], }, - p(text: "So the predicate is discriminating at the one exclusion, and the exclusion is not a tuning knob: it is the statement that the join asks about the MERGED tree, and a claim the remover's own head already retired is not in that tree."), + p(text: "**The two candidates the first run reported as findings, named rather than left as a fraction.** With only the first exclusion the same range answered two findings that were never reds: `fdc7292d997` (#10443, 09-05, adds `gunbc.harness.harness_cli` importing `harness_engine_backend_url`) against `597a56f629b` (09-06, removes it), and `b25b3dc7f7d` (#10863, 09-08, adds `extdeps.colo.databank` importing `DensityMarketedMax`) against `0d6b6653e0f` (#10888, 09-10, removes the variant). In both, a THIRD commit between the pair had already retired the import (`d79e5a64dcb`, #10617, for the first), so nothing claimed the entry when it left. Two artefacts with one cause is a defect in the retrospective's pair predicate, not in the join, and it is built in rather than described: the instrument excludes a claim any subject between the pair retired, with its own control beside the first exclusion's. An open-PR-set join has no between: a merged change is not an open sibling. The symmetric case — a subject between the pair RE-ADDING the surface entry — is not excluded and is named here as the next widening; no candidate in the range had it."), + p(text: "So the predicate is discriminating at the exclusion it carries, and the exclusion is not a tuning knob: it is the statement that the join asks about the MERGED tree, and a claim retired before the entry left is not in that tree."), ] } @@ -77,11 +82,20 @@ fn section_4() -> List { header: row(cells: [cell(text: "Option"), cell(text: "Where the verdict is read"), cell(text: "What it needs"), cell(text: "Verdict")]), alignments: [AlignNone, AlignNone, AlignNone, AlignNone], rows: [ - row(cells: [cell(text: "A — parse-phase rider, ADVISORY"), cell(text: "the run log, plus a `::warning::` annotation on the PR's checks tab (the one surface `contents: read` can write)"), cell(text: "the open-PR set: `pull-requests: read` on the emitted witnesses job (`gunbc.witness_floor_workflow` carries `contents: read` only today — a permission row, not a job) and a fetch of `refs/pull/N/head` for each"), cell(text: "**recommended first move**")]), - row(cells: [cell(text: "B — dashboard-side advisory at merge readiness"), cell(text: "where the merge decision is actually made"), cell(text: "the same join, computed outside this repository; no CI cost at all"), cell(text: "the better READER, but out of this repo's authority; named so the operator can choose it")]), + row(cells: [cell(text: "A — parse-phase rider, ADVISORY"), cell(text: "the run log, plus a `::warning::` annotation on the PR's checks tab (the one surface `contents: read` can write)"), cell(text: "the open-PR set: `pull-requests: read` on the emitted witnesses job (`gunbc.witness_floor_workflow` carries `contents: read` only today — a permission row, not a job) and a fetch of `refs/pull/N/head` for each"), cell(text: "stops at the permission row: a permission on a required job is permanent surface on the merge path acquired for an advisory, and that trade is the operator's (manager ruling 2026-09-11)")]), + row(cells: [cell(text: "B — dashboard-side advisory at merge readiness"), cell(text: "where the merge decision is actually made"), cell(text: "the same join, computed outside this repository; no CI cost at all"), cell(text: "**preferred by the manager's 2026-09-11 ruling**, costed in §4.1: the better READER, and it never touches the merge path")]), row(cells: [cell(text: "C — required, blocking"), cell(text: "the merge"), cell(text: "a measured hit rate first"), cell(text: "**refused as a first move** (brief); the ceiling gates here instead once its prerequisites clear")]), ], }, + h3(text: "4.1 Costing the dashboard-side reader against the CI rider"), + ul(items: [ + li(text: "**Inputs.** Both need the same two things per open PR: its merge base against `main` and its head, both of which the dashboard already tracks (it detects merges and keys reviews to `headRefOid`), and a checkout with those objects, which the dashboard's poller also has. The CI rider gets them from the PR's own checkout plus a fetch of `refs/pull/N/head` for each sibling and an API read of the open set — the read that needs the permission."), + li(text: "**Compute.** Identical: one `joint_claim_join` delta per subject from touched files (parse only), one intersection. The rider recomputes every sibling's delta on every push of every PR, so it is paid N times per push across N open PRs; the dashboard reader computes each PR's delta once per head it observes and re-joins on any head change, so it is paid once per head. On a busy day the rider's bill is quadratic in pushes, the reader's is linear in heads."), + li(text: "**Freshness.** The rider's verdict is pinned to the PR's last push; the reader re-joins whenever ANY head changes, so a sibling opened after a PR's last push is joined without a push. That is the exact residual §4 names against the rider, and the reader does not have it."), + li(text: "**Surface.** The rider adds a permission row to a required job's emitted YAML and a stderr line nobody on the merge path reads; the reader adds a column to the readiness summary the merge decision is actually taken from, and touches nothing in this repository's CI."), + li(text: "**Authority.** The reader lives outside this repository, so this record can specify its input and its finding shape (§5) but not land it. That is the one cost on its side, and it is a routing cost, not a runtime one."), + ]), + p(text: "On every axis but authority the reader wins, and the authority cost is exactly the one the manager's ruling accepts. So the standing recommendation is: **the rider is not built**; checkpoint 2 becomes the dashboard consumer of the instrument's finding shape, with `joint_claim_join` as its computation, and the permission question never reaches the operator."), p(text: "**The freshness caveat, stated so it is not discovered.** A rider runs at push time, so its verdict is exactly as fresh as the green beside it: it sees the siblings open at that run and cannot see one opened after the PR's last push. Each pair gets two chances (either side's later run) — every instance here had both PRs open at the later side's run — but a PR that sits for hours after its last run, which is the stale-green situation itself, is not re-joined. That residual is the reason this is beneath the ceiling and not a substitute for it."), p(text: "**Hit-rate instrument.** Advisory-first is honest only if the rate is measured: the retrospective of §3.1, re-expressed over the real index as a `gunbc` entry, is that instrument — run over `main` history it answers precision and recall for any window, without log mining. A checkpoint that flips anything to blocking cites that entry's output, never a count in prose."), ] @@ -102,24 +116,27 @@ fn section_5() -> List { fn section_6() -> List { [ - h2(text: "6. gunbc-private: the same exposure, and the join is public-only today"), + h2(text: "6. gunbc-private: the same exposure, the join is public-only today, and that is a TYPED coverage obligation"), ul(items: [ li(text: "The private witness workflow (`strategy.private_witness_workflow`) builds the PUBLIC seed at `main`, copies the private `dag/` to `public/private_overlay/dag`, and runs `gunbc run` entries over the composed roots. It has no parse phase and no `claim_executor --required-ci`, so no `DeclarationIndex` is ever built over the private corpus; and the overlay path is outside `DAG_PARSE_SWEEP_ROOTS` (`src/v1`, `dag`, `src/v2`), so even a parse sweep there would not see it. The overlay's own annotation names the dissolution — an extra `--source-root` outside the process workspace root — and that trigger is what makes the same records derivable for private."), li(text: "Private merges with MERGE COMMITS (`Merge pull request #62`), not squash; the class is the same, since a merge commit composes the trees at the same moment a squash does."), li(text: "**The larger private exposure is CROSS-REPO, and no join in this record reaches it.** 113 of the 114 private modules import from 77 distinct public modules. A public PR that shrinks a surface is never compiled against private at all — not before merge, not after — and private learns of it only when its own next run resolves against a `main` that has moved. That is the join of §2 with the private corpus standing as one more open sibling whose claims are the whole overlay's import set; it is derivable the day the sweep roots admit the overlay, and it is warm-badger-62's territory (#46 against the workflow, a compiler-hole scope against the public roster), so it is named here and not touched."), ]), + p(text: "The gap is carried as a `GuaranteeStall` row and not as this prose: `gunbc.guarantee_stall.joint_claim_join_private_corpus_unindexed_stall`, subject the private corpus's absence from any `DeclarationIndex`, blocker `AwaitsOneGrounding` on the overlay's own extra-source-root trigger, and a next-rung trigger naming the capability — the private overlay as an admitted source root of the parse sweep, at which point its whole import-claim set is one more subject of `joint_claim_join`. A reader who later finds this join cited as coverage of the private corpus finds the row first."), ] } fn section_7() -> List { [ - h2(text: "7. Sequencing — the operator picks a checkpoint; none is built by this record"), + h2(text: "7. Sequencing — what is landed, what is ruled, what is the operator's"), ul(items: [ - li(text: "**Checkpoint 1 — the instrument.** A `gunbc` entry over the real `DeclarationIndex` that computes a commit's or a PR's surface-removal and added-claim deltas and joins a set of them, with the §5 finding shape and the retrospective mode over `main` history. Positive control: the three 2026-09-10 pairs as fixtures; discriminating RED: the exclusion arm on a pair whose remover retires the claim. This is the hit-rate instrument of §4 and the only artifact that makes any later number re-derivable."), - li(text: "**Checkpoint 2 — option A, advisory.** The rider in the parse phase joining against the open-PR set via route RECOMPUTE; the one emitted-workflow change is the `pull-requests: read` permission row. Reports, annotates, never pushes a phase failure. Its declared population is the `ImportMember` channel; `CitedSymbol` and `RosteredRow` are the same rider with the other two fields and land as their own checkpoints so each carries its own positive control."), - li(text: "**Checkpoint 3 — blocking, IF EVER.** Only from the instrument's measured precision over a stated window, and only if the ceiling is still deferred; if `receipt_is_admissible` gates first, checkpoint 3 is never built and checkpoint 2 dissolves with it."), + li(text: "**The within-tree field-set half is NOT a checkpoint here (§2.3).** Its home is `witness_that_fails_to_compile_is_absent_rather_than_red`, its trigger is widened by the receipt this change appends, and discharging it is that row's work, in the parse phase's existing reach, independent of everything below."), + li(text: "**Checkpoint 1 — the instrument. LANDED in this change.** `v1_compiler.bin.joint_claim_join`: per-subject deltas over the real index, the join with its exclusion, retrospective mode over a commit range, and fixture-boundary controls in its own tests — one positive control per instance shape of §2 (variant deleted, re-export dropped, name renamed), one discriminating RED per exclusion (a remover that retires the claim, and a third change that retires it between the pair, are candidates and not findings), one for the pair predicate, one for a claim already present at the claimer's base. Nothing on the merge path executes it."), + li(text: "**Checkpoint 2 — the consumer. RULED: the dashboard reader, not the CI rider.** The rider stops at the `pull-requests: read` permission row on a required job, which is the operator's trade; the reader (§4.1) wins on compute, freshness and surface and never touches the merge path. Its input is the open-PR set the dashboard already holds; its computation is `joint_claim_join`; its output is the §5 finding shape in the readiness summary. It lives outside this repository and is dispatched from there."), + li(text: "**Checkpoint 3 — blocking, IF EVER.** Only from the instrument's measured precision over a stated range, cited by invocation as §3.1 does, and only if the ceiling is still deferred; if `receipt_is_admissible` gates first, checkpoint 3 is never built and the consumer dissolves with this record."), + li(text: "**The private extension.** Downstream of checkpoint 1, gated by the stall row of §6; the manager dispatches it with §6 as the brief once the instrument exists."), ]), - p(text: "What is refused as a first move, so it is not re-proposed: a new required job (roster closed); blocking on the join (rate unmeasured); a pairwise compile of open PRs (the shape §3 shows is unnecessary); and any framing of this join as the class's wall — the wall is the merge-time freshness requirement, landed and deferred by ruling, and this record retires with it."), + p(text: "What is refused, so it is not re-proposed: a new required job (roster closed); a permission row on a required job for an advisory (the operator's trade, not a lane's); blocking on the join (rate unmeasured until the instrument has a window behind it); a pairwise compile of open PRs (the shape §3 shows is unnecessary); and any framing of this join as the class's wall — the wall is the merge-time freshness requirement, landed and deferred by ruling, and this record retires with it."), ] } @@ -138,6 +155,6 @@ data joint_incompatibility_claim_join_plan: Plan = Plan { slug: "joint-incompatibility-claim-join", title: "[SCOPE] Jointly-incompatible open PRs — a claim-channel × surface join over the declaration index, advisory-first", body: joint_incompatibility_claim_join_body(), - retirement: PlanRetiresWhen { condition: unbound_dissolution(description: "Delete this record when either holds: the merge-time freshness requirement of gunbc.plans.ci_merge_freshness §3 is in force as a witnessed property (receipt_is_admissible gating), at which point every join scoped here is redundant and any landed rider is deleted with this record; or checkpoint 2 has landed and its instrument has measured a window, at which point this scope is superseded by the rider's own carrier and the plan is redundant.") + retirement: PlanRetiresWhen { condition: unbound_dissolution(description: "Delete this record when either holds: the merge-time freshness requirement of gunbc.plans.ci_merge_freshness §3 is in force as a witnessed property (receipt_is_admissible gating), at which point every join scoped here is redundant and any landed rider is deleted with this record; or the checkpoint-2 consumer has landed and the instrument has measured a window, at which point this scope is superseded by the rider's own carrier and the plan is redundant.") } } diff --git a/dag/gunbc/recurring_failure_mode/witness_that_fails_to_compile_is_absent_rather_than_red.dag b/dag/gunbc/recurring_failure_mode/witness_that_fails_to_compile_is_absent_rather_than_red.dag index ac012199071..7a55b48dfbc 100644 --- a/dag/gunbc/recurring_failure_mode/witness_that_fails_to_compile_is_absent_rather_than_red.dag +++ b/dag/gunbc/recurring_failure_mode/witness_that_fails_to_compile_is_absent_rather_than_red.dag @@ -12,6 +12,8 @@ data witness_that_fails_to_compile_is_absent_rather_than_red: RecurringFailureMo "THE CORE IS A TWO-CLAIM COLLAPSE: `the compiler behaves correctly` and `the witness that enrols that behaviour as executing evidence works` are DIFFERENT CLAIMS, and running the subject as a direct fixture establishes only the first. The mechanism of the misread is that holding the stronger claim's output makes the weaker one feel already answered — the author has the passing fixtures in hand, which is exactly why the file meant to carry them never gets checked. ", "RECOGNITION RULE: ask what would have caught it, and IF THE ONLY ANSWER IS A RUN AIMED AT A DIFFERENT QUESTION, the class has no dedicated detector. The specimen surfaced from a whole-tree advisory census re-derivation chasing an unrelated hypothesis about a denominator, as blocking error number one, after the arms were green, the PR was open and an APPROVE was already recorded. THE ONE INSTRUMENT THAT SEES IT is a whole-tree compile INCLUDING the test roots; compiling the subject, or entry-scoping to the witness itself, does not. ", "Rung found at 1, mitigatable, held only by an unrelated run happening to range over the file, which is not a mechanism. CEILING 3, structurally guaranteed: the population is decidable and closed — every file under the claim roots — so a required step that compiles them and refuses a non-compiling claim file makes the state unwritable in an `Accepted` tree. NEXT TRIGGER, capability and not artifact: the required floor must COMPILE every claim-root file and refuse a claim file that does not compile, rather than only executing the claim files that compile — AN EXECUTION ROSTER KEYED TO COMPILING FILES CANNOT SEE A FILE THAT FELL OUT OF IT, the same shape as a check whose population IS its own roster. Specimen gunbc#10337: `brand_nominal_identity_witness_test` named a host builtin in its import list; four direct fixtures had already proven the compiler refuses, and the witness enrolling them had never run. ", + "THE TRIGGER'S GRAIN IS NARROWER THAN THE RULED CAPABILITY, AND THIS RECEIPT WIDENS IT RATHER THAN LETTING THE NARROW TEXT STAND (sunny-tern-813 ruling, relayed and verified by crisp-lynx-452, 2026-09-11; filed from the joint-incompatibility scope, gunbc.plans.joint_incompatibility_claim_join section 2.3). The row above names the capability as `COMPILE every claim-root file`. The ruled capability is WIDER: compile every CONSTRUCTION SITE of a type whose required fields changed, AND every claim-root file regardless of gate closure -- and a construction site can live in a module that is not a claim root at all. DESIGN section 4b(3): a trigger naming less than the capability it restores will be satisfied while the capability stays dead, and the review tell is a grain mismatch between the loss sentence and the trigger sentence. The loss sentence of the second specimen is construction-site grain (`every construction site of that type in the tree`); the trigger sentence is claim-root-file grain. THAT IS THE MISMATCH. So the trigger, as of this receipt, reads at the ruled width: THE REQUIRED FLOOR MUST COMPILE EVERY CLAIM-ROOT FILE AND EVERY CONSTRUCTION SITE OF A TYPE WHOSE DECLARED FIELD SET CHANGED BETWEEN BASE AND HEAD, REGARDLESS OF GATE CLOSURE, AND REFUSE ANY THAT DOES NOT COMPILE. Discharging the narrower sentence alone does not retire this row. ", + "WHAT THE JOINT-INCOMPATIBILITY SCOPE ESTABLISHED ABOUT THIS ROW, so the two are not conflated: the cross-change join over the declaration index (v1_compiler.bin.joint_claim_join) keys on a module's EXPORT SURFACE -- declared, variants, reexported -- and a field is not a surface entry, so that join is silent on every specimen of this row by construction; the two classes share a shape (a change compiled against a population its diff does not name) and not a mechanism. The within-tree half needs no open-PR set and no cross-change anything: the removed side is `decl_fields` base-minus-head per declaration, derivable from the same base reconstruction the wave wall performs, and the claiming side needs no precise construction-site channel because the COMPILE is the oracle and the index only selects what to compile -- every module whose `referenced` or `authored_type_references` names the changed declaration, where over-collection costs compile time and never a false finding. That is the selection this row's widened trigger names, and it is this row's to discharge, not a checkpoint of the join's plan. ", ], evidence: [], diff --git a/dag/gunbc/seed_growth_admission.dag b/dag/gunbc/seed_growth_admission.dag index 6f91bbadf2e..063a69cfa59 100644 --- a/dag/gunbc/seed_growth_admission.dag +++ b/dag/gunbc/seed_growth_admission.dag @@ -28,6 +28,7 @@ import gunbc.bootstrap_operation_seed_growth { cli_dispatch_host_boundary_seed_growth_justification } import gunbc.generated_artifact_boundary_seed_growth { generated_artifact_boundary_seed_growth_justification } +import gunbc.joint_claim_join_seed_growth { joint_claim_join_seed_growth_justification } import gunbc.regen_convergence_seed_growth { regen_convergence_seed_growth_justification } import gunbc.witness_runtime_cause_seed_growth { witness_runtime_cause_seed_growth_justification } import gunbc.required_lane_judgment_seed_growth { required_lane_judgment_seed_growth_justification } @@ -251,7 +252,8 @@ fn seed_growth_justification_roster() -> List { filesystem_create_new_seed_growth_justification, required_lane_judgment_seed_growth_justification, param_binder_seed_growth_justification, - live_tree_stamp_detector_seed_growth_justification + live_tree_stamp_detector_seed_growth_justification, + joint_claim_join_seed_growth_justification ] } diff --git a/src/v1/stage0/Cargo.toml b/src/v1/stage0/Cargo.toml index e485d48eacc..0c1be947195 100644 --- a/src/v1/stage0/Cargo.toml +++ b/src/v1/stage0/Cargo.toml @@ -109,6 +109,12 @@ path = "src/bin/discover_source_root_ingest.rs" name = "v1_src_dag_parse" path = "src/bin/v1_src_dag_parse.rs" +# Checkpoint 1 of gunbc.plans.joint_incompatibility_claim_join: the surface-removal x added-import-claim +# join over the declaration index, retrospective over a commit range. An instrument; nothing gates on it. +[[bin]] +name = "joint_claim_join" +path = "src/bin/joint_claim_join.rs" + # Floor witness for extdep REST transport collection (cli_run::collect_rest_transport_operations). # Parses the eight-file authority closure live, resolves tracked ops, and checks # fingerprint uniqueness + derive_op_effect obligations. SCAFFOLD — dissolves when diff --git a/src/v1/stage0/src/bin/joint_claim_join.rs b/src/v1/stage0/src/bin/joint_claim_join.rs new file mode 100644 index 00000000000..2f87eb80086 --- /dev/null +++ b/src/v1/stage0/src/bin/joint_claim_join.rs @@ -0,0 +1,594 @@ +//! THE JOINT-CLAIM JOIN — checkpoint 1 of `gunbc.plans.joint_incompatibility_claim_join`. +//! +//! WHAT IT ANSWERS. Two changes that are each green on their own head, textually disjoint and +//! merge-clean can be jointly incompatible: one SHRINKS a module's export surface and the other +//! ADDS an import claim on the entry that left. Each change's CI compiles its head against `main` +//! at run start, never against its sibling, so the union is first compiled by whoever lands after +//! both. This instrument computes, per change, the two set differences the join needs — surface +//! entries removed, import claims added (and retired) — and intersects them across a set of +//! changes. It is an INSTRUMENT: it reports, it exits by what it found, and nothing gates on it. +//! +//! THE CUT IS THE SURFACE, NOT THE EDIT. The removed side is `import_surface_has`'s own union — +//! `declared ∪ variants ∪ reexported` — taken base-minus-head, because of the three 2026-09-10 +//! instances one was a deletion, one a rename and one a MOVE to another module (the declaration +//! survived; its old home stopped exporting it). Keying on the kind of edit would have caught one +//! of three; a dropped re-export is the fourth kind the same surface covers, and has its control. +//! +//! THE EXCLUSION, AND WHY IT IS NOT A KNOB. A claim is joined only if it is STILL LIVE when the +//! surface entry leaves: not retired by the removing change's own diff, and — in retrospective +//! mode, where the subjects are a history rather than a set of open siblings — not retired by any +//! change that landed between the two. Both are one statement: the join asks about the MERGED +//! tree, and a retired claim is not in that tree. Without it the join over fourteen days of `main` +//! is a superset that is never wrong and never useful, DESIGN §5's absorbing fallback read from +//! the other side; `raw_candidates` is still reported beside `findings` so the width the exclusion +//! removes is a number, not a memory. An open-PR-set join needs only the first half, because a +//! merged change is not an open sibling. +//! +//! WHAT IT REUSES RATHER THAN RE-DERIVES. Records come from `declaration_index::record_from_module` +//! through `namespace_wave_admission::base_records`, the same parse the wave wall reconstructs a +//! baseline from; touched paths come from `diff_sides` over `git diff --name-status -z -M`, which +//! is the LISTING that establishes which side a path exists on, so a `git show` failure here is a +//! refusal and never evidence of absence. +//! +//! RETROSPECTIVE MODE. `joint_claim_join .. [--window-seconds N]` walks the +//! first-parent commits of the range, takes each commit against its first parent as one subject, +//! and joins every ordered pair whose commit times are within the window. Its ground truth is the +//! `ImportMemberAbsent` reds the declarations rider reported on `main` in the same range. +#![allow(clippy::disallowed_macros)] + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; +use std::process::ExitCode; + +use v1_compiler::cli_run::declaration_index::ModuleDeclarationRecord; +use v1_compiler::cli_run::namespace_wave_admission::{ + base_records, diff_sides, git_stdout, in_sweep_scope, +}; + +/// Which of the three export-surface sets a removed name left. Kept so a dropped re-export reads +/// as what it is rather than as a phantom deletion. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum SurfaceKind { + Declared, + Variant, + Reexported, +} + +pub fn surface_kind_label(kind: SurfaceKind) -> &'static str { + match kind { + SurfaceKind::Declared => "declared", + SurfaceKind::Variant => "variant", + SurfaceKind::Reexported => "reexported", + } +} + +/// One `import target { member }` claim authored in `module`, located at its member. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ImportClaimSite { + pub module: String, + pub target: String, + pub member: String, + pub rel_path: String, + pub offset: i64, +} + +/// The identity a claim keeps across edits: `(module, target, member)`. Offsets shift under any +/// edit above the line, so they are not part of it. +pub type ClaimKey = (String, String, String); + +/// A free function rather than a method so it is citable as a `DeclarationRef` (no `impl` block). +pub fn claim_key(c: &ImportClaimSite) -> ClaimKey { + (c.module.clone(), c.target.clone(), c.member.clone()) +} + +/// One change's delta: what its head no longer exports, and which claims it added or retired. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ClaimDelta { + pub removed_surface: BTreeMap<(String, String), SurfaceKind>, + pub added_claims: Vec, + pub retired_claims: Vec, +} + +fn surface_of(record: &ModuleDeclarationRecord) -> BTreeMap { + let mut out = BTreeMap::new(); + for n in &record.reexported { + out.insert(n.clone(), SurfaceKind::Reexported); + } + for n in &record.variants { + out.insert(n.clone(), SurfaceKind::Variant); + } + for n in &record.declared { + out.insert(n.clone(), SurfaceKind::Declared); + } + out +} + +fn claim_sites(records: &[ModuleDeclarationRecord]) -> Vec { + let mut out = Vec::new(); + for record in records { + for claim in &record.imports { + for (member, location) in &claim.members { + out.push(ImportClaimSite { + module: record.module_path.clone(), + target: claim.target.clone(), + member: member.clone(), + rel_path: location.file.clone(), + offset: location.offset, + }); + } + } + } + out +} + +/// The delta of one change, from the records of ONLY the files its diff touched, on both sides. +/// An untouched file has the same record on both sides and contributes nothing to a difference, +/// so passing it is harmless and omitting it is the whole economy. +pub fn claim_delta( + base: &[ModuleDeclarationRecord], + head: &[ModuleDeclarationRecord], +) -> ClaimDelta { + let head_by_module: BTreeMap<&str, &ModuleDeclarationRecord> = + head.iter().map(|r| (r.module_path.as_str(), r)).collect(); + let mut removed_surface = BTreeMap::new(); + for record in base { + let head_surface = head_by_module + .get(record.module_path.as_str()) + .map(|r| surface_of(r)) + .unwrap_or_default(); + for (name, kind) in surface_of(record) { + if !head_surface.contains_key(&name) { + removed_surface.insert((record.module_path.clone(), name), kind); + } + } + } + let base_claims = claim_sites(base); + let head_claims = claim_sites(head); + let base_keys: BTreeSet<_> = base_claims.iter().map(claim_key).collect(); + let head_keys: BTreeSet<_> = head_claims.iter().map(claim_key).collect(); + ClaimDelta { + removed_surface, + added_claims: head_claims + .into_iter() + .filter(|c| !base_keys.contains(&claim_key(c))) + .collect(), + retired_claims: base_claims + .into_iter() + .filter(|c| !head_keys.contains(&claim_key(c))) + .collect(), + } +} + +/// One change under the join, named by whatever identity the caller has — a commit sha, a PR. +#[derive(Debug, Clone)] +pub struct JointClaimSubject { + pub id: String, + pub delta: ClaimDelta, +} + +/// The typed, located finding: the entry that left, which surface it left, who removed it, and +/// the site that claims it. One finding per (removed entry × claiming site), never per pair. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JointClaimFinding { + pub removed_module: String, + pub removed_name: String, + pub removed_from: SurfaceKind, + pub removing_subject: String, + pub claiming_subject: String, + pub claim: ImportClaimSite, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct JointClaimJoin { + /// Hits before the retirement exclusion — the candidate list, reported so the width the + /// exclusion removed stays visible. + pub raw_candidates: usize, + pub findings: Vec, +} + +/// Join every ordered pair `(remover, claimer)` the predicate admits. `claim_retired_between` +/// answers whether some OTHER subject, landing between the pair, retired the claim key: true in +/// retrospective mode when a third commit fixed the importer before the entry left; always false +/// over a set of open siblings, where nothing has landed between anything. +pub fn joint_claim_join( + subjects: &[JointClaimSubject], + admits_pair: &dyn Fn(&JointClaimSubject, &JointClaimSubject) -> bool, + claim_retired_between: &dyn Fn(&JointClaimSubject, &JointClaimSubject, &ClaimKey) -> bool, +) -> JointClaimJoin { + let mut out = JointClaimJoin::default(); + for remover in subjects { + if remover.delta.removed_surface.is_empty() { + continue; + } + let retired: BTreeSet<_> = remover.delta.retired_claims.iter().map(claim_key).collect(); + for claimer in subjects { + if claimer.id == remover.id || !admits_pair(remover, claimer) { + continue; + } + for claim in &claimer.delta.added_claims { + let entry = (claim.target.clone(), claim.member.clone()); + let Some(kind) = remover.delta.removed_surface.get(&entry) else { + continue; + }; + out.raw_candidates += 1; + let key = claim_key(claim); + if retired.contains(&key) || claim_retired_between(remover, claimer, &key) { + continue; + } + out.findings.push(JointClaimFinding { + removed_module: entry.0, + removed_name: entry.1, + removed_from: *kind, + removing_subject: remover.id.clone(), + claiming_subject: claimer.id.clone(), + claim: claim.clone(), + }); + } + } + } + out +} + +pub fn render_finding(f: &JointClaimFinding) -> String { + format!( + "joint_claim_join: FINDING `{}` no longer exports `{}` ({}) at {}; claimed at {} by `{}` \ + importing `{}` from `{}` ({}:{})", + f.removed_module, + f.removed_name, + surface_kind_label(f.removed_from), + f.removing_subject, + f.claiming_subject, + f.claim.module, + f.claim.member, + f.claim.target, + f.claim.rel_path, + f.claim.offset + ) +} + +/// One commit as a subject: its diff against its first parent, both sides parsed from git. +struct CommitSubject { + sha: String, + time: i64, + subject_line: String, +} + +fn commit_delta(workspace: &Path, sha: &str) -> Result { + let parent = format!("{sha}^"); + let name_status = git_stdout( + workspace, + &["diff", "--name-status", "-z", "-M", &parent, sha], + )?; + let (head_touched, base_side) = diff_sides(&name_status); + let mut base = Vec::new(); + for rel in base_side.iter().filter(|p| in_sweep_scope(p)) { + let content = git_stdout(workspace, &["show", &format!("{parent}:{rel}")]) + .map_err(|e| format!("{sha}: cannot read {rel} at its parent ({e})"))?; + base.extend(base_records(rel, &content)?); + } + let mut head = Vec::new(); + for rel in head_touched.iter().filter(|p| in_sweep_scope(p)) { + let content = git_stdout(workspace, &["show", &format!("{sha}:{rel}")]) + .map_err(|e| format!("{sha}: cannot read {rel} at the commit ({e})"))?; + head.extend(base_records(rel, &content)?); + } + Ok(claim_delta(&base, &head)) +} + +fn usage() -> ExitCode { + eprintln!( + "usage: joint_claim_join .. [--window-seconds N] (default window 259200)" + ); + ExitCode::from(2) +} + +fn main() -> ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + let mut range = None; + let mut window_seconds: i64 = 3 * 24 * 3600; + let mut i = 0; + while i < args.len() { + match args[i].as_str() { + "--window-seconds" => { + let Some(v) = args.get(i + 1).and_then(|v| v.parse::().ok()) else { + return usage(); + }; + window_seconds = v; + i += 2; + } + other if other.contains("..") => { + range = Some(other.to_string()); + i += 1; + } + _ => return usage(), + } + } + let Some(range) = range else { return usage() }; + let workspace = match std::env::current_dir() { + Ok(d) => d, + Err(e) => { + eprintln!("joint_claim_join: current_dir: {e}"); + return ExitCode::from(1); + } + }; + let listing = match git_stdout( + &workspace, + &[ + "log", + "--first-parent", + "--reverse", + "--format=%H %ct %s", + &range, + ], + ) { + Ok(l) => l, + Err(e) => { + eprintln!("joint_claim_join: the range does not resolve ({e})"); + return ExitCode::from(1); + } + }; + let mut commits = Vec::new(); + for line in listing.lines() { + let mut parts = line.splitn(3, ' '); + let (Some(sha), Some(time)) = (parts.next(), parts.next()) else { + continue; + }; + let Ok(time) = time.parse::() else { + continue; + }; + commits.push(CommitSubject { + sha: sha.to_string(), + time, + subject_line: parts.next().unwrap_or("").to_string(), + }); + } + let mut subjects = Vec::new(); + let mut times: BTreeMap = BTreeMap::new(); + let mut lines: BTreeMap = BTreeMap::new(); + for c in &commits { + match commit_delta(&workspace, &c.sha) { + Ok(delta) => { + times.insert(c.sha.clone(), c.time); + lines.insert(c.sha.clone(), c.subject_line.clone()); + subjects.push(JointClaimSubject { + id: c.sha.clone(), + delta, + }); + } + Err(e) => { + // A subject whose delta is unobservable makes the join unobservable: refuse, never + // continue over a quieter population. + eprintln!("joint_claim_join: NOT EVALUATED — {e}"); + return ExitCode::from(1); + } + } + } + let with_removals = subjects + .iter() + .filter(|s| !s.delta.removed_surface.is_empty()) + .count(); + let with_added = subjects + .iter() + .filter(|s| !s.delta.added_claims.is_empty()) + .count(); + let join = joint_claim_join( + &subjects, + &|a, b| (times[&a.id] - times[&b.id]).abs() <= window_seconds, + &|a, b, key| { + let (lo, hi) = ( + times[&a.id].min(times[&b.id]), + times[&a.id].max(times[&b.id]), + ); + subjects.iter().any(|s| { + let t = times[&s.id]; + t > lo && t < hi && s.delta.retired_claims.iter().any(|c| claim_key(c) == *key) + }) + }, + ); + for f in &join.findings { + eprintln!( + "{}\n remover: {}\n claimer: {}", + render_finding(f), + lines[&f.removing_subject], + lines[&f.claiming_subject] + ); + } + eprintln!( + "joint_claim_join: range={range} subjects={} with_removals={with_removals} \ + with_added_claims={with_added} window_seconds={window_seconds} raw_candidates={} findings={}", + subjects.len(), + join.raw_candidates, + join.findings.len() + ); + if join.findings.is_empty() { + ExitCode::SUCCESS + } else { + ExitCode::from(3) + } +} + +// THE FIXTURE BOUNDARY IS WHERE THE RED IS AUTHORABLE (DESIGN §4b). Each arm below hands the real +// parser real source and asks the real delta and join; nothing is hand-built into a record. +#[cfg(test)] +mod tests { + use super::*; + + fn records(rel: &str, src: &str) -> Vec { + base_records(rel, src).unwrap_or_else(|e| panic!("fixture must parse: {e}")) + } + + fn subject(id: &str, base: &[(&str, &str)], head: &[(&str, &str)]) -> JointClaimSubject { + let base: Vec<_> = base.iter().flat_map(|(r, s)| records(r, s)).collect(); + let head: Vec<_> = head.iter().flat_map(|(r, s)| records(r, s)).collect(); + JointClaimSubject { + id: id.to_string(), + delta: claim_delta(&base, &head), + } + } + + const CARRIER_BOTH: &str = + "module fx.carrier\n\ntype Read\n = DurableOriginRead\n | DisposableCacheRead\n"; + const CARRIER_ONE: &str = "module fx.carrier\n\ntype Read\n = DurableOriginRead\n"; + const WITNESS_CLAIMS: &str = + "module fx.witness\n\nimport fx.carrier { DisposableCacheRead }\n\ndata x: Int = 1\n"; + const WITNESS_SILENT: &str = "module fx.witness\n\ndata x: Int = 1\n"; + + fn all(_: &JointClaimSubject, _: &JointClaimSubject) -> bool { + true + } + + fn never_between( + _: &JointClaimSubject, + _: &JointClaimSubject, + _: &(String, String, String), + ) -> bool { + false + } + + /// POSITIVE CONTROL, the #10933 × #10934 shape: a variant deleted on one side, imported on the + /// other, in different files. One finding, naming the variant surface and the claiming site. + #[test] + fn variant_deleted_by_one_change_and_imported_by_another_is_one_located_finding() { + let remover = subject( + "remover", + &[("dag/fx/carrier.dag", CARRIER_BOTH)], + &[("dag/fx/carrier.dag", CARRIER_ONE)], + ); + let claimer = subject("claimer", &[], &[("dag/fx/witness.dag", WITNESS_CLAIMS)]); + let join = joint_claim_join(&[remover, claimer], &all, &never_between); + assert_eq!(join.raw_candidates, 1); + assert_eq!(join.findings.len(), 1, "{join:?}"); + let f = &join.findings[0]; + assert_eq!( + (f.removed_module.as_str(), f.removed_name.as_str()), + ("fx.carrier", "DisposableCacheRead") + ); + assert_eq!(f.removed_from, SurfaceKind::Variant); + assert_eq!(f.removing_subject, "remover"); + assert_eq!(f.claiming_subject, "claimer"); + assert_eq!(f.claim.module, "fx.witness"); + assert_eq!(f.claim.rel_path, "dag/fx/witness.dag"); + assert!( + f.claim.offset > 0, + "the claim is located at its member, not at the file start" + ); + } + + /// THE DISCRIMINATING RED FOR THE EXCLUSION. Same removal, but the remover's own diff also + /// retires the claim — the merged tree carries no dangling import. The candidate is counted + /// and the finding is not emitted; a join without the exclusion would report it. + #[test] + fn a_remover_that_retires_the_claim_itself_is_a_candidate_and_not_a_finding() { + let remover = subject( + "remover", + &[ + ("dag/fx/carrier.dag", CARRIER_BOTH), + ("dag/fx/witness.dag", WITNESS_CLAIMS), + ], + &[ + ("dag/fx/carrier.dag", CARRIER_ONE), + ("dag/fx/witness.dag", WITNESS_SILENT), + ], + ); + let claimer = subject( + "claimer", + &[("dag/fx/witness.dag", WITNESS_SILENT)], + &[("dag/fx/witness.dag", WITNESS_CLAIMS)], + ); + let join = joint_claim_join(&[remover, claimer], &all, &never_between); + assert_eq!(join.raw_candidates, 1, "the superset still sees it"); + assert!(join.findings.is_empty(), "{join:?}"); + } + + /// THE RETROSPECTIVE'S SECOND EXCLUSION. The two 09-05/09-08 artefacts the first run of this + /// instrument reported were both a THIRD commit retiring the claim between the pair. Handed a + /// predicate that says so, the candidate is counted and the finding is not emitted. + #[test] + fn a_claim_retired_by_a_third_change_between_the_pair_is_a_candidate_and_not_a_finding() { + let remover = subject( + "remover", + &[("dag/fx/carrier.dag", CARRIER_BOTH)], + &[("dag/fx/carrier.dag", CARRIER_ONE)], + ); + let claimer = subject("claimer", &[], &[("dag/fx/witness.dag", WITNESS_CLAIMS)]); + let join = joint_claim_join(&[remover, claimer], &all, &|_, _, key| { + key.2 == "DisposableCacheRead" + }); + assert_eq!(join.raw_candidates, 1); + assert!(join.findings.is_empty(), "{join:?}"); + } + + /// A dropped re-export: NO declaration is deleted anywhere; the remover drops its own import, + /// so the name leaves its `reexported` surface, and a sibling imports it from there. No 09-10 + /// instance had this shape (instance 2 was a MOVE, which is `declared` leaving); the surface + /// covers it and this is its control. + #[test] + fn a_dropped_reexport_is_a_finding_that_names_the_reexported_surface() { + let actuator_before = + "module fx.actuator\n\nimport fx.rest { commit_ambiguous }\n\ndata y: Int = 2\n"; + let actuator_after = "module fx.actuator\n\ndata y: Int = 2\n"; + let mint = "module fx.mint\n\nimport fx.actuator { commit_ambiguous }\n\ndata z: Int = 3\n"; + let remover = subject( + "remover", + &[("dag/fx/actuator.dag", actuator_before)], + &[("dag/fx/actuator.dag", actuator_after)], + ); + let claimer = subject("claimer", &[], &[("dag/fx/mint.dag", mint)]); + let join = joint_claim_join(&[remover, claimer], &all, &never_between); + assert_eq!(join.findings.len(), 1, "{join:?}"); + assert_eq!(join.findings[0].removed_from, SurfaceKind::Reexported); + assert_eq!(join.findings[0].removed_name, "commit_ambiguous"); + } + + /// The #10865 shape: a rename is a removal on its old-name half, and that half is what fires. + /// A MOVE (#10923: the fn re-homed to another module) is the same delta on the old home. + #[test] + fn a_rename_fires_on_the_old_name_and_not_on_the_new_one() { + let before = "module fx.density\n\nfn grain_admits_single_cabinet() -> Bool { true }\n"; + let after = "module fx.density\n\nfn grain_offers_single_cabinet() -> Bool { true }\n"; + let census = "module fx.census\n\nimport fx.density { grain_admits_single_cabinet }\n\ndata w: Int = 4\n"; + let remover = subject( + "remover", + &[("dag/fx/density.dag", before)], + &[("dag/fx/density.dag", after)], + ); + let claimer = subject("claimer", &[], &[("dag/fx/census.dag", census)]); + let join = joint_claim_join(&[remover, claimer], &all, &never_between); + assert_eq!(join.findings.len(), 1, "{join:?}"); + assert_eq!(join.findings[0].removed_name, "grain_admits_single_cabinet"); + assert_eq!(join.findings[0].removed_from, SurfaceKind::Declared); + } + + /// The pair predicate is the caller's: a pair it refuses is neither a candidate nor a finding. + #[test] + fn a_pair_outside_the_admitted_window_is_not_joined() { + let remover = subject( + "remover", + &[("dag/fx/carrier.dag", CARRIER_BOTH)], + &[("dag/fx/carrier.dag", CARRIER_ONE)], + ); + let claimer = subject("claimer", &[], &[("dag/fx/witness.dag", WITNESS_CLAIMS)]); + let join = joint_claim_join(&[remover, claimer], &|_, _| false, &never_between); + assert_eq!(join.raw_candidates, 0); + assert!(join.findings.is_empty()); + } + + /// An import that was already on the claimer's base is not an ADDED claim: the remover's own + /// CI would have seen it on `main`, so the join must not report it a second time. + #[test] + fn a_claim_already_present_at_the_claimer_base_is_not_added() { + let remover = subject( + "remover", + &[("dag/fx/carrier.dag", CARRIER_BOTH)], + &[("dag/fx/carrier.dag", CARRIER_ONE)], + ); + let claimer = subject( + "claimer", + &[("dag/fx/witness.dag", WITNESS_CLAIMS)], + &[("dag/fx/witness.dag", WITNESS_CLAIMS)], + ); + let join = joint_claim_join(&[remover, claimer], &all, &never_between); + assert_eq!(join.raw_candidates, 0); + assert!(join.findings.is_empty()); + } +} From ec1949847edf295210b2488ce80c98d231e73b4a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 11 Sep 2026 17:08:00 +0000 Subject: [PATCH 4/4] State the population compile's boundary on the row: compilability of the claim-root population, not evaluation of it A file that compiles and whose test fns are declared but never reached from the entry passes that wall untouched (cool-badger-34's four fns, 2026-09-11). That hole is discriminating_arm_built_but_never_enrolled's, named as the neighbour so the word POPULATION is not later cited for a scope the trigger never claimed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QzQcQRu3WKxJuWV2zaiHSW --- .../witness_that_fails_to_compile_is_absent_rather_than_red.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/gunbc/recurring_failure_mode/witness_that_fails_to_compile_is_absent_rather_than_red.dag b/dag/gunbc/recurring_failure_mode/witness_that_fails_to_compile_is_absent_rather_than_red.dag index 7a55b48dfbc..7bd61217165 100644 --- a/dag/gunbc/recurring_failure_mode/witness_that_fails_to_compile_is_absent_rather_than_red.dag +++ b/dag/gunbc/recurring_failure_mode/witness_that_fails_to_compile_is_absent_rather_than_red.dag @@ -13,7 +13,7 @@ data witness_that_fails_to_compile_is_absent_rather_than_red: RecurringFailureMo "RECOGNITION RULE: ask what would have caught it, and IF THE ONLY ANSWER IS A RUN AIMED AT A DIFFERENT QUESTION, the class has no dedicated detector. The specimen surfaced from a whole-tree advisory census re-derivation chasing an unrelated hypothesis about a denominator, as blocking error number one, after the arms were green, the PR was open and an APPROVE was already recorded. THE ONE INSTRUMENT THAT SEES IT is a whole-tree compile INCLUDING the test roots; compiling the subject, or entry-scoping to the witness itself, does not. ", "Rung found at 1, mitigatable, held only by an unrelated run happening to range over the file, which is not a mechanism. CEILING 3, structurally guaranteed: the population is decidable and closed — every file under the claim roots — so a required step that compiles them and refuses a non-compiling claim file makes the state unwritable in an `Accepted` tree. NEXT TRIGGER, capability and not artifact: the required floor must COMPILE every claim-root file and refuse a claim file that does not compile, rather than only executing the claim files that compile — AN EXECUTION ROSTER KEYED TO COMPILING FILES CANNOT SEE A FILE THAT FELL OUT OF IT, the same shape as a check whose population IS its own roster. Specimen gunbc#10337: `brand_nominal_identity_witness_test` named a host builtin in its import list; four direct fixtures had already proven the compiler refuses, and the witness enrolling them had never run. ", "THE TRIGGER'S GRAIN IS NARROWER THAN THE RULED CAPABILITY, AND THIS RECEIPT WIDENS IT RATHER THAN LETTING THE NARROW TEXT STAND (sunny-tern-813 ruling, relayed and verified by crisp-lynx-452, 2026-09-11; filed from the joint-incompatibility scope, gunbc.plans.joint_incompatibility_claim_join section 2.3). The row above names the capability as `COMPILE every claim-root file`. The ruled capability is WIDER: compile every CONSTRUCTION SITE of a type whose required fields changed, AND every claim-root file regardless of gate closure -- and a construction site can live in a module that is not a claim root at all. DESIGN section 4b(3): a trigger naming less than the capability it restores will be satisfied while the capability stays dead, and the review tell is a grain mismatch between the loss sentence and the trigger sentence. The loss sentence of the second specimen is construction-site grain (`every construction site of that type in the tree`); the trigger sentence is claim-root-file grain. THAT IS THE MISMATCH. So the trigger, as of this receipt, reads at the ruled width: THE REQUIRED FLOOR MUST COMPILE EVERY CLAIM-ROOT FILE AND EVERY CONSTRUCTION SITE OF A TYPE WHOSE DECLARED FIELD SET CHANGED BETWEEN BASE AND HEAD, REGARDLESS OF GATE CLOSURE, AND REFUSE ANY THAT DOES NOT COMPILE. Discharging the narrower sentence alone does not retire this row. ", - "WHAT THE JOINT-INCOMPATIBILITY SCOPE ESTABLISHED ABOUT THIS ROW, so the two are not conflated: the cross-change join over the declaration index (v1_compiler.bin.joint_claim_join) keys on a module's EXPORT SURFACE -- declared, variants, reexported -- and a field is not a surface entry, so that join is silent on every specimen of this row by construction; the two classes share a shape (a change compiled against a population its diff does not name) and not a mechanism. The within-tree half needs no open-PR set and no cross-change anything: the removed side is `decl_fields` base-minus-head per declaration, derivable from the same base reconstruction the wave wall performs, and the claiming side needs no precise construction-site channel because the COMPILE is the oracle and the index only selects what to compile -- every module whose `referenced` or `authored_type_references` names the changed declaration, where over-collection costs compile time and never a false finding. That is the selection this row's widened trigger names, and it is this row's to discharge, not a checkpoint of the join's plan. ", + "WHAT THE JOINT-INCOMPATIBILITY SCOPE ESTABLISHED ABOUT THIS ROW, so the two are not conflated: the cross-change join over the declaration index (v1_compiler.bin.joint_claim_join) keys on a module's EXPORT SURFACE -- declared, variants, reexported -- and a field is not a surface entry, so that join is silent on every specimen of this row by construction; the two classes share a shape (a change compiled against a population its diff does not name) and not a mechanism. The within-tree half needs no open-PR set and no cross-change anything: the removed side is `decl_fields` base-minus-head per declaration, derivable from the same base reconstruction the wave wall performs, and the claiming side needs no precise construction-site channel because the COMPILE is the oracle and the index only selects what to compile -- every module whose `referenced` or `authored_type_references` names the changed declaration, where over-collection costs compile time and never a false finding. That is the selection this row's widened trigger names, and it is this row's to discharge, not a checkpoint of the join's plan. THE BOUNDARY OF THAT TRIGGER, STATED SO THE WORD POPULATION IS NOT LATER READ AS MORE THAN IT SAYS: the population compile establishes COMPILABILITY of the claim-root population and NOT EVALUATION of it. A claim file that compiles and whose `test fn`s are declared but absent from the entry's conjunction sails through that wall untouched (cool-badger-34, 2026-09-11: four such fns in a witness module type-checked, read as coverage, were never evaluated, and turned the suite red when enrolled). That second hole -- compiles YES, evaluated NO -- is owned by the neighbour row gunbc.recurring_failure_mode.discriminating_arm_built_but_never_enrolled, whose detector counts constructed fixtures against enrolled assertions; it is not this row's and this trigger does not close it. ", ], evidence: [],