diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77cfa1ac666..a6fd3512f0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,7 +217,7 @@ jobs: ci: runs-on: [self-hosted, linux, arm64] needs: [build, regen] - timeout-minutes: 100 + timeout-minutes: 110 steps: - name: Checkout uses: actions/checkout@v5 @@ -276,7 +276,7 @@ jobs: "$ROOT/target/release/claim_executor" --source-root "$ROOT/dag" --source-root "$ROOT/src/v2" --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_ci_floor_plan --notice-title "v2 claim corpus" env: GUNBC_FLOOR_PHASE_JOURNAL: target/floor-phase-journal.tsv - timeout-minutes: 65 + timeout-minutes: 75 - name: Floor cgroup peak post-read (calibration; survives a killed floor) run: | # 🟑 dissolve-on: ci_floor_phase_journal_if β€” bounded foreign-executor (GitHub Actions run:) shell transport for exactly four journal leaves: FileExists, header log, prefixed file read, and absent log. If/else control flow is already v2.std.orchestration intent; only those leaves remain medium-as-string Run.command values. DISSOLVES WHEN bash-emit (#5828 / ROADMAP shell-effectplan-to-bash / shellβ†’intent Phase 2) binds typed filesystem observation plus diagnostic-log operations through host_effect_apply, so ci_floor_phase_journal_if contains no opaque shell command text. diff --git a/dag/gunbc/ci_spec.dag b/dag/gunbc/ci_spec.dag index 53903ad5a9e..e40ef9b5855 100644 --- a/dag/gunbc/ci_spec.dag +++ b/dag/gunbc/ci_spec.dag @@ -84,13 +84,13 @@ data gunbc_ci_ordinary_floor_budget_minutes: Minute = minute(count: 55) data gunbc_ci_admission_postcondition_budget_minutes: Minute = minute(count: 5) -data gunbc_ci_walk_overhead_allowance_minutes: Minute = minute(count: 5) +data gunbc_ci_walk_overhead_allowance_minutes: Minute = minute(count: 15) fn gunbc_ci_workflow_wrapper_budget_minutes() -> Minute { minute(count: minute_count(m: gunbc_ci_ordinary_floor_budget_minutes) + minute_count(m: gunbc_ci_admission_postcondition_budget_minutes) + minute_count(m: gunbc_ci_walk_overhead_allowance_minutes)) } -data gunbc_ci_two_population_budget_note: String = "The CI walk owns two disjoint wall allowances: 55 minutes for ordinary batches and 5 minutes for green-only admission postconditions. WalkPlan carries both so claim_executor, not the outer runner, prevents ordinary work from consuming the admission allowance. The wall spent OUTSIDE both populations β€” the pre-walk capture subprocess, plan resolve, finalization and receipt writes, phase-journal flushes β€” is neither ordinary nor postcondition work, so the GitHub step wrapper reserves the named gunbc_ci_walk_overhead_allowance_minutes term for it rather than silently letting a slow pre-walk eat the ordinary allowance from the outside (review 47517; the two watchdogs bound their own populations, and the wrapper is strictly the three-term sum, 65 minutes today). A budget refusal is located to its population inside the executor; a wrapper kill with both populations under budget is located to the overhead term." +data gunbc_ci_two_population_budget_note: String = "The CI walk owns two disjoint wall allowances: 55 minutes for ordinary batches and 5 minutes for green-only admission postconditions. WalkPlan carries both so claim_executor, not the outer runner, prevents ordinary work from consuming the admission allowance. The wall spent OUTSIDE both populations β€” the pre-walk capture subprocess, plan resolve, finalization and receipt writes, phase-journal flushes β€” is neither ordinary nor postcondition work, so the GitHub step wrapper reserves the named gunbc_ci_walk_overhead_allowance_minutes term for it rather than silently letting a slow pre-walk eat the ordinary allowance from the outside (review 47517; the two watchdogs bound their own populations, and the wrapper is strictly the three-term sum, 75 minutes today). A budget refusal is located to its population inside the executor; a wrapper kill with both populations under budget is located to the overhead term. The overhead term was raised 5β†’15 minutes (2026-08-05): measured CI floor pre-walk plus eager compile-clean install routinely exceeds 10 minutes before the ordinary population clock starts, so a 5-minute allowance let the GitHub step wrapper kill the job while ordinary budget remained β€” silent wrapper kill, not ordinary exhaustion." data gunbc_ci_floor_batch_stop_policy_note: String = "Floor batch stop policy (operator ruling 2026-07-22, fail-fast lane): claim_executor's walk halt is event-scoped so cheap-gate-early reorder does not truncate main's ledger. pull_request β†’ StopBeforeDependents (fail-fast: a cheap-gate red stops before compile/corpus). push (main) and schedule (falsifier) β†’ FullLedger (record batch failure and continue; exit non-zero at end). Unknown/unrecognized events default to FullLedger β€” never silently stop-early and lose ledger (the absorbing fallback). Authority: gunbc_ci_floor_batch_stop_policy_for_github_event in v2.workflow.ci_floor_plan; consumer: claim_executor.rs run_walk (see gunbc_ci_floor_batch_stop_policy_claim_executor_seed_note)." diff --git a/dag/gunbc/v1_interpreter_primitive_surface.dag b/dag/gunbc/v1_interpreter_primitive_surface.dag index 54a4f876576..0cb0b0fdbf5 100644 --- a/dag/gunbc/v1_interpreter_primitive_surface.dag +++ b/dag/gunbc/v1_interpreter_primitive_surface.dag @@ -1040,6 +1040,51 @@ fn v1_interpreter_authored_roster_arms() -> List List { "witness_layer_roots_compile_clean_check", "witness_layer_roots_compile_clean_emit_check", "consume_floor_compile_clean_gate_verdict", + "consume_floor_compile_clean_gate_failure_detail", + "record_regen_verify_gate_failure_detail", + "consume_regen_verify_gate_failure_detail", + "record_generated_artifact_drift_gate_failure_detail", + "consume_generated_artifact_drift_gate_failure_detail", "witness_compile_clean_cli_floor_verdicts_agree", "test_migration_debt_module_count", "test_migration_debt_total_loc", diff --git a/dag/test/claim/ci_workflow_witness_test.dag b/dag/test/claim/ci_workflow_witness_test.dag index 1d0fbad99ad..5c811b5eec5 100644 --- a/dag/test/claim/ci_workflow_witness_test.dag +++ b/dag/test/claim/ci_workflow_witness_test.dag @@ -42,7 +42,7 @@ test fn ci_workflow_witness_holds() -> Bool { && ci_job_backstop_dropped_the_selection_control_term() && (minute_count(m: gunbc_ci_ordinary_floor_budget_minutes) == 55) && (minute_count(m: gunbc_ci_admission_postcondition_budget_minutes) == 5) - && (minute_count(m: gunbc_ci_walk_overhead_allowance_minutes) == 5) + && (minute_count(m: gunbc_ci_walk_overhead_allowance_minutes) == 15) && (minute_count(m: gunbc_ci_workflow_wrapper_budget_minutes()) == minute_count(m: gunbc_ci_ordinary_floor_budget_minutes) + minute_count(m: gunbc_ci_admission_postcondition_budget_minutes) + minute_count(m: gunbc_ci_walk_overhead_allowance_minutes)) && (gunbc_ci_workflow_wrapper_timeout_minutes == minute_count(m: gunbc_ci_workflow_wrapper_budget_minutes())) } diff --git a/dag/test/claim/floor_gate_failure_receipt_witness_test.dag b/dag/test/claim/floor_gate_failure_receipt_witness_test.dag new file mode 100644 index 00000000000..0705e7e9d1e --- /dev/null +++ b/dag/test/claim/floor_gate_failure_receipt_witness_test.dag @@ -0,0 +1,108 @@ +module test.claim.floor_gate_failure_receipt_witness + +import std.process { ProcessExit, ExitSuccess, exit_failure } +import gunbc.test_module_hygiene { failure_receipt_companion, Optional, Present } +import gunbc.generated_artifact_assessment { + GeneratedArtifactObservation, GeneratedArtifactObserved, assess_generated_artifact_population, + first_failing_diagnostic_path +} +import gunbc.generated_artifact { CiYamlArtifact, GitignoreArtifact } +import tools.dag_compile_clean_gate { dag_compile_clean_failure_reason } +import tools.generated_artifact_gate { generated_artifact_drift_failure_reason } +import tools.regen_verify_transport { regen_verify_failure_detail } +import tools.regen_verify_gate { regen_verify_failure_reason } + +data witness_note: String = "Floor gate failure-receipt companions must name located reasons on Bool(false), not only returned Bool(false). Controls pin the naming convention via gunbc.test_module_hygiene.failure_receipt_companion and the reason projection helpers without re-running the expensive live gates." + +data planted_regen_stderr: String = "Stage0 is stale: 1 generated file(s) differ from fresh self-compile.\nChanged generated file(s): v1_compiler_emit_rust.rs\n" + +data planted_artifact_path: String = ".github/workflows/ci.yml" + +fn constructed_drift_observation(path: String) -> GeneratedArtifactObserved { + GeneratedArtifactObserved { + artifact: CiYamlArtifact, + path: path, + generated_content: "expected bytes", + committed_content: concat("expected bytes", "\n# drift"), + extra_validation: true + } +} + +test fn regen_verify_failure_detail_names_drifted_file_holds() -> Bool { + string_contains( + s: regen_verify_failure_detail(stdout: "regen_divergence_count=1\n", stderr: planted_regen_stderr), + pattern: "v1_compiler_emit_rust.rs" + ) +} + +test fn regen_verify_failure_detail_refuses_empty_capture_holds() -> Bool { + string_contains( + s: regen_verify_failure_detail(stdout: "", stderr: ""), + pattern: "no diagnostic output" + ) +} + +test fn regen_verify_failure_reason_empty_only_on_success() -> Bool { + regen_verify_failure_reason(exit: ExitSuccess) == "" +} + +test fn generated_artifact_drift_verdict_names_planted_path_holds() -> Bool { + let observations = [ + constructed_drift_observation(path: planted_artifact_path), + GeneratedArtifactObserved { + artifact: GitignoreArtifact, + path: ".gitignore", + generated_content: "same", + committed_content: "same", + extra_validation: true + } + ] + string_contains( + s: first_failing_diagnostic_path( + expected: [CiYamlArtifact, GitignoreArtifact], + observations: observations + ), + pattern: planted_artifact_path + ) + && !assess_generated_artifact_population( + expected: [CiYamlArtifact, GitignoreArtifact], + observations: observations + ) +} + +test fn generated_artifact_drift_failure_reason_empty_only_on_success() -> Bool { + generated_artifact_drift_failure_reason(exit: ExitSuccess) == "" + && string_contains( + s: generated_artifact_drift_failure_reason( + exit: exit_failure( + reason: concat("generated-artifact drift/validation failed: ", planted_artifact_path) + ) + ), + pattern: planted_artifact_path + ) +} + +test fn dag_compile_clean_failure_reason_empty_only_on_success() -> Bool { + dag_compile_clean_failure_reason(exit: ExitSuccess) == "" + && string_contains( + s: dag_compile_clean_failure_reason( + exit: exit_failure(reason: "compile-clean: unresolved import in dag/test/example.dag") + ), + pattern: "dag/test/example.dag" + ) +} + +test fn floor_gate_failure_receipt_companion_names_holds() -> Bool { + match failure_receipt_companion(function: "dag_compile_clean_gate_passes") { + Present { value: companion } => companion == "dag_compile_clean_gate_failure_receipt" + _ => false + } + && match failure_receipt_companion(function: "generated_artifact_drift_gate_passes") { + Present { value: companion } => companion == "generated_artifact_drift_gate_failure_receipt" + _ => false + } + && match failure_receipt_companion(function: "regen_verify_gate_passes") { + Present { value: companion } => companion == "regen_verify_gate_failure_receipt" + _ => false + } +} diff --git a/dag/tools/dag_compile_clean_gate.dag b/dag/tools/dag_compile_clean_gate.dag index 1edf36be8ad..04717f3dcc6 100644 --- a/dag/tools/dag_compile_clean_gate.dag +++ b/dag/tools/dag_compile_clean_gate.dag @@ -3,6 +3,15 @@ module tools.dag_compile_clean_gate import std.process { ProcessExit, ExitSuccess, exit_failure } import tools.dag_compile_clean_transport { run_clean_tree_compile } +data dag_compile_clean_failure_receipt_note: String = "The loudness half: the floor witness collapses this gate's ProcessExit to Bool via tools.ci_gates.exit_ok, so compile-clean reds reported only returned Bool(false). The gate consumes the executor's one in-run compile receipt; the failure-receipt companion projects the same receipt's located diagnostic without a second compile." + +fn dag_compile_clean_failure_reason(exit: ProcessExit) -> String { + match exit { + ExitSuccess => "" + ExitFailure { code: _, reason: reason } => reason + } +} + func run_dag_compile_clean_gate_body() -> ProcessExit { if run_clean_tree_compile() { ExitSuccess @@ -17,6 +26,10 @@ func run_dag_compile_clean_gate() -> ProcessExit { run_dag_compile_clean_gate_body() } +func dag_compile_clean_failure_receipt() -> String { + consume_floor_compile_clean_gate_failure_detail() +} + func main() -> ProcessExit { run_dag_compile_clean_gate_body() } diff --git a/dag/tools/floor_effect_gate_witness.dag b/dag/tools/floor_effect_gate_witness.dag index 8ab96097931..9e422beac3c 100644 --- a/dag/tools/floor_effect_gate_witness.dag +++ b/dag/tools/floor_effect_gate_witness.dag @@ -11,6 +11,9 @@ import tools.extdeps_scope_placement_gate { scope_placement_failure_reason, scope_placement_membership_verdict, } +import tools.dag_compile_clean_gate { dag_compile_clean_failure_receipt } +import tools.generated_artifact_gate { generated_artifact_drift_failure_receipt } +import tools.regen_verify_gate { regen_verify_failure_receipt } import gunbc.test_module_hygiene { failure_receipt_companion, } @@ -22,7 +25,7 @@ data extdeps_scope_placement_gate_passes_name: String = "extdeps_scope_placement data scope_placement_gate_loudness_seed_probe_passes_name: String = "scope_placement_gate_loudness_seed_probe_passes" -data floor_gate_failure_receipt_note: String = "claim_executor derives a failure-receipt companion from a Bool witness's own name and evaluates it in THIS entry's context, so the companion must be declared here beside the witness it explains β€” not only in the gate module that builds the reason. Naming is mechanical: _passes or _holds yields _failure_receipt, delegated to gunbc.test_module_hygiene.failure_receipt_companion (the single authority; cli_run::failure_receipt_companion routes through the same module). scope_placement_gate_loudness_seed_probe_* is a hermetic always-false _passes witness + failure_receipt companion enrolled solely so dag/test/claim/extdeps_scope_placement_gate_loudness_witness_test.dag can execute production seed_runner_bool_false_failure_detail against a located receipt without re-running the wet placement gate; it must stay OFF CiSpec.witness_entries and OFF any future entry-scoped *_passes auto-discovery (auto-enrollment would red CI). Mute frontier (identity join on this entry, 2026-08-05): eight _passes witnesses, two _failure_receipt companions (extdeps_scope_placement_gate_failure_receipt, scope_placement_gate_loudness_seed_probe_failure_receipt; _companion_name helpers are not companions); two companions join gates β€” extdeps_scope_placement_gate (production, landed gunbc#7644) and scope_placement_gate_loudness_seed_probe (hermetic test probe, not production). Six production gates still return bare returned Bool(false) on refusal with no located receipt: cheap_claim_pool_gate, dag_compile_clean_gate, emit_host_gate, extdeps_external_authority_gate, generated_artifact_drift_gate, regen_verify_gate. Dissolution trigger: a gate leaves this frontier when this entry declares its _failure_receipt companion beside the _passes witness. Frontier counts are hand-authored prose debt β€” dissolve-on: derived projection from this entry's declarations (see loudness_witness_note sequencing note)." +data floor_gate_failure_receipt_note: String = "claim_executor derives a failure-receipt companion from a Bool witness's own name and evaluates it in THIS entry's context, so the companion must be declared here beside the witness it explains β€” not only in the gate module that builds the reason. Naming is mechanical: _passes or _holds yields _failure_receipt, delegated to gunbc.test_module_hygiene.failure_receipt_companion (the single authority; cli_run::failure_receipt_companion routes through the same module). scope_placement_gate_loudness_seed_probe_* is a hermetic always-false _passes witness + failure_receipt companion enrolled solely so dag/test/claim/extdeps_scope_placement_gate_loudness_witness_test.dag can execute production seed_runner_bool_false_failure_detail against a located receipt without re-running the wet placement gate; it must stay OFF CiSpec.witness_entries and OFF any future entry-scoped *_passes auto-discovery (auto-enrollment would red CI). Mute frontier (identity join on this entry, 2026-08-05): eight _passes witnesses, five _failure_receipt companions (extdeps_scope_placement_gate_failure_receipt, scope_placement_gate_loudness_seed_probe_failure_receipt, dag_compile_clean_gate_failure_receipt, generated_artifact_drift_gate_failure_receipt, regen_verify_gate_failure_receipt; _companion_name helpers are not companions). Three production gates still return bare returned Bool(false) on refusal with no located receipt: cheap_claim_pool_gate, emit_host_gate, extdeps_external_authority_gate. Dissolution trigger: a gate leaves this frontier when this entry declares its _failure_receipt companion beside the _passes witness. Frontier counts are hand-authored prose debt β€” dissolve-on: derived projection from this entry's declarations (see loudness_witness_note sequencing note)." fn extdeps_scope_placement_gate_failure_receipt_companion_name() -> String { match failure_receipt_companion(function: extdeps_scope_placement_gate_passes_name) { @@ -71,6 +74,18 @@ func scope_placement_gate_loudness_seed_probe_failure_receipt() -> String { ) } +func dag_compile_clean_gate_failure_receipt() -> String { + dag_compile_clean_failure_receipt() +} + +func generated_artifact_drift_gate_failure_receipt() -> String { + generated_artifact_drift_failure_receipt() +} + func regen_verify_gate_passes() -> Bool { tools.ci_gates.exit_ok(e: run_spec_gate(g: RegenVerifyGate)) } + +func regen_verify_gate_failure_receipt() -> String { + regen_verify_failure_receipt() +} diff --git a/dag/tools/generated_artifact_gate.dag b/dag/tools/generated_artifact_gate.dag index 352fedf13b5..2debf80f6b8 100644 --- a/dag/tools/generated_artifact_gate.dag +++ b/dag/tools/generated_artifact_gate.dag @@ -30,15 +30,21 @@ data run_generated_artifact_drift_gate_body_note: String = "One live pass: obser func run_generated_artifact_drift_gate_body() -> ProcessExit { let observations = observe_generated_artifact_population() let all_ok = assess_generated_artifact_population(expected: committed_generated_artifacts(), observations: observations) + let reason = if all_ok { + "" + } else { + concat( + concat("generated-artifact drift/validation failed: ", first_failing_diagnostic_path(expected: committed_generated_artifacts(), observations: observations)), + " β€” it is a GENERATED projection of its .dag authority; do not hand-edit, regenerate via main_wet on dag/tools/generated_artifact_gate.dag" + ) + } + if !all_ok { + record_generated_artifact_drift_gate_failure_detail(detail: reason) + } return if all_ok { ExitSuccess } else { - exit_failure( - reason: concat( - concat("generated-artifact drift/validation failed: ", first_failing_diagnostic_path(expected: committed_generated_artifacts(), observations: observations)), - " β€” it is a GENERATED projection of its .dag authority; do not hand-edit, regenerate via main_wet on dag/tools/generated_artifact_gate.dag" - ) - ) + exit_failure(reason: reason) } } @@ -50,6 +56,19 @@ func run_generated_artifact_drift_gate() -> ProcessExit { run_generated_artifact_drift_gate_body() } +data generated_artifact_drift_failure_receipt_note: String = "The loudness half: the floor witness collapses this gate's ProcessExit to Bool via tools.ci_gates.exit_ok, so a generated-artifact drift red reported only returned Bool(false) without the located path. The gate body records the reason from its single observe_generated_artifact_population pass; the failure-receipt companion consumes that retained detail instead of re-observing." + +fn generated_artifact_drift_failure_reason(exit: ProcessExit) -> String { + match exit { + ExitSuccess => "" + ExitFailure { code: _, reason: reason } => reason + } +} + +func generated_artifact_drift_failure_receipt() -> String { + consume_generated_artifact_drift_gate_failure_detail() +} + func main_wet() -> ProcessExit { fold(committed_generated_artifacts(), init: ExitSuccess, f: fn(acc, a) { if exit_ok(e: acc) { diff --git a/dag/tools/regen_verify_gate.dag b/dag/tools/regen_verify_gate.dag index 5a51ee53082..ce404657bf7 100644 --- a/dag/tools/regen_verify_gate.dag +++ b/dag/tools/regen_verify_gate.dag @@ -1,15 +1,25 @@ module tools.regen_verify_gate import std.process { ProcessExit, ExitSuccess, exit_failure } -import tools.regen_verify_transport { run_regen_verify } +import tools.regen_verify_transport { run_regen_verify_verdict } + +data regen_verify_failure_receipt_note: String = "The loudness half (gunbc#7644 diagnosis): the floor witness collapses this gate's ProcessExit to Bool via tools.ci_gates.exit_ok, so regen-verify reds reported only returned Bool(false). The gate body records the located reason from the single regen_stage0 capture; the failure-receipt companion consumes that retained detail instead of re-running regen." + +fn regen_verify_failure_reason(exit: ProcessExit) -> String { + match exit { + ExitSuccess => "" + ExitFailure { code: _, reason: reason } => reason + } +} func run_regen_verify_gate_body() -> ProcessExit { - return if run_regen_verify() { - ExitSuccess - } else { - exit_failure( - reason: "regen-verify gate failed: committed stage0 does not match a fresh single-pass regen (the Β§7 self-host fixed-point wall)" - ) + let verdict = run_regen_verify_verdict() + match verdict { + ExitSuccess => verdict + ExitFailure { code: _, reason: reason } => { + record_regen_verify_gate_failure_detail(detail: reason) + verdict + } } } @@ -17,6 +27,10 @@ func run_regen_verify_gate() -> ProcessExit { run_regen_verify_gate_body() } +func regen_verify_failure_receipt() -> String { + consume_regen_verify_gate_failure_detail() +} + func main() -> ProcessExit { run_regen_verify_gate_body() } diff --git a/dag/tools/regen_verify_transport.dag b/dag/tools/regen_verify_transport.dag index f97d224f3db..d0d2b85c8e2 100644 --- a/dag/tools/regen_verify_transport.dag +++ b/dag/tools/regen_verify_transport.dag @@ -1,15 +1,49 @@ module tools.regen_verify_transport -import tools.host_prelude { run_witness_bin_program } +import extdeps.git.inspect +import std.process { ProcessExit, ExitSuccess, exit_failure } +import v2.std.text { string_is_empty } +import tools.host_prelude { witness_bin_ready, witness_bin_release_path } import tools.self_host_realized_comparison_transport { - realized_comparison_args, - realized_comparison_invoke + realized_comparison_invoke, + realized_comparison_verify_args, +} + +data regen_verify_capture_note: String = "The regen-verify gate must surface drifted generated file names on failure. regen_stage0 writes the located reason to stderr (Changed generated file(s): ...) and regen_divergence_count=N to stdout; WitnessBin.Run captures both so the gate verdict and failure receipt share one observation without a second regen invocation." + +fn regen_verify_failure_detail(stdout: String, stderr: String) -> String { + if !string_is_empty(s: stderr) { + stderr + } else if !string_is_empty(s: stdout) { + concat("regen-verify gate failed: ", stdout) + } else { + "regen-verify gate failed: witness binary exited nonzero with no diagnostic output" + } +} + +fn run_regen_verify_verdict() -> ProcessExit { + let root = git.Inspect.Toplevel() + if witness_bin_ready(root: root.path, bin: realized_comparison_invoke()) { + let result = gunbc.WitnessBin.Run( + workdir: root.path, + bin_path: witness_bin_release_path(root: root.path, bin_name: realized_comparison_invoke().bin_name), + args: realized_comparison_verify_args() + ) + if result.success { + ExitSuccess + } else { + exit_failure( + reason: regen_verify_failure_detail(stdout: result.stdout, stderr: result.stderr) + ) + } + } else { + exit_failure(reason: "regen-verify gate failed: regen_stage0 witness binary unavailable") + } } fn run_regen_verify() -> Bool { - run_witness_bin_program( - invoke: realized_comparison_invoke(), - prereqs: [], - args: concat(realized_comparison_args(), ["--verify"]) - ) + match run_regen_verify_verdict() { + ExitSuccess => true + _ => false + } } diff --git a/src/v1/04_method.dag b/src/v1/04_method.dag index 44cccd469b6..d8c8c91d531 100644 --- a/src/v1/04_method.dag +++ b/src/v1/04_method.dag @@ -138,6 +138,11 @@ fn builtin_function_registry() -> Map { let m = map_insert(m, "witness_layer_roots_compile_clean_check", bool_type) let m = map_insert(m, "witness_layer_roots_compile_clean_emit_check", bool_type) let m = map_insert(m, "consume_floor_compile_clean_gate_verdict", bool_type) + let m = map_insert(m, "consume_floor_compile_clean_gate_failure_detail", string_type) + let m = map_insert(m, "record_regen_verify_gate_failure_detail", unit_type) + let m = map_insert(m, "consume_regen_verify_gate_failure_detail", string_type) + let m = map_insert(m, "record_generated_artifact_drift_gate_failure_detail", unit_type) + let m = map_insert(m, "consume_generated_artifact_drift_gate_failure_detail", string_type) let m = map_insert(m, "witness_compile_clean_cli_floor_verdicts_agree", bool_type) let m = map_insert(m, "test_migration_debt_module_count", int_type) let m = map_insert(m, "test_migration_debt_total_loc", int_type) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 1602875e0bd..60b45271a3d 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -3972,11 +3972,78 @@ fn append_test_floor_compile_clean_inject(sources: &mut Vec> = Mutex::new(None); +static REGEN_VERIFY_GATE_FAILURE_DETAIL: Mutex> = Mutex::new(None); +static GENERATED_ARTIFACT_DRIFT_GATE_FAILURE_DETAIL: Mutex> = Mutex::new(None); + +pub fn record_regen_verify_gate_failure_detail(detail: String) { + if let Ok(mut guard) = REGEN_VERIFY_GATE_FAILURE_DETAIL.lock() { + *guard = Some(detail); + } +} + +pub fn consume_regen_verify_gate_failure_detail() -> String { + match REGEN_VERIFY_GATE_FAILURE_DETAIL.lock() { + Ok(guard) => guard.clone().unwrap_or_else(|| { + "regen_verify failure detail unavailable (gate body did not run in this process)" + .to_string() + }), + Err(e) => format!("regen_verify failure detail refused: gate detail lock poisoned ({e})"), + } +} + +pub fn record_generated_artifact_drift_gate_failure_detail(detail: String) { + if let Ok(mut guard) = GENERATED_ARTIFACT_DRIFT_GATE_FAILURE_DETAIL.lock() { + *guard = Some(detail); + } +} + +#[cfg(test)] +fn reset_regen_verify_gate_failure_detail_for_test() { + if let Ok(mut guard) = REGEN_VERIFY_GATE_FAILURE_DETAIL.lock() { + *guard = None; + } +} + +#[cfg(test)] +fn reset_generated_artifact_drift_gate_failure_detail_for_test() { + if let Ok(mut guard) = GENERATED_ARTIFACT_DRIFT_GATE_FAILURE_DETAIL.lock() { + *guard = None; + } +} + +pub fn consume_generated_artifact_drift_gate_failure_detail() -> String { + match GENERATED_ARTIFACT_DRIFT_GATE_FAILURE_DETAIL.lock() { + Ok(guard) => guard.clone().unwrap_or_else(|| { + "generated-artifact drift failure detail unavailable (gate body did not run in this process)" + .to_string() + }), + Err(e) => format!( + "generated-artifact drift failure detail refused: gate detail lock poisoned ({e})" + ), + } +} + +fn format_first_compile_clean_hard_diagnostic(diagnostics: &im::Vector>) -> String { + diagnostics + .iter() + .find(|d| compile_clean_diagnostic_is_hard(d)) + .map(|d| { + format!( + "compile-clean: {}", + diagnostic_to_message(d.diagnostic.clone()) + ) + }) + .unwrap_or_else(|| { + "dag compile-clean gate failed: no hard diagnostic located in compile receipt" + .to_string() + }) +} + /// Cost snapshot of the ONE compile-clean leg β€” the "receipt keys" half of the /// prelude-coverage-hole follow-up (`gunbc_ci_floor_batch_clamp_note` row (a)): /// the leg's wall, its closure unit count (modules in the compiled closure β€” the @@ -4091,7 +4158,7 @@ fn floor_compile_clean_emit_ok(sources: Vec> fn floor_compile_clean_emit_ok_via_index( sources: Vec>, index_roots: &[String], -) -> bool { +) -> (bool, String) { use crate::v1_compiler_artifact::RenderTarget; use crate::v1_compiler_complexity::empty_complexity_report; let index = process_shared_index(index_roots); @@ -4107,12 +4174,15 @@ fn floor_compile_clean_emit_ok_via_index( Ok(resolved) => resolved, Err(msg) => { eprintln!("compile-clean: hard diagnostics:\n{msg}"); - return false; + return (false, format!("compile-clean: {msg}")); } }; if compile_clean_pipeline_has_hard_errors(compile_clean_diags.as_ref()) { eprint_compile_clean_hard_diagnostics(compile_clean_diags.as_ref()); - return false; + return ( + false, + format_first_compile_clean_hard_diagnostic(compile_clean_diags.as_ref()), + ); } let newline_indices: Rc>> = Rc::new(si.values().cloned().collect::>()); @@ -4126,10 +4196,12 @@ fn floor_compile_clean_emit_ok_via_index( }); let result = v1_compiler_compile::emit_resolved_for_target(resolved, RenderTarget::Dag); if result.files.is_empty() { - eprintln!("floor compile-clean: refused β€” compile produced zero files (empty emit set)"); - return false; + let detail = "floor compile-clean: refused β€” compile produced zero files (empty emit set)" + .to_string(); + eprintln!("{detail}"); + return (false, detail); } - true + (true, String::new()) } fn produce_floor_compile_clean_receipt() -> FloorCompileCleanReceipt { @@ -4169,7 +4241,7 @@ fn produce_floor_compile_clean_receipt() -> FloorCompileCleanReceipt { let closure_units = sources.len() as u128; let leg_started = std::time::Instant::now(); let _ = drain_module_typecheck_walls(); - let ok = floor_compile_clean_emit_ok_via_index(sources, &index_roots); + let (ok, failure_detail) = floor_compile_clean_emit_ok_via_index(sources, &index_roots); let wall_ms = leg_started.elapsed().as_millis(); let module_typecheck_walls = drain_module_typecheck_walls(); write_floor_compile_clean_cost_receipt( @@ -4186,7 +4258,7 @@ fn produce_floor_compile_clean_receipt() -> FloorCompileCleanReceipt { pass_subject, }); } - FloorCompileCleanReceipt::Compiled { ok } + FloorCompileCleanReceipt::Compiled { ok, failure_detail } } } } @@ -4247,7 +4319,7 @@ pub fn install_floor_compile_clean_receipt() -> Result<(), String> { "claim_executor: floor compile-clean β€” one whole-tree --target dag compile via shared index (gate consumes receipt; batch-2 resolves reuse the typed store)" ); let receipt = produce_floor_compile_clean_receipt(); - if let FloorCompileCleanReceipt::Compiled { ok } = &receipt { + if let FloorCompileCleanReceipt::Compiled { ok, .. } = &receipt { eprintln!("claim_executor: floor compile-clean receipt ok={ok}"); } *guard = Some(receipt); @@ -4299,7 +4371,32 @@ pub fn consume_floor_compile_clean_gate_verdict() -> bool { eprintln!("compile-clean gate: refused ({reason})"); false } - Some(FloorCompileCleanReceipt::Compiled { ok }) => *ok, + Some(FloorCompileCleanReceipt::Compiled { ok, .. }) => *ok, + } +} + +/// Failure-receipt companion for the compile-clean gate: reads the in-run receipt only. +pub fn consume_floor_compile_clean_gate_failure_detail() -> String { + let guard = match FLOOR_COMPILE_CLEAN_RECEIPT.lock() { + Ok(g) => g, + Err(e) => { + return format!("compile-clean failure detail refused: receipt lock poisoned ({e})"); + } + }; + match guard.as_ref() { + None => "compile-clean failure detail unavailable: no in-run compile receipt".to_string(), + Some(FloorCompileCleanReceipt::Skipped { reason }) => { + format!("compile-clean gate skipped: {reason}") + } + Some(FloorCompileCleanReceipt::Refused { reason }) => reason.clone(), + Some(FloorCompileCleanReceipt::Compiled { + ok: true, + failure_detail: _, + }) => String::new(), + Some(FloorCompileCleanReceipt::Compiled { + ok: false, + failure_detail, + }) => failure_detail.clone(), } } @@ -4502,7 +4599,7 @@ pub fn compile_clean_floor_verdict_whole_tree() -> Result { None => return Ok(true), Some(s) => s, }; - Ok(floor_compile_clean_emit_ok_via_index(sources, &roots)) + Ok(floor_compile_clean_emit_ok_via_index(sources, &roots).0) } /// CLI compile-clean verdict: same source closure and diagnostic policy as the floor, @@ -5195,7 +5292,7 @@ mod compile_clean_via_index_verdict_equivalence { fn verdicts(&self) -> (bool, bool) { let raw = floor_compile_clean_emit_ok(self.sources.clone()); let via_index = - floor_compile_clean_emit_ok_via_index(self.sources.clone(), &self.roots); + floor_compile_clean_emit_ok_via_index(self.sources.clone(), &self.roots).0; (raw, via_index) } } @@ -34241,6 +34338,7 @@ mod witness_layer_roots_compile_clean_tests { reset_floor_compile_clean_receipt_for_test(); install_floor_compile_clean_receipt_fixture(FloorCompileCleanReceipt::Compiled { ok: false, + failure_detail: "compile-clean: synthetic hard diagnostic".to_string(), }); assert!( !consume_floor_compile_clean_gate_verdict(), @@ -34249,6 +34347,28 @@ mod witness_layer_roots_compile_clean_tests { }); } + #[test] + fn regen_verify_gate_failure_detail_unavailable_without_prior_record() { + reset_regen_verify_gate_failure_detail_for_test(); + assert!( + consume_regen_verify_gate_failure_detail() + .contains("gate body did not run in this process"), + "missing record must refuse with a located message, not imply an empty success verdict" + ); + } + + #[test] + fn regen_verify_gate_failure_detail_round_trips_recorded_reason() { + reset_regen_verify_gate_failure_detail_for_test(); + record_regen_verify_gate_failure_detail( + "Changed generated file(s): v1_compiler_emit_rust.rs".to_string(), + ); + assert_eq!( + consume_regen_verify_gate_failure_detail(), + "Changed generated file(s): v1_compiler_emit_rust.rs" + ); + } + /// Β§5 discriminating RED (end-to-end): real whole-tree compile with an injected broken module /// must refuse through install_floor_compile_clean_receipt β†’ consume_floor_compile_clean_gate_verdict. /// Ignored in CI: ~minutes cold whole-tree compile; recorded execution receipt in PR #6361 body. diff --git a/src/v1/stage0/src/v1_compiler_infer_method.rs b/src/v1/stage0/src/v1_compiler_infer_method.rs index d38de71487a..1aead324a50 100644 --- a/src/v1/stage0/src/v1_compiler_infer_method.rs +++ b/src/v1/stage0/src/v1_compiler_infer_method.rs @@ -375,6 +375,31 @@ pub fn builtin_function_registry() -> Rc>> { "consume_floor_compile_clean_gate_verdict".to_string(), bool_type(), ); + let m = v1_rt::rc_map_insert( + m.clone(), + "consume_floor_compile_clean_gate_failure_detail".to_string(), + string_type(), + ); + let m = v1_rt::rc_map_insert( + m.clone(), + "record_regen_verify_gate_failure_detail".to_string(), + unit_type(), + ); + let m = v1_rt::rc_map_insert( + m.clone(), + "consume_regen_verify_gate_failure_detail".to_string(), + string_type(), + ); + let m = v1_rt::rc_map_insert( + m.clone(), + "record_generated_artifact_drift_gate_failure_detail".to_string(), + unit_type(), + ); + let m = v1_rt::rc_map_insert( + m.clone(), + "consume_generated_artifact_drift_gate_failure_detail".to_string(), + string_type(), + ); let m = v1_rt::rc_map_insert( m.clone(), "witness_compile_clean_cli_floor_verdicts_agree".to_string(), diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index c8f2bd1403b..450b76274b9 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -11394,6 +11394,32 @@ macro_rules! v1_builtin_arms { crate::cli_run::consume_floor_compile_clean_gate_verdict(), ))), + arm "free_call.consume_floor_compile_clean_gate_failure_detail" { "consume_floor_compile_clean_gate_failure_detail" } => Ok(Some(Value::Str( + crate::cli_run::consume_floor_compile_clean_gate_failure_detail(), + ))), + + arm "free_call.record_regen_verify_gate_failure_detail" { "record_regen_verify_gate_failure_detail" } => { + if let [Value::Str(detail)] = $positional.as_slice() { + crate::cli_run::record_regen_verify_gate_failure_detail(detail.clone()); + } + Ok(Some(Value::Unit)) + }, + + arm "free_call.consume_regen_verify_gate_failure_detail" { "consume_regen_verify_gate_failure_detail" } => Ok(Some(Value::Str( + crate::cli_run::consume_regen_verify_gate_failure_detail(), + ))), + + arm "free_call.record_generated_artifact_drift_gate_failure_detail" { "record_generated_artifact_drift_gate_failure_detail" } => { + if let [Value::Str(detail)] = $positional.as_slice() { + crate::cli_run::record_generated_artifact_drift_gate_failure_detail(detail.clone()); + } + Ok(Some(Value::Unit)) + }, + + arm "free_call.consume_generated_artifact_drift_gate_failure_detail" { "consume_generated_artifact_drift_gate_failure_detail" } => Ok(Some(Value::Str( + crate::cli_run::consume_generated_artifact_drift_gate_failure_detail(), + ))), + arm "free_call.witness_compile_clean_cli_floor_verdicts_agree" { "witness_compile_clean_cli_floor_verdicts_agree" } => Ok(Some(Value::Bool( crate::cli_run::witness_compile_clean_cli_floor_verdicts_agree(), ))), diff --git a/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs b/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs index bbac606371f..efbb9de88a0 100644 --- a/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs +++ b/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs @@ -93,6 +93,11 @@ pub enum EvalBuiltinArm { FreeCallWitnessLayerRootsCompileCleanCheck, FreeCallWitnessLayerRootsCompileCleanEmitCheck, FreeCallConsumeFloorCompileCleanGateVerdict, + FreeCallConsumeFloorCompileCleanGateFailureDetail, + FreeCallRecordRegenVerifyGateFailureDetail, + FreeCallConsumeRegenVerifyGateFailureDetail, + FreeCallRecordGeneratedArtifactDriftGateFailureDetail, + FreeCallConsumeGeneratedArtifactDriftGateFailureDetail, FreeCallWitnessCompileCleanCliFloorVerdictsAgree, FreeCallTestMigrationDebtModuleCount, FreeCallTestMigrationDebtTotalLoc, @@ -224,6 +229,11 @@ pub fn lookup_eval_builtin_inner(spelling: &str) -> Option { "witness_layer_roots_compile_clean_check" => Some(EvalBuiltinArm::FreeCallWitnessLayerRootsCompileCleanCheck), "witness_layer_roots_compile_clean_emit_check" => Some(EvalBuiltinArm::FreeCallWitnessLayerRootsCompileCleanEmitCheck), "consume_floor_compile_clean_gate_verdict" => Some(EvalBuiltinArm::FreeCallConsumeFloorCompileCleanGateVerdict), + "consume_floor_compile_clean_gate_failure_detail" => Some(EvalBuiltinArm::FreeCallConsumeFloorCompileCleanGateFailureDetail), + "record_regen_verify_gate_failure_detail" => Some(EvalBuiltinArm::FreeCallRecordRegenVerifyGateFailureDetail), + "consume_regen_verify_gate_failure_detail" => Some(EvalBuiltinArm::FreeCallConsumeRegenVerifyGateFailureDetail), + "record_generated_artifact_drift_gate_failure_detail" => Some(EvalBuiltinArm::FreeCallRecordGeneratedArtifactDriftGateFailureDetail), + "consume_generated_artifact_drift_gate_failure_detail" => Some(EvalBuiltinArm::FreeCallConsumeGeneratedArtifactDriftGateFailureDetail), "witness_compile_clean_cli_floor_verdicts_agree" => Some(EvalBuiltinArm::FreeCallWitnessCompileCleanCliFloorVerdictsAgree), "test_migration_debt_module_count" => Some(EvalBuiltinArm::FreeCallTestMigrationDebtModuleCount), "test_migration_debt_total_loc" => Some(EvalBuiltinArm::FreeCallTestMigrationDebtTotalLoc), @@ -353,6 +363,11 @@ macro_rules! eval_builtin_inner_arm { ("free_call.witness_layer_roots_compile_clean_check") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallWitnessLayerRootsCompileCleanCheck }; ("free_call.witness_layer_roots_compile_clean_emit_check") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallWitnessLayerRootsCompileCleanEmitCheck }; ("free_call.consume_floor_compile_clean_gate_verdict") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallConsumeFloorCompileCleanGateVerdict }; + ("free_call.consume_floor_compile_clean_gate_failure_detail") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallConsumeFloorCompileCleanGateFailureDetail }; + ("free_call.record_regen_verify_gate_failure_detail") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallRecordRegenVerifyGateFailureDetail }; + ("free_call.consume_regen_verify_gate_failure_detail") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallConsumeRegenVerifyGateFailureDetail }; + ("free_call.record_generated_artifact_drift_gate_failure_detail") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallRecordGeneratedArtifactDriftGateFailureDetail }; + ("free_call.consume_generated_artifact_drift_gate_failure_detail") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallConsumeGeneratedArtifactDriftGateFailureDetail }; ("free_call.witness_compile_clean_cli_floor_verdicts_agree") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallWitnessCompileCleanCliFloorVerdictsAgree }; ("free_call.test_migration_debt_module_count") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallTestMigrationDebtModuleCount }; ("free_call.test_migration_debt_total_loc") => { $crate::v1_interpreter_dispatch_generated::EvalBuiltinArm::FreeCallTestMigrationDebtTotalLoc };