@openmixer/plugin-qualify 0.9.0 → 0.11.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 (81) hide show
  1. package/README.md +59 -9
  2. package/bin/plugin-qualify.mjs +62 -29
  3. package/dist/clap-loader.d.ts +1 -1
  4. package/dist/clap-loader.js +3 -3
  5. package/dist/clap-loader.js.map +1 -1
  6. package/dist/clap-qualify.d.ts +21 -45
  7. package/dist/clap-qualify.js +60 -82
  8. package/dist/clap-qualify.js.map +1 -1
  9. package/dist/cost-fields.d.ts +26 -0
  10. package/dist/cost-fields.js +30 -0
  11. package/dist/cost-fields.js.map +1 -1
  12. package/dist/host-profiles.d.ts +14 -0
  13. package/dist/host-profiles.js +27 -1
  14. package/dist/host-profiles.js.map +1 -1
  15. package/dist/hosting-codes.d.ts +6 -2
  16. package/dist/hosting-codes.js +9 -4
  17. package/dist/hosting-codes.js.map +1 -1
  18. package/dist/hosting-suitability.d.ts +43 -4
  19. package/dist/hosting-suitability.js +46 -8
  20. package/dist/hosting-suitability.js.map +1 -1
  21. package/dist/index.d.ts +5 -0
  22. package/dist/index.js +5 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/judge-cli.d.ts +41 -0
  25. package/dist/judge-cli.js +114 -0
  26. package/dist/judge-cli.js.map +1 -0
  27. package/dist/measure-host.js +2 -2
  28. package/dist/measurement/format.d.ts +1 -1
  29. package/dist/measurement/probe-json.js +1 -0
  30. package/dist/measurement/probe-json.js.map +1 -1
  31. package/dist/measurement/vocabulary.d.ts +1 -1
  32. package/dist/measurement/vocabulary.js +7 -1
  33. package/dist/measurement/vocabulary.js.map +1 -1
  34. package/dist/probe-judge.d.ts +60 -0
  35. package/dist/probe-judge.js +213 -0
  36. package/dist/probe-judge.js.map +1 -0
  37. package/dist/probe-output-fixtures.d.ts +25 -0
  38. package/dist/probe-output-fixtures.js +134 -0
  39. package/dist/probe-output-fixtures.js.map +1 -0
  40. package/dist/probe-output.d.ts +150 -0
  41. package/dist/probe-output.js +369 -0
  42. package/dist/probe-output.js.map +1 -0
  43. package/dist/probe-runner.d.ts +79 -0
  44. package/dist/probe-runner.js +191 -0
  45. package/dist/probe-runner.js.map +1 -0
  46. package/dist/qualification-record.d.ts +80 -0
  47. package/dist/qualification-record.js +85 -0
  48. package/dist/qualification-record.js.map +1 -0
  49. package/dist/qualify-cli.js +1 -1
  50. package/dist/qualify-cli.js.map +1 -1
  51. package/dist/qualify-declarations.d.ts +26 -6
  52. package/dist/qualify-declarations.js +25 -7
  53. package/dist/qualify-declarations.js.map +1 -1
  54. package/fixtures/clap-scan/fake.json +30 -6
  55. package/fixtures/probe-output/memory-limit-refused.json +190 -0
  56. package/fixtures/probe-output/plugin-crash.json +236 -0
  57. package/fixtures/probe-output/success-msi.json +363 -0
  58. package/package.json +7 -2
  59. package/src/clap-loader.ts +3 -2
  60. package/src/clap-qualify.ts +70 -124
  61. package/src/cost-fields.ts +39 -0
  62. package/src/host-profiles.ts +29 -1
  63. package/src/hosting-codes.ts +9 -4
  64. package/src/hosting-suitability.ts +82 -11
  65. package/src/index.ts +5 -0
  66. package/src/judge-cli.ts +133 -0
  67. package/src/measure-host.ts +2 -2
  68. package/src/measurement/format.ts +1 -1
  69. package/src/measurement/probe-json.ts +1 -0
  70. package/src/measurement/vocabulary.ts +8 -1
  71. package/src/probe-judge.ts +264 -0
  72. package/src/probe-output-fixtures.ts +146 -0
  73. package/src/probe-output.ts +505 -0
  74. package/src/probe-runner.ts +244 -0
  75. package/src/qualification-record.ts +142 -0
  76. package/src/qualify-cli.ts +1 -1
  77. package/src/qualify-declarations.ts +28 -8
  78. package/tools/ai-trace-commit-check.sh +114 -0
  79. package/tools/ai-trace-patterns.json +33 -0
  80. package/tools/benchmark.py +13 -1
  81. package/tools/private-refs-check.sh +90 -0
@@ -0,0 +1,363 @@
1
+ {
2
+ "format": "plugin-hostd.probe-output",
3
+ "formatVersion": "1.0",
4
+ "run": {
5
+ "id": "msi-20261017T221500Z-3569027",
6
+ "startedAt": "2026-10-17T22:15:00+02:00",
7
+ "elapsedMs": 184220,
8
+ "complete": true,
9
+ "outcome": "probe.ok",
10
+ "reason": "reference",
11
+ "protocol": {
12
+ "name": "plugin-hostd.probe-protocol",
13
+ "version": 1
14
+ },
15
+ "tools": {
16
+ "runner": "plugin-hostd-probe 0.1.0",
17
+ "child": "plugin-hostd-probe-clap 0.1.0",
18
+ "clapCore": "0.9.0",
19
+ "interposer": "rt-interpose 1"
20
+ },
21
+ "via": "runner"
22
+ },
23
+ "plugin": {
24
+ "format": "clap",
25
+ "id": "org.example.plate",
26
+ "name": "Plate",
27
+ "vendor": "Example",
28
+ "version": "1.4.2",
29
+ "ownerPackage": "example-plate-1.4.2-1.fc44.x86_64"
30
+ },
31
+ "binary": {
32
+ "path": "/usr/lib64/clap/example-plate.clap",
33
+ "sha256": "356902794a4eec2122989e072271ef995968131145db918d9a3084ce582f244b",
34
+ "files": [
35
+ {
36
+ "path": "example-plate.clap",
37
+ "sha256": "356902794a4eec2122989e072271ef995968131145db918d9a3084ce582f244b"
38
+ }
39
+ ],
40
+ "layout": {
41
+ "scheme": "omx-layout/1",
42
+ "sha256": "0a3e01399262321514e05af0575ebb0b207a35567ac262a060fde655ee069805"
43
+ }
44
+ },
45
+ "descriptor": {
46
+ "path": "descriptors/2/clap/org.example.plate@1.4.2-356902794a4e.json",
47
+ "sha256": "cc312bdd5a9aa62796ebcb5a648c354c5ee45a6a99892425495ccb58a31bc44e",
48
+ "bytes": 9412
49
+ },
50
+ "machine": {
51
+ "class": {
52
+ "arch": "x86_64",
53
+ "family": "intel",
54
+ "coreBand": "17-32",
55
+ "clockBand": "4.5GHz+",
56
+ "preemptRt": false,
57
+ "cstatesLimited": true
58
+ },
59
+ "profile": {
60
+ "governor": "performance",
61
+ "turbo": true,
62
+ "smt": true,
63
+ "preempt": "PREEMPT_DYNAMIC",
64
+ "cpuidle": [
65
+ {
66
+ "cpu": 22,
67
+ "state": "POLL",
68
+ "latencyUs": 0,
69
+ "disabled": false,
70
+ "usageDelta": 118
71
+ },
72
+ {
73
+ "cpu": 22,
74
+ "state": "C1_ACPI",
75
+ "latencyUs": 1,
76
+ "disabled": false,
77
+ "usageDelta": 40210
78
+ },
79
+ {
80
+ "cpu": 22,
81
+ "state": "C2_ACPI",
82
+ "latencyUs": 127,
83
+ "disabled": true,
84
+ "usageDelta": 0
85
+ },
86
+ {
87
+ "cpu": 22,
88
+ "state": "C3_ACPI",
89
+ "latencyUs": 1048,
90
+ "disabled": true,
91
+ "usageDelta": 0
92
+ }
93
+ ],
94
+ "meetsProtocol": true,
95
+ "declaredName": "omx-live-rig"
96
+ },
97
+ "pinnedCpus": "22",
98
+ "reference": {
99
+ "rig": "msi",
100
+ "image": "omx-ref-2026-10"
101
+ }
102
+ },
103
+ "offer": {
104
+ "id": "omx-clap-host",
105
+ "clapExtensions": [
106
+ "clap.audio-ports",
107
+ "clap.latency",
108
+ "clap.log",
109
+ "clap.params",
110
+ "clap.state",
111
+ "clap.thread-check",
112
+ "clap.preset-load/2",
113
+ "clap.track-info/1",
114
+ "clap.remote-controls/2"
115
+ ],
116
+ "realtime": "none"
117
+ },
118
+ "limits": {
119
+ "set": "default",
120
+ "applied": [
121
+ {
122
+ "limit": "wall-clock",
123
+ "value": 600,
124
+ "unit": "s",
125
+ "by": "supervisor"
126
+ },
127
+ {
128
+ "limit": "cpu-time",
129
+ "value": 540,
130
+ "unit": "s",
131
+ "by": "rlimit"
132
+ },
133
+ {
134
+ "limit": "memory",
135
+ "value": 2147483648,
136
+ "unit": "bytes",
137
+ "by": "cgroup"
138
+ },
139
+ {
140
+ "limit": "tasks",
141
+ "value": 64,
142
+ "unit": "count",
143
+ "by": "cgroup"
144
+ },
145
+ {
146
+ "limit": "files",
147
+ "value": 256,
148
+ "unit": "count",
149
+ "by": "rlimit"
150
+ },
151
+ {
152
+ "limit": "file-size",
153
+ "value": 67108864,
154
+ "unit": "bytes",
155
+ "by": "rlimit"
156
+ },
157
+ {
158
+ "limit": "core-dump",
159
+ "value": 0,
160
+ "unit": "bytes",
161
+ "by": "rlimit"
162
+ },
163
+ {
164
+ "limit": "rt-priority",
165
+ "value": 0,
166
+ "by": "rlimit"
167
+ },
168
+ {
169
+ "limit": "cpus",
170
+ "value": "22",
171
+ "by": "affinity"
172
+ },
173
+ {
174
+ "limit": "no-new-privs",
175
+ "by": "prctl"
176
+ },
177
+ {
178
+ "limit": "network",
179
+ "value": "none",
180
+ "by": "netns"
181
+ }
182
+ ],
183
+ "unavailable": []
184
+ },
185
+ "steps": [
186
+ {
187
+ "step": "hash",
188
+ "outcome": "probe.ok",
189
+ "elapsedMs": 4
190
+ },
191
+ {
192
+ "step": "instantiate",
193
+ "outcome": "probe.ok",
194
+ "elapsedMs": 31
195
+ },
196
+ {
197
+ "step": "lifecycle",
198
+ "outcome": "probe.ok",
199
+ "elapsedMs": 2210
200
+ },
201
+ {
202
+ "step": "latency",
203
+ "rate": 48000,
204
+ "quantum": 128,
205
+ "outcome": "probe.ok",
206
+ "elapsedMs": 310
207
+ },
208
+ {
209
+ "step": "cost",
210
+ "rate": 48000,
211
+ "quantum": 128,
212
+ "outcome": "probe.ok",
213
+ "elapsedMs": 30020
214
+ },
215
+ {
216
+ "step": "rt-safety",
217
+ "rate": 48000,
218
+ "quantum": 128,
219
+ "outcome": "probe.ok",
220
+ "elapsedMs": 4100
221
+ },
222
+ {
223
+ "step": "latency",
224
+ "rate": 96000,
225
+ "quantum": 128,
226
+ "outcome": "probe.ok",
227
+ "elapsedMs": 305
228
+ },
229
+ {
230
+ "step": "cost",
231
+ "rate": 96000,
232
+ "quantum": 128,
233
+ "outcome": "probe.ok",
234
+ "elapsedMs": 30011
235
+ },
236
+ {
237
+ "step": "rt-safety",
238
+ "rate": 96000,
239
+ "quantum": 128,
240
+ "outcome": "probe.ok",
241
+ "elapsedMs": 4080
242
+ },
243
+ {
244
+ "step": "note-sweep",
245
+ "outcome": "probe.not-attempted",
246
+ "elapsedMs": 0,
247
+ "detail": "no note input"
248
+ }
249
+ ],
250
+ "grid": [
251
+ {
252
+ "rate": 48000,
253
+ "quantum": 128,
254
+ "outcome": "probe.ok",
255
+ "latency": {
256
+ "declaredFrames": 0,
257
+ "measuredFrames": 0,
258
+ "method": "impulse-onset"
259
+ },
260
+ "cost": {
261
+ "unit": "ns-per-sample",
262
+ "median": 41.2,
263
+ "percentile": {
264
+ "p": 95,
265
+ "value": 47.9
266
+ },
267
+ "min": 39.8,
268
+ "max": 112.4,
269
+ "iqr": 3.1,
270
+ "blocks": 11250,
271
+ "warmupBlocks": 64,
272
+ "warmupMedian": 58.0,
273
+ "timingFloorNsPerBlock": 180
274
+ },
275
+ "rtSafety": {
276
+ "interposer": true,
277
+ "swept": true,
278
+ "sweepApplicable": true,
279
+ "blocks": 512,
280
+ "allocations": 0,
281
+ "locks": 0,
282
+ "syscalls": 0,
283
+ "threadViolations": 0,
284
+ "processErrors": 0,
285
+ "events": []
286
+ }
287
+ },
288
+ {
289
+ "rate": 96000,
290
+ "quantum": 128,
291
+ "outcome": "probe.ok",
292
+ "latency": {
293
+ "declaredFrames": 0,
294
+ "measuredFrames": 0,
295
+ "method": "impulse-onset"
296
+ },
297
+ "cost": {
298
+ "unit": "ns-per-sample",
299
+ "median": 40.7,
300
+ "percentile": {
301
+ "p": 95,
302
+ "value": 46.3
303
+ },
304
+ "min": 39.1,
305
+ "max": 98.0,
306
+ "iqr": 2.8,
307
+ "blocks": 22500,
308
+ "warmupBlocks": 64,
309
+ "timingFloorNsPerBlock": 180,
310
+ "samples": {
311
+ "name": "cost-96000-128",
312
+ "path": "probes/org.example.plate@1.4.2-356902794a4e.cost-96000-128.bin",
313
+ "sha256": "5f649130dafd22f1edc70441f9455415d0856f0f7d26cf7274463b82ac2c5813",
314
+ "bytes": 180000
315
+ }
316
+ },
317
+ "rtSafety": {
318
+ "interposer": true,
319
+ "swept": true,
320
+ "sweepApplicable": true,
321
+ "blocks": 512,
322
+ "allocations": 0,
323
+ "locks": 0,
324
+ "syscalls": 0,
325
+ "threadViolations": 0,
326
+ "processErrors": 0,
327
+ "events": []
328
+ }
329
+ }
330
+ ],
331
+ "behaviour": {
332
+ "lifecycle": {
333
+ "cycles": 30,
334
+ "instantiated": 30,
335
+ "failed": 0
336
+ },
337
+ "threads": {
338
+ "before": 2,
339
+ "afterFirstInstantiate": 2,
340
+ "afterAllFreed": 2
341
+ },
342
+ "memory": {
343
+ "rssStartKb": 9120,
344
+ "rssMaxKb": 14988
345
+ },
346
+ "features": {
347
+ "extensionsAsked": [
348
+ "clap.audio-ports",
349
+ "clap.params",
350
+ "clap.state",
351
+ "clap.latency"
352
+ ]
353
+ }
354
+ },
355
+ "logs": [
356
+ {
357
+ "name": "stderr",
358
+ "path": "probes/org.example.plate@1.4.2-356902794a4e.stderr.log",
359
+ "sha256": "c46928bc87de23b9a8bd235aa1fe67ccb1542c8389ad3f02bf4ab5cff506e98f",
360
+ "bytes": 212
361
+ }
362
+ ]
363
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmixer/plugin-qualify",
3
- "version": "0.9.0",
3
+ "version": "0.11.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",
@@ -67,6 +67,7 @@
67
67
  "dist",
68
68
  "src",
69
69
  "!src/**/*.test.ts",
70
+ "!src/**/*.support.*",
70
71
  "tools",
71
72
  "!tools/test_*.py",
72
73
  "!**/__pycache__",
@@ -81,7 +82,8 @@
81
82
  "test": "vitest run src",
82
83
  "lint": "tsc -p tsconfig.lint.json",
83
84
  "test:py": "python3 -m pytest tools -q",
84
- "check:generated": "node tools/gen-python-declarations.mjs --check"
85
+ "check:generated": "node tools/gen-python-declarations.mjs --check",
86
+ "check:private-refs": "tools/private-refs-check.sh && tools/private-refs-check.sh --self-test"
85
87
  },
86
88
  "devDependencies": {
87
89
  "@types/node": "^24.9.0",
@@ -92,5 +94,8 @@
92
94
  "publishConfig": {
93
95
  "access": "public",
94
96
  "provenance": true
97
+ },
98
+ "dependencies": {
99
+ "@openmixer/omx-contract": "^2.16.0"
95
100
  }
96
101
  }
@@ -25,6 +25,7 @@ import {
25
25
  CLAP_SCAN_TOOL_DEFAULT,
26
26
  CLAP_SCAN_TOOL_ENV,
27
27
  CLAP_SYSTEM_DIR,
28
+ CLAP_SYSTEM_DIR_ENV,
28
29
  CLAP_USER_DIR,
29
30
  } from "./qualify-declarations.js";
30
31
 
@@ -428,11 +429,11 @@ export function clapLoader(options: ClapLoaderOptions = {}): PluginLoader {
428
429
 
429
430
  /**
430
431
  * The directories `--all` walks, in order and once each: every non-empty entry of `CLAP_PATH`, then
431
- * `~/.clap`, then `/usr/lib/clap`.
432
+ * `~/.clap`, then `/usr/lib/clap` (or the directory `OMX_CLAP_SYSTEM_DIR` names).
432
433
  */
433
434
  export function clapSearchPath(env: Readonly<Record<string, string | undefined>>, home: string): string[] {
434
435
  const named = (env[CLAP_PATH_ENV] ?? "").split(":").filter((d) => d !== "");
435
- return [...new Set([...named, join(home, CLAP_USER_DIR), CLAP_SYSTEM_DIR].map((d) => resolve(d)))];
436
+ return [...new Set([...named, join(home, CLAP_USER_DIR), env[CLAP_SYSTEM_DIR_ENV] || CLAP_SYSTEM_DIR].map((d) => resolve(d)))];
436
437
  }
437
438
 
438
439
  export interface ClapDiscoverOptions extends ClapLoaderOptions {
@@ -10,27 +10,26 @@
10
10
  * read, a prober that is absent, crashed or timed out, is a {@link ClapQualifyError}: nothing was
11
11
  * measured, and nothing is rated.
12
12
  */
13
- import { spawnSync } from "node:child_process";
14
13
  import { existsSync } from "node:fs";
15
14
 
16
- import {
17
- CLAP_QUALIFY_TIMEOUT_S,
18
- CLAP_QUALIFY_TOOL_NAME,
19
- CLAP_SWEEP_BLOCKS,
20
- CLAP_SWEEP_FRAMES,
21
- RT_INTERPOSE_NAME,
22
- } from "./qualify-declarations.js";
15
+ import { CLAP_QUALIFY_TOOL_NAME, RT_INTERPOSE_NAME } from "./qualify-declarations.js";
23
16
  import {
24
17
  HOSTING_CODE,
25
18
  clapRtReasons,
26
19
  contributedHostingRating,
20
+ noteSweepReasons,
27
21
  rtSafetyReasons,
28
22
  type HostingReason,
29
23
  type PluginClapMeasurement,
24
+ type PluginNoteSweepMeasurement,
30
25
  type PluginRtSafetyMeasurement,
31
26
  } from "./hosting-suitability.js";
32
- import { OPENMIXER_CONSOLE_PROFILE, type HostProfile } from "./host-profiles.js";
33
- import type { PluginTarget } from "./loader.js";
27
+ import {
28
+ OMX_CLAP_HOST_CONFIGURED_EXTENSIONS,
29
+ OMX_CLAP_HOST_EXTENSIONS,
30
+ OPENMIXER_CONSOLE_PROFILE,
31
+ type HostProfile,
32
+ } from "./host-profiles.js";
34
33
  import type { QualifyVerdict } from "./qualify-cli.js";
35
34
  import { RATING_SEVERITY, worstRating, type SuitabilityRating } from "./rating.js";
36
35
  import { halfPopulatedRefusal, locateNativeTools } from "./tool-paths.js";
@@ -51,6 +50,12 @@ export interface ClapRtSafetyReading extends PluginRtSafetyMeasurement {
51
50
  readonly events?: readonly string[];
52
51
  }
53
52
 
53
+ /** What the note sweep counted; `applicable: false` is a plugin the prober did not play. */
54
+ export interface ClapNoteSweepReading extends PluginNoteSweepMeasurement {
55
+ /** The first events the interposer recorded inside `process()` while the plugin was played. */
56
+ readonly events?: readonly string[];
57
+ }
58
+
54
59
  /** The CLAP facts as the prober printed them: the measurement, and whether any output was not finite. */
55
60
  export interface ClapProbedFacts extends PluginClapMeasurement {
56
61
  readonly nonFinite: boolean;
@@ -65,6 +70,8 @@ export interface ClapQualifyLine {
65
70
  readonly hostExtensions: readonly string[];
66
71
  readonly clap: ClapProbedFacts;
67
72
  readonly rtSafety: ClapRtSafetyReading;
73
+ /** The note sweep: the plugin played through its note input, counted apart. */
74
+ readonly noteSweep: ClapNoteSweepReading;
68
75
  /**
69
76
  * The pin (spec §11): the `.clap`'s digest by its name in its directory and the layout fingerprint
70
77
  * the prober computed through plugin-hostd's `pin.h`, or the reason it took none.
@@ -143,6 +150,31 @@ function readRtSafety(v: unknown): ClapRtSafetyReading {
143
150
  };
144
151
  }
145
152
 
153
+ function readNoteSweep(v: unknown): ClapNoteSweepReading {
154
+ if (!isRecord(v)) return bad("no noteSweep");
155
+ if (!bool(v, "applicable", "noteSweep")) return { applicable: false };
156
+ const dialect = str(v, "dialect", "noteSweep");
157
+ if (dialect !== "clap" && dialect !== "midi") return bad(`noteSweep dialect ${dialect}`);
158
+ const played = {
159
+ applicable: true,
160
+ dialect,
161
+ blocks: count(v, "blocks", "noteSweep"),
162
+ blockFrames: count(v, "blockFrames", "noteSweep"),
163
+ noteEvents: count(v, "noteEvents", "noteSweep"),
164
+ processErrors: count(v, "processErrors", "noteSweep"),
165
+ nonFinite: bool(v, "nonFinite", "noteSweep"),
166
+ } as const;
167
+ if (!bool(v, "interposer", "noteSweep")) return { ...played, interposer: false };
168
+ return {
169
+ ...played,
170
+ interposer: true,
171
+ allocationsInRun: count(v, "allocationsInRun", "noteSweep"),
172
+ locksInRun: count(v, "locksInRun", "noteSweep"),
173
+ syscallsInRun: count(v, "syscallsInRun", "noteSweep"),
174
+ events: strings(v, "events", "noteSweep"),
175
+ };
176
+ }
177
+
146
178
  /** One line of the prober's stdout, or a {@link ClapQualifyError} saying how it differs. */
147
179
  export function parseClapQualifyLine(text: string): ClapQualifyLine {
148
180
  let v: unknown;
@@ -167,6 +199,7 @@ export function parseClapQualifyLine(text: string): ClapQualifyLine {
167
199
  hostExtensions: strings(v, "hostExtensions", "the line"),
168
200
  clap: readClap(v["clap"]),
169
201
  rtSafety: readRtSafety(v["rtSafety"]),
202
+ noteSweep: readNoteSweep(v["noteSweep"]),
170
203
  integrity: readIntegrity(v["integrity"]) ?? bad("no integrity"),
171
204
  };
172
205
  }
@@ -178,8 +211,13 @@ export function parseClapQualifyOutput(stdout: string): ClapQualifyLine[] {
178
211
 
179
212
  // --- the verdict document ------------------------------------------------------------------------
180
213
 
181
- /** The document's schema name; a reader refuses any other. */
182
- export const RT_SAFETY_DOCUMENT_SCHEMA = "plugin-qualify.rt-safety/1";
214
+ /**
215
+ * The document's schema name; a reader refuses any other it does not know. `/2` is `/1` with
216
+ * `measured.noteSweep` added and the note sweep's reasons folded into `verdict` (a plugin with no
217
+ * 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.
219
+ */
220
+ export const RT_SAFETY_DOCUMENT_SCHEMA = "plugin-qualify.rt-safety/2";
183
221
 
184
222
  /**
185
223
  * The RT-safety verdict of one plugin binary for one console: keyed by the binary's SHA-256 and
@@ -207,6 +245,7 @@ export interface RtSafetyDocument {
207
245
  readonly hostExtensions: readonly string[];
208
246
  readonly clap: ClapProbedFacts;
209
247
  readonly rtSafety: ClapRtSafetyReading;
248
+ readonly noteSweep: ClapNoteSweepReading;
210
249
  readonly integrity: IntegrityMeasurement;
211
250
  };
212
251
  }
@@ -225,7 +264,7 @@ export function rtSafetyDocument(line: ClapQualifyLine, profile: HostProfile = O
225
264
  offered === null
226
265
  ? [{ dimension: "rtSafety", kind: "objective", rating: "unknown", code: HOSTING_CODE.clapFormatNotHosted, params: {} }]
227
266
  : offeredAsProfile
228
- ? [...rtSafetyReasons(line.rtSafety), ...clapRtReasons(line.clap)]
267
+ ? [...rtSafetyReasons(line.rtSafety), ...clapRtReasons(line.clap), ...noteSweepReasons(line.noteSweep, line.clap.noteInputs)]
229
268
  : [
230
269
  {
231
270
  dimension: "rtSafety",
@@ -244,7 +283,13 @@ export function rtSafetyDocument(line: ClapQualifyLine, profile: HostProfile = O
244
283
  plugin: { uri: line.uri, format: "clap", binary: line.binary },
245
284
  console: profile.id,
246
285
  verdict: { rating, rtSafe: rating === "suitable", ...(rating === "suitable" || deciding === undefined ? {} : { deciding }), reasons },
247
- measured: { hostExtensions: line.hostExtensions, clap: line.clap, rtSafety: line.rtSafety, integrity: line.integrity },
286
+ measured: {
287
+ hostExtensions: line.hostExtensions,
288
+ clap: line.clap,
289
+ rtSafety: line.rtSafety,
290
+ noteSweep: line.noteSweep,
291
+ integrity: line.integrity,
292
+ },
248
293
  };
249
294
  }
250
295
 
@@ -265,45 +310,7 @@ export function rtSafetyDocumentName(doc: RtSafetyDocument): string {
265
310
  return `${doc.plugin.binary.sha256}.${doc.console}.rt-safety.json`;
266
311
  }
267
312
 
268
- // --- running the prober --------------------------------------------------------------------------
269
-
270
- /** What a run of the prober gave back; `error` is a spawn failure, `signal` a death. */
271
- export interface ClapQualifyRunResult {
272
- readonly status: number | null;
273
- readonly signal?: string | null;
274
- readonly stdout: string;
275
- readonly stderr?: string;
276
- readonly error?: Error;
277
- }
278
-
279
- export type ClapQualifyRun = (
280
- tool: string,
281
- args: readonly string[],
282
- env: Readonly<Record<string, string | undefined>>,
283
- timeoutMs: number,
284
- ) => ClapQualifyRunResult;
285
-
286
- const spawnQualify: ClapQualifyRun = (tool, args, env, timeoutMs) => {
287
- const r = spawnSync(tool, [...args], { encoding: "utf8", env: { ...env }, timeout: timeoutMs, maxBuffer: 64 << 20 });
288
- return {
289
- status: r.status,
290
- signal: r.signal,
291
- stdout: r.stdout ?? "",
292
- stderr: r.stderr ?? "",
293
- ...(r.error === undefined ? {} : { error: r.error }),
294
- };
295
- };
296
-
297
- export interface ClapQualifyOptions {
298
- /** The rates the plugin is activated and swept at. */
299
- readonly rates: readonly number[];
300
- /** Runs the prober; the default spawns it. */
301
- readonly run?: ClapQualifyRun;
302
- /** Where the two settings are read from; the default is `process.env`. */
303
- readonly env?: Readonly<Record<string, string | undefined>>;
304
- /** Whether a file exists; the default asks the filesystem. */
305
- readonly exists?: (path: string) => boolean;
306
- }
313
+ // --- the prober and the interposer a run uses ---------------------------------------------------
307
314
 
308
315
  /**
309
316
  * The prober and the interposer this run uses: each its setting, else both from one place, the
@@ -327,76 +334,15 @@ export function clapQualifyTools(
327
334
  }
328
335
 
329
336
  /**
330
- * Qualify the plugins of one `.clap` (all of them, or the one `id` names) under the interposer.
331
- * Absent tools, a crash, a timeout, a load failure or an unreadable line throw: none of them is a
332
- * measurement.
337
+ * What the prober must offer beyond the console's extensions to measure for `profile`: the extra
338
+ * extensions of its list, when the prober can offer each and the list holds the console's; `undefined`
339
+ * for a profile the prober cannot stand in for (no CLAP path, or an extension it cannot offer), which
340
+ * is judged from the console's run and reads `unknown` there.
333
341
  */
334
- export function qualifyClap(binaryPath: string, id: string | undefined, options: ClapQualifyOptions): ClapQualifyLine[] {
335
- const env = options.env ?? process.env;
336
- const { tool, interposer } = clapQualifyTools(env, options.exists ?? existsSync);
337
- const args = [
338
- "--rates",
339
- options.rates.join(","),
340
- "--block",
341
- String(CLAP_SWEEP_FRAMES),
342
- "--blocks",
343
- String(CLAP_SWEEP_BLOCKS),
344
- binaryPath,
345
- ...(id === undefined ? [] : [id]),
346
- ];
347
- const r = (options.run ?? spawnQualify)(tool, args, { ...env, LD_PRELOAD: interposer }, CLAP_QUALIFY_TIMEOUT_S * 1000);
348
- if (r.error !== undefined) throw new ClapQualifyError(`cannot run ${tool}: ${r.error.message}`);
349
- if (r.signal) throw new ClapQualifyError(`${binaryPath} killed the prober (${r.signal})`);
350
- if (r.status !== 0) throw new ClapQualifyError(`${tool} exited ${String(r.status)} on ${binaryPath}: ${(r.stderr ?? "").trim()}`);
351
- const lines = parseClapQualifyOutput(r.stdout);
352
- if (lines.length === 0) throw new ClapQualifyError(`${tool} printed nothing for ${binaryPath}`);
353
- return lines;
354
- }
355
-
356
- // --- the CLI's CLAP pass -------------------------------------------------------------------------
357
-
358
- /** What the CLAP pass of one run produced: a document per plugin and console, and a row per verdict. */
359
- export interface ClapQualifyPass {
360
- readonly documents: readonly RtSafetyDocument[];
361
- readonly verdicts: readonly QualifyVerdict[];
362
- /** Each target that could not be measured, with why; its verdict rows read `unknown`, unmeasured. */
363
- readonly failures: readonly { readonly uri: string; readonly problem: string }[];
364
- }
365
-
366
- /**
367
- * Qualify every CLAP target, one prober run per target (a crash takes only that plugin), and judge
368
- * each line under every host profile. A target without a binary path (a bare `urn:clap:` id) has
369
- * nothing to load and is a failure, never a pass.
370
- */
371
- export function qualifyClapTargets(
372
- targets: readonly PluginTarget[],
373
- options: ClapQualifyOptions & { readonly profiles?: readonly HostProfile[] },
374
- ): ClapQualifyPass {
375
- const profiles = options.profiles ?? [OPENMIXER_CONSOLE_PROFILE];
376
- const documents: RtSafetyDocument[] = [];
377
- const verdicts: QualifyVerdict[] = [];
378
- const failures: { uri: string; problem: string }[] = [];
379
- for (const target of targets) {
380
- const id = target.uri.startsWith("urn:clap:") ? target.uri.slice("urn:clap:".length) : undefined;
381
- let lines: ClapQualifyLine[];
382
- try {
383
- if (target.binaryPath === undefined) throw new ClapQualifyError(`${target.uri}: no .clap to load; name the file or a directory holding it`);
384
- lines = qualifyClap(target.binaryPath, id, options);
385
- } catch (e) {
386
- if (!(e instanceof ClapQualifyError)) throw e;
387
- failures.push({ uri: target.uri, problem: e.message });
388
- for (const profile of profiles) {
389
- verdicts.push({ uri: target.uri, host: profile.id, measured: false, rating: "unknown", qualified: false, deciding: "not-measured" });
390
- }
391
- continue;
392
- }
393
- for (const line of lines) {
394
- for (const profile of profiles) {
395
- const doc = rtSafetyDocument(line, profile);
396
- documents.push(doc);
397
- verdicts.push(rtSafetyVerdict(doc));
398
- }
399
- }
400
- }
401
- return { documents, verdicts, failures };
342
+ export function proberOffer(profile: HostProfile): readonly string[] | undefined {
343
+ const wanted = profile.clapExtensions;
344
+ if (wanted === null || !OMX_CLAP_HOST_EXTENSIONS.every((e) => wanted.includes(e))) return undefined;
345
+ const extra = wanted.filter((e) => !OMX_CLAP_HOST_EXTENSIONS.includes(e));
346
+ if (!extra.every((e) => OMX_CLAP_HOST_CONFIGURED_EXTENSIONS.includes(e))) return undefined;
347
+ return OMX_CLAP_HOST_CONFIGURED_EXTENSIONS.filter((e) => extra.includes(e));
402
348
  }