diff --git a/DECISIONS.md b/DECISIONS.md index 38752b0..7923672 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1407,16 +1407,36 @@ SSS: рубрика на узел без нового понятия порож словаре, тесте готовности, автомате переходов, `split.md` и трёх местах `tasks.py`. -**ГГГ. Имена секций роадмапа временные.** `умеет` / `строим` / `направления` / -`станок` приняты как рабочие и признаны неудачными на месте — особенно `станок` -(слово пришло из `CLAUDE.md`, где «общий станок» уже значит инструмент, -врывающийся в спринт). Вопрос запаркован до отдельного захода. +**ГГГ. Имена секций роадмапа — `готово` / `запланировано` / `направления` / +`разработка`.** Первый набор (`умеет` / `строим` / `станок`) прожил один заход и +был признан неудачным. Из четырёх предложенных имён отвергнуто одно, и по +проверяемой причине: **`окружение` уже занято** — в `architecture.md` это боевое +окружение приложения, «где работает, что рядом, кто перезапускает», и одно слово +в двух смыслах развело бы документы канона. Взято `разработка`. + +Принятый компромисс назван вслух: `готово` слегка тянет обратно в трекерную рамку +«состояние работы», тогда как секция про **возможность**. Перевесила читаемость с +первого взгляда, а смысл несут заголовки целей внутри секции. Так же принято, что +цель в `запланировано` может быть уже наполовину построена: это очередь, а не +«не начато», а «в работе» живёт в `SPRINT.md`. + +**ДДД. Секции роадмапа канонические, секции беклога — нет.** Разница выведена, а +не назначена: у секций роадмапа есть **семантика** (достигнутое, очередь, долгое, +не про продукт), в первую пишет сам `close`, и роадмап, названный по-своему, +читался бы только своим автором. Секции беклога (`ядро`, `инфра`) семантики не +несут — это полки. Поэтому `check` проверяет у роадмапа три вещи: состав закреплён +(чужая секция — ошибка), все четыре обязаны быть, язык один на весь индекс; +`--roadmap-sections` у `init` упразднён. Английский набор — `done` | `planned` | +`directions` | `tooling`. + +Проверено на том самом случае, ради которого правило и заводилось: секция «Что +уже пройдено», которую healthlog вёл руками, теперь называется ошибкой поимённо. ### Что из этого следует -78. **Секция достигнутого — единственная, чьё имя знает скрипт.** Остальные - берутся из заголовков индекса как есть; в эту `close` пишет сам, поэтому её - имя живёт в `docs/.pm.json`, ключ `tasks.achieved_section`. +78. **Ключа `tasks.achieved_section` не появилось.** Секция достигнутого + опознаётся по каноническому имени в любом из двух языков, и лишний knob не + заводится: канонический состав отвечает на тот же вопрос надёжнее конфига. 79. **`reopen` цели снимает строку достигнутого.** Иначе роадмап продолжает утверждать, что приложение умеет то, что вернулось в работу. 80. **Прозаический раздел в индексе — дрейф.** Любой `##` проверка считает diff --git a/TODO.md b/TODO.md index 1f615e8..d4617e2 100644 --- a/TODO.md +++ b/TODO.md @@ -167,15 +167,11 @@ jellybit 43. Шаги повышения — [changelog.md](av-dev-pm/skills/can - [ ] род работы и раздел «Затрагивает» — **не задним числом**: сперва то, что идёт в ближайший набор (`sprint take` без них откажет), остальное по ходу переоценки (PPP) -- [ ] секции роадмапа: `порядок` → `строим`, `темы` → `направления`, завести - `умеет` и `станок`; прозаические разделы healthlog («Что уже пройдено», - «Почему в таком порядке») разложить — звенья строками в `умеет`, - обоснование очереди прозой внутри `строим` (тема 19, 80) +- [ ] секции роадмапа: `порядок` → `запланировано`, `темы` → `направления`, + завести `готово` и `разработка`; прозаические разделы healthlog («Что уже + пройдено», «Почему в таком порядке») разложить — звенья строками в + `готово`, обоснование очереди прозой внутри `запланировано` (тема 19, 80). + `check` теперь называет чужую секцию ошибкой, так что шаг обязателен - [ ] переформулировать цели ответом на «что приложение будет уметь»; цели не - про приложение («Процесс и качество разработки» в jellybit) — в `станок` - -## 7. Имена секций роадмапа — выбрать хорошие (тема 19, ГГГ) - -- [ ] `умеет` / `строим` / `направления` / `станок` приняты как временные. - Особенно плох `станок`; заодно проверить, не плохо ли то же слово в - `CLAUDE.md`, откуда оно взято («общий станок, врывающийся в спринт») + про приложение («Процесс и качество разработки» в jellybit) — в + `разработка` diff --git a/av-dev-pm/skills/canon/references/changelog.md b/av-dev-pm/skills/canon/references/changelog.md index a291496..b99e99e 100644 --- a/av-dev-pm/skills/canon/references/changelog.md +++ b/av-dev-pm/skills/canon/references/changelog.md @@ -31,11 +31,13 @@ upgrade` идёт по записям снизу вверх от версии п 2. **Раздел «Затрагивает»** в теле задачи — перечень границ, которых изменение касается (эндпоинт, таблица и миграция, формат на диске, публичный тип). Как и критерии приёмки, требуется к взятию в спринт, а не к заведению. -3. **Секции роадмапа** — четыре вместо двух: `умеет` (достигнутые цели строкой - с датой, без ссылки на файл), `строим` (очередь значима), `направления` - (очереди нет), `станок` (инструмент и процесс, не возможности приложения). - Имя секции достигнутого скрипт знает по конфигу — `tasks.achieved_section`. - Имена **временные** и будут пересмотрены (DECISIONS, тема 19). +3. **Секции роадмапа** — четыре вместо двух и **канонические**, в отличие от + секций беклога: `готово` (достигнутые цели строкой с датой, без ссылки на + файл), `запланировано` (очередь значима), `направления` (очереди нет), + `разработка` (инструмент и процесс, не возможности приложения). Английский + вариант — `done` | `planned` | `directions` | `tooling`, один язык на весь + индекс. Переименованию проектом не подлежат: у каждой свой смысл, и в первую + пишет сам `close`; `tasks.py check` проверяет состав. 4. **Умолчание профиля ревью сменилось** — это не раскладка, но проектный текст под него уже написан. `standard` стал рабочим умолчанием: миграция схемы, публичный контракт и инвариант ступень больше **не** поднимают, `wide` @@ -43,7 +45,7 @@ upgrade` идёт по записям снизу вверх от версии п в `docs/review.md` остаётся на месте, но его содержимое надо перечитать. **Что переехало:** `docs/tasks/PLAN.md` → `docs/tasks/ROADMAP.md`; достигнутая -цель — из небытия в секцию `умеет`: `close <цель> --implemented` удаляет файл, но +цель — из небытия в секцию `готово`: `close <цель> --implemented` удаляет файл, но **оставляет строку с датой**. Прежде роадмап отвечал только «что осталось», и половину его вопроса вели прозой руками. Вместе с файлом переименован ключ конфига `tasks.plan` → `tasks.roadmap` и токены @@ -74,16 +76,16 @@ upgrade` идёт по записям снизу вверх от версии п что для этого проекта считается **новым понятием** и **правилом идентичности**, — и убрать остальное, иначе проект возвращает себе прежнюю частоту полного набора уточнением. -7. Переименовать секции роадмапа: `порядок` → `строим`, `темы` → - `направления`; завести `умеет` **первой** и `станок` последней. Прозаические - разделы вроде «Что уже пройдено», которые велись руками, разложить: звенья — - строками в `умеет` (дата, слаг, что стало возможно), обоснование очереди - оставить прозой в `строим`. Любой `##` в индексе проверка считает секцией, - поэтому прозаический заголовок здесь — дрейф. +7. Переименовать секции роадмапа: `порядок` → `запланировано`, `темы` → + `направления`; завести `готово` **первой** и `разработка` последней. + Прозаические разделы вроде «Что уже пройдено», которые велись руками, + разложить: звенья — строками в `готово` (дата, слаг, что стало возможно), + обоснование очереди оставить прозой в `запланировано`. Любой `##` в индексе + проверка считает секцией, и теперь `check` называет чужую секцию ошибкой. 8. Переформулировать цели ответом на **«что приложение будет уметь»**: не «Работа со слиянием», а «Исход слияния не зависит от порядка доставки». Свойство поведения — законная цель. Цель, которая не про приложение - (процесс, инструмент), переезжает в `станок`. + (процесс, инструмент), переезжает в `разработка`. 9. `[epic]`, если он в проекте заводился: это либо цель, либо набор задач под общей целью. `check` назовёт его неизвестным типом. 10. `docs/.pm.json`: `"canon": 3`. diff --git a/av-dev-pm/skills/init/SKILL.md b/av-dev-pm/skills/init/SKILL.md index 070bd68..695a36e 100644 --- a/av-dev-pm/skills/init/SKILL.md +++ b/av-dev-pm/skills/init/SKILL.md @@ -49,7 +49,7 @@ description: "Завести новый проект — сессия вопро 5. **Чем краснеет гейт.** Какие проверки обязательны; что красит безусловно; чего в гейте намеренно не будет и кто тогда это гоняет. 6. **Первые цели.** Возможности приложения, а не задачи: три-пять целей в - `строим`, каждая — ответ на «что приложение будет уметь», с + `запланировано`, каждая — ответ на «что приложение будет уметь», с обоснованием очереди прозой. ### Как вести diff --git a/av-dev-pm/skills/session/references/cadence.md b/av-dev-pm/skills/session/references/cadence.md index 3541ebf..41620a6 100644 --- a/av-dev-pm/skills/session/references/cadence.md +++ b/av-dev-pm/skills/session/references/cadence.md @@ -175,8 +175,9 @@ ## Шаг 4. Выбор цели и набор спринта -1. **Покажи состояние проекта**: секцию `умеет` (что уже сделано — это половина - ответа на «где мы»), затем `строим` с обоснованием очереди, `направления`, и +1. **Покажи состояние проекта**: секцию `готово` (что приложение уже умеет — + это половина ответа на «где мы»), затем `запланировано` с обоснованием + очереди, `направления`, и по каждой цели-кандидату — сколько под ней задач без открытых вопросов (`list --goal <слаг>`). Цель без готовых задач набором не станет: её сперва надо декомпозировать. diff --git a/av-dev-pm/skills/tasks/SKILL.md b/av-dev-pm/skills/tasks/SKILL.md index 4e65f96..2bbf715 100644 --- a/av-dev-pm/skills/tasks/SKILL.md +++ b/av-dev-pm/skills/tasks/SKILL.md @@ -69,17 +69,26 @@ docs/tasks/ **Четыре секции роадмапа, и первая отвечает на половину вопроса:** -| Секция | Что в ней | -| --- | --- | -| `умеет` | достигнутые цели — строкой с датой, **без ссылки на файл**: файл удалён, поведение живёт в спеках | -| `строим` | очередь значима и обосновывается прозой рядом | -| `направления` | очереди нет, тянутся долго | -| `станок` | инструмент и процесс разработки — не возможности приложения, и потому отдельно | +| Секция | Англ. | Что в ней | +| --- | --- | --- | +| `готово` | `done` | достигнутые цели — строкой с датой, **без ссылки на файл**: файл удалён, поведение живёт в спеках | +| `запланировано` | `planned` | очередь значима и обосновывается прозой рядом | +| `направления` | `directions` | очереди нет, тянутся долго | +| `разработка` | `tooling` | инструмент и процесс — не возможности приложения, и потому отдельно | -Имена секций сейчас **временные**: они читаются хуже, чем должны, и будут -пересмотрены (`DECISIONS.md`, тема 19). Проект вправе назвать свои иначе — -секции берутся из заголовков индекса, — но дом достигнутого скрипт обязан знать -по имени, и оно живёт в `docs/.pm.json`, ключ `tasks.achieved_section`. +**Секции роадмапа канонические, секции беклога — нет**, и разница не в любви к +единообразию. У каждой секции роадмапа свой смысл, в первую пишет сам `close`, и +роадмап, названный по-своему, читался бы только своим автором. Секции беклога +(`ядро`, `инфра`) смысла не несут — это полки, и остаются делом проекта. + +Отсюда три правила, которые проверяет `tasks.py check`: **состав закреплён** +(чужая секция — ошибка, а не вольность), **все четыре обязаны быть** (нет +секции — нет ответа на её часть вопроса), **язык один на весь индекс**. +`--roadmap-sections` у `init` нет: выбирать нечего. + +Оговорка про `разработка`: слово `окружение` сюда не годится — в +`architecture.md` оно уже значит боевое окружение приложения, и одно слово в двух +смыслах развело бы документы канона. **Секции «блокеры» в беклоге нет.** Блокер — это *состояние* (спринт не может продолжаться ни одной задачей), а не полка: он живёт ровно до ответа человека, и @@ -101,7 +110,7 @@ docs/tasks/ всех наборов без отдельного журнала. **У достигнутой цели запись остаётся, и это единственное исключение.** Файл -удаляется так же, а строка переезжает в секцию `умеет` с датой. Причина в том, +удаляется так же, а строка переезжает в секцию `готово` с датой. Причина в том, что цель — не работа, а **возможность**: «что приложение умеет» это половина вопроса, ради которого роадмап и открывают, и стирать её вместе с файлом значит оставить инструмент, отвечающий только «что осталось». Вторым домом это не @@ -163,9 +172,9 @@ stateDiagram-v2 отведена отдельная секция роадмапа, чтобы они были видны в том же экране и при этом не читались как возможности продукта. -Секция выбирается так: очередь значима и обоснована прозой — `строим`; тянется -долго и очереди не имеет — `направления`; не про приложение — `станок`; -достигнутое кладёт туда сам `close`. +Секция выбирается так: очередь значима и обоснована прозой — `запланировано`; +тянется долго и очереди не имеет — `направления`; не про приложение — +`разработка`; в `готово` кладёт сам `close`. - **Список задач цели выводится, а не хранится.** В теле цели — зачем она и что считается её завершением; перечня задач там нет. Он был бы третьим индексом и @@ -175,7 +184,7 @@ stateDiagram-v2 - **Статус цели выводится.** Цель достигнута, когда у неё не осталось открытых задач; `[x]`/`[~]` руками не ведутся, а `close` цели с живыми задачами скрипт запретит. Достижение — `close <цель> --implemented`: файл удаляется, - строка с датой переезжает в `умеет`. Ошиблись — `reopen` вернёт файл и + строка с датой переезжает в `готово`. Ошиблись — `reopen` вернёт файл и **снимет строку достигнутого**, чтобы роадмап не утверждал того, чего нет. Единственная оговорка: цель без задач неотличима — «ещё не разобрана» или «всё закрыто». Различает **тег `decomposed`** в мете цели: он ставится, когда цель разложена на задачи. Тег, а не строка в теле — diff --git a/av-dev-pm/skills/tasks/references/task-format.md b/av-dev-pm/skills/tasks/references/task-format.md index 3189445..b88c187 100644 --- a/av-dev-pm/skills/tasks/references/task-format.md +++ b/av-dev-pm/skills/tasks/references/task-format.md @@ -187,7 +187,7 @@ которой задачи есть, а цель с тегом и без задач — прямое приглашение закрыть. - Цель живёт в `ROADMAP.md` и **никогда** — в `BACKLOG.md` или `SPRINT.md`. - **Достигнутая цель не исчезает.** `close <слаг> --implemented` удаляет файл и - переносит строку в секцию `умеет` с датой: + переносит строку в секцию `готово` с датой: `- 2026-08-04 \`merge-order\` — Исход слияния не зависит от порядка доставки. …` Ссылки на файл в ней нет — файл удалён, а битая ссылка это ошибка `check`. Поведение живёт в спеках проекта; роадмап отвечает, **когда и в каком порядке** @@ -219,7 +219,7 @@ | Файл | Что отвечает | Секции | | --- | --- | --- | -| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | `умеет` (достигнутое), `строим` (очередь значима), `направления` (очереди нет), `станок` (не про приложение) | +| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические: `готово`, `запланировано`, `направления`, `разработка` (англ. `done`, `planned`, `directions`, `tooling`) | | `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию ядро/инфра) | | `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» | | `REJECTED.md` | что ушло без реализации и почему | — | @@ -239,11 +239,13 @@ Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила бы правилу «эскалируем немедленно», поэтому `init` её не заводит, а `check` говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её. В секции -**`строим`** очередь значима и обосновывается прозой; двигают строку -`move --section строим --after <другой>`. В секции **`умеет`** строки -не той формы, что у прочих индексов: дата, слаг, заголовок — как в -`REJECTED.md`, и по той же причине (файла уже нет, ссылаться некуда). Имена -секций временные, см. SKILL.md. +**`запланировано`** очередь значима и обосновывается прозой; двигают строку +`move --section запланировано --after <другой>`. В секции **`готово`** +строки не той формы, что у прочих индексов: дата, слаг, заголовок — как в +`REJECTED.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 fbceadd..a324c5a 100755 --- a/av-dev-pm/skills/tasks/scripts/tasks.py +++ b/av-dev-pm/skills/tasks/scripts/tasks.py @@ -12,7 +12,10 @@ av-dev, и подгоняется под него проект. Имена вн docs/tasks/ items/ задачи и цели файлами, .md - ROADMAP.md состояние проекта: что уже умеет и чего ещё не умеет + ROADMAP.md состояние проекта: что уже умеет и чего ещё не умеет. + Секции канонические: готово | запланировано | + направления | разработка (или done | planned | + directions | tooling — один язык на весь индекс) BACKLOG.md что можно взять — только задачи, целей здесь нет SPRINT.md текущий спринт: цель, набор, дата, слаг REJECTED.md ушедшее БЕЗ реализации, с причиной и датой @@ -46,7 +49,7 @@ av-dev, и подгоняется под него проект. Имена вн разъедется на синонимах, и отбор по роду перестанет отвечать. Использование: - tasks.py init [--dir DIR] [--sections …] [--roadmap-sections …] [--items …] + tasks.py init [--dir DIR] [--sections …] [--items …] [--backlog …] … tasks.py check [--dir DIR] [--fix] tasks.py list [--dir DIR] [--stale] [--section S] [--type T] [--tag a,b] [--goal S] [--kind K] [--index backlog|sprint|roadmap|all] @@ -114,9 +117,6 @@ DEFAULTS = { "sprint": "SPRINT.md", "rejected": "REJECTED.md", "sprint_section": "Набор", - # Секция роадмапа, куда переезжает достигнутая цель. Скрипт обязан знать её - # имя: остальные секции он берёт из заголовков как есть, а в эту пишет сам. - "achieved_section": "умеет", "criteria_heading": "Критерии приёмки", "surface_heading": "Затрагивает", "questions_heading": "Вопросы", @@ -128,7 +128,23 @@ DEFAULTS = { PATH_KEYS = ("items", "backlog", "roadmap", "sprint", "rejected") DEFAULT_SECTIONS = "ядро,инфра" -DEFAULT_ROADMAP_SECTIONS = "умеет,строим,направления,станок" + +# Секции роадмапа **канонические**, в отличие от секций беклога. Причина не в +# любви к единообразию: у каждой своя семантика — достигнутое, очередь, долгие +# направления, работа над инструментом, — в первую пишет сам `close`, и роадмап, +# названный по-своему, читался бы только своим автором. Секции беклога семантики +# не несут, это полки, и остаются делом проекта. +# +# Пара на секцию: русское имя и английское. Проект держит **один язык на весь +# индекс** — вперемешку это дрейф, который check называет вслух. +ROADMAP_SECTIONS = ( + ("готово", "done"), # достигнутое: что приложение уже умеет + ("запланировано", "planned"), # очередь значима, обоснована прозой + ("направления", "directions"), # очереди нет, тянутся долго + ("разработка", "tooling"), # инструмент и процесс, не про приложение +) +ACHIEVED, PLANNED = 0, 1 # индексы в ROADMAP_SECTIONS +DEFAULT_ROADMAP_SECTIONS = ",".join(ru for ru, _ in ROADMAP_SECTIONS) # Мета — список под заголовком, поле на строку. Старая форма (все поля одной # строкой через `·`) читается по-прежнему: у проектов на диске лежат файлы в @@ -934,7 +950,10 @@ def check(lay: Layout, fix: bool = False) -> int: if name not in tasks: errors.append(f"{label[kind]}:{entry['line']}: ссылка на несуществующий" f" {lay.cfg['items']}/{name}") - errors += index_lint(read_lines(lay.index(kind)), label[kind]) + lines = read_lines(lay.index(kind)) + errors += index_lint(lines, label[kind]) + if kind == "roadmap": + errors += roadmap_lint(lines, label[kind]) if goal_of_sprint and goal_of_sprint + ".md" not in tasks: errors.append(f"{label['sprint']}: цель «{goal_of_sprint}» не найдена" @@ -1104,6 +1123,52 @@ def section_headers(lines: list[str]) -> list[tuple[int, str]]: return [(i, m.group(1)) for i, line in enumerate(lines) if (m := SECTION.match(line))] +def canon_section(lines: list[str], which: int) -> str | None: + """Имя канонической секции роадмапа **как она названа в этом индексе**. + + Проект пишет либо русские имена, либо английские; скрипт принимает оба и + возвращает то, что нашёл, — иначе `insert_entry` промахнётся мимо секции, + которая есть, но названа на другом языке. + """ + names = {n.lower() for n in ROADMAP_SECTIONS[which]} + for _, section in section_headers(lines): + if section.lower() in names: + return section + return None + + +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] = {} + langs: set[int] = set() + for _, section in section_headers(lines): + i = known.get(section.lower()) + if i is None: + ru = ", ".join(pair[0] for pair in ROADMAP_SECTIONS) + errors.append(f"{label}: секция «{section}» не из канона роадмапа" + f" ({ru}). Секции роадмапа несут смысл и потому" + f" закреплены; прозаический заголовок здесь — секция," + f" в которую может уехать цель") + continue + langs.add(0 if section.lower() == ROADMAP_SECTIONS[i][0] else 1) + if i in seen: + errors.append(f"{label}: секция «{section}» повторяет «{seen[i]}» —" + f" это одна и та же секция на двух языках") + else: + seen[i] = section + missing = [ROADMAP_SECTIONS[i][0] for i in range(len(ROADMAP_SECTIONS)) + if i not in seen] + if missing: + errors.append(f"{label}: нет секций: {', '.join(missing)}." + f" Роадмап отвечает на «что умеет и чего не умеет»" + f" целиком — отсутствующая секция это отсутствующий ответ") + if len(langs) > 1: + errors.append(f"{label}: секции вперемешку на двух языках — выбери один") + return errors + + def find_section(lines: list[str], name: str) -> tuple[int | None, str]: for i, s in section_headers(lines): if s.lower() == (name or "").lower(): @@ -1474,6 +1539,17 @@ def cmd_move(lay: Layout, a: argparse.Namespace) -> int: return EXIT_OK +def section_of_achieved(lines: list[str]) -> str: + """Секция достигнутого этого роадмапа. Нет её — отказ по существу: цель + закрыть некуда, и молча удалить её значит стереть половину ответа.""" + name = canon_section(lines, ACHIEVED) + if name is None: + ru, en = ROADMAP_SECTIONS[ACHIEVED] + raise Usage(f"в роадмапе нет секции достигнутого («{ru}» или «{en}») —" + f" закрывать цель некуда. Заведи секцию и повтори") + return name + + def is_achieved(task: dict, a: argparse.Namespace) -> bool: """Цель, закрытая как достигнутая: `close --implemented` без причины.""" return task["type"] == GOAL and not a.reason @@ -1528,20 +1604,22 @@ def cmd_close(lay: Layout, a: argparse.Namespace) -> int: for kind_index, (lines, ei) in places.items(): lines.pop(ei) if achieved is not None and kind_index == "roadmap": - insert_entry(lines, lay.cfg["achieved_section"], achieved, first=True) + insert_entry(lines, section_of_achieved(lines), achieved, first=True) achieved = None plan.index(lay, kind_index, lines) if achieved is not None: # строки в роадмапе не было — чиним lines = read_lines(lay.index("roadmap")) - insert_entry(lines, lay.cfg["achieved_section"], achieved, first=True) + insert_entry(lines, section_of_achieved(lines), achieved, first=True) plan.index(lay, "roadmap", lines) plan.delete(path) plan.commit() if is_achieved(task, a): + # Имя берём из индекса: проект мог назвать секции по-английски. + where = (canon_section(read_lines(lay.index("roadmap")), ACHIEVED) + or ROADMAP_SECTIONS[ACHIEVED][0]) print(f"{a.slug}: цель достигнута — строка перенесена в" - f" {lay.name('roadmap')}, секция «{lay.cfg['achieved_section']}»," - f" файл удалён") + f" {lay.name('roadmap')}, секция «{where}», файл удалён") else: print(f"{a.slug}: {'записано в ' + lay.name('rejected') + ' + удалено' if a.reason else 'удалено (реализовано, есть коммит)'}") if "sprint" in places and a.reason: @@ -1655,7 +1733,7 @@ def cmd_reopen(lay: Layout, a: argparse.Namespace) -> int: for line in removed: print(f" снята строка {lay.name('rejected')}: {line.strip()}") for line in unachieved: - print(f" снята строка «{lay.cfg['achieved_section']}» в" + print(f" снята строка достигнутого в" f" {lay.name('roadmap')}: {line.strip()}") print(" роадмап больше не утверждает, что приложение это умеет") print(" сверь тело: оно восстановлено на момент удаления, всё позднейшее" @@ -2064,13 +2142,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"- **{lay.cfg['achieved_section']}** — достигнутое: строку пишет\n" + f"- **{ROADMAP_SECTIONS[ACHIEVED][0]}** — достигнутое: строку пишет\n" " `tasks.py close <цель> --implemented`, ссылки на файл в ней нет —\n" " файл удаляется, поведение живёт в спеках;\n" - "- **строим** — очередь значима и обосновывается прозой;\n" + "- **запланировано** — очередь значима и обосновывается прозой;\n" "- **направления** — очереди нет, тянутся долго;\n" - "- **станок** — инструмент и процесс разработки, не возможности\n" - " приложения. Отдельно, чтобы не смешиваться с ними.\n\n" + "- **разработка** — инструмент и процесс, не возможности приложения.\n" + " Отдельно, чтобы не читаться как обещание продукта.\n\n" + "Секции **канонические** и переименованию проектом не подлежат:\n" + "у каждой свой смысл, и в первую пишет сам `close`. Английский\n" + "вариант — done | planned | directions | tooling, один язык на весь\n" + "индекс.\n\n" + "".join(f"## {s}\n\n" for s in roadmap_sections)) out[lay.index("sprint")] = empty_sprint(lay) out[lay.index("rejected")] = ( @@ -2100,8 +2182,9 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int: if lay.index("backlog").exists(): raise Usage(f"{lay.index('backlog')} уже есть — каталог задач заведён") - sections, roadmap_sections = uniq_sections(a.sections), uniq_sections(a.roadmap_sections) - if not sections or not roadmap_sections: + sections = uniq_sections(a.sections) + roadmap_sections = uniq_sections(DEFAULT_ROADMAP_SECTIONS) + if not sections: raise Usage("пустой список секций") blockers = [s for s in sections if s.lower() in BLOCKER_SECTIONS] if blockers: @@ -2118,7 +2201,7 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int: plan.commit() print(f"каталог задач заведён: {root}") print(f" секции беклога: {', '.join(sections)};" - f" части роадмапа: {', '.join(roadmap_sections)}") + f" секции роадмапа канонические: {', '.join(roadmap_sections)}") if cfg: pm = (root / PM_CONFIG_REL).resolve() print(f" имена частей записаны в {pm if pm.is_file() else root / CONFIG_NAME}") @@ -2260,17 +2343,6 @@ def scan_list_file(path: Path) -> dict: return found -def first_open_section(raw: str) -> str: - """Секция, куда adopt кладёт выведенные цели: первая **не** достигнутая. - - Первой в роадмапе идёт секция достигнутого, и класть в неё цель, выведенную - из шага плана, значит объявить сделанным то, что ещё не начато. - """ - done = DEFAULTS["achieved_section"].lower() - names = [s.strip() for s in raw.split(",") if s.strip()] - return next((s for s in names if s.lower() != done), "строим") - - def cmd_adopt_scan(a: argparse.Namespace) -> int: sources = [Path(s) for s in a.sources] for s in sources: @@ -2286,7 +2358,7 @@ def cmd_adopt_scan(a: argparse.Namespace) -> int: rejected += sc.get("rejected", []) unclassified += sc.get("unclassified", []) goals += [{"slug": "", "title": g["title"], - "section": first_open_section(a.roadmap_sections), + "section": ROADMAP_SECTIONS[PLANNED][0], "from": g["from"], "step": g.get("step"), "done": g.get("done"), "body": f"Выведена из шага «{g['title']}» ({g['from']})." + ("\n\nШаг помечен закрытым — цель, скорее всего," @@ -2320,7 +2392,7 @@ def cmd_adopt_scan(a: argparse.Namespace) -> int: "sources": [str(s) for s in sources], "path_map": path_map, "sections_backlog": uniq_sections(a.sections), - "sections_roadmap": uniq_sections(a.roadmap_sections), + "sections_roadmap": uniq_sections(DEFAULT_ROADMAP_SECTIONS), "section_map": {}, "goals": goals, "items": items, @@ -2688,7 +2760,6 @@ def main() -> int: p = sub.add_parser("init", help="завести каталог задач в новом проекте") p.add_argument("--dir") p.add_argument("--sections", default=DEFAULT_SECTIONS) - p.add_argument("--roadmap-sections", dest="roadmap_sections", default=DEFAULT_ROADMAP_SECTIONS) p.add_argument("--items") p.add_argument("--backlog") p.add_argument("--roadmap") @@ -2702,7 +2773,7 @@ def main() -> int: s.add_argument("--target", default="docs/tasks") s.add_argument("--out", default="tasks-adopt-plan.json") s.add_argument("--sections", default=DEFAULT_SECTIONS) - s.add_argument("--roadmap-sections", dest="roadmap_sections", default=DEFAULT_ROADMAP_SECTIONS) + s = asub.add_parser("apply", help="записать каталог по подтверждённой карте") s.add_argument("--plan", required=True) s.add_argument("--refs", nargs="*", help="файлы и каталоги, где чинить ссылки на слаги")