@hraness/dawg 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +655 -141
  3. package/README.md +27 -25
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +759 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +182 -99
  8. package/core/expression.ts +158 -6
  9. package/core/fx.ts +354 -32
  10. package/core/granular.ts +91 -0
  11. package/core/instruments.ts +180 -0
  12. package/core/keys.ts +573 -4
  13. package/core/loop.ts +5 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/resonators.ts +306 -4
  17. package/core/routing.ts +165 -0
  18. package/core/score.ts +1181 -16
  19. package/core/sdk/eval-child.ts +7 -2
  20. package/core/sdk/eval.ts +35 -6
  21. package/core/sdk/print.ts +354 -6
  22. package/core/sdk/sync-lyrics.ts +49 -0
  23. package/core/sdk/v1.ts +3160 -69
  24. package/core/sections.ts +298 -24
  25. package/core/sing.ts +815 -0
  26. package/core/strings.ts +237 -2
  27. package/core/style-provenance.ts +80 -0
  28. package/core/styles/africa-mena-southasia.ts +2893 -0
  29. package/core/styles/americas.ts +3810 -0
  30. package/core/styles/art.ts +4993 -0
  31. package/core/styles/base.ts +123 -0
  32. package/core/styles/cycles.ts +106 -0
  33. package/core/styles/electronic.ts +2723 -0
  34. package/core/styles/europe-asia-pacific.ts +2838 -0
  35. package/core/styles/excerpt.ts +29 -0
  36. package/core/styles/gamelan.ts +283 -0
  37. package/core/styles/generate.ts +2199 -0
  38. package/core/styles/index.ts +515 -0
  39. package/core/styles/parts.ts +106 -0
  40. package/core/styles/pop.ts +3189 -0
  41. package/core/styles/rock.ts +2993 -0
  42. package/core/styles/roots.ts +4175 -0
  43. package/core/styles/schema.ts +429 -0
  44. package/core/styles/taxonomy.ts +940 -0
  45. package/core/styles/validate.ts +528 -0
  46. package/core/tempo.ts +32 -2
  47. package/core/tuning.ts +19 -3
  48. package/core/vocoder.ts +524 -0
  49. package/core/winds.ts +652 -0
  50. package/guides/agent.md +29 -0
  51. package/guides/arrange.md +30 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -6
  54. package/guides/chords.md +16 -14
  55. package/guides/effects.md +17 -15
  56. package/guides/faders.md +20 -16
  57. package/guides/files.md +13 -9
  58. package/guides/getting-started.md +15 -11
  59. package/guides/keys.md +19 -15
  60. package/guides/media.md +17 -12
  61. package/guides/mix.md +12 -6
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/performance.md +15 -10
  65. package/guides/play.md +21 -13
  66. package/guides/project.md +25 -8
  67. package/guides/providers.md +20 -14
  68. package/guides/resample.md +32 -0
  69. package/guides/rhythm.md +17 -13
  70. package/guides/sessions.md +17 -7
  71. package/guides/show-me.md +31 -0
  72. package/guides/sound.md +25 -9
  73. package/guides/sounds.md +15 -12
  74. package/guides/styles.md +31 -0
  75. package/guides/tempo.md +15 -10
  76. package/guides/tracks.md +15 -10
  77. package/guides/tuning.md +32 -0
  78. package/guides/voice.md +31 -0
  79. package/guides/web-search.md +18 -8
  80. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  81. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  82. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  83. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  84. package/native/prebuilt/manifest.json +21 -0
  85. package/package.json +5 -2
  86. package/src/agent/agent.ts +131 -13
  87. package/src/agent/calibration-tools.ts +53 -0
  88. package/src/agent/chord-tools.ts +153 -1
  89. package/src/agent/clip-tools.ts +453 -0
  90. package/src/agent/command-agent.ts +369 -0
  91. package/src/agent/drum-tools.ts +2 -2
  92. package/src/agent/expression-tools.ts +91 -0
  93. package/src/agent/gateway.ts +246 -60
  94. package/src/agent/models.ts +53 -12
  95. package/src/agent/ops.ts +15 -1
  96. package/src/agent/pack-tools.ts +1 -1
  97. package/src/agent/planner.ts +13 -0
  98. package/src/agent/portable-schema.ts +80 -0
  99. package/src/agent/preview-tool.ts +5 -1
  100. package/src/agent/provider.ts +22 -8
  101. package/src/agent/resample-tool.ts +131 -0
  102. package/src/agent/rhythm-tools.ts +1 -1
  103. package/src/agent/section-tools.ts +1 -1
  104. package/src/agent/show-me.ts +497 -0
  105. package/src/agent/steer.ts +15 -0
  106. package/src/agent/style-tools.ts +217 -0
  107. package/src/agent/tool-error.ts +12 -0
  108. package/src/agent/tools.ts +307 -37
  109. package/src/agent/usage.ts +2 -2
  110. package/src/agent/voice-tools.ts +925 -0
  111. package/src/agent/xcb-agent.ts +16 -6
  112. package/src/argv.ts +38 -0
  113. package/src/audio/analysis.ts +253 -0
  114. package/src/audio/arrange.ts +58 -3
  115. package/src/audio/autotune-engine.ts +101 -0
  116. package/src/audio/autotune.ts +640 -0
  117. package/src/audio/clips.ts +240 -0
  118. package/src/audio/doctor.ts +86 -0
  119. package/src/audio/dsp/bandbank.ts +138 -0
  120. package/src/audio/dsp/envelope.ts +171 -0
  121. package/src/audio/dsp/follow.ts +120 -0
  122. package/src/audio/dsp/formant.ts +427 -0
  123. package/src/audio/dsp/glottal.ts +243 -0
  124. package/src/audio/dsp/interp.ts +7 -2
  125. package/src/audio/dsp/lpc.ts +50 -0
  126. package/src/audio/dsp/periodicity.ts +59 -0
  127. package/src/audio/dsp/pitch.ts +995 -0
  128. package/src/audio/dsp/psola.ts +199 -0
  129. package/src/audio/dsp/shift.ts +256 -0
  130. package/src/audio/effects/chain.ts +21 -4
  131. package/src/audio/effects/common.ts +57 -0
  132. package/src/audio/effects/convolution.ts +113 -5
  133. package/src/audio/effects/filter.ts +48 -69
  134. package/src/audio/effects/formant.ts +263 -0
  135. package/src/audio/effects/gaze.ts +354 -0
  136. package/src/audio/engine.ts +206 -33
  137. package/src/audio/fit.ts +80 -3
  138. package/src/audio/granular.ts +352 -21
  139. package/src/audio/instrument-check.ts +67 -47
  140. package/src/audio/instruments.ts +36 -3
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +427 -0
  143. package/src/audio/keys/engine.ts +156 -20
  144. package/src/audio/keys/organ.ts +1335 -0
  145. package/src/audio/keys/piano.ts +72 -8
  146. package/src/audio/keys/sympathetic.ts +127 -0
  147. package/src/audio/kits.ts +135 -6
  148. package/src/audio/live-worker.ts +3 -1
  149. package/src/audio/live.ts +247 -26
  150. package/src/audio/native.ts +615 -0
  151. package/src/audio/preview.ts +45 -3
  152. package/src/audio/render-worker.ts +2 -0
  153. package/src/audio/renderer.ts +2 -0
  154. package/src/audio/resample.ts +286 -0
  155. package/src/audio/resonators.ts +10 -2
  156. package/src/audio/sampler.ts +252 -24
  157. package/src/audio/samples.ts +46 -3
  158. package/src/audio/sing/analysis.ts +193 -0
  159. package/src/audio/sing/engine.ts +949 -0
  160. package/src/audio/strings/body.ts +29 -16
  161. package/src/audio/strings/bow.ts +699 -0
  162. package/src/audio/strings/engine.ts +274 -22
  163. package/src/audio/strings/measure.test-helpers.ts +2 -0
  164. package/src/audio/synth/oscillators.ts +31 -21
  165. package/src/audio/synth/voice.ts +34 -1
  166. package/src/audio/vocoder/bank.ts +314 -0
  167. package/src/audio/vocoder/carrier.ts +165 -0
  168. package/src/audio/vocoder/control.ts +68 -0
  169. package/src/audio/vocoder/detect.ts +50 -0
  170. package/src/audio/vocoder/index.ts +304 -0
  171. package/src/audio/vocoder/talkbox.ts +143 -0
  172. package/src/audio/wav.ts +608 -85
  173. package/src/audio/winds/engine.ts +306 -0
  174. package/src/audio/winds/filters.ts +153 -0
  175. package/src/audio/winds/pitch.ts +104 -0
  176. package/src/audio/winds/trim.ts +104 -0
  177. package/src/audio/winds/trims.ts +917 -0
  178. package/src/audio/winds/trims1.ts +297 -0
  179. package/src/audio/winds/voice.ts +492 -0
  180. package/src/auth/cli.ts +38 -36
  181. package/src/auth/credentials.ts +30 -1
  182. package/src/auth/login.ts +15 -9
  183. package/src/auth/tui.ts +19 -10
  184. package/src/commands/arrange.ts +44 -29
  185. package/src/commands/autotune.ts +421 -0
  186. package/src/commands/calibration.ts +74 -0
  187. package/src/commands/clips.ts +887 -0
  188. package/src/commands/drums.ts +3 -2
  189. package/src/commands/edit.ts +11 -4
  190. package/src/commands/expression.ts +104 -23
  191. package/src/commands/formant.ts +221 -0
  192. package/src/commands/fx.ts +105 -33
  193. package/src/commands/grammar.ts +558 -0
  194. package/src/commands/granular.ts +7 -2
  195. package/src/commands/help.ts +652 -339
  196. package/src/commands/history.ts +18 -0
  197. package/src/commands/keys.ts +293 -17
  198. package/src/commands/modal.ts +67 -4
  199. package/src/commands/music.ts +1 -1
  200. package/src/commands/nearest.ts +53 -0
  201. package/src/commands/pack.ts +9 -2
  202. package/src/commands/param-range.ts +56 -0
  203. package/src/commands/parses.ts +130 -0
  204. package/src/commands/progression.ts +170 -0
  205. package/src/commands/resample.ts +281 -0
  206. package/src/commands/rhythm.ts +3 -0
  207. package/src/commands/rig.ts +8 -25
  208. package/src/commands/sample.ts +49 -2
  209. package/src/commands/shift.ts +119 -0
  210. package/src/commands/sing.ts +478 -0
  211. package/src/commands/string.ts +28 -2
  212. package/src/commands/strum.ts +485 -0
  213. package/src/commands/style.ts +415 -0
  214. package/src/commands/time.ts +6 -3
  215. package/src/commands/tuning.ts +3 -3
  216. package/src/commands/vocal-pitch.ts +616 -0
  217. package/src/commands/vocal.ts +147 -0
  218. package/src/commands/vocoder.ts +627 -0
  219. package/src/commands/wind.ts +244 -0
  220. package/src/fs/durable.ts +50 -0
  221. package/src/lang/glossary.ts +493 -0
  222. package/src/launch-args.ts +163 -0
  223. package/src/main.ts +1521 -255
  224. package/src/media/cli.ts +20 -3
  225. package/src/media/import.ts +3 -1
  226. package/src/project/check.ts +26 -3
  227. package/src/project/clip-pins.ts +72 -0
  228. package/src/project/init.ts +23 -8
  229. package/src/project/sync.ts +418 -86
  230. package/src/render.ts +36 -1
  231. package/src/session/daemon.ts +3 -0
  232. package/src/session/meta.ts +14 -0
  233. package/src/session/origin.ts +154 -0
  234. package/src/session/port.ts +22 -4
  235. package/src/session/presence.ts +50 -7
  236. package/src/session/protocol.ts +5 -1
  237. package/src/session/rebase.ts +18 -5
  238. package/src/session/receipt.ts +258 -0
  239. package/src/session/store.ts +65 -43
  240. package/src/tui/arrange-menu.ts +65 -31
  241. package/src/tui/audition.ts +1 -1
  242. package/src/tui/euclid.ts +18 -13
  243. package/src/tui/fader.ts +228 -41
  244. package/src/tui/granular-menu.ts +2 -4
  245. package/src/tui/menu-clips.ts +297 -0
  246. package/src/tui/menu-time.ts +20 -13
  247. package/src/tui/menu-voice.ts +405 -0
  248. package/src/tui/menu.ts +1377 -193
  249. package/src/tui/modal-menu.ts +38 -11
  250. package/src/tui/performance-menu.ts +46 -1
  251. package/src/tui/play-chords.ts +108 -5
  252. package/src/tui/play-mode.ts +16 -1
  253. package/src/tui/play-session.ts +81 -16
  254. package/src/tui/sing-menu.ts +278 -0
  255. package/src/tui/style-menu.ts +104 -0
  256. package/src/tui/vocoder-menu.ts +244 -0
  257. package/src/tui/wind-menu.ts +144 -0
  258. package/src/version.ts +8 -0
  259. package/src/web/fetch.ts +115 -29
  260. package/tui/activity.ts +180 -9
  261. package/tui/app.ts +274 -40
  262. package/tui/clip-row.ts +132 -0
  263. package/tui/delight.ts +144 -0
  264. package/tui/drawer.ts +70 -22
  265. package/tui/frame-gate.ts +76 -0
  266. package/tui/grammar.ts +112 -63
  267. package/tui/guide.ts +42 -4
  268. package/tui/highway.ts +269 -25
  269. package/tui/hints.ts +192 -0
  270. package/tui/input.ts +60 -9
  271. package/tui/keys.ts +1 -1
  272. package/tui/play-strip.ts +68 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +144 -14
  275. package/tui/theme.ts +27 -0
@@ -0,0 +1,493 @@
1
+ /**
2
+ * One vocabulary for dawg: the ten topic ids every door accepts (`/help`,
3
+ * `/guide`, `/menu`), the canonical word for each concept with the words it
4
+ * retired, and the British spellings dawg writes the American way.
5
+ *
6
+ * Retired words keep working wherever they were typed (aliases); they appear
7
+ * in no user-facing text except alias notes. The consistency lint imports
8
+ * TERMS and SPELLING; help, guides and docs are checked against them.
9
+ *
10
+ * Pure data and string helpers, no imports: the site can load it too.
11
+ */
12
+
13
+ /** The ten topics, in the order every door lists them. */
14
+ export const TOPICS = [
15
+ "sound",
16
+ "voice",
17
+ "effects",
18
+ "rhythm",
19
+ "chords",
20
+ "mix",
21
+ "arrange",
22
+ "project",
23
+ "keys",
24
+ "agent",
25
+ ] as const;
26
+
27
+ export type TopicId = (typeof TOPICS)[number];
28
+
29
+ /** One line per topic, for `/help` and the guide index. */
30
+ export const TOPIC_SUMMARY: Readonly<Record<TopicId, string>> = {
31
+ sound: "instruments, presets, performance, samples",
32
+ voice: "sing, lyrics, clips, pitch, autotune, formant, vocoder",
33
+ effects: "filter, delay, reverb, guitar rig, shoegaze, more",
34
+ rhythm: "drums, euclid, grid, grooves, kits, sample packs",
35
+ chords: "key, tuning, progressions, chord play",
36
+ mix: "volume, pan, mute, solo, automation, master",
37
+ arrange: "notes, tracks, sections, form, styles",
38
+ project: "transport, tempo, meter, loop, export, undo, sessions",
39
+ keys: "the keys of every screen",
40
+ agent: "model, model key, show-me",
41
+ };
42
+
43
+ /**
44
+ * Old ids and everyday words that open a topic. The left side is what a
45
+ * person may type after `/help`, `/guide` or `/menu`; the right is the
46
+ * topic it opens. A guide or menu node with the same id still wins in its
47
+ * own door (`/guide performance` opens the performance guide).
48
+ */
49
+ export const TOPIC_ALIASES: Readonly<Record<string, TopicId>> = {
50
+ // retired topic ids
51
+ music: "arrange",
52
+ session: "project",
53
+ window: "keys",
54
+ // sound
55
+ sounds: "sound",
56
+ instrument: "sound",
57
+ instruments: "sound",
58
+ performance: "sound",
59
+ expression: "sound",
60
+ samples: "sound",
61
+ // voice
62
+ vocal: "voice",
63
+ vocals: "voice",
64
+ sing: "voice",
65
+ clips: "voice",
66
+ lyrics: "voice",
67
+ autotune: "voice",
68
+ formant: "voice",
69
+ vocoder: "voice",
70
+ // effects
71
+ fx: "effects",
72
+ effect: "effects",
73
+ rig: "effects",
74
+ // rhythm
75
+ drums: "rhythm",
76
+ grooves: "rhythm",
77
+ patterns: "rhythm",
78
+ kits: "rhythm",
79
+ packs: "rhythm",
80
+ // chords and key
81
+ key: "chords",
82
+ scale: "chords",
83
+ scales: "chords",
84
+ tuning: "chords",
85
+ progression: "chords",
86
+ // mix
87
+ master: "mix",
88
+ automation: "mix",
89
+ mixer: "mix",
90
+ // arrange
91
+ arrangement: "arrange",
92
+ sections: "arrange",
93
+ form: "arrange",
94
+ style: "arrange",
95
+ styles: "arrange",
96
+ genre: "arrange",
97
+ tracks: "arrange",
98
+ notes: "arrange",
99
+ // project
100
+ transport: "project",
101
+ tempo: "project",
102
+ meter: "project",
103
+ time: "project",
104
+ export: "project",
105
+ files: "project",
106
+ undo: "project",
107
+ // keys
108
+ keyboard: "keys",
109
+ shortcuts: "keys",
110
+ mouse: "keys",
111
+ // agent
112
+ model: "agent",
113
+ models: "agent",
114
+ showme: "agent",
115
+ "show-me": "agent",
116
+ sessions: "project",
117
+ login: "agent",
118
+ ai: "agent",
119
+ };
120
+
121
+ /** A topic id for `word` (an id or an alias), or undefined. */
122
+ export function resolveTopic(word: string | undefined): TopicId | undefined {
123
+ const name = (word ?? "").trim().toLowerCase().replace(/^\//, "");
124
+ if ((TOPICS as readonly string[]).includes(name)) return name as TopicId;
125
+ return TOPIC_ALIASES[name];
126
+ }
127
+
128
+ /** Levenshtein distance, small inputs only. */
129
+ function distance(a: string, b: string): number {
130
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
131
+ for (let i = 1; i <= a.length; i++) {
132
+ let previous = row[0]!;
133
+ row[0] = i;
134
+ for (let j = 1; j <= b.length; j++) {
135
+ const current = row[j]!;
136
+ row[j] = Math.min(
137
+ row[j]! + 1,
138
+ row[j - 1]! + 1,
139
+ previous + (a[i - 1] === b[j - 1] ? 0 : 1),
140
+ );
141
+ previous = current;
142
+ }
143
+ }
144
+ return row[b.length]!;
145
+ }
146
+
147
+ /**
148
+ * The closest topic id to `word`, comparing against ids and aliases (an
149
+ * alias answers with its topic). Ties go to the earlier topic.
150
+ */
151
+ export function nearestTopic(
152
+ word: string,
153
+ extra: readonly string[] = [],
154
+ ): string | undefined {
155
+ const name = word.trim().toLowerCase().replace(/^\//, "");
156
+ // The same reach as nearestCommand: one edit for a short word, two else.
157
+ const limit = name.length <= 4 ? 1 : 2;
158
+ let best: { id: string; score: number } | undefined;
159
+ const candidates: [string, string][] = [
160
+ ...TOPICS.map((id): [string, string] => [id, id]),
161
+ ...extra.map((id): [string, string] => [id, id]),
162
+ ...Object.entries(TOPIC_ALIASES),
163
+ ];
164
+ for (const [spelling, id] of candidates) {
165
+ const score = distance(name, spelling);
166
+ if (score <= limit && (!best || score < best.score)) best = { id, score };
167
+ }
168
+ return best?.id;
169
+ }
170
+
171
+ /**
172
+ * `no topic X · did you mean Y · /help` for any door; without a near
173
+ * topic, `no topic X · /help`.
174
+ */
175
+ export function topicMiss(word: string, extra: readonly string[] = []): string {
176
+ const name = word.trim().replace(/^\//, "");
177
+ const near = nearestTopic(name, extra);
178
+ return near
179
+ ? `no topic ${name} · did you mean ${near} · /help`
180
+ : `no topic ${name} · /help`;
181
+ }
182
+
183
+ /**
184
+ * Guitar presets that `track <name>` still creates (an alias); `rig
185
+ * <name>` is the canonical word for the amp and pedals.
186
+ */
187
+ export const GUITAR_TRACK_PRESETS: readonly string[] = [
188
+ "jangle",
189
+ "punk",
190
+ "funk",
191
+ "ragged",
192
+ "gtr-lead",
193
+ "gtr-metal",
194
+ "bachata",
195
+ ];
196
+
197
+ /** One concept: its one word, the words it retired, and typed aliases. */
198
+ export type Term = Readonly<{
199
+ term: string;
200
+ meaning: string;
201
+ /**
202
+ * Retired words, as phrases matched whole-word in prose. Only phrases
203
+ * that are unambiguous in dawg's text are listed (not "part" or "take").
204
+ */
205
+ losers: readonly string[];
206
+ /** Typed spellings that still work and are documented only as aliases. */
207
+ aliases: readonly string[];
208
+ /**
209
+ * Longer phrases that contain a loser but are canonical (`ctrl-p play
210
+ * mode` holds the loser `ctrl-p play`); they are blanked before matching.
211
+ */
212
+ allowed?: readonly string[];
213
+ }>;
214
+
215
+ export const TERMS: readonly Term[] = [
216
+ { term: "track", meaning: "one instrument line", losers: [], aliases: [] },
217
+ {
218
+ term: "note",
219
+ meaning: "a pitched or drum event",
220
+ losers: [],
221
+ aliases: [],
222
+ },
223
+ {
224
+ term: "drum",
225
+ meaning: "one row of a drum track (kick, snare, hat)",
226
+ losers: ["drum voice", "drum voices"],
227
+ aliases: [],
228
+ },
229
+ {
230
+ term: "sample",
231
+ meaning: "an audio file a sampler plays, or a pack item",
232
+ losers: ["sample voice", "use a sound"],
233
+ aliases: [],
234
+ },
235
+ {
236
+ term: "instrument",
237
+ meaning: "the engine a track plays",
238
+ losers: ["browse sounds"],
239
+ aliases: [],
240
+ },
241
+ {
242
+ term: "preset",
243
+ meaning: "a saved setting of an instrument or effect",
244
+ losers: [],
245
+ aliases: ["presets"],
246
+ },
247
+ {
248
+ term: "sound",
249
+ meaning: "the menu section that shapes the focused track",
250
+ losers: [],
251
+ aliases: [],
252
+ },
253
+ {
254
+ term: "voice",
255
+ meaning:
256
+ "sung or spoken audio: sing, lyrics, clips, pitch, autotune, formant, vocoder",
257
+ losers: [],
258
+ aliases: [],
259
+ },
260
+ {
261
+ term: "register",
262
+ meaning: "an organ stop or combo selection",
263
+ losers: [],
264
+ aliases: [],
265
+ },
266
+ {
267
+ term: "polyphony",
268
+ meaning: "simultaneous grains or notes",
269
+ losers: [],
270
+ aliases: ["voice V"],
271
+ },
272
+ {
273
+ term: "effect",
274
+ meaning: "a processor on a track; fx is only the verb",
275
+ losers: ["FX"],
276
+ aliases: ["fx"],
277
+ },
278
+ {
279
+ term: "lane",
280
+ meaning: "an automation lane only",
281
+ losers: [],
282
+ aliases: [],
283
+ },
284
+ {
285
+ term: "clip",
286
+ meaning: "an audio region on a track",
287
+ losers: [],
288
+ aliases: [],
289
+ },
290
+ {
291
+ term: "section",
292
+ meaning: "a named bar range",
293
+ losers: [],
294
+ aliases: [],
295
+ },
296
+ { term: "form", meaning: "the order of sections", losers: [], aliases: [] },
297
+ {
298
+ term: "style",
299
+ meaning: "a generative song recipe",
300
+ losers: ["genre", "genres"],
301
+ aliases: ["genre"],
302
+ },
303
+ {
304
+ term: "groove",
305
+ meaning: "a ready-made drum pattern",
306
+ losers: ["drum pattern", "drum patterns"],
307
+ aliases: ["pattern"],
308
+ },
309
+ {
310
+ term: "idiom",
311
+ meaning: "the progression flavor chords suggest from",
312
+ losers: [],
313
+ aliases: ["chords style"],
314
+ },
315
+ {
316
+ term: "kit",
317
+ meaning: "a set of drum samples",
318
+ losers: ["drum kit", "drum kits"],
319
+ aliases: [],
320
+ },
321
+ {
322
+ term: "key",
323
+ meaning: "tonic and scale (key A dorian)",
324
+ losers: [],
325
+ aliases: ["scale"],
326
+ },
327
+ {
328
+ term: "tuning",
329
+ meaning: "how pitches are tuned (edo, scl, ratios)",
330
+ losers: ["temperament"],
331
+ aliases: [],
332
+ },
333
+ {
334
+ term: "tempo",
335
+ meaning: "speed, displayed as BPM",
336
+ losers: [],
337
+ aliases: ["bpm"],
338
+ },
339
+ { term: "loop", meaning: "the playback region", losers: [], aliases: [] },
340
+ {
341
+ term: "remove",
342
+ meaning: "delete something",
343
+ losers: [],
344
+ aliases: ["rm", "delete"],
345
+ },
346
+ {
347
+ term: "list",
348
+ meaning: "show choices",
349
+ losers: [],
350
+ aliases: ["presets", "ls"],
351
+ },
352
+ {
353
+ term: "agent",
354
+ meaning: "the optional model that acts in dawg",
355
+ losers: ["AI assistant", "assistant"],
356
+ aliases: [],
357
+ },
358
+ {
359
+ term: "model",
360
+ meaning: "which LLM the agent uses",
361
+ losers: [],
362
+ aliases: ["models"],
363
+ },
364
+ {
365
+ term: "agent key",
366
+ meaning: "the optional provider key, set with model key",
367
+ losers: ["sign in", "sign-in", "signed in", "signs in", "login"],
368
+ aliases: ["login"],
369
+ },
370
+ {
371
+ term: "play",
372
+ meaning: "the transport (Space)",
373
+ losers: [],
374
+ aliases: [],
375
+ },
376
+ {
377
+ term: "play mode",
378
+ meaning: "ctrl-p turns the computer keyboard into an instrument",
379
+ losers: ["keys mode", "keyboard mode", "ctrl-p play", "Ctrl-P play"],
380
+ aliases: [],
381
+ allowed: ["ctrl-p play mode", "Ctrl-P play mode"],
382
+ },
383
+ {
384
+ term: "rig",
385
+ meaning: "a guitar amp and pedal preset (rig jangle)",
386
+ losers: GUITAR_TRACK_PRESETS.map((name) => `track ${name}`),
387
+ aliases: GUITAR_TRACK_PRESETS.map((name) => `track ${name}`),
388
+ },
389
+ {
390
+ term: "sawtooth",
391
+ meaning: "the saw wave, listed once as sawtooth",
392
+ losers: ["saw square", "pluck bass saw"],
393
+ aliases: ["saw"],
394
+ },
395
+ {
396
+ term: "now / next",
397
+ meaning: "prompt modes: send now, or run after the current request",
398
+ losers: ["STEER", "QUEUE"],
399
+ aliases: [],
400
+ },
401
+ ];
402
+
403
+ /** British spellings dawg writes the American way (lowercase keys). */
404
+ export const SPELLING: Readonly<Record<string, string>> = {
405
+ centre: "center",
406
+ centred: "centered",
407
+ centres: "centers",
408
+ modelled: "modeled",
409
+ modelling: "modeling",
410
+ behaviour: "behavior",
411
+ behaviours: "behaviors",
412
+ cancelled: "canceled",
413
+ cancelling: "canceling",
414
+ colour: "color",
415
+ colours: "colors",
416
+ coloured: "colored",
417
+ analyse: "analyze",
418
+ analysed: "analyzed",
419
+ analyses: "analyzes",
420
+ labelled: "labeled",
421
+ labelling: "labeling",
422
+ towards: "toward",
423
+ normalise: "normalize",
424
+ normalised: "normalized",
425
+ normalises: "normalizes",
426
+ organise: "organize",
427
+ organised: "organized",
428
+ recognise: "recognize",
429
+ recognised: "recognized",
430
+ realise: "realize",
431
+ optimise: "optimize",
432
+ optimised: "optimized",
433
+ favourite: "favorite",
434
+ favourites: "favorites",
435
+ grey: "gray",
436
+ travelling: "traveling",
437
+ levelled: "leveled",
438
+ signalled: "signaled",
439
+ synthesise: "synthesize",
440
+ synthesised: "synthesized",
441
+ quantise: "quantize",
442
+ quantised: "quantized",
443
+ randomise: "randomize",
444
+ randomised: "randomized",
445
+ customise: "customize",
446
+ initialise: "initialize",
447
+ licence: "license",
448
+ dialogue: "dialog",
449
+ catalogue: "catalog",
450
+ };
451
+
452
+ /** Typed spellings kept for old projects and muscle memory (aliases). */
453
+ export const SPELLING_ALIASES: readonly string[] = ["phasercentre"];
454
+
455
+ /** Whether a line is an alias note, where retired words may appear. */
456
+ export function isAliasNote(line: string): boolean {
457
+ return /\balias(es)?\b|\bretired\b|\bold spelling\b/i.test(line);
458
+ }
459
+
460
+ /**
461
+ * Retired words and British spellings in `text`, one finding per hit:
462
+ * `losers` match whole-word (case-sensitive for all-caps losers such as
463
+ * FX), spellings case-insensitively. Alias-note lines are skipped.
464
+ */
465
+ export function lintText(text: string): string[] {
466
+ const hits: string[] = [];
467
+ for (const line of text.split("\n")) {
468
+ if (isAliasNote(line)) continue;
469
+ for (const { term, losers, allowed = [] } of TERMS) {
470
+ let text = line;
471
+ for (const phrase of allowed) text = text.split(phrase).join(" ");
472
+ for (const loser of losers) {
473
+ // Case-sensitive, so `FX` is not `fx`; a capital first letter
474
+ // (sentence start) still matches a lowercase loser.
475
+ const escaped = loser.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
476
+ const first = escaped[0]!;
477
+ const head =
478
+ first.toUpperCase() === first
479
+ ? first
480
+ : `[${first}${first.toUpperCase()}]`;
481
+ const pattern = new RegExp(
482
+ `(?<![\\w/.\`-])${head}${escaped.slice(1)}(?![\\w\`-])`,
483
+ );
484
+ if (pattern.test(text)) hits.push(`${loser} → ${term}: ${line.trim()}`);
485
+ }
486
+ }
487
+ for (const word of line.match(/[A-Za-z]+/g) ?? []) {
488
+ const american = SPELLING[word.toLowerCase()];
489
+ if (american) hits.push(`${word} → ${american}: ${line.trim()}`);
490
+ }
491
+ }
492
+ return hits;
493
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The `dawg [options]` launch grammar and the small argv contract the plain
3
+ * subcommands share. Pure: no I/O, so every mistake is rejected before
4
+ * `.dawg/` (or any project file) could be written.
5
+ */
6
+ export { parseSimpleArgv, type SimpleArgv } from "./argv.ts";
7
+ import { parseThemeName, THEME_NAMES, type ThemeName } from "../tui/theme.ts";
8
+
9
+ /** Subcommands that parse their own argv; the launch grammar skips them. */
10
+ export const SUBCOMMANDS: readonly string[] = [
11
+ "login",
12
+ "logout",
13
+ "auth",
14
+ "model",
15
+ "sessions",
16
+ "render",
17
+ "init",
18
+ "check",
19
+ "media",
20
+ ];
21
+ export const VALUE_FLAGS = [
22
+ "--session",
23
+ "--track",
24
+ "--import",
25
+ "--export",
26
+ "--theme",
27
+ ] as const;
28
+ export type ValueFlag = (typeof VALUE_FLAGS)[number];
29
+ export const BOOLEAN_FLAGS: readonly string[] = [
30
+ "--new",
31
+ "--demo",
32
+ "--help",
33
+ "-h",
34
+ "--version",
35
+ "-v",
36
+ "--reduce-motion",
37
+ "--no-mouse",
38
+ ];
39
+
40
+ /** A `--track` value resolved to the id `/track` would use. */
41
+ export type TrackArg = { id: string; name: string };
42
+
43
+ export type LaunchArgs = {
44
+ subcommand: string | undefined;
45
+ flags: ReadonlySet<string>;
46
+ session: string | undefined;
47
+ track: TrackArg | undefined;
48
+ importPath: string | undefined;
49
+ exportPath: string | undefined;
50
+ theme: ThemeName | undefined;
51
+ };
52
+
53
+ export type LaunchParse =
54
+ { ok: true; args: LaunchArgs } | { ok: false; problem: string };
55
+
56
+ const TRACK_NAME = /^[a-z0-9._ -]{1,64}$/i;
57
+
58
+ /**
59
+ * `--track Bass Guitar` → id `bass-guitar`, name `bass guitar`: the rule
60
+ * `/track` applies (letters, digits, `.`, `_`, `-` and spaces, at most 64),
61
+ * lowercased with runs of spaces collapsed. Undefined when invalid.
62
+ */
63
+ export function normalizeTrackArg(value: string): TrackArg | undefined {
64
+ const name = value.trim().replace(/\s+/g, " ").toLowerCase();
65
+ if (!TRACK_NAME.test(name)) return undefined;
66
+ // `.` and `..` are path segments, never track ids.
67
+ if (/^\.+$/.test(name)) return undefined;
68
+ return { id: name.replace(/ /g, "-"), name };
69
+ }
70
+
71
+ /** The existing track `arg` names (by id or name), else `arg.id`. */
72
+ export function resolveTrackArg(
73
+ tracks: readonly { id: string; name?: string | undefined }[],
74
+ arg: TrackArg,
75
+ ): string {
76
+ const found =
77
+ tracks.find((track) => track.id === arg.id) ??
78
+ tracks.find((track) => track.id.toLowerCase() === arg.id) ??
79
+ tracks.find((track) => (track.name ?? track.id).toLowerCase() === arg.name);
80
+ return found?.id ?? arg.id;
81
+ }
82
+
83
+ /**
84
+ * Parses `dawg [options]`. A subcommand as the first word ends parsing (the
85
+ * subcommand owns the rest). Value flags need a value that does not start
86
+ * with `-`; each may be given once; `--track` and `--theme` are validated
87
+ * here so a bad value is one line and exit 2, never a stack trace.
88
+ */
89
+ export function parseLaunchArgs(argv: readonly string[]): LaunchParse {
90
+ const flags = new Set<string>();
91
+ const values = new Map<ValueFlag, string>();
92
+ const first = argv[0];
93
+ if (first !== undefined && !first.startsWith("-")) {
94
+ if (SUBCOMMANDS.includes(first))
95
+ return { ok: true, args: emptyArgs(first, flags) };
96
+ return { ok: false, problem: `unknown command · ${clip(first)}` };
97
+ }
98
+ for (let index = 0; index < argv.length; index += 1) {
99
+ const arg = argv[index]!;
100
+ if ((VALUE_FLAGS as readonly string[]).includes(arg)) {
101
+ const flag = arg as ValueFlag;
102
+ const value = argv[index + 1];
103
+ if (value === undefined || value.length === 0 || value.startsWith("-"))
104
+ return { ok: false, problem: `${flag} needs a value` };
105
+ if (values.has(flag))
106
+ return { ok: false, problem: `${flag} given twice` };
107
+ values.set(flag, value);
108
+ index += 1;
109
+ continue;
110
+ }
111
+ if (BOOLEAN_FLAGS.includes(arg)) {
112
+ flags.add(arg);
113
+ continue;
114
+ }
115
+ if (arg.startsWith("-"))
116
+ return { ok: false, problem: `unknown option · ${clip(arg)}` };
117
+ return { ok: false, problem: `unknown command · ${clip(arg)}` };
118
+ }
119
+ let track: TrackArg | undefined;
120
+ const trackValue = values.get("--track");
121
+ if (trackValue !== undefined) {
122
+ track = normalizeTrackArg(trackValue);
123
+ if (!track) return { ok: false, problem: "invalid track name" };
124
+ }
125
+ let theme: ThemeName | undefined;
126
+ const themeValue = values.get("--theme");
127
+ if (themeValue !== undefined) {
128
+ theme = parseThemeName(themeValue);
129
+ if (!theme)
130
+ return {
131
+ ok: false,
132
+ problem: `unknown theme ${clip(themeValue)} · ${THEME_NAMES.join(" | ")}`,
133
+ };
134
+ }
135
+ return {
136
+ ok: true,
137
+ args: {
138
+ subcommand: undefined,
139
+ flags,
140
+ session: values.get("--session"),
141
+ track,
142
+ importPath: values.get("--import"),
143
+ exportPath: values.get("--export"),
144
+ theme,
145
+ },
146
+ };
147
+ }
148
+
149
+ function emptyArgs(subcommand: string, flags: Set<string>): LaunchArgs {
150
+ return {
151
+ subcommand,
152
+ flags,
153
+ session: undefined,
154
+ track: undefined,
155
+ importPath: undefined,
156
+ exportPath: undefined,
157
+ theme: undefined,
158
+ };
159
+ }
160
+
161
+ function clip(value: string): string {
162
+ return value.length > 40 ? `${value.slice(0, 40)}…` : value;
163
+ }