diff --git a/DECISIONS.md b/DECISIONS.md index fe54089..ccf2672 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1746,11 +1746,13 @@ ADR, запискам разведки и сообщениям коммитов смысл — сошлись на смысле, потому что метрики, логи, инфраструктура и выкладка в эту секцию просятся и так. -**ШШШ. Версия канона не менялась, и это законно.** Ни один проект на каноне 3 не -стоит: healthlog и jellybit держат канон 2, повышение только предстоит. Запись -версии 3 правится **как черновик**, а не как история: тот, кто по ней поедет, -увидит сразу `Сопровождение`. Версия отделяет одно **состояние проектов** от -другого, а не одну редакцию текста от другой. +**ШШШ. Версия канона не менялась, и это законно.** ~~Ни один проект на каноне 3 +не стоит: healthlog и jellybit держат канон 2, повышение только предстоит.~~ + +**Отменено в тот же день (тема 26).** Посылка была ложной: healthlog уже переехал +на канон 3, и правка записи версии 3 задним числом переписывала то, по чему он +ехал. Правило осталось верным, применение — нет: черновиком запись версии +является ровно до того, как **первый** проект по ней поехал. **ЩЩЩ. Сопровождение и эксплуатация — целое и часть, и словарь у трёх мест общий.** Тема живёт в трёх документах, и раньше каждое место говорило своим @@ -1781,3 +1783,54 @@ ADR, запискам разведки и сообщениям коммитов переименовании `Разработка` → `Сопровождение` проверка назвала секцию роадмапа чужой и остановилась: регистр она правит сама, смысл — нет. Ровно то поведение, которое нужно проекту при повышении канона. + +## 26. Канон 4: правка задним числом отменена (2026-08-04) + +### Что было + +Секцию `Разработка` переименовали в `Сопровождение` без повышения версии канона — +на посылке «ни один проект на каноне 3 не стоит» (тема 25, ШШШ). Посылка +оказалась ложной: healthlog уже переехал, `docs/.pm.json` держит `"canon": 3`, а +роадмап — секцию `Разработка` с прописной. Правка записи версии 3 переписывала +то, по чему он ехал. + +### Решено + +**ЭЭЭ. Запись версии — черновик ровно до первого переехавшего проекта.** После +этого она **история**, и любое изменение канона заводит новую версию, даже если +меняется одно слово. Проверять это дёшево: `grep '"canon"' */docs/.pm.json` по +живым проектам. Дорого — обратное: проект, повышенный по тексту, которого больше +не существует, невоспроизводим. + +Запись версии 3 восстановлена дословно (`Разработка` | `Tooling`), переименование +уехало в версию 4. jellybit, стоящий на каноне 2, прочтёт обе записи подряд и +заведёт `Разработка`, чтобы через шаг переименовать; в шаг версии 3 добавлена +оговорка «едешь сразу на 4 — заводи `Готово` последней и не переставляй дважды». +Лишний шаг — плата за честную историю, и она мала. + +**ЮЮЮ. `Готово` переехало вниз, и порядок секций стал каноническим.** +Достигнутое **копится**: через год этой секции больше, чем всех остальных +вместе, — и стоя первой она отодвигает за экран ровно то, ради чего роадмап +открывают чаще всего. Порядок теперь проверяется (`roadmap_lint`) и правится +(`check --fix` переставляет секции вместе с содержимым): без проверки порядок +разъедется молча, а переставлять секцию с десятком строк руками — работа, на +которой ошибаются. + +**ЯЯЯ. Индексы позиций считаются из самого кортежа.** `ACHIEVED` был `0` и стал +`3`; хардкод индексов пережил бы перестановку молча и сломал бы `close`. Теперь +`PLANNED, DIRECTIONS, OPERATIONS, ACHIEVED = range(len(ROADMAP_SECTIONS))` — +переставили секцию, индексы переехали сами. + +### Что из этого следует + +98. **Отбивка нужна и перед заголовком.** Перестановка блоков ставит два + заголовка вплотную — `spaced_sections` правил только строку после. Дефект + нашёлся сразу же, на первой перестановке демо-набора: класс правки, + существующий только потому, что появилась другая правка. +99. **`check --fix` переставляет, но не переименовывает.** Чужую секцию он + оставляет ошибкой, и на переименовании `Разработка` → `Сопровождение` + останавливается: имя — решение человека, порядок — механика. Тот же разрез, + что между регистром (правит) и составом (не трогает). +100. **Версия канона отделяет состояния проектов, а не редакции текста** — и + ровно поэтому её нельзя не поднять, когда состояние хоть одного проекта + уже зафиксировано. diff --git a/TODO.md b/TODO.md index da3fef0..0751f6f 100644 --- a/TODO.md +++ b/TODO.md @@ -116,9 +116,10 @@ - [ ] `canon adopt`; `docs/backlog/` → `docs/tasks/` - [ ] `architecture.md` 1662 строки → обзор, остаток маркерами (W) - [ ] после выноса поведения — замерить остаток `architecture.md` и решить по - каталожной форме: жмёт → канон версии **4** для `architecture.md` и - `review.md`, точка входа `README.md` (тема 16, GGG, 65; версию 3 занял - роадмап с родом работы, тема 17, 68) + каталожной форме: жмёт → **следующая** версия канона для + `architecture.md` и `review.md`, точка входа `README.md` (тема 16, GGG, + 65; версию 3 занял роадмап с родом работы, тема 17, 68; версию 4 — + секция `Сопровождение` и порядок секций, тема 26) - [ ] завести `security.md` с периметром первой строкой (J) - [ ] `review-journal.md` → `review.md` + настройка конвейера (K, L) - [ ] `conventions.md` → `conventions/`, `local-research.md` → `research/` (G) @@ -162,7 +163,8 @@ jellybit 43. Шаги повышения — [changelog.md](av-dev-pm/skills/canon/references/changelog.md), запись «Версия 3»; делаются скиллом `av-dev-pm:canon` в режиме `upgrade`. -- [ ] healthlog: `PLAN.md` → `ROADMAP.md`, ссылки, `"canon": 3` +- [x] healthlog: `PLAN.md` → `ROADMAP.md`, ссылки, `"canon": 3` — сделано, + лежит в рабочем дереве проекта некоммитнутым - [ ] jellybit: то же - [ ] род работы и раздел «Затрагивает» — **не задним числом**: сперва то, что идёт в ближайший набор (`sprint take` без них откажет), остальное по ходу @@ -181,3 +183,11 @@ jellybit 43. Шаги повышения — [changelog.md](av-dev-pm/skills/can - [ ] заголовки задач в форму действия — **не задним числом**: по мере попадания задачи в работу. `check` печатает их число, `task-form` предложит формулировки пачкой (тема 20, ЕЕЕ) + +**Канон 4** — сверх того (changelog, запись «Версия 4»): + +- [ ] healthlog: `## Разработка` → `## Сопровождение`, поле «Секция» в целях этой + секции, `check --fix` (переставит `Готово` вниз и поправит отбивку), + `"canon": 4` +- [ ] jellybit едет сразу на 4: `Готово` заводить **последней**, секцию + сопровождения — сразу с новым именем, переставлять дважды не нужно diff --git a/av-dev-pm/skills/canon/references/canon.md b/av-dev-pm/skills/canon/references/canon.md index 2bba986..af9a5d5 100644 --- a/av-dev-pm/skills/canon/references/canon.md +++ b/av-dev-pm/skills/canon/references/canon.md @@ -1,6 +1,6 @@ # Канон документов проекта -**Версия 3.** +**Версия 4.** Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs` читают его, а не пересказывают: три описания одной раскладки разъедутся, и @@ -350,7 +350,7 @@ kebab-case. ```json { - "canon": 2, + "canon": 4, "migrations": "internal/store/migrations", "tasks": { "backlog": "INDEX.md" diff --git a/av-dev-pm/skills/canon/references/changelog.md b/av-dev-pm/skills/canon/references/changelog.md index df995f8..fd9cedb 100644 --- a/av-dev-pm/skills/canon/references/changelog.md +++ b/av-dev-pm/skills/canon/references/changelog.md @@ -13,6 +13,61 @@ upgrade` идёт по записям снизу вверх от версии п --- +## Версия 4 — 2026-08-04 + +Одна секция роадмапа переименована, и вместе с именем расширен её смысл; +достигнутое переехало вниз. Плюс общий словарь для трёх мест канона, которые +говорят про одну тему разными словами. Раскладка не меняется, файлов не +прибавляется. + +**Что переехало:** секция роадмапа `Разработка` → **`Сопровождение`** (англ. +`Tooling` → **`Operations`**). Прежнее имя называло слишком много: роадмап +**весь** про разработку, и секция с таким именем не отличалась от остальных +ничем. + +**Что добавилось:** + +1. **Смысл секции расширен.** Было «инструмент и процесс», стало «чем держат + проект: инструмент, процесс, эксплуатация». Метрики, логи, инфраструктура, + выкладка и дежурство — сюда же. Расширение не косметическое: английское + `Operations` при узком смысле обещало бы эксплуатацию, а внутри лежал бы + линтер. +2. **Общий словарь трёх мест** — [canon.md](canon.md), раздел «Сопровождение и + эксплуатация». Сопровождение — всё, чем держат проект; эксплуатация — его + часть, работа системы на проде. `ROADMAP.md`, секция `Сопровождение` — план + работ; `architecture.md`, раздел «Эксплуатация» — как устроено сейчас; + эксплуатационный проход ревью — оптика проверки. Слить их в одно слово + нельзя: они отвечают на разные вопросы. Слово **«поддержка» не употребляется + вовсе** — в нём слышится помощь пользователю. +3. **Граница с возможностями проходит по тому, кто наблюдает.** «Приложение + сообщает о своём состоянии» — возможность приложения, её место среди прочих + целей. «Дежурный видит состояние на одном экране» — сопровождение. Одни и те + же метрики попадают в разные секции роадмапа, и это верно. +4. **Порядок секций стал каноническим**, и `Готово` переехало **вниз**: + `Запланировано` | `Направления` | `Сопровождение` | `Готово`. Достигнутое + копится — через год этой секции больше, чем всех остальных вместе, — и стоя + первой она отодвигает за экран то, ради чего роадмап открывают чаще всего. + Порядок проверяет `tasks.py check`, переставляет `check --fix`. +5. **Заголовок секции отбивается пустой строкой с обеих сторон.** Прежде + проверялась только строка после заголовка; перестановка секций двигает целые + блоки, и два заголовка оказываются вплотную. Правит `check --fix`. + +**Что сделать проекту:** + +1. Переименовать заголовок секции в `docs/tasks/ROADMAP.md`: `## Разработка` → + `## Сопровождение` (или `## Tooling` → `## Operations`, если индекс + английский). **`check --fix` этого не сделает**: регистр канонической секции + он правит сам, а чужую секцию только называет ошибкой — смысл за человеком. +2. Поправить поле `- **Секция:**` в файлах целей, которые в ней лежат. Порядок + именно такой: сперва заголовок, потом `python3 tasks.py check --dir + docs/tasks` покажет расхождение поимённо. +3. Перечитать состав секции: цели про выкладку, метрики, логи и инфраструктуру, + если они лежали в `Направлениях` за неимением места, переезжают сюда. +4. Прогнать `python3 tasks.py check --dir docs/tasks --fix`: он переставит + секции роадмапа в канонический порядок (`Готово` уедет вниз вместе со всем + содержимым) и поправит отбивку заголовков. +5. `docs/.pm.json`: `"canon": 4`. + ## Версия 3 — 2026-08-04 Роадмап стал **состоянием проекта**, а не очередью работ: цель — возможность @@ -34,9 +89,8 @@ upgrade` идёт по записям снизу вверх от версии п 3. **Секции роадмапа** — четыре вместо двух и **канонические**, в отличие от секций беклога: `Готово` (достигнутые цели строкой с датой, без ссылки на файл), `Запланировано` (очередь значима), `Направления` (очереди нет), - `Сопровождение` (чем держат проект: инструмент, процесс, эксплуатация — - не возможности приложения). Английский - вариант — `Done` | `Planned` | `Directions` | `Operations`, один язык на весь + `Разработка` (инструмент и процесс, не возможности приложения). Английский + вариант — `Done` | `Planned` | `Directions` | `Tooling`, один язык на весь индекс. Переименованию проектом не подлежат: у каждой свой смысл, и в первую пишет сам `close`; `tasks.py check` проверяет состав. 4. **Форма заголовка записи** — по типу: задача отвечает на «что нужно сделать» @@ -93,7 +147,9 @@ upgrade` идёт по записям снизу вверх от версии п идентичности**, — и убрать остальное, иначе проект возвращает себе прежнюю частоту полного набора уточнением. 7. Переименовать секции роадмапа: `порядок` → `Запланировано`, `темы` → - `Направления`; завести `Готово` **первой** и `Сопровождение` последней. + `Направления`; завести `Готово` **первой** и `Разработка` последней + (порядок секций поменялся в версии 4 — если едешь сразу на неё, заводи + `Готово` последней и не переставляй дважды). Прозаические разделы вроде «Что уже пройдено», которые велись руками, разложить: звенья — строками в `Готово` (дата, слаг, что стало возможно), обоснование очереди оставить прозой в `Запланировано`. Любой `##` в индексе @@ -101,7 +157,7 @@ upgrade` идёт по записям снизу вверх от версии п 8. Переформулировать цели ответом на **«что приложение будет уметь»**: не «Работа со слиянием», а «Исход слияния не зависит от порядка доставки». Свойство поведения — законная цель. Цель, которая не про приложение - (процесс, инструмент, эксплуатация), переезжает в `Сопровождение`. + (процесс, инструмент), переезжает в `Разработка`. 9. `[epic]`, если он в проекте заводился: это либо цель, либо набор задач под общей целью. `check` назовёт его неизвестным типом. 10. Прогнать `python3 tasks.py check --dir docs/tasks --fix`: он поднимет diff --git a/av-dev-pm/skills/canon/references/skeletons.md b/av-dev-pm/skills/canon/references/skeletons.md index 8d318fd..cb8a975 100644 --- a/av-dev-pm/skills/canon/references/skeletons.md +++ b/av-dev-pm/skills/canon/references/skeletons.md @@ -392,7 +392,7 @@ severity стоит здесь, а не выводится каждым прох ```json { - "canon": 2 + "canon": 4 } ``` diff --git a/av-dev-pm/skills/canon/scripts/docs.py b/av-dev-pm/skills/canon/scripts/docs.py index a658002..3671b73 100644 --- a/av-dev-pm/skills/canon/scripts/docs.py +++ b/av-dev-pm/skills/canon/scripts/docs.py @@ -25,7 +25,7 @@ from dataclasses import dataclass, field from pathlib import Path from typing import NoReturn -CANON_VERSION = 3 +CANON_VERSION = 4 OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4 diff --git a/av-dev-pm/skills/tasks/SKILL.md b/av-dev-pm/skills/tasks/SKILL.md index e3a7cc4..da174f5 100644 --- a/av-dev-pm/skills/tasks/SKILL.md +++ b/av-dev-pm/skills/tasks/SKILL.md @@ -67,30 +67,36 @@ docs/tasks/ подо что берут.** Цель в спринт взять нельзя, поэтому в списке берущихся ей не место. -**Четыре секции роадмапа, и первая отвечает на половину вопроса:** +**Четыре секции роадмапа, и последняя отвечает на половину вопроса:** | Секция | Англ. | Что в ней | | --- | --- | --- | -| `Готово` | `Done` | достигнутые цели — строкой с датой, **без ссылки на файл**: файл удалён, поведение живёт в спеках | | `Запланировано` | `Planned` | очередь значима и обосновывается прозой рядом | | `Направления` | `Directions` | очереди нет, тянутся долго | | `Сопровождение` | `Operations` | чем держат проект: инструмент, процесс, эксплуатация — не возможности приложения, и потому отдельно | +| `Готово` | `Done` | достигнутые цели — строкой с датой, **без ссылки на файл**: файл удалён, поведение живёт в спеках | + +**Порядок тоже канонический, и `Готово` стоит последним не из скромности.** +Достигнутое **копится**: через год этой секции больше, чем всех остальных +вместе. Стоя первой, она отодвигает за экран ровно то, ради чего роадмап +открывают чаще всего, — что делается сейчас и что дальше. Порядок проверяет +`check`, переставляет `check --fix`. **Секции роадмапа канонические, секции беклога — нет**, и разница не в любви к -единообразию. У каждой секции роадмапа свой смысл, в первую пишет сам `close`, и +единообразию. У каждой секции роадмапа свой смысл, в достигнутое пишет сам `close`, и роадмап, названный по-своему, читался бы только своим автором. Секции беклога (`Ядро`, `Инфра`) смысла не несут — это полки, и остаются делом проекта. -Отсюда три правила, которые проверяет `tasks.py check`: **состав закреплён** +Отсюда четыре правила, которые проверяет `tasks.py check`: **состав закреплён** (чужая секция — ошибка, а не вольность), **все четыре обязаны быть** (нет -секции — нет ответа на её часть вопроса), **язык один на весь индекс**. -`--roadmap-sections` у `init` нет: выбирать нечего. +секции — нет ответа на её часть вопроса), **язык один на весь индекс**, **порядок +канонический**. `--roadmap-sections` у `init` нет: выбирать нечего. -**Заголовок секции — с прописной, после него пустая строка.** Во всех индексах -одинаково, включая секции беклога, которые проект называет сам. Написание -канонических секций правит `check --fix` (заодно и ссылку на секцию в мете -файлов: имя секции принадлежит заголовку индекса, файл на неё только -ссылается); отбивку он ставит везде. +**Заголовок секции отбит пустой строкой с обеих сторон и написан с прописной.** +Во всех индексах одинаково, включая секции беклога, которые проект называет сам. +Написание канонических секций правит `check --fix` (заодно и ссылку на секцию в +мете файлов: имя секции принадлежит заголовку индекса, файл на неё только +ссылается); отбивку и порядок он правит везде. Оговорка про `Сопровождение`: слово `окружение` сюда не годится — в `architecture.md` оно уже значит боевое окружение приложения, и одно слово в двух diff --git a/av-dev-pm/skills/tasks/references/task-format.md b/av-dev-pm/skills/tasks/references/task-format.md index 52a9de0..a3db50c 100644 --- a/av-dev-pm/skills/tasks/references/task-format.md +++ b/av-dev-pm/skills/tasks/references/task-format.md @@ -225,7 +225,7 @@ | Файл | Что отвечает | Секции | | --- | --- | --- | -| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические: `Готово`, `Запланировано`, `Направления`, `Сопровождение` (англ. `Done`, `Planned`, `Directions`, `Operations`) | +| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические и в этом порядке: `Запланировано`, `Направления`, `Сопровождение`, `Готово` (англ. `Planned`, `Directions`, `Operations`, `Done`) | | `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию Ядро/Инфра) | | `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» | | `REJECTED.md` | что ушло без реализации и почему | — | @@ -250,11 +250,13 @@ строки не той формы, что у прочих индексов: дата, слаг, заголовок — как в `REJECTED.md`, и по той же причине (файла уже нет, ссылаться некуда). -**Секции роадмапа закреплены** — состав, полнота и единство языка проверяются -`check`; секции беклога проект называет сам. Почему так — SKILL.md. +**Секции роадмапа закреплены** — состав, полнота, единство языка и **порядок** +проверяются `check`; секции беклога проект называет сам. Почему так — SKILL.md. +Порядок закреплён потому, что `Готово` копится: стоя первым, достигнутое +отодвигает за экран то, ради чего роадмап открывают чаще всего. -**Заголовок секции пишется с прописной, и после него идёт пустая строка** — во -всех индексах, включая секции беклога, имена которых выбирает проект. Написание +**Заголовок секции пишется с прописной и отбивается пустой строкой с обеих +сторон** — во всех индексах, включая секции беклога, имена которых выбирает проект. Написание канонических секций и отбивку правит `check --fix`; он же сводит написание секции в мете файла с заголовком индекса — **имя секции принадлежит заголовку**, файл на неё лишь ссылается, и принадлежность сверяется по нижнему регистру. diff --git a/av-dev-pm/skills/tasks/scripts/tasks.py b/av-dev-pm/skills/tasks/scripts/tasks.py index 44fd1ca..157d19a 100755 --- a/av-dev-pm/skills/tasks/scripts/tasks.py +++ b/av-dev-pm/skills/tasks/scripts/tasks.py @@ -13,9 +13,9 @@ av-dev, и подгоняется под него проект. Имена вн docs/tasks/ items/ задачи и цели файлами, .md ROADMAP.md состояние проекта: что уже умеет и чего ещё не умеет. - Секции канонические: Готово | Запланировано | - Направления | Сопровождение (или Done | Planned | - Directions | Operations — один язык на весь индекс) + Секции канонические и в этом порядке: Запланировано | + Направления | Сопровождение | Готово (или Planned | + Directions | Operations | Done — один язык на индекс) BACKLOG.md что можно взять — только задачи, целей здесь нет SPRINT.md текущий спринт: цель, набор, дата, слаг REJECTED.md ушедшее БЕЗ реализации, с причиной и датой @@ -131,7 +131,7 @@ DEFAULT_SECTIONS = "Ядро,Инфра" # Секции роадмапа **канонические**, в отличие от секций беклога. Причина не в # любви к единообразию: у каждой своя семантика — достигнутое, очередь, долгие -# направления, работа над инструментом, — в первую пишет сам `close`, и роадмап, +# направления, работа по сопровождению, — в достигнутое пишет сам `close`, и роадмап, # названный по-своему, читался бы только своим автором. Секции беклога семантики # не несут, это полки, и остаются делом проекта. # @@ -139,13 +139,17 @@ DEFAULT_SECTIONS = "Ядро,Инфра" # индекс** — вперемешку это дрейф, который check называет вслух. Сверка везде # идёт по нижнему регистру, а пишется — как здесь: заголовок предложением, с # прописной. +# Порядок значим и проверяется: достигнутое **копится**, и стоя первым оно со +# временем отодвигает за экран всё, ради чего роадмап открывают. ROADMAP_SECTIONS = ( - ("Готово", "Done"), # достигнутое: что приложение уже умеет ("Запланировано", "Planned"), # очередь значима, обоснована прозой ("Направления", "Directions"), # очереди нет, тянутся долго ("Сопровождение", "Operations"), # чем держат проект, а не что умеет приложение + ("Готово", "Done"), # достигнутое: что приложение уже умеет ) -ACHIEVED, PLANNED = 0, 1 # индексы в ROADMAP_SECTIONS +# Позиции — из самого кортежа, а не числами: переставили секцию — индексы +# переехали сами. +PLANNED, DIRECTIONS, OPERATIONS, ACHIEVED = range(len(ROADMAP_SECTIONS)) DEFAULT_ROADMAP_SECTIONS = ",".join(ru for ru, _ in ROADMAP_SECTIONS) # Мета — список под заголовком, поле на строку. Старая форма (все поля одной @@ -568,17 +572,28 @@ def action_title(title: str) -> bool: def spaced_sections(lines: list[str]) -> list[str]: - """Отбивка после заголовка секции. Заголовок, пустая строка, потом - содержимое — во всех индексах одинаково. + """Отбивка вокруг заголовка секции: пустая строка перед ним и после него. Живёт на записи, а не на вставке: через `Plan.index` проходит **каждая** запись индекса, и чинить отбивку в каждом месте вставки значило бы - полагаться на то, что ни одно из них не забыли.""" + полагаться на то, что ни одно из них не забыли. Перед заголовком — не + педантизм: перестановка секций двигает целые блоки, и два заголовка легко + оказываются вплотную друг к другу. + + Заодно схлопывает подряд идущие пустые строки: удаление строки индекса + оставляет после себя две, и без этого шага пустоты копятся.""" out: list[str] = [] for i, line in enumerate(lines): + if SECTION.match(line): + if out and out[-1].strip(): + out.append("") + out.append(line) + if i + 1 < len(lines) and lines[i + 1].strip(): + out.append("") + continue + if not line.strip() and out and not out[-1].strip(): + continue out.append(line) - if SECTION.match(line) and i + 1 < len(lines) and lines[i + 1].strip(): - out.append("") return out @@ -591,9 +606,6 @@ def index_lint(lines: list[str], label: str) -> list[str]: for num, line in enumerate(lines, 1): if (m := SECTION.match(line)): section = m.group(1) - if num < len(lines) and lines[num].strip(): - errors.append(f"{label}:{num}: после заголовка «{section}» нет" - f" пустой строки; починит `check --fix`") continue if not line.startswith("- ["): continue @@ -610,6 +622,13 @@ def index_lint(lines: list[str], label: str) -> list[str]: f" (первая — строка {seen[target]})") else: seen[target] = num + # Оформление сверяется **самим нормализатором**, а не своим набором условий: + # два описания одного правила разъедутся, и `check` начнёт молчать о том, + # что `--fix` правит (или наоборот). + if spaced_sections(lines) != lines: + errors.append(f"{label}: оформление секций — заголовок отбивается пустой" + f" строкой с обеих сторон, подряд идущих пустых строк не" + f" бывает; починит `check --fix`") return errors @@ -1218,8 +1237,34 @@ def canon_section(lines: list[str], which: int) -> str | None: return None +def roadmap_ordered(lines: list[str]) -> list[str]: + """Секции роадмапа, переставленные в канонический порядок вместе с их + содержимым. Преамбула остаётся на месте. + + Чужая секция останавливает перестановку целиком: её место в порядке + неизвестно, а угадывать значило бы переложить чьи-то строки наугад. О ней + скажет `roadmap_lint`, и человек решит сам.""" + heads = [(i, m.group(1)) for i, line in enumerate(lines) if (m := SECTION.match(line))] + if not heads: + return lines + order = {n.lower(): i for i, pair in enumerate(ROADMAP_SECTIONS) for n in pair} + if any(name.lower() not in order for _, name in heads): + return lines + blocks = [] + for k, (i, name) in enumerate(heads): + end = heads[k + 1][0] if k + 1 < len(heads) else len(lines) + blocks.append((order[name.lower()], lines[i:end])) + if [rank for rank, _ in blocks] == sorted(rank for rank, _ in blocks): + return lines + out = lines[:heads[0][0]] + for _, block in sorted(blocks, key=lambda b: b[0]): + out = out + block + return out + + def roadmap_lint(lines: list[str], label: str) -> list[str]: - """Секции роадмапа: все канонические, все на месте, все на одном языке.""" + """Секции роадмапа: все канонические, все на месте, все на одном языке и в + каноническом порядке.""" known = {n.lower(): i for i, pair in enumerate(ROADMAP_SECTIONS) for n in pair} errors: list[str] = [] seen: dict[int, str] = {} @@ -1251,6 +1296,12 @@ def roadmap_lint(lines: list[str], label: str) -> list[str]: f" целиком — отсутствующая секция это отсутствующий ответ") if len(langs) > 1: errors.append(f"{label}: секции вперемешку на двух языках — выбери один") + if not missing and roadmap_ordered(lines) != lines: + canon = ", ".join(pair[0] for pair in ROADMAP_SECTIONS) + errors.append(f"{label}: секции не в каноническом порядке ({canon}) —" + f" достигнутое копится и потому стоит последним, иначе оно" + f" отодвигает за экран то, ради чего роадмап открывают;" + f" переставит `check --fix`") return errors @@ -2199,6 +2250,11 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]: lines[j] = f"## {want}" fixed.append(f"{lay.name(kind)}: секция «{m.group(1)}» → «{want}»") dirty.add(kind) + if (moved := roadmap_ordered(lines)) != lines: + lines[:] = moved + fixed.append(f"{lay.name(kind)}: секции переставлены в канонический" + f" порядок ({', '.join(p[0] for p in ROADMAP_SECTIONS)})") + dirty.add(kind) if spaced_sections(lines) != lines: fixed.append(f"{lay.name(kind)}: отбивка после заголовков секций") dirty.add(kind) @@ -2272,16 +2328,17 @@ def init_files(lay: Layout, sections: list[str], roadmap_sections: list[str], f"Цель — возможность приложения, файл `[goal]` в `{lay.cfg['items']}/`; её\n" "задачи здесь **не перечисляются** — перечень даёт\n" "`tasks.py list --goal <слаг>`.\n\n" + f"- **{ROADMAP_SECTIONS[PLANNED][0]}** — очередь значима и обосновывается прозой;\n" + f"- **{ROADMAP_SECTIONS[DIRECTIONS][0]}** — очереди нет, тянутся долго;\n" + f"- **{ROADMAP_SECTIONS[OPERATIONS][0]}** — чем держат проект: инструмент,\n" + " процесс, эксплуатация. Не возможности приложения, и отдельно —\n" + " чтобы не читаться как обещание продукта;\n" f"- **{ROADMAP_SECTIONS[ACHIEVED][0]}** — достигнутое: строку пишет\n" " `tasks.py close <цель> --implemented`, ссылки на файл в ней нет —\n" - " файл удаляется, поведение живёт в спеках;\n" - f"- **{ROADMAP_SECTIONS[PLANNED][0]}** — очередь значима и обосновывается прозой;\n" - f"- **{ROADMAP_SECTIONS[2][0]}** — очереди нет, тянутся долго;\n" - f"- **{ROADMAP_SECTIONS[3][0]}** — чем держат проект: инструмент,\n" - " процесс, эксплуатация. Не возможности приложения, и отдельно —\n" - " чтобы не читаться как обещание продукта.\n\n" + " файл удаляется, поведение живёт в спеках. Стоит последней: копится.\n\n" "Секции **канонические** и переименованию проектом не подлежат:\n" - "у каждой свой смысл, и в первую пишет сам `close`. Английский\n" + "у каждой свой смысл, и в достигнутое пишет сам `close`. Порядок\n" + "тоже канонический. Английский\n" f"вариант — {' | '.join(pair[1] for pair in ROADMAP_SECTIONS)}," " один язык на весь\nиндекс.\n\n" + "".join(f"## {s}\n\n" for s in roadmap_sections))