@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.
- package/README.md +59 -9
- package/bin/plugin-qualify.mjs +62 -29
- package/dist/clap-loader.d.ts +1 -1
- package/dist/clap-loader.js +3 -3
- package/dist/clap-loader.js.map +1 -1
- package/dist/clap-qualify.d.ts +21 -45
- package/dist/clap-qualify.js +60 -82
- package/dist/clap-qualify.js.map +1 -1
- package/dist/cost-fields.d.ts +26 -0
- package/dist/cost-fields.js +30 -0
- package/dist/cost-fields.js.map +1 -1
- package/dist/host-profiles.d.ts +14 -0
- package/dist/host-profiles.js +27 -1
- package/dist/host-profiles.js.map +1 -1
- package/dist/hosting-codes.d.ts +6 -2
- package/dist/hosting-codes.js +9 -4
- package/dist/hosting-codes.js.map +1 -1
- package/dist/hosting-suitability.d.ts +43 -4
- package/dist/hosting-suitability.js +46 -8
- package/dist/hosting-suitability.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/judge-cli.d.ts +41 -0
- package/dist/judge-cli.js +114 -0
- package/dist/judge-cli.js.map +1 -0
- package/dist/measure-host.js +2 -2
- package/dist/measurement/format.d.ts +1 -1
- package/dist/measurement/probe-json.js +1 -0
- package/dist/measurement/probe-json.js.map +1 -1
- package/dist/measurement/vocabulary.d.ts +1 -1
- package/dist/measurement/vocabulary.js +7 -1
- package/dist/measurement/vocabulary.js.map +1 -1
- package/dist/probe-judge.d.ts +60 -0
- package/dist/probe-judge.js +213 -0
- package/dist/probe-judge.js.map +1 -0
- package/dist/probe-output-fixtures.d.ts +25 -0
- package/dist/probe-output-fixtures.js +134 -0
- package/dist/probe-output-fixtures.js.map +1 -0
- package/dist/probe-output.d.ts +150 -0
- package/dist/probe-output.js +369 -0
- package/dist/probe-output.js.map +1 -0
- package/dist/probe-runner.d.ts +79 -0
- package/dist/probe-runner.js +191 -0
- package/dist/probe-runner.js.map +1 -0
- package/dist/qualification-record.d.ts +80 -0
- package/dist/qualification-record.js +85 -0
- package/dist/qualification-record.js.map +1 -0
- package/dist/qualify-cli.js +1 -1
- package/dist/qualify-cli.js.map +1 -1
- package/dist/qualify-declarations.d.ts +26 -6
- package/dist/qualify-declarations.js +25 -7
- package/dist/qualify-declarations.js.map +1 -1
- package/fixtures/clap-scan/fake.json +30 -6
- package/fixtures/probe-output/memory-limit-refused.json +190 -0
- package/fixtures/probe-output/plugin-crash.json +236 -0
- package/fixtures/probe-output/success-msi.json +363 -0
- package/package.json +7 -2
- package/src/clap-loader.ts +3 -2
- package/src/clap-qualify.ts +70 -124
- package/src/cost-fields.ts +39 -0
- package/src/host-profiles.ts +29 -1
- package/src/hosting-codes.ts +9 -4
- package/src/hosting-suitability.ts +82 -11
- package/src/index.ts +5 -0
- package/src/judge-cli.ts +133 -0
- package/src/measure-host.ts +2 -2
- package/src/measurement/format.ts +1 -1
- package/src/measurement/probe-json.ts +1 -0
- package/src/measurement/vocabulary.ts +8 -1
- package/src/probe-judge.ts +264 -0
- package/src/probe-output-fixtures.ts +146 -0
- package/src/probe-output.ts +505 -0
- package/src/probe-runner.ts +244 -0
- package/src/qualification-record.ts +142 -0
- package/src/qualify-cli.ts +1 -1
- package/src/qualify-declarations.ts +28 -8
- package/tools/ai-trace-commit-check.sh +114 -0
- package/tools/ai-trace-patterns.json +33 -0
- package/tools/benchmark.py +13 -1
- package/tools/private-refs-check.sh +90 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* Judging from a probe output. The probe output holds facts (`probe-output.ts`); this module turns
|
|
5
|
+
* them into the facts the one judge already reads and lets that judge speak. There is no second
|
|
6
|
+
* judge: a CLAP plugin's verdict is {@link rtSafetyDocument}'s fold of the same reasons it folds
|
|
7
|
+
* for a prober line, an LV2 plugin's is {@link rtSafetyReasons}, and cost is read by
|
|
8
|
+
* {@link hostingCostReasons} at the host's rate and quantum.
|
|
9
|
+
*
|
|
10
|
+
* What a probe output does not carry, this module does not invent:
|
|
11
|
+
*
|
|
12
|
+
* - `noteInputs` is the descriptor's fact. Without one the run's note sweep stands in: a plugin
|
|
13
|
+
* whose sweep was applicable has at least one note input; one whose sweep was not applicable is
|
|
14
|
+
* read as having none, so an instrument that could not be played reads as an effect. The caller
|
|
15
|
+
* that holds the descriptor passes the real count.
|
|
16
|
+
* - The note sweep's block count is the parameter sweep's: both run in one prober process at one
|
|
17
|
+
* block count, and the schema holds the note sweep's counts but not its blocks.
|
|
18
|
+
*/
|
|
19
|
+
import { DEFAULT_HOSTING_POLICY } from "./qualify-declarations.js";
|
|
20
|
+
import {
|
|
21
|
+
rtSafetyDocument,
|
|
22
|
+
type ClapNoteSweepReading,
|
|
23
|
+
type ClapQualifyLine,
|
|
24
|
+
type ClapRtSafetyReading,
|
|
25
|
+
type RtSafetyDocument,
|
|
26
|
+
} from "./clap-qualify.js";
|
|
27
|
+
import { gridCostReadings } from "./cost-fields.js";
|
|
28
|
+
import {
|
|
29
|
+
hostingCostReasons,
|
|
30
|
+
rtSafetyReasons,
|
|
31
|
+
contributedHostingRating,
|
|
32
|
+
type HostingReason,
|
|
33
|
+
} from "./hosting-suitability.js";
|
|
34
|
+
import type { HostProfile } from "./host-profiles.js";
|
|
35
|
+
import { PROBE_NOT_ATTEMPTED, PROBE_OK, type ProbeGridPoint, type ProbeOutput } from "./probe-output.js";
|
|
36
|
+
import type { QualifyVerdict } from "./qualify-cli.js";
|
|
37
|
+
import { RATING_SEVERITY, worstRating, type SuitabilityRating } from "./rating.js";
|
|
38
|
+
import type { IntegrityMeasurement } from "./measurement/format.js";
|
|
39
|
+
|
|
40
|
+
/** Facts of the descriptor, which the probe output repeats none of. */
|
|
41
|
+
export interface JudgeFacts {
|
|
42
|
+
/** Note input ports the plugin declares. */
|
|
43
|
+
readonly noteInputs?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A sum of one count over the points, or `undefined` when a point lacks it: a missing count is not zero. */
|
|
47
|
+
function total(points: readonly ProbeGridPoint[], count: (p: ProbeGridPoint) => number | undefined): number | undefined {
|
|
48
|
+
let sum = 0;
|
|
49
|
+
for (const p of points) {
|
|
50
|
+
const n = count(p);
|
|
51
|
+
if (n === undefined) return undefined;
|
|
52
|
+
sum += n;
|
|
53
|
+
}
|
|
54
|
+
return sum;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The points whose run ended well: a point that crashed measured nothing. */
|
|
58
|
+
function okPoints(output: ProbeOutput): ProbeGridPoint[] {
|
|
59
|
+
return output.grid.filter((p) => p.outcome === PROBE_OK);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The interposer's counts over every grid point that carries them. The interposer must have been
|
|
64
|
+
* loaded at every such point; the sweep counts as done when every point swept, and as applicable
|
|
65
|
+
* when any did. A count a point lacks leaves the total absent, and the judge reads that as
|
|
66
|
+
* unmeasured.
|
|
67
|
+
*/
|
|
68
|
+
export function rtSafetyOfProbeOutput(output: ProbeOutput): ClapRtSafetyReading {
|
|
69
|
+
const points = okPoints(output).filter((p) => p.rtSafety !== undefined);
|
|
70
|
+
if (points.length === 0 || points.some((p) => p.rtSafety?.interposer !== true)) return { interposer: false };
|
|
71
|
+
const reading = (p: ProbeGridPoint) => p.rtSafety;
|
|
72
|
+
const allocations = total(points, (p) => reading(p)?.allocations);
|
|
73
|
+
const locks = total(points, (p) => reading(p)?.locks);
|
|
74
|
+
const syscalls = total(points, (p) => reading(p)?.syscalls);
|
|
75
|
+
return {
|
|
76
|
+
interposer: true,
|
|
77
|
+
swept: points.every((p) => reading(p)?.swept === true),
|
|
78
|
+
sweepApplicable: points.some((p) => reading(p)?.sweepApplicable === true),
|
|
79
|
+
blocks: total(points, (p) => reading(p)?.blocks ?? 0) ?? 0,
|
|
80
|
+
blockFrames: points[0]?.quantum ?? 0,
|
|
81
|
+
...(allocations === undefined ? {} : { allocationsInRun: allocations }),
|
|
82
|
+
...(locks === undefined ? {} : { locksInRun: locks }),
|
|
83
|
+
...(syscalls === undefined ? {} : { syscallsInRun: syscalls }),
|
|
84
|
+
events: points.flatMap((p) => reading(p)?.events ?? []),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function noteSweepOfProbeOutput(output: ProbeOutput, rt: ClapRtSafetyReading): ClapNoteSweepReading {
|
|
89
|
+
const played = okPoints(output).find((p) => p.noteSweep?.applicable === true)?.noteSweep;
|
|
90
|
+
if (played === undefined) return { applicable: false };
|
|
91
|
+
const dialect: { dialect?: "clap" | "midi" } = {};
|
|
92
|
+
if (played.dialect === "clap" || played.dialect === "midi") dialect.dialect = played.dialect;
|
|
93
|
+
const base = { applicable: true, ...dialect, blocks: rt.blocks ?? 0, blockFrames: rt.blockFrames ?? 0, noteEvents: 0, processErrors: 0, nonFinite: false };
|
|
94
|
+
if (!rt.interposer) return { ...base, interposer: false };
|
|
95
|
+
return {
|
|
96
|
+
...base,
|
|
97
|
+
interposer: true,
|
|
98
|
+
...(played.allocations === undefined ? {} : { allocationsInRun: played.allocations }),
|
|
99
|
+
...(played.locks === undefined ? {} : { locksInRun: played.locks }),
|
|
100
|
+
...(played.syscalls === undefined ? {} : { syscallsInRun: played.syscalls }),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function integrityOfProbeOutput(output: ProbeOutput): IntegrityMeasurement {
|
|
105
|
+
const { files, layout, unpinnable } = output.binary;
|
|
106
|
+
if (layout === undefined || files.length === 0) {
|
|
107
|
+
return { unmeasuredReason: unpinnable === undefined ? "probe-error" : "unpinnable", unmeasuredDetail: unpinnable ?? "the probe output holds no layout pin" };
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
...(output.plugin.version === undefined ? {} : { version: output.plugin.version }),
|
|
111
|
+
files: files.slice().sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
|
|
112
|
+
layout,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A CLAP probe output as the prober's line it stands for, so that {@link rtSafetyDocument} judges it.
|
|
118
|
+
* The facts the line holds and a probe output does not (the port layout, the features) are not read
|
|
119
|
+
* by the rtSafety fold and are left empty here.
|
|
120
|
+
*/
|
|
121
|
+
export function clapLineFromProbeOutput(output: ProbeOutput, facts: JudgeFacts = {}): ClapQualifyLine {
|
|
122
|
+
const rtSafety = rtSafetyOfProbeOutput(output);
|
|
123
|
+
const noteSweep = noteSweepOfProbeOutput(output, rtSafety);
|
|
124
|
+
const points = okPoints(output);
|
|
125
|
+
const latency: Record<string, { declaredFrames: number; measuredFrames: number }> = {};
|
|
126
|
+
for (const p of points) {
|
|
127
|
+
if (p.latency !== undefined && latency[String(p.rate)] === undefined) latency[String(p.rate)] = p.latency;
|
|
128
|
+
}
|
|
129
|
+
const withRt = points.filter((p) => p.rtSafety !== undefined);
|
|
130
|
+
return {
|
|
131
|
+
uri: `urn:clap:${output.plugin.id}`,
|
|
132
|
+
id: output.plugin.id,
|
|
133
|
+
binary: { path: output.binary.path, sha256: output.binary.sha256 },
|
|
134
|
+
hostExtensions: output.offer.clapExtensions ?? [],
|
|
135
|
+
clap: {
|
|
136
|
+
mainInputChannels: 0,
|
|
137
|
+
mainOutputChannels: 0,
|
|
138
|
+
sidechainInputs: 0,
|
|
139
|
+
features: [],
|
|
140
|
+
headlessOk: output.steps.some((s) => s.step === "instantiate" && s.outcome === PROBE_OK),
|
|
141
|
+
noteInputs: facts.noteInputs ?? (noteSweep.applicable ? 1 : 0),
|
|
142
|
+
latency,
|
|
143
|
+
threadViolations: total(withRt, (p) => p.rtSafety?.threadViolations ?? 0) ?? 0,
|
|
144
|
+
processErrors: total(withRt, (p) => p.rtSafety?.processErrors ?? 0) ?? 0,
|
|
145
|
+
// the sweep ran as many blocks as the points that counted say; a run with none measured nothing
|
|
146
|
+
processCalls: rtSafety.interposer ? (rtSafety.blocks ?? 0) : points.length,
|
|
147
|
+
nonFinite: output.steps.some((s) => s.outcome === "probe.non-finite-output"),
|
|
148
|
+
},
|
|
149
|
+
rtSafety,
|
|
150
|
+
noteSweep,
|
|
151
|
+
integrity: integrityOfProbeOutput(output),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** One reason as the record writes it: the code is a string since a failed run's reason is the run's outcome code. */
|
|
156
|
+
export interface RecordReason {
|
|
157
|
+
readonly dimension: string;
|
|
158
|
+
readonly rating: SuitabilityRating;
|
|
159
|
+
readonly code: string;
|
|
160
|
+
readonly params: Readonly<Record<string, string | number>>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const asRecordReason = (r: HostingReason): RecordReason => ({ dimension: r.dimension, rating: contributedHostingRating(r), code: r.code, params: r.params });
|
|
164
|
+
|
|
165
|
+
/** What one host profile makes of one probe output. */
|
|
166
|
+
export interface HostJudgment {
|
|
167
|
+
readonly verdict: QualifyVerdict;
|
|
168
|
+
/** In-process only for a suitable plugin on a host that runs its plugins in its own process, else isolated. */
|
|
169
|
+
readonly hosting: "in-process" | "isolated";
|
|
170
|
+
readonly reasons: readonly RecordReason[];
|
|
171
|
+
/** The cost reasons at this host's rate and quantum, kept beside the verdict, not folded into it. */
|
|
172
|
+
readonly cost: { readonly rate: number; readonly quantum: number; readonly reasons: readonly RecordReason[] };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface Judgment {
|
|
176
|
+
readonly output: ProbeOutput;
|
|
177
|
+
readonly hosts: readonly HostJudgment[];
|
|
178
|
+
/** For a CLAP plugin, the document of each host that today's judge writes; the pins are taken from these. */
|
|
179
|
+
readonly documents: readonly RtSafetyDocument[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function decidingOf(reasons: readonly RecordReason[], rating: SuitabilityRating): RecordReason | undefined {
|
|
183
|
+
if (rating === "suitable") return undefined;
|
|
184
|
+
return [...reasons].sort((a, b) => RATING_SEVERITY[b.rating] - RATING_SEVERITY[a.rating]).find((r) => r.rating === rating);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function costOf(output: ProbeOutput, profile: HostProfile): HostJudgment["cost"] {
|
|
188
|
+
const { rate, quantum } = profile.cost;
|
|
189
|
+
const reasons = hostingCostReasons(
|
|
190
|
+
{ cpuCost: { perRateQuantum: gridCostReadings(output.grid, PROBE_OK) } },
|
|
191
|
+
{ rate, quantum, profile },
|
|
192
|
+
DEFAULT_HOSTING_POLICY,
|
|
193
|
+
);
|
|
194
|
+
return { rate, quantum, reasons: reasons.map(asRecordReason) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The outcome that ended the run badly, or `undefined` when it ended well. The run's own outcome is the
|
|
199
|
+
* first word; a grid point that failed under a run that says `probe.ok` is a document that contradicts
|
|
200
|
+
* itself, and is read as the failure it records rather than as a pass that skipped the point.
|
|
201
|
+
*/
|
|
202
|
+
function failedOutcome(output: ProbeOutput): string | undefined {
|
|
203
|
+
if (output.run.outcome !== PROBE_OK) return output.run.outcome;
|
|
204
|
+
return output.grid.find((p) => p.outcome !== PROBE_OK && p.outcome !== PROBE_NOT_ATTEMPTED)?.outcome;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Every profile's judgment of one probe output.
|
|
209
|
+
*
|
|
210
|
+
* A run that did not end `probe.ok` is `unknown` under every profile, with the run's outcome code as
|
|
211
|
+
* the reason: the measurement did not finish, so there is nothing to rate, and it is not a plugin
|
|
212
|
+
* `unsuitable` on evidence the run never gathered. A step that was not attempted is part of a run that
|
|
213
|
+
* ended well (a plugin with no note input has no note sweep).
|
|
214
|
+
*/
|
|
215
|
+
export function judgeProbeOutput(output: ProbeOutput, profiles: readonly HostProfile[], facts: JudgeFacts = {}): Judgment {
|
|
216
|
+
const uri = output.plugin.format === "clap" ? `urn:clap:${output.plugin.id}` : output.plugin.id;
|
|
217
|
+
const failure = failedOutcome(output);
|
|
218
|
+
if (failure !== undefined) {
|
|
219
|
+
const reason: RecordReason = { dimension: "rtSafety", rating: "unknown", code: failure, params: {} };
|
|
220
|
+
return {
|
|
221
|
+
output,
|
|
222
|
+
documents: [],
|
|
223
|
+
hosts: profiles.map((profile) => ({
|
|
224
|
+
verdict: { uri, host: profile.id, measured: false, rating: "unknown", qualified: false, deciding: failure },
|
|
225
|
+
hosting: "isolated",
|
|
226
|
+
reasons: [reason],
|
|
227
|
+
cost: costOf(output, profile),
|
|
228
|
+
})),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const line = output.plugin.format === "clap" ? clapLineFromProbeOutput(output, facts) : undefined;
|
|
232
|
+
const documents: RtSafetyDocument[] = [];
|
|
233
|
+
const hosts = profiles.map((profile): HostJudgment => {
|
|
234
|
+
let reasons: RecordReason[];
|
|
235
|
+
if (line !== undefined) {
|
|
236
|
+
const doc = rtSafetyDocument(line, profile);
|
|
237
|
+
documents.push(doc);
|
|
238
|
+
reasons = doc.verdict.reasons.map(asRecordReason);
|
|
239
|
+
} else {
|
|
240
|
+
reasons = rtSafetyReasons(rtSafetyOfProbeOutput(output)).map(asRecordReason);
|
|
241
|
+
}
|
|
242
|
+
const rating = worstRating(reasons.map((r) => r.rating));
|
|
243
|
+
const deciding = decidingOf(reasons, rating);
|
|
244
|
+
return {
|
|
245
|
+
verdict: {
|
|
246
|
+
uri,
|
|
247
|
+
host: profile.id,
|
|
248
|
+
measured: true,
|
|
249
|
+
rating,
|
|
250
|
+
qualified: rating === "suitable",
|
|
251
|
+
...(deciding === undefined ? {} : { deciding: deciding.code }),
|
|
252
|
+
},
|
|
253
|
+
hosting: rating === "suitable" && profile.isolation === "in-process" ? "in-process" : "isolated",
|
|
254
|
+
reasons,
|
|
255
|
+
cost: costOf(output, profile),
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
return { output, hosts, documents };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The verdict rows of a judgment, as the CLI prints them. */
|
|
262
|
+
export function judgmentVerdicts(judgment: Judgment): QualifyVerdict[] {
|
|
263
|
+
return judgment.hosts.map((h) => h.verdict);
|
|
264
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The probe-output documents the tests build on, read from `fixtures/probe-output/`: plugin-hostd's
|
|
5
|
+
* three example documents (a success on the reference rig, a crash, a refusal for memory). Kept
|
|
6
|
+
* out of the test files so every test reads the same documents; a `.test.ts` imported for a helper
|
|
7
|
+
* runs its whole suite in each importer.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
import type { ClapQualifyLine } from "./clap-qualify.js";
|
|
14
|
+
import { OMX_CLAP_HOST_EXTENSIONS } from "./host-profiles.js";
|
|
15
|
+
import { readProbeOutput, type ProbeOutput } from "./probe-output.js";
|
|
16
|
+
|
|
17
|
+
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "..", "fixtures", "probe-output");
|
|
18
|
+
|
|
19
|
+
export function fixtureText(name: string): string {
|
|
20
|
+
return readFileSync(join(FIXTURES, `${name}.json`), "utf8");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A fixture as a mutable JSON value, for the tests that break one field of it. */
|
|
24
|
+
export function fixtureJson(name: string): Record<string, any> {
|
|
25
|
+
return JSON.parse(fixtureText(name));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A fixture through the reader; throws when the reader refuses it. */
|
|
29
|
+
export function readFixture(name: string): ProbeOutput {
|
|
30
|
+
const r = readProbeOutput(fixtureText(name));
|
|
31
|
+
if (!r.ok) throw new Error(`${name}: ${r.code}: ${r.problem}`);
|
|
32
|
+
return r.output;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The probe output that carries what a prober line carries, for the proofs that a verdict read from a
|
|
37
|
+
* probe output equals the verdict read from today's document. Built on the success fixture, so its machine,
|
|
38
|
+
* limits and run are a real document's.
|
|
39
|
+
*
|
|
40
|
+
* What the line holds and a probe output cannot is not encoded: the port layout and the features (no
|
|
41
|
+
* verdict reads them), the note sweep's process errors and non-finite flag, and a block count apart
|
|
42
|
+
* from the sweep's (the probe output has one, the blocks the sweep processed, which is `processCalls`).
|
|
43
|
+
*/
|
|
44
|
+
export function probeOutputOfLine(line: ClapQualifyLine): Record<string, any> {
|
|
45
|
+
const doc = fixtureJson("success-msi");
|
|
46
|
+
const rt = line.rtSafety;
|
|
47
|
+
const note = line.noteSweep;
|
|
48
|
+
const pin = line.integrity;
|
|
49
|
+
const rates = Object.keys(line.clap.latency);
|
|
50
|
+
const quantum = rt.blockFrames ?? 512;
|
|
51
|
+
doc["plugin"] = { format: "clap", id: line.id, ...(pin.version === undefined ? {} : { version: pin.version }) };
|
|
52
|
+
doc["binary"] = {
|
|
53
|
+
path: line.binary.path,
|
|
54
|
+
sha256: line.binary.sha256,
|
|
55
|
+
files: pin.files ?? [],
|
|
56
|
+
...(pin.layout === undefined ? {} : { layout: pin.layout }),
|
|
57
|
+
...(pin.unmeasuredReason === undefined ? {} : { unpinnable: pin.unmeasuredDetail ?? pin.unmeasuredReason }),
|
|
58
|
+
};
|
|
59
|
+
delete doc["descriptor"];
|
|
60
|
+
doc["offer"] = { id: "default", clapExtensions: [...line.hostExtensions], realtime: "none" };
|
|
61
|
+
doc["steps"] = [{ step: "hash", outcome: "probe.ok", elapsedMs: 1 }, { step: "instantiate", outcome: "probe.ok", elapsedMs: 1 }];
|
|
62
|
+
const first: Record<string, unknown> = {};
|
|
63
|
+
if (rt.interposer) {
|
|
64
|
+
Object.assign(first, {
|
|
65
|
+
rtSafety: {
|
|
66
|
+
interposer: true,
|
|
67
|
+
swept: rt.swept,
|
|
68
|
+
sweepApplicable: rt.sweepApplicable,
|
|
69
|
+
blocks: line.clap.processCalls,
|
|
70
|
+
allocations: rt.allocationsInRun,
|
|
71
|
+
locks: rt.locksInRun,
|
|
72
|
+
syscalls: rt.syscallsInRun,
|
|
73
|
+
threadViolations: line.clap.threadViolations,
|
|
74
|
+
processErrors: line.clap.processErrors,
|
|
75
|
+
events: rt.events ?? [],
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
} else first["rtSafety"] = { interposer: false };
|
|
79
|
+
if (note.applicable) {
|
|
80
|
+
first["noteSweep"] = {
|
|
81
|
+
applicable: true,
|
|
82
|
+
...(note.dialect === undefined ? {} : { dialect: note.dialect }),
|
|
83
|
+
...(note.interposer === true ? { allocations: note.allocationsInRun, locks: note.locksInRun, syscalls: note.syscallsInRun } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const at = rates.length > 0 ? rates : ["48000"];
|
|
87
|
+
doc["grid"] = at.map((rate, i) => ({
|
|
88
|
+
rate: Number(rate),
|
|
89
|
+
quantum,
|
|
90
|
+
outcome: "probe.ok",
|
|
91
|
+
...(line.clap.latency[rate] === undefined ? {} : { latency: { ...line.clap.latency[rate], method: "impulse-onset" } }),
|
|
92
|
+
...(i === 0 ? first : {}),
|
|
93
|
+
}));
|
|
94
|
+
return doc;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const PIN = {
|
|
98
|
+
version: "1",
|
|
99
|
+
files: [{ path: "rt-safe.clap", sha256: "a".repeat(64) }],
|
|
100
|
+
layout: { scheme: "omx-layout/1", sha256: "b".repeat(64) },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** A prober line for a clean stereo effect, with `patch` merged over it. */
|
|
104
|
+
export function clapLineFixture(patch: {
|
|
105
|
+
rtSafety?: object;
|
|
106
|
+
clap?: object;
|
|
107
|
+
hostExtensions?: string[];
|
|
108
|
+
noteSweep?: object;
|
|
109
|
+
integrity?: object;
|
|
110
|
+
} = {}): ClapQualifyLine {
|
|
111
|
+
return {
|
|
112
|
+
uri: "urn:clap:org.example.fx",
|
|
113
|
+
id: "org.example.fx",
|
|
114
|
+
binary: { path: "/x/rt-safe.clap", sha256: "a".repeat(64) },
|
|
115
|
+
hostExtensions: patch.hostExtensions ?? [...OMX_CLAP_HOST_EXTENSIONS],
|
|
116
|
+
clap: {
|
|
117
|
+
mainInputChannels: 2,
|
|
118
|
+
mainOutputChannels: 2,
|
|
119
|
+
sidechainInputs: 0,
|
|
120
|
+
features: ["audio-effect"],
|
|
121
|
+
headlessOk: true,
|
|
122
|
+
noteInputs: 0,
|
|
123
|
+
latency: { "48000": { declaredFrames: 0, measuredFrames: 0 }, "96000": { declaredFrames: 4, measuredFrames: 4 } },
|
|
124
|
+
threadViolations: 0,
|
|
125
|
+
processErrors: 0,
|
|
126
|
+
processCalls: 512,
|
|
127
|
+
nonFinite: false,
|
|
128
|
+
...patch.clap,
|
|
129
|
+
},
|
|
130
|
+
rtSafety: {
|
|
131
|
+
interposer: true,
|
|
132
|
+
swept: true,
|
|
133
|
+
sweepApplicable: true,
|
|
134
|
+
blocks: 512,
|
|
135
|
+
blockFrames: 512,
|
|
136
|
+
allocationsInRun: 0,
|
|
137
|
+
locksInRun: 0,
|
|
138
|
+
syscallsInRun: 0,
|
|
139
|
+
events: [],
|
|
140
|
+
...patch.rtSafety,
|
|
141
|
+
},
|
|
142
|
+
noteSweep: { applicable: false, ...patch.noteSweep },
|
|
143
|
+
integrity: { ...PIN, ...patch.integrity },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|