From fee6b9f1bca831eccf40f8a763f677acf90723a7 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 23 Aug 2026 20:07:02 +0000 Subject: [PATCH 1/2] The 57s qualified-spelling claim is a whole-tree policy resolve, not name resolution The floor line that opened this (qualified_spelling_takes_the_shared_layer, wall_ms=57337, +1.22GB, bare arm free) reads as "qualified-name resolution is expensive". It is not. Qualified resolution is a map_get. Measured, four orderings plus a discriminating control: a qualified PATTERN HEAD costs nothing, and the premium lands on whichever claim first compiles a qualified TYPE ANNOTATION -- once per process, then 5ms forever after. The mechanism is severity classification, not resolution. A qualified annotation's authored name misses env.source_visible_names (which carries the bare imported names), so the masked type-ref arm emits an advisory UnlistedImportUse. Classifying that one diagnostic calls compile_clean_unlisted_import_use_blocks_from_policy, which resolves and typechecks a whole separate entry closure over default_source_roots() -- the whole tree -- to evaluate one nullary Bool. A bare annotation emits no such diagnostic and skips it; a qualified pattern head never enters that arm. This is gunbc.ci_spec's already-documented cost B from 2026-07-25, whose dissolve-on (scope the policy resolve to the policy module's own import closure) was never discharged. That note measured 57.8s for a green compile paying this tail against the floor's 57337ms. Shape, answering the brief: constant per process, independent of the subject compiled, and corpus-denominated. Same probe, roots varied: 216ms warm against 44886ms when from_policy's whole-tree root set diverges from the caller's and pays a cold load -- 44.9s locally against 57.3s on the floor. Diagnosis only; no repair, and the 1.22GB is reported as consistent-with rather than measured, since the witness is quarantined and the floor line was not re-run. Co-Authored-By: Claude Opus 5 --- ...alified_name_resolution_cost_2026-08-23.md | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 docs/probes/qualified_name_resolution_cost_2026-08-23.md diff --git a/docs/probes/qualified_name_resolution_cost_2026-08-23.md b/docs/probes/qualified_name_resolution_cost_2026-08-23.md new file mode 100644 index 00000000000..a67975e90f6 --- /dev/null +++ b/docs/probes/qualified_name_resolution_cost_2026-08-23.md @@ -0,0 +1,257 @@ +# The qualified-pattern-head cost is not the pattern head (2026-08-23) + +**Subject:** the cost observation carried forward, unrepaired, from gunbc#9004 — +`test.claim.qualified_pattern_head_witness_test.qualified_pattern_head_binds_the_instantiation` +measured `cpu_ms 58579`, `rss delta +1.21GB`, `outcome=timed_out` against a 5000ms budget under +the required-floor harness, while its bare control passed. That PR named the arms, ruled out two +causes, named a next discriminator, and explicitly did **not** claim the head was the cause. + +**This probe runs the discriminator the PR named, and the attributed subject is EXONERATED.** +A qualified pattern head costs nothing measurable. The cost attaches to a *qualified type +annotation*, it is **one-time per process**, and it is paid by whichever claim happens to compile +the first dotted type annotation in that process. + +## Method — a factorial probe, not a re-measurement of the witness + +The witness's two arms differ in **two** ways at once: the pattern head spelling AND the parameter +type annotation spelling. Both of its arms are therefore confounded for the question "what costs". +`dag/test/claim/qualpat_cost_probe.dag` splits the two axes over one fixture +(`test.fixture.qualpat_provider`, unchanged), all five probes carrying identical imports: + +| probe | pattern head | type annotation | +|---|---|---| +| A | qualified | qualified | +| B | bare | bare | +| C | bare | **qualified** | +| D | **qualified** | bare | +| E | (no match, no import — trivial control) | — | + +Each probe is one `compile_dag_rust_emit_check` call. All five run in ONE `claim_batch` process, so +the entry resolve is paid once and outside the fold; the per-claim `cpu` figures below are the +marginal cost of the compile itself. Release build, remote amd64 runner, `--source-root dag +--source-root src/v2`. + +**The order is varied deliberately**, because the first measurement showed the premium on whichever +probe ran first and a single ordering cannot distinguish "this probe is expensive" from "the first +probe is expensive". + +## Measured — four orderings + +| run order | A | B | C | D | E | +|---|---|---|---|---|---| +| A,B,C,D,E | **256** | 5 | 5 | 5 | 1 | +| E,B,C,D,A | 5 | 6 | **222** | 5 | 40 | +| D,B,E,C,A | 6 | 6 | **201** | 44 | 1 | +| C,A,D,B,E | 7 | 5 | **251** | 5 | 1 | + +(cpu ms per claim; **bold** = the premium payer in that run. All five probes PASS in every run.) + +## What the table says + +1. **The qualified pattern head is free.** D carries a qualified head and a bare annotation. It + costs 5ms in every run where it is not first, and 44ms when it runs first — which is the same + as the trivial control E costs when *it* runs first (40ms), i.e. generic first-call warmup and + nothing more. Running D first does **not** discharge the premium: C still pays 201ms afterwards. +2. **A qualified type annotation carries the premium, once.** In every ordering the premium lands + on the first probe whose source contains a dotted *type annotation* — A when A leads, C in all + three orderings where a bare-annotation probe leads. Once paid, every later dotted-annotation + compile costs 5ms: in run 1, A pays 256ms and C costs 5ms; in run 4, C pays 251ms and A costs 7ms. +3. **It is one-time per process, not per call**, and therefore attributed to an arbitrary claim — + whichever one happens to reach a dotted type annotation first. + +That is sufficient to retire the witness's implied attribution: the arm named +`qualified_pattern_head_binds_the_instantiation` was charged for a cost its pattern head does not +cause. Its two arms differed in the annotation too, and the annotation is the axis that pays. + +## Ruled out, each by reading the producer + +- **Name lookup itself.** `symbol_index_lookup` is a `map_get` over `entries: Rc>`; + the `.clone()` at that call site is an `Rc` refcount bump, not a map copy. `global_bare_lookup` + on a miss is likewise a `map_get` returning `Absent`. +- **The census fill.** `compile_dag_rust_emit_check` reaches `compile_sources`, which takes + `default_compile_pipeline_options()`, whose `census_only_sources` is `[]`. So + `parse_census_fill_sources` — the whole-tree parse that `gunbc.ci_spec`'s clamp note measures at + ~23s local — has an empty input on this path and cannot be the term. Worth stating positively: + the qualified reference resolves here with **no** census to resolve against. +- **The floor prepared-inventory digest.** `floor_prepared_inventory_digest()` clones a `String` + precomputed on the authority; it does not re-hash the corpus per call. +- **`module_path_index`.** Cached per thread and, per #9004's own ledger reading, paid outside the + fold. (Noted in passing, not as this probe's finding: `build_module_path_index` returns + `index.clone()` — a full deep copy of the whole-corpus `HashMap` — on every call + including every cache HIT. That is a copied-accumulator cost shape under DESIGN §6, far too + small to be this defect, and is not repaired here.) + +## What is NOT claimed + +**The magnitude gap is open and is not papered over.** The premium measured here is ~200-250ms; +the floor observed 58579ms and +1.21GB. This probe establishes the *shape* (one-time per process) +and the *trigger* (first dotted type annotation), and it exonerates the pattern head. It does +**not** establish that the same term accounts for 58.6s and 1.21GB at floor corpus scale — that +would be the rung inflation DESIGN §4b names as worse than sitting low. The remaining work is to +name the lazily-built structure and show it is corpus-denominated. + +## THE MECHANISM — found, and it is not name resolution at all + +Qualified-name resolution is cheap. What a qualified type annotation does is emit an **advisory +diagnostic**, and the cost is in **classifying that diagnostic's severity**. + +``` +compile_dag_rust_emit_check + -> compile_sources (the compile itself: ~8ms of trace_mark phases) + -> result.diagnostics.filter(compile_clean_diagnostic_is_hard) + CompilerDiagnostic::UnlistedImportUse { .. } + -> compile_clean_unlisted_import_use_blocks_cached() <-- thread_local CACHED + -> compile_clean_unlisted_import_use_blocks_from_policy() + let roots = default_source_roots(); <-- THE WHOLE TREE + resolve_entry_graph_shared(&roots, "dag/gunbc/compile_clean_diagnostic_policy.dag") + run_in_context_with_args(ctx, "compile_clean_unlisted_import_use_blocks", &[]) +``` + +**A whole-tree resolve + typecheck of a separate entry closure, to evaluate one nullary `Bool`.** +It is `thread_local`-cached, so it is paid **once per process** by whichever claim first produces +an `UnlistedImportUse`, and it is **independent of what was compiled**. + +**Why only the qualified spelling triggers it.** `UnlistedImportUse` comes from the masked type-ref +arm of `resolve_node_bounded` (`v1_compiler_infer_resolve.rs`), which fires when the **authored** +type name is absent from `env.source_visible_names`. The import list contributes **bare** names +(`QualpatResult`), so the authored name `test.fixture.qualpat_provider.QualpatResult` misses and the +advisory is emitted; a bare annotation hits and emits nothing. A qualified **pattern head** never +enters that type-ref arm at all — it goes through `lookup_variant_in_type` / `symbol_index_lookup` — +which is exactly why probe D is free. + +So the qualified/bare asymmetry that made this look like a resolution-cost defect is really: +*the qualified spelling is the only one that produces the diagnostic whose severity lookup is +corpus-denominated.* + +### Confirming control + +Order B then D — both bare annotations, D carrying the qualified pattern head: 72ms / 8ms, and +`dag/gunbc/compile_clean_diagnostic_policy.dag` **does not appear at all** in `span_nanos_by_entry`. +In both qualified-annotation orderings it appears exactly once, at 259ms and 269ms — the premium, +to within ~10ms. The axis switches the policy resolve on and off. + +## This is a KNOWN, DOCUMENTED, UNFIXED defect — and the floor number matches its original measurement + +`gunbc.ci_spec` `gunbc_ci_floor_batch_clamp_note` already carries it, measured 2026-07-25: + +> **(B) THE POLICY TAIL, after the compile:** `compile_clean_unlisted_import_use_blocks_from_policy` +> calls `default_source_roots()` (WHOLE TREE) + `resolve_entry_graph_shared` to evaluate ONE nullary +> Bool fn. ~34-42s, **once per process**, and INDEPENDENT of what was compiled — proven by running +> the same module with only its own source root: zero pool, all phases 0ms, still 42.4s wall. […] +> a RED compile never reaches the gate (measured 27.4s total, ~2s tail) while a **GREEN compile pays +> it in full (57.8s total, ~34s tail)**. + +**57.8s there; `wall_ms=57337` on the floor line that opened this investigation.** That note's +`dissolve-on` names the repair — *"scope the policy resolve to the policy module's own import +closure"* — and it was never discharged. The defect did not reappear; it never left. What is new +here is **why one arm and not the other**: the trigger is the authored-spelling miss in +`source_visible_names`, which is what makes a green qualified compile pay the tail and a green bare +compile skip it. + +## Superlinearity — answering the shape question directly + +**It is not superlinear in the witness. It is a fixed, corpus-denominated, once-per-process tail.** +Two consequences, and the second is the one that matters: + +- It does **not** grow with the subject compiled. Nothing about the probe's size, import count, or + qualified-name length changes it; a second qualified compile in the same process costs 5ms. +- It **does** grow with the corpus, because `default_source_roots()` is the whole tree and the + resolve is a full entry closure. Measured, same probe, same process, varying only the roots + `claim_batch` was given: + +| roots given to claim_batch | B (bare) | C (first qualified) | +|---|---|---| +| `dag` + `src/v2` | 48ms | **216ms** | +| `dag` only | 12967ms | **44886ms** | + +The dag-only arm is ~208× more expensive — because `from_policy` asks for `default_source_roots()` +regardless of what the process was given, so its whole-tree resolve is a **different key** from the +warm shared index and pays a cold whole-tree load. **44.9s locally, against 57.3s on the floor** — +the same order, on the same mechanism. That closes the magnitude gap this probe had left open. + +So the urgency test the brief posed is answered: the cost is constant per process but +**corpus-denominated**, and it is paid on a *cold* index whenever the policy resolve's root set +diverges from the caller's. Both halves grow with the repository. + +## Is this path scheduled for deletion? (do not optimise a corpse) + +Partly, and it changes who should fix it rather than whether. `cli_run.rs` is deleted wholesale by +`integration/cli-run-cut`, and `compile_clean_unlisted_import_use_blocks_from_policy` lives there. +But no bounded event retires it: v1 is *semantics frozen, maintenance active* with no cutover date +(DESIGN §3, `gunbc.v1_maintenance_standing`), and the required floor runs this path on every run +today. The named repair — scope the policy resolve to the policy module's own import closure — is +small, is already written down as that note's dissolve-on, and does not extend the seed's surface. + +## What is claimed, and what is not + +**Claimed, by execution:** the qualified *pattern head* costs nothing (four orderings, plus a +control in which the policy entry is absent from the span table); the premium is the first +`UnlistedImportUse` in a process; the payer is `compile_clean_unlisted_import_use_blocks_from_policy` +resolving a whole-tree entry closure to compute one `Bool`; the cost is corpus-denominated and +reaches 44.9s locally on a cold root set. + +**Not claimed:** that the floor's 1.22GB is *entirely* this term. The floor line was not +re-measured under this probe — the witness is quarantined as of #9031 and the offline recipe is +what ran here. The wall figures agree (44.9s local cold vs 57.3s floor, and the 57.8s the 2026-07-25 +note measured for exactly this tail), and the RSS shape is consistent with a whole-tree entry +resolve, but "consistent with" is not "measured", and this document does not upgrade it. + +## Apparatus — re-run recipe + +The probe module is reproduced here rather than left in the tree: it has no consumer, so as a +committed `.dag` it would be experimental residue (DESIGN §6). Write it to +`dag/test/claim/qualpat_cost_probe.dag` (the name deliberately does NOT end in `_test.dag`, so +floor discovery cannot enrol it), then run — one remote dispatch, since the runners are amd64 and +session containers are arm64: + +``` +ctrl-build --remote -- bash -lc 'cargo build --release --bin claim_batch && \ + ./target/release/claim_batch --entry dag/test/claim/qualpat_cost_probe.dag \ + --functions probe_c_bare_head_qual_annot,probe_a_qual_head_qual_annot,probe_d_qual_head_bare_annot \ + --source-root dag --source-root src/v2' +``` + +Vary the `--functions` order to move the premium; drop `--source-root src/v2` for the cold arm. + +```dag +module test.claim.qualpat_cost_probe + +fn qualpat_cost_check(src: String, path: String) -> Bool { + compile_dag_rust_emit_check(src, path, [], []) +} + +test fn probe_a_qual_head_qual_annot() -> Bool { + qualpat_cost_check( + "module qualpat_a_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload, QualpatOk, QualpatErr \}\n\nfn qualpat_a_read(r: test.fixture.qualpat_provider.QualpatResult) -> String \{\n match r \{\n test.fixture.qualpat_provider.QualpatOk \{ value: v \} => v.root\n test.fixture.qualpat_provider.QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n", + "src/qualpat_a_mod.rs" + ) +} + +test fn probe_b_bare_head_bare_annot() -> Bool { + qualpat_cost_check( + "module qualpat_b_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload, QualpatOk, QualpatErr \}\n\nfn qualpat_b_read(r: QualpatResult) -> String \{\n match r \{\n QualpatOk \{ value: v \} => v.root\n QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n", + "src/qualpat_b_mod.rs" + ) +} + +test fn probe_c_bare_head_qual_annot() -> Bool { + qualpat_cost_check( + "module qualpat_c_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload, QualpatOk, QualpatErr \}\n\nfn qualpat_c_read(r: test.fixture.qualpat_provider.QualpatResult) -> String \{\n match r \{\n QualpatOk \{ value: v \} => v.root\n QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n", + "src/qualpat_c_mod.rs" + ) +} + +test fn probe_d_qual_head_bare_annot() -> Bool { + qualpat_cost_check( + "module qualpat_d_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload, QualpatOk, QualpatErr \}\n\nfn qualpat_d_read(r: QualpatResult) -> String \{\n match r \{\n test.fixture.qualpat_provider.QualpatOk \{ value: v \} => v.root\n test.fixture.qualpat_provider.QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n", + "src/qualpat_d_mod.rs" + ) +} + +test fn probe_e_trivial_no_import() -> Bool { + qualpat_cost_check( + "module qualpat_e_mod\n\nfn qualpat_e_read(x: Int) -> Int \{ x \}\n", + "src/qualpat_e_mod.rs" + ) +} +``` From 4aff5e6adca188c5825a3cfd18f8b9bd5de71929 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 23 Aug 2026 20:19:48 +0000 Subject: [PATCH 2/2] Scope the compile-clean policy read to its own import closure, refusing rather than widening Discharges the cost-B half of gunbc.ci_spec gunbc_ci_floor_batch_clamp_note's dissolve-on, which has named this exact repair since 2026-07-25 and was never landed. No second row is filed beside it: the obligation is discharged in place, because landing the fix while leaving its obligation open would be two authorities for one fact. compile_clean_unlisted_import_use_blocks_from_policy resolved and typechecked an entry closure over default_source_roots() -- the whole tree -- to evaluate one nullary Bool. That is not merely oversized (the policy module has three imports); it is a function answering a question about the caller's world by consulting a different one, which is why it presents as cost but is correctness-shaped, and why "n is small here" was never available. It now assembles the policy entry's own import closure and resolves that explicit source set. Every arm that cannot produce the exact closure REFUSES with a located message naming the module and the path. There is deliberately no whole-tree fallback: that arm would restore today's cost, zero the deficit's frequency by construction, and make the widening unrankable ever after (DESIGN section 5). It is a new closure builder rather than a reuse of resolve_virtual_source_with_imports because that BFS silently SKIPS an unresolvable import -- a silent skip here would answer the policy question from a graph missing the module the answer depends on. MEASURED, same probe and orderings, all probes PASS so the narrowed closure still returns the policy Bool: roots dag only C first qualified 44886ms -> 109ms roots dag + src/v2 C first qualified 216ms -> 151ms The qualified/bare asymmetry is gone rather than reduced: on the cold root set the qualified arm is now CHEAPER than the bare one. RESIDUE, reported rather than absorbed: the bare arm's 12967ms on the dag-only root set did not move (12881ms). The bare arm pays it too, so it was never part of the qualified asymmetry and this repair does not touch it. NOT UPGRADED: the floor's 1.22GB is still unattributed by execution. If the memory line survives this repair that is a second defect to find, not one to absorb here. v1 freeze admission: PURPOSE test (operator ruling 2026-08-20) -- a defect repair on a path the required floor executes every run, not growth on a v1 surface for v1's own sake. Co-Authored-By: Claude Opus 5 --- dag/gunbc/ci_spec.dag | 2 +- ...alified_name_resolution_cost_2026-08-23.md | 47 ++++++++++++ src/v1/stage0/src/cli_run.rs | 75 ++++++++++++++++++- 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/dag/gunbc/ci_spec.dag b/dag/gunbc/ci_spec.dag index 508595ad328..e3f904211ab 100644 --- a/dag/gunbc/ci_spec.dag +++ b/dag/gunbc/ci_spec.dag @@ -215,7 +215,7 @@ fn ci_spec_with_discovery_scan_dirs(spec: CiSpec, dirs: List) -> CiSpec data gunbc_ci_floor_batch_wall_budget_note: String = "SUPERSEDED by the derived clamp gunbc_ci_floor_batch_clamp_note (Piece 3, 2026-07-24): the hand-set gunbc_ci_floor_batch_wall_budget_seconds list this note governed is deleted, replaced by overhead + units*rate computed at run time; this note is kept for the operator-signed static-era history (including the two 1320->1440->1680 raises below) and the raise discipline, which carries forward to the clamp constants. THE COST WALL (CI floor endgame D5, operator brief 2026-07-23 — the resolve-regression journey's section-3 finding made mechanism: nothing redded a merge that added floor minutes, so every recovered minute was re-spent by the next lane's enrollment, and the step timeout's own bounce history 30 -> 60 -> 90 -> 120 -> 180 -> 270 -> 55 is the record of the budget being raised to fit). Per-BATCH wall budgets for gunbc_ci_floor_plan (named gunbc_ci_floor_batches through the static era this note records), enforced by claim_executor at walk time: each batch's measured wall is recorded as a typed receipt row (target/floor-batch-wall-receipt.txt), and a batch over its budget is a typed, located refusal — FLOOR-BATCH-OVER-BUDGET naming the batch index, measured wall, budget row, and this carrier — that reds the walk. A failure arm refuses, never widens: over-budget never triggers a rerun, a wider scope, or a silent cap raise. Denominated PER-BATCH, never per-run, because plumbing PRs have a structurally different cost profile (attribution doc section 9.2: a PR touching host_prelude/cli_run legitimately runs 46 of 55 wet rows — a per-run wall would red every such PR spuriously; per-batch budgets absorb the profile where it lands). RULING RECONCILIATION (the operator dispatching the 2026-07-23 endgame brief is the sign-off on this reading): the standing rule 'no wall-clock term in any verdict' (ci_floor_materialization_receipt_note, operator ruling 2026-07-10) governs WITNESS VERDICTS — a witness's pass/fail must never depend on how long it took. Batch budgets do not touch witness verdicts: they are admission/scheduling facts at the walk grain, the same split the 5-second fast-lane eval law already uses (gunbc_ci_fast_lane_rule_note: the deadline is lane admission, not a verdict term), so a batch refusal names the BATCH, and every witness verdict inside it stands as evaluated. RAISE DISCIPLINE: raising any budget requires appending a dated receipt note here naming the run id and the enrollment that grew the batch — the same discipline as the timeout-note bounce history — never a silent number edit. Budget basis (run 30009199696, post-#7122, with the endgame fixes priced in): batch 1 cheap gates 185s measured -> 125s post-pool, budget 240; batch 2 receipt consume ~0s, budget 60; batch 3 discovery 1024s measured (corpus-denominated resolve wall, lever-1 re-diagnosis pending), budget 1320; batch 4 wet corpora 649s plumbing-profile measured -> ~175s post-re-home, budget 420; batch 5 emit-host 7s, budget 120; batch 6 ingest 414s measured (4 composed overlay children — the genuine mktemp constraint), budget 600; batch 7 reads-real-bytes 206s, budget 420. Sum 3180s = 53min under the 55min step cap. OPERATOR SIGNATURE (briansrls, 2026-07-23): the admission/verdict reading above is affirmed — per-batch wall budgets are scheduling-admission facts, not wall-clock terms in witness verdicts; this line is the declared human-signed exemption row the 2026-07-10 ruling requires. Raise discipline amended: RAISING any budget row requires a new dated operator-signed line here naming the run id and the enrollment that grew the batch; TIGHTENING may land by ordinary receipt note. The on-call remedy for FLOOR-BATCH-OVER-BUDGET is diagnose-or-signed-raise, never rerun. FOLLOW-UP ROWS (operator rework push, 2026-07-23): (a) PRELUDE COVERAGE HOLE — the ~5min before batch-1 arms (naming-hygiene walk, policy install, plan resolve/eval, governor arm, eager compile-clean install) sits OUTSIDE every batch budget and can only red at the 55min step cap; a prelude budget row lands when the phase_mark walls get their own receipt keys. (b) IDENTITY-KEYED BUDGETS — rows are keyed by batch INDEX today (the coverage witness pins length, so a schedule change reds loudly rather than misassigning); keying by batch content-identity dissolves the index coupling and rides the ComputationIdentity lane. (c) K-CAP UNION-RSS RECEIPT — cheap_gate_pool_max_claims_per_child=16 is provisional until the pooled child's union RSS is receipted (claim_batch already prints per-shard-peak-rss; the first CI runs' logs back or re-size the cap; local proxy receipt: the 4-gate outer child peaked 1.82GB with the 12-claim pool nested). OPERATOR-SIGNED RAISE (briansrls, 2026-07-24): batch 3 (row index [2]) 1320 -> 1440 seconds, +9.1 percent (operator: 'lets raise the budget slightly more to give it some room over main' / 'like 10%'; 24min keeps the row's whole-minute grain). RUN IDS: 30063268739 @ e7a9006f wall_ms=1629287, 30058048605 wall_ms=1344115, 30055079462 wall_ms=1380561 — all batch=3 against budget_ms=1320000 on PR #7137. THE ENROLLMENT THAT GREW THE BATCH: none, and that mismatch with this discipline's template IS the receipt. Batch 3 is the affected-set-selected discovery corpus, so its wall is denominated in the DIFF's affected entry count, not in a roster edit: #7137 touches gunbc.ci_layer_roots (broadly imported) and src/v1/stage0/src/cli_run.rs, selecting 839-841 entries where a typical main push selects 600-601. Per-entry cost is at PARITY with green controls, so nothing regressed — #7137 1.602/1.609/1.646 s-per-entry vs main 1.574, clever-ram-110 1.756, clever-pike-49 1.504. The retired 1320 divided by 841 entries = 1.570 s-per-entry, BELOW main's own 1.574 measured rate: the row had zero headroom for any broad-touch diff and would red a no-op PR that merely touched a widely-imported carrier. 1440/841 = 1.712 s-per-entry, ~8.8 percent over main's rate — that is the 'room over main'. HONEST RESIDUAL: 1440 covers three of the four observed runs; the 1629287 outlier (run 30063268739, host peaked 9GB vs 16GB on the other three, 1.937 s-per-entry) still reds, and that is the wall working as designed — the remedy stays diagnose-or-signed-raise, never rerun. SUM ACCOUNTING (the one invariant this raise spends): the ceiling sum moves 3180 -> 3300 seconds = exactly 55min, so this note's original 'Sum 3180s = 53min under the 55min step cap' margin is now CONSUMED. Ceilings are per-batch maxima rather than a predicted run wall, but the sum was this note's stated safety argument and it no longer holds with margin, so it is recorded here rather than dropped. FOLLOW-UP (not landed here): restore margin by TIGHTENING the rows with the largest declared-vs-measured gap once post-merge receipts exist — batch 4 (budget 420, ~175s measured post-re-home) and batch 6 (budget 600, 414s measured) — which lands by ordinary receipt note under this same discipline. OPERATOR-SIGNED RAISE (briansrls, 2026-07-24, second): batch 3 (row index [2]) 1440 -> 1680 seconds, +16.7 percent (28min, whole-minute grain). Operator's word this raise: 'you may bump the budget to 1680 - we're redoing it completely anyway' — so 1680 is a STOPGAP to green this PR while the batch-budget mechanism itself is reworked (the FOLLOW-UP ROWS above: prelude coverage, identity-keyed budgets, k-cap union-RSS), not a claim that 1680 is the settled ceiling. WHY 1440 WAS UNDER-SIZED: the first raise sized off three observations (1344/1381/1629) where one landed on a fast host; five batch-3 walls are now observed on #7137 — 1344115, 1380561, 1558226 (run 30081341899 @ 06a1f2c, host srv4-03), 1581666 (run 30067149030 @ 93642bc, host srv4-03), 1629287 (run 30063268739) — and THREE of five exceed 1440, so 1440 reds the majority, not a lone outlier. This is host variance on the ~840-entry affected set, not a per-entry regression (per-entry stays at parity with green controls, ~1.6-1.9 s-per-entry across hosts). THE ENROLLMENT THAT GREW THE BATCH: still none — batch 3 is affected-set-denominated and #7137 touches broadly-imported carriers (gunbc.ci_layer_roots, cli_run.rs), the same profile the FLOOR-BATCH-OVER-BUDGET template's 'enrollment' field does not capture. SIZING: 1680 clears all five observed walls (max 1629287) with ~3 percent margin. SUM ACCOUNTING CORRECTED: the prior line's 'sum = 55min, margin consumed' was over-conservative — per-batch maxima never co-occur, and run 30081341899's ACTUAL floor step wall was ~44min (b1 185s + b2 ~0s + b3 1558s + b4 175s + b5 7s + b6 414s + b7 206s), well under the 55min step timeout that is the real constraint; the ceiling-sum (now 3540s) is a paper figure nothing hits. The remedy stays diagnose-or-signed-raise, never rerun." -data gunbc_ci_floor_batch_clamp_note: String = "DERIVED per-batch wall clamp (Piece 3, ci-two-tier-placement-redesign.md §9.8, operator 2026-07-24). Supersedes the hand-set gunbc_ci_floor_batch_wall_budget_seconds list (deleted; its two operator-signed raises 1320->1440->1680 are the static-era history kept in gunbc_ci_floor_batch_wall_budget_note): a scalar wall budget conflated workload size (affected-set selection is diff-proportional BY DESIGN, ~5x swing), host speed (±20% fleet envelope), and the quantity actually worth bounding (per-unit cost creep). The clamp re-denominates — per batch, clamp_ms = overhead_seconds*1000 + runtime_unit_count * per_unit_ms — computed by claim_executor from THIS authority plus the affected-set-selected unit count it alone knows (the schedule holds one opaque discovery runnable; the witness count is runtime, not schedule data). Rows are index-aligned to the batch list gunbc_ci_floor_ordinary_batches, minus the trailing scoped-witness batch which owns its own clamp (the length-match witness witness_scoped_batch_is_singleton_and_outside_positional_clamps pins the alignment, mirroring the deleted list's discipline). The load-bearing row is the discovery witness batch (index 2): overhead 300s + 1000ms/witness, so a full corpus of ~2316 witnesses clamps at ~44min (under the 55-min step cap, ~1.6x the ~25-min healthy wall) while every legitimate observed full-corpus wall (1344-1629s) passes, and a runaway reds proportionally instead of at the fixed 1680s that the two hand-raises had to keep chasing. Fixed-count gate batches carry per_unit_ms 0 (their count does not vary, so overhead IS the clamp) at their measured basis. Index 3 (wet corpora) stays a fixed overhead pending a wet-per-witness rate from the D2 probe — a declared calibration gap, not a hidden default. SIGNED CONSTANTS (operator, 2026-07-24): the witness aggregate coefficient 1000ms and the discovery overhead 300s; the per-WITNESS hard max is NOT redefined here — it stays the single fast-lane authority gunbc_ci_fast_lane_eval_budget_ms (5s). BASIS OF THE 1000ms COEFFICIENT (operator, 2026-07-24): the aggregate coefficient is denominated against the observed 0.58-0.70 s/witness (the 1344-1629s full-corpus fleet envelope over ~2316 witnesses) on the srv fleet host class (arm64 self-hosted, capped runners) at the adaptive governor's realized worker width. Naming host-class-x-worker-width as the basis makes a future width or fleet change a DELIBERATE re-sign of this constant (s/witness re-denominates when the fleet or width moves), never a rediscovered fleet-wide red; the ~1.4-1.7x headroom the 1000ms average carries over the observed top rate IS the >=~1.6x runaway the clamp is sized to catch, and sub-threshold creep below that ratio is owned by the gauntlet's per-cadence s/unit receipt (not this clamp). RAISE DISCIPLINE (carried from gunbc_ci_floor_batch_wall_budget_note): raising any overhead or rate requires an appended dated operator-signed line naming the run id and the enrollment that grew the batch; tightening may land by ordinary receipt note; unit counts need no signature (the schedule computes them). The clamp is interim mechanics — the structural wall is the complexity lens (§8, cost <= a + b*n asserted at compile time), and the clamp demotes to host-pathology backstop when that lens goes Blocking. The 55-min step cap stays the absolute backstop for the total floor wall; GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS lowers the COMPUTED clamp (min), never raises — the RED-control hook, never an escape hatch.OPERATOR-SIGNED RAISE (briansrls, 2026-07-25): batch 4 (row index [3]) 420 -> 540 seconds, +28.6 percent. THE MARGIN RULING THIS INSTANTIATES, signed once for the budget FAMILY rather than re-litigated per incident: a clamp was doing double duty as merge-refusal threshold ('this must not merge') and growth detector ('something got slower - look'). Tight margins serve the second job and demonstrably worked - the perturb row's 53 -> 141s growth was caught precisely because the clamp was tight - but they make the FIRST job fire on host roulette, and a breach that means 'you landed on srv2-02' trains the on-call to rerun, which is the crying-wolf failure mode wearing budget clothes. POLICY: clamps are sized to cover MEASURED fleet spread, so a breach means content grew, never which host answered. The growth-detector job moves to per-row trend receipts on the falsifier cadence (row grew >2x against its dated basis = a counted drift receipt), which is what makes a larger clamp margin safe - sensitivity is preserved at row grain while merge-refusal stops firing on variance. BASIS OF 540: post-re-home projection 377s (the worst of the two observed walls, 613864ms on srv2-02, minus the ~237s of eval the six re-homed rows carried) x 1.2 worst observed fleet spread = ~452s, + ~20 percent policy margin = 540 (whole-minute grain, 9min). RUN IDS: 30148859947 @ 619bba5 wall_ms=613864 units=74 host srv2-02; 30163496549 @ 9f87967 wall_ms=571556 units=74 host srv3-01 - identical content, 7.5 percent apart, which IS the spread this raise funds. THE ENROLLMENT THAT GREW THE BATCH: none - and as with the batch-3 raises, that mismatch with this discipline's template IS the receipt. Batch 4 SHRANK this cycle (six rows re-homed to FalsifierCadenceJob, units 74 -> 68); the raise buys spread coverage on a batch that got smaller, not headroom for growth. WHY THIS IS NOT THE FORBIDDEN WIDEN: batch 4's per_unit_ms is 0, which this note ALREADY declares 'a fixed overhead pending a wet-per-witness rate from the D2 probe - a declared calibration gap, not a hidden default'; 540 funds that gap with an honest, dated, dissolving interim instead of leaving the flat rate to fire on host variance. It refuses exactly as before - only the threshold moved, and it moved on a stated measurement, not to make a red go away. HOST SPREAD IS NOT WEATHER and has a named owner: srv1/srv2 still run the pre-#7213 sccache units and are typed expected-latent-defective until re-provisioned (the A1/reach gap), so part of the 7.5-20 percent collapses when the fleet lane re-converges them; this margin covers the genuine hardware-heterogeneity residue, eventually the machine-shape lane's. DISSOLVE-ON: the dag_compile_clean_perturb_receipts_holds diagnosis lands (141s, 38 percent of the remaining batch; if the growth is the cold-spawn / per-entry-reconcile class the wasted_ms=166525 reading supports, returning it toward its 53s basis puts worst case near 350s - under even the retired 420) AND the per-unit rate replaces the flat clamp from the D2 probe; then this row recalibrates by ordinary receipt note under the tightening rule. SAME DISEASE, NOT YET DOSED: gunbc_falsifier_self_host_wet_receipt_wall_budget (600s, the 707s falsifier red) rests on a ten-day-stale basis with zero spread allowance. The margin POLICY above governs it too, but the NUMBER is deliberately not moved here - it is gated on its own sccache-vs-growth attribution, so that dosing lands with that measurement rather than by analogy. WORKER COUNTER-SIGNATURE (claude, 2026-07-25, per this discipline's dated-line requirement): I ran the dissolve-on diagnosis and it REFUTES the mechanism this note hypothesised. The growth is NOT the cold-spawn / per-entry-reconcile class the wasted_ms=166525 reading suggested. Measured by execution on the perturb spawn shape (trivial 1-module compile, 4 source roots) there are TWO independent corpus-denominated costs, in different places. (A) THE CENSUS, inside reconcile: main.rs:491-514 reads every indexed module outside the closure off disk into census_only_sources, and v1_compiler_compile.rs:2580 parse_census_fill_sources parses all ~2543 of them BETWEEN compile.normalize.done and compile.reconcile.done - so it books to the reconcile bin and reads as 'reconcile is slow' when it is a whole-tree parse wearing reconcile's label. ~23s local, ~12-13s on fleet. (B) THE POLICY TAIL, after the compile: cli_run.rs:2105 compile_clean_unlisted_import_use_blocks_from_policy calls default_source_roots() (WHOLE TREE) + resolve_entry_graph_shared to evaluate ONE nullary Bool fn. ~34-42s, once per process, and INDEPENDENT of what was compiled - proven by running the same module with only its own source root: zero pool, all phases 0ms, still 42.4s wall. THE ASYMMETRY that closes the arithmetic: main.rs:566/598 exit(1) on hard diagnostics, so a RED compile never reaches the gate (measured 27.4s total, ~2s tail) while a GREEN compile pays it in full (57.8s total, ~34s tail). The perturb row makes 4 spawns, 2 red and 2 green: ~2x14.5s + ~2x60s = ~149s against the measured 148495.7ms. Pre-#7179 that same row is 4 spawns x census-only = ~52s, which IS the 53s basis. So cost B is the ENTIRE 53->148s regression (~94s of the 148s) and cost A was always present. CONSEQUENCE FOR THIS ROW: the note's projected 'near 350s' still holds directionally but by a different mechanism - fixing B alone returns the row to ~55s, so worst case lands well under even the retired 420 and the 540 becomes pure spread coverage, which is what this note already says it is for. NO BISECT WAS RUN AND NONE IS NEEDED: the three-run measurement attributes the split directly, so the bisect across #7178/#7179 named in the probe plan is superseded work (DESIGN 2 - redundant work is not free just because it would confirm). DISSOLVE-ON, unchanged in shape but now half-discharged: the diagnosis half is DONE (this line); recalibration still waits on the cost-B fix landing (scope the policy resolve to the policy module's own import closure) plus the D2 wet-per-witness rate replacing per_unit_ms 0 on this row.ROW REMOVAL (claude, 2026-07-26, D3b gate flips): two rows DELETED from the tail, 7 -> 5, because their batches no longer exist — SourceRootIngestGate and SelfHostReadsRealBytesGate moved to the falsifier cadence (gunbc.commit_workflow gate_gauntlet_flip_basis_note), and each occupied a batch of its own. This is not a budget change: no surviving row's numbers moved, so no re-signature is owed under the raise discipline above. WHICH TWO, and why the obvious answer was wrong: the removed rows are the LAST two (600s and 420s), NOT the 120s/600s pair a reading of the schedule order suggests. Verified by execution rather than inferred — emit_host_gate_passes sits at batch index 4 BOTH before and after the flip, which is only possible if the two vanished batches were at indices 5 and 6, after it. The 120s row at index 4 is emit_host's and survives. RECORDED BECAUSE THE ALIGNMENT IS POSITIONAL AND HAND-MAINTAINED: an index-aligned list carries no evidence of which row belongs to which batch, so a plausible-but-wrong deletion here is silent — it mis-clamps a surviving batch and reads as a passing length check. The length witness (witness_floor_batch_clamp_params_cover_schedule) proves the COUNT and cannot prove the MAPPING; that gap is the standing argument for deriving these rows from the schedule rather than pairing them by position." +data gunbc_ci_floor_batch_clamp_note: String = "DERIVED per-batch wall clamp (Piece 3, ci-two-tier-placement-redesign.md §9.8, operator 2026-07-24). Supersedes the hand-set gunbc_ci_floor_batch_wall_budget_seconds list (deleted; its two operator-signed raises 1320->1440->1680 are the static-era history kept in gunbc_ci_floor_batch_wall_budget_note): a scalar wall budget conflated workload size (affected-set selection is diff-proportional BY DESIGN, ~5x swing), host speed (±20% fleet envelope), and the quantity actually worth bounding (per-unit cost creep). The clamp re-denominates — per batch, clamp_ms = overhead_seconds*1000 + runtime_unit_count * per_unit_ms — computed by claim_executor from THIS authority plus the affected-set-selected unit count it alone knows (the schedule holds one opaque discovery runnable; the witness count is runtime, not schedule data). Rows are index-aligned to the batch list gunbc_ci_floor_ordinary_batches, minus the trailing scoped-witness batch which owns its own clamp (the length-match witness witness_scoped_batch_is_singleton_and_outside_positional_clamps pins the alignment, mirroring the deleted list's discipline). The load-bearing row is the discovery witness batch (index 2): overhead 300s + 1000ms/witness, so a full corpus of ~2316 witnesses clamps at ~44min (under the 55-min step cap, ~1.6x the ~25-min healthy wall) while every legitimate observed full-corpus wall (1344-1629s) passes, and a runaway reds proportionally instead of at the fixed 1680s that the two hand-raises had to keep chasing. Fixed-count gate batches carry per_unit_ms 0 (their count does not vary, so overhead IS the clamp) at their measured basis. Index 3 (wet corpora) stays a fixed overhead pending a wet-per-witness rate from the D2 probe — a declared calibration gap, not a hidden default. SIGNED CONSTANTS (operator, 2026-07-24): the witness aggregate coefficient 1000ms and the discovery overhead 300s; the per-WITNESS hard max is NOT redefined here — it stays the single fast-lane authority gunbc_ci_fast_lane_eval_budget_ms (5s). BASIS OF THE 1000ms COEFFICIENT (operator, 2026-07-24): the aggregate coefficient is denominated against the observed 0.58-0.70 s/witness (the 1344-1629s full-corpus fleet envelope over ~2316 witnesses) on the srv fleet host class (arm64 self-hosted, capped runners) at the adaptive governor's realized worker width. Naming host-class-x-worker-width as the basis makes a future width or fleet change a DELIBERATE re-sign of this constant (s/witness re-denominates when the fleet or width moves), never a rediscovered fleet-wide red; the ~1.4-1.7x headroom the 1000ms average carries over the observed top rate IS the >=~1.6x runaway the clamp is sized to catch, and sub-threshold creep below that ratio is owned by the gauntlet's per-cadence s/unit receipt (not this clamp). RAISE DISCIPLINE (carried from gunbc_ci_floor_batch_wall_budget_note): raising any overhead or rate requires an appended dated operator-signed line naming the run id and the enrollment that grew the batch; tightening may land by ordinary receipt note; unit counts need no signature (the schedule computes them). The clamp is interim mechanics — the structural wall is the complexity lens (§8, cost <= a + b*n asserted at compile time), and the clamp demotes to host-pathology backstop when that lens goes Blocking. The 55-min step cap stays the absolute backstop for the total floor wall; GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS lowers the COMPUTED clamp (min), never raises — the RED-control hook, never an escape hatch.OPERATOR-SIGNED RAISE (briansrls, 2026-07-25): batch 4 (row index [3]) 420 -> 540 seconds, +28.6 percent. THE MARGIN RULING THIS INSTANTIATES, signed once for the budget FAMILY rather than re-litigated per incident: a clamp was doing double duty as merge-refusal threshold ('this must not merge') and growth detector ('something got slower - look'). Tight margins serve the second job and demonstrably worked - the perturb row's 53 -> 141s growth was caught precisely because the clamp was tight - but they make the FIRST job fire on host roulette, and a breach that means 'you landed on srv2-02' trains the on-call to rerun, which is the crying-wolf failure mode wearing budget clothes. POLICY: clamps are sized to cover MEASURED fleet spread, so a breach means content grew, never which host answered. The growth-detector job moves to per-row trend receipts on the falsifier cadence (row grew >2x against its dated basis = a counted drift receipt), which is what makes a larger clamp margin safe - sensitivity is preserved at row grain while merge-refusal stops firing on variance. BASIS OF 540: post-re-home projection 377s (the worst of the two observed walls, 613864ms on srv2-02, minus the ~237s of eval the six re-homed rows carried) x 1.2 worst observed fleet spread = ~452s, + ~20 percent policy margin = 540 (whole-minute grain, 9min). RUN IDS: 30148859947 @ 619bba5 wall_ms=613864 units=74 host srv2-02; 30163496549 @ 9f87967 wall_ms=571556 units=74 host srv3-01 - identical content, 7.5 percent apart, which IS the spread this raise funds. THE ENROLLMENT THAT GREW THE BATCH: none - and as with the batch-3 raises, that mismatch with this discipline's template IS the receipt. Batch 4 SHRANK this cycle (six rows re-homed to FalsifierCadenceJob, units 74 -> 68); the raise buys spread coverage on a batch that got smaller, not headroom for growth. WHY THIS IS NOT THE FORBIDDEN WIDEN: batch 4's per_unit_ms is 0, which this note ALREADY declares 'a fixed overhead pending a wet-per-witness rate from the D2 probe - a declared calibration gap, not a hidden default'; 540 funds that gap with an honest, dated, dissolving interim instead of leaving the flat rate to fire on host variance. It refuses exactly as before - only the threshold moved, and it moved on a stated measurement, not to make a red go away. HOST SPREAD IS NOT WEATHER and has a named owner: srv1/srv2 still run the pre-#7213 sccache units and are typed expected-latent-defective until re-provisioned (the A1/reach gap), so part of the 7.5-20 percent collapses when the fleet lane re-converges them; this margin covers the genuine hardware-heterogeneity residue, eventually the machine-shape lane's. DISSOLVE-ON: the dag_compile_clean_perturb_receipts_holds diagnosis lands (141s, 38 percent of the remaining batch; if the growth is the cold-spawn / per-entry-reconcile class the wasted_ms=166525 reading supports, returning it toward its 53s basis puts worst case near 350s - under even the retired 420) AND the per-unit rate replaces the flat clamp from the D2 probe; then this row recalibrates by ordinary receipt note under the tightening rule. SAME DISEASE, NOT YET DOSED: gunbc_falsifier_self_host_wet_receipt_wall_budget (600s, the 707s falsifier red) rests on a ten-day-stale basis with zero spread allowance. The margin POLICY above governs it too, but the NUMBER is deliberately not moved here - it is gated on its own sccache-vs-growth attribution, so that dosing lands with that measurement rather than by analogy. WORKER COUNTER-SIGNATURE (claude, 2026-07-25, per this discipline's dated-line requirement): I ran the dissolve-on diagnosis and it REFUTES the mechanism this note hypothesised. The growth is NOT the cold-spawn / per-entry-reconcile class the wasted_ms=166525 reading suggested. Measured by execution on the perturb spawn shape (trivial 1-module compile, 4 source roots) there are TWO independent corpus-denominated costs, in different places. (A) THE CENSUS, inside reconcile: main.rs:491-514 reads every indexed module outside the closure off disk into census_only_sources, and v1_compiler_compile.rs:2580 parse_census_fill_sources parses all ~2543 of them BETWEEN compile.normalize.done and compile.reconcile.done - so it books to the reconcile bin and reads as 'reconcile is slow' when it is a whole-tree parse wearing reconcile's label. ~23s local, ~12-13s on fleet. (B) THE POLICY TAIL, after the compile: cli_run.rs:2105 compile_clean_unlisted_import_use_blocks_from_policy calls default_source_roots() (WHOLE TREE) + resolve_entry_graph_shared to evaluate ONE nullary Bool fn. ~34-42s, once per process, and INDEPENDENT of what was compiled - proven by running the same module with only its own source root: zero pool, all phases 0ms, still 42.4s wall. THE ASYMMETRY that closes the arithmetic: main.rs:566/598 exit(1) on hard diagnostics, so a RED compile never reaches the gate (measured 27.4s total, ~2s tail) while a GREEN compile pays it in full (57.8s total, ~34s tail). The perturb row makes 4 spawns, 2 red and 2 green: ~2x14.5s + ~2x60s = ~149s against the measured 148495.7ms. Pre-#7179 that same row is 4 spawns x census-only = ~52s, which IS the 53s basis. So cost B is the ENTIRE 53->148s regression (~94s of the 148s) and cost A was always present. CONSEQUENCE FOR THIS ROW: the note's projected 'near 350s' still holds directionally but by a different mechanism - fixing B alone returns the row to ~55s, so worst case lands well under even the retired 420 and the 540 becomes pure spread coverage, which is what this note already says it is for. NO BISECT WAS RUN AND NONE IS NEEDED: the three-run measurement attributes the split directly, so the bisect across #7178/#7179 named in the probe plan is superseded work (DESIGN 2 - redundant work is not free just because it would confirm). DISSOLVE-ON, unchanged in shape but now half-discharged: the diagnosis half is DONE (this line); recalibration still waits on the cost-B fix landing (scope the policy resolve to the policy module's own import closure) plus the D2 wet-per-witness rate replacing per_unit_ms 0 on this row. COST-B FIX LANDED (claude, 2026-08-23, lively-bat-222): the repair this line names is done, and the dissolve-on is discharged to its remaining half -- only the D2 wet-per-witness rate is still outstanding. compile_clean_unlisted_import_use_blocks_from_policy no longer resolves default_source_roots(); it assembles the policy entry's OWN import closure (three imports) and resolves that explicit source set, with every unresolvable-import and unreadable-file arm REFUSING by name rather than widening back to the whole tree -- the absorbing fallback is what would have restored the cost while zeroing its frequency. HOW IT RESURFACED, which is the part worth carrying: the cost was re-found from the other end as a floor line reading COMPLETED-OVER-COST-REQUIREMENT qualified_spelling_takes_the_shared_layer wall_ms=57337, against the 57.8s this note measured for exactly this tail. It presented as a qualified-name RESOLUTION defect because only a qualified spelling triggers it -- a qualified type annotation's authored name misses env.source_visible_names, which carries the BARE imported names, so the masked type-ref arm emits an advisory UnlistedImportUse and classifying that one diagnostic is what reaches this policy read; a bare annotation emits nothing and skips it. Qualified resolution itself is a map_get and costs nothing, and a qualified PATTERN HEAD never enters that arm at all. MEASURED, post-fix, same probe and process, only the caller roots varied: the cold arm 44886ms to 109ms, and the qualified/bare asymmetry is gone rather than reduced. NOT CLAIMED: that the floor's 1.22GB RSS is entirely this term -- the wall figures agree three ways but the memory line was never attributed by execution, and if it survives the fix that is a second defect to find rather than one to absorb into this row. Full method and the four orderings: docs/probes/qualified_name_resolution_cost_2026-08-23.md.ROW REMOVAL (claude, 2026-07-26, D3b gate flips): two rows DELETED from the tail, 7 -> 5, because their batches no longer exist — SourceRootIngestGate and SelfHostReadsRealBytesGate moved to the falsifier cadence (gunbc.commit_workflow gate_gauntlet_flip_basis_note), and each occupied a batch of its own. This is not a budget change: no surviving row's numbers moved, so no re-signature is owed under the raise discipline above. WHICH TWO, and why the obvious answer was wrong: the removed rows are the LAST two (600s and 420s), NOT the 120s/600s pair a reading of the schedule order suggests. Verified by execution rather than inferred — emit_host_gate_passes sits at batch index 4 BOTH before and after the flip, which is only possible if the two vanished batches were at indices 5 and 6, after it. The 120s row at index 4 is emit_host's and survives. RECORDED BECAUSE THE ALIGNMENT IS POSITIONAL AND HAND-MAINTAINED: an index-aligned list carries no evidence of which row belongs to which batch, so a plausible-but-wrong deletion here is silent — it mis-clamps a surviving batch and reads as a passing length check. The length witness (witness_floor_batch_clamp_params_cover_schedule) proves the COUNT and cannot prove the MAPPING; that gap is the standing argument for deriving these rows from the schedule rather than pairing them by position." data gunbc_ci_native_bundle_batch_clamp_basis_note: String = "NATIVE BUNDLE ROW (index 5, operator ruling via Dispatch A->C, 2026-08-02): the production selector enrollment adds one isolated fixed-count batch, so the positional clamp table must add exactly one companion row; co-locating it with an unrelated batch is rejected because that would obscure co-residency and the transition memory receipt. Measured acceptance basis: batch wall 77.020s for three selected members; cold compile 367209379ns, warm artifact lookup 8040ns, native execution 39332338ns, and interpreter oracle 17407839ns. The fixed 125s overhead is the observed whole-batch wall times the family's 1.6x fleet-spread factor, rounded up to a whole five seconds (1.62x measured), and per_unit_ms stays 0 because slice 1 has a fixed three-member population with an explicit next-tranche scaling trigger. Memory basis: RSS peak 4801241088 bytes and cgroup peak 11940978688 bytes. The Runnable remains Substantial and isolated, preserving the measured 7.14GB gap between process RSS and cgroup peak rather than disguising that footprint through co-residence. The clamp bounds wall admission; the memory governor remains the fail-closed capacity authority." diff --git a/docs/probes/qualified_name_resolution_cost_2026-08-23.md b/docs/probes/qualified_name_resolution_cost_2026-08-23.md index a67975e90f6..a1e6cf70363 100644 --- a/docs/probes/qualified_name_resolution_cost_2026-08-23.md +++ b/docs/probes/qualified_name_resolution_cost_2026-08-23.md @@ -196,6 +196,53 @@ what ran here. The wall figures agree (44.9s local cold vs 57.3s floor, and the note measured for exactly this tail), and the RSS shape is consistent with a whole-tree entry resolve, but "consistent with" is not "measured", and this document does not upgrade it. +## THE REPAIR — landed in this change, and re-measured + +`compile_clean_unlisted_import_use_blocks_from_policy` no longer calls `default_source_roots()`. +It assembles the policy entry's **own import closure** (`compile_clean_policy_entry_closure_sources`) +and resolves that explicit source set through `resolved_graph_from_sources(.., Strict)`. + +**Every failure arm refuses; none widens.** An import naming no module in the roots, or a file that +cannot be read, returns a located `Err` naming the module and the path. It must never fall back to +the whole tree: that arm would restore today's cost, zero the deficit's frequency by construction, +and make the widening unrankable ever after (DESIGN §5, the absorbing fallback). This is also why +the closure builder is a new function rather than a reuse of `resolve_virtual_source_with_imports`, +whose BFS *silently skips* an import it cannot resolve — a silent skip here would answer the policy +question from a graph missing the module the answer depends on. + +### Measured, same probe, same orderings, post-fix + +| roots | probe | pre-fix | post-fix | +|---|---|---|---| +| `dag` only | C (first qualified) | 44886ms | **109ms** | +| `dag` only | B (bare) | 12967ms | 12881ms | +| `dag` only | A (second qualified) | 6ms | 6ms | +| `dag` + `src/v2` | C (first qualified) | 216ms | **151ms** | +| `dag` + `src/v2` | A, D, B, E | 5-7ms | 6-8ms | + +All probes PASS, so the narrowed closure still resolves and still returns the policy `Bool` — the +repair removed work, not evidence. + +**The asymmetry is gone rather than reduced.** On the cold root set the qualified arm went from +3.5× the bare arm to *cheaper* than it (109ms against 12881ms, the bare arm now merely paying the +generic first-call warmup). There is no longer a qualified-spelling premium to attribute. + +### The residue this exposes — reported, not absorbed + +**`B` did not move: 12967ms → 12881ms on the `dag`-only root set.** That cost is paid by the *bare* +arm too, so it was never part of the qualified asymmetry and this repair does not touch it. It is a +separate cold-index term for a root set that does not match the process's warm index, and it is +named here rather than folded into this row's result — a fix that quietly widened its own claim to +cover a neighbouring cost would be the same conflation this document exists to undo. + +## The RSS claim is NOT upgraded by the repair + +The floor's 1.22GB was never attributed by execution and still is not. Three wall figures agree +(44.9s local cold, 57.3s floor, 57.8s in the 2026-07-25 note) and a whole-tree entry resolve is a +plausible shape for GB-scale growth, but plausible-shape is not measurement. If the memory line +survives on the floor after this repair, that is a **second defect to find**, not a result to absorb +into this one. + ## Apparatus — re-run recipe The probe module is reproduced here rather than left in the tree: it has no consumer, so as a diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 02f2dec47fa..088a2a12db1 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -3079,11 +3079,84 @@ const COMPILE_CLEAN_DIAGNOSTIC_POLICY_ENTRY: &str = "dag/gunbc/compile_clean_dia /// Whether `UnlistedImportUse` blocks compile-clean per the single policy row /// (`compile_clean_unlisted_import_use_enforcement` in `gunbc.compile_clean_diagnostic_policy`). /// Both the floor receipt path and the CLI transport must read this — never restate the predicate. +/// The policy entry's OWN import closure, as an explicit source set. +/// +/// Why this exists rather than a whole-tree resolve: reading one nullary `Bool` used to cost a +/// full resolve+typecheck over `default_source_roots()` — the whole tree — which is both +/// enormously oversized (the policy module has three imports) and, more seriously, a function +/// answering a question about the CALLER's world by consulting a DIFFERENT one. Measured, same +/// probe, only the caller's roots varied: 216ms when the caller's roots happened to match the +/// whole-tree key, 44886ms when they did not and the resolve paid a cold whole-tree load +/// (`docs/probes/qualified_name_resolution_cost_2026-08-23.md`). +/// +/// FAIL-CLOSED BY CONSTRUCTION (DESIGN §5): every arm that cannot produce the exact closure +/// REFUSES with a typed, located message naming the module and the file. It must never fall back +/// to the whole tree — that arm would restore today's cost, zero the deficit's frequency by +/// construction, and make the widening unrankable ever after. Note the contrast with +/// `resolve_virtual_source_with_imports`, whose BFS silently SKIPS an import it cannot resolve; +/// a silent skip here would narrow the policy closure and answer from a graph missing the very +/// module the answer depends on. +fn compile_clean_policy_entry_closure_sources( + roots: &[String], + entry_rel: &str, +) -> Result>, String> { + let ws = process_workspace_root(); + let module_index = build_module_path_index(roots); + let read = |rel: &str| -> Result { + let abs = ws.join(rel); + std::fs::read_to_string(&abs).map_err(|e| { + format!( + "compile_clean_diagnostic_policy closure: cannot read `{}` ({e})", + abs.display() + ) + }) + }; + + let entry_content = read(entry_rel)?; + let mut seen: HashMap> = HashMap::new(); + let mut queue: Vec = vec![entry_content.clone()]; + while let Some(content) = queue.pop() { + for module_path in extract_import_paths(&content) { + let Some(rel_path) = module_index.get(&module_path) else { + return Err(format!( + "compile_clean_diagnostic_policy closure: import `{module_path}` \ + (reached from `{entry_rel}`) names no module in the source roots" + )); + }; + // `entry_rel` may be absolute (an entry argument typically is) while the module + // index stores workspace-relative keys, so compare canonically — a string compare + // would miss and admit the entry twice under two spellings. + if same_canonical_file(rel_path, entry_rel) || seen.contains_key(rel_path) { + continue; + } + let file_content = read(rel_path)?; + seen.insert( + rel_path.clone(), + Rc::new(v1_compiler_compile::SourceFile { + path: rel_path.clone(), + content: file_content.clone(), + }), + ); + queue.push(file_content); + } + } + + let mut sources: Vec> = + seen.into_iter().map(|(_, v)| v).collect(); + sources.sort_by(|a, b| a.path.cmp(&b.path)); + sources.push(Rc::new(v1_compiler_compile::SourceFile { + path: entry_rel.to_string(), + content: entry_content, + })); + Ok(sources) +} + pub fn compile_clean_unlisted_import_use_blocks_from_policy() -> Result { let roots = default_source_roots(); let entry = resolve_entry_file_under_roots(&roots, COMPILE_CLEAN_DIAGNOSTIC_POLICY_ENTRY) .map_err(|e| format!("compile_clean_diagnostic_policy resolve: {e}"))?; - let (graph, indices) = resolve_entry_graph_shared(&roots, &entry) + let sources = compile_clean_policy_entry_closure_sources(&roots, &entry)?; + let (graph, indices) = resolved_graph_from_sources(sources, ResolveTypecheckGate::Strict) .map_err(|e| format!("compile_clean_diagnostic_policy resolve: {e}"))?; let ctx = make_eval_context(&graph, indices, v1_interpreter::ExecutionMode::Hermetic); match v1_interpreter::run_in_context_with_args(