@openmixer/plugin-qualify 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +203 -2
- package/bin/plugin-qualify.mjs +77 -0
- package/dist/clap-loader.d.ts +147 -0
- package/dist/clap-loader.js +252 -0
- package/dist/clap-loader.js.map +1 -0
- package/dist/clap-qualify.d.ts +126 -0
- package/dist/clap-qualify.js +272 -0
- package/dist/clap-qualify.js.map +1 -0
- package/dist/host-profiles.d.ts +42 -0
- package/dist/host-profiles.js +106 -0
- package/dist/host-profiles.js.map +1 -0
- package/dist/hosting-codes.d.ts +62 -0
- package/dist/hosting-codes.js +72 -0
- package/dist/hosting-codes.js.map +1 -0
- package/dist/hosting-suitability.d.ts +354 -0
- package/dist/hosting-suitability.js +654 -0
- package/dist/hosting-suitability.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +26 -0
- package/dist/loader.js +9 -0
- package/dist/loader.js.map +1 -0
- package/dist/lv2-loader.d.ts +7 -0
- package/dist/lv2-loader.js +50 -0
- package/dist/lv2-loader.js.map +1 -0
- package/dist/measurement/cli.d.ts +168 -0
- package/dist/measurement/cli.js +353 -0
- package/dist/measurement/cli.js.map +1 -0
- package/dist/measurement/fixtures.d.ts +23 -0
- package/dist/measurement/fixtures.js +129 -0
- package/dist/measurement/fixtures.js.map +1 -0
- package/dist/measurement/format.d.ts +483 -0
- package/dist/measurement/format.js +627 -0
- package/dist/measurement/format.js.map +1 -0
- package/dist/measurement/guards.d.ts +33 -0
- package/dist/measurement/guards.js +61 -0
- package/dist/measurement/guards.js.map +1 -0
- package/dist/measurement/index.d.ts +36 -0
- package/dist/measurement/index.js +39 -0
- package/dist/measurement/index.js.map +1 -0
- package/dist/measurement/merge.d.ts +143 -0
- package/dist/measurement/merge.js +315 -0
- package/dist/measurement/merge.js.map +1 -0
- package/dist/measurement/plan.d.ts +127 -0
- package/dist/measurement/plan.js +141 -0
- package/dist/measurement/plan.js.map +1 -0
- package/dist/measurement/probe-json.d.ts +61 -0
- package/dist/measurement/probe-json.js +253 -0
- package/dist/measurement/probe-json.js.map +1 -0
- package/dist/measurement/provenance.d.ts +91 -0
- package/dist/measurement/provenance.js +173 -0
- package/dist/measurement/provenance.js.map +1 -0
- package/dist/measurement/sweep-jsonl.d.ts +40 -0
- package/dist/measurement/sweep-jsonl.js +154 -0
- package/dist/measurement/sweep-jsonl.js.map +1 -0
- package/dist/measurement/vocabulary.d.ts +125 -0
- package/dist/measurement/vocabulary.js +217 -0
- package/dist/measurement/vocabulary.js.map +1 -0
- package/dist/qualify-cli.d.ts +83 -0
- package/dist/qualify-cli.js +224 -0
- package/dist/qualify-cli.js.map +1 -0
- package/dist/qualify-declarations.d.ts +98 -0
- package/dist/qualify-declarations.js +76 -0
- package/dist/qualify-declarations.js.map +1 -0
- package/dist/qualify-options.d.ts +22 -0
- package/dist/qualify-options.js +36 -0
- package/dist/qualify-options.js.map +1 -0
- package/dist/rating.d.ts +32 -0
- package/dist/rating.js +30 -0
- package/dist/rating.js.map +1 -0
- package/dist/tool-paths.d.ts +12 -0
- package/dist/tool-paths.js +29 -0
- package/dist/tool-paths.js.map +1 -0
- package/fixtures/clap-scan/controls.json +8 -0
- package/fixtures/clap-scan/fake.json +16 -0
- package/fixtures/clap-scan/mixed.json +18 -0
- package/fixtures/zero-path.lv2/manifest.ttl +3 -0
- package/fixtures/zero-path.lv2/zero-path.c +85 -0
- package/fixtures/zero-path.lv2/zero-path.ttl +21 -0
- package/package.json +91 -3
- package/src/clap-loader.ts +393 -0
- package/src/clap-qualify.ts +387 -0
- package/src/host-profiles.ts +122 -0
- package/src/hosting-codes.ts +71 -0
- package/src/hosting-suitability.ts +982 -0
- package/src/index.ts +19 -0
- package/src/loader.ts +30 -0
- package/src/lv2-loader.ts +50 -0
- package/src/measurement/cli.ts +458 -0
- package/src/measurement/fixtures.ts +141 -0
- package/src/measurement/format.ts +1064 -0
- package/src/measurement/guards.ts +64 -0
- package/src/measurement/index.ts +39 -0
- package/src/measurement/merge.ts +434 -0
- package/src/measurement/plan.ts +219 -0
- package/src/measurement/probe-json.ts +271 -0
- package/src/measurement/provenance.ts +220 -0
- package/src/measurement/sweep-jsonl.ts +173 -0
- package/src/measurement/vocabulary.ts +298 -0
- package/src/qualify-cli.ts +277 -0
- package/src/qualify-declarations.ts +117 -0
- package/src/qualify-options.ts +49 -0
- package/src/rating.ts +53 -0
- package/src/tool-paths.ts +32 -0
- package/tools/_declarations.py +21 -0
- package/tools/benchmark.py +2462 -0
- package/tools/gen-python-declarations.d.mts +4 -0
- package/tools/gen-python-declarations.mjs +92 -0
- package/tools/hosting-sweep.mjs +51 -0
- package/tools/lv2-measure.mjs +204 -0
- package/tools/lv2_presets.py +132 -0
- package/tools/scan.py +432 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* Which HOST a plugin has earned: the console's own RT thread, or a `mod-host` process.
|
|
5
|
+
*
|
|
6
|
+
* "Thoroughly tested" is not a hand list and never an author's opinion. It is the derived
|
|
7
|
+
* verdict `hosting.rating === "suitable"`, computed on every read from measurements the
|
|
8
|
+
* catalog holds. `path` follows the rating and nothing else: `suitable` earns `in-process`,
|
|
9
|
+
* every rating below it is `isolated` with the reasons that decided it.
|
|
10
|
+
*
|
|
11
|
+
* The shape mirrors {@link import('./destination-suitability.js').classifyForRole} because
|
|
12
|
+
* the same law governs both — a standing is DERIVED, absence is an admission rather than a
|
|
13
|
+
* pass, and an override may only ever demote. What differs is the stake. A destination
|
|
14
|
+
* verdict that is wrong puts a reverb in a wedge; a hosting verdict that is wrong puts an
|
|
15
|
+
* untested plugin on the thread the whole desk's audio runs on, where its crash is not its
|
|
16
|
+
* own. So every dimension here fails CLOSED: no measurement means `unknown`, and `unknown`
|
|
17
|
+
* means isolated.
|
|
18
|
+
*/
|
|
19
|
+
import { RATING_SEVERITY, worstRating, } from "./rating.js";
|
|
20
|
+
// The in-process feature list and the CLAP host extensions are the `openmixer-console` host
|
|
21
|
+
// profile's; they live with the profiles (`host-profiles.ts`) and are re-exported here, where the
|
|
22
|
+
// native test (`lv2-inprocess-features.sh`) and the contract-limits render read them.
|
|
23
|
+
export { OMX_CLAP_HOST_EXTENSIONS, OMX_INPROCESS_FEATURES, OPENMIXER_CONSOLE_PROFILE } from "./host-profiles.js";
|
|
24
|
+
import { OMX_CLAP_HOST_EXTENSIONS, OMX_INPROCESS_FEATURES, OPENMIXER_CONSOLE_PROFILE } from "./host-profiles.js";
|
|
25
|
+
/** The catalog key of a CLAP plugin is `urn:clap:<clap_plugin_descriptor.id>` (CLAP spec §6). */
|
|
26
|
+
export const CLAP_URI_PREFIX = "urn:clap:";
|
|
27
|
+
/** Whether `uri` is a CLAP catalog key — the key grammar, for a plugin the catalog may not hold. */
|
|
28
|
+
export function isClapUri(uri) {
|
|
29
|
+
return uri.startsWith(CLAP_URI_PREFIX);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether the plugin `uri` names is hosted through the CLAP path: the descriptor's DECLARED
|
|
33
|
+
* format where the catalog holds one, else the key grammar (a `urn:clap:` uri the catalog never
|
|
34
|
+
* scanned is still a CLAP the console has no LV2 host for).
|
|
35
|
+
*/
|
|
36
|
+
export function isClapPlugin(uri, descriptor) {
|
|
37
|
+
return descriptor?.format === "clap" || (descriptor?.format === undefined && isClapUri(uri));
|
|
38
|
+
}
|
|
39
|
+
/** The CLAP descriptor feature that makes a plugin an audio effect (`clap/plugin-features.h`). */
|
|
40
|
+
export const CLAP_FEATURE_AUDIO_EFFECT = "audio-effect";
|
|
41
|
+
/** The CLAP descriptor feature that makes a plugin an instrument (`clap/plugin-features.h`). */
|
|
42
|
+
export const CLAP_FEATURE_INSTRUMENT = "instrument";
|
|
43
|
+
/**
|
|
44
|
+
* Features that disqualify outright rather than merely being unprovided: they need a host we
|
|
45
|
+
* are not and will not become inside the RT thread.
|
|
46
|
+
*/
|
|
47
|
+
const DISQUALIFYING_FEATURES = [
|
|
48
|
+
"http://lv2plug.in/ns/ext/instance-access",
|
|
49
|
+
"http://lv2plug.in/ns/ext/data-access",
|
|
50
|
+
"http://lv2plug.in/ns/extensions/ui#idleInterface",
|
|
51
|
+
];
|
|
52
|
+
/** Providable only while the console pins its quantum; without a pinned quantum they are
|
|
53
|
+
* promises the stage cannot keep, and §3 conditions them on exactly that. */
|
|
54
|
+
const QUANTUM_PINNED_FEATURES = [
|
|
55
|
+
"http://lv2plug.in/ns/ext/buf-size#fixedBlockLength",
|
|
56
|
+
"http://lv2plug.in/ns/ext/buf-size#powerOf2BlockLength",
|
|
57
|
+
];
|
|
58
|
+
/** The verdict's message codes, declared in their union list file. */
|
|
59
|
+
import { HOSTING_CODE } from "./hosting-codes.js";
|
|
60
|
+
export { HOSTING_CODE };
|
|
61
|
+
/**
|
|
62
|
+
* The refusals libomx-clap-core returns from `open`, `activate` and the timed open, each named by the KEY of the
|
|
63
|
+
* `HOSTING_CODE` it answers with — never by the string, so no second spelling of a code exists. The console's
|
|
64
|
+
* generator renders them into omx-clap-host's `clap_host_limits.h` as `CLAP_HOST_CODE_<NAME>`, the left column's
|
|
65
|
+
* name in upper snake case. The facts the qualifier judges from a measurement (latency, thread violations, process
|
|
66
|
+
* errors, RT safety) are the console's and are not here.
|
|
67
|
+
*/
|
|
68
|
+
export const CLAP_CORE_REFUSALS = {
|
|
69
|
+
headlessFailed: "clapHeadlessFailed",
|
|
70
|
+
notAudioEffect: "clapNotAudioEffect",
|
|
71
|
+
noteInput: "clapNoteInput",
|
|
72
|
+
noAudioInput: "topologyNoAudioInput",
|
|
73
|
+
noAudioOutput: "topologyNoAudioOutput",
|
|
74
|
+
widerThanStrip: "topologyWiderThanStrip",
|
|
75
|
+
extraInputs: "topologyExtraInputs",
|
|
76
|
+
crashedLive: "stabilityCrashedLive",
|
|
77
|
+
outputNonFinite: "outputNonFinite",
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* What a reason may contribute. Unlike the destination classifier, every reason here is
|
|
81
|
+
* objective — there is no taste in whether a plugin allocates inside `run()`. The cautions
|
|
82
|
+
* that look advisory (`spawns-threads`, `links-gui-toolkit`) are MEASURED facts, so they are
|
|
83
|
+
* objective too and cap the plugin at `conditional` by their own rating rather than by a kind
|
|
84
|
+
* rule. An advisory reason, if one is ever added, may never do worse than `conditional`.
|
|
85
|
+
*/
|
|
86
|
+
export function contributedHostingRating(reason) {
|
|
87
|
+
if (reason.kind === "advisory" && RATING_SEVERITY[reason.rating] > RATING_SEVERITY.conditional) {
|
|
88
|
+
return "conditional";
|
|
89
|
+
}
|
|
90
|
+
return reason.rating;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Seeded from the crash notes of 2026-09-03/04. A raw crash matching NO row is
|
|
94
|
+
* `unknown` — the table blames a plugin only where a named investigation did.
|
|
95
|
+
*/
|
|
96
|
+
export const CRASH_SIGNATURES = [
|
|
97
|
+
{
|
|
98
|
+
id: "pipewire-jack-close-double-free",
|
|
99
|
+
attribution: "host",
|
|
100
|
+
topFrame: "pw_memmap_free",
|
|
101
|
+
signal: "SIGSEGV",
|
|
102
|
+
note: "2026-09-04-pipewire-jack-client-close-double-free.md",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "pipewire-jack-close-double-free-abort",
|
|
106
|
+
attribution: "host",
|
|
107
|
+
topFrame: "tcache_double_free_verify",
|
|
108
|
+
signal: "SIGABRT",
|
|
109
|
+
note: "2026-09-04-pipewire-jack-client-close-double-free.md",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "mod-host-effects-remove",
|
|
113
|
+
attribution: "host",
|
|
114
|
+
topFrame: "effects_remove",
|
|
115
|
+
note: "2026-09-03-mod-host-crash-cluster2-3-investigation.md",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "plugin-instantiate-layout",
|
|
119
|
+
attribution: "plugin",
|
|
120
|
+
topFrame: "lilv_instance_instantiate",
|
|
121
|
+
note: "2026-09-03-mod-host-crash-cluster2-3-investigation.md",
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
/**
|
|
125
|
+
* Map a raw crash to blame. Fails to `unknown`: a signature must match the frame AND, where
|
|
126
|
+
* the row names one, the signal. An unmatched crash never reads as the plugin's fault.
|
|
127
|
+
*/
|
|
128
|
+
export function attributeCrash(crash, table = CRASH_SIGNATURES, pluginBinary) {
|
|
129
|
+
// A sanitizer caught a memory error and the faulting frame is in the plugin's OWN binary:
|
|
130
|
+
// that is the plugin's (plugin-qualify spec §2 — the #98 zero-size atom:Path overread).
|
|
131
|
+
if (crash.sanitizer !== undefined &&
|
|
132
|
+
pluginBinary !== undefined &&
|
|
133
|
+
crash.frameObject === pluginBinary) {
|
|
134
|
+
return "plugin";
|
|
135
|
+
}
|
|
136
|
+
if (crash.topFrame === undefined)
|
|
137
|
+
return "unknown";
|
|
138
|
+
for (const row of table) {
|
|
139
|
+
if (row.topFrame !== crash.topFrame)
|
|
140
|
+
continue;
|
|
141
|
+
if (row.signal !== undefined && crash.signal !== undefined && row.signal !== crash.signal) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
return row.attribution;
|
|
145
|
+
}
|
|
146
|
+
return "unknown";
|
|
147
|
+
}
|
|
148
|
+
export { DEFAULT_HOSTING_POLICY } from "./qualify-declarations.js";
|
|
149
|
+
import { DEFAULT_HOSTING_POLICY } from "./qualify-declarations.js";
|
|
150
|
+
function reason(dimension, kind, rating, code, params = {}) {
|
|
151
|
+
return { dimension, kind, rating, code, params };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Refuse an illegal override at the door. A curator may take a plugin OFF the RT thread and
|
|
155
|
+
* may never put one on it — no measurement, no in-process, and no "force" exists anywhere.
|
|
156
|
+
* Throws rather than coercing, because silently demoting a bad value would hide the mistake.
|
|
157
|
+
*/
|
|
158
|
+
export function assertHostingOverride(value, where) {
|
|
159
|
+
if (value === undefined || value === null)
|
|
160
|
+
return undefined;
|
|
161
|
+
if (value === "isolated")
|
|
162
|
+
return "isolated";
|
|
163
|
+
throw new Error(`${where}: hostingOverride may only be "isolated" (got ${JSON.stringify(value)}). ` +
|
|
164
|
+
`An override can demote a plugin to the isolated host; it can never grant in-process ` +
|
|
165
|
+
`hosting, which is earned from measurements alone.`);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The level at which a rate is DEAD against the plugin's own best: `policy.soakDeadDropDb` or
|
|
169
|
+
* more below it. A ratio against the plugin itself, never an absolute floor — a quiet plugin is
|
|
170
|
+
* not a dead one. The one application of that threshold; the soak rule below and the report's
|
|
171
|
+
* short-render column both ask it.
|
|
172
|
+
*/
|
|
173
|
+
export function deadAgainstBest(levelDbfs, bestDbfs, policy) {
|
|
174
|
+
return bestDbfs > -Infinity && levelDbfs < bestDbfs - policy.soakDeadDropDb;
|
|
175
|
+
}
|
|
176
|
+
/** A soak rate that settled or opened ABOVE this is blasting: it voids the soak, and a rate
|
|
177
|
+
* settled above it cannot be the reference, or every sane rate reads dead against it. */
|
|
178
|
+
const BLAST_DBFS = 6;
|
|
179
|
+
/**
|
|
180
|
+
* Read a soak, rate by rate. A rate voids it if the plugin did not instantiate or died there,
|
|
181
|
+
* emitted a non-finite window, settled dead against its own best rate ({@link deadAgainstBest};
|
|
182
|
+
* settled = the LAST window, never the first — Calf opens at +200 dBFS at 192 k), spent more
|
|
183
|
+
* than half its windows silent, or opened above +6 dBFS. A few silent windows in ten thousand is
|
|
184
|
+
* a gate closing on a swept threshold — the plugin working, not dying.
|
|
185
|
+
*
|
|
186
|
+
* `undefined` when there is no soak, or it covered no rate: nothing was read.
|
|
187
|
+
*/
|
|
188
|
+
export function soakFindings(soak, policy) {
|
|
189
|
+
if (soak === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
const rates = Object.entries(soak.perRate);
|
|
192
|
+
if (rates.length === 0)
|
|
193
|
+
return undefined;
|
|
194
|
+
const gone = (v) => v.died === true || v.instantiated === false;
|
|
195
|
+
const settled = rates.map(([, v]) => gone(v) || (v.lastRmsDbfs ?? -Infinity) > BLAST_DBFS ? -Infinity : v.lastRmsDbfs ?? -Infinity);
|
|
196
|
+
const best = Math.max(...settled);
|
|
197
|
+
const failures = [];
|
|
198
|
+
for (const [rate, v] of rates) {
|
|
199
|
+
if (gone(v)) {
|
|
200
|
+
failures.push(`died@${rate}`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if ((v.nonFiniteWindows ?? 0) > 0) {
|
|
204
|
+
failures.push(`non-finite@${rate}`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (deadAgainstBest(v.lastRmsDbfs ?? -Infinity, best, policy)) {
|
|
208
|
+
failures.push(`dead@${rate}`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if ((v.silentWindows ?? 0) > (v.windows ?? 1) * 0.5) {
|
|
212
|
+
failures.push(`mostly-silent@${rate}`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const first = v.firstRmsDbfs ?? -99;
|
|
216
|
+
if (first > BLAST_DBFS)
|
|
217
|
+
failures.push(`startup-blast@${rate}(${first.toFixed(0)}dBFS)`);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
seconds: Math.min(...rates.map(([, v]) => v.seconds ?? 0)),
|
|
221
|
+
failures,
|
|
222
|
+
ratesTested: rates.length,
|
|
223
|
+
rssGrowthKb: Math.max(...rates.map(([, v]) => v.rssGrowthKb ?? 0)),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Threads the plugin's first instance brought with it. */
|
|
227
|
+
export function threadsSpawned(threads) {
|
|
228
|
+
return threads !== undefined && threads.afterFirstInstantiate > threads.before
|
|
229
|
+
? threads.afterFirstInstantiate - threads.before
|
|
230
|
+
: 0;
|
|
231
|
+
}
|
|
232
|
+
export function stabilityReasons(m, policy, pluginBinary,
|
|
233
|
+
/** Whether the plugin shares its process with others (profile isolation, §3a): threads it
|
|
234
|
+
* spawns and a GUI toolkit it links are cautions only there. */
|
|
235
|
+
sharesProcess = true) {
|
|
236
|
+
const out = [];
|
|
237
|
+
if (m === undefined || m.lifecycle === undefined) {
|
|
238
|
+
return [reason("stability", "objective", "unknown", HOSTING_CODE.stabilityUnmeasured)];
|
|
239
|
+
}
|
|
240
|
+
if (m.crashedLive === true) {
|
|
241
|
+
out.push(reason("stability", "objective", "unsuitable", HOSTING_CODE.stabilityCrashedLive, {}));
|
|
242
|
+
}
|
|
243
|
+
const attributions = (m.crashes ?? []).map((c) => attributeCrash(c, CRASH_SIGNATURES, pluginBinary));
|
|
244
|
+
if (attributions.includes("plugin")) {
|
|
245
|
+
out.push(reason("stability", "objective", "unsuitable", HOSTING_CODE.stabilityCrashedAttributed));
|
|
246
|
+
}
|
|
247
|
+
else if (attributions.includes("unknown")) {
|
|
248
|
+
out.push(reason("stability", "objective", "unknown", HOSTING_CODE.stabilityCrashUnattributed));
|
|
249
|
+
}
|
|
250
|
+
const cycles = m.lifecycle.instantiated;
|
|
251
|
+
if (cycles < policy.cycleFloor) {
|
|
252
|
+
out.push(reason("stability", "objective", "unknown", HOSTING_CODE.stabilityBelowFloor, {
|
|
253
|
+
cycles,
|
|
254
|
+
floor: policy.cycleFloor,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
// Any failure voids the soak: the plugin has not proven a clean show, so it reads unsoaked —
|
|
258
|
+
// unknown, isolated — and never a crash, because silence at a rate is a measurement of
|
|
259
|
+
// audio, not of the process.
|
|
260
|
+
const soak = soakFindings(m.soak, policy);
|
|
261
|
+
const soaked = soak === undefined || soak.failures.length > 0 ? 0 : soak.seconds;
|
|
262
|
+
if (soaked < policy.soakSeconds || m.soak?.sweptParams !== true) {
|
|
263
|
+
out.push(reason("stability", "objective", "unknown", HOSTING_CODE.stabilityUnsoaked, {
|
|
264
|
+
soakSeconds: soaked,
|
|
265
|
+
required: policy.soakSeconds,
|
|
266
|
+
...(soak !== undefined && soak.failures.length > 0 ? { failures: soak.failures.join(" ") } : {}),
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
const spawned = sharesProcess ? threadsSpawned(m.threads) : 0;
|
|
270
|
+
if (spawned > 0) {
|
|
271
|
+
out.push(reason("stability", "objective", "conditional", HOSTING_CODE.stabilitySpawnsThreads, {
|
|
272
|
+
threads: spawned,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
if (sharesProcess && m.linksGuiToolkit === true) {
|
|
276
|
+
out.push(reason("stability", "objective", "conditional", HOSTING_CODE.stabilityLinksGuiToolkit));
|
|
277
|
+
}
|
|
278
|
+
if (out.length === 0) {
|
|
279
|
+
out.push(reason("stability", "objective", "suitable", HOSTING_CODE.stabilitySoaked, {
|
|
280
|
+
cycles,
|
|
281
|
+
soakSeconds: soaked,
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Whether an interposer count is a MEASUREMENT: the interposer was loaded, all three counts are
|
|
288
|
+
* present, and the controls were swept — or the plugin has none a sweep could move, so its
|
|
289
|
+
* at-rest count is the only state there is. Measured at rest otherwise is not measured.
|
|
290
|
+
*/
|
|
291
|
+
export function rtSafetyMeasured(m) {
|
|
292
|
+
return (m !== undefined &&
|
|
293
|
+
m.interposer === true &&
|
|
294
|
+
(m.swept === true || m.sweepApplicable === false) &&
|
|
295
|
+
m.allocationsInRun !== undefined &&
|
|
296
|
+
m.syscallsInRun !== undefined &&
|
|
297
|
+
m.locksInRun !== undefined);
|
|
298
|
+
}
|
|
299
|
+
/** A measured count above zero — the disqualifier, asked in one place. */
|
|
300
|
+
export function rtSafetyViolating(m) {
|
|
301
|
+
return (rtSafetyMeasured(m) &&
|
|
302
|
+
(m?.allocationsInRun ?? 0) + (m?.syscallsInRun ?? 0) + (m?.locksInRun ?? 0) > 0);
|
|
303
|
+
}
|
|
304
|
+
export function rtSafetyReasons(m) {
|
|
305
|
+
if (m === undefined || !rtSafetyMeasured(m)) {
|
|
306
|
+
return [reason("rtSafety", "objective", "unknown", HOSTING_CODE.rtSafetyUnmeasured)];
|
|
307
|
+
}
|
|
308
|
+
if (m.variable === true) {
|
|
309
|
+
return [reason("rtSafety", "objective", "unknown", HOSTING_CODE.rtSafetyNotReproducible)];
|
|
310
|
+
}
|
|
311
|
+
if (rtSafetyViolating(m)) {
|
|
312
|
+
return [
|
|
313
|
+
reason("rtSafety", "objective", "unsuitable", HOSTING_CODE.rtSafetyViolations, {
|
|
314
|
+
allocations: m.allocationsInRun ?? 0,
|
|
315
|
+
syscalls: m.syscallsInRun ?? 0,
|
|
316
|
+
locks: m.locksInRun ?? 0,
|
|
317
|
+
}),
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
return [
|
|
321
|
+
reason("rtSafety", "objective", "suitable", HOSTING_CODE.rtSafetyClean, {
|
|
322
|
+
blocks: m.blocks ?? 0,
|
|
323
|
+
}),
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
export function featureReasons(features, descriptor, provided = OMX_INPROCESS_FEATURES, quantumPinned = false,
|
|
327
|
+
/** The host feeds MIDI input (an instrument host, §3a): a MIDI input is no caution there. */
|
|
328
|
+
instruments = false) {
|
|
329
|
+
const out = [];
|
|
330
|
+
if (features === undefined) {
|
|
331
|
+
out.push(reason("features", "objective", "unknown", HOSTING_CODE.featuresUnscanned));
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const required = features.required ?? [];
|
|
335
|
+
// A host that LISTS one of these provides it: refused are only those it does not list.
|
|
336
|
+
const refused = (quantumPinned ? DISQUALIFYING_FEATURES : [...DISQUALIFYING_FEATURES, ...QUANTUM_PINNED_FEATURES])
|
|
337
|
+
.filter((f) => !provided.includes(f));
|
|
338
|
+
const disqualifying = required.filter((f) => refused.includes(f));
|
|
339
|
+
if (disqualifying.length > 0) {
|
|
340
|
+
out.push(reason("features", "objective", "unsuitable", HOSTING_CODE.featuresDisqualifying, {
|
|
341
|
+
features: disqualifying.join(" "),
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
const missing = required.filter((f) => !provided.includes(f) && !refused.includes(f) && !QUANTUM_PINNED_FEATURES.includes(f));
|
|
345
|
+
if (missing.length > 0) {
|
|
346
|
+
out.push(reason("features", "objective", "unsuitable", HOSTING_CODE.featuresMissing, {
|
|
347
|
+
features: missing.join(" "),
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
if ((features.cvPorts ?? 0) > 0) {
|
|
351
|
+
out.push(reason("features", "objective", "unsuitable", HOSTING_CODE.featuresCv));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (descriptor.hasMidiIn && !instruments) {
|
|
355
|
+
out.push(reason("features", "objective", "conditional", HOSTING_CODE.featuresMidiIn));
|
|
356
|
+
}
|
|
357
|
+
if (out.length === 0) {
|
|
358
|
+
out.push(reason("features", "objective", "suitable", HOSTING_CODE.featuresProvided));
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
export function hostingCostReasons(descriptor, options, policy) {
|
|
363
|
+
const perRate = descriptor.cpuCost?.perRate?.[String(options.rate)];
|
|
364
|
+
if (perRate === undefined) {
|
|
365
|
+
return [
|
|
366
|
+
reason("cost", "objective", "unknown", HOSTING_CODE.costUnmeasured, {
|
|
367
|
+
rate: options.rate,
|
|
368
|
+
quantum: options.quantum ?? 0,
|
|
369
|
+
}),
|
|
370
|
+
];
|
|
371
|
+
}
|
|
372
|
+
const out = [];
|
|
373
|
+
const p95 = perRate.coreFractionP95;
|
|
374
|
+
// The per-instance ceiling is the HOST's budget (§3a), not the policy's.
|
|
375
|
+
const ceiling = (options.profile ?? OPENMIXER_CONSOLE_PROFILE).cost.coreFractionCeiling;
|
|
376
|
+
if (p95 > ceiling) {
|
|
377
|
+
out.push(reason("cost", "objective", "unsuitable", HOSTING_CODE.costAboveCeiling, {
|
|
378
|
+
coreFractionP95: p95,
|
|
379
|
+
ceiling,
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
const median = perRate.nsPerSampleMedian;
|
|
383
|
+
const p95ns = perRate.nsPerSampleP95;
|
|
384
|
+
if (median > 0 && p95ns / median > policy.costTailRatioCeiling) {
|
|
385
|
+
out.push(reason("cost", "objective", "conditional", HOSTING_CODE.costSpiky, {
|
|
386
|
+
tailRatio: p95ns / median,
|
|
387
|
+
ceiling: policy.costTailRatioCeiling,
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
if (out.length === 0) {
|
|
391
|
+
out.push(reason("cost", "objective", "suitable", HOSTING_CODE.costWithinCeiling, {
|
|
392
|
+
coreFractionP95: p95,
|
|
393
|
+
ceiling,
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
export function hostingLatencyReasons(descriptor, handling = "compensated") {
|
|
399
|
+
const latency = descriptor.latency;
|
|
400
|
+
// Fails closed, like every other dimension: a plugin with no latency figure at all, or
|
|
401
|
+
// one the benchmark declared unmeasurable, has not been judged and does not pass by
|
|
402
|
+
// default. The oracle's first fixture omitted latency and still expected `suitable`,
|
|
403
|
+
// which is how this branch was missing — a self-consistent suite proving nothing.
|
|
404
|
+
if (latency === undefined || latency.unmeasurable !== undefined) {
|
|
405
|
+
return [reason("latency", "objective", "unknown", HOSTING_CODE.latencyUnmeasured)];
|
|
406
|
+
}
|
|
407
|
+
if (latency.scalingClass === "nonconforming") {
|
|
408
|
+
return [reason("latency", "objective", "unknown", HOSTING_CODE.latencyNonconforming)];
|
|
409
|
+
}
|
|
410
|
+
// A parameter-dependent latency is ALLOWED in-process: the live latency port is read every
|
|
411
|
+
// cycle, so a moving latency is reported rather than assumed. It is a caution, not a bar.
|
|
412
|
+
// A host that does not compensate aligns on nothing, so a moving figure costs it nothing.
|
|
413
|
+
if (latency.paramSweep !== undefined && handling === "compensated") {
|
|
414
|
+
return [reason("latency", "objective", "conditional", HOSTING_CODE.latencyParameterDependent)];
|
|
415
|
+
}
|
|
416
|
+
return [reason("latency", "objective", "suitable", HOSTING_CODE.latencyResolved)];
|
|
417
|
+
}
|
|
418
|
+
export function hostingTopologyReasons(descriptor, instruments = false) {
|
|
419
|
+
const { audioInputs: ins, audioOutputs: outs } = descriptor;
|
|
420
|
+
// An instrument host feeds MIDI: no audio input but a MIDI input and an audio output is an instrument.
|
|
421
|
+
if (ins === 0 && instruments && descriptor.hasMidiIn === true && outs > 0) {
|
|
422
|
+
return [reason("topology", "objective", "suitable", HOSTING_CODE.topologyInstrument, { ins, outs })];
|
|
423
|
+
}
|
|
424
|
+
if (ins === 0) {
|
|
425
|
+
return [reason("topology", "objective", "unsuitable", HOSTING_CODE.topologyNoAudioInput)];
|
|
426
|
+
}
|
|
427
|
+
if (outs === 0) {
|
|
428
|
+
return [reason("topology", "objective", "unsuitable", HOSTING_CODE.topologyNoAudioOutput)];
|
|
429
|
+
}
|
|
430
|
+
if ((ins === 1 && outs === 1) || (ins === 2 && outs === 2)) {
|
|
431
|
+
return [
|
|
432
|
+
reason("topology", "objective", "suitable", HOSTING_CODE.topologyMatched, { ins, outs }),
|
|
433
|
+
];
|
|
434
|
+
}
|
|
435
|
+
return [
|
|
436
|
+
reason("topology", "objective", "conditional", HOSTING_CODE.topologyExtraInputs, { ins, outs }),
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* The CLAP arm of `topology` (CLAP spec §3): the qualifier's main-port facts feed the same
|
|
441
|
+
* dimension the LV2 scan feeds. No measurement → `unknown` (`hosting.clap.unqualified`), never
|
|
442
|
+
* a guess from the descriptor.
|
|
443
|
+
*/
|
|
444
|
+
export function clapTopologyReasons(clap, instruments = false) {
|
|
445
|
+
if (clap === undefined) {
|
|
446
|
+
return [reason("topology", "objective", "unknown", HOSTING_CODE.clapUnqualified)];
|
|
447
|
+
}
|
|
448
|
+
const ins = clap.mainInputChannels;
|
|
449
|
+
const outs = clap.mainOutputChannels;
|
|
450
|
+
if (ins === 0 && instruments && clap.noteInputs > 0 && outs > 0 && outs <= 2) {
|
|
451
|
+
return [reason("topology", "objective", "suitable", HOSTING_CODE.topologyInstrument, { ins, outs })];
|
|
452
|
+
}
|
|
453
|
+
if (ins === 0)
|
|
454
|
+
return [reason("topology", "objective", "unsuitable", HOSTING_CODE.topologyNoAudioInput)];
|
|
455
|
+
if (outs === 0)
|
|
456
|
+
return [reason("topology", "objective", "unsuitable", HOSTING_CODE.topologyNoAudioOutput)];
|
|
457
|
+
if (ins > 2 || outs > 2) {
|
|
458
|
+
return [reason("topology", "objective", "unsuitable", HOSTING_CODE.topologyWiderThanStrip, { ins, outs })];
|
|
459
|
+
}
|
|
460
|
+
if (clap.sidechainInputs > 0) {
|
|
461
|
+
return [reason("topology", "objective", "conditional", HOSTING_CODE.topologyExtraInputs, { ins, outs, sidechains: clap.sidechainInputs })];
|
|
462
|
+
}
|
|
463
|
+
return [reason("topology", "objective", "suitable", HOSTING_CODE.topologyMatched, { ins, outs })];
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* The CLAP arm of `latency` (CLAP spec §3): declared = measured at EVERY declared rate earns;
|
|
467
|
+
* one rate that lies refuses (`unsuitable` — compensation reads the figure as truth); no rate
|
|
468
|
+
* measured is `unknown`.
|
|
469
|
+
*/
|
|
470
|
+
export function clapLatencyReasons(clap) {
|
|
471
|
+
const rates = clap ? Object.keys(clap.latency) : [];
|
|
472
|
+
if (clap === undefined || rates.length === 0) {
|
|
473
|
+
return [reason("latency", "objective", "unknown", HOSTING_CODE.latencyUnmeasured)];
|
|
474
|
+
}
|
|
475
|
+
for (const rate of rates) {
|
|
476
|
+
const at = clap.latency[rate];
|
|
477
|
+
if (at.declaredFrames !== at.measuredFrames) {
|
|
478
|
+
return [
|
|
479
|
+
reason("latency", "objective", "unsuitable", HOSTING_CODE.clapLatencyMismatch, {
|
|
480
|
+
rate: Number(rate), declared: at.declaredFrames, measured: at.measuredFrames,
|
|
481
|
+
}),
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return [reason("latency", "objective", "suitable", HOSTING_CODE.latencyResolved)];
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* The CLAP arm of `features` (CLAP spec §3): an audio effect that initialised and activated
|
|
489
|
+
* HEADLESS with only OMX_CLAP_HOST_EXTENSIONS offered, and no note input (the measurement's or the
|
|
490
|
+
* scan's `hasMidiIn`). Every miss is its own code; a note input is an instrument, `unsuitable`.
|
|
491
|
+
*/
|
|
492
|
+
export function clapFeatureReasons(clap, profile = OPENMIXER_CONSOLE_PROFILE, scannedNoteInput = false) {
|
|
493
|
+
// A host with no CLAP path cannot run one, whatever it measured (§3a).
|
|
494
|
+
if (profile.clapExtensions === null) {
|
|
495
|
+
return [reason("features", "objective", "unsuitable", HOSTING_CODE.clapFormatNotHosted)];
|
|
496
|
+
}
|
|
497
|
+
if (clap === undefined) {
|
|
498
|
+
return [reason("features", "objective", "unknown", HOSTING_CODE.clapUnqualified)];
|
|
499
|
+
}
|
|
500
|
+
const out = [];
|
|
501
|
+
const offered = profile.clapExtensions;
|
|
502
|
+
const kindHosted = clap.features.includes(CLAP_FEATURE_AUDIO_EFFECT) || (profile.instruments && clap.features.includes(CLAP_FEATURE_INSTRUMENT));
|
|
503
|
+
if (!kindHosted) {
|
|
504
|
+
out.push(reason("features", "objective", "unsuitable", HOSTING_CODE.clapNotAudioEffect, { features: clap.features.join(" ") }));
|
|
505
|
+
}
|
|
506
|
+
if (!clap.headlessOk) {
|
|
507
|
+
out.push(reason("features", "objective", "unsuitable", HOSTING_CODE.clapHeadlessFailed));
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
// The headless pass was taken with OMX_CLAP_HOST_EXTENSIONS offered: a host offering less
|
|
511
|
+
// has not been shown to initialise the plugin.
|
|
512
|
+
const uncovered = OMX_CLAP_HOST_EXTENSIONS.filter((e) => !offered.includes(e));
|
|
513
|
+
if (uncovered.length > 0) {
|
|
514
|
+
out.push(reason("features", "objective", "unknown", HOSTING_CODE.clapExtensionsNotCovered, { extensions: uncovered.join(" ") }));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if ((clap.noteInputs > 0 || scannedNoteInput) && !profile.instruments) {
|
|
518
|
+
out.push(reason("features", "objective", "unsuitable", HOSTING_CODE.clapNoteInput, { noteInputs: clap.noteInputs }));
|
|
519
|
+
}
|
|
520
|
+
if (out.length === 0)
|
|
521
|
+
out.push(reason("features", "objective", "suitable", HOSTING_CODE.featuresProvided));
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* The CLAP additions to `rtSafety` (CLAP spec §3): the interposer's counts are read by
|
|
526
|
+
* `rtSafetyReasons` exactly as for an LV2; these are the two violations only a CLAP can commit
|
|
527
|
+
* — a strict thread-check violation and a `CLAP_PROCESS_ERROR` — each `unsuitable`. A sweep
|
|
528
|
+
* that made no `process()` call measured nothing.
|
|
529
|
+
*/
|
|
530
|
+
export function clapRtReasons(clap) {
|
|
531
|
+
if (clap === undefined || clap.processCalls === 0) {
|
|
532
|
+
return [reason("rtSafety", "objective", "unknown", HOSTING_CODE.clapUnqualified)];
|
|
533
|
+
}
|
|
534
|
+
const out = [];
|
|
535
|
+
if (clap.threadViolations > 0) {
|
|
536
|
+
out.push(reason("rtSafety", "objective", "unsuitable", HOSTING_CODE.clapThreadViolation, { violations: clap.threadViolations }));
|
|
537
|
+
}
|
|
538
|
+
if (clap.processErrors > 0) {
|
|
539
|
+
out.push(reason("rtSafety", "objective", "unsuitable", HOSTING_CODE.clapProcessError, { errors: clap.processErrors }));
|
|
540
|
+
}
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* The verdict. Worst-wins across every dimension, exactly as `classifyForRole` folds — with
|
|
545
|
+
* three gates that can poison the whole result before the fold is even read: a curator
|
|
546
|
+
* override, a sweep whose positive control did not fire, and figures taken on a different
|
|
547
|
+
* host than the console is running.
|
|
548
|
+
*/
|
|
549
|
+
export function classifyPluginHosting(descriptor, options) {
|
|
550
|
+
const policy = options.policy ?? DEFAULT_HOSTING_POLICY;
|
|
551
|
+
const profile = options.profile ?? OPENMIXER_CONSOLE_PROFILE;
|
|
552
|
+
const sharesProcess = profile.isolation !== "per-process";
|
|
553
|
+
// The descriptor's own measured blocks and nothing else: they reached it through the
|
|
554
|
+
// interchange's one door (`measurement-catalog.ts`), whose per-dimension precedence is the
|
|
555
|
+
// only precedence there is. A second "passed measurements win" seam here was how an offline
|
|
556
|
+
// report could agree with itself while every served descriptor read unknown.
|
|
557
|
+
// The CLAP arm (2026-09-26-clap-hosting-path.md §3): the SAME six dimensions, fed from the
|
|
558
|
+
// qualifier's CLAP facts where the LV2 scan's facts do not exist for the format. Stability,
|
|
559
|
+
// the interposer's counts and the cost are format-blind and read unchanged.
|
|
560
|
+
const reasons = descriptor.format === "clap"
|
|
561
|
+
? [
|
|
562
|
+
...stabilityReasons(descriptor.stability, policy, options.pluginBinary, sharesProcess),
|
|
563
|
+
...rtSafetyReasons(descriptor.rtSafety),
|
|
564
|
+
...clapRtReasons(descriptor.clap),
|
|
565
|
+
...clapFeatureReasons(descriptor.clap, profile, descriptor.hasMidiIn),
|
|
566
|
+
...hostingCostReasons(descriptor, options, policy),
|
|
567
|
+
...clapLatencyReasons(descriptor.clap),
|
|
568
|
+
...clapTopologyReasons(descriptor.clap, profile.instruments),
|
|
569
|
+
]
|
|
570
|
+
: [
|
|
571
|
+
...stabilityReasons(descriptor.stability, policy, options.pluginBinary, sharesProcess),
|
|
572
|
+
...rtSafetyReasons(descriptor.rtSafety),
|
|
573
|
+
...featureReasons(descriptor.lv2Features, descriptor, profile.lv2Features, options.quantum !== undefined, profile.instruments),
|
|
574
|
+
...hostingCostReasons(descriptor, options, policy),
|
|
575
|
+
...hostingLatencyReasons(descriptor, profile.latency),
|
|
576
|
+
...hostingTopologyReasons(descriptor, profile.instruments),
|
|
577
|
+
];
|
|
578
|
+
// A sweep whose positive control did not fire proves nothing about anything it measured.
|
|
579
|
+
// The whole roster reads `unknown` — the false-signals law, made mechanical.
|
|
580
|
+
if (options.sweepControlFired === false) {
|
|
581
|
+
reasons.unshift(reason("stability", "objective", "unknown", HOSTING_CODE.sweepControlDidNotFire));
|
|
582
|
+
}
|
|
583
|
+
// A dnf upgrade must not carry a verdict forward. Different host, no `suitable`.
|
|
584
|
+
const figureHost = descriptor.stability?.runHost;
|
|
585
|
+
if (options.consoleHost !== undefined &&
|
|
586
|
+
figureHost !== undefined &&
|
|
587
|
+
figureHost !== options.consoleHost) {
|
|
588
|
+
reasons.unshift(reason("stability", "objective", "unknown", HOSTING_CODE.hostProvenanceDiffers, {
|
|
589
|
+
measuredOn: figureHost,
|
|
590
|
+
runningOn: options.consoleHost,
|
|
591
|
+
}));
|
|
592
|
+
}
|
|
593
|
+
if (options.override === "isolated") {
|
|
594
|
+
reasons.unshift(reason("stability", "objective", "unsuitable", HOSTING_CODE.overrideIsolated));
|
|
595
|
+
}
|
|
596
|
+
const rating = worstRating(reasons.map(contributedHostingRating));
|
|
597
|
+
const deciding = [...reasons]
|
|
598
|
+
.sort((a, b) => RATING_SEVERITY[contributedHostingRating(b)] - RATING_SEVERITY[contributedHostingRating(a)])
|
|
599
|
+
.find((r) => contributedHostingRating(r) === rating);
|
|
600
|
+
return {
|
|
601
|
+
rating,
|
|
602
|
+
// In-process is a place only an in-process host has; every other host runs it in its own.
|
|
603
|
+
path: rating === "suitable" && profile.isolation === "in-process" ? "in-process" : "isolated",
|
|
604
|
+
reasons,
|
|
605
|
+
...(rating === "suitable" ? {} : { deciding }),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* The REALISED host of one racked plugin — a different fact from the verdict, and kept in a
|
|
610
|
+
* different field for that reason.
|
|
611
|
+
*
|
|
612
|
+
* `classifyPluginHosting` answers what a plugin EARNED. This answers where it RUNS, which
|
|
613
|
+
* three things decide in order, each with its own reason so the operator reads the first one
|
|
614
|
+
* that applies rather than a fold:
|
|
615
|
+
*
|
|
616
|
+
* 1. the console's switch is off — nothing runs in-process, whatever it earned;
|
|
617
|
+
* 1a. the plugin is LINKED into the engine — it runs in-process wherever a body exists, with no
|
|
618
|
+
* verdict to read;
|
|
619
|
+
* 2. nothing could be judged (no verdict: the console has observed no rate and quantum) or the
|
|
620
|
+
* verdict is below `suitable` — the plugin's own deciding reason stands;
|
|
621
|
+
* 3. it earned in-process and there is no in-process body to run it in yet.
|
|
622
|
+
*
|
|
623
|
+
* Order matters and is the operator's, not the machine's: a console-wide fact is a better
|
|
624
|
+
* answer to "why is this plugin not on the fast path" than a per-plugin one, because it is the
|
|
625
|
+
* one the operator can act on.
|
|
626
|
+
*/
|
|
627
|
+
export function realisedHosting(verdict, realisation) {
|
|
628
|
+
const rating = verdict?.rating ?? "unknown";
|
|
629
|
+
if (!realisation.inProcessEnabled) {
|
|
630
|
+
return { path: "isolated", rating, reason: HOSTING_CODE.disabled };
|
|
631
|
+
}
|
|
632
|
+
if (realisation.linked === true) {
|
|
633
|
+
return realisation.inProcessAvailable
|
|
634
|
+
? { path: "in-process", rating, reason: HOSTING_CODE.linked }
|
|
635
|
+
: { path: "isolated", rating, reason: HOSTING_CODE.noRealisation };
|
|
636
|
+
}
|
|
637
|
+
if (verdict === undefined) {
|
|
638
|
+
return { path: "isolated", rating, reason: HOSTING_CODE.unjudged };
|
|
639
|
+
}
|
|
640
|
+
if (verdict.path === "isolated") {
|
|
641
|
+
return {
|
|
642
|
+
path: "isolated",
|
|
643
|
+
rating,
|
|
644
|
+
// `deciding` is present for every rating below `suitable`; the fall-back names the one
|
|
645
|
+
// state that could produce an isolated verdict with no reason at all — nothing judged.
|
|
646
|
+
reason: verdict.deciding?.code ?? HOSTING_CODE.unjudged,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (!realisation.inProcessAvailable) {
|
|
650
|
+
return { path: "isolated", rating, reason: HOSTING_CODE.noRealisation };
|
|
651
|
+
}
|
|
652
|
+
return { path: "in-process", rating, reason: HOSTING_CODE.realisedInProcess };
|
|
653
|
+
}
|
|
654
|
+
//# sourceMappingURL=hosting-suitability.js.map
|