diff --git a/.githooks/pre-push b/.githooks/pre-push index 539496e366d..968dbe2f67a 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -3,16 +3,8 @@ set -euo pipefail ROOT="$(git rev-parse --show-toplevel)" cd "$ROOT" -ensure_bins() { - if [[ -x "$ROOT/target/release/claim_batch" ]]; then - CLAIM_BATCH="$ROOT/target/release/claim_batch" - elif [[ -x "$ROOT/target/debug/claim_batch" ]]; then - CLAIM_BATCH="$ROOT/target/debug/claim_batch" - else - echo "[pre-push] claim_batch not built; compiling debug (one-time)..." - (cd "$ROOT" && CTRL_BUILD_WRAP_CARGO=0 cargo build -p v1-compiler --bin claim_batch) - CLAIM_BATCH="$ROOT/target/debug/claim_batch" - fi -} -ensure_bins -exec "$CLAIM_BATCH" --pre-push +echo "[pre-push] cargo fmt --all --check" +if ! cargo fmt --all --check; then + echo "[pre-push] rustfmt drift — run: cargo fmt --all (then commit and re-push, or 'git push --no-verify' to skip)" >&2 + exit 1 +fi diff --git a/dag/gunbc/commit_workflow.dag b/dag/gunbc/commit_workflow.dag index aaf796d2184..8b16c75dfaa 100644 --- a/dag/gunbc/commit_workflow.dag +++ b/dag/gunbc/commit_workflow.dag @@ -53,6 +53,10 @@ data githooks_pre_push_artifact_drift_gate_removed_disposition: Disposition = Te 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)." } +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." +} + data commit_gate_rust_suite_removed_disposition: Disposition = Terminal { reason: "The v1 Rust test suite (cargo fmt --all --check plus cargo nextest run -p v1-compiler-tests) was REMOVED from CI 2026-07-11 (operator ruling) — its RustMonolithGate enrollment, the rust_tests job, and rust_gates_ci.dag's CI consumers are all gone. This records the removal as intentional, not silent (the review-flagged concern). Rationale: rust_tests was NON-required (only the ci job is protection-required on the gunbc main ruleset; rust_tests and ci_regen were not) AND red on main (it exercises the v1 seed, which is being deleted under the §7 burn-down), so it delivered a permanent non-required red — the §5 tolerated-red anti-pattern — at ~37 GiB, the fleet's single largest CI memory job. AMENDED 2026-07-15 (operator ruling, the falsifier lane): the fmt HALF of this removal is REVERSED — cargo fmt --all --check is re-enrolled as a standalone build-job step (gunbc.ci_spec ci_fmt_gate_note / ci_fmt_gate_line). Every rationale above is a fact about NEXTEST (37 GiB, red, seed-runtime); fmt is 4.3s measured, parse-only, needs no build, and is green — it was collateral damage of a §3 fusion, since tools.rust_gates_ci.run_gates welds both into one ProcessExit, so killing the bundle killed the cheap green half. This amendment does NOT re-enroll RustMonolithGate (still REJECTED below) and does not return nextest to CI. The former claim 'cargo fmt stays enforced by the pre-push hook (CommitCargoFmtCheck on GitPrePushHook)' is RETRACTED as the enforcement authority: a hook is an escape hatch (§5) — opt-in per clone via a manual core.hooksPath config, bypassable with --no-verify, and absent in container worktrees — and it is PROVEN ineffective, not merely theoretically weak: #6658 landed an unformatted .rs on main 2026-07-15 and no gate caught it. The hook remains as fast local feedback, never as the wall. Coverage now: cargo fmt = the required CI path (ci needs:[build], so a red fmt step blocks the required job by construction); cargo nextest run -p v1-compiler-tests still runs LOCALLY, not in CI. Return trigger for NEXTEST — NONE by design: that suite does not re-enroll. It is retired WITH the v1 seed and deleted at §7 terminal collapse. Re-enrolling RustMonolithGate onto the required ci floor is explicitly REJECTED — it would move the ~37 GiB red job into the required merge gate. Compile-level v1 seed breakage is still caught by the required floor's DagCompileCleanGate and RegenVerifyGate; only the seed's runtime test behavior moves local." } @@ -238,15 +242,6 @@ data commit_gate_roster: List = [ }, surfaces: [GithubActionsCiJob] }, - CommitCheckEnrollment { - check: CommitWitnessClaim { - entry: doc_reachability_witness_entry, - check_fns: ["doc_graph_has_no_orphan_docs", "doc_graph_has_no_dangling_links"], - kind: CorpusWitnessKind, - span: SpanUndeclared - }, - surfaces: [GitPrePushHook] - }, CommitCheckEnrollment { check: CommitWitnessClaim { entry: "src/v2/test/claim/emitted_binary_effect_confinement_red_control_test.dag", diff --git a/dag/gunbc/githooks_pre_commit_emit.dag b/dag/gunbc/githooks_pre_commit_emit.dag index 77ec1fa5612..dfc26684eb2 100644 --- a/dag/gunbc/githooks_pre_commit_emit.dag +++ b/dag/gunbc/githooks_pre_commit_emit.dag @@ -1,9 +1,21 @@ module gunbc.githooks_pre_commit_emit -import gunbc.githooks_pre_push_emit { emit_freshness_path_fn } import gunbc.local_tidy_spec { local_tidy_trigger_globs, LocalTidyCargoFmtCheck } import extdeps.git { git_toplevel_shell_subexpr } +fn emit_glob_case_arms(globs: List) -> String { + fold(globs, init: "", f: fn(acc, g) { + concat(acc, concat("\n ", concat(g, "*) return 0 ;;"))) + }) +} + +fn emit_freshness_path_fn(label: String, globs: List) -> String { + concat( + concat(label, "_path() {\n case \"$1\" in"), + concat(emit_glob_case_arms(globs: globs), "\n esac\n return 1\n}\n") + ) +} + data githooks_pre_commit_note: String = "Commit-time fmt gate (operator request 2026-07-12: add fmt to the commit hook). The staged path set is matched against the same LocalTidyCargoFmtCheck trigger globs the pre-push hook and the CI rust gate consume — one fmt-relevance authority, three surfaces. Both commit-time and push-time hooks are CHECK-ONLY: on drift they refuse with the fix recipe instead of mutating the index or auto-committing (git add -u under a partially-staged tree would sweep unstaged edits into the commit — a silent widen; push-time auto-commit was retired in #6706 non-invasive gates, and #6720 carries the same check-only policy into the typed plan/Rust transport). dissolve-on: shell emission via the grammar path (the bash medium modeled as GrammarRelationRow rows read forward, superseding these concat-built shell string literals — same trigger as the githooks_pre_push_emit sibling; the medium-as-string debt is tracked once for the hook-emit family, here explicitly rather than implicitly via the sibling roster). Follow-on (not modeled here): a GitPreCommitHook CommitWorkflowSurface row binding this artifact into commit_workflow_realization_bindings." fn emit_pre_commit_staged_scan() -> String { diff --git a/dag/gunbc/githooks_pre_push_cli.dag b/dag/gunbc/githooks_pre_push_cli.dag index 39fee92d517..c76df394a27 100644 --- a/dag/gunbc/githooks_pre_push_cli.dag +++ b/dag/gunbc/githooks_pre_push_cli.dag @@ -5,5 +5,5 @@ import gunbc.githooks_pre_push_plan { pre_push_active_kinds, pre_push_source_roo import extdeps.git.hooks { PrePushStdinRow, pre_push_zero_sha, pre_push_stdin_field_names } data githooks_pre_push_cli_stage0_receiver_disposition: Disposition = Terminal { - reason: "stage0's `claim_batch --pre-push` argv dispatch (src/v1/stage0/src/pre_push.rs via cli_run::handle_pre_push) is the CLI transport handler for this interface (DESIGN §3 transport-is-a-handler): stdin parse and git diff collection are transport; gate policy is the typed plan in gunbc.githooks_pre_push_plan (pre_push_active_kinds derived from commit_gate_roster + local_tidy_trigger_globs + witness-corpus binding from #6706); the binary interprets the returned PrePushGateKind rows and emits typed receipts — no opaque imperative policy fork. The emitted `.githooks/pre-push` is a thin shim (`ensure_bins` + `exec claim_batch --pre-push`)." + reason: "stage0's `claim_batch --pre-push` argv dispatch (src/v1/stage0/src/pre_push.rs via cli_run::handle_pre_push) is the CLI transport handler for this interface (DESIGN §3 transport-is-a-handler): stdin parse and git diff collection are transport; gate policy is the typed plan in gunbc.githooks_pre_push_plan (pre_push_active_kinds derived from commit_gate_roster + local_tidy_trigger_globs + witness-corpus binding from #6706); the binary interprets the returned PrePushGateKind rows and emits typed receipts — no opaque imperative policy fork. NO LONGER HOOK-INVOKED as of 2026-07-25 (operator ruling, gunbc.commit_workflow githooks_pre_push_slimmed_to_fmt_disposition): the emitted `.githooks/pre-push` is fmt-only and does not reference claim_batch at all, so this transport is now an ON-DEMAND LOCAL RUNNER — `claim_batch --pre-push` still works for a developer who wants the affected-set corpus locally, it is simply not on the push path. It is deliberately NOT deleted in that change: src/v1/stage0/src/pre_push.rs is a row in gunbc.stage0_crate_layout_generated, so removing it cascades into the emitted-crate-partition machinery (DESIGN open thread) rather than being a hook-scoped edit. 🟡 dissolve-on: delete this module, gunbc.githooks_pre_push_plan, pre_push.rs and the `--pre-push` argv row together when the emitted crate partition lands — or sooner if the on-demand runner is found to have no users, which is a census question, not a hook question." } diff --git a/dag/gunbc/githooks_pre_push_emit.dag b/dag/gunbc/githooks_pre_push_emit.dag index 605d50ba6e1..281c185660f 100644 --- a/dag/gunbc/githooks_pre_push_emit.dag +++ b/dag/gunbc/githooks_pre_push_emit.dag @@ -1,24 +1,34 @@ module gunbc.githooks_pre_push_emit import extdeps.git { git_toplevel_shell_subexpr } - -fn emit_glob_case_arms(globs: List) -> String { - fold(globs, init: "", f: fn(acc, g) { - concat(acc, concat("\n ", concat(g, "*) return 0 ;;"))) - }) +import gunbc.commit_workflow { + commit_gate_roster, + project_local_tidy_checks, + LocalTidyCheck, + LocalTidyWitnessCheck, + LocalTidyCargoFmtCheck, } -fn emit_freshness_path_fn(label: String, globs: List) -> String { - concat( - concat(label, "_path() {\n case \"$1\" in"), - concat(emit_glob_case_arms(globs: globs), "\n esac\n return 1\n}\n") - ) +data pre_push_fmt_fail_recipe: String = "rustfmt drift — run: cargo fmt --all (then commit and re-push, or 'git push --no-verify' to skip)" + +data pre_push_fmt_only_note: String = "The pre-push hook is fmt-only, restored to CLAUDE.md's documented contract ('pre-push runs cargo fmt') by operator ruling 2026-07-25. It was previously a shim that exec'd `claim_batch --pre-push`, which ran the affected-set witness corpus and would cargo-build claim_batch from inside the push when the binary was missing. That weight bought ZERO enforcement: a git hook is opt-in per clone (core.hooksPath), bypassable with --no-verify, and absent in container worktrees, and gunbc.commit_workflow's own commit_gate_rust_suite_removed_disposition already records it PROVEN ineffective (#6658 landed an unformatted .rs on main and no hook caught it). That is the worst of both worlds by construction — no enforcement, but enough weight to make a legitimately-red mid-lane branch feel unpushable, which is the pressure that converts honest reds into workaround greens (priced receipt: three rounds of hand-import workarounds and one decorative gate on the namespace type-binding lane, 2026-07-25). A check that cannot enforce should at least be instant. CI loses nothing: cargo fmt is a required-path build step (gunbc.ci_spec ci_fmt_gate_note) and the witness corpus is the required `ci` floor, which also stamps the merge-admission receipt. The hook body is DERIVED from commit_gate_roster's GitPrePushHook enrollments via project_local_tidy_checks, so the roster stays the single authority (DESIGN section 3) — re-heavying the hook requires a roster edit that githooks_pre_push_emit_test catches, never a silent drift." + +fn emit_local_tidy_line(check: LocalTidyCheck) -> String { + match check { + LocalTidyCargoFmtCheck => + concat( + "echo \"[pre-push] cargo fmt --all --check\"\nif ! cargo fmt --all --check; then\n echo \"[pre-push] ", + concat(pre_push_fmt_fail_recipe, "\" >&2\n exit 1\nfi\n") + ) + LocalTidyWitnessCheck { entry: _, check_fns: _ } => "" + } } -fn emit_ensure_bins() -> String { - concat( - "ensure_bins() {\n if [[ -x \"$ROOT/target/release/claim_batch\" ]]; then\n CLAIM_BATCH=\"$ROOT/target/release/claim_batch\"\n elif [[ -x \"$ROOT/target/debug/claim_batch\" ]]; then\n CLAIM_BATCH=\"$ROOT/target/debug/claim_batch\"\n else\n echo \"[pre-push] claim_batch not built; compiling debug (one-time)...\"\n (cd \"$ROOT\" && CTRL_BUILD_WRAP_CARGO=0 cargo build -p v1-compiler --bin claim_batch)\n CLAIM_BATCH=\"$ROOT/target/debug/claim_batch\"\n fi\n}\n", - "ensure_bins\nexec \"$CLAIM_BATCH\" --pre-push\n" +fn emit_pre_push_body() -> String { + fold( + project_local_tidy_checks(roster: commit_gate_roster), + init: "", + f: fn(acc, check) { concat(acc, emit_local_tidy_line(check: check)) } ) } @@ -26,6 +36,6 @@ fn expected_githooks_pre_push_sh() -> String { concat( "#!/usr/bin/env bash\n# GENERATED by dag/gunbc/githooks_pre_push_emit.dag — DO NOT HAND-EDIT.\n", "set -euo pipefail\nROOT=\"", concat(git_toplevel_shell_subexpr(), "\"\ncd \"$ROOT\"\n"), - emit_ensure_bins() + emit_pre_push_body() ) } diff --git a/dag/test/claim/githooks_pre_push_emit_test.dag b/dag/test/claim/githooks_pre_push_emit_test.dag index 35d2efd65e1..606cd9e29cb 100644 --- a/dag/test/claim/githooks_pre_push_emit_test.dag +++ b/dag/test/claim/githooks_pre_push_emit_test.dag @@ -15,39 +15,54 @@ fn witness_generated_header() -> Bool { && has(s: sh, p: "set -euo pipefail") } -fn witness_thin_shim_exec() -> Bool { +fn witness_fmt_is_the_body() -> Bool { let sh = expected_githooks_pre_push_sh() - has(s: sh, p: "ensure_bins") - && has(s: sh, p: "exec \"$CLAIM_BATCH\" --pre-push") - && !has(s: sh, p: "while IFS=' ' read -r") - && !has(s: sh, p: "declare -a PUSH_CHANGED") - && !has(s: sh, p: "fresh_doc_path") + has(s: sh, p: "cargo fmt --all --check") + && has(s: sh, p: "rustfmt drift") + && has(s: sh, p: "exit 1") } -fn witness_claim_batch_wired() -> Bool { +fn witness_no_claim_batch_dependency() -> Bool { let sh = expected_githooks_pre_push_sh() - has(s: sh, p: "claim_batch") && has(s: sh, p: "--pre-push") + !has(s: sh, p: "claim_batch") + && !has(s: sh, p: "--pre-push") + && !has(s: sh, p: "ensure_bins") + && !has(s: sh, p: "CLAIM_BATCH") } -fn witness_no_inline_gate_logic() -> Bool { +fn witness_no_in_hook_build() -> Bool { let sh = expected_githooks_pre_push_sh() - !has(s: sh, p: "run_generated_artifact_drift_gate_body") - && !has(s: sh, p: "cargo fmt --all --check") + !has(s: sh, p: "cargo build") + && !has(s: sh, p: "CTRL_BUILD_WRAP_CARGO") +} + +fn witness_no_corpus_or_witness_run() -> Bool { + let sh = expected_githooks_pre_push_sh() + !has(s: sh, p: "gunbc_ci_witness_corpus_only_batches") && !has(s: sh, p: "doc_graph_has_no_orphan_docs") - && !has(s: sh, p: "gunbc_ci_witness_corpus_only_batches") - && !has(s: sh, p: "chore: apply cargo fmt") + && !has(s: sh, p: "--claim-run") + && !has(s: sh, p: "run_generated_artifact_drift_gate_body") +} + +fn witness_no_tree_mutation() -> Bool { + let sh = expected_githooks_pre_push_sh() + !has(s: sh, p: "chore: apply cargo fmt") && !has(s: sh, p: "chore: regenerate committed projections") + && !has(s: sh, p: "git add") + && !has(s: sh, p: "git commit") } fn witness_line_budget() -> Bool { let sh = expected_githooks_pre_push_sh() - length(sh) < 2000 + length(sh) < 700 } test fn githooks_pre_push_emit_witnesses() -> Bool { witness_generated_header() - && witness_thin_shim_exec() - && witness_claim_batch_wired() - && witness_no_inline_gate_logic() + && witness_fmt_is_the_body() + && witness_no_claim_batch_dependency() + && witness_no_in_hook_build() + && witness_no_corpus_or_witness_run() + && witness_no_tree_mutation() && witness_line_budget() }