@openmixer/plugin-qualify 0.12.0 → 0.13.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.
@@ -10,13 +10,41 @@
10
10
  * whether the plugin has nothing to measure or whether OUR PROBE failed — and each with
11
11
  * a plain description a UI can render.
12
12
  *
13
- * Everything is a data table, not a switch. Adding a term is one row; a consumer that
14
- * meets a term it does not know must be able to fall back on the `kind`, which is why the
15
- * readers below return a term record rather than a bare string.
13
+ * Everything is a data table, not a switch. A consumer that meets a term it does not know
14
+ * must be able to fall back on the `kind`, which is why the readers below return a term record
15
+ * rather than a bare string.
16
+ *
17
+ * The WORDS are omx-contract's (`data/measurement.json` and `data/resource-profile.json`, rendered
18
+ * as `<ITEM>_WORDS`), in its order: a word is added there, once, and every consumer reads it. What
19
+ * stays here is the prose, keyed by the contract's word type, so a word the contract adds without
20
+ * a description here, or a description for a word it lacks, is a type error.
16
21
  */
17
22
 
23
+ import {
24
+ MEASUREMENT_COST_METHODS_WORDS,
25
+ MEASUREMENT_DIMENSIONS_WORDS,
26
+ MEASUREMENT_LATENCY_EXCLUSIONS_WORDS,
27
+ MEASUREMENT_LATENCY_METHODS_WORDS,
28
+ MEASUREMENT_LATENCY_SCALING_NOTES_WORDS,
29
+ MEASUREMENT_LATENCY_SCALING_WORDS,
30
+ MEASUREMENT_UNMEASURED_KINDS_WORDS,
31
+ MEASUREMENT_UNMEASURED_REASONS,
32
+ MEASUREMENT_UNMEASURED_REASONS_WORDS,
33
+ MEASUREMENT_VERDICT_RATINGS_WORDS,
34
+ MEASUREMENT_VOICE_BASES_WORDS,
35
+ RESOURCE_HOST_ISOLATIONS_WORDS,
36
+ RESOURCE_HOST_LATENCY_HANDLING_WORDS,
37
+ } from "@openmixer/omx-contract";
18
38
  import { nonEmptyString } from "./guards.js";
19
39
 
40
+ /** Pairs each word, in the contract's order, with its description here. */
41
+ function described<Code extends string>(
42
+ words: readonly Code[],
43
+ descriptions: Readonly<Record<Code, string>>,
44
+ ): readonly { readonly code: Code; readonly description: string }[] {
45
+ return words.map((code) => ({ code, description: descriptions[code] }));
46
+ }
47
+
20
48
  // ---------------------------------------------------------------------------
21
49
  // Why a figure is absent
22
50
  // ---------------------------------------------------------------------------
@@ -35,124 +63,75 @@ import { nonEmptyString } from "./guards.js";
35
63
  * a finding nor a failure; it is the shape a PARTIAL run has, and partial runs are
36
64
  * first-class here.
37
65
  */
38
- export type UnmeasuredKind = "nothing-to-measure" | "probe-failed" | "not-attempted";
66
+ export type UnmeasuredKind = (typeof MEASUREMENT_UNMEASURED_KINDS_WORDS)[number];
67
+
68
+ /** One reason a figure may be absent: a word of the contract's MEASUREMENT_UNMEASURED_REASONS. */
69
+ export type UnmeasuredCode = (typeof MEASUREMENT_UNMEASURED_REASONS_WORDS)[number];
39
70
 
40
71
  /** One term of the "why is there no figure" vocabulary. */
41
72
  export interface UnmeasuredTerm {
42
- readonly code: string;
73
+ readonly code: UnmeasuredCode;
43
74
  readonly kind: UnmeasuredKind;
44
75
  /** Plain description, English. A UI localises from `code`; this is for logs and specs. */
45
76
  readonly description: string;
46
77
  }
47
78
 
48
79
  /**
49
- * Every reason a figure may be absent. These were derived from a sweep of 958 plugins — each
50
- * one is an outcome that sweep actually produced, not a hypothetical.
80
+ * Every reason a figure may be absent, in plain English. These were derived from a sweep of 958
81
+ * plugins — each one is an outcome that sweep actually produced, not a hypothetical. The words and
82
+ * their kinds are the contract's.
51
83
  */
52
- export const UNMEASURED_TERMS: readonly UnmeasuredTerm[] = [
53
- {
54
- code: "no-audio-in",
55
- kind: "nothing-to-measure",
56
- description: "No audio input port: an instrument, a generator or an analyser. Nothing to drive.",
57
- },
58
- {
59
- code: "no-audio-out",
60
- kind: "nothing-to-measure",
61
- description: "No audio output port: a meter or a sink. Nothing to read.",
62
- },
63
- {
64
- code: "silent-output",
65
- kind: "nothing-to-measure",
66
- description:
67
- "Instantiated and driven, but emitted nothing under a stimulus that should have passed. " +
84
+ const UNMEASURED_DESCRIPTIONS: Readonly<Record<UnmeasuredCode, string>> = {
85
+ "no-audio-in":
86
+ "No audio input port: an instrument, a generator or an analyser. Nothing to drive.",
87
+ "no-audio-out":
88
+ "No audio output port: a meter or a sink. Nothing to read.",
89
+ "silent-output":
90
+ "Instantiated and driven, but emitted nothing under a stimulus that should have passed. " +
68
91
  "A convolver with no impulse response, a vocoder with no carrier, a gate that never opened.",
69
- },
70
- {
71
- code: "needs-carrier",
72
- kind: "nothing-to-measure",
73
- description: "Requires a second audio input (a carrier or side-chain) the prober did not supply.",
74
- },
75
- {
76
- code: "needs-impulse-response",
77
- kind: "nothing-to-measure",
78
- description: "A convolver requiring an IR file the prober did not load.",
79
- },
80
- {
81
- code: "unpinnable",
82
- kind: "nothing-to-measure",
83
- description:
84
- "The files the host loads cannot be pinned (plugin-qualify spec §11.1): a path holding ',', '=' or " +
92
+ "needs-carrier":
93
+ "Requires a second audio input (a carrier or side-chain) the prober did not supply.",
94
+ "needs-impulse-response":
95
+ "A convolver requiring an IR file the prober did not load.",
96
+ "unpinnable":
97
+ "The files the host loads cannot be pinned (plugin-qualify spec §11.1): a path holding ',', '=' or " +
85
98
  "whitespace, a manifest using @base or a percent-encoded IRI, or a CLAP plugin with more parameters than the " +
86
99
  "host accepts. The plugin stays unpinned, never partly pinned.",
87
- },
88
- {
89
- code: "no-voices",
90
- kind: "nothing-to-measure",
91
- description:
92
- "An instrument whose voice-info declares a voice capacity of 0: there is no voice to hold down, so its " +
100
+ "no-voices":
101
+ "An instrument whose voice-info declares a voice capacity of 0: there is no voice to hold down, so its " +
93
102
  "cost with notes held cannot be taken.",
94
- },
95
- {
96
- code: "notes-unplayable",
97
- kind: "nothing-to-measure",
98
- description: "An instrument whose note input reads no dialect the prober speaks (CLAP's or MIDI's): it cannot be played.",
99
- },
100
- {
101
- code: "voices-unknown",
102
- kind: "not-attempted",
103
- description:
104
- "An instrument that declares no voice capacity, measured by a run given no probe count (`--probe-notes`): " +
103
+ "notes-unplayable":
104
+ "An instrument whose note input reads no dialect the prober speaks (CLAP's or MIDI's): it cannot be played.",
105
+ "voices-unknown":
106
+ "An instrument that declares no voice capacity, measured by a run given no probe count (`--probe-notes`): " +
105
107
  "the run did not know how many notes to hold, so it timed none.",
106
- },
107
- {
108
- code: "refused-instantiation",
109
- kind: "probe-failed",
110
- description: "The plugin would not instantiate at this rate or block size.",
111
- },
112
- {
113
- code: "probe-crashed",
114
- kind: "probe-failed",
115
- description: "The probe worker died (signal or abort) while hosting this plugin.",
116
- },
117
- {
118
- code: "probe-timeout",
119
- kind: "probe-failed",
120
- description: "The probe exceeded its per-plugin time limit and was abandoned.",
121
- },
122
- {
123
- code: "non-finite-output",
124
- kind: "probe-failed",
125
- description: "Output contained NaN or infinity, so no onset or block time could be trusted.",
126
- },
127
- {
128
- code: "not-installed",
129
- kind: "probe-failed",
130
- description:
131
- "The URI was not present in this machine's LV2 world. A fact about this installation, " +
108
+ "refused-instantiation":
109
+ "The plugin would not instantiate at this rate or block size.",
110
+ "probe-crashed":
111
+ "The probe worker died (signal or abort) while hosting this plugin.",
112
+ "probe-timeout":
113
+ "The probe exceeded its per-plugin time limit and was abandoned.",
114
+ "non-finite-output":
115
+ "Output contained NaN or infinity, so no onset or block time could be trusted.",
116
+ "not-installed":
117
+ "The URI was not present in this machine's LV2 world. A fact about this installation, " +
132
118
  "not about the plugin — another host may well have it.",
133
- },
134
- {
135
- code: "worker-nonconforming",
136
- kind: "probe-failed",
137
- description:
138
- "The plugin's LV2_Worker_Interface leaves work or work_response NULL, which the LV2 worker " +
119
+ "worker-nonconforming":
120
+ "The plugin's LV2_Worker_Interface leaves work or work_response NULL, which the LV2 worker " +
139
121
  "specification does not allow: it was refused before any call, not measured.",
140
- },
141
- {
142
- code: "probe-error",
143
- kind: "probe-failed",
144
- description:
145
- "The probe reported a failure with no more specific term. Its own words are kept verbatim " +
122
+ "probe-error":
123
+ "The probe reported a failure with no more specific term. Its own words are kept verbatim " +
146
124
  "in the measurement's `unmeasuredDetail`, so a coarse code never destroys the detail.",
147
- },
148
- {
149
- code: "not-attempted",
150
- kind: "not-attempted",
151
- description: "This run did not cover this plugin for this dimension. A partial run, not a finding.",
152
- },
153
- ];
125
+ "not-attempted":
126
+ "This run did not cover this plugin for this dimension. A partial run, not a finding.",
127
+ };
154
128
 
155
- const UNMEASURED_BY_CODE: ReadonlyMap<string, UnmeasuredTerm> = new Map(
129
+ /** Every reason a figure may be absent, in the contract's order, of the contract's kind. */
130
+ export const UNMEASURED_TERMS: readonly UnmeasuredTerm[] = Object.values(MEASUREMENT_UNMEASURED_REASONS).map(
131
+ (reason) => ({ code: reason.word, kind: reason.group, description: UNMEASURED_DESCRIPTIONS[reason.word] }),
132
+ );
133
+
134
+ const UNMEASURED_BY_CODE: ReadonlyMap<string, UnmeasuredTerm> = new Map<string, UnmeasuredTerm>(
156
135
  UNMEASURED_TERMS.map((term) => [term.code, term]),
157
136
  );
158
137
 
@@ -192,25 +171,19 @@ export interface ScalingTerm {
192
171
  readonly description: string;
193
172
  }
194
173
 
195
- export const SCALING_TERMS: readonly ScalingTerm[] = [
196
- {
197
- code: "fixed-frame",
198
- description: "Constant frame count at every rate — an FFT window. Milliseconds halve when the rate doubles.",
199
- },
200
- {
201
- code: "fixed-time",
202
- description: "Constant milliseconds at every rate — a look-ahead. Frames double when the rate doubles.",
203
- },
204
- { code: "zero", description: "No latency a mix can notice at any measured rate." },
205
- {
206
- code: "nonconforming",
207
- description:
208
- "Fits neither law at a magnitude that reaches a mix: the plugin changes what it does with " +
209
- "the rate. Flagged rather than averaged; nothing may be projected from it.",
210
- },
211
- { code: "single-rate", description: "One usable reading, so no law can be fitted." },
212
- { code: "unmeasured", description: "No rate produced a figure." },
213
- ];
174
+ /** A word of the contract's MEASUREMENT_LATENCY_SCALING. */
175
+ export type ScalingCode = (typeof MEASUREMENT_LATENCY_SCALING_WORDS)[number];
176
+
177
+ export const SCALING_TERMS: readonly ScalingTerm[] = described(MEASUREMENT_LATENCY_SCALING_WORDS, {
178
+ "fixed-frame": "Constant frame count at every rate — an FFT window. Milliseconds halve when the rate doubles.",
179
+ "fixed-time": "Constant milliseconds at every rate — a look-ahead. Frames double when the rate doubles.",
180
+ zero: "No latency a mix can notice at any measured rate.",
181
+ nonconforming:
182
+ "Fits neither law at a magnitude that reaches a mix: the plugin changes what it does with " +
183
+ "the rate. Flagged rather than averaged; nothing may be projected from it.",
184
+ "single-rate": "One usable reading, so no law can be fitted.",
185
+ unmeasured: "No rate produced a figure.",
186
+ });
214
187
 
215
188
  const SCALING_CODES: ReadonlySet<string> = new Set(SCALING_TERMS.map((t) => t.code));
216
189
 
@@ -225,60 +198,55 @@ export function isKnownScalingCode(code: string): boolean {
225
198
  * detector's rather than the plugin's, and `informational-rate`, where only a reading below
226
199
  * the operational rates breaks the law (`lv2-measurement-interchange.md` §1a).
227
200
  */
228
- export const SCALING_NOTE_TERMS = [
229
- {
230
- code: "below-budget",
231
- description:
232
- "The readings fit no law, but every one of them is inside the per-channel latency budget, " +
233
- "so the disagreement is measurement noise rather than plugin behaviour.",
234
- },
235
- {
236
- code: "informational-rate",
237
- description:
238
- "The readings together fit no law, but those at the operational rates (48 kHz and up) do; " +
239
- "the reading below them stays in perRate as information and does not flag the plugin.",
240
- },
241
- ] as const satisfies readonly ScalingTerm[];
201
+ export const SCALING_NOTE_TERMS = described(MEASUREMENT_LATENCY_SCALING_NOTES_WORDS, {
202
+ "below-budget":
203
+ "The readings fit no law, but every one of them is inside the per-channel latency budget, " +
204
+ "so the disagreement is measurement noise rather than plugin behaviour.",
205
+ "informational-rate":
206
+ "The readings together fit no law, but those at the operational rates (48 kHz and up) do; " +
207
+ "the reading below them stays in perRate as information and does not flag the plugin.",
208
+ });
242
209
 
243
210
  /** One of the {@link SCALING_NOTE_TERMS} codes — the words `latency.scalingNote` may carry. */
244
- export type ScalingNoteCode = (typeof SCALING_NOTE_TERMS)[number]["code"];
211
+ export type ScalingNoteCode = (typeof MEASUREMENT_LATENCY_SCALING_NOTES_WORDS)[number];
245
212
 
246
213
  /**
247
214
  * Why one rate's reading was excluded from the fit and from every rate resolution. A
248
215
  * failed reading served as an exact answer is worse than no reading, because a zero reads
249
216
  * as confidence.
250
217
  */
251
- export const EXCLUSION_TERMS: readonly ScalingTerm[] = [
252
- {
253
- code: "impossible-zero",
254
- description:
255
- "Zero frames at this rate while another rate measured non-zero. Both scaling laws map zero " +
256
- "to zero, so no single latency produces this; the onset detector was defeated.",
257
- },
258
- ];
218
+ export const EXCLUSION_TERMS: readonly ScalingTerm[] = described(MEASUREMENT_LATENCY_EXCLUSIONS_WORDS, {
219
+ "impossible-zero":
220
+ "Zero frames at this rate while another rate measured non-zero. Both scaling laws map zero " +
221
+ "to zero, so no single latency produces this; the onset detector was defeated.",
222
+ });
259
223
 
260
224
  // ---------------------------------------------------------------------------
261
225
  // How a figure was taken
262
226
  // ---------------------------------------------------------------------------
263
227
 
264
- /** The measurement methods the format has names for. */
228
+ /** How a latency figure was taken: a word of the contract's MEASUREMENT_LATENCY_METHODS. */
229
+ export type LatencyMethodCode = (typeof MEASUREMENT_LATENCY_METHODS_WORDS)[number];
230
+
231
+ /** How a cost figure was taken: a word of the contract's MEASUREMENT_COST_METHODS. */
232
+ export type CostMethodCode = (typeof MEASUREMENT_COST_METHODS_WORDS)[number];
233
+
234
+ /** The measurement methods the format has names for: the latency methods, then the cost methods. */
265
235
  export const METHOD_TERMS: readonly ScalingTerm[] = [
266
- {
267
- code: "impulse-onset",
268
- description:
236
+ ...described(MEASUREMENT_LATENCY_METHODS_WORDS, {
237
+ "impulse-onset":
269
238
  "Latency as the round-trip delay from a unit impulse to the first output sample above the " +
270
239
  "residual floor. Measures the plugin, not what it declares.",
271
- },
272
- {
273
- code: "declared-port",
274
- description: "Latency read from the plugin's own lv2:latency output port. What it CLAIMS.",
275
- },
276
- {
277
- code: "block-time-percentile",
278
- description:
240
+ "declared-port": "Latency read from the plugin's own lv2:latency output port. What it CLAIMS.",
241
+ "note-onset":
242
+ "An instrument's latency: the delay from a note on to the first output sample above the " +
243
+ "residual floor. Measures what the plugin does with a held note.",
244
+ }),
245
+ ...described(MEASUREMENT_COST_METHODS_WORDS, {
246
+ "block-time-percentile":
279
247
  "Cost as a percentile of per-block wall time under a sustained stimulus, warm-up blocks " +
280
248
  "discarded, normalised to nanoseconds per sample.",
281
- },
249
+ }),
282
250
  ];
283
251
 
284
252
  const METHOD_CODES: ReadonlySet<string> = new Set(METHOD_TERMS.map((t) => t.code));
@@ -295,7 +263,7 @@ export function isKnownMethodCode(code: string): boolean {
295
263
  * hosting verdict — whether a plugin survives, whether its `run()` is RT-clean, what it
296
264
  * demands of a host — and merge under exactly the same per-plugin, per-DIMENSION precedence.
297
265
  */
298
- export const MEASUREMENT_DIMENSIONS = ["latency", "cost", "stability", "rtSafety", "features"] as const;
266
+ export const MEASUREMENT_DIMENSIONS = MEASUREMENT_DIMENSIONS_WORDS;
299
267
 
300
268
  /**
301
269
  * The dimensions the standalone prober (`scan.py` + `benchmark.py`, driven by `lv2-measure.mjs`
@@ -321,19 +289,19 @@ export function readDimension(value: unknown): MeasurementDimension | undefined
321
289
  }
322
290
 
323
291
  /**
324
- * The four ratings a verdict speaks (`lv2-measurement-interchange.md` §3b) — the ONE list; the
325
- * rating vocabulary (`rating.ts`) derives its type from it, so the document and the classifier
326
- * can never disagree on a word.
292
+ * The four ratings a verdict speaks (`lv2-measurement-interchange.md` §3b): the contract's
293
+ * MEASUREMENT_VERDICT_RATINGS. The rating vocabulary (`rating.ts`) derives its type from it, so
294
+ * the document and the classifier can never disagree on a word.
327
295
  */
328
- export const VERDICT_RATINGS = ["suitable", "conditional", "unknown", "unsuitable"] as const;
296
+ export const VERDICT_RATINGS = MEASUREMENT_VERDICT_RATINGS_WORDS;
329
297
  export type VerdictRating = (typeof VERDICT_RATINGS)[number];
330
298
 
331
- /** How a host runs its plugins. */
332
- export const HOST_ISOLATIONS = ["in-process", "per-process", "shared-process"] as const;
299
+ /** How a host runs its plugins: the contract's RESOURCE_HOST_ISOLATIONS. */
300
+ export const HOST_ISOLATIONS = RESOURCE_HOST_ISOLATIONS_WORDS;
333
301
  export type HostIsolation = (typeof HOST_ISOLATIONS)[number];
334
302
 
335
- /** Whether a host aligns on a plugin's reported latency (`compensated`) or only passes it on. */
336
- export const HOST_LATENCY_HANDLING = ["compensated", "uncompensated"] as const;
303
+ /** Whether a host aligns on a plugin's reported latency (`compensated`) or only passes it on: the contract's RESOURCE_HOST_LATENCY_HANDLING. */
304
+ export const HOST_LATENCY_HANDLING = RESOURCE_HOST_LATENCY_HANDLING_WORDS;
337
305
  export type HostLatencyHandling = (typeof HOST_LATENCY_HANDLING)[number];
338
306
 
339
307
  /**
@@ -342,7 +310,7 @@ export type HostLatencyHandling = (typeof HOST_LATENCY_HANDLING)[number];
342
310
  * the caller gave (`--probe-notes`, the console's `SYNTH_VOICE_PROBE_NOTES`), used when the plugin declares
343
311
  * none or when the caller asked for that count whatever it declares.
344
312
  */
345
- export const VOICE_BASES = ["declared", "probe"] as const;
313
+ export const VOICE_BASES = MEASUREMENT_VOICE_BASES_WORDS;
346
314
  export type VoiceBasis = (typeof VOICE_BASES)[number];
347
315
 
348
316
  export function isVoiceBasis(value: unknown): value is VoiceBasis {