From 4422638f57e50b5f360169ca94e856d12c42d226 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 11 Aug 2026 11:18:00 +0000 Subject: [PATCH 1/4] Make the witness-roster walk demand-directed instead of unconditional pre-plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit claim_executor ran discover_floor_witness_roster_with_snapshot once up front, BEFORE resolving the plan, on the stated ground that a naming violation should be "the cheapest possible failure". Measured, that walk is the most expensive phase in the process: 5.9 min of a 56.5-min ordinary floor (run 31477894666), and ~6 min of a ~15-min regen whose plan has exactly two nodes. It is expensive because "naming hygiene" is a misleading label. The four rules in v2.workflow.floor_naming_hygiene are string predicates over file paths and line prefixes, but the roster producer they are reached through also builds module-graph facts, runs a second strict reference-resolution pass, computes path indexes, and runs inert-lens reachability plus the construction- justification census. A two-node regen plan paid all of it to discover a roster it never reads. Hygiene is a property of the witness ROSTER, so it is now paid by the plans that have one: the walk moves to the existing `schedules_discovery` predicate, after the plan's batches settle. The roster is memoized by request digest (IN_PROCESS_ROSTER_BY_REQUEST), so plans that DO schedule discovery pay exactly what they paid before — the corpus batch hits the memo this call fills. Plans that do not schedule discovery pay nothing, and cannot be unhygienic: they have no roster. The walk-attempt id is minted unconditionally as before; it is a tracing coordinate every later phase stamps, and it is not the expensive part. This also closes the PRELUDE COVERAGE HOLE gunbc.ci_spec gunbc_ci_floor_batch_wall_budget_note already names: the walk sat outside every batch budget and could only red at the step cap. It is now inside the region the plan accounts for, or absent. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018ZrxLJrt8ASGLegiK72fAi --- src/v1/stage0/src/bin/claim_executor.rs | 85 ++++++++++++++----------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 6dbad8ae39c..98e59189a5b 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -10288,47 +10288,22 @@ fn run() -> Result { v1_compiler::cli_run::install_group_syntax(&source_roots); phase_mark("output-policy + group-syntax install"); - // Under the opt-in inversion the plan's DiscoveryBatches carry explicit entries - // only (or are absent entirely on an empty roster), and the explicit-only path - // skips the tree-walk naming hygiene (`test fn` outside `*_test.dag`, `__` - // basenames) that glob discovery used to run. A witness must stay NAMEABLE even - // when not enrolled (an unnameable witness could never be opted in), so the plan - // path always runs the fail-closed walk once up front — before the (expensive) - // plan evaluation, so a naming violation is the cheapest possible failure. - { - let excludes = v1_compiler::cli_run::witness_exclusion_substrings(); - let walk_attempt_id = match floor_walk_attempt_id() { - Ok(id) => id, - Err(msg) => { - eprintln!("claim_executor: witness naming hygiene walk-attempt refusal: {msg}"); - return Err(ExitCode::from(1)); - } - }; - if std::env::var("GUNBC_FLOOR_WALK_ATTEMPT_ID") - .map(|v| v.trim().is_empty()) - .unwrap_or(true) - { - std::env::set_var("GUNBC_FLOOR_WALK_ATTEMPT_ID", &walk_attempt_id); - } - let discovery_consumer = match floor_worker_role.as_ref() { - Some(FloorWorkerRole::Scoped { .. }) => floor_discovery_consumer_role_from_env(), - Some(FloorWorkerRole::Ordinary) | None => FloorDiscoveryConsumerRole::Producer, - }; - if let Err(msg) = discover_floor_witness_roster_with_snapshot( - &source_roots, - &[], - &excludes, - &[], - &walk_attempt_id, - discovery_consumer, - "Hermetic", - &source_roots, - ) { - eprintln!("claim_executor: witness naming hygiene (pre-plan walk): {msg}"); + // The walk-attempt id is a tracing coordinate every later phase stamps, so it is + // minted unconditionally here. The corpus WALK it used to gate is not: see the + // demand-directed hygiene walk after the plan's batches settle. + let floor_walk_attempt_id_value = match floor_walk_attempt_id() { + Ok(id) => id, + Err(msg) => { + eprintln!("claim_executor: witness naming hygiene walk-attempt refusal: {msg}"); return Err(ExitCode::from(1)); } + }; + if std::env::var("GUNBC_FLOOR_WALK_ATTEMPT_ID") + .map(|v| v.trim().is_empty()) + .unwrap_or(true) + { + std::env::set_var("GUNBC_FLOOR_WALK_ATTEMPT_ID", &floor_walk_attempt_id_value); } - phase_mark("naming-hygiene walk"); if perturb_check { return run_perturb_check(&source_roots, &plan_entry, &plan_function); @@ -10495,6 +10470,40 @@ fn run() -> Result { Runnable::DiscoveryBatch { .. } | Runnable::ScopedWitnessBatch { .. } ) }); + // Witness naming hygiene (`test fn` outside `*_test.dag`, `__` basenames) is a + // property of the witness ROSTER, so it is paid by the plans that have one. It ran + // unconditionally before plan evaluation until this change, on the stated ground + // that a naming violation should be "the cheapest possible failure"; measured, the + // walk is the most expensive phase in the process (5.9 min of a 56.5-min floor, + // ~6 min of a ~15-min regen), because the roster producer it calls builds + // module-graph facts, a second strict reference-resolution pass, inert-lens + // reachability and the construction-justification census. A two-node regen plan + // paid all of it to discover a roster it never reads. The roster is memoized by + // request digest (IN_PROCESS_ROSTER_BY_REQUEST), so plans that DO schedule + // discovery pay exactly what they paid before — the corpus batch hits the memo + // this call fills. Plans that do not schedule discovery now pay nothing, and + // cannot: they have no roster to be unhygienic about. + if schedules_discovery { + let excludes = v1_compiler::cli_run::witness_exclusion_substrings(); + let discovery_consumer = match floor_worker_role.as_ref() { + Some(FloorWorkerRole::Scoped { .. }) => floor_discovery_consumer_role_from_env(), + Some(FloorWorkerRole::Ordinary) | None => FloorDiscoveryConsumerRole::Producer, + }; + if let Err(msg) = discover_floor_witness_roster_with_snapshot( + &source_roots, + &[], + &excludes, + &[], + &floor_walk_attempt_id_value, + discovery_consumer, + "Hermetic", + &source_roots, + ) { + eprintln!("claim_executor: witness naming hygiene (roster walk): {msg}"); + return Err(ExitCode::from(1)); + } + } + phase_mark("naming-hygiene walk"); let fast_lane_eval_budget_ms: Option = if schedules_discovery { match run_value(&plan_ctx, "gunbc_ci_fast_lane_eval_budget_ms") { Ok(Value::Int(n)) if n > 0 => Some(n as u64), From ff43f423e0066569fa88dc94415339210f8576b0 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 11 Aug 2026 11:33:11 +0000 Subject: [PATCH 2/4] Update the design authority for the demand-directed hygiene walk (review 51099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 51099 (cursor/composer-2.5, REQUEST_CHANGES) correctly caught that #8140 changed documented CI behavior without updating its authority. DESIGN.md Building & checks stated the opposite of the new code: "the executor runs the zero-enrollment naming walk when no discovery batch is scheduled" That clause is superseded here in gunbc.design_document (DESIGN.md is generated from it; the heal job regenerates the projection). Both of the reviewer's findings are recorded rather than only the first: (a) SCOPE — discovery-free plans no longer run the corpus-wide nameability rules. Declared as a narrowing, with the honest coverage argument: every PR runs the `ci` job, whose plan schedules discovery and walks the full tree, so per-PR coverage is unchanged; what is deleted is a second redundant walk on regen-only and plan-artifact-only runs. Explicitly NOT backstopped by the affected-set falsifier, which has produced no green verdict since 2026-08-03. (b) ORDERING — for plans that DO schedule discovery the walk now runs after plan resolve/eval, so a naming violation pays ~0.5 min of plan resolution before refusing. The reviewer asked whether the trade is intentional: it is, and it is priced — ~0.5 min later on the refusing path against ~6 min saved on every regen. A dissolve-on is recorded: the clause, the separate walk, and the `__`-basename rule all retire when the placement rules move to canonical source ingestion and test identities derive from parser-produced declarations. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018ZrxLJrt8ASGLegiK72fAi --- dag/gunbc/design_document.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/gunbc/design_document.dag b/dag/gunbc/design_document.dag index c64a93e9839..52f7591be90 100644 --- a/dag/gunbc/design_document.dag +++ b/dag/gunbc/design_document.dag @@ -229,7 +229,7 @@ fn building_checks_blocks() -> List { li(text: "`cargo test --workspace` · `cargo clippy --all-targets -- -D warnings` · `cargo fmt --all --check`"), li(text: "one-time per clone: `git config core.hooksPath .githooks` — the only documented manual seed; generated pre-commit/pre-push hooks then idempotently converge `merge.generated-artifact.driver` and re-assert `core.hooksPath` via argv derived from `gunbc.repo_local_git_config` (clones that skip hooksPath degrade to vanilla text-merge for generated-artifact paths; drift gate still guards at CI). The driver REFUSES rather than answering `true`: git reaches a low-level merge driver only when both sides changed the path since the merge base — measured on a four-case matrix, one-sided and identical changes never reach it — and taking the ours side there dropped the other side's authority-derived bytes with no conflict, twice on #7836 against the stage0 seed. It now leaves the ours side in the worktree with no conflict markers, marks the path unmerged, and prints the regeneration recipe; the class is mechanically preventable, not structural, and its next-rung trigger is the commit-writer binding rows in `gunbc.commit_workflow`"), li(text: "explicit actuator (CI / tooling): `gunbc run --source-root dag --source-root src/v2 --entry dag/gunbc/repo_local_git_config.dag --function converge`"), - li(text: "CI (`gunbc ci` generates `.github/workflows/ci.yml`; all `v1-compiler` seed bins, no shell gate): **one** composed floor pass — `claim_executor --source-root src/v2 --source-root dag --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_ci_floor_plan`. The v2 scheduler decides the batches from the single-authority spec (`gunbc.ci_spec`); the only structural fact the plan adds is *compile-clean gates the rest* (batch-1 `dag_compile_clean_gate` → batch-2 everything else, a dependency edge). Witness enrollment: **discovery shrunk by the affected set** (operator acceptance 2026-07-09, firing the 2026-07-04 opt-in inversion's dissolve-on — see `gunbc.ci_spec` `ci_spec_discovery_flip_note`): the corpus batch scans `CiSpec.discovery_scan_dirs` (plus the source-root `*_test.dag` walk) with `SelectionApplied`, so a PR runs the tree-wide witness corpus (~1,721 rows at flip time) shrunk to the diff's affected set; selection is fail-closed (a provenance gap refuses, never widens; host-scaffold/live-tree rows never predict-skip) and the scheduled `affected-set-falsifier` (every 4 hours) runs the corpus cold with predictions recorded, so a missing selection edge surfaces as a counted divergence within one cadence window. `CiSpec.witness_entries` (`CommitWitnessClaim` rows on the `GithubActionsCiJob` surface, projected by `project_ci_floor_witness_entries`) remains the explicit-entry roster — execution-kind rows with their own resource profile, and any row that must run as declared. Empty entries AND empty discovery dirs = zero witness-corpus nodes (the regen spec's shape). Naming hygiene stays fail-closed (a `test fn` outside `*_test.dag` is still a violation — the executor runs the zero-enrollment naming walk when no discovery batch is scheduled), and tree-wide unselected discovery remains the **local** path (`claim_batch --roster-from-discovery --source-root dag --source-root src/v2 --scan-dir dag/test/claim --scan-dir src/v2/test/claim/manual`). Batch-2 also carries the effectful gates: the rust fmt gate (when a `.rs` changes; nextest was removed from CI 2026-07-11 — operator ruling recorded in `gunbc.commit_workflow` `commit_gate_rust_suite_removed_disposition`, the suite runs locally only; clippy removed from CI 2026-07-08 — crate-wide `#![allow(clippy::all)]` made it zero-signal over ~44m/run, still available as a local dev check), emit-host MVP smokes, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`, and its per-PR scope is the same import-closure authority (2026-07-16, channel 2 of the 2026-07-10 grain fork): an all-`.dag`/docs diff compiles only the affected shard-entry closures (`tools.dag_compile_clean_scope`, host fast path `entry_file_touched_via_import_closure`); any non-selectable touched path (`.rs`, workflow yml, manifests), any non-docs departed path, or any selection refusal keeps the whole-tree baseline, loudly; the falsifier cadence carries the deterministic whole-tree cold control (`GUNBC_CI_COMPILE_CLEAN_COLD_CONTROL=1`, widen-only). The regen step (self-host fixed-point) is scoped the same way at its own closure (#6732 + this change's departed-path guard): `regen_floor_skip_witness` skips the pull_request step only when a non-empty merge-base diff is provably disjoint from the regen input set (`cli_run::regen_input_sources` — the SAME closure authority `regen_stage0` compiles — plus the src/v1/** prefix and Cargo/toolchain config); empty diffs, departed non-docs paths, and every failure arm run regen, and the skip is shell-gated to pull_request events so main pushes stay the unconditional cold control. Parse is grammar-owned: `ingest_yaml_source` (`dag/extdeps/languages/yaml/ingest.dag`), no shell/host parser."), + li(text: "CI (`gunbc ci` generates `.github/workflows/ci.yml`; all `v1-compiler` seed bins, no shell gate): **one** composed floor pass — `claim_executor --source-root src/v2 --source-root dag --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_ci_floor_plan`. The v2 scheduler decides the batches from the single-authority spec (`gunbc.ci_spec`); the only structural fact the plan adds is *compile-clean gates the rest* (batch-1 `dag_compile_clean_gate` → batch-2 everything else, a dependency edge). Witness enrollment: **discovery shrunk by the affected set** (operator acceptance 2026-07-09, firing the 2026-07-04 opt-in inversion's dissolve-on — see `gunbc.ci_spec` `ci_spec_discovery_flip_note`): the corpus batch scans `CiSpec.discovery_scan_dirs` (plus the source-root `*_test.dag` walk) with `SelectionApplied`, so a PR runs the tree-wide witness corpus (~1,721 rows at flip time) shrunk to the diff's affected set; selection is fail-closed (a provenance gap refuses, never widens; host-scaffold/live-tree rows never predict-skip) and the scheduled `affected-set-falsifier` (every 4 hours) runs the corpus cold with predictions recorded, so a missing selection edge surfaces as a counted divergence within one cadence window. `CiSpec.witness_entries` (`CommitWitnessClaim` rows on the `GithubActionsCiJob` surface, projected by `project_ci_floor_witness_entries`) remains the explicit-entry roster — execution-kind rows with their own resource profile, and any row that must run as declared. Empty entries AND empty discovery dirs = zero witness-corpus nodes (the regen spec's shape). Naming hygiene stays fail-closed (a `test fn` outside `*_test.dag` is still a violation) but is **demand-directed, not unconditional** (#8140, 2026-08-11, superseding the prior clause 'the executor runs the zero-enrollment naming walk when no discovery batch is scheduled'): `claim_executor` ran the whole-source-root roster walk BEFORE resolving the plan on every invocation, on the stated ground that a naming violation should be 'the cheapest possible failure'; measured, it was the most expensive phase in the process — 5.9 min of a 56.5-min ordinary floor (run 31477894666) and ~6 min of a ~15-min regen whose plan has two nodes and no roster — because the producer it reaches also builds module-graph facts, runs a second strict reference-resolution pass, and runs inert-lens reachability plus the construction-justification census. The walk now runs where the demand is structural (`schedules_discovery`: the plan carries a discovery or scoped-witness batch), at unchanged whole-source-root scope and unchanged cost — the roster is memoized by request digest, so the corpus batch hits the memo this call fills, and the coordinator's expected pre-plan digest is byte-identical. TWO CONSEQUENCES, both deliberate. (a) SCOPE: discovery-free plans (regen, plan-artifact) no longer run the corpus-wide nameability rules at all. Per-PR coverage is unchanged because every PR runs the `ci` job, whose plan schedules discovery and walks the full tree; what is lost is a *second* redundant walk on regen-only and plan-artifact-only runs. This is a scope narrowing declared here rather than silently taken, and it is NOT backstopped by the affected-set falsifier — that cadence has produced no green verdict since 2026-08-03. (b) ORDERING: for plans that DO schedule discovery the walk now runs after plan resolve/eval, since batches are known only then, so a naming violation pays full plan resolution (~0.5 min) before refusing. The 'cheapest possible failure' ordering is knowingly traded for deleting the same walk from every plan that never needed it; the trade is ~0.5 min later on the refusing path against ~6 min saved on every regen. Dissolve-on: the placement rules move to canonical source ingestion and test identities derive from parser-produced declarations, at which point the separate walk, this clause, and the `__`-basename rule all retire. Tree-wide unselected discovery remains the **local** path (`claim_batch --roster-from-discovery --source-root dag --source-root src/v2 --scan-dir dag/test/claim --scan-dir src/v2/test/claim/manual`). Batch-2 also carries the effectful gates: the rust fmt gate (when a `.rs` changes; nextest was removed from CI 2026-07-11 — operator ruling recorded in `gunbc.commit_workflow` `commit_gate_rust_suite_removed_disposition`, the suite runs locally only; clippy removed from CI 2026-07-08 — crate-wide `#![allow(clippy::all)]` made it zero-signal over ~44m/run, still available as a local dev check), emit-host MVP smokes, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`, and its per-PR scope is the same import-closure authority (2026-07-16, channel 2 of the 2026-07-10 grain fork): an all-`.dag`/docs diff compiles only the affected shard-entry closures (`tools.dag_compile_clean_scope`, host fast path `entry_file_touched_via_import_closure`); any non-selectable touched path (`.rs`, workflow yml, manifests), any non-docs departed path, or any selection refusal keeps the whole-tree baseline, loudly; the falsifier cadence carries the deterministic whole-tree cold control (`GUNBC_CI_COMPILE_CLEAN_COLD_CONTROL=1`, widen-only). The regen step (self-host fixed-point) is scoped the same way at its own closure (#6732 + this change's departed-path guard): `regen_floor_skip_witness` skips the pull_request step only when a non-empty merge-base diff is provably disjoint from the regen input set (`cli_run::regen_input_sources` — the SAME closure authority `regen_stage0` compiles — plus the src/v1/** prefix and Cargo/toolchain config); empty diffs, departed non-docs paths, and every failure arm run regen, and the skip is shell-gated to pull_request events so main pushes stay the unconditional cold control. Parse is grammar-owned: `ingest_yaml_source` (`dag/extdeps/languages/yaml/ingest.dag`), no shell/host parser."), ]), ] } From dfefcc4c6f81403afc8864de495bb594dcf6d04b Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 11 Aug 2026 11:40:20 +0000 Subject: [PATCH 3/4] chore: regenerate drifted generated artifacts (ci auto-heal) --- DESIGN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESIGN.md b/DESIGN.md index e64bda94b2a..2bf6c59f6c8 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -174,4 +174,4 @@ hollow alias (minimality ≠ grounding) · state-space conflation (an `Option`/` - `cargo test --workspace` · `cargo clippy --all-targets -- -D warnings` · `cargo fmt --all --check` - one-time per clone: `git config core.hooksPath .githooks` — the only documented manual seed; generated pre-commit/pre-push hooks then idempotently converge `merge.generated-artifact.driver` and re-assert `core.hooksPath` via argv derived from `gunbc.repo_local_git_config` (clones that skip hooksPath degrade to vanilla text-merge for generated-artifact paths; drift gate still guards at CI). The driver REFUSES rather than answering `true`: git reaches a low-level merge driver only when both sides changed the path since the merge base — measured on a four-case matrix, one-sided and identical changes never reach it — and taking the ours side there dropped the other side's authority-derived bytes with no conflict, twice on #7836 against the stage0 seed. It now leaves the ours side in the worktree with no conflict markers, marks the path unmerged, and prints the regeneration recipe; the class is mechanically preventable, not structural, and its next-rung trigger is the commit-writer binding rows in `gunbc.commit_workflow` - explicit actuator (CI / tooling): `gunbc run --source-root dag --source-root src/v2 --entry dag/gunbc/repo_local_git_config.dag --function converge` -- CI (`gunbc ci` generates `.github/workflows/ci.yml`; all `v1-compiler` seed bins, no shell gate): **one** composed floor pass — `claim_executor --source-root src/v2 --source-root dag --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_ci_floor_plan`. The v2 scheduler decides the batches from the single-authority spec (`gunbc.ci_spec`); the only structural fact the plan adds is *compile-clean gates the rest* (batch-1 `dag_compile_clean_gate` → batch-2 everything else, a dependency edge). Witness enrollment: **discovery shrunk by the affected set** (operator acceptance 2026-07-09, firing the 2026-07-04 opt-in inversion's dissolve-on — see `gunbc.ci_spec` `ci_spec_discovery_flip_note`): the corpus batch scans `CiSpec.discovery_scan_dirs` (plus the source-root `*_test.dag` walk) with `SelectionApplied`, so a PR runs the tree-wide witness corpus (~1,721 rows at flip time) shrunk to the diff's affected set; selection is fail-closed (a provenance gap refuses, never widens; host-scaffold/live-tree rows never predict-skip) and the scheduled `affected-set-falsifier` (every 4 hours) runs the corpus cold with predictions recorded, so a missing selection edge surfaces as a counted divergence within one cadence window. `CiSpec.witness_entries` (`CommitWitnessClaim` rows on the `GithubActionsCiJob` surface, projected by `project_ci_floor_witness_entries`) remains the explicit-entry roster — execution-kind rows with their own resource profile, and any row that must run as declared. Empty entries AND empty discovery dirs = zero witness-corpus nodes (the regen spec's shape). Naming hygiene stays fail-closed (a `test fn` outside `*_test.dag` is still a violation — the executor runs the zero-enrollment naming walk when no discovery batch is scheduled), and tree-wide unselected discovery remains the **local** path (`claim_batch --roster-from-discovery --source-root dag --source-root src/v2 --scan-dir dag/test/claim --scan-dir src/v2/test/claim/manual`). Batch-2 also carries the effectful gates: the rust fmt gate (when a `.rs` changes; nextest was removed from CI 2026-07-11 — operator ruling recorded in `gunbc.commit_workflow` `commit_gate_rust_suite_removed_disposition`, the suite runs locally only; clippy removed from CI 2026-07-08 — crate-wide `#![allow(clippy::all)]` made it zero-signal over ~44m/run, still available as a local dev check), emit-host MVP smokes, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`, and its per-PR scope is the same import-closure authority (2026-07-16, channel 2 of the 2026-07-10 grain fork): an all-`.dag`/docs diff compiles only the affected shard-entry closures (`tools.dag_compile_clean_scope`, host fast path `entry_file_touched_via_import_closure`); any non-selectable touched path (`.rs`, workflow yml, manifests), any non-docs departed path, or any selection refusal keeps the whole-tree baseline, loudly; the falsifier cadence carries the deterministic whole-tree cold control (`GUNBC_CI_COMPILE_CLEAN_COLD_CONTROL=1`, widen-only). The regen step (self-host fixed-point) is scoped the same way at its own closure (#6732 + this change's departed-path guard): `regen_floor_skip_witness` skips the pull_request step only when a non-empty merge-base diff is provably disjoint from the regen input set (`cli_run::regen_input_sources` — the SAME closure authority `regen_stage0` compiles — plus the src/v1/** prefix and Cargo/toolchain config); empty diffs, departed non-docs paths, and every failure arm run regen, and the skip is shell-gated to pull_request events so main pushes stay the unconditional cold control. Parse is grammar-owned: `ingest_yaml_source` (`dag/extdeps/languages/yaml/ingest.dag`), no shell/host parser. +- CI (`gunbc ci` generates `.github/workflows/ci.yml`; all `v1-compiler` seed bins, no shell gate): **one** composed floor pass — `claim_executor --source-root src/v2 --source-root dag --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_ci_floor_plan`. The v2 scheduler decides the batches from the single-authority spec (`gunbc.ci_spec`); the only structural fact the plan adds is *compile-clean gates the rest* (batch-1 `dag_compile_clean_gate` → batch-2 everything else, a dependency edge). Witness enrollment: **discovery shrunk by the affected set** (operator acceptance 2026-07-09, firing the 2026-07-04 opt-in inversion's dissolve-on — see `gunbc.ci_spec` `ci_spec_discovery_flip_note`): the corpus batch scans `CiSpec.discovery_scan_dirs` (plus the source-root `*_test.dag` walk) with `SelectionApplied`, so a PR runs the tree-wide witness corpus (~1,721 rows at flip time) shrunk to the diff's affected set; selection is fail-closed (a provenance gap refuses, never widens; host-scaffold/live-tree rows never predict-skip) and the scheduled `affected-set-falsifier` (every 4 hours) runs the corpus cold with predictions recorded, so a missing selection edge surfaces as a counted divergence within one cadence window. `CiSpec.witness_entries` (`CommitWitnessClaim` rows on the `GithubActionsCiJob` surface, projected by `project_ci_floor_witness_entries`) remains the explicit-entry roster — execution-kind rows with their own resource profile, and any row that must run as declared. Empty entries AND empty discovery dirs = zero witness-corpus nodes (the regen spec's shape). Naming hygiene stays fail-closed (a `test fn` outside `*_test.dag` is still a violation) but is **demand-directed, not unconditional** (#8140, 2026-08-11, superseding the prior clause 'the executor runs the zero-enrollment naming walk when no discovery batch is scheduled'): `claim_executor` ran the whole-source-root roster walk BEFORE resolving the plan on every invocation, on the stated ground that a naming violation should be 'the cheapest possible failure'; measured, it was the most expensive phase in the process — 5.9 min of a 56.5-min ordinary floor (run 31477894666) and ~6 min of a ~15-min regen whose plan has two nodes and no roster — because the producer it reaches also builds module-graph facts, runs a second strict reference-resolution pass, and runs inert-lens reachability plus the construction-justification census. The walk now runs where the demand is structural (`schedules_discovery`: the plan carries a discovery or scoped-witness batch), at unchanged whole-source-root scope and unchanged cost — the roster is memoized by request digest, so the corpus batch hits the memo this call fills, and the coordinator's expected pre-plan digest is byte-identical. TWO CONSEQUENCES, both deliberate. (a) SCOPE: discovery-free plans (regen, plan-artifact) no longer run the corpus-wide nameability rules at all. Per-PR coverage is unchanged because every PR runs the `ci` job, whose plan schedules discovery and walks the full tree; what is lost is a *second* redundant walk on regen-only and plan-artifact-only runs. This is a scope narrowing declared here rather than silently taken, and it is NOT backstopped by the affected-set falsifier — that cadence has produced no green verdict since 2026-08-03. (b) ORDERING: for plans that DO schedule discovery the walk now runs after plan resolve/eval, since batches are known only then, so a naming violation pays full plan resolution (~0.5 min) before refusing. The 'cheapest possible failure' ordering is knowingly traded for deleting the same walk from every plan that never needed it; the trade is ~0.5 min later on the refusing path against ~6 min saved on every regen. Dissolve-on: the placement rules move to canonical source ingestion and test identities derive from parser-produced declarations, at which point the separate walk, this clause, and the `__`-basename rule all retire. Tree-wide unselected discovery remains the **local** path (`claim_batch --roster-from-discovery --source-root dag --source-root src/v2 --scan-dir dag/test/claim --scan-dir src/v2/test/claim/manual`). Batch-2 also carries the effectful gates: the rust fmt gate (when a `.rs` changes; nextest was removed from CI 2026-07-11 — operator ruling recorded in `gunbc.commit_workflow` `commit_gate_rust_suite_removed_disposition`, the suite runs locally only; clippy removed from CI 2026-07-08 — crate-wide `#![allow(clippy::all)]` made it zero-signal over ~44m/run, still available as a local dev check), emit-host MVP smokes, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`, and its per-PR scope is the same import-closure authority (2026-07-16, channel 2 of the 2026-07-10 grain fork): an all-`.dag`/docs diff compiles only the affected shard-entry closures (`tools.dag_compile_clean_scope`, host fast path `entry_file_touched_via_import_closure`); any non-selectable touched path (`.rs`, workflow yml, manifests), any non-docs departed path, or any selection refusal keeps the whole-tree baseline, loudly; the falsifier cadence carries the deterministic whole-tree cold control (`GUNBC_CI_COMPILE_CLEAN_COLD_CONTROL=1`, widen-only). The regen step (self-host fixed-point) is scoped the same way at its own closure (#6732 + this change's departed-path guard): `regen_floor_skip_witness` skips the pull_request step only when a non-empty merge-base diff is provably disjoint from the regen input set (`cli_run::regen_input_sources` — the SAME closure authority `regen_stage0` compiles — plus the src/v1/** prefix and Cargo/toolchain config); empty diffs, departed non-docs paths, and every failure arm run regen, and the skip is shell-gated to pull_request events so main pushes stay the unconditional cold control. Parse is grammar-owned: `ingest_yaml_source` (`dag/extdeps/languages/yaml/ingest.dag`), no shell/host parser. From b6c50f2ce739d15358598622407bb211914b2c8e Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 11 Aug 2026 11:50:12 +0000 Subject: [PATCH 4/4] Repair the stale output-policy comment left by the walk move (review 51101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 51101 (cursor/composer-2.5) caught claim_executor.rs:10283 still asserting "The walk still runs before plan evaluation, so a naming violation stays the cheapest failure" — the exact opposite of what this PR does. Non-blocking as a defect, but it is the same class the PR itself is about: a comment standing as authority for behavior the code no longer has. #8140's own receipt was a block comment whose stated premise had been false for months. The paragraph's real subject — install output policy BEFORE the walk so the whole-tree read is funnelled rather than emitting ~2.3k `[file] read` lines — is unchanged and still correct; the walk simply moved further away from it. Rewritten to say that, rather than deleted, so the ordering requirement keeps its rationale. Swept the rest of claim_executor.rs / cli_run.rs for other assertions of the old ordering: the only remaining hits are this PR's own comment describing the prior behavior in the past tense. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018ZrxLJrt8ASGLegiK72fAi --- src/v1/stage0/src/bin/claim_executor.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 98e59189a5b..84f97413699 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -10274,13 +10274,15 @@ fn run() -> Result { }; // Install the host-effect trace policy from the .dag authority FIRST — before the - // naming-hygiene walk below and every subsequent corpus read — so `[file] read` / + // naming-hygiene walk and every subsequent corpus read — so `[file] read` / // `[rest]` / `[hermetic:mock]` etc. are funnelled per `gunbc.output_policy` // (Instrumentation is Suppressed at Normal, the CI default) instead of flooding the // floor log. Installing AFTER the walk (the prior order) left the walk's whole-tree // read at the `Full` default — ~2.3k `[file] read` lines, the firehose the - // observation-emit census (`gunbc.observation_emit_census`) targets. The walk still - // runs before plan evaluation, so a naming violation stays the cheapest failure. + // observation-emit census (`gunbc.observation_emit_census`) targets. That ordering + // requirement is unchanged by #8140: the walk moved AFTER plan evaluation (it is now + // demand-directed on `schedules_discovery`), so this install precedes it by even + // more, and the walk's whole-tree read is still policy-funnelled. v1_compiler::cli_run::install_output_policy(&source_roots); // Install the per-target group-marker syntax (GitHub Actions `::group::` vs a // plain-terminal header) from the .dag authority, so the parallel walk folds each