From 5ad0278a450254b1b832fbc528eb787eb0d057bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 01:23:53 +0000 Subject: [PATCH 01/15] CI witness opt-in inversion: zero witnesses by default, enrollment by declared roster row Operator decision 2026-07-04: the tree-wide glob roster (543 files / ~1,551 test fns, growing quadratically - a witness is definable between any two pipeline segments) pushed both CI jobs to the 90-min timeout: maximum cost, zero delivered signal, every merge effectively fail-open. Existence no longer enrolls a witness in CI. - CiSpec.witness_entries: the opt-in roster, projected from CommitWitnessClaim rows on the GithubActionsCiJob surface of commit_gate_roster (the existing enrollment authority - no new mechanism). Empty today. - ci_floor_plan: an empty roster puts NO witness-corpus node in the plan (structural omission, not an empty glob - the executor's empty-corpus fail-closed diagnostic stays meaningful); a non-empty roster becomes explicit-entries discovery batches (scan_dirs/discovery_scope_dirs empty, so the glob path is unreachable from CI), partitioned corpus vs execution by path prefix, still compile-gated and heavy-serialized. The dedicated grounding whole-tree node dissolved into the roster. - claim_executor: when a plan carries no DiscoveryBatch, run the naming- hygiene tree walk (test fn outside *_test.dag, __ basenames) once, fail-closed - a witness must stay nameable even when not enrolled. - Witness tests inverted: live plan asserts discovery tracks the roster (zero today); the enrollment machinery is proven on synthetic rosters (presence, partition, gating after compile, heavy serialization, exclusion edges) with red discrimination. - Rust lane, same inversion: the two whole-tree tests (func_env whole-tree ptr-count, whole-corpus semantic oracle) are now #[ignore] with written reasons per the #5427 discipline; run via -- --ignored. - Found and fixed two files the v1 interpreter grammar could not parse (top-level // comments): affected_set_floor_runner.dag - which made resolve_floor_runner_context fail and silently disabled the affected-set skip, falling back to full corpus on every floor run - and phase_profile_proof_plan.dag. Comments converted to data-string markers. - Local tree-wide discovery unchanged (claim_batch --roster-from-discovery). - Dissolve-on: affected-set selection + floor memoization make per-PR selection-by-affectedness affordable; enrollment returns to discovery shrunk by the affected set. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- dag/gunbc/ci_spec.dag | 20 +- dag/gunbc/ci_workflow.dag | 3 +- dag/gunbc/commit_workflow.dag | 22 ++- dag/gunbc/design_document.dag | 2 +- .../plans/ci_selection_vs_scheduling.dag | 2 + .../plans/mechanism-inventory-red-controls.md | 4 +- src/v1/stage0/src/bin/bootstrap_witness.rs | 36 +++- src/v1/stage0/src/bin/claim_executor.rs | 26 +++ src/v1/stage0/src/bin/diagnostics_witness.rs | 38 ++-- .../src/bin/effects_rest_transport_witness.rs | 4 +- src/v1/stage0/src/cli_run.rs | 91 +++++---- .../stage0/src/v1_compiler_infer_patterns.rs | 24 +-- src/v1/stage0/src/v1_interpreter.rs | 2 +- src/v1/tests/src/func_env_scope_chain_test.rs | 1 + .../src/func_env_semantic_equivalence_test.rs | 1 + .../tests/src/interp_wire_serialize_test.rs | 2 +- .../test/claim/ci_floor_plan_witness_test.dag | 181 ++++++++++++------ src/v2/test/claim/ci_spec_witness_test.dag | 1 + src/v2/workflow/affected_set_floor_runner.dag | 7 +- src/v2/workflow/ci_floor_plan.dag | 144 +++++++------- src/v2/workflow/phase_profile_proof_plan.dag | 7 +- 21 files changed, 399 insertions(+), 219 deletions(-) diff --git a/dag/gunbc/ci_spec.dag b/dag/gunbc/ci_spec.dag index 126b064a526..6dc16761ba1 100644 --- a/dag/gunbc/ci_spec.dag +++ b/dag/gunbc/ci_spec.dag @@ -13,8 +13,10 @@ import gunbc.ci_layer_roots { import gunbc.commit_workflow { commit_gate_roster, project_ci_floor_gates, - project_ci_rust_job_gates + project_ci_rust_job_gates, + project_ci_floor_witness_entries } +import std.realization_schedule { ScheduleWitnessEntry } import gunbc.ci_failure_class { infra_retry_grep_alternation } import gunbc.ci_deploy_access { DeployAccess, ci_deploy_srv1_access } import gunbc.ci_deploy_target_host { gunbc_ci_deploy_srv1_runner_spec } @@ -39,8 +41,11 @@ type DeployStage { runner: RunnerSpec } +data ci_spec_witness_entries_note: String = "witness_entries is the opt-in CI witness roster (operator inversion 2026-07-04): the floor runs exactly these declared witnesses, never a tree scan; empty means the plan carries no witness-corpus node at all. Enrollment authority: gunbc.commit_workflow.commit_gate_roster CommitWitnessClaim rows on the GithubActionsCiJob surface (see ci_witness_optin_inversion there)." + type CiSpec { gates: List + witness_entries: List diff_policy: DiffPolicy notice_title: String deploy_stages: List @@ -48,6 +53,8 @@ type CiSpec { data gunbc_ci_floor_gates: List = project_ci_floor_gates(roster: commit_gate_roster) +data gunbc_ci_floor_witness_entries: List = project_ci_floor_witness_entries(roster: commit_gate_roster) + data gunbc_ci_rust_job_gates: List = project_ci_rust_job_gates(roster: commit_gate_roster) data gunbc_ci_gates: List = concat(gunbc_ci_floor_gates, gunbc_ci_rust_job_gates) @@ -69,6 +76,7 @@ data gunbc_ci_deploy_stages: List = [gunbc_ci_deploy_srv1_stage] data gunbc_ci_spec: CiSpec = { gates: gunbc_ci_floor_gates, + witness_entries: gunbc_ci_floor_witness_entries, diff_policy: { base: "origin/main", head: "HEAD", @@ -78,6 +86,16 @@ data gunbc_ci_spec: CiSpec = { deploy_stages: gunbc_ci_deploy_stages, } +fn ci_spec_with_witness_entries(spec: CiSpec, entries: List) -> CiSpec { + CiSpec { + gates: spec.gates, + witness_entries: entries, + diff_policy: spec.diff_policy, + notice_title: spec.notice_title, + deploy_stages: spec.deploy_stages, + } +} + data ci_release_features: String = "text_lookup_work_counter" fn ci_repo_root_shell() -> String { diff --git a/dag/gunbc/ci_workflow.dag b/dag/gunbc/ci_workflow.dag index 5036abc79e5..728803c2f09 100644 --- a/dag/gunbc/ci_workflow.dag +++ b/dag/gunbc/ci_workflow.dag @@ -12,7 +12,7 @@ import gunbc.merge_admission_produce { ci_merge_admission_gate_script, } import gunbc.ci_workflow_expressions { ci_deploy_push_to_main_if } -import gunbc.commit_workflow { commit_gate_roster, project_ci_floor_gates } +import gunbc.commit_workflow { commit_gate_roster, project_ci_floor_gates, project_ci_floor_witness_entries } import gunbc.runner_spec_from_offer { gunbc_ci_runner_spec } import std.disposition { Disposition, Scaffold, SingleAuthority, Terminal } import std.decl_ref { DeclarationRef, WholeDeclaration } @@ -145,6 +145,7 @@ fn ci_prelude_steps() -> List { fn ci_floor_spec_from_roster() -> CiSpec { { gates: project_ci_floor_gates(roster: commit_gate_roster), + witness_entries: project_ci_floor_witness_entries(roster: commit_gate_roster), diff_policy: gunbc_ci_spec.diff_policy, notice_title: gunbc_ci_spec.notice_title, deploy_stages: [], diff --git a/dag/gunbc/commit_workflow.dag b/dag/gunbc/commit_workflow.dag index 8ca0e43649f..c23dcb5d9b7 100644 --- a/dag/gunbc/commit_workflow.dag +++ b/dag/gunbc/commit_workflow.dag @@ -9,7 +9,7 @@ import gunbc.ci_gate { import gunbc.generated_artifact { GeneratedArtifact, CiYamlArtifact, GithooksPrePushArtifact, artifact_path } -import std.realization_schedule { string_list_eq } +import std.realization_schedule { string_list_eq, ScheduleWitnessEntry } data doc_reachability_witness_entry: String = "dag/test/claim/doc_reachability_witness_test.dag" @@ -171,6 +171,26 @@ fn project_ci_rust_job_gates(roster: List) -> List ) } +data ci_witness_optin_inversion: String = "CI witness enrollment is OPT-IN (operator inversion, 2026-07-04). Witness existence in the tree does not enroll it in CI: the tree-wide glob roster grows quadratically with the corpus (a witness is definable between any two pipeline segments) and had reached the point where the floor timed out - maximum cost, zero delivered signal. A witness gates CI exactly when a CommitWitnessClaim row on the GithubActionsCiJob surface declares it, projected by project_ci_floor_witness_entries into the floor plan's explicit entries (entry x check_fn). Tree-wide discovery remains the local path (claim_batch --roster-from-discovery). dissolve-on: affected-set selection (v2.lens.affected_set) plus floor memoization make per-PR selection-by-affectedness affordable; re-enroll the corpus through the affected set and dissolve this projection back into discovery." + +fn witness_entries_from_enrollments(rows: List) -> List { + fold(rows, init: [], f: fn(acc, row) { + match row.check { + CommitWitnessClaim { entry, check_fns } => + concat(acc, fold(check_fns, init: [], f: fn(fns, check_fn) { + concat(fns, [ScheduleWitnessEntry { entry: entry, function: check_fn }]) + })) + _ => acc + } + }) +} + +fn project_ci_floor_witness_entries(roster: List) -> List { + witness_entries_from_enrollments( + rows: filter(roster, e => enrollment_has_surface(e: e, surface: GithubActionsCiJob)) + ) +} + fn local_tidy_check_eq(a: LocalTidyCheck, b: LocalTidyCheck) -> Bool { match a { LocalTidyWitnessCheck { entry, check_fns } => diff --git a/dag/gunbc/design_document.dag b/dag/gunbc/design_document.dag index 598b946d7ab..4ed1b273677 100644 --- a/dag/gunbc/design_document.dag +++ b/dag/gunbc/design_document.dag @@ -157,7 +157,7 @@ fn building_checks_blocks() -> List { ul(items: [ li(text: "`cargo test --workspace` · `cargo clippy --all-targets -- -D warnings` · `cargo fmt --all --check`"), li(text: "one-time: `git config core.hooksPath .githooks` (pre-push runs `cargo fmt`)"), - 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_batches`. 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 **discovery is tree-wide and marker-driven** (roster derived from `gunbc.ci_layer_roots`: `witness_layer_roots = [src/v2, dag]`, `witness_discovery_scan_dirs = [dag/test/claim]`): the source-root pass walks every `*_test.dag` under **both** trees and auto-enrolls each `test fn` — fail-closed, a `test fn` in a non-`_test.dag` file is a hygiene violation, so a witness cannot silently hide; the scan-dir pass adds `unified_claim_*` BoolWitnessClaim decls. So a new witness is floor-covered by *naming it* `_test.dag` with a `test fn`, not by hand-enrolling it (`discover_floor_corpus_rows` in `cli_run.rs`). Batch-2 also carries the effectful gates: rust fmt/clippy monolith gate (when a `.rs` changes), emit-host MVP smokes, layering-imports scan + perturb receipts, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`. 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_batches`. 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 is OPT-IN** (operator inversion 2026-07-04): the floor runs exactly `CiSpec.witness_entries` — `CommitWitnessClaim` rows on the `GithubActionsCiJob` surface of `gunbc.commit_workflow.commit_gate_roster`, projected by `project_ci_floor_witness_entries` — and an **empty roster puts zero witness-corpus nodes in the plan**. Existence ≠ enrollment: the tree-wide glob roster grows quadratically with the corpus (a witness is definable between any two pipeline segments) and had reached the 90-min timeout — maximum cost, zero delivered signal, every merge fail-open. 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 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`). Dissolve-on: affected-set selection (`v2.lens.affected_set`) + floor memoization make per-PR selection-by-affectedness affordable; enrollment then returns to discovery shrunk by the affected set. Batch-2 also carries the effectful gates: rust fmt/clippy monolith gate (when a `.rs` changes), emit-host MVP smokes, layering-imports scan + perturb receipts, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`. Parse is grammar-owned: `ingest_yaml_source` (`dag/extdeps/languages/yaml/ingest.dag`), no shell/host parser."), ]), ] } diff --git a/dag/gunbc/plans/ci_selection_vs_scheduling.dag b/dag/gunbc/plans/ci_selection_vs_scheduling.dag index af8ffdd7aaa..02526f399f9 100644 --- a/dag/gunbc/plans/ci_selection_vs_scheduling.dag +++ b/dag/gunbc/plans/ci_selection_vs_scheduling.dag @@ -64,6 +64,8 @@ fn ci_selection_vs_scheduling_body() -> List { p(text: "Affected-set and parallelization-by-realization are the **same work from two ends**: selection shrinks **what** runs per-PR (to the affected set); realization shrinks **how long** it takes (width / placement / cache / Share). Together they make *\"run the affected set per-PR, fast\"* the default — which dissolves the need for a cost-based nightly dump entirely. The only residue is (1) non-hermetic and (2) the selector backstop — neither is \"expensive\"; both are about soundness / determinism."), h2(text: "6. The seam, precisely"), p(text: "§4 selection produces the must-run set; §1 scheduling runs **that** set affordably (parallelism / cache) and runs the **complement** on a backstop cadence. Selection decides *membership*; scheduling decides *cadence / placement / width*. Cost lives only on the scheduling side."), + h2(text: "7. 2026-07-04 — the run-all baseline stood down IN CI (opt-in inversion)"), + p(text: "Step 1's \"sound baseline\" stopped being a baseline the day the floor stopped *completing*: at ~1,551 discovered witnesses the composed pass hit the 90-minute job timeout on every run — maximum cost, **zero delivered signal**, and every merge effectively fail-open (a gate that always times out gates nothing). A run-all that never finishes is not a superset of the affected set; it is the empty set billed at full price. Operator decision: invert to **declared opt-in enrollment** — `CiSpec.witness_entries`, projected from `CommitWitnessClaim` rows on the `GithubActionsCiJob` surface of `commit_gate_roster`; an empty roster puts zero witness-corpus nodes in the floor plan. This is NOT the §0 cost-routed selection this doc rejects: no affected test is demoted by expense, because *no* witness currently gates by discovery — the inversion replaces an always-red-by-timeout gate with an honest, completing floor (gates + declared witnesses), and the discovery roster remains the local path (`claim_batch --roster-from-discovery`). The §3 ladder is unchanged as the *destination*: land affected-set selection + floor memoization, then re-enroll the corpus through the affected set and dissolve the opt-in roster (dissolve-on marker lives on `ci_witness_optin_inversion` in `gunbc.commit_workflow`). Found during the inversion: `resolve_floor_runner_context` had been failing on a comment block the v1 grammar cannot parse (`affected_set_floor_runner.dag`), so the affected-set skip silently fell back to full-corpus on every run — step 2 was dead in CI while the doc described it as CI-enrolled."), ] } diff --git a/docs/plans/mechanism-inventory-red-controls.md b/docs/plans/mechanism-inventory-red-controls.md index 842d49c30b7..0caa32e1113 100644 --- a/docs/plans/mechanism-inventory-red-controls.md +++ b/docs/plans/mechanism-inventory-red-controls.md @@ -21,7 +21,7 @@ All gates reduce to `result.success` of a host effect (or `ProcessExit` → `exi | SelfHostRealizedComparisonGate | both sides read real emitted bytes + staleness fixed-point (`self_host_realized_comparison_gate.dag:60-67`) | **WEAK** — subset roster `["lib.rs","v1_rt.rs"]` = 2 of ~89 files, self-labeled INTERIM (`gate:18-19`, `transport:17`); no planted-stale red fixture | wired, weak-red | | EmitDeterminismGate | two sequential emits byte-identical (`dag/tools/emit_determinism_gate.dag:6-15`) | `diff -r` is the discriminator (`emit_determinism_transport.dag:48`); no injected-nondeterminism control; theoretical vacuity if the corpus emitted nothing (diff of two empty dirs passes) | wired, self-diff only | -Corpus runnables: `floor_corpus_node` discovery over `witness_layer_roots = ["dag","src/v2"]` + scan dirs (`dag/gunbc/ci_layer_roots.dag:3-5`); `execution_corpus_node` scoped to `src/v2/test/claim/execution` with empty excludes; `grounding_whole_tree` single claim (`ci_floor_plan.dag:193-199`). **Enrollment is opt-out by path substring** (`witness_exclusion_substrings`, `ci_layer_roots.dag:7-19`): a broad substring (`test/manual/`, `test/claim/execution/`) silently un-enrolls any witness whose path contains it, with no red signal. This is exactly how the grounding lens's live whole-tree witness went dark (§3, Group D). +Corpus runnables (rewritten 2026-07-04, witness opt-in inversion): witness enrollment in CI is now **opt-in by declared roster row** — `CiSpec.witness_entries` (`CommitWitnessClaim` rows on the `GithubActionsCiJob` surface of `commit_gate_roster`, projected by `project_ci_floor_witness_entries`), partitioned into `floor_corpus_node` (explicit entries, no scan dirs) and `execution_corpus_node` (rows under `src/v2/test/claim/execution/`); an **empty roster puts no corpus node in the plan at all**. The glob discovery over `witness_layer_roots` + scan dirs remains the local `claim_batch --roster-from-discovery` path only. The prior hazard this section flagged — **enrollment opt-out by path substring** silently un-enrolling witnesses with no red signal — is dissolved in CI by construction: nothing is enrolled by scanning, so nothing can be silently *un*-enrolled by an exclusion (`witness_exclusion_substrings` now only shapes the local discovery roster). The `grounding_whole_tree` dedicated single claim was dissolved into the roster; re-enrolling it is a declared `CommitWitnessClaim` row for `src/v2/test/manual/grounding_lens_whole_tree_test.dag`. ## 2. Commit workflow / merge path (outside the floor) @@ -46,7 +46,7 @@ The #5433 inert-lens backstop **does execute** in the floor (`dag/test/claim/ine **C — wired, synthetic-only (no live-corpus read) (~28):** visibility, complexity, cost, complexity_lowering, synthesis, unit_modeling, coverage, table_decision_tree, application(+serializer), identical_variant_payload, structural_resolution, structural_similarity, simulated_relationship, edit_locus/affected_set, wiring_liveness_preflight, mock_totality, grounding_ledger, registry, testgen, and the 8 family-eval lenses (effect, ownership, parallelism, idempotency, unused_parameters, subsumption, discrimination, leaf_model_verification). These execute in the floor but would never catch a real corpus violation. -**D — inert (3):** **grounding** (its live whole-tree witness `src/v2/test/manual/grounding_lens_whole_tree_test.dag` is exclusion-hidden by `test/manual/` — note it IS separately wired as the floor's `grounding_whole_tree_runnable` (`ci_floor_plan.dag:193-199`), so "inert" here means dark to *discovery*, not to the floor; the discovery-exclusion + explicit-runnable pairing is itself a dual-representation to keep coherent), **fact_density** (fires inside the compile-lens bundle but no discovered `test fn` gates its verdict), **affected_set_examples** (fixture carrier, no witness). +**D — inert (3):** **grounding** (its live whole-tree witness `src/v2/test/manual/grounding_lens_whole_tree_test.dag` is dark to local discovery via the `test/manual/` exclusion, and since the 2026-07-04 opt-in inversion the floor's dedicated `grounding_whole_tree_runnable` is gone too — the discovery-exclusion + explicit-runnable dual-representation is dissolved; re-enrollment is a single declared `CommitWitnessClaim` roster row on the `GithubActionsCiJob` surface), **fact_density** (fires inside the compile-lens bundle but no discovered `test fn` gates its verdict), **affected_set_examples** (fixture carrier, no witness). ## 4. Caches diff --git a/src/v1/stage0/src/bin/bootstrap_witness.rs b/src/v1/stage0/src/bin/bootstrap_witness.rs index e81e70a5d6f..2581af654e7 100644 --- a/src/v1/stage0/src/bin/bootstrap_witness.rs +++ b/src/v1/stage0/src/bin/bootstrap_witness.rs @@ -425,9 +425,17 @@ fn floor_smoke() { "stage0 Cargo.toml missing under {}", ws.display() ); - assert!(ws.join("dag").is_dir(), "dag/ missing under {}", ws.display()); + assert!( + ws.join("dag").is_dir(), + "dag/ missing under {}", + ws.display() + ); let [v1_root, dag_root] = source_roots(); - assert!(v1_root.is_dir(), "v1 source root missing: {}", v1_root.display()); + assert!( + v1_root.is_dir(), + "v1 source root missing: {}", + v1_root.display() + ); assert!( dag_root.is_dir(), "dag source root missing: {}", @@ -1200,8 +1208,14 @@ fn floor_suite() -> Vec { fn expensive_suite() -> Vec { vec![ ("stage0_cargo_check", stage0_cargo_check), - ("strict_compile_diagnostic_count", strict_compile_diagnostic_count), - ("stage0_compile_accepts_dag_target", stage0_compile_accepts_dag_target), + ( + "strict_compile_diagnostic_count", + strict_compile_diagnostic_count, + ), + ( + "stage0_compile_accepts_dag_target", + stage0_compile_accepts_dag_target, + ), ( "stage0_compile_imports_ephemeral_generated_source_root", stage0_compile_imports_ephemeral_generated_source_root, @@ -1241,9 +1255,13 @@ fn run_suite(tests: &[WitnessCase]) -> ExitCode { } fn main() -> ExitCode { - let suite = std::env::args().nth(1).unwrap_or_else(|| "--suite".to_string()); + let suite = std::env::args() + .nth(1) + .unwrap_or_else(|| "--suite".to_string()); let suite_name = if suite == "--suite" { - std::env::args().nth(2).unwrap_or_else(|| "floor".to_string()) + std::env::args() + .nth(2) + .unwrap_or_else(|| "floor".to_string()) } else { suite }; @@ -1254,7 +1272,11 @@ fn main() -> ExitCode { "ci" => ci_suite(), "l4" => vec![("bootstrap_l4_structural", bootstrap_l4_structural)], "all" => all_suite(), - other => return fail(format!("unknown suite {other:?}; expected floor|expensive|ci|l4|all")), + other => { + return fail(format!( + "unknown suite {other:?}; expected floor|expensive|ci|l4|all" + )) + } }; run_suite(&tests) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index aff417fae69..0c93f4e205d 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -1766,6 +1766,32 @@ fn run() -> Result { return Err(ExitCode::from(1)); } + // The opt-in witness roster can be empty, removing every DiscoveryBatch from the + // plan — and with it the only CI path through the tree-walk naming hygiene + // (`test fn` outside `*_test.dag`, `__` basenames). A witness must stay NAMEABLE + // even when not enrolled (an unnameable witness could never be opted in), so run + // the same fail-closed walk once here, discarding the roster. Skipped when a + // DiscoveryBatch is scheduled: that batch runs the identical checks itself. + let plan_has_discovery_batch = batches + .iter() + .flatten() + .any(|r| matches!(r, Runnable::DiscoveryBatch { .. })); + if !plan_has_discovery_batch { + let excludes: Vec = v1_compiler::cli_run::FLOOR_DISCOVERY_EXCLUDES + .iter() + .map(|s| s.to_string()) + .collect(); + if let Err(msg) = v1_compiler::cli_run::check_floor_filename_hygiene(&source_roots) + .and_then(|_| { + v1_compiler::cli_run::discover_floor_corpus_rows(&source_roots, &[], &excludes) + .map(|_| ()) + }) + { + eprintln!("claim_executor: witness naming hygiene (zero-enrollment walk): {msg}"); + return Err(ExitCode::from(1)); + } + } + let spawn_width = match eval_spawn_width(&source_roots, &plan_entry, &plan_function) { Ok(w) => w, Err(msg) => { diff --git a/src/v1/stage0/src/bin/diagnostics_witness.rs b/src/v1/stage0/src/bin/diagnostics_witness.rs index a0ba019dc2f..4782d422ab9 100644 --- a/src/v1/stage0/src/bin/diagnostics_witness.rs +++ b/src/v1/stage0/src/bin/diagnostics_witness.rs @@ -165,9 +165,10 @@ fn diagnostic_messages(result: &PipelineResult) -> Vec { } fn has_arity_mismatch(result: &PipelineResult) -> bool { - result.diagnostics.iter().any(|d| { - matches!(&*d.diagnostic, CompilerDiagnostic::ArityMismatch { .. }) - }) + result + .diagnostics + .iter() + .any(|d| matches!(&*d.diagnostic, CompilerDiagnostic::ArityMismatch { .. })) } fn first_arity_mismatch_message(result: &PipelineResult) -> Option { @@ -192,17 +193,32 @@ fn import_resolution_variants_and_span(module_index: &ModuleIndex) { assert_eq!(missing_result.diagnostics.len(), 1); let missing = &missing_result.diagnostics[0]; assert!( - matches!(&*missing.diagnostic, CompilerDiagnostic::MissingExport { .. }), + matches!( + &*missing.diagnostic, + CompilerDiagnostic::MissingExport { .. } + ), "expected MissingExport, got: {:?}", missing.diagnostic ); let missing_msg = diagnostic_to_message(missing.diagnostic.clone()); - assert!(missing_msg.contains("NonExistent"), "missing export: {missing_msg}"); - assert!(missing_msg.contains("provider"), "target module: {missing_msg}"); - assert!(missing_msg.contains("consumer"), "importing module: {missing_msg}"); + assert!( + missing_msg.contains("NonExistent"), + "missing export: {missing_msg}" + ); + assert!( + missing_msg.contains("provider"), + "target module: {missing_msg}" + ); + assert!( + missing_msg.contains("consumer"), + "importing module: {missing_msg}" + ); let (line, col) = diag_line_col(missing, missing_export, "consumer.dag"); assert_eq!(line, 2, "span should be on import line"); - assert_eq!(col, 19, "span should point at missing name, not import keyword"); + assert_eq!( + col, 19, + "span should point at missing name, not import keyword" + ); let unresolved = "module consumer\nimport nonexistent { Thing }\n"; let unresolved_result = compile_multi(module_index, &[("consumer.dag", unresolved)]); @@ -286,8 +302,7 @@ fn type_and_arity_discrimination(module_index: &ModuleIndex) { diagnostic_messages(¶meterized_result) ); - let user_defined = - "module custom\ntype Widget { label: String }\ntype Bag { item: Widget }\n"; + let user_defined = "module custom\ntype Widget { label: String }\ntype Bag { item: Widget }\n"; let user_defined_result = compile_multi(module_index, &[("custom.dag", user_defined)]); assert!( !has_arity_mismatch(&user_defined_result), @@ -319,7 +334,8 @@ fn empty_list_literal_context(module_index: &ModuleIndex) { diagnostic_messages(&wrong_result) ); - let ok = "module elist_ok\nimport std.types { List }\nfn make_list() -> List {\n []\n}\n"; + let ok = + "module elist_ok\nimport std.types { List }\nfn make_list() -> List {\n []\n}\n"; let ok_result = compile_multi(module_index, &[("elist_ok.dag", ok)]); let ok_diags: Vec<_> = ok_result .diagnostics diff --git a/src/v1/stage0/src/bin/effects_rest_transport_witness.rs b/src/v1/stage0/src/bin/effects_rest_transport_witness.rs index b74998bff00..c80673f3771 100644 --- a/src/v1/stage0/src/bin/effects_rest_transport_witness.rs +++ b/src/v1/stage0/src/bin/effects_rest_transport_witness.rs @@ -5,10 +5,8 @@ use std::process::ExitCode; use std::rc::Rc; use v1_compiler::cli_run::workspace_root; +use v1_compiler::cli_run::{collect_rest_transport_operations, DeclaredRestTransportOp}; use v1_compiler::extdeps_uri_path::{parse_path_template, PathTemplateParseResult}; -use v1_compiler::cli_run::{ - collect_rest_transport_operations, DeclaredRestTransportOp, -}; use v1_compiler::std_effects::{ derive_op_effect, generate_idempotency_obligations, is_idempotent_effect, DeriveOpEffectResult, EffectShape, HttpMethod, diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index ca833d3ee3f..1067e74a623 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -552,7 +552,9 @@ fn pool_roots_for_module_graph_closure(source_roots: &[String]) -> Vec { .collect() } -fn path_to_source_lookup(index: &ModuleSourceIndex) -> HashMap> { +fn path_to_source_lookup( + index: &ModuleSourceIndex, +) -> HashMap> { let mut out = HashMap::new(); for sf in index.values() { let rel = workspace_relative_repo_path(&sf.path); @@ -681,9 +683,7 @@ fn resolve_transitively( let mut path_lookup = path_to_source_lookup(index); for entry in &entry_sources { let rel = workspace_relative_repo_path(&entry.path); - path_lookup - .entry(rel) - .or_insert_with(|| entry.clone()); + path_lookup.entry(rel).or_insert_with(|| entry.clone()); path_lookup .entry(entry.path.clone()) .or_insert_with(|| entry.clone()); @@ -762,7 +762,9 @@ fn entry_source_from_index_or_disk( })) } -fn load_sources(source_roots: &[String]) -> Result>, String> { +fn load_sources( + source_roots: &[String], +) -> Result>, String> { let index = build_module_index(source_roots); let facts = build_module_graph_facts_live(source_roots); let first_root = std::path::Path::new(&source_roots[0]); @@ -1669,11 +1671,8 @@ pub fn whole_tree_resolved_ctx( } pub fn closure_subject_for_entry(index: &MultiEntryIndex, entry: &str) -> Result { - let sources = load_sources_for_entry_with_index( - &index.source_files, - &index.module_graph_facts, - entry, - )?; + let sources = + load_sources_for_entry_with_index(&index.source_files, &index.module_graph_facts, entry)?; Ok(subject_digest_for_closure(&sources)) } @@ -4261,9 +4260,7 @@ fn call_floor_host_scaffold_would_skip( .item_registry .contains_key("floor_host_scaffold_would_skip") { - return Err( - "floor_host_scaffold_would_skip missing from floor runner context".to_string(), - ); + return Err("floor_host_scaffold_would_skip missing from floor runner context".to_string()); } let paths: Vec = changed_paths .iter() @@ -4375,7 +4372,8 @@ fn witness_test_fn_uses_live_host_scan(entry_content: &str, function: &str) -> b } let decl_needle = format!("test fn {function}"); if let Some(start) = entry_content.find(&decl_needle) { - let decl_tail = &entry_content[start..entry_content.len().min(start + decl_needle.len() + 120)]; + let decl_tail = + &entry_content[start..entry_content.len().min(start + decl_needle.len() + 120)]; if decl_tail.contains(FLOOR_HOST_SCAFFOLD_WITNESS_MARKER) { return true; } @@ -4934,7 +4932,7 @@ fn run_discovery_rows( &index.module_graph_facts, &row.entry, ) - .map_err(|msg| format!("load sources failed for {}: {}", row.entry, msg))?; + .map_err(|msg| format!("load sources failed for {}: {}", row.entry, msg))?; let closure_subject = subject_digest_for_closure(&sources); let resolve_started = std::time::Instant::now(); set_phase(FloorPhase::Resolve, &row.entry); @@ -5168,10 +5166,7 @@ diff --git a/src/v2/lens/affected_set.dag b/src/v2/lens/affected_set.dag let pairs = scan_test_decl_lines(source); assert_eq!( pairs, - vec![ - ("witness_a".to_string(), 5), - ("witness_b".to_string(), 7) - ] + vec![("witness_a".to_string(), 5), ("witness_b".to_string(), 7)] ); } @@ -5183,32 +5178,51 @@ diff --git a/src/v2/lens/affected_set.dag b/src/v2/lens/affected_set.dag "clean_tree_holds" )); // File-wide fail-closed: sibling witnesses in the same entry also run. - assert!(super::witness_test_fn_uses_live_host_scan(source, "pure_holds")); + assert!(super::witness_test_fn_uses_live_host_scan( + source, + "pure_holds" + )); } #[test] fn witness_test_fn_uses_live_host_scan_pure_entry_stays_kernel_eligible() { let source = "module m\n\ntest fn pure_holds() -> Bool { true }\n\ntest fn also_pure() -> Bool { false }\n"; - assert!(!super::witness_test_fn_uses_live_host_scan(source, "pure_holds")); - assert!(!super::witness_test_fn_uses_live_host_scan(source, "also_pure")); + assert!(!super::witness_test_fn_uses_live_host_scan( + source, + "pure_holds" + )); + assert!(!super::witness_test_fn_uses_live_host_scan( + source, + "also_pure" + )); } #[test] fn witness_test_fn_uses_live_host_scan_detects_declared_marker() { - let source = "module m\n\ntest fn marked_holds() -> Bool { // floor:host_scaffold\n true\n}\n"; - assert!(super::witness_test_fn_uses_live_host_scan(source, "marked_holds")); + let source = + "module m\n\ntest fn marked_holds() -> Bool { // floor:host_scaffold\n true\n}\n"; + assert!(super::witness_test_fn_uses_live_host_scan( + source, + "marked_holds" + )); } #[test] fn witness_test_fn_uses_live_host_scan_follows_same_file_helper() { let source = "module m\n\nfn helper_holds() -> Bool {\n layer_import_facts(std_roots: [], extdeps_roots: [])\n}\n\ntest fn witness_holds() -> Bool {\n helper_holds()\n}\n"; - assert!(super::witness_test_fn_uses_live_host_scan(source, "witness_holds")); + assert!(super::witness_test_fn_uses_live_host_scan( + source, + "witness_holds" + )); } #[test] fn witness_test_fn_uses_live_host_scan_follows_nested_same_file_helper() { let source = "module m\n\nfn helper_b() -> Bool {\n realization_vocab_containment_clean_live(scan_roots: roots)\n}\n\nfn helper_a() -> Bool {\n helper_b()\n}\n\ntest fn witness_holds() -> Bool {\n helper_a()\n}\n"; - assert!(super::witness_test_fn_uses_live_host_scan(source, "witness_holds")); + assert!(super::witness_test_fn_uses_live_host_scan( + source, + "witness_holds" + )); } #[test] @@ -11276,8 +11290,8 @@ fn serialize_variant_to_wire_json( ctx: &v1_interpreter::InterpContext, ) -> WireSerializeResult { use crate::v1_compiler_emit_rust::{ - policy_is_string_variant, policy_is_untagged, policy_serde_tag_field, - rust_serde_tag_attr, rust_tagged_object_policy, wire_variant_tag_for_policy, + policy_is_string_variant, policy_is_untagged, policy_serde_tag_field, rust_serde_tag_attr, + rust_tagged_object_policy, wire_variant_tag_for_policy, }; let policy = resolve_coproduct_wire_policy( type_name, @@ -11366,11 +11380,11 @@ fn serialize_untagged_variant( #[cfg(test)] mod import_closure_equivalence_tests { use super::{ - build_module_index, build_module_graph_facts_live, build_multi_entry_index, + build_module_graph_facts_live, build_module_index, build_multi_entry_index, closure_subject_for_entry, default_source_roots, import_closure_live_paths, module_graph_facts_build_count_for_test, reset_module_graph_facts_build_count_for_test, - resolve_transitively, resolve_transitively_bfs_legacy, workspace_relative_repo_path, - witness_layer_roots, + resolve_transitively, resolve_transitively_bfs_legacy, witness_layer_roots, + workspace_relative_repo_path, }; use std::collections::HashMap; use std::path::PathBuf; @@ -11397,8 +11411,8 @@ mod import_closure_equivalence_tests { let ws = workspace_root(); let entry_abs = ws.join(entry_rel); let index = build_module_index(pool_roots); - let content = std::fs::read_to_string(&entry_abs) - .unwrap_or_else(|e| panic!("read {entry_rel}: {e}")); + let content = + std::fs::read_to_string(&entry_abs).unwrap_or_else(|e| panic!("read {entry_rel}: {e}")); let entry_source = Rc::new(crate::v1_compiler_compile::SourceFile { path: entry_abs.to_string_lossy().into_owned(), content, @@ -11600,9 +11614,7 @@ mod import_closure_equivalence_tests { let mut without_entry: std::collections::BTreeSet = live .iter() .map(|p| workspace_relative_repo_path(p)) - .filter(|p| { - p != "src/v2/test/claim/manual/coproduct_reflection_conformance_test.dag" - }) + .filter(|p| p != "src/v2/test/claim/manual/coproduct_reflection_conformance_test.dag") .collect(); let repointed = import_closure_live_paths( "src/v2/test/claim/manual/coproduct_reflection_conformance_test.dag", @@ -11613,10 +11625,11 @@ mod import_closure_equivalence_tests { .iter() .map(|p| workspace_relative_repo_path(p)) .collect(); - assert_ne!(without_entry, full, "RED control: dropped entry must diverge"); - without_entry.insert( - "src/v2/std/__bogus_never_imported__.dag".to_string(), + assert_ne!( + without_entry, full, + "RED control: dropped entry must diverge" ); + without_entry.insert("src/v2/std/__bogus_never_imported__.dag".to_string()); assert_ne!( without_entry, full, "RED control: bogus path must diverge from live closure" diff --git a/src/v1/stage0/src/v1_compiler_infer_patterns.rs b/src/v1/stage0/src/v1_compiler_infer_patterns.rs index 6f0ee53342a..1dd9fc95134 100644 --- a/src/v1/stage0/src/v1_compiler_infer_patterns.rs +++ b/src/v1/stage0/src/v1_compiler_infer_patterns.rs @@ -8,6 +8,9 @@ use crate::std_syntax::LiteralValue::LitBool; pub use crate::std_types::SourceSpan; pub use crate::v1_compiler_infer_env::TypeEnv; pub use crate::v1_compiler_infer_env::{lookup_type, lookup_type_by_name}; +pub use crate::v1_compiler_infer_resolve::{ + is_user_generic_use_site, resolve_generic_use_decl, substitute_type_slots, +}; pub use crate::v1_compiler_infer_types::{ child_type_node, emit_map_has, extract_optional_inner_node, }; @@ -22,11 +25,7 @@ use crate::v1_std_core::InferredNode::{CompilerError, Resolved, TypeVariable}; use crate::v1_std_core::MatchPattern::LitPattern; pub use crate::v1_std_core::{ arm_pattern, authored_name_at, error_type, find_child_named, generic_param_name_at, - is_compiler_error, kernel_span, - make_error_node, no_span, none_type, with_optional_cardinality, -}; -pub use crate::v1_compiler_infer_resolve::{ - is_user_generic_use_site, resolve_generic_use_decl, substitute_type_slots, + is_compiler_error, kernel_span, make_error_node, no_span, none_type, with_optional_cardinality, }; pub use crate::v1_std_core::{ Cardinality, CompilerDiagnostic, Connective, ErrorNode, ExprData, InferredNode, MatchPattern, @@ -361,14 +360,9 @@ pub fn generic_use_slot_bindings( ) } -pub fn expand_scrut_type_for_variant_lookup( - scrut_node: Rc, - env: Rc, -) -> Rc { +pub fn expand_scrut_type_for_variant_lookup(scrut_node: Rc, env: Rc) -> Rc { let name = authored_name_at(env.source_indices.clone(), scrut_node.clone()); - if (scrut_node.connective.clone() == Connective::Disj) - || is_witness_type_name(name.clone()) - { + if (scrut_node.connective.clone() == Connective::Disj) || is_witness_type_name(name.clone()) { return scrut_node.clone(); } if (((scrut_node.connective.clone() == Connective::NoConnective) @@ -383,8 +377,7 @@ pub fn expand_scrut_type_for_variant_lookup( { match decl.inferred.clone().as_deref().cloned() { Some(InferredNode::Resolved { node: target, .. }) => { - let subst = - generic_use_slot_bindings(scrut_node.clone(), env.clone()); + let subst = generic_use_slot_bindings(scrut_node.clone(), env.clone()); let expanded = substitute_type_slots( target, subst, @@ -453,8 +446,7 @@ pub fn lookup_variant_in_type( node: scrut_node, .. } => { let source_indices = env.source_indices.clone(); - let scrut_node = - expand_scrut_type_for_variant_lookup(scrut_node.clone(), env.clone()); + let scrut_node = expand_scrut_type_for_variant_lookup(scrut_node.clone(), env.clone()); let scrut_opt = (scrut_node.return_cardinality.clone() == Cardinality::CardOptional); if (((scrut_node.connective.clone() == Connective::NoConnective) && ((scrut_node.children.clone().len() as i64) == 0)) diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 44a06ca753d..d735e4e645b 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -8,6 +8,7 @@ use std::time::Instant; use im_rc::HashMap as HamtMap; use im_rc::Vector as RrbVector; +use crate::cli_run::value_to_wire_json; use crate::std_syntax::BinOp; use crate::std_syntax::LiteralValue; use crate::v1_compiler_emit::{extract_string_interp_parts, has_mock_prefix}; @@ -31,7 +32,6 @@ use crate::v1_std_core::{ FieldValueShape, InferredNode, MatchPattern, MethodSemantics, NewlineIndex, Node, SourceSpan, StringPart, UnaryOpKind, VarBindingKind, }; -use crate::cli_run::value_to_wire_json; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Symbol(u32); diff --git a/src/v1/tests/src/func_env_scope_chain_test.rs b/src/v1/tests/src/func_env_scope_chain_test.rs index ae1ecf6ffa4..149dce4f1ad 100644 --- a/src/v1/tests/src/func_env_scope_chain_test.rs +++ b/src/v1/tests/src/func_env_scope_chain_test.rs @@ -252,6 +252,7 @@ fn func_env_unique_sig_ptr_count_matches_defined_functions() { } #[test] +#[ignore = "CI witness opt-in inversion (2026-07-04): whole-tree resolve over dag+src/v1 — the rust-lane twin of the corpus witnesses inverted out of the per-PR floor (run-everything had pushed both CI jobs to the 90-min timeout: max cost, zero signal). Run explicitly: cargo nextest run -p v1-compiler-tests -- --ignored func_env_whole_tree_unique_ptr_count_equals_local_definitions. Re-enroll when affected-set selection + floor memoization land (see ci_witness_optin_inversion in gunbc.commit_workflow)."] fn func_env_whole_tree_unique_ptr_count_equals_local_definitions() { let mut exclude_subpaths: Vec = FLOOR_DISCOVERY_EXCLUDES .iter() diff --git a/src/v1/tests/src/func_env_semantic_equivalence_test.rs b/src/v1/tests/src/func_env_semantic_equivalence_test.rs index 8750a91281e..d1164fcf20e 100644 --- a/src/v1/tests/src/func_env_semantic_equivalence_test.rs +++ b/src/v1/tests/src/func_env_semantic_equivalence_test.rs @@ -145,6 +145,7 @@ fn baseline_corpus_roots(corpus_dir: &Path) -> Vec { } #[test] +#[ignore = "CI witness opt-in inversion (2026-07-04): whole-corpus strict resolve + semantic-oracle snapshot over dag+src/v1 — the rust-lane twin of the corpus witnesses inverted out of the per-PR floor (run-everything had pushed both CI jobs to the 90-min timeout: max cost, zero signal). Run explicitly: cargo nextest run -p v1-compiler-tests -- --ignored func_env_whole_corpus_semantic_oracle_matches_pre_change_baseline. Re-enroll when affected-set selection + floor memoization land (see ci_witness_optin_inversion in gunbc.commit_workflow)."] fn func_env_whole_corpus_semantic_oracle_matches_pre_change_baseline() { let fixture_path = workspace_root().join(BASELINE_FIXTURE); let raw = fs::read_to_string(&fixture_path) diff --git a/src/v1/tests/src/interp_wire_serialize_test.rs b/src/v1/tests/src/interp_wire_serialize_test.rs index 9ae249aaf7a..bd93a4904a7 100644 --- a/src/v1/tests/src/interp_wire_serialize_test.rs +++ b/src/v1/tests/src/interp_wire_serialize_test.rs @@ -1,8 +1,8 @@ use std::rc::Rc; +use v1_compiler::cli_run::value_to_wire_json; use v1_compiler::v1_compiler_compile::{compile_to_resolved, ResolvedPipelineResult}; use v1_compiler::v1_interpreter::{self, Value}; -use v1_compiler::cli_run::value_to_wire_json; use crate::helpers::{resolve_imports_transitively, workspace_root}; diff --git a/src/v2/test/claim/ci_floor_plan_witness_test.dag b/src/v2/test/claim/ci_floor_plan_witness_test.dag index 5009677f0f3..0edbf6c2c85 100644 --- a/src/v2/test/claim/ci_floor_plan_witness_test.dag +++ b/src/v2/test/claim/ci_floor_plan_witness_test.dag @@ -4,10 +4,14 @@ import std.realization { Runnable, RunnableSingleClaim, RunnableDiscoveryBatch, } import v2.workflow.ci_floor_plan { - gunbc_ci_floor_batches, corpus_runnable, - gunbc_ci_floor_schedule_without_serialization, + gunbc_ci_floor_batches, + floor_schedule_for, + corpus_runnable_for, execution_corpus_runnable_for, + floor_corpus_exclusion_resource_edges, gunbc_ci_floor_plan_serializes_heavy_resolves, gunbc_ci_floor_plan_co_runs_heavy_without_serialization, + floor_plan_serializes_heavy_resolves_for, + floor_plan_co_runs_heavy_without_serialization_for, gate_runnable, witness_floor_corpus_exclusion_edges_exist, witness_floor_compile_gate_honest_heavy_profile, @@ -20,14 +24,42 @@ import v2.workflow.ci_floor_plan { witness_corpus_effective_widths_fit_runner_cap, witness_floor_derived_width_leaves_headroom_at_cap, } -import gunbc.ci_layer_roots { witness_discovery_scan_dirs, witness_layer_roots } -import gunbc.ci_spec { gunbc_ci_spec, EmitHostGate, LayeringImportsGate } -import std.realization_schedule { runnable_forbids_corpus_co_residence } -import v2.std.algebra { length, list_snoc_item } +import gunbc.ci_layer_roots { witness_layer_roots } +import gunbc.ci_spec { gunbc_ci_spec, ci_spec_with_witness_entries, CiSpec, EmitHostGate, LayeringImportsGate } +import std.realization_schedule { runnable_forbids_corpus_co_residence, ScheduleWitnessEntry, schedule_witness_entry_eq } +import v2.std.algebra { length, list_snoc_item, skip } import v2.std.collection { List } +import std.types { Int } data corpus_sentinel: String = "__corpus__" +data synthetic_optin_rows_note: String = "Synthetic opt-in rows: pure plan inputs, never executed. They exercise the enrollment machinery (partition, gating, serialization) that the live plan - whose roster is empty under the opt-in inversion - no longer exercises." + +data synthetic_corpus_row: ScheduleWitnessEntry = ScheduleWitnessEntry { + entry: "dag/test/claim/synthetic_optin_corpus_witness_test.dag", + function: "synthetic_optin_corpus_witness" +} + +data synthetic_execution_row: ScheduleWitnessEntry = ScheduleWitnessEntry { + entry: "src/v2/test/claim/execution/synthetic_optin_exec_witness_test.dag", + function: "synthetic_optin_exec_witness" +} + +data optin_spec: CiSpec = ci_spec_with_witness_entries( + spec: gunbc_ci_spec, + entries: [synthetic_corpus_row, synthetic_execution_row] +) + +data corpus_only_spec: CiSpec = ci_spec_with_witness_entries( + spec: gunbc_ci_spec, + entries: [synthetic_corpus_row] +) + +data zero_roster_spec: CiSpec = ci_spec_with_witness_entries( + spec: gunbc_ci_spec, + entries: [] +) + fn runnable_function(r: Runnable) -> String { match r { RunnableSingleClaim { entry: _, function: f, profile: _ } => f @@ -44,6 +76,34 @@ fn list_contains_str(xs: List, target: String) -> Bool { fold(xs, init: false, f: fn(acc, x) { acc || (x == target) }) } +fn schedule_discovery_batch_count(batches: List>) -> Int { + fold(batches, init: 0, f: fn(acc, b) { + fold(b, init: acc, f: fn(inner, r) { + if runnable_function(r: r) == corpus_sentinel { inner + 1 } else { inner } + }) + }) +} + +fn batch_index_of(batches: List>, target: String) -> Int { + if length(xs: batches) == 0 { + 0 - 1 + } else if list_contains_str(xs: batch_functions(b: batches.first()), target: target) { + 0 + } else { + let rest = batch_index_of(batches: skip(xs: batches, n: 1), target: target) + if rest < 0 { rest } else { rest + 1 } + } +} + +fn witness_live_floor_discovery_tracks_optin_roster() -> Bool { + let live_count = schedule_discovery_batch_count(batches: gunbc_ci_floor_batches()) + if length(xs: gunbc_ci_spec.witness_entries) == 0 { + live_count == 0 + } else { + live_count > 0 + } +} + fn witness_compile_root_runs_first() -> Bool { let batches = gunbc_ci_floor_batches() (batches.first().length() == 1) && @@ -59,53 +119,60 @@ fn witness_independent_jobs_parallelize() -> Bool { gunbc_ci_floor_batches().skip(n: 1).first().length() > 1 } -fn witness_discovery_scan_dirs_in_plan() -> Bool { - match corpus_runnable() { - RunnableDiscoveryBatch { source_roots: _, scan_dirs: dirs, explicit_entries: _, skip_unaffected_node_frontier: _, exclude_substrings: _, discovery_scope_dirs: _, spawn_width_cap: _, profile: _ } => - list_contains_str(xs: dirs, target: "dag/test/claim") - _ => false - } +fn witness_plan_serializes_heavy_resolves() -> Bool { + gunbc_ci_floor_plan_serializes_heavy_resolves() && + gunbc_ci_floor_plan_co_runs_heavy_without_serialization() } -fn witness_scan_dirs_match_layer_authority() -> Bool { - match corpus_runnable() { - RunnableDiscoveryBatch { source_roots: _, scan_dirs: dirs, explicit_entries: _, skip_unaffected_node_frontier: _, exclude_substrings: _, discovery_scope_dirs: _, spawn_width_cap: _, profile: _ } => - list_contains_str(xs: dirs, target: witness_discovery_scan_dirs.first()) +fn discovery_carries_exactly(r: Runnable, expected: ScheduleWitnessEntry) -> Bool { + match r { + RunnableDiscoveryBatch { source_roots: roots, scan_dirs: dirs, explicit_entries: entries, skip_unaffected_node_frontier: _, exclude_substrings: _, discovery_scope_dirs: scope, spawn_width_cap: _, profile: _ } => + (length(xs: dirs) == 0) && + (length(xs: scope) == 0) && + (length(xs: entries) == 1) && + schedule_witness_entry_eq(a: entries.first(), b: expected) && + list_contains_str(xs: roots, target: witness_layer_roots.first()) _ => false } } -fn witness_corpus_has_no_explicit_entries() -> Bool { - match corpus_runnable() { - RunnableDiscoveryBatch { source_roots: roots, scan_dirs: _, explicit_entries: entries, skip_unaffected_node_frontier: _, exclude_substrings: _, discovery_scope_dirs: _, spawn_width_cap: _, profile: _ } => - (length(xs: entries) == 0) && - list_contains_str(xs: roots, target: "src/v2") && - list_contains_str(xs: roots, target: "dag") && - list_contains_str(xs: roots, target: witness_layer_roots.first()) - _ => false - } +fn witness_optin_corpus_carries_exactly_roster_rows() -> Bool { + discovery_carries_exactly(r: corpus_runnable_for(spec: optin_spec), expected: synthetic_corpus_row) } -fn witness_corpus_skip_node_frontier_enabled() -> Bool { - match corpus_runnable() { - RunnableDiscoveryBatch { source_roots: _, scan_dirs: _, explicit_entries: _, skip_unaffected_node_frontier: enabled, exclude_substrings: _, discovery_scope_dirs: _, spawn_width_cap: _, profile: _ } => - enabled - _ => false - } +fn witness_optin_execution_partition() -> Bool { + discovery_carries_exactly(r: execution_corpus_runnable_for(spec: optin_spec), expected: synthetic_execution_row) } -fn witness_source_ingest_serialized_after_corpus() -> Bool { - let batches = gunbc_ci_floor_batches() - let corpus_layer = batches.skip(n: 1).first() - let ingest_layer = batches.skip(n: 3).first() - list_contains_str(xs: batch_functions(b: corpus_layer), target: corpus_sentinel) && - !list_contains_str(xs: batch_functions(b: corpus_layer), target: "source_root_ingest_gate_passes") && - list_contains_str(xs: batch_functions(b: ingest_layer), target: "source_root_ingest_gate_passes") +fn witness_optin_roster_drives_corpus_presence() -> Bool { + (schedule_discovery_batch_count(batches: floor_schedule_for(spec: zero_roster_spec, serialize_heavy: true)) == 0) && + (schedule_discovery_batch_count(batches: floor_schedule_for(spec: corpus_only_spec, serialize_heavy: true)) == 1) && + (schedule_discovery_batch_count(batches: floor_schedule_for(spec: optin_spec, serialize_heavy: true)) == 2) } -fn witness_plan_serializes_heavy_resolves() -> Bool { - gunbc_ci_floor_plan_serializes_heavy_resolves() && - gunbc_ci_floor_plan_co_runs_heavy_without_serialization() +fn witness_optin_plan_gates_corpus_after_compile() -> Bool { + let batches = floor_schedule_for(spec: optin_spec, serialize_heavy: true) + !list_contains_str(xs: batch_functions(b: batches.first()), target: corpus_sentinel) && + (batch_index_of(batches: batches, target: "dag_compile_clean_gate_passes") == 0) && + (batch_index_of(batches: batches, target: corpus_sentinel) >= 1) +} + +fn witness_optin_source_ingest_serialized_after_corpus() -> Bool { + let batches = floor_schedule_for(spec: optin_spec, serialize_heavy: true) + let corpus_idx = batch_index_of(batches: batches, target: corpus_sentinel) + let ingest_idx = batch_index_of(batches: batches, target: "source_root_ingest_gate_passes") + (corpus_idx >= 1) && (ingest_idx > corpus_idx) +} + +fn witness_optin_exclusion_edges_track_roster() -> Bool { + witness_floor_corpus_exclusion_edges_exist(spec: optin_spec) && + (length(xs: floor_corpus_exclusion_resource_edges(spec: zero_roster_spec)) == 0) && + witness_floor_no_corpus_resource_edge_to_compile_anchor(spec: optin_spec) +} + +fn witness_optin_plan_serializes_heavy_resolves() -> Bool { + floor_plan_serializes_heavy_resolves_for(spec: optin_spec) && + floor_plan_co_runs_heavy_without_serialization_for(spec: optin_spec) } fn batch_has_both_emit_and_corpus(b: List) -> Bool { @@ -114,14 +181,14 @@ fn batch_has_both_emit_and_corpus(b: List) -> Bool { list_contains_str(xs: fns, target: corpus_sentinel) } -fn witness_emit_host_isolated_from_corpus() -> Bool { - fold(gunbc_ci_floor_batches(), init: true, f: fn(ok, b) { +fn witness_optin_emit_host_isolated_from_corpus() -> Bool { + fold(floor_schedule_for(spec: optin_spec, serialize_heavy: true), init: true, f: fn(ok, b) { ok && !batch_has_both_emit_and_corpus(b: b) }) } -fn witness_emit_host_co_runs_corpus_without_serialization() -> Bool { - fold(gunbc_ci_floor_schedule_without_serialization(), init: false, f: fn(found, b) { +fn witness_optin_emit_host_co_runs_corpus_without_serialization() -> Bool { + fold(floor_schedule_for(spec: optin_spec, serialize_heavy: false), init: false, f: fn(found, b) { found || batch_has_both_emit_and_corpus(b: b) }) } @@ -136,19 +203,23 @@ fn witness_light_gate_profile_allows_corpus_co_residence() -> Bool { test fn ci_floor_plan_witnesses() -> Bool { witness_floor_compile_gate_honest_heavy_profile() && - witness_floor_no_corpus_resource_edge_to_compile_anchor() && - witness_floor_corpus_exclusion_edges_exist() && + witness_live_floor_discovery_tracks_optin_roster() && witness_compile_root_runs_first() && witness_corpus_not_before_compile() && witness_independent_jobs_parallelize() && - witness_discovery_scan_dirs_in_plan() && - witness_scan_dirs_match_layer_authority() && - witness_corpus_has_no_explicit_entries() && - witness_corpus_skip_node_frontier_enabled() && - witness_source_ingest_serialized_after_corpus() && - witness_plan_serializes_heavy_resolves() && - witness_emit_host_isolated_from_corpus() && - witness_emit_host_co_runs_corpus_without_serialization() && + witness_plan_serializes_heavy_resolves() +} + +test fn ci_floor_optin_roster_witnesses() -> Bool { + witness_optin_roster_drives_corpus_presence() && + witness_optin_corpus_carries_exactly_roster_rows() && + witness_optin_execution_partition() && + witness_optin_plan_gates_corpus_after_compile() && + witness_optin_source_ingest_serialized_after_corpus() && + witness_optin_exclusion_edges_track_roster() && + witness_optin_plan_serializes_heavy_resolves() && + witness_optin_emit_host_isolated_from_corpus() && + witness_optin_emit_host_co_runs_corpus_without_serialization() && witness_emit_host_profile_forbids_corpus_co_residence() && witness_light_gate_profile_allows_corpus_co_residence() } diff --git a/src/v2/test/claim/ci_spec_witness_test.dag b/src/v2/test/claim/ci_spec_witness_test.dag index 0c336477a4e..7c41184436c 100644 --- a/src/v2/test/claim/ci_spec_witness_test.dag +++ b/src/v2/test/claim/ci_spec_witness_test.dag @@ -21,6 +21,7 @@ data reflect_gate_type: Symbol = ^Gate data spec_alternate_notice: CiSpec = { gates: [], + witness_entries: [], diff_policy: { base: "upstream/dev", head: "HEAD", diff --git a/src/v2/workflow/affected_set_floor_runner.dag b/src/v2/workflow/affected_set_floor_runner.dag index 16e55941eed..796fa4bc95e 100644 --- a/src/v2/workflow/affected_set_floor_runner.dag +++ b/src/v2/workflow/affected_set_floor_runner.dag @@ -32,11 +32,8 @@ type FloorWitnessRow { function: String } -// Declared on a `test fn` line (or its body) when the witness reads the live tree -// via host intrinsics. The claim_executor host-scaffold classifier honors this -// marker so detection is not limited to Rust substring heuristics alone. -// DISSOLVE-ON: ROADMAP `2-host-scaffold-classifier-defork` — substrate-declared -// `reads_live_tree` disposition on TestClaim rows. +data floor_host_scaffold_witness_marker_note: String = "Declared on a `test fn` line (or its body) when the witness reads the live tree via host intrinsics. The claim_executor host-scaffold classifier honors this marker so detection is not limited to Rust substring heuristics alone. DISSOLVE-ON: ROADMAP `2-host-scaffold-classifier-defork` - substrate-declared `reads_live_tree` disposition on TestClaim rows. (Was a // comment block: the v1 interpreter grammar has no comments, so it broke resolve_floor_runner_context and silently disabled the affected-set skip - every floor run fell back to full corpus. Found during the 2026-07-04 CI witness opt-in inversion.)" + data floor_host_scaffold_witness_marker: String = "floor:host_scaffold" type FloorDiffLineTouch { diff --git a/src/v2/workflow/ci_floor_plan.dag b/src/v2/workflow/ci_floor_plan.dag index 67f6b9bc2a1..c07da7e8d80 100644 --- a/src/v2/workflow/ci_floor_plan.dag +++ b/src/v2/workflow/ci_floor_plan.dag @@ -1,13 +1,13 @@ module v2.workflow.ci_floor_plan -import gunbc.ci_layer_roots { witness_layer_roots, witness_discovery_scan_dirs, witness_exclusion_substrings } +import gunbc.ci_layer_roots { witness_layer_roots, witness_exclusion_substrings } import gunbc.ci_spec { gunbc_ci_spec, CiSpec, Gate, RustMonolithGate, EmitHostGate, LayeringImportsGate, ExtdepsExternalAuthorityGate, DagCompileCleanGate, GeneratedArtifactDriftGate, SourceRootIngestGate, RegenVerifyGate, SelfHostRealizedComparisonGate, EmitDeterminismGate } -import v2.std.algebra { length, list_snoc_item, skip, zip_map } +import v2.std.algebra { filter, length, list_snoc_item, skip, zip_map } import v2.std.collection { List } import v2.std.dependency { DataDependsOn, ResourceDependsOn, DependencyView } import v2.std.node { Atom, Node, TypeNode } @@ -39,6 +39,7 @@ import std.realization_schedule { Runnable, RunnableSingleClaim, RunnableDiscoveryBatch, + ScheduleWitnessEntry, RunnableResourceProfile, runnable_resource_profile, runnable_resource_profile_negligible, @@ -75,14 +76,28 @@ data floor_corpus_node: Node = Node { children: [] } -data floor_grounding_whole_tree_node: Node = Node { - kind: TypeNode { connective: Atom { identity: ^gunbc_floor_grounding_whole_tree } }, - children: [] +data ci_floor_witness_optin_note: String = "CI witness enrollment is OPT-IN (operator inversion, 2026-07-04). The floor runs exactly spec.witness_entries (declared on the GithubActionsCiJob surface of gunbc.commit_workflow.commit_gate_roster), never a tree scan: scan_dirs and discovery_scope_dirs stay empty so the executor's glob path is unreachable from CI, and an EMPTY roster puts no witness-corpus node in the plan at all - the executor's empty-corpus fail-closed diagnostic stays meaningful for accidental emptiness. Tree-wide discovery remains the local path (claim_batch --roster-from-discovery). The former always-on grounding whole-tree node dissolved into this roster: re-enroll it as a CommitWitnessClaim row for src/v2/test/manual/grounding_lens_whole_tree_test.dag. Rows under the execution-corpus scope keep their own node so the measured per-witness RSS width still governs them." + +data execution_corpus_scope_prefix: String = "src/v2/test/claim/execution/" + +fn corpus_witness_entries(spec: CiSpec) -> List { + filter(xs: spec.witness_entries, predicate: fn(w) { + !starts_with(s: w.entry, prefix: execution_corpus_scope_prefix) + }) } -data floor_grounding_whole_tree_entry: String = "src/v2/test/manual/grounding_lens_whole_tree_test.dag" +fn execution_witness_entries(spec: CiSpec) -> List { + filter(xs: spec.witness_entries, predicate: fn(w) { + starts_with(s: w.entry, prefix: execution_corpus_scope_prefix) + }) +} -data floor_grounding_whole_tree_function: String = "grounding_lens_whole_tree_witnesses" +fn corpus_plan_nodes(spec: CiSpec) -> List { + concat( + if length(xs: corpus_witness_entries(spec: spec)) > 0 { [floor_corpus_node] } else { [] }, + if length(xs: execution_witness_entries(spec: spec)) > 0 { [execution_corpus_node] } else { [] } + ) +} fn gate_node(g: Gate) -> Node { match g { @@ -182,27 +197,11 @@ fn corpus_source_roots() -> List { witness_layer_roots } -fn grounding_whole_tree_profile() -> RunnableResourceProfile { - runnable_resource_profile( - heavy_whole_tree_resolve: true, - spawns_host_compiler: false, - memory: runnable_memory_substantial(predicted_peak: gunbc_ci_floor_per_shard_peak_rss_bytes()) - ) -} - -fn grounding_whole_tree_runnable() -> Runnable { - RunnableSingleClaim { - entry: floor_grounding_whole_tree_entry, - function: floor_grounding_whole_tree_function, - profile: grounding_whole_tree_profile() - } -} - -fn corpus_runnable() -> Runnable { +fn corpus_runnable_for(spec: CiSpec) -> Runnable { RunnableDiscoveryBatch { source_roots: corpus_source_roots(), - scan_dirs: witness_discovery_scan_dirs, - explicit_entries: [], + scan_dirs: [], + explicit_entries: corpus_witness_entries(spec: spec), skip_unaffected_node_frontier: true, exclude_substrings: witness_exclusion_substrings, discovery_scope_dirs: [], @@ -224,14 +223,14 @@ fn execution_corpus_spawn_width() -> Int { gunbc_ci_execution_corpus_available_width(each: gunbc_ci_floor_execution_corpus_per_witness_rss) } -fn execution_corpus_runnable() -> Runnable { +fn execution_corpus_runnable_for(spec: CiSpec) -> Runnable { RunnableDiscoveryBatch { source_roots: corpus_source_roots(), scan_dirs: [], - explicit_entries: [], + explicit_entries: execution_witness_entries(spec: spec), skip_unaffected_node_frontier: true, exclude_substrings: [], - discovery_scope_dirs: ["src/v2/test/claim/execution"], + discovery_scope_dirs: [], spawn_width_cap: execution_corpus_spawn_width(), profile: runnable_resource_profile( heavy_whole_tree_resolve: true, @@ -242,7 +241,7 @@ fn execution_corpus_runnable() -> Runnable { } fn floor_nodes(spec: CiSpec) -> List { - fold(spec.gates, init: [floor_corpus_node, execution_corpus_node, floor_grounding_whole_tree_node], f: fn(acc, g) { + fold(spec.gates, init: corpus_plan_nodes(spec: spec), f: fn(acc, g) { list_snoc_item(xs: acc, item: gate_node(g: g)) }) } @@ -266,23 +265,23 @@ fn floor_resource_depends_on(source: Node, dependent: Node) -> DependencyView { } fn floor_corpus_exclusion_resource_edges(spec: CiSpec) -> List { - let after_floor_corpus = [ - floor_resource_depends_on(source: floor_corpus_node, dependent: execution_corpus_node), - floor_resource_depends_on(source: floor_corpus_node, dependent: floor_grounding_whole_tree_node), - floor_resource_depends_on(source: execution_corpus_node, dependent: floor_grounding_whole_tree_node) - ] + let corpus_nodes = corpus_plan_nodes(spec: spec) + let after_floor_corpus = if length(xs: corpus_nodes) > 1 { + [floor_resource_depends_on(source: floor_corpus_node, dependent: execution_corpus_node)] + } else { + [] + } fold(spec.gates, init: after_floor_corpus, f: fn(acc, g) { let node = gate_node(g: g) if floor_resource_anchor_node(node: node) { acc } else if runnable_excludes_corpus_co_residence(profile: gate_runnable_profile(g: g)) { - list_snoc_item( - xs: list_snoc_item( - xs: acc, - item: floor_resource_depends_on(source: floor_corpus_node, dependent: node) - ), - item: floor_resource_depends_on(source: execution_corpus_node, dependent: node) - ) + fold(corpus_nodes, init: acc, f: fn(edges, corpus_node) { + list_snoc_item( + xs: edges, + item: floor_resource_depends_on(source: corpus_node, dependent: node) + ) + }) } else { acc } @@ -300,7 +299,6 @@ fn floor_heavy_resolve_chain_resource_edges(spec: CiSpec) -> List List List { fold( spec.gates, - init: [ - floor_depends_on_compile(dependent: floor_corpus_node), - floor_depends_on_compile(dependent: execution_corpus_node), - floor_depends_on_compile(dependent: floor_grounding_whole_tree_node) - ], + init: fold(corpus_plan_nodes(spec: spec), init: [], f: fn(acc, corpus_node) { + list_snoc_item(xs: acc, item: floor_depends_on_compile(dependent: corpus_node)) + }), f: fn(acc, g) { match g { DagCompileCleanGate => acc @@ -368,10 +364,6 @@ fn floor_dependencies_with(spec: CiSpec, serialize_heavy: Bool) -> List List { - floor_dependencies_with(spec: spec, serialize_heavy: true) -} - fn floor_unrunnable_sentinel() -> Runnable { RunnableSingleClaim { entry: "", @@ -382,11 +374,9 @@ fn floor_unrunnable_sentinel() -> Runnable { fn runnable_for_node(node: Node, spec: CiSpec) -> Runnable { if node == floor_corpus_node { - corpus_runnable() + corpus_runnable_for(spec: spec) } else if node == execution_corpus_node { - execution_corpus_runnable() - } else if node == floor_grounding_whole_tree_node { - grounding_whole_tree_runnable() + execution_corpus_runnable_for(spec: spec) } else { fold(spec.gates, init: floor_unrunnable_sentinel(), f: fn(acc, g) { if node == gate_node(g: g) { gate_runnable(g: g) } else { acc } @@ -400,15 +390,15 @@ fn runnables_for_batch(batch: ExecutorBatch, spec: CiSpec) -> List { }) } -fn floor_schedule_for_dependencies(dependencies: List) -> List> { +fn floor_schedule_for(spec: CiSpec, serialize_heavy: Bool) -> List> { match parallel_executor_plan_from_dependencies( - nodes: floor_nodes(spec: gunbc_ci_spec), - dependencies: dependencies, + nodes: floor_nodes(spec: spec), + dependencies: floor_dependencies_with(spec: spec, serialize_heavy: serialize_heavy), scheduled: [] ) { ExecutorRunComplete { batches: batches, scheduled: _ } => fold(batches, init: [], f: fn(acc, batch) { - list_snoc_item(xs: acc, item: runnables_for_batch(batch: batch, spec: gunbc_ci_spec)) + list_snoc_item(xs: acc, item: runnables_for_batch(batch: batch, spec: spec)) }) ExecutorRunRunnable { batch: _, scheduled: _, remaining: _ } => [[floor_unrunnable_sentinel()]] ExecutorRunBlocked { evidence: _, batches: _, scheduled: _, remaining: _ } => [[floor_unrunnable_sentinel()]] @@ -416,24 +406,26 @@ fn floor_schedule_for_dependencies(dependencies: List) -> List List> { - floor_schedule_for_dependencies(dependencies: floor_dependencies(spec: gunbc_ci_spec)) + floor_schedule_for(spec: gunbc_ci_spec, serialize_heavy: true) } fn gunbc_ci_floor_schedule_without_serialization() -> List> { - floor_schedule_for_dependencies( - dependencies: floor_dependencies_with(spec: gunbc_ci_spec, serialize_heavy: false) - ) + floor_schedule_for(spec: gunbc_ci_spec, serialize_heavy: false) } -fn gunbc_ci_floor_realization_plan() -> RealizationPlan { +fn gunbc_ci_floor_realization_plan_for(spec: CiSpec) -> RealizationPlan { RealizationPlan { target: ci_floor_target_digest objective: RealizationObjective { goals: width_fold_objective_goals() } - schedule: gunbc_ci_floor_schedule_inner() + schedule: floor_schedule_for(spec: spec, serialize_heavy: true) total: cost_account_predicted_zero() } } +fn gunbc_ci_floor_realization_plan() -> RealizationPlan { + gunbc_ci_floor_realization_plan_for(spec: gunbc_ci_spec) +} + fn gunbc_ci_floor_batches() -> List> { gunbc_ci_floor_realization_plan().schedule } @@ -554,6 +546,18 @@ fn floor_plan_at_most_one_heavy_gate_resolve_per_batch(batches: List Bool { + floor_plan_at_most_one_heavy_gate_resolve_per_batch( + batches: floor_schedule_for(spec: spec, serialize_heavy: true) + ) +} + +fn floor_plan_co_runs_heavy_without_serialization_for(spec: CiSpec) -> Bool { + floor_plan_at_most_one_heavy_gate_resolve_per_batch( + batches: floor_schedule_for(spec: spec, serialize_heavy: false) + ) == false +} + fn gunbc_ci_floor_plan_serializes_heavy_resolves() -> Bool { floor_plan_at_most_one_heavy_gate_resolve_per_batch(batches: gunbc_ci_floor_batches()) } @@ -564,8 +568,8 @@ fn gunbc_ci_floor_plan_co_runs_heavy_without_serialization() -> Bool { ) == false } -fn witness_floor_corpus_exclusion_edges_exist() -> Bool { - length(xs: floor_corpus_exclusion_resource_edges(spec: gunbc_ci_spec)) > 0 +fn witness_floor_corpus_exclusion_edges_exist(spec: CiSpec) -> Bool { + length(xs: floor_corpus_exclusion_resource_edges(spec: spec)) > 0 } fn witness_floor_compile_gate_honest_heavy_profile() -> Bool { @@ -573,8 +577,8 @@ fn witness_floor_compile_gate_honest_heavy_profile() -> Bool { && runnable_excludes_corpus_co_residence(profile: gate_runnable_profile(g: DagCompileCleanGate)) } -fn witness_floor_no_corpus_resource_edge_to_compile_anchor() -> Bool { - fold(floor_corpus_exclusion_resource_edges(spec: gunbc_ci_spec), init: true, f: fn(ok, edge) { +fn witness_floor_no_corpus_resource_edge_to_compile_anchor(spec: CiSpec) -> Bool { + fold(floor_corpus_exclusion_resource_edges(spec: spec), init: true, f: fn(ok, edge) { ok && (edge.dependent != floor_compile_root_node()) }) } diff --git a/src/v2/workflow/phase_profile_proof_plan.dag b/src/v2/workflow/phase_profile_proof_plan.dag index e7825769ae4..3fe505172d1 100644 --- a/src/v2/workflow/phase_profile_proof_plan.dag +++ b/src/v2/workflow/phase_profile_proof_plan.dag @@ -1,10 +1,5 @@ module v2.workflow.phase_profile_proof_plan -// Acceptance fixture for GUNBC_FLOOR_PHASE_PROFILE: single-witness plan at spawn_width=1. -// Phase ticks are a process-global last-writer-wins sampler (not per-thread trace); width=1 -// keeps the witness deterministic. Parallel-width heartbeat semantics are covered by the -// module doc in phase_profile.rs. - import std.realization_schedule { Runnable, RunnableSingleClaim, @@ -13,6 +8,8 @@ import std.realization_schedule { import std.measure { HardwareThreadCount, hardware_thread_count } import std.types { List } +data phase_profile_proof_plan_note: String = "Acceptance fixture for GUNBC_FLOOR_PHASE_PROFILE: single-witness plan at spawn_width=1. Phase ticks are a process-global last-writer-wins sampler (not per-thread trace); width=1 keeps the witness deterministic. Parallel-width heartbeat semantics are covered by the module doc in phase_profile.rs. (Was a // comment block: the v1 interpreter grammar has no comments, so claim_executor --plan-entry on this file failed to resolve; found during the 2026-07-04 CI witness opt-in inversion.)" + data phase_profile_gate_entry: String = "dsl/tools/floor_effect_gate_witness.dag" fn phase_profile_proof_batches() -> List> { From 31f63a7dac32e6ce750055622645f9aedb32e415 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 01:45:58 +0000 Subject: [PATCH 02/15] Enroll the first opt-in CI witness: variant_owner_expected_type (single pure-fn canary) Per operator intent: exactly one witness in CI for now, to observe the floor's behavior around a known-tiny payload. The canary is dag/test/claim/variant_owner_expected_type_test.dag :: expected_type_picks_variant_owner_not_alpha_order - a single test fn over self-contained pure functions (no host intrinsics, one std.logic import; measured locally: 3ms resolve / 2-module closure, ~0ms eval), so whatever time the CI witness lane now costs is the floor's fixed overhead, not the witness. Also: run the witness naming-hygiene walk unconditionally in claim_executor's plan path, BEFORE plan evaluation - explicit-entries discovery batches skip the glob scan that used to carry the placement checks, and pre-plan ordering makes a naming violation the cheapest possible failure. Verified by execution: red probe (planted test fn in a non-_test.dag file) fails closed with the placement diagnostic in seconds; green probe on the live tree passes the walk and proceeds to plan eval; the enrolled canary runs green via claim_batch. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- dag/gunbc/commit_workflow.dag | 7 ++++ src/v1/stage0/src/bin/claim_executor.rs | 49 ++++++++++++------------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/dag/gunbc/commit_workflow.dag b/dag/gunbc/commit_workflow.dag index c23dcb5d9b7..ada100b3cbb 100644 --- a/dag/gunbc/commit_workflow.dag +++ b/dag/gunbc/commit_workflow.dag @@ -84,6 +84,13 @@ data commit_gate_roster: List = [ check: CommitSpecGate { gate: RustMonolithGate }, surfaces: [GithubActionsRustTestsJob] }, + CommitCheckEnrollment { + check: CommitWitnessClaim { + entry: "dag/test/claim/variant_owner_expected_type_test.dag", + check_fns: ["expected_type_picks_variant_owner_not_alpha_order"] + }, + surfaces: [GithubActionsCiJob] + }, CommitCheckEnrollment { check: CommitWitnessClaim { entry: doc_reachability_witness_entry, diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 0c93f4e205d..d9f51df4dac 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -1732,6 +1732,29 @@ fn run() -> Result { } }; + // 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: Vec = v1_compiler::cli_run::FLOOR_DISCOVERY_EXCLUDES + .iter() + .map(|s| s.to_string()) + .collect(); + if let Err(msg) = v1_compiler::cli_run::check_floor_filename_hygiene(&source_roots) + .and_then(|_| { + v1_compiler::cli_run::discover_floor_corpus_rows(&source_roots, &[], &excludes) + .map(|_| ()) + }) + { + eprintln!("claim_executor: witness naming hygiene (pre-plan walk): {msg}"); + return Err(ExitCode::from(1)); + } + } + // Install the host-effect trace policy from the .dag authority once, before // discovery threads spawn, so `[file] read` / `[rest]` / `[hermetic:mock]` etc. // are funnelled per `gunbc.output_policy` instead of flooding the floor log. @@ -1766,32 +1789,6 @@ fn run() -> Result { return Err(ExitCode::from(1)); } - // The opt-in witness roster can be empty, removing every DiscoveryBatch from the - // plan — and with it the only CI path through the tree-walk naming hygiene - // (`test fn` outside `*_test.dag`, `__` basenames). A witness must stay NAMEABLE - // even when not enrolled (an unnameable witness could never be opted in), so run - // the same fail-closed walk once here, discarding the roster. Skipped when a - // DiscoveryBatch is scheduled: that batch runs the identical checks itself. - let plan_has_discovery_batch = batches - .iter() - .flatten() - .any(|r| matches!(r, Runnable::DiscoveryBatch { .. })); - if !plan_has_discovery_batch { - let excludes: Vec = v1_compiler::cli_run::FLOOR_DISCOVERY_EXCLUDES - .iter() - .map(|s| s.to_string()) - .collect(); - if let Err(msg) = v1_compiler::cli_run::check_floor_filename_hygiene(&source_roots) - .and_then(|_| { - v1_compiler::cli_run::discover_floor_corpus_rows(&source_roots, &[], &excludes) - .map(|_| ()) - }) - { - eprintln!("claim_executor: witness naming hygiene (zero-enrollment walk): {msg}"); - return Err(ExitCode::from(1)); - } - } - let spawn_width = match eval_spawn_width(&source_roots, &plan_entry, &plan_function) { Ok(w) => w, Err(msg) => { From eb5a23a7b67848e0342e0867bc42e6bad1e5c709 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 01:49:14 +0000 Subject: [PATCH 03/15] Fix clippy absurd_extreme_comparisons on the emitted-Rust error ratchet (pre-existing on main; surfaces on any .rs-touching PR) The ratchet constant deliberately sits at its minimum (0); scoped allow with justification keeps raising the ceiling a one-constant edit instead of hardcoding == 0. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- src/v1/stage0/src/bin/bootstrap_witness.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/v1/stage0/src/bin/bootstrap_witness.rs b/src/v1/stage0/src/bin/bootstrap_witness.rs index 2581af654e7..73db4dd02d8 100644 --- a/src/v1/stage0/src/bin/bootstrap_witness.rs +++ b/src/v1/stage0/src/bin/bootstrap_witness.rs @@ -651,13 +651,19 @@ fn bootstrap_stage0_to_stage1() { check_stderr ); - assert!( - error_count <= EMITTED_RUST_ERROR_RATCHET, - "emitted Rust errors {} exceeds ratchet {} — \ - fix codegen or update EMITTED_RUST_ERROR_RATCHET if increase is justified", - error_count, - EMITTED_RUST_ERROR_RATCHET - ); + // The ratchet is a tunable ceiling that currently sits at its minimum (0), which + // makes `<=` degenerate; keep the ratchet comparison rather than hardcoding `== 0` + // so raising the ceiling stays a one-constant edit. + #[allow(clippy::absurd_extreme_comparisons)] + { + assert!( + error_count <= EMITTED_RUST_ERROR_RATCHET, + "emitted Rust errors {} exceeds ratchet {} — \ + fix codegen or update EMITTED_RUST_ERROR_RATCHET if increase is justified", + error_count, + EMITTED_RUST_ERROR_RATCHET + ); + } let _ = std::fs::remove_dir_all(&stage1_dir); } From 32ec4ec0e6d4e6997b44041bfd17afb13b88bd20 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 02:24:26 +0000 Subject: [PATCH 04/15] Eliminate every degradation arm in floor selection: declared machinery fails immediately Operator decision: 'fail closed' here means DON'T PROCEED, never 'silently run more'. skip_unaffected_node_frontier: true is a declared capability, so every input it needs is required - a failure is a loud typed error: - git diff observation failed -> error (was: run full corpus). The FloorGitDiffOutcome degradation enum is deleted; an absent diff is no longer a representable input state to selection. To run without selection, declare skip_unaffected_node_frontier: false - an explicit bit, not a silent state. - diff->declaration attribution failed -> error (was: run full corpus) - affected-set runner unresolvable -> error naming the declared entry (was: run full corpus - the arm that hid the broken runner for weeks) - same per shard -> error through the shard join (was: run all rows in shard) - precompute_would_skip / per-witness would_skip / frontier-touch fns erroring -> error naming the witness (was: run anyway) - roster row file unreadable -> error (was: silently reclassify as host-scaffold) Rationale: the 'safe' superset fallback conflated broken artifacts with missing observations, hid the defect permanently (no consumer of the eprintln), and at corpus scale 'run more' inverted into fail-open-by-timeout. The runner provisioning, fetch step, and observers are all declared by this repo's own spec, so there is no 'environment' excuse category: a missing input is a bug in a declared step and stops the floor. cargo build green, clippy -D warnings green, fmt clean; floor-scoped lib unit tests and a live red/green probe through a discovery batch are running and any regression they surface lands as a follow-up. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- src/v1/stage0/src/cli_run.rs | 177 ++++++++++++++++------------------- 1 file changed, 83 insertions(+), 94 deletions(-) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 1067e74a623..b404e47b337 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -3687,11 +3687,6 @@ impl Default for DiscoveryCorpusOptions { } } -enum FloorGitDiffOutcome { - ObservationFailClosed { reason: String }, - UnifiedProduced(String), -} - #[derive(Debug, Clone, PartialEq, Eq)] struct FileLineRange { start: i64, @@ -4381,23 +4376,23 @@ fn witness_test_fn_uses_live_host_scan(entry_content: &str, function: &str) -> b false } -fn read_entry_content_for_host_scaffold(entry: &str) -> (String, bool) { - match std::fs::read_to_string(entry) { - Ok(content) => (content, false), - Err(e) => { - eprintln!( - "claim_executor: failed to read entry {entry} for host-scaffold classification ({e}) — fail-closed, treating as host-scaffold" - ); - (String::new(), true) - } - } +fn read_entry_content_for_host_scaffold(entry: &str) -> Result { + std::fs::read_to_string(entry).map_err(|e| { + format!( + "failed to read entry {entry} for host-scaffold classification: {e} — a \ + discovered roster row's file must be readable; no silent reclassification" + ) + }) } -fn discovery_rows_include_host_scaffold(rows: &[DiscoveryRow]) -> bool { - rows.iter().any(|row| { - let (content, read_failed) = read_entry_content_for_host_scaffold(&row.entry); - read_failed || witness_test_fn_uses_live_host_scan(&content, &row.function) - }) +fn discovery_rows_include_host_scaffold(rows: &[DiscoveryRow]) -> Result { + for row in rows { + let content = read_entry_content_for_host_scaffold(&row.entry)?; + if witness_test_fn_uses_live_host_scan(&content, &row.function) { + return Ok(true); + } + } + Ok(false) } fn floor_diff_edits_from_diff_text( @@ -4565,10 +4560,10 @@ fn entry_touches_rerun_frontier( Ok(Some(true)) => return Ok(true), Ok(Some(false)) | Ok(None) => {} Err(msg) => { - eprintln!( - "claim_executor: test_claim_evaluation_touches_rerun_frontier failed ({msg}) — fail-closed, running entry witnesses" - ); - return Ok(true); + return Err(format!( + "test_claim_evaluation_touches_rerun_frontier failed ({msg}) — declared \ + selection machinery must evaluate; no silent run-everything fallback" + )); } } match call_test_claim_fn_bool( @@ -4581,10 +4576,10 @@ fn entry_touches_rerun_frontier( Ok(Some(true)) => return Ok(true), Ok(Some(false)) | Ok(None) => {} Err(msg) => { - eprintln!( - "claim_executor: floor_claim_touches_rerun_frontier failed ({msg}) — fail-closed, running entry witnesses" - ); - return Ok(true); + return Err(format!( + "floor_claim_touches_rerun_frontier failed ({msg}) — declared selection \ + machinery must evaluate; no silent run-everything fallback" + )); } } } @@ -4650,65 +4645,62 @@ pub fn run_discovery_corpus_with_options( return Err("discovery roster produced no rows (empty corpus → fail closed)".to_string()); } set_phase(FloorPhase::Discovery, "discovery-roster"); - let diff_outcome = if options.skip_unaffected_node_frontier { - match floor_git_diff_range() { - Ok(text) => FloorGitDiffOutcome::UnifiedProduced(text), - Err(msg) => { - eprintln!( - "claim_executor: git diff unavailable ({msg}) — fail-closed, running full corpus" - ); - FloorGitDiffOutcome::ObservationFailClosed { reason: msg } - } - } + // No degradation arm: skip_unaffected_node_frontier: true is a DECLARED capability, + // so every input it needs (the git-diff observation, the frontier attribution, the + // affected-set runner) must be present — a failure is a loud typed error, never a + // silent run-everything fallback. To run without selection, declare the flag false. + let diff_text = if options.skip_unaffected_node_frontier { + floor_git_diff_range().map_err(|msg| { + format!( + "git diff observation failed ({msg}) — skip_unaffected_node_frontier: true \ + requires the declared fetch/observe steps to succeed; no silent \ + full-corpus fallback (declare the flag false to run without selection)" + ) + })? } else { - FloorGitDiffOutcome::UnifiedProduced(String::new()) - }; - let line_ranges_by_file = match &diff_outcome { - FloorGitDiffOutcome::ObservationFailClosed { .. } => HashMap::new(), - FloorGitDiffOutcome::UnifiedProduced(text) => parse_unified_diff_line_ranges(text), - }; - let changed_new_lines_by_file = match &diff_outcome { - FloorGitDiffOutcome::ObservationFailClosed { .. } => HashMap::new(), - FloorGitDiffOutcome::UnifiedProduced(text) => parse_unified_diff_changed_new_lines(text), + String::new() }; + let line_ranges_by_file = parse_unified_diff_line_ranges(&diff_text); + let changed_new_lines_by_file = parse_unified_diff_changed_new_lines(&diff_text); let changed_paths: Vec = line_ranges_by_file.keys().cloned().collect(); - let (skip_enabled, diff_edits) = if options.skip_unaffected_node_frontier - && !line_ranges_by_file.is_empty() - { - let frontier_index = build_multi_entry_index(source_roots); - match floor_diff_edits_from_line_ranges( - &frontier_index, - &line_ranges_by_file, - &changed_new_lines_by_file, - ) { - Ok(edits) => (true, edits), - Err(msg) => { - eprintln!( - "claim_executor: node-frontier population fail-closed ({msg}) — running full corpus" - ); - (false, FloorDiffEdits::default()) + let (skip_enabled, diff_edits) = + if options.skip_unaffected_node_frontier && !line_ranges_by_file.is_empty() { + let frontier_index = build_multi_entry_index(source_roots); + match floor_diff_edits_from_line_ranges( + &frontier_index, + &line_ranges_by_file, + &changed_new_lines_by_file, + ) { + Ok(edits) => (true, edits), + Err(msg) => { + return Err(format!( + "node-frontier population failed ({msg}) — the diff-to-declaration \ + attribution is declared selection machinery; no silent full-corpus \ + fallback" + )); + } } - } - } else { - (false, FloorDiffEdits::default()) - }; + } else { + (false, FloorDiffEdits::default()) + }; let floor_runner_ctx = if options.skip_unaffected_node_frontier { match resolve_floor_runner_context(source_roots) { Ok((graph, source_indices)) => { Some(make_eval_context(&graph, source_indices, execution_mode)) } Err(msg) => { - eprintln!( - "claim_executor: floor runner resolve failed ({msg}) — fail-closed, running full corpus" - ); - None + return Err(format!( + "floor runner resolve failed ({msg}) — skip_unaffected_node_frontier: \ + true declares the affected-set machinery ({FLOOR_RUNNER_ENTRY}) and it \ + must resolve; no silent full-corpus fallback" + )); } } } else { None }; let skip_precompute = if skip_enabled { - let host_scaffold_corpus = discovery_rows_include_host_scaffold(&rows); + let host_scaffold_corpus = discovery_rows_include_host_scaffold(&rows)?; match floor_runner_ctx.as_ref() { Some(ctx) => { let precompute = if host_scaffold_corpus { @@ -4731,10 +4723,10 @@ pub fn run_discovery_corpus_with_options( match precompute { Ok(skip) => skip, Err(msg) => { - eprintln!( - "claim_executor: floor precompute_would_skip failed ({msg}) — fail-closed, running precompute" - ); - false + return Err(format!( + "floor precompute_would_skip failed ({msg}) — declared selection \ + machinery must evaluate; no silent fallback" + )); } } } @@ -4801,16 +4793,15 @@ pub fn run_discovery_corpus_with_options( let index = build_multi_entry_index(&roots); let runner = if skip_for_shards { match resolve_floor_runner_context(&roots) { - Ok((graph, source_indices)) => Some(make_eval_context( - &graph, - source_indices, - execution_mode, - )), + Ok((graph, source_indices)) => { + Some(make_eval_context(&graph, source_indices, execution_mode)) + } Err(msg) => { - eprintln!( - "claim_executor: floor runner resolve failed in shard ({msg}) — fail-closed, running all rows in shard" - ); - None + return Err(format!( + "floor runner resolve failed in shard ({msg}) — declared \ + affected-set machinery must resolve; no silent \ + run-everything fallback" + )); } } } else { @@ -4923,7 +4914,6 @@ fn run_discovery_rows( let mut current_entry_frontier_nodes: Vec = Vec::new(); let mut current_entry_closure_files: HashSet = HashSet::new(); let mut current_entry_content: String = String::new(); - let mut current_entry_host_scaffold_fail_closed: bool = false; let whole_tree_published_keys = whole_tree_published_keys.map(Rc::new); for row in rows { if current_entry.as_deref() != Some(row.entry.as_str()) { @@ -4972,9 +4962,7 @@ fn run_discovery_rows( } ctx = Some(entry_ctx); current_entry = Some(row.entry.clone()); - let (content, read_failed) = read_entry_content_for_host_scaffold(&row.entry); - current_entry_content = content; - current_entry_host_scaffold_fail_closed = read_failed; + current_entry_content = read_entry_content_for_host_scaffold(&row.entry)?; } let function_edited = skip_enabled && diff_edits.edited_test_fns.iter().any(|(file, func)| { @@ -4986,8 +4974,8 @@ fn run_discovery_rows( .iter() .any(|file| touched_file_in_import_closure(file, ¤t_entry_closure_files)); let should_skip = if skip_enabled { - let host_scaffold_witness = current_entry_host_scaffold_fail_closed - || witness_test_fn_uses_live_host_scan(¤t_entry_content, &row.function); + let host_scaffold_witness = + witness_test_fn_uses_live_host_scan(¤t_entry_content, &row.function); match floor_runner_ctx { Some(runner_ctx) => { let skip = if host_scaffold_witness { @@ -5012,11 +5000,12 @@ fn run_discovery_rows( match skip { Ok(skip) => skip, Err(msg) => { - eprintln!( - "claim_executor: floor would_skip failed ({msg}) — fail-closed, running {} ({})", + return Err(format!( + "floor would_skip failed for {} ({}): {msg} — declared \ + selection machinery must evaluate; no silent \ + run-everything fallback", row.function, row.entry - ); - false + )); } } } From 73c03e0eb325a96b0041d09d74109ea1c4335f92 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 02:52:32 +0000 Subject: [PATCH 05/15] Floor prelude: resolve the plan entry once, stamp every phase with wall-clock marks The 'gunbc ci' step went silent for 35+ minutes after the release build on the self-hosted runner - all of it pre-witness prelude: naming walk, output-policy install, plan-entry resolve, interpreted scheduler eval (gunbc_ci_floor_batches), then a SECOND resolve of the same plan entry for spawn-width, then another interpreted eval. Nothing printed until all of it finished, so a 30-minute prelude was indistinguishable from a hang. - resolve_entry_graph(plan_entry) now runs once; the hermetic plan eval and the wet spawn-width eval share the resolved graph (the double-paid-compute trap from the floor memoization thread, paid at minutes per resolve). - claim_executor stamps [t+Xs] phase marks: hygiene walk, policy install, plan resolved, plan evaluated, width evaluated / walk starting. The floor log now itemizes its own prelude. eval_plan stays as a resolve+eval wrapper for the --perturb-check path. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- src/v1/stage0/src/bin/claim_executor.rs | 72 ++++++++++++++++++------- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index d9f51df4dac..dec017cdd4f 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -843,28 +843,34 @@ fn run_discovery_batch_node( } } -fn eval_plan( - source_roots: &[String], +fn eval_plan_in_ctx( + plan_ctx: &InterpContext, plan_entry: &str, plan_function: &str, ) -> Result>, String> { set_phase(FloorPhase::Gate, &format!("{plan_entry}::{plan_function}")); - let (plan_graph, plan_indices) = resolve_entry_graph(source_roots, plan_entry) - .map_err(|msg| format!("resolve failed for plan {}:\n{}", plan_entry, msg))?; - let plan_ctx = make_eval_context(&plan_graph, plan_indices, ExecutionMode::Hermetic); - let plan_value = run_value(&plan_ctx, plan_function).map_err(|msg| { + let plan_value = run_value(plan_ctx, plan_function).map_err(|msg| { format!( "plan eval failed ({}::{}): {}", plan_entry, plan_function, msg ) })?; - let batches = batches_from_plan(&plan_value, &plan_ctx) + let batches = batches_from_plan(&plan_value, plan_ctx) .map_err(|msg| format!("malformed plan value: {}", msg))?; - drop(plan_value); - drop(plan_graph); Ok(batches) } +fn eval_plan( + source_roots: &[String], + plan_entry: &str, + plan_function: &str, +) -> Result>, String> { + let (plan_graph, plan_indices) = resolve_entry_graph(source_roots, plan_entry) + .map_err(|msg| format!("resolve failed for plan {}:\n{}", plan_entry, msg))?; + let plan_ctx = make_eval_context(&plan_graph, plan_indices, ExecutionMode::Hermetic); + eval_plan_in_ctx(&plan_ctx, plan_entry, plan_function) +} + fn spawn_width_function_name(plan_function: &str) -> Option { match plan_function { "gunbc_ci_floor_batches" => Some("gunbc_ci_plan_spawn_width".to_string()), @@ -934,17 +940,14 @@ fn read_host_memory_budget_bytes() -> Option { None } -fn eval_spawn_width( - source_roots: &[String], +fn eval_spawn_width_in( + plan_ctx: &InterpContext, plan_entry: &str, plan_function: &str, ) -> Result { let Some(width_fn) = spawn_width_function_name(plan_function) else { return Ok(1); }; - let (plan_graph, plan_indices) = resolve_entry_graph(source_roots, plan_entry) - .map_err(|msg| format!("resolve failed for spawn width {}:\n{}", plan_entry, msg))?; - let plan_ctx = make_eval_context(&plan_graph, plan_indices, ExecutionMode::Wet); let budget_bytes = read_host_memory_budget_bytes().unwrap_or(0); match budget_bytes { 0 => eprintln!( @@ -954,7 +957,7 @@ fn eval_spawn_width( } let budget_arg = i64::try_from(budget_bytes).unwrap_or(i64::MAX); let width_value = run_in_context_with_args( - &plan_ctx, + plan_ctx, &width_fn, &[( Some("memory_budget_bytes".to_string()), @@ -968,7 +971,7 @@ fn eval_spawn_width( plan_entry, width_fn, e ) })?; - hardware_thread_count_from_value(&width_value, &plan_ctx) + hardware_thread_count_from_value(&width_value, plan_ctx) } struct WalkOutcome { @@ -1732,6 +1735,18 @@ fn run() -> Result { } }; + // Coarse wall-clock phase marks: the pre-walk phases (hygiene walk, policy + // install, plan resolve, plan eval, width eval) are interpreter-heavy and used + // to be SILENT — a 30-minute prelude looked identical to a hang. Every phase + // now stamps a line so the floor log itemizes its own time. + let floor_started = Instant::now(); + let phase_mark = |label: &str| { + eprintln!( + "claim_executor: [t+{:.1}s] {label}", + floor_started.elapsed().as_secs_f64() + ); + }; + // 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`, `__` @@ -1754,6 +1769,7 @@ fn run() -> Result { return Err(ExitCode::from(1)); } } + phase_mark("naming-hygiene walk complete"); // Install the host-effect trace policy from the .dag authority once, before // discovery threads spawn, so `[file] read` / `[rest]` / `[hermetic:mock]` etc. @@ -1763,18 +1779,34 @@ fn run() -> Result { // plain-terminal header) from the .dag authority, so the parallel walk folds each // batch's host-effect traces into a collapsible group. v1_compiler::cli_run::install_group_syntax(&source_roots); + phase_mark("output policy + group syntax installed"); if perturb_check { return run_perturb_check(&source_roots, &plan_entry, &plan_function); } - let batches = match eval_plan(&source_roots, &plan_entry, &plan_function) { + // Resolve the plan entry ONCE and evaluate both the batches (hermetic) and the + // spawn width (wet) from the same resolved graph — this resolve was previously + // paid twice back-to-back (the §2 double-paid-compute trap, at minutes each). + let (plan_graph, plan_indices) = match resolve_entry_graph(&source_roots, &plan_entry) { + Ok(resolved) => resolved, + Err(msg) => { + eprintln!("claim_executor: resolve failed for plan {plan_entry}:\n{msg}"); + return Err(ExitCode::from(1)); + } + }; + phase_mark("plan entry resolved"); + + let plan_ctx = make_eval_context(&plan_graph, plan_indices.clone(), ExecutionMode::Hermetic); + let batches = match eval_plan_in_ctx(&plan_ctx, &plan_entry, &plan_function) { Ok(b) => b, Err(msg) => { eprintln!("claim_executor: {msg}"); return Err(ExitCode::from(1)); } }; + drop(plan_ctx); + phase_mark("plan evaluated"); eprintln!( "claim_executor: [{}] executor plan = {} batch(es) from {}::{}", @@ -1789,13 +1821,16 @@ fn run() -> Result { return Err(ExitCode::from(1)); } - let spawn_width = match eval_spawn_width(&source_roots, &plan_entry, &plan_function) { + let width_ctx = make_eval_context(&plan_graph, plan_indices, ExecutionMode::Wet); + let spawn_width = match eval_spawn_width_in(&width_ctx, &plan_entry, &plan_function) { Ok(w) => w, Err(msg) => { eprintln!("claim_executor: {msg}"); return Err(ExitCode::from(1)); } }; + drop(width_ctx); + drop(plan_graph); eprintln!( "claim_executor: spawn_width={} from {}::{}", spawn_width, @@ -1804,6 +1839,7 @@ fn run() -> Result { .as_deref() .unwrap_or("") ); + phase_mark("spawn width evaluated; starting batch walk"); let outcome = run_walk(&source_roots, &batches, spawn_width); match peak_rss_bytes() { From 2e658fffca9034e394900bc03c09762cdb2d2e95 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 03:26:49 +0000 Subject: [PATCH 06/15] CI job timeout: 90 -> 10 minutes (operator inversion of the timeout ratchet) The prior ratchet went 30 -> 50 -> 90, each raise buying headroom for the unfixed resolve cost; at 90 the queue backed up and runs still died at the cap having delivered zero signal. The budget is now a forcing function: the floor must fit it, and the [t+Xs] phase marks itemize exactly which phase does not. Jobs stay red-by-timeout until resolve memoization lands - an honest red, unlike a 90-minute lane that never completed. ci.yml hand-synced with the ci_workflow.dag authority (3 job sites); the generated-artifact drift gate byte-verifies the pairing. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- .github/workflows/ci.yml | 6 +++--- dag/gunbc/ci_workflow.dag | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bc564e741f..a94b20d4ee0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ env: jobs: ci: runs-on: [self-hosted, linux, arm64] - timeout-minutes: 90 + timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v5 @@ -99,7 +99,7 @@ jobs: "$ROOT/target/release/gunbc" run --source-root dag --source-root src/v2 --entry dag/tools/merge_admission_gate.dag --function main rust_tests: runs-on: [self-hosted, linux, arm64] - timeout-minutes: 90 + timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v5 @@ -201,7 +201,7 @@ jobs: deploy_dashboard_srv1: runs-on: [self-hosted, linux, arm64, srv1] needs: [ci] - timeout-minutes: 90 + timeout-minutes: 10 if: github.ref == 'refs/heads/main' && github.event_name == 'push' steps: - name: Checkout diff --git a/dag/gunbc/ci_workflow.dag b/dag/gunbc/ci_workflow.dag index 728803c2f09..1b51d721b9f 100644 --- a/dag/gunbc/ci_workflow.dag +++ b/dag/gunbc/ci_workflow.dag @@ -268,10 +268,10 @@ fn ci_deploy_step(stage: DeployStage) -> Step { } } -data gunbc_ci_job_timeout_policy_minutes: Int = 90 +data gunbc_ci_job_timeout_policy_minutes: Int = 10 data gunbc_ci_job_timeout_policy_disposition: Disposition = Terminal { - reason: "gunbc CI policy (operator, 2026-07-02; interim 2026-07-03): CI jobs time out at 90 minutes. The 30min smoke alarm and subsequent 50min raise both sat below the live floor wall: batch-1 compile-clean alone ran ~48min still in progress when a 50min job cap canceled run 28645912887 (plus ~2min release build overhead in the same step). Measured whole-step wall is ~50min+ on ARM fleet (ROADMAP §3 `2-compile-clean-serial`); 90min is interim headroom until compile-clean is scoped/memoized below 30min again. default_job_timeout_minutes (extdeps/github/actions.dag) still models GitHub's 360min platform default — workflow-layer policy on top." + reason: "gunbc CI policy (operator, 2026-07-04): CI jobs time out at 10 minutes - a deliberate INVERSION of the prior ratchet (30min alarm -> 50min -> 90min, each raise buying headroom for the unfixed resolve cost; at 90min the queue backed up and runs still timed out delivering zero signal). The budget is now a forcing function: the floor must FIT it, and the itemized [t+Xs] phase marks say exactly which phase does not (measured 2026-07-04: plan-closure resolve was 15min+, whole-tree compile-clean ~48min on the ARM fleet - both resolve-dominated; see the floor-shared-compute-memoization plan). Every job stays red-by-timeout until resolve memoization lands; that red is honest, unlike a 90min lane that never completed. default_job_timeout_minutes (extdeps/github/actions.dag) still models GitHub's 360min platform default - workflow-layer policy on top." } fn ci_deploy_job(stage: DeployStage) -> Job { From d4fd494297a41c9219ac5cba0f18276dea1e2cb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 03:58:46 +0000 Subject: [PATCH 07/15] Resolver graph-major design doc: once-per-node module evaluation (operator session 2026-07-04) Names the target architecture for the resolve problem measured during the CI dig-out: request-major private universes -> cheap edge graph + reverse-reachable minimal set + forward once-per-node evaluation (construction, not cache lookup), windowed-frontier memory (interface vs body artifacts), isolation-as-purity, and the full v1/v2 change surface. Staged S1 (union resolve, in-process, ships in the current PR line) / S2a (module nodes + Merkle keys) / S2b (persistence, gated on determinism #5941) / S3 (shared store) so it is not a 10-PR arc. Cross-linked from the M1/M2 memoization doc it supersedes (that doc's own M1 dissolution trigger anticipated exactly this design). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- .../plans/floor-shared-compute-memoization.md | 2 + docs/plans/resolver-graph-major-design.md | 91 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 docs/plans/resolver-graph-major-design.md diff --git a/docs/plans/floor-shared-compute-memoization.md b/docs/plans/floor-shared-compute-memoization.md index 776d8b14d20..c9be2565bd0 100644 --- a/docs/plans/floor-shared-compute-memoization.md +++ b/docs/plans/floor-shared-compute-memoization.md @@ -2,6 +2,8 @@ **Status:** M1 LANDED + verified (2026-07-02). M2 remains a forward-only tracker (no present-day sharing opportunity — see §3). This file is no longer a pre-implementation sketch; it is the M1 receipt + M2 open-thread record. Any M2 code still returns to the operator for approval. +**SUCCEEDED BY (2026-07-04):** [resolver graph-major design](resolver-graph-major-design.md) — the module-grained generalization this doc's own M1 dissolution trigger anticipated ("dependency-batched resolve"). M1's entry-keyed memo and the dormant #4867 entry-keyed disk cache are both the wrong-unit special case measured there (entry-grained sharing captures ~105s of a multi-thousand-second redundancy; the unit must be the module). M2's `RunnableCompile` shape is absorbed as that design's module node; its determinism gate (#5941) now scopes only the persistent tier. + **M1 — within-walk resolve memo — is implemented as construction, not the sketch's proposed validation.** `claim_executor.rs` `run_walk` maintains a cross-batch `walk_memo: HashMap` keyed directly off `RunnableResourceProfile.heavy_whole_tree_resolve` (see §3-M1's `use_walk_memo`), so a heavy whole-tree resolve that runs isolated per batch is **unwritable** — stronger than the sketch's proposed `ResolveScope = Isolated | Shared` field, which would have needed a lens to forbid the heavy+isolated combination. The construction guarantee lives at the carrier; no `ResolveScope` field and no lens were added. Receipt: two purity oracles in the `claim_executor` bin tests — `memo_warm_cold_results_are_identical` (warm memo path is byte-identical to the cold `run_shared_entry_claims` path — the §5 purity oracle) and `memo_deduplicates_resolve_count` (resolve fires exactly once across batch boundaries; goes RED if the memo is bypassed). Run: `cargo test -p v1-compiler --bin claim_executor memo_` — 2 pass, 0.37s (2026-07-02). **Grounded 2026-06-29.** Root measurement: 537s clean-tree compile. Design principle: DESIGN.md §2 (minimize redundancy), §5 (correctness by construction, not validation). diff --git a/docs/plans/resolver-graph-major-design.md b/docs/plans/resolver-graph-major-design.md new file mode 100644 index 00000000000..e3090b9ffa2 --- /dev/null +++ b/docs/plans/resolver-graph-major-design.md @@ -0,0 +1,91 @@ +# Resolver graph-major design — once-per-node module evaluation + +**Status:** design draft, 2026-07-04, from the CI dig-out session (PR #6232). Operator direction: aggressive — the request-major resolver is the top time-waster in the system. This doc names the target architecture, the guard rails, and the change surface in both trees; §7 splits it into a ship-now stage and a gated tail so it is NOT a 10-PR arc. + +DESIGN refs: §2 (minimize redundancy — one concept, every scale), §5 (correctness by construction: once-per-node by *schedule*, not by cache lookup), §6 (denominate in displaced cost). Supersedes-and-absorbs: [floor-shared-compute-memoization](floor-shared-compute-memoization.md) (M1 landed there is the entry-grained special case of this design; its own dissolution trigger — "dependency-batched resolve" — is this document). + +--- + +## 0. The problem, measured (2026-07-04 receipts) + +- Resolution is request-major: every entry gets a private universe. `resolve_entry_graph(entry)` walks the entry's import closure backward and re-parses + re-infers every module in it, sharing nothing with any other resolve — not across entries in one process, not across processes in one run, not across runs. +- Five-entry census (static import closures): `unicode_xid` 2 modules · canary 2 · `realization_schedule_witness` 11 · `commit_workflow_witness` 163 · `ci_floor_plan` 188. Pairwise overlap `commit_workflow ∩ ci_floor_plan` = **160 shared modules**. Sum if resolved separately = 366; distinct = **194**. At n=5 the redundancy is already 1.9×; at the corpus's ~543 witness entries the shared std/spec prefix is re-inferred hundreds of times per run. +- Cost is wildly non-uniform per module: 2-module closure = **7ms**; 11-module closure (generic-heavy `std.realization_schedule` prefix) = **killed at 5 minutes** (loaded box, ≤2.5× inflation). The expensive modules are exactly the widely-shared ones. (Non-uniformity itself is a separate lane: see resolver-pathology-profile-receipt.md — graph-major removes the *redundancy* factor, not a pathological per-module cost.) +- Fleet numbers: floor prelude (plan-closure resolve ×2 + interpreted scheduler evals) ran **35+ minutes silent** before the first output line; whole-tree compile-clean ≈ **48 min**. CI job timeout is now **10 minutes** (operator inversion, forcing function) — the floor cannot fit it until this design lands. +- Prior mechanisms and why they underdelivered — all the same lesson, *wrong unit / wrong key / wrong medium*: + - **M1 `walk_memo`** (landed #6008): keyed by **entry**; deduplicates the same gate entry across batches (~105s/run). Zero cross-entry sharing — 543 distinct witness entries get 543 private universes regardless. + - **`resolved_graph_cache.rs`** (#4867, dormant): keyed by **entry + compiler binary** (cold on every commit), serialized as **monolithic multi-GiB JSON** (unusable when warm). + - **`SharedClaims`** (within-batch) and the resolver's `seen` set (within-one-closure): correct, but scoped below the redundancy. + +## 1. The inversion: request-major → graph-major + +Current shape: "run e" → walk backward from e, build private universe, discard. "run f" → repeat the shared prefix. + +Target shape (operator's formulation, 2026-07-04): + +1. **Edge graph first (cheap, text-level).** One pass over the tree extracts module→import edges — milliseconds; the whole graph (~5.5k nodes) trivially fits in memory. This artifact exists (`build_multi_entry_index` host-side; `module_graph`/`decl_index` model-side). +2. **Minimal set up front.** Given requested targets {e, f, …}: union of reverse-reachability over the edge graph → the induced subgraph → topological order. (Sets, not paths.) +3. **Forward, once per node.** Evaluate module nodes in dependency order; each node consumes its direct dependencies' **results**; requests are just leaf nodes. "Resolve a→d twice" becomes **unrepresentable** — a node appears once in the schedule. This is the §5 construction rung; a memoized backward resolver is the validation rung (the redundant recompute stays writable, one cache-key bug from returning). +4. **The kernel already exists one layer up.** `parallel_executor_plan_from_dependencies` + `RealizationPlan` schedule the CI floor's eleven gates exactly this way. Module inference is the same concept at a different scale (§2: one kernel, N workloads); this design points the existing scheduler at module nodes instead of inventing a resolver-private one. + +## 2. The windowed frontier (memory model) + +Holding "the whole graph" means holding the **edges** (cheap facts). Evaluating node `d` needs exactly: `d`'s source + the **results** of `d`'s direct dependencies. Not a→c's internals, not their inference traces. When `b`'s last dependent completes, `b`'s working state is dropped; what survives is its compact keyed result. Peak memory ≈ frontier width × result size, not closure size. + +Two immutable artifacts per node (the rustc `rmeta`/rlib split): +- **ModuleInterface** — exported type/fn signatures and data heads. This is all *inference of dependents* may read (the §3 rename test: below-boundary representation is opaque). Small; participates in dependents' keys and in the evaluation window. +- **ModuleBody** — the evaluable form of the module's definitions. Needed only at *interpretation* time (the interpreter executes imported fns); loaded on demand, never part of the inference window. + +## 3. Isolation = purity, not privacy + +In a dependency DAG, influence flows one way: e and f **cannot** affect d's meaning; if they could, that is a bug (a back-edge or impure evaluation) to fail on loudly, never a reason for private universes. The historical fresh-universe-per-witness guarded against *incidental shared mutable state*, not legitimate influence. The discipline that makes sharing sound: node results are **immutable values keyed by content** — then evaluation order is unobservable in any result, and any counterexample is the cache-impurity failure mode (DESIGN recurring list), walled by the oracles in §6. + +Key shape (Merkle): `key(m) = H(source_bytes(m), key(dep_1), …, key(dep_n), resolver_identity)`. Keys ground through `std.content_hash` — and this is the dissolution site for the §3 fnv1a64 dual-surface thread (v1 `atom_identity_hash`/`hash_combine` vs `std.content_hash`: module keys are the v2-carrier consumer that thread was waiting for). `resolver_identity` (compiler binary hash) stays in the key until resolve semantics are explicitly versioned — honest (never serves a stale-semantics artifact) at the cost of cross-commit reuse; removing it is a later, separately-justified step. **In-process sharing (S1, §7) needs none of this** — within one process, one tree snapshot, purity of resolve is the same assumption M1 already shipped on. + +## 4. Change surface — v1 (host, `src/v1/stage0`) + +| Site | Today | Becomes | +|---|---|---| +| `cli_run.rs run_discovery_corpus_with_options` / `run_discovery_rows` | per-row `resolve_entry_with_index_for_discovery_corpus` — one private closure per entry file (~543/run tree-wide) | rows are leaves: one shared resolution of the union closure; per-entry contexts assemble over shared immutable module facts | +| `cli_run.rs resolve_entry_graph` and callers (`resolve_floor_runner_context`, `floor_git_diff_range`→`floor_diff_observe`, `install_output_policy`, `install_group_syntax`) | 4–5 separate private resolves per `claim_executor` process | consumers of one process-level store; union-resolved once | +| shard threads in `run_discovery_corpus` | re-`build_multi_entry_index` + re-resolve runner context **per thread** | share the immutable store (needs `Rc`→`Arc` on resolved artifacts, or evaluate-prefix-then-fanout; decide early — this is the one real engineering constraint) | +| `claim_executor.rs run_walk` `walk_memo` (M1) | entry-keyed context memo | dissolves into the store (M1's own dissolution trigger) | +| `claim_batch.rs` discovery branch | per-row serial resolves | same store; the local corpus path gets the same collapse | +| `v1_compiler_compile.rs compile_to_resolved` | whole-closure monolithic resolve+infer | **the deep cut (S2 only):** per-module parse+infer consuming dep ModuleInterfaces; `ResolvedGraph` becomes an assembled view. S1 does NOT touch this — it changes how often the monolith runs, not its internals | +| `resolved_graph_cache.rs` (#4867) | dormant entry-keyed JSON cache | deleted, superseded by the per-module store (S2) | +| `v1_interpreter` interior caches | per-context | must key per-context or per-content; covered by the purity oracle | + +## 5. Change surface — v2 (substrate, `src/v2` + `dag`) + +| Site | Role | +|---|---| +| `std.realization_schedule` | inhabit the staged `RunnableCompile` node (M2's shape) as the module-node workload: `{ module, source_key, dep_keys, profile }`; the resolver plan is a `RealizationPlan` over these — modeled in v2, realized by the v1 host, same seam as the CI floor | +| `v2.workflow.executor` | unchanged — the point is reuse of `parallel_executor_plan_from_dependencies` for module nodes | +| `v2.std.dependency` (`DependencyView`) | module import edges as rows — same vocabulary as floor edges | +| `module_graph` / `decl_index` (v2) | the model-side edge-graph authority; a witness asserts host `build_multi_entry_index` edges == model edges (kills the dual representation) | +| `std.content_hash` | key authority (fnv1a64 convergence thread lands here) | +| `gunbc.ci_floor_measurement` / `ci_budget_tree` | width/memory rows for the module lane: peak = frontier × interface size | +| `v2.std.determinism` (#5941) | gates ONLY the persistent tier (S2b): serving stored artifacts across runs requires proven determinism; in-process S1 does not | + +## 6. Oracles & red controls (all execute) + +1. **Purity oracle:** graph-major result == request-major result, byte-identical, for a sampled witness set (extend `memo_warm_cold_results_are_identical`). Red control: plant an interior-mutable leak (order-dependent memo) → oracle goes red. +2. **Once-per-node receipt:** evaluation counter == distinct node count for the run; a private re-resolve sneaking back turns the receipt red (extends `memo_deduplicates_resolve_count`). +3. **Collision honesty:** co-residence of previously never-co-resolved modules can surface name collisions (see resolver-type-name-collision-wall.md). The purity oracle catches divergence; a collision is a **loud typed error**, never a silently different resolution. +4. **Planted-staleness (S2b only):** mutate a dep after key computation → keyed lookup must miss, never serve. +5. **Budget receipt:** the `[t+Xs]` floor phase marks (landed 2026-07-04) are the standing measurement; the 10-minute job timeout is the acceptance bar. + +## 7. Staging — deliberately NOT a 10-PR arc + +**S1 — union resolve, in-process (ship in the current PR line; no substrate migration, no persistence, no determinism dependency).** Stop fragmenting demand: one `claim_executor`/`claim_batch` process resolves the union of everything it will need — prelude entries + all roster rows — once, forward, and every consumer assembles from the shared facts. This is the operator's "walk from a once, then evaluate e, then f" implemented with the existing monolithic resolver (the union closure IS one resolve; the `seen` set already deduplicates inside it). Displacement: N overlapping resolves per process → 1; the corpus's per-entry bill collapses to (union once) + (cheap per-entry assembly). Risks owned: name-collision surfacing (oracle §6.3), `Rc` thread boundary (§4). + +**S2a — module nodes + Merkle keys, still in-process.** Split `compile_to_resolved` at module boundaries (ModuleInterface/ModuleBody), schedule via `RunnableCompile` nodes, get frontier-window memory and per-module parallel inference (topo antichains × Altra cores — the 48-minute whole-tree resolve is single-threaded today). The one genuinely deep change (modular inference). + +**S2b — persistence across runs (gated on #5941).** Store node artifacts by Merkle key; a PR's run evaluates changed modules + dependents + uncached leaves. This — not S1 — is what makes the 10-minute budget comfortable rather than merely reachable; S1 makes a run cost ~1× union-resolve instead of ~N×, S2b makes warm runs cost ~Δ. + +**S3 — shared store across runner slots.** A Realization handler change (local disk → shared), no new semantics. + +## 8. Dissolution triggers + +- S1 dissolves into S2a when module nodes land (the union monolith becomes the degenerate one-batch plan). +- This doc dissolves into the carriers when: the resolver plan is expressed as `RealizationPlan` rows, the oracles of §6 are enrolled witnesses, and the floor fits its declared budget — at which point DESIGN's open-thread bullet and this file are redundant with the model. From 57495117e9e89117770ecf9ce41316f237520105 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 04:19:49 +0000 Subject: [PATCH 08/15] Hand-sync generated docs to their edited authorities (DESIGN.md CI bullet, ci-selection-vs-scheduling section 7) The interpreted main_wet regeneration was killed twice by container restarts (~2 CPU-hours each attempt, itself a receipt for the resolver-graph-major design). The generated outputs are hand-applied to byte-match what the emitters produce from the already-committed authorities (dag/gunbc/design_document.dag, dag/gunbc/plans/ci_selection_vs_scheduling.dag); the GeneratedArtifactDriftGate adjudicates exactness once the floor fits its 10-minute budget. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- DESIGN.md | 2 +- docs/plans/ci-selection-vs-scheduling.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/DESIGN.md b/DESIGN.md index 1ebc6c3d813..5ae8b484ead 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -92,4 +92,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: `git config core.hooksPath .githooks` (pre-push runs `cargo fmt`) -- 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_batches`. 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 **discovery is tree-wide and marker-driven** (roster derived from `gunbc.ci_layer_roots`: `witness_layer_roots = [src/v2, dag]`, `witness_discovery_scan_dirs = [dag/test/claim]`): the source-root pass walks every `*_test.dag` under **both** trees and auto-enrolls each `test fn` — fail-closed, a `test fn` in a non-`_test.dag` file is a hygiene violation, so a witness cannot silently hide; the scan-dir pass adds `unified_claim_*` BoolWitnessClaim decls. So a new witness is floor-covered by *naming it* `_test.dag` with a `test fn`, not by hand-enrolling it (`discover_floor_corpus_rows` in `cli_run.rs`). Batch-2 also carries the effectful gates: rust fmt/clippy monolith gate (when a `.rs` changes), emit-host MVP smokes, layering-imports scan + perturb receipts, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`. 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_batches`. 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 is OPT-IN** (operator inversion 2026-07-04): the floor runs exactly `CiSpec.witness_entries` — `CommitWitnessClaim` rows on the `GithubActionsCiJob` surface of `gunbc.commit_workflow.commit_gate_roster`, projected by `project_ci_floor_witness_entries` — and an **empty roster puts zero witness-corpus nodes in the plan**. Existence ≠ enrollment: the tree-wide glob roster grows quadratically with the corpus (a witness is definable between any two pipeline segments) and had reached the 90-min timeout — maximum cost, zero delivered signal, every merge fail-open. 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 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`). Dissolve-on: affected-set selection (`v2.lens.affected_set`) + floor memoization make per-PR selection-by-affectedness affordable; enrollment then returns to discovery shrunk by the affected set. Batch-2 also carries the effectful gates: rust fmt/clippy monolith gate (when a `.rs` changes), emit-host MVP smokes, layering-imports scan + perturb receipts, source-root-ingest, and the `ci.yml` drift+parse gate (ci.yml == `gunbc ci` output). The compile-clean gate is `--target dag`. Parse is grammar-owned: `ingest_yaml_source` (`dag/extdeps/languages/yaml/ingest.dag`), no shell/host parser. diff --git a/docs/plans/ci-selection-vs-scheduling.md b/docs/plans/ci-selection-vs-scheduling.md index b179f93b477..5592e24c6a7 100644 --- a/docs/plans/ci-selection-vs-scheduling.md +++ b/docs/plans/ci-selection-vs-scheduling.md @@ -43,6 +43,10 @@ Affected-set and parallelization-by-realization are the **same work from two end §4 selection produces the must-run set; §1 scheduling runs **that** set affordably (parallelism / cache) and runs the **complement** on a backstop cadence. Selection decides *membership*; scheduling decides *cadence / placement / width*. Cost lives only on the scheduling side. +## 7. 2026-07-04 — the run-all baseline stood down IN CI (opt-in inversion) + +Step 1's "sound baseline" stopped being a baseline the day the floor stopped *completing*: at ~1,551 discovered witnesses the composed pass hit the 90-minute job timeout on every run — maximum cost, **zero delivered signal**, and every merge effectively fail-open (a gate that always times out gates nothing). A run-all that never finishes is not a superset of the affected set; it is the empty set billed at full price. Operator decision: invert to **declared opt-in enrollment** — `CiSpec.witness_entries`, projected from `CommitWitnessClaim` rows on the `GithubActionsCiJob` surface of `commit_gate_roster`; an empty roster puts zero witness-corpus nodes in the floor plan. This is NOT the §0 cost-routed selection this doc rejects: no affected test is demoted by expense, because *no* witness currently gates by discovery — the inversion replaces an always-red-by-timeout gate with an honest, completing floor (gates + declared witnesses), and the discovery roster remains the local path (`claim_batch --roster-from-discovery`). The §3 ladder is unchanged as the *destination*: land affected-set selection + floor memoization, then re-enroll the corpus through the affected set and dissolve the opt-in roster (dissolve-on marker lives on `ci_witness_optin_inversion` in `gunbc.commit_workflow`). Found during the inversion: `resolve_floor_runner_context` had been failing on a comment block the v1 grammar cannot parse (`affected_set_floor_runner.dag`), so the affected-set skip silently fell back to full-corpus on every run — step 2 was dead in CI while the doc described it as CI-enrolled. + ## Dissolution trigger (DESIGN §6) Delete this doc when the per-PR gate is the affected set (step 2 landed on top of #5427's baseline), the nightly cadence is the full-corpus backstop + non-hermetic residue (no cost-based selector), and the completeness lens runs executably — at which point the carriers (the lens + the CI-gen spec) state the policy and this framing is redundant. From 14ba3c5d548a587bbb64ab6346c08231f2166981 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 04:36:49 +0000 Subject: [PATCH 09/15] claim_batch: honor GUNBC_FLOOR_PHASE_PROFILE (same install as claim_executor) Without it, claim_batch diagnostics cannot attribute time to resolve/typecheck/eval phases - a 20-minute silent resolve is uninterpretable, and the resolver investigation needs per-phase receipts. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- src/v1/stage0/src/bin/claim_batch.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/v1/stage0/src/bin/claim_batch.rs b/src/v1/stage0/src/bin/claim_batch.rs index a3d9f65d2b0..ef0cdb9a694 100644 --- a/src/v1/stage0/src/bin/claim_batch.rs +++ b/src/v1/stage0/src/bin/claim_batch.rs @@ -456,6 +456,10 @@ fn run() -> Result { // Funnel host-effect traces per the .dag output policy (see claim_executor). v1_compiler::cli_run::install_output_policy(&source_roots); + // GUNBC_FLOOR_PHASE_PROFILE support (same as claim_executor): without this, + // claim_batch diagnostics cannot attribute time to resolve/typecheck/eval + // phases — a 20-minute silent resolve is uninterpretable. + let _phase_profile = v1_compiler::cli_run::PhaseProfile::install_from_env(); let (entry_groups, discovery_notice) = if let Some(disc) = parsed.discovery { if let Err(e) = check_floor_filename_hygiene(&source_roots) { From 40adccaa4598ad3f653cfbc4933603dc4ec073e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 04:52:58 +0000 Subject: [PATCH 10/15] Resolver design: phase verdict (typecheck-dominant) + one-scheduler-no-special-cases section; per-module typecheck attribution Instrumented, isolated measurement: the 11-module closure finishes parse+resolve+normalize in ~1.1s then sits in typecheck 13+ minutes - inference is ~99.9% of cold resolve cost. Reprioritizes the plan: pathology lane first (per-module [typecheck-attribution] lines now name any module over 2s), unification (module obligations under the existing executor) second, union-of-global-passes demoted to a ~1s-class cut. Adds the operator's formulation as design section 1b: resolution is the system's one special case - the Bazel-shaped executor schedules eleven coarse gates while the dominant work hides in one opaque recursive host call; module resolution IS dependency execution over the same substrate. Maps the external witness-frontier-cursor vocabulary onto existing authorities (Runnable/RunnableCompile, Realization content-hash key + EffectShape, executor batches, receipts) per the section-3 anti-nickname discipline, and adopts the four-terminal-state invariant. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- docs/plans/resolver-graph-major-design.md | 20 ++++++++++++++++++++ src/v1/stage0/src/cli_run.rs | 11 +++++++++++ 2 files changed, 31 insertions(+) diff --git a/docs/plans/resolver-graph-major-design.md b/docs/plans/resolver-graph-major-design.md index e3090b9ffa2..9377d878b98 100644 --- a/docs/plans/resolver-graph-major-design.md +++ b/docs/plans/resolver-graph-major-design.md @@ -11,6 +11,7 @@ DESIGN refs: §2 (minimize redundancy — one concept, every scale), §5 (correc - Resolution is request-major: every entry gets a private universe. `resolve_entry_graph(entry)` walks the entry's import closure backward and re-parses + re-infers every module in it, sharing nothing with any other resolve — not across entries in one process, not across processes in one run, not across runs. - Five-entry census (static import closures): `unicode_xid` 2 modules · canary 2 · `realization_schedule_witness` 11 · `commit_workflow_witness` 163 · `ci_floor_plan` 188. Pairwise overlap `commit_workflow ∩ ci_floor_plan` = **160 shared modules**. Sum if resolved separately = 366; distinct = **194**. At n=5 the redundancy is already 1.9×; at the corpus's ~543 witness entries the shared std/spec prefix is re-inferred hundreds of times per run. - Cost is wildly non-uniform per module: 2-module closure = **7ms**; 11-module closure (generic-heavy `std.realization_schedule` prefix) = **killed at 5 minutes** (loaded box, ≤2.5× inflation). The expensive modules are exactly the widely-shared ones. (Non-uniformity itself is a separate lane: see resolver-pathology-profile-receipt.md — graph-major removes the *redundancy* factor, not a pathological per-module cost.) +- **Phase verdict (isolated, instrumented, 2026-07-04):** with `GUNBC_FLOOR_PHASE_PROFILE` wired into `claim_batch`, the 11-module closure enters `phase=typecheck` at **t≈1.1s** — parse + name-resolution + normalize together are ~1 second — and then sits in typecheck for **13+ minutes and counting**. Inference is ~99.9% of the cold cost; the per-entry uncached global passes are a ~1-second item. Consequences: (a) the pathology lane (which module, why) is the top prize — a `[typecheck-attribution] module=… ms=…` line now names any module over 2s; (b) S1's union-of-global-passes is demoted to a minor cut; (c) the in-process `typed_module_cache` already amortizes the dominant phase across entries *within one index* — the surviving redundancy is per-shard, per-subprocess, per-run cold starts of that cache. Open oddity to settle: a 188-module *superset* closure resolved in under ~10 minutes (contended) while this 11-module subset exceeds 20 — plausibly the discovery path's *advisory* typecheck gate vs the single-entry *strict* gate checking different amounts; one controlled A/B settles it. - Fleet numbers: floor prelude (plan-closure resolve ×2 + interpreted scheduler evals) ran **35+ minutes silent** before the first output line; whole-tree compile-clean ≈ **48 min**. CI job timeout is now **10 minutes** (operator inversion, forcing function) — the floor cannot fit it until this design lands. - Prior mechanisms and why they underdelivered — all the same lesson, *wrong unit / wrong key / wrong medium*: - **M1 `walk_memo`** (landed #6008): keyed by **entry**; deduplicates the same gate entry across batches (~105s/run). Zero cross-entry sharing — 543 distinct witness entries get 543 private universes regardless. @@ -28,6 +29,25 @@ Target shape (operator's formulation, 2026-07-04): 3. **Forward, once per node.** Evaluate module nodes in dependency order; each node consumes its direct dependencies' **results**; requests are just leaf nodes. "Resolve a→d twice" becomes **unrepresentable** — a node appears once in the schedule. This is the §5 construction rung; a memoized backward resolver is the validation rung (the redundant recompute stays writable, one cache-key bug from returning). 4. **The kernel already exists one layer up.** `parallel_executor_plan_from_dependencies` + `RealizationPlan` schedule the CI floor's eleven gates exactly this way. Module inference is the same concept at a different scale (§2: one kernel, N workloads); this design points the existing scheduler at module nodes instead of inventing a resolver-private one. +## 1b. One scheduler, no special cases (operator formulation, 2026-07-04) + +The root defect, named: **resolution is the system's one special case.** The repo already owns a Bazel-shaped dependency executor — nodes, `DependencyView` edges, antichain batches, width from the budget tree — and uses it to schedule eleven coarse CI gates. Meanwhile the work that is 99.9% of the wall clock (module typecheck obligations) hides inside one opaque recursive host call, on one core, invisible to scheduling, receipts, coalescing, and budgets. "DAG processing for the DAG itself" is not a metaphor: module resolution IS dependency execution over the same substrate, and exempting it from the system's own model is the §7 recursion left unfinished (the compiler must be an ordinary substrate fact; today its front half isn't). + +Vocabulary mapping (§3 discipline — the concepts already have names here; do not mint nicknames): + +| external formulation | existing authority | +|---|---| +| Obligation (node × input × context, `demanded_by` witnesses) | `Runnable` + a demand set; the artifact-bearing variant is M2's staged `RunnableCompile` | +| ExecutionKey (node, input digest, artifact digest, config, effect shape) | the Realization content-hash key + `EffectShape` — "one kernel, N handlers" applied to compile steps | +| antichain frontier / cursor | the executor's batches (`parallel_executor_plan_from_dependencies`) | +| typed Receipt instead of log-line truth | receipts + the merge-admission stamp; the `[t+Xs]` phase marks are the interim form | +| "witnesses are assertions over graph cuts; run the graph, not the witnesses" | the end-state: witnesses check materialized node values at declared cuts; today's corpus witnesses are leaf assertions whose heavy shared "path" is module resolution itself — so the first obligation corpus the unified cursor runs is **modules** | +| refusal instead of degradation; budget-exceeded = error, not fallback | landed 2026-07-04 (degradation arms deleted; 10-min budget as forcing function) | + +Adopted invariant (four terminal states, no fifth): every enrolled witness ends **Checked** (receipt) | **Skipped** (selector proof) | **Refused** (machinery/input absent — loud) | **Not enrolled**. "Silently fell back to something else" is the state that destroyed the gate and is now unrepresentable on the paths converted so far. + +The one structural primitive the executor lacks for unification (named by the M2 doc already): edges carry no payload — there is no obligation that *produces an artifact consumed by dependents*. Module obligations need exactly that (ModuleInterface flowing along edges). That is S2a's real content. + ## 2. The windowed frontier (memory model) Holding "the whole graph" means holding the **edges** (cheap facts). Evaluating node `d` needs exactly: `d`'s source + the **results** of `d`'s direct dependencies. Not a→c's internals, not their inference traces. When `b`'s last dependent completes, `b`'s working state is dropped; what survives is its compact keyed result. Peak memory ≈ frontier width × result size, not closure size. diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index b404e47b337..9bb234983b1 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -1067,12 +1067,23 @@ fn reconcile_with_typed_cache( let tc_result = match cached { Some(hit) => hit, None => { + let module_tc_started = std::time::Instant::now(); let computed = v1_compiler_infer::typecheck_module( resolved.clone(), module_index.clone(), source_indices.clone(), intern_table.clone(), ); + // Per-module attribution for the typecheck-dominant resolves measured + // 2026-07-04 (a closure sat in typecheck for 13+ min after ~1s of + // parse+resolve+normalize). Threshold keeps the floor log quiet; + // anything over it is a pathology-lane candidate by name. + let module_tc_ms = module_tc_started.elapsed().as_millis(); + if module_tc_ms >= 2_000 { + eprintln!( + "[typecheck-attribution] module={mod_name} ms={module_tc_ms}" + ); + } typed_cache .borrow_mut() .insert(mod_name.clone(), computed.clone()); From eb3955b73504ae62fd7f18cf3b8443274f3a5e5a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 05:11:20 +0000 Subject: [PATCH 11/15] S1a: process-level resolve store - fixed-entry machinery resolves once per process First implementation increment of the resolver graph-major design (and of the stratification rule: resolution is declared shared work, not ambient per-consumer control flow). A thread-local store keyed by (source_roots, entry) now serves the floor runner context, the diff observer, the output policy, the group syntax, the wet-hermetic roster prefix, and the executor's plan entry - each was previously a private resolve, re-paid per consumer and per discovery batch within one process. Failure semantics unchanged: a miss resolves exactly as before, including the typed error path; the store never converts an error into a fallback. Receipt: process_resolve_store_dedupes_repeat_resolve - Rc identity on the second resolve proves zero recompute (0.00s, fixture tree under target/). Thread-local by design: resolved graphs are Rc-based (not Send); shard threads keep their own store rather than smuggling Rc across threads. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- src/v1/stage0/src/bin/claim_executor.rs | 9 +-- src/v1/stage0/src/cli_run.rs | 96 ++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index dec017cdd4f..df96013eb16 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -11,9 +11,10 @@ use std::time::Instant; use v1_compiler::cli_run::workspace_root; use v1_compiler::cli_run::{ compute_histogram_data, compute_witness_timing_rows, make_eval_context, resolve_entry_graph, - run_claim, run_discovery_corpus_with_options, run_value, set_phase, top_n_slowest_witnesses, - ClaimOutcome, DiscoveryCorpusOptions, DiscoverySummary, FloorPhase, HistogramData, - PhaseProfile, TimingPercentiles, WitnessTimingRow, DEFAULT_SLOWEST_WITNESS_ATTRIBUTION_N, + resolve_entry_graph_shared, run_claim, run_discovery_corpus_with_options, run_value, set_phase, + top_n_slowest_witnesses, ClaimOutcome, DiscoveryCorpusOptions, DiscoverySummary, FloorPhase, + HistogramData, PhaseProfile, TimingPercentiles, WitnessTimingRow, + DEFAULT_SLOWEST_WITNESS_ATTRIBUTION_N, }; use v1_compiler::v1_interpreter::{ color_enabled, paint, run_in_context_with_args, sgr, ExecutionMode, InterpContext, Value, @@ -1788,7 +1789,7 @@ fn run() -> Result { // Resolve the plan entry ONCE and evaluate both the batches (hermetic) and the // spawn width (wet) from the same resolved graph — this resolve was previously // paid twice back-to-back (the §2 double-paid-compute trap, at minutes each). - let (plan_graph, plan_indices) = match resolve_entry_graph(&source_roots, &plan_entry) { + let (plan_graph, plan_indices) = match resolve_entry_graph_shared(&source_roots, &plan_entry) { Ok(resolved) => resolved, Err(msg) => { eprintln!("claim_executor: resolve failed for plan {plan_entry}:\n{msg}"); diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 9bb234983b1..3924e8510d1 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -828,6 +828,52 @@ pub fn resolve_entry_graph( resolve_entry_graph_with_index(&index, &facts, entry_file) } +// Process-level (per-thread) resolve store — the S1a increment of the resolver +// graph-major design (docs/plans/resolver-graph-major-design.md). Within one +// process the source tree is a fixed snapshot, so a resolved entry graph is a +// pure fact of (source_roots, entry) — the same purity assumption the walk memo +// (M1) and typed_module_cache already ship on. Routing every fixed-entry +// consumer (floor runner context, diff observer, output policy, group syntax, +// the executor's plan entry) through this store makes "resolve the same declared +// machinery twice in one process" unwritable on these paths, with failure +// semantics unchanged: a miss resolves exactly as before, including the typed +// error path. Thread-local by design: resolved graphs are Rc-based (not Send); +// shard threads keep their own store rather than smuggling Rc across threads. +thread_local! { + #[allow(clippy::type_complexity)] + static PROCESS_RESOLVE_STORE: RefCell< + HashMap< + (String, String), + ( + Rc, + Rc>>, + ), + >, + > = RefCell::new(HashMap::new()); +} + +pub fn resolve_entry_graph_shared( + source_roots: &[String], + entry_file: &str, +) -> Result< + ( + Rc, + Rc>>, + ), + String, +> { + let key = (source_roots.join("\u{1f}"), entry_file.to_string()); + let hit = PROCESS_RESOLVE_STORE.with(|s| s.borrow().get(&key).cloned()); + if let Some(found) = hit { + return Ok(found); + } + let resolved = resolve_entry_graph(source_roots, entry_file)?; + PROCESS_RESOLVE_STORE.with(|s| { + s.borrow_mut().insert(key, resolved.clone()); + }); + Ok(resolved) +} + pub struct MultiEntryIndex { source_files: ModuleSourceIndex, module_graph_facts: ModuleGraphFactsLive, @@ -1080,9 +1126,7 @@ fn reconcile_with_typed_cache( // anything over it is a pathology-lane candidate by name. let module_tc_ms = module_tc_started.elapsed().as_millis(); if module_tc_ms >= 2_000 { - eprintln!( - "[typecheck-attribution] module={mod_name} ms={module_tc_ms}" - ); + eprintln!("[typecheck-attribution] module={mod_name} ms={module_tc_ms}"); } typed_cache .borrow_mut() @@ -1243,7 +1287,7 @@ pub fn install_output_policy(source_roots: &[String]) { v1_interpreter::Verbosity::Normal => (false, false), }; let entry = "dag/gunbc/output_policy.dag"; - let (graph, indices) = match resolve_entry_graph(source_roots, entry) { + let (graph, indices) = match resolve_entry_graph_shared(source_roots, entry) { Ok(g) => g, Err(_) => return, }; @@ -1299,7 +1343,7 @@ pub fn install_group_syntax(source_roots: &[String]) { .map(|v| v == "true") .unwrap_or(false); let entry = "dag/extdeps/render/surface.dag"; - let (graph, indices) = match resolve_entry_graph(source_roots, entry) { + let (graph, indices) = match resolve_entry_graph_shared(source_roots, entry) { Ok(g) => g, Err(_) => return, }; @@ -3011,7 +3055,7 @@ pub fn wet_hermetic_scaffold_roster_entry_prefix( ) -> Result { let entry = resolve_entry_file_under_roots(source_roots, WET_HERMETIC_EQUIVALENCE_WITNESS_ENTRY)?; - let (graph, source_indices) = resolve_entry_graph(source_roots, &entry)?; + let (graph, source_indices) = resolve_entry_graph_shared(source_roots, &entry)?; let sources = load_sources_for_entry(source_roots, &entry)?; let entry_source = sources .iter() @@ -3708,7 +3752,7 @@ fn floor_git_diff_range() -> Result { use v1_interpreter::Value; let roots = default_source_roots(); let entry = "src/v2/workflow/floor_diff_observe.dag"; - let (graph, indices) = resolve_entry_graph(&roots, entry) + let (graph, indices) = resolve_entry_graph_shared(&roots, entry) .map_err(|e| format!("floor_diff_observe resolve: {e}"))?; let ctx = make_eval_context(&graph, indices, v1_interpreter::ExecutionMode::Wet); let result = @@ -4152,7 +4196,7 @@ fn resolve_floor_runner_context( ), String, > { - resolve_entry_graph(source_roots, FLOOR_RUNNER_ENTRY) + resolve_entry_graph_shared(source_roots, FLOOR_RUNNER_ENTRY) } fn call_floor_kernel_would_skip( @@ -11663,3 +11707,39 @@ mod import_closure_equivalence_tests { assert_eq!(norm(from_rel), norm(from_abs)); } } + +#[cfg(test)] +mod process_resolve_store_tests { + use super::*; + + // S1a purity + dedup receipt: the second resolve of the same (roots, entry) + // must be served from the process store — Rc identity proves zero recompute. + // A tiny self-contained fixture tree keeps this test milliseconds-cold. + #[test] + fn process_resolve_store_dedupes_repeat_resolve() { + // Fixture must live under the workspace (build_module_path_index requires it); + // target/ is workspace-local and git-ignored. + let dir = workspace_root() + .join("target") + .join(format!("gunbc-resolve-store-test-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("create fixture dir"); + std::fs::write( + dir.join("store_probe.dag"), + "module store_probe\n\nfn probe() -> Bool {\n true\n}\n", + ) + .expect("write fixture module"); + let roots = vec![dir.to_string_lossy().into_owned()]; + let entry = dir.join("store_probe.dag").to_string_lossy().into_owned(); + + let (g1, i1) = resolve_entry_graph_shared(&roots, &entry).expect("first resolve"); + let (g2, i2) = resolve_entry_graph_shared(&roots, &entry).expect("second resolve"); + assert!( + Rc::ptr_eq(&g1, &g2), + "second resolve must be the stored graph (Rc identity), not a recompute" + ); + assert!(Rc::ptr_eq(&i1, &i2), "source indices must be stored too"); + + let _ = std::fs::remove_dir_all(&dir); + } +} From 24b8743e3cf08059acff150b5cfeb6a58e1cd1e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 05:23:02 +0000 Subject: [PATCH 12/15] typecheck attribution: start markers (profile-gated) name the pathological module Verdict from the instrumented run: all std modules in the closure - including std.algebra and std.realization_schedule themselves - typecheck in under 2s each; the call that runs 17+ minutes is typecheck_module on the 80-line CONSUMER module (test.claim.realization_schedule_witness). The inference pathology is at generic use sites, not definition sites. Next lane: the existing type-env lookup profiler (text_lookup_work_counter feature, reset_type_env_lookup_profile) pointed at that one module. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- src/v1/stage0/src/cli_run.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 3924e8510d1..e36fec8a162 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -1113,6 +1113,9 @@ fn reconcile_with_typed_cache( let tc_result = match cached { Some(hit) => hit, None => { + if phase_profile::phase_profile_enabled() { + eprintln!("[typecheck-attribution] module={mod_name} start"); + } let module_tc_started = std::time::Instant::now(); let computed = v1_compiler_infer::typecheck_module( resolved.clone(), From af8a3edecfd7c8d8f02cea99094c2d5b6cd1bcee Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 05:57:22 +0000 Subject: [PATCH 13/15] Phase heartbeat: live type-env amplification counters; pathology named Heartbeats now carry env_flatten/env_builds/env_merges/env_rewires from the existing v1_compiler_infer_env atomics. First live capture on the pathological entry: env_flatten grows ~80k/sec unbounded (9.7M by t=120s) while builds=11, merges=37, rewires=0 stay flat. Root site: record_lit_variant_fields_from_visible_env (v1_compiler_infer.rs, generated from v1.compiler.infer) - every VARIANT LITERAL flattens the entire visible environment (recursive parent fold; the counter) and then scans every visible binding with expand_type_for_field_access per candidate to find the variant's owner. Cost = variant literals x |visible env| x expansion - definitions cheap, consumers explode. 04_env.dag:35 carries an invariant expecting flatten_visible_parent_recurses==0 on import chains; live count says 9.7M in 2 minutes. Fix shape (next block, dag source + regen): a variant-name -> owner index built once per env, and expected-type short-circuit before any global scan. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- src/v1/stage0/src/phase_profile.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/v1/stage0/src/phase_profile.rs b/src/v1/stage0/src/phase_profile.rs index 00bc9fbe41b..d6de9d2d0d1 100644 --- a/src/v1/stage0/src/phase_profile.rs +++ b/src/v1/stage0/src/phase_profile.rs @@ -68,9 +68,16 @@ impl PhaseProfileInner { Some(s) => format!(" signal={s} flushed=1"), None => String::new(), }; + // Live type-env amplification counters (v1_compiler_infer_env atomics): during a + // long typecheck the heartbeat shows WHICH counter grows without bound — the + // 2026-07-04 use-site inference pathology instrument. Cheap relaxed loads. + let flatten = crate::v1_compiler_infer_env::flatten_visible_parent_recurses(); + let builds = crate::v1_compiler_infer_env::build_type_env_calls(); + let merges = crate::v1_compiler_infer_env::merge_type_env_cache_calls(); + let rewires = crate::v1_compiler_infer_env::rewire_type_env_parent_links_calls(); let _ = writeln!( io::stderr(), - "[phase-profile] tick={tick} phase={} elapsed_ms={elapsed_ms} phase_elapsed_ms={phase_elapsed_ms} context={} reason={reason}{signal_suffix}", + "[phase-profile] tick={tick} phase={} elapsed_ms={elapsed_ms} phase_elapsed_ms={phase_elapsed_ms} context={} reason={reason} env_flatten={flatten} env_builds={builds} env_merges={merges} env_rewires={rewires}{signal_suffix}", self.phase.tag(), self.context, ); From d44a846ed371d2ee3227629da7997df63312e1e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 06:15:40 +0000 Subject: [PATCH 14/15] Resolver design: operator ruling - constructor ambiguity is an ERROR; resolution follows the binding edge The ambiguity support (bare constructor names legal across coproducts, expected-type tie-break, whole-env owner scan as fallback) is itself the bug - accidental semantics, never intended. The DAG already provides the namespace: imports bind arm names to arm nodes; the parent edge names the owner; resolution is following the edge already held. Rules: bound-name resolution via parent edge; unbound constructor literal = typed error (today a silent undeclared-dependency fail-open via the scan); double binding = collision error at env build; patterns resolve via scrutinee type; expected-type owner-picking deleted. The pathological scan is removed, not memoized. Census: 5,526 arm names, 221 global collisions (uniqueness must be scoped to co-visibility); <=4,572 unbound arm-name uses upper bound, dominated by legal pattern positions. Control run killed at 60+ min still inside one typecheck_module call on the 80-line module. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- docs/plans/resolver-graph-major-design.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/plans/resolver-graph-major-design.md b/docs/plans/resolver-graph-major-design.md index 9377d878b98..8c8f9b8d170 100644 --- a/docs/plans/resolver-graph-major-design.md +++ b/docs/plans/resolver-graph-major-design.md @@ -48,6 +48,22 @@ Adopted invariant (four terminal states, no fifth): every enrolled witness ends The one structural primitive the executor lacks for unification (named by the M2 doc already): edges carry no payload — there is no obligation that *produces an artifact consumed by dependents*. Module obligations need exactly that (ModuleInterface flowing along edges). That is S2a's real content. +## 1c. Operator ruling (2026-07-04): constructor ambiguity is an ERROR; resolution follows the binding edge + +The measured inference pathology (§0 phase verdict) traced to constructor-ownership resolution: bare variant names, legal cross-coproduct name sharing, expected-type tie-breaking, and — when the scope lookup misses (every *imported* constructor: the import binds the name to the ARM node, and `lookup_variant_parent_enum` rejects it for not being the parent Disj) — a per-literal whole-environment flatten+scan (`record_lit_variant_fields_from_visible_env`, ~1M invocations / 9.7M parent-recurses in 120s on an 80-line module). Operator ruling: **the ambiguity support itself is the bug** — accidental semantics, never intended. Do not optimize the search; delete it. + +The DAG already provides the namespace: an arm is a node whose parent edge names its owning coproduct, and the resolver's export machinery already binds imported arm names to those arm nodes (`get_exported_names` includes `get_variant_names`). Rules: + +1. A constructor literal's name must be **bound in scope** (local declaration or explicit import); owner = the binding's **parent edge**. O(1), no scan, no cache, no new syntax. +2. **Unbound constructor name = typed error.** (Today the scan silently resolves constructors that were never imported — an undeclared-dependency fail-open.) +3. **Name bound twice in one scope = collision error at env construction** (the only survivor of the index sketch: a one-fold collision detector in `build_type_env`). +4. **Patterns resolve via the scrutinee's type** — already unambiguous; unimported arm names stay legal in pattern position. +5. **Expected-type-as-owner-picker is deleted**; expected types return to ordinary mismatch checking. + +Consequences: `record_lit_variant_fields_from_visible_env` and both its call sites are removed, not memoized; the `04_env.dag` flatten-recurse==0 invariant becomes true and its witness gates the regression; the variant-owner canary fixture (two local coproducts sharing `SharedVariant`) becomes illegal by design — it is rewritten as the red-control asserting the collision diagnostic, and a different tiny witness takes the CI canary slot. Migration = census of (a) co-visible arm-name collisions and (b) never-imported constructor literals, then mechanical import additions. Fix home: `v1.compiler.infer` / `infer_env` dag sources + seed regen (RegenVerifyGate holds committed == regenerated). + +Census receipts (text-level, 2026-07-04): 5,526 distinct arm names corpus-wide; **221 collide globally** (e.g. `Absent` owned by 4 coproducts, `Assistant` by two API models) — so the uniqueness rule MUST be scoped to co-visibility, not global; most global collisions never share a scope. Upper bound on scan-reliant uses: 1,086 files reference arm names neither imported nor locally declared (4,572 distinct name-uses) — dominated by match patterns, which stay legal (rule 4); the literal-position migration subset is countable precisely once the resolver-side census runs (cheap after the fix lands). Termination receipt: the control typecheck of the 80-line module was killed at **60+ minutes still inside the single `typecheck_module` call**. + ## 2. The windowed frontier (memory model) Holding "the whole graph" means holding the **edges** (cheap facts). Evaluating node `d` needs exactly: `d`'s source + the **results** of `d`'s direct dependencies. Not a→c's internals, not their inference traces. When `b`'s last dependent completes, `b`'s working state is dropped; what survives is its compact keyed result. Peak memory ≈ frontier width × result size, not closure size. From c57cf3302b8d68d45a3a19cb8c388c4cf5459a79 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 06:22:38 +0000 Subject: [PATCH 15/15] Resolver design: operator sequencing - union resolve first as interim with explicit contract; flat name visibility refinement Union resolve leads the resolve lane with a recorded contract: minimum upper bound (resolve cost <= 1x union closure, receipt-enforced), successor must be maximally parallel (topo-antichain module typecheck, budget-tree width) and frontier-window efficient; it dissolves into S2a and must not grow features that delay that. Namespacing ruling refined per operator: visibility is FLAT - locals + direct imports only, never transitive - making collision detection per-file with zero graph traversal; types propagate by graph identity underneath. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DcouXUHJmm4NPqwDyvnDKf --- docs/plans/resolver-graph-major-design.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/plans/resolver-graph-major-design.md b/docs/plans/resolver-graph-major-design.md index 8c8f9b8d170..30d422b5419 100644 --- a/docs/plans/resolver-graph-major-design.md +++ b/docs/plans/resolver-graph-major-design.md @@ -113,7 +113,9 @@ Key shape (Merkle): `key(m) = H(source_bytes(m), key(dep_1), …, key(dep_n), re ## 7. Staging — deliberately NOT a 10-PR arc -**S1 — union resolve, in-process (ship in the current PR line; no substrate migration, no persistence, no determinism dependency).** Stop fragmenting demand: one `claim_executor`/`claim_batch` process resolves the union of everything it will need — prelude entries + all roster rows — once, forward, and every consumer assembles from the shared facts. This is the operator's "walk from a once, then evaluate e, then f" implemented with the existing monolithic resolver (the union closure IS one resolve; the `seen` set already deduplicates inside it). Displacement: N overlapping resolves per process → 1; the corpus's per-entry bill collapses to (union once) + (cheap per-entry assembly). Risks owned: name-collision surfacing (oracle §6.3), `Rc` thread boundary (§4). +**Operator sequencing + expectations (2026-07-04):** start the resolve lane with union resolve, explicitly as an INTERIM step with a contract: (a) **minimum upper bound** — a process's resolve cost is ≤ 1× its union closure, never N×, enforced by the once-per-node receipt; (b) the successor (S2a module obligations) must be **maximally parallel** — per-module typecheck scheduled as topo antichains with width from the budget tree, not the serial monolith union resolve keeps; (c) efficiency — frontier-window memory (§2), not closure-lifetime retention. Union resolve dissolves into S2a; it must not grow features that delay that dissolution. Name visibility is FLAT (§1c refinement): a module's name universe = its own declarations ∪ direct import lists — never transitive; collision detection is therefore per-file with zero graph traversal; types still propagate transitively by graph identity underneath (names are a surface-binding concern only). + +**S1 — union resolve, in-process (no substrate migration, no persistence, no determinism dependency).** Stop fragmenting demand: one `claim_executor`/`claim_batch` process resolves the union of everything it will need — prelude entries + all roster rows — once, forward, and every consumer assembles from the shared facts. This is the operator's "walk from a once, then evaluate e, then f" implemented with the existing monolithic resolver (the union closure IS one resolve; the `seen` set already deduplicates inside it). Displacement: N overlapping resolves per process → 1; the corpus's per-entry bill collapses to (union once) + (cheap per-entry assembly). Risks owned: name-collision surfacing (oracle §6.3), `Rc` thread boundary (§4). **S2a — module nodes + Merkle keys, still in-process.** Split `compile_to_resolved` at module boundaries (ModuleInterface/ModuleBody), schedule via `RunnableCompile` nodes, get frontier-window memory and per-module parallel inference (topo antichains × Altra cores — the 48-minute whole-tree resolve is single-threaded today). The one genuinely deep change (modular inference).