From 797130e8b35c406e8909580f4451a65b3f20b74c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 08:44:16 +0000 Subject: [PATCH 1/4] WalkPlan: give walk plans an on-success stage population, parsed strictly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation commit of the success-stages redesign (operator design ruling 2026-07-30). This lands the carrier, the strict parser, the renames, and sequential fail-fast stage execution in the executor. Every plan declares on_success_stages: [] today, so runtime behavior is unchanged until the merge-admission migration populates the floor plan's stages in a follow-up commit on this branch. THE CARRIER. std.realization_schedule gains type WalkPlan { batches: List> on_success_stages: List> } Two populations with different ordering laws, and the type says so where a bare List> could not: batches are the ordinary floor under FloorBatchStopPolicy; on_success_stages run only after the ordinary floor completed AND its receipts wrote, each stage a barrier, always fail-fast between stages — FullLedger is an ordinary-floor policy and never applies between stages. The carrier note records BOTH prior defects: the trailing-batch fail-open, and its sibling — members WITHIN a stage run concurrently, so anything sequential must be ONE claim whose body sequences its steps (the first repair draft re-created exactly that bug and the note names it so the next author does not). THE RENAMES, because a function whose value now carries postcondition stages must not keep a name that says it returns only batches: gunbc_ci_floor_batches -> gunbc_ci_floor_plan gunbc_ci_regen_floor_batches -> gunbc_ci_regen_floor_plan gunbc_ci_plan_artifact_batches -> gunbc_ci_plan_artifact_plan gunbc_falsifier_batches -> gunbc_falsifier_plan The four argv/step consumers followed automatically because they derive from the floor_plan_function / plan_artifact_plan_function / regen_floor_plan_function / falsifier_plan_function constants — the constants are the single naming authority and were renamed with the functions; ci.yml and falsifier.yml regenerate with the new names at all three invocation sites. The internal batch builders survive as *_ordinary_batches, which the structural witnesses now target. The budget_red_control fixture renames with the floor fn it impersonates (the clamp arms by name). Executor string keys (clamp gate, falsifier budget flag, eager compile-clean install, arm-time refusal roster) renamed in the same motion. THE PARSER is one and strict. walk_plan_from_plan requires BOTH fields; a plan with no postconditions declares an empty list, never omits the field, and there is deliberately NO fallback from a failed record parse to a bare-list reading — that fallback would run a malformed plan with its success stages silently dropped, the silent-widen arm section 5 forbids. STAGE EXECUTION in run_walk: gated on the whole ordinary floor contract this process owns — batch verdicts AND every receipt write — not just any_failed. Members execute serially in-process via run_memo_shared_claims with a stage-local memo, so an entry shared by stages resolves once and is reused; serial execution provides more order than the contract promises, which is safe, while the contract itself promises none. A non-claim runnable in a stage is a typed refusal, never a widen. Stage materialization is structurally NOT folded into the floor materialization receipt: stage memo contexts drop after it is written. A new receipt class, target/floor-on-success-receipt.txt, records declared/run counts, per-stage verdicts, and on_success_resolves_total — separate from the ordinary resolve receipt BY LIFECYCLE, so ci_floor_declared_resolve_count measures exactly the population it always did. On a red ordinary floor the receipt still writes, loudly, with skipped=ordinary_floor_failed and zero stages run — a typed diagnostic, never an admission artifact. VERIFIED BY EXECUTION: RED a bare-list plan fn refuses: "malformed plan value (gunbc_ci_floor_ordinary_batches): WalkPlan missing field `batches`", exit 1 — no fallback. (This control also caught a missed internal call site at ci_floor_plan.dag:1360.) GREEN gunbc_ci_floor_plan parses to the same 5 batches, then this container's pre-existing FloorBudgetBelowMinimumFootprint fail-fast — which itself proves the parse ran first. ci_floor_plan_witness_test 14/14 · ci_spec_witness_test 27/27 · falsifier_workflow_witness_test 11/11 · gunbc_invoke_witness_test 7/7 · pr_native_batch_test 2/2 · realization_schedule_witness 4/4. Generated artifacts regenerate; cargo fmt --check clean; claim_executor and gunbc build clean. (Workspace-wide cargo build fails in v1-stage0-std-core with 7 pre-existing errors, present on clean origin/main with this change stashed; CI builds named bins only.) REMAINING ON THIS BRANCH, per the settled design: executor-level floor finalization (resolve law + materialization disclosure in-process, the two GitHub gate steps deleted), the tools.merge_admission entry (capture_tested_subject as the first ordinary batch, stamp_tested_floor, refresh_target_and_gate), attempt-scoped receipt schema v2, the task #12 dead-emitter deletion, timeout derivation as the two-budget sum, and the operator's witness list including the stage-barrier latch test. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N7LfCpidMuDjV3FxYUHv1K --- .github/workflows/ci.yml | 4 +- .github/workflows/falsifier.yml | 4 +- dag/gunbc/ci_spec.dag | 8 +- dag/gunbc/commit_workflow.dag | 4 +- dag/gunbc/falsifier_workflow.dag | 2 +- dag/gunbc/plans/ci_process_end_to_end.dag | 4 +- dag/gunbc/proactive_verification_ledger.dag | 4 +- dag/std/realization_schedule.dag | 7 + dag/test/claim/gunbc_invoke_witness_test.dag | 8 +- docs/plans/ci-process-end-to-end.md | 4 +- src/v1/stage0/src/bin/claim_executor.rs | 264 ++++++++++++++++-- src/v1/stage0/src/cli_run.rs | 2 +- .../self_host/wet_receipt_enrollment.dag | 2 +- .../test/claim/ci_floor_plan_witness_test.dag | 16 +- src/v2/test/claim/ci_spec_witness_test.dag | 4 +- src/v2/test/claim/pr_native_batch_test.dag | 4 +- .../claim/realization_schedule_witness.dag | 4 +- .../floor_skip/budget_red_control_plan.dag | 10 +- src/v2/workflow/ci_floor_plan.dag | 55 +++- 19 files changed, 333 insertions(+), 77 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7b40847cec..3a2d3806169 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,7 +166,7 @@ jobs: exit 0 fi fi - "$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_regen_floor_batches --notice-title "self-host fixed-point (regen + staleness) — required; own regen job upstream of ci" + "$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_regen_floor_plan --notice-title "self-host fixed-point (regen + staleness) — required; own regen job upstream of ci" timeout-minutes: 15 ci: runs-on: [self-hosted, linux, arm64] @@ -210,7 +210,7 @@ jobs: # REMOVED 2026-07-21 (gunbc#7023 / proud-cat-517): the documentation_only_skip shell shortcut exited before claim_executor, bypassing the witness corpus entirely on docs-only PRs. That bypassed the existing ReadsLiveTree never-predict-skip lane — doc_reachability_witness_test.dag already declared ReadsLiveTree since #6654, but the shell never reached selection. Docs-only PRs now run the normal floor (SelectionApplied): import-closure skips non-live-tree witnesses cheaply; ReadsLiveTree rows (doc-graph wall, corpus-read host-fed lenses) always run. The isolated pre-executor skip-label binary and its unreachable persistent disposition transport retired afterward; compile-clean docs-universe scoping remains independently exercised on the live floor path. git fetch --no-tags origin main 2>/dev/null || true set +e - "$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_batches --notice-title "v2 claim corpus" + "$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" FLOOR_EXIT=$? ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) CI_FLOOR_EXIT=$FLOOR_EXIT "$ROOT/target/release/gunbc" run --source-root dag --entry dag/tools/merge_admission_stamp.dag --function main diff --git a/.github/workflows/falsifier.yml b/.github/workflows/falsifier.yml index 7df10e92076..870c760d349 100644 --- a/.github/workflows/falsifier.yml +++ b/.github/workflows/falsifier.yml @@ -108,7 +108,7 @@ jobs: id: floor run: | ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) - "$ROOT/target/release/claim_executor" --source-root dag --source-root src/v2 --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_falsifier_batches + "$ROOT/target/release/claim_executor" --source-root dag --source-root src/v2 --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_falsifier_plan env: GUNBC_CI_DIFF_BASE: HEAD~20 GUNBC_CI_DIFF_HEAD: HEAD @@ -116,7 +116,7 @@ jobs: - name: compile-clean whole-tree cold control (scoped-PR admission counterpart) run: | ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) - "$ROOT/target/release/claim_executor" --source-root dag --source-root src/v2 --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_ci_plan_artifact_batches + "$ROOT/target/release/claim_executor" --source-root dag --source-root src/v2 --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_ci_plan_artifact_plan env: GUNBC_CI_COMPILE_CLEAN_COLD_CONTROL: 1 timeout-minutes: 30 diff --git a/dag/gunbc/ci_spec.dag b/dag/gunbc/ci_spec.dag index 1b21db66d8e..1a850100615 100644 --- a/dag/gunbc/ci_spec.dag +++ b/dag/gunbc/ci_spec.dag @@ -187,7 +187,7 @@ fn ci_spec_with_discovery_scan_dirs(spec: CiSpec, dirs: List) -> CiSpec } } -data gunbc_ci_floor_batch_wall_budget_note: String = "SUPERSEDED by the derived clamp gunbc_ci_floor_batch_clamp_note (Piece 3, 2026-07-24): the hand-set gunbc_ci_floor_batch_wall_budget_seconds list this note governed is deleted, replaced by overhead + units*rate computed at run time; this note is kept for the operator-signed static-era history (including the two 1320->1440->1680 raises below) and the raise discipline, which carries forward to the clamp constants. THE COST WALL (CI floor endgame D5, operator brief 2026-07-23 — the resolve-regression journey's section-3 finding made mechanism: nothing redded a merge that added floor minutes, so every recovered minute was re-spent by the next lane's enrollment, and the step timeout's own bounce history 30 -> 60 -> 90 -> 120 -> 180 -> 270 -> 55 is the record of the budget being raised to fit). Per-BATCH wall budgets for gunbc_ci_floor_batches, enforced by claim_executor at walk time: each batch's measured wall is recorded as a typed receipt row (target/floor-batch-wall-receipt.txt), and a batch over its budget is a typed, located refusal — FLOOR-BATCH-OVER-BUDGET naming the batch index, measured wall, budget row, and this carrier — that reds the walk. A failure arm refuses, never widens: over-budget never triggers a rerun, a wider scope, or a silent cap raise. Denominated PER-BATCH, never per-run, because plumbing PRs have a structurally different cost profile (attribution doc section 9.2: a PR touching host_prelude/cli_run legitimately runs 46 of 55 wet rows — a per-run wall would red every such PR spuriously; per-batch budgets absorb the profile where it lands). RULING RECONCILIATION (the operator dispatching the 2026-07-23 endgame brief is the sign-off on this reading): the standing rule 'no wall-clock term in any verdict' (ci_floor_materialization_receipt_note, operator ruling 2026-07-10) governs WITNESS VERDICTS — a witness's pass/fail must never depend on how long it took. Batch budgets do not touch witness verdicts: they are admission/scheduling facts at the walk grain, the same split the 5-second fast-lane eval law already uses (gunbc_ci_fast_lane_rule_note: the deadline is lane admission, not a verdict term), so a batch refusal names the BATCH, and every witness verdict inside it stands as evaluated. RAISE DISCIPLINE: raising any budget requires appending a dated receipt note here naming the run id and the enrollment that grew the batch — the same discipline as the timeout-note bounce history — never a silent number edit. Budget basis (run 30009199696, post-#7122, with the endgame fixes priced in): batch 1 cheap gates 185s measured -> 125s post-pool, budget 240; batch 2 receipt consume ~0s, budget 60; batch 3 discovery 1024s measured (corpus-denominated resolve wall, lever-1 re-diagnosis pending), budget 1320; batch 4 wet corpora 649s plumbing-profile measured -> ~175s post-re-home, budget 420; batch 5 emit-host 7s, budget 120; batch 6 ingest 414s measured (4 composed overlay children — the genuine mktemp constraint), budget 600; batch 7 reads-real-bytes 206s, budget 420. Sum 3180s = 53min under the 55min step cap. OPERATOR SIGNATURE (briansrls, 2026-07-23): the admission/verdict reading above is affirmed — per-batch wall budgets are scheduling-admission facts, not wall-clock terms in witness verdicts; this line is the declared human-signed exemption row the 2026-07-10 ruling requires. Raise discipline amended: RAISING any budget row requires a new dated operator-signed line here naming the run id and the enrollment that grew the batch; TIGHTENING may land by ordinary receipt note. The on-call remedy for FLOOR-BATCH-OVER-BUDGET is diagnose-or-signed-raise, never rerun. FOLLOW-UP ROWS (operator rework push, 2026-07-23): (a) PRELUDE COVERAGE HOLE — the ~5min before batch-1 arms (naming-hygiene walk, policy install, plan resolve/eval, governor arm, eager compile-clean install) sits OUTSIDE every batch budget and can only red at the 55min step cap; a prelude budget row lands when the phase_mark walls get their own receipt keys. (b) IDENTITY-KEYED BUDGETS — rows are keyed by batch INDEX today (the coverage witness pins length, so a schedule change reds loudly rather than misassigning); keying by batch content-identity dissolves the index coupling and rides the ComputationIdentity lane. (c) K-CAP UNION-RSS RECEIPT — cheap_gate_pool_max_claims_per_child=16 is provisional until the pooled child's union RSS is receipted (claim_batch already prints per-shard-peak-rss; the first CI runs' logs back or re-size the cap; local proxy receipt: the 4-gate outer child peaked 1.82GB with the 12-claim pool nested). OPERATOR-SIGNED RAISE (briansrls, 2026-07-24): batch 3 (row index [2]) 1320 -> 1440 seconds, +9.1 percent (operator: 'lets raise the budget slightly more to give it some room over main' / 'like 10%'; 24min keeps the row's whole-minute grain). RUN IDS: 30063268739 @ e7a9006f wall_ms=1629287, 30058048605 wall_ms=1344115, 30055079462 wall_ms=1380561 — all batch=3 against budget_ms=1320000 on PR #7137. THE ENROLLMENT THAT GREW THE BATCH: none, and that mismatch with this discipline's template IS the receipt. Batch 3 is the affected-set-selected discovery corpus, so its wall is denominated in the DIFF's affected entry count, not in a roster edit: #7137 touches gunbc.ci_layer_roots (broadly imported) and src/v1/stage0/src/cli_run.rs, selecting 839-841 entries where a typical main push selects 600-601. Per-entry cost is at PARITY with green controls, so nothing regressed — #7137 1.602/1.609/1.646 s-per-entry vs main 1.574, clever-ram-110 1.756, clever-pike-49 1.504. The retired 1320 divided by 841 entries = 1.570 s-per-entry, BELOW main's own 1.574 measured rate: the row had zero headroom for any broad-touch diff and would red a no-op PR that merely touched a widely-imported carrier. 1440/841 = 1.712 s-per-entry, ~8.8 percent over main's rate — that is the 'room over main'. HONEST RESIDUAL: 1440 covers three of the four observed runs; the 1629287 outlier (run 30063268739, host peaked 9GB vs 16GB on the other three, 1.937 s-per-entry) still reds, and that is the wall working as designed — the remedy stays diagnose-or-signed-raise, never rerun. SUM ACCOUNTING (the one invariant this raise spends): the ceiling sum moves 3180 -> 3300 seconds = exactly 55min, so this note's original 'Sum 3180s = 53min under the 55min step cap' margin is now CONSUMED. Ceilings are per-batch maxima rather than a predicted run wall, but the sum was this note's stated safety argument and it no longer holds with margin, so it is recorded here rather than dropped. FOLLOW-UP (not landed here): restore margin by TIGHTENING the rows with the largest declared-vs-measured gap once post-merge receipts exist — batch 4 (budget 420, ~175s measured post-re-home) and batch 6 (budget 600, 414s measured) — which lands by ordinary receipt note under this same discipline. OPERATOR-SIGNED RAISE (briansrls, 2026-07-24, second): batch 3 (row index [2]) 1440 -> 1680 seconds, +16.7 percent (28min, whole-minute grain). Operator's word this raise: 'you may bump the budget to 1680 - we're redoing it completely anyway' — so 1680 is a STOPGAP to green this PR while the batch-budget mechanism itself is reworked (the FOLLOW-UP ROWS above: prelude coverage, identity-keyed budgets, k-cap union-RSS), not a claim that 1680 is the settled ceiling. WHY 1440 WAS UNDER-SIZED: the first raise sized off three observations (1344/1381/1629) where one landed on a fast host; five batch-3 walls are now observed on #7137 — 1344115, 1380561, 1558226 (run 30081341899 @ 06a1f2c, host srv4-03), 1581666 (run 30067149030 @ 93642bc, host srv4-03), 1629287 (run 30063268739) — and THREE of five exceed 1440, so 1440 reds the majority, not a lone outlier. This is host variance on the ~840-entry affected set, not a per-entry regression (per-entry stays at parity with green controls, ~1.6-1.9 s-per-entry across hosts). THE ENROLLMENT THAT GREW THE BATCH: still none — batch 3 is affected-set-denominated and #7137 touches broadly-imported carriers (gunbc.ci_layer_roots, cli_run.rs), the same profile the FLOOR-BATCH-OVER-BUDGET template's 'enrollment' field does not capture. SIZING: 1680 clears all five observed walls (max 1629287) with ~3 percent margin. SUM ACCOUNTING CORRECTED: the prior line's 'sum = 55min, margin consumed' was over-conservative — per-batch maxima never co-occur, and run 30081341899's ACTUAL floor step wall was ~44min (b1 185s + b2 ~0s + b3 1558s + b4 175s + b5 7s + b6 414s + b7 206s), well under the 55min step timeout that is the real constraint; the ceiling-sum (now 3540s) is a paper figure nothing hits. The remedy stays diagnose-or-signed-raise, never rerun." +data gunbc_ci_floor_batch_wall_budget_note: String = "SUPERSEDED by the derived clamp gunbc_ci_floor_batch_clamp_note (Piece 3, 2026-07-24): the hand-set gunbc_ci_floor_batch_wall_budget_seconds list this note governed is deleted, replaced by overhead + units*rate computed at run time; this note is kept for the operator-signed static-era history (including the two 1320->1440->1680 raises below) and the raise discipline, which carries forward to the clamp constants. THE COST WALL (CI floor endgame D5, operator brief 2026-07-23 — the resolve-regression journey's section-3 finding made mechanism: nothing redded a merge that added floor minutes, so every recovered minute was re-spent by the next lane's enrollment, and the step timeout's own bounce history 30 -> 60 -> 90 -> 120 -> 180 -> 270 -> 55 is the record of the budget being raised to fit). Per-BATCH wall budgets for gunbc_ci_floor_plan (named gunbc_ci_floor_batches through the static era this note records), enforced by claim_executor at walk time: each batch's measured wall is recorded as a typed receipt row (target/floor-batch-wall-receipt.txt), and a batch over its budget is a typed, located refusal — FLOOR-BATCH-OVER-BUDGET naming the batch index, measured wall, budget row, and this carrier — that reds the walk. A failure arm refuses, never widens: over-budget never triggers a rerun, a wider scope, or a silent cap raise. Denominated PER-BATCH, never per-run, because plumbing PRs have a structurally different cost profile (attribution doc section 9.2: a PR touching host_prelude/cli_run legitimately runs 46 of 55 wet rows — a per-run wall would red every such PR spuriously; per-batch budgets absorb the profile where it lands). RULING RECONCILIATION (the operator dispatching the 2026-07-23 endgame brief is the sign-off on this reading): the standing rule 'no wall-clock term in any verdict' (ci_floor_materialization_receipt_note, operator ruling 2026-07-10) governs WITNESS VERDICTS — a witness's pass/fail must never depend on how long it took. Batch budgets do not touch witness verdicts: they are admission/scheduling facts at the walk grain, the same split the 5-second fast-lane eval law already uses (gunbc_ci_fast_lane_rule_note: the deadline is lane admission, not a verdict term), so a batch refusal names the BATCH, and every witness verdict inside it stands as evaluated. RAISE DISCIPLINE: raising any budget requires appending a dated receipt note here naming the run id and the enrollment that grew the batch — the same discipline as the timeout-note bounce history — never a silent number edit. Budget basis (run 30009199696, post-#7122, with the endgame fixes priced in): batch 1 cheap gates 185s measured -> 125s post-pool, budget 240; batch 2 receipt consume ~0s, budget 60; batch 3 discovery 1024s measured (corpus-denominated resolve wall, lever-1 re-diagnosis pending), budget 1320; batch 4 wet corpora 649s plumbing-profile measured -> ~175s post-re-home, budget 420; batch 5 emit-host 7s, budget 120; batch 6 ingest 414s measured (4 composed overlay children — the genuine mktemp constraint), budget 600; batch 7 reads-real-bytes 206s, budget 420. Sum 3180s = 53min under the 55min step cap. OPERATOR SIGNATURE (briansrls, 2026-07-23): the admission/verdict reading above is affirmed — per-batch wall budgets are scheduling-admission facts, not wall-clock terms in witness verdicts; this line is the declared human-signed exemption row the 2026-07-10 ruling requires. Raise discipline amended: RAISING any budget row requires a new dated operator-signed line here naming the run id and the enrollment that grew the batch; TIGHTENING may land by ordinary receipt note. The on-call remedy for FLOOR-BATCH-OVER-BUDGET is diagnose-or-signed-raise, never rerun. FOLLOW-UP ROWS (operator rework push, 2026-07-23): (a) PRELUDE COVERAGE HOLE — the ~5min before batch-1 arms (naming-hygiene walk, policy install, plan resolve/eval, governor arm, eager compile-clean install) sits OUTSIDE every batch budget and can only red at the 55min step cap; a prelude budget row lands when the phase_mark walls get their own receipt keys. (b) IDENTITY-KEYED BUDGETS — rows are keyed by batch INDEX today (the coverage witness pins length, so a schedule change reds loudly rather than misassigning); keying by batch content-identity dissolves the index coupling and rides the ComputationIdentity lane. (c) K-CAP UNION-RSS RECEIPT — cheap_gate_pool_max_claims_per_child=16 is provisional until the pooled child's union RSS is receipted (claim_batch already prints per-shard-peak-rss; the first CI runs' logs back or re-size the cap; local proxy receipt: the 4-gate outer child peaked 1.82GB with the 12-claim pool nested). OPERATOR-SIGNED RAISE (briansrls, 2026-07-24): batch 3 (row index [2]) 1320 -> 1440 seconds, +9.1 percent (operator: 'lets raise the budget slightly more to give it some room over main' / 'like 10%'; 24min keeps the row's whole-minute grain). RUN IDS: 30063268739 @ e7a9006f wall_ms=1629287, 30058048605 wall_ms=1344115, 30055079462 wall_ms=1380561 — all batch=3 against budget_ms=1320000 on PR #7137. THE ENROLLMENT THAT GREW THE BATCH: none, and that mismatch with this discipline's template IS the receipt. Batch 3 is the affected-set-selected discovery corpus, so its wall is denominated in the DIFF's affected entry count, not in a roster edit: #7137 touches gunbc.ci_layer_roots (broadly imported) and src/v1/stage0/src/cli_run.rs, selecting 839-841 entries where a typical main push selects 600-601. Per-entry cost is at PARITY with green controls, so nothing regressed — #7137 1.602/1.609/1.646 s-per-entry vs main 1.574, clever-ram-110 1.756, clever-pike-49 1.504. The retired 1320 divided by 841 entries = 1.570 s-per-entry, BELOW main's own 1.574 measured rate: the row had zero headroom for any broad-touch diff and would red a no-op PR that merely touched a widely-imported carrier. 1440/841 = 1.712 s-per-entry, ~8.8 percent over main's rate — that is the 'room over main'. HONEST RESIDUAL: 1440 covers three of the four observed runs; the 1629287 outlier (run 30063268739, host peaked 9GB vs 16GB on the other three, 1.937 s-per-entry) still reds, and that is the wall working as designed — the remedy stays diagnose-or-signed-raise, never rerun. SUM ACCOUNTING (the one invariant this raise spends): the ceiling sum moves 3180 -> 3300 seconds = exactly 55min, so this note's original 'Sum 3180s = 53min under the 55min step cap' margin is now CONSUMED. Ceilings are per-batch maxima rather than a predicted run wall, but the sum was this note's stated safety argument and it no longer holds with margin, so it is recorded here rather than dropped. FOLLOW-UP (not landed here): restore margin by TIGHTENING the rows with the largest declared-vs-measured gap once post-merge receipts exist — batch 4 (budget 420, ~175s measured post-re-home) and batch 6 (budget 600, 414s measured) — which lands by ordinary receipt note under this same discipline. OPERATOR-SIGNED RAISE (briansrls, 2026-07-24, second): batch 3 (row index [2]) 1440 -> 1680 seconds, +16.7 percent (28min, whole-minute grain). Operator's word this raise: 'you may bump the budget to 1680 - we're redoing it completely anyway' — so 1680 is a STOPGAP to green this PR while the batch-budget mechanism itself is reworked (the FOLLOW-UP ROWS above: prelude coverage, identity-keyed budgets, k-cap union-RSS), not a claim that 1680 is the settled ceiling. WHY 1440 WAS UNDER-SIZED: the first raise sized off three observations (1344/1381/1629) where one landed on a fast host; five batch-3 walls are now observed on #7137 — 1344115, 1380561, 1558226 (run 30081341899 @ 06a1f2c, host srv4-03), 1581666 (run 30067149030 @ 93642bc, host srv4-03), 1629287 (run 30063268739) — and THREE of five exceed 1440, so 1440 reds the majority, not a lone outlier. This is host variance on the ~840-entry affected set, not a per-entry regression (per-entry stays at parity with green controls, ~1.6-1.9 s-per-entry across hosts). THE ENROLLMENT THAT GREW THE BATCH: still none — batch 3 is affected-set-denominated and #7137 touches broadly-imported carriers (gunbc.ci_layer_roots, cli_run.rs), the same profile the FLOOR-BATCH-OVER-BUDGET template's 'enrollment' field does not capture. SIZING: 1680 clears all five observed walls (max 1629287) with ~3 percent margin. SUM ACCOUNTING CORRECTED: the prior line's 'sum = 55min, margin consumed' was over-conservative — per-batch maxima never co-occur, and run 30081341899's ACTUAL floor step wall was ~44min (b1 185s + b2 ~0s + b3 1558s + b4 175s + b5 7s + b6 414s + b7 206s), well under the 55min step timeout that is the real constraint; the ceiling-sum (now 3540s) is a paper figure nothing hits. The remedy stays diagnose-or-signed-raise, never rerun." type FloorBatchClamp { overhead_seconds: Int @@ -355,9 +355,9 @@ fn git_fetch_script(policy: DiffPolicy) -> String { } data floor_plan_entry: String = "src/v2/workflow/ci_floor_plan.dag" -data floor_plan_function: String = "gunbc_ci_floor_batches" -data plan_artifact_plan_function: String = "gunbc_ci_plan_artifact_batches" -data regen_floor_plan_function: String = "gunbc_ci_regen_floor_batches" +data floor_plan_function: String = "gunbc_ci_floor_plan" +data plan_artifact_plan_function: String = "gunbc_ci_plan_artifact_plan" +data regen_floor_plan_function: String = "gunbc_ci_regen_floor_plan" fn scheduler_invoke_with(spec: CiSpec, plan_function: String) -> String { diff --git a/dag/gunbc/commit_workflow.dag b/dag/gunbc/commit_workflow.dag index b1233d1f529..66d53f11902 100644 --- a/dag/gunbc/commit_workflow.dag +++ b/dag/gunbc/commit_workflow.dag @@ -87,11 +87,11 @@ type CommitWorkflowRealizationBinding { } data githooks_pre_push_artifact_drift_gate_removed_disposition: Disposition = Terminal { - reason: "GeneratedArtifactDriftGate's GitPrePushHook enrollment REMOVED 2026-07-22 (fleet incident: this gate broke pushes fleet-wide — measured ~61min/2,176 witnesses on a rebased force-push touching measure.dag, since committed_generated_artifacts() regeneration walks the whole generated-artifact corpus, not an affected-set slice). Fail-closed verification before removal (DESIGN §5 — do not drop a check without confirming the authoritative boundary still covers it): GeneratedArtifactDriftGate stays enrolled on GithubActionsCiJob, which the required `ci` job runs via src/v2/workflow/ci_floor_plan.dag gunbc_ci_floor_batches (dag/gunbc/ci_spec.dag ci_gate list, RunnableSingleClaim on generated_artifact_drift_gate_passes); that same floor run is what stamps the merge-admission receipt tools.merge_admission_gate.dag reads at merge time, so drift is still a hard merge-time wall — pre-push was a redundant SECOND full-corpus run of the same unscoped check (DESIGN §2: duplicated work, not added safety), not the only place it was enforced. This mirrors the fmt precedent recorded in commit_gate_rust_suite_removed_disposition below: a git hook is opt-in per clone (core.hooksPath), bypassable with --no-verify, and absent in container worktrees, so it was never a sound enforcement AUTHORITY — only fast local feedback. The pre-push hook keeps the cheap guards (uncommitted-tree check, doc-reachability witness on the ReadsLiveTree entry, cargo fmt --all --check) and gunbc_ci_witness_corpus_only_batches' declared affected-set-scoped test gate (see its own note: deliberately narrowed to the hermetic discovery corpus, not this whole-corpus regen sweep). No skip env var was added — the heavy arm is gone from the model, not hidden behind a toggle (DESIGN §5 no-escape-hatches)." + reason: "GeneratedArtifactDriftGate's GitPrePushHook enrollment REMOVED 2026-07-22 (fleet incident: this gate broke pushes fleet-wide — measured ~61min/2,176 witnesses on a rebased force-push touching measure.dag, since committed_generated_artifacts() regeneration walks the whole generated-artifact corpus, not an affected-set slice). Fail-closed verification before removal (DESIGN §5 — do not drop a check without confirming the authoritative boundary still covers it): GeneratedArtifactDriftGate stays enrolled on GithubActionsCiJob, which the required `ci` job runs via src/v2/workflow/ci_floor_plan.dag gunbc_ci_floor_plan (dag/gunbc/ci_spec.dag ci_gate list, RunnableSingleClaim on generated_artifact_drift_gate_passes); that same floor run is what stamps the merge-admission receipt tools.merge_admission_gate.dag reads at merge time, so drift is still a hard merge-time wall — pre-push was a redundant SECOND full-corpus run of the same unscoped check (DESIGN §2: duplicated work, not added safety), not the only place it was enforced. This mirrors the fmt precedent recorded in commit_gate_rust_suite_removed_disposition below: a git hook is opt-in per clone (core.hooksPath), bypassable with --no-verify, and absent in container worktrees, so it was never a sound enforcement AUTHORITY — only fast local feedback. The pre-push hook keeps the cheap guards (uncommitted-tree check, doc-reachability witness on the ReadsLiveTree entry, cargo fmt --all --check) and gunbc_ci_witness_corpus_only_batches' declared affected-set-scoped test gate (see its own note: deliberately narrowed to the hermetic discovery corpus, not this whole-corpus regen sweep). No skip env var was added — the heavy arm is gone from the model, not hidden behind a toggle (DESIGN §5 no-escape-hatches)." } data githooks_pre_push_slimmed_to_fmt_disposition: Disposition = Terminal { - reason: "The pre-push hook was SLIMMED TO FMT-ONLY 2026-07-25 (operator ruling): 'Pre-push hook restored to fmt-only per CLAUDE.md's documented contract; the witness corpus belongs to CI, not the push path.' Removed from the GitPrePushHook surface: the doc-reachability CommitWitnessClaim enrollment, and (in gunbc.githooks_pre_push_emit) the whole `claim_batch --pre-push` shim — the affected-set WitnessCorpusRun gate, the DocWitnessRun gate, and the in-hook `cargo build -p v1-compiler --bin claim_batch` that ran from inside a push when the binary was missing. Fail-closed verification before removal (DESIGN §5 — do not drop a check without confirming the authoritative boundary still covers it): cargo fmt --all --check remains a required-path CI step (gunbc.ci_spec ci_fmt_gate_note — build runs it first, and ci needs:[build]); the witness corpus remains the required `ci` floor via gunbc_ci_floor_batches, which also stamps the merge-admission receipt tools.merge_admission_gate.dag reads; and dag/test/claim/doc_reachability_witness_test.dag keeps its cadence through the floor's discovery-corpus scan (CiSpec.discovery_scan_dirs covers dag/test/claim), so the doc-reachability rows lose no coverage — only their redundant second run on the push path. NOTHING LOSES ENFORCEMENT BECAUSE THE HOOK NEVER PROVIDED ANY: it is opt-in per clone (core.hooksPath), bypassable with --no-verify, absent in container worktrees, and PROVEN ineffective by #6658 (an unformatted .rs landed on main and no hook caught it) — the same argument commit_gate_rust_suite_removed_disposition and githooks_pre_push_artifact_drift_gate_removed_disposition already make. What the weight DID buy was harm: a non-authority check heavy enough to make a legitimately-red mid-lane branch feel unpushable is pressure that converts honest reds into workaround greens, and it was priced — three rounds of hand-import workarounds, one decorative gate, and the normalization of --no-verify on the namespace type-binding lane (2026-07-25). A check that cannot enforce should at least be instant. Explicitly DECLINED as the alternative: branch-level expected-red exemption machinery — building configuration so a non-authority check can be tolerated invests in a mechanism that should not carry this weight at all, and slimming dissolves the need. No skip env var was added; the heavy arms are gone from the model, not hidden behind a toggle (DESIGN §5 no-escape-hatches). Re-heavying requires a GitPrePushHook roster edit, which dag/test/claim/githooks_pre_push_emit_test.dag reds on." + reason: "The pre-push hook was SLIMMED TO FMT-ONLY 2026-07-25 (operator ruling): 'Pre-push hook restored to fmt-only per CLAUDE.md's documented contract; the witness corpus belongs to CI, not the push path.' Removed from the GitPrePushHook surface: the doc-reachability CommitWitnessClaim enrollment, and (in gunbc.githooks_pre_push_emit) the whole `claim_batch --pre-push` shim — the affected-set WitnessCorpusRun gate, the DocWitnessRun gate, and the in-hook `cargo build -p v1-compiler --bin claim_batch` that ran from inside a push when the binary was missing. Fail-closed verification before removal (DESIGN §5 — do not drop a check without confirming the authoritative boundary still covers it): cargo fmt --all --check remains a required-path CI step (gunbc.ci_spec ci_fmt_gate_note — build runs it first, and ci needs:[build]); the witness corpus remains the required `ci` floor via gunbc_ci_floor_plan, which also stamps the merge-admission receipt tools.merge_admission_gate.dag reads; and dag/test/claim/doc_reachability_witness_test.dag keeps its cadence through the floor's discovery-corpus scan (CiSpec.discovery_scan_dirs covers dag/test/claim), so the doc-reachability rows lose no coverage — only their redundant second run on the push path. NOTHING LOSES ENFORCEMENT BECAUSE THE HOOK NEVER PROVIDED ANY: it is opt-in per clone (core.hooksPath), bypassable with --no-verify, absent in container worktrees, and PROVEN ineffective by #6658 (an unformatted .rs landed on main and no hook caught it) — the same argument commit_gate_rust_suite_removed_disposition and githooks_pre_push_artifact_drift_gate_removed_disposition already make. What the weight DID buy was harm: a non-authority check heavy enough to make a legitimately-red mid-lane branch feel unpushable is pressure that converts honest reds into workaround greens, and it was priced — three rounds of hand-import workarounds, one decorative gate, and the normalization of --no-verify on the namespace type-binding lane (2026-07-25). A check that cannot enforce should at least be instant. Explicitly DECLINED as the alternative: branch-level expected-red exemption machinery — building configuration so a non-authority check can be tolerated invests in a mechanism that should not carry this weight at all, and slimming dissolves the need. No skip env var was added; the heavy arms are gone from the model, not hidden behind a toggle (DESIGN §5 no-escape-hatches). Re-heavying requires a GitPrePushHook roster edit, which dag/test/claim/githooks_pre_push_emit_test.dag reds on." } data commit_gate_rust_suite_removed_disposition: Disposition = Terminal { diff --git a/dag/gunbc/falsifier_workflow.dag b/dag/gunbc/falsifier_workflow.dag index 9b926180c0c..cd75d96f5dd 100644 --- a/dag/gunbc/falsifier_workflow.dag +++ b/dag/gunbc/falsifier_workflow.dag @@ -65,7 +65,7 @@ fn gunbc_falsifier_step_timeout_basis_is_measured_not_composed() -> Bool { data gunbc_falsifier_step_timeout_note: String = "120 -> 170 (2026-07-11, run 29135185172 receipt): the first corpus-reaching cold run was TIME-killed at the 120-min step ceiling with the corpus incomplete (1970 rows, closure 1396 modules, width 1) and floor peak 16146612224 bytes against a 16GiB slot budget, still growing - so 120 censored the measurement without bounding anything real (the memory cap is the true wall). 170 lets a nightly run reach either completion (exact-labeled receipt) or the cap kill (censored-labeled receipt); both are the falsifier doing its measurement job, and the job backstop DERIVES from this value so it stretches in step. Revisit down when the resolver graph-major module split (S2a move 2) shrinks cold-resolve wall and residency." -data falsifier_plan_function: String = "gunbc_falsifier_batches" +data falsifier_plan_function: String = "gunbc_falsifier_plan" fn falsifier_invoke() -> String { concat( diff --git a/dag/gunbc/plans/ci_process_end_to_end.dag b/dag/gunbc/plans/ci_process_end_to_end.dag index bbdaa18f618..cbaa69b6bf5 100644 --- a/dag/gunbc/plans/ci_process_end_to_end.dag +++ b/dag/gunbc/plans/ci_process_end_to_end.dag @@ -16,7 +16,7 @@ fn ci_process_end_to_end_body() -> List { p(text: "**The deliverable is therefore not \"faster CI.\"** Faster CI is the displaced-cost *symptom* you feel (*\"move with confidence\"*). The deliverable is **shared abstractions, proven by CI consuming them**: one Materialization kernel (collapsing the caches), one Placement authority (collapsing jobs/threads/sessions), one secrets model. Speed falls *out* of that. Priced in elegance instead, \"flex every layer\" is unbounded (the §6 purity trap) — so the lane is on-dial only insofar as CI-timeliness is the *cheapest path to the pain*, and each shared abstraction is pulled in **as CI actually flexes it**, not by taxonomy."), hr(), h2(text: "1. The pipeline as it runs today"), - p(text: "One PR push → one CI run: trigger (`ci.yml` on `pull_request`+`workflow_dispatch`) → GitHub starts the run → concurrency-group eval → GitHub places the **single** `jobs.ci` (no `matrix`) on the first idle `[self-hosted, linux, arm64]` runner → the runner builds `v1-compiler` and invokes `claim_executor … --plan-function gunbc_ci_floor_batches` → the executor derives batches from `ci_floor_plan.dag`, reads the live cgroup budget, shards by `spawn_width`, and runs the `.dag` witness corpus + gates → exit code → GitHub marks the check → dashboard gates review/merge."), + p(text: "One PR push → one CI run: trigger (`ci.yml` on `pull_request`+`workflow_dispatch`) → GitHub starts the run → concurrency-group eval → GitHub places the **single** `jobs.ci` (no `matrix`) on the first idle `[self-hosted, linux, arm64]` runner → the runner builds `v1-compiler` and invokes `claim_executor … --plan-function gunbc_ci_floor_plan` → the executor derives batches from `ci_floor_plan.dag`, reads the live cgroup budget, shards by `spawn_width`, and runs the `.dag` witness corpus + gates → exit code → GitHub marks the check → dashboard gates review/merge."), p(text: "`ci.yml` itself is a byte-for-byte projection of `gunbc.ci_yaml_emit` (`ci_workflow.dag` → `ci_yaml_emit`, authored steps incl. the build command and the `claim_executor` invocation), drift-gated by `tools/ci_yaml_gate.dag`. So the **workflow description is on fabric.** The question §2 answers is whether the *fabric* is."), hr(), h2(text: "2. The causal chain — what's on `.dag` today vs not"), @@ -34,7 +34,7 @@ fn ci_process_end_to_end_body() -> List { row(cells: [cell(text: "6"), cell(text: "the runner's cgroup cap bounds it"), cell(text: "🔴"), cell(text: "—"), cell(text: "`TasksMax`/`MemoryMax` host-set by hand **(G3)**; `.dag` only *reads* it live — adaptive, not authoritative")]), row(cells: [cell(text: "7"), cell(text: "steps: isolate toolchain, checkout, setup-rust, cache"), cell(text: "🟢 *list* / 🔴 *tools*"), cell(text: "**~15s**"), cell(text: "step list derived; bodies shell out to git/rustup/cache")]), row(cells: [cell(text: "8"), cell(text: "`cargo build -p v1-compiler` + freshness/exists verify"), cell(text: "🟢"), cell(text: "**~1m45s**"), cell(text: "command + §5 guards derived; sccache-warm (847/859 hits) at `BUILD_JOBS=1`")]), - row(cells: [cell(text: "9"), cell(text: "`claim_executor … gunbc_ci_floor_batches`"), cell(text: "🔵"), cell(text: "*(batches ↓)*"), cell(text: "**executor interprets `ci_floor_plan.dag`; batches from dependency edges** — strongest on-fabric link")]), + row(cells: [cell(text: "9"), cell(text: "`claim_executor … gunbc_ci_floor_plan`"), cell(text: "🔵"), cell(text: "*(batches ↓)*"), cell(text: "**executor interprets `ci_floor_plan.dag`; batches from dependency edges** — strongest on-fabric link")]), row(cells: [cell(text: "·"), cell(text: "⤷ batch 1 — compile-clean gate"), cell(text: "🔵"), cell(text: "**~22s**"), cell(text: "`gunbc compile --target rust` over the whole tree + a RED control")]), row(cells: [cell(text: "·"), cell(text: "⤷ batch 2 — corpus + rust monolith + 5 gates (width 8)"), cell(text: "🔵/🔴"), cell(text: "**~43m20s**"), cell(text: "**87% of the run; one atomic rust node holds it open — see §6**")]), row(cells: [cell(text: "·"), cell(text: "⤷ batch 3 — source-root-ingest + self-host closure"), cell(text: "🔵"), cell(text: "**~4m29s**"), cell(text: "2 serial (width-1) sub-shells of heavy `gunbc run`s")]), diff --git a/dag/gunbc/proactive_verification_ledger.dag b/dag/gunbc/proactive_verification_ledger.dag index a0d6c77e30f..30d7cc81fd3 100644 --- a/dag/gunbc/proactive_verification_ledger.dag +++ b/dag/gunbc/proactive_verification_ledger.dag @@ -45,9 +45,9 @@ type FamilyVerificationLedgerRow { dissolve_trigger: String } -data spine_witness_parity_recipe: String = "claim_executor --source-root dag --source-root src/v2 --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_falsifier_batches (batch 0 = SelectionPredictOnly whole-corpus cold affected-set falsifier; zero unexplained divergence vs predict-only expectations — a rerun-everything arm is the absorbing-fallback hard reject)" +data spine_witness_parity_recipe: String = "claim_executor --source-root dag --source-root src/v2 --plan-entry src/v2/workflow/ci_floor_plan.dag --plan-function gunbc_falsifier_plan (batch 0 = SelectionPredictOnly whole-corpus cold affected-set falsifier; zero unexplained divergence vs predict-only expectations — a rerun-everything arm is the absorbing-fallback hard reject)" -data spine_compile_clean_recipe: String = "claim_executor batch-1 dag_compile_clean_gate (--target dag whole-tree; same authority as gunbc_ci_floor_batches)" +data spine_compile_clean_recipe: String = "claim_executor batch-1 dag_compile_clean_gate (--target dag whole-tree; same authority as gunbc_ci_floor_plan)" data spine_regen_recipe: String = "regen_stage0 --emit-fresh target/v2-emit-fresh-realize --write-manifest target/v2-emit-fresh-realize/roster_manifest.txt --verify (RegenVerifyGate transport)" diff --git a/dag/std/realization_schedule.dag b/dag/std/realization_schedule.dag index 84988dec723..50ca258b7bd 100644 --- a/dag/std/realization_schedule.dag +++ b/dag/std/realization_schedule.dag @@ -205,6 +205,13 @@ type Runnable } | RunnableKernelWorkload { fused_op_count: Int } +data walk_plan_note: String = "A walk is TWO populations with DIFFERENT ordering laws, and the type says so where a bare List> could not. `batches` are the ordinary floor: batch boundaries order them, and a failure's consequence is the walk's FloorBatchStopPolicy (StopBeforeDependents on pull_request, FullLedger on push/schedule — where a failed batch deliberately does NOT stop the walk, because the per-batch ledger on main is bisection evidence, operator ruling 2026-07-23). `on_success_stages` run ONLY when the ordinary floor completed AND its receipts finalized and validated; each stage is a barrier — stage N fully joins with zero failures and a written stage receipt before stage N+1 starts — and stage-to-stage execution is ALWAYS fail-fast, regardless of the ordinary stop policy: FullLedger is an ordinary-floor policy and never applies between stages. WHY THE SECOND POPULATION EXISTS: work whose correctness is conditional on the whole floor being green (the merge-admission stamp is the first occupant — stamping Success is only true if reaching it proves green) cannot be an ordinary trailing batch, because under FullLedger a trailing batch is still reached after a red batch. The prior attempt encoded exactly that and shipped a fail-open; a second attempt then declared stamp-then-gate as one stage and rediscovered the SIBLING defect — members WITHIN a stage run concurrently (distinct entries become distinct spawned units), so intra-stage order does not exist and anything sequential must be ONE claim whose body sequences its steps, or two singleton stages. Both defects are why this is a named type with a note rather than a convention (operator design ruling 2026-07-30). Every plan function returns WalkPlan — a plan with no postconditions returns on_success_stages: [] — and the executor has ONE strict parser: a malformed or missing field is a hard error, never a fallback to a bare-list reading." + +type WalkPlan { + batches: List> + on_success_stages: List> +} + fn node_frontier_selection_applied(sel: NodeFrontierSelection) -> Bool { match sel { SelectionApplied => true diff --git a/dag/test/claim/gunbc_invoke_witness_test.dag b/dag/test/claim/gunbc_invoke_witness_test.dag index 3553af5276a..5973462950d 100644 --- a/dag/test/claim/gunbc_invoke_witness_test.dag +++ b/dag/test/claim/gunbc_invoke_witness_test.dag @@ -53,7 +53,7 @@ test fn gunbc_invoke_scheduler_matches_modeled_shell_holds() -> Bool { let modeled = claim_executor_run_plan_shell( source_roots: witness_layer_roots, plan_entry: "src/v2/workflow/ci_floor_plan.dag", - plan_function: "gunbc_ci_floor_batches", + plan_function: "gunbc_ci_floor_plan", notice_title: Present { value: gunbc_ci_spec.notice_title }, rooted: true ) @@ -79,7 +79,7 @@ test fn gunbc_invoke_empty_notice_title_omits_flag_holds() -> Bool { let shell = claim_executor_run_plan_shell( source_roots: witness_layer_roots, plan_entry: "src/v2/workflow/ci_floor_plan.dag", - plan_function: "gunbc_ci_floor_batches", + plan_function: "gunbc_ci_floor_plan", notice_title: Present { value: "" }, rooted: true ) @@ -94,14 +94,14 @@ test fn gunbc_invoke_transport_argv_empty_notice_title_omits_flag_holds() -> Boo let argv = claim_executor_run_plan_transport_argv( source_roots: witness_layer_roots, plan_entry: "src/v2/workflow/ci_floor_plan.dag", - plan_function: "gunbc_ci_floor_batches", + plan_function: "gunbc_ci_floor_plan", notice_title: Present { value: "" } ) !transport_argv_contains_flag(argv: argv, flag: "--notice-title") && claim_executor_run_plan_transport_argv( source_roots: witness_layer_roots, plan_entry: "src/v2/workflow/ci_floor_plan.dag", - plan_function: "gunbc_ci_floor_batches", + plan_function: "gunbc_ci_floor_plan", notice_title: Absent ) == argv } diff --git a/docs/plans/ci-process-end-to-end.md b/docs/plans/ci-process-end-to-end.md index 92e41cef76c..600b7cedf07 100644 --- a/docs/plans/ci-process-end-to-end.md +++ b/docs/plans/ci-process-end-to-end.md @@ -15,7 +15,7 @@ CI is **ROADMAP §1**, and it is *upstream of every §0 claim*: a flaky or green ## 1. The pipeline as it runs today -One PR push → one CI run: trigger (`ci.yml` on `pull_request`+`workflow_dispatch`) → GitHub starts the run → concurrency-group eval → GitHub places the **single** `jobs.ci` (no `matrix`) on the first idle `[self-hosted, linux, arm64]` runner → the runner builds `v1-compiler` and invokes `claim_executor … --plan-function gunbc_ci_floor_batches` → the executor derives batches from `ci_floor_plan.dag`, reads the live cgroup budget, shards by `spawn_width`, and runs the `.dag` witness corpus + gates → exit code → GitHub marks the check → dashboard gates review/merge. +One PR push → one CI run: trigger (`ci.yml` on `pull_request`+`workflow_dispatch`) → GitHub starts the run → concurrency-group eval → GitHub places the **single** `jobs.ci` (no `matrix`) on the first idle `[self-hosted, linux, arm64]` runner → the runner builds `v1-compiler` and invokes `claim_executor … --plan-function gunbc_ci_floor_plan` → the executor derives batches from `ci_floor_plan.dag`, reads the live cgroup budget, shards by `spawn_width`, and runs the `.dag` witness corpus + gates → exit code → GitHub marks the check → dashboard gates review/merge. `ci.yml` itself is a byte-for-byte projection of `gunbc.ci_yaml_emit` (`ci_workflow.dag` → `ci_yaml_emit`, authored steps incl. the build command and the `claim_executor` invocation), drift-gated by `tools/ci_yaml_gate.dag`. So the **workflow description is on fabric.** The question §2 answers is whether the *fabric* is. @@ -36,7 +36,7 @@ One PR push → one CI run: trigger (`ci.yml` on `pull_request`+`workflow_dispat | 6 | the runner's cgroup cap bounds it | 🔴 | — | `TasksMax`/`MemoryMax` host-set by hand **(G3)**; `.dag` only *reads* it live — adaptive, not authoritative | | 7 | steps: isolate toolchain, checkout, setup-rust, cache | 🟢 *list* / 🔴 *tools* | **~15s** | step list derived; bodies shell out to git/rustup/cache | | 8 | `cargo build -p v1-compiler` + freshness/exists verify | 🟢 | **~1m45s** | command + §5 guards derived; sccache-warm (847/859 hits) at `BUILD_JOBS=1` | -| 9 | `claim_executor … gunbc_ci_floor_batches` | 🔵 | *(batches ↓)* | **executor interprets `ci_floor_plan.dag`; batches from dependency edges** — strongest on-fabric link | +| 9 | `claim_executor … gunbc_ci_floor_plan` | 🔵 | *(batches ↓)* | **executor interprets `ci_floor_plan.dag`; batches from dependency edges** — strongest on-fabric link | | · | ⤷ batch 1 — compile-clean gate | 🔵 | **~22s** | `gunbc compile --target rust` over the whole tree + a RED control | | · | ⤷ batch 2 — corpus + rust monolith + 5 gates (width 8) | 🔵/🔴 | **~43m20s** | **87% of the run; one atomic rust node holds it open — see §6** | | · | ⤷ batch 3 — source-root-ingest + self-host closure | 🔵 | **~4m29s** | 2 serial (width-1) sub-shells of heavy `gunbc run`s | diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 28f3d55b79d..95bf8b3a210 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -571,6 +571,50 @@ fn batches_from_plan(plan: &Value, ctx: &InterpContext) -> Result>, + on_success_stages: Vec>, +} + +/// Strict WalkPlan parser. Deliberately NO fallback from a failed record parse to a +/// bare-`List>` reading: that fallback would let a malformed plan run +/// with its success stages silently dropped — the silent-widen arm §5 forbids. A plan +/// with no postconditions declares `on_success_stages: []`; it never omits the field. +fn walk_plan_from_plan(plan: &Value, ctx: &InterpContext) -> Result { + let fields = match plan { + Value::Record { fields, .. } => fields, + Value::Variant { fields, .. } => fields, + other => { + return Err(format!( + "expected a WalkPlan record {{ batches, on_success_stages }}, got {} — \ + every plan function returns WalkPlan (std.realization_schedule \ + walk_plan_note); there is deliberately no bare-list fallback", + ctx.format_value(other) + )) + } + }; + let batches_val = ctx + .field(fields, "batches") + .ok_or_else(|| "WalkPlan missing field `batches`".to_string())?; + let stages_val = ctx.field(fields, "on_success_stages").ok_or_else(|| { + "WalkPlan missing field `on_success_stages` — a plan with no postconditions \ + declares an empty list, never omits the field" + .to_string() + })?; + Ok(ParsedWalkPlan { + batches: batches_from_plan(batches_val, ctx) + .map_err(|msg| format!("WalkPlan.batches: {msg}"))?, + on_success_stages: batches_from_plan(stages_val, ctx) + .map_err(|msg| format!("WalkPlan.on_success_stages: {msg}"))?, + }) +} + struct ClaimResult { function: String, ok: bool, @@ -1516,7 +1560,7 @@ fn eval_plan_in_ctx( plan_ctx: &InterpContext, plan_entry: &str, plan_function: &str, -) -> Result>, String> { +) -> Result { set_phase(FloorPhase::Gate, &format!("{plan_entry}::{plan_function}")); let plan_value = run_value(plan_ctx, plan_function).map_err(|msg| { format!( @@ -1524,16 +1568,15 @@ fn eval_plan_in_ctx( plan_entry, plan_function, msg ) })?; - let batches = batches_from_plan(&plan_value, plan_ctx) - .map_err(|msg| format!("malformed plan value: {}", msg))?; - Ok(batches) + walk_plan_from_plan(&plan_value, plan_ctx) + .map_err(|msg| format!("malformed plan value ({plan_function}): {msg}")) } fn eval_plan( source_roots: &[String], plan_entry: &str, plan_function: &str, -) -> Result>, String> { +) -> Result { 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); @@ -2371,6 +2414,54 @@ fn write_witness_row_cost_drift_receipt_at( true } +/// The on-success stage receipt — a SEPARATE receipt class from the ordinary floor +/// receipts, by lifecycle (ruling 2026-07-30): stages run only after the ordinary +/// receipts finalized, so their resolves and walls are not part of the population +/// `ci_floor_declared_resolve_count` measures, and folding them in would re-open the +/// count-collision this separation exists to close. On a skip (ordinary floor failed) +/// the receipt still writes, LOUDLY, with skipped=ordinary_floor_failed and zero +/// stages run — a typed diagnostic, never an admission artifact. +fn write_on_success_receipt( + stage_rows: &[(usize, bool)], + resolves_total: u64, + skipped_ordinary_failed: bool, + declared_stage_count: usize, +) -> bool { + let mut body = String::new(); + if skipped_ordinary_failed { + body.push_str("skipped=ordinary_floor_failed\n"); + } + body.push_str(&format!( + "on_success_stages_declared={declared_stage_count}\n" + )); + body.push_str(&format!("on_success_stages_run={}\n", stage_rows.len())); + body.push_str(&format!("on_success_resolves_total={resolves_total}\n")); + for (stage_index, passed) in stage_rows { + body.push_str(&format!( + "on_success_stage_{}={}\n", + stage_index + 1, + if *passed { "passed" } else { "failed" } + )); + } + let base = std::path::Path::new("target"); + let path = base.join("floor-on-success-receipt.txt"); + if let Err(e) = std::fs::create_dir_all(base).and_then(|_| std::fs::write(&path, &body)) { + eprintln!( + "claim_executor: failed to write on-success receipt {}: {e} — walk fails closed here", + path.display() + ); + return false; + } + eprintln!( + "[receipt] on-success stages: {} of {} run, {} resolve(s) (receipt: {})", + stage_rows.len(), + declared_stage_count, + resolves_total, + path.display() + ); + true +} + fn write_resolve_receipt_at(base: &std::path::Path, batch_records: &[BatchRecord]) -> bool { let mut resolves_total: u64 = 0; let mut resolve_ms_total: u128 = 0; @@ -2609,6 +2700,7 @@ fn batch_unit_lane( fn run_walk( source_roots: &[String], batches: &[Vec], + on_success_stages: &[Vec], governor: &Arc, fast_lane_eval_budget_ms: Option, falsifier_self_host_wet_budgets: FalsifierSelfHostWetBudgets, @@ -2883,14 +2975,132 @@ fn run_walk( // Drop, so they must die before the materialization receipt is written. drop(walk_memo); let materialization_receipt_ok = write_materialization_receipt(); + // Ordinary-floor verdict INCLUDING receipt construction: a receipt-write failure + // is an ordinary-floor failure, so success stages are gated on the whole floor + // contract this process owns, not just the batch verdicts (ruling 2026-07-30). + let ordinary_failed = any_failed + || !resolve_receipt_ok + || !batch_wall_receipt_ok + || !gate_warm_cost_receipt_ok + || !witness_row_cost_receipt_ok + || !witness_row_cost_drift_receipt_ok + || !materialization_receipt_ok; + // On-success stages: run only on a fully-green ordinary floor; each stage is a + // barrier and stage-to-stage execution is ALWAYS fail-fast — FloorBatchStopPolicy + // is an ordinary-floor policy and never applies between stages. Members execute + // serially in-process (run_memo_shared_claims) with a stage-local memo, so one + // entry shared by several stages resolves once and is reused; intra-stage order is + // NOT part of the contract (walk_plan_note) — anything sequential must be one + // claim, and serial execution here merely provides more order than is promised. + // Their memo contexts drop AFTER write_materialization_receipt above, so stage + // materialization is structurally NOT folded into the floor receipt. + let mut on_success_failed = false; + if !on_success_stages.is_empty() { + if ordinary_failed { + eprintln!( + "claim_executor: ordinary floor failed — {} on-success stage(s) NOT run \ + (green-only by construction)", + on_success_stages.len() + ); + let _ = write_on_success_receipt(&[], 0, true, on_success_stages.len()); + } else { + let mut stage_rows: Vec<(usize, bool)> = Vec::new(); + let mut stage_resolves: u64 = 0; + let mut stage_memo: std::collections::HashMap<(String, ExecutionMode), InterpContext> = + std::collections::HashMap::new(); + for (si, stage) in on_success_stages.iter().enumerate() { + let mut stage_failed = false; + for runnable in stage { + match runnable { + Runnable::SingleClaim { + entry, + function, + execution_mode, + .. + } => { + let results = run_memo_shared_claims( + source_roots, + entry, + std::slice::from_ref(function), + *execution_mode, + &mut stage_memo, + ); + for r in results { + if r.resolve_nanos > 0 { + stage_resolves += 1; + } + if r.ok { + eprintln!( + "{}", + format_args!( + "\u{2713} PASS [on-success stage {}] {}", + si + 1, + r.function + ) + ); + } else { + stage_failed = true; + eprintln!( + "\u{2717} FAIL [on-success stage {}] {} ({})", + si + 1, + r.function, + r.detail + ); + failure_details.push(format!( + "on-success stage {} fn={} detail={}", + si + 1, + r.function, + r.detail + )); + } + } + } + other => { + // Typed refusal, never a widen: stages admit single claims + // only. A discovery batch in a postcondition position has + // no defined green-only meaning. + stage_failed = true; + let label = match other { + Runnable::DiscoveryBatch { .. } => "RunnableDiscoveryBatch", + Runnable::SingleClaim { .. } => unreachable!(), + }; + eprintln!( + "\u{2717} FAIL [on-success stage {}] {} refused: on-success \ + stages admit RunnableSingleClaim only", + si + 1, + label + ); + failure_details.push(format!( + "on-success stage {} refused non-claim runnable {}", + si + 1, + label + )); + } + } + } + stage_rows.push((si, !stage_failed)); + if stage_failed { + on_success_failed = true; + eprintln!( + "claim_executor: on-success stage {} failed — remaining stage(s) NOT run \ + (stages are fail-fast regardless of the ordinary stop policy)", + si + 1 + ); + break; + } + } + if !write_on_success_receipt( + &stage_rows, + stage_resolves, + false, + on_success_stages.len(), + ) { + on_success_failed = true; + } + } + } WalkOutcome { - any_failed: any_failed - || !resolve_receipt_ok - || !batch_wall_receipt_ok - || !gate_warm_cost_receipt_ok - || !witness_row_cost_receipt_ok - || !witness_row_cost_drift_receipt_ok - || !materialization_receipt_ok, + any_failed: ordinary_failed || on_success_failed, batches_run, failure_details, } @@ -2970,9 +3180,9 @@ fn perturb_function_to_false(path: &Path, function: &str) -> Result<(), String> /// plan functions whose schedule is a floor walk and therefore subject to arm-time /// `FloorBudgetBelowMinimumFootprint` refusal in claim_executor. const FLOOR_ARM_TIME_BUDGET_REFUSAL_PLAN_FUNCTIONS: &[&str] = &[ - "gunbc_ci_floor_batches", - "gunbc_ci_plan_artifact_batches", - "gunbc_falsifier_batches", + "gunbc_ci_floor_plan", + "gunbc_ci_plan_artifact_plan", + "gunbc_falsifier_plan", ]; fn plan_requires_floor_arm_time_budget_refusal(plan_function: &str) -> bool { @@ -2984,13 +3194,16 @@ fn run_perturb_check( plan_entry: &str, plan_function: &str, ) -> Result { - let batches = match eval_plan(source_roots, plan_entry, plan_function) { + let walk_plan = match eval_plan(source_roots, plan_entry, plan_function) { Ok(b) => b, Err(msg) => { eprintln!("claim_executor: --perturb-check: {msg}"); return Err(ExitCode::from(2)); } }; + // --perturb-check exercises the ordinary gating batch only; success stages are + // out of its subject (they cannot run when the planted gate fails, by construction). + let batches = walk_plan.batches; if batches.len() < 2 { eprintln!( "claim_executor: --perturb-check needs a plan with >= 2 batches to witness the \ @@ -3089,6 +3302,7 @@ fn run_perturb_check( let outcome = run_walk( &[temp_root], &remapped, + &[], &Arc::new(MemoryGovernor::from_environment(1)), None, FalsifierSelfHostWetBudgets::default(), @@ -3294,13 +3508,15 @@ fn run() -> Result { phase_mark("plan resolve"); 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) { + let walk_plan = 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)); } }; + let batches = walk_plan.batches; + let on_success_stages = walk_plan.on_success_stages; // Fast-lane 5s rule (operator 2026-07-12): a plan that schedules a discovery batch // must declare the per-witness eval budget; a missing/mistyped row refuses the run // (fail-closed), while discovery-free plans (regen, plan-artifact) never read it. @@ -3327,7 +3543,7 @@ fn run() -> Result { } else { None }; - let falsifier_self_host_wet_budgets = if plan_function == "gunbc_falsifier_batches" { + let falsifier_self_host_wet_budgets = if plan_function == "gunbc_falsifier_plan" { FalsifierSelfHostWetBudgets { wall_budget_ms: match read_positive_budget_ms( &plan_ctx, @@ -3428,8 +3644,7 @@ fn run() -> Result { // fail-closed at arm time (the fast-lane-budget pattern). Scoped to the full floor plan only: // the plan-artifact shortcut runs a single batch of the same schedule and the falsifier // carries its own receipt budgets, so neither reads these lists. - let batch_clamp_params: Option> = if plan_function == "gunbc_ci_floor_batches" - { + let batch_clamp_params: Option> = if plan_function == "gunbc_ci_floor_plan" { match read_floor_batch_clamp_params(&plan_ctx, batches.len()) { Ok(v) => Some(v), Err(msg) => { @@ -3483,11 +3698,9 @@ fn run() -> Result { // Plans whose schedule carries the compile-clean gate node: the gate only CONSUMES the // in-run whole-tree compile receipt, so these plans must arm the lazy install. - // `gunbc_ci_plan_artifact_batches` is batch 1 of the floor schedule (the docs-only + // `gunbc_ci_plan_artifact_plan` is batch 1 of the floor schedule (the docs-only // shortcut) — same gate node, same receipt dependency. - if plan_function == "gunbc_ci_floor_batches" - || plan_function == "gunbc_ci_plan_artifact_batches" - { + if plan_function == "gunbc_ci_floor_plan" || plan_function == "gunbc_ci_plan_artifact_plan" { enable_floor_compile_clean_lazy_install(&source_roots); // Eager install on the MAIN thread (lever 1, PR #6766): the receipt compile // rides this thread's `process_shared_index` — the same thread-local universe @@ -3505,13 +3718,14 @@ fn run() -> Result { let outcome = run_walk( &source_roots, &batches, + &on_success_stages, &governor, fast_lane_eval_budget_ms, falsifier_self_host_wet_budgets, batch_stop_policy, batch_clamp_params.as_deref(), budget_tighten_ms, - plan_function == "gunbc_falsifier_batches", + plan_function == "gunbc_falsifier_plan", Path::new("dag/gunbc/witness_row_cost_basis.tsv"), ); // Floor receipts block — data, not outcomes. One named group; pulse glyphs only diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index d2613c409db..366dc297d2c 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -3270,7 +3270,7 @@ enum FloorCompileCleanReceipt { static FLOOR_COMPILE_CLEAN_RECEIPT: Mutex> = Mutex::new(None); -/// When set by `claim_executor` for `gunbc_ci_floor_batches`, the first gate consume installs +/// When set by `claim_executor` for `gunbc_ci_floor_plan`, the first gate consume installs /// the one whole-tree receipt (after plan resolve has warmed the module-graph facts cache). static FLOOR_COMPILE_CLEAN_LAZY_INSTALL: AtomicBool = AtomicBool::new(false); /// Floor CI runs through `claim_executor`; env-based scoping detection alone missed some diff --git a/src/v2/compiler/self_host/wet_receipt_enrollment.dag b/src/v2/compiler/self_host/wet_receipt_enrollment.dag index b5d7b0d529d..b011503a695 100644 --- a/src/v2/compiler/self_host/wet_receipt_enrollment.dag +++ b/src/v2/compiler/self_host/wet_receipt_enrollment.dag @@ -25,7 +25,7 @@ import v2.std.text { String } data falsifier_self_host_wet_enrollment_note: String = "Nightly falsifier Wet batch enrollment authority (Wave 2 CI substrate, 2026-07-15; review 44453): falsifier_self_host_wet_entries = template rows (always enrolled when green) PLUS module rows with a restored SelfHostWetReceiptBinding (seed_emitter_behavioral_wet_module_bindings) — disposition-independent so executed wet receipts can re-enroll on the green nightly roster while frontier rows remain SeedRetained until BuildGreen authorization. No binding ⇒ no green wet enrollment. Quarantine expect_red receipts stay on falsifier_self_host_wet_known_red_entries until local wet greens and the binding restores." -data falsifier_self_host_wet_cadence_rehome_note: String = "2026-07-25 REHOME off affected-set falsifier (run attribution on #7199 / srv1-02): template wet receipts are build-verification (emit+cargo), not an affected-set control. Measured: namespace_import_closure ~21min and self_host_logic ~34min against a post-hoc 600s whole-receipt budget (July-15 basis) — wall spent before refuse. Cadence flag false unschedules gunbc_falsifier_batches green Wet batch only; roster retained. Kill-at-deadline LANDED 2026-07-25 (shell waits SIGKILL process group at the ceiling; completion-side backstop retained). Review #7199 OR for batch-2: either reactivate as expect_red with kill-at-deadline, OR park on a separate build-verification cadence with owner+date. Chosen arm: separate build-verification cadence (templates mix already-green ~100s receipts with over-budget ones — a single expect_red batch would unexpected-green the fast rows). See falsifier_self_host_wet_build_verification_cadence_note. SelfEmitted assemble-red receipts are the other arm's live consumer: falsifier_self_host_wet_known_red_entries (Wet expect_red quarantine on the falsifier)." +data falsifier_self_host_wet_cadence_rehome_note: String = "2026-07-25 REHOME off affected-set falsifier (run attribution on #7199 / srv1-02): template wet receipts are build-verification (emit+cargo), not an affected-set control. Measured: namespace_import_closure ~21min and self_host_logic ~34min against a post-hoc 600s whole-receipt budget (July-15 basis) — wall spent before refuse. Cadence flag false unschedules gunbc_falsifier_plan green Wet batch only; roster retained. Kill-at-deadline LANDED 2026-07-25 (shell waits SIGKILL process group at the ceiling; completion-side backstop retained). Review #7199 OR for batch-2: either reactivate as expect_red with kill-at-deadline, OR park on a separate build-verification cadence with owner+date. Chosen arm: separate build-verification cadence (templates mix already-green ~100s receipts with over-budget ones — a single expect_red batch would unexpected-green the fast rows). See falsifier_self_host_wet_build_verification_cadence_note. SelfEmitted assemble-red receipts are the other arm's live consumer: falsifier_self_host_wet_known_red_entries (Wet expect_red quarantine on the falsifier)." data falsifier_self_host_wet_build_verification_cadence_note: String = "INTERIM home for green-wet TEMPLATE receipts (falsifier_self_host_wet_template_entries) after the 2026-07-25 rehome. Owner: #7199 falsifier lane (briansrls). Target date: 2026-08-08 — by then either (a) a dedicated build-verification workflow enrolls these rows under gunbc_falsifier_self_host_wet_receipt_wall_budget with kill-at-deadline on a sccache-healthy runner class, or (b) falsifier_self_host_wet_cadence_active flips true after a dated under-budget receipt on that runner class (typed AND). Until then: local recipe claim_batch --wet --source-root dag --source-root src/v2 --entry --functions ; falsifier green Wet batch stays unscheduled. Distinct from falsifier_self_host_wet_known_red_entries (assemble-red SelfEmitted expect_red quarantine — live on the falsifier, not this cadence)." 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 61f0c5f12bd..3af0594ba67 100644 --- a/src/v2/test/claim/ci_floor_plan_witness_test.dag +++ b/src/v2/test/claim/ci_floor_plan_witness_test.dag @@ -4,8 +4,8 @@ import std.realization { RunnableDiscoveryBatch, } import v2.workflow.ci_floor_plan { - gunbc_falsifier_batches, - gunbc_ci_floor_batches, + gunbc_falsifier_ordinary_batches, + gunbc_ci_floor_ordinary_batches, floor_schedule_for, corpus_runnable_for, execution_corpus_runnable_for, corpus_witness_entries, execution_witness_entries, @@ -175,7 +175,7 @@ fn batch_index_of(batches: List>, target: String) -> Int { } fn witness_live_floor_discovery_tracks_optin_roster() -> Bool { - let live_count = schedule_discovery_batch_count(batches: gunbc_ci_floor_batches()) + let live_count = schedule_discovery_batch_count(batches: gunbc_ci_floor_ordinary_batches()) if length(xs: gunbc_ci_spec.witness_entries) == 0 && length(xs: gunbc_ci_spec.discovery_scan_dirs) == 0 { live_count == 0 } else { @@ -184,7 +184,7 @@ fn witness_live_floor_discovery_tracks_optin_roster() -> Bool { } fn witness_cheap_gates_precede_compile_root() -> Bool { - let batches = gunbc_ci_floor_batches() + let batches = gunbc_ci_floor_ordinary_batches() let compile_idx = batch_index_of(batches: batches, target: "dag_compile_clean_gate_passes") (compile_idx == 1) && floor_batch_has_any_cheap_gate(batch: batches.first()) @@ -200,12 +200,12 @@ fn witness_cheap_gate_membership_declared() -> Bool { } fn witness_corpus_not_before_compile() -> Bool { - let batches = gunbc_ci_floor_batches() + let batches = gunbc_ci_floor_ordinary_batches() !list_contains_str(xs: batch_functions(b: batches.first()), target: corpus_sentinel) } fn witness_independent_jobs_parallelize() -> Bool { - fold(gunbc_ci_floor_batches().skip(n: 2), init: false, f: fn(found_parallel, batch) { + fold(gunbc_ci_floor_ordinary_batches().skip(n: 2), init: false, f: fn(found_parallel, batch) { found_parallel || (length(xs: batch) > 1) }) } @@ -358,7 +358,7 @@ fn discovery_batch_declares_hermetic(r: Runnable) -> Bool { fn witness_hermetic_floor_posture_declared() -> Bool { discovery_batch_declares_hermetic(r: corpus_runnable_for(spec: gunbc_ci_spec)) && !discovery_batch_declares_hermetic(r: execution_corpus_runnable_for(spec: gunbc_ci_spec)) - && discovery_batch_declares_hermetic(r: gunbc_falsifier_batches().first().first()) + && discovery_batch_declares_hermetic(r: gunbc_falsifier_ordinary_batches().first().first()) } test fn witness_emit_host_profile_forbids_corpus_co_residence() -> Bool { @@ -515,7 +515,7 @@ fn falsifier_cadence_witness_batch_holds(r: Runnable) -> Bool { } fn falsifier_plan_structure_holds() -> Bool { - let batches = gunbc_falsifier_batches() + let batches = gunbc_falsifier_ordinary_batches() let predict_ok = (batches.first().length() == 1) && falsifier_predict_only_batch_holds(r: batches.first().first()) let wet_enrolled = falsifier_self_host_wet_batch_enrolled() diff --git a/src/v2/test/claim/ci_spec_witness_test.dag b/src/v2/test/claim/ci_spec_witness_test.dag index d7860235ff8..8a3dcfd2329 100644 --- a/src/v2/test/claim/ci_spec_witness_test.dag +++ b/src/v2/test/claim/ci_spec_witness_test.dag @@ -153,7 +153,7 @@ test fn witness_run_script_invokes_scheduler() -> Bool { let floor_script = gunbc_ci_floor_only_script(spec: gunbc_ci_spec) string_contains(s: script, pattern: git_fetch_script(policy: gunbc_ci_spec.diff_policy)) && string_contains(s: script, pattern: scheduler_invoke(spec: gunbc_ci_spec)) && - string_contains(s: script, pattern: "--plan-function gunbc_ci_floor_batches") && + string_contains(s: script, pattern: "--plan-function gunbc_ci_floor_plan") && !string_contains(s: floor_script, pattern: "--bin claim_batch") && !string_contains(s: floor_script, pattern: "cargo build") && !string_contains(s: script, pattern: "cargo run -p v1-compiler --release --bin claim_executor") @@ -256,7 +256,7 @@ test fn witness_documentation_only_floor_shortcut_retired() -> Bool { !string_contains(s: script, pattern: "if [ \"$_ci_floor_skip\" = \"documentation_only_skip\" ]; then") && !string_contains(s: script, pattern: "floor skipped (CI_FLOOR_DISPOSITION=") && !string_contains(s: script, pattern: "CI_FLOOR_DISPOSITION=documentation_only_skipped") && - string_contains(s: script, pattern: "--plan-function gunbc_ci_floor_batches") + string_contains(s: script, pattern: "--plan-function gunbc_ci_floor_plan") } test fn witness_merge_base_ref_single_authority() -> Bool { diff --git a/src/v2/test/claim/pr_native_batch_test.dag b/src/v2/test/claim/pr_native_batch_test.dag index b88e654305d..59a95a13f7e 100644 --- a/src/v2/test/claim/pr_native_batch_test.dag +++ b/src/v2/test/claim/pr_native_batch_test.dag @@ -3,7 +3,7 @@ module v2.test.claim.pr_native_batch_test import v2.workflow.ci_floor_plan { gunbc_pr_native_entries, gunbc_pr_native_batch_enrolled, - gunbc_ci_floor_batches, + gunbc_ci_floor_ordinary_batches, gunbc_ci_floor_realization_plan } import v2.compiler.self_host.native_routing_frontier { native_routed_rows } @@ -17,6 +17,6 @@ test fn witness_zero_routed_families_enrolled() -> Bool { } test fn witness_floor_batches_unchanged_when_unenrolled() -> Bool { - length(xs: gunbc_ci_floor_batches()) == length(xs: gunbc_ci_floor_realization_plan().schedule) + length(xs: gunbc_ci_floor_ordinary_batches()) == length(xs: gunbc_ci_floor_realization_plan().schedule) } diff --git a/src/v2/test/claim/realization_schedule_witness.dag b/src/v2/test/claim/realization_schedule_witness.dag index e32838daac5..22a61a85fcb 100644 --- a/src/v2/test/claim/realization_schedule_witness.dag +++ b/src/v2/test/claim/realization_schedule_witness.dag @@ -6,13 +6,13 @@ import v2.workflow.ci_floor_plan { cheap_floor_gate_step_labels } fn witness_batches_are_plan_schedule() -> Bool { schedule_generates_identical_schedule( plan: gunbc_ci_floor_realization_plan(), - schedule: gunbc_ci_floor_batches() + schedule: gunbc_ci_floor_ordinary_batches() ) } fn witness_batches_identical_to_inner_schedule() -> Bool { schedule_eq( - left: gunbc_ci_floor_batches(), + left: gunbc_ci_floor_ordinary_batches(), right: gunbc_ci_floor_schedule_inner() ) } diff --git a/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag b/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag index ab7ad0fd1f8..812ab65cf32 100644 --- a/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag +++ b/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag @@ -3,12 +3,13 @@ module v2.test.fixture.floor_skip.budget_red_control_plan import std.realization_schedule { Runnable, RunnableSingleClaim, + WalkPlan, runnable_resource_profile_negligible } import v2.std.collection { List } import std.types { Int } -data budget_red_control_plan_note: String = "THE COST WALL's by-execution RED-control fixture (endgame D5): a minimal plan whose fn is NAMED gunbc_ci_floor_batches so claim_executor arms the per-batch derived clamp against it — one tiny hermetic batch, one generous overhead row (600s) with rate 0, so the computed clamp is 600s. RED direction: GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS=0 tightens the budget to zero, the batch overruns by construction, and the run must exit nonzero with the typed FLOOR-BATCH-OVER-BUDGET refusal naming batch 1. GREEN direction: the same invocation without the env exits zero. The injection is tighten-only (min), so this fixture can force the refusal but can never widen a real budget — a fault injection, not an escape hatch. Recipe: target/release/claim_executor --source-root dag --source-root src/v2 --plan-entry src/v2/test/fixture/floor_skip/budget_red_control_plan.dag --plan-function gunbc_ci_floor_batches. Lives under test/fixture/floor_skip/ (discovery-excluded at dir grain) and declares no test fns, so it is invisible to the witness corpus — driven only by this recipe." +data budget_red_control_plan_note: String = "THE COST WALL's by-execution RED-control fixture (endgame D5): a minimal plan whose fn is NAMED gunbc_ci_floor_plan so claim_executor arms the per-batch derived clamp against it — one tiny hermetic batch, one generous overhead row (600s) with rate 0, so the computed clamp is 600s. RED direction: GUNBC_FLOOR_BATCH_BUDGET_TIGHTEN_MS=0 tightens the budget to zero, the batch overruns by construction, and the run must exit nonzero with the typed FLOOR-BATCH-OVER-BUDGET refusal naming batch 1. GREEN direction: the same invocation without the env exits zero. The injection is tighten-only (min), so this fixture can force the refusal but can never widen a real budget — a fault injection, not an escape hatch. Recipe: target/release/claim_executor --source-root dag --source-root src/v2 --plan-entry src/v2/test/fixture/floor_skip/budget_red_control_plan.dag --plan-function gunbc_ci_floor_plan. Lives under test/fixture/floor_skip/ (discovery-excluded at dir grain) and declares no test fns, so it is invisible to the witness corpus — driven only by this recipe." fn budget_red_control_claim() -> Runnable { RunnableSingleClaim { @@ -18,8 +19,11 @@ fn budget_red_control_claim() -> Runnable { } } -fn gunbc_ci_floor_batches() -> List> { - [[budget_red_control_claim()]] +fn gunbc_ci_floor_plan() -> WalkPlan { + WalkPlan { + batches: [[budget_red_control_claim()]], + on_success_stages: [], + } } fn gunbc_ci_floor_batch_clamp_overhead_seconds() -> List { diff --git a/src/v2/workflow/ci_floor_plan.dag b/src/v2/workflow/ci_floor_plan.dag index 2e97c3c590c..e21b2072f0f 100644 --- a/src/v2/workflow/ci_floor_plan.dag +++ b/src/v2/workflow/ci_floor_plan.dag @@ -66,7 +66,8 @@ import std.realization_schedule { runnable_forbids_corpus_co_residence, cost_account_predicted_zero, runnable_selection_applied, - runnable_step_label + runnable_step_label, + WalkPlan, } import v2.lens.schedule_lens { schedule_lens_verdict_for_ci_floor, schedule_lens_batch_has_any_cheap_gate } import v2.std.lens_verdict { Holds, NotApplicable, Unrealized, Violation } @@ -388,14 +389,14 @@ fn witness_no_corpus_batch_contains_cheap_gate(batches: List>) -> } fn witness_cheap_gates_precede_all_corpus_batches() -> Bool { - let batches = gunbc_ci_floor_batches() + let batches = gunbc_ci_floor_ordinary_batches() witness_cheap_gates_only_in_early_batch(batches: batches) && witness_no_corpus_batch_contains_cheap_gate(batches: batches) } fn witness_non_cheap_gate_absent_from_early_batch() -> Bool { !floor_batch_contains_function( - batch: gunbc_ci_floor_batches().first(), + batch: gunbc_ci_floor_ordinary_batches().first(), target: ci_floor_compile_gate_fn ) } @@ -451,7 +452,7 @@ fn gunbc_ci_floor_batch_clamp_per_unit_ms() -> List { } fn witness_floor_batch_clamp_params_cover_schedule() -> Bool { - length(xs: gunbc_ci_floor_batch_clamp_params) == length(xs: gunbc_ci_floor_batches()) + length(xs: gunbc_ci_floor_batch_clamp_params) == length(xs: gunbc_ci_floor_ordinary_batches()) } fn witness_floor_batch_clamp_overhead_all_positive() -> Bool { @@ -1024,7 +1025,7 @@ fn gunbc_falsifier_substrate_long_lane_batch() -> Runnable { } } -fn gunbc_falsifier_batches() -> List> { +fn gunbc_falsifier_ordinary_batches() -> List> { let base = [[gunbc_falsifier_predict_only_batch()]] let with_wet = falsifier_batches_append_if_enrolled( acc: base, @@ -1134,7 +1135,7 @@ fn gunbc_pr_native_batch() -> Runnable { } } -fn gunbc_ci_floor_batches() -> List> { +fn gunbc_ci_floor_ordinary_batches() -> List> { falsifier_batches_append_if_enrolled( acc: gunbc_ci_floor_realization_plan().schedule, enrolled: gunbc_pr_native_batch_enrolled(), @@ -1142,7 +1143,7 @@ fn gunbc_ci_floor_batches() -> List> { ) } -data gunbc_ci_witness_corpus_only_batches_note: String = "The pre-push local test gate (consumer: dag/gunbc/githooks_pre_push_emit.dag NEED_WITNESS). It is the SAME affected-set witness corpus CI runs (corpus_runnable_for(gunbc_ci_spec), SelectionApplied scoped to the push diff via the executor's git-diff observation), MINUS two lanes on purpose: (1) the whole-tree --target dag compile-clean gate (the compile-clean anchor batch of gunbc_ci_floor_batches, after the cheap-gate early batch) - infeasible per-push (~72min emit), CI-owned; and (2) the Wet execution + bin-witness lanes (execution_corpus_node / bin_witness_corpus_node) - they need prebuilt release witness bins and spawn a host compiler, so they stay CI-owned. This is a DECLARED scope narrowing, not a silent one (the enforcement-intent rule): the hermetic discovery corpus is the affected-set bulk and catches the resolve / typecheck / witness-logic red class before push; CI remains the whole wall. Selection stays fail-closed exactly as the floor (ci_spec_discovery_flip_note; compile_door_ledger; cli_run AFFECTED-SET REFUSAL): a diff-observation / provenance gap REFUSES with a typed, located diagnostic — it never widens to the whole corpus and never silently skips (DESIGN §5: refuse, never widen)." +data gunbc_ci_witness_corpus_only_batches_note: String = "The pre-push local test gate (consumer: dag/gunbc/githooks_pre_push_emit.dag NEED_WITNESS). It is the SAME affected-set witness corpus CI runs (corpus_runnable_for(gunbc_ci_spec), SelectionApplied scoped to the push diff via the executor's git-diff observation), MINUS two lanes on purpose: (1) the whole-tree --target dag compile-clean gate (the compile-clean anchor batch of gunbc_ci_floor_plan, after the cheap-gate early batch) - infeasible per-push (~72min emit), CI-owned; and (2) the Wet execution + bin-witness lanes (execution_corpus_node / bin_witness_corpus_node) - they need prebuilt release witness bins and spawn a host compiler, so they stay CI-owned. This is a DECLARED scope narrowing, not a silent one (the enforcement-intent rule): the hermetic discovery corpus is the affected-set bulk and catches the resolve / typecheck / witness-logic red class before push; CI remains the whole wall. Selection stays fail-closed exactly as the floor (ci_spec_discovery_flip_note; compile_door_ledger; cli_run AFFECTED-SET REFUSAL): a diff-observation / provenance gap REFUSES with a typed, located diagnostic — it never widens to the whole corpus and never silently skips (DESIGN §5: refuse, never widen)." fn gunbc_ci_witness_corpus_only_batches() -> List> { [[corpus_runnable_for(spec: gunbc_ci_spec)]] @@ -1194,7 +1195,7 @@ fn witness_regen_spec_carries_no_witness_corpus() -> Bool { length(xs: corpus_plan_nodes(spec: gunbc_ci_regen_spec)) == 0 } -fn gunbc_ci_regen_floor_batches() -> List> { +fn gunbc_ci_regen_floor_ordinary_batches() -> List> { gunbc_ci_floor_realization_plan_for(spec: gunbc_ci_regen_spec).schedule } @@ -1226,8 +1227,38 @@ fn floor_compile_clean_batch(batches: List>) -> List List> { - floor_compile_clean_batch(batches: gunbc_ci_floor_batches()) +fn gunbc_ci_plan_artifact_ordinary_batches() -> List> { + floor_compile_clean_batch(batches: gunbc_ci_floor_ordinary_batches()) +} + +data walk_plan_uniformity_note: String = "EVERY plan function returns WalkPlan, including the three with no postconditions — on_success_stages: [] is a declared empty set, not an omission the executor papers over. The executor has ONE strict parser for the record shape; there is deliberately NO fallback from a failed record parse to a bare-List> reading, because that fallback would let a malformed plan silently run with its success stages dropped — the exact silent-widen shape §5 forbids. The `_plan` names replace the `_batches` names in the same motion (operator ruling 2026-07-30): a function whose value now carries postcondition stages must not keep a name that says it returns only batches, or the next author reasonably assumes the stages are not part of the value. The four argv/step consumers follow automatically because they derive from the floor_plan_function / plan_artifact_plan_function / regen_floor_plan_function / falsifier_plan_function constants (gunbc.ci_spec, gunbc.falsifier_workflow) — those constants are the single naming authority, renamed with the functions." + +fn gunbc_ci_floor_plan() -> WalkPlan { + WalkPlan { + batches: gunbc_ci_floor_ordinary_batches(), + on_success_stages: [], + } +} + +fn gunbc_ci_regen_floor_plan() -> WalkPlan { + WalkPlan { + batches: gunbc_ci_regen_floor_ordinary_batches(), + on_success_stages: [], + } +} + +fn gunbc_ci_plan_artifact_plan() -> WalkPlan { + WalkPlan { + batches: gunbc_ci_plan_artifact_ordinary_batches(), + on_success_stages: [], + } +} + +fn gunbc_falsifier_plan() -> WalkPlan { + WalkPlan { + batches: gunbc_falsifier_ordinary_batches(), + on_success_stages: [], + } } fn gunbc_ci_floor_schedule_lens_holds() -> Bool { @@ -1283,7 +1314,7 @@ fn floor_plan_co_runs_heavy_without_serialization_for(spec: CiSpec) -> Bool { } fn gunbc_ci_floor_plan_serializes_heavy_resolves() -> Bool { - floor_plan_at_most_one_heavy_gate_resolve_per_batch(batches: gunbc_ci_floor_batches()) + floor_plan_at_most_one_heavy_gate_resolve_per_batch(batches: gunbc_ci_floor_ordinary_batches()) } fn witness_floor_corpus_exclusion_edges_exist(spec: CiSpec) -> Bool { @@ -1326,7 +1357,7 @@ fn witness_string_lists_have_same_members(a: List, b: List) -> B fn witness_floor_schedule_preserves_enrolled_set() -> Bool { witness_string_lists_have_same_members( a: gunbc_ci_floor_enrolled_runnable_labels_for(spec: gunbc_ci_spec), - b: gunbc_ci_floor_schedule_runnable_labels(batches: gunbc_ci_floor_batches()) + b: gunbc_ci_floor_schedule_runnable_labels(batches: gunbc_ci_floor_ordinary_batches()) ) } From 912ecf1f654205df8f406c26fae03dfbecac8b4c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 17:40:22 +0000 Subject: [PATCH 2/4] Floor finalization moves in-executor; the two receipt-gate steps delete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of the success-stages redesign (operator ruling 2026-07-30): the resolve-count law and the materialization disclosure law now run INSIDE claim_executor as ordinary-floor finalization, after the receipts write and before any on-success stage. The old shape ran them as two GitHub shell steps AFTER the floor step, so a receipt-law violation could red the job after admission had already stamped — the can-red-after-admission hole. In here, a violation is an ordinary-floor failure and blocks every on-success stage by construction. MECHANISM. The floor plan projects its law into its own closure: gunbc_ci_floor_declared_resolve_count in ci_floor_plan.dag reads the ci_materialization authority (single authority; the projection is a read, not a second declaration). The executor reads it fail-closed at arm time for the floor plan only — a floor-named plan whose law cannot be read refuses the run rather than walking without its contract — and validates: law 1: resolves_total (recomputed from batch_records by the SAME nonzero-resolve_nanos rule the receipt writer uses, never re-parsed from the file we just wrote) equals the declared count law 2: the materialization receipt exists, keyed/unkeyed/duplicated parse, and keyed_calls is nonzero Violations are typed FLOOR-FINALIZATION-REFUSED lines, counted into failure_details. Regen, falsifier, and plan-artifact declare no finalization and are byte-unchanged in behavior. DELETED, NOT UNWIRED: both gate scripts and their Scaffold rows in ci_materialization, both step constructors in ci_workflow, and the two aux terms in the ci job backstop (100 -> 90, still the exact step-sum + prelude). A surviving script would be a second representation of the floor-completion rule whose still-green tests could mask a live-path regression — the exact shape that hid the lost admission fetch. The receipt FILES keep writing; they are observability, and only the shell re-validation of them is gone. ci_spec_witness_test gains the negative witness that both step names are absent from the emitted workflow. THE FIXTURE follows the name it impersonates: budget_red_control_plan declares the law (1 — it runs exactly one single-claim batch) because the floor name now arms finalization as well as the clamp; before that row landed, running it WITHOUT the law was the executable RED of the fail-closed read. ONE MORE TRANSCRIPTION DELETED, caught by this change redding it: ci_job_backstop_equals_one_hundred_minutes mirrored the derived step-sum as a literal 100, so this correct change reported as a failure and the reviewer move would have been editing the number to 90 — the same class as the roadmap 63-declared pin, deleted under the same ruling. The magnitude-independent invariants stay: exact step-sum + prelude (its conscious double-entry copy updated to the four remaining aux terms) and the dropped selection-control term. VERIFIED BY EXECUTION: projection reads 1 from the plan closure (gunbc run) RED fixture without the law: "gunbc_ci_floor_declared_resolve_count unavailable (fail-closed)", exit 1, before any batch walks unit arms (cargo test, 2 new): count mismatch refuses in BOTH directions; matching count leaves exactly the missing-receipt refusal when no materialization file exists — absence never passes ci_spec_witness_test 28/28 · ci_workflow_witness_holds PASS ci.yml regenerates: both gate steps gone, ci job timeout 100 -> 90 NOT PROVEN HERE: the full-floor green path through finalization — the fixture walk now proceeds past the arm into the eager whole-tree compile-clean install (~72min class) and was killed at a local 10-minute cap, and the real floor fail-fasts on this container's memory. Owed to the fleet run with the rest of the merge bar. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N7LfCpidMuDjV3FxYUHv1K --- .github/workflows/ci.yml | 22 +- dag/gunbc/ci_materialization.dag | 72 ------- dag/gunbc/ci_workflow.dag | 38 +--- dag/test/claim/ci_workflow_witness_test.dag | 7 +- src/v1/stage0/src/bin/claim_executor.rs | 201 +++++++++++++++++- src/v2/test/claim/ci_spec_witness_test.dag | 19 +- .../floor_skip/budget_red_control_plan.dag | 6 + src/v2/workflow/ci_floor_plan.dag | 7 + 8 files changed, 229 insertions(+), 143 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a2d3806169..d7916e5d9cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -171,7 +171,7 @@ jobs: ci: runs-on: [self-hosted, linux, arm64] needs: [build, regen] - timeout-minutes: 100 + timeout-minutes: 90 steps: - name: Checkout uses: actions/checkout@v5 @@ -225,26 +225,6 @@ jobs: if [ -f "$d/memory.peak" ]; then echo "[calibration] floor_peak_post=$(cat "$d/memory.peak") floor_outcome=${{ steps.floor.outcome }} cgroup=$d (observation label: floor_outcome=success is an EXACT point; any other outcome is a CENSORED lower bound - the process was killed while exceeding the read, so true demand is strictly greater. scope per the floor_peak_pre line: reset=ok means this IS the floor peak; scope=span compares against pre_peak)"; else echo "[calibration] floor_peak_post floor_outcome=${{ steps.floor.outcome }} scope=unavailable (no memory.peak ancestor)"; fi if: always() timeout-minutes: 5 - - name: Floor resolve receipt gate (declared cold-resolve count) - run: | - if ! test -f target/floor-resolve-receipt.txt; then echo "floor resolve receipt missing - fail closed"; exit 1; fi - n=$(sed -n 's/^resolves_total=//p' target/floor-resolve-receipt.txt) - if test -z "$n"; then echo "floor resolve receipt malformed - fail closed"; exit 1; fi - if test "$n" -ne 1; then echo "floor resolve count $n differs from declared 1: duplicate-computation debt changed - update ci_floor_declared_resolve_count consciously (dag/gunbc/ci_materialization.dag)"; exit 1; fi - echo "floor resolve count $n matches declared 1 (declared cold-resolve receipt)" - timeout-minutes: 5 - - name: Floor materialization receipt gate (demand ledger; disclosure gate — count pins retracted, walls tracked in ci_materialization.dag) - run: | - if ! test -f target/floor-materialization-receipt.txt; then echo "floor materialization receipt missing - fail closed"; exit 1; fi - k=$(sed -n 's/^keyed_calls=//p' target/floor-materialization-receipt.txt) - u=$(sed -n 's/^unkeyed_calls=//p' target/floor-materialization-receipt.txt) - d=$(sed -n 's/^duplicated_keys=//p' target/floor-materialization-receipt.txt) - if test -z "$k"; then echo "floor materialization receipt malformed - fail closed"; exit 1; fi - if test -z "$u"; then echo "floor materialization receipt malformed - fail closed"; exit 1; fi - if test -z "$d"; then echo "floor materialization receipt malformed - fail closed"; exit 1; fi - if test "$k" -eq 0; then echo "floor evaluated zero keyed calls - ledger disabled or floor empty - fail closed"; exit 1; fi - echo "floor materialization receipt: keyed=$k unkeyed=$u duplicated=$d (disclosure gate; count pins retracted 2026-07-10 — schedule-jittered per-ctx grain + diff-dependent affected-set denominator; walls forward: unkeyed==0 on closure identity, duplicated on structural frame grain)" - timeout-minutes: 5 - name: Merge-admission gate (receipt required; freshness block held until GatingEnforced) run: | 'git' 'fetch' '--no-tags' 'origin' 'main' && 'env' '-C' '.' 'target/release/gunbc' 'run' '--source-root' 'dag' '--entry' 'dag/tools/merge_admission_gate.dag' '--function' 'main' diff --git a/dag/gunbc/ci_materialization.dag b/dag/gunbc/ci_materialization.dag index bc5bb3dde41..52cbfec922e 100644 --- a/dag/gunbc/ci_materialization.dag +++ b/dag/gunbc/ci_materialization.dag @@ -171,15 +171,6 @@ data ci_floor_resolve_receipt_path: String = "target/floor-resolve-receipt.txt" data ci_floor_declared_resolve_count: Int = 1 -data ci_floor_resolve_receipt_gate_shell_emit_dissolution_trigger: Disposition = Scaffold { - dissolves_to: RealizationDispatch, - bind: DeclarationRef { - module_path: "gunbc.ci_materialization", - decl_name: "ci_floor_resolve_receipt_gate_script", - field: WholeDeclaration - } -} - data ci_sccache_provider_shell_injection_dissolution_trigger: Disposition = Scaffold { dissolves_to: RealizationDispatch, bind: DeclarationRef { @@ -193,67 +184,4 @@ data ci_floor_materialization_receipt_note: String = "Increment 1 of INFERRED ma data ci_floor_materialization_receipt_path: String = "target/floor-materialization-receipt.txt" -data ci_floor_materialization_receipt_gate_shell_emit_dissolution_trigger: Disposition = Scaffold { - dissolves_to: RealizationDispatch, - bind: DeclarationRef { - module_path: "gunbc.ci_materialization", - decl_name: "ci_floor_materialization_receipt_gate_script", - field: WholeDeclaration - } -} - -fn ci_floor_materialization_receipt_gate_script() -> String { - let missing = concat( - concat("if ! test -f ", ci_floor_materialization_receipt_path), - "; then echo \"floor materialization receipt missing - fail closed\"; exit 1; fi\n" - ) - let read_keyed = concat( - concat("k=$(sed -n 's/^keyed_calls=//p' ", ci_floor_materialization_receipt_path), - ")\n" - ) - let read_unkeyed = concat( - concat("u=$(sed -n 's/^unkeyed_calls=//p' ", ci_floor_materialization_receipt_path), - ")\n" - ) - let read_duplicated = concat( - concat("d=$(sed -n 's/^duplicated_keys=//p' ", ci_floor_materialization_receipt_path), - ")\n" - ) - let malformed = "if test -z \"$k\"; then echo \"floor materialization receipt malformed - fail closed\"; exit 1; fi\nif test -z \"$u\"; then echo \"floor materialization receipt malformed - fail closed\"; exit 1; fi\nif test -z \"$d\"; then echo \"floor materialization receipt malformed - fail closed\"; exit 1; fi\n" - let keyed_nonzero = "if test \"$k\" -eq 0; then echo \"floor evaluated zero keyed calls - ledger disabled or floor empty - fail closed\"; exit 1; fi\n" - let ok_line = "echo \"floor materialization receipt: keyed=$k unkeyed=$u duplicated=$d (disclosure gate; count pins retracted 2026-07-10 — schedule-jittered per-ctx grain + diff-dependent affected-set denominator; walls forward: unkeyed==0 on closure identity, duplicated on structural frame grain)\"" - concat( - concat(concat(missing, read_keyed), concat(read_unkeyed, read_duplicated)), - concat(concat(malformed, keyed_nonzero), ok_line) - ) -} - -fn ci_floor_resolve_receipt_gate_script() -> String { - concat( - concat( - concat( - concat("if ! test -f ", ci_floor_resolve_receipt_path), - concat("; then echo \"floor resolve receipt missing - fail closed\"; exit 1; fi\n", "n=$(sed -n 's/^resolves_total=//p' ") - ), - concat( - concat(ci_floor_resolve_receipt_path, ")\n"), - "if test -z \"$n\"; then echo \"floor resolve receipt malformed - fail closed\"; exit 1; fi\n" - ) - ), - concat( - concat( - concat("if test \"$n\" -ne ", to_string(ci_floor_declared_resolve_count)), - concat("; then echo \"floor resolve count $n differs from declared ", to_string(ci_floor_declared_resolve_count)) - ), - concat( - ": duplicate-computation debt changed - update ci_floor_declared_resolve_count consciously (dag/gunbc/ci_materialization.dag)\"; exit 1; fi\n", - concat( - concat("echo \"floor resolve count $n matches declared ", to_string(ci_floor_declared_resolve_count)), - " (declared cold-resolve receipt)\"" - ) - ) - ) - ) -} - data ci_sccache_provider_skip_is_counted_note: String = "The skip arm is LOUD and COUNTED, not silent (§5: a degradation must be a typed, located, countable diagnostic so its frequency is observable and prioritizable). This guard was a bare `if` with no else until 2026-07-24 — when the daemon was down the release build simply ran uncached, and the deficit's frequency was zero by construction, so it never ranked for fixing (shell→dag census §4.I lists it as an absorbing fallback). It is NOT yet a refusal: STEP 2 of the srvN build-cache design flips this to fail-closed, and that flip is operator-sequenced AFTER a live T4 read-back proves P1b provisioning converges on srv1/srv2/srv4 — refusing before then would red the whole fleet, which is why the interim is loudness rather than a wall. The receipt line names the host, so a single misconfigured runner is attributable rather than fleet-wide noise." diff --git a/dag/gunbc/ci_workflow.dag b/dag/gunbc/ci_workflow.dag index e630d73d7f9..d6908c07211 100644 --- a/dag/gunbc/ci_workflow.dag +++ b/dag/gunbc/ci_workflow.dag @@ -20,10 +20,6 @@ import v2.workflow.ci_merge_admission_gate_emit { import gunbc.ci_workflow_expressions { ci_deploy_push_to_main_if, ci_regen_heal_if } import gunbc.commit_workflow { commit_gate_roster, project_ci_floor_gates, project_ci_floor_witness_entries } import gunbc.ci_runner_target { gunbc_ci_selected_runner_spec, ci_runner_target_ram_speed_budget, selected_ci_runner_target } -import gunbc.ci_materialization { - ci_floor_resolve_receipt_gate_script, - ci_floor_materialization_receipt_gate_script -} import gunbc.ci_compile_jobs { ci_compile_jobs, compile_jobs_count_or_serial } import gunbc.ci_floor_measurement { gunbc_ci_runner_slot_ram_speed_ceiling } import v2.workflow.ci_floor_peak_emit { @@ -366,34 +362,6 @@ fn ci_floor_peak_post_step() -> Step { } } -fn ci_floor_resolve_receipt_gate_step() -> Step { - RunStep { - name: Present { value: "Floor resolve receipt gate (declared cold-resolve count)" }, - id: none, - run: ci_floor_resolve_receipt_gate_script(), - shell: none, - env: none, - working_directory: none, - if_condition: none, - continue_on_error: none, - timeout_minutes: Present { value: gunbc_ci_aux_step_timeout_minutes } - } -} - -fn ci_floor_materialization_receipt_gate_step() -> Step { - RunStep { - name: Present { value: "Floor materialization receipt gate (demand ledger; disclosure gate — count pins retracted, walls tracked in ci_materialization.dag)" }, - id: none, - run: ci_floor_materialization_receipt_gate_script(), - shell: none, - env: none, - working_directory: none, - if_condition: none, - continue_on_error: none, - timeout_minutes: Present { value: gunbc_ci_aux_step_timeout_minutes } - } -} - data merge_admission_gate_step_name: String = "Merge-admission gate (receipt required; freshness block held until GatingEnforced)" data merge_admission_ci_gate_step_disposition: Disposition = Terminal { @@ -479,8 +447,10 @@ fn gunbc_ci_build_job_backstop_timeout_minutes() -> Int { gunbc_ci_aux_step_timeout_minutes + gunbc_ci_release_build_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_artifact_transfer_step_timeout_minutes + gunbc_ci_prelude_allowance_minutes } +data gunbc_ci_receipt_gate_steps_moved_in_executor_note: String = "The resolve-receipt and materialization-receipt gate STEPS are deleted (ruling 2026-07-30): their laws — resolves_total equals the declared count, and the materialization receipt exists/parses/keyed nonzero — now run INSIDE claim_executor as ordinary-floor finalization (run_walk, FloorFinalization), so a receipt-law violation blocks every on-success stage instead of redding a step AFTER admission had already stamped. The two aux terms leave the backstop sum in the same motion, keeping it the exact step-sum + prelude the witness pins. The receipt FILES keep writing — they are observability the falsifier and operators read — only the shell re-validation of them is gone, because keeping it would be a second representation of the floor-completion rule." + fn gunbc_ci_job_backstop_timeout_minutes() -> Int { - gunbc_ci_artifact_transfer_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_floor_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_prelude_allowance_minutes + gunbc_ci_artifact_transfer_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_floor_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_prelude_allowance_minutes } fn gunbc_ci_regen_job_backstop_timeout_minutes() -> Int { @@ -615,7 +585,7 @@ fn ci_job() -> Job { id: "ci", name: none, runner: gunbc_ci_selected_runner_spec(), - steps: concat(ci_floor_job_prelude_steps(), [ci_release_bins_download_step(), ci_release_bins_unpack_verify_step(), ci_floor_peak_pre_step(), ci_floor_step(), ci_floor_peak_post_step(), ci_floor_resolve_receipt_gate_step(), ci_floor_materialization_receipt_gate_step(), ci_merge_admission_gate_step()]), + steps: concat(ci_floor_job_prelude_steps(), [ci_release_bins_download_step(), ci_release_bins_unpack_verify_step(), ci_floor_peak_pre_step(), ci_floor_step(), ci_floor_peak_post_step(), ci_merge_admission_gate_step()]), needs: ["build", "regen"], env: none, outputs: none, diff --git a/dag/test/claim/ci_workflow_witness_test.dag b/dag/test/claim/ci_workflow_witness_test.dag index f77df21c333..ff2675942f7 100644 --- a/dag/test/claim/ci_workflow_witness_test.dag +++ b/dag/test/claim/ci_workflow_witness_test.dag @@ -13,12 +13,10 @@ import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly fn ci_job_backstop_is_step_sum_plus_prelude() -> Bool { - gunbc_ci_job_backstop_timeout_minutes() == (gunbc_ci_artifact_transfer_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_floor_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_prelude_allowance_minutes) + gunbc_ci_job_backstop_timeout_minutes() == (gunbc_ci_artifact_transfer_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_floor_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_prelude_allowance_minutes) } -fn ci_job_backstop_equals_one_hundred_minutes() -> Bool { - gunbc_ci_job_backstop_timeout_minutes() == 100 -} +data backstop_magnitude_pin_deleted_note: String = "ci_job_backstop_equals_one_hundred_minutes DELETED (the live-state count-transcription class, operator ruling 2026-07-30 — same class as the roadmap 63-declared pin): it mirrored the derived step-sum as a literal, so any legitimate step roster change redded it and the reviewer's move was editing the number until it matched. It redded here when the two receipt-gate steps moved in-executor (100 -> 90) — a correct change reported as a failure. The invariants that MATTER are magnitude-independent and stay asserted: the backstop equals the exact step-sum + prelude, and the selection-control term stayed dropped." fn ci_job_backstop_dropped_the_selection_control_term() -> Bool { !(gunbc_ci_job_backstop_timeout_minutes() == 115) @@ -26,6 +24,5 @@ fn ci_job_backstop_dropped_the_selection_control_term() -> Bool { test fn ci_workflow_witness_holds() -> Bool { ci_job_backstop_is_step_sum_plus_prelude() - && ci_job_backstop_equals_one_hundred_minutes() && ci_job_backstop_dropped_the_selection_control_term() } diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 95bf8b3a210..266043c21fb 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -2697,10 +2697,85 @@ fn batch_unit_lane( } } +/// The floor plan's finalization laws, read from the plan closure at arm time (the +/// clamp-params pattern) and enforced INSIDE the walk as ordinary-floor finalization +/// (ruling 2026-07-30). These were two GitHub shell steps — the resolve-receipt gate +/// and the materialization-receipt gate — which ran AFTER the floor step, so a +/// receipt-law violation could red the job after admission had already stamped. In +/// here, a violation is an ordinary-floor failure and blocks every on-success stage. +/// Scoped to the floor plan: regen/falsifier/plan-artifact declare no finalization and +/// are unchanged. +struct FloorFinalization { + declared_resolve_count: i64, +} + +/// Validate the floor's finalization laws against the walk's own records. Returns the +/// list of typed refusals (empty = contract satisfied). The receipt FILES keep being +/// written by the walk — they are observability — this validates the laws the deleted +/// shell steps used to re-derive from those files. +fn validate_floor_finalization( + fin: &FloorFinalization, + batch_records: &[BatchRecord], +) -> Vec { + let mut refusals = Vec::new(); + // Law 1 — the declared cold-resolve count (gunbc.ci_materialization + // ci_floor_resolve_receipt_note): resolves_total is recomputed from the SAME rule + // write_resolve_receipt_at uses, never re-parsed from the file we just wrote. + let mut resolves_total: u64 = 0; + for rec in batch_records { + for result in &rec.results { + if result.resolve_nanos > 0 { + resolves_total += 1; + } + } + } + if resolves_total as i64 != fin.declared_resolve_count { + refusals.push(format!( + "floor resolve count {} differs from declared {}: duplicate-computation debt \ + changed - update ci_floor_declared_resolve_count consciously \ + (dag/gunbc/ci_materialization.dag)", + resolves_total, fin.declared_resolve_count + )); + } + // Law 2 — materialization disclosure (ci_floor_materialization_receipt_note): + // receipt exists, keyed/unkeyed/duplicated parse, keyed nonzero. Read from the + // file because the accumulator is process-global and already harvested into it. + let path = std::path::Path::new("target/floor-materialization-receipt.txt"); + match std::fs::read_to_string(path) { + Err(_) => refusals.push("floor materialization receipt missing - fail closed".to_string()), + Ok(body) => { + let field = |key: &str| -> Option { + body.lines() + .find_map(|l| l.strip_prefix(key)) + .and_then(|v| v.trim().parse::().ok()) + }; + match ( + field("keyed_calls="), + field("unkeyed_calls="), + field("duplicated_keys="), + ) { + (Some(k), Some(_u), Some(_d)) => { + if k == 0 { + refusals.push( + "floor evaluated zero keyed calls - ledger disabled or floor empty - \ + fail closed" + .to_string(), + ); + } + } + _ => refusals + .push("floor materialization receipt malformed - fail closed".to_string()), + } + } + } + refusals +} + fn run_walk( source_roots: &[String], batches: &[Vec], on_success_stages: &[Vec], + floor_finalization: Option<&FloorFinalization>, governor: &Arc, fast_lane_eval_budget_ms: Option, falsifier_self_host_wet_budgets: FalsifierSelfHostWetBudgets, @@ -2978,13 +3053,34 @@ fn run_walk( // Ordinary-floor verdict INCLUDING receipt construction: a receipt-write failure // is an ordinary-floor failure, so success stages are gated on the whole floor // contract this process owns, not just the batch verdicts (ruling 2026-07-30). - let ordinary_failed = any_failed + let mut ordinary_failed = any_failed || !resolve_receipt_ok || !batch_wall_receipt_ok || !gate_warm_cost_receipt_ok || !witness_row_cost_receipt_ok || !witness_row_cost_drift_receipt_ok || !materialization_receipt_ok; + // Floor finalization laws (the in-executor form of the deleted resolve/ + // materialization gate steps): validated AFTER the receipts wrote and BEFORE the + // on-success stages, so a violation blocks admission instead of post-dating it. + if let Some(fin) = floor_finalization { + let refusals = validate_floor_finalization(fin, &batch_records); + if refusals.is_empty() { + if !ordinary_failed { + eprintln!( + "claim_executor: floor contract finalized — resolve count matches declared {} \ + and materialization disclosure holds", + fin.declared_resolve_count + ); + } + } else { + ordinary_failed = true; + for msg in refusals { + eprintln!("claim_executor: FLOOR-FINALIZATION-REFUSED: {msg}"); + failure_details.push(format!("floor finalization refused: {msg}")); + } + } + } // On-success stages: run only on a fully-green ordinary floor; each stage is a // barrier and stage-to-stage execution is ALWAYS fail-fast — FloorBatchStopPolicy // is an ordinary-floor policy and never applies between stages. Members execute @@ -3303,6 +3399,7 @@ fn run_perturb_check( &[temp_root], &remapped, &[], + None, &Arc::new(MemoryGovernor::from_environment(1)), None, FalsifierSelfHostWetBudgets::default(), @@ -3517,6 +3614,33 @@ fn run() -> Result { }; let batches = walk_plan.batches; let on_success_stages = walk_plan.on_success_stages; + // Floor finalization: the floor plan declares its resolve law in the plan closure + // (gunbc_ci_floor_declared_resolve_count, projecting the ci_materialization + // authority). Read fail-closed — a floor plan whose law cannot be read refuses the + // run rather than walking without its contract. Other plans declare none. + let floor_finalization: Option = if plan_function == "gunbc_ci_floor_plan" { + match run_value(&plan_ctx, "gunbc_ci_floor_declared_resolve_count") { + Ok(Value::Int(n)) => Some(FloorFinalization { + declared_resolve_count: n, + }), + Ok(other) => { + eprintln!( + "claim_executor: gunbc_ci_floor_declared_resolve_count must be an Int, got \ + {other:?} (fail-closed)" + ); + return Err(ExitCode::from(1)); + } + Err(msg) => { + eprintln!( + "claim_executor: gunbc_ci_floor_declared_resolve_count unavailable \ + (fail-closed): {msg}" + ); + return Err(ExitCode::from(1)); + } + } + } else { + None + }; // Fast-lane 5s rule (operator 2026-07-12): a plan that schedules a discovery batch // must declare the per-witness eval budget; a missing/mistyped row refuses the run // (fail-closed), while discovery-free plans (regen, plan-artifact) never read it. @@ -3719,6 +3843,7 @@ fn run() -> Result { &source_roots, &batches, &on_success_stages, + floor_finalization.as_ref(), &governor, fast_lane_eval_budget_ms, falsifier_self_host_wet_budgets, @@ -3876,6 +4001,80 @@ fn main() -> ExitCode { mod tests { use super::*; + fn finalization_record(resolve_counts: &[u64]) -> BatchRecord { + BatchRecord { + batch_index: 0, + wall_nanos: 0, + clamp_ms: None, + unit_count: 0, + results: resolve_counts + .iter() + .map(|n| ClaimResult { + function: "fixture".to_string(), + ok: true, + detail: String::new(), + wall_nanos: 0, + resolve_nanos: *n as u128, + corpus_resolve_nanos: 0, + corpus_eval_nanos: 0, + corpus_witnesses: 0, + witness_row_costs: Vec::new(), + }) + .collect(), + } + } + + // Floor finalization law 1 (in-executor form of the deleted resolve-receipt gate + // step): the count is recomputed from batch records by the same nonzero- + // resolve_nanos rule the receipt writer uses. Declared-vs-actual mismatch refuses + // in BOTH directions; match passes. The materialization arm is exercised through + // the missing-file refusal (no target/ receipt exists under cargo test), which + // also pins that law 2 cannot silently pass when the receipt is absent. + #[test] + fn floor_finalization_refuses_on_resolve_count_mismatch_both_directions() { + let fin = FloorFinalization { + declared_resolve_count: 1, + }; + let over = [finalization_record(&[5, 7])]; + let refusals = validate_floor_finalization(&fin, &over); + assert!( + refusals + .iter() + .any(|m| m.contains("floor resolve count 2 differs from declared 1")), + "over-count must refuse: {refusals:?}" + ); + let under = [finalization_record(&[])]; + let refusals = validate_floor_finalization(&fin, &under); + assert!( + refusals + .iter() + .any(|m| m.contains("floor resolve count 0 differs from declared 1")), + "under-count must refuse: {refusals:?}" + ); + } + + #[test] + fn floor_finalization_matching_count_leaves_only_the_materialization_arm() { + let fin = FloorFinalization { + declared_resolve_count: 2, + }; + let records = [finalization_record(&[3, 0, 9])]; + let refusals = validate_floor_finalization(&fin, &records); + // resolve law satisfied (two nonzero-resolve results); under cargo test no + // materialization receipt file exists, so exactly the missing-receipt refusal + // remains — proving law 2 fails closed on absence rather than passing. + assert!( + !refusals.iter().any(|m| m.contains("differs from declared")), + "resolve law must be satisfied: {refusals:?}" + ); + assert!( + refusals + .iter() + .any(|m| m.contains("floor materialization receipt missing")), + "absent receipt must refuse, never pass: {refusals:?}" + ); + } + // D2 wiring pin: the fast-exit consumes exactly this mapping, so the terminal // process code stays behavior-identical to the ExitCode return it replaced. #[test] diff --git a/src/v2/test/claim/ci_spec_witness_test.dag b/src/v2/test/claim/ci_spec_witness_test.dag index 8a3dcfd2329..70eb1bdaecb 100644 --- a/src/v2/test/claim/ci_spec_witness_test.dag +++ b/src/v2/test/claim/ci_spec_witness_test.dag @@ -39,11 +39,8 @@ import gunbc.repo_identity { gunbc_default_branch_name, } import extdeps.git { GitRemoteRefParts, git_remote_ref } -import gunbc.ci_materialization { - ci_floor_resolve_receipt_gate_script, - ci_floor_materialization_receipt_gate_script, -} import gunbc.ci_workflow { ci_selection_control_script } +import gunbc.ci_yaml_emit { expected_ci_yml } import gunbc.merge_admission_produce { merge_admission_merge_base_ref, } @@ -303,20 +300,22 @@ test fn witness_selection_control_scoped_red_control() -> Bool { !selection_control_scoped_holds_for(s: missing_suite_invocation) } +data receipt_gate_scripts_deleted_note: String = "The resolve-receipt and materialization-receipt gate SCRIPTS are deleted, not just unwired (ruling 2026-07-30): their laws run inside claim_executor as ordinary-floor finalization, and a surviving script would be a second representation of the floor-completion rule whose still-green tests could mask a live-path regression — the exact shape that hid the lost admission fetch. This witness therefore asserts their step names are ABSENT from the emitted workflow, and the disposition-transport check below keeps covering only the scripts that still exist." + +test fn witness_receipt_gate_steps_absent_from_workflow() -> Bool { + let yaml = expected_ci_yml() + !string_contains(s: yaml, pattern: "Floor resolve receipt gate") && + !string_contains(s: yaml, pattern: "Floor materialization receipt gate") +} + test fn witness_documentation_only_disposition_transport_retired() -> Bool { let marker_path = "target/ci-floor-disposition.txt" let skipped_disposition = "documentation_only_skipped" let floor = gunbc_ci_floor_only_script(spec: gunbc_ci_spec) - let resolve_gate = ci_floor_resolve_receipt_gate_script() - let materialization_gate = ci_floor_materialization_receipt_gate_script() let selection_gate = ci_selection_control_script() let admission_gate = ci_merge_admission_gate_script() !string_contains(s: floor, pattern: marker_path) && !string_contains(s: floor, pattern: skipped_disposition) && - !string_contains(s: resolve_gate, pattern: marker_path) && - !string_contains(s: resolve_gate, pattern: skipped_disposition) && - !string_contains(s: materialization_gate, pattern: marker_path) && - !string_contains(s: materialization_gate, pattern: skipped_disposition) && !string_contains(s: selection_gate, pattern: marker_path) && !string_contains(s: selection_gate, pattern: skipped_disposition) && !string_contains(s: admission_gate, pattern: marker_path) && diff --git a/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag b/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag index 812ab65cf32..e8e43118b25 100644 --- a/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag +++ b/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag @@ -19,6 +19,12 @@ fn budget_red_control_claim() -> Runnable { } } +data budget_red_control_declared_resolve_note: String = "The fixture impersonates the floor plan BY NAME so claim_executor arms the derived clamp against it — and as of the in-executor floor finalization (ruling 2026-07-30) that name also arms the resolve law, so the fixture must declare it or the run refuses before walking (that refusal is itself the finalization read's executable RED, exercised before this row landed). Declared 1: the fixture runs exactly one single-claim batch, which pays exactly one entry resolve." + +fn gunbc_ci_floor_declared_resolve_count() -> Int { + 1 +} + fn gunbc_ci_floor_plan() -> WalkPlan { WalkPlan { batches: [[budget_red_control_claim()]], diff --git a/src/v2/workflow/ci_floor_plan.dag b/src/v2/workflow/ci_floor_plan.dag index e21b2072f0f..acbf321da29 100644 --- a/src/v2/workflow/ci_floor_plan.dag +++ b/src/v2/workflow/ci_floor_plan.dag @@ -11,6 +11,7 @@ import v2.compiler.self_host.wet_receipt_enrollment { falsifier_self_host_wet_known_red_roster } import v2.compiler.self_host.native_routing_frontier { native_routed_rows, NativeRoutingFrontierRow } +import gunbc.ci_materialization { ci_floor_declared_resolve_count } import gunbc.ci_spec { gunbc_ci_spec, gunbc_ci_regen_spec, CiSpec, ci_spec_with_discovery_scan_dirs, @@ -1233,6 +1234,12 @@ fn gunbc_ci_plan_artifact_ordinary_batches() -> List> { data walk_plan_uniformity_note: String = "EVERY plan function returns WalkPlan, including the three with no postconditions — on_success_stages: [] is a declared empty set, not an omission the executor papers over. The executor has ONE strict parser for the record shape; there is deliberately NO fallback from a failed record parse to a bare-List> reading, because that fallback would let a malformed plan silently run with its success stages dropped — the exact silent-widen shape §5 forbids. The `_plan` names replace the `_batches` names in the same motion (operator ruling 2026-07-30): a function whose value now carries postcondition stages must not keep a name that says it returns only batches, or the next author reasonably assumes the stages are not part of the value. The four argv/step consumers follow automatically because they derive from the floor_plan_function / plan_artifact_plan_function / regen_floor_plan_function / falsifier_plan_function constants (gunbc.ci_spec, gunbc.falsifier_workflow) — those constants are the single naming authority, renamed with the functions." +data gunbc_ci_floor_declared_resolve_count_projection_note: String = "Projection of gunbc.ci_materialization.ci_floor_declared_resolve_count INTO THE PLAN CLOSURE, so claim_executor can read the resolve law from the same resolved plan context it already holds — the clamp-params pattern. The authority stays in ci_materialization (single authority, with its receipt history); this fn is a read, not a second declaration. Consumed by the executor's floor finalization (ruling 2026-07-30): the resolve-count law and the materialization disclosure law moved from two GitHub shell steps into run_walk's ordinary-floor finalization, so a receipt-law violation is an ORDINARY-FLOOR failure that blocks every on-success stage — the old step ordering could red AFTER admission had already stamped, which is the can-red-after-admission hole this closes. The two workflow steps are deleted in the same motion; keeping them would be a second representation of the floor-completion rule." + +fn gunbc_ci_floor_declared_resolve_count() -> Int { + ci_floor_declared_resolve_count +} + fn gunbc_ci_floor_plan() -> WalkPlan { WalkPlan { batches: gunbc_ci_floor_ordinary_batches(), From b87ee29f25b253806d1282b49fcb3db3b7d2d3bf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 18:00:42 +0000 Subject: [PATCH 3/4] Attempt-scoped admission carrier: receipt v2, TestedSubject, WrongAttempt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 slice of the success-stages redesign (operator ruling 2026-07-30): the pure carrier for attempt-scoped merge admission, landed add-replacement beside the live v1 format. No consumer migrates yet — the wet entry, the plan wiring, and the v1/emitter-web deletion follow on this branch — so runtime behavior is unchanged; this slice is the vocabulary the migration will speak, proven hermetically before anything wet depends on it. THE CARRIER. MergeAdmissionReceiptV2 carries the walk-attempt identity IN THE PAYLOAD, not just the path: a misrouted read must fail on content too. TestedSubject {attempt_id, head_sha, base_ref, base_tree_hash} is the subject the floor tested, captured pre-floor and bound into the receipt by the stamp — the stamp must never re-read HEAD or re-observe the target at stamp time, because main can advance during a tens-of-minutes floor and a post-floor observation would claim the floor tested a tree it never saw. THE VERDICT gains MergeDeniedWrongAttempt, checked FIRST: a receipt from another attempt is neither stale nor fresh — it is not the subject, and asking whether a foreign receipt's base is current answers a question about the wrong run. Growing the coproduct forced every exhaustive match to take a position (non-fold residue, no wildcards): the gate's verdict_reason, would_block, and the enforcement/actuator witnesses each gained the arm consciously, and the compiler's exhaustiveness refusal located the one site the sweep missed (receipt_is_admissible) — the coproduct growth is complete by construction, not by grep. ATTEMPT IDENTITY composes from GITHUB_RUN_ID + RUN_ATTEMPT + JOB, pure composition here, env observation deferred to the wet entry. Every part must be nonempty or the composition refuses — never a silent 'local' constant, which would make every non-GitHub run one attempt and leave the wrong-attempt refusal unreachable off CI (the unreachable-arm shape §5 forbids). Non-GitHub execution supplies GUNBC_WALK_ATTEMPT_ID explicitly, same nonempty law. WIRES are self-identifying (schema tag line 1), unlike positional v1: a v1 wire fed to the v2 parser refuses on the tag rather than mis-binding fields by position, and the two v2 wire kinds refuse each other. Paths are attempt-scoped: .gunbc/merge-admission//{tested-subject, floor-receipt}.wire. VERIFIED BY EXECUTION — 13/13 new hermetic witnesses, including the ordering proof (a receipt that is simultaneously foreign, Failure, stale- base, AND stale-roster classifies WrongAttempt — precedence, not coincidence), both cross-parser refusals, both roundtrips, the nonempty refusals, and path distinctness across attempts. Existing admission witnesses regress clean: producer 15/15, enforcement 13/13, actuator 8/8. Generated artifacts unchanged (model + tests only). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N7LfCpidMuDjV3FxYUHv1K --- dag/gunbc/merge_admission.dag | 44 +++- dag/gunbc/merge_admission_produce.dag | 118 ++++++++++- .../merge_admission_actuator_witness_test.dag | 7 + .../merge_admission_attempt_witness_test.dag | 197 ++++++++++++++++++ ...rge_admission_enforcement_witness_test.dag | 4 + dag/tools/merge_admission_gate.dag | 1 + 6 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 dag/test/claim/merge_admission_attempt_witness_test.dag diff --git a/dag/gunbc/merge_admission.dag b/dag/gunbc/merge_admission.dag index c50765498de..a18c772269a 100644 --- a/dag/gunbc/merge_admission.dag +++ b/dag/gunbc/merge_admission.dag @@ -1,6 +1,6 @@ module gunbc.merge_admission -import std.types { ContentHash, CommitSha, String, List, Bool, Int } +import std.types { NonEmptyStr, ContentHash, CommitSha, String, List, Bool, Int } import extdeps.github.checks { CheckConclusion, check_conclusion_is_success, @@ -167,6 +167,7 @@ type MergeAdmissionVerdict | MergeDeniedStaleBase | MergeDeniedStaleRoster | MergeDeniedNotSuccess + | MergeDeniedWrongAttempt data merge_admission_enforcement_policy: AdmissionPolicy = KeyedReceipt @@ -199,12 +200,52 @@ fn classify_merge_admission_verdict( } } +data merge_admission_attempt_scope_note: String = "ATTEMPT SCOPE (ruling 2026-07-30): the receipt carries the walk-attempt identity in the PAYLOAD, not just the path, and the gate requires receipt.attempt_id == current — path identity alone is not enough, because a misrouted read must fail on the content too. A receipt from another attempt is neither stale nor fresh: it is NOT THE SUBJECT, so MergeDeniedWrongAttempt is checked FIRST, before conclusion, base, and roster — asking whether a foreign receipt's base is current would be answering a question about the wrong run. The identity derives from GITHUB_RUN_ID + GITHUB_RUN_ATTEMPT + GITHUB_JOB on GitHub; non-GitHub execution must supply GUNBC_WALK_ATTEMPT_ID explicitly or the walk-side composition refuses — never a silent constant like a bare local, which would make every local run one attempt and the wrong-attempt refusal unreachable off CI. Deleting or recreating the attempt directory at walk start remains hygiene; correctness never depends on cleaning a shared path." + +type TestedSubject { + attempt_id: NonEmptyStr + head_sha: CommitSha + base_ref: String + base_tree_hash: ContentHash +} + +data tested_subject_note: String = "THE SUBJECT THE FLOOR TESTED, captured as the first ordinary batch — before any substantive floor work — and bound into the receipt by the stamp. The stamp must NOT re-read HEAD or re-observe the merge target at stamp time: main can advance during a floor that runs for tens of minutes, and a receipt binding post-floor observations would claim the floor tested a tree it never saw. The gate then fetches the CURRENT target and classifies tested_base_tree_hash against it — two base facts with different jobs, never one observation doing both (the deleted single pre-floor fetch was exactly that fusion, and the freshness hole it left is what the old separate gate step's own fresh fetch existed to catch)." + +type MergeAdmissionReceiptV2 { + attempt_id: NonEmptyStr + pr_number: Int? + tested_head_sha: CommitSha + tested_base_tree_hash: ContentHash + gate_roster_hash: ContentHash + conclusion: CheckConclusion +} + +fn classify_merge_admission_verdict_v2( + current_attempt_id: NonEmptyStr, + receipt: MergeAdmissionReceiptV2, + merge_target_tree_hash: ContentHash, + current_roster_hash: ContentHash, +) -> MergeAdmissionVerdict { + if !((receipt.attempt_id as String) == (current_attempt_id as String)) { + MergeDeniedWrongAttempt + } else if !check_conclusion_admits_merge_admission_floor(c: receipt.conclusion) { + MergeDeniedNotSuccess + } else if receipt.tested_base_tree_hash != merge_target_tree_hash { + MergeDeniedStaleBase + } else if receipt.gate_roster_hash != current_roster_hash { + MergeDeniedStaleRoster + } else { + MergeAdmitted + } +} + fn merge_admission_verdict_would_block(v: MergeAdmissionVerdict) -> Bool { match v { MergeAdmitted => false MergeDeniedStaleBase => true MergeDeniedStaleRoster => true MergeDeniedNotSuccess => true + MergeDeniedWrongAttempt => true } } @@ -236,6 +277,7 @@ fn receipt_is_admissible( MergeDeniedStaleBase => false MergeDeniedStaleRoster => false MergeDeniedNotSuccess => false + MergeDeniedWrongAttempt => false } } diff --git a/dag/gunbc/merge_admission_produce.dag b/dag/gunbc/merge_admission_produce.dag index 65b52b44dc4..0f29259d8ae 100644 --- a/dag/gunbc/merge_admission_produce.dag +++ b/dag/gunbc/merge_admission_produce.dag @@ -1,6 +1,6 @@ module gunbc.merge_admission_produce -import std.types { ContentHash, CommitSha, String, Int, List, Bool, GitRef } +import std.types { NonEmptyStr, ContentHash, CommitSha, String, Int, List, Bool, GitRef } import std.disposition { Disposition, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } import gunbc.repo_identity { gunbc_merge_target_ref } @@ -21,6 +21,8 @@ import extdeps.github.checks { Failure, } import gunbc.merge_admission { + TestedSubject, + MergeAdmissionReceiptV2, MergeAdmissionReceipt, mint_merge_admission_receipt, current_gate_roster_hash, @@ -33,6 +35,120 @@ data merge_admission_receipt_schema: String = "gunbc.merge_admission_receipt.v1" data merge_admission_receipt_rel_path: String = ".gunbc/merge-admission-receipt.wire" +data merge_admission_receipt_schema_v2: String = "gunbc.merge_admission_receipt.v2" + +data merge_admission_tested_subject_schema: String = "gunbc.merge_admission_tested_subject.v1" + +fn merge_admission_attempt_dir(root: String, attempt_id: NonEmptyStr) -> String { + concat(concat(root, "/.gunbc/merge-admission/"), attempt_id as String) +} + +fn merge_admission_tested_subject_path(root: String, attempt_id: NonEmptyStr) -> String { + concat(merge_admission_attempt_dir(root: root, attempt_id: attempt_id), "/tested-subject.wire") +} + +fn merge_admission_floor_receipt_path(root: String, attempt_id: NonEmptyStr) -> String { + concat(merge_admission_attempt_dir(root: root, attempt_id: attempt_id), "/floor-receipt.wire") +} + +data compose_walk_attempt_id_note: String = "Pure composition of the walk-attempt identity from its parts; the ENV OBSERVATION lives with the wet entry, this fn only joins and refuses. Every part must be nonempty — an empty run id, attempt number, or job name yields Absent, and the caller REFUSES the run rather than substituting a constant: a silent local would make every non-GitHub run one attempt, so the wrong-attempt refusal could never fire off CI, which is exactly the unreachable-arm shape §5 forbids. Non-GitHub execution supplies GUNBC_WALK_ATTEMPT_ID explicitly and it passes through compose_explicit_walk_attempt_id under the same nonempty law." + +fn compose_walk_attempt_id(run_id: String, run_attempt: String, job: String) -> NonEmptyStr? { + if (run_id == "") || (run_attempt == "") || (job == "") { + none + } else { + Present { + value: concat(concat(concat(concat(run_id, "-"), run_attempt), "-"), job) as NonEmptyStr + } + } +} + +fn compose_explicit_walk_attempt_id(explicit: String) -> NonEmptyStr? { + if explicit == "" { none } else { Present { value: explicit as NonEmptyStr } } +} + +fn render_tested_subject_wire(subject: TestedSubject) -> String { + concat( + concat( + concat(concat(merge_admission_tested_subject_schema, " +"), concat(subject.attempt_id as String, " +")), + concat(subject.base_ref, " +") + ), + concat(concat(subject.base_tree_hash, " +"), subject.head_sha) + ) +} + +fn parse_tested_subject_wire(text: String) -> TestedSubject? { + let lines = receipt_wire_lines(text: text) + if length(lines) < 5 { + none + } else if !(lines.first() == merge_admission_tested_subject_schema) { + none + } else if lines.skip(n: 1).first() == "" { + none + } else { + Present { + value: TestedSubject { + attempt_id: lines.skip(n: 1).first() as NonEmptyStr, + base_ref: lines.skip(n: 2).first(), + base_tree_hash: lines.skip(n: 3).first(), + head_sha: lines.skip(n: 4).first(), + } + } + } +} + +data receipt_wire_v2_note: String = "Wire v2 is SELF-IDENTIFYING: line 1 is the schema tag, so a v1 wire (positional, no tag) read by the v2 parser refuses on the tag line instead of mis-binding fields by position — and a v2 wire read by the v1 parser mis-parses into a conclusion-line mismatch, also a refusal. Lines: schema, attempt_id, tested_base_tree_hash, gate_roster_hash, conclusion, tested_head_sha, then the optional pr number. The v1 wire, path, and type stay until the entry migration completes and the old consumers delete with the emitter web (add-replacement, never in-place mutation of a live format)." + +fn render_receipt_wire_v2(receipt: MergeAdmissionReceiptV2) -> String { + let core = concat( + concat( + concat(concat(merge_admission_receipt_schema_v2, " +"), concat(receipt.attempt_id as String, " +")), + concat(concat(receipt.tested_base_tree_hash, " +"), concat(receipt.gate_roster_hash, " +")) + ), + concat(concat(check_conclusion_wire_label(c: receipt.conclusion), " +"), receipt.tested_head_sha) + ) + match receipt.pr_number { + Present { value: n } => concat(core, concat(" +", to_string(n))) + Absent => core + } +} + +fn parse_receipt_wire_v2(text: String) -> MergeAdmissionReceiptV2? { + let lines = receipt_wire_lines(text: text) + if length(lines) < 6 { + none + } else if !(lines.first() == merge_admission_receipt_schema_v2) { + none + } else if lines.skip(n: 1).first() == "" { + none + } else { + match parse_check_conclusion(raw: lines.skip(n: 4).first()) { + Present { value: conclusion } => + Present { + value: MergeAdmissionReceiptV2 { + attempt_id: lines.skip(n: 1).first() as NonEmptyStr, + pr_number: if length(lines) >= 7 { parse_int(s: lines.skip(n: 6).first()) } else { none }, + tested_head_sha: lines.skip(n: 5).first(), + tested_base_tree_hash: lines.skip(n: 2).first(), + gate_roster_hash: lines.skip(n: 3).first(), + conclusion: conclusion, + } + } + Absent => none + } + } +} + fn merge_admission_receipt_path(root: String) -> String { concat(concat(root, "/"), merge_admission_receipt_rel_path) } diff --git a/dag/test/claim/merge_admission_actuator_witness_test.dag b/dag/test/claim/merge_admission_actuator_witness_test.dag index 073914461a7..5f898641c36 100644 --- a/dag/test/claim/merge_admission_actuator_witness_test.dag +++ b/dag/test/claim/merge_admission_actuator_witness_test.dag @@ -30,6 +30,7 @@ test fn enforcement_denies_stale_base_verdict() -> Bool { MergeAdmitted => false MergeDeniedStaleRoster => false MergeDeniedNotSuccess => false + MergeDeniedWrongAttempt => false } } @@ -43,6 +44,7 @@ test fn enforcement_denies_stale_roster_verdict() -> Bool { MergeAdmitted => false MergeDeniedStaleBase => false MergeDeniedNotSuccess => false + MergeDeniedWrongAttempt => false } } @@ -56,6 +58,7 @@ test fn enforcement_admits_fresh_verdict() -> Bool { MergeDeniedStaleBase => false MergeDeniedStaleRoster => false MergeDeniedNotSuccess => false + MergeDeniedWrongAttempt => false } } @@ -73,6 +76,7 @@ test fn enforcement_denies_skipped_fresh_receipt() -> Bool { current_roster_hash: scenario_roster_r0, ) { MergeDeniedNotSuccess => true + MergeDeniedWrongAttempt => false MergeAdmitted => false MergeDeniedStaleBase => false MergeDeniedStaleRoster => false @@ -93,6 +97,7 @@ test fn enforcement_denies_failure_fresh_receipt() -> Bool { current_roster_hash: scenario_roster_r0, ) { MergeDeniedNotSuccess => true + MergeDeniedWrongAttempt => false MergeAdmitted => false MergeDeniedStaleBase => false MergeDeniedStaleRoster => false @@ -109,6 +114,7 @@ test fn enforcement_red_on_revert_stale_base() -> Bool { MergeDeniedStaleBase => false MergeDeniedStaleRoster => false MergeDeniedNotSuccess => false + MergeDeniedWrongAttempt => false } let stale = match classify_merge_admission_verdict( receipt: scenario_pr_x_receipt, @@ -119,6 +125,7 @@ test fn enforcement_red_on_revert_stale_base() -> Bool { MergeAdmitted => false MergeDeniedStaleRoster => false MergeDeniedNotSuccess => false + MergeDeniedWrongAttempt => false } fresh && stale } diff --git a/dag/test/claim/merge_admission_attempt_witness_test.dag b/dag/test/claim/merge_admission_attempt_witness_test.dag new file mode 100644 index 00000000000..fe88f4630e5 --- /dev/null +++ b/dag/test/claim/merge_admission_attempt_witness_test.dag @@ -0,0 +1,197 @@ +module test.claim.merge_admission_attempt_witness + +import std.types { NonEmptyStr, ContentHash, CommitSha } +import extdeps.github.checks { Success, Failure } +import gunbc.merge_admission { + TestedSubject, + MergeAdmissionReceiptV2, + MergeAdmissionVerdict, + MergeAdmitted, + MergeDeniedStaleBase, + MergeDeniedStaleRoster, + MergeDeniedNotSuccess, + MergeDeniedWrongAttempt, + classify_merge_admission_verdict_v2, + merge_admission_verdict_would_block, +} +import gunbc.merge_admission_produce { + merge_admission_receipt_schema_v2, + merge_admission_tested_subject_schema, + merge_admission_tested_subject_path, + merge_admission_floor_receipt_path, + compose_walk_attempt_id, + compose_explicit_walk_attempt_id, + render_tested_subject_wire, + parse_tested_subject_wire, + render_receipt_wire_v2, + parse_receipt_wire_v2, + render_receipt_wire, + stamp_floor_receipt_with_conclusion, +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data attempt_a: NonEmptyStr = "12345-1-ci" +data attempt_b: NonEmptyStr = "12345-2-ci" +data head_sha_fx: CommitSha = "abc1234def5678901234567890abcd1234567890" +data base_tree_fx: ContentHash = "tree-base-current" +data base_tree_stale_fx: ContentHash = "tree-base-stale" +data roster_fx: ContentHash = "roster-current" + +fn receipt_fx(attempt: NonEmptyStr, base: ContentHash) -> MergeAdmissionReceiptV2 { + MergeAdmissionReceiptV2 { + attempt_id: attempt, + pr_number: Present { value: 7470 }, + tested_head_sha: head_sha_fx, + tested_base_tree_hash: base, + gate_roster_hash: roster_fx, + conclusion: Success, + } +} + +test fn matching_attempt_fresh_base_admits() -> Bool { + match classify_merge_admission_verdict_v2( + current_attempt_id: attempt_a, + receipt: receipt_fx(attempt: attempt_a, base: base_tree_fx), + merge_target_tree_hash: base_tree_fx, + current_roster_hash: roster_fx, + ) { MergeAdmitted => true _ => false } +} + +test fn foreign_attempt_refuses_as_wrong_attempt() -> Bool { + let v = classify_merge_admission_verdict_v2( + current_attempt_id: attempt_a, + receipt: receipt_fx(attempt: attempt_b, base: base_tree_fx), + merge_target_tree_hash: base_tree_fx, + current_roster_hash: roster_fx, + ) + match v { MergeDeniedWrongAttempt => merge_admission_verdict_would_block(v: v) _ => false } +} + +test fn wrong_attempt_precedes_every_other_refusal() -> Bool { + let r = MergeAdmissionReceiptV2 { + attempt_id: attempt_b, + pr_number: none, + tested_head_sha: head_sha_fx, + tested_base_tree_hash: base_tree_stale_fx, + gate_roster_hash: "roster-stale", + conclusion: Failure, + } + match classify_merge_admission_verdict_v2( + current_attempt_id: attempt_a, + receipt: r, + merge_target_tree_hash: base_tree_fx, + current_roster_hash: roster_fx, + ) { MergeDeniedWrongAttempt => true _ => false } +} + +test fn matching_attempt_stale_base_is_stale_not_wrong_attempt() -> Bool { + match classify_merge_admission_verdict_v2( + current_attempt_id: attempt_a, + receipt: receipt_fx(attempt: attempt_a, base: base_tree_stale_fx), + merge_target_tree_hash: base_tree_fx, + current_roster_hash: roster_fx, + ) { MergeDeniedStaleBase => true _ => false } +} + +test fn wire_v2_roundtrips_all_fields_with_pr() -> Bool { + let r = receipt_fx(attempt: attempt_a, base: base_tree_fx) + match parse_receipt_wire_v2(text: render_receipt_wire_v2(receipt: r)) { + Present { value: parsed } => + ((parsed.attempt_id as String) == (attempt_a as String)) + && (parsed.tested_head_sha == head_sha_fx) + && (parsed.tested_base_tree_hash == base_tree_fx) + && (parsed.gate_roster_hash == roster_fx) + && match parsed.pr_number { Present { value: n } => n == 7470 Absent => false } + && match parsed.conclusion { Success => true _ => false } + Absent => false + } +} + +test fn wire_v2_roundtrips_without_pr() -> Bool { + let r = MergeAdmissionReceiptV2 { + attempt_id: attempt_a, + pr_number: none, + tested_head_sha: head_sha_fx, + tested_base_tree_hash: base_tree_fx, + gate_roster_hash: roster_fx, + conclusion: Success, + } + match parse_receipt_wire_v2(text: render_receipt_wire_v2(receipt: r)) { + Present { value: parsed } => + match parsed.pr_number { Absent => (parsed.attempt_id as String) == (attempt_a as String) Present { value: _ } => false } + Absent => false + } +} + +test fn v1_wire_refused_by_v2_parser() -> Bool { + let v1 = render_receipt_wire(receipt: stamp_floor_receipt_with_conclusion( + pr_number: Present { value: 7470 }, + head_sha: head_sha_fx, + base_tree_hash: base_tree_fx, + conclusion: Success, + )) + match parse_receipt_wire_v2(text: v1) { + Present { value: _ } => false + Absent => true + } +} + +test fn tested_subject_wire_roundtrips() -> Bool { + let subj = TestedSubject { + attempt_id: attempt_a, + head_sha: head_sha_fx, + base_ref: "origin/main", + base_tree_hash: base_tree_fx, + } + match parse_tested_subject_wire(text: render_tested_subject_wire(subject: subj)) { + Present { value: parsed } => + ((parsed.attempt_id as String) == (attempt_a as String)) + && (parsed.head_sha == head_sha_fx) + && (parsed.base_ref == "origin/main") + && (parsed.base_tree_hash == base_tree_fx) + Absent => false + } +} + +test fn subject_wire_refused_by_receipt_parser_and_vice_versa() -> Bool { + let subj = render_tested_subject_wire(subject: TestedSubject { + attempt_id: attempt_a, + head_sha: head_sha_fx, + base_ref: "origin/main", + base_tree_hash: base_tree_fx, + }) + let rec = render_receipt_wire_v2(receipt: receipt_fx(attempt: attempt_a, base: base_tree_fx)) + match parse_receipt_wire_v2(text: subj) { Present { value: _ } => false Absent => true } + && match parse_tested_subject_wire(text: rec) { Present { value: _ } => false Absent => true } +} + +test fn attempt_id_composes_from_nonempty_parts() -> Bool { + match compose_walk_attempt_id(run_id: "12345", run_attempt: "1", job: "ci") { + Present { value: id } => (id as String) == "12345-1-ci" + Absent => false + } +} + +test fn attempt_id_refuses_any_empty_part() -> Bool { + match compose_walk_attempt_id(run_id: "", run_attempt: "1", job: "ci") { Present { value: _ } => false Absent => true } + && match compose_walk_attempt_id(run_id: "12345", run_attempt: "", job: "ci") { Present { value: _ } => false Absent => true } + && match compose_walk_attempt_id(run_id: "12345", run_attempt: "1", job: "") { Present { value: _ } => false Absent => true } + && match compose_explicit_walk_attempt_id(explicit: "") { Present { value: _ } => false Absent => true } +} + +test fn attempt_paths_are_attempt_scoped_and_distinct() -> Bool { + let subj = merge_admission_tested_subject_path(root: "/repo", attempt_id: attempt_a) + let rec_a = merge_admission_floor_receipt_path(root: "/repo", attempt_id: attempt_a) + let rec_b = merge_admission_floor_receipt_path(root: "/repo", attempt_id: attempt_b) + string_contains(s: subj, pattern: "/.gunbc/merge-admission/12345-1-ci/") + && string_contains(s: rec_a, pattern: "/12345-1-ci/") + && !(rec_a == rec_b) + && !(subj == rec_a) +} + +test fn schemas_are_distinct_and_versioned() -> Bool { + !(merge_admission_receipt_schema_v2 == merge_admission_tested_subject_schema) + && string_contains(s: merge_admission_receipt_schema_v2, pattern: ".v2") +} diff --git a/dag/test/claim/merge_admission_enforcement_witness_test.dag b/dag/test/claim/merge_admission_enforcement_witness_test.dag index edfd46a47b8..e4adf8ec1f0 100644 --- a/dag/test/claim/merge_admission_enforcement_witness_test.dag +++ b/dag/test/claim/merge_admission_enforcement_witness_test.dag @@ -130,6 +130,8 @@ test fn wire_stale_base_fails_verdict_parse_path() -> Bool { MergeAdmitted => false MergeDeniedStaleRoster => false MergeDeniedNotSuccess => false + MergeDeniedWrongAttempt => false + MergeDeniedWrongAttempt => false } Absent => false } @@ -145,6 +147,7 @@ test fn fresh_verdict_is_merge_admitted() -> Bool { MergeDeniedStaleBase => false MergeDeniedStaleRoster => false MergeDeniedNotSuccess => false + MergeDeniedWrongAttempt => false } } @@ -155,6 +158,7 @@ test fn red_verdict_is_merge_denied_not_success() -> Bool { current_roster_hash: current_gate_roster_hash, ) { MergeDeniedNotSuccess => true + MergeDeniedWrongAttempt => false MergeAdmitted => false MergeDeniedStaleBase => false MergeDeniedStaleRoster => false diff --git a/dag/tools/merge_admission_gate.dag b/dag/tools/merge_admission_gate.dag index 866382c7163..1d88d0f93fa 100644 --- a/dag/tools/merge_admission_gate.dag +++ b/dag/tools/merge_admission_gate.dag @@ -30,6 +30,7 @@ fn verdict_reason(v: MergeAdmissionVerdict) -> String { MergeDeniedStaleBase => "stale base tree — re-run CI against current main" MergeDeniedStaleRoster => "stale gate roster — re-run CI after criteria change" MergeDeniedNotSuccess => "receipt conclusion is not success" + MergeDeniedWrongAttempt => "receipt belongs to a different walk attempt — not the subject; re-run CI" } } From 2e895b0ba884a4d58bddfb66dcaa75cebda64dc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 19:31:02 +0000 Subject: [PATCH 4/4] Carry finalization policy in WalkPlan; regenerate the stale stage0 seed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two halves: review finding 4, and the fix for the regen red CI just reported on this branch. WALKFINALIZATION IN THE CARRIER. The finalization policy was selected by plan-function name — `if plan_function == "gunbc_ci_floor_plan" read the law from the closure` — which is the same hidden seed-roster convention the WalkPlan carrier was built to remove, reintroduced in the same PR. The RED fixture made the coupling visible: it had to impersonate the production plan name and then define a projection fn because that name silently enrolled the contract. The policy is now a FIELD: type WalkFinalization = NoWalkFinalization | FloorFinalization { declared_resolve_count: Int require_materialization_disclosure: Bool } type WalkPlan { batches, finalization, on_success_stages } Every plan declares it explicitly — the floor carries its law, regen / plan-artifact / falsifier say NoWalkFinalization, never omission. The strict parser requires the field, so a floor plan without its law is now UNWRITABLE rather than discovered missing at arm time — the fail-closed arm-time read this replaces is deleted along with the projection fn and the fixture's name-impersonated law. Schedule lenses and plan artifacts can now see the policy, because it is part of the parsed value. THE REGEN RED, diagnosed from the job log: regen_verify_gate_passes returned false on this branch's previous head because the WalkPlan carrier change touched dag/std/realization_schedule.dag, which is in v1's regen input closure, and the committed stage0 seed was stale against it. regen_stage0 confirms the diagnosis exactly: of 108 written files, precisely ONE differs — std_realization_schedule.rs, the emitted Rust of the carrier — now regenerated against the current tree (including WalkFinalization, so one seed covers both changes) and proven byte-stable on a second regen run. VERIFIED: cargo build clean; the two floor-finalization unit arms pass against the field-carried struct; gunbc-eval of gunbc_ci_floor_plan shows the plan value carrying FloorFinalization { declared_resolve_count 1, require_materialization_disclosure true }; generated artifacts regenerate clean; fmt clean. Local executor parse probes of the fixture and regen plans were killed by this container's timeout during the multi-minute prelude, before reaching the parse line — inconclusive, not failing; the pushed run is the parse evidence. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01N7LfCpidMuDjV3FxYUHv1K --- dag/std/realization_schedule.dag | 10 ++ src/v1/stage0/src/bin/claim_executor.rs | 97 +++++++++++++------ src/v1/stage0/src/std_realization_schedule.rs | 60 ++++++++++++ .../floor_skip/budget_red_control_plan.dag | 11 ++- src/v2/workflow/ci_floor_plan.dag | 16 +-- 5 files changed, 156 insertions(+), 38 deletions(-) diff --git a/dag/std/realization_schedule.dag b/dag/std/realization_schedule.dag index c8e3d7c106d..87f6452a09b 100644 --- a/dag/std/realization_schedule.dag +++ b/dag/std/realization_schedule.dag @@ -209,8 +209,18 @@ data walk_plan_note: String = "A walk is TWO populations with DIFFERENT ordering WHAT THE EXECUTOR ACTUALLY PROVIDES TODAY, stated because a carrier that promises more than its executor delivers is the same defect this type exists to end (review 2026-07-30). The stage BARRIER is real: stage N completes before stage N+1 starts, and a failed stage prevents every later one. THREE GAPS remain, and none is currently reachable because every plan declares an empty stage list: (1) members within a stage run SERIALLY, not concurrently — serial is a strictly stronger order than the contract promises, so no current caller is misled, but wall time, peak memory, and overlap are resource facts a future author would measure wrongly; (2) stages execute through run_memo_shared_claims directly rather than the ordinary unit-lane partition, so they bypass governor admission, batch clamps, and resource-profile enforcement — safe for the negligible admission claims that will occupy them, unsafe for the substantial or host-compiler-spawning claim the generic carrier permits; (3) ONE aggregate stage receipt is written after the whole sequence rather than one per stage before the next begins. The repair is to extract the ordinary batch machinery into a reusable run_stage(stage) — group by entry and mode, acquire governor admission, spawn eligible units, join, write THAT stage receipt — so both populations share one executor and differ only in ordering and failure policy. Until that lands, do not place a Substantial or host-compiler runnable in a stage; the arm-time validator refuses discovery runnables outright, and the profile restriction is the next wall. Every plan function returns WalkPlan — a plan with no postconditions returns on_success_stages: [] — and the executor has ONE strict parser: a malformed or missing field is a hard error, never a fallback to a bare-list reading." +data walk_finalization_note: String = "THE PLAN CARRIES ITS FINALIZATION POLICY; the executor never infers it from a function's spelling. The first cut selected finalization by plan-function name (if plan_function == gunbc_ci_floor_plan read the law from the closure) — the same hidden seed-roster convention the WalkPlan carrier was built to remove, reintroduced in the same PR (review 2026-07-30), and the RED fixture made the coupling visible by having to impersonate the production name to arm the contract. As a FIELD, the policy is part of the parsed value: a floor plan without its law is now UNWRITABLE (the strict parser refuses a missing field) rather than checked at arm time, schedule lenses and plan artifacts can see it, and the fixture declares its own policy instead of name-impersonating into one. NoWalkFinalization is the declared empty policy — regen, falsifier, and the plan-artifact shortcut say so explicitly, never by omission." + +type WalkFinalization + = NoWalkFinalization + | FloorFinalization { + declared_resolve_count: Int + require_materialization_disclosure: Bool + } + type WalkPlan { batches: List> + finalization: WalkFinalization on_success_stages: List> } diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 1c65b6c8ba9..83cfcc544c2 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -579,9 +579,62 @@ fn batches_from_plan(plan: &Value, ctx: &InterpContext) -> Result>, + finalization: Option, on_success_stages: Vec>, } +/// Parse `WalkFinalization` — the plan-carried policy (walk_finalization_note): the +/// executor never selects finalization by a plan function's spelling. A missing or +/// unrecognized variant is a hard error, so a floor plan without its law is +/// unwritable rather than discovered at arm time. +fn finalization_from_value( + v: &Value, + ctx: &InterpContext, +) -> Result, String> { + match v { + Value::Variant { + variant_name, + fields, + .. + } => { + if ctx.sym_eq(*variant_name, "NoWalkFinalization") { + Ok(None) + } else if ctx.sym_eq(*variant_name, "FloorFinalization") { + let declared = match ctx.field(fields, "declared_resolve_count") { + Some(Value::Int(n)) => *n, + other => { + return Err(format!( + "FloorFinalization.declared_resolve_count must be an Int, got {other:?}" + )) + } + }; + let disclosure = match ctx.field(fields, "require_materialization_disclosure") { + Some(Value::Bool(b)) => *b, + other => { + return Err(format!( + "FloorFinalization.require_materialization_disclosure must be a Bool, \ + got {other:?}" + )) + } + }; + Ok(Some(FloorFinalization { + declared_resolve_count: declared, + require_materialization_disclosure: disclosure, + })) + } else { + Err(format!( + "WalkPlan.finalization: unknown variant (expected NoWalkFinalization or \ + FloorFinalization)" + )) + } + } + other => Err(format!( + "WalkPlan.finalization must be a WalkFinalization variant, got {}", + ctx.format_value(other) + )), + } +} + /// Strict WalkPlan parser. Deliberately NO fallback from a failed record parse to a /// bare-`List>` reading: that fallback would let a malformed plan run /// with its success stages silently dropped — the silent-widen arm §5 forbids. A plan @@ -607,9 +660,16 @@ fn walk_plan_from_plan(plan: &Value, ctx: &InterpContext) -> Result]) -> Vec refusals.push("floor materialization receipt missing - fail closed".to_string()), @@ -3980,33 +4044,10 @@ fn run() -> Result { return Err(ExitCode::from(1)); } } - // Floor finalization: the floor plan declares its resolve law in the plan closure - // (gunbc_ci_floor_declared_resolve_count, projecting the ci_materialization - // authority). Read fail-closed — a floor plan whose law cannot be read refuses the - // run rather than walking without its contract. Other plans declare none. - let floor_finalization: Option = if plan_function == "gunbc_ci_floor_plan" { - match run_value(&plan_ctx, "gunbc_ci_floor_declared_resolve_count") { - Ok(Value::Int(n)) => Some(FloorFinalization { - declared_resolve_count: n, - }), - Ok(other) => { - eprintln!( - "claim_executor: gunbc_ci_floor_declared_resolve_count must be an Int, got \ - {other:?} (fail-closed)" - ); - return Err(ExitCode::from(1)); - } - Err(msg) => { - eprintln!( - "claim_executor: gunbc_ci_floor_declared_resolve_count unavailable \ - (fail-closed): {msg}" - ); - return Err(ExitCode::from(1)); - } - } - } else { - None - }; + // Floor finalization is carried BY the plan value (walk_finalization_note) — the + // name-keyed read this replaces was the same seed-roster convention the carrier + // was built to remove, reintroduced and then caught in review. + let floor_finalization: Option = walk_plan.finalization; // Fast-lane 5s rule (operator 2026-07-12): a plan that schedules a discovery batch // must declare the per-witness eval budget; a missing/mistyped row refuses the run // (fail-closed), while discovery-free plans (regen, plan-artifact) never read it. @@ -4402,6 +4443,7 @@ mod tests { fn floor_finalization_refuses_on_resolve_count_mismatch_both_directions() { let fin = FloorFinalization { declared_resolve_count: 1, + require_materialization_disclosure: true, }; let over = [finalization_record(&[5, 7])]; let refusals = validate_floor_finalization(&fin, &over); @@ -4425,6 +4467,7 @@ mod tests { fn floor_finalization_matching_count_leaves_only_the_materialization_arm() { let fin = FloorFinalization { declared_resolve_count: 2, + require_materialization_disclosure: true, }; let records = [finalization_record(&[3, 0, 9])]; let refusals = validate_floor_finalization(&fin, &records); diff --git a/src/v1/stage0/src/std_realization_schedule.rs b/src/v1/stage0/src/std_realization_schedule.rs index 3c33365e3d8..9da34c4b70e 100644 --- a/src/v1/stage0/src/std_realization_schedule.rs +++ b/src/v1/stage0/src/std_realization_schedule.rs @@ -5,6 +5,7 @@ use self::CostBasis::*; use self::NodeFrontierSelection::*; use self::Runnable::*; use self::RunnableMemoryClass::*; +use self::WalkFinalization::*; use self::WitnessKind::*; use self::WitnessSpan::*; pub use crate::std_execution_mode::execution_mode_eq; @@ -308,6 +309,65 @@ pub enum Runnable { }, } +pub fn walk_plan_note() -> String { + thread_local! { + static CACHED: String = { + "A walk is TWO populations with DIFFERENT ordering laws, and the type says so where a bare List> could not. `batches` are the ordinary floor: batch boundaries order them, and a failure's consequence is the walk's FloorBatchStopPolicy (StopBeforeDependents on pull_request, FullLedger on push/schedule — where a failed batch deliberately does NOT stop the walk, because the per-batch ledger on main is bisection evidence, operator ruling 2026-07-23). `on_success_stages` run ONLY when the ordinary floor completed AND its receipts finalized and validated; each stage is a barrier — stage N fully completes with zero failures before stage N+1 starts — and stage-to-stage execution is ALWAYS fail-fast, regardless of the ordinary stop policy: FullLedger is an ordinary-floor policy and never applies between stages. WHY THE SECOND POPULATION EXISTS: work whose correctness is conditional on the whole floor being green (the merge-admission stamp is the first occupant — stamping Success is only true if reaching it proves green) cannot be an ordinary trailing batch, because under FullLedger a trailing batch is still reached after a red batch. The prior attempt encoded exactly that and shipped a fail-open; a second attempt then declared stamp-then-gate as one stage and rediscovered the SIBLING defect — members WITHIN a stage run concurrently (distinct entries become distinct spawned units), so intra-stage order does not exist and anything sequential must be ONE claim whose body sequences its steps, or two singleton stages. Both defects are why this is a named type with a note rather than a convention (operator design ruling 2026-07-30).\n\nWHAT THE EXECUTOR ACTUALLY PROVIDES TODAY, stated because a carrier that promises more than its executor delivers is the same defect this type exists to end (review 2026-07-30). The stage BARRIER is real: stage N completes before stage N+1 starts, and a failed stage prevents every later one. THREE GAPS remain, and none is currently reachable because every plan declares an empty stage list: (1) members within a stage run SERIALLY, not concurrently — serial is a strictly stronger order than the contract promises, so no current caller is misled, but wall time, peak memory, and overlap are resource facts a future author would measure wrongly; (2) stages execute through run_memo_shared_claims directly rather than the ordinary unit-lane partition, so they bypass governor admission, batch clamps, and resource-profile enforcement — safe for the negligible admission claims that will occupy them, unsafe for the substantial or host-compiler-spawning claim the generic carrier permits; (3) ONE aggregate stage receipt is written after the whole sequence rather than one per stage before the next begins. The repair is to extract the ordinary batch machinery into a reusable run_stage(stage) — group by entry and mode, acquire governor admission, spawn eligible units, join, write THAT stage receipt — so both populations share one executor and differ only in ordering and failure policy. Until that lands, do not place a Substantial or host-compiler runnable in a stage; the arm-time validator refuses discovery runnables outright, and the profile restriction is the next wall. Every plan function returns WalkPlan — a plan with no postconditions returns on_success_stages: [] — and the executor has ONE strict parser: a malformed or missing field is a hard error, never a fallback to a bare-list reading.".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + +pub fn walk_finalization_note() -> String { + thread_local! { + static CACHED: String = { + "THE PLAN CARRIES ITS FINALIZATION POLICY; the executor never infers it from a function's spelling. The first cut selected finalization by plan-function name (if plan_function == gunbc_ci_floor_plan read the law from the closure) — the same hidden seed-roster convention the WalkPlan carrier was built to remove, reintroduced in the same PR (review 2026-07-30), and the RED fixture made the coupling visible by having to impersonate the production name to arm the contract. As a FIELD, the policy is part of the parsed value: a floor plan without its law is now UNWRITABLE (the strict parser refuses a missing field) rather than checked at arm time, schedule lenses and plan artifacts can see it, and the fixture declares its own policy instead of name-impersonating into one. NoWalkFinalization is the declared empty policy — regen, falsifier, and the plan-artifact shortcut say so explicitly, never by omission.".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum WalkFinalization { + NoWalkFinalization, + FloorFinalization { + declared_resolve_count: i64, + require_materialization_disclosure: bool, + }, +} +impl WalkFinalization { + pub fn declared_resolve_count(&self) -> i64 { + match self { + WalkFinalization::NoWalkFinalization => { + panic!("no declared_resolve_count on unit variant") + } + WalkFinalization::FloorFinalization { + declared_resolve_count: __val, + .. + } => __val.clone(), + } + } + pub fn require_materialization_disclosure(&self) -> bool { + match self { + WalkFinalization::NoWalkFinalization => { + panic!("no require_materialization_disclosure on unit variant") + } + WalkFinalization::FloorFinalization { + require_materialization_disclosure: __val, + .. + } => __val.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct WalkPlan { + pub batches: Rc>>>>, + pub finalization: Rc, + pub on_success_stages: Rc>>>>, +} + pub fn node_frontier_selection_applied(sel: NodeFrontierSelection) -> bool { match sel.clone() { NodeFrontierSelection::SelectionApplied => true, diff --git a/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag b/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag index e8e43118b25..f1dc8b9a945 100644 --- a/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag +++ b/src/v2/test/fixture/floor_skip/budget_red_control_plan.dag @@ -4,6 +4,7 @@ import std.realization_schedule { Runnable, RunnableSingleClaim, WalkPlan, + FloorFinalization, runnable_resource_profile_negligible } import v2.std.collection { List } @@ -19,15 +20,15 @@ fn budget_red_control_claim() -> Runnable { } } -data budget_red_control_declared_resolve_note: String = "The fixture impersonates the floor plan BY NAME so claim_executor arms the derived clamp against it — and as of the in-executor floor finalization (ruling 2026-07-30) that name also arms the resolve law, so the fixture must declare it or the run refuses before walking (that refusal is itself the finalization read's executable RED, exercised before this row landed). Declared 1: the fixture runs exactly one single-claim batch, which pays exactly one entry resolve." - -fn gunbc_ci_floor_declared_resolve_count() -> Int { - 1 -} +data budget_red_control_declared_resolve_note: String = "The fixture DECLARES its finalization policy in its own plan value (declared 1: exactly one single-claim batch paying exactly one entry resolve) — it no longer needs a name-impersonated projection fn, because the policy is a WalkPlan FIELD (walk_finalization_note): the executor reads the parsed value, and a floor plan without its law is unwritable rather than discovered missing at arm time." fn gunbc_ci_floor_plan() -> WalkPlan { WalkPlan { batches: [[budget_red_control_claim()]], + finalization: FloorFinalization { + declared_resolve_count: 1, + require_materialization_disclosure: true, + }, on_success_stages: [], } } diff --git a/src/v2/workflow/ci_floor_plan.dag b/src/v2/workflow/ci_floor_plan.dag index acbf321da29..75eebbfd346 100644 --- a/src/v2/workflow/ci_floor_plan.dag +++ b/src/v2/workflow/ci_floor_plan.dag @@ -69,6 +69,9 @@ import std.realization_schedule { runnable_selection_applied, runnable_step_label, WalkPlan, + WalkFinalization, + NoWalkFinalization, + FloorFinalization, } import v2.lens.schedule_lens { schedule_lens_verdict_for_ci_floor, schedule_lens_batch_has_any_cheap_gate } import v2.std.lens_verdict { Holds, NotApplicable, Unrealized, Violation } @@ -1234,15 +1237,13 @@ fn gunbc_ci_plan_artifact_ordinary_batches() -> List> { data walk_plan_uniformity_note: String = "EVERY plan function returns WalkPlan, including the three with no postconditions — on_success_stages: [] is a declared empty set, not an omission the executor papers over. The executor has ONE strict parser for the record shape; there is deliberately NO fallback from a failed record parse to a bare-List> reading, because that fallback would let a malformed plan silently run with its success stages dropped — the exact silent-widen shape §5 forbids. The `_plan` names replace the `_batches` names in the same motion (operator ruling 2026-07-30): a function whose value now carries postcondition stages must not keep a name that says it returns only batches, or the next author reasonably assumes the stages are not part of the value. The four argv/step consumers follow automatically because they derive from the floor_plan_function / plan_artifact_plan_function / regen_floor_plan_function / falsifier_plan_function constants (gunbc.ci_spec, gunbc.falsifier_workflow) — those constants are the single naming authority, renamed with the functions." -data gunbc_ci_floor_declared_resolve_count_projection_note: String = "Projection of gunbc.ci_materialization.ci_floor_declared_resolve_count INTO THE PLAN CLOSURE, so claim_executor can read the resolve law from the same resolved plan context it already holds — the clamp-params pattern. The authority stays in ci_materialization (single authority, with its receipt history); this fn is a read, not a second declaration. Consumed by the executor's floor finalization (ruling 2026-07-30): the resolve-count law and the materialization disclosure law moved from two GitHub shell steps into run_walk's ordinary-floor finalization, so a receipt-law violation is an ORDINARY-FLOOR failure that blocks every on-success stage — the old step ordering could red AFTER admission had already stamped, which is the can-red-after-admission hole this closes. The two workflow steps are deleted in the same motion; keeping them would be a second representation of the floor-completion rule." - -fn gunbc_ci_floor_declared_resolve_count() -> Int { - ci_floor_declared_resolve_count -} - fn gunbc_ci_floor_plan() -> WalkPlan { WalkPlan { batches: gunbc_ci_floor_ordinary_batches(), + finalization: FloorFinalization { + declared_resolve_count: ci_floor_declared_resolve_count, + require_materialization_disclosure: true, + }, on_success_stages: [], } } @@ -1250,6 +1251,7 @@ fn gunbc_ci_floor_plan() -> WalkPlan { fn gunbc_ci_regen_floor_plan() -> WalkPlan { WalkPlan { batches: gunbc_ci_regen_floor_ordinary_batches(), + finalization: NoWalkFinalization, on_success_stages: [], } } @@ -1257,6 +1259,7 @@ fn gunbc_ci_regen_floor_plan() -> WalkPlan { fn gunbc_ci_plan_artifact_plan() -> WalkPlan { WalkPlan { batches: gunbc_ci_plan_artifact_ordinary_batches(), + finalization: NoWalkFinalization, on_success_stages: [], } } @@ -1264,6 +1267,7 @@ fn gunbc_ci_plan_artifact_plan() -> WalkPlan { fn gunbc_falsifier_plan() -> WalkPlan { WalkPlan { batches: gunbc_falsifier_ordinary_batches(), + finalization: NoWalkFinalization, on_success_stages: [], } }