@openmixer/plugin-qualify 0.11.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.
Files changed (51) hide show
  1. package/README.md +7 -1
  2. package/dist/clap-qualify.d.ts +60 -4
  3. package/dist/clap-qualify.js +141 -3
  4. package/dist/clap-qualify.js.map +1 -1
  5. package/dist/host-profiles.d.ts +1 -2
  6. package/dist/host-profiles.js +12 -2
  7. package/dist/host-profiles.js.map +1 -1
  8. package/dist/hosting-codes.d.ts +77 -57
  9. package/dist/hosting-codes.js +89 -77
  10. package/dist/hosting-codes.js.map +1 -1
  11. package/dist/hosting-suitability.d.ts +12 -4
  12. package/dist/hosting-suitability.js +18 -16
  13. package/dist/hosting-suitability.js.map +1 -1
  14. package/dist/measurement/cli.d.ts +8 -1
  15. package/dist/measurement/cli.js +15 -1
  16. package/dist/measurement/cli.js.map +1 -1
  17. package/dist/measurement/format.d.ts +45 -5
  18. package/dist/measurement/format.js +46 -3
  19. package/dist/measurement/format.js.map +1 -1
  20. package/dist/measurement/probe-json.js +3 -0
  21. package/dist/measurement/probe-json.js.map +1 -1
  22. package/dist/measurement/sweep-jsonl.js +2 -1
  23. package/dist/measurement/sweep-jsonl.js.map +1 -1
  24. package/dist/measurement/vocabulary.d.ts +39 -22
  25. package/dist/measurement/vocabulary.js +93 -145
  26. package/dist/measurement/vocabulary.js.map +1 -1
  27. package/dist/probe-judge.d.ts +6 -1
  28. package/dist/probe-judge.js +7 -3
  29. package/dist/probe-judge.js.map +1 -1
  30. package/dist/probe-output.d.ts +3 -0
  31. package/dist/probe-output.js +5 -0
  32. package/dist/probe-output.js.map +1 -1
  33. package/dist/qualification-record.d.ts +3 -1
  34. package/dist/qualification-record.js.map +1 -1
  35. package/fixtures/clap-scan/fake.json +109 -7
  36. package/fixtures/probe-output/success-msi.json +11 -0
  37. package/package.json +2 -2
  38. package/src/clap-qualify.ts +177 -5
  39. package/src/host-profiles.ts +12 -2
  40. package/src/hosting-codes.ts +182 -77
  41. package/src/hosting-suitability.ts +25 -18
  42. package/src/measurement/cli.ts +21 -1
  43. package/src/measurement/format.ts +81 -3
  44. package/src/measurement/probe-json.ts +3 -0
  45. package/src/measurement/sweep-jsonl.ts +2 -1
  46. package/src/measurement/vocabulary.ts +148 -149
  47. package/src/probe-judge.ts +13 -5
  48. package/src/probe-output.ts +6 -0
  49. package/src/qualification-record.ts +9 -2
  50. package/tools/benchmark.py +104 -13
  51. package/tools/lv2-measure.mjs +2 -0
@@ -109,6 +109,17 @@
109
109
  "clap.params",
110
110
  "clap.state",
111
111
  "clap.thread-check",
112
+ "clap.note-ports",
113
+ "clap.note-name",
114
+ "clap.voice-info",
115
+ "clap.event-registry",
116
+ "clap.timer-support",
117
+ "clap.posix-fd-support",
118
+ "clap.thread-pool",
119
+ "clap.tail",
120
+ "clap.surround/4",
121
+ "clap.ambisonic/3",
122
+ "clap.audio-ports-config",
112
123
  "clap.preset-load/2",
113
124
  "clap.track-info/1",
114
125
  "clap.remote-controls/2"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmixer/plugin-qualify",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Qualify audio plugins for real-time hosting: scan, offline latency and cost measurement, a mod-host hosting sweep, and a fail-closed rating.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -96,6 +96,6 @@
96
96
  "provenance": true
97
97
  },
98
98
  "dependencies": {
99
- "@openmixer/omx-contract": "^2.16.0"
99
+ "@openmixer/omx-contract": "^3.0.0"
100
100
  }
101
101
  }
@@ -19,10 +19,13 @@ import {
19
19
  contributedHostingRating,
20
20
  noteSweepReasons,
21
21
  rtSafetyReasons,
22
+ type ClapLatencyAtRate,
22
23
  type HostingReason,
23
24
  type PluginClapMeasurement,
24
25
  type PluginNoteSweepMeasurement,
25
26
  type PluginRtSafetyMeasurement,
27
+ type PluginStabilityMeasurement,
28
+ type QualifiedPlugin,
26
29
  } from "./hosting-suitability.js";
27
30
  import {
28
31
  OMX_CLAP_HOST_CONFIGURED_EXTENSIONS,
@@ -33,7 +36,9 @@ import {
33
36
  import type { QualifyVerdict } from "./qualify-cli.js";
34
37
  import { RATING_SEVERITY, worstRating, type SuitabilityRating } from "./rating.js";
35
38
  import { halfPopulatedRefusal, locateNativeTools } from "./tool-paths.js";
36
- import { readIntegrity, type IntegrityMeasurement } from "./measurement/format.js";
39
+ import { readIntegrity, type IntegrityMeasurement, type VoicesHeld } from "./measurement/format.js";
40
+ import { isVoiceBasis } from "./measurement/vocabulary.js";
41
+ import { coreFraction } from "./cost-fields.js";
37
42
 
38
43
  export class ClapQualifyError extends Error {
39
44
  constructor(message: string) {
@@ -61,6 +66,45 @@ export interface ClapProbedFacts extends PluginClapMeasurement {
61
66
  readonly nonFinite: boolean;
62
67
  }
63
68
 
69
+ /** One rate's cost: the block times in nanoseconds per sample, warm-up dropped, and the notes held for an instrument. */
70
+ export interface ClapCostFigure {
71
+ readonly nsPerSampleMedian: number;
72
+ readonly nsPerSampleP95: number;
73
+ readonly nsPerSampleMin: number;
74
+ readonly nsPerSampleMax: number;
75
+ /** The median of the dropped warm-up blocks: the evidence that dropping them was warranted. */
76
+ readonly warmupNsPerSampleMedian: number;
77
+ readonly blocks: number;
78
+ /** For an instrument: the notes held while it was timed. Absent for an effect. */
79
+ readonly voices?: VoicesHeld;
80
+ }
81
+
82
+ /** One rate the cost arm did not time, and why; an instrument's declared capacity is kept when it has one. */
83
+ export interface ClapCostUnmeasured {
84
+ readonly unmeasuredReason: string;
85
+ readonly unmeasuredDetail: string;
86
+ readonly voices?: { readonly capacity: number };
87
+ }
88
+
89
+ export type ClapCostAtRate = ClapCostFigure | ClapCostUnmeasured;
90
+
91
+ export function isClapCostFigure(at: ClapCostAtRate): at is ClapCostFigure {
92
+ return !("unmeasuredReason" in at);
93
+ }
94
+
95
+ /**
96
+ * The cost arm: every rate timed with every parameter swept, an effect fed noise and an instrument
97
+ * holding notes (`notes-held`), each rate's figures or the reason it has none.
98
+ */
99
+ export interface ClapCostReading {
100
+ readonly stimulus: "notes-held" | "noise";
101
+ readonly blockFrames: number;
102
+ readonly warmupBlocks: number;
103
+ /** The percentile `nsPerSampleP95` is taken at. */
104
+ readonly percentile: number;
105
+ readonly perRate: Readonly<Record<string, ClapCostAtRate>>;
106
+ }
107
+
64
108
  /** One plugin's line from `clap-qualify`. */
65
109
  export interface ClapQualifyLine {
66
110
  readonly uri: string;
@@ -72,6 +116,8 @@ export interface ClapQualifyLine {
72
116
  readonly rtSafety: ClapRtSafetyReading;
73
117
  /** The note sweep: the plugin played through its note input, counted apart. */
74
118
  readonly noteSweep: ClapNoteSweepReading;
119
+ /** The cost arm; absent when the plugin did not initialise and activate headless. */
120
+ readonly cost?: ClapCostReading;
75
121
  /**
76
122
  * The pin (spec §11): the `.clap`'s digest by its name in its directory and the layout fingerprint
77
123
  * the prober computed through plugin-hostd's `pin.h`, or the reason it took none.
@@ -114,10 +160,17 @@ function readClap(v: unknown): ClapProbedFacts {
114
160
  if (!isRecord(v)) return bad("no clap facts");
115
161
  const latencyIn = v["latency"];
116
162
  if (!isRecord(latencyIn)) return bad("clap has no latency map");
117
- const latency: Record<string, { declaredFrames: number; measuredFrames: number }> = {};
163
+ const latency: Record<string, ClapLatencyAtRate> = {};
118
164
  for (const [rate, at] of Object.entries(latencyIn)) {
119
165
  if (!/^[1-9]\d*$/.test(rate) || !isRecord(at)) return bad(`clap latency at ${rate}`);
120
- latency[rate] = { declaredFrames: count(at, "declaredFrames", `latency ${rate}`), measuredFrames: count(at, "measuredFrames", `latency ${rate}`) };
166
+ if (at["from"] !== undefined && at["from"] !== "note") return bad(`clap latency at ${rate} from ${JSON.stringify(at["from"])}`);
167
+ if (at["heard"] !== undefined && (at["from"] !== "note" || at["heard"] !== false)) return bad(`clap latency at ${rate} heard`);
168
+ latency[rate] = {
169
+ declaredFrames: count(at, "declaredFrames", `latency ${rate}`),
170
+ measuredFrames: count(at, "measuredFrames", `latency ${rate}`),
171
+ ...(at["from"] === "note" ? { from: "note" as const } : {}),
172
+ ...(at["heard"] === false ? { heard: false } : {}),
173
+ };
121
174
  }
122
175
  return {
123
176
  mainInputChannels: count(v, "mainInputChannels", "clap"),
@@ -175,6 +228,120 @@ function readNoteSweep(v: unknown): ClapNoteSweepReading {
175
228
  };
176
229
  }
177
230
 
231
+ function figure(o: Record<string, unknown>, key: string, where: string): number {
232
+ const v = o[key];
233
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : bad(`${where} has no figure ${key}`);
234
+ }
235
+
236
+ function readVoices(v: unknown, where: string): { held?: number; basis?: VoicesHeld["basis"]; capacity?: number } {
237
+ if (!isRecord(v)) return bad(`${where} voices is not an object`);
238
+ const out: { held?: number; basis?: VoicesHeld["basis"]; capacity?: number } = {};
239
+ if ("held" in v || "basis" in v) {
240
+ const basis = v["basis"];
241
+ if (!isVoiceBasis(basis)) return bad(`${where} voices basis ${JSON.stringify(basis)}`);
242
+ const held = count(v, "held", `${where} voices`);
243
+ if (held === 0) return bad(`${where} holds no note`);
244
+ out.held = held;
245
+ out.basis = basis;
246
+ }
247
+ if ("capacity" in v) out.capacity = count(v, "capacity", `${where} voices`);
248
+ return out;
249
+ }
250
+
251
+ function readCostAtRate(v: unknown, where: string, notes: boolean): ClapCostAtRate {
252
+ if (!isRecord(v)) return bad(`${where} is not an object`);
253
+ const voices = v["voices"] === undefined ? undefined : readVoices(v["voices"], where);
254
+ if (voices !== undefined && !notes) return bad(`${where} holds notes for a plugin fed noise`);
255
+ if (v["unmeasuredReason"] !== undefined) {
256
+ if (voices?.held !== undefined) return bad(`${where} held notes and timed nothing`);
257
+ return {
258
+ unmeasuredReason: str(v, "unmeasuredReason", where),
259
+ unmeasuredDetail: str(v, "unmeasuredDetail", where),
260
+ ...(voices?.capacity === undefined ? {} : { voices: { capacity: voices.capacity } }),
261
+ };
262
+ }
263
+ if (notes && (voices === undefined || voices.held === undefined || voices.basis === undefined)) {
264
+ return bad(`${where} timed an instrument without saying what it held`);
265
+ }
266
+ const blocks = count(v, "blocks", where);
267
+ if (blocks === 0) return bad(`${where} timed no block`);
268
+ return {
269
+ nsPerSampleMedian: figure(v, "nsPerSampleMedian", where),
270
+ nsPerSampleP95: figure(v, "nsPerSampleP95", where),
271
+ nsPerSampleMin: figure(v, "nsPerSampleMin", where),
272
+ nsPerSampleMax: figure(v, "nsPerSampleMax", where),
273
+ warmupNsPerSampleMedian: figure(v, "warmupNsPerSampleMedian", where),
274
+ blocks,
275
+ ...(voices?.held === undefined || voices.basis === undefined
276
+ ? {}
277
+ : { voices: { held: voices.held, basis: voices.basis, ...(voices.capacity === undefined ? {} : { capacity: voices.capacity }) } }),
278
+ };
279
+ }
280
+
281
+ function readCost(v: unknown): ClapCostReading {
282
+ if (!isRecord(v)) return bad("cost is not an object");
283
+ const stimulus = str(v, "stimulus", "cost");
284
+ if (stimulus !== "notes-held" && stimulus !== "noise") return bad(`cost stimulus ${stimulus}`);
285
+ const perRateIn = v["perRate"];
286
+ if (!isRecord(perRateIn)) return bad("cost has no perRate map");
287
+ const perRate: Record<string, ClapCostAtRate> = {};
288
+ for (const [rate, at] of Object.entries(perRateIn)) {
289
+ if (!/^[1-9]\d*$/.test(rate)) return bad(`cost rate ${rate}`);
290
+ perRate[rate] = readCostAtRate(at, `cost at ${rate}`, stimulus === "notes-held");
291
+ }
292
+ return {
293
+ stimulus,
294
+ blockFrames: count(v, "blockFrames", "cost"),
295
+ warmupBlocks: count(v, "warmupBlocks", "cost"),
296
+ percentile: figure(v, "percentile", "cost"),
297
+ perRate,
298
+ };
299
+ }
300
+
301
+ /**
302
+ * The cost arm's figures as the classifier's cost gate reads them, per rate: the median and the P95
303
+ * per sample, and the P95 as a fraction of one core at that rate. A rate the arm did not time is absent.
304
+ */
305
+ export function clapCostPerRate(
306
+ cost: ClapCostReading | undefined,
307
+ ): Record<string, { coreFractionP95: number; nsPerSampleMedian: number; nsPerSampleP95: number; voices?: VoicesHeld }> {
308
+ const out: Record<string, { coreFractionP95: number; nsPerSampleMedian: number; nsPerSampleP95: number; voices?: VoicesHeld }> = {};
309
+ for (const [rate, at] of Object.entries(cost?.perRate ?? {})) {
310
+ if (!isClapCostFigure(at)) continue;
311
+ const fraction = coreFraction(at.nsPerSampleP95, Number(rate));
312
+ if (fraction === undefined) continue;
313
+ out[rate] = {
314
+ coreFractionP95: fraction,
315
+ nsPerSampleMedian: at.nsPerSampleMedian,
316
+ nsPerSampleP95: at.nsPerSampleP95,
317
+ ...(at.voices === undefined ? {} : { voices: at.voices }),
318
+ };
319
+ }
320
+ return out;
321
+ }
322
+
323
+ /**
324
+ * A prober line as the plugin the full classifier (`classifyPluginHosting`) reads: the CLAP facts, the
325
+ * interposer's counts with the note sweep beside them, and the cost arm's figures per rate. The prober
326
+ * measures no stability (that is the soak's), so the caller passes what it has; without it the verdict
327
+ * is `unknown` on stability, never a pass.
328
+ */
329
+ export function qualifiedPluginOfClapLine(line: ClapQualifyLine, stability?: PluginStabilityMeasurement): QualifiedPlugin {
330
+ const { events: _events, ...rtSafety } = line.rtSafety;
331
+ const { events: _noteEvents, ...noteSweep } = line.noteSweep;
332
+ const { nonFinite: _nonFinite, ...clap } = line.clap;
333
+ return {
334
+ format: "clap",
335
+ audioInputs: line.clap.mainInputChannels,
336
+ audioOutputs: line.clap.mainOutputChannels,
337
+ hasMidiIn: line.clap.noteInputs > 0,
338
+ clap,
339
+ rtSafety: { ...rtSafety, noteSweep },
340
+ cpuCost: { perRate: clapCostPerRate(line.cost) },
341
+ ...(stability === undefined ? {} : { stability }),
342
+ };
343
+ }
344
+
178
345
  /** One line of the prober's stdout, or a {@link ClapQualifyError} saying how it differs. */
179
346
  export function parseClapQualifyLine(text: string): ClapQualifyLine {
180
347
  let v: unknown;
@@ -200,6 +367,7 @@ export function parseClapQualifyLine(text: string): ClapQualifyLine {
200
367
  clap: readClap(v["clap"]),
201
368
  rtSafety: readRtSafety(v["rtSafety"]),
202
369
  noteSweep: readNoteSweep(v["noteSweep"]),
370
+ ...(v["cost"] === undefined ? {} : { cost: readCost(v["cost"]) }),
203
371
  integrity: readIntegrity(v["integrity"]) ?? bad("no integrity"),
204
372
  };
205
373
  }
@@ -215,9 +383,11 @@ export function parseClapQualifyOutput(stdout: string): ClapQualifyLine[] {
215
383
  * The document's schema name; a reader refuses any other it does not know. `/2` is `/1` with
216
384
  * `measured.noteSweep` added and the note sweep's reasons folded into `verdict` (a plugin with no
217
385
  * note input gets none, so its `/2` verdict is its `/1` verdict); every `/1` field is unchanged, so
218
- * a `/1` reader of `verdict.rating` and `verdict.rtSafe` reads a `/2` document right.
386
+ * a `/1` reader of `verdict.rating` and `verdict.rtSafe` reads a `/2` document right. `/3` is `/2`
387
+ * with `measured.cost` added (the cost arm, an instrument's with the notes it held), which no
388
+ * verdict reads.
219
389
  */
220
- export const RT_SAFETY_DOCUMENT_SCHEMA = "plugin-qualify.rt-safety/2";
390
+ export const RT_SAFETY_DOCUMENT_SCHEMA = "plugin-qualify.rt-safety/3";
221
391
 
222
392
  /**
223
393
  * The RT-safety verdict of one plugin binary for one console: keyed by the binary's SHA-256 and
@@ -246,6 +416,7 @@ export interface RtSafetyDocument {
246
416
  readonly clap: ClapProbedFacts;
247
417
  readonly rtSafety: ClapRtSafetyReading;
248
418
  readonly noteSweep: ClapNoteSweepReading;
419
+ readonly cost?: ClapCostReading;
249
420
  readonly integrity: IntegrityMeasurement;
250
421
  };
251
422
  }
@@ -288,6 +459,7 @@ export function rtSafetyDocument(line: ClapQualifyLine, profile: HostProfile = O
288
459
  clap: line.clap,
289
460
  rtSafety: line.rtSafety,
290
461
  noteSweep: line.noteSweep,
462
+ ...(line.cost === undefined ? {} : { cost: line.cost }),
291
463
  integrity: line.integrity,
292
464
  },
293
465
  };
@@ -42,8 +42,7 @@ export const OMX_INPROCESS_FEATURES: readonly string[] = [
42
42
  * than on the desk. The list's home is the CLAP library — libomx-clap-core's own
43
43
  * `clap_host_extensions.h`, what its host object offers — and this is its mirror, held equal to the
44
44
  * header at the pinned omx-clap-host (by the console's conformance test), so the verdict never
45
- * promises more or less than the host provides. Nothing else: no `gui`, no `timer-support`, no
46
- * `posix-fd-support`, no `thread-pool`.
45
+ * promises more or less than the host provides. Nothing else: no `gui`.
47
46
  */
48
47
  export const OMX_CLAP_HOST_EXTENSIONS: readonly string[] = [
49
48
  "clap.log",
@@ -52,6 +51,17 @@ export const OMX_CLAP_HOST_EXTENSIONS: readonly string[] = [
52
51
  "clap.params",
53
52
  "clap.audio-ports",
54
53
  "clap.state",
54
+ "clap.note-ports",
55
+ "clap.note-name",
56
+ "clap.voice-info",
57
+ "clap.event-registry",
58
+ "clap.timer-support",
59
+ "clap.posix-fd-support",
60
+ "clap.thread-pool",
61
+ "clap.tail",
62
+ "clap.surround/4",
63
+ "clap.ambisonic/3",
64
+ "clap.audio-ports-config",
55
65
  ];
56
66
 
57
67
  /**
@@ -1,82 +1,187 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-or-later
2
2
  // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
3
  /**
4
- * The hosting verdict's message codes. Codes, never prose: the
5
- * surface renders the operator's locale.
4
+ * The hosting verdict's message codes. Codes, never prose: the surface renders the operator's
5
+ * locale.
6
6
  *
7
- * A UNION list file: `.gitattributes` marks it
8
- * `merge=union`, so two lanes that each add a code merge with both kept. Only the table lives
9
- * here, one `key: 'code',` per line, with keys and codes unique; the registry-hotspots gate
10
- * refuses anything else.
7
+ * omx-clap-core declares every hosting code now (the contract split moved `data/hosting/*.json`
8
+ * there, byte for byte): one `HOSTING_<DIMENSION>_CODES` table per dimension, rendered into the
9
+ * TypeScript modules its package installs under `pkg-config --variable=tsdir omx-clap-core`
10
+ * (BUILDING.md) — `omx-clap-core-vocabulary.js` for the dimensions carried from omx-contract,
11
+ * `omx-clap-core-hosting.js` for `hosting.idle.*`, its own (both with a `.d.ts`). {@link
12
+ * HOSTING_CODE} is those tables flattened into one `key: code` map, loaded at import: a code
13
+ * neither module declares cannot be named here, and a dimension either adds arrives without an
14
+ * edit.
11
15
  */
12
- export const HOSTING_CODE = {
13
- stabilitySoaked: 'hosting.stability.soaked',
14
- stabilityCrashedAttributed: 'hosting.stability.crashed-attributed-plugin',
15
- stabilityCrashedLive: 'hosting.stability.crashed-live-on-this-rig',
16
- stabilityBelowFloor: 'hosting.stability.cycles-below-floor',
17
- stabilityUnsoaked: 'hosting.stability.unsoaked',
18
- stabilitySoakFailedAtRate: 'hosting.stability.soak-failed-at-rate',
19
- stabilityUnmeasured: 'hosting.stability.unmeasured',
20
- stabilityCrashUnattributed: 'hosting.stability.crash-unattributed',
21
- stabilitySpawnsThreads: 'hosting.stability.spawns-threads',
22
- stabilityLinksGuiToolkit: 'hosting.stability.links-gui-toolkit',
23
- rtSafetyClean: 'hosting.rt-safety.clean-under-sweep',
24
- rtSafetyViolations: 'hosting.rt-safety.violations-in-run',
25
- rtSafetyUnmeasured: 'hosting.rt-safety.unmeasured',
26
- rtSafetyNotReproducible: 'hosting.rt-safety.not-reproducible',
27
- // The note sweep: an instrument played under the interposer, a count of its own.
28
- rtSafetyNotesClean: 'hosting.rt-safety.clean-under-notes',
29
- rtSafetyNotesViolations: 'hosting.rt-safety.violations-under-notes',
30
- rtSafetyNotesUnmeasured: 'hosting.rt-safety.notes-unmeasured',
31
- featuresProvided: 'hosting.features.all-provided',
32
- featuresMissing: 'hosting.features.required-not-provided',
33
- featuresDisqualifying: 'hosting.features.host-not-provided',
34
- featuresCv: 'hosting.features.cv-ports',
35
- featuresMidiIn: 'hosting.features.midi-in-fed-empty',
36
- featuresUnscanned: 'hosting.features.scan-predates-field',
37
- costWithinCeiling: 'hosting.cost.within-ceiling',
38
- costAboveCeiling: 'hosting.cost.above-ceiling',
39
- costSpiky: 'hosting.cost.spiky-tail',
40
- costUnmeasured: 'hosting.cost.unmeasured-at-rate-and-quantum',
41
- costFieldsMissing: 'hosting.cost.fields-missing',
42
- latencyResolved: 'hosting.latency.resolved',
43
- latencyUnmeasured: 'hosting.latency.unmeasured',
44
- latencyNonconforming: 'hosting.latency.nonconforming',
45
- latencyParameterDependent: 'hosting.latency.parameter-dependent',
46
- topologyMatched: 'hosting.topology.matched',
47
- topologyNoAudioInput: 'hosting.topology.no-audio-input',
48
- topologyNoAudioOutput: 'hosting.topology.no-audio-output',
49
- topologyExtraInputs: 'hosting.topology.extra-inputs-fed-silence',
50
- topologyInstrument: 'hosting.topology.instrument',
51
- // The CLAP arm: the SAME dimensions fed from CLAP facts,
52
- // minted here where the LV2 table has no code for the fact.
53
- topologyWiderThanStrip: 'hosting.topology.wider-than-strip',
54
- clapLatencyMismatch: 'hosting.clap.latency-mismatch',
55
- clapNotAudioEffect: 'hosting.clap.not-audio-effect',
56
- clapHeadlessFailed: 'hosting.clap.headless-failed',
57
- clapNoteInput: 'hosting.clap.note-input',
58
- clapThreadViolation: 'hosting.clap.thread-violation',
59
- clapProcessError: 'hosting.clap.process-error',
60
- clapUnqualified: 'hosting.clap.unqualified',
61
- clapFormatNotHosted: 'hosting.clap.format-not-hosted',
62
- clapExtensionsNotCovered: 'hosting.clap.extensions-not-covered',
63
- outputNonFinite: 'hosting.output.non-finite',
64
- workerNonconforming: 'hosting.worker.nonconforming',
65
- // The pin (spec §11; one-contract §10.3): a host's refusal at add, by the daemon's code.
66
- integrityUnpinned: 'hosting.integrity.unpinned',
67
- integrityBinaryMismatch: 'hosting.integrity.binary-mismatch',
68
- integrityLayoutMismatch: 'hosting.integrity.layout-mismatch',
69
- overrideIsolated: 'hosting.override.curator-isolated',
70
- hostProvenanceDiffers: 'hosting.provenance.host-differs',
71
- sweepControlDidNotFire: 'hosting.provenance.sweep-control-did-not-fire',
72
- // The five REALISED reasons. Everything above answers what a plugin EARNED; these answer
73
- // what the console DID with that, and the difference between the two is the operator's
74
- // whole question when a slot is not on the fast path.
75
- disabled: 'hosting.disabled',
76
- unjudged: 'hosting.unjudged',
77
- noRealisation: 'hosting.no-realisation',
78
- realisedInProcess: 'hosting.realised.in-process',
79
- // A plugin LINKED into the engine (the static-link registry) runs in-process without a
80
- // verdict: the library's oracles are its proof.
81
- linked: 'hosting.linked',
82
- } as const;
16
+ import { execFileSync } from "node:child_process";
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ /** The two TypeScript modules omx-clap-core's `tsdir` carries, each a `HOSTING_<DIMENSION>_CODES` source. */
22
+ const CLAP_CORE_TS_MODULES = ["omx-clap-core-vocabulary.js", "omx-clap-core-hosting.js"] as const;
23
+
24
+ /** This package's own root, resolved from this module so it follows wherever it is installed. */
25
+ const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
26
+
27
+ /** `native/fetch-deps.sh`'s own checkout of omx-clap-core, for a build with no omx-clap-core-devel installed. */
28
+ const DEPS_TS_DIR = join(PACKAGE_ROOT, "native/deps/omx-clap-core/ts");
29
+
30
+ /** Where `make install` of omx-clap-host puts them (BUILDING.md), for a run with no `pkg-config` binary at all. */
31
+ const DEFAULT_SYSTEM_TS_DIR = "/usr/share/omx-clap-core/ts";
32
+
33
+ /** `pkg-config --variable=tsdir omx-clap-core`, or `undefined` where pkg-config or the package is absent. */
34
+ function pkgConfigTsDir(env: Readonly<Record<string, string | undefined>>): string | undefined {
35
+ try {
36
+ const out = execFileSync(env.PKG_CONFIG ?? "pkg-config", ["--variable=tsdir", "omx-clap-core"], {
37
+ encoding: "utf8",
38
+ stdio: ["ignore", "pipe", "ignore"],
39
+ }).trim();
40
+ return out === "" ? undefined : out;
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Where omx-clap-core's TypeScript modules are looked for, in order: `OMX_CLAP_CORE_TSDIR` when
48
+ * set (and only that), else `pkg-config --variable=tsdir omx-clap-core`, else the checkout
49
+ * `native/fetch-deps.sh` stages, else the path `make install` of omx-clap-host uses by default —
50
+ * a packaged plugin-qualify's runtime has no `pkg-config` binary to ask.
51
+ */
52
+ export function clapCoreTsDirCandidates(env: Readonly<Record<string, string | undefined>> = process.env): readonly string[] {
53
+ const set = env.OMX_CLAP_CORE_TSDIR;
54
+ if (set !== undefined && set !== "") return [set];
55
+ const fromPkgConfig = pkgConfigTsDir(env);
56
+ return [...(fromPkgConfig === undefined ? [] : [fromPkgConfig]), DEPS_TS_DIR, DEFAULT_SYSTEM_TS_DIR];
57
+ }
58
+
59
+ function locateClapCoreTsDir(): string {
60
+ const candidates = clapCoreTsDirCandidates();
61
+ const dir = candidates.find((d) => CLAP_CORE_TS_MODULES.every((m) => existsSync(join(d, m))));
62
+ if (dir === undefined) {
63
+ throw new Error(
64
+ `omx-clap-core's TypeScript modules (${CLAP_CORE_TS_MODULES.join(", ")}) were not found in any of: ` +
65
+ `${candidates.join(", ")}. Install omx-clap-core-devel (pkg-config omx-clap-core), set ` +
66
+ "OMX_CLAP_CORE_TSDIR to its tsdir, or run native/fetch-deps.sh.",
67
+ );
68
+ }
69
+ return dir;
70
+ }
71
+
72
+ /** True for an export name that is one of omx-clap-core's hosting tables. */
73
+ export function isHostingTableName(name: string): boolean {
74
+ return /^HOSTING_[A-Z0-9_]+_CODES$/.test(name);
75
+ }
76
+
77
+ type HostingEntries = Record<string, { readonly code: string }>;
78
+
79
+ async function loadHostingTables(): Promise<Record<string, HostingEntries>> {
80
+ const dir = locateClapCoreTsDir();
81
+ const tables: Record<string, HostingEntries> = {};
82
+ for (const name of CLAP_CORE_TS_MODULES) {
83
+ // The modules are self-contained ES modules in a directory with no package.json: Node 18 reads such a .js as
84
+ // CommonJS and refuses `export`, so the source is imported as a data: module, which is a module on every Node.
85
+ const source = readFileSync(join(dir, name), "utf8");
86
+ const mod: Record<string, unknown> = await import(`data:text/javascript;base64,${Buffer.from(source).toString("base64")}`);
87
+ for (const [exportName, table] of Object.entries(mod)) {
88
+ if (isHostingTableName(exportName)) tables[exportName] = table as HostingEntries;
89
+ }
90
+ }
91
+ return tables;
92
+ }
93
+
94
+ function flatten(tables: Record<string, HostingEntries>): HostingCodeTable {
95
+ const table: Record<string, string> = {};
96
+ for (const [name, entries] of Object.entries(tables)) {
97
+ for (const [key, entry] of Object.entries(entries)) {
98
+ if (key in table) throw new Error(`omx-clap-core declares the hosting key ${key} twice (${name})`);
99
+ table[key] = entry.code;
100
+ }
101
+ }
102
+ return Object.freeze(table) as unknown as HostingCodeTable;
103
+ }
104
+
105
+ /**
106
+ * omx-clap-core's hosting codes, by key, across its two TypeScript modules — hand-kept here
107
+ * (not derived from a static import: the source is a runtime-resolved install directory, not an
108
+ * npm dependency) and held to the real modules by
109
+ * `src/contract-vocabulary-conformance.test.ts`, which turns red on any drift either way.
110
+ */
111
+ export interface HostingCodeTable {
112
+ readonly clapExtensionsNotCovered: "hosting.clap.extensions-not-covered";
113
+ readonly clapFormatNotHosted: "hosting.clap.format-not-hosted";
114
+ readonly clapHeadlessFailed: "hosting.clap.headless-failed";
115
+ readonly clapLatencyMismatch: "hosting.clap.latency-mismatch";
116
+ readonly clapNoteInput: "hosting.clap.note-input";
117
+ readonly clapProcessError: "hosting.clap.process-error";
118
+ readonly clapThreadViolation: "hosting.clap.thread-violation";
119
+ readonly clapUnqualified: "hosting.clap.unqualified";
120
+ readonly costAboveCeiling: "hosting.cost.above-ceiling";
121
+ readonly costFieldsMissing: "hosting.cost.fields-missing";
122
+ readonly costSpiky: "hosting.cost.spiky-tail";
123
+ readonly costUnmeasured: "hosting.cost.unmeasured-at-rate-and-quantum";
124
+ readonly costWithinCeiling: "hosting.cost.within-ceiling";
125
+ readonly featuresAtomInUnfed: "hosting.features.atom-in-unfed";
126
+ readonly featuresDisqualifying: "hosting.features.host-not-provided";
127
+ readonly featuresMidiIn: "hosting.features.midi-in-fed-empty";
128
+ readonly featuresMissing: "hosting.features.missing";
129
+ readonly featuresPortsUnreadable: "hosting.features.ports-unreadable";
130
+ readonly featuresProvided: "hosting.features.all-provided";
131
+ readonly featuresUnscanned: "hosting.features.scan-predates-field";
132
+ readonly idleHoldRefused: "hosting.idle.hold-refused";
133
+ readonly latencyNonconforming: "hosting.latency.nonconforming";
134
+ readonly latencyParameterDependent: "hosting.latency.parameter-dependent";
135
+ readonly latencyResolved: "hosting.latency.resolved";
136
+ readonly latencyUnmeasured: "hosting.latency.unmeasured";
137
+ readonly outputNonFinite: "hosting.output.non-finite";
138
+ readonly overrideIsolated: "hosting.override.curator-isolated";
139
+ readonly patchBusy: "hosting.patch.busy";
140
+ readonly patchNotWritable: "hosting.patch.not-writable";
141
+ readonly patchTooLarge: "hosting.patch.too-large";
142
+ readonly presetNotFound: "hosting.preset.not-found";
143
+ readonly hostProvenanceDiffers: "hosting.provenance.host-differs";
144
+ readonly sweepControlDidNotFire: "hosting.provenance.sweep-control-did-not-fire";
145
+ readonly disabled: "hosting.disabled";
146
+ readonly linked: "hosting.linked";
147
+ readonly noRealisation: "hosting.no-realisation";
148
+ readonly realisedInProcess: "hosting.realised.in-process";
149
+ readonly unjudged: "hosting.unjudged";
150
+ readonly rtSafetyClean: "hosting.rt-safety.clean-under-sweep";
151
+ readonly rtSafetyNotReproducible: "hosting.rt-safety.not-reproducible";
152
+ readonly rtSafetyNotesClean: "hosting.rt-safety.clean-under-notes";
153
+ readonly rtSafetyNotesUnmeasured: "hosting.rt-safety.notes-unmeasured";
154
+ readonly rtSafetyNotesViolations: "hosting.rt-safety.violations-under-notes";
155
+ readonly rtSafetyUnmeasured: "hosting.rt-safety.unmeasured";
156
+ readonly rtSafetyViolations: "hosting.rt-safety.violations-in-run";
157
+ readonly stabilityBelowFloor: "hosting.stability.cycles-below-floor";
158
+ readonly stabilityCrashUnattributed: "hosting.stability.crash-unattributed";
159
+ readonly stabilityCrashedAttributed: "hosting.stability.crashed-attributed-plugin";
160
+ readonly stabilityCrashedLive: "hosting.stability.crashed-live-on-this-rig";
161
+ readonly stabilityLinksGuiToolkit: "hosting.stability.links-gui-toolkit";
162
+ readonly stabilitySoakFailedAtRate: "hosting.stability.soak-failed-at-rate";
163
+ readonly stabilitySoaked: "hosting.stability.soaked";
164
+ readonly stabilitySpawnsThreads: "hosting.stability.spawns-threads";
165
+ readonly stabilityUnmeasured: "hosting.stability.unmeasured";
166
+ readonly stabilityUnsoaked: "hosting.stability.unsoaked";
167
+ readonly stateNotACopy: "hosting.state.not-a-copy";
168
+ readonly statePathOnAudio: "hosting.state.path-on-audio";
169
+ readonly statePathOutside: "hosting.state.path-outside";
170
+ readonly statePathSymlink: "hosting.state.path-symlink";
171
+ readonly statePathTooLong: "hosting.state.path-too-long";
172
+ readonly statePathUnmakeable: "hosting.state.path-unmakeable";
173
+ readonly stateUnreadable: "hosting.state.unreadable";
174
+ readonly topologyAmbisonicConfig: "hosting.topology.ambisonic-config";
175
+ readonly topologyChannelMapUnknown: "hosting.topology.channel-map-unknown";
176
+ readonly topologyInstrument: "hosting.topology.instrument";
177
+ readonly topologyMatched: "hosting.topology.matched";
178
+ readonly topologyNoAudioInput: "hosting.topology.no-audio-input";
179
+ readonly topologyNoAudioOutput: "hosting.topology.no-audio-output";
180
+ readonly topologyPortTooWide: "hosting.topology.port-too-wide";
181
+ readonly topologyPortWithoutChannels: "hosting.topology.port-without-channels";
182
+ readonly topologyTooManyPorts: "hosting.topology.too-many-ports";
183
+ readonly topologyWiderThanStrip: "hosting.topology.wider-than-strip";
184
+ readonly workerNonconforming: "hosting.worker.nonconforming";
185
+ }
186
+
187
+ export const HOSTING_CODE: HostingCodeTable = flatten(await loadHostingTables());