1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct AgentTopic {
15 pub id: &'static str,
17 pub aliases: &'static [&'static str],
19 pub category: &'static str,
21 pub summary: &'static str,
23 pub body: &'static str,
26}
27
28pub fn agent_topics() -> &'static [AgentTopic] {
30 TOPICS
31}
32
33pub fn find_agent_topic(query: &str) -> Option<&'static AgentTopic> {
35 let query = query.trim();
36 TOPICS.iter().find(|topic| {
37 topic.id.eq_ignore_ascii_case(query)
38 || topic
39 .aliases
40 .iter()
41 .any(|alias| alias.eq_ignore_ascii_case(query))
42 })
43}
44
45static TOPICS: &[AgentTopic] = &[
46 AgentTopic {
47 id: r#"syntax"#,
48 aliases: &[r#"carriers"#, r#"carrier"#, r#"annotations"#, r#"notation"#],
49 category: r#"Basics"#,
50 summary: r#"the carrier convention: the two vehicles, key=value, compact codes, the -hex= rule"#,
51 body: r#"croma round-trips MusicXML through ABC. ABC 2.1 cannot natively express every MusicXML fact, so croma stores those facts in namespaced **carriers** that ride inside the ABC text and are re-applied on the way back to MusicXML. Every other ABC tool ignores them, so the file stays playable in abc2midi / abcm2ps / abcjs while croma keeps full fidelity.
52
53Two vehicles:
54- inline `[I:croma-<name> k=v ...]` (or, for the seven coded carriers below — eight codes, since `croma-harmony-text` has two — the compact `[I:cr <code> ...]`) — anchored to the following note / chord / barline / `[M:]` / `[K:]`. The default; use it for per-note and per-measure facts.
55- header `%%croma-<name> ...` — anchored to a voice or the score. Use only for score/voice-level facts. **Never compacted** — headers always use the long name.
56
57Fields are `key=value`, space-separated; double-quote a value with spaces (`name="Snare Drum"`). A boolean carrier carries no fields — the bare name is the flag (e.g. `[I:croma-musicxml-forward]`, or compact `[I:cr mf]`).
58
59**Compact spelling.** croma writes the seven most frequent carriers compact by default (eight codes below — `croma-harmony-text` has two, `ht` and `htx`) — `[I:cr <code> ...]` instead of `[I:croma-<name> ...]` — because it is shorter (~14.5% smaller exported ABC). Write compact yourself; the old long spelling still reads (with a one-time deprecation warning per kind) until croma 2.0, and `croma fmt` migrates any long spelling it finds back to compact automatically.
60
61| Code | Long name | Fields |
62|---|---|---|
63| `dp` | `croma-direction-placement` | `dp=<a\|b>` (a=above, b=below) |
64| `ht` | `croma-harmony-text` | `ht text="..."` or `ht text-hex=...` |
65| `htx` | `croma-harmony-text` | `htx` (the textless flag; no fields) |
66| `le` | `croma-lyric-extend` | `le=<verse>` |
67| `mr` | `croma-meter-restatement` | `mr` (flag) |
68| `kr` | `croma-key-restatement` | `kr` (flag) |
69| `ec` | `croma-ending-close` | `ec t=<s\|d> l=<l\|r> n="..."` |
70| `mf` | `croma-musicxml-forward` | `mf` (flag) |
71
72The other 15 carriers (e.g. `croma-clef-cursor`, `croma-musicxml-tuplet`, `croma-xvoice-slur`) have no code and keep their long spelling; run `croma agent <topic>` for each one's syntax.
73
74The `-hex=` rule: inside an inline `[I:...]`, the characters `]`, `%`, and raw control characters break the ABC tokenizer. When a free-text value contains one, croma emits a hex variant of the field instead — `text="John"` becomes `text-hex=4a6f686e` (the UTF-8 bytes as lowercase hex). Header `%%` lines are line-level and do not need it.
75
76Run `croma agent <topic>` for any specific notation, then `croma xml file.abc` to confirm it persisted."#,
77 },
78 AgentTopic {
79 id: r#"score"#,
80 aliases: &[
81 r#"%%score"#,
82 r#"grouping"#,
83 r#"staves"#,
84 r#"%%staves"#,
85 r#"staff-grouping"#,
86 ],
87 category: r#"Basics"#,
88 summary: r#"%%score staff grouping: ( ) overlay one staff, { } grand staff, [ ]/bare one part each"#,
89 body: r#"`%%score` (synonym: `%%staves`) is standard ABC, not a croma carrier — documented here because agents routinely misuse it. It lays voices out into MusicXML parts and staves:
90
91- bare voices and `[ ]` brackets: ONE single-staff part per voice, in directive order (`[ ]` only draws a visual bracket over the group);
92- `( )` merges its voices onto ONE staff of one part — overlay streams sharing a staff (divisi, e.g. soprano + alto on one staff). NOT for separate instruments: two instruments in `( )` end up crammed onto a single crowded staff;
93- `{ }` is a piano-style grand staff — ONE part with a separate staff per member — ONLY when every member is a staff of the same voice (`Piano`, `Piano#2`: croma's `#N` staff-continuation naming). Braced voices with distinct ids (`{RH LH}`) do NOT merge — each stays its own single-staff part and the brace is visual grouping only. Never brace unrelated instruments just to group them;
94- a voice the directive does not mention is appended as its own single-staff part; with no directive at all, every voice is its own single-staff part. If several `%%score` lines name voices, the last one wins.
95
96Example — five single-staff instruments plus one piano grand staff:
97```
98%%score Lead Arp Pad Kick Bass {Piano Piano#2}
99V:Piano
100E F G A |
101V:Piano#2 clef=bass
102C, C, G, G, |
103```
104Verify: `croma xml f.abc | grep -cE '<part id|<staves'` (here: six `<part id`, one `<staves>2`)"#,
105 },
106 AgentTopic {
107 id: r#"part-instrument"#,
108 aliases: &[
109 r#"croma-musicxml-instrument"#,
110 r#"instrument-def"#,
111 r#"midi-instrument"#,
112 r#"drum-kit"#,
113 ],
114 category: r#"Instruments & chord symbols"#,
115 summary: r#"a part's declared instrument (name, MIDI channel/program/volume/pan), incl. drum kits"#,
116 body: r#"Persists a part's `<score-instrument>` + `<midi-instrument>` — id, name, and MIDI channel/program/volume/pan/midi-unpitched, including percussion kits ABC cannot name.
117
118Vehicle: header `%%croma-musicxml-instrument` (one per declared instrument), placed under the voice's `V:` line.
119
120Example:
121```
122V:P1
123%%croma-musicxml-instrument id="P1-I1" name="Snare Drum" channel=10 midi-unpitched=39
124```
125Verify: `croma xml f.abc | grep -E '<score-instrument|<midi-instrument'`"#,
126 },
127 AgentTopic {
128 id: r#"note-instrument"#,
129 aliases: &[r#"croma-note-instrument"#, r#"per-note-instrument"#],
130 category: r#"Instruments & chord symbols"#,
131 summary: r#"which declared instrument sounds a single note"#,
132 body: r#"Persists a per-note `<instrument id=.../>` reference — which of the part's declared instruments sounds this note (a multi-instrument part, e.g. a drum staff).
133
134Vehicle: inline `[I:croma-note-instrument id="..."]` before the note.
135
136Example: `[I:croma-note-instrument id="P1-I1"]C`
137
138Pair it with `croma agent part-instrument`, which declares the ids.
139Verify: `croma xml f.abc | grep '<instrument '`"#,
140 },
141 AgentTopic {
142 id: r#"harmony-text"#,
143 aliases: &[
144 r#"croma-harmony-text"#,
145 r#"chord-symbol-text"#,
146 r#"ht"#,
147 r#"htx"#,
148 ],
149 category: r#"Instruments & chord symbols"#,
150 summary: r#"the printed text/provenance of a chord symbol (<harmony><kind text=...>)"#,
151 body: r#"Persists a chord symbol's `<harmony><kind text=...>` provenance: `Textless` (a kind with no printed text) vs `Text(value)` (an explicit printed quality) vs (carrier absent) an ABC-native chord whose text croma rebuilds from the chord string.
152
153Vehicle: inline `[I:cr ht ...]` (code `ht` for text, `htx` for the textless flag) immediately before the quoted chord symbol. Free text uses the `-hex=` rule. The long spelling `[I:croma-harmony-text ...]` still reads (deprecated until croma 2.0; `croma fmt` migrates it automatically).
154
155Examples:
156```
157[I:cr ht text="dim"]"Bdim"B
158[I:cr htx]"C"C
159```
160Verify: `croma xml f.abc | grep -A2 '<harmony'`"#,
161 },
162 AgentTopic {
163 id: r#"lyric-extend"#,
164 aliases: &[r#"croma-lyric-extend"#, r#"melisma"#, r#"extend"#, r#"le"#],
165 category: r#"Lyrics"#,
166 summary: r#"a same-note melisma <extend/> on the primary syllable of a verse"#,
167 body: r#"Persists the primary syllable's same-note `<extend/>` melisma flag for a verse — a syllable held with an extender that `w:` alone cannot mark on that note.
168
169Vehicle: inline `[I:cr le=N]` (code `le`) before the note; `w:` still carries the syllable text. The long spelling `[I:croma-lyric-extend verse=N]` still reads (deprecated until croma 2.0; `croma fmt` migrates it automatically).
170
171Example: `[I:cr le=1]A2`
172Verify: `croma xml f.abc | grep '<extend'`"#,
173 },
174 AgentTopic {
175 id: r#"lyric-duplicate"#,
176 aliases: &[r#"croma-lyric-duplicate"#, r#"same-note-syllable"#],
177 category: r#"Lyrics"#,
178 summary: r#"an extra same-note, same-verse syllable that w: cannot spell (hex text)"#,
179 body: r#"Persists an additional `<lyric>` syllable on the SAME note and SAME verse — two syllables on one note, which `w:` cannot express. Free text uses the `-hex=` rule.
180
181Vehicle: inline `[I:croma-lyric-duplicate verse=N text="..."]` (optional `extend=1`) before the note.
182
183Example: `[I:croma-lyric-duplicate verse=1 text="John"]C`
184Verify: `croma xml f.abc | grep -c '<lyric'`"#,
185 },
186 AgentTopic {
187 id: r#"tempo"#,
188 aliases: &[r#"croma-tempo"#, r#"tempo-text"#],
189 category: r#"Tempo"#,
190 summary: r#"a printed tempo whose <words> text Q: cannot hold (hex text)"#,
191 body: r#"Persists a printed/sound tempo whose `<words>` text the ABC `Q:` field cannot hold — role, words, bpm and reference beat. Free text uses the `-hex=` rule.
192
193Vehicle: inline `[I:croma-tempo role=printed ...]` (voice-scoped).
194
195Example: `[I:croma-tempo role=printed text-hex=416c6c6567726f bpm=100 beat-n=1 beat-d=4]`
196Verify: `croma xml f.abc | grep -E '<metronome|<words'`"#,
197 },
198 AgentTopic {
199 id: r#"sound-tempo"#,
200 aliases: &[r#"croma-sound-tempo"#, r#"playback-tempo"#],
201 category: r#"Tempo"#,
202 summary: r#"a playback-only tempo (<sound tempo>) with no printed metronome"#,
203 body: r#"Persists a playback-only tempo — a MusicXML `<sound tempo=...>` with NO printed metronome — as bpm + reference beat, so the round trip does not print a metronome the source never showed.
204
205Vehicle: inline `[I:croma-sound-tempo bpm=N beat-n=.. beat-d=.. text=".."]` (voice-scoped).
206
207Example: `[I:croma-sound-tempo bpm=80 beat-n=1 beat-d=4 text="80"]`
208Verify: `croma xml f.abc | grep '<sound tempo'`"#,
209 },
210 AgentTopic {
211 id: r#"direction-placement"#,
212 aliases: &[
213 r#"croma-direction-placement"#,
214 r#"dynamic-placement"#,
215 r#"wedge-placement"#,
216 r#"dp"#,
217 ],
218 category: r#"Directions"#,
219 summary: r#"above/below placement for MusicXML direction decorations"#,
220 body: r#"Persists a MusicXML `<direction placement=...>` on decorations that ABC spells as `!decor!`, including dynamics and hairpin wedges.
221
222Vehicle: inline `[I:cr dp=<a|b>]` (code `dp`; `a`=above, `b`=below) immediately before the decoration. The long spelling `[I:croma-direction-placement placement=above|below]` still reads (deprecated until croma 2.0; `croma fmt` migrates it automatically).
223
224Example: `[I:cr dp=a]!p!C`
225Verify: `croma xml f.abc | grep '<direction placement'`"#,
226 },
227 AgentTopic {
228 id: r#"initial-key"#,
229 aliases: &[r#"croma-initial-key"#, r#"per-voice-key"#],
230 category: r#"Key & meter"#,
231 summary: r#"a per-voice initial <key> the header K: cannot encode"#,
232 body: r#"Persists a per-voice initial `<key>` that differs from the score `K:` header — fifths plus explicit accidentals the header cannot encode for one voice.
233
234Vehicle: inline `[I:croma-initial-key fifths=N accidentals=STEP:ALTER,...]` at the start of the voice.
235
236Example: `[I:croma-initial-key fifths=0 accidentals=F:1,C:1]`
237Verify: `croma xml f.abc | grep -A3 '<key'`"#,
238 },
239 AgentTopic {
240 id: r#"initial-meter"#,
241 aliases: &[r#"croma-initial-meter"#, r#"per-voice-meter"#],
242 category: r#"Key & meter"#,
243 summary: r#"a per-voice initial <time> (display + common/cut symbol) the header M: cannot encode"#,
244 body: r#"Persists a per-voice initial `<time>` — its display string and common/cut symbol — that differs from the score `M:` header (e.g. header `M:C|` but this part starts numeric `2/2`).
245
246Vehicle: inline `[I:croma-initial-meter display=".." symbol=common|cut]` at the start of the voice.
247
248Example: `[I:croma-initial-meter display="2/2" symbol=cut]`
249Verify: `croma xml f.abc | grep -A3 '<time'`"#,
250 },
251 AgentTopic {
252 id: r#"key-restatement"#,
253 aliases: &[r#"croma-key-restatement"#, r#"redundant-key"#, r#"kr"#],
254 category: r#"Key & meter"#,
255 summary: r#"flag: the following [K:] is a redundant restatement that must survive ABC dedupe"#,
256 body: r#"A flag marking the following inline `[K:]` as a redundant restatement of the already-effective key that must survive ABC's dedupe (the source restated `<key>` even though it was unchanged).
257
258Vehicle: boolean inline `[I:cr kr]` (code `kr`) immediately before the `[K:]`. The long spelling `[I:croma-key-restatement]` still reads (deprecated until croma 2.0; `croma fmt` migrates it automatically).
259
260Example: `[I:cr kr] [K:F]`
261Verify: `croma xml f.abc | grep -c '<key'`"#,
262 },
263 AgentTopic {
264 id: r#"meter-restatement"#,
265 aliases: &[r#"croma-meter-restatement"#, r#"redundant-meter"#, r#"mr"#],
266 category: r#"Key & meter"#,
267 summary: r#"flag: the following [M:] is a redundant restatement that must survive ABC dedupe"#,
268 body: r#"A flag marking the following inline `[M:]` as a redundant restatement of the already-effective meter that must survive ABC's dedupe.
269
270Vehicle: boolean inline `[I:cr mr]` (code `mr`) immediately before the `[M:]`. The long spelling `[I:croma-meter-restatement]` still reads (deprecated until croma 2.0; `croma fmt` migrates it automatically).
271
272Example: `[I:cr mr] [M:4/4]`
273Verify: `croma xml f.abc | grep -c '<time'`"#,
274 },
275 AgentTopic {
276 id: r#"time-symbol"#,
277 aliases: &[r#"croma-time-symbol"#, r#"common-cut"#],
278 category: r#"Key & meter"#,
279 summary: r#"a <time symbol="common|cut"> when the M: display is numeric, not C / C|"#,
280 body: r#"Persists `<time symbol="common|cut">` when the meter DISPLAY is numeric (not the ABC `C` / `C|` shorthand) — e.g. a 4/4 printed with the common-time glyph.
281
282Vehicle: a whole-line `%%croma-time-symbol ...` before the header `M:`, or inline `[I:croma-time-symbol symbol=cut]` before a mid-tune `[M:]`.
283
284Example: `[I:croma-time-symbol symbol=cut]`
285Verify: `croma xml f.abc | grep '<time symbol'`"#,
286 },
287 AgentTopic {
288 id: r#"forward"#,
289 aliases: &[
290 r#"croma-musicxml-forward"#,
291 r#"invisible-rest-gap"#,
292 r#"mf"#,
293 ],
294 category: r#"Cursor & structure"#,
295 summary: r#"flag: re-emit <forward> (a silent cursor advance) for an invisible-rest gap, not a <rest>"#,
296 body: r#"A flag telling croma to re-emit the following invisible-rest gap as a MusicXML `<forward>` (a silent cursor advance) rather than a `<note><rest>`.
297
298Vehicle: boolean inline `[I:cr mf]` (code `mf`) before the invisible rest (`x`). The long spelling `[I:croma-musicxml-forward]` still reads (deprecated until croma 2.0; `croma fmt` migrates it automatically).
299
300Example: `[I:cr mf]x4`
301Verify: `croma xml f.abc | grep '<forward'`"#,
302 },
303 AgentTopic {
304 id: r#"sequence-backup"#,
305 aliases: &[r#"croma-musicxml-sequence-backup"#, r#"backup"#],
306 category: r#"Cursor & structure"#,
307 summary: r#"the explicit <backup> duration between two voice sequences in a measure"#,
308 body: r#"Persists the explicit `<backup>` duration between two voice sequences in a measure when it overshoots the prior cursor — a backup ABC's voice model cannot otherwise spell.
309
310Vehicle: inline `[I:croma-musicxml-sequence-backup n=NUM d=DEN]` on the first event of the later sequence (voice-scoped).
311
312Example: `[I:croma-musicxml-sequence-backup n=3 d=8]`
313Verify: `croma xml f.abc | grep '<backup'`"#,
314 },
315 AgentTopic {
316 id: r#"wide-tuplet"#,
317 aliases: &[r#"croma-musicxml-tuplet"#, r#"tuplet"#],
318 category: r#"Cursor & structure"#,
319 summary: r#"a tuplet whose actual_notes is outside ABC's (p:q:r range (2..=9)"#,
320 body: r#"Persists a tuplet whose `actual_notes` is outside ABC's `(p:q:r` range (2..=9) — e.g. an 11:8 — as id, actual, normal and role, so the `<time-modification>` / `<tuplet>` survive.
321
322Vehicle: inline `[I:croma-musicxml-tuplet id=N actual=A normal=B role=start|continue|stop]` on each member.
323
324Example: `[I:croma-musicxml-tuplet id=1 actual=11 normal=8 role=start]C`
325Verify: `croma xml f.abc | grep -A2 '<time-modification'`"#,
326 },
327 AgentTopic {
328 id: r#"after-grace"#,
329 aliases: &[r#"croma-after-grace"#, r#"trailing-grace"#],
330 category: r#"Cursor & structure"#,
331 summary: r#"flag: the following {...} grace group is an after-grace bound to the PRECEDING note"#,
332 body: r#"A flag marking the following `{...}` grace group as an AFTER-grace bound to the preceding note (a trailing ornament), distinct from an ordinary leading grace.
333
334Vehicle: boolean inline `[I:croma-after-grace]` between the note and its trailing `{...}`.
335
336Example: `C[I:croma-after-grace]{de}`
337Verify: `croma xml f.abc | grep '<grace'`"#,
338 },
339 AgentTopic {
340 id: r#"clef-change"#,
341 aliases: &[r#"croma-clef-cursor"#, r#"mid-tune-clef"#],
342 category: r#"Cursor & structure"#,
343 summary: r#"a mid-tune <clef> wrapped in <backup>/<forward> (clef text + cursor durations; hex)"#,
344 body: r#"Persists a mid-tune `<clef>` that the source wrapped in `<backup>`/`<forward>` cursor moves — the clef text plus the back / pre-back durations. Clef text uses the `-hex=` rule when hostile.
345
346Vehicle: inline `[I:croma-clef-cursor clef=".." back-n=.. back-d=.. [pre-back-n=.. pre-back-d=..]]`.
347
348Example: `[I:croma-clef-cursor clef="bass" back-n=1 back-d=4]`
349Verify: `croma xml f.abc | grep '<clef'`"#,
350 },
351 AgentTopic {
352 id: r#"barline-style"#,
353 aliases: &[r#"croma-barline-style"#, r#"dashed-barline"#],
354 category: r#"Cursor & structure"#,
355 summary: r#"a dashed barline (<bar-style>dashed</bar-style>) — ABC has no glyph"#,
356 body: r#"Persists a barline style ABC has no glyph for — currently `dashed` (`<bar-style>dashed</bar-style>`).
357
358Vehicle: inline `[I:croma-barline-style style=dashed]` before the barline.
359
360Example: `[I:croma-barline-style style=dashed] |`
361Verify: `croma xml f.abc | grep '<bar-style'`"#,
362 },
363 AgentTopic {
364 id: r#"measure-number"#,
365 aliases: &[r#"croma-measure-number"#, r#"pickup-number"#],
366 category: r#"Cursor & structure"#,
367 summary: r#"a <measure number=...> that differs from croma's canonical 1-based index (hex)"#,
368 body: r#"Persists a `<measure number=...>` that differs from croma's canonical 1-based index — a pickup `0`, a repeated movement-local number, or a label like `X1`. Odd values use the `-hex=` rule.
369
370Vehicle: inline `[I:croma-measure-number n=...]` at the start of the measure.
371
372Example: `[I:croma-measure-number n=0]`
373Verify: `croma xml f.abc | grep '<measure number'`"#,
374 },
375 AgentTopic {
376 id: r#"ending-close"#,
377 aliases: &[r#"croma-ending-close"#, r#"volta-close"#, r#"ec"#],
378 category: r#"Cursor & structure"#,
379 summary: r#"an explicit volta-bracket close (<ending type="stop|discontinue">)"#,
380 body: r#"Persists an explicit volta-bracket close — `<ending type="stop|discontinue">` plus its side (location) and numbers — that ABC's repeat syntax does not spell on its own.
381
382Vehicle: inline `[I:cr ec t=<s|d> l=<l|r> n="..."]` (code `ec`; `t`: s=stop, d=discontinue; `l`: l=left, r=right). The long spelling `[I:croma-ending-close type=stop|discontinue location=right number=".."]` still reads (deprecated until croma 2.0; `croma fmt` migrates it automatically).
383
384Example: `[I:cr ec t=d l=r n="1,2"]`
385Verify: `croma xml f.abc | grep '<ending'`"#,
386 },
387 AgentTopic {
388 id: r#"xvoice-slur"#,
389 aliases: &[
390 r#"croma-xvoice-slur"#,
391 r#"slur-across-voices"#,
392 r#"cross-voice-slur"#,
393 ],
394 category: r#"Cursor & structure"#,
395 summary: r#"a slur whose start and stop are in different voices"#,
396 body: r#"A `<slur>` whose start and stop are in different voices cannot be spelled with ABC `(`/`)`, which pair within one `V:` stream. Carry each end instead.
397
398Syntax: `[I:croma-xvoice-slur pair=N role=start|stop]` on the note at each end. `pair` is a shared id that re-pairs the two ends across voices; `role` says which end.
399
400Example (slur from voice 1 into voice 2):
401```
402%%score (1 2)
403V:1
404[I:croma-xvoice-slur pair=1 role=start]C D E F |
405V:2
406[I:croma-xvoice-slur pair=1 role=stop]G A B z |
407```
408Becomes `<slur type="start"/>` on C (voice 1) and `<slur type="stop"/>` on G (voice 2).
409Verify: `croma xml f.abc | grep '<slur'`"#,
410 },
411];
412
413#[cfg(test)]
414#[path = "agent_tests.rs"]
415mod tests;