@openmixer/plugin-qualify 0.18.0 → 0.18.1

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 (54) hide show
  1. package/README.md +84 -2
  2. package/bin/plugin-qualify-sync.mjs +3 -1
  3. package/dist/distributed-records.d.ts +4 -2
  4. package/dist/distributed-records.js +1 -1
  5. package/dist/distributed-records.js.map +1 -1
  6. package/dist/host-admission.d.ts +2 -0
  7. package/dist/host-admission.js +12 -3
  8. package/dist/host-admission.js.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +2 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/probe-judge.d.ts +11 -0
  13. package/dist/probe-judge.js +4 -1
  14. package/dist/probe-judge.js.map +1 -1
  15. package/dist/probe-runner.js +3 -2
  16. package/dist/probe-runner.js.map +1 -1
  17. package/dist/qualification-record.d.ts +90 -8
  18. package/dist/qualification-record.js +18 -13
  19. package/dist/qualification-record.js.map +1 -1
  20. package/dist/qualify-adapter-pass.d.ts +67 -0
  21. package/dist/qualify-adapter-pass.js +255 -0
  22. package/dist/qualify-adapter-pass.js.map +1 -0
  23. package/dist/qualify-declarations.d.ts +25 -0
  24. package/dist/qualify-declarations.js +17 -1
  25. package/dist/qualify-declarations.js.map +1 -1
  26. package/dist/qualify-rejudge.d.ts +81 -0
  27. package/dist/qualify-rejudge.js +421 -0
  28. package/dist/qualify-rejudge.js.map +1 -0
  29. package/dist/qualify-sync.d.ts +72 -1
  30. package/dist/qualify-sync.js +176 -17
  31. package/dist/qualify-sync.js.map +1 -1
  32. package/fixtures/records-0.16.1/http___example.org_raffo@unversioned-2060c09ff626.default.probe.json +46 -0
  33. package/fixtures/records-0.16.1/http___example.org_raffo@unversioned-2060c09ff626.default.qualification.json +185 -0
  34. package/fixtures/records-0.16.1/http___lv2plug.in_plugins_eg-amp@unversioned-f046a60efd45.default.probe.json +51 -0
  35. package/fixtures/records-0.16.1/http___lv2plug.in_plugins_eg-amp@unversioned-f046a60efd45.default.qualification.json +232 -0
  36. package/fixtures/records-0.16.1/https___github.com_lucianodato_noise-repellent@unversioned-c3b5e21dd9a7.default.probe.json +51 -0
  37. package/fixtures/records-0.16.1/https___github.com_lucianodato_noise-repellent@unversioned-c3b5e21dd9a7.default.qualification.json +245 -0
  38. package/fixtures/records-0.16.1/in.lsp-plug.impulse_responses_mono@unversioned-d5235d2f5f53.custom.probe.json +52 -0
  39. package/fixtures/records-0.16.1/in.lsp-plug.impulse_responses_mono@unversioned-d5235d2f5f53.custom.qualification.json +145 -0
  40. package/fixtures/records-0.16.1/in.lsp-plug.trigger_midi_mono@unversioned-d5235d2f5f53.custom.probe.json +52 -0
  41. package/fixtures/records-0.16.1/in.lsp-plug.trigger_midi_mono@unversioned-d5235d2f5f53.custom.qualification.json +155 -0
  42. package/package.json +1 -1
  43. package/src/distributed-records.ts +1 -1
  44. package/src/host-admission.ts +13 -3
  45. package/src/index.ts +2 -0
  46. package/src/probe-judge.ts +8 -1
  47. package/src/probe-runner.ts +2 -2
  48. package/src/qualification-record.ts +100 -15
  49. package/src/qualify-adapter-pass.ts +278 -0
  50. package/src/qualify-declarations.ts +19 -1
  51. package/src/qualify-rejudge.ts +452 -0
  52. package/src/qualify-sync.ts +192 -17
  53. package/tools/lv2_resolve.py +49 -0
  54. package/tools/mod-host-admit.mjs +21 -5
@@ -0,0 +1,145 @@
1
+ {
2
+ "schema": "plugin-qualify.qualification-record/1",
3
+ "plugin": {
4
+ "format": "clap",
5
+ "id": "in.lsp-plug.impulse_responses_mono"
6
+ },
7
+ "binary": {
8
+ "path": "/usr/lib/clap/lsp-plugins.clap",
9
+ "sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2",
10
+ "files": [
11
+ {
12
+ "path": "lsp-plugins.clap",
13
+ "sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2"
14
+ }
15
+ ],
16
+ "layout": {
17
+ "scheme": "omx-layout/1",
18
+ "sha256": "86f71f835f0db0150b6b219b738b97ae0d9d423d2305e29ebdb172b8e20a6381"
19
+ }
20
+ },
21
+ "verdicts": [
22
+ {
23
+ "host": "omx-clap-host",
24
+ "measured": true,
25
+ "rating": "unsuitable",
26
+ "qualified": false,
27
+ "hosting": "isolated",
28
+ "arm": "declaration",
29
+ "deciding": "hosting.rt-safety.violations-in-run",
30
+ "reasons": [
31
+ {
32
+ "dimension": "rtSafety",
33
+ "rating": "unsuitable",
34
+ "code": "hosting.rt-safety.violations-in-run",
35
+ "params": {
36
+ "allocations": 0,
37
+ "syscalls": 1,
38
+ "locks": 0
39
+ }
40
+ }
41
+ ]
42
+ }
43
+ ],
44
+ "tiers": {
45
+ "isolated": {
46
+ "host": "omx-clap-host",
47
+ "measured": true,
48
+ "qualified": false,
49
+ "rating": "unsuitable",
50
+ "deciding": "hosting.rt-safety.violations-in-run"
51
+ },
52
+ "offeredOn": [],
53
+ "dimmed": "omx-clap-host: hosting.rt-safety.violations-in-run"
54
+ },
55
+ "cost": [
56
+ {
57
+ "host": "omx-clap-host",
58
+ "rate": 48000,
59
+ "quantum": 128,
60
+ "reasons": [
61
+ {
62
+ "dimension": "cost",
63
+ "rating": "unknown",
64
+ "code": "hosting.cost.unmeasured-at-rate-and-quantum",
65
+ "params": {
66
+ "rate": 48000,
67
+ "quantum": 128
68
+ }
69
+ }
70
+ ]
71
+ }
72
+ ],
73
+ "measurements": {
74
+ "grid": [
75
+ {
76
+ "rate": 48000,
77
+ "quantum": 512,
78
+ "outcome": "probe.ok",
79
+ "latency": {
80
+ "declaredFrames": 0,
81
+ "measuredFrames": 0
82
+ },
83
+ "cost": {
84
+ "median": 213.506,
85
+ "percentile": {
86
+ "p": 95,
87
+ "value": 230.143
88
+ },
89
+ "blocks": 512,
90
+ "warmupBlocks": 64
91
+ },
92
+ "rtSafety": {
93
+ "interposer": true,
94
+ "swept": true,
95
+ "sweepApplicable": true,
96
+ "blocks": 512,
97
+ "allocations": 0,
98
+ "locks": 0,
99
+ "syscalls": 1,
100
+ "threadViolations": 0,
101
+ "processErrors": 0,
102
+ "events": [
103
+ "nanosleep"
104
+ ]
105
+ },
106
+ "noteSweep": {
107
+ "applicable": false
108
+ }
109
+ }
110
+ ]
111
+ },
112
+ "notMeasured": [
113
+ {
114
+ "dimension": "stability",
115
+ "code": "hosting.stability.unsoaked"
116
+ }
117
+ ],
118
+ "tool": {
119
+ "runner": "plugin-hostd-probe 0.13.1",
120
+ "child": "clap-qualify",
121
+ "interposer": "rt_interpose.so"
122
+ },
123
+ "measuredAt": "2026-10-28T20:56:58+00:00",
124
+ "probe": {
125
+ "file": "in.lsp-plug.impulse_responses_mono@unversioned-d5235d2f5f53.custom.probe.json",
126
+ "sha256": "bc818dfeff46238de3b5558ab8d54aa5677a18f6e3cad79c67bc117313d0a79d",
127
+ "bytes": 3894,
128
+ "protocol": {
129
+ "name": "plugin-hostd.probe-protocol",
130
+ "version": 1
131
+ },
132
+ "reason": "on-request",
133
+ "outcome": "probe.ok",
134
+ "complete": true,
135
+ "machineClass": {
136
+ "arch": "aarch64",
137
+ "family": "arm",
138
+ "coreBand": "3-4",
139
+ "clockBand": "1.5-2.5GHz",
140
+ "preemptRt": false,
141
+ "cstatesLimited": false
142
+ },
143
+ "meetsProtocol": false
144
+ }
145
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "format": "plugin-hostd.probe-output",
3
+ "formatVersion": "1.1",
4
+ "run": {
5
+ "id": "20261028T210152Z-321dd-bb8cc3c7",
6
+ "startedAt": "2026-10-28T21:01:52+00:00",
7
+ "elapsedMs": 332,
8
+ "complete": true,
9
+ "outcome": "probe.ok",
10
+ "reason": "on-request",
11
+ "protocol": {"name": "plugin-hostd.probe-protocol", "version": 1},
12
+ "tools": {"runner": "plugin-hostd-probe 0.13.1", "child": "clap-qualify", "interposer": "rt_interpose.so"},
13
+ "via": "runner"
14
+ },
15
+ "plugin": {"format": "clap", "id": "in.lsp-plug.trigger_midi_mono"},
16
+ "binary": {"path": "/usr/lib/clap/lsp-plugins.clap", "sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2", "files": [{"path": "lsp-plugins.clap", "sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2"}], "layout": {"scheme": "omx-layout/1", "sha256": "5a2b073f777379e75dcd931ff9930352d12dcfab9e1fb599d3b1d45d62fe3e1d"}},
17
+ "machine": {
18
+ "class": {"arch": "aarch64", "family": "arm", "coreBand": "3-4", "clockBand": "1.5-2.5GHz", "preemptRt": false, "cstatesLimited": false},
19
+ "profile": {"governor": "powersave", "turbo": false, "smt": false, "preempt": "PREEMPT",
20
+ "cpuidle": [],
21
+ "meetsProtocol": false, "unmet": ["probe.profile.governor", "probe.profile.not-pinned"]},
22
+ "pinnedCpus": "0-3"
23
+ },
24
+ "offer": {"id": "custom", "clapExtensions": ["clap.log", "clap.thread-check", "clap.latency", "clap.params", "clap.audio-ports", "clap.state", "clap.note-ports", "clap.note-name", "clap.voice-info", "clap.event-registry", "clap.timer-support", "clap.posix-fd-support", "clap.thread-pool", "clap.tail", "clap.surround/4", "clap.ambisonic/3", "clap.audio-ports-config", "clap.preset-load/2", "clap.track-info/1", "clap.remote-controls/2"], "realtime": "none"},
25
+ "limits": {"set": "default",
26
+ "applied": [
27
+ {"limit": "wall-clock", "value": 60, "unit": "s", "by": "supervisor"},
28
+ {"limit": "cpu-time", "value": 540, "unit": "s", "by": "rlimit"},
29
+ {"limit": "memory", "value": 4294967296, "unit": "bytes", "by": "rlimit"},
30
+ {"limit": "files", "value": 256, "unit": "count", "by": "rlimit"},
31
+ {"limit": "file-size", "value": 67108864, "unit": "bytes", "by": "rlimit"},
32
+ {"limit": "core-dump", "value": 0, "unit": "bytes", "by": "rlimit"},
33
+ {"limit": "rt-priority", "value": 0, "by": "rlimit"},
34
+ {"limit": "no-new-privs", "by": "prctl"}
35
+ ],
36
+ "unavailable": [
37
+ {"limit": "tasks", "code": "probe.limit.not-implemented"}
38
+ ]
39
+ },
40
+ "steps": [
41
+ {"step": "hash", "outcome": "probe.ok", "elapsedMs": 0},
42
+ {"step": "lifecycle", "outcome": "probe.ok", "elapsedMs": 332, "exitStatus": 0},
43
+ {"step": "instantiate", "outcome": "probe.ok", "elapsedMs": 0, "detail": "read from the child's line"},
44
+ {"step": "latency", "rate": 48000, "quantum": 512, "outcome": "probe.ok", "elapsedMs": 0, "detail": "read from the child's line"},
45
+ {"step": "rt-safety", "outcome": "probe.ok", "elapsedMs": 0, "detail": "counts are totals over every rate, read from the child's line"},
46
+ {"step": "note-sweep", "outcome": "probe.ok", "elapsedMs": 0, "detail": "counts are totals over every rate, read from the child's line"}
47
+ ],
48
+ "grid": [
49
+ {"rate": 48000, "quantum": 512, "outcome": "probe.ok", "latency": {"declaredFrames": 0, "measuredFrames": 0, "method": "impulse-onset"}, "cost": {"unit": "ns-per-sample", "median": 47.9961, "percentile": {"p": 95, "value": 57.2365}, "min": 31.5742, "max": 114.258, "blocks": 512, "warmupBlocks": 64}, "rtSafety": {"interposer": true, "swept": true, "sweepApplicable": true, "blocks": 512, "allocations": 0, "locks": 0, "syscalls": 0, "threadViolations": 0, "processErrors": 0, "events": []}, "noteSweep": {"applicable": true, "dialect": "midi", "allocations": 0, "locks": 0, "syscalls": 1}}
50
+ ],
51
+ "logs": [{"name": "stderr", "path": "/tmp/plugin-qualify-probe-0t8B87/child.stderr.log", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "bytes": 0}]
52
+ }
@@ -0,0 +1,155 @@
1
+ {
2
+ "schema": "plugin-qualify.qualification-record/1",
3
+ "plugin": {
4
+ "format": "clap",
5
+ "id": "in.lsp-plug.trigger_midi_mono"
6
+ },
7
+ "binary": {
8
+ "path": "/usr/lib/clap/lsp-plugins.clap",
9
+ "sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2",
10
+ "files": [
11
+ {
12
+ "path": "lsp-plugins.clap",
13
+ "sha256": "d5235d2f5f53eb3641dc9225ff5a912b5eae718cd0abbecd46350dba666f19a2"
14
+ }
15
+ ],
16
+ "layout": {
17
+ "scheme": "omx-layout/1",
18
+ "sha256": "5a2b073f777379e75dcd931ff9930352d12dcfab9e1fb599d3b1d45d62fe3e1d"
19
+ }
20
+ },
21
+ "verdicts": [
22
+ {
23
+ "host": "omx-clap-host",
24
+ "measured": true,
25
+ "rating": "unsuitable",
26
+ "qualified": false,
27
+ "hosting": "isolated",
28
+ "arm": "declaration",
29
+ "deciding": "hosting.rt-safety.violations-under-notes",
30
+ "reasons": [
31
+ {
32
+ "dimension": "rtSafety",
33
+ "rating": "suitable",
34
+ "code": "hosting.rt-safety.clean-under-sweep",
35
+ "params": {
36
+ "blocks": 512
37
+ }
38
+ },
39
+ {
40
+ "dimension": "rtSafety",
41
+ "rating": "unsuitable",
42
+ "code": "hosting.rt-safety.violations-under-notes",
43
+ "params": {
44
+ "allocations": 0,
45
+ "syscalls": 1,
46
+ "locks": 0
47
+ }
48
+ }
49
+ ]
50
+ }
51
+ ],
52
+ "tiers": {
53
+ "isolated": {
54
+ "host": "omx-clap-host",
55
+ "measured": true,
56
+ "qualified": false,
57
+ "rating": "unsuitable",
58
+ "deciding": "hosting.rt-safety.violations-under-notes"
59
+ },
60
+ "offeredOn": [],
61
+ "dimmed": "omx-clap-host: hosting.rt-safety.violations-under-notes"
62
+ },
63
+ "cost": [
64
+ {
65
+ "host": "omx-clap-host",
66
+ "rate": 48000,
67
+ "quantum": 128,
68
+ "reasons": [
69
+ {
70
+ "dimension": "cost",
71
+ "rating": "unknown",
72
+ "code": "hosting.cost.unmeasured-at-rate-and-quantum",
73
+ "params": {
74
+ "rate": 48000,
75
+ "quantum": 128
76
+ }
77
+ }
78
+ ]
79
+ }
80
+ ],
81
+ "measurements": {
82
+ "grid": [
83
+ {
84
+ "rate": 48000,
85
+ "quantum": 512,
86
+ "outcome": "probe.ok",
87
+ "latency": {
88
+ "declaredFrames": 0,
89
+ "measuredFrames": 0
90
+ },
91
+ "cost": {
92
+ "median": 47.9961,
93
+ "percentile": {
94
+ "p": 95,
95
+ "value": 57.2365
96
+ },
97
+ "blocks": 512,
98
+ "warmupBlocks": 64
99
+ },
100
+ "rtSafety": {
101
+ "interposer": true,
102
+ "swept": true,
103
+ "sweepApplicable": true,
104
+ "blocks": 512,
105
+ "allocations": 0,
106
+ "locks": 0,
107
+ "syscalls": 0,
108
+ "threadViolations": 0,
109
+ "processErrors": 0,
110
+ "events": []
111
+ },
112
+ "noteSweep": {
113
+ "applicable": true,
114
+ "dialect": "midi",
115
+ "allocations": 0,
116
+ "locks": 0,
117
+ "syscalls": 1
118
+ }
119
+ }
120
+ ]
121
+ },
122
+ "notMeasured": [
123
+ {
124
+ "dimension": "stability",
125
+ "code": "hosting.stability.unsoaked"
126
+ }
127
+ ],
128
+ "tool": {
129
+ "runner": "plugin-hostd-probe 0.13.1",
130
+ "child": "clap-qualify",
131
+ "interposer": "rt_interpose.so"
132
+ },
133
+ "measuredAt": "2026-10-28T21:01:52+00:00",
134
+ "probe": {
135
+ "file": "in.lsp-plug.trigger_midi_mono@unversioned-d5235d2f5f53.custom.probe.json",
136
+ "sha256": "17986ad33bc5087175fcce9512761cc28fb66d18beb01598ee2e72b5c5302fa6",
137
+ "bytes": 3978,
138
+ "protocol": {
139
+ "name": "plugin-hostd.probe-protocol",
140
+ "version": 1
141
+ },
142
+ "reason": "on-request",
143
+ "outcome": "probe.ok",
144
+ "complete": true,
145
+ "machineClass": {
146
+ "arch": "aarch64",
147
+ "family": "arm",
148
+ "coreBand": "3-4",
149
+ "clockBand": "1.5-2.5GHz",
150
+ "preemptRt": false,
151
+ "cstatesLimited": false
152
+ },
153
+ "meetsProtocol": false
154
+ }
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmixer/plugin-qualify",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "Qualify audio plugins for real-time hosting: scan, offline latency and cost measurement, and a fail-closed rating.",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -153,7 +153,7 @@ export function distributedUri(item: SyncItem, r: DistributedRecord): string {
153
153
  }
154
154
 
155
155
  /** What the sync's own judgment would make of the record's verdict for the host, as `outcomeOf` reads a run. */
156
- function outcomeOfVerdict(v: RecordVerdict): SyncOutcome {
156
+ export function outcomeOfVerdict(v: Pick<RecordVerdict, "measured" | "rating" | "deciding">): SyncOutcome {
157
157
  if (!v.measured) return v.deciding === "probe.crashed" ? "crashed" : v.deciding === "probe.timeout" ? "hung" : "unmeasured";
158
158
  if (v.rating === "suitable" || v.rating === "conditional") return "qualified";
159
159
  return v.rating === "unsuitable" ? "refused" : "unmeasured";
@@ -12,6 +12,7 @@ import { spawnSync } from "node:child_process";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
14
  import type { HostProfile } from "./host-profiles.js";
15
+ import { ADMIT_STEP_MS_ENV } from "./qualify-declarations.js";
15
16
 
16
17
  /** The profile ids whose verdict is measured by a run. */
17
18
  export const ADMITTED_HOSTS = ["mod-host", "jalv"] as const;
@@ -29,13 +30,16 @@ export function admittedHostOf(profile: HostProfile): AdmittedHost | undefined {
29
30
  */
30
31
  export const ADAPTER_HOST = "omx-clap-host";
31
32
 
33
+ /** The profiles whose LV2 plugins run under that adapter: omx-clap-host, and the Pi boards it runs on (`host-profiles.ts`). */
34
+ export const ADAPTER_RUN_HOSTS: readonly string[] = [ADAPTER_HOST, "openmixer-pi-4gb", "openmixer-pi-8gb"];
35
+
32
36
  /**
33
37
  * Whether this run decides the profile's verdict for a plugin of this format. A mod-host or jalv run always does, an
34
38
  * unmeasured one making the verdict unknown; the adapter's only when it measured, since the omx-clap-host verdict
35
39
  * otherwise stands on the measurement.
36
40
  */
37
41
  export function runDecides(profile: HostProfile, format: string, admission: HostAdmission): boolean {
38
- return admittedHostOf(profile) !== undefined || (profile.id === ADAPTER_HOST && format === "lv2" && admission.measured);
42
+ return admittedHostOf(profile) !== undefined || (ADAPTER_RUN_HOSTS.includes(profile.id) && format === "lv2" && admission.measured);
39
43
  }
40
44
 
41
45
  /** What running the plugin under one host came to. */
@@ -144,7 +148,10 @@ const MOD_HOST_ADMIT = fileURLToPath(new URL("../tools/mod-host-admit.mjs", impo
144
148
  /** The runs against the installed hosts. */
145
149
  export const SPAWNED_ADMISSIONS: AdmissionRuns = {
146
150
  adapter(uri, env) {
147
- const r = spawnSync(process.execPath, [MOD_HOST_ADMIT, uri, ADAPTER_HOST], { encoding: "utf8", timeout: MOD_HOST_ADMISSION_MS, ...(env === undefined ? {} : { env: { ...env } }) });
151
+ // two commands at the limit the pass gives each, and the tool's own start and end: it kills the host itself first
152
+ const stepMs = Number(env?.[ADMIT_STEP_MS_ENV] ?? 0);
153
+ const timeout = stepMs > 0 ? 2 * stepMs + 15000 : MOD_HOST_ADMISSION_MS;
154
+ const r = spawnSync(process.execPath, [MOD_HOST_ADMIT, uri, ADAPTER_HOST], { encoding: "utf8", timeout, killSignal: "SIGKILL", ...(env === undefined ? {} : { env: { ...env } }) });
148
155
  if (r.error !== undefined || r.status !== 0) {
149
156
  throw new Error(`${ADAPTER_HOST} admission of ${uri} gave no answer (${r.error?.message ?? `exit ${String(r.status)}`}): ${(r.stderr ?? "").trim()}`);
150
157
  }
@@ -158,7 +165,10 @@ export const SPAWNED_ADMISSIONS: AdmissionRuns = {
158
165
  return JSON.parse(r.stdout) as ModHostRun;
159
166
  },
160
167
  jalv(uri, env) {
161
- const r = spawnSync("jalv", ["-n", "plugin-qualify", uri], { encoding: "utf8", timeout: JALV_HOSTED_MS, input: "", ...(env === undefined ? {} : { env: { ...env } }) });
168
+ // SIGKILL at the deadline: jalv hosting some plugins ignores SIGTERM, and one stuck in jack_client_open never reaches
169
+ // the loop that reads the flag TERM sets, so a TERM left spawnSync waiting with it (#187). Alive at the deadline is
170
+ // all the verdict asks, so nothing is lost by not letting it exit cleanly.
171
+ const r = spawnSync("jalv", ["-n", "plugin-qualify", uri], { encoding: "utf8", timeout: JALV_HOSTED_MS, killSignal: "SIGKILL", input: "", ...(env === undefined ? {} : { env: { ...env } }) });
162
172
  const code = (r.error as NodeJS.ErrnoException | undefined)?.code;
163
173
  if (code === "ENOENT") return { installed: false, stillRunning: false, status: null, signal: null, stderr: "" };
164
174
  return { installed: true, stillRunning: code === "ETIMEDOUT", status: r.status, signal: code === "ETIMEDOUT" ? null : r.signal, stderr: r.stderr ?? "" };
package/src/index.ts CHANGED
@@ -31,6 +31,8 @@ export * from "./probe-merge.js";
31
31
  export * from "./judge-cli.js";
32
32
  export * from "./whole-file.js";
33
33
  export * from "./qualify-sync.js";
34
+ export * from "./qualify-rejudge.js";
35
+ export * from "./qualify-adapter-pass.js";
34
36
  export * from "./distributed-records.js";
35
37
  export * from "./records-package.js";
36
38
  export * from "./host-admission.js";
@@ -51,6 +51,11 @@ export interface JudgeFacts {
51
51
  * judged by it as well as by its declaration; one that has none is judged from its declaration alone.
52
52
  */
53
53
  readonly admissions?: Readonly<Record<string, HostAdmission>>;
54
+ /**
55
+ * The run each profile's verdict needs and the figures do not hold, by profile id (`qualify-rejudge.ts`): an `unknown`
56
+ * reason in that verdict, so that a verdict never claims a run nobody made.
57
+ */
58
+ readonly owed?: Readonly<Record<string, RecordReason>>;
54
59
  }
55
60
 
56
61
  /** A sum of one count over the points, or `undefined` when a point lacks it: a missing count is not zero. */
@@ -244,7 +249,7 @@ function costOf(output: ProbeOutput, profile: HostProfile): HostJudgment["cost"]
244
249
  * first word; a grid point that failed under a run that says `probe.ok` is a document that contradicts
245
250
  * itself, and is read as the failure it records rather than as a pass that skipped the point.
246
251
  */
247
- function failedOutcome(output: ProbeOutput): string | undefined {
252
+ export function failedOutcome(output: ProbeOutput): string | undefined {
248
253
  if (output.run.outcome !== PROBE_OK) return output.run.outcome;
249
254
  // a cell that outgrew the memory bound is a recorded outcome of that cell, judged per host below, not the run's end
250
255
  return output.grid.find((p) => p.outcome !== PROBE_OK && p.outcome !== PROBE_NOT_ATTEMPTED && !isOutOfMemory(p.outcome))?.outcome;
@@ -313,6 +318,8 @@ export function judgeProbeOutput(output: ProbeOutput, profiles: readonly HostPro
313
318
  };
314
319
  }
315
320
  }
321
+ const owed = facts.owed?.[profile.id];
322
+ if (owed !== undefined) reasons = [owed, ...reasons];
316
323
  const rating = worstRating(reasons.map((r) => r.rating));
317
324
  const deciding = decidingOf(reasons, rating);
318
325
  return {
@@ -22,7 +22,7 @@ import type { HostProfile } from "./host-profiles.js";
22
22
  import { OPENMIXER_CONSOLE_PROFILE, shippedHostProfile } from "./host-profiles.js";
23
23
  import { IN_PROCESS_TIER_HOST, ISOLATED_TIER_HOST } from "./qualification-record.js";
24
24
  import type { PluginTarget } from "./loader.js";
25
- import { ADAPTER_HOST, SPAWNED_ADMISSIONS, admitPlugin, admitUnderAdapter, admittedHostOf, type AdmissionRuns, type HostAdmission } from "./host-admission.js";
25
+ import { ADAPTER_RUN_HOSTS, SPAWNED_ADMISSIONS, admitPlugin, admitUnderAdapter, admittedHostOf, type AdmissionRuns, type HostAdmission } from "./host-admission.js";
26
26
  import { judgeProbeOutput, judgmentVerdicts, type Judgment } from "./probe-judge.js";
27
27
  import { readProbeOutput, type ProbeOutput } from "./probe-output.js";
28
28
  import { budgetCells, gridCells, hostCells, runnerInvocations, type GridCell, type GridSpec, type RunnerInvocation } from "./measurement-grid.js";
@@ -299,7 +299,7 @@ export function qualifyTargets(
299
299
  // an LV2 plugin is also run once under plugin-hostd's LV2 adapter, recorded beside the verdicts (#223)
300
300
  const adapter = target.format === "lv2" ? admitUnderAdapter(target.uri, admissionRuns, admissionEnv) : undefined;
301
301
  // and that run decides its omx-clap-host verdict, the isolated tier
302
- if (adapter !== undefined) admissions[ADAPTER_HOST] = adapter;
302
+ if (adapter !== undefined) for (const host of ADAPTER_RUN_HOSTS) admissions[host] = adapter;
303
303
  if (target.format === "lv2") {
304
304
  for (const profile of judgedProfiles) {
305
305
  const host = admittedHostOf(profile);
@@ -43,6 +43,28 @@ export interface RecordVerdict {
43
43
  readonly reasons: readonly RecordReason[];
44
44
  }
45
45
 
46
+ /**
47
+ * An LV2 plugin's run under the adapter. The top is what the judge reads. `--adapter-only` adds the versions of the hosts
48
+ * it ran under, when, and how each cell went.
49
+ */
50
+ export interface AdapterBlock {
51
+ readonly host: string;
52
+ readonly measured: boolean;
53
+ readonly admitted: boolean;
54
+ readonly code?: string;
55
+ readonly params: Readonly<Record<string, string | number>>;
56
+ readonly versions?: Readonly<Record<string, string>>;
57
+ readonly measuredAt?: string;
58
+ readonly cells?: readonly {
59
+ readonly rate: number;
60
+ readonly quantum: number;
61
+ readonly measured: boolean;
62
+ readonly admitted: boolean;
63
+ readonly code?: string;
64
+ readonly params: Readonly<Record<string, string | number>>;
65
+ }[];
66
+ }
67
+
46
68
  /** One tier of the console as a verdict: the host it is judged under and whether the plugin qualified there. */
47
69
  export interface TierVerdict {
48
70
  readonly host: string;
@@ -50,9 +72,15 @@ export interface TierVerdict {
50
72
  readonly qualified: boolean;
51
73
  readonly rating: string;
52
74
  readonly deciding?: string;
75
+ /** The isolated tier of a `conditional` plugin: it is offered, and runs alone in a worker of its own, never grouped. */
76
+ readonly runsAlone?: true;
53
77
  }
54
78
 
55
- /** The console's two tiers (what the console's roster offers): offered where the verdict qualified, dimmed with the reason when neither did. */
79
+ /**
80
+ * The console's two tiers (what the console's roster offers): in-process where its verdict qualified; isolated where its
81
+ * verdict qualified or is `conditional`, which pins `isolated` and runs alone (`runsAlone`); dimmed with the reason when
82
+ * neither. `qualified` stays `rating === "suitable"`, as the interchange's reader holds it.
83
+ */
56
84
  export interface RecordTiers {
57
85
  readonly inProcess?: TierVerdict;
58
86
  readonly isolated?: TierVerdict;
@@ -117,8 +145,11 @@ const word = (key: keyof typeof MEASUREMENT_UNMEASURED_REASONS): string => MEASU
117
145
  /** The same, for the test. */
118
146
  export const memoryOfCellForTest = (p: ProbeGridPoint): Pick<RecordGridPoint, "memory"> => memoryOfCell(p);
119
147
 
148
+ /** The contract's word for a cell whose run ended with this outcome, `probe-error` for one it has no word of. */
149
+ export const unmeasuredWordOf = (outcome: string): string => OUTCOME_WORD[outcome] ?? word("probeError");
150
+
120
151
  /** A cell's footprint with what it comes to; only a footprint with no reason beside it comes to anything a client may add up. */
121
- function memoryOfCell(p: ProbeGridPoint): Pick<RecordGridPoint, "memory"> {
152
+ export function memoryOfCell(p: ProbeGridPoint): Pick<RecordGridPoint, "memory"> {
122
153
  if (p.memory === undefined) return {};
123
154
  const first = p.memory.unmeasuredReason === undefined ? firstInstanceKb(p.memory) : undefined;
124
155
  const per = p.memory.unmeasuredReason === undefined ? perInstanceKb(p.memory) : undefined;
@@ -181,6 +212,51 @@ export interface HistoryBlock {
181
212
  readonly verdicts: readonly ({ readonly host: string } & Adjustment)[];
182
213
  }
183
214
 
215
+ /** One measurement a record lacks, and what it keeps from being decided. */
216
+ export interface OwedMeasurement {
217
+ /** `adapter-run`: the plugin under omx-clap-host's LV2 adapter. `host-run`: under the host named. `qualification`: the whole of it. */
218
+ readonly measurement: "adapter-run" | "host-run" | "qualification";
219
+ readonly host?: string;
220
+ readonly code: string;
221
+ readonly detail?: string;
222
+ }
223
+
224
+ /**
225
+ * The verdicts are not the ones of the run that took the figures: `plugin-qualify-sync --rejudge` derived them later, from
226
+ * those figures, under the rules of `by`. `figures` says who took the figures and when; the verdicts claim nothing more.
227
+ */
228
+ export interface RejudgedBlock {
229
+ /** The UTC time the verdicts last changed. */
230
+ readonly at: string;
231
+ /** The plugin-qualify whose rules judged them. */
232
+ readonly by: string;
233
+ readonly figures: {
234
+ readonly tool: Readonly<Record<string, string>>;
235
+ readonly measuredAt: string;
236
+ readonly runId: string;
237
+ /** The footprints, when a memory-only pass took them later than the rest. */
238
+ readonly memory?: { readonly tool: Readonly<Record<string, string>>; readonly measuredAt: string };
239
+ /** The adapter run, when an adapter-only pass took it later than the rest. */
240
+ readonly adapter?: { readonly versions: Readonly<Record<string, string>>; readonly measuredAt: string };
241
+ };
242
+ /** What these verdicts could not decide and left `unknown`; absent when nothing is owed. */
243
+ readonly owes?: readonly OwedMeasurement[];
244
+ }
245
+
246
+ /** The memory-only pass that took this record's footprints: every other figure is older and comes from `tool`. */
247
+ export interface MemoryRunBlock {
248
+ readonly tool: Readonly<Record<string, string>>;
249
+ readonly measuredAt: string;
250
+ readonly runId: string;
251
+ readonly cells: readonly {
252
+ readonly rate: number;
253
+ readonly quantum: number;
254
+ /** How the cell's run ended; its figures are in the grid's point of that cell. */
255
+ readonly outcome: string;
256
+ readonly memoryLimit?: { readonly bytes: number; readonly mechanism: string };
257
+ }[];
258
+ }
259
+
184
260
  export interface QualificationRecord {
185
261
  readonly schema: typeof QUALIFICATION_RECORD_SCHEMA;
186
262
  readonly plugin: { readonly format: string; readonly id: string; readonly name?: string };
@@ -192,7 +268,7 @@ export interface QualificationRecord {
192
268
  * An LV2 plugin run under plugin-hostd's LV2 adapter (omx-clap-host), as under mod-host: admitted, or refused with
193
269
  * its code, or `measured: false` when it was not tried. Beside the verdicts, which it does not change (#223).
194
270
  */
195
- readonly adapter?: { readonly host: string; readonly measured: boolean; readonly admitted: boolean; readonly code?: string; readonly params: Readonly<Record<string, string | number>> };
271
+ readonly adapter?: AdapterBlock;
196
272
  readonly tiers: RecordTiers;
197
273
  /** The cost reasons at each host's rate and quantum, beside the verdicts; an instrument's with the notes it held. */
198
274
  readonly cost: readonly {
@@ -223,6 +299,10 @@ export interface QualificationRecord {
223
299
  readonly history?: readonly HistoryBlock[];
224
300
  /** Where the record came from when it was not measured on this device: the distributed set's provenance, as the set's package wrote it. */
225
301
  readonly provenance?: Readonly<Record<string, unknown>>;
302
+ /** Present when the verdicts were derived later from the figures below, measuring nothing. */
303
+ readonly rejudged?: RejudgedBlock;
304
+ /** Present when a memory-only pass merged footprints into the grid. */
305
+ readonly memoryRun?: MemoryRunBlock;
226
306
  /** The probe output this record judged. */
227
307
  readonly probe: {
228
308
  readonly file: string;
@@ -264,18 +344,32 @@ function tierOf(judgment: Judgment, host: string | undefined): TierVerdict | und
264
344
  export function tiersOf(judgment: Judgment): RecordTiers {
265
345
  const inProcess = tierOf(judgment, IN_PROCESS_TIER_HOST);
266
346
  const isolated = tierOf(judgment, ISOLATED_TIER_HOST[judgment.output.plugin.format]);
347
+ const runsAlone = isolated?.measured === true && isolated.rating === "conditional";
267
348
  const offeredOn: ("in-process" | "isolated")[] = [];
268
349
  if (inProcess?.qualified === true) offeredOn.push("in-process");
269
- if (isolated?.qualified === true) offeredOn.push("isolated");
350
+ if (isolated?.qualified === true || runsAlone) offeredOn.push("isolated");
270
351
  const why = [inProcess, isolated].flatMap((t) => (t === undefined ? [] : [`${t.host}: ${t.deciding ?? t.rating}`]));
271
352
  return {
272
353
  ...(inProcess === undefined ? {} : { inProcess }),
273
- ...(isolated === undefined ? {} : { isolated }),
354
+ ...(isolated === undefined ? {} : { isolated: { ...isolated, ...(runsAlone ? { runsAlone: true as const } : {}) } }),
274
355
  offeredOn,
275
356
  ...(offeredOn.length === 0 ? { dimmed: why.length === 0 ? "no tier was judged" : why.join("; ") } : {}),
276
357
  };
277
358
  }
278
359
 
360
+ /** A judgment's verdicts as the record writes them. */
361
+ export const verdictRows = (judgment: Judgment): RecordVerdict[] =>
362
+ judgment.hosts.map((h) => ({
363
+ host: h.verdict.host,
364
+ measured: h.verdict.measured,
365
+ rating: h.verdict.rating,
366
+ qualified: h.verdict.qualified,
367
+ hosting: h.hosting,
368
+ arm: h.arm,
369
+ ...(h.verdict.deciding === undefined ? {} : { deciding: h.verdict.deciding }),
370
+ reasons: h.reasons,
371
+ }));
372
+
279
373
  export function buildQualificationRecord(
280
374
  judgment: Judgment,
281
375
  copy: { file: string; sha256: string; bytes: number },
@@ -287,16 +381,7 @@ export function buildQualificationRecord(
287
381
  plugin: { format: o.plugin.format, id: o.plugin.id, ...(o.plugin.name === undefined ? {} : { name: o.plugin.name }) },
288
382
  ...(o.plugin.version === undefined ? {} : { testedVersion: o.plugin.version }),
289
383
  binary: o.binary,
290
- verdicts: judgment.hosts.map((h) => ({
291
- host: h.verdict.host,
292
- measured: h.verdict.measured,
293
- rating: h.verdict.rating,
294
- qualified: h.verdict.qualified,
295
- hosting: h.hosting,
296
- arm: h.arm,
297
- ...(h.verdict.deciding === undefined ? {} : { deciding: h.verdict.deciding }),
298
- reasons: h.reasons,
299
- })),
384
+ verdicts: verdictRows(judgment),
300
385
  ...(judgment.adapter === undefined ? {} : { adapter: judgment.adapter }),
301
386
  tiers: tiersOf(judgment),
302
387
  cost: judgment.hosts.map((h) => ({ host: h.verdict.host, ...h.cost })),