textmachine/docs/scripts/githooks/pre-commit

94 lines
8.3 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# Зонный pre-commit фрагмент docs/. Ставится тем же диспетчером .git/hooks/pre-commit, что и
# фрагмент фронта (диспетчер перебирает */scripts/githooks/pre-commit).
#
# ┌─ ЧТО ЭТО ДЕЛАЕТ И ЧЕГО НЕ ДЕЛАЕТ — читать до того, как ругаться на него ───────────────────┐
# │ ДЕЛАЕТ: при коммите, задевающем ЛЮБОЙ док, проверяет file:line-якоря живых доков; при │
# │ коммите с D-логом или PROGRESS дополнительно сверяет три носителя номера головы и │
# │ производные числа доков (счёт очереди, зон, вес открытых строк регистра платформы). │
# │ НЕ ДЕЛАЕТ: не блокирует коммит НИКОГДА — только печатает предупреждение и выходит 0. │
# │ Не трогает содержимое, не правит файлы, не лезет в сеть, не запускает тесты. │
# │ ЕСЛИ МЕШАЕТ: `git commit --no-verify` обходит всю цепочку; выключить только этот фрагмент — │
# │ `chmod -x docs/scripts/githooks/pre-commit`. Ни то, ни другое ничего не ломает. │
# │ ПОЧЕМУ ВООБЩЕ: голова CURRENT-STATE отставала ТРИЖДЫ у трёх РАЗНЫХ оркестраторов │
# │ (D39.81 · D39.83 · D39.112 п.5б). Одна ошибка у независимых сессий на одном поле — │
# │ свойство поля, а не сессий, поэтому проверка механическая, а не нормой прозой. │
# │ ПОЧЕМУ ПРЕДУПРЕЖДАЕТ, А НЕ БЛОКИРУЕТ: фрагмент общий, он сработает у сессии, которой я не │
# │ писал промт. Останавливать чужой коммит — решение владельца, не моё (оркестратор №15). │
# └────────────────────────────────────────────────────────────────────────────────────────────┘
#
# Ложные тревоги, которых здесь СОЗНАТЕЛЬНО избегают, — это важнее, чем полнота: хук, который
# врёт, учат игнорировать, и тогда он хуже отсутствующего.
# · числа считаются по СОДЕРЖИМОМУ КОММИТА (`--from-index`), а не по рабочему дереву, иначе
# частичный стейдж (своя правка в общем файле) давал бы ругань на то, что в коммит не поедет;
# · голова сверяется ТОЛЬКО когда в коммите есть D-лог, то есть когда идёт ратификация — норма
# требует бампать голову в том же коммите, что аппенд ноты, и вот этот случай и ловится;
# коммит, который правит PROGRESS по другому поводу, про голову не спрашивают;
# · линт якорей (D39.148) читает СКАНИРУЕМЫЕ доки по содержимому коммита — иначе незакоммиченное
# дерево живого полигона в docs/experiments/ давало бы ругань на чужой WIP; ЦЕЛИ якорей берутся
# из дерева осознанно: код не входит в docs-коммит, и его истина — дерево.
staged=$(git diff --cached --name-only)
[ -z "$staged" ] && exit 0
dlog='docs/architecture/05-decisions-log.md'
prog='docs/PROGRESS.md'
# Быстрый выход: коммит не задевает документацию — нам тут нечего делать. Триггер ШИРЕ носителей
# чисел (D39.148): якоря `file:line` живут во ВСЕХ доках, включая промты сессий, поэтому линт обязан
# видеть и промт-коммит. Полигонский фриз-коммит, задевший docs/experiments/, изредка увидит
# предупреждение — приемлемо: оно warn-only и самоописано.
printf '%s\n' "$staged" | grep -qE '^(docs/.*|CLAUDE\.md)$' || exit 0
warn=0
# 1) Голова. Только при ратификации (D-лог в коммите).
if printf '%s\n' "$staged" | grep -qx "$dlog"; then
after() {
if printf '%s\n' "$staged" | grep -qx "$1"; then git show ":$1" 2>/dev/null; else git show "HEAD:$1" 2>/dev/null; fi
}
note=$(after "$dlog" | grep -oE '^## D39\.[0-9]+' | tail -1 | sed 's/^## //')
banner=$(after "$dlog" | head -1 | grep -oE 'D1D39\.[0-9]+' | sed 's/^D1//')
claimed=$(after "$prog" | head -8 | grep -oE 'голова D39\.[0-9]+' | head -1 | sed 's/^голова //')
if [ -n "$note" ] && [ -n "$claimed" ] && [ "$note" != "$claimed" ]; then
echo "pre-commit ⚠ docs: голова разошлась — последняя нота D-лога $note, CURRENT-STATE говорит $claimed." >&2
echo " Норма: голова бампается в ТОМ ЖЕ коммите, что аппенд ноты (промт оркестратора)." >&2
warn=1
fi
if [ -n "$note" ] && [ -n "$banner" ] && [ "$note" != "$banner" ]; then
echo "pre-commit ⚠ docs: шапка-диапазон D-лога $banner при последней ноте $note." >&2
warn=1
fi
fi
# 2) Производные числа — по содержимому коммита, и ТОЛЬКО когда в коммите носители этих чисел
# (гейтовка прежняя, D39.112). Ненайденный литерал скрипт тоже считает расхождением: проверка,
# молча перестающая проверять, хуже отсутствующей.
if printf '%s\n' "$staged" | grep -qE "^($dlog|$prog)$"; then
if command -v python3 >/dev/null 2>&1 && [ -f docs/scripts/counts.py ]; then
if ! out=$(python3 docs/scripts/counts.py --check --from-index 2>&1); then
echo "pre-commit ⚠ docs: производные числа разошлись с пере-счётом:" >&2
# Печатаем строки-находки, а если их нет (скрипт УПАЛ) — весь вывод: предупреждение с пустым
# телом нечитаемо и учит игнорировать хук.
printf '%s\n' "$out" | grep '✗' >&2 || printf '%s\n' "$out" | tail -5 >&2
warn=1
fi
fi
fi
# 3) Якоря file:line живых доков (D39.148 — ревизия D39.126 «--lint только руками»). Сканируемые
# доки читаются по содержимому коммита, поэтому чужой незакоммиченный WIP невидим; ЦЕЛИ якорей
# читаются из дерева — код не часть docs-коммита. ~0.2 с, warn-only.
if command -v python3 >/dev/null 2>&1 && [ -f docs/scripts/counts.py ]; then
if ! out=$(python3 docs/scripts/counts.py --lint --from-index 2>&1); then
echo "pre-commit ⚠ docs: мёртвые или переросшие file:line-якоря:" >&2
# Печатаем строки-находки, а если их нет (скрипт УПАЛ) — весь вывод: предупреждение с пустым
# телом нечитаемо и учит игнорировать хук.
printf '%s\n' "$out" | grep '✗' >&2 || printf '%s\n' "$out" | tail -5 >&2
warn=1
fi
fi
[ "$warn" -eq 1 ] && echo "pre-commit: это ПРЕДУПРЕЖДЕНИЕ — коммит не остановлен (обход: --no-verify)." >&2
exit 0