Bash · 5111 bytes Raw Blame History
1 #!/usr/bin/env bash
2 # Pre-push gate — run the cheap checks CI will otherwise catch later.
3 #
4 # Mirrors the three green-required jobs of the `CI` workflow:
5 # 1. ruff check
6 # 2. mypy --strict
7 # 3. pytest on the unit suite (no slow marker)
8 #
9 # Plus a best-effort grep for two patterns that CI has historically
10 # failed on but unit tests don't catch:
11 # - slow tests asserting `plan is not None` (CPU-only CI has no plan)
12 # - tests pinning `dlm_version == N` for a stale N (schema bumps break these)
13 #
14 # Usage:
15 # ./scripts/pregate.sh # runs the full gate
16 # ./scripts/pregate.sh --fast # skip mypy + pytest, only ruff + pattern checks
17 # ./scripts/pregate.sh --coverage # also mirror the Ubuntu package coverage gates
18 #
19 # Wire as a git hook with:
20 # ln -s ../../scripts/pregate.sh .git/hooks/pre-push
21
22 set -euo pipefail
23
24 cd "$(git rev-parse --show-toplevel)"
25 export UV_CACHE_DIR="${UV_CACHE_DIR:-.uv-cache}"
26
27 fast=0
28 coverage=0
29 while [[ $# -gt 0 ]]; do
30 case "$1" in
31 --fast)
32 fast=1
33 ;;
34 --coverage)
35 coverage=1
36 ;;
37 *)
38 echo "usage: ./scripts/pregate.sh [--fast] [--coverage]" >&2
39 exit 2
40 ;;
41 esac
42 shift
43 done
44
45 echo "==> ruff check"
46 uv run ruff check .
47
48 echo "==> ruff format (check)"
49 uv run ruff format --check . || {
50 echo " hint: run \`uv run ruff format .\` to fix"
51 exit 1
52 }
53
54 if [[ $fast -eq 0 ]]; then
55 echo "==> mypy --strict"
56 uv run mypy src
57
58 echo "==> pytest (unit; no slow marker)"
59 uv run pytest tests/unit -q --no-header
60 fi
61
62 if [[ $coverage -eq 1 ]]; then
63 echo "==> coverage gates (Ubuntu mirror)"
64 ./scripts/coverage-gates.sh
65 fi
66
67 # --- Pattern checks that mirror known CI foot-guns --------------------
68
69 echo "==> advisory: assert plan is not None in slow tests"
70 # Slow-marked tests that hard-assert the plan can fail on CPU-only CI
71 # unless they're upstream-guarded (e.g., `trained_store` fixture skips
72 # first). This is advisory: a match is worth a look, not necessarily
73 # a bug. Prefer `pytest.skip(...)` when the test body itself owns the
74 # doctor() call.
75 advisory_hits=$(git grep -n "assert plan is not None" -- 'tests/integration/**' 2>/dev/null || true)
76 if [[ -n "$advisory_hits" ]]; then
77 echo "$advisory_hits" | sed 's/^/ advisory: /'
78 echo " (advisory only — confirm each has an upstream plan guard)"
79 fi
80
81 echo "==> modality scatter outside dlm.modality"
82 # Sprint 38 B8.6: every `spec.modality == "..."` comparison lives in
83 # src/dlm/modality/. Callers elsewhere go through predicate flags
84 # (accepts_images, accepts_audio, requires_processor) or
85 # modality_for(spec).dispatch_export() / .load_processor(). New
86 # scatter means the modality abstraction is leaking again — refuse
87 # the push and route the new code into the dispatch package instead.
88 scatter=$(git grep -nE 'spec\.modality ==' -- 'src/dlm/**' 2>/dev/null | grep -v "src/dlm/modality/" || true)
89 if [[ -n "$scatter" ]]; then
90 echo "$scatter"
91 echo " modality scatter outside src/dlm/modality/ — route through modality_for(spec)."
92 exit 1
93 fi
94
95 echo "==> new sprint jargon in src/dlm"
96 # Sprint 39 M4: planning terms like `Sprint 23` or `audit-08` should
97 # not leak into newly added product/runtime strings under src/dlm.
98 # Compare the current tree against the upstream merge-base when one
99 # exists, so committed fixes in the working tree override older
100 # branch-local additions that have not been pushed yet.
101 collect_src_dlm_diff() {
102 local upstream
103 upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null || true)
104 if [[ -n "$upstream" ]]; then
105 local merge_base
106 merge_base=$(git merge-base "$upstream" HEAD 2>/dev/null || true)
107 if [[ -n "$merge_base" ]]; then
108 git diff --unified=0 --no-color "$merge_base" -- 'src/dlm/**' 2>/dev/null || true
109 return
110 fi
111 fi
112
113 git diff --unified=0 --no-color HEAD -- 'src/dlm/**' 2>/dev/null || true
114 }
115
116 jargon_hits=$(
117 collect_src_dlm_diff | awk '
118 /^diff --git / {
119 file = $4
120 sub("^b/", "", file)
121 next
122 }
123 /^\+\+\+ b\// {
124 file = substr($0, 7)
125 next
126 }
127 /^\+[^+]/ && ($0 ~ /Sprint [0-9]+/ || $0 ~ /audit-[0-9]+/) {
128 print file ":" substr($0, 2)
129 }
130 ' | sort -u
131 )
132 if [[ -n "$jargon_hits" ]]; then
133 echo "$jargon_hits"
134 echo " new Sprint/audit jargon leaked into src/dlm/ — translate it into product or operator language."
135 exit 1
136 fi
137
138 echo "==> stale dlm_version pin"
139 # Any test that hard-pins a frontmatter version exact-match should use
140 # >= so schema bumps don't retroactively break the test. Exact pins are
141 # fine in unit-test-of-migrator paths (compares against a literal).
142 stale=$(git grep -nE 'fm\.dlm_version ==|frontmatter\.dlm_version ==' -- 'tests/integration/**' 2>/dev/null || true)
143 if [[ -n "$stale" ]]; then
144 echo "$stale"
145 echo " integration tests pinning dlm_version exact — prefer >= so schema bumps don't break."
146 exit 1
147 fi
148
149 echo "==> gate clean"