-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprecommit.sh
More file actions
277 lines (254 loc) · 11.8 KB
/
precommit.sh
File metadata and controls
277 lines (254 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#!/bin/bash
# Pre-commit preflight — run this BEFORE `git commit` to fix everything in one pass.
# Usage: bash scripts/precommit.sh
#
# What it does:
# 1. Auto-formats staged files (ruff format)
# 2. Reports lint errors (ruff check)
# 3. Reports type errors (mypy)
# 4. Reports doc drift (test/command counts)
# 5. Reports vulture dead code
# 6. Re-stages auto-fixed files
#
# After this passes, `git commit` will succeed on first try.
set -e
STAGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$' || true)
STAGED_SH=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sh$' || true)
if [ -z "$STAGED_PY" ] && [ -z "$STAGED_SH" ]; then
echo "No Python or shell files staged."
exit 0
fi
ERRORS=0
# 0. Line-ending normalization for shell scripts.
# Windows editors write CRLF by default. .gitattributes specifies LF for .sh
# but that only applies at commit-write time; the working copy still has CRLF
# while shellcheck runs. Normalize staged .sh files to LF before any check
# sees them. This eliminates the "dos2unix then re-stage" dance.
if [ -n "$STAGED_SH" ]; then
echo "=== Normalize .sh line endings ==="
if command -v dos2unix &>/dev/null; then
echo "$STAGED_SH" | xargs dos2unix 2>&1 | grep -v "converting" || true
else
# Fallback: sed strips \r. Works even without dos2unix.
while IFS= read -r f; do
[ -f "$f" ] && sed -i 's/\r$//' "$f"
done <<< "$STAGED_SH"
fi
echo "$STAGED_SH" | xargs git add
echo " Normalized and re-staged."
fi
# 0c. U+FFFD scan. Any staged file containing the UTF-8 replacement-character
# byte sequence (EF BF BD) is silently corrupted — typically the result of
# writing non-ASCII via a Bash heredoc on Windows where bytes get re-encoded
# as cp1252. The corruption commits cleanly, tests pass, and the bug only
# surfaces months later when the file lands in a context that hits the JSON
# serializer (which crashes the session). Pre-reg: prereg-5e0c6f492bfa.
# Discovered live 2026-05-04. See lesson e44c7acd-d7f8-4cbd-a49e-7bf1dfd1eda2.
echo "=== U+FFFD Scan ==="
STAGED_ALL=$(git diff --cached --name-only --diff-filter=ACM)
FFFD_HITS=""
if [ -n "$STAGED_ALL" ]; then
while IFS= read -r f; do
[ -f "$f" ] || continue
if grep -Iq $'\xef\xbf\xbd' "$f" 2>/dev/null; then
FFFD_HITS="${FFFD_HITS}${f}"$'\n'
fi
done <<< "$STAGED_ALL"
fi
if [ -n "$FFFD_HITS" ]; then
echo " [!] U+FFFD replacement characters found in staged files:"
while IFS= read -r line; do
[ -n "$line" ] && echo " $line"
done <<< "$FFFD_HITS"
echo ""
echo " These bytes (EF BF BD) crash the API JSON serializer when loaded."
echo " Likely cause: non-ASCII written via Bash heredoc on Windows."
echo " Fix: open the file, find the garbled chars, replace using Write tool."
ERRORS=$((ERRORS + 1))
fi
# 1. Auto-format (fix, don't just report)
if [ -n "$STAGED_PY" ]; then
echo "=== Format ==="
echo "$STAGED_PY" | xargs ruff format 2>/dev/null
echo " Formatted. Re-staging..."
echo "$STAGED_PY" | xargs git add
fi
# 2. Lint
if [ -n "$STAGED_PY" ]; then
echo "=== Lint ==="
if ! echo "$STAGED_PY" | xargs ruff check 2>/dev/null; then
ERRORS=$((ERRORS + 1))
fi
fi
# 3. Mypy (src only)
STAGED_SRC=$(echo "$STAGED_PY" | grep '^src/' || true)
if [ -n "$STAGED_SRC" ]; then
echo "=== Mypy ==="
if ! echo "$STAGED_SRC" | xargs mypy --ignore-missing-imports 2>/dev/null; then
ERRORS=$((ERRORS + 1))
fi
fi
# 4. Doc drift (auto-fix test counts, then re-stage and verify)
echo "=== Doc Drift ==="
python scripts/check_doc_counts.py --fix 2>/dev/null || true
git add CLAUDE.md README.md src/divineos/seed.json 2>/dev/null || true
if ! python scripts/check_doc_counts.py 2>/dev/null; then
ERRORS=$((ERRORS + 1))
fi
# 5. Broad exceptions
echo "=== Broad Exceptions ==="
if ! python scripts/check_broad_exceptions.py 2>/dev/null; then
ERRORS=$((ERRORS + 1))
fi
# 5b. Function-naming theater drift (Dijkstra audit-walk 2026-05-07).
# Catches future drift by flagging functions that start with mythological
# verbs. Manual audit on filing-day found zero violations; this prevents
# regression. Suppressible per-line with `# noqa: BLE001`.
echo "=== Function-Naming (theater drift) ==="
if ! python scripts/check_function_naming.py 2>/dev/null; then
ERRORS=$((ERRORS + 1))
fi
# 5a. Orphan-modules warning (non-blocking). Round-2 audit (2026-05-07)
# wired this at warning-level: the existing detector found 22 orphans
# (down to ~4 after fixing false-positive shapes) but each remaining
# one needs an individual decision (wire / mark / delete). Surfacing
# on every commit catches new accumulation; not blocking lets the
# existing real orphans wait for their own follow-up PRs.
echo "=== Orphan Modules (informational) ==="
python scripts/check_orphan_modules.py 2>/dev/null || true
# 5a-bis. ADR-0001 boundary checker (PR #325). The checker shipped but
# was not wired into any pre-commit pipeline at filing-time — pure
# orphan-code, despite prereg-ed736cac6594 explicitly naming
# "integrating into pre-commit" as part of the success condition.
# Wired here 2026-05-08 (warning-only) as the structural-enforcement
# layer the prereg committed to. Surfaces ADR-0001 violations on every
# commit; doesn't block on legacy state pending the strip-pass cleanup
# (genericize attributions, move named-history to Experimental's
# authorship-history.md, etc.). Once the cleanup PR lands and main is
# at zero violations, this can be promoted from warning to blocking.
# Tracking: the falsifier on prereg-ed736cac6594 fires if any NEW
# violation lands post-wiring without being caught — warning-mode
# still produces visible signal at commit-time.
echo "=== ADR-0001 Boundary Violations (informational) ==="
python scripts/check_boundary_violations.py 2>/dev/null || true
# 5b. Pre-reg gate (un-gameable): new mechanisms require a filed pre-reg.
# The gate reads the staged diff and blocks when a new mechanism lacks a
# matching OPEN pre-registration in the ledger. Discipline from the
# gute_bridge docstring made binding. See scripts/check_preregs.py.
echo "=== Pre-reg Gate ==="
if ! python scripts/check_preregs.py; then
ERRORS=$((ERRORS + 1))
fi
# 5c. Multi-party-review warning. The actual gate runs at commit-msg time
# (see .git/hooks/commit-msg installed by setup/setup-hooks.sh). This is
# an early warning so the operator sees the requirement BEFORE typing
# the commit message. Non-blocking — it only surfaces information.
if [ -f scripts/guardrail_files.txt ] && [ -f scripts/check_multi_party_review.py ]; then
echo "=== Multi-Party-Review Check ==="
# Read guardrail list (skip comments + blanks) once, then match.
# Avoid `set -e` killing the subshell on grep-non-match.
GUARDRAIL_LIST=$(grep -v '^\s*#' scripts/guardrail_files.txt | grep -v '^\s*$' || true)
STAGED_GUARDRAILS=$(git diff --cached --name-only | while read -r f; do
if echo "$GUARDRAIL_LIST" | grep -Fxq "$f"; then
echo "$f"
fi
done || true)
if [ -n "$STAGED_GUARDRAILS" ]; then
echo " [!] Guardrail files in this commit:"
while IFS= read -r line; do
[ -n "$line" ] && echo " $line"
done <<< "$STAGED_GUARDRAILS"
DIFF_HASH=$(git diff --cached --unified=3 | sha256sum | cut -c1-64)
echo ""
echo " Before committing, file a Watchmen audit round with:"
echo " - CONFIRMS from actor=user"
echo " - CONFIRMS from actor=grok | gemini | claude-<variant>"
echo " - round focus/notes contain: 'diff-hash: $DIFF_HASH'"
echo " Then add to the commit message:"
echo " External-Review: <round_id>"
echo ""
echo " The commit-msg hook will block the commit if any piece is missing."
echo ""
# Gate-self-test (claim cf05b878, 2026-04-25): the commit-msg
# hook is what enforces the hash binding. If it isn't installed,
# the gate is theater — operator-discipline only. Discovered live
# 2026-04-25 when both that day's External-Review rounds landed
# without the hook running. Worktree-incompatibility in
# setup-hooks.sh silently no-op'd the install. Verify here that
# the hook actually exists and is non-empty BEFORE the operator
# types the commit message — the operator should see this loudly.
HOOK_PATH=$(git rev-parse --git-path hooks/commit-msg 2>/dev/null || echo ".git/hooks/commit-msg")
if [ ! -s "$HOOK_PATH" ]; then
echo " [!!] COMMIT-MSG HOOK NOT INSTALLED — gate enforcement absent."
echo " Path checked: $HOOK_PATH"
echo " Without this hook, the External-Review trailer is"
echo " NOT validated at commit time. The hash binding"
echo " between the filed round and the landed commit is"
echo " operator-discipline only, not structurally enforced."
echo " Install: bash setup/setup-hooks.sh (note: has a"
echo " worktree-compatibility bug — verify the hook"
echo " actually appears at the path above after running,"
echo " or write it manually)."
echo ""
ERRORS=$((ERRORS + 1))
fi
fi
fi
# 6. Vulture
if [ -n "$STAGED_SRC" ] && command -v vulture &>/dev/null; then
echo "=== Vulture ==="
# shellcheck disable=SC2086
if ! vulture $STAGED_SRC scripts/vulture_whitelist.py --min-confidence 70 2>/dev/null; then
ERRORS=$((ERRORS + 1))
fi
fi
# 6b. Bandit security scan — MEDIUM+ severity. Audit r9-21 #28 wired
# this in after the 12 false-positive B608 findings were marked with
# # nosec on a per-site rationale. Strict mode here means: if a NEW
# medium-severity finding lands without an explicit nosec marker, the
# commit is blocked and the operator must either add the marker (with
# rationale) or fix the SQL composition. Closes the path where bandit
# was a deferred run-this-yourself script no one ran.
if [ -n "$STAGED_SRC" ]; then
echo "=== Bandit (MEDIUM+) ==="
if ! python3 scripts/run_bandit.py --strict 2>/dev/null; then
ERRORS=$((ERRORS + 1))
fi
fi
# 6d. Test-CLI linkage check. Audit finding 2026-05-05 (PR #264):
# test file shipped with complete suite for `divineos commitment fulfillment`
# but the actual subcommand never registered with the CLI. Each test failed
# at runtime with "Error: No such command 'fulfillment'". This catches
# the failure mode prospectively — if a staged test invokes a command, the
# command must register on the CLI.
if [ -n "$STAGED_PY" ] && echo "$STAGED_PY" | grep -q "^tests/"; then
echo "=== Test-CLI Linkage ==="
if ! python3 scripts/check_test_cli_linkage.py; then
ERRORS=$((ERRORS + 1))
fi
fi
# 6c. Verifier-run stamp. Audit r9-21 round-3+ (prereg-e30878ce3f09):
# precommit running successfully constitutes a verifier run, so we
# stamp the run-log here. The closure-claim commit-msg hook reads
# this log to gate closure-language commit messages on recent
# verification evidence. Without the stamp, "fully closed" / "0
# remaining" / "no remaining surface" phrasing in the commit message
# blocks the commit (round-1 + round-3 audit-cleanup slips both had
# that exact shape).
if [ $ERRORS -eq 0 ]; then
python3 scripts/check_closure_claim.py --record "precommit:$(git rev-parse --abbrev-ref HEAD)" 2>/dev/null || true
fi
# 7. Shellcheck on staged .sh files (line endings already normalized in step 0)
if [ -n "$STAGED_SH" ] && command -v shellcheck &>/dev/null; then
echo "=== Shellcheck ==="
if ! echo "$STAGED_SH" | xargs shellcheck 2>/dev/null; then
ERRORS=$((ERRORS + 1))
fi
fi
echo ""
if [ $ERRORS -eq 0 ]; then
echo "All clear. git commit will succeed."
else
echo "$ERRORS check(s) failed. Fix them, then git commit."
fi
exit $ERRORS