ADR 0109: Unified declarative catalog of parametric Intent Melody (TOML + code binding of args)¶
Status: Accepted · Implemented
Date: 2026-05-11
Related ADRs¶
| ADR | Role |
|---|---|
| 0081 | Parametric tails for editor line ranges |
| 0060 | Command Melody, CascadeChord, parity with c: |
| 0108 | Web portal and parametric melody wai:… |
| 0030 | command_id, registry layers |
| 0008 | MCP / command execution |
Summary¶
- Intent Melody catalog:
[[melody_root]]+[[tail_wire_class]]in TOML. - Migration from legacy
[aliases]/[[parametric]]; binders in code. - Implemented: loader, runtime catalog, palette/chord; plugins — separate model.
Implementation snapshot¶
| Element | Value |
|---|---|
| TOML | [[melody_root]], [[tail_wire_class]] |
| Docs | intent-melody-language-v1; no legacy [aliases] |
Outside ADR¶
| Document | Role |
|---|---|
| intent-melody-language-v1.md | IML v1 |
IntentMelody/intent-melody-aliases.toml |
Registry alias → command_id |
On IML: this ADR defines an architectural path to IML “v2” not as a new language from scratch but as a natural extension of v1: the same wire for the user (wai:…, els:…), but a unified declarative root catalog ([[melody_root]]), explicit shape, presentation ([[tail_wire_class]]), and deterministic args assembly in code. A normative “IML v2” document may be written separately after fields stabilize; until then the source of truth is here and updates to intent-melody-language-v1.md.
1. Context¶
Today parametric melody forms are scattered:
- A flat
[aliases]table in TOML only maps short mnemonic →command_id(IntentMelodyAliases). - Special cases are hard-coded in
ParametricIntentMelody(parsingwai:…,els:start:end, “palette only” lists, chord heuristics, hints). - The palette (
IdeCommandPaletteFilterOrchestrator) and chord (CascadeChordIntentSession) must duplicate semantics of what is parametric, when Enter fires, and which arguments apply.
That scales poorly: each new parametric root needs edits in several places and behavior easily diverges between c: and Ctrl+K.
2. Problem¶
- No unified shape description: what is the parametric “base”, tail syntax, whether chord needs Enter, how slots map to JSON command args.
- Alias declaration and parametric declaration are split — conflicts with the IML principle (“one line — one intent” on top of the shared command canon).
- C# attributes like
[Parameter(Position, Type, Description)]on execution methods do not match the args assembly point: the command executor takescommand_id+ JSON dictionary; building that dictionary depends on IDE context (file, editor text, column normalization for ranges, etc.), not only CLR parameter DTOs.
Requirement: one declarative source of truth, while complex semantics stay in small, named code, not growing switches on alias strings.
3. Decision¶
3.1. Two layers: catalog in TOML + binding to command_id in code¶
-
Target root declaration — one array
[[melody_root]](§3.2.1): each row is one slug,command_id,shape = "simple" | "parametric"; forparametric, the same fields today split between[aliases]and[[parametric]](tail_signature,wire_class,chord_commit, …). One loader pass, one DTO, no duplicate slug/command_idor table drift — simpler long term. -
TOML migration: it is acceptable to convert
intent-melody-aliases.tomlto target[[melody_root]](§3.2.1) in one manual edit (or script) — a long transitional format is not required. If phased adoption is easier, the file may temporarily stay flat[aliases]+[[parametric]](+ optional[alias_type],[[tail_wire_class]], §3.2.2); the loader normalizes to the same internal snapshot as from[[melody_root]]. All in one** file (a second file remains the exception when volume grows). -
tail_signature— one string in canonical slot notation after the root’s first:, see §3.3. The loader derives parse strategy from that signature (no separatesyntaxin config). Inter-slot presentation (which symbols are allowed between slots in wire) — optional registry[[tail_wire_class]]in §3.3 — so alternatives like:or space are not hard-coded only in C#, but live beside the catalog in a narrow “literal or literal” form. -
No
binderfield in TOML. The catalog describes only tail shape; mapping parsed slots toargsis part ofcommand_idexecution and/or a thinParametricMelodyArgsBuilderlayer chosen deterministically by (command_id,tail_signature, IDE context), without “magic strings” in configuration.
Invariant: command_id remains the canonical execution key (as MCP); TOML must not become Turing-complete — only route string form → args assembly by deterministic rules in code.
Linking [aliases] and [[parametric]]: alias_type (migration phase only)¶
While the file still has flat [aliases] and separate [[parametric]], not unified [[melody_root]]: short mnemonic in [aliases] (slug → command_id), tail shape in [[parametric]] with the same alias. After moving to [[melody_root]], separate [alias_type] is unnecessary — shape plays that role. Below — rules for the transitional format.
To align loader and humans on whether a target is simple (no tail parse) or parametric, introduce alias_type:
| Value | Meaning |
|---|---|
simple |
A single line without slot parse after the root is enough to execute (like most c: roots today). |
parametric |
This slug requires a [[parametric]] row with matching command_id and shape fields (tail_signature, …). |
Two modes.
- Default — inference: if
alias_typeis empty (or missing for a slug), type isparametricwhen[[parametric]]exists for the samealias, elsesimple. One file and one load pass suffice; duplicate is optional. - Explicit mode: optional
[alias_type]table with the same keys as[aliases]and valuessimple/parametric. Use for strict validation (explicitparametricwithout[[parametric]]→ dev error;simplewith[[parametric]]for the same slug → error or overlap forbidden — loader policy) and so diffs clearly show row class without reading the whole[[parametric]]array.
If you keep the temporary layout in §3.2.2, flat [aliases] need not disappear immediately — compatibility with the accepted file format; [alias_type] is added only when explicit validation is needed. On manual migration to [[melody_root]] (§3.2.1), neither [alias_type] nor [[parametric]] are needed for the same roots.
Consistency invariant: for each slug, command_id in [aliases] and in matching [[parametric]] (if any) must match.
3.2. TOML record shapes¶
Field illustration (contract for the loader; file schema version keys — see unification note below). tail_signature meta-notation — §3.3. On migration to [[melody_root]], schema version keys (melody_catalog_schema_version vs parametric_schema_version) should collapse to one key so the loader reads a single version.
3.2.1. Target unified model (long term): [[melody_root]]¶
One array for the whole c: root catalog: slug, command_id, shape. For shape = "parametric" — shape fields; for simple — only slug and command_id (and rare shared UX fields if needed). No duplicate slug across tables.
Optional show_usage_hint_if_bare_slug (bool): if true, in palette c: typing only the root without args shows a tail-form hint line, not the catalog command row (els/eld and stubs may default true in the loader). If false (or omitted where default is false), bare slug behaves like a normal palette command when resolved.
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. Optional: flat [aliases] + [[parametric]] (phased adoption)¶
If target [[melody_root]] is not introduced in one commit (e.g. loader learns the old file shape first), the previous layout is allowed temporarily (plus optional [alias_type] — §3.1). Otherwise skip this block — go straight to §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 fields normalize “instant on obvious single root vs Enter only” without a separate C# HashSet.
3.3. Canonical meta-notation: <name:type> and clean TOML¶
To define parameters unambiguously in the catalog without nested-table clutter, fix a string tail signature after alias:
- Each slot is
<identifier:type>. - Multiple slots are separated by one literal
:between closing>and the next<.
Illustrative full forms (document canon / “how a human reads it”; c: prefix in palette is outside):
Canon example (tail after c:) |
Meaning |
|---|---|
wai:<url:url> |
One parameter url of type url. |
els:<start:ln>:<end:ln> |
start and end — line numbers (1-based inclusive); slot type ln (or linenumber) in meta-notation, see 0081. Slot :int for the same chain is still supported as equivalent at parse time. |
tail_signature in TOML carries only the part after the root (without repeating the slug): e.g. "<url:url>" or "<start:ln>:<end:ln>" — see §3.2 ([[melody_root]] or [[parametric]]).
Types (url, int, later path, string, …) — closed registry in code: unknown type at catalog load → configuration error (dev) or ignore overlay row with log (product — separate decision).
Wire compatibility. User input stays simple: wai:https://… and els:5:15 (no < >). tail_signature is for description (readability, hints, validators, args mapping) only; users do not type it.
Wire between slots: not one separator for the whole language, not one per melody¶
One global inter-slot separator for all roots conflicts with natural wire: after wai: the whole tail is one URL with its own :, which cannot be split with the same symbol as an els chain. A per-[[parametric]] separator field is flexible but becomes a zoo for users and docs (“here :, there space”).
Acceptable compromise — three levels (per-row separator is not baseline; presentation rules for a form class live in the bundle beside the catalog and/or minimal code fallback, see §3.3):
- One “free” slot (remainder of line) — e.g.
<url:url>without a second slot: after canonicalalias:(first literal:in wire separates root from value; further:inside the value are not inter-slot. Spaces, brackets, Cyrillic in the same value are allowed; URL validity — after the parser.) - Simple slot chain from a closed type set (e.g.
intandln/linenumberfor consecutive line numbers): inter-slot separator comes from a form class (kind = "delimited_slots"+ literal alternatives in TOML or inference fromtail_signature), not a per-melody string. For v1 backward compatibility, several equivalent writings of the same semantics may be allowed (e.g.els:5:15andels 5 15), normalized before parse; docs and hints still show one canonical form. - Exotic — composite payload where boundaries cannot be told from
:, multiple named segments with different delimiters, etc.: only thenwire_profile(or explicit override) and when a real scenario appears — short follow-up ADR; until then stay on levels 1–2.
Colons inside a single url slot are covered by level 1. Multiple URLs in one wire without “whole remainder” is level 3 — explicit profile or separate ADR.
Registry [[tail_wire_class]]: presentation grammar between slots¶
To avoid a “symbol table only in code”, the same catalog file (or bundle block before [[parametric]]) can describe wire classes in a narrow presentation language: essentially literal or literal between slots, not full EBNF.
id— class name; referenced bywire_classin[[parametric]]if you do not rely only on auto-inference fromtail_signature.kind: e.g.single_remainder(one slot — whole tail after first root separator) ordelimited_slots(slot chain; only listed separators between adjacent values).between_slots_any_of— array of strings (often one character each): in TOML cleaner than mixing with|in one field; semantically literal alternatives at the presentation layer (minimal “symbol or symbol” without full EBNF); on ambiguity loader policy chooses (see note below).kindand allowed fields tie to a closed registry in code: code remains the executor of this mini-descriptor (fewkindtemplates), not a duplicate of every[[parametric]]row.
Declarative intent is preserved: melody inventory and semantic shape stay in [[parametric]] (or [[melody_root]]); inter-slot symbol class is a separate small data layer, overridable like the catalog. Args assembly after parse — still in code by (command_id, tail_signature, IDE context).
Note. Order of trying alternatives and normalization ( ↔ : etc.) are fixed by loader and tests; for MVP a unique integer pair after normalization is enough.
Meta-notation is compact in TOML as one tail_signature string, names parameters for hints and MCP args binding; substantive assembly after parse — in code, deterministically by (command_id, tail_signature, IDE context), see §3.1.
3.4. Rejected alternatives¶
| Alternative | Why not as the only layer |
|---|---|
| C# attributes only | Loses overlay without rebuild; parametric form is about string language and palette, not CLR signature; IDE context cannot be fully declarative. |
Code only (switch on alias) |
Observed coupling between palette, chord, and MCP; sync errors. |
| One huge TOML without code | Editor column math, file bounds, complex URL normalization must live in tested code. |
Acceptable compromise: TOML = shape (tail_signature); IDE core = parse + deterministic args assembly for built-in command_id; complex forms and plugins — see §3.5 and §3.6.
3.5. Where TOML is enough, and where XML (or code only)¶
Distinguish parametric catalog entries from parse grammar / rule tree.
-
tail_signatureand other scalars (chord_commit, flags, hints) — in TOML on one catalog row ([[melody_root]]target or migration[[parametric]]), without growing tables: slot names and types are embedded intail_signature(§3.3). -
Full grammar (alternatives, branch priority, nested constructs, “this prefix first else that”) in pure TOML quickly becomes unreadable nesting or repeated lists.
-
When grammar is truly branchy and should be edited without recompile or schema-validated, a hierarchical format fits — often XML (or JSON/YAML + JSON Schema): sequence/choice, optional XSD/RNG. TOML then references a resource, e.g.
grammar_ref = "bundled:melody/strategies/foo.xml"or path relative to overlay — one level of indirection, no catalog duplication. -
While the strategy family is small, parsing
tail_signaturein code + deterministic args assembly is enough; moving parse to XML is not required until many parsers or an external rule editor is needed.
Summary: TOML holds the catalog and compact slot signature (tail_signature); body of a complex strategy — as complexity grows either code or an external declarative file (XML a reasonable candidate for deep nesting); mixing heavy grammar and TOML in one blob is not the goal of this ADR.
3.6. Extensibility: plugins and who carries code¶
A catalog without binder in TOML targets core: fixed slot types and a table mapping (command_id, tail_signature) → args assembly.
When plugins appear, parametric extension is a plugin boundary, not “another magic TOML string”:
- A plugin registering a new
command_idand/or melody must provide handling for what it adds: at minimum parse/validate tail wire and build arguments for its command (or explicit failure with a clear error). - TOML may still declare
slug/alias,command_id,tail_signature— as an intent contract (in target form, fields on[[melody_root]]), but “string → args” for a plugin stays in plugin code, not in core config without an extension host.
So there is no “guess a constant in TOML”: core has an unambiguous table; plugins contract through an extension API (modularity details — separate ADR when plugins are real).
4. Consequences¶
Positive
- One place to add a root long term: one
[[melody_root]]row (shape = "parametric") + a new args-assembly handler in code when needed; during migration — equivalent[[parametric]]. Inter-slot presentation in[[tail_wire_class]], withoutseparatoron every melody. - Unified rules for palette
c:, CascadeChord (Ctrl+K), and any other transport of the same string. - Easier IML documentation: parametric shape beside alias in data, not spread through
ParametricIntentMelody.cs.
Negative / risks
- Migration of existing
ParametricIntentMelody(PaletteOnlyAliases, hard-codedwai, hints) → catalog load + strategy fromtail_signatureand args assembly for core. - Startup validation: duplicate
slugin[[melody_root]]; unknown type intail_signatureor unsupported (command_id,tail_signature) — explicit dev error; user overlay — logged row rejection. Same for unknownwire_class,kindvstail_signatureconflict, emptybetween_slots_any_ofwhen[[tail_wire_class]]is used. With[alias_type]— conflict with actual[[parametric]]orcommand_idmismatch between[aliases]and[[parametric]]. - Optional complexity: loader for external grammar via
grammar_ref(§3.5); plugins as extension boundary — §3.6.
5. Adoption steps (guide)¶
- Add loader DTOs: internal snapshot as
[[melody_root]]-like list (even if the file is still[aliases]+[[parametric]]from §3.2.2); optional[[tail_wire_class]]. Extendintent-melody-aliases.toml(second file + merge only when truly needed, see §3.1). - Implement
ParametricMelodyCatalog/ shared melody root catalog (read-only after startup), no slug duplication across layers after normalization. - Convert
intent-melody-aliases.tomlto[[melody_root]](§3.2.1) — immediately by hand or after a short §3.2.2 phase; remove duplicate[aliases]/[[parametric]]for the same slugs. - Gradually replace branches in
IdeCommandPaletteFilterOrchestratorandCascadeChordIntentSessionwith catalog lookup + deterministic args by (command_id,tail_signature). - Remove duplicates:
PaletteOnlyAliasesas primary truth — move to TOML; in code — strategy fromtail_signatureand deterministic args for built-in commands. - Update intent-melody-language-v1.md: link this ADR and catalog fields (
[[melody_root]]and migration shape). - As needed (§3.5):
grammar_refon a catalog row ([[melody_root]]or[[parametric]]) or external strategy registry + parser for external format (XML/JSON + schema). Separately when a plugin model exists — §3.6.
Implementation status (Implemented — steps 1–6 of §5; §3.5–3.6 out of scope)¶
Alignment with adoption steps (§5 below):
| Step | ADR content | Fact in code/data |
|---|---|---|
| 1 | Loader DTOs, migration TOML normalization | IntentMelodyAliases → IntentMelodyBundleState / IntentMelodyCatalogSnapshot + overlay merge |
| 2 | Read-only root catalog | IntentMelodyCatalog + snapshot from bundle |
| 3 | Target [[melody_root]] in bundle |
IntentMelody/intent-melody-aliases.toml: melody_catalog_schema_version, only [[melody_root]] and [[tail_wire_class]] (no [aliases] / [[parametric]]) |
| 4 | Palette and chord via catalog | IdeCommandPaletteFilterOrchestrator, CascadeChordIntentSession, MelodyPaletteLineCommandPaletteExtensions + ParametricIntentMelody use IntentMelodyCatalog |
| 5 | Remove PaletteOnlyAliases as truth |
“Palette hint only” list in TOML (show_usage_hint_if_bare_slug); code keeps helper IsPaletteOnlyAlias (reads catalog), not a static slug list |
| 6 | Update intent-melody-language-v1.md |
Links to ADR 0109 and [[melody_root]] / [[tail_wire_class]] fields — in intent-melody-language-v1.md |
Step 7 (grammar_ref, external parse strategies) and §3.6 (plugins with their own binders) were optional / next era in the ADR — not “unfinished core”, but extensions after complexity grows.
Behavior and test details:
- Load and validation:
IntentMelodyAliases.Buildparses[[tail_wire_class]]and[[melody_root]], checkswire_class,chord_commit,kindvstail_signature(IntentMelodyTailSemantics). - Runtime catalog: read-only
IntentMelodyCatalogSnapshot+IntentMelodyCatalog; parametric parse uses wire classes (SingleRemainder/DelimitedSlots) fromParametricIntentMelody. chord_commit:ParametricIntentMelody.ChordDefersInstantExecuteFor*(enterand similar defer;immediate/instant— do not).palette_hint_slug:ParametricIntentMelody.ResolvePaletteHintKey; in bundle (e.g.wai→wai-url).- Tests:
IntentMelodyCatalogWireTests,ParametricIntentMelodyTests,IntentMelodyAliasesTests.
6. Link to MCP and command registry¶
This ADR does not change the ide_execute_command contract: still command_id + JSON args. The change is only how args are produced from a melody string and how UX is aligned across surfaces. Per-command argument schema may still be duplicated in command documentation / codegen 0018 (if adopted for IDE), orthogonal to the catalog ([[melody_root]] / migration [[parametric]]).