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. Проблема¶
- Нет единого описания формы: что является «базой» параметра, какой синтаксис хвоста, нужен ли Enter в аккорде, как маппить слоты в JSON args команд.
- Декларация alias и декларация параметричности разводятся — конфликт с принципом IML («одна строка — одно намерение» поверх общего канона команд).
- C#-атрибуты вида
[Parameter(Position, Type, Description)]на методах выполнения не совпадают с точкой сборки аргументов: исполнитель команд принимаетcommand_id+ JSON словарь; сборка этого словаря зависит от контекста IDE (файл, текст редактора, нормализация колонок для диапазонов и т.д.), а не только от DTO параметров CLR.
Требование: одна декларативная правда, при этом сложную семантику оставить в маленьком, именованном коде, а не в разрастающихся switch по строкам alias.
3. Решение¶
3.1. Два слоя: каталог в TOML + связывание с command_id в коде¶
-
Целевая декларация корней — один массив
[[melody_root]](§3.2.1): каждая строка — один slug,command_id,shape = "simple" | "parametric"; дляparametricте же поля, что сегодня размазаны между[aliases]и[[parametric]](tail_signature,wire_class,chord_commit, …). Один проход загрузчика, один DTO, без дубля slug/command_idи без рассинхрона таблиц — долгосрочно проще. -
Миграция TOML: допустимо сразу перевести
intent-melody-aliases.tomlна целевой[[melody_root]](§3.2.1) одной ручной правкой (или скриптом) — отдельный «долгий» переходный формат не обязателен. Если удобнее поэтапно, файл может временно оставаться как плоский[aliases]+[[parametric]](+ опционально[alias_type],[[tail_wire_class]], §3.2.2); загрузчик нормализует это в тот же внутренний снимок, что и из[[melody_root]]. Всё в одном файле (второй файл — по-прежнему исключение при выросшем объёме). -
tail_signature— одна строка в канонической метанотации слотов после первого:у корня, см. §3.3. По этой сигнатуре загрузчик выводит стратегию разбора (отдельногоsyntaxв конфигурации нет). Межслотовую presentation (какие символы допустимы между слотами в wire) см. опциональный реестр[[tail_wire_class]]в §3.3 — тогда альтернативы типа:или пробел не забиты в отдельной «таблице только в коде», а живут рядом с каталогом, в узком виде «литерал или литерал». -
Нет поля
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, …). |
Два режима.
- По умолчанию — вывод: если секция
alias_typeне заполнена (или ключ для slug отсутствует), тип считаетсяparametric, когда существует[[parametric]]с тем жеalias, иначеsimple. Одного файла и одного загрузочного прохода достаточно, дубль не обязателен. - Явный режим: опциональная таблица
[alias_type]с теми же ключами, что ключи[aliases], и значениямиsimple/parametric. Использовать для жёсткой валидации (explicitparametricбез[[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):
- Один «свободный» слот (остаток строки) — например
<url:url>без второго слота: после канонического префиксаalias:(первое литеральное:в wire отделяет корень от значения; дальнейшие:внутри значения не являются межслотовыми. Пробелы, скобки, кириллица в том же значении допустимы; валидность URL — после разборщика.) - Простая цепочка слотов из закрытого набора типов (например
intиln/linenumberдля номеров строк подряд): межслотовый разделитель задаётся классом формы (kind = "delimited_slots"+ альтернатива литералов в TOML или вывод по шаблонуtail_signature), а не отдельной строкой «у этой мелодии:, у той пробел». По мере нужды для обратной совместимости с текущим v1 можно допускать несколько эквивалентных написаний одной и той же семантики (напримерels:5:15иels 5 15), нормализуя перед разбором; в подсказках и документе всё же держать один канонический вид. - Экзотика — составной 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 (или только код)¶
Различаем каталог параметрических записей и собственно грамматику / дерево правил разбора.
-
tail_signatureи прочие скаляры (chord_commit, флаги, подсказки) — в TOML в одной строке каталога ([[melody_root]]целевой или[[parametric]]на миграции), без разрастания таблиц: имена слотов и типы вшиты вtail_signature(§3.3). -
Полная грамматика (альтернативы, приоритет веток, вложенные конструкции, «сначала этот префикс, иначе тот») в чистом TOML быстро превращается в плохо читаемую вложенность или в повтор костыльных списков.
-
Когда грамматика реально ветвистая и её хотят править без перекомпиляции или валидировать схемой, уместен отдельный формат с иерархией — чаще всего XML (или JSON/YAML + JSON Schema): удобно выразить sequence/choice, при необходимости приложить XSD/RNG. Запись в TOML тогда ссылается на ресурс, например
grammar_ref = "bundled:melody/strategies/foo.xml"или путь относительно оверлея — ровно один уровень косвенности, без дублирования каталога. -
Пока семейство стратегий мало, достаточно разбора
tail_signatureв коде + детерминированной сборки args; вынос разбора в XML не обязателен — только когда появится много парсеров или потребность в внешнем редакторе правил.
Итого: TOML держит каталог и компактную сигнатуру слотов (tail_signature); тело сложной стратегии — по мере роста либо код, либо внешний декларативный файл (XML как разумный кандидат для глубоко вложенных правил); смешивать тяжёлую грамматику и TOML в одном файле целым полотном — не цель этого ADR.
3.6. Расширяемость: плагины и кто несёт код¶
Каталог без binder в TOML рассчитан на ядро: фиксированный набор типов слотов и таблица соответствий «(command_id, tail_signature) → сборка args».
Когда появятся плагины, параметрическое расширение — это расширение границы плагина, а не «ещё одна магическая строка в TOML»:
- Плагин, регистрирующий новый
command_idи/или новую мелодию, обязан предоставить обработку того, что добавляет: минимум разбор / валидацию хвоста в wire-форме и построение аргументов для своей команды (или явный отказ с понятной ошибкой). - 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. Шаги внедрения (ориентир)¶
- Завести DTO загрузки: внутренний снимок в виде
[[melody_root]]-подобного списка (даже если файл пока в формате[aliases]+[[parametric]]из §3.2.2); при необходимости[[tail_wire_class]]. Расширениеintent-melody-aliases.toml(второй файл + merge только при реальной нужде см. §3.1). - Реализовать
ParametricMelodyCatalog/ общий каталог корней мелодии (read-only после старта), не дублируя slug между слоями после нормализации. - Перевести
intent-melody-aliases.tomlна[[melody_root]](§3.2.1) — сразу вручную или после короткого этапа с §3.2.2; убрать дубли[aliases]/[[parametric]]для тех же slug. - Постепенно заменить ветви в
IdeCommandPaletteFilterOrchestratorиCascadeChordIntentSessionна обращение к каталогу + детерминированную сборку args по (command_id,tail_signature). - Удалить дубли:
PaletteOnlyAliasesкак главный источник правды — перенести в TOML; в коде — вывод стратегии изtail_signatureи детерминированная сборка args для встроенных команд. - Обновить intent-melody-language-v1.md: ссылка на этот ADR и на поля каталога (
[[melody_root]]и миграционный вид). - По необходимости (§3.5): поле
grammar_refна строку каталога ([[melody_root]]или[[parametric]]) или на сторонний реестр стратегий + парсер внешнего формата (XML/JSON + схема). Отдельно: когда появится модель плагинов — см. §3.6.
Статус реализации (Implemented — шаги 1–6 §5; §3.5–3.6 вне объёма)¶
Соответствие шагам внедрения (раздел §5 ниже):
| Шаг | Содержание ADR | Факт в коде/данных |
|---|---|---|
| 1 | DTO загрузки, нормализация миграционного TOML | IntentMelodyAliases → IntentMelodyBundleState / 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; в бандле (напр.wai→wai-url).- Тесты:
IntentMelodyCatalogWireTests,ParametricIntentMelodyTests,IntentMelodyAliasesTests.
6. Связка с MCP и реестром команд¶
Этот ADR не меняет контракт ide_execute_command: по-прежнему command_id + JSON args. Изменение — только способ производить args из строки мелодии и согласовать UX между поверхностями. Схема аргументов конкретной команды по-прежнему может дублироваться в документации команд / кодогенерации 0018 (если примут для IDE), ортогонально каталогу ([[melody_root]] / миграционный [[parametric]]).