@hraness/dawg 0.0.0-stage → 0.3.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 (113) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/DAWG.md +327 -0
  3. package/LICENSE +21 -0
  4. package/README.md +213 -2
  5. package/core/diff.ts +249 -0
  6. package/core/drums.ts +102 -0
  7. package/core/key.ts +43 -0
  8. package/core/loop.ts +78 -0
  9. package/core/pitch.ts +60 -0
  10. package/core/score.ts +1388 -0
  11. package/core/sdk/eval-child.ts +113 -0
  12. package/core/sdk/eval.ts +257 -0
  13. package/core/sdk/print.ts +393 -0
  14. package/core/sdk/v1.ts +954 -0
  15. package/core/slug.ts +19 -0
  16. package/package.json +45 -4
  17. package/src/agent/agent.ts +853 -0
  18. package/src/agent/brief.ts +160 -0
  19. package/src/agent/gateway.ts +441 -0
  20. package/src/agent/models.ts +633 -0
  21. package/src/agent/ops.ts +157 -0
  22. package/src/agent/planner.ts +259 -0
  23. package/src/agent/provider.ts +454 -0
  24. package/src/agent/sse.ts +114 -0
  25. package/src/agent/tools.ts +1373 -0
  26. package/src/agent/usage.ts +296 -0
  27. package/src/agent/workspace.ts +683 -0
  28. package/src/agent/xcb-agent.ts +262 -0
  29. package/src/agent/xcb.ts +579 -0
  30. package/src/audio/click.ts +125 -0
  31. package/src/audio/clock.ts +68 -0
  32. package/src/audio/engine.ts +841 -0
  33. package/src/audio/live.ts +152 -0
  34. package/src/audio/lock.ts +57 -0
  35. package/src/audio/player.ts +134 -0
  36. package/src/audio/render-worker.ts +68 -0
  37. package/src/audio/renderer.ts +174 -0
  38. package/src/audio/sampler.ts +292 -0
  39. package/src/audio/samples.ts +683 -0
  40. package/src/audio/wav.ts +861 -0
  41. package/src/auth/cli.ts +231 -0
  42. package/src/auth/credentials.ts +411 -0
  43. package/src/auth/discover.ts +481 -0
  44. package/src/auth/login.ts +1191 -0
  45. package/src/auth/openrouter.ts +206 -0
  46. package/src/auth/picker.ts +282 -0
  47. package/src/auth/runner.ts +207 -0
  48. package/src/auth/tui.ts +107 -0
  49. package/src/commands/edit.ts +170 -0
  50. package/src/commands/help.ts +247 -0
  51. package/src/commands/history.ts +69 -0
  52. package/src/commands/music.ts +461 -0
  53. package/src/commands/sample.ts +302 -0
  54. package/src/daemon.ts +31 -0
  55. package/src/main.ts +2209 -0
  56. package/src/media/analyze.ts +364 -0
  57. package/src/media/backend.ts +253 -0
  58. package/src/media/cli.ts +173 -0
  59. package/src/media/download.ts +281 -0
  60. package/src/media/dsp.ts +281 -0
  61. package/src/media/import.ts +130 -0
  62. package/src/media/lyrics.ts +201 -0
  63. package/src/media/notes.ts +363 -0
  64. package/src/media/paths.ts +168 -0
  65. package/src/media/process.ts +226 -0
  66. package/src/media/registry.ts +9 -0
  67. package/src/media/sidecar.ts +72 -0
  68. package/src/media/stemdeck.ts +254 -0
  69. package/src/media/stems.ts +173 -0
  70. package/src/media/tools.ts +292 -0
  71. package/src/media/types.ts +92 -0
  72. package/src/media/vendor/basic-pitch.ts +261 -0
  73. package/src/media/vendor/drums.ts +817 -0
  74. package/src/media/vendor/grid.ts +203 -0
  75. package/src/media/vendor/util.ts +139 -0
  76. package/src/media/vendor/wav.ts +233 -0
  77. package/src/project/check.ts +80 -0
  78. package/src/project/init.ts +253 -0
  79. package/src/project/sync.ts +432 -0
  80. package/src/project/typecheck.ts +149 -0
  81. package/src/render.ts +121 -0
  82. package/src/session/attach.ts +181 -0
  83. package/src/session/client.ts +498 -0
  84. package/src/session/daemon.ts +740 -0
  85. package/src/session/delta.ts +249 -0
  86. package/src/session/list.ts +180 -0
  87. package/src/session/lock.ts +92 -0
  88. package/src/session/meta.ts +253 -0
  89. package/src/session/naming.ts +430 -0
  90. package/src/session/port.ts +481 -0
  91. package/src/session/presence.ts +159 -0
  92. package/src/session/protocol.ts +618 -0
  93. package/src/session/rebase.ts +168 -0
  94. package/src/session/store.ts +581 -0
  95. package/src/tui/menu.ts +1083 -0
  96. package/src/tui/play-mode.ts +442 -0
  97. package/src/tui/play-session.ts +636 -0
  98. package/src/web/fetch.ts +340 -0
  99. package/src/web/http.ts +137 -0
  100. package/src/web/search.ts +681 -0
  101. package/tui/activity.ts +364 -0
  102. package/tui/app.ts +1372 -0
  103. package/tui/drums.ts +65 -0
  104. package/tui/highway.ts +921 -0
  105. package/tui/input.ts +63 -0
  106. package/tui/keys.ts +102 -0
  107. package/tui/layers.ts +80 -0
  108. package/tui/play-strip.ts +143 -0
  109. package/tui/prompt.ts +609 -0
  110. package/tui/render.ts +124 -0
  111. package/tui/screen.ts +247 -0
  112. package/tui/text.ts +72 -0
  113. package/tui/theme.ts +451 -0
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Direct music commands for drums, effects, and solo.
3
+ *
4
+ * Grammar (case-insensitive, one command per prompt; beats are score beats):
5
+ *
6
+ * hit <voice> [at] <beat> [vel <0..1>]
7
+ * pattern <voice> <beat> [<beat> ...] [vel <0..1>] up to 64 beats
8
+ * pattern <voice> every <step> [from <beat>] [vel <0..1>] fills the loop
9
+ * clear <voice> drum hits only
10
+ * filter <cutoff hz> [<resonance 0..1>] | filter off
11
+ * delay <beats> [<feedback 0..0.9> [<mix 0..1>]] | delay off
12
+ * reverb <mix 0..1> [<size 0..1>] | reverb off
13
+ * automate <lane> at <beat> <value> | clear <lane> automation
14
+ * lanes: filter (cutoff hz), resonance (0..1), delay-feedback (0..0.9),
15
+ * delay-mix (0..1); volume and pan lanes live in the core prompt parser
16
+ * solo | unsolo
17
+ *
18
+ * Voices: kick (bd), snare (sd), clap (cp), rim (perc), tom, hat (hh),
19
+ * openhat (oh). Drum commands require a kit track (`instrument kit`).
20
+ *
21
+ * Parsing is pure and bounded; applying returns the next immutable score
22
+ * plus the session event kind/payload, leaving persistence to the caller.
23
+ */
24
+ import {
25
+ SCORE_LIMITS,
26
+ createScore,
27
+ updateTrack,
28
+ setTrackAutomation,
29
+ AUTOMATION_LANES,
30
+ type AutomationParameter,
31
+ type NoteInput,
32
+ type TrackScore,
33
+ } from "../../core/score.ts";
34
+ import {
35
+ drumVoiceForPitch,
36
+ drumVoicePitch,
37
+ isDrumInstrument,
38
+ parseDrumVoice,
39
+ type DrumVoice,
40
+ } from "../../core/drums.ts";
41
+
42
+ export const MAX_PATTERN_BEATS = 64;
43
+ export const MAX_PATTERN_HITS = 256;
44
+ export const MIN_PATTERN_STEP = 0.125;
45
+ const DEFAULT_HIT_VELOCITY = 0.9;
46
+
47
+ export type MusicCommand =
48
+ | {
49
+ type: "drum-hits";
50
+ voice: DrumVoice;
51
+ beats: readonly number[];
52
+ velocity: number;
53
+ }
54
+ | {
55
+ type: "drum-every";
56
+ voice: DrumVoice;
57
+ step: number;
58
+ from: number;
59
+ velocity: number;
60
+ }
61
+ | { type: "drum-clear"; voice: DrumVoice }
62
+ | { type: "filter"; cutoff: number; resonance: number }
63
+ | { type: "filter-off" }
64
+ | { type: "delay"; beats: number; feedback: number; mix: number }
65
+ | { type: "delay-off" }
66
+ | { type: "reverb"; mix: number; size: number }
67
+ | { type: "reverb-off" }
68
+ | {
69
+ type: "effect-automation";
70
+ parameter: EffectAutomationParameter;
71
+ beat: number;
72
+ value: number;
73
+ }
74
+ | { type: "effect-automation-clear"; parameter: EffectAutomationParameter }
75
+ | { type: "solo"; solo: boolean };
76
+
77
+ /** Automation lanes owned by the effect grammar (volume/pan are core). */
78
+ export type EffectAutomationParameter = Exclude<
79
+ AutomationParameter,
80
+ "volume" | "pan"
81
+ >;
82
+
83
+ const LANE_ALIASES: Readonly<Record<string, EffectAutomationParameter>> =
84
+ Object.freeze({
85
+ filter: "filter",
86
+ cutoff: "filter",
87
+ resonance: "resonance",
88
+ res: "resonance",
89
+ "filter-resonance": "resonance",
90
+ "delay-feedback": "delay-feedback",
91
+ "delay-fb": "delay-feedback",
92
+ feedback: "delay-feedback",
93
+ "delay-mix": "delay-mix",
94
+ });
95
+
96
+ export function parseEffectLane(
97
+ name: string,
98
+ ): EffectAutomationParameter | undefined {
99
+ return Object.prototype.hasOwnProperty.call(LANE_ALIASES, name)
100
+ ? LANE_ALIASES[name]
101
+ : undefined;
102
+ }
103
+
104
+ export type MusicResult = Readonly<{
105
+ /** `true` when the command applied; `false` carries a failure message. */
106
+ ok: boolean;
107
+ message: string;
108
+ next?: TrackScore;
109
+ kind?: string;
110
+ payload?: Record<string, unknown>;
111
+ }>;
112
+
113
+ const NUMBER = "(\\d+(?:\\.\\d+)?)";
114
+ const VELOCITY = `(?:\\s+(?:vel|velocity)\\s+${NUMBER})?`;
115
+
116
+ export function parseMusicCommand(prompt: string): MusicCommand | undefined {
117
+ const text = prompt.trim().toLowerCase().replace(/\s+/g, " ");
118
+ if (text.length > 1_024) return undefined;
119
+ if (text === "solo") return { type: "solo", solo: true };
120
+ if (text === "unsolo") return { type: "solo", solo: false };
121
+
122
+ const hit = text.match(
123
+ new RegExp(`^hit ([a-z-]+) (?:at )?${NUMBER}${VELOCITY}$`),
124
+ );
125
+ if (hit) {
126
+ const voice = parseDrumVoice(hit[1]!);
127
+ const beat = Number(hit[2]);
128
+ const velocity = parseVelocity(hit[3]);
129
+ if (!voice || !validBeat(beat) || velocity === undefined) return undefined;
130
+ return { type: "drum-hits", voice, beats: [beat], velocity };
131
+ }
132
+
133
+ const every = text.match(
134
+ new RegExp(
135
+ `^pattern ([a-z-]+) every ${NUMBER}(?: from ${NUMBER})?${VELOCITY}$`,
136
+ ),
137
+ );
138
+ if (every) {
139
+ const voice = parseDrumVoice(every[1]!);
140
+ const step = Number(every[2]);
141
+ const from = every[3] === undefined ? 0 : Number(every[3]);
142
+ const velocity = parseVelocity(every[4]);
143
+ if (
144
+ !voice ||
145
+ !Number.isFinite(step) ||
146
+ step < MIN_PATTERN_STEP ||
147
+ step > SCORE_LIMITS.maxBars * SCORE_LIMITS.maxBeatsPerBar ||
148
+ !validBeat(from) ||
149
+ velocity === undefined
150
+ )
151
+ return undefined;
152
+ return { type: "drum-every", voice, step, from, velocity };
153
+ }
154
+
155
+ const pattern = text.match(
156
+ new RegExp(`^pattern ([a-z-]+)((?: ${NUMBER})+)${VELOCITY}$`),
157
+ );
158
+ if (pattern) {
159
+ const voice = parseDrumVoice(pattern[1]!);
160
+ const beats = pattern[2]!.trim().split(" ").map(Number);
161
+ const velocity = parseVelocity(pattern[4]);
162
+ if (
163
+ !voice ||
164
+ beats.length > MAX_PATTERN_BEATS ||
165
+ !beats.every(validBeat) ||
166
+ velocity === undefined
167
+ )
168
+ return undefined;
169
+ return {
170
+ type: "drum-hits",
171
+ voice,
172
+ beats: [...new Set(beats)].sort((a, b) => a - b),
173
+ velocity,
174
+ };
175
+ }
176
+
177
+ const clear = text.match(/^clear ([a-z-]+)$/);
178
+ if (clear) {
179
+ const voice = parseDrumVoice(clear[1]!);
180
+ if (voice) return { type: "drum-clear", voice };
181
+ return undefined;
182
+ }
183
+
184
+ if (/^(?:filter|lowpass) off$/.test(text)) return { type: "filter-off" };
185
+ const filter = text.match(
186
+ new RegExp(`^(?:filter|lowpass) ${NUMBER}(?: ${NUMBER})?$`),
187
+ );
188
+ if (filter) {
189
+ const cutoff = Number(filter[1]);
190
+ const resonance = filter[2] === undefined ? 0 : Number(filter[2]);
191
+ if (
192
+ !validCutoff(cutoff) ||
193
+ !Number.isFinite(resonance) ||
194
+ resonance > SCORE_LIMITS.maxFilterResonance
195
+ )
196
+ return undefined;
197
+ return { type: "filter", cutoff, resonance };
198
+ }
199
+
200
+ if (text === "delay off") return { type: "delay-off" };
201
+ const delay = text.match(
202
+ new RegExp(`^delay ${NUMBER}(?: ${NUMBER}(?: ${NUMBER})?)?$`),
203
+ );
204
+ if (delay) {
205
+ const beats = Number(delay[1]);
206
+ const feedback = delay[2] === undefined ? 0.3 : Number(delay[2]);
207
+ const mix = delay[3] === undefined ? 0.35 : Number(delay[3]);
208
+ if (
209
+ !Number.isFinite(beats) ||
210
+ beats < SCORE_LIMITS.minDelayBeats ||
211
+ beats > SCORE_LIMITS.maxDelayBeats ||
212
+ !Number.isFinite(feedback) ||
213
+ feedback > SCORE_LIMITS.maxDelayFeedback ||
214
+ !Number.isFinite(mix) ||
215
+ mix > SCORE_LIMITS.maxDelayMix
216
+ )
217
+ return undefined;
218
+ return { type: "delay", beats, feedback, mix };
219
+ }
220
+
221
+ if (/^reverb off$/.test(text)) return { type: "reverb-off" };
222
+ const reverb = text.match(new RegExp(`^reverb ${NUMBER}(?: ${NUMBER})?$`));
223
+ if (reverb) {
224
+ const mix = Number(reverb[1]);
225
+ const size = reverb[2] === undefined ? 0.5 : Number(reverb[2]);
226
+ if (
227
+ !Number.isFinite(mix) ||
228
+ mix > SCORE_LIMITS.maxReverbMix ||
229
+ !Number.isFinite(size) ||
230
+ size > SCORE_LIMITS.maxReverbSize
231
+ )
232
+ return undefined;
233
+ return { type: "reverb", mix, size };
234
+ }
235
+
236
+ const clearLane = text.match(/^(?:clear|reset) ([a-z-]+) automation$/);
237
+ if (clearLane) {
238
+ const parameter = parseEffectLane(clearLane[1]!);
239
+ return parameter
240
+ ? { type: "effect-automation-clear", parameter }
241
+ : undefined;
242
+ }
243
+ const automate = text.match(
244
+ new RegExp(`^(?:automate|automation) ([a-z-]+) at ${NUMBER} ${NUMBER}$`),
245
+ );
246
+ if (automate) {
247
+ const parameter = parseEffectLane(automate[1]!);
248
+ const beat = Number(automate[2]);
249
+ const value = Number(automate[3]);
250
+ if (!parameter || !validBeat(beat)) return undefined;
251
+ const { min, max } = AUTOMATION_LANES[parameter];
252
+ if (!Number.isFinite(value) || value < min || value > max) return undefined;
253
+ return { type: "effect-automation", parameter, beat, value };
254
+ }
255
+ return undefined;
256
+ }
257
+
258
+ /**
259
+ * Apply a parsed command to `trackId`. `newId(index)` must return a unique
260
+ * note id for each drum hit created by this command.
261
+ */
262
+ export function applyMusicCommand(
263
+ score: TrackScore,
264
+ trackId: string,
265
+ command: MusicCommand,
266
+ newId: (index: number) => string,
267
+ ): MusicResult {
268
+ const track = score.tracks.find((candidate) => candidate.id === trackId);
269
+ if (!track) return { ok: false, message: `no track · ${trackId}` };
270
+
271
+ if (command.type === "solo") {
272
+ const next = updateTrack(score, trackId, { solo: command.solo });
273
+ return {
274
+ ok: true,
275
+ message: `${command.solo ? "solo" : "unsolo"} · ${trackId}`,
276
+ next,
277
+ kind: "score.track",
278
+ payload: { trackId, patch: { solo: command.solo } },
279
+ };
280
+ }
281
+ if (command.type === "filter" || command.type === "filter-off") {
282
+ const filter =
283
+ command.type === "filter"
284
+ ? { cutoff: command.cutoff, resonance: command.resonance }
285
+ : null;
286
+ return {
287
+ ok: true,
288
+ message: filter
289
+ ? `filter · ${filter.cutoff} Hz res ${filter.resonance}`
290
+ : "filter · off",
291
+ next: updateTrack(score, trackId, { filter }),
292
+ kind: "score.effect",
293
+ payload: { trackId, effect: "filter", value: filter },
294
+ };
295
+ }
296
+ if (command.type === "delay" || command.type === "delay-off") {
297
+ const delay =
298
+ command.type === "delay"
299
+ ? { beats: command.beats, feedback: command.feedback, mix: command.mix }
300
+ : null;
301
+ return {
302
+ ok: true,
303
+ message: delay
304
+ ? `delay · ${delay.beats} beats fb ${delay.feedback} mix ${delay.mix}`
305
+ : "delay · off",
306
+ next: updateTrack(score, trackId, { delay }),
307
+ kind: "score.effect",
308
+ payload: { trackId, effect: "delay", value: delay },
309
+ };
310
+ }
311
+ if (command.type === "reverb" || command.type === "reverb-off") {
312
+ const reverb =
313
+ command.type === "reverb"
314
+ ? { mix: command.mix, size: command.size }
315
+ : null;
316
+ return {
317
+ ok: true,
318
+ message: reverb
319
+ ? `reverb · mix ${reverb.mix} size ${reverb.size}`
320
+ : "reverb · off",
321
+ next: updateTrack(score, trackId, { reverb }),
322
+ kind: "score.effect",
323
+ payload: { trackId, effect: "reverb", value: reverb },
324
+ };
325
+ }
326
+ if (
327
+ command.type === "effect-automation" ||
328
+ command.type === "effect-automation-clear"
329
+ ) {
330
+ const { parameter } = command;
331
+ let points: { tick: number; value: number }[] = [];
332
+ if (command.type === "effect-automation") {
333
+ const tick = Math.round(command.beat * score.ticksPerBeat);
334
+ const merged = new Map(
335
+ (track[AUTOMATION_LANES[parameter].field] ?? []).map((point) => [
336
+ point.tick,
337
+ point,
338
+ ]),
339
+ );
340
+ merged.set(tick, { tick, value: command.value });
341
+ points = [...merged.values()].sort((a, b) => a.tick - b.tick);
342
+ if (points.length > SCORE_LIMITS.maxAutomationPoints)
343
+ return { ok: false, message: `${parameter} automation is full` };
344
+ }
345
+ return {
346
+ ok: true,
347
+ message: `automation · ${parameter} ${points.length} point${points.length === 1 ? "" : "s"}`,
348
+ next: setTrackAutomation(score, trackId, parameter, points),
349
+ kind: "score.automation",
350
+ payload: { trackId, parameter, points },
351
+ };
352
+ }
353
+
354
+ if (!isDrumInstrument(track.instrument))
355
+ return {
356
+ ok: false,
357
+ message: `${trackId} is not a drum track · try instrument kit`,
358
+ };
359
+
360
+ if (command.type === "drum-clear") {
361
+ const removed = score.notes.filter(
362
+ (note) =>
363
+ note.trackId === trackId &&
364
+ drumVoiceForPitch(note.pitch) === command.voice,
365
+ );
366
+ if (removed.length === 0)
367
+ return { ok: false, message: `no ${command.voice} hits` };
368
+ const next = createScore({
369
+ ...score.toJSON(),
370
+ notes: score.notes.filter((note) => !removed.includes(note)),
371
+ });
372
+ return {
373
+ ok: true,
374
+ message: `cleared ${removed.length} ${command.voice} hit${removed.length === 1 ? "" : "s"}`,
375
+ next,
376
+ kind: "score.drums",
377
+ payload: { trackId, voice: command.voice, removed: removed.length },
378
+ };
379
+ }
380
+
381
+ const beats =
382
+ command.type === "drum-hits"
383
+ ? command.beats
384
+ : everyBeats(command.from, command.step, score.bars * score.beatsPerBar);
385
+ if (beats.length > MAX_PATTERN_HITS)
386
+ return { ok: false, message: `pattern exceeds ${MAX_PATTERN_HITS} hits` };
387
+ const pitch = drumVoicePitch(command.voice);
388
+ const existing = new Set(
389
+ score.notes
390
+ .filter((note) => note.trackId === trackId && note.pitch === pitch)
391
+ .map((note) => note.startTick),
392
+ );
393
+ const durationTicks = Math.max(1, Math.round(score.ticksPerBeat / 4));
394
+ const hits: NoteInput[] = [];
395
+ for (const beat of beats) {
396
+ const startTick = Math.round(beat * score.ticksPerBeat);
397
+ if (startTick > SCORE_LIMITS.maxTick || existing.has(startTick)) continue;
398
+ existing.add(startTick);
399
+ hits.push({
400
+ id: newId(hits.length),
401
+ trackId,
402
+ startTick,
403
+ durationTicks,
404
+ pitch,
405
+ velocity: command.velocity,
406
+ });
407
+ }
408
+ if (hits.length === 0)
409
+ return { ok: false, message: `${command.voice} already there` };
410
+ if (score.notes.length + hits.length > SCORE_LIMITS.maxNotes)
411
+ return {
412
+ ok: false,
413
+ message: `score is full (${SCORE_LIMITS.maxNotes} notes)`,
414
+ };
415
+ const next = createScore({
416
+ ...score.toJSON(),
417
+ notes: [...score.notes, ...hits],
418
+ });
419
+ return {
420
+ ok: true,
421
+ message: `+${hits.length} ${command.voice} hit${hits.length === 1 ? "" : "s"}`,
422
+ next,
423
+ kind: "score.drums",
424
+ payload: {
425
+ trackId,
426
+ voice: command.voice,
427
+ ticks: hits.map((hit) => hit.startTick),
428
+ },
429
+ };
430
+ }
431
+
432
+ function everyBeats(from: number, step: number, loopBeats: number): number[] {
433
+ const beats: number[] = [];
434
+ // Index-based stepping avoids accumulating floating-point drift.
435
+ for (let index = 0; beats.length <= MAX_PATTERN_HITS; index += 1) {
436
+ const beat = from + index * step;
437
+ if (beat >= loopBeats) break;
438
+ beats.push(beat);
439
+ }
440
+ return beats;
441
+ }
442
+
443
+ function parseVelocity(value: string | undefined): number | undefined {
444
+ if (value === undefined) return DEFAULT_HIT_VELOCITY;
445
+ const velocity = Number(value);
446
+ return Number.isFinite(velocity) && velocity <= 1 ? velocity : undefined;
447
+ }
448
+
449
+ function validBeat(beat: number): boolean {
450
+ return (
451
+ Number.isFinite(beat) && beat >= 0 && beat * 4_096 <= SCORE_LIMITS.maxTick
452
+ );
453
+ }
454
+
455
+ function validCutoff(cutoff: number): boolean {
456
+ return (
457
+ Number.isFinite(cutoff) &&
458
+ cutoff >= SCORE_LIMITS.minFilterCutoff &&
459
+ cutoff <= SCORE_LIMITS.maxFilterCutoff
460
+ );
461
+ }