#!/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
