@@ -7,11 +7,11 @@
ровно в одном из них за раз. `REJECTED.md` индексом не считается: он не говорит,
где запись числится, он кладбище ушедшего.
Раскладка. Путь каталога — `tasks/` в корне репозитория, жёстко. Каталог
принадлежит этому плагин у , а не канону документов: `docs/` ведёт другой плагин, и
проект, поставивший учёт работ без него, каталога `docs/` не имеет вовсе . Имена
внутри и **версия формата ** живут в `tasks/.tasks.json` ; журнал версий —
references/changelog.md рядом со скриптом .
Раскладка. Путь каталога — `tasks/` в корне репозитория по умолчанию; другой
называется ключом `[tasks] dir`. Каталог принадлежит этому скилл у , а не канону
документов: учёт работ ведут и в проекте, который к канону не приведён . Имена
частей и **версия раскладки ** живут в `.av-dev.toml` в корне ; журнал версий —
references/changelog.md скилла doc-canon .
tasks/
items/ задачи и цели файлами, <slug>.md
@@ -102,32 +102,47 @@ goal | feature | fix | chore | research, по-английски, как и пр
import argparse
import datetime
import importlib . util
import json
import re
import subprocess
import sys
from pathlib import Path
from types import ModuleType
CONFIG_NAME = " .tasks.json " # дом настроек и версии: свой файл в каталоге
PM_CONFIG_REL = " ../.pm.json " # прежний дом настроек: docs/.pm.json, ключ "tasks"
# Версия формата задач — **своя, а не канона документов**. Число живёт ключом
# `tasks` в `.tasks.json`, журнал версий — references/changelog.md рядом со
# скриптом, повышает его операция `upgrade` скилла `av-dev:task-track`.
def _load_shared ( ) - > ModuleType :
""" Общий читатель `.av-dev.toml` — `shared/config.py` этого же плагина.
Путь считается от файла скрипта: зовут его из репозитория проекта, где
дерева плагина в текущем каталоге нет.
"""
path = Path ( __file__ ) . resolve ( ) . parents [ 3 ] / " shared " / " config.py "
spec = importlib . util . spec_from_file_location ( " avdev_config " , path )
if spec is None or spec . loader is None :
print ( f " ОТКАЗ: не читается { path } — общий читатель настроек; "
f " переустанови плагин av-dev " , file = sys . stderr )
sys . exit ( 3 )
module = importlib . util . module_from_spec ( spec )
spec . loader . exec_module ( module )
return module
conf = _load_shared ( )
CONFIG_NAME = conf . CONFIG_NAME # дом настроек и версии: `.av-dev.toml` в корне
# Версия раскладки — **одна на плагин**, и живёт она в `shared/config.py`.
# Своей у каталога задач больше нет: пока плагинов было три и ставились они
# порознь, проект мог иметь учёт работ без канона документов, и общее число
# было бы домом, которого у половины проектов нет. Плагин один — довод ушёл, а
# два числа вместо одного оставляли бы вопрос «по какому журналу повышать».
#
# Число именно своё, потому что плагин ставитс я в одиночку: проект, взявший учёт
# работ без канона документов, каталога `docs/` не имеет вовсе, а значит не имеет
# и версии канона — сверять было бы не с чем. Копия чужого числа в этом скрипте
# была бы вторым домом для одной версии и разъехалась бы молча при обновлении
# одного плагина без другого.
#
# Переезды каталога задач, случившиеся до появления этого числа (в корень —
# канон 11, отмена спринтов — канон 12), задним числом сюда не переписаны: они
# уже названы журналом канона, и второй перечень тех же шагов разошёлся бы с
# первым. Версия 1 — формат на день её появления, что бы проекту ни пришлось
# пройти до неё.
FORMAT_VERSION = 1
VERSION_KEY = " tasks "
# Переезды каталога, случившиеся до слияни я ( в корень, отмена спринтов), задним
# числом в журнал не переписаны: они названы прежними журналами, и второй
# перечень тех же шагов разошёлся бы с первым.
FORMAT_VERSION = conf . VERSION
VERSION_KEY = conf . VERSION_KEY
EXIT_OK = 0
EXIT_DRIFT = 1
@@ -135,6 +150,11 @@ EXIT_USAGE = 2
EXIT_ENV = 3
EXIT_INTERNAL = 4
# Ключ `dir` в DEFAULTS не входит намеренно: он говорит, **где** каталог, а не
# как названы его части, и в `Layout` (тот про имена внутри) ему делать нечего.
DIR_KEY = " dir "
DEFAULT_DIR = " tasks "
DEFAULTS = {
" items " : " items " ,
" backlog " : " BACKLOG.md " ,
@@ -435,9 +455,14 @@ class Layout:
""" Каталог задач и имена его частей. Всё настраивается: у соседнего проекта
может быть другой подкаталог и другие имена индексов, а семантика та же. """
def __init__ ( self , root : Path , cfg : dict ) :
def __init__ ( self , root : Path , cfg : dict , project : Path | None = None ,
full : dict | None = None ) :
self . root = root
self . cfg = { * * DEFAULTS , * * cfg }
# Корень проекта — там, где лежит `.av-dev.toml`. Он нужен отдельно от
# каталога задач: версия объявлена в корне, а имена частей — внутри.
self . project = project or root
self . full = full or { }
self . cfg = { * * DEFAULTS , * * { k : v for k , v in cfg . items ( ) if k != DIR_KEY } }
self . items = root / self . cfg [ " items " ]
def index ( self , kind : str ) - > Path :
@@ -451,64 +476,28 @@ class Layout:
return ( " backlog " , " roadmap " )
def load_config ( root : Path ) - > dict :
""" Настройки каталога задач и версия его формата .
def load_config ( project : Path ) - > dict :
""" Весь `.av-dev.toml` проекта. Секция задач берётся из него отдельно .
Дом — `<каталог задач>/.tasks.json`: **свой файл у своего плагина**. Ключ
`tasks` в `docs/.pm.json` читается, пока живы проекты, заведённые до раскола
плагинов, и только когда своего файла нет; когда есть оба, побеждает свой, и
об этом говорится вслух — молча выбранный из двух конфиг это дрейф, который
потом никто не объяснит.
Порядок именно такой, а не наоборот, потому что `docs/` принадлежит другому
плагину. Проект, поставивший учёт задач без канона документов, каталога
`docs/` не имеет вовсе, и дом настроек, лежащий в чужом дереве, был бы домом,
которого у половины проектов нет.
Версия формата (ключ `tasks`) читается **только из своего файла**: прежний
дом её не знал и знать не может, и молча выведенная из его отсутствия версия
была бы догадкой о том, что чинится одной строкой.
Дом настроек — **корень репозитория**, а не каталог задач: файл держит
версию раскладки, которая одна на плагин, и ключ `[tasks] dir`, который
говорит, где каталог лежит. Настройка внутри настраиваемого каталога не
смогла бы сказать, где он.
"""
path = root / CONFIG_NAME
pm = ( root / PM_CONFIG_REL ) . resolve ( )
if path . is_file ( ) :
# Чужой конфиг здесь только повод для замечания, поэтому его поломка не
# наша: битый `docs/.pm.json` не должен ронять задачи, у которых свой
# файл на месте и читается.
try :
stale = pm . is_file ( ) and isinstance ( _read_json ( pm ) . get ( " tasks " ) , dict )
except Env :
stale = False
if stale :
print ( f " ЗАМЕЧАНИЕ настройки взяты из { path } ; ключ «tasks» в { pm } "
f " остался от прежней раскладки и не читается — убери его " ,
file = sys . stderr )
return _validate_config ( _read_json ( path ) , path )
if pm . is_file ( ) :
data = _read_json ( pm )
section = data . get ( " tasks " , { } )
if not isinstance ( section , dict ) :
raise Env ( f " { pm } : ключ «tasks» — ожидался объект с настройками " )
if section :
print ( f " ЗАМЕЧАНИЕ настройки взяты из ключа «tasks» в { pm } — это "
f " прежний дом. Перенеси их в { path } : каталог docs/ ведёт "
f " другой плагин, и его может не быть " , file = sys . stderr )
return _validate_config ( section , pm )
return { }
def _read_json ( path : Path ) - > dict :
try :
data = json . loads ( path . read_text ( encoding = " utf-8 " ) )
except json . JSONDecodeError as e :
raise Env ( f " { path } : не разбирается как JSON — { e } " ) from e
if not isinstance ( data , dict ) :
raise Env ( f " { path } : ожидался объект с настройками " )
data = conf . read ( project )
except conf . ConfigError as e :
raise Env ( str ( e ) ) from e
_validate_config ( conf . section ( data , " tasks " ) , project / CONFIG_NAME )
return data
def tasks_section ( full : dict ) - > dict :
return conf . section ( full , " tasks " )
def _validate_config ( data : dict , path : Path ) - > dict :
unknown = set ( data ) - set ( DEFAULTS ) - { VERSION_KEY }
unknown = set ( data ) - set ( DEFAULTS ) - { DIR_KEY }
# Ключ «plan» был домом оглавления целей до того, как файл стал ROADMAP.md.
# Без этой ветки проект со старым конфигом получал бы «неизвестный ключ» и
# искал опечатку там, где на самом деле переименование канона.
@@ -518,19 +507,12 @@ def _validate_config(data: dict, path: Path) -> dict:
f " av-dev:doc-canon (upgrade), а не правь ключ в одиночку: "
f " файл и ссылки на него переезжают вместе с ним " )
if unknown :
known = sorted ( { * DEFAULTS , VERSION_KEY } )
raise Env ( f " { path } : неизвестные ключи: { ' , ' . join ( sorted ( unknown ) ) } "
f " (известны: { ' , ' . join ( known ) } ) " )
# Версия — единственный ключ-число: остальные это имена файлов и заголовков.
# Б итое число тут останавливает работу целиком (код 3), а не идёт дрейфом,
# потому что «на какой версии формата каталог» решает, чему верить дальше.
got = data . get ( VERSION_KEY )
if VERSION_KEY in data and ( isinstance ( got , bool ) or not isinstance ( got , int ) ) :
raise Env ( f " { path } : ключ « { VERSION_KEY } » — версия формата задач, "
f " ожидалось целое число, а не { got !r} " )
known = sorted ( { * DEFAULTS , DIR_KEY } )
raise Env ( f " { path } : неизвестные ключи в секции [tasks]: "
f " { ' , ' . join ( sorted ( unknown ) ) } (известны: { ' , ' . join ( known ) } ) " )
# Версия раскладки лежит ключом верхнего уровня и проверяется общим
# ч итателем: здесь судится только секция задач, и все её ключи — строки.
for key , value in data . items ( ) :
if key == VERSION_KEY :
continue
if not isinstance ( value , str ) or not value . strip ( ) :
raise Env ( f " { path } : ключ « { key } » — ожидалась непустая строка " )
if key in PATH_KEYS and ( value . startswith ( " / " ) or " .. " in Path ( value ) . parts ) :
@@ -538,17 +520,15 @@ def _validate_config(data: dict, path: Path) -> dict:
return data
def config_home ( root : Path ) - > Path | None :
def config_home ( lay : Layout ) - > Path | None :
""" Откуда настройки читаются на самом деле — и куда, значит, слать чинить.
Порядок тот же, что в `load_config`: свой `.tasks.json` побеждает. Без этой
функции сообщения об ошибке звали бы править файл, который не читается.
Дом один — `.av-dev.toml` в корне проекта; None значит «файла нет, работаем
на умолчаниях». Без этой функции сообщения об ошибке звали бы править файл,
которого нет.
"""
path = root / CONFIG_NAME
if path . is_file ( ) :
return path
pm = ( root / PM_CONFIG_REL ) . resolve ( )
return pm if pm . is_file ( ) else None
path = lay . project / CONFIG_NAME
return path if path . is_file ( ) else None
def config_problems ( lay : Layout ) - > list [ str ] :
@@ -558,7 +538,7 @@ def config_problems(lay: Layout) -> list[str]:
check обвинять невиновных: «ссылка на несуществующий файл», хотя файл на
месте, а мимо смотрит конфиг.
"""
where = str ( config_home ( lay . root ) or " умолчания (конфига нет) " )
where = str ( config_home ( lay ) or " умолчания (конфига нет) " )
out = [ ]
if not lay . items . is_dir ( ) :
out . append ( f " { where } : items = « { lay . cfg [ ' items ' ] } » → { lay . items } — каталога нет " )
@@ -582,36 +562,43 @@ def version_problems(lay: Layout) -> list[str]:
бы объявить каталог приведённым к формату, шагов которого никто не делал.
Заводит число `init`, двигает — операция `upgrade` скилла.
"""
path = lay . root / CONFIG_NAME
# Прежний дом (`docs/.pm.json`) версии не знает, поэтому спрашиваем строго
# свой файл: «конфиг нашёлся» и «версия объявлена» это разные события.
path = lay . project / CONFIG_NAME
if not path . is_file ( ) :
return [ f " нет { path } — версия формата задач не объявлена. "
f " Заведи файл с « { VERSION_KEY } »: { FORMAT_VERSION } (журнал версий — "
f " references/changelog.md скилла av-dev:task-track) " ]
# Что число целое, уже проверил `_validate_config` — иначе сюда не дошли бы
legacy = conf . legacy_files ( lay . project , lay . root )
if legacy :
return [ f " нет { path } , а прежняя раскладка на месте "
f " ( { ' , ' . join ( legacy ) } ): перенеси настройки и удали старые "
f " файлы операцией upgrade скилла av-dev:doc-canon " ]
return [ f " нет { path } — версия раскладки не объявлена. "
f " Заведи файл с « { VERSION_KEY } = { FORMAT_VERSION } » (журнал "
f " версий — references/changelog.md скилла av-dev:doc-canon) " ]
# Что число целое, уже проверил общий читатель — иначе сюда не дошли бы
# вовсе (код 3). Здесь `isinstance` значит ровно «ключ есть».
got = lay . cfg . get ( VERSION_KEY )
if not isinstance ( got , int ) :
return [ f " { path } : нет ключа « { VERSION_KEY } » — версия формата не объявлена, "
f " текущая { FORMAT_VERSION } " ]
got = conf . version ( lay . full )
if got is None :
return [ f " { path } : нет ключа « { VERSION_KEY } » — версия раскладки не "
f " объявлена, текущая { FORMAT_VERSION } " ]
if got < FORMAT_VERSION :
return [ f " каталог приведён к формату версии { got } , текущая — "
return [ f " проект приведён к раскладке версии { got } , текущая — "
f " { FORMAT_VERSION } : нужно повышение по журналу "
f " (скилл av-dev:task-track , операция upgrade) " ]
f " (скилл av-dev:doc-canon , операция upgrade) " ]
if got > FORMAT_VERSION :
return [ f " каталог приведён к формату версии { got } , а скрипт знает "
return [ f " проект приведён к раскладке версии { got } , а скрипт знает "
f " { FORMAT_VERSION } : устарел плагин, обнови маркетплейс " ]
return [ ]
def looks_like_tasks ( p : Path ) - > bool :
if ( p / CONFIG_NAME ) . is_file ( ) :
return True
try : # индекс мог быть переименован через конфиг
name = load_config ( p ) . get ( " backlog " , DEFAULTS [ " backlog " ] )
except Env :
name = DEFAULTS [ " backlog " ]
def looks_like_tasks ( p : Path , names : dict | None = None ) - > bool :
""" Каталог задач узнаётся индексом, а не служебным файлом.
Служебный файл теперь лежит в корне проекта и о каталоге говорит ключом
`[tasks] dir`; узнавать каталог по нему значило бы объявить его задачами
ровно там, куда указывает ключ, — даже если по этому пути пусто.
Имя индекса берётся из настроек: проект вправе назвать его по-своему, и
поиск по умолчанию не нашёл бы переименованного каталога вовсе.
"""
name = ( names or { } ) . get ( " backlog " ) or DEFAULTS [ " backlog " ]
return ( p / name ) . is_file ( )
@@ -619,33 +606,51 @@ def resolve_layout(explicit: str | None) -> Layout:
""" Каталог задач для команд, кроме init.
Цепочка разрешения: явный `--dir` (обязан быть внутри рабочего каталога) →
`.tasks.json` или умолчания вверх от текущего каталога. Указатель в
`CLAUDE.md` проекта — звено между ними, но читает его агент и передаёт
сюда `--dir`: скрипт не разбирает чужую документацию.
ключ `[tasks] dir` из `.av-dev.toml` в корне → умолчание `tasks/` вверх от
текущего каталога. Указатель в `CLAUDE.md` проекта — звено между первым и
вторым, но читает его агент и передаёт сюда `--dir`: скрипт не разбирает
чужую документацию.
"""
here = Path . cwd ( ) . resolve ( )
project = conf . find_root ( here )
full = load_config ( project ) if project else { }
names = tasks_section ( full )
if explicit :
root = Path ( explicit )
if not dir_within_cwd ( root ) :
raise Env ( f " --dir вне рабочего каталога: { explicit } " )
if not looks_like_tasks ( root ) :
if not looks_like_tasks ( root , names ) :
raise Env ( f " задач нет в « { explicit } »; "
f " новый проект — tasks.py init --dir { explicit } " )
return Layout ( root , load_config ( root ) )
here = Path . cwd ( ) . resolve ( )
return Layout ( root , names , project or root . resolve ( ) , full )
if project :
candidate = project / names . get ( DIR_KEY , DEFAULT_DIR )
if looks_like_tasks ( candidate , names ) :
return Layout ( relative_if_inside ( candidate , here ) , names , project , full )
# Проект без `.av-dev.toml` — учёт работ ведут и до того, как канон заведён.
# Тогда каталог ищется умолчанием вверх, а версия объявится на `adopt`.
for base in ( here , * here . parents ) :
for candidate in ( base , base / " tasks " , base / " docs/tasks " , base / " doc/tasks " ) :
if looks_like_tasks ( candidate ) :
try :
rel = candidate . relative_to ( here )
except ValueError :
rel = candidate
return Layout ( rel if str ( rel ) != " . " else candidate , load_config ( candidate ) )
for candidate in ( base , base / DEFAULT_DIR , base / " docs/tasks " , base / " doc/tasks " ) :
if looks_like_tasks ( candidate , names ) :
return Layout ( relative_if_inside ( candidate , here ) , names ,
project or base , full )
if ( base / " .git " ) . exists ( ) :
break # выше корня репозитория не ищем
raise Env ( " каталог задач не найден: ни --dir, ни tasks/ вверх от "
f " { here } . Путь всегда tasks/ в корне репозитория; прежний "
" docs/tasks переезжает по записи 11 журнала версий канона, "
" новый проект — tasks.py init --dir tasks " )
raise Env ( f " каталог задач не найден: ни --dir, ни ключ [tasks] { DIR_KEY } в "
f " { CONFIG_NAME } , ни { DEFAULT_DIR } / вверх от { here } . "
f " Новый проект — tasks.py init --dir { DEFAULT_DIR } " )
def relative_if_inside ( path : Path , here : Path ) - > Path :
""" Путь покороче для сообщений, если каталог лежит под текущим. """
try :
rel = path . relative_to ( here )
except ValueError :
return path
return path if str ( rel ) == " . " else rel
# --- Чтение индексов ---
@@ -1079,7 +1084,7 @@ def check(lay: Layout, fix: bool = False) -> int:
for p in problems :
print ( f " КОНФИГ { p } " )
print ( " \n сперва конфиг: пока он мимо, всё остальное диагностируется ложно "
f " (правь { config_home ( lay . root ) or lay . root / CONFIG_NAME } "
f " (правь { config_home ( lay ) or lay . project / CONFIG_NAME } "
f " или переименуй файлы) " )
return EXIT_ENV
@@ -2596,15 +2601,14 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]:
def init_files ( lay : Layout , sections : list [ str ] , roadmap_sections : list [ str ] ,
cfg : dict ) - > dict [ Path , str ] :
out : dict [ Path , str ] = { }
# Ф айл заводится всегда, даже когда все имена умолчательные: в нём живёт
# версия формата , а версия — не настройка, от которой можно отказаться.
#
# Пишем всегда в свой `.tasks.json`, даже когда рядом живёт `docs/.pm.json`:
# дом настроек принадлежит этому плагину, а `docs/` — другому, и его в
# проекте может не быть. load_config читает свой файл первым, так что
# записанное сюда и прочитается отсюда.
out [ lay . root / CONFIG_NAME ] = json . dumps ( cfg , ensure_ascii = False ,
indent = 2 ) + " \n "
# Служебный ф айл заводится всегда, даже когда все имена умолчательные: в нём
# живёт версия раскладки , а версия — не настройка, от которой можно
# отказаться. Файл уже есть (проект под каноном, заводят только задачи) —
# он не перезаписывается: комментарии в нём принадлежат человеку. Тогда
# недостающие ключи секции дописываются построчно, и делает это `cmd_init`
# после записи файлов, потому что правка идёт по живому файлу, а не планом.
if not ( lay . project / CONFIG_NAME ) . is_file ( ) :
out [ lay . project / CONFIG_NAME ] = conf . skeleton ( FORMAT_VERSION , tasks = cfg )
out [ lay . index ( " backlog " ) ] = (
" # Беклог \n \n "
f " Что **можно взять**. Одна задача = один файл ` { lay . cfg [ ' items ' ] } /<slug>.md` \n "
@@ -2670,8 +2674,18 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int:
# Имена частей — следом и только те, что названы явно: умолчание, записанное
# в файл, стало бы вторым домом для того же имени. В раскладку версия не
# идёт — `Layout` про имена, и число среди имён там ничего не значит.
cfg = { VERSION_KEY : FORMAT_VERSION , * * names }
lay = Layout ( root , names )
# Каталог задач называется ключом `dir`, если он не умолчательный: без него
# `.av-dev.toml` не сможет сказать, где искать, и разрешение уедет на
# умолчание — молча и в другой каталог.
project = conf . find_root ( ) or Path . cwd ( ) . resolve ( )
cfg = dict ( names )
try :
rel = root . resolve ( ) . relative_to ( project ) . as_posix ( )
except ValueError :
raise Usage ( f " каталог задач { root } вне проекта { project } " ) from None
if rel != DEFAULT_DIR :
cfg [ DIR_KEY ] = rel
lay = Layout ( root , names , project , load_config ( project ) )
if lay . index ( " backlog " ) . exists ( ) :
raise Usage ( f " { lay . index ( ' backlog ' ) } уже есть — каталог задач заведён " )
@@ -2692,12 +2706,13 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int:
for path , text in init_files ( lay , sections , roadmap_sections , cfg ) . items ( ) :
plan . file ( path , text )
plan . commit ( )
added = conf . merge_section ( project , " tasks " , cfg ) if cfg else [ ]
print ( f " каталог задач заведён: { root } " )
print ( f " секции беклога: { ' , ' . join ( sections ) } ; "
f " секции роадмапа канонические: { ' , ' . join ( roadmap_sections ) } " )
what = ( " версия формата и имена частей записаны " if names
else " версия формата записана " )
print ( f " { what } в { root / CONFIG_NAME } : формат { FORMAT_VERSION } " )
if added :
print ( f " дописано в [tasks] { project / CONFIG_NAME } : { ' , ' . join ( added ) } " )
print ( f " версия раскладки в { project / CONFIG_NAME } : { FORMAT_VERSION } " )
return EXIT_OK
@@ -2999,7 +3014,8 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int:
root = Path ( pl [ " target " ] )
if not dir_within_cwd ( root ) :
raise Usage ( f " target вне рабочего каталога: { root } " )
lay = Layout ( root , { } )
project = conf . find_root ( ) or Path . cwd ( ) . resolve ( )
lay = Layout ( root , { } , project , load_config ( project ) )
sections = pl . get ( " sections_backlog " ) or uniq_sections ( DEFAULT_SECTIONS )
roadmap_sections = pl . get ( " sections_roadmap " ) or uniq_sections ( DEFAULT_ROADMAP_SECTIONS )
known_sections = { s . lower ( ) for s in sections }
@@ -3058,8 +3074,7 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int:
# Версия та же, что у `init`: каталог выводится из чужой раскладки сегодня и
# сегодняшним форматом, сколько бы лет ни было тому, из чего он выведен.
# Имён частей здесь нет — адаптация раскладывает всё по умолчаниям.
for path , text in init_files ( lay , sections , roadmap_sections ,
{ VERSION_KEY : FORMAT_VERSION } ) . items ( ) :
for path , text in init_files ( lay , sections , roadmap_sections , { } ) . items ( ) :
wr . file ( path , text )
backlog_lines = init_files ( lay , sections , roadmap_sections , { } ) [ lay . index ( " backlog " ) ] . splitlines ( )
roadmap_lines = init_files ( lay , sections , roadmap_sections , { } ) [ lay . index ( " roadmap " ) ] . splitlines ( )