feat: хуки переносимы за пределы python, строгий формат TODO(#N) #10

Merged
claude-secretary merged 3 commits from feature/hooks-8-9 into main 2026-08-26 14:32:15 +07:00

Закрывает #8 и #9. Оба дефекта нашлись при раскатке общих хуков на репозитории jamzap: часть хуков молча не работала, часть падала на том, что нарушением не является.

no-broken-repo-paths: служебные каталоги больше не ссылки (#9)

.git/ содержимым репозитория не является ни в одном репозитории, но существует всегда — поэтому упоминание рантайм-файла .git/config.lock в комментарии про конкурентный доступ считалось висячей ссылкой. Гейт падал на коде, который чинить не нужно, а обойти это можно было только exclude-ом на весь файл.

Для остального спрашиваем сам git: путь под .gitignore — артефакт (node_modules/, dist/, .venv/), и его отсутствие нарушением не является. Своего списка имён не держим: у каждого потребителя он свой и уже описан в его .gitignore.

no-issue-refs-in-comments: работает вне python (#8)

Хук искал комментарий только по символу #, а в определении нёс types: [python]. В TS-репозитории он не находил ни одного комментария и молча проходил — это хуже отсутствия хука: конфиг заявляет гейт, которого нет.

  • синтаксис выбирается по расширению: // и /* */ для C-подобных, # для остальных;
  • types снят — скоуп задаёт потребитель через files:, как у no-broken-repo-paths;
  • выбор по расширению, а не «все сразу», намеренный: в .ts символ # начинает приватное поле класса (#count);
  • строковые литералы, включая шаблонные, комментарием не считаются — "https://example.com" не выглядит началом //-комментария;
  • блочный комментарий переносит состояние между строками.

todo-needs-issue: только голый номер (#8)

Внутренний [^)]* пропускал что угодно перед номером, поэтому TODO(jamzap/backend#402) проходил наравне с TODO(#402). Формат теперь строгий: TODO(#N), несколько ссылок через запятую — TODO(#12, #34).

TODO живёт внутри своего репозитория, кросс-проектных не заводим: зависимость от чужой задачи оформляется своей issue, а она уже ссылается на чужую — так связь видна в трекере, а не только в комментарии.

Исключение для TODO внутри no-issue-refs-in-comments сужено до того же формата, чтобы оба хука говорили о префиксе одно и то же. Оговорка: кросс-проектную форму TODO(ns/proj#N) сам no-issue-refs-in-comments не ловит — там #N стоит вплотную к букве и под #NNN не подпадает. Расширять ISSUE_REF_RE не стал: это вышло бы за рамки задачи и потянуло ложные срабатывания на URL-фрагментах. Формат ловит todo-needs-issue.

Обратная совместимость

У потребителей с префиксами обновление rev: покрасит TODO на первом же касании файла. Это ожидаемо и лечится переписыванием пунктуации; в jamzap уже сделано (jamzap/mobile#158 — 11 TODO приведены, кросс-реповые заменены зеркальными задачами).

Версия поднята до 0.11.0.

Проверки

bash fixtures/run_smoke.sh — все кейсы зелёные. Добавлено семь: TS-комментарии (чистый, //-нарушитель, блочный многострочный), сузившееся исключение TODO, служебные каталоги, служебные каталоги плюс настоящая висячая ссылка, строгий формат TODO (префикс отвергается, запятая принимается).

Каждая правка проверена мутацией — снятие любой роняет свой кейс:

мутация падает
убрать отсечку .git broken_paths_service_paths_ok
убрать проверку git-ignored broken_paths_service_paths_ok
вернуть только #-синтаксис issue_refs_good_ts
вернуть широкий TODO_RE issue_refs_loose_todo_fail

Кейс на служебные каталоги строит временное репо, а не использует обычную fixture: в linked worktree .git — файл, а не каталог, и проверка проходила бы независимо от правки. Первая версия этого кейса именно так и обманывала — поймал при мутационной проверке.

Отдельно проверено, что отсечка служебных каталогов не глушит хук целиком: файл с теми же путями плюс настоящая висячая ссылка по-прежнему падает.

Закрывает #8 и #9. Оба дефекта нашлись при раскатке общих хуков на репозитории jamzap: часть хуков молча не работала, часть падала на том, что нарушением не является. ## `no-broken-repo-paths`: служебные каталоги больше не ссылки (#9) `.git/` содержимым репозитория не является ни в одном репозитории, но существует всегда — поэтому упоминание рантайм-файла `.git/config.lock` в комментарии про конкурентный доступ считалось висячей ссылкой. Гейт падал на коде, который чинить не нужно, а обойти это можно было только `exclude`-ом на весь файл. Для остального спрашиваем сам git: путь под `.gitignore` — артефакт (`node_modules/`, `dist/`, `.venv/`), и его отсутствие нарушением не является. Своего списка имён не держим: у каждого потребителя он свой и уже описан в его `.gitignore`. ## `no-issue-refs-in-comments`: работает вне python (#8) Хук искал комментарий только по символу `#`, а в определении нёс `types: [python]`. В TS-репозитории он не находил ни одного комментария и **молча проходил** — это хуже отсутствия хука: конфиг заявляет гейт, которого нет. - синтаксис выбирается по расширению: `//` и `/* */` для C-подобных, `#` для остальных; - `types` снят — скоуп задаёт потребитель через `files:`, как у `no-broken-repo-paths`; - выбор по расширению, а не «все сразу», намеренный: в `.ts` символ `#` начинает приватное поле класса (`#count`); - строковые литералы, включая шаблонные, комментарием не считаются — `"https://example.com"` не выглядит началом `//`-комментария; - блочный комментарий переносит состояние между строками. ## `todo-needs-issue`: только голый номер (#8) Внутренний `[^)]*` пропускал что угодно перед номером, поэтому `TODO(jamzap/backend#402)` проходил наравне с `TODO(#402)`. Формат теперь строгий: `TODO(#N)`, несколько ссылок через запятую — `TODO(#12, #34)`. TODO живёт внутри своего репозитория, кросс-проектных не заводим: зависимость от чужой задачи оформляется своей issue, а она уже ссылается на чужую — так связь видна в трекере, а не только в комментарии. Исключение для TODO внутри `no-issue-refs-in-comments` сужено до того же формата, чтобы оба хука говорили о префиксе одно и то же. Оговорка: кросс-проектную форму `TODO(ns/proj#N)` сам `no-issue-refs-in-comments` не ловит — там `#N` стоит вплотную к букве и под `#NNN` не подпадает. Расширять `ISSUE_REF_RE` не стал: это вышло бы за рамки задачи и потянуло ложные срабатывания на URL-фрагментах. Формат ловит `todo-needs-issue`. ## Обратная совместимость У потребителей с префиксами обновление `rev:` покрасит TODO на первом же касании файла. Это ожидаемо и лечится переписыванием пунктуации; в jamzap уже сделано (`jamzap/mobile#158` — 11 TODO приведены, кросс-реповые заменены зеркальными задачами). Версия поднята до `0.11.0`. ## Проверки `bash fixtures/run_smoke.sh` — все кейсы зелёные. Добавлено семь: TS-комментарии (чистый, `//`-нарушитель, блочный многострочный), сузившееся исключение TODO, служебные каталоги, служебные каталоги плюс настоящая висячая ссылка, строгий формат TODO (префикс отвергается, запятая принимается). Каждая правка проверена мутацией — снятие любой роняет свой кейс: | мутация | падает | |---|---| | убрать отсечку `.git` | `broken_paths_service_paths_ok` | | убрать проверку git-ignored | `broken_paths_service_paths_ok` | | вернуть только `#`-синтаксис | `issue_refs_good_ts` | | вернуть широкий `TODO_RE` | `issue_refs_loose_todo_fail` | Кейс на служебные каталоги строит временное репо, а не использует обычную fixture: в linked worktree `.git` — файл, а не каталог, и проверка проходила бы независимо от правки. Первая версия этого кейса именно так и обманывала — поймал при мутационной проверке. Отдельно проверено, что отсечка служебных каталогов не глушит хук целиком: файл с теми же путями плюс настоящая висячая ссылка по-прежнему падает.
feat: хуки переносимы за пределы python, строгий формат TODO(#N)
Some checks failed
ci / smoke (push) Has been cancelled
ci / smoke (pull_request) Has been cancelled
1d63e811dd
Закрывает senokosov/pre-commit-hooks#8 и #9. Оба нашлись при раскатке
общих хуков на репозитории jamzap: часть из них молча не работала, часть
падала на том, что нарушением не является.

no-broken-repo-paths: служебные каталоги больше не ссылки (#9)

`.git/` содержимым репозитория не является ни в одном репозитории, но
существует всегда — поэтому упоминание рантайм-файла `.git/config.lock`
в комментарии про конкурентный доступ считалось висячей ссылкой и роняло
гейт на коде, который чинить не нужно. Для остального спрашиваем сам git:
путь под `.gitignore` — артефакт (`node_modules/`, `dist/`, `.venv/`), и
его отсутствие нарушением не является. Своего списка имён не держим: у
каждого потребителя он свой и уже описан в его `.gitignore`.

no-issue-refs-in-comments: работает вне python (#8)

Хук искал комментарий только по `#`, а в определении нёс `types: [python]`.
В TS-репозитории он не находил ни одного комментария и **молча проходил** —
хуже, чем отсутствие хука: конфиг заявлял гейт, которого нет. Теперь
синтаксис выбирается по расширению файла (`//` и `/* */` для C-подобных),
`types` снят — скоуп задаёт потребитель через `files:`, как у
no-broken-repo-paths. Выбор по расширению, а не «все сразу», намеренный:
в `.ts` символ `#` начинает приватное поле класса. Строковые литералы,
включая шаблонные, комментарием не считаются.

todo-needs-issue: только голый номер (#8)

Внутренний `[^)]*` пропускал что угодно перед номером, поэтому
`TODO(jamzap/backend#402)` проходил наравне с `TODO(#402)`. Формат теперь
строгий: `TODO(#N)`, несколько ссылок через запятую. TODO живёт внутри
своего репозитория, кросс-проектных не заводим — зависимость от чужой
задачи оформляется своей issue, а она уже ссылается на чужую. Исключение
для TODO внутри no-issue-refs-in-comments сужено до того же формата, чтобы
оба хука говорили о префиксе одно и то же.

Обратная совместимость: у потребителей с префиксами обновление `rev:`
покрасит их на первом же касании файла. Это ожидаемо и лечится
переписыванием пунктуации; в jamzap уже сделано.

Все три правки покрыты smoke-кейсами и проверены мутацией: снятие любой
роняет свой кейс. Кейс на служебные каталоги строит временное репо —
в linked worktree `.git` это файл, а не каталог, и проверка на обычной
fixture проходила бы независимо от правки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix: покрыть манифест тестами, спрашивать check-ignore от корня
Some checks failed
ci / smoke (push) Has been cancelled
ci / smoke (pull_request) Has been cancelled
e7976f5500
По результатам независимого ревью PR #10.

B1. Обе правки в `.pre-commit-hooks.yaml` не были покрыты: smoke зовёт
скрипты напрямую, минуя фильтр `types` pre-commit'а, а регулярку
`todo-needs-issue` дублировал литералом. Откат обеих оставлял набор
зелёным — то есть несущая часть фикса #8 (снятие `types: [python]`)
регрессировала бы незаметно. Теперь smoke читает поля из манифеста как из
источника правды и отдельным кейсом стережёт отсутствие `types`.

Заодно вскрылось, что fixtures/todo/prefixed.py матчился по слову-маркеру
в собственном докстринге, а не по проверяемой строке: кейс проходил бы при
любой регулярке. Докстринг переформулирован.

B2. `git check-ignore` наследовал cwd процесса, тогда как существование
пути проверяется относительно `_REPO_ROOT`. Два базиса для одного пути:
при запуске из подкаталога настоящая висячая ссылка тихо пропадала, а
легально игнорируемый артефакт наоборот флагался. Под самим pre-commit
cwd всегда корень, поэтому в бою не стреляло, но контракт «резолв от
git-корня, а не от cwd» в репозитории уже зафиксирован кейсом
broken_paths_bad_from_subdir_fail, и правка #9 его частично отменяла.

R5. Из SLASH_SUFFIXES убраны .css/.scss/.less: hex-цвет в комментарии
(`// раньше был #336699`) неотличим от ссылки на задачу, а стили в
задачу #8 и не входили — новый класс ложных срабатываний не нужен.

R7. Явная кодировка при чтении файла, как у соседнего хука: под
LC_ALL=C падение декодирования глушило хук в тихий проход.

R9. Политика версий описывала намерение, а не практику: репозиторий с
самого начала вводит fail-условия в minor. Оговорка про 0.x дописана,
иначе README противоречит сам себе.

R10, R12. `rev:` в README и примере подтянуты к текущей версии;
комментарий в smoke указывал не на тот путь.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Owner

Итерация 2 — правки по ревью

Оба блокера закрыты, плюс четыре замечания, которые дешевле починить, чем откладывать.

B1 — манифест не был покрыт. Smoke зовёт скрипты напрямую, минуя фильтр types pre-commit'а, а регулярку todo-needs-issue дублировал литералом. Откат обеих правок в .pre-commit-hooks.yaml оставлял набор зелёным — то есть несущая часть фикса #8 регрессировала бы незаметно. Теперь hook_field читает поля из манифеста (только stdlib, в CI нет PyYAML), а отсутствие types у no-issue-refs-in-comments стережёт отдельный кейс.

Побочно вскрылось неприятное: fixtures/todo/prefixed.py матчился по слову-маркеру в собственном докстринге, а не по проверяемой строке — кейс проходил бы при любой регулярке. Докстринг переформулирован; после этого откат entry: кейс роняет.

B2 — два базиса для одного пути. git check-ignore наследовал cwd процесса, а существование пути проверялось от _REPO_ROOT. При запуске из подкаталога настоящая висячая ссылка тихо пропадала, а легально игнорируемый артефакт наоборот флагался. Под самим pre-commit cwd всегда корень, поэтому в бою не стреляло — но контракт «резолв от git-корня, а не от cwd» в репозитории уже зафиксирован кейсом broken_paths_bad_from_subdir_fail, и правка #9 его частично отменяла. Добавлен кейс с .gitignore в подкаталоге: мутация ловится.

Взято из замечаний:

  • R5.css/.scss/.less убраны из SLASH_SUFFIXES. Hex-цвет в комментарии (// раньше был #336699) неотличим от ссылки на задачу, а стили в #8 и не входили: новый класс ложняков не нужен.
  • R7 — явная кодировка при чтении, как у соседнего хука; под LC_ALL=C падение декодирования глушило хук в тихий проход.
  • R9 — политика версий описывала намерение, а не практику: репозиторий с самого начала вводит fail-условия в minor (v0.4.0, v0.7.0, v0.9.0). Дописана оговорка про 0.x и прямое следствие для потребителя — бамп minor может покрасить код, обновлять rev: стоит отдельным PR.
  • R10, R12rev: в README и в примере подтянуты к текущей версии; комментарий в smoke указывал не на тот путь.

Вынесено в #11: построчный разбор комментариев (многострочный шаблонный литерал, второй комментарий в строке, апостроф в JSX), непокрытая обработка строковых литералов, #-режим для разметки, зависимость вердикта от .git/info/exclude, асимметрия пробелов в TODO. Чинить их стоит не по одному, а переиспользованием машины состояний из no-broken-repo-paths — сейчас логика дублирована в двух скриптах с разной зрелостью.

Мутационная проверка после правок — каждая роняет свой кейс:

мутация падает
вернуть types: [python] в манифест manifest_no_issue_refs_has_no_types
откатить entry: у todo-needs-issue todo_needs_issue_prefixed_match
check-ignore в cwd процесса broken_paths_ignore_asked_from_repo_root_fail
убрать отсечку .git broken_paths_service_paths_ok
убрать проверку git-ignored broken_paths_service_paths_ok
только #-синтаксис issue_refs_good_ts
убрать обработку /* и перенос блока issue_refs_block_multiline_ts_fail
вернуть широкий TODO_RE issue_refs_loose_todo_fail

bash fixtures/run_smoke.sh — зелёный.

Ограничение ревью, которое стоит знать: у ревьюера не было доступа к телам #8 и #9 (в его окружении нет forgejo-MCP), он сверялся с описанием PR и коммита. Пункты приёмки из тел issue я перечитал сам — расхождений с реализацией нет.

## Итерация 2 — правки по ревью Оба блокера закрыты, плюс четыре замечания, которые дешевле починить, чем откладывать. **B1 — манифест не был покрыт.** Smoke зовёт скрипты напрямую, минуя фильтр `types` pre-commit'а, а регулярку `todo-needs-issue` дублировал литералом. Откат обеих правок в `.pre-commit-hooks.yaml` оставлял набор зелёным — то есть несущая часть фикса #8 регрессировала бы незаметно. Теперь `hook_field` читает поля из манифеста (только stdlib, в CI нет PyYAML), а отсутствие `types` у `no-issue-refs-in-comments` стережёт отдельный кейс. Побочно вскрылось неприятное: `fixtures/todo/prefixed.py` матчился по слову-маркеру в **собственном докстринге**, а не по проверяемой строке — кейс проходил бы при любой регулярке. Докстринг переформулирован; после этого откат `entry:` кейс роняет. **B2 — два базиса для одного пути.** `git check-ignore` наследовал cwd процесса, а существование пути проверялось от `_REPO_ROOT`. При запуске из подкаталога настоящая висячая ссылка тихо пропадала, а легально игнорируемый артефакт наоборот флагался. Под самим pre-commit cwd всегда корень, поэтому в бою не стреляло — но контракт «резолв от git-корня, а не от cwd» в репозитории уже зафиксирован кейсом `broken_paths_bad_from_subdir_fail`, и правка #9 его частично отменяла. Добавлен кейс с `.gitignore` в подкаталоге: мутация ловится. **Взято из замечаний:** - **R5** — `.css/.scss/.less` убраны из `SLASH_SUFFIXES`. Hex-цвет в комментарии (`// раньше был #336699`) неотличим от ссылки на задачу, а стили в #8 и не входили: новый класс ложняков не нужен. - **R7** — явная кодировка при чтении, как у соседнего хука; под `LC_ALL=C` падение декодирования глушило хук в тихий проход. - **R9** — политика версий описывала намерение, а не практику: репозиторий с самого начала вводит fail-условия в minor (`v0.4.0`, `v0.7.0`, `v0.9.0`). Дописана оговорка про `0.x` и прямое следствие для потребителя — бамп minor может покрасить код, обновлять `rev:` стоит отдельным PR. - **R10, R12** — `rev:` в README и в примере подтянуты к текущей версии; комментарий в smoke указывал не на тот путь. **Вынесено в #11:** построчный разбор комментариев (многострочный шаблонный литерал, второй комментарий в строке, апостроф в JSX), непокрытая обработка строковых литералов, `#`-режим для разметки, зависимость вердикта от `.git/info/exclude`, асимметрия пробелов в `TODO`. Чинить их стоит не по одному, а переиспользованием машины состояний из `no-broken-repo-paths` — сейчас логика дублирована в двух скриптах с разной зрелостью. **Мутационная проверка после правок** — каждая роняет свой кейс: | мутация | падает | |---|---| | вернуть `types: [python]` в манифест | `manifest_no_issue_refs_has_no_types` | | откатить `entry:` у `todo-needs-issue` | `todo_needs_issue_prefixed_match` | | `check-ignore` в cwd процесса | `broken_paths_ignore_asked_from_repo_root_fail` | | убрать отсечку `.git` | `broken_paths_service_paths_ok` | | убрать проверку git-ignored | `broken_paths_service_paths_ok` | | только `#`-синтаксис | `issue_refs_good_ts` | | убрать обработку `/*` и перенос блока | `issue_refs_block_multiline_ts_fail` | | вернуть широкий `TODO_RE` | `issue_refs_loose_todo_fail` | `bash fixtures/run_smoke.sh` — зелёный. Ограничение ревью, которое стоит знать: у ревьюера не было доступа к телам #8 и #9 (в его окружении нет forgejo-MCP), он сверялся с описанием PR и коммита. Пункты приёмки из тел issue я перечитал сам — расхождений с реализацией нет.
ci: раннеры homedevlab отвечают на forgejo-runner, не ubuntu-latest
All checks were successful
ci / smoke (push) Successful in 5s
ci / smoke (pull_request) Successful in 5s
6bbcd59be0
Задание с runs-on: ubuntu-latest не совпадает ни с одним раннером флота
(все три зарегистрированы с лейблом forgejo-runner) и молча висит в
waiting, пока следующий push его не отменит. Отсюда цепочка cancelled в
истории: последний успешный прогон здесь — 19.05.2026, то есть гейт,
который README обещает как условие merge, три месяца не работал.

Лейбл приведён к конвенции флота — так же в senokosov/baton.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-secretary deleted branch feature/hooks-8-9 2026-08-26 14:32:15 +07:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
senokosov/pre-commit-hooks!10
No description provided.