@openmixer/plugin-qualify 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +153 -6
  2. package/bin/plugin-qualify-records-package.mjs +31 -0
  3. package/bin/plugin-qualify-sync.mjs +6 -1
  4. package/bin/plugin-qualify.mjs +12 -1
  5. package/dist/distributed-records.d.ts +88 -0
  6. package/dist/distributed-records.js +210 -0
  7. package/dist/distributed-records.js.map +1 -0
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.js +5 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/measurement-grid.d.ts +49 -0
  12. package/dist/measurement-grid.js +76 -0
  13. package/dist/measurement-grid.js.map +1 -0
  14. package/dist/memory-bound.d.ts +23 -0
  15. package/dist/memory-bound.js +46 -0
  16. package/dist/memory-bound.js.map +1 -0
  17. package/dist/probe-judge.js +22 -1
  18. package/dist/probe-judge.js.map +1 -1
  19. package/dist/probe-merge.d.ts +30 -0
  20. package/dist/probe-merge.js +80 -0
  21. package/dist/probe-merge.js.map +1 -0
  22. package/dist/probe-output.d.ts +8 -0
  23. package/dist/probe-output.js +7 -0
  24. package/dist/probe-output.js.map +1 -1
  25. package/dist/probe-runner.d.ts +26 -2
  26. package/dist/probe-runner.js +38 -10
  27. package/dist/probe-runner.js.map +1 -1
  28. package/dist/qualification-record.d.ts +19 -1
  29. package/dist/qualification-record.js +30 -1
  30. package/dist/qualification-record.js.map +1 -1
  31. package/dist/qualify-cli.d.ts +9 -0
  32. package/dist/qualify-cli.js +30 -1
  33. package/dist/qualify-cli.js.map +1 -1
  34. package/dist/qualify-declarations.d.ts +15 -0
  35. package/dist/qualify-declarations.js +15 -0
  36. package/dist/qualify-declarations.js.map +1 -1
  37. package/dist/qualify-options.js +11 -1
  38. package/dist/qualify-options.js.map +1 -1
  39. package/dist/qualify-sync.d.ts +18 -1
  40. package/dist/qualify-sync.js +142 -12
  41. package/dist/qualify-sync.js.map +1 -1
  42. package/dist/records-package.d.ts +63 -0
  43. package/dist/records-package.js +300 -0
  44. package/dist/records-package.js.map +1 -0
  45. package/package.json +3 -2
  46. package/src/distributed-records.ts +251 -0
  47. package/src/index.ts +5 -0
  48. package/src/measurement-grid.ts +97 -0
  49. package/src/memory-bound.ts +55 -0
  50. package/src/probe-judge.ts +25 -1
  51. package/src/probe-merge.ts +89 -0
  52. package/src/probe-output.ts +13 -0
  53. package/src/probe-runner.ts +75 -13
  54. package/src/qualification-record.ts +44 -2
  55. package/src/qualify-cli.ts +32 -1
  56. package/src/qualify-declarations.ts +17 -0
  57. package/src/qualify-options.ts +11 -1
  58. package/src/qualify-sync.ts +147 -11
  59. package/src/records-package.ts +333 -0
  60. package/tools/{ai-trace-commit-check.sh → commit-message-check.sh} +16 -16
  61. package/tools/{ai-trace-patterns.json → commit-message-patterns.json} +1 -1
@@ -0,0 +1,300 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * `plugin-qualify-records-package`: the Debian package of a device's qualification records, so other devices of the
5
+ * machine class install them as the distributed layer instead of measuring the same plugin set again.
6
+ *
7
+ * In: a qualification directory as plugin-qualify-sync leaves it. Out: `omx-qualification-<class>-<set>_<version>_all.deb`
8
+ * installing every record and the probe output it judged under `/usr/share/omx/qualification/<class>/<set>/`, beside
9
+ * the set's `provenance.json`. A record goes in only with its probe output, byte for byte the copy the record names;
10
+ * an unmeasured record never applies and stays out. The archive is written here, `ar` and two `tar.xz` members, so a
11
+ * machine without dpkg builds it too; the tar members are GNU tar's, owned by root, in name order, at one time.
12
+ */
13
+ import { createHash } from "node:crypto";
14
+ import { spawnSync } from "node:child_process";
15
+ import { closeSync, copyFileSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { DISTRIBUTED_DIR_SYSTEM, DISTRIBUTED_LAYER_SINCE, DISTRIBUTED_PROVENANCE_FILE, DISTRIBUTED_PROVENANCE_SCHEMA } from "./distributed-records.js";
19
+ import { sha256Hex } from "./probe-output.js";
20
+ import { QUALIFICATION_RECORD_SCHEMA } from "./qualification-record.js";
21
+ export const RECORDS_PACKAGE_MAINTAINER = "Pau Aliagas <linuxnow@gmail.com>";
22
+ /** A word a Debian package name can hold: lowercase letters, digits, `+`, `-` and `.`, a letter or digit first. */
23
+ const DEB_WORD = /^[a-z0-9][a-z0-9+.-]*$/;
24
+ /** dpkg counts Installed-Size in kibibytes. */
25
+ const KIB = 2 ** 10;
26
+ const DEB_VERSION = /^[0-9][A-Za-z0-9.+~]*$/;
27
+ export function recordsPackageHelp() {
28
+ return [
29
+ "plugin-qualify-records-package: the Debian package of a device's qualification records, for the distributed layer",
30
+ "",
31
+ " plugin-qualify-records-package <qualification dir> --host-class <class> --set <set> --provenance <json> [options]",
32
+ "",
33
+ " --host-class <class> the machine class the records were measured on (rpi5): the directory they install in",
34
+ " --set <set> the plugin set they cover (zynthian-vangelis)",
35
+ " --provenance <json> a JSON file, or the JSON itself: the image, the package versions, the source revisions",
36
+ " --version <version> the package's version (default: today, 2026.10.29)",
37
+ " --out <dir> where the .deb is written (default: .)",
38
+ " --maintainer <who> the package's Maintainer field",
39
+ " --help this text",
40
+ "",
41
+ `the package installs ${DISTRIBUTED_DIR_SYSTEM}/<class>/<set>/ and depends on plugin-qualify (>= ${DISTRIBUTED_LAYER_SINCE})`,
42
+ "exit 0 the package was written, 2 it could not be",
43
+ "",
44
+ ].join("\n");
45
+ }
46
+ export function parseRecordsPackageArgs(argv) {
47
+ const values = new Map();
48
+ const positional = [];
49
+ const valued = new Set(["--host-class", "--set", "--provenance", "--version", "--out", "--maintainer"]);
50
+ for (let i = 0; i < argv.length; i++) {
51
+ const a = argv[i];
52
+ if (a === "--help")
53
+ return { ok: true, help: true };
54
+ if (valued.has(a)) {
55
+ const v = argv[++i];
56
+ if (v === undefined)
57
+ return { ok: false, error: `${a} needs a value` };
58
+ values.set(a, v);
59
+ }
60
+ else if (a.startsWith("--"))
61
+ return { ok: false, error: `unknown argument ${a}` };
62
+ else
63
+ positional.push(a);
64
+ }
65
+ if (positional.length !== 1)
66
+ return { ok: false, error: "name one qualification directory" };
67
+ for (const flag of ["--host-class", "--set", "--provenance"])
68
+ if (!values.has(flag))
69
+ return { ok: false, error: `${flag} is needed` };
70
+ const hostClass = values.get("--host-class");
71
+ const set = values.get("--set");
72
+ for (const [what, v] of [["--host-class", hostClass], ["--set", set]]) {
73
+ if (!DEB_WORD.test(v))
74
+ return { ok: false, error: `${what} ${v}: a package name holds lowercase letters, digits, + - and . only` };
75
+ }
76
+ const version = values.get("--version");
77
+ if (version !== undefined && !DEB_VERSION.test(version))
78
+ return { ok: false, error: `--version ${version} is not a Debian version` };
79
+ return {
80
+ ok: true,
81
+ args: {
82
+ stateDir: positional[0],
83
+ hostClass,
84
+ set,
85
+ provenance: values.get("--provenance"),
86
+ ...(version === undefined ? {} : { version }),
87
+ out: values.get("--out") ?? ".",
88
+ maintainer: values.get("--maintainer") ?? RECORDS_PACKAGE_MAINTAINER,
89
+ },
90
+ };
91
+ }
92
+ /** The package of a class and a set. */
93
+ export const recordsPackageName = (hostClass, set) => `omx-qualification-${hostClass}-${set}`;
94
+ /** Today as a package version, `2026.10.29`, and as a date, `2026-10-29`, in local time. */
95
+ export function today(now = new Date()) {
96
+ const p = (n) => String(n).padStart(2, "0");
97
+ const [y, m, d] = [String(now.getFullYear()), p(now.getMonth() + 1), p(now.getDate())];
98
+ return { version: `${y}.${m}.${d}`, date: `${y}-${m}-${d}` };
99
+ }
100
+ /** The files of a qualification directory that go in the package: each record with its probe output, and what stays out, by name and why. */
101
+ export function collectRecords(stateDir) {
102
+ const names = readdirSync(stateDir).sort();
103
+ const records = [];
104
+ const files = [];
105
+ const left = [];
106
+ for (const name of names) {
107
+ if (name.endsWith(".unmeasured.json")) {
108
+ left.push({ name, why: "unmeasured, never applies" });
109
+ continue;
110
+ }
111
+ if (!name.endsWith(".qualification.json"))
112
+ continue;
113
+ let record;
114
+ try {
115
+ record = JSON.parse(readFileSync(join(stateDir, name), "utf8"));
116
+ }
117
+ catch {
118
+ left.push({ name, why: "does not read" });
119
+ continue;
120
+ }
121
+ if (record.schema !== QUALIFICATION_RECORD_SCHEMA || typeof record.probe?.file !== "string" || record.probe.file.includes("/")) {
122
+ left.push({ name, why: `not a ${QUALIFICATION_RECORD_SCHEMA} naming its probe output` });
123
+ continue;
124
+ }
125
+ let probe;
126
+ try {
127
+ probe = readFileSync(join(stateDir, record.probe.file));
128
+ }
129
+ catch {
130
+ left.push({ name, why: `its probe output ${record.probe.file} is not there` });
131
+ continue;
132
+ }
133
+ if (sha256Hex(probe) !== record.probe.sha256) {
134
+ left.push({ name, why: `its probe output ${record.probe.file} is not the copy it names` });
135
+ continue;
136
+ }
137
+ records.push(name);
138
+ files.push(name, record.probe.file);
139
+ }
140
+ return { records, files: [...new Set(files)].sort(), left };
141
+ }
142
+ /** The provenance argument read: a file's JSON, or the JSON itself. */
143
+ function readProvenanceArg(arg) {
144
+ const text = arg.trimStart().startsWith("{") ? arg : readFileSync(arg, "utf8");
145
+ const v = JSON.parse(text);
146
+ if (typeof v !== "object" || v === null || Array.isArray(v))
147
+ throw new Error("--provenance is not a JSON object");
148
+ return v;
149
+ }
150
+ // --- the archive ------------------------------------------------------------------------------------------------------
151
+ /** One `ar` member header: name, time, owner, group, mode, size, as dpkg reads them. */
152
+ function arHeader(name, size, mtime) {
153
+ const field = (v, n) => v.padEnd(n, " ").slice(0, n);
154
+ return Buffer.from(`${field(name, 16)}${field(String(mtime), 12)}${field("0", 6)}${field("0", 6)}${field("100644", 8)}${field(String(size), 10)}\`\n`, "latin1");
155
+ }
156
+ function writeAr(path, members, mtime) {
157
+ const fd = openSync(path, "w", 0o644);
158
+ try {
159
+ writeSync(fd, Buffer.from("!<arch>\n"));
160
+ for (const m of members) {
161
+ writeSync(fd, arHeader(m.name, m.bytes.length, mtime));
162
+ writeSync(fd, m.bytes);
163
+ if (m.bytes.length % 2 === 1)
164
+ writeSync(fd, Buffer.from("\n"));
165
+ }
166
+ }
167
+ finally {
168
+ closeSync(fd);
169
+ }
170
+ }
171
+ /** A tree as a `tar.xz`, every entry `./`-rooted, owned by root, in name order, at `mtime`. */
172
+ function tarXz(tree, out, mtime) {
173
+ const r = spawnSync("tar", ["--create", "--xz", "--format=gnu", "--owner=0", "--group=0", "--numeric-owner", "--sort=name", `--mtime=@${String(mtime)}`, "--mode=u=rwX,go=rX", "-f", out, "-C", tree, "."], { encoding: "utf8", env: { ...process.env, XZ_OPT: "-6" } });
174
+ if (r.status !== 0)
175
+ throw new Error(`tar: ${r.error?.message ?? r.stderr.trim()}`);
176
+ }
177
+ /** The package of `args`, written into `args.out`. Throws, naming why, when it cannot be built. */
178
+ export function buildRecordsPackage(args, now = new Date()) {
179
+ const name = recordsPackageName(args.hostClass, args.set);
180
+ const day = today(now);
181
+ const version = args.version ?? day.version;
182
+ statSync(args.stateDir);
183
+ const { records, files, left } = collectRecords(args.stateDir);
184
+ if (records.length === 0)
185
+ throw new Error(`${args.stateDir} holds no record that can go in the package`);
186
+ const provenance = {
187
+ ...readProvenanceArg(args.provenance),
188
+ schema: DISTRIBUTED_PROVENANCE_SCHEMA,
189
+ set: args.set,
190
+ hostClass: args.hostClass,
191
+ package: { name, version },
192
+ date: day.date,
193
+ };
194
+ const work = mkdtempSync(join(tmpdir(), "records-package-"));
195
+ try {
196
+ const data = join(work, "data");
197
+ const rel = join("usr", "share", "omx", "qualification", args.hostClass, args.set);
198
+ const dir = join(data, rel);
199
+ mkdirSync(dir, { recursive: true });
200
+ for (const f of files)
201
+ copyFileSync(join(args.stateDir, f), join(dir, f));
202
+ writeFileSync(join(dir, DISTRIBUTED_PROVENANCE_FILE), `${JSON.stringify(provenance, null, 2)}\n`);
203
+ const installed = [...files, DISTRIBUTED_PROVENANCE_FILE].sort();
204
+ let kib = 0;
205
+ const sums = [];
206
+ for (const f of installed) {
207
+ const bytes = readFileSync(join(dir, f));
208
+ kib += Math.ceil(bytes.length / KIB);
209
+ sums.push(`${createHash("md5").update(bytes).digest("hex")} ${join(rel, f)}`);
210
+ }
211
+ const control = join(work, "control");
212
+ mkdirSync(control);
213
+ writeFileSync(join(control, "control"), [
214
+ `Package: ${name}`,
215
+ `Version: ${version}`,
216
+ "Architecture: all",
217
+ `Maintainer: ${args.maintainer}`,
218
+ `Installed-Size: ${String(kib)}`,
219
+ `Depends: plugin-qualify (>= ${DISTRIBUTED_LAYER_SINCE}~)`,
220
+ "Section: sound",
221
+ "Priority: optional",
222
+ "Multi-Arch: foreign",
223
+ `Description: qualification records of the ${args.set} plugin set for ${args.hostClass}`,
224
+ ` The plugin-qualify records of the ${args.set} plugin set, measured on a`,
225
+ ` ${args.hostClass} device. plugin-qualify-sync takes a plugin from them instead of`,
226
+ " measuring it, and only when every file of the installed plugin hashes as the",
227
+ " measured one did; any other plugin is measured on the device as before.",
228
+ "",
229
+ ].join("\n"));
230
+ writeFileSync(join(control, "md5sums"), `${sums.join("\n")}\n`);
231
+ const mtime = Math.floor(now.getTime() / 1000);
232
+ tarXz(control, join(work, "control.tar.xz"), mtime);
233
+ tarXz(data, join(work, "data.tar.xz"), mtime);
234
+ mkdirSync(args.out, { recursive: true });
235
+ const path = join(args.out, `${name}_${version}_all.deb`);
236
+ writeAr(path, [
237
+ { name: "debian-binary", bytes: Buffer.from("2.0\n") },
238
+ { name: "control.tar.xz", bytes: readFileSync(join(work, "control.tar.xz")) },
239
+ { name: "data.tar.xz", bytes: readFileSync(join(work, "data.tar.xz")) },
240
+ ], mtime);
241
+ return { path, name, version, records: records.length, files: installed.length, left };
242
+ }
243
+ finally {
244
+ rmSync(work, { recursive: true, force: true });
245
+ }
246
+ }
247
+ /** A `.deb` read with nothing but `ar`'s layout and tar: its members, its control fields, its files; unpacked into `into` when given. */
248
+ export function readDeb(path, into) {
249
+ const bytes = readFileSync(path);
250
+ if (bytes.subarray(0, 8).toString("latin1") !== "!<arch>\n")
251
+ throw new Error(`${path}: not an ar archive`);
252
+ const members = new Map();
253
+ for (let at = 8; at < bytes.length;) {
254
+ const head = bytes.subarray(at, at + 60).toString("latin1");
255
+ const name = head.slice(0, 16).trim().replace(/\/$/, "");
256
+ const size = Number(head.slice(48, 58).trim());
257
+ members.set(name, bytes.subarray(at + 60, at + 60 + size));
258
+ at += 60 + size + (size % 2);
259
+ }
260
+ const work = mkdtempSync(join(tmpdir(), "read-deb-"));
261
+ try {
262
+ const list = (member, extract) => {
263
+ const file = join(work, member);
264
+ writeFileSync(file, members.get(member) ?? Buffer.alloc(0));
265
+ const r = spawnSync("tar", ["-tvJf", file], { encoding: "utf8", maxBuffer: 256 << 20 });
266
+ if (r.status !== 0)
267
+ throw new Error(`${member}: ${r.stderr.trim()}`);
268
+ if (extract !== undefined) {
269
+ mkdirSync(extract, { recursive: true });
270
+ const x = spawnSync("tar", ["-xJf", file, "-C", extract], { encoding: "utf8" });
271
+ if (x.status !== 0)
272
+ throw new Error(`${member}: ${x.stderr.trim()}`);
273
+ }
274
+ return r.stdout
275
+ .split("\n")
276
+ .filter((l) => l.startsWith("-"))
277
+ .map((l) => l.slice(l.indexOf("./")))
278
+ .sort();
279
+ };
280
+ const controlDir = join(work, "control");
281
+ const controlFiles = list("control.tar.xz", controlDir);
282
+ const control = {};
283
+ for (const line of readFileSync(join(controlDir, "control"), "utf8").split("\n")) {
284
+ const m = /^([A-Za-z-]+): (.*)$/.exec(line);
285
+ if (m !== null)
286
+ control[m[1]] = m[2];
287
+ }
288
+ return {
289
+ members: [...members.keys()],
290
+ debianBinary: (members.get("debian-binary") ?? Buffer.alloc(0)).toString("latin1"),
291
+ control,
292
+ controlFiles,
293
+ files: list("data.tar.xz", into),
294
+ };
295
+ }
296
+ finally {
297
+ rmSync(work, { recursive: true, force: true });
298
+ }
299
+ }
300
+ //# sourceMappingURL=records-package.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"records-package.js","sourceRoot":"","sources":["../src/records-package.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmixer/plugin-qualify",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
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",
@@ -18,7 +18,8 @@
18
18
  },
19
19
  "bin": {
20
20
  "plugin-qualify": "./bin/plugin-qualify.mjs",
21
- "plugin-qualify-sync": "./bin/plugin-qualify-sync.mjs"
21
+ "plugin-qualify-sync": "./bin/plugin-qualify-sync.mjs",
22
+ "plugin-qualify-records-package": "./bin/plugin-qualify-records-package.mjs"
22
23
  },
23
24
  "main": "./dist/index.js",
24
25
  "types": "./dist/index.d.ts",
@@ -0,0 +1,251 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * The distributed layer: qualification records another device of the same machine class wrote for a plugin set,
5
+ * installed read-only under `<dir>/<host class>/<set>/` beside the set's `provenance.json`. plugin-qualify-sync reads
6
+ * it before measuring a plugin, and takes a plugin from it instead of measuring it only when every file the record
7
+ * names hashes, here, as it did where it was measured. The name of a plugin finds its record and never decides
8
+ * anything: a record is never trusted by name alone.
9
+ *
10
+ * What applies is copied into the device's own `qualification_dir`, the record with the set's provenance added and
11
+ * its probe output byte for byte, and the plugin's pin is the one that probe output holds. From then on the sync
12
+ * treats the plugin as one it measured.
13
+ */
14
+ import { readFileSync, readdirSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+
17
+ import { clapPinUri, pinVerdictOf, type PinEntry } from "./pins-file.js";
18
+ import { integrityOfProbeOutput } from "./probe-judge.js";
19
+ import { readProbeOutput, sha256Hex } from "./probe-output.js";
20
+ import { QUALIFICATION_RECORD_SCHEMA, type QualificationRecord, type RecordVerdict } from "./qualification-record.js";
21
+ import type { FileDigest, SyncItem, SyncOutcome, SyncPlugin } from "./qualify-sync.js";
22
+ import type { SuitabilityRating } from "./rating.js";
23
+ import { writeFileWhole } from "./whole-file.js";
24
+
25
+ /** Where a system's packages install the distributed records. */
26
+ export const DISTRIBUTED_DIR_SYSTEM = "/usr/share/omx/qualification";
27
+
28
+ /** The file of a set that says where its records come from. */
29
+ export const DISTRIBUTED_PROVENANCE_FILE = "provenance.json";
30
+ export const DISTRIBUTED_PROVENANCE_SCHEMA = "plugin-qualify.distributed-provenance/1";
31
+
32
+ /** The first plugin-qualify that reads the layer: what a package of records depends on. */
33
+ export const DISTRIBUTED_LAYER_SINCE = "0.17.0";
34
+
35
+ /** Where the records of a set come from. The keys below are always there; the rest is what the builder was given. */
36
+ export interface DistributedProvenance {
37
+ readonly schema: typeof DISTRIBUTED_PROVENANCE_SCHEMA;
38
+ readonly set: string;
39
+ readonly hostClass: string;
40
+ /** The package that installed the set. */
41
+ readonly package: { readonly name: string; readonly version: string };
42
+ /** The day the package was built (YYYY-MM-DD). */
43
+ readonly date: string;
44
+ readonly [key: string]: unknown;
45
+ }
46
+
47
+ /** The device tree's model file, where a board names itself. */
48
+ export const BOARD_MODEL_FILE = "/proc/device-tree/model";
49
+
50
+ /** The board's own model, or `undefined` on a machine without a device tree. */
51
+ export function readBoardModel(): string | undefined {
52
+ try {
53
+ return readFileSync(BOARD_MODEL_FILE, "utf8");
54
+ } catch {
55
+ return undefined;
56
+ }
57
+ }
58
+
59
+ /** The host class of a board the layer knows by its device-tree model: `rpi5` for a Raspberry Pi 5 or its compute module. */
60
+ export function hostClassOfBoard(model: string | undefined): string | undefined {
61
+ if (model === undefined) return undefined;
62
+ const m = /^Raspberry Pi (?:Compute Module )?(\d)(?:\s|$)/.exec(model.replace(/\0/g, "").trim());
63
+ return m === null ? undefined : `rpi${m[1]!}`;
64
+ }
65
+
66
+ /** One distributed record, read: where it is, what it says, and the set it came with. */
67
+ export interface DistributedRecord {
68
+ readonly path: string;
69
+ readonly record: QualificationRecord;
70
+ readonly set: string;
71
+ readonly provenance: DistributedProvenance;
72
+ }
73
+
74
+ /** The layer as one run reads it: the records of the host class, by what finds them. */
75
+ export interface DistributedLayer {
76
+ /** By the `.clap` path the record measured, made absolute. */
77
+ readonly clap: ReadonlyMap<string, readonly DistributedRecord[]>;
78
+ /** By the LV2 plugin's uri. */
79
+ readonly lv2: ReadonlyMap<string, readonly DistributedRecord[]>;
80
+ readonly sets: readonly string[];
81
+ readonly records: number;
82
+ }
83
+
84
+ export const EMPTY_LAYER: DistributedLayer = { clap: new Map(), lv2: new Map(), sets: [], records: 0 };
85
+
86
+ const RECORD_SUFFIX = ".qualification.json";
87
+
88
+ const names = (dir: string): string[] => {
89
+ try {
90
+ return readdirSync(dir).sort();
91
+ } catch {
92
+ return [];
93
+ }
94
+ };
95
+
96
+ /**
97
+ * The records of one host class under each directory, in the order given. A set without a provenance that reads, or
98
+ * whose provenance names another set or class, is not read; a record that does not read as a record of this schema is
99
+ * named and left out.
100
+ */
101
+ export function readDistributedLayer(dirs: readonly string[], hostClass: string, log: (line: string) => void = () => {}): DistributedLayer {
102
+ const clap = new Map<string, DistributedRecord[]>();
103
+ const lv2 = new Map<string, DistributedRecord[]>();
104
+ const sets: string[] = [];
105
+ let records = 0;
106
+ const add = (map: Map<string, DistributedRecord[]>, key: string, r: DistributedRecord): void => {
107
+ map.set(key, [...(map.get(key) ?? []), r]);
108
+ };
109
+ for (const base of dirs) {
110
+ const classDir = join(base, hostClass);
111
+ for (const set of names(classDir)) {
112
+ const dir = join(classDir, set);
113
+ let provenance: DistributedProvenance;
114
+ try {
115
+ provenance = JSON.parse(readFileSync(join(dir, DISTRIBUTED_PROVENANCE_FILE), "utf8")) as DistributedProvenance;
116
+ } catch (e) {
117
+ if ((e as NodeJS.ErrnoException).code === "ENOTDIR") continue;
118
+ log(`distributed: ${dir}: ${(e as NodeJS.ErrnoException).code === "ENOENT" ? `no ${DISTRIBUTED_PROVENANCE_FILE}` : `${DISTRIBUTED_PROVENANCE_FILE} does not read`}, not read`);
119
+ continue;
120
+ }
121
+ if (provenance.schema !== DISTRIBUTED_PROVENANCE_SCHEMA || provenance.set !== set || provenance.hostClass !== hostClass) {
122
+ log(`distributed: ${dir}: its ${DISTRIBUTED_PROVENANCE_FILE} is not of set ${set} and host class ${hostClass} (${DISTRIBUTED_PROVENANCE_SCHEMA}), not read`);
123
+ continue;
124
+ }
125
+ sets.push(set);
126
+ for (const name of names(dir)) {
127
+ if (!name.endsWith(RECORD_SUFFIX)) continue;
128
+ const path = join(dir, name);
129
+ let record: QualificationRecord;
130
+ try {
131
+ record = JSON.parse(readFileSync(path, "utf8")) as QualificationRecord;
132
+ } catch {
133
+ log(`distributed: ${path}: does not read, left out`);
134
+ continue;
135
+ }
136
+ if (record.schema !== QUALIFICATION_RECORD_SCHEMA || typeof record.binary?.path !== "string" || !Array.isArray(record.binary.files) || record.binary.files.length === 0) {
137
+ log(`distributed: ${path}: not a ${QUALIFICATION_RECORD_SCHEMA} with its binary's files, left out`);
138
+ continue;
139
+ }
140
+ const r: DistributedRecord = { path, record, set, provenance };
141
+ records++;
142
+ if (record.plugin.format === "clap") add(clap, resolve(record.binary.path), r);
143
+ else if (record.plugin.format === "lv2") add(lv2, record.plugin.id, r);
144
+ }
145
+ }
146
+ }
147
+ return { clap, lv2, sets, records };
148
+ }
149
+
150
+ /** The uri a record's plugin has on this machine: the installed `.clap`'s path and the id, or the LV2 uri. */
151
+ export function distributedUri(item: SyncItem, r: DistributedRecord): string {
152
+ return item.format === "clap" ? clapPinUri(join(item.root, item.files[0]!), `urn:clap:${r.record.plugin.id}`) : r.record.plugin.id;
153
+ }
154
+
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 {
157
+ if (!v.measured) return v.deciding === "probe.crashed" ? "crashed" : v.deciding === "probe.timeout" ? "hung" : "unmeasured";
158
+ if (v.rating === "suitable" || v.rating === "conditional") return "qualified";
159
+ return v.rating === "unsuitable" ? "refused" : "unmeasured";
160
+ }
161
+
162
+ /** What the layer makes of one item. */
163
+ export type DistributedMatch =
164
+ /** No distributed record names the plugin: it is measured, and nothing is said. */
165
+ | { readonly status: "none" }
166
+ /** Every reason a record found was not taken, one line each; the plugin is measured. */
167
+ | { readonly status: "ignored"; readonly lines: readonly string[] }
168
+ | { readonly status: "applies"; readonly set: string; readonly records: readonly DistributedRecord[]; readonly files: readonly string[] };
169
+
170
+ /**
171
+ * Whether the item is taken from the layer. The records that find it are tried one set at a time, in the order the
172
+ * layer read them. Of a set, only the records whose every file is installed with the SHA-256 it recorded (computed
173
+ * here by `digest`) count: a record of other bytes is another build of the plugin and is passed over. The set applies
174
+ * when at least one record of these bytes holds a verdict for `host`, and none of those verdicts is one the source
175
+ * could not measure; a set with no record of these bytes is a binary that differs.
176
+ */
177
+ export function matchDistributed(
178
+ layer: DistributedLayer,
179
+ item: SyncItem,
180
+ host: string,
181
+ digest: (files: readonly string[]) => readonly FileDigest[] | undefined,
182
+ ): DistributedMatch {
183
+ const found = item.format === "clap" ? layer.clap.get(resolve(join(item.root, item.files[0]!))) : layer.lv2.get(item.uri ?? "");
184
+ if (found === undefined || found.length === 0) return { status: "none" };
185
+ const lines: string[] = [];
186
+ const say = (line: string): void => {
187
+ if (!lines.includes(line)) lines.push(line);
188
+ };
189
+ for (const set of [...new Set(found.map((r) => r.set))]) {
190
+ // a set may hold records of earlier bytes of the plugin beside the ones of these: only the ones of these bytes count
191
+ const records = found.filter((r) => {
192
+ if (r.set !== set || r.record.plugin.format !== item.format) return false;
193
+ const wanted = r.record.binary.files;
194
+ const now = digest(wanted.map((f) => f.path));
195
+ return now !== undefined && wanted.every((f) => now.find((d) => d.path === f.path)?.sha256 === f.sha256);
196
+ });
197
+ if (records.length === 0) {
198
+ say(`distributed: ${distributedUri(item, found.find((r) => r.set === set)!)}: binary differs`);
199
+ continue;
200
+ }
201
+ const judged = records.filter((r) => r.record.verdicts.some((v) => v.host === host));
202
+ if (judged.length === 0) {
203
+ say(`distributed: ${distributedUri(item, records[0]!)}: no verdict for ${host} in ${set}`);
204
+ continue;
205
+ }
206
+ const unmeasured = judged.find((r) => outcomeOfVerdict(r.record.verdicts.find((v) => v.host === host)!) === "unmeasured");
207
+ if (unmeasured !== undefined) {
208
+ say(`distributed: ${distributedUri(item, unmeasured)}: unmeasured in ${set}, measured here`);
209
+ continue;
210
+ }
211
+ return { status: "applies", set, records, files: [...new Set(records.flatMap((r) => r.record.binary.files.map((f) => f.path)))] };
212
+ }
213
+ return { status: "ignored", lines };
214
+ }
215
+
216
+ /**
217
+ * The records of a match copied into `qualificationDir` and the plugins they make: the probe output first, byte for
218
+ * byte and read back through this version's reader, its SHA-256 the one the record names; then the record with the
219
+ * set's provenance added. A record whose probe output is missing, altered or unreadable throws, naming it, and the
220
+ * caller measures the plugin instead. Records of the other tier (no verdict for `host`) are copied and make no plugin,
221
+ * as the sweep writes them.
222
+ */
223
+ export function applyDistributed(qualificationDir: string, item: SyncItem, match: Extract<DistributedMatch, { status: "applies" }>, host: string): SyncPlugin[] {
224
+ const copies = match.records.map((r) => {
225
+ const probeName = r.record.probe.file;
226
+ const probePath = join(r.path, "..", probeName);
227
+ let bytes: Buffer;
228
+ try {
229
+ bytes = readFileSync(probePath);
230
+ } catch (e) {
231
+ throw new Error(`${probeName}: ${(e as NodeJS.ErrnoException).code ?? String(e)}`);
232
+ }
233
+ if (sha256Hex(bytes) !== r.record.probe.sha256) throw new Error(`${probeName}: not the probe output the record names (its SHA-256 differs)`);
234
+ const read = readProbeOutput(bytes.toString("utf8"));
235
+ if (!read.ok) throw new Error(`${probeName}: ${read.code}: ${read.problem}`);
236
+ return { r, bytes, output: read.output };
237
+ });
238
+ const plugins: SyncPlugin[] = [];
239
+ for (const { r, bytes, output } of copies) {
240
+ writeFileWhole(join(qualificationDir, r.record.probe.file), bytes);
241
+ const recordPath = join(qualificationDir, r.path.slice(r.path.lastIndexOf("/") + 1));
242
+ writeFileWhole(recordPath, `${JSON.stringify({ ...r.record, provenance: r.provenance }, null, 2)}\n`);
243
+ const v = r.record.verdicts.find((x) => x.host === host);
244
+ if (v === undefined) continue;
245
+ const word = v.measured ? pinVerdictOf(v.rating as SuitabilityRating) : undefined;
246
+ const pin: PinEntry = { uri: distributedUri(item, r), integrity: integrityOfProbeOutput(output), ...(word === undefined ? {} : { verdict: word }) };
247
+ const outcome = outcomeOfVerdict(v);
248
+ plugins.push({ uri: pin.uri, outcome, ...(v.deciding === undefined ? {} : { code: v.deciding }), pin, record: recordPath });
249
+ }
250
+ return plugins;
251
+ }
package/src/index.ts CHANGED
@@ -24,9 +24,14 @@ export * from "./probe-output.js";
24
24
  export * from "./probe-judge.js";
25
25
  export * from "./qualification-record.js";
26
26
  export * from "./probe-runner.js";
27
+ export * from "./measurement-grid.js";
28
+ export * from "./memory-bound.js";
29
+ export * from "./probe-merge.js";
27
30
  export * from "./judge-cli.js";
28
31
  export * from "./whole-file.js";
29
32
  export * from "./qualify-sync.js";
33
+ export * from "./distributed-records.js";
34
+ export * from "./records-package.js";
30
35
  export * from "./host-admission.js";
31
36
  export * from "./history-file.js";
32
37
  export * from "./history-adjust.js";