Harden the docs guard against its own failure modes and make errata findable from the map instead of only from the tail of the log
This commit is contained in:
parent
ca6f0a73c4
commit
0e189d2a4a
3 changed files with 158 additions and 102 deletions
|
|
@ -1,6 +1,16 @@
|
|||
# Журнал решений оркестратора — контракт D1–D39.112 (развязки 04.07 · пакеты 09–10.07 · приёмка/качество-первым/пивот/эмпирика 11–12.07 · арх-ресет+стройка пере-прогонного стека 13–19.07)
|
||||
|
||||
> **⟶ КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Читая контракт целиком, держи под рукой, что чем перекрыто:
|
||||
> ⚠ **Навигация (актуализация 07.08, эра D39.1xx):** append-only-дисциплина (D23.3) означает, что
|
||||
> НЕВЕРНЫЙ ФАКТ внутри старой ноты не переписывается, а получает эрратау — и тогда он опасен ровно
|
||||
> тем, что грепающий его найдёт ПЕРВЫМ, а эрратау ниже может не дочитать. Поэтому поправки к фактам
|
||||
> живут ЗДЕСЬ, в карте, а не только в хвосте: **D39.109 п.3 читать через D39.112 п.2(а)** («30 суток
|
||||
> — не буква NIST: SHALL требует установить overall-таймаут, 30 суток это SHOULD; сбрасывает ли его
|
||||
> успешная ре-аутентификация, NIST не говорит — наш жёсткий предел это НАШ выбор, строже нормы») ·
|
||||
> **D39.109 п.4 читать через D39.106 п.3** (PD-59 superseded, пайп-путь `supervisor.go` = дев-режим)
|
||||
> · **D39.109 п.1 и «живой уязвимости не нашла» читать через D39.112 п.2(в)** (отказ в обслуживании
|
||||
> входа ЕСТЬ — PD-80) · **D39.110 п.1 «дословно по смыслу» читать как ПЕРЕСКАЗ** решения, не цитату.
|
||||
|
||||
> ⚠ **Навигация (актуализация 04.08):** два supersede-указателя эры D39.9x: **D39.88/D39.84 п.8 (право самокоммита фронта/платформы) → отозвано, коммитит ТОЛЬКО оркестратор** (D39.98 п.3; тела переписаны на месте с санкцией владельца, pre-rewrite — git `2b166b7`) · **норма 30.07 «короткая приёмка» → амендирована владельцем 03.08: приёмка всегда адверсариальная** (носитель — промт оркестратора §Анти-паттерны + D39.101 п.1) — упоминания «короткой приёмки» в телах читать через эту пометку · **двухсекционная редакторская инъекция и смягчающая роль маркера ⟨проверить⟩ → доктрина инжекта D39.104**: банк на проводе = ЗАКОН для всех ролей независимо от статуса строки, право «перевести иначе» упразднено, блок редактора — ЕДИНЫЙ, маркер с провода снимается (статусы строк и подписная таблица не меняются; внесение в движок — строка 134 после пробы 18) — упоминания двухсекционки/смягчения в телах читать через это.
|
||||
> ⚠ **Навигация (актуализация 01.08):** карта ниже детально покрывает D1–D39.28; решения D39.29+ живут хронологически в теле файла, **свежая голова — С ХВОСТА** (новые ноты аппендятся вниз). Сводка текущей головы и очередь — CURRENT-STATE в `../PROGRESS.md`.
|
||||
> - **Полностью superseded:** **D1 (моно-редактор) → D17 → D30.1 (редактор БИЛИНГВ)** · D9 (ja-приёмка) → D18 (蛊真人 zh→ru; ja — второй прогон) · D17 → D30.1 · скобка D19.4 «ключ без data-sharing» и D20.3 «чистый ключ = блокер пилота» → **D27** (единый ключ С data-sharing, отключение перед продом) · exp04-дефолт «editor grok-4.3» (D3) → D30.1 (grok reasoning-off из редакторских ролей СНЯТ — no-op; кандидат glm-5-билингв; gemini — премиум-эскалация только за санитайзером D30.3). · **Транспорт шва D39.85 §1 (stdout-пайп, платформа-родитель) → D39.106** (журнал в каталоге книги + systemd-юнит-на-прогон); остальное D39.85 в силе.
|
||||
|
|
|
|||
|
|
@ -1,79 +1,95 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Печатает производные числа доков, которые до сих пор переписывались руками.
|
||||
|
||||
Заведён после верификации записей №15 (D39.112 п.4): за одну сессию четыре из десяти
|
||||
рукописных копий производных чисел разошлись с источником. Норма D39.101 требует считать
|
||||
очередь «только скриптом» — скрипта при этом не существовало, и норма исполнялась глазами.
|
||||
Заведён после верификации записей №15 (D39.112 п.4): за одну сессию четыре из десяти рукописных
|
||||
копий производных чисел разошлись с источником. Норма D39.101 требует считать очередь «только
|
||||
скриптом» — скрипта при этом не существовало, и норма исполнялась глазами.
|
||||
|
||||
Числа, которые печатает этот файл, в доках писать литералами больше не надо: строка носителя
|
||||
несёт команду. Кто хочет литерал — обязан сверить его этой командой в том же касании.
|
||||
Числа, которые печатает этот файл, в доках писать литералами больше не надо: строка носителя несёт
|
||||
команду. Кто всё же пишет литерал — обязан сверить его этой командой в том же касании.
|
||||
|
||||
Использование: python3 docs/scripts/counts.py [--check]
|
||||
без флага — печатает числа для вставки в доки
|
||||
--check — сверяет литералы, найденные в доках, с пере-счётом; ненулевой код = расхождение
|
||||
python3 docs/scripts/counts.py печатает числа
|
||||
python3 docs/scripts/counts.py --check сверяет литералы доков с пере-счётом; код 1 = расхождение
|
||||
python3 docs/scripts/counts.py --check --from-index то же, но по СОДЕРЖИМОМУ КОММИТА
|
||||
(индекс для застейдженного файла, HEAD для остальных)
|
||||
|
||||
⚠ Про `--from-index`: без него скрипт читает рабочее дерево, и при ЧАСТИЧНОМ стейдже (техника
|
||||
«застейджить только свою правку в общем файле») он честно ругается на то, что в коммит не поедет.
|
||||
Хук зовёт именно `--from-index`, чтобы не давать ложных тревог — хук, который врёт, учат игнорировать.
|
||||
|
||||
⚠ Дисциплина самого скрипта: если ОЖИДАЕМЫЙ литерал в доке не найден, это РАСХОЖДЕНИЕ, а не тишина.
|
||||
Проверка, которая молча перестала проверять из-за переформулировки, хуже отсутствующей.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
PROGRESS = ROOT / "docs" / "PROGRESS.md"
|
||||
DLOG = ROOT / "docs" / "architecture" / "05-decisions-log.md"
|
||||
REGISTER = ROOT / "platform" / "docs" / "DEFECT_REGISTER.md"
|
||||
PROGRESS = "docs/PROGRESS.md"
|
||||
DLOG = "docs/architecture/05-decisions-log.md"
|
||||
REGISTER = "platform/docs/DEFECT_REGISTER.md"
|
||||
|
||||
# Строка таблицы бэклога: «| 145 | ...» или «| 13а | ...». Заголовки-разделители («| **— ... —** |»)
|
||||
# и шапка таблицы под этот вид не подходят и потому не считаются.
|
||||
BACKLOG_ROW = re.compile(r"^\| (\d+[а-яё]?) \|")
|
||||
REGISTER_ROW = re.compile(r"^\| (PD-\d+) \|")
|
||||
NOTE = re.compile(r"^## (D39\.\d+)")
|
||||
|
||||
FROM_INDEX = "--from-index" in sys.argv
|
||||
|
||||
|
||||
def read(rel: str) -> str:
|
||||
"""Содержимое файла: из рабочего дерева либо из того, что поедет в коммит."""
|
||||
if not FROM_INDEX:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
staged = subprocess.run(
|
||||
["git", "diff", "--cached", "--name-only"], cwd=ROOT, capture_output=True, text=True
|
||||
).stdout.split()
|
||||
ref = f":{rel}" if rel in staged else f"HEAD:{rel}"
|
||||
got = subprocess.run(["git", "show", ref], cwd=ROOT, capture_output=True, text=True)
|
||||
if got.returncode != 0:
|
||||
# Новый файл вне индекса или отсутствующий в HEAD — читаем дерево, но говорим об этом.
|
||||
print(f" (⚠ {rel}: {ref} недоступен, читаю рабочее дерево)", file=sys.stderr)
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
return got.stdout
|
||||
|
||||
|
||||
def cells(line: str) -> list[str]:
|
||||
return [c.strip() for c in line.split("|")]
|
||||
|
||||
|
||||
def backlog() -> dict:
|
||||
rows = [l for l in PROGRESS.read_text(encoding="utf-8").splitlines() if BACKLOG_ROW.match(l)]
|
||||
def backlog(text: str) -> dict:
|
||||
rows = [l for l in text.splitlines() if BACKLOG_ROW.match(l)]
|
||||
ids = [BACKLOG_ROW.match(l).group(1) for l in rows]
|
||||
dupes = [i for i, n in Counter(ids).items() if n > 1]
|
||||
strict = sum(1 for l in rows if cells(l)[3] == "бэкенд")
|
||||
wide = sum(1 for l in rows if "бэкенд" in cells(l)[3])
|
||||
soon = [BACKLOG_ROW.match(l).group(1) for l in rows if "скоро" in cells(l)[4]]
|
||||
blockers = sum(1 for l in rows if "блокер-очереди" in cells(l)[4])
|
||||
return {
|
||||
"всего": len(rows),
|
||||
"бэкенд строго": strict,
|
||||
"бэкенд широко": wide,
|
||||
"скоро": len(soon),
|
||||
"скоро поимённо": soon,
|
||||
"блокеров очереди": blockers,
|
||||
"дубли ID": dupes,
|
||||
"бэкенд строго": sum(1 for l in rows if cells(l)[3] == "бэкенд"),
|
||||
"бэкенд широко": sum(1 for l in rows if "бэкенд" in cells(l)[3]),
|
||||
"скоро": sum(1 for l in rows if "скоро" in cells(l)[4]),
|
||||
"блокеров очереди": sum(1 for l in rows if "блокер-очереди" in cells(l)[4]),
|
||||
"дубли ID": [i for i, n in Counter(ids).items() if n > 1],
|
||||
}
|
||||
|
||||
|
||||
def register() -> dict:
|
||||
rows = [l for l in REGISTER.read_text(encoding="utf-8").splitlines() if REGISTER_ROW.match(l)]
|
||||
weight = Counter()
|
||||
status = Counter()
|
||||
open_ids = []
|
||||
def register(text: str) -> dict:
|
||||
rows = [l for l in text.splitlines() if REGISTER_ROW.match(l)]
|
||||
weight, status, open_ids = Counter(), Counter(), []
|
||||
for l in rows:
|
||||
c = cells(l)
|
||||
st, sev = c[6], c[3].replace("*", "")
|
||||
if st == "open":
|
||||
status["open"] += 1
|
||||
open_ids.append(c[1])
|
||||
# Вес читается по вхождению слова: колонка часто несёт оговорку («major (для промта…)»).
|
||||
weight["major" if "major" in sev else "minor" if "minor" in sev else "info" if "info" in sev else sev] += 1
|
||||
elif st.startswith("fixed"):
|
||||
status["fixed"] += 1
|
||||
elif st.startswith("accepted-risk"):
|
||||
status["accepted-risk"] += 1
|
||||
else:
|
||||
status["иное: " + st[:32]] += 1
|
||||
status["иное"] += 1
|
||||
return {
|
||||
"всего строк": len(rows),
|
||||
"по статусу": dict(status),
|
||||
|
|
@ -82,37 +98,59 @@ def register() -> dict:
|
|||
}
|
||||
|
||||
|
||||
def head() -> dict:
|
||||
"""Голова = последняя нота D-лога; она же обязана стоять в шапке CURRENT-STATE."""
|
||||
notes = [NOTE.match(l).group(1) for l in DLOG.read_text(encoding="utf-8").splitlines() if NOTE.match(l)]
|
||||
last = notes[-1] if notes else None
|
||||
first = PROGRESS.read_text(encoding="utf-8").splitlines()[:6]
|
||||
def head(dlog: str, prog: str) -> dict:
|
||||
notes = [NOTE.match(l).group(1) for l in dlog.splitlines() if NOTE.match(l)]
|
||||
banner = re.search(r"D1–(D39\.\d+)", dlog.splitlines()[0]) if dlog else None
|
||||
claimed = None
|
||||
for l in first:
|
||||
for l in prog.splitlines()[:8]:
|
||||
m = re.search(r"голова (D39\.\d+)", l)
|
||||
if m:
|
||||
claimed = m.group(1)
|
||||
break
|
||||
banner = re.search(r"D1–(D39\.\d+)", DLOG.read_text(encoding="utf-8").splitlines()[0])
|
||||
last = notes[-1] if notes else None
|
||||
b = banner.group(1) if banner else None
|
||||
return {
|
||||
"последняя нота D-лога": last,
|
||||
"шапка D-лога": b,
|
||||
"голова в CURRENT-STATE": claimed,
|
||||
"шапка D-лога": banner.group(1) if banner else None,
|
||||
"сходится": last == claimed == (banner.group(1) if banner else None),
|
||||
"сходится": bool(last) and last == claimed == b,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
b, r, h = backlog(), register(), head()
|
||||
check = "--check" in sys.argv
|
||||
# Литералы, которые доки обязаны держать в согласии с пере-счётом. Каждый ОБЯЗАН найтись:
|
||||
# ненайденный литерал = проверка перестала проверять, и это докладывается как расхождение.
|
||||
# region ограничивает поиск, чтобы историческая секция со старым числом не давала ложной тревоги.
|
||||
LITERALS = [
|
||||
(PROGRESS, "current-state", r"всего \*\*(\d+)\*\* строк", ("backlog", "всего")),
|
||||
(PROGRESS, "current-state", r"зона бэкенд \*\*(\d+)\*\* строго", ("backlog", "бэкенд строго")),
|
||||
(PROGRESS, "current-state", r"/ \*\*(\d+)\*\* широко", ("backlog", "бэкенд широко")),
|
||||
(PROGRESS, "current-state", r"«скоро» \*\*(\d+)\*\*", ("backlog", "скоро")),
|
||||
(PROGRESS, "current-state", r"\*\*(\d+) major\*\*", ("register-weight", "major")),
|
||||
]
|
||||
|
||||
|
||||
def region(text: str, name: str) -> str:
|
||||
"""current-state = шапка PROGRESS до таблицы бэклога; там и живут сводные числа."""
|
||||
if name != "current-state":
|
||||
return text
|
||||
cut = text.find("## Бэклог")
|
||||
return text[: cut if cut > 0 else len(text)]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
dlog, prog, reg = read(DLOG), read(PROGRESS), read(REGISTER)
|
||||
b, r, h = backlog(prog), register(reg), head(dlog, prog)
|
||||
check = "--check" in sys.argv
|
||||
src = "содержимое коммита (индекс/HEAD)" if FROM_INDEX else "рабочее дерево"
|
||||
|
||||
print(f"ИСТОЧНИК: {src}\n")
|
||||
print("ГОЛОВА")
|
||||
for k, v in h.items():
|
||||
print(f" {k}: {v}")
|
||||
print("\nБЭКЛОГ (docs/PROGRESS.md)")
|
||||
print("\nБЭКЛОГ")
|
||||
for k, v in b.items():
|
||||
print(f" {k}: {v if k != 'скоро поимённо' else '·'.join(v)}")
|
||||
print("\nРЕГИСТР ПЛАТФОРМЫ (platform/docs/DEFECT_REGISTER.md)")
|
||||
print(f" {k}: {v}")
|
||||
print("\nРЕГИСТР ПЛАТФОРМЫ")
|
||||
for k, v in r.items():
|
||||
print(f" {k}: {v if k != 'открытые' else ' '.join(v)}")
|
||||
|
||||
|
|
@ -123,36 +161,29 @@ def main() -> int:
|
|||
if not h["сходится"]:
|
||||
bad.append(
|
||||
f"голова разошлась: последняя нота {h['последняя нота D-лога']}, "
|
||||
f"CURRENT-STATE {h['голова в CURRENT-STATE']}, шапка D-лога {h['шапка D-лога']}"
|
||||
f"шапка D-лога {h['шапка D-лога']}, CURRENT-STATE {h['голова в CURRENT-STATE']}"
|
||||
)
|
||||
if b["дубли ID"]:
|
||||
bad.append(f"дубли ID строк бэклога: {b['дубли ID']}")
|
||||
# Литералы в доках: ищем только те формы, которые доки реально используют.
|
||||
prog = PROGRESS.read_text(encoding="utf-8")
|
||||
m = re.search(r"всего \*\*(\d+)\*\* строк", prog)
|
||||
if m and int(m.group(1)) != b["всего"]:
|
||||
bad.append(f"счёт очереди в CURRENT-STATE {m.group(1)} против пере-счёта {b['всего']}")
|
||||
m = re.search(r"зона бэкенд \*\*(\d+)\*\* строго .*?/ \*\*(\d+)\*\* широко", prog)
|
||||
if m and (int(m.group(1)), int(m.group(2))) != (b["бэкенд строго"], b["бэкенд широко"]):
|
||||
bad.append(
|
||||
f"счёт зоны бэкенд {m.group(1)}/{m.group(2)} против пере-счёта "
|
||||
f"{b['бэкенд строго']}/{b['бэкенд широко']}"
|
||||
)
|
||||
for path in (PROGRESS, ROOT / "platform" / "docs" / "platform-PROGRESS.md"):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for mm in re.finditer(r"\*\*(\d+) major\*\*", text):
|
||||
if int(mm.group(1)) != r["вес открытых"].get("major", 0):
|
||||
bad.append(
|
||||
f"{path.name}: «{mm.group(1)} major» против пере-счёта "
|
||||
f"{r['вес открытых'].get('major', 0)}"
|
||||
)
|
||||
|
||||
texts = {PROGRESS: prog, REGISTER: reg}
|
||||
for path, reg_name, pattern, (kind, key) in LITERALS:
|
||||
want = b[key] if kind == "backlog" else r["вес открытых"].get(key, 0)
|
||||
found = re.search(pattern, region(texts[path], reg_name))
|
||||
if not found:
|
||||
bad.append(
|
||||
f"{path}: литерал по шаблону /{pattern}/ НЕ НАЙДЕН — либо формулировку сменили, "
|
||||
f"либо число выпало; проверка этого числа перестала работать (ожидалось {want})"
|
||||
)
|
||||
elif int(found.group(1)) != want:
|
||||
bad.append(f"{path}: «{found.group(0)}» против пере-счёта {want}")
|
||||
|
||||
if bad:
|
||||
print("\nРАСХОЖДЕНИЯ:")
|
||||
for line in bad:
|
||||
print(" ✗ " + line)
|
||||
return 1
|
||||
print("\nЛитералы в доках сходятся с пере-счётом.")
|
||||
print(f"\nЛитералы сходятся с пере-счётом ({len(LITERALS)} проверок).")
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,28 @@
|
|||
#!/bin/sh
|
||||
# Зонный pre-commit фрагмент docs/. Запускается тем же диспетчером .git/hooks/pre-commit,
|
||||
# что и фрагмент фронта (он перебирает */scripts/githooks/pre-commit).
|
||||
# Зонный pre-commit фрагмент docs/. Ставится тем же диспетчером .git/hooks/pre-commit, что и
|
||||
# фрагмент фронта (диспетчер перебирает */scripts/githooks/pre-commit).
|
||||
#
|
||||
# Зачем: за одну сессию №15 четыре из десяти рукописных копий производных чисел разошлись с
|
||||
# источником, а голова CURRENT-STATE отставала ТРЕТИЙ раз в истории проекта — у трёх разных
|
||||
# оркестраторов (D39.81, D39.83, D39.112 п.5б). Одна и та же ошибка у независимых сессий на одном
|
||||
# поле — свойство поля, а не сессий, поэтому проверка механическая, а не нормой прозой.
|
||||
# ┌─ ЧТО ЭТО ДЕЛАЕТ И ЧЕГО НЕ ДЕЛАЕТ — читать до того, как ругаться на него ───────────────────┐
|
||||
# │ ДЕЛАЕТ: при коммите, задевающем D-лог или PROGRESS, сверяет три носителя номера головы и │
|
||||
# │ производные числа доков (счёт очереди, зон, вес открытых строк регистра платформы). │
|
||||
# │ НЕ ДЕЛАЕТ: не блокирует коммит НИКОГДА — только печатает предупреждение и выходит 0. │
|
||||
# │ Не трогает содержимое, не правит файлы, не лезет в сеть, не запускает тесты. │
|
||||
# │ ЕСЛИ МЕШАЕТ: `git commit --no-verify` обходит всю цепочку; выключить только этот фрагмент — │
|
||||
# │ `chmod -x docs/scripts/githooks/pre-commit`. Ни то, ни другое ничего не ломает. │
|
||||
# │ ПОЧЕМУ ВООБЩЕ: голова CURRENT-STATE отставала ТРИЖДЫ у трёх РАЗНЫХ оркестраторов │
|
||||
# │ (D39.81 · D39.83 · D39.112 п.5б). Одна ошибка у независимых сессий на одном поле — │
|
||||
# │ свойство поля, а не сессий, поэтому проверка механическая, а не нормой прозой. │
|
||||
# │ ПОЧЕМУ ПРЕДУПРЕЖДАЕТ, А НЕ БЛОКИРУЕТ: фрагмент общий, он сработает у сессии, которой я не │
|
||||
# │ писал промт. Останавливать чужой коммит — решение владельца, не моё (оркестратор №15). │
|
||||
# └────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
#
|
||||
# ⚠ ПРЕДУПРЕЖДАЕТ, НЕ БЛОКИРУЕТ (решение оркестратора №15, 07.08): фрагмент лежит в общем
|
||||
# диспетчере и сработает на коммите ЛЮБОЙ сессии, включая те, которым я не писал промт. Жёсткий
|
||||
# гейт, останавливающий чужой коммит, ставится только словом владельца. Обход, если понадобится:
|
||||
# git commit --no-verify.
|
||||
# Ложные тревоги, которых здесь СОЗНАТЕЛЬНО избегают, — это важнее, чем полнота: хук, который
|
||||
# врёт, учат игнорировать, и тогда он хуже отсутствующего.
|
||||
# · числа считаются по СОДЕРЖИМОМУ КОММИТА (`--from-index`), а не по рабочему дереву, иначе
|
||||
# частичный стейдж (своя правка в общем файле) давал бы ругань на то, что в коммит не поедет;
|
||||
# · голова сверяется ТОЛЬКО когда в коммите есть D-лог, то есть когда идёт ратификация — норма
|
||||
# требует бампать голову в том же коммите, что аппенд ноты, и вот этот случай и ловится;
|
||||
# коммит, который правит PROGRESS по другому поводу, про голову не спрашивают.
|
||||
|
||||
staged=$(git diff --cached --name-only)
|
||||
[ -z "$staged" ] && exit 0
|
||||
|
|
@ -18,37 +30,40 @@ staged=$(git diff --cached --name-only)
|
|||
dlog='docs/architecture/05-decisions-log.md'
|
||||
prog='docs/PROGRESS.md'
|
||||
|
||||
# Быстрый выход: ни один из двух носителей не в коммите — нам тут нечего делать.
|
||||
printf '%s\n' "$staged" | grep -qE "^($dlog|$prog)$" || exit 0
|
||||
|
||||
# Содержимое ПОСЛЕ коммита: для застейдженного файла это индекс, для нетронутого — HEAD.
|
||||
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 -6 | grep -oE 'голова D39\.[0-9]+' | head -1 | sed 's/^голова //')
|
||||
|
||||
warn=0
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# Производные числа доков — сверка пере-счётом (docs/scripts/counts.py). Считает по РАБОЧЕМУ
|
||||
# дереву: при частичном стейдже это advisory, а не приговор.
|
||||
# 2) Производные числа — по содержимому коммита. Ненайденный литерал скрипт тоже считает
|
||||
# расхождением: проверка, молча перестающая проверять, хуже отсутствующей.
|
||||
if command -v python3 >/dev/null 2>&1 && [ -f docs/scripts/counts.py ]; then
|
||||
out=$(python3 docs/scripts/counts.py --check 2>&1) || {
|
||||
echo "pre-commit ⚠ docs: литералы в доках разошлись с пере-счётом:" >&2
|
||||
if ! out=$(python3 docs/scripts/counts.py --check --from-index 2>&1); then
|
||||
echo "pre-commit ⚠ docs: производные числа разошлись с пере-счётом:" >&2
|
||||
printf '%s\n' "$out" | grep '✗' >&2
|
||||
warn=1
|
||||
}
|
||||
fi
|
||||
fi
|
||||
|
||||
[ "$warn" -eq 1 ] && echo "pre-commit: это ПРЕДУПРЕЖДЕНИЕ, коммит не остановлен." >&2
|
||||
[ "$warn" -eq 1 ] && echo "pre-commit: это ПРЕДУПРЕЖДЕНИЕ — коммит не остановлен (обход: --no-verify)." >&2
|
||||
exit 0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue