Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 5 additions & 13 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 4 additions & 9 deletions dag/gunbc/commit_workflow.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
Expand Down Expand Up @@ -238,15 +242,6 @@ data commit_gate_roster: List<CommitCheckEnrollment> = [
},
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",
Expand Down
14 changes: 13 additions & 1 deletion dag/gunbc/githooks_pre_commit_emit.dag
Original file line number Diff line number Diff line change
@@ -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>) -> 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>) -> 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 {
Expand Down
2 changes: 1 addition & 1 deletion dag/gunbc/githooks_pre_push_cli.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
40 changes: 25 additions & 15 deletions dag/gunbc/githooks_pre_push_emit.dag
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
module gunbc.githooks_pre_push_emit

import extdeps.git { git_toplevel_shell_subexpr }

fn emit_glob_case_arms(globs: List<String>) -> 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>) -> 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)) }
)
}

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()
)
}
49 changes: 32 additions & 17 deletions dag/test/claim/githooks_pre_push_emit_test.dag
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Loading