#!/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' # ⚠ БЫСТРОГО ВЫХОДА ПО «коммит не задевает 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 'D1–D39\.[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 # 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 [ "$warn" -eq 1 ]; then echo "pre-commit: это ПРЕДУПРЕЖДЕНИЕ — коммит не остановлен (обход: --no-verify)." >&2 echo " Считаешь, что хук неправ или мешает? Напиши ВЛАДЕЛЬЦУ — не обходи молча и не подпирай хаком." >&2 echo " Предупреждение про ЧУЖОЙ док (не твоя зона)? Чинить его НЕ надо и нельзя — сообщи владельцу" >&2 echo " и коммить дальше: якорь в чужом доке уехал из-за твоей правки цели, чинит его владелец зоны." >&2 fi exit 0