@openmixer/plugin-qualify 0.18.5 → 0.19.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 (88) hide show
  1. package/README.md +19 -730
  2. package/bin/plugin-qualify-sync.mjs +1 -1
  3. package/bin/plugin-qualify.mjs +7 -0
  4. package/dist/clap-qualify.d.ts +1 -1
  5. package/dist/compare.d.ts +311 -0
  6. package/dist/compare.js +502 -0
  7. package/dist/compare.js.map +1 -0
  8. package/dist/distributed-records.d.ts +0 -2
  9. package/dist/distributed-records.js +0 -2
  10. package/dist/distributed-records.js.map +1 -1
  11. package/dist/host-admission.d.ts +2 -2
  12. package/dist/host-admission.js +7 -0
  13. package/dist/host-admission.js.map +1 -1
  14. package/dist/hosting-codes.generated.d.ts +1 -0
  15. package/dist/hosting-codes.generated.js +1 -0
  16. package/dist/hosting-codes.generated.js.map +1 -1
  17. package/dist/hosting-suitability.d.ts +3 -1
  18. package/dist/hosting-suitability.js +9 -0
  19. package/dist/hosting-suitability.js.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +1 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/lv2-loader.js +6 -5
  24. package/dist/lv2-loader.js.map +1 -1
  25. package/dist/measurement/format.d.ts +15 -0
  26. package/dist/measurement/format.js +19 -0
  27. package/dist/measurement/format.js.map +1 -1
  28. package/dist/measurement/sweep-jsonl.js +5 -1
  29. package/dist/measurement/sweep-jsonl.js.map +1 -1
  30. package/dist/measurement-grid.js +1 -1
  31. package/dist/pins-file.d.ts +22 -2
  32. package/dist/pins-file.js +118 -17
  33. package/dist/pins-file.js.map +1 -1
  34. package/dist/qualification-record.d.ts +26 -0
  35. package/dist/qualification-record.js +21 -0
  36. package/dist/qualification-record.js.map +1 -1
  37. package/dist/qualify-declarations.d.ts +6 -0
  38. package/dist/qualify-declarations.js +8 -0
  39. package/dist/qualify-declarations.js.map +1 -1
  40. package/dist/qualify-isolated.d.ts +16 -3
  41. package/dist/qualify-isolated.js +31 -3
  42. package/dist/qualify-isolated.js.map +1 -1
  43. package/dist/qualify-pool.d.ts +129 -0
  44. package/dist/qualify-pool.js +290 -0
  45. package/dist/qualify-pool.js.map +1 -0
  46. package/dist/qualify-rejudge.js +7 -0
  47. package/dist/qualify-rejudge.js.map +1 -1
  48. package/dist/qualify-sync.d.ts +36 -1
  49. package/dist/qualify-sync.js +144 -20
  50. package/dist/qualify-sync.js.map +1 -1
  51. package/dist/records-package.d.ts +5 -0
  52. package/dist/records-package.js +25 -8
  53. package/dist/records-package.js.map +1 -1
  54. package/fixtures/clap-scan/fake.json +8 -2
  55. package/fixtures/probe-output/success-msi.json +2 -2
  56. package/package.json +3 -3
  57. package/src/clap-qualify.ts +1 -1
  58. package/src/compare.ts +699 -0
  59. package/src/distributed-records.ts +0 -3
  60. package/src/host-admission.ts +9 -2
  61. package/src/hosting-codes.generated.ts +1 -0
  62. package/src/hosting-suitability.ts +14 -0
  63. package/src/index.ts +1 -0
  64. package/src/lv2-loader.ts +5 -4
  65. package/src/measurement/format.ts +32 -0
  66. package/src/measurement/sweep-jsonl.ts +5 -1
  67. package/src/measurement-grid.ts +1 -1
  68. package/src/pins-file.ts +109 -16
  69. package/src/qualification-record.ts +38 -0
  70. package/src/qualify-declarations.ts +8 -0
  71. package/src/qualify-isolated.ts +31 -5
  72. package/src/qualify-pool.ts +318 -0
  73. package/src/qualify-rejudge.ts +8 -0
  74. package/src/qualify-sync.ts +140 -19
  75. package/src/records-package.ts +25 -8
  76. package/tools/benchmark.py +9 -3
  77. package/tools/compare-host.mjs +432 -0
  78. package/tools/compare-run.mjs +355 -0
  79. package/tools/descriptors.py +27 -0
  80. package/tools/isolated-step.mjs +3 -1
  81. package/tools/lilv_load.py +25 -0
  82. package/tools/lv2_resolve.py +3 -1
  83. package/tools/pool-step.mjs +186 -0
  84. package/tools/scan.py +16 -20
  85. package/tools/audit-overlap.sh +0 -250
  86. package/tools/commit-message-check.sh +0 -128
  87. package/tools/commit-message-patterns.json +0 -41
  88. package/tools/private-refs-check.sh +0 -90
@@ -24,21 +24,22 @@ import { homedir, tmpdir } from "node:os";
24
24
  import { basename, dirname, join, resolve, sep } from "node:path";
25
25
 
26
26
  import { clapQualifyTools } from "./clap-qualify.js";
27
- import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, matchDistributed, readBoardModel, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
27
+ import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, outcomeOfVerdict, matchDistributed, readBoardModel, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
28
28
  import { clapSearchPath, parseClapScan } from "./clap-loader.js";
29
29
  import { HOSTING_CODE } from "./hosting-codes.js";
30
30
  import { shippedHostProfile, type HostProfile } from "./host-profiles.js";
31
31
  import { lv2BundlesOnPath, manifestPlugins } from "./lv2-loader.js";
32
- import { judgmentPinEntries, pinsFile, type PinEntry } from "./pins-file.js";
32
+ import { judgmentPinEntries, pinClearLine, pinLineUri, pinsFile, type PinEntry } from "./pins-file.js";
33
+ import { POOL_RESULT, completePool, judgePoolRun, pinAfterPool, pinFromRecordWithPool, poolEnv, spawnedPoolRunner, type PoolRunner } from "./qualify-pool.js";
33
34
  import { hostdPinFiles, type PinFilesRun } from "./hostd-pin-files.js";
34
35
  import type { Judgment } from "./probe-judge.js";
35
36
  import { qualifyTargets, type ProbeRun } from "./probe-runner.js";
36
37
  import { GRID_FULL, parseGridSpec, type GridSpec } from "./measurement-grid.js";
37
38
  import { adapterPass, type AdapterPassOptions, type AdapterPassResult } from "./qualify-adapter-pass.js";
38
39
  import { ISOLATED_RESULT, baseSteps, completeIsolated, isolatedCell, missingSteps, judgeIsolatedRun, owesIsolated, pinAfterStep, spawnedIsolatedRunner, type IsolatedRunner } from "./qualify-isolated.js";
39
- import { lv2World, rejudgeState, memoryPass, type MemoryPassResult, type RejudgeResult } from "./qualify-rejudge.js";
40
+ import { lv2World, rejudgeRecord, rejudgeState, rejudgeToolName, memoryPass, type MemoryPassResult, type RejudgeResult } from "./qualify-rejudge.js";
40
41
  import { memoryBoundOf, parseMemoryLimit, type MemoryBound } from "./memory-bound.js";
41
- import { rewriteRecord, writeQualification, type NotMeasured, type QualificationRecord } from "./qualification-record.js";
42
+ import { QUALIFICATION_RECORD_SCHEMA, latencyNeverTaken, rewriteRecord, writeQualification, type NotMeasured, type QualificationRecord } from "./qualification-record.js";
42
43
  import { ADMIT_STEP_MS_ENV, DESCRIPTOR_DIR_STATE_SUBDIR, DESCRIPTOR_DIR_SYSTEM, MEMORY_PASS_PROBE_NOTES, QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
43
44
  import { toolsDir } from "./tool-paths.js";
44
45
  import { writeFileWhole } from "./whole-file.js";
@@ -57,7 +58,7 @@ export const ZYNTHIAN_LV2_DIRS = ["/zynthian/zynthian-plugins/lv2", "/zynthian/z
57
58
  export interface SyncSetting {
58
59
  readonly key: string;
59
60
  readonly meaning: string;
60
- /** The default as `--help` and the README print it. */
61
+ /** The default as `--help` and docs/overview.md print it. */
61
62
  readonly default: string;
62
63
  }
63
64
 
@@ -142,6 +143,8 @@ export interface SyncArgs {
142
143
  readonly adapterOnly: boolean;
143
144
  /** `--isolated-only`: the missing isolated step of each record already judged, extending its run. */
144
145
  readonly isolatedOnly: boolean;
146
+ /** `--pool-only`: the pool step of each CLAP record already judged that owes it, extending its run and giving its pin the word. */
147
+ readonly poolOnly: boolean;
145
148
  readonly help: boolean;
146
149
  /** The settings given as flags, by key. */
147
150
  readonly values: ReadonlyMap<string, string>;
@@ -162,6 +165,7 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
162
165
  let memoryOnly = false;
163
166
  let adapterOnly = false;
164
167
  let isolatedOnly = false;
168
+ let poolOnly = false;
165
169
  let help = false;
166
170
  for (let i = 0; i < argv.length; i++) {
167
171
  const a = argv[i]!;
@@ -174,6 +178,7 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
174
178
  else if (a === "--memory-only") memoryOnly = true;
175
179
  else if (a === "--adapter-only") adapterOnly = true;
176
180
  else if (a === "--isolated-only") isolatedOnly = true;
181
+ else if (a === "--pool-only") poolOnly = true;
177
182
  else if (a === "--help") help = true;
178
183
  else if (a === "--grid-hosts") values.set("grid", "hosts");
179
184
  else if (a === "--config" || byFlag.has(a)) {
@@ -183,9 +188,9 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
183
188
  else values.set(byFlag.get(a)!, v);
184
189
  } else return { ok: false, error: `unknown argument ${a}` };
185
190
  }
186
- if (rejudge && (memoryOnly || adapterOnly || isolatedOnly || force || refreshPinsOnly)) return { ok: false, error: "--rejudge measures nothing: it goes with none of --memory-only, --adapter-only, --force and --refresh-pins" };
187
- if ([memoryOnly, adapterOnly, isolatedOnly, refreshPinsOnly].filter(Boolean).length > 1) return { ok: false, error: "--memory-only, --adapter-only and --refresh-pins are different runs: one at a time" };
188
- return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, noDistributed, noWait, refreshPins: refreshPinsOnly, rejudge, memoryOnly, adapterOnly, isolatedOnly, help, values } };
191
+ if (rejudge && (memoryOnly || adapterOnly || isolatedOnly || poolOnly || force || refreshPinsOnly)) return { ok: false, error: "--rejudge measures nothing: it goes with none of --memory-only, --adapter-only, --isolated-only, --pool-only, --force and --refresh-pins" };
192
+ if ([memoryOnly, adapterOnly, isolatedOnly, poolOnly, refreshPinsOnly].filter(Boolean).length > 1) return { ok: false, error: "--memory-only, --adapter-only, --isolated-only, --pool-only and --refresh-pins are different runs: one at a time" };
193
+ return { ok: true, args: { ...(config === undefined ? {} : { config }), system, force, noDistributed, noWait, refreshPins: refreshPinsOnly, rejudge, memoryOnly, adapterOnly, isolatedOnly, poolOnly, help, values } };
189
194
  }
190
195
 
191
196
  export function syncHelpText(): string {
@@ -202,6 +207,7 @@ export function syncHelpText(): string {
202
207
  " --rejudge derive every record's verdicts, tiers and pin again from the figures it holds; measures nothing",
203
208
  " --memory-only measure only the footprint of each recorded plugin at the Pi's cells and merge it; with --force, those already merged too",
204
209
  " --isolated-only run only the isolated step each record still owes, adding it to the record's run",
210
+ " --pool-only run only the pool step each CLAP record still owes: a plugin that asks for the thread pool is run with it off and on, and its pin gets pool=cores or pool=off",
205
211
  " --adapter-only run each LV2 plugin only under omx-clap-host's LV2 adapter at the Pi's cells, on private JACK servers, and merge it; with --force, those already done too",
206
212
  " --grid-hosts the fast grid, the same as --grid hosts (7 cells with the shipped hosts)",
207
213
  " --help this text",
@@ -522,6 +528,25 @@ export function machineReason(p: Pick<SyncPlugin, "outcome" | "code" | "detail">
522
528
  return /wrote no document|cannot run the probe runner/.test(p.detail ?? "") ? "the measurement wrote no document" : undefined;
523
529
  }
524
530
 
531
+ /**
532
+ * The figure the records of a recorded entry never took, or `undefined`: a record whose probe ran before that figure's
533
+ * pass ({@link latencyNeverTaken}) owes a measurement, and the sync measures the entry again although its bytes are the
534
+ * ones it recorded. A record that cannot be read owes nothing here: the sync's other checks deal with it.
535
+ */
536
+ export function figureOwed(entry: Pick<SyncEntry, "plugins">): "latency" | undefined {
537
+ for (const p of entry.plugins) {
538
+ if (p.record === undefined) continue;
539
+ let r: QualificationRecord;
540
+ try {
541
+ r = JSON.parse(readFileSync(p.record, "utf8")) as QualificationRecord;
542
+ } catch {
543
+ continue;
544
+ }
545
+ if (r.schema === QUALIFICATION_RECORD_SCHEMA && latencyNeverTaken(r)) return "latency";
546
+ }
547
+ return undefined;
548
+ }
549
+
525
550
  /** What the quick qualification never measures, stated in every record it writes. */
526
551
  export const QUICK_NOT_MEASURED: readonly NotMeasured[] = [{ dimension: "stability", code: HOSTING_CODE.stabilityUnsoaked }];
527
552
 
@@ -621,6 +646,8 @@ export interface SyncContext {
621
646
  readonly pinFilesRun?: PinFilesRun;
622
647
  /** The isolated step, a stand-in in tests. */
623
648
  readonly isolatedRunner?: IsolatedRunner;
649
+ /** The pool step, a stand-in in tests. */
650
+ readonly poolRunner?: PoolRunner;
624
651
  }
625
652
 
626
653
  /**
@@ -738,6 +765,13 @@ function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgmen
738
765
  code = undefined;
739
766
  }
740
767
  }
768
+ // the pool is for a CLAP plugin that runs: the thread pool is granted by measuring it, here and nowhere else
769
+ if (judgment.output.plugin.format === "clap" && pin !== undefined && pin.verdict !== "refused" && outcome === "qualified") {
770
+ const runner = ctx.poolRunner ?? spawnedPoolRunner();
771
+ const step = judgePoolRun(runner(pin.uri, isolatedCell(record, host), poolEnv(ctx.env, ctx.settings.budgetS)));
772
+ steps.push(step);
773
+ pin = pinAfterPool(pin, step);
774
+ }
741
775
  rewriteRecord(recordPath, { ...record, run: { id: judgment.output.run.id, steps } });
742
776
  const plugin: SyncPlugin = { uri: pin?.uri ?? judgment.output.plugin.id, outcome, ...(code === undefined ? {} : { code }), ...(pin === undefined ? {} : { pin }), record: recordPath };
743
777
  return { plugin, judgedHere: judgment.hosts.some((h) => h.verdict.host === host) };
@@ -866,8 +900,8 @@ export function qualifyItem(ctx: SyncContext, item: SyncItem, files: readonly Fi
866
900
  * The isolated steps the records of a state still owe, each extending its record's run. A plugin's pin follows its step, and
867
901
  * a step that could not run stays owed: the next pass tries it again.
868
902
  */
869
- export function isolatedPass(ctx: SyncContext, state: SyncState): { readonly state: SyncState; readonly isolated: number; readonly refused: number; readonly notRun: number } {
870
- const count = { isolated: 0, refused: 0, notRun: 0 };
903
+ export function isolatedPass(ctx: SyncContext, state: SyncState): { readonly state: SyncState; readonly isolated: number; readonly refused: number; readonly notRun: number; readonly repinned: number } {
904
+ const count = { isolated: 0, refused: 0, notRun: 0, repinned: 0 };
871
905
  const runner = ctx.isolatedRunner ?? spawnedIsolatedRunner();
872
906
  const host = ctx.settings.hostProfile.id;
873
907
  const entries: Record<string, SyncEntry> = {};
@@ -885,7 +919,17 @@ export function isolatedPass(ctx: SyncContext, state: SyncState): { readonly sta
885
919
  return p;
886
920
  }
887
921
  const done = completeIsolated(path, record, p.pin?.uri ?? record.plugin.id, isolatedCell(record, host), runner, isolatedEnv(ctx));
888
- if (done === undefined) return p;
922
+ if (done === undefined) {
923
+ // a plugin the isolated step already decided but the state holds no pin for: the record keeps the bytes and the
924
+ // step, and the pin follows them (hostd names the files, as at every other pin)
925
+ if (p.pin !== undefined) return p;
926
+ const rebuilt = withHostdFiles(ctx, pinFromRecordWithPool(record));
927
+ if (rebuilt === undefined) return p;
928
+ count.repinned++;
929
+ ctx.log(`${p.uri}: pinned again from its record (isolated step ${rebuilt.verdict ?? "undecided"})`);
930
+ const { code: _c, ...rest } = p;
931
+ return rest.outcome === "qualified" && rebuilt.verdict === "refused" ? { ...rest, outcome: "refused" as const, pin: rebuilt } : { ...rest, pin: rebuilt };
932
+ }
889
933
  ctx.log(`${p.uri}: isolated step ${done.step.result}${done.step.code === undefined ? "" : ` (${done.step.code})`}`);
890
934
  const pin = pinAfterStep(p.pin, done.step);
891
935
  const { pin: _pin, code: _code, ...rest } = p;
@@ -899,7 +943,44 @@ export function isolatedPass(ctx: SyncContext, state: SyncState): { readonly sta
899
943
  }),
900
944
  };
901
945
  }
902
- return { ...count, state: count.isolated + count.refused + count.notRun === 0 ? state : { ...state, entries } };
946
+ return { ...count, state: count.isolated + count.refused + count.notRun + count.repinned === 0 ? state : { ...state, entries } };
947
+ }
948
+
949
+ /**
950
+ * The pool steps the CLAP records of a state still owe, each extending its record's run. A plugin's pin takes the pool word
951
+ * of its step (`cores`, or `off` for a plugin that was measured and gains nothing) and none for a plugin that does not ask
952
+ * for the pool. A step that could not run stays owed: the next pass tries it again.
953
+ */
954
+ export function poolPass(ctx: SyncContext, state: SyncState): { readonly state: SyncState; readonly cores: number; readonly off: number; readonly notAsked: number; readonly notRun: number } {
955
+ const count = { cores: 0, off: 0, notAsked: 0, notRun: 0 };
956
+ const runner = ctx.poolRunner ?? spawnedPoolRunner();
957
+ const host = ctx.settings.hostProfile.id;
958
+ const entries: Record<string, SyncEntry> = {};
959
+ for (const [key, entry] of Object.entries(state.entries)) {
960
+ entries[key] = {
961
+ ...entry,
962
+ plugins: entry.plugins.map((p) => {
963
+ if (p.record === undefined || !p.record.endsWith(".qualification.json") || p.pin === undefined || p.pin.verdict === "refused") return p;
964
+ const path = join(ctx.settings.qualificationDir, basename(p.record));
965
+ let record: QualificationRecord;
966
+ try {
967
+ record = JSON.parse(readFileSync(path, "utf8")) as QualificationRecord;
968
+ } catch {
969
+ ctx.log(`${p.uri}: its record does not read; no pool step`);
970
+ return p;
971
+ }
972
+ const done = completePool(path, record, p.pin.uri, isolatedCell(record, host), runner, poolEnv(ctx.env, ctx.settings.budgetS));
973
+ if (done === undefined) return p;
974
+ ctx.log(`${p.uri}: pool step ${done.step.result}${done.step.code === undefined ? "" : ` (${done.step.code})`}`);
975
+ if (done.step.result === POOL_RESULT.cores) count.cores++;
976
+ else if (done.step.result === POOL_RESULT.off) count.off++;
977
+ else if (done.step.result === POOL_RESULT.notAsked) count.notAsked++;
978
+ else count.notRun++;
979
+ return { ...p, pin: pinAfterPool(p.pin, done.step)! };
980
+ }),
981
+ };
982
+ }
983
+ return { ...count, state: count.cores + count.off + count.notAsked + count.notRun === 0 ? state : { ...state, entries } };
903
984
  }
904
985
 
905
986
  // --- the distributed records ----------------------------------------------------------------------------------------
@@ -938,10 +1019,35 @@ function takeDistributed(ctx: SyncContext, layer: DistributedLayer, item: SyncIt
938
1019
  ctx.log(`distributed: ${distributedUri(item, r)}: its record lacks the step${missing.length === 1 ? "" : "s"} ${missing.join(", ")}; measured here`);
939
1020
  return undefined;
940
1021
  }
1022
+ if (latencyNeverTaken(r.record)) {
1023
+ ctx.log(`distributed: ${distributedUri(item, r)}: its record never took the latency figure (a probe from before that pass); measured here`);
1024
+ return undefined;
1025
+ }
1026
+ }
1027
+ // The set was judged by the plugin-qualify that measured it. Its figures are carried across and judged again under
1028
+ // today's rules, as an upgrade's are (`--rejudge`): a verdict of an older judge (RT-unsafe read as refused, before
1029
+ // #196) is never taken as it stands. A record today's judge cannot decide for this host is measured here.
1030
+ const by = rejudgeToolName();
1031
+ const at = new Date(started).toISOString();
1032
+ const records = [];
1033
+ for (const r of match.records) {
1034
+ let record: QualificationRecord;
1035
+ try {
1036
+ record = rejudgeRecord(r.record, readFileSync(join(r.path, "..", r.record.probe.file)), { host, by, at }).record;
1037
+ } catch (e) {
1038
+ ctx.log(`distributed: ${distributedUri(item, r)}: ${(e as Error).message}; measured here`);
1039
+ return undefined;
1040
+ }
1041
+ const v = record.verdicts.find((x) => x.host === host);
1042
+ if (v !== undefined && outcomeOfVerdict(v) === "unmeasured") {
1043
+ ctx.log(`distributed: ${distributedUri(item, r)}: judged again, ${v.deciding ?? "unknown"} for ${host}; measured here`);
1044
+ return undefined;
1045
+ }
1046
+ records.push({ ...r, record });
941
1047
  }
942
1048
  let plugins: SyncPlugin[];
943
1049
  try {
944
- plugins = applyDistributed(ctx.settings.qualificationDir, item, match, host);
1050
+ plugins = applyDistributed(ctx.settings.qualificationDir, item, { ...match, records }, host);
945
1051
  } catch (e) {
946
1052
  ctx.log(`distributed: ${distributedUri(item, match.records[0]!)}: ${(e as Error).message}; measured here`);
947
1053
  return undefined;
@@ -980,7 +1086,7 @@ export function syncPinsText(state: SyncState): string {
980
1086
  /** The pin lines, by uri. */
981
1087
  function pinLinesOf(text: string): Map<string, string> {
982
1088
  const out = new Map<string, string>();
983
- for (const line of text.split("\n")) if (line !== "" && !line.startsWith("#")) out.set(line.split(" ")[1]!, line);
1089
+ for (const line of text.split("\n")) if (line !== "" && !line.startsWith("#")) out.set(pinLineUri(line)!, line);
984
1090
  return out;
985
1091
  }
986
1092
 
@@ -1181,6 +1287,8 @@ export interface SyncSummary {
1181
1287
  readonly machineFailure?: string;
1182
1288
  /** `--isolated-only`: how the steps ended. */
1183
1289
  readonly isolatedOnly?: { readonly isolated: number; readonly refused: number; readonly notRun: number };
1290
+ /** `--pool-only`: the plugins granted the thread pool, those measured and not granted it, those that do not ask for it, and those the step could not run for. */
1291
+ readonly poolOnly?: { readonly cores: number; readonly off: number; readonly notAsked: number; readonly notRun: number };
1184
1292
  /** `--adapter-only`: the plugins run, those done already at these host versions, and those whose host could not be run. */
1185
1293
  readonly adapterOnly?: { readonly measured: number; readonly already: number; readonly failed: number };
1186
1294
  }
@@ -1266,7 +1374,7 @@ export function pluginCountsLine(c: PluginCounts): string {
1266
1374
  }
1267
1375
 
1268
1376
  export function summaryLine(s: SyncSummary): string {
1269
- return `qualified ${String(s.qualified)}, skipped ${String(s.skipped)}, refused ${String(s.refused)}, crashed ${String(s.crashed)}, hung ${String(s.hung)}, unmeasured ${String(s.unmeasured)}, distributed ${String(s.distributed)}${s.refreshed ? `, pins refreshed ${String(s.refreshed)}` : ""}${s.rejudge === undefined ? "" : `, rejudged ${String(s.rejudge.changed)}, unchanged ${String(s.rejudge.unchanged)}, owing ${String(s.rejudge.owing)}`}${s.memoryOnly === undefined ? "" : `, memory measured ${String(s.memoryOnly.measured)}, already ${String(s.memoryOnly.already)}, failed ${String(s.memoryOnly.failed)}, leaks not measured`}${s.adapterOnly === undefined ? "" : `, adapter run ${String(s.adapterOnly.measured)}, already ${String(s.adapterOnly.already)}, failed ${String(s.adapterOnly.failed)}`}, ${s.seconds.toFixed(1)} s`;
1377
+ return `qualified ${String(s.qualified)}, skipped ${String(s.skipped)}, refused ${String(s.refused)}, crashed ${String(s.crashed)}, hung ${String(s.hung)}, unmeasured ${String(s.unmeasured)}, distributed ${String(s.distributed)}${s.refreshed ? `, pins refreshed ${String(s.refreshed)}` : ""}${s.rejudge === undefined ? "" : `, rejudged ${String(s.rejudge.changed)}, unchanged ${String(s.rejudge.unchanged)}, owing ${String(s.rejudge.owing)}`}${s.memoryOnly === undefined ? "" : `, memory measured ${String(s.memoryOnly.measured)}, already ${String(s.memoryOnly.already)}, failed ${String(s.memoryOnly.failed)}, leaks not measured`}${s.poolOnly === undefined ? "" : `, pool granted ${String(s.poolOnly.cores)}, off ${String(s.poolOnly.off)}, not asked ${String(s.poolOnly.notAsked)}, not run ${String(s.poolOnly.notRun)}`}${s.adapterOnly === undefined ? "" : `, adapter run ${String(s.adapterOnly.measured)}, already ${String(s.adapterOnly.already)}, failed ${String(s.adapterOnly.failed)}`}, ${s.seconds.toFixed(1)} s`;
1270
1378
  }
1271
1379
 
1272
1380
  /** The exit status `flock -n` returns when the lock is held (-E), told apart from a failure of flock itself. */
@@ -1320,6 +1428,10 @@ export interface SyncRunOptions {
1320
1428
  readonly isolatedRunner?: IsolatedRunner;
1321
1429
  /** `--isolated-only`: take only the isolated steps records still owe. */
1322
1430
  readonly isolatedOnly?: boolean;
1431
+ /** The pool step, a stand-in in tests. */
1432
+ readonly poolRunner?: PoolRunner;
1433
+ /** `--pool-only`: take only the pool steps records still owe. */
1434
+ readonly poolOnly?: boolean;
1323
1435
  /** `--adapter-only`: take only the adapter runs; `force` takes those already done again. */
1324
1436
  readonly adapterOnly?: boolean;
1325
1437
  /** Stand-ins for the pass's hosts, in tests. */
@@ -1388,6 +1500,7 @@ function addSummaries(a: SyncSummary, b: SyncSummary): SyncSummary {
1388
1500
  ...(b.rejudge === undefined ? (a.rejudge === undefined ? {} : { rejudge: a.rejudge }) : { rejudge: b.rejudge }),
1389
1501
  ...(b.memoryOnly === undefined ? (a.memoryOnly === undefined ? {} : { memoryOnly: a.memoryOnly }) : { memoryOnly: b.memoryOnly }),
1390
1502
  ...(b.isolatedOnly === undefined ? (a.isolatedOnly === undefined ? {} : { isolatedOnly: a.isolatedOnly }) : { isolatedOnly: b.isolatedOnly }),
1503
+ ...(b.poolOnly === undefined ? (a.poolOnly === undefined ? {} : { poolOnly: a.poolOnly }) : { poolOnly: b.poolOnly }),
1391
1504
  ...(b.adapterOnly === undefined ? (a.adapterOnly === undefined ? {} : { adapterOnly: a.adapterOnly }) : { adapterOnly: b.adapterOnly }),
1392
1505
  };
1393
1506
  }
@@ -1413,7 +1526,7 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
1413
1526
  {
1414
1527
  const started = Date.now();
1415
1528
  const dirs = [...settings.clapDirs, ...settings.lv2Dirs];
1416
- const ctx: SyncContext = { settings, env, log, ...(options.pinFilesRun === undefined ? {} : { pinFilesRun: options.pinFilesRun }), ...(options.isolatedRunner === undefined ? {} : { isolatedRunner: options.isolatedRunner }) };
1529
+ const ctx: SyncContext = { settings, env, log, ...(options.pinFilesRun === undefined ? {} : { pinFilesRun: options.pinFilesRun }), ...(options.isolatedRunner === undefined ? {} : { isolatedRunner: options.isolatedRunner }), ...(options.poolRunner === undefined ? {} : { poolRunner: options.poolRunner }) };
1417
1530
  // the pins first, before the quiet wait and any measurement: an upgrade of plugin-hostd that collects more files
1418
1531
  // than the pins on disk hold refuses those plugins until their pins name them (plugin-qualify#239)
1419
1532
  const fresh = refreshPins(ctx, readSyncState(settings.stateFile, log));
@@ -1433,6 +1546,12 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
1433
1546
  const summary = await sendRefreshed(settings, r.state, fresh.refreshed.length, started, log);
1434
1547
  return { ...summary, isolatedOnly: { isolated: r.isolated, refused: r.refused, notRun: r.notRun } };
1435
1548
  }
1549
+ if (options.poolOnly === true) {
1550
+ const r = poolPass(ctx, fresh.state);
1551
+ if (r.state !== fresh.state) writeFileWhole(settings.stateFile, `${JSON.stringify(r.state, null, 1)}\n`);
1552
+ const summary = await sendRefreshed(settings, r.state, fresh.refreshed.length, started, log);
1553
+ return { ...summary, poolOnly: { cores: r.cores, off: r.off, notAsked: r.notAsked, notRun: r.notRun } };
1554
+ }
1436
1555
  if (options.adapterOnly === true) {
1437
1556
  const r: AdapterPassResult = adapterPass(ctx, fresh.state, { ...options.adapterPass, force: options.force === true });
1438
1557
  log(`adapter runs under ${Object.entries(r.versions).map(([k, v]) => `${k} ${v}`).join(", ")}`);
@@ -1480,7 +1599,8 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
1480
1599
  for (const item of items) {
1481
1600
  const recorded = recordedAsIs(item, entries[item.key]);
1482
1601
  const retry = recorded.same ? entries[item.key]!.plugins.map(machineReason).find((r) => r !== undefined) : undefined;
1483
- if (recorded.same && options.force !== true && retry === undefined) {
1602
+ const owed = recorded.same && options.force !== true && retry === undefined ? figureOwed(entries[item.key]!) : undefined;
1603
+ if (recorded.same && options.force !== true && retry === undefined && owed === undefined) {
1484
1604
  if (pass === 0) count.skipped += entries[item.key]!.plugins.length;
1485
1605
  continue;
1486
1606
  }
@@ -1498,6 +1618,7 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
1498
1618
  continue;
1499
1619
  }
1500
1620
  if (recorded.same && retry !== undefined) log(`${item.key}: unmeasured last run by the machine (${retry}); measured again`);
1621
+ if (owed !== undefined) log(`${item.key}: its record never took the ${owed} figure (a probe from before that pass); measured again`);
1501
1622
  const files = recorded.files ?? digestFiles(item.root, item.files);
1502
1623
  if (files === undefined) {
1503
1624
  log(`${item.key}: a file of it cannot be read; skipped this run`);
@@ -1544,7 +1665,7 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
1544
1665
  if (unreadable) log("a plugin directory could not be read; no record or descriptor is removed this run");
1545
1666
  else for (const f of pruneStale(settings, stateBefore, state)) log(`${f}: removed, its plugin is gone or changed`);
1546
1667
  // a plugin that was pinned and is not any more is also forgotten by a running daemon
1547
- const changed = [...changedPinLines(before, after), ...removedPinUris(before, after).map((uri) => `pin_clear ${uri}`)];
1668
+ const changed = [...changedPinLines(before, after), ...removedPinUris(before, after).map(pinClearLine)];
1548
1669
  const sent = await sendPins(settings.hostdPort, settings.hostdFeedbackPort, changed);
1549
1670
  if ("unsent" in sent) {
1550
1671
  const why = {
@@ -1555,7 +1676,7 @@ async function runLocked(settings: SyncSettings, options: SyncRunOptions): Promi
1555
1676
  log(`${why}; the ${String(changed.length)} changed pins apply at its next start, from the pin file`);
1556
1677
  }
1557
1678
  const replies = "replies" in sent ? sent.replies : [];
1558
- for (let i = 0; i < replies.length; i++) if (replies[i] !== "resp 0") log(`plugin-hostd answered ${replies[i]!} to ${changed[i]!.split(" ")[1]!}`);
1679
+ for (let i = 0; i < replies.length; i++) if (replies[i] !== "resp 0") log(`plugin-hostd answered ${replies[i]!} to ${pinLineUri(changed[i]!)!}`);
1559
1680
  const taken = replies.filter((r) => r === "resp 0").length;
1560
1681
  for (const [why, n] of byMachine) if (n > 1) log(`${String(n)} plugins unmeasured by the machine (${why}); the first is listed above`);
1561
1682
  const [only] = byMachine.size === 1 ? [...byMachine] : [];
@@ -4,7 +4,8 @@
4
4
  * `plugin-qualify-records-package`: the Debian package of a device's qualification records, so other devices of the
5
5
  * machine class install them as the distributed layer instead of measuring the same plugin set again.
6
6
  *
7
- * In: a qualification directory as plugin-qualify-sync leaves it. Out: `omx-qualification-<class>-<set>_<version>_all.deb`
7
+ * In: a qualification directory as plugin-qualify-sync leaves it. Out: `plugin-db-qualification-<class>-<set>_<version>_all.deb`,
8
+ * a package of plugin-db's qualification layer,
8
9
  * installing every record and the probe output it judged under `/usr/share/omx/qualification/<class>/<set>/`, beside
9
10
  * the set's `provenance.json`. A record goes in only with its probe output, byte for byte the copy the record names;
10
11
  * an unmeasured record never applies and stays out. The archive is written here, `ar` and two `tar.xz` members, so a
@@ -16,7 +17,7 @@ import { closeSync, copyFileSync, mkdirSync, mkdtempSync, openSync, readFileSync
16
17
  import { tmpdir } from "node:os";
17
18
  import { join } from "node:path";
18
19
 
19
- import { DISTRIBUTED_DIR_SYSTEM, DISTRIBUTED_LAYER_SINCE, DISTRIBUTED_PROVENANCE_FILE, DISTRIBUTED_PROVENANCE_SCHEMA, type DistributedProvenance } from "./distributed-records.js";
20
+ import { DISTRIBUTED_DIR_SYSTEM, DISTRIBUTED_PROVENANCE_FILE, DISTRIBUTED_PROVENANCE_SCHEMA, type DistributedProvenance } from "./distributed-records.js";
20
21
  import { sha256Hex } from "./probe-output.js";
21
22
  import { QUALIFICATION_RECORD_SCHEMA } from "./qualification-record.js";
22
23
 
@@ -53,7 +54,7 @@ export function recordsPackageHelp(): string {
53
54
  " --maintainer <who> the package's Maintainer field",
54
55
  " --help this text",
55
56
  "",
56
- `the package installs ${DISTRIBUTED_DIR_SYSTEM}/<class>/<set>/ and depends on plugin-qualify (>= ${DISTRIBUTED_LAYER_SINCE})`,
57
+ `the package installs ${DISTRIBUTED_DIR_SYSTEM}/<class>/<set>/ and depends on this plugin-qualify's minor, up to the next one`,
57
58
  "exit 0 the package was written, 2 it could not be",
58
59
  "",
59
60
  ].join("\n");
@@ -97,7 +98,21 @@ export function parseRecordsPackageArgs(argv: readonly string[]): { ok: true; ar
97
98
  }
98
99
 
99
100
  /** The package of a class and a set. */
100
- export const recordsPackageName = (hostClass: string, set: string): string => `omx-qualification-${hostClass}-${set}`;
101
+ export const recordsPackageName = (hostClass: string, set: string): string => `plugin-db-qualification-${hostClass}-${set}`;
102
+
103
+ /** The plugin-qualify a package of records depends on: the minor that wrote them, up to the next minor. */
104
+ export function recordsDependsRange(version: string): { floor: string; ceiling: string } | undefined {
105
+ const m = /^(\d+)\.(\d+)\.\d+/.exec(version);
106
+ if (m === null) return undefined;
107
+ const major = Number(m[1]);
108
+ const minor = Number(m[2]);
109
+ return { floor: `${String(major)}.${String(minor)}.0`, ceiling: `${String(major)}.${String(minor + 1)}` };
110
+ }
111
+
112
+ /** This plugin-qualify's own version, read where it is installed. */
113
+ function ownVersion(): string {
114
+ return (JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { version?: string }).version ?? "";
115
+ }
101
116
 
102
117
  /** Today as a package version, `2026.10.29`, and as a date, `2026-10-29`, in local time. */
103
118
  export function today(now = new Date()): { version: string; date: string } {
@@ -199,6 +214,8 @@ export interface BuiltRecordsPackage {
199
214
  /** The package of `args`, written into `args.out`. Throws, naming why, when it cannot be built. */
200
215
  export function buildRecordsPackage(args: RecordsPackageArgs, now = new Date()): BuiltRecordsPackage {
201
216
  const name = recordsPackageName(args.hostClass, args.set);
217
+ const range = recordsDependsRange(ownVersion());
218
+ if (range === undefined) throw new Error("this plugin-qualify names no version, so the package cannot say which one reads it");
202
219
  const day = today(now);
203
220
  const version = args.version ?? day.version;
204
221
  statSync(args.stateDir);
@@ -238,13 +255,13 @@ export function buildRecordsPackage(args: RecordsPackageArgs, now = new Date()):
238
255
  "Architecture: all",
239
256
  `Maintainer: ${args.maintainer}`,
240
257
  `Installed-Size: ${String(kib)}`,
241
- `Depends: plugin-qualify (>= ${DISTRIBUTED_LAYER_SINCE}~)`,
258
+ `Depends: plugin-qualify (>= ${range.floor}~), plugin-qualify (<< ${range.ceiling}~)`,
242
259
  "Section: sound",
243
260
  "Priority: optional",
244
261
  "Multi-Arch: foreign",
245
- `Description: qualification records of the ${args.set} plugin set for ${args.hostClass}`,
246
- ` The plugin-qualify records of the ${args.set} plugin set, measured on a`,
247
- ` ${args.hostClass} device. plugin-qualify-sync takes a plugin from them instead of`,
262
+ `Description: plugin-db qualification layer: ${args.set} set for ${args.hostClass}`,
263
+ ` The qualification layer of the plugin database: the plugin-qualify records`,
264
+ ` of the ${args.set} plugin set, measured on a ${args.hostClass} device. plugin-qualify-sync takes a plugin from them instead of`,
248
265
  " measuring it, and only when every file of the installed plugin hashes as the",
249
266
  " measured one did; any other plugin is measured on the device as before.",
250
267
  "",
@@ -1201,7 +1201,9 @@ class _Host:
1201
1201
  def __init__(self, rate, block):
1202
1202
  import ctypes
1203
1203
 
1204
- import lilv
1204
+ from lilv_load import load_lilv
1205
+
1206
+ lilv = load_lilv()
1205
1207
 
1206
1208
  self.urids = {}
1207
1209
 
@@ -1362,7 +1364,9 @@ def probe_one(host, uri, symbol, cycles=DEFAULT_RUN_CYCLES):
1362
1364
  """
1363
1365
  import numpy as np
1364
1366
 
1365
- import lilv
1367
+ from lilv_load import load_lilv
1368
+
1369
+ lilv = load_lilv()
1366
1370
 
1367
1371
  plugin = host.world.get_all_plugins().get_by_uri(host.world.new_uri(uri))
1368
1372
  if plugin is None:
@@ -1453,7 +1457,9 @@ def instantiate_connected(host, plugin, overrides=None):
1453
1457
  """
1454
1458
  import numpy as np
1455
1459
 
1456
- import lilv
1460
+ from lilv_load import load_lilv
1461
+
1462
+ lilv = load_lilv()
1457
1463
 
1458
1464
  host.worker.reset()
1459
1465
  instance = lilv.Instance(plugin, float(host.rate), host.features)