textmachine/docs/scripts/githooks/pre-commit

145 lines
14 KiB
Bash
Executable file
Raw Permalink 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 дополнительно сверяет три носителя номера головы и │
# │ производные числа доков (счёт очереди, зон, вес открытых строк регистра платформы). │
# │ БЛОКИРУЕТ (с 17.09, D39.260 п.3 — слово владельца «блокировать, исправлять, но не хакать»): │
# │ расхождение ГОЛОВЫ и производных ЧИСЕЛ. Это свои носители, они читаются по содержимому │
# │ коммита, и ложного срабатывания у них нет по построению — лечится только правкой. │
# │ ПРЕДУПРЕЖДАЕТ (не блокирует): якоря и неотслеживаемые файлы. Цели якорей читаются из ДЕРЕВА, │
# │ поэтому чужой незакоммиченный код роняет их ТРАНЗИЕНТНО; блокирующий гейт, который врёт │
# │ на чужой работе, начинают обходить — а это ровно то, что запрещено тем же словом. │
# │ Не трогает содержимое, не правит файлы, не лезет в сеть, не запускает тесты. │
# │ ЕСЛИ МЕШАЕТ: `git commit --no-verify` обходит всю цепочку; выключить только этот фрагмент — │
# │ `chmod -x docs/scripts/githooks/pre-commit`. Ни то, ни другое ничего не ломает. │
# │ ⚠ НО: обход — это ДЕЙСТВИЕ, а не мнение. Считаешь, что хук неправ, шумит, мешает или ловит │
# │ не то — НАПИШИ ВЛАДЕЛЬЦУ (он передаст оркестратору), а не обходи молча и не подпирай │
# │ хаком/воркэраундом. Тихая деградация гейта не оставляет следа: следующая сессия получит │
# │ его же в прежнем виде и потратит то же время, а мы не узнаем, что он врал. Жалоба на │
# │ гейт — полезный сигнал и повод его починить; молчаливый обход — потеря сигнала. │
# │ ПОЧЕМУ ВООБЩЕ: голова CURRENT-STATE отставала ТРИЖДЫ у трёх РАЗНЫХ оркестраторов │
# │ (D39.81 · D39.83 · D39.112 п.5б). Одна ошибка у независимых сессий на одном поле — │
# │ свойство поля, а не сессий, поэтому проверка механическая, а не нормой прозой. │
# │ ⛔ ПРЕЖНИЙ ДОВОД ОТМЕНЁН 17.09. Он гласил: «фрагмент общий, останавливать чужой коммит — │
# │ решение владельца, не моё (оркестратор №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'
# ⚠ Третий носитель с 06.09 (D39.218): таблица бэклога и её бюллетень уехали в отдельный файл,
# и без него `--check` не звали бы РОВНО на тех коммитах, что меняют таблицу.
backlog='docs/BACKLOG.md'
# ⚠ БЫСТРОГО ВЫХОДА ПО «коммит не задевает docs/» БОЛЬШЕ НЕТ (21.08, разбор с внешним ревьюером).
# Он ампутировал ЦЕЛЕВУЮ сторону дельта-сверки якорей: якорь протухает, когда двигается ЦЕЛЬ, а цели
# у нас в основном КОД — то есть главный двигатель протухания приезжал ровно тем коммитом, на котором
# хук молча выходил. Плюс расширение области линта на platform/docs и frontend/docs в старый триггер
# не попадало вовсе. Цена отмены нулевая: линт ~0.2 с и warn-only; блок производных ЧИСЕЛ по-прежнему
# гейчен составом коммита ниже (он дорогой и осмыслен только при ратификации).
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
block=1
fi
if [ -n "$note" ] && [ -n "$banner" ] && [ "$note" != "$banner" ]; then
echo "pre-commit ⚠ docs: шапка-диапазон D-лога $banner при последней ноте $note." >&2
block=1
fi
fi
# 2) Производные числа — по содержимому коммита, и ТОЛЬКО когда в коммите носители этих чисел
# (гейтовка прежняя, D39.112). Ненайденный литерал скрипт тоже считает расхождением: проверка,
# молча перестающая проверять, хуже отсутствующей.
if printf '%s\n' "$staged" | grep -qE "^($dlog|$prog|$backlog)$"; 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
block=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
# Untracked-под-коммитом: pathspec-форма коммита МОЛЧА роняет неотслеживаемые файлы.
# `git commit -- <пути>` обходит индекс и берёт рабочее дерево только для ОТСЛЕЖИВАЕМЫХ путей,
# поэтому новый файл, даже застейдженный, в коммит не попадает. Ровно так 31.08 уехал коммит, чей
# `migrations.sha256` уже ссылался на три миграции, оставшиеся снаружи; на свежем клоне схема
# оказалась бы короче на три шага, а симптом — «почему схема не та» у следующего человека.
# ⚠ Шум низок ПО ПОСТРОЕНИЮ: gitignored файлы (books/, .env, песочницы) в `--porcelain` не видны,
# поэтому сюда попадает только по-настоящему новое и неигнорируемое — то есть ровно тот случай,
# ради которого предупреждение и стоит.
untracked=$(git status --porcelain --untracked-files=normal | sed -n 's/^?? //p')
if [ -n "$untracked" ]; then
dirs=$(printf '%s\n' "$staged" | sed 's#/[^/]*$##' | sort -u)
hits=''
for d in $dirs; do
h=$(printf '%s\n' "$untracked" | grep -E "^${d}/[^/]*$" || true)
[ -n "$h" ] && hits="${hits}${h}
"
done
if [ -n "$hits" ]; then
echo "pre-commit ⚠ неотслеживаемые файлы в каталогах ЭТОГО коммита — pathspec-форма их РОНЯЕТ:" >&2
printf '%s' "$hits" | sed 's/^/ ?? /' >&2
echo ' Нужны в коммите? git add -- <путь>, затем проверь состав: git show --name-only' >&2
warn=1
fi
fi
if [ "${block:-0}" -eq 1 ]; then
echo "pre-commit ⛔ КОММИТ ОСТАНОВЛЕН: расхождение головы или производных чисел (D39.260 п.3)." >&2
echo " Это свои носители и ложных тревог у них нет: лечится правкой, а не обходом." >&2
echo " Обход --no-verify существует, но он ДЕЙСТВИЕ, а не мнение: считаешь гейт неправым — напиши владельцу." >&2
exit 1
fi
if [ "$warn" -eq 1 ]; then
echo "pre-commit: это ПРЕДУПРЕЖДЕНИЕ — коммит не остановлен (обход: --no-verify)." >&2
echo " Считаешь, что хук неправ или мешает? Напиши ВЛАДЕЛЬЦУ — не обходи молча и не подпирай хаком." >&2
echo " Предупреждение про ЧУЖОЙ док (не твоя зона)? Чинить его НЕ надо и нельзя — сообщи владельцу" >&2
echo " и коммить дальше: якорь в чужом доке уехал из-за твоей правки цели, чинит его владелец зоны." >&2
fi
exit 0