Перейти к содержанию

ADR 0109: Единый декларативный каталог параметрических Intent Melody (TOML + кодовое связывание args)

Статус: Accepted · Implemented
Дата: 2026-05-11

Связанные ADR

ADR Роль
0081 параметрические хвосты для диапазонов строк редактора
0060 Command Melody, CascadeChord, паритет с c:
0108 веб-портал и параметрическая мелодия wai:
0030 command_id, слои реестра
0008 MCP / исполнение команд

Резюме

  • Каталог Intent Melody: [[melody_root]] + [[tail_wire_class]] в TOML.
  • Миграция с legacy [aliases] / [[parametric]]; binders в коде.
  • Implemented: загрузчик, рантайм-каталог, палитра/аккорд; плагины — отдельная модель.

Снимок реализации

Элемент Значение
TOML [[melody_root]], [[tail_wire_class]]
Docs intent-melody-language-v1; без legacy [aliases]

Вне ADR

Документ Роль
intent-melody-language-v1.md IML v1
IntentMelody/intent-melody-aliases.toml реестр alias → command_id
Про IML: по сути этот ADR задаёт архитектурный путь к IML «v2» не как к отдельному языку с нуля, а как к естественному надстрою v1: тот же wire для пользователя (wai:…, els:…), но единый декларативный каталог корней ([[melody_root]]), явное shape, presentation ([[tail_wire_class]]) и детерминированная сборка args в коде. Нормативную запись вида «IML v2» при желании можно оформить отдельным документом после стабилизации полей; до тех пор источник правды здесь и в обновлениях к intent-melody-language-v1.md.

1. Контекст

Сегодня параметрические формы мелодии разнесены:

  • Плоская таблица [aliases] в TOML задаёт только соответствие короткой мнемоники → command_id (IntentMelodyAliases).
  • Специальные случаи жёстко закодированы в ParametricIntentMelody (разбор wai:…, els:start:end, списки «только палитра», эвристики для аккорда, подсказки).
  • Палитра (IdeCommandPaletteFilterOrchestrator) и аккорд (CascadeChordIntentSession) должны дублировать семантику «что параметрическое, когда Enter, какие аргументы».

Это масштабируется хуже: каждый новый параметрический корень требует правок в нескольких местах и легко расходится по поведению между c: и Ctrl+K.


2. Проблема

  1. Нет единого описания формы: что является «базой» параметра, какой синтаксис хвоста, нужен ли Enter в аккорде, как маппить слоты в JSON args команд.
  2. Декларация alias и декларация параметричности разводятся — конфликт с принципом IML («одна строка — одно намерение» поверх общего канона команд).
  3. C#-атрибуты вида [Parameter(Position, Type, Description)] на методах выполнения не совпадают с точкой сборки аргументов: исполнитель команд принимает command_id + JSON словарь; сборка этого словаря зависит от контекста IDE (файл, текст редактора, нормализация колонок для диапазонов и т.д.), а не только от DTO параметров CLR.

Требование: одна декларативная правда, при этом сложную семантику оставить в маленьком, именованном коде, а не в разрастающихся switch по строкам alias.


3. Решение

3.1. Два слоя: каталог в TOML + связывание с command_id в коде

  1. Целевая декларация корней — один массив [[melody_root]] (§3.2.1): каждая строка — один slug, command_id, shape = "simple" | "parametric"; для parametric те же поля, что сегодня размазаны между [aliases] и [[parametric]] (tail_signature, wire_class, chord_commit, …). Один проход загрузчика, один DTO, без дубля slug/command_id и без рассинхрона таблиц — долгосрочно проще.

  2. Миграция TOML: допустимо сразу перевести intent-melody-aliases.toml на целевой [[melody_root]] (§3.2.1) одной ручной правкой (или скриптом) — отдельный «долгий» переходный формат не обязателен. Если удобнее поэтапно, файл может временно оставаться как плоский [aliases] + [[parametric]] (+ опционально [alias_type], [[tail_wire_class]], §3.2.2); загрузчик нормализует это в тот же внутренний снимок, что и из [[melody_root]]. Всё в одном файле (второй файл — по-прежнему исключение при выросшем объёме).

  3. tail_signature — одна строка в канонической метанотации слотов после первого : у корня, см. §3.3. По этой сигнатуре загрузчик выводит стратегию разбора (отдельного syntax в конфигурации нет). Межслотовую presentation (какие символы допустимы между слотами в wire) см. опциональный реестр [[tail_wire_class]] в §3.3 — тогда альтернативы типа : или пробел не забиты в отдельной «таблице только в коде», а живут рядом с каталогом, в узком виде «литерал или литерал».

  4. Нет поля binder в TOML. Каталог описывает только форму хвоста; перевод разобранных слотов в args — это часть кода исполнения command_id и/или тонкий слой ParametricMelodyArgsBuilder, который выбирается детерминированно по (command_id, tail_signature, контекст IDE), без «магических строк» в конфигурации.

Инвариант: command_id остаётся каноническим ключом выполнения (как MCP); TOML не становится Turing-complete — только маршрутизация формы строки → сборка args по детерминированным правилам в коде.

Связь [aliases] и [[parametric]]: alias_type (только этап миграции)

Пока в файле ещё плоский [aliases] и отдельный [[parametric]], а не единый [[melody_root]]: короткая мнемоника в [aliases] (slug → command_id), форма хвоста в [[parametric]] с тем же alias. После перехода на [[melody_root]] отдельная [alias_type] не нужна — роль играет поле shape. Ниже — правила для переходного формата.

Чтобы загрузчик и люди одинаково понимали, простая ли это цель без хвоста или ожидается параметрический разбор, можно ввести alias_type:

Значение Смысл
simple Одна строка без разбора слотов после корня достаточна для выполнения (как большинство c:-корней сегодня).
parametric Для этого slug обязательна строка [[parametric]] с согласованным command_id и полями формы (tail_signature, …).

Два режима.

  1. По умолчанию — вывод: если секция alias_type не заполнена (или ключ для slug отсутствует), тип считается parametric, когда существует [[parametric]] с тем же alias, иначе simple. Одного файла и одного загрузочного прохода достаточно, дубль не обязателен.
  2. Явный режим: опциональная таблица [alias_type] с теми же ключами, что ключи [aliases], и значениями simple / parametric. Использовать для жёсткой валидации (explicit parametric без [[parametric]] → ошибка в dev; simple при наличии [[parametric]] для того же slug → ошибка или запрет пересечения — политика загрузчика) и чтобы в диффе однозначно было видно класс строки без чтения всего массива [[parametric]].

Если держишь временную компоновку §3.2.2, плоский [aliases] там не обязан сразу исчезать — так сохраняется совместимость с уже принятым форматом файла; [alias_type] добавляется точечно, если нужна явная валидация. При ручной миграции сразу на [[melody_root]] (§3.2.1) ни [alias_type], ни [[parametric]] для тех же корней не нужны.

Инвариант согласованности: для каждого slug command_id в [aliases] и в соответствующей [[parametric]] (если есть) должен совпадать.

3.2. Схемы записи в TOML

Иллюстрация полей (контракт для загрузчика; имена ключей версии файла см. ниже про сведение к одному). Метанотация tail_signature§3.3. Версию схемы файла (melody_catalog_schema_version vs parametric_schema_version) при миграции на [[melody_root]] разумно свести к одному ключу, чтобы загрузчик читал одну версию.

3.2.1. Целевая единая модель (долгосрочно): [[melody_root]]

Один массив на весь каталог корней c:: slug, command_id, shape. Для shape = "parametric" задаются поля формы; для simple — только slug и command_id (и при необходимости редкие UX-поля, общие для обоих). Дубликата slug между таблицами нет.

Опционально show_usage_hint_if_bare_slug (bool): если true, в палитре c: при вводе только корня без аргументов показывается строка подсказки по форме хвоста, а не строка команды из каталога (els/eld и заглушки могут получать true по умолчанию в загрузчике). Если false (или поле опущено там, где умолчание ложно), голый slug ведёт себя как обычная команда палитры при наличии резолва.

melody_catalog_schema_version = 1

[[tail_wire_class]]
id = "url_remainder"
kind = "single_remainder"

[[tail_wire_class]]
id = "int_chain_colon_space"
kind = "delimited_slots"
between_slots_any_of = [":", " "]

[[melody_root]]
slug = "br"
command_id = "build_solution_ui"
shape = "simple"

[[melody_root]]
slug = "wai"
command_id = "show_web_ai_portal_page"
shape = "parametric"
tail_signature = "<url:url>"
wire_class = "url_remainder"
chord_commit = "enter"
palette_hint_slug = "wai-url"
show_usage_hint_if_bare_slug = false

[[melody_root]]
slug = "els"
command_id = "select"
shape = "parametric"
tail_signature = "<start:ln>:<end:ln>"
wire_class = "int_chain_colon_space"
chord_commit = "enter"
show_usage_hint_if_bare_slug = true

3.2.2. Опционально: плоский [aliases] + [[parametric]] (пошаговое внедрение)

Если целевой [[melody_root]] вводится не одним коммитом (например, загрузчик сначала учится только старому виду файла), допустима временно прежняя компоновка (плюс опционально [alias_type]§3.1). Иначе этот блок можно не использовать — сразу целевая схема из §3.2.1.

parametric_schema_version = 1

[aliases]
wai = "show_web_ai_portal_page"
els = "select"
eld = "apply_edit"

[alias_type]
wai = "parametric"
els = "parametric"
eld = "parametric"

[[tail_wire_class]]
id = "url_remainder"
kind = "single_remainder"

[[tail_wire_class]]
id = "int_chain_colon_space"
kind = "delimited_slots"
between_slots_any_of = [":", " "]

[[parametric]]
alias = "wai"
command_id = "show_web_ai_portal_page"
tail_signature = "<url:url>"
wire_class = "url_remainder"
chord_commit = "enter"
palette_hint_slug = "wai-url"

[[parametric]]
alias = "els"
command_id = "select"
tail_signature = "<start:ln>:<end:ln>"
wire_class = "int_chain_colon_space"
chord_commit = "enter"

[[parametric]]
alias = "eld"
command_id = "apply_edit"
tail_signature = "<start:ln>:<end:ln>"
wire_class = "int_chain_colon_space"
chord_commit = "enter"

Поля chord_commit нормализуют правило «мгновенно по одномуочевидному корню vs только Enter» без отдельного HashSet в C#.

3.3. Каноническая метанотация: <имя:тип> и аккуратный TOML

Чтобы в каталоге однозначно задавать параметры без «каши» из вложенных таблиц, фиксируем строчную сигнатуру хвоста после alias:

  • Каждый слот записывается как <идентификатор:тип>.
  • Несколько слотов подряд разделяются одним литеральным : между закрывающей > и следующей <.

Иллюстративные полные формы (канон документа / «как это читать человеку»; префикс c: в палитре снаружи):

Пример канона (хвост после c:) Смысл
wai:<url:url> Один параметр url типа url.
els:<start:ln>:<end:ln> start и end — номера строк (1-based inclusive); в метанотации слот ln (или linenumber), см. 0081. Слот :int для той же цепочки пока поддерживается как эквивалент при разборе.

Значение поля tail_signature в TOML задаёт только часть после корня (без повторного slug в строке): например "<url:url>" или "<start:ln>:<end:ln>" (краткий тип слота ln = номер строки) — см. §3.2 ([[melody_root]] или [[parametric]]).

Типы (url, int, позже path, string, …) — закрытый реестр в коде: при загрузке каталога неизвестный тип → ошибка конфигурации (dev) или игнор строки overlay с логом (продакт — отдельно).

Wire-совместимость. Пользовательский ввод остаётся простым: wai:https://… и els:5:15 (без < >). tail_signature нужна только для описания (читаемость, подсказки, валидаторы, маппинг args) и не вводится пользователем.

Wire между слотами: не один сепаратор на весь язык и не свой на каждую мелодию

Один глобальный межслотовый разделитель для всех корней конфликует с естественным wire: после wai: весь хвост — один URL со своими :, его нельзя «резать» тем же символом, что цепочку els. Назначать отдельный separator в каждой записи [[parametric]] технически гибко, но для пользователя и доков это зоопарк («тут :, там пробел»).

Приемлемый компромисс — три уровня (поле separator на каждую [[parametric]] не базовое; правила presentation для класса формы — в бандле рядом с каталогом и/или минимальный fallback в коде, см. §3.3):

  1. Один «свободный» слот (остаток строки) — например <url:url> без второго слота: после канонического префикса alias: (первое литеральное : в wire отделяет корень от значения; дальнейшие : внутри значения не являются межслотовыми. Пробелы, скобки, кириллица в том же значении допустимы; валидность URL — после разборщика.)
  2. Простая цепочка слотов из закрытого набора типов (например int и ln / linenumber для номеров строк подряд): межслотовый разделитель задаётся классом формы (kind = "delimited_slots" + альтернатива литералов в TOML или вывод по шаблону tail_signature), а не отдельной строкой «у этой мелодии :, у той пробел». По мере нужды для обратной совместимости с текущим v1 можно допускать несколько эквивалентных написаний одной и той же семантики (например els:5:15 и els 5 15), нормализуя перед разбором; в подсказках и документе всё же держать один канонический вид.
  3. Экзотика — составной payload, когда без явного профиля нельзя отличить границу значений от :, нужны несколько именованных сегментов с разными делимитерами и т.п.: только тогда wire_profile (или эксплицитное поле переопределения) и при появлении реального сценария — короткий follow-up ADR; до этого держаться уровней 1–2.

Двоеточия внутри значения для одного слота типа url покрываются уровнем 1. Если понадобятся несколько URL в одном wire без «остатка целиком», это уже уровень 3 — явный профиль или отдельный ADR.

Реестр [[tail_wire_class]]: presentation grammar между слотами

Чтобы не держать «таблицу символов только в коде», в том же файле каталога (или бандловом блоке перед [[parametric]]) можно описать классы wire узким языком уровня presentation: по сути литерал или литерал между слотами, без полноценного EBNF.

  • id — имя класса; на него может ссылаться wire_class в [[parametric]], если не хочешь опираться только на автоматический вывод из tail_signature.
  • kind: например single_remainder (один слот — весь хвост после первого разделителя корня alias:) или delimited_slots (цепочка слотов; между соседними значениями разрешены только перечисленные разделители).
  • between_slots_any_of — массив строк (часто один символ каждая): в TOML это ровнее, чем писать смесь через | в одной строке поля; семантически это альтернатива литералов на presentation-слое (минимальный «symbol or symbol» без полного EBNF): при неоднозначности выбор задаёт политика загрузчика (см. примечание ниже).
  • kind и набор допустимых полей завязаны на закрытый реестр реализации: код остаётся исполнителем этого мини-описателя (немного шаблонов kind), а не местом для дубля всех строк [[parametric]].

Декларативность не пропадает: инвентарь мелодий и их семантическая форма по-прежнему в [[parametric]]; класс межслотовых символов — отдельный маленький слой данных, переопределяемый оверлеем так же, как сам каталог. Сборка args после разбора — по-прежнему в коде по (command_id, tail_signature, контекст IDE).

Примечание. Порядок перебора альтернатив и нормализация (: и т.д.) фиксируются загрузчиком и тестами; для MVP достаточно однозначной пары целых после нормализации.

Метанотация компактна в TOML одной строкой tail_signature, задаёт имена параметров для подсказок и биндинга к MCP args; содержательная сборка после разбора — в коде, детерминированно по (command_id, tail_signature, контекст IDE), см. §3.1.

3.4. Отклонённые альтернативы

Альтернатива Почему не как единственный слой
Только атрибуты C# Теряется оверлей без пересборки; параметрическая форма — про строковый язык и палитру, не про сигнатуру CLR; контекст IDE не выразить декларативно целиком.
Только код (switch по alias) Уже наблюдаемая связность между палитрой, аккордом и MCP; ошибки синхронизации.
Один огромный TOML без кода Вычисление колонок по строкам редактора, границ файлов, сложная нормализация URL — должны жить в проверенном коде.

Приемлемый компромисс: TOML = форма (tail_signature); ядро IDE = разбор + детерминированная сборка args для встроенных command_id; сложные формы и плагины — см. §3.5 и §3.6.

3.5. Где TOML достаточно, а где XML (или только код)

Различаем каталог параметрических записей и собственно грамматику / дерево правил разбора.

  1. tail_signature и прочие скаляры (chord_commit, флаги, подсказки) — в TOML в одной строке каталога ([[melody_root]] целевой или [[parametric]] на миграции), без разрастания таблиц: имена слотов и типы вшиты в tail_signature (§3.3).

  2. Полная грамматика (альтернативы, приоритет веток, вложенные конструкции, «сначала этот префикс, иначе тот») в чистом TOML быстро превращается в плохо читаемую вложенность или в повтор костыльных списков.

  3. Когда грамматика реально ветвистая и её хотят править без перекомпиляции или валидировать схемой, уместен отдельный формат с иерархией — чаще всего XML (или JSON/YAML + JSON Schema): удобно выразить sequence/choice, при необходимости приложить XSD/RNG. Запись в TOML тогда ссылается на ресурс, например grammar_ref = "bundled:melody/strategies/foo.xml" или путь относительно оверлея — ровно один уровень косвенности, без дублирования каталога.

  4. Пока семейство стратегий мало, достаточно разбора tail_signature в коде + детерминированной сборки args; вынос разбора в XML не обязателен — только когда появится много парсеров или потребность в внешнем редакторе правил.

Итого: TOML держит каталог и компактную сигнатуру слотов (tail_signature); тело сложной стратегии — по мере роста либо код, либо внешний декларативный файл (XML как разумный кандидат для глубоко вложенных правил); смешивать тяжёлую грамматику и TOML в одном файле целым полотном — не цель этого ADR.

3.6. Расширяемость: плагины и кто несёт код

Каталог без binder в TOML рассчитан на ядро: фиксированный набор типов слотов и таблица соответствий «(command_id, tail_signature) → сборка args».

Когда появятся плагины, параметрическое расширение — это расширение границы плагина, а не «ещё одна магическая строка в TOML»:

  1. Плагин, регистрирующий новый command_id и/или новую мелодию, обязан предоставить обработку того, что добавляет: минимум разбор / валидацию хвоста в wire-форме и построение аргументов для своей команды (или явный отказ с понятной ошибкой).
  2. TOML может по-прежнему объявлять slug / alias, command_id, tail_signature — как контракт намерения (в целевой форме это поля [[melody_root]]), но исполнение связки «строка → args» для плагина остаётся в коде плагина, а не в конфиге ядра без хоста расширений.

Так нет «чел в TOML гадает константу»: у ядра таблица однозначна, у плагина — договор через API расширений (детали модульности — отдельный ADR, когда плагины станут реальностью).


4. Последствия

Положительные

  • Одна точка добавления корня на долгосрочную перспективу: строка [[melody_root]] (shape = "parametric") + при необходимости новый обработчик сборки args в коде; на этапе миграции эквивалент — [[parametric]]. Presentation между слотов — в [[tail_wire_class]], без separator на каждую мелодию.
  • Единые правила для палитры c:, CascadeChord (Ctrl+K) и любых других транспортов той же строки.
  • Проще документировать IML: форма параметра рядом с alias в данных, а не размазана по ParametricIntentMelody.cs.

Отрицательные / риски

  • Нужна миграция существующего ParametricIntentMelody (PaletteOnlyAliases, хардкод wai, подсказки) → загрузка каталога + вывод стратегии из tail_signature и сборка args для ядра.
  • Валидация при старте: дубликат slug в [[melody_root]]; неизвестный тип в tail_signature или неподдерживаемая комбинация (command_id, tail_signature) — явная ошибка в dev-сборках; в пользовательском overlay — понятное логируемое отклонение строки. То же для неизвестного wire_class, конфликта kind с фактической tail_signature и «пустых» альтернатив в between_slots_any_of (если реестр [[tail_wire_class]] используется). При использовании [alias_type] — конфликт с фактическим наличием [[parametric]] или рассогласование command_id между [aliases] и [[parametric]].
  • Опциональное усложнение: загрузчик внешней грамматики по grammar_ref (§3.5); плагины как граница расширения — §3.6.

5. Шаги внедрения (ориентир)

  1. Завести DTO загрузки: внутренний снимок в виде [[melody_root]]-подобного списка (даже если файл пока в формате [aliases] + [[parametric]] из §3.2.2); при необходимости [[tail_wire_class]]. Расширение intent-melody-aliases.toml (второй файл + merge только при реальной нужде см. §3.1).
  2. Реализовать ParametricMelodyCatalog / общий каталог корней мелодии (read-only после старта), не дублируя slug между слоями после нормализации.
  3. Перевести intent-melody-aliases.toml на [[melody_root]] (§3.2.1) — сразу вручную или после короткого этапа с §3.2.2; убрать дубли [aliases] / [[parametric]] для тех же slug.
  4. Постепенно заменить ветви в IdeCommandPaletteFilterOrchestrator и CascadeChordIntentSession на обращение к каталогу + детерминированную сборку args по (command_id, tail_signature).
  5. Удалить дубли: PaletteOnlyAliases как главный источник правды — перенести в TOML; в коде — вывод стратегии из tail_signature и детерминированная сборка args для встроенных команд.
  6. Обновить intent-melody-language-v1.md: ссылка на этот ADR и на поля каталога ([[melody_root]] и миграционный вид).
  7. По необходимости (§3.5): поле grammar_ref на строку каталога ([[melody_root]] или [[parametric]]) или на сторонний реестр стратегий + парсер внешнего формата (XML/JSON + схема). Отдельно: когда появится модель плагинов — см. §3.6.

Статус реализации (Implemented — шаги 1–6 §5; §3.5–3.6 вне объёма)

Соответствие шагам внедрения (раздел §5 ниже):

Шаг Содержание ADR Факт в коде/данных
1 DTO загрузки, нормализация миграционного TOML IntentMelodyAliasesIntentMelodyBundleState / IntentMelodyCatalogSnapshot + merge оверлея
2 Read-only каталог корней IntentMelodyCatalog + снимок из бандла
3 Целевой [[melody_root]] в бандле IntentMelody/intent-melody-aliases.toml: melody_catalog_schema_version, только [[melody_root]] и [[tail_wire_class]] (без [aliases] / [[parametric]])
4 Палитра и аккорд через каталог IdeCommandPaletteFilterOrchestrator, CascadeChordIntentSession, MelodyPaletteLineCommandPaletteExtensions + ParametricIntentMelody опираются на IntentMelodyCatalog
5 Убрать PaletteOnlyAliases как источник правды Список «только подсказка в палитре» задан в TOML (show_usage_hint_if_bare_slug); в коде остаётся лишь хелпер IsPaletteOnlyAlias (читает каталог), не статический перечень slug
6 Обновить intent-melody-language-v1.md Ссылки на ADR 0109 и поля [[melody_root]] / [[tail_wire_class]] — в intent-melody-language-v1.md

Шаг 7 (поле grammar_ref, внешние стратегии разбора) и §3.6 (плагины с собственным кодом binders) в ADR изначально опциональны / следующая эпоха — не часть «недоделанного ядра», а расширения после роста сложности.

Детали поведения и тестов:

  • Загрузка и валидация: IntentMelodyAliases.Build разбирает [[tail_wire_class]] и [[melody_root]], проверяет wire_class, chord_commit, согласованность kind с tail_signature (IntentMelodyTailSemantics).
  • Каталог в рантайме: read-only снимок IntentMelodyCatalogSnapshot + IntentMelodyCatalog; разбор параметрики использует классы проводов (SingleRemainder / DelimitedSlots) из ParametricIntentMelody.
  • chord_commit: ParametricIntentMelody.ChordDefersInstantExecuteFor* (enter и аналоги откладывают, immediate / instant — нет).
  • palette_hint_slug: ParametricIntentMelody.ResolvePaletteHintKey; в бандле (напр. waiwai-url).
  • Тесты: IntentMelodyCatalogWireTests, ParametricIntentMelodyTests, IntentMelodyAliasesTests.

6. Связка с MCP и реестром команд

Этот ADR не меняет контракт ide_execute_command: по-прежнему command_id + JSON args. Изменение — только способ производить args из строки мелодии и согласовать UX между поверхностями. Схема аргументов конкретной команды по-прежнему может дублироваться в документации команд / кодогенерации 0018 (если примут для IDE), ортогонально каталогу ([[melody_root]] / миграционный [[parametric]]).