@hraness/dawg 0.6.1 → 0.8.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 (277) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/DAWG.md +454 -140
  3. package/README.md +30 -26
  4. package/core/autotune.ts +1119 -0
  5. package/core/chords.ts +271 -23
  6. package/core/clips.ts +499 -0
  7. package/core/diff.ts +184 -104
  8. package/core/expression.ts +15 -0
  9. package/core/fx.ts +99 -29
  10. package/core/ids.ts +52 -0
  11. package/core/instruments.ts +19 -0
  12. package/core/keys.ts +3 -3
  13. package/core/loop.ts +8 -0
  14. package/core/lyrics.ts +297 -0
  15. package/core/master.ts +3 -3
  16. package/core/range.ts +581 -0
  17. package/core/resonators.ts +16 -2
  18. package/core/routing.ts +165 -0
  19. package/core/score.ts +955 -14
  20. package/core/sdk/eval-child.ts +7 -2
  21. package/core/sdk/eval.ts +35 -6
  22. package/core/sdk/print.ts +287 -5
  23. package/core/sdk/sync-lyrics.ts +49 -0
  24. package/core/sdk/v1.ts +2132 -46
  25. package/core/sections.ts +480 -35
  26. package/core/sing.ts +815 -0
  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/guides/agent.md +29 -0
  50. package/guides/arrange.md +31 -0
  51. package/guides/audio.md +31 -0
  52. package/guides/audition.md +20 -14
  53. package/guides/automation.md +12 -7
  54. package/guides/chords.md +15 -15
  55. package/guides/effects.md +17 -16
  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 +15 -7
  62. package/guides/music.md +23 -8
  63. package/guides/notes.md +15 -9
  64. package/guides/panes.md +32 -0
  65. package/guides/performance.md +15 -13
  66. package/guides/play.md +21 -13
  67. package/guides/project.md +26 -8
  68. package/guides/providers.md +20 -14
  69. package/guides/resample.md +15 -9
  70. package/guides/rhythm.md +18 -13
  71. package/guides/sessions.md +17 -7
  72. package/guides/show-me.md +31 -0
  73. package/guides/sound.md +25 -9
  74. package/guides/sounds.md +15 -13
  75. package/guides/styles.md +31 -0
  76. package/guides/tape.md +32 -0
  77. package/guides/tempo.md +15 -10
  78. package/guides/tracks.md +15 -10
  79. package/guides/tuning.md +32 -0
  80. package/guides/voice.md +31 -0
  81. package/guides/web-search.md +18 -8
  82. package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
  83. package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
  84. package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
  85. package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
  86. package/native/prebuilt/manifest.json +21 -0
  87. package/package.json +6 -2
  88. package/src/agent/agent.ts +130 -18
  89. package/src/agent/calibration-tools.ts +53 -0
  90. package/src/agent/clip-tools.ts +453 -0
  91. package/src/agent/command-agent.ts +378 -0
  92. package/src/agent/drum-tools.ts +2 -2
  93. package/src/agent/expression-tools.ts +1 -1
  94. package/src/agent/gateway.ts +246 -60
  95. package/src/agent/models.ts +53 -12
  96. package/src/agent/ops.ts +27 -1
  97. package/src/agent/pack-tools.ts +1 -1
  98. package/src/agent/planner.ts +25 -0
  99. package/src/agent/portable-schema.ts +80 -0
  100. package/src/agent/preview-tool.ts +4 -1
  101. package/src/agent/provider.ts +22 -8
  102. package/src/agent/range-tools.ts +216 -0
  103. package/src/agent/rhythm-tools.ts +1 -1
  104. package/src/agent/section-tools.ts +1 -1
  105. package/src/agent/show-me.ts +497 -0
  106. package/src/agent/steer.ts +15 -0
  107. package/src/agent/style-tools.ts +217 -0
  108. package/src/agent/tool-error.ts +12 -0
  109. package/src/agent/tools.ts +110 -23
  110. package/src/agent/usage.ts +2 -2
  111. package/src/agent/voice-tools.ts +925 -0
  112. package/src/agent/xcb-agent.ts +13 -10
  113. package/src/argv.ts +38 -0
  114. package/src/audio/analysis.ts +253 -0
  115. package/src/audio/arrange.ts +49 -5
  116. package/src/audio/audio-command.ts +218 -0
  117. package/src/audio/autotune-engine.ts +101 -0
  118. package/src/audio/autotune.ts +640 -0
  119. package/src/audio/clips.ts +240 -0
  120. package/src/audio/devices.ts +264 -0
  121. package/src/audio/doctor.ts +157 -0
  122. package/src/audio/dsp/bandbank.ts +138 -0
  123. package/src/audio/dsp/envelope.ts +10 -0
  124. package/src/audio/dsp/follow.ts +120 -0
  125. package/src/audio/dsp/formant.ts +427 -0
  126. package/src/audio/dsp/glottal.ts +243 -0
  127. package/src/audio/dsp/interp.ts +7 -2
  128. package/src/audio/dsp/lpc.ts +50 -0
  129. package/src/audio/dsp/periodicity.ts +59 -0
  130. package/src/audio/dsp/pitch.ts +995 -0
  131. package/src/audio/dsp/psola.ts +199 -0
  132. package/src/audio/effects/chain.ts +3 -1
  133. package/src/audio/effects/common.ts +43 -0
  134. package/src/audio/effects/convolution.ts +7 -4
  135. package/src/audio/effects/filter.ts +48 -69
  136. package/src/audio/effects/formant.ts +263 -0
  137. package/src/audio/engine.ts +360 -40
  138. package/src/audio/fit.ts +35 -3
  139. package/src/audio/instrument-check.ts +59 -43
  140. package/src/audio/instruments.ts +4 -0
  141. package/src/audio/keys/calibration.ts +56 -0
  142. package/src/audio/keys/electric.ts +8 -1
  143. package/src/audio/keys/engine.ts +13 -1
  144. package/src/audio/keys/piano.ts +22 -2
  145. package/src/audio/kits.ts +135 -6
  146. package/src/audio/live.ts +114 -20
  147. package/src/audio/native.ts +615 -0
  148. package/src/audio/preview.ts +30 -2
  149. package/src/audio/render-worker.ts +2 -0
  150. package/src/audio/renderer.ts +2 -0
  151. package/src/audio/resample.ts +2 -1
  152. package/src/audio/sampler.ts +85 -4
  153. package/src/audio/samples.ts +20 -2
  154. package/src/audio/sing/analysis.ts +193 -0
  155. package/src/audio/sing/engine.ts +949 -0
  156. package/src/audio/strings/bow.ts +48 -5
  157. package/src/audio/strings/engine.ts +8 -2
  158. package/src/audio/synth/oscillators.ts +31 -21
  159. package/src/audio/synth/voice.ts +34 -1
  160. package/src/audio/vocoder/bank.ts +314 -0
  161. package/src/audio/vocoder/carrier.ts +165 -0
  162. package/src/audio/vocoder/control.ts +68 -0
  163. package/src/audio/vocoder/detect.ts +50 -0
  164. package/src/audio/vocoder/index.ts +304 -0
  165. package/src/audio/vocoder/talkbox.ts +143 -0
  166. package/src/audio/wav.ts +500 -77
  167. package/src/audio/winds/engine.ts +5 -1
  168. package/src/audio/winds/trim.ts +28 -4
  169. package/src/audio/winds/trims1.ts +297 -0
  170. package/src/audio/winds/voice.ts +15 -2
  171. package/src/auth/cli.ts +38 -36
  172. package/src/auth/credentials.ts +30 -1
  173. package/src/auth/login.ts +15 -9
  174. package/src/auth/tui.ts +19 -10
  175. package/src/commands/arrange.ts +82 -32
  176. package/src/commands/autotune.ts +421 -0
  177. package/src/commands/calibration.ts +74 -0
  178. package/src/commands/clips.ts +887 -0
  179. package/src/commands/drums.ts +3 -2
  180. package/src/commands/edit.ts +11 -4
  181. package/src/commands/expression.ts +1 -1
  182. package/src/commands/formant.ts +221 -0
  183. package/src/commands/fx.ts +101 -32
  184. package/src/commands/grammar.ts +560 -0
  185. package/src/commands/help.ts +774 -366
  186. package/src/commands/history.ts +139 -10
  187. package/src/commands/keys.ts +25 -8
  188. package/src/commands/modal.ts +1 -1
  189. package/src/commands/music.ts +1 -1
  190. package/src/commands/nearest.ts +53 -0
  191. package/src/commands/pack.ts +9 -2
  192. package/src/commands/param-range.ts +56 -0
  193. package/src/commands/parses.ts +135 -0
  194. package/src/commands/progression.ts +170 -0
  195. package/src/commands/range.ts +763 -0
  196. package/src/commands/resample.ts +13 -10
  197. package/src/commands/rhythm.ts +3 -0
  198. package/src/commands/rig.ts +3 -24
  199. package/src/commands/sample.ts +11 -1
  200. package/src/commands/sing.ts +474 -0
  201. package/src/commands/strum.ts +13 -1
  202. package/src/commands/style.ts +415 -0
  203. package/src/commands/time.ts +6 -3
  204. package/src/commands/tuning.ts +3 -3
  205. package/src/commands/vocal-pitch.ts +617 -0
  206. package/src/commands/vocal.ts +147 -0
  207. package/src/commands/vocoder.ts +627 -0
  208. package/src/commands/wind.ts +2 -2
  209. package/src/fs/durable.ts +50 -0
  210. package/src/identity/actor.ts +127 -0
  211. package/src/lang/glossary.ts +511 -0
  212. package/src/launch-args.ts +267 -0
  213. package/src/main.ts +2508 -308
  214. package/src/media/cli.ts +20 -3
  215. package/src/media/import.ts +3 -1
  216. package/src/project/check.ts +21 -2
  217. package/src/project/clip-pins.ts +72 -0
  218. package/src/project/init.ts +23 -8
  219. package/src/project/sync.ts +418 -86
  220. package/src/render.ts +20 -1
  221. package/src/session/client.ts +107 -3
  222. package/src/session/clipboard.ts +79 -0
  223. package/src/session/daemon.ts +199 -5
  224. package/src/session/live-host.ts +232 -0
  225. package/src/session/meta.ts +14 -0
  226. package/src/session/origin.ts +174 -0
  227. package/src/session/port.ts +109 -14
  228. package/src/session/presence.ts +34 -4
  229. package/src/session/protocol.ts +365 -7
  230. package/src/session/rebase.ts +63 -12
  231. package/src/session/receipt.ts +265 -0
  232. package/src/session/shared-live.ts +131 -0
  233. package/src/session/store.ts +126 -45
  234. package/src/tui/arrange-menu.ts +225 -23
  235. package/src/tui/audition.ts +1 -1
  236. package/src/tui/euclid.ts +113 -27
  237. package/src/tui/fader.ts +282 -42
  238. package/src/tui/granular-menu.ts +2 -4
  239. package/src/tui/knob-fields.ts +107 -0
  240. package/src/tui/knob-map.ts +198 -0
  241. package/src/tui/menu-clips.ts +297 -0
  242. package/src/tui/menu-time.ts +20 -13
  243. package/src/tui/menu-voice.ts +405 -0
  244. package/src/tui/menu.ts +987 -179
  245. package/src/tui/modal-menu.ts +6 -6
  246. package/src/tui/performance-menu.ts +5 -2
  247. package/src/tui/play-chords.ts +4 -2
  248. package/src/tui/play-mode.ts +15 -1
  249. package/src/tui/play-session.ts +243 -28
  250. package/src/tui/sing-menu.ts +278 -0
  251. package/src/tui/style-menu.ts +104 -0
  252. package/src/tui/tape-mode.ts +580 -0
  253. package/src/tui/tape-view.ts +263 -0
  254. package/src/tui/vocoder-menu.ts +244 -0
  255. package/src/tui/wind-menu.ts +3 -3
  256. package/src/version.ts +8 -0
  257. package/src/web/fetch.ts +115 -29
  258. package/tui/activity.ts +180 -9
  259. package/tui/app.ts +491 -84
  260. package/tui/clip-row.ts +132 -0
  261. package/tui/delight.ts +193 -0
  262. package/tui/drawer.ts +233 -27
  263. package/tui/frame-gate.ts +76 -0
  264. package/tui/grammar.ts +143 -65
  265. package/tui/guide.ts +50 -5
  266. package/tui/highway.ts +309 -30
  267. package/tui/hints.ts +197 -0
  268. package/tui/hits.ts +7 -1
  269. package/tui/input.ts +60 -9
  270. package/tui/keys.ts +1 -1
  271. package/tui/knobs.ts +268 -0
  272. package/tui/play-strip.ts +80 -14
  273. package/tui/prompt.ts +1 -1
  274. package/tui/screen.ts +151 -18
  275. package/tui/tape.ts +439 -0
  276. package/tui/text.ts +35 -2
  277. package/tui/theme.ts +46 -1
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Actor identity placeholder: who made an edit, stable across windows and
3
+ * restarts on one machine. No keys and no login; a later signed identity can
4
+ * bind to this id. It is sent only on the session protocol (`hello`), never
5
+ * to model gateways or anywhere else.
6
+ *
7
+ * File: `<config>/actor.json` = `{ "id": "a_<22 base32>", "name": "$USER" }`,
8
+ * created once with an exclusive atomic write so concurrent first runs agree.
9
+ */
10
+ import { link, mkdir, readFile, rm, writeFile } from "node:fs/promises";
11
+ import { randomUUID } from "node:crypto";
12
+ import { userInfo } from "node:os";
13
+ import { join } from "node:path";
14
+ import { configDir } from "../auth/credentials.ts";
15
+ import { randomBase32, setIdSalt } from "../../core/ids.ts";
16
+
17
+ export type Actor = Readonly<{ id: string; name: string }>;
18
+
19
+ export const ACTOR_ID = /^a_[a-z2-7]{22}$/;
20
+ const MAX_NAME_LENGTH = 64;
21
+
22
+ export function isActorId(value: unknown): value is string {
23
+ return typeof value === "string" && ACTOR_ID.test(value);
24
+ }
25
+
26
+ export function actorPath(dir: string = configDir()): string {
27
+ return join(dir, "actor.json");
28
+ }
29
+
30
+ function defaultName(env: Readonly<Record<string, string | undefined>>) {
31
+ let name = env.USER || env.LOGNAME || "";
32
+ if (!name) {
33
+ try {
34
+ name = userInfo().username;
35
+ } catch {
36
+ name = "";
37
+ }
38
+ }
39
+ return cleanName(name) || "me";
40
+ }
41
+
42
+ function cleanName(value: unknown): string {
43
+ if (typeof value !== "string") return "";
44
+ return value
45
+ .replace(/[\u0000-\u001f\u007f]/g, "")
46
+ .trim()
47
+ .slice(0, MAX_NAME_LENGTH);
48
+ }
49
+
50
+ function parseActor(text: string): Actor | undefined {
51
+ try {
52
+ const value = JSON.parse(text) as Record<string, unknown>;
53
+ if (typeof value !== "object" || value === null) return undefined;
54
+ if (!isActorId(value.id)) return undefined;
55
+ return { id: value.id, name: cleanName(value.name) || "me" };
56
+ } catch {
57
+ return undefined;
58
+ }
59
+ }
60
+
61
+ let cached: Actor | undefined;
62
+
63
+ /** A process-local actor that is never written (tests, read-only configs). */
64
+ export function ephemeralActor(name = "me"): Actor {
65
+ return { id: `a_${randomBase32(22)}`, name: cleanName(name) || "me" };
66
+ }
67
+
68
+ /**
69
+ * Reads the actor file, creating it on first run. A corrupt file is replaced
70
+ * (the old id is unrecoverable anyway). Never throws: an unwritable config
71
+ * dir yields an ephemeral actor for this process.
72
+ */
73
+ export async function loadActor(
74
+ options: {
75
+ dir?: string;
76
+ env?: Readonly<Record<string, string | undefined>>;
77
+ } = {},
78
+ ): Promise<Actor> {
79
+ const env = options.env ?? process.env;
80
+ const dir = options.dir ?? configDir(env);
81
+ const path = actorPath(dir);
82
+ const existing = await readFile(path, "utf8").then(
83
+ parseActor,
84
+ () => undefined,
85
+ );
86
+ if (existing) return existing;
87
+ const fresh = ephemeralActor(defaultName(env));
88
+ try {
89
+ await mkdir(dir, { recursive: true, mode: 0o700 });
90
+ const temp = `${path}.${randomUUID()}.tmp`;
91
+ await writeFile(temp, `${JSON.stringify(fresh)}\n`, { mode: 0o600 });
92
+ try {
93
+ // link() fails if the file exists: the first writer wins, others adopt it.
94
+ await link(temp, path);
95
+ } catch (error) {
96
+ const code = (error as { code?: string }).code;
97
+ if (code !== "EEXIST") throw error;
98
+ const winner = await readFile(path, "utf8").then(
99
+ parseActor,
100
+ () => undefined,
101
+ );
102
+ if (winner) return winner;
103
+ // Corrupt file: replace it.
104
+ await rm(path, { force: true });
105
+ await link(temp, path);
106
+ } finally {
107
+ await rm(temp, { force: true });
108
+ }
109
+ return fresh;
110
+ } catch {
111
+ return fresh;
112
+ }
113
+ }
114
+
115
+ /** Process-wide actor, loaded once; also salts `newId`. */
116
+ export async function currentActor(): Promise<Actor> {
117
+ if (!cached) {
118
+ cached = await loadActor();
119
+ setIdSalt(cached.id);
120
+ }
121
+ return cached;
122
+ }
123
+
124
+ /** Tests only. */
125
+ export function resetActorCache(): void {
126
+ cached = undefined;
127
+ }
@@ -0,0 +1,511 @@
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
+ term: "range",
403
+ meaning: "bars a to b on one track or all (copy bass 5-6 to 7)",
404
+ losers: [],
405
+ aliases: [],
406
+ },
407
+ {
408
+ term: "clipboard",
409
+ meaning: "what copy without `to` holds until paste (tape c, x, v)",
410
+ losers: [],
411
+ aliases: [],
412
+ },
413
+ {
414
+ term: "pane",
415
+ meaning: "one terminal on a shared session, with a letter and own undo",
416
+ losers: [],
417
+ aliases: [],
418
+ },
419
+ ];
420
+
421
+ /** British spellings dawg writes the American way (lowercase keys). */
422
+ export const SPELLING: Readonly<Record<string, string>> = {
423
+ centre: "center",
424
+ centred: "centered",
425
+ centres: "centers",
426
+ modelled: "modeled",
427
+ modelling: "modeling",
428
+ behaviour: "behavior",
429
+ behaviours: "behaviors",
430
+ cancelled: "canceled",
431
+ cancelling: "canceling",
432
+ colour: "color",
433
+ colours: "colors",
434
+ coloured: "colored",
435
+ analyse: "analyze",
436
+ analysed: "analyzed",
437
+ analyses: "analyzes",
438
+ labelled: "labeled",
439
+ labelling: "labeling",
440
+ towards: "toward",
441
+ normalise: "normalize",
442
+ normalised: "normalized",
443
+ normalises: "normalizes",
444
+ organise: "organize",
445
+ organised: "organized",
446
+ recognise: "recognize",
447
+ recognised: "recognized",
448
+ realise: "realize",
449
+ optimise: "optimize",
450
+ optimised: "optimized",
451
+ favourite: "favorite",
452
+ favourites: "favorites",
453
+ grey: "gray",
454
+ travelling: "traveling",
455
+ levelled: "leveled",
456
+ signalled: "signaled",
457
+ synthesise: "synthesize",
458
+ synthesised: "synthesized",
459
+ quantise: "quantize",
460
+ quantised: "quantized",
461
+ randomise: "randomize",
462
+ randomised: "randomized",
463
+ customise: "customize",
464
+ initialise: "initialize",
465
+ licence: "license",
466
+ dialogue: "dialog",
467
+ catalogue: "catalog",
468
+ };
469
+
470
+ /** Typed spellings kept for old projects and muscle memory (aliases). */
471
+ export const SPELLING_ALIASES: readonly string[] = ["phasercentre"];
472
+
473
+ /** Whether a line is an alias note, where retired words may appear. */
474
+ export function isAliasNote(line: string): boolean {
475
+ return /\balias(es)?\b|\bretired\b|\bold spelling\b/i.test(line);
476
+ }
477
+
478
+ /**
479
+ * Retired words and British spellings in `text`, one finding per hit:
480
+ * `losers` match whole-word (case-sensitive for all-caps losers such as
481
+ * FX), spellings case-insensitively. Alias-note lines are skipped.
482
+ */
483
+ export function lintText(text: string): string[] {
484
+ const hits: string[] = [];
485
+ for (const line of text.split("\n")) {
486
+ if (isAliasNote(line)) continue;
487
+ for (const { term, losers, allowed = [] } of TERMS) {
488
+ let text = line;
489
+ for (const phrase of allowed) text = text.split(phrase).join(" ");
490
+ for (const loser of losers) {
491
+ // Case-sensitive, so `FX` is not `fx`; a capital first letter
492
+ // (sentence start) still matches a lowercase loser.
493
+ const escaped = loser.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
494
+ const first = escaped[0]!;
495
+ const head =
496
+ first.toUpperCase() === first
497
+ ? first
498
+ : `[${first}${first.toUpperCase()}]`;
499
+ const pattern = new RegExp(
500
+ `(?<![\\w/.\`-])${head}${escaped.slice(1)}(?![\\w\`-])`,
501
+ );
502
+ if (pattern.test(text)) hits.push(`${loser} → ${term}: ${line.trim()}`);
503
+ }
504
+ }
505
+ for (const word of line.match(/[A-Za-z]+/g) ?? []) {
506
+ const american = SPELLING[word.toLowerCase()];
507
+ if (american) hits.push(`${word} → ${american}: ${line.trim()}`);
508
+ }
509
+ }
510
+ return hits;
511
+ }