канон 4: секция «Сопровождение», «Готово» вниз, порядок закреплён

healthlog уже переехал на канон 3, а переименование секции я внёс
правкой записи версии 3 задним числом — то есть переписал текст, по
которому он ехал. Посылка «ни один проект на каноне 3 не стоит» была
ложной, решение ШШШ отменено.

Запись версии — черновик ровно до первого переехавшего проекта. После
этого она история, и любое изменение канона заводит новую версию, даже
если меняется одно слово. Проверять дёшево: grep '"canon"' по живым
проектам. Дорого обратное — проект, повышенный по тексту, которого
больше не существует, невоспроизводим.

Запись версии 3 восстановлена дословно (Разработка | Tooling),
переименование уехало в версию 4. jellybit, стоящий на каноне 2,
прочтёт обе записи подряд и заведёт Разработка, чтобы через шаг
переименовать; в шаг версии 3 добавлена оговорка «едешь сразу на 4 —
заводи Готово последней и не переставляй дважды».

«Готово» переехало вниз, и порядок секций стал каноническим.
Достигнутое копится: через год этой секции больше, чем всех остальных
вместе, и стоя первой она отодвигает за экран то, ради чего роадмап
открывают чаще всего. Порядок проверяет roadmap_lint, переставляет
check --fix — вместе с содержимым секций, потому что двигать десяток
строк руками это работа, на которой ошибаются. Чужую секцию
перестановка не трогает вовсе: её место в порядке неизвестно.

Индексы позиций считаются из самого кортежа: ACHIEVED был 0 и стал 3,
хардкод пережил бы перестановку молча и сломал бы close.

Обкатка нашла два дефекта оформления, оба порождённые самой
перестановкой. Отбивка нужна и перед заголовком — сдвиг блоков ставит
два заголовка вплотную. Удаление строки индекса оставляет две пустые
подряд, и пустоты копятся. Проверка оформления теперь сверяется с самим
нормализатором, а не своим набором условий: два описания одного правила
разъедутся, и check начнёт молчать о том, что --fix правит.

CANON_VERSION = 4 в docs.py, примеры .pm.json в canon.md и skeletons.md.

DECISIONS тема 26 (ЭЭЭ, ЮЮЮ, ЯЯЯ, следствия 98–100).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-08-04 20:29:15 +03:00
co-authored by Claude Opus 5
parent d7e9740c73
commit 069205ac69
9 changed files with 240 additions and 56 deletions
+58 -5
View File
@@ -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. **Версия канона отделяет состояния проектов, а не редакции текста** — и
ровно поэтому её нельзя не поднять, когда состояние хоть одного проекта
уже зафиксировано.
+14 -4
View File
@@ -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: `Готово` заводить **последней**, секцию
сопровождения — сразу с новым именем, переставлять дважды не нужно
+2 -2
View File
@@ -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"
+61 -5
View File
@@ -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`: он поднимет
@@ -392,7 +392,7 @@ severity стоит здесь, а не выводится каждым прох
```json
{
"canon": 2
"canon": 4
}
```
+1 -1
View File
@@ -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
+17 -11
View File
@@ -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` оно уже значит боевое окружение приложения, и одно слово в двух
@@ -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`; он же сводит написание
секции в мете файла с заголовком индекса — **имя секции принадлежит заголовку**,
файл на неё лишь ссылается, и принадлежность сверяется по нижнему регистру.
+79 -22
View File
@@ -13,9 +13,9 @@ av-dev, и подгоняется под него проект. Имена вн
docs/tasks/
items/ задачи и цели файлами, <slug>.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))