From 9c7a28d2804e0fa9b5f5c8cc4d8f2d094a6b4b37 Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Mon, 17 Aug 2026 01:30:35 +0300 Subject: [PATCH] Weave the anchor lint into the docs pre-commit hook, warn-only, reading scanned docs from the commit itself --- docs/PROGRESS.md | 2 +- docs/README.md | 2 +- docs/architecture/05-decisions-index.md | 3 +- docs/architecture/05-decisions-log.md | 14 +++- docs/scripts/counts.py | 96 +++++++++++++++++++++++-- docs/scripts/githooks/pre-commit | 43 ++++++++--- 6 files changed, 140 insertions(+), 20 deletions(-) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index a64c426b..c6fb75d1 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,6 +1,6 @@ # Журнал прогресса -> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-17, голова D39.147 — КУРС: движок и платформа до «работает и отдаёт результат», фронт морозится ДАЛЬШЕ лендинга P7. ОЧЕРЕДЬ №18 (единственный носитель — здесь): (а) приёмка живого P7 → лендинг **БЕЗ S5 и без разморозки** (D39.147: P7 зону не размораживает) · (а2) промт бэкенд-пака честности ВЫДАН — `BACKEND_HONESTY_PACK_SESSION_PROMPT.md` (176 · 181 · 172-г · 187; п.5 живой вахты — только при названной владельцем сумме), запуск по слову владельца; рубежи D39.141 закрыты двумя проходами опровергателя + сверкой вливания (⚠ урок в норму: первый опровергатель САМ принёс ложную атрибуцию кодов 0–3 ноте D39.131, поймал её автор сверкой с телом ноты — рубежи не заменяют друг друга, они ловят разное) · (б) лендинг петель полигона — ⚠ в дереве НЕзадокументированная работа 16.08 (`eval/dovodka/PLAN-16-08.md`, `ja6.py`, `naklon.py`, свежие фризы) без записи в журнале, состав выяснить у полигона · (в) вахта весов V4-Pro-0813 с порогом 116 в риге (172-г) · (г) свободные бэкенд: 160 Этап 0, 181 (дедлайн — первый пере-прогон) · (д) строка 148 после фраз владельца; лента нот эры — реестр `architecture/05-decisions-index.md`). Оркестраторов ДВА (решение владельца 07.08): этот — движок/платформа/фронт/доки; параллельный (РОЛЬЮ, без номера — счётчик один, D39.112 п.6) — приёмка полигона. Одновременно не запускаются; CURRENT-STATE ведут оба, чужие строки не трогают. Норма изоляции панелей после инцидента отката чужих файлов — D39.113, гардрейлы в CLAUDE.md. +> **⟶ ТЕКУЩЕЕ СОСТОЯНИЕ** (на 2026-08-17, голова D39.148 — КУРС: движок и платформа до «работает и отдаёт результат», фронт морозится ДАЛЬШЕ лендинга P7; гейт доков усилен линтом якорей в хуке (D39.148). ОЧЕРЕДЬ №18 (единственный носитель — здесь): (а) приёмка живого P7 → лендинг **БЕЗ S5 и без разморозки** (D39.147: P7 зону не размораживает) · (а2) промт бэкенд-пака честности ВЫДАН — `BACKEND_HONESTY_PACK_SESSION_PROMPT.md` (176 · 181 · 172-г · 187; п.5 живой вахты — только при названной владельцем сумме), запуск по слову владельца; рубежи D39.141 закрыты двумя проходами опровергателя + сверкой вливания (⚠ урок в норму: первый опровергатель САМ принёс ложную атрибуцию кодов 0–3 ноте D39.131, поймал её автор сверкой с телом ноты — рубежи не заменяют друг друга, они ловят разное) · (б) лендинг петель полигона — ⚠ в дереве НЕзадокументированная работа 16.08 (`eval/dovodka/PLAN-16-08.md`, `ja6.py`, `naklon.py`, свежие фризы) без записи в журнале, состав выяснить у полигона · (в) вахта весов V4-Pro-0813 с порогом 116 в риге (172-г) · (г) свободные бэкенд: 160 Этап 0, 181 (дедлайн — первый пере-прогон) · (д) строка 148 после фраз владельца; лента нот эры — реестр `architecture/05-decisions-index.md`). Оркестраторов ДВА (решение владельца 07.08): этот — движок/платформа/фронт/доки; параллельный (РОЛЬЮ, без номера — счётчик один, D39.112 п.6) — приёмка полигона. Одновременно не запускаются; CURRENT-STATE ведут оба, чужие строки не трогают. Норма изоляции панелей после инцидента отката чужих файлов — D39.113, гардрейлы в CLAUDE.md. > - **Эра №15 закрыта — семь приёмок, все ПРИНЯТЫ и залендены**; лента, коммиты и разборы — D39.109–123 (D-лог) и реестр нот `architecture/05-decisions-index.md`; снимок прежних бюллетеней этой шапки — архив-слайс `-08-02-04`. > - **ЖИВОЕ:** полигон — **фаза Д ИДЁТ** (заказ 10.08; деньги санкционированы 15.08 напрямую полигону — ⚠ числа потолка в носителях расходятся, фактическую цепь сверить при лендинге петель; ja-книга `enkan_no_hate_ja`; при ратификации фазы Д в D-ноту: декой-правило + обязательный кросс-семейный опровергатель приёмки — одобрены 10.08; свежие фриз-коммиты полигона в дереве — НЕ трогать) · **платформа — P7 ЗАПУЩЕН 16.08** (промт `platform/docs/PLATFORM_P7_SESSION_PROMPT.md`, строит по канону 0.3.0; приёмка — очередь №18; первым шагом пака — рантбук migrate end-to-end, предусловие выката) · **фронт ЗАМОРОЖЕН** (D39.136 п.2 + **D39.147: морозится ДАЛЬШЕ лендинга P7 — слово владельца 17.08; P7 зону НЕ размораживает, S5-промт не выдаётся, разморозка отдельным словом по достижении сквозного пути**; перечень первого касания зоны — зеркало 0.3.0 + перегенерация типов + моки + гейт утечки конвейера + ФС-1..12 + Ф-63/Ф-28 + фразы В-11 по словарю кодов — не отменён, ждёт разморозки) · **контракт 0.3.0 ФИНАЛЕН в каноне** (батч+дофикс D39.142/143 · модель подписи «один ОК» D39.144 · дочистка D39.145; зеркало фронта отстаёт ратифицированно) · закрытые стройки эры — лентой нот: эмиттер D39.131 · P5/P6 D39.130/132 · migrate D39.134 · S4+0.2.3 D39.135 · DeepSeek-репин D39.137 (тела — D-лог и слайсы). > - **Открыто на владельце:** **развязка git с origin** (локальная линия ИСТИННА, force-push его рукой; не пуллить) · Приложение А контракта (148: фразы — по словарю кодов 0.3.0, структура готова) · продуктовое слово «остановлена: лимиты» (В-3) · В-4/Ф-30 (глава без заголовка — движковая половина строка 160) · мини-проба флора 44 (одобрена, промт не выдан; число — после ре-пробы 188) · подпись денежного шага 46 · лист В-3+К-6 · авто-резюм paused (вопрос платформы) · **схема time-based DeepSeek** (доклад — архив-слайс `PROGRESS-2026-08-14-15`; рекомендация: пик оставить + операционное правило «прогоны в долины», scheduler не начинать без ответа вендора об отметке тарификации) · **вход ратификации фазы Д:** посылка «dspro дешевле glm» в пике ПЕРЕВЁРНУТА (×1.26 дороже, D39.137 п.4) · **возражение Sol по проходу `tier` эксп-23** (пере-судить починенным ригом или оставить с записанным возражением — секция «Полигон») · фронт-вопросы зонного журнала: В-7 (плотность; держит Ф-54) · В-8 (слово состояния в дереве) · В-9 (языки интерфейса — механизм готов, ПТ-36) · Ф-38 (вкладка «Замечания»). Закрытые пункты листа (PD-104 · В-10 · В-11-форма · Ф-56/57/61/62/63 · Ф-28 · ПТ-33-граница · 116 · 126 · 172-пин · PD-203 · санкции фазы Д · дизайн 160/161 · title) — в нотах D39.136–145, здесь не держатся. diff --git a/docs/README.md b/docs/README.md index a69a10bc..45089c26 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,7 +16,7 @@ - `experiments/` — эмпирика полигона: [00-provider-quirks.md](experiments/00-provider-quirks.md) — **читать перед любым вызовом провайдера**; [08-cost-model-v2.md](experiments/08-cost-model-v2.md) — денежная модель; [09-pilot-protocol.md](experiments/09-pilot-protocol.md) — пилот Ф2.5; остальные 01–21 — отчёты ЗАКРЫТЫХ экспериментов (21 принят D39.117); ⚠ 22/23 — залендены с ревью-шапками приёмки №16 (10.08), НЕ ратифицированы: выводы заморожены до фазы Д (шапка первична; 18–20 — с ревью-шапками приёмки D39.108). - `research/` — фактура ресёрчей 01–28; у принятых — ревью-шапки, часть тел под ⚠ superseded: **читай баннер прежде содержимого**. Ключевые для навигации: 15 голос · 16 ридер-IDE · 17 внешняя критика · 18 рычаги качества · 19 нарезка · 20 банк-майнинг · 21 обзор транспорта · 22 доменные харнессы · 23 шов движок↔платформа (транспорт superseded D39.106) · 25 холодное ревью шва — форма D39.106, отвергнутые альтернативы, требования к эмиттеру (читать перед любым кодом стыка) · 24 арбитраж банка (ПРИНЯТ D39.102: консилиум закрыт классом, вход фикс-пака банка — §G) · 26 официальные практики Anthropic (D39.121: записка-план, сниппеты хендоффов, карта «уже делаем/перенять») · **28 контракт-ревью API v0 (ПРИНЯТ D39.138: решения владельца 16.08 — §8, состав батча 0.3.0 — §5; НОСИТЕЛЬ для исполняющих сессий, читать оригинал, не пересказ)**. - [PROGRESS.md](PROGRESS.md) — журнал: CURRENT-STATE + **ЕДИНЫЙ БЭКЛОГ** (единственный трекер) + живой хвост хроники. НЕ источник решений. -- `scripts/counts.py` — **производные числа доков считаются им, а не руками** (голова по трём носителям · счёт очереди и зон · вес открытых строк регистра платформы · полнота реестра нот); `--check` даёт ненулевой код на расхождении; `--lint` — линтер file:line-якорей живых доков (D39.126). Его же зовёт зонный хук `scripts/githooks/pre-commit` при коммите, задевающем D-лог или PROGRESS — предупреждает, не блокирует. Заведено по D39.112 п.5б: голова отставала трижды у трёх разных оркестраторов. +- `scripts/counts.py` — **производные числа доков считаются им, а не руками** (голова по трём носителям · счёт очереди и зон · вес открытых строк регистра платформы · полнота реестра нот); `--check` даёт ненулевой код на расхождении; `--lint` — линтер file:line-якорей живых доков (D39.126). Его же зовёт зонный хук `scripts/githooks/pre-commit`: **`--lint` якорей — при коммите, задевающем ЛЮБОЙ док или `CLAUDE.md`; `--check` чисел и головы — при коммите с D-логом или PROGRESS** (оба `--from-index`, предупреждает, не блокирует; вплетение линта — ревизия D39.148). Заведено по D39.112 п.5б: голова отставала трижды у трёх разных оркестраторов. - Активные хендофф-промты сессий (состав обновляется при каждом лендинге — норма D39.80): [ORCHESTRATOR_SESSION_PROMPT.md](ORCHESTRATOR_SESSION_PROMPT.md) (роль/нормы; состояния не дублирует) · **Батч 0.3.0 ОТРАБОТАН ЦЕЛИКОМ (D39.142+D39.143): канон 0.3.0 финален**, промт в `archive/prompts/`, отчёт — `archive/reports/CONTRACT_BATCH_0.3.0_REPORT.md` · **Полигон: [POLYGON_EXP2223_REDO_SESSION_PROMPT.md](POLYGON_EXP2223_REDO_SESSION_PROMPT.md)** — фаза Д доводки эксп-22/23 ИДЁТ (заказ владельца 10.08; потолки фазы поднимал владелец напрямую — числа в CURRENT-STATE; выводы 22/23 заморожены до неё), хендофф живой сессии — [POLYGON_PHASE_D_HANDOFF.md](POLYGON_PHASE_D_HANDOFF.md) (испр. оркестратором №18: файл заленден `67e3260`, в составе не значился) · [POLYGON_PACKAGE4_SESSION_PROMPT.md](POLYGON_PACKAGE4_SESSION_PROMPT.md) (отложен). **Фронт: ЗОНА ЗАМОРОЖЕНА (D39.136 п.2)** — S4 ИСПОЛНЕН и ПРИНЯТ (D39.135, лендинг `267aa35`; контракт 0.2.3 в каноне; промт в `frontend/docs/archive/`); ⚠ **разморозка БОЛЬШЕ НЕ привязана к P7** — слово владельца 17.08 (D39.147): зона морозится дальше лендинга P7, S5-промт не выдаётся, курс — движок и платформа до «работает и отдаёт результат»; разморозка отдельным словом. Перечень первого касания (зеркало 0.3.0 · типы · моки · гейт утечки · ФС-1..12 · Ф-63/Ф-28 · фразы В-11 по словарю кодов; ~~В-10~~ закрыт «вежливость») не отменён, ждёт разморозки; живой smoke против дев-стенда легитимен и во фризе; фикс-лист ФС — запись приёмки 15.08 в зонном журнале · **Платформа: активного промта НЕТ** — P6 + дофикс ИСПОЛНЕНЫ и ПРИНЯТЫ (D39.132, промты в `platform/docs/archive/`); **P7 ЗАПУЩЕН 16.08, сессия ЖИВАЯ**: `platform/docs/PLATFORM_P7_SESSION_PROMPT.md` (читающая поверхность по финальному канону 0.3.0 + модель ошибок + форварды + PD-104 + демонтаж гейта полноты банка; приёмка — оркестратор №18) · **Бэкенд: [BACKEND_HONESTY_PACK_SESSION_PROMPT.md](BACKEND_HONESTY_PACK_SESSION_PROMPT.md) ВЫДАН 17.08, запуск по слову владельца** (оба рубежа D39.141 закрыты: механическая сверка 11/11 блоков + ДВА прохода опровергателя другой моделью + узкая сверка вливания находок; первый проход сам принёс ошибку атрибуции — она вскрыта автором и подтверждена вторым проходом) (пак «честные числа и статусы»: строки 176 · 181 · 172-г · 187; п.5 живой вахты весов исполняется ТОЛЬКО при названной владельцем сумме) — ранее: пере-пин цен DeepSeek ИСПОЛНЕН и ПРИНЯТ (D39.137, лендинг `76049bb`; промт в `archive/prompts/`), потолки снова защищают; `tmctl migrate` — D39.134 (`d55edd4`), деплой эмиттер-бинаря разблокирован. Эмиттер шва ИСПОЛНЕН и ПРИНЯТ (D39.131, промт в архиве). Все ОТРАБОТАННЫЕ промты — в `archive/prompts/`, отчёты с ревью-шапками — в `archive/reports/`; статусы паков — CURRENT-STATE и D-лог, здесь не дублируются. Зонные журналы фронта/платформы — `frontend-PROGRESS.md` / `platform-PROGRESS.md` в их зонах (прогресс зон только там, D39.100). - Зоны фронта (чужие, читать при касании стыка; каждая ведёт СВОЙ зонный бэклог — единый бэклог их строк не принимает, D39.84): [../frontend/](../frontend/) — веб-интерфейс: промт фронт-сессий S0–S7 + [STACK_DECISIONS.md](../frontend/docs/STACK_DECISIONS.md) (пины версий точными числами и ловушки, сверены с вебом 02.08) + [BACKLOG.md](../frontend/docs/BACKLOG.md) · [../platform/](../platform/) — SaaS control plane: README + [BACKLOG.md](../platform/BACKLOG.md) (П-1..П-14) + `docs/` (зонный журнал `platform-PROGRESS.md` · регистр дефектов · архив промтов P0–P5). - `archive/` — история ([правила архива](archive/README.md)): закрытые промты (`prompts/`) · отчёты с ревью-шапками (`reports/` — на них ссылаются приёмки) · исполненные арх-доки (`architecture/`) · слайсы хроники `PROGRESS-*.md`. Инструкции оттуда не исполнять. diff --git a/docs/architecture/05-decisions-index.md b/docs/architecture/05-decisions-index.md index 39c394fe..05cf2f09 100644 --- a/docs/architecture/05-decisions-index.md +++ b/docs/architecture/05-decisions-index.md @@ -1,4 +1,4 @@ -# Реестр D-нот — карта актуальности v2 (D1–D39.147; строка 167; титул — носитель головы, бампать при каждом аппенде — испр. оркестратором №17 15.08 по аудиту: отставал на четыре ноты) +# Реестр D-нот — карта актуальности v2 (D1–D39.148; строка 167; титул — носитель головы, бампать при каждом аппенде — испр. оркестратором №17 15.08 по аудиту: отставал на четыре ноты) > Одна строка на КАЖДУЮ ноту журнала решений: № · дата · суть · статус · где тело · темы. Ведёт оркестратор при лендингах: новая нота = новая строка ТЕМ ЖЕ коммитом (полноту сторожит `docs/scripts/counts.py --check`). Суть и статус здесь — НАВИГАЦИЯ, не контракт: при конфликте побеждает ТЕЛО ноты (живой [`05-decisions-log.md`](05-decisions-log.md) → слайсы `../archive/architecture/05-decisions-*.md`). Несущие эрраты к фактам старых нот живут в шапке живого D-лога — их читать ОБЯЗАТЕЛЬНО, реестр их не дублирует. Темы (для грепа оси «какой закон по X»): деньги · шов · банк · промпт · судья · гейты · контракт · платформа · фронт · полигон · ToS · общность · процесс · нарезка · голос · инфра · модели. > Родословная: прозаическая карта D1–D39.28 (ревизии D31–D38.2) пересобрана в реестр докс-паком 167 (09.08.2026, №16): извлечение 6+6 агентов по worksheet `../archive/reports/DOC_AUDIT_INVENTORY_2026-08-09.md`, полнота сверена скриптом против заголовков всех шести файлов. «тело: жив» = живой D-лог; «слайс N» = `../archive/architecture/05-decisions-N.md`. @@ -200,3 +200,4 @@ | D39.145 | 16.08 | Промт P7 пересобран (слово владельца: перечитать после батча, без воды): два рубежа D39.141 — автор-перечтение (7 протуханий, худшее «кадр конца разбора» против канона) + опровергатель (8 HIGH: построенный гейт полноты reconcile.go:934-938 — демонтаж заказан явно; недочистка D39.144 — эррата 16.08-д, канон/компаньон дочищены; экспорт в P8 явно; structure_version/genre/halt_reason/heading/Note.code влиты); форма — конкретика ~120 строк, правило пофайлового диффа против required канона; PD-241 императивом | ЖИВОЕ: P7 готов, запуск — слово владельца | жив | платформа промпт контракт процесс | | D39.146 | 16.08 | Закрытие сессии №17, передача №18: лента D39.134–145 (приёмки migrate/S4/DeepSeek, контракт-ревью+батч+дофикс — канон 0.3.0 финален, модель подписи один ОК, аудит бэклога потерь 0, реструктуризация роли); очередь №18 — приёмка запущенного P7 → S5+разморозка фронта, лендинг петель фазы Д, вахта весов с порогом 116, свободные 160/181; CURRENT-STATE ужат, доки проверены двумя аудиторами готовности | ЖИВОЕ: очередь — CURRENT-STATE | жив | хендофф оркестратор | | D39.147 | 17.08 | Слово владельца напрямую №18: фронт морозится ДАЛЬШЕ лендинга P7 (условие разморозки D39.136 п.2 амендировано — P7 зону не размораживает, S5 не выдаётся, разморозка отдельным словом), курс — движок и платформа до «работает и отдаёт результат», моки фронта при разморозке уступают дев-стенду платформы; инвентарь трёх аудитов кодом: ассемблера книги нет нигде и export всегда exit 0 (строка 49), дыры выдачи молчат в тексте (новая 193), боевой конфиг без банкового контура = 140 внутри wire-батча 182, беспагинационные чтения стора под 10s (дописка 54), платформа после P7 читаема но без экспорта и эскроу (136/137/П-18), блокирующих строк движка 18 из 103 и два тяжелейших не про код (54, 16) | ЖИВОЕ: курс и условие разморозки | жив | процесс фронт платформа владелец | +| D39.148 | 17.08 | Ревизия D39.126: `--lint` якорей вплетён в pre-commit хук доков (warn-only, `--from-index`), триггер линта — любой док или CLAUDE.md, `--check` остаётся на D-логе/PROGRESS; дизайн против ложных тревог — сканируемые доки по содержимому коммита, цели якорей из дерева, состав коммита одним вызовом; верификация реальными тест-коммитами поймала два дефекта (падение на отсутствующем файле, пустое тело предупреждения); метод-урок: хук судится тест-коммитом, а не ручным прогоном — git отдаёт хуку временный индекс через GIT_INDEX_FILE | ЖИВОЕ: действующая форма гейта доков | жив | процесс инфра доки | diff --git a/docs/architecture/05-decisions-log.md b/docs/architecture/05-decisions-log.md index 970e8b1b..c65c07fe 100644 --- a/docs/architecture/05-decisions-log.md +++ b/docs/architecture/05-decisions-log.md @@ -1,4 +1,4 @@ -# Журнал решений оркестратора — контракт D1–D39.147 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`) +# Журнал решений оркестратора — контракт D1–D39.148 (живой файл: карта · эрраты · живые тела · голова D39.124+ (подрезка D39.139); тела закрытых эр — в слайсах `docs/archive/architecture/`, указатель ниже; реестр всех нот — `05-decisions-index.md`) > **⟶ КАРТА АКТУАЛЬНОСТИ (ревизия D31, продлена до D38.2 [12.07]; исторические записи ниже НЕ переписываются — дисциплина D23.3).** Работая с контрактом (греп номера: живой файл → слайсы, целиком НЕ читать — D39.125), держи под рукой, что чем перекрыто: > ⚠ **Эррата 09.08 (D39.125):** D39.111 п.1 предписывал промту S3 «максимум = баланс МИНУС открытые холды» — формула ОШИБОЧНА (вычитание дважды), исправлена D39.115 п.2(а): максимум = Balance КАК ЕСТЬ; тело D39.111 живёт ниже в этом файле (голова D39.106+). @@ -471,3 +471,15 @@ **2. Инвентарь под этот курс (три читающих аудита №18, всё грунтовано кодом; знание, не заказ — носители названы).** (а) **Движок не умеет отдать книгу:** `tmctl export` пишет только stdout — JSON либо ленту с баннерами `=== CHAPTER n CHUNK m ===` (`cmd/tmctl/render.go:477`, баннер флагнутого `:502`); ассемблера в файл нет ни в движке, ни в платформе, а `export` возвращает успех даже при 100% pending — «never an exit-2 sentinel … export is an audit projection, not a run verdict» (`cmd/tmctl/render.go:474-475`) — лестницы `run_complete/structurally_complete/publishable` в коде 0 вхождений. Носитель — **строка 49** (этап В D15.2), подтверждена кодом, вес не меняется. (б) **Дыры выдачи молчат в самом тексте** — новая **строка 193**. (в) **Боевой конфиг переводит «голым»** (0 вхождений терминолога/банкноты/майнера/классификатора/голоса/репейра в `configs/pipeline-c1.yaml`, `escalation.budget_usd: 0`): это **строка 140**, и её внесение ратифицированно живёт в wire-батче **182** — одним касанием перед первым платным прогоном; отдельной развилки нет (уточнение владельца 17.08 на вопрос №18). (г) **Масштаб:** жёстких потолков размера в коде нет, но чтения стора беспагинационны под `opTimeout = 10s` — **дописка в строку 54**. (д) **Платформа после P7 — «читаемая», не «законченная»:** экспорт-ручки отложены в P8 явно, денежный контур = холд-с-обрезкой без эскроу (перерасход срезается до холда и остаётся текстом в колонке, `platform/internal/pgstore/credits.go:212-235`), сверки с провайдером нет — **строки 136/137, П-18** зоны. (е) Раскладка бэклога движка по критерию «мешает ли сквозному пути»: 103 строки зоны, блокирующих **18** (+6 спорных), и два тяжелейших — **54** и **16** — не про код вовсе: книгу больше 10 глав и редакторскую волну не гоняли НИ РАЗУ, их закрывает только платный прогон. **3. Синхронизировано этим же коммитом:** CURRENT-STATE · `README.md` · ПТ-20 реестра требований · пинг в зонный журнал фронта. Очередь №18: приёмка P7 → лендинг **без** S5 и без разморозки; промт бэкенд-пака честности (176 · 181 · 172-г · 187) выдан и ждёт слова владельца на запуск. (17.08.2026, оркестратор №18) ✅ + +## D39.148 — РЕВИЗИЯ D39.126: линт якорей вплетён в pre-commit хук доков (warn-only); попутно закрыт метод-урок «как проверять хук» (17.08). ✅ + +**1. Решение (слово владельца 17.08 по докладу параллельного ревьюера; ревизия, а не недоделка).** Хук зоны `docs/` зовёт теперь ОБА режима `counts.py` — `--check` (числа и голова) и `--lint` (file:line-якоря), оба `--from-index`, оба warn-only, коммит не блокируется никогда. Прежняя запись «хук зовёт только `--check`, `--lint` руками» (D39.126) СНЯТА. Довод: ручной запуск жил только в промте оркестратора, то есть держался прозой — ровно тот класс, из-за которого хук и заводился (голова отставала трижды у трёх разных сессий, D39.112 п.5б); а ловил он реальное — протухший якорь `render.go`, внесённый в канон оркестратором №18 17.08. Цена — 0.21 с на docs-коммит. + +**2. Дизайн — четыре уточнения, каждое против названного канала вреда «хук, который врёт, учат игнорировать».** (а) **Сканируемые доки читаются по содержимому коммита** (индекс для застейдженного, HEAD для прочих): 24 из 87 сканируемых доков — `docs/experiments/`, зона живого полигона с постоянным незакоммиченным деревом, и чтение дерева давало бы ругань на чужой WIP. (б) **ЦЕЛИ якорей — из рабочего дерева**: код не входит в docs-коммит, его актуальная истина — дерево (целей в `eval/` всего 4 из 392, риск мизерный). (в) **Триггер линта шире носителей чисел** — любой `docs/**` или `CLAUDE.md`: якоря живут и в промтах сессий; полигонский фриз-коммит изредка увидит warn-only строку, это принято. (г) **Состав коммита и состояние дерева считаются по одному разу на процесс**: наивная реализация звала git на каждый из 87 доков. + +**3. Верификация — реальными тест-коммитами, а не ручным прогоном скрипта, и она поймала ДВА моих дефекта.** Четыре сценария на стендовом репозитории: чужой modified-WIP и чужой untracked рядом с чистым своим доком → тишина · свой мёртвый якорь в коммите → предупреждение с телом, коммит проходит · коммит вне `docs/` → хук не вмешивается · снесённый в дереве `CLAUDE.md` → без падения. Поймано и исправлено до лендинга: оптимизация «чистый файл читаем с диска» падала `FileNotFoundError` на файле, которого в дереве нет (прежняя ветка молча пропускала), и хук на падении скрипта печатал шапку предупреждения с ПУСТЫМ телом — теперь при отсутствии строк-находок печатается хвост вывода. + +**4. Метод-урок (записан, потому что куплен ошибкой оркестратора №18 в тот же день).** Хук нельзя судить, запуская его руками в шелле: при частичном коммите (`git commit -- <пути>`, наша каноническая форма) git строит ВРЕМЕННЫЙ индекс и отдаёт его хуку через `GIT_INDEX_FILE`, поэтому `git diff --cached` и `git show :файл` ВНУТРИ хука видят состав коммита, а ручной прогон — настоящий индекс с чужим стейджем. №18 на этом основании объявил хук мёртвым для наших коммитов; диагноз опровергнут экспериментом параллельного ревьюера и пере-воспроизведён №18. ⚠ Опровергающая улика лежала в его же наблюдении (фронтовый фрагмент того же диспетчера отрабатывал на pathspec-коммитах) и была объяснена, а не проверена — «связность вместо истинности» из дисциплины ревьюера. Проверка хука = тест-коммит, не вызов скрипта. + +**5. Носители:** докстринг `docs/scripts/counts.py` (строка «--lint руками» переписана), шапка `docs/scripts/githooks/pre-commit`, `docs/README.md`. (17.08.2026, оркестратор №18) ✅ diff --git a/docs/scripts/counts.py b/docs/scripts/counts.py index d487ccef..9ef5bc55 100644 --- a/docs/scripts/counts.py +++ b/docs/scripts/counts.py @@ -14,6 +14,8 @@ (индекс для застейдженного файла, HEAD для остальных) python3 docs/scripts/counts.py --lint линтер формы (строка 167): file:line-якоря живых доков (файл существует? строка в пределах файла?); код 1 = мёртвые якоря + python3 docs/scripts/counts.py --lint --from-index то же, но сканируемые доки читаются ПО + СОДЕРЖИМОМУ КОММИТА (цели якорей — всегда из дерева) --check также сторожит ПОЛНОТУ реестра нот (05-decisions-index.md): каждый заголовок `## D…`/`### D…` живого D-лога и слайсов обязан иметь строку реестра, и наоборот (новая нота = новая строка ТЕМ ЖЕ коммитом). @@ -29,8 +31,10 @@ (--check/голова — D39.112; полнота реестра нот и --lint якорей — D39.126, строка 167). Красный --check или --lint НЕ обходить и не «чинить» подгонкой доков/реестра под зелень — это тот же запрет, что подгонка тестов (D39.121/CLAUDE.md). Непонятно, мешает или кажется неправым → пинг оркестратору -через владельца. Блокировать он и не может: pre-commit хук зовёт только --check и ПРЕДУПРЕЖДАЕТ, ---lint запускается руками; препятствие, которое хочется обойти, — повод для пинга, не для обхода. +через владельца. Блокировать он и не может: pre-commit хук зовёт --check и --lint (оба `--from-index`) +и ТОЛЬКО ПРЕДУПРЕЖДАЕТ, выходя нулём — ⚠ прежняя редакция этой строки говорила «--lint запускается +руками», это снято ревизией D39.148 (довод: ручной прогон жил только в промте оркестратора, а ловил +реальные протухшие якоря); препятствие, которое хочется обойти, — повод для пинга, не для обхода. """ from __future__ import annotations @@ -62,14 +66,58 @@ NOTE = re.compile(r"^## (D(\d+)\.(\d+))") # эра не прибита к 39 FROM_INDEX = "--from-index" in sys.argv +_STAGED: list[str] | None = None + + +def staged_files() -> list[str]: + """Состав коммита — ОДНИМ вызовом на процесс. + + Кэш здесь не микро-оптимизация: линт обходит ~90 доков, и вызов на каждый давал бы под две + сотни сабпроцессов на коммит (поймано ревью при вплетении --lint в хук, D39.148). + ⚠ При ЧАСТИЧНОМ коммите (`git commit -- <пути>`) git строит временный индекс и отдаёт его хуку + через GIT_INDEX_FILE, поэтому `git diff --cached` ВНУТРИ хука показывает именно состав коммита, + а не то, что лежало в настоящем индексе. Тот же GIT_INDEX_FILE наследуется этим процессом, так + что и `git show :файл` ниже читает ту же временную картину. Прогон скрипта РУКАМИ, вне коммита, + видит настоящий индекс — это другая среда, и на ней выводы о поведении хука не строятся. + """ + global _STAGED + if _STAGED is None: + _STAGED = subprocess.run( + ["git", "diff", "--cached", "--name-only"], cwd=ROOT, capture_output=True, text=True + ).stdout.split() + return _STAGED + + +_WT: dict[str, str] | None = None + + +def worktree_state() -> dict[str, str]: + """Путь → 'modified' | 'untracked' для файлов, чьё дерево может расходиться с HEAD. + + Считается ОДИН раз на процесс. Чего в словаре НЕТ — то чисто, и его дерево побайтно равно HEAD; + такой файл читается с диска без сабпроцесса. + ⚠ Двумя plumbing-командами, по одному пути на строку, а НЕ `git status --porcelain`: у того на + переименовании запись несёт ПАРУ путей, и наивный разбор принял бы второй за отдельную строку + с мусорным кодом состояния. + """ + global _WT + if _WT is None: + def lines(*args: str) -> list[str]: + return subprocess.run( + ["git", *args], cwd=ROOT, capture_output=True, text=True + ).stdout.splitlines() + + _WT = {p: "modified" for p in lines("diff", "--name-only", "--no-renames", "HEAD")} + for p in lines("ls-files", "--others", "--exclude-standard"): + _WT[p] = "untracked" + return _WT + + 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}" + ref = f":{rel}" if rel in staged_files() else f"HEAD:{rel}" got = subprocess.run(["git", "show", ref], cwd=ROOT, capture_output=True, text=True) if got.returncode != 0: # Новый файл вне индекса или отсутствующий в HEAD — читаем дерево, но говорим об этом. @@ -234,6 +282,37 @@ LINT_SKIP_DOCS = { } +def doc_text(rel: str) -> str | None: + """Текст СКАНИРУЕМОГО ДОКА для линта. + + Под `--from-index` — то, что поедет в коммит (индекс для застейдженного, HEAD для прочих): + иначе чужой незакоммиченный WIP давал бы ложные тревоги — в `docs/experiments/` постоянно живёт + рабочее дерево полигона, а хук, который врёт, учат игнорировать (D39.148). + `None` = файла нет ни в коммите, ни в HEAD (untracked чужой) — такой док не судим вовсе. + ⚠ ЦЕЛИ якорей (`backend/*.go` и т.п.) читаются НЕ здесь, а из рабочего дерева, и это осознанно: + код не входит в docs-коммит, и его актуальная истина — дерево, а не HEAD. + """ + if not FROM_INDEX: + return (ROOT / rel).read_text(encoding="utf-8") + if rel in staged_files(): + ref = f":{rel}" + else: + state = worktree_state().get(rel) + if state is None: + # Чистый отслеживаемый файл: дерево побайтно равно HEAD, читаем с диска БЕЗ сабпроцесса. + # На 87 сканируемых доках это разница между 87 вызовами `git show` и единицами. + # ⚠ Файла может не быть вовсе (снесён в дереве; в списке целей есть жёсткая позиция + # CLAUDE.md) — тогда судить нечего, а НЕ падать: гейт, падающий вместо вердикта, даёт + # предупреждение с пустым телом (поймано тест-коммитом при вплетении, D39.148). + path = ROOT / rel + return path.read_text(encoding="utf-8") if path.is_file() else None + if state == "untracked": + return None # чужой untracked док — не в коммите и не в HEAD, не судим + ref = f"HEAD:{rel}" + got = subprocess.run(["git", "show", ref], cwd=ROOT, capture_output=True, text=True) + return got.stdout if got.returncode == 0 else None + + def lint_anchors() -> list[str]: bad = [] targets = [ROOT / "CLAUDE.md"] + [ @@ -242,7 +321,10 @@ def lint_anchors() -> list[str]: ] line_counts: dict[Path, int] = {} for doc in sorted(targets): - for n, line in enumerate(doc.read_text(encoding="utf-8").splitlines(), 1): + text = doc_text(str(doc.relative_to(ROOT))) + if text is None: + continue + for n, line in enumerate(text.splitlines(), 1): for m in ANCHOR.finditer(line): rel, lineno = m.group(1).lstrip("/"), int(m.group(2)) if "~" in rel or "/" not in rel or "..." in rel: diff --git a/docs/scripts/githooks/pre-commit b/docs/scripts/githooks/pre-commit index 8c47fce2..178923b5 100755 --- a/docs/scripts/githooks/pre-commit +++ b/docs/scripts/githooks/pre-commit @@ -3,7 +3,8 @@ # фрагмент фронта (диспетчер перебирает */scripts/githooks/pre-commit). # # ┌─ ЧТО ЭТО ДЕЛАЕТ И ЧЕГО НЕ ДЕЛАЕТ — читать до того, как ругаться на него ───────────────────┐ -# │ ДЕЛАЕТ: при коммите, задевающем D-лог или PROGRESS, сверяет три носителя номера головы и │ +# │ ДЕЛАЕТ: при коммите, задевающем ЛЮБОЙ док, проверяет file:line-якоря живых доков; при │ +# │ коммите с D-логом или PROGRESS дополнительно сверяет три носителя номера головы и │ # │ производные числа доков (счёт очереди, зон, вес открытых строк регистра платформы). │ # │ НЕ ДЕЛАЕТ: не блокирует коммит НИКОГДА — только печатает предупреждение и выходит 0. │ # │ Не трогает содержимое, не правит файлы, не лезет в сеть, не запускает тесты. │ @@ -22,7 +23,10 @@ # частичный стейдж (своя правка в общем файле) давал бы ругань на то, что в коммит не поедет; # · голова сверяется ТОЛЬКО когда в коммите есть D-лог, то есть когда идёт ратификация — норма # требует бампать голову в том же коммите, что аппенд ноты, и вот этот случай и ловится; -# коммит, который правит PROGRESS по другому поводу, про голову не спрашивают. +# коммит, который правит PROGRESS по другому поводу, про голову не спрашивают; +# · линт якорей (D39.148) читает СКАНИРУЕМЫЕ доки по содержимому коммита — иначе незакоммиченное +# дерево живого полигона в docs/experiments/ давало бы ругань на чужой WIP; ЦЕЛИ якорей берутся +# из дерева осознанно: код не входит в docs-коммит, и его истина — дерево. staged=$(git diff --cached --name-only) [ -z "$staged" ] && exit 0 @@ -30,8 +34,11 @@ 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 +# Быстрый выход: коммит не задевает документацию — нам тут нечего делать. Триггер ШИРЕ носителей +# чисел (D39.148): якоря `file:line` живут во ВСЕХ доках, включая промты сессий, поэтому линт обязан +# видеть и промт-коммит. Полигонский фриз-коммит, задевший docs/experiments/, изредка увидит +# предупреждение — приемлемо: оно warn-only и самоописано. +printf '%s\n' "$staged" | grep -qE '^(docs/.*|CLAUDE\.md)$' || exit 0 warn=0 @@ -55,12 +62,30 @@ if printf '%s\n' "$staged" | grep -qx "$dlog"; then fi fi -# 2) Производные числа — по содержимому коммита. Ненайденный литерал скрипт тоже считает -# расхождением: проверка, молча перестающая проверять, хуже отсутствующей. +# 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 --check --from-index 2>&1); then - echo "pre-commit ⚠ docs: производные числа разошлись с пере-счётом:" >&2 - printf '%s\n' "$out" | grep '✗' >&2 + 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