feat(max-filename-words): хук потолка слов в имени модуля (grandfather от merge-base) #7

Merged
claude-secretary merged 4 commits from feature/max-filename-words into main 2026-08-27 11:56:27 +07:00

Реализация ADR-0025 §7 (senokosov/baton#1307). Новый хук max-filename-words рядом с no-underscore-filenames.

Что делает

Имя .py-модуля не длиннее N значимых слов (env MAX_FILENAME_WORDS, дефолт 2). Стем без .py, ведущий test_ отброшен, split по _, пустые сегменты не в счёт. Истинный dunder (__init__.py, …) и conftest.py — skip. Меряет число слов, не символов: taskinstance ок, org_repo_host_config — нет.

Grandfather от базовой ревизии (тот же механизм, что у check-file-length с v0.13.0): FAIL только на файле, НОВОМ относительно базы (добавлен или переименован-в-длинное). Уже существующий там длинный файл — WARN(grandfather), коммит разрешён. База зависит от способа вызова: в CI-инвокации pre-commit run --from-ref X --to-ref Y — merge-base X и Y, в обычном commit-хуке и при --all-filesHEAD. Вне git — FAIL. Скоуп и точечные исключения — files:/exclude: у consumer'а.

Файлы

  • scripts/check_max_filename_words.py + console-script check-max-filename-words в pyproject.toml
  • .pre-commit-hooks.yaml: id: max-filename-words
  • fixtures/filename_words/ + smoke-кейсы, fixtures/check_grandfather_baseline.sh + 4 кейса CI-инвокации
  • README.md, examples/.pre-commit-config.example.yaml: секция хука, пример конфига, пин v0.14.0

Что изменилось после первого ревью

Первая редакция брала за базу HEAD и в CI отдавала зелёный вхолостую. В инвокации pre-commit run --from-ref/--to-ref (так гоняют все консьюмеры) рабочее дерево уже стоит на проверяемом коммите, поэтому HEAD:path существует у любого добавленного веткой файла: grandfather срабатывал всегда, и новый длинноимённый модуль проезжал зелёным — то есть хук не делал ровно того, ради чего заведён. Тот же класс бага, что вылечен в #15 для check-file-length; здесь он пойман до релиза.

Smoke этого не видит по построению — он зовёт скрипт напрямую списком файлов, где HEAD и так «старая» версия. Поэтому проверка живёт в fixtures/check_grandfather_baseline.sh, который поднимает настоящий репозиторий и зовёт pre-commit как CI: новый длинноимённый файл, правка legacy-модуля без переименования, переименование короткого имени в длинное, локальный commit-хук. Возврат базы на HEAD роняет два кейса из четырёх.

По второму ревью: .*/alembic/versions/.* в примере конфига не исключал плоскую раскладку src/alembic/versions/... (имя ревизии автогенерируется многословным, а файл миграции всегда новый — grandfather не спасает никогда), поправлено на (.*/)?…; пофайловые WARN печатаются перед сводкой; мусор в MAX_FILENAME_WORDS даёт вердикт, а не трейсбек; кейс dunder-скипа перестал быть вакуумным; words-кейсы гейта ветвятся от собственной базы.

Верификация

  • bash fixtures/run_smoke.sh — все кейсы зелёные (16 в секции хука).
  • bash fixtures/check_grandfather_baseline.sh — 12 кейсов, включая 4 новых; мутация (база обратно на HEAD) роняет ровно ci_new_long_name_fails и ci_rename_to_long_fails.
  • python3 fixtures/check_wheel_matches_sources.py — колесо несёт текущие исходники.
  • Прогон по дереву baton (394 файла backend/core + backend/modules) → exit=0: grandfather не валит существующий долг.

Версия

0.13.00.14.0: новый хук, minor.

После мержа — за @volody (релизные шаги)

Тег v0.14.0 и его push. Отдельно: на remote нет тегов v0.10.0v0.13.0, хотя README пинит rev: v0.13.0 — консьюмер такой пин не зарезолвит. Заведено отдельной задачей.

В senokosov/baton — bump rev: и регистрация инстанса max-filename-words со скоупом под текущую раскладку (backend/core/backend/modules, exclude db/models/alembic/versions/proto/generated) отдельным PR, по команде. Скоуп из тела baton#1307 (backend/baton) устарел после модульного рефактора.

Реализация ADR-0025 §7 ([senokosov/baton#1307](https://git.homedevlab.ru/senokosov/baton/issues/1307)). Новый хук `max-filename-words` рядом с `no-underscore-filenames`. ## Что делает Имя `.py`-модуля не длиннее N значимых слов (env `MAX_FILENAME_WORDS`, дефолт `2`). Стем без `.py`, ведущий `test_` отброшен, split по `_`, пустые сегменты не в счёт. Истинный dunder (`__init__.py`, …) и `conftest.py` — skip. Меряет **число слов, не символов**: `taskinstance` ок, `org_repo_host_config` — нет. **Grandfather от базовой ревизии** (тот же механизм, что у `check-file-length` с `v0.13.0`): FAIL только на файле, НОВОМ относительно базы (добавлен или переименован-в-длинное). Уже существующий там длинный файл — `WARN(grandfather)`, коммит разрешён. База зависит от способа вызова: в CI-инвокации `pre-commit run --from-ref X --to-ref Y` — merge-base `X` и `Y`, в обычном commit-хуке и при `--all-files` — `HEAD`. Вне git — FAIL. Скоуп и точечные исключения — `files:`/`exclude:` у consumer'а. ## Файлы - `scripts/check_max_filename_words.py` + console-script `check-max-filename-words` в `pyproject.toml` - `.pre-commit-hooks.yaml`: `id: max-filename-words` - `fixtures/filename_words/` + smoke-кейсы, `fixtures/check_grandfather_baseline.sh` + 4 кейса CI-инвокации - `README.md`, `examples/.pre-commit-config.example.yaml`: секция хука, пример конфига, пин `v0.14.0` ## Что изменилось после первого ревью **Первая редакция брала за базу `HEAD` и в CI отдавала зелёный вхолостую.** В инвокации `pre-commit run --from-ref/--to-ref` (так гоняют все консьюмеры) рабочее дерево уже стоит на проверяемом коммите, поэтому `HEAD:path` существует у **любого** добавленного веткой файла: grandfather срабатывал всегда, и новый длинноимённый модуль проезжал зелёным — то есть хук не делал ровно того, ради чего заведён. Тот же класс бага, что вылечен в [#15](https://git.homedevlab.ru/senokosov/pre-commit-hooks/pulls/15) для `check-file-length`; здесь он пойман до релиза. Smoke этого не видит по построению — он зовёт скрипт напрямую списком файлов, где `HEAD` и так «старая» версия. Поэтому проверка живёт в `fixtures/check_grandfather_baseline.sh`, который поднимает настоящий репозиторий и зовёт `pre-commit` как CI: новый длинноимённый файл, правка legacy-модуля без переименования, переименование короткого имени в длинное, локальный commit-хук. Возврат базы на `HEAD` роняет два кейса из четырёх. По второму ревью: `.*/alembic/versions/.*` в примере конфига не исключал плоскую раскладку `src/alembic/versions/...` (имя ревизии автогенерируется многословным, а файл миграции всегда новый — grandfather не спасает никогда), поправлено на `(.*/)?…`; пофайловые WARN печатаются перед сводкой; мусор в `MAX_FILENAME_WORDS` даёт вердикт, а не трейсбек; кейс dunder-скипа перестал быть вакуумным; words-кейсы гейта ветвятся от собственной базы. ## Верификация - `bash fixtures/run_smoke.sh` — все кейсы зелёные (16 в секции хука). - `bash fixtures/check_grandfather_baseline.sh` — 12 кейсов, включая 4 новых; мутация (база обратно на `HEAD`) роняет ровно `ci_new_long_name_fails` и `ci_rename_to_long_fails`. - `python3 fixtures/check_wheel_matches_sources.py` — колесо несёт текущие исходники. - Прогон по дереву baton (394 файла `backend/core` + `backend/modules`) → `exit=0`: grandfather не валит существующий долг. ## Версия `0.13.0` → **`0.14.0`**: новый хук, minor. ## После мержа — за @volody (релизные шаги) Тег `v0.14.0` и его push. Отдельно: на remote нет тегов `v0.10.0`–`v0.13.0`, хотя README пинит `rev: v0.13.0` — консьюмер такой пин не зарезолвит. Заведено отдельной задачей. В `senokosov/baton` — bump `rev:` и регистрация инстанса `max-filename-words` со скоупом под текущую раскладку (`backend/core`/`backend/modules`, exclude `db/models`/`alembic/versions`/`proto/generated`) отдельным PR, по команде. Скоуп из тела baton#1307 (`backend/baton`) устарел после модульного рефактора.
feat(max-filename-words): хук потолка слов в имени модуля (grandfather по HEAD)
Some checks failed
ci / smoke (push) Has been cancelled
ci / smoke (pull_request) Has been cancelled
0939993a7a
Реализация ADR-0025 §7 (senokosov/baton#1307). Имя .py-модуля не длиннее
N значимых слов (env MAX_FILENAME_WORDS, дефолт 2): стем без .py, ведущий
test_ отброшен, split по _. Dunder и conftest.py — skip. FAIL только на
НОВОМ относительно HEAD файле; уже-существующий длинный — WARN(grandfather),
как check-file-length (вне git — FAIL). Скоуп — files/exclude consumer'а.

- scripts/check_max_filename_words.py + console-script в pyproject
- .pre-commit-hooks.yaml: id max-filename-words
- fixtures/filename_words/ + smoke-кейсы (grandfather / new-file FAIL /
  test_-drop / env-порог)
- README: секция хука + пример конфига
fix(max-filename-words): ниты ревью — истинный dunder + exclude в README-примере
Some checks failed
ci / smoke (push) Has been cancelled
ci / smoke (pull_request) Has been cancelled
2b5091bef0
- dunder-скип сужен: только __…__.py (истинный dunder), не любой ведущий __
  (__foo_bar_baz.py теперь проверяется) + smoke-кейс
- README-пример consumer-конфига исключает alembic/versions + proto/generated
  (автоген-имена многословны → иначе гарантированный FAIL у consumer'а)

env-валидация MAX_FILENAME_WORDS оставлена fail-loud (wontfix: мисконфиг,
не рантайм-риск).
Author
Owner

Независимое ревью (субагент, свежий контекст): APPROVE, блокеров нет. Диспозиция нитов:

  • fixed (2b5091b): dunder-скип сужен до истинного __…__.py (не любой ведущий __) + smoke-кейс.
  • fixed (2b5091b): README-пример consumer-конфига исключает alembic/versions + proto/generated (автоген-имена многословны → иначе гарантированный FAIL при копировании примера).
  • wontfix: валидация MAX_FILENAME_WORDS — оставлена fail-loud (нечисловое → ValueError; отрицательное = мисконфиг, не рантайм-риск).

Smoke: 11/11 кейсов зелёные (bash fixtures/run_smoke.sh → All smoke cases passed). Прогон по дереву baton (394 файла) → exit 0, grandfather не валит существующее.

Независимое ревью (субагент, свежий контекст): **APPROVE, блокеров нет**. Диспозиция нитов: - **fixed** (2b5091b): dunder-скип сужен до истинного `__…__.py` (не любой ведущий `__`) + smoke-кейс. - **fixed** (2b5091b): README-пример consumer-конфига исключает `alembic/versions` + `proto/generated` (автоген-имена многословны → иначе гарантированный FAIL при копировании примера). - **wontfix**: валидация `MAX_FILENAME_WORDS` — оставлена fail-loud (нечисловое → `ValueError`; отрицательное = мисконфиг, не рантайм-риск). Smoke: 11/11 кейсов зелёные (`bash fixtures/run_smoke.sh` → All smoke cases passed). Прогон по дереву baton (394 файла) → exit 0, grandfather не валит существующее.
Author
Owner

Тот же класс бага, что чинится в #15 — здесь он ещё не выпущен, поэтому ловим до релиза, а не после.

scripts/check_max_filename_words.py:42 определяет «файл уже был» так:

["git", "cat-file", "-e", f"HEAD:{path}"]

В настоящем git pre-commit-хуке это верно: новый коммит ещё не создан, файла в HEAD нет → FAIL, как и задумано. Но в CI-инвокации (pre-commit run --from-ref X --to-ref Y, так гоняют все четыре консьюмера) рабочее дерево уже стоит на проверяемом коммите, и любой файл, добавленный в MR, в HEAD присутствует. Значит _in_head() вернёт True, сработает grandfather, и новый модуль с длинным именем проедет зелёным — то есть хук не сделает ровно того, ради чего заведён.

Проявляется это только в способе вызова, поэтому текущие смоук-кейсы его не увидят: они дёргают скрипт напрямую списком файлов.

В #15 лечится так: базовая ревизия берётся из PRE_COMMIT_FROM_REF/PRE_COMMIT_TO_REF (pre-commit выставляет их ровно в этом режиме) как merge-base, и HEAD остаётся только там, где он действительно означает «до правки». Merge-base, а не вершина from-ref, — иначе переименование, приехавшее в целевую ветку после ветвления, засчитается текущему MR.

Перед мержем этого PR стоит сделать то же самое, причём после #15 — там появится fixtures/check_grandfather_baseline.sh, который поднимает настоящий репозиторий и зовёт pre-commit как CI; по его образцу проверка этого хука пишется в несколько строк. Заголовок PR («grandfather по HEAD») тогда тоже перестанет быть точным.

Тот же класс бага, что чинится в [#15](https://git.homedevlab.ru/senokosov/pre-commit-hooks/pulls/15) — здесь он ещё не выпущен, поэтому ловим до релиза, а не после. `scripts/check_max_filename_words.py:42` определяет «файл уже был» так: ```python ["git", "cat-file", "-e", f"HEAD:{path}"] ``` В настоящем git pre-commit-хуке это верно: новый коммит ещё не создан, файла в `HEAD` нет → FAIL, как и задумано. Но в CI-инвокации (`pre-commit run --from-ref X --to-ref Y`, так гоняют все четыре консьюмера) рабочее дерево уже стоит на проверяемом коммите, и **любой** файл, добавленный в MR, в `HEAD` присутствует. Значит `_in_head()` вернёт `True`, сработает grandfather, и новый модуль с длинным именем проедет зелёным — то есть хук не сделает ровно того, ради чего заведён. Проявляется это только в способе вызова, поэтому текущие смоук-кейсы его не увидят: они дёргают скрипт напрямую списком файлов. В [#15](https://git.homedevlab.ru/senokosov/pre-commit-hooks/pulls/15) лечится так: базовая ревизия берётся из `PRE_COMMIT_FROM_REF`/`PRE_COMMIT_TO_REF` (pre-commit выставляет их ровно в этом режиме) как merge-base, и `HEAD` остаётся только там, где он действительно означает «до правки». Merge-base, а не вершина from-ref, — иначе переименование, приехавшее в целевую ветку после ветвления, засчитается текущему MR. Перед мержем этого PR стоит сделать то же самое, причём после #15 — там появится `fixtures/check_grandfather_baseline.sh`, который поднимает настоящий репозиторий и зовёт pre-commit как CI; по его образцу проверка этого хука пишется в несколько строк. Заголовок PR («grandfather по HEAD») тогда тоже перестанет быть точным.
claude-secretary force-pushed feature/max-filename-words from 2b5091bef0
Some checks failed
ci / smoke (push) Has been cancelled
ci / smoke (pull_request) Has been cancelled
to 76494a255c
All checks were successful
ci / smoke (push) Successful in 14s
ci / smoke (pull_request) Successful in 14s
2026-08-27 11:44:32 +07:00
Compare
fix(max-filename-words): ниты ревью — exclude в плоской раскладке, вывод, кейсы
All checks were successful
ci / smoke (push) Successful in 14s
ci / smoke (pull_request) Successful in 14s
b686d85c99
- README-пример и examples/: `.*/alembic/versions/.*` не исключал
  `src/alembic/versions/...` (`.*/` требует непустой сегмент). Имя
  ревизии автогенерируется многословным, а файл миграции всегда новый —
  grandfather не спасает, и скопировавший пример консьюмер получал
  блокировку любой новой миграции. Обе формы — `(.*/)?…`.
- Вывод: пофайловые WARN(grandfather) идут перед сводкой (порядок как в
  check_file_length.sh — иначе при наличии fails первым читается «коммит
  разрешён»), склонение «2 слова» / «5 слов».
- Мусор и ноль в MAX_FILENAME_WORDS дают вердикт, а не трейсбек.
- Smoke: кейс dunder-скипа был вакуумным (закоммиченный `__init__.py`
  прошёл бы и без skip'а), теперь untracked `__foo_bar_baz__.py` сверх
  порога; FAIL-кейсы пиннят подстроку вердикта, а не только exit=1.
- Гейт: words-кейсы ветвятся от собственной `words-target`, а не от
  общей `target`, которую двигают file-length-кейсы выше.
- README: счётчики хуков в «Структуре репы» и перенос grandfathered-
  модуля в другой каталог.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-secretary changed title from feat(max-filename-words): хук потолка слов в имени модуля (grandfather по HEAD) to feat(max-filename-words): хук потолка слов в имени модуля (grandfather от merge-base) 2026-08-27 11:55:34 +07:00
claude-secretary deleted branch feature/max-filename-words 2026-08-27 11:56:27 +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!7
No description provided.