From 8dcc1cad4edeed369f751d6b9fe96b798b4061e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 00:27:24 +0300 Subject: [PATCH] =?UTF-8?q?docs(process):=20=D0=BA=D0=B0=D0=BD=D0=BE=D0=BD?= =?UTF-8?q?=20=D0=B1=D0=B5=D0=B7=20=D0=BF=D1=80=D0=BE=D1=82=D0=B8=D0=B2?= =?UTF-8?q?=D0=BE=D1=80=D0=B5=D1=87=D0=B8=D0=B9,=20=D0=B2=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B0=20=D0=BA=D0=BE?= =?UTF-8?q?=D1=80=D0=BE=D1=87=D0=B5=20(#701)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сверка PROCESS.md, ролевых выжимок, AGENTS.md, TESTING.md, CONTRIBUTING.md и скриптов по 26 найденным расхождениям (D1–D26): трейлеры по классам изменений, gate:small как единственный источник состава, пороги ревью, путь реестра мутантов, golden по ci:golden, порядок чтения промпта ревью. - scripts/change-classes.mjs: классы A/B/C/D — один модуль для process-gate и проверки трейлеров. - commit-msg: коммит только с файлами класса C (документация) трейлеров не требует; указанные трейлеры по-прежнему проверяются. - Маршрут автора без docs/STATUS.md: 5345 → 4703 слова. - Промпт ревью читает SCOPE → AGENTS → REVIEWER, как ROUTES.reviewer. Issue: #701 User-Visible: no Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd --- .github/workflows/_process.yml | 6 +-- AGENTS.md | 9 ++-- CONTRIBUTING.md | 9 ++-- PROCESS.md | 73 ++++++++++++++++++-------- docs/DEVELOPMENT.md | 5 +- docs/LESSONS.md | 7 +-- docs/STATUS.md | 6 ++- docs/TESTING.md | 23 +++++--- docs/process/AUTHOR.md | 21 ++++---- docs/process/REVIEWER.md | 7 +-- docs/specs/README.md | 13 ++--- scripts/change-classes.mjs | 43 +++++++++++++++ scripts/entry-cost.mjs | 5 +- scripts/gate-small.mjs | 3 +- scripts/mutation-registry.mjs | 23 ++++++++ scripts/process-gate.mjs | 42 ++------------- scripts/validate-commit-provenance.mjs | 19 +++++-- test/commit-provenance.test.mjs | 16 ++++++ test/entry-cost.test.mjs | 10 ++++ test/pre-push-gate.test.mjs | 1 + test/process-digests.test.mjs | 2 +- 21 files changed, 232 insertions(+), 111 deletions(-) create mode 100644 scripts/change-classes.mjs diff --git a/.github/workflows/_process.yml b/.github/workflows/_process.yml index d8b7a396..5076cdd5 100644 --- a/.github/workflows/_process.yml +++ b/.github/workflows/_process.yml @@ -1152,15 +1152,15 @@ jobs: ограничитель: «features are built, improved and accepted only if they serve a job listed here». Первый вопрос к задаче — какую строку Core user jobs она закрывает. - 2. docs/process/REVIEWER.md — обязанности ревьюера: позиция, + 2. AGENTS.md — карта пакета, правило №1, классы изменений, треки, + трейлеры и ожидание вердикта; сами правила — по его ссылкам. + 3. docs/process/REVIEWER.md — обязанности ревьюера: позиция, ревью ТЗ, код-ревью, объём гейтов, повторный раунд, находки и вердикт. Раздел PROCESS.md по ссылке открывай, когда пункт касается твоего решения; при расхождении прав PROCESS.md. Если файла в материале нет — читай PROCESS.md §2.4, §2.7, §2.10, §4, §7.2, §8, §12. Задача правит сам конвейер, гейты или процесс — PROCESS.md целиком, §10 в первую очередь. - 3. AGENTS.md — карта пакета, правило №1, классы изменений, треки, - трейлеры и ожидание вердикта; сами правила — по его ссылкам. 4. Тело issue #${{ github.event.issue.number }} и все комментарии. 5. Если меняется видимое поведение — docs/USER-GUIDE.ru.md: терминология интерфейса берётся оттуда, а не изобретается. diff --git a/AGENTS.md b/AGENTS.md index 48a9f97d..210cd886 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,9 @@ leak interactions into View. For work that changes visible behaviour, also read route and `test/entry-cost.test.mjs` keeps this list equal to its routes: - author (analysis, spec, implementation, infrastructure): `docs/SCOPE.md` → - `AGENTS.md` → `docs/process/AUTHOR.md` → `docs/STATUS.md`; + `AGENTS.md` → `docs/process/AUTHOR.md`, then the task packet + (`node scripts/task-packet.mjs --issue NN`); the status snapshot + (docs/STATUS.md) only when resuming a session or preparing a release; - reviewer (spec or code): `docs/SCOPE.md` → `AGENTS.md` → `docs/process/REVIEWER.md`, then the issue body and its comments; - changing the pipeline, the gates or the process itself: `docs/SCOPE.md` → @@ -100,8 +102,9 @@ batched comment with a proposed default for each question and `blocked` on top o ## Commits and branches Hooks install themselves on `npm ci` (`prepare` → `scripts/install-hooks.mjs`); -`git config core.hooksPath` must print `.githooks`. Every non-merge commit -carries **terminal** trailers: +`git config core.hooksPath` must print `.githooks`. Every non-merge commit that +touches anything outside class C (docs) carries **terminal** trailers; a +docs-only commit needs none (`PROCESS.md` §3 п.10, #701): ```text Issue: #123 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49033063..6d4269f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,12 +94,11 @@ issue, but they do not replace it or maintain a separate checklist. ```bash git clone --filter=blob:none https://github.com/Matysh/houseplan-card && cd houseplan-card -npm ci # frontend toolchain +npm ci # frontend toolchain; `prepare` installs .githooks npm run typecheck # tsc --noEmit (strict) npm test # node:test — pure logic, i18n parity, tap-action security npm run build # tsc + rollup → dist/houseplan-card.js -pip install pytest voluptuous && python -m pytest tests_backend -q # pure backend tests -npm install # also installs .githooks through the prepare script +pip install -r tests_backend/requirements.txt && python -m pytest tests_backend -q # CI pins, HA harness included ``` ### Why `--filter=blob:none` (#345) @@ -127,7 +126,9 @@ Linux CI or WSL (`bash scripts/wsl-setup.sh --verify`). Without an importable ## Ground rules - **Docs in the same commit**: CHANGELOG entry for user-visible changes; - `docs/STATUS.md` for state changes; `docs/DEVELOPMENT.md` for new gotchas. + `docs/ARCHITECTURE.md` for changes to the data model, WS API or coordinate + system; `docs/STATUS.md` for state changes; `docs/DEVELOPMENT.md` for new + gotchas. A docs-only commit needs no trailers (`PROCESS.md` §3 п.10). - Every UI string goes through `src/i18n/.json`; follow the [Translations](#translations) flow for registry and backend parity. - The committed bundle changes only in a release candidate: `npm run diff --git a/PROCESS.md b/PROCESS.md index a879b453..8e7a6117 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -7,8 +7,10 @@ > агенты/сессии · любой агент может взять любую роль · инфраструктурные задачи > входят в общий флоу сразу на `S7-code-review`. > -> **Область действия:** обязателен для владельца и для любого агента. Читается -> сразу после `docs/SCOPE.md` и `AGENTS.md`, до `docs/STATUS.md`. Живёт в +> **Область действия:** обязателен для владельца и для любого агента. Целиком +> его читает тот, кто правит конвейер, гейты или сам процесс, — сразу после +> `docs/SCOPE.md` и `AGENTS.md`, до `docs/STATUS.md`; автор и ревьюер входят через +> ролевые конспекты ниже и открывают раздел канона по ссылке (#634, #701). Живёт в > репозитории: до августа 2026 канон лежал только в папке владельца, и свежий клон > его не содержал вовсе. > @@ -24,8 +26,9 @@ > метках и больше нигде: Project v2 не используется. При расхождении > документации с GitHub побеждает GitHub. Этот файл — единственный полный канон > процесса в репозитории; `AGENTS.md` — его короткое обязательное резюме, а -> внешний `CODEX-RUNBOOK.md` — только маршрутизатор к канону и историческим -> инструкциям. Текущие версии, состояние конкретных issue, runtime pins и списки +> внешние `CODEX-RUNBOOK.md` и `CLAUDE.md` папки владельца — только маршрутизаторы +> к канону и историческим инструкциям: правил в них нет, и в замер цены входа +> (`scripts/entry-cost.mjs`) они не входят — CI их не видит (#701). Текущие версии, состояние конкретных issue, runtime pins и списки > jobs не копируются в производную прозу: они читаются из своих исполняемых > источников. > При расхождении этого документа с `.github/workflows/*.yml` и `scripts/*` @@ -201,8 +204,9 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready → «Взял: <роль> · сессия · ветка `issue/-`». - **WIP-лимиты:** не более **1** issue в «В разработке» на исполнителя, не более **3** одновременно на цикл релиза, не более **2** в «Код-ревью». -- **Трассируемость:** ветка `issue/-`; каждый коммит несёт трейлеры - `Issue: #` и `User-Visible: yes|no`. +- **Трассируемость:** ветка `issue/-`; каждый коммит с файлами классов A, B или D несёт трейлеры + `Issue: #` и `User-Visible: yes|no`; коммит только из документации — без + трейлеров (§3 п.10, #701). - **Автотесты — часть реализации, а не отдельная фаза.** Каждый AC, помеченный `unit`/`backend`/`smoke`/`golden`, получает свою проверку здесь же. «Тестирование вне жизненного цикла» означает отсутствие фазы ручного @@ -251,7 +255,7 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready → а два теста были записаны в закрытие coverage-ratchet под именами, обещавшими то, чего они не проверяли (#430). - Мутант в `scripts/mutation-gate.mjs` обязателен, когда защита живёт в + Мутант в реестре `scripts/mutation-registry.mjs` обязателен, когда защита живёт в продуктовом коде и проверяется дорогим гейтом (смок, бэкенд, golden): там ревьюер не воспроизведёт отрицательный прогон второй раз. Для чистых юнитов достаточно прогона со снятой защитой, приведённого в документе. @@ -385,7 +389,7 @@ S1-new → S2-analysis → S3-spec → S4-spec-review ⟲ → S5-ready → fast-forward (`--commit-if-stale`, коммит класса C) — и публикацией документа ревью ТЗ прямо в `dev`. В ветке задачи индекс не пересобирается — ни при приведении к dev, ни при публикации документа код-ревью: иначе две параллельные задачи -конфликтуют на нём по построению. Руками не правится. Конфликт ребейза, в котором **все** пути — +конфликтуют на нём по построению. Сам `INDEX.md` руками не правится никогда. Конфликт ребейза, в котором **все** пути — `INDEX.md`, отказом не считается (#643): `scripts/rebase-generated.mjs` пересобирает индекс по каталогу на остановке и продолжает ребейз — так делают приведение к dev, слияние кандидата и авторский `rebase-on-dev.mjs`; индекс @@ -401,9 +405,10 @@ Validate кандидата. Сегодня пять чисел исходник patch-id кандидата слияния: вердикт к работе задачи остаётся в силе. Шаг Validate «индекс ревью совпадает с каталогом» красит push в `dev`, где `INDEX.md` расходится с каталогом (на issue-ветках не судится: их переписывает конвейер). -Правка `docs/reviews/` -руками — перенос в `legacy/`, удаление — сопровождается -`node scripts/reviews-index.mjs` в том же коммите. Прежде чем брать +Ручная правка каталога +`docs/reviews/` — перенос документов в `legacy/`, удаление — сопровождается +пересборкой индекса `node scripts/reviews-index.mjs` в том же коммите: индекс +меняет генератор, а не рука. Прежде чем брать задачу по подсистеме, стоит прочитать её строки в индексе: что находили и чем закрывали — там, а не в тысяче файлов. Уроки, пережившие свою задачу, собираются в `docs/LESSONS.md` с датой и ссылкой на источник. @@ -448,8 +453,10 @@ patch-id кандидата слияния: вердикт к работе за 1. **Никаких изменений в код, если нет issue** и он не помечен «Готово к разработке» или дальше. -2. **Issue не может быть взят в разработку**, пока у него нет ТЗ с зелёным ревью, - пронумерованных AC с указанием доказательства и назначенного исполнителя. +2. **Issue не может быть взят в разработку**, пока у него нет ТЗ в объёме трека + (§5: на `track:ask` — с зелёным ревью ТЗ; на `show` — до трёх AC; на `ship` — + строка «что меняется и чем проверить»), доказательства для каждого AC и + назначенного исполнителя. Инфраструктурная задача ТЗ не пишет (§1). 3. **Issue не может быть взят дважды.** Занятие фиксируется назначением, меткой и комментарием с именем ветки. У одного исполнителя одновременно не более одного issue в разработке. @@ -469,14 +476,17 @@ patch-id кандидата слияния: вердикт к работе за правка. Блокирующая находка отправляет текущий issue в «Заблокировано». 10. **Каждый коммит класса A и B несёт трейлер `Issue: #NN`**, ветка называется `issue/NN-slug`, а `User-Visible: yes` требует правок в **обоих** changelog в - том же коммите. После `cherry-pick -x` служебная строка `(cherry picked + том же коммите. Документационный коммит — только файлы класса C — трейлеров + не требует (#701, как `skip issue` у CPython); хук `commit-msg`, CI и + `process-gate` судят одинаково. Коммит класса D несёт `Release:` (п.12). После `cherry-pick -x` служебная строка `(cherry picked from ...)` должна оставаться выше финального блока трейлеров: перед push проверяем порядок через `git show -s --format=full HEAD`. 11. **Документация — в том же коммите, что поведение.** Отдельным «допишу потом» коммитом документация не бывает. 12. **Сгенерированное не коммитится само по себе.** Только релизный промоушен или - принятие эталонов с доказательством ревью: URL Linux CI run либо хеш - аттестованного WSL-артефакта. + принятие эталонов с доказательством ревью: трейлер `Release:` и ровно один + источник — URL Linux CI run (`Baseline-Reviewed:`) либо хеш аттестованного + WSL-артефакта (`Baseline-Reviewed-Local:`), §10.1. 13. **Golden-эталоны принимаются только** `npm run golden:accept -- --reviewed` по полному Linux-артефакту: либо GitHub CI, либо `npm run golden:wsl:capture` в WSL/ext4 с clean опубликованным SHA и машинно-проверяемым паспортом. Второй @@ -516,8 +526,9 @@ patch-id кандидата слияния: вердикт к работе за - **Заход и цикл — разные величины.** Заход — сколько раз ревью отработало; он виден в имени документа (`-r1`, `-r2`, …) и нужен, чтобы два документа не затёрли друг друга. Цикл — единица бюджета §4. Заходов законно бывает больше, - чем циклов, поэтому порог проверки 7 в `scripts/process-gate.mjs` выше лимита - циклов (шесть документов = четыре цикла плюс два ребейза). + чем циклов, поэтому порог проверки №7 в `scripts/process-gate.mjs` + (`REVIEW_DOC_LIMIT` — шесть документов одного вида на issue) выше лимита + циклов: четыре цикла плюс два ребейза. - Метка `review-4` ставится, когда исчерпан **бюджет циклов**; конвейер снимать её не вправе — это решение владельца. Если бюджет пересчитан и оказался ниже лимита, конвейер сообщает пересчёт, но метку не трогает. @@ -628,7 +639,7 @@ patch-id кандидата слияния: вердикт к работе за | Аналитик | разбор, оценки, поверхности | окончательно ставить приоритет | | Автор ТЗ | раздел `## ТЗ` в теле issue | ревьюить своё ТЗ | | Ревьюер ТЗ | `docs/reviews/SPEC-REVIEW-NN-rN.md` | править ТЗ вместо автора | -| Разработчик | код, автотесты, документация, changelog | ревьюить свой код, принимать golden | +| Разработчик | код, автотесты, документация, changelog | ревьюить свой код; принимать golden — кроме сдвига своей задачи с меткой `ci:golden` по §3 п.13 (§8, #697) | | Ревьюер кода | `docs/reviews/CODE-REVIEW-*-rN.md`, проверка AC | править продуктовый код | | Релиз-менеджер | пре-релиз, стабильный релиз, закрытие issue | добавлять код в релизный коммит | | Владелец | приоритет, ценность, скоуп, отклонение, арбитраж, хотфикс | — | @@ -733,9 +744,16 @@ issue #NN ## 8. Гейты **Локальный гейт перед выходом из «В разработке»** — минимальный набор, -покрывающий изменённые поверхности (действующее правило владельца): +покрывающий изменённые поверхности (действующее правило владельца). +Обязательную часть исполняет одна команда, `npm run gate:small` +(`scripts/gate-small.mjs` — единственный источник её состава, #701): сборка с +typecheck, юниты, целостность и бюджет бандла, `no-new-any`, запрет синхронного +чтения layout в render, запрет записи смоков в приватное состояние, +`lint:unused` и вывод `smoke-select`. Команды ниже — то же самое по отдельности +плюс то, что по диффу и AC: ``` +npm run gate:small # обязательная часть одной командой npx tsc --noEmit npm test npm run build && node scripts/bundle-policy.mjs --verify HEAD @@ -779,7 +797,8 @@ npx tsc -p tsconfig.junction-parity.json && node scripts/fix-test-build.mjs \ (`workflow_dispatch`, только артефакт) с приёмкой вручную: `npm run docs:accept -- --reviewed --from=<распакованный артефакт>` (#246). Ручной путь остаётся релиз-менеджеру, если бот недоступен. Съёмка на своей машине даёт байтово другой PNG при том же -кадре, и набор из «не того» браузера переписывает все десять файлов без единого +кадре, и набор из «не того» браузера переписывает все кадры (их число — в +`demo/docs/screenshots.mjs`) без единого содержательного изменения. Приёмка отказывает, если кандидат снят не с этого дерева, не тем капчуром, не называет свой Chromium или неполон; коммит бота проверяет релиз-менеджер, коммит ручной приёмки делает человек. @@ -938,7 +957,8 @@ Performance зелёные на точном SHA, плюс зелёный E2E н включая свежий контейнер облачного агента, без отдельного шага в инструкции. - **`commit-msg`** — есть, работает. Отклоняет коммит без терминального - `Issue: #NN`, требует ровно один `User-Visible: yes|no`, а для коммитов, + `Issue: #NN`, требует ровно один `User-Visible: yes|no` (коммит только из + файлов класса C — без трейлеров, §3 п.10, #701), а для коммитов, трогающих `demo/golden/baselines/**`, — `Release:` плюс ровно один источник: `Baseline-Reviewed: ` либо `Baseline-Reviewed-Local: sha256:<хеш аттестации>`. Локальный хеш обязан @@ -991,7 +1011,9 @@ Performance зелёные на точном SHA, плюс зелёный E2E н `Baseline-Reviewed: <ссылка на прогон CI>` либо `Baseline-Reviewed-Local: sha256:<хеш аттестации>`; 6. релизный коммит не содержит изменений в `src/` и `custom_components/**/*.py`; -7. документов ревью на один issue не больше четырёх (`-r1`…`-r4`). +7. документов ревью одного вида (`SPEC-REVIEW`, `CODE-REVIEW`) на один issue + не больше шести (`-r1`…`-r6`, `REVIEW_DOC_LIMIT`): четыре цикла плюс два + ребейза (§4). С токеном GitHub: @@ -1012,6 +1034,11 @@ Validate стартует от этого push и успевает прочит документ ревью: он ложится в ветку задачи, пока та в `S4-spec-review` или `S7-code-review`, то есть заведомо вне рабочего множества. +**Инфраструктурный диапазон статуса не требует** (#562, §1): если в диапазоне нет +ни одного файла класса A, задача ещё не вошла в поток — она войдёт в него сразу на +`S7-code-review`, — и отсутствие S-метки не отказ. Признак механический, по +диффу, а не по метке `infra`. + **При продвижении в `main` не перепроверяются коммиты, уже достижимые из prerelease-тега.** После выпуска беты их issue по §2.8 должны быть закрыты, а stable fast-forward снова включает эти коммиты в диапазон `old-main..candidate`. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 5c2e2f15..787ed880 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -330,7 +330,10 @@ the `dev` branch. Access to the owner's instances is not documented here. - The card module URL contains `?v=`. Browsers keep the ES module in memory cache: after deploying new JS **bump VERSION in const.py and restart HA**, - otherwise a plain F5 will keep the old version. + otherwise a plain F5 will keep the old version. This is a deployment step — a + release candidate or your own local stand. An ordinary task commit bumps neither + `VERSION` nor the committed bundle: both change only in a commit with a + `Release:` trailer (#657, `PROCESS.md` §1). - After a page reload the HA frontend (with kiosk-mode) sometimes leaves the view empty ("InvalidStateError: Transition was aborted", hui-view is not created for 1–2 min). Cured by repeating the SPA navigation: pushState + a location-changed event, or just waiting. diff --git a/docs/LESSONS.md b/docs/LESSONS.md index 0e7764bd..d1532299 100644 --- a/docs/LESSONS.md +++ b/docs/LESSONS.md @@ -3,8 +3,9 @@ Датированные выводы, пережившие свою задачу (#635). Каждый — одной мыслью, со ссылкой на источник: документ ревью, issue, аудит. Полный след решений — в `docs/reviews/INDEX.md` и комментариях issue; здесь только то, что меняет -поведение на следующей задаче. Конспекты ролей (#634) и промпт ревьюера -ссылаются сюда. +поведение на следующей задаче. Читается перед задачей в затронутой подсистеме +вместе с её строками в `docs/reviews/INDEX.md` (PROCESS.md, «Индекс документов +ревью»); конспекты ролей сюда не ведут — урок, ставший правилом, живёт в каноне. | Дата | Урок | Источник | |---|---|---| @@ -12,7 +13,7 @@ | 2026-09-06 | Число смоков/тестов не живёт в тексте: считается командой (`npm run inventory`). | #472, PROCESS.md | | 2026-09-09 | Мутанты по диффу — на кандидате, не на каждом push: 48 из 56 часов job-минут за два дня ушли в отменённые прогоны. | #510 | | 2026-09-13 | Ненулевой exit гарда ≠ «мутант пойман»: setup-failure, invalid-mutation и survived различаются; засчитывается только assertion-killed. | #558, #568 | -| 2026-09-18 | Ревью на не принятых эталонах — потерянный цикл: `golden:verify` ревьюер гоняет лично, эталоны принимаются до `S7`. | CODE-REVIEW-598-r1 H1 | +| 2026-09-18 | Ревью на не принятых эталонах — потерянный цикл: `golden:verify` ревьюер гоняет лично, эталоны принимаются до `S7`. С #697 — только у задачи с `ci:golden`; сдвиг остальных принимает бот на `dev` перед бетой. | CODE-REVIEW-598-r1 H1, #697 | | 2026-09-20 | Стенд ≠ Home Assistant: нативный `` и `ha-dialog` — разные ветки; свидетель для реального HA — фикстура #505, а не golden стенда. | аудит 22.09 A/H1, #607, #609 | | 2026-09-20 | Контролируемое поле без `live()` показывает не то число, что в состоянии; клампить на коммите (`change`), не на каждом символе. | аудит 22.09 A/H2, #608 | | 2026-09-21 | Лог без итоговой строки — обрыв, а не «ok»: отчёт судит по маркеру завершения и исходу шага, не по отсутствию `FAIL`. | #604 | diff --git a/docs/STATUS.md b/docs/STATUS.md index 7f442983..a98ee5b3 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -3,8 +3,10 @@ > The current state for a resuming session: a generated snapshot, the current > cycle and the standing decisions that explain it. Rules are not here — the > process is `PROCESS.md`, release mechanics are `docs/DEVELOPMENT.md` › Release, -> and task scope and status live in GitHub Issues and their labels. Update a row -> in the same commit as the change it describes (`PROCESS.md` §2.6). +> and task scope and status live in GitHub Issues and their labels. The snapshot +> below is generated — never edit it by hand; the prose sections after it are +> edited by hand, a row in the same commit as the change it describes +> (`PROCESS.md` §2.6). ## Snapshot diff --git a/docs/TESTING.md b/docs/TESTING.md index 27df6afe..385469ad 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -28,10 +28,12 @@ golden, а то, чего автоматика не видит, собрано подложки, Glow и декора проверяет пустую страницу; солнце с азимутом, при котором луч не достигает единственного окна (#89), — та же ошибка в геометрии. -4. **Тест, охраняющий механизм, сопровождается мутантом** в - `scripts/mutation-registry.mjs`: 2–5 строк патча, воспроизводящего поломку, - против которой тест заведён, и тест обязан на ней краснеть. Чистым функциям - с обычными юнитами мутант не нужен. +4. **Мутант в `scripts/mutation-registry.mjs` обязателен, когда защита живёт в + продуктовом коде и проверяется дорогим гейтом** (смок, бэкенд, golden): + 2–5 строк патча, воспроизводящего поломку, против которой тест заведён, и + тест обязан на ней краснеть. Для чистых юнитов достаточно прогона со снятой + защитой, приведённого в документе ревью. Правило одно — PROCESS.md §2.7 + (#701); на треке `show` отсутствие мутанта — Low (§10.4). 5. **Тавтологический ассерт — читающий то же свойство, которое код только что выставил, — не пишется вовсе.** Он может упасть только при удалении строки, но не при её неработоспособности. @@ -45,7 +47,8 @@ golden, а то, чего автоматика не видит, собрано Проверка: `node scripts/mutation-gate.mjs --check` — якоря патчей живы; полный прогон — workflow `mutation-gate.yml` (шесть чересполосных шардов -`--shard=i/6` — при четырёх шард упёрся в потолок 60 минут на 810 мутантах, #604; +`--shard=i/6` — при четырёх шард упёрся в потолок 60 минут, #604; число мутантов +считает `npm run inventory`, а не этот текст; один зафиксированный commit/tree для всего прогона; каждый артефакт несёт identity и исход шага прогона, а отдельный агрегатор fail-closed отвергает смешанные, неполные или прерванные по таймауту evidence даже при частичном @@ -250,6 +253,12 @@ c._drag = { id, sx, sy }; // private-ok: #NNN состояние жеста, о ## Локальный набор перед пушем (#343) +Обязательный локальный набор — `npm run gate:small`; его состав живёт в +`scripts/gate-small.mjs` и нигде больше не переписывается (#701). Хук `pre-push` +гоняет его сам для веток задач — таблица «В хуке» ниже. Ручной +`node scripts/pre-push-gate.mjs` из этого раздела — расширенный прогон: +типы, юниты, смоки и мутанты по диффу. + Красный CI — дорогой способ узнать о проблеме: пять минут ожидания, а при код-ревью ещё и лишний раунд. Прецедент назван в задаче: находка r2-H1 в #329 стоила целого раунда и ловилась локальным `npm test`. @@ -736,8 +745,8 @@ See `demo/performance/README.md` for commands and the budget-review contract. Остальные пункты с пометкой `[manual]` описывали обычное поведение режимов, редакторов, диалогов, правил значков и `houseplan-space-card`. Для них нужен смок, а не ручная проверка: задача, которая трогает такую поверхность без -смока, пишет его в том же коммите (правило `[auto: …]` выше). Пункты -`[manual]` разделов ниже остаются как есть. +смока, пишет его в том же коммите (правило `[auto: …]` выше). Оставшиеся пункты +`[manual]` разделов выше остаются как есть. ## Release regression quickies diff --git a/docs/process/AUTHOR.md b/docs/process/AUTHOR.md index dd43246b..5c0721c8 100644 --- a/docs/process/AUTHOR.md +++ b/docs/process/AUTHOR.md @@ -94,12 +94,14 @@ ## Реализация (`S6-in-progress`) - Занятие: `Взял: <роль> · сессия · ветка issue/NN-slug`; WIP — одна - задача в разработке на исполнителя + задача в разработке на исполнителя, не больше трёх на цикл релиза и двух в + код-ревью ([§2.6](../../PROCESS.md#26-в-разработке--реализация), [§7.2](../../PROCESS.md#72-шаблоны-комментариев)). -- Ветка `issue/-`; каждый коммит несёт трейлеры `Issue: #` и - `User-Visible: yes|no`. `User-Visible: yes` требует правок в обоих changelog - в том же коммите. После `cherry-pick -x` трейлеры остаются последним блоком +- Ветка `issue/-`; каждый коммит с файлами классов A, B или D несёт + трейлеры `Issue: #` и `User-Visible: yes|no`, коммит только из + документации — без трейлеров. `User-Visible: yes` требует правок в обоих + changelog в том же коммите. После `cherry-pick -x` трейлеры остаются последним блоком ([§2.6](../../PROCESS.md#26-в-разработке--реализация), [§3 п.10](../../PROCESS.md#3-правила)). - Автотесты — часть реализации: каждый AC с пометкой `unit` / `backend` / @@ -137,13 +139,12 @@ ## Гейты перед хендоффом -- Минимальный набор по изменённым поверхностям: `npx tsc --noEmit`, - `npm test`, `npm run build` + `bundle-policy --verify`, `smoke-select` и - целевые смоки, `no-new-any`; по диффу — `model-invariants`, +- Обязательная часть — `npm run gate:small`: его состав живёт в + `scripts/gate-small.mjs` и нигде не переписывается. По диффу и AC сверх + него — целевые смоки из вывода `smoke-select`, `model-invariants`, `pytest tests_backend`, junction parity; `golden:verify` — только с меткой - `ci:golden`. Команды — - в каноне ([§8](../../PROCESS.md#8-гейты)); `npm run gate:small` собирает - обязательную часть (`docs/TESTING.md`, «Локальный набор перед пушем»). + `ci:golden` ([§8](../../PROCESS.md#8-гейты); `docs/TESTING.md`, «Локальный + набор перед пушем»). - Бандл в коммит задачи не идёт: сборка переписывает отслеживаемый `dist/`, перед коммитом — `npm run bundle:clean`; хук `commit-msg` отклоняет пути бандла без трейлера `Release:` (#657, [§1](../../PROCESS.md#1-основное-правило)). diff --git a/docs/process/REVIEWER.md b/docs/process/REVIEWER.md index 2a2e2bd9..cc12d09e 100644 --- a/docs/process/REVIEWER.md +++ b/docs/process/REVIEWER.md @@ -83,9 +83,10 @@ ([§8](../../PROCESS.md#8-гейты)). - Условие честности сужения: ревьюер обязан перечислить, какие гейты прогнал, какие нет и почему ([§8](../../PROCESS.md#8-гейты)). -- Зелёный `pytest tests_backend` без Home Assistant скипает `test_ha_*.py` и - ничего не доказывает — это «чего не проверял» (`docs/TESTING.md`; - [§8](../../PROCESS.md#8-гейты)). +- Зелёный `pytest tests_backend` без Home Assistant `test_ha_*.py` не + собирает вовсе — их нет ни в `passed`, ни в `skipped` (строка `HA harness NOT + collected`), и такой прогон про HA ничего не доказывает — это «чего не + проверял» (`docs/TESTING.md`; [§8](../../PROCESS.md#8-гейты)). ## Повторный раунд diff --git a/docs/specs/README.md b/docs/specs/README.md index 7d59772a..cdaffb1e 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -8,13 +8,8 @@ Здесь остались только ТЗ, на которые ссылаются живые код, тесты и документы (ADR, `ISOMETRIC.md`, `SUN.md`, `RADAR.md`, `LIGHT.md`, `DECOR-EDITOR.md`, support-relay), и те, на которые ссылаются они сами. Остальные ТЗ выпущенных задач перенесены в [`legacy/specs/`](../../legacy/specs/) (#682): историю не переписываем, ссылки из документов ревью ведут по SHA и живут дальше. Каталог нужен `scripts/task-packet.mjs` и проверке 3 `scripts/process-gate.mjs`. -## Обязательные release-артефакты ТЗ +## Правила ТЗ -Если задача меняет пользовательское поведение, её ТЗ обязано явно перечислить: - -- записи в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md`; -- затронутую пользовательскую документацию; -- требуемые screenshots/golden и способ их review, если меняется визуал; -- release/performance/security artifacts, если они входят в acceptance gate. - -Отсутствие этого раздела не означает, что документация необязательна. Для чистого refactoring ТЗ должно прямо зафиксировать отсутствие пользовательских изменений и перечислить технические доказательства безопасного поведения. +Здесь не живут (#701): архив правил не задаёт. Обязательные разделы ТЗ, включая +release-артефакты (changelog RU+EN, документация, скриншоты и golden, perf и +security, если они входят в приёмку), — `PROCESS.md` §7.1 и DoR `S5-ready` (§2). diff --git a/scripts/change-classes.mjs b/scripts/change-classes.mjs new file mode 100644 index 00000000..8d711832 --- /dev/null +++ b/scripts/change-classes.mjs @@ -0,0 +1,43 @@ +// Классы изменений, PROCESS.md §1 — одна таблица на все гейты (#701). +// +// Вынесена из process-gate.mjs: её читает и `validate-commit-provenance.mjs` +// (хук commit-msg), которому process-gate сам импортирует — общий модуль +// снимает круговую зависимость. +// Порядок важен: D проверяется первым, иначе собранный бандл попадёт в A, +// а demo/golden/baselines — в B. +const CLASS_D = [ + /^dist\//, + /^custom_components\/houseplan\/frontend\//, + /^demo\/srv\/assets\/houseplan-card\.js$/, + /^demo\/golden\/baselines\//, +]; +const CLASS_A = [ + /^src\//, + /^custom_components\/houseplan\/.*\.py$/, + /^hacs\.json$/, + /^custom_components\/.*\/manifest\.json$/, + /^custom_components\/.*\/translations\//, +]; +const CLASS_B = [ + /^test\//, /^tests_backend\//, /^demo\//, /^scripts\//, + /^\.github\//, /^\.githooks\//, /^rollup\.config\.mjs$/, /^tsconfig.*\.json$/, + /^package(-lock)?\.json$/, /^pytest\.ini$/, /^\.gitignore$/, /^\.gitattributes$/, + // Пины toolchain — производные от validate.yml (#496), конфиг сборки. + /^\.nvmrc$/, /^\.python-version$/, +]; +const CLASS_C = [ + /^docs\//, /^README/, /^CHANGELOG/, /^AGENTS\.md$/, /^LICENSE$/, + /^CONTRIBUTING\.md$/, /^PROCESS.*\.md$/, /^(CODE|SPEC)-REVIEW-.*\.md$/, + // #682: архив выпущенного — документы ревью и ТЗ прошлых линий. Только + // Markdown; исполняемого там нет (#678 вынес всё прочее из дерева). + /^legacy\//, +]; + + +export function classify(path) { + if (CLASS_D.some((r) => r.test(path))) return 'D'; + if (CLASS_A.some((r) => r.test(path))) return 'A'; + if (CLASS_B.some((r) => r.test(path))) return 'B'; + if (CLASS_C.some((r) => r.test(path))) return 'C'; + return '?'; +} diff --git a/scripts/entry-cost.mjs b/scripts/entry-cost.mjs index 5caf7d16..f54bde7f 100644 --- a/scripts/entry-cost.mjs +++ b/scripts/entry-cost.mjs @@ -25,9 +25,12 @@ const ROOT = fileURLToPath(new URL('..', import.meta.url)); /** Маршруты входа по роли. `budget: null` — только замер, без порога. */ export const ROUTES = Object.freeze({ + // #701: `docs/STATUS.md` ушёл из входа автора — снимок версий и цикла нужен, + // когда сессия возобновляет работу или готовит релиз, а задачу ведёт её пакет + // (`task-packet.mjs`). Минус 691 слово на каждом входе. author: { budget: 12000, // AC1 #634 - files: ['docs/SCOPE.md', 'AGENTS.md', 'docs/process/AUTHOR.md', 'docs/STATUS.md'], + files: ['docs/SCOPE.md', 'AGENTS.md', 'docs/process/AUTHOR.md'], }, reviewer: { budget: 9000, diff --git a/scripts/gate-small.mjs b/scripts/gate-small.mjs index 4f4a0777..d227d21b 100644 --- a/scripts/gate-small.mjs +++ b/scripts/gate-small.mjs @@ -1,7 +1,8 @@ #!/usr/bin/env node // Локальный гейт лёгкого трека одной командой (#479): `npm run gate:small`. // -// PROCESS §8 перечисляет автору шесть команд, и в #476 они гонялись +// Этот файл — единственный источник состава обязательной части §8 (#701): +// канон и конспекты его не переписывают, а называют. В #476 команды гонялись // последовательно, вперемешку с гейтами, к задаче не относящимися. Здесь // обязательная часть §8 начинается параллельно — сборка с typecheck, «новый // код не добавляет any», выбор смоков по диффу. Юниты читают свежий `dist`, diff --git a/scripts/mutation-registry.mjs b/scripts/mutation-registry.mjs index 5db56988..4071bbd9 100644 --- a/scripts/mutation-registry.mjs +++ b/scripts/mutation-registry.mjs @@ -8158,6 +8158,29 @@ const MUTANT_DEFINITIONS = [ replace: ' .hdr > .head { flex-wrap: wrap; padding: 5px 8px; gap: 6px; }', }], }, + // #701: документационный коммит трейлеров не требует — и только он. + { + id: 'docs-only-commit-needs-trailers-again', + guard: 'node --test --test-name-pattern="#701" test/commit-provenance.test.mjs', + because: '#701 (PROCESS §3 п.10): rule #1 guards product code, not a typo in a guide; a docs-only ' + + 'commit carries no Issue/User-Visible trailers', + patches: [{ + file: 'scripts/validate-commit-provenance.mjs', + find: ' const exempt = isDocsOnlyCommit(changedFiles) && !issues.length && !visible.length;', + replace: ' const exempt = false; // mutant: every commit needs trailers', + }], + }, + { + id: 'docs-only-exemption-leaks-to-code', + guard: 'node --test --test-name-pattern="#701" test/commit-provenance.test.mjs', + because: '#701: one file outside class C makes the commit subject to rule #1 again; the exemption ' + + 'must not cover a commit that also touches src/**', + patches: [{ + file: 'scripts/validate-commit-provenance.mjs', + find: " return changedFiles.length > 0 && changedFiles.every((file) => classify(file.replaceAll('\\\\', '/')) === 'C');", + replace: " return changedFiles.length > 0 && changedFiles.some((file) => classify(file.replaceAll('\\\\', '/')) === 'C'); // mutant", + }], + }, { id: 'header-menu-drops-pdf', guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs ' diff --git a/scripts/process-gate.mjs b/scripts/process-gate.mjs index 157d7d40..d72260a8 100644 --- a/scripts/process-gate.mjs +++ b/scripts/process-gate.mjs @@ -39,36 +39,11 @@ import { fileURLToPath } from 'node:url'; import { resolveValidationRange } from './validate-commit-provenance.mjs'; -// --- классы изменений, PROCESS.md §1 --- -// Порядок важен: D проверяется первым, иначе собранный бандл попадёт в A, -// а demo/golden/baselines — в B. -const CLASS_D = [ - /^dist\//, - /^custom_components\/houseplan\/frontend\//, - /^demo\/srv\/assets\/houseplan-card\.js$/, - /^demo\/golden\/baselines\//, -]; -const CLASS_A = [ - /^src\//, - /^custom_components\/houseplan\/.*\.py$/, - /^hacs\.json$/, - /^custom_components\/.*\/manifest\.json$/, - /^custom_components\/.*\/translations\//, -]; -const CLASS_B = [ - /^test\//, /^tests_backend\//, /^demo\//, /^scripts\//, - /^\.github\//, /^\.githooks\//, /^rollup\.config\.mjs$/, /^tsconfig.*\.json$/, - /^package(-lock)?\.json$/, /^pytest\.ini$/, /^\.gitignore$/, /^\.gitattributes$/, - // Пины toolchain — производные от validate.yml (#496), конфиг сборки. - /^\.nvmrc$/, /^\.python-version$/, -]; -const CLASS_C = [ - /^docs\//, /^README/, /^CHANGELOG/, /^AGENTS\.md$/, /^LICENSE$/, - /^CONTRIBUTING\.md$/, /^PROCESS.*\.md$/, /^(CODE|SPEC)-REVIEW-.*\.md$/, - // #682: архив выпущенного — документы ревью и ТЗ прошлых линий. Только - // Markdown; исполняемого там нет (#678 вынес всё прочее из дерева). - /^legacy\//, -]; +// Классы изменений (PROCESS.md §1) живут в change-classes.mjs (#701): их +// читает и хук commit-msg, который судит, нужен ли коммиту трейлер. +import { classify } from './change-classes.mjs'; + +export { classify }; const CHANGELOGS = ['docs/CHANGELOG.md', 'docs/CHANGELOG.ru.md']; @@ -90,13 +65,6 @@ export const RULES = { 10: 'DoR по моменту коммита', }; -export function classify(path) { - if (CLASS_D.some((r) => r.test(path))) return 'D'; - if (CLASS_A.some((r) => r.test(path))) return 'A'; - if (CLASS_B.some((r) => r.test(path))) return 'B'; - if (CLASS_C.some((r) => r.test(path))) return 'C'; - return '?'; -} // --- разбор коммитов --- // Тело коммита многострочное, поэтому поля режутся не по переводам строк: diff --git a/scripts/validate-commit-provenance.mjs b/scripts/validate-commit-provenance.mjs index 9deb371c..56755ac2 100644 --- a/scripts/validate-commit-provenance.mjs +++ b/scripts/validate-commit-provenance.mjs @@ -4,6 +4,7 @@ import { basename } from 'node:path'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { bundleCommitErrors } from './bundle-policy.mjs'; +import { classify } from './change-classes.mjs'; const TRAILER = /^([A-Za-z][A-Za-z0-9-]*):\s*(.*?)\s*$/; export const ENFORCEMENT_BOUNDARY = '8e2973fa7a7cb1a80204ff95ecf3f2d7c36ed2ce'; @@ -52,15 +53,27 @@ export function terminalTrailers(message) { return out; } +/** + * #701 (PROCESS.md §3 п.10): документационный коммит — только файлы класса C — + * трейлеров не требует, как `skip issue` у CPython. Правило №1 охраняет + * продуктовый код, а не опечатку в гайде. Пустой список файлов (сообщение без + * `--staged`) — не документационный коммит: судить нечем, правило прежнее. + * Трейлеры, если они есть, судятся всегда. + */ +export function isDocsOnlyCommit(changedFiles = []) { + return changedFiles.length > 0 && changedFiles.every((file) => classify(file.replaceAll('\\', '/')) === 'C'); +} + export function validateCommitMessage(message, changedFiles = [], { baselineIndex = undefined, authorDate = null } = {}) { const trailers = terminalTrailers(message); const errors = []; const issues = trailers.get('Issue') || []; - if (!issues.length || issues.some((value) => !/^#[1-9][0-9]*$/.test(value))) { + const visible = trailers.get('User-Visible') || []; + const exempt = isDocsOnlyCommit(changedFiles) && !issues.length && !visible.length; + if (!exempt && (!issues.length || issues.some((value) => !/^#[1-9][0-9]*$/.test(value)))) { errors.push("missing or invalid terminal 'Issue: #' trailer"); } - const visible = trailers.get('User-Visible') || []; - if (visible.length !== 1 || !/^(yes|no)$/.test(visible[0])) { + if (!exempt && (visible.length !== 1 || !/^(yes|no)$/.test(visible[0]))) { errors.push("expected exactly one terminal 'User-Visible: yes|no' trailer"); } const normalizedFiles = changedFiles.map((file) => file.replaceAll('\\', '/')); diff --git a/test/commit-provenance.test.mjs b/test/commit-provenance.test.mjs index b6b4315d..dc546a01 100644 --- a/test/commit-provenance.test.mjs +++ b/test/commit-provenance.test.mjs @@ -105,3 +105,19 @@ test('the audited beta.2 baseline exception is exact and golden-only', () => { assert.equal(validateHistoricalCommit(`${audited.slice(0, -1)}2`, message, changed).length, 2); assert.match(validateHistoricalCommit(audited, 'Update baseline', changed)[0], /Issue/); }); + +// #701 (PROCESS.md §3 п.10): трейлеры — только на коммитах с продуктовыми и +// инфраструктурными файлами; документационный коммит их не требует. +test('#701: документационный коммит (только класс C) трейлеров не требует', () => { + assert.deepEqual(validateCommitMessage('docs: fix a typo in the guide', ['docs/USER-GUIDE.md', 'README.md']), []); + assert.deepEqual(validateCommitMessage('docs: changelog wording', ['docs/CHANGELOG.md']), []); + // Хоть один файл вне класса C — прежнее правило. + assert.equal(validateCommitMessage('fix: x', ['docs/USER-GUIDE.md', 'src/card.ts']).length, 2); + assert.equal(validateCommitMessage('test: x', ['test/a.test.mjs']).length, 2); + assert.equal(validateCommitMessage('build: x', ['dist/houseplan-card.js']).length >= 2, true); + // Судить нечем — не документационный коммит. + assert.equal(validateCommitMessage('docs: typo', []).length, 2); + // Трейлер, если он есть, судится всегда: кривой номер — ошибка и в docs-коммите. + assert.equal(validateCommitMessage('docs: typo\n\nIssue: #x', ['docs/a.md']).length, 2); + assert.deepEqual(validateCommitMessage('docs: typo\n\nIssue: #9\nUser-Visible: no', ['docs/a.md']), []); +}); diff --git a/test/entry-cost.test.mjs b/test/entry-cost.test.mjs index dea89645..93ef1f2d 100644 --- a/test/entry-cost.test.mjs +++ b/test/entry-cost.test.mjs @@ -44,3 +44,13 @@ test('#634 entry-cost: AGENTS.md называет те же маршруты в assert.deepEqual(route('reviewer'), ROUTES.reviewer.files); assert.deepEqual(route('changing the pipeline'), ROUTES.canon.files); }); + +test('#701 D14: промпт ревьюера читает маршрут reviewer в том же порядке, что AGENTS.md', async () => { + const { readFileSync } = await import('node:fs'); + const { ROUTES } = await import('../scripts/entry-cost.mjs'); + const workflow = readFileSync(new URL('../.github/workflows/_process.yml', import.meta.url), 'utf8'); + const start = workflow.indexOf('Прочитай в этом порядке, прежде чем судить:'); + assert.ok(start > 0, 'нумерованный порядок чтения в промпте найден'); + const items = [...workflow.slice(start, start + 3000).matchAll(/^\s+(\d)\. (\S+)/gm)].slice(0, 3).map((m) => m[2]); + assert.deepEqual(items, ROUTES.reviewer.files); +}); diff --git a/test/pre-push-gate.test.mjs b/test/pre-push-gate.test.mjs index 284e3f89..876aea00 100644 --- a/test/pre-push-gate.test.mjs +++ b/test/pre-push-gate.test.mjs @@ -134,6 +134,7 @@ const HOOK_FILES = [ 'scripts/pre-push-gate.mjs', 'scripts/branch-state.mjs', 'scripts/process-gate.mjs', + 'scripts/change-classes.mjs', // #701: классы изменений — общие для гейта и трейлеров 'scripts/validate-commit-provenance.mjs', 'scripts/bundle-policy.mjs', // #657: правило бандла в проверке происхождения 'scripts/bundle-tree.mjs', diff --git a/test/process-digests.test.mjs b/test/process-digests.test.mjs index 87cba7c2..5600eccd 100644 --- a/test/process-digests.test.mjs +++ b/test/process-digests.test.mjs @@ -50,7 +50,7 @@ const KEY_RULES = { ['5-треки-ship-show-ask--метка-владельца', 'ожидаемое поведение уже зафиксировано'], ['71-цепочка', 'Владельцу задаются только продуктовые вопросы'], ['71-цепочка', 'issue остаётся в `S3-spec` и получает `blocked`'], - ['26-в-разработке--реализация', 'каждый коммит несёт трейлеры `Issue: #` и `User-Visible: yes|no`'], + ['26-в-разработке--реализация', 'каждый коммит с файлами классов A, B или D несёт трейлеры `Issue: #` и `User-Visible: yes|no`'], ['3-правила', '`User-Visible: yes` требует правок в обоих changelog в том же коммите'], ['26-в-разработке--реализация', 'шесть классов риска'], ['26-в-разработке--реализация', 'Скоуп не расширяется'],