Skip to main content

max / audiofiles

Refresh the canonical hooks: gate pushes on internal-dep coherence pre-push grew a first gate that reads the tree's manifests and refuses a push leaving an in-house `version` requirement that cannot resolve against the crate it names. It is installed in every repo now, not only those with a root workspace: MNW has no root manifest and is the repo that spent a day unable to resolve quasi. Master: _private/infra/bootstrap/githooks.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-16 17:22 UTC
Signed with PGP, not checked
Commit: 4481d97a51dbb181ab283f20ff70eb946ab7972d
Parent: 0e80b19
2 files changed, +326 insertions, -8 deletions
@@ -1,10 +1,30 @@
1 1 #!/bin/bash
2 - # Canonical pre-push gate: blocks a push whose test targets do not build.
2 + # Canonical pre-push gate. Two gates, and they answer different questions:
3 + #
4 + # internal deps does every in-house `version` requirement in the tree still
5 + # resolve against the crate it names? Runs in EVERY repo.
6 + # test targets do this repo's test targets build? Runs where there is a
7 + # root Cargo.toml to run one command in.
3 8 #
4 9 # DO NOT EDIT IN PLACE. The master is _private/infra/bootstrap/githooks/pre-push.
5 10 #
6 11 # Bypass for a work-in-progress push: git push --no-verify
7 12 #
13 + # WHY THE FIRST GATE EXISTS. Every cross-repo dependency carries a `version`
14 + # alongside its `git` URL, so cargo refuses a sibling it was not written against
15 + # instead of compiling something surprising. That requirement is the protection
16 + # and it is also the maintenance: bumping a library's minor breaks every consumer
17 + # whose requirement excludes it, and CLAUDE.md's rule is that the bump and the
18 + # forward fix are one pass. Nothing enforced the rule, so quasi went 0.11 -> 0.14
19 + # over two evenings and MNW's server could not resolve at all for a day. The
20 + # nightly sweep found it and a red cell in a grid is not the same as being told.
21 + #
22 + # This gate is that rule, mechanised, at the moment it is broken: the push that
23 + # would leave a consumer unable to build is the push that is refused. It reads
24 + # the WORKING COPIES in the tree, not the remotes, because `~/Code/.cargo/config.toml`
25 + # redirects every one of these dependencies to the working copy -- so a local bump
26 + # breaks a consumer's build here whether or not it has been pushed anywhere.
27 + #
8 28 # `cargo check` and `cargo clippy` both compile only the lib and bin targets, so a
9 29 # break confined to `tests/` or a `#[cfg(test)]` module is clean under both and
10 30 # lands unnoticed (goingson's sqlx 0.9 upgrade shipped exactly that way).
@@ -15,10 +35,6 @@
15 35 # `--workspace` is load-bearing wherever default-members is narrower than the
16 36 # workspace: goingson's is src-tauri alone, so a bare `cargo test --no-run` would
17 37 # skip core, db-sqlite, go-mcp and got.
18 - #
19 - # Only installed in repos with a root Cargo.toml. MNW and synckit have none by
20 - # design (standalone crates, no root workspace), so there is no one command to run
21 - # and they get the pre-commit gates only.
22 38 set -euo pipefail
23 39
24 40 ROOT="$(git rev-parse --show-toplevel)"
@@ -28,10 +44,9 @@
28 44 # get the profile's PATH, and a hook that cannot find cargo is worse than none.
29 45 export PATH="$HOME/.cargo/bin:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
30 46
31 - [ -f "$ROOT/Cargo.toml" ] || exit 0
32 -
33 47 # Refs arrive on stdin as "<local ref> <local sha> <remote ref> <remote sha>".
34 - # A branch deletion has an all-zero local sha and no tree to build.
48 + # A branch deletion has an all-zero local sha and no tree to push. Read once,
49 + # ahead of both gates: stdin is not seekable and a second reader gets nothing.
35 50 pushing=0
36 51 while read -r _local_ref local_sha _remote_ref _remote_sha; do
37 52 case "$local_sha" in
@@ -40,6 +55,25 @@
40 55 done
41 56 [ "$pushing" -eq 1 ] || exit 0
42 57
58 + # ── gate 1: internal dependency coherence ──────────────────────────────────
59 + #
60 + # Deliberately no cargo: this reads manifests and answers in well under a second,
61 + # where `cargo metadata` on the server is tens of seconds and fails outright on
62 + # exactly the state being detected.
63 + CODE_ROOT="${CODE_ROOT:-$HOME/Code}"
64 + if [ -d "$CODE_ROOT" ] && command -v python3 >/dev/null 2>&1; then
65 + if ! python3 "$ROOT/scripts/githooks/internal-deps.py" "$CODE_ROOT" "$ROOT"; then
66 + echo "pre-push: push aborted (use --no-verify to bypass)."
67 + exit 1
68 + fi
69 + fi
70 +
71 + # ── gate 2: test targets build ─────────────────────────────────────────────
72 + #
73 + # MNW and synckit have no root Cargo.toml by design (standalone crates, no root
74 + # workspace), so there is no one command to run and they get gate 1 only.
75 + [ -f "$ROOT/Cargo.toml" ] || exit 0
76 +
43 77 echo "pre-push: building test targets (cargo test --no-run --workspace)..."
44 78 if ! cargo test --no-run --workspace; then
45 79 echo "pre-push: test targets failed to build."
@@ -1,0 +1,284 @@
1 + #!/usr/bin/env python3
2 + """Do the tree's in-house `version` requirements still resolve?
3 +
4 + DO NOT EDIT IN PLACE. The master is _private/infra/bootstrap/githooks/internal-deps.py.
5 +
6 + Usage:
7 + internal-deps.py <tree-root> [repo-root]
8 +
9 + With a repo root, only pairs that repo is on either side of can fail the run;
10 + everything else is reported as a note. Without one, every pair is graded, which
11 + is the whole-tree report:
12 +
13 + python3 internal-deps.py ~/Code
14 +
15 + WHAT IT GRADES. Every dependency in the tree that carries both a `git` URL on one
16 + of our forges and a `version` requirement, against the version in the working
17 + copy of the crate that URL names. That is the pairing cargo enforces and the one
18 + that broke: a requirement of "0.11" against a sibling that has moved to 0.14 is
19 + not a warning, it is a graph that will not resolve on any machine.
20 +
21 + WHY WORKING COPIES AND NOT REMOTES. `~/Code/.cargo/config.toml` patches every one
22 + of these dependencies to the working copy in the tree, so what is on disk here is
23 + what every local build reads. A bump that has not been pushed yet breaks its
24 + consumers just as thoroughly, and finding that out at push time is the point.
25 +
26 + WHAT IT DOES NOT GRADE, on purpose:
27 +
28 + crates.io deps the makeover suite and friends resolve from the registry,
29 + where working ahead of a release is normal and a tree
30 + version above the published one is not a finding. The
31 + sweep's `coherence` check grades those against the index.
32 + path deps no version requirement to be wrong about.
33 + ranges and wildcards `>=`, `<`, `*` and comma lists are deliberate statements
34 + about a span, not a pin that drifts. Counted as unchecked.
35 + """
36 +
37 + import os
38 + import re
39 + import sys
40 + import tomllib
41 +
42 + # The forges that make a git URL ours. A dependency on somebody else's git repo
43 + # is not something this tree can forward-fix.
44 + OURS = re.compile(r"(makenot\.work|git\.sr\.ht/~maxmj)", re.I)
45 +
46 + # Directories that hold code we do not grade: retired, staged for deletion, or
47 + # not ours. Mirrors the sweep's exclusions rather than inventing a second list.
48 + SKIP_DIRS = {
49 + "target", ".git", "node_modules", "dist", "vendor",
50 + "_archive", "_scratch", "trash", "_meta", "vtebench",
51 + }
52 + MAX_DEPTH = 4
53 +
54 + DEP_SECTIONS = ("dependencies", "dev-dependencies", "build-dependencies")
55 +
56 +
57 + def manifests(root):
58 + """Every Cargo.toml in the tree, shallow-walked."""
59 + out = []
60 + stack = [(root, 0)]
61 + while stack:
62 + d, depth = stack.pop()
63 + try:
64 + entries = list(os.scandir(d))
65 + except OSError:
66 + continue
67 + for e in entries:
68 + if e.is_file() and e.name == "Cargo.toml":
69 + out.append(e.path)
70 + elif e.is_dir() and e.name not in SKIP_DIRS and depth < MAX_DEPTH:
71 + stack.append((e.path, depth + 1))
72 + return out
73 +
74 +
75 + def load(path):
76 + try:
77 + with open(path, "rb") as fh:
78 + return tomllib.load(fh)
79 + except (OSError, tomllib.TOMLDecodeError):
80 + return None
81 +
82 +
83 + def dep_tables(doc):
84 + """Every dependency table in a manifest, including per-target and workspace."""
85 + for section in DEP_SECTIONS:
86 + table = doc.get(section)
87 + if isinstance(table, dict):
88 + yield table
89 + for cfg in (doc.get("target") or {}).values():
90 + if not isinstance(cfg, dict):
91 + continue
92 + for section in DEP_SECTIONS:
93 + table = cfg.get(section)
94 + if isinstance(table, dict):
95 + yield table
96 + ws = doc.get("workspace") or {}
97 + table = ws.get("dependencies")
98 + if isinstance(table, dict):
99 + yield table
100 +
101 +
102 + def parse_version(v):
103 + """A version as a 3-tuple, prerelease dropped. Junk sorts as (0, 0, 0)."""
104 + core = str(v).split("+")[0].split("-")[0]
105 + parts = []
106 + for piece in core.split(".")[:3]:
107 + try:
108 + parts.append(int(piece))
109 + except ValueError:
110 + parts.append(0)
111 + while len(parts) < 3:
112 + parts.append(0)
113 + return tuple(parts)
114 +
115 +
116 + def satisfies(req, version):
117 + """Cargo's default (caret) requirement semantics. None means 'not graded'.
118 +
119 + The rule that matters here is the 0.x one: under 0.1.0 and above, the MINOR
120 + is the compatibility boundary, which is why a 0.11 requirement rejects 0.14
121 + outright rather than treating it as a newer patch.
122 + """
123 + req = req.strip()
124 + if not req or any(c in req for c in "<>*,~"):
125 + return None
126 + # A prerelease satisfies nothing that does not ask for a prerelease of the
127 + # same version, so a plain requirement rejects it. This is the shape the
128 + # maturity ladder produces at beta entry: a sibling at 1.0.0-beta.1 does not
129 + # resolve for a consumer requiring "1.0", and cargo says so.
130 + if "-" in str(version).split("+")[0] and "-" not in req:
131 + return False
132 + exact = req.startswith("=")
133 + req = req.lstrip("^=").strip()
134 + if not req:
135 + return None
136 + given = req.split(".")
137 + try:
138 + r = [int(p) for p in given[:3]]
139 + except ValueError:
140 + return None
141 + v = parse_version(version)
142 + if exact:
143 + return tuple(v[: len(r)]) == tuple(r)
144 + if r[0] > 0:
145 + return v[0] == r[0] and v[1:] >= tuple(r[1:] + [0] * (2 - len(r[1:])))
146 + if len(r) == 1:
147 + return v[0] == 0
148 + if r[1] > 0:
149 + return v[0] == 0 and v[1] == r[1] and v[2] >= (r[2] if len(r) > 2 else 0)
150 + # 0.0.x: every patch is its own compatibility island.
151 + if len(r) > 2:
152 + return v[:3] == (0, 0, r[2])
153 + return v[0] == 0 and v[1] == 0
154 +
155 +
156 + def main():
157 + if len(sys.argv) < 2:
158 + print(__doc__.strip(), file=sys.stderr)
159 + return 2
160 + tree = os.path.realpath(sys.argv[1])
161 + repo = os.path.realpath(sys.argv[2]) if len(sys.argv) > 2 else None
162 +
163 + paths = manifests(tree)
164 + docs = {p: load(p) for p in paths}
165 +
166 + # Workspace versions first: a member saying `version.workspace = true` gets
167 + # its number from the root, and reporting it as 0.0.0 would be a false break.
168 + ws_version = {}
169 + for p, doc in docs.items():
170 + if not doc:
171 + continue
172 + v = ((doc.get("workspace") or {}).get("package") or {}).get("version")
173 + if isinstance(v, str):
174 + ws_version[os.path.dirname(p)] = v
175 +
176 + def resolve_version(manifest_path, pkg):
177 + v = pkg.get("version")
178 + if isinstance(v, str):
179 + return v
180 + d = os.path.dirname(manifest_path)
181 + while d.startswith(tree):
182 + if d in ws_version:
183 + return ws_version[d]
184 + d = os.path.dirname(d)
185 + return None
186 +
187 + # crate name -> (version, manifest path)
188 + versions = {}
189 + for p, doc in docs.items():
190 + if not doc:
191 + continue
192 + pkg = doc.get("package")
193 + if not isinstance(pkg, dict) or not isinstance(pkg.get("name"), str):
194 + continue
195 + v = resolve_version(p, pkg)
196 + if v:
197 + versions[pkg["name"]] = (v, p)
198 +
199 + broken, unchecked, absent, graded = [], 0, set(), 0
200 + for p, doc in docs.items():
201 + if not doc:
202 + continue
203 + for table in dep_tables(doc):
204 + for key, spec in table.items():
205 + if not isinstance(spec, dict):
206 + continue
207 + git = spec.get("git")
208 + req = spec.get("version")
209 + if not isinstance(git, str) or not isinstance(req, str):
210 + continue
211 + if not OURS.search(git):
212 + continue
213 + name = spec.get("package") if isinstance(spec.get("package"), str) else key
214 + known = versions.get(name)
215 + if known is None:
216 + # A repo that is not on this machine (ripgrow lives on mbp
217 + # only). Not a finding: nothing here can be wrong about it.
218 + absent.add(name)
219 + continue
220 + verdict = satisfies(req, known[0])
221 + if verdict is None:
222 + unchecked += 1
223 + continue
224 + graded += 1
225 + if not verdict:
226 + broken.append((p, name, req, known[0], known[1]))
227 +
228 + if not broken:
229 + print(
230 + f"pre-push: internal deps coherent ({graded} requirements"
231 + + (f", {unchecked} unchecked" if unchecked else "")
232 + + (f", {len(absent)} crates not in this tree" if absent else "")
233 + + ")."
234 + )
235 + return 0
236 +
237 + def rel(path):
238 + return os.path.relpath(path, tree)
239 +
240 + ours, theirs = [], []
241 + for item in broken:
242 + consumer_manifest, name, req, have, provider_manifest = item
243 + mine = repo is not None and (
244 + consumer_manifest.startswith(repo + os.sep)
245 + or provider_manifest.startswith(repo + os.sep)
246 + )
247 + (ours if mine else theirs).append(item)
248 +
249 + for consumer_manifest, name, req, have, provider_manifest in ours + theirs:
250 + print(
251 + f" {rel(consumer_manifest)}: requires {name} \"{req}\", "
252 + f"the tree has {have} ({rel(provider_manifest)})",
253 + file=sys.stderr,
254 + )
255 +
256 + if repo is None:
257 + print(f"internal deps: {len(broken)} unresolvable requirements.", file=sys.stderr)
258 + return 1
259 +
260 + if not ours:
261 + # Somebody else's skew. Worth seeing, never worth blocking this push on:
262 + # a gate that fails for a reason the pusher cannot fix is a gate that
263 + # gets bypassed by reflex, and then it is not a gate.
264 + print(
265 + f"pre-push: {len(theirs)} unresolvable requirements elsewhere in the "
266 + "tree (listed above, not this push's).",
267 + )
268 + return 0
269 +
270 + print("", file=sys.stderr)
271 + print(
272 + "pre-push: this push leaves a dependency that cannot resolve.\n"
273 + " A version requirement states which major a consumer was written against,\n"
274 + " so bumping a library and fixing its consumers is one pass (CLAUDE.md,\n"
275 + " \"a breaking bump of an in-house crate is forward-fixed, in the same pass\").\n"
276 + " Fix: bump the requirement in the manifests above, make the consumers\n"
277 + " compile, and push them with this one.",
278 + file=sys.stderr,
279 + )
280 + return 1
281 +
282 +
283 + if __name__ == "__main__":
284 + sys.exit(main())