@openmixer/plugin-qualify 0.20.0 → 0.20.3

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 (117) hide show
  1. package/bin/plugin-qualify-records-package.mjs +9 -7
  2. package/bin/plugin-qualify-sync.mjs +1 -0
  3. package/bin/plugin-qualify.mjs +34 -0
  4. package/catalogs/ca.json +8 -0
  5. package/dist/clap-loader.js +2 -1
  6. package/dist/clap-loader.js.map +1 -1
  7. package/dist/clap-qualify.d.ts +11 -0
  8. package/dist/clap-qualify.js +16 -0
  9. package/dist/clap-qualify.js.map +1 -1
  10. package/dist/distributed-records.d.ts +16 -3
  11. package/dist/distributed-records.js +38 -4
  12. package/dist/distributed-records.js.map +1 -1
  13. package/dist/host-admission.d.ts +25 -0
  14. package/dist/host-admission.js +41 -5
  15. package/dist/host-admission.js.map +1 -1
  16. package/dist/hostd-search-path.d.ts +38 -0
  17. package/dist/hostd-search-path.js +58 -0
  18. package/dist/hostd-search-path.js.map +1 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +2 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/jack-registry.js +1 -1
  23. package/dist/judged-layer.d.ts +91 -0
  24. package/dist/judged-layer.js +144 -0
  25. package/dist/judged-layer.js.map +1 -0
  26. package/dist/judgement-line.d.ts +6 -0
  27. package/dist/judgement-line.js +20 -0
  28. package/dist/judgement-line.js.map +1 -0
  29. package/dist/lv2-loader.d.ts +33 -1
  30. package/dist/lv2-loader.js +82 -19
  31. package/dist/lv2-loader.js.map +1 -1
  32. package/dist/measurement/cli.d.ts +2 -0
  33. package/dist/measurement/cli.js +25 -2
  34. package/dist/measurement/cli.js.map +1 -1
  35. package/dist/measurement/format.d.ts +2 -0
  36. package/dist/measurement/format.js +5 -0
  37. package/dist/measurement/format.js.map +1 -1
  38. package/dist/measurement/vocabulary.js +3 -3
  39. package/dist/measurement/vocabulary.js.map +1 -1
  40. package/dist/measurement-grid.d.ts +1 -1
  41. package/dist/measurement-grid.js +5 -3
  42. package/dist/measurement-grid.js.map +1 -1
  43. package/dist/pins-file.d.ts +3 -2
  44. package/dist/pins-file.js +3 -2
  45. package/dist/pins-file.js.map +1 -1
  46. package/dist/probe-judge.js +5 -0
  47. package/dist/probe-judge.js.map +1 -1
  48. package/dist/probe-output.d.ts +10 -0
  49. package/dist/probe-output.js +16 -0
  50. package/dist/probe-output.js.map +1 -1
  51. package/dist/probe-runner.js +24 -5
  52. package/dist/probe-runner.js.map +1 -1
  53. package/dist/qualification-record.d.ts +13 -0
  54. package/dist/qualification-record.js +25 -3
  55. package/dist/qualification-record.js.map +1 -1
  56. package/dist/qualify-adapter-pass.d.ts +1 -2
  57. package/dist/qualify-adapter-pass.js +6 -6
  58. package/dist/qualify-adapter-pass.js.map +1 -1
  59. package/dist/qualify-declarations.d.ts +23 -6
  60. package/dist/qualify-declarations.js +25 -6
  61. package/dist/qualify-declarations.js.map +1 -1
  62. package/dist/qualify-isolated.d.ts +14 -3
  63. package/dist/qualify-isolated.js +22 -9
  64. package/dist/qualify-isolated.js.map +1 -1
  65. package/dist/qualify-pool.js +4 -3
  66. package/dist/qualify-pool.js.map +1 -1
  67. package/dist/qualify-rejudge.js +3 -3
  68. package/dist/qualify-rejudge.js.map +1 -1
  69. package/dist/qualify-sync.d.ts +18 -6
  70. package/dist/qualify-sync.js +68 -38
  71. package/dist/qualify-sync.js.map +1 -1
  72. package/dist/records-package.d.ts +31 -4
  73. package/dist/records-package.js +149 -24
  74. package/dist/records-package.js.map +1 -1
  75. package/dist/run-home.d.ts +4 -0
  76. package/dist/run-home.js +26 -0
  77. package/dist/run-home.js.map +1 -0
  78. package/fixtures/records-rpi5-0.19.1/http___drumkv1.sourceforge.net_lv2@0.2-7d179bdcf9e5.default.probe.json +58 -0
  79. package/fixtures/records-rpi5-0.19.1/http___drumkv1.sourceforge.net_lv2@0.2-7d179bdcf9e5.default.qualification.json +305 -0
  80. package/fixtures/records-rpi5-0.19.1/http___samplv1.sourceforge.net_lv2@0.2-c9cd8fb3102f.default.probe.json +58 -0
  81. package/fixtures/records-rpi5-0.19.1/http___samplv1.sourceforge.net_lv2@0.2-c9cd8fb3102f.default.qualification.json +301 -0
  82. package/package.json +7 -5
  83. package/schema/codes.json +15 -0
  84. package/src/clap-loader.ts +2 -1
  85. package/src/clap-qualify.ts +25 -0
  86. package/src/distributed-records.ts +41 -4
  87. package/src/host-admission.ts +52 -5
  88. package/src/hostd-search-path.ts +85 -0
  89. package/src/index.ts +2 -0
  90. package/src/jack-registry.ts +1 -1
  91. package/src/judged-layer.ts +197 -0
  92. package/src/judgement-line.ts +22 -0
  93. package/src/lv2-loader.ts +89 -17
  94. package/src/measurement/cli.ts +26 -2
  95. package/src/measurement/format.ts +8 -0
  96. package/src/measurement/vocabulary.ts +3 -3
  97. package/src/measurement-grid.ts +5 -3
  98. package/src/pins-file.ts +3 -2
  99. package/src/probe-judge.ts +5 -0
  100. package/src/probe-output.ts +23 -0
  101. package/src/probe-runner.ts +25 -4
  102. package/src/qualification-record.ts +32 -3
  103. package/src/qualify-adapter-pass.ts +6 -5
  104. package/src/qualify-declarations.ts +27 -6
  105. package/src/qualify-isolated.ts +24 -9
  106. package/src/qualify-pool.ts +4 -3
  107. package/src/qualify-rejudge.ts +3 -3
  108. package/src/qualify-sync.ts +74 -41
  109. package/src/records-package.ts +156 -25
  110. package/src/run-home.ts +28 -0
  111. package/tools/_declarations.py +2 -1
  112. package/tools/descriptors.py +40 -3
  113. package/tools/gen-python-declarations.mjs +1 -0
  114. package/tools/isolated-step.mjs +24 -3
  115. package/tools/lv2-measure.mjs +21 -2
  116. package/tools/mod-host-admit.mjs +57 -1
  117. package/tools/scan.py +29 -5
@@ -14,9 +14,16 @@ import { ORACLE_FLOOR_RATES, RT_HARD_TARGET } from "@openmixer/omx-contract";
14
14
  export const COST_REFERENCE_RATE = 96000;
15
15
 
16
16
  /**
17
- * The reference quantum in frames: the rig's buffer, and the block `benchmark.py` runs.
17
+ * The reference quantum in frames: the buffer the console aims at (96000/256), the quantum its budget is judged at,
18
+ * and the block `benchmark.py` runs.
18
19
  */
19
- export const QUANTUM_FRAMES = 1024;
20
+ export const QUANTUM_FRAMES = 256;
21
+
22
+ /**
23
+ * The long quantum, in frames: the console's basis until 0.20.2, kept as a measured cell so a plugin's cost at a large
24
+ * buffer stays on record beside its cost at the console's.
25
+ */
26
+ export const LONG_QUANTUM_FRAMES = 1024;
20
27
 
21
28
  /** The rate the offline capture window is sized at; other rates scale from it. */
22
29
  export const MEASURE_BASE_RATE = 48000;
@@ -31,8 +38,8 @@ export const OPERATIONAL_RATE_FLOOR = 48000;
31
38
  /** The rates a run measures when `--rates` is not given — recorded in `run.rates`: omx-contract's ORACLE_FLOOR_RATES. */
32
39
  export const QUALIFY_RATES: readonly number[] = ORACLE_FLOOR_RATES;
33
40
 
34
- /** The quanta (frames) a run rates cost at when `--quanta` is not given: the contract's hard-target quantum, then 256, 512 and the reference. */
35
- export const QUALIFY_QUANTA: readonly number[] = [RT_HARD_TARGET.quantum, 256, 512, QUANTUM_FRAMES];
41
+ /** The quanta (frames) a run rates cost at when `--quanta` is not given: the contract's hard-target quantum, the reference, 512 and the long quantum. */
42
+ export const QUALIFY_QUANTA: readonly number[] = [RT_HARD_TARGET.quantum, QUANTUM_FRAMES, 512, LONG_QUANTUM_FRAMES];
36
43
 
37
44
  /** Seconds one plugin's offline pass may take before its worker is killed and it reads as a timeout. */
38
45
  export const PLUGIN_TIMEOUT_S = 15;
@@ -174,13 +181,22 @@ export const MEMORY_BOUND_EXIT_STATUS = 121;
174
181
  /**
175
182
  * The memory-only pass (`plugin-qualify-sync --memory-only`). With `PROBE_MEMORY_ONLY_ENV` set the probers take the
176
183
  * footprint and nothing else; the probe runner passes its environment on, so its limits hold unchanged.
177
- * `PROBE_NOTES_ENV` is the count of notes an instrument that declares no voice capacity holds, basis `probe`:
184
+ * `PROBE_NOTES_ENV` is the count of notes an instrument that declares no voice capacity holds, basis `probe`, in every
185
+ * LV2 run (probe-runner) and in the memory-only pass:
178
186
  * `MEMORY_PASS_PROBE_NOTES` unless `memory_notes` says otherwise.
179
187
  */
180
188
  export const PROBE_MEMORY_ONLY_ENV = "OMX_PROBE_MEMORY_ONLY";
181
189
  export const PROBE_NOTES_ENV = "OMX_PROBE_NOTES";
182
190
  export const MEMORY_PASS_PROBE_NOTES = 16;
183
191
 
192
+ /**
193
+ * The counts of notes an LV2 instrument is timed at, besides its probe count, for its cost against the notes held
194
+ * (the rate's `voiceCurve`, probe output 1.4): the counts the Pi measurements held Surge XT at
195
+ * (openmixer.org/notes/instruments-on-a-pi-5), so a voice limit shows as the count past which the cost stops growing.
196
+ */
197
+ export const PROBE_NOTES_CURVE_ENV = "OMX_PROBE_NOTES_CURVE";
198
+ export const PROBE_NOTES_CURVE = [1, 8, 16, 32, 48, 64] as const;
199
+
184
200
  /**
185
201
  * What `tools/mod-host-admit.mjs` is told in an adapter-only pass: how long the host has for one command, its address
186
202
  * space (`ulimit -v`, in KB) and the directory for the state files of the plugin it loads.
@@ -239,6 +255,8 @@ export const DESCRIPTOR_CODE_SCHEMA_MISSING = "qualify.descriptor.schema-missing
239
255
  export const DESCRIPTOR_CODE_SCHEMA_FLOOR = "qualify.descriptor.schema-floor";
240
256
  export const DESCRIPTOR_CODE_BINARY_UNHASHED = "qualify.descriptor.binary-unhashed";
241
257
  export const DESCRIPTOR_CODE_DIR_UNWRITABLE = "qualify.descriptor.dir-unwritable";
258
+ /** Two ports of one plugin carry the same symbol: neither can be addressed by it, so the plugin is not described. Args: `symbol`. */
259
+ export const DESCRIPTOR_CODE_DUPLICATE_SYMBOL = "qualify.descriptor.duplicate-symbol";
242
260
 
243
261
 
244
262
 
@@ -317,11 +335,14 @@ const piMemory = (ramGib: number): NonNullable<HostBudget["memory"]> => ({
317
335
  /**
318
336
  * The cells a Raspberry Pi runs: zynthian-sys's JACK settings, 48000/128 on a Pi 5 with the V5 kit and 48000/256 on a Pi 4 and
319
337
  * the other kits, the one a Pi 5 with no kit card also ran its stress and first boot at. The Zynthian benchmark's matrix sweeps
320
- * more cells; these two are what a Pi is set to.
338
+ * more cells; these two are what a Pi is set to. Every Pi matrix carries 96000 beside 48000, so each of the two quanta
339
+ * is also run at twice the base rate: the passes that follow these cells (adapter, isolated, memory, pool) cost twice as long.
321
340
  */
322
341
  export const PI_CELLS = [
323
342
  { rate: MEASURE_BASE_RATE, quantum: 128 },
324
343
  { rate: MEASURE_BASE_RATE, quantum: 256 },
344
+ { rate: 2 * MEASURE_BASE_RATE, quantum: 128 },
345
+ { rate: 2 * MEASURE_BASE_RATE, quantum: 256 },
325
346
  ];
326
347
 
327
348
  export const HOST_BUDGETS = {
@@ -16,6 +16,7 @@ import { spawnSync } from "node:child_process";
16
16
  import { existsSync, readFileSync } from "node:fs";
17
17
  import { basename, join } from "node:path";
18
18
 
19
+ import { withRunHome } from "./run-home.js";
19
20
  import { ADAPTER_PASS_CELLS, startPrivateJack, type JackStarter } from "./qualify-adapter-pass.js";
20
21
  import type { GridCell } from "./measurement-grid.js";
21
22
  import { clapPinUri, type PinEntry } from "./pins-file.js";
@@ -100,16 +101,18 @@ function figuresOf(run: IsolatedRunFacts): IsolatedFigures | undefined {
100
101
 
101
102
  /**
102
103
  * A verdict whose only gap is memory nobody measured: it reads `unknown` and every reason that is not `suitable` is
103
- * `hosting.memory.unmeasured`. Nothing says the plugin is unfit, so it is hosted in a worker of its own until its
104
- * footprint is measured, and the isolated step is what shows it running alone.
104
+ * `hosting.memory.unmeasured`, or a `conditional` one the isolated step answers (an RT violation seen in the run). Nothing
105
+ * says the plugin is unfit, so it is hosted in a worker of its own until its footprint is measured, and the isolated step
106
+ * is what shows it running alone. A conditional reason beside the memory gap owes the step as a conditional verdict does:
107
+ * whether one run saw a violation and another did not must not decide whether the step is run (#319).
105
108
  */
106
109
  export function memoryGapOnly(v: Pick<RecordVerdict, "measured" | "rating" | "reasons">): boolean {
107
110
  if (!v.measured || v.rating !== "unknown") return false;
108
111
  const open = v.reasons.filter((r) => r.rating !== "suitable");
109
- return open.length > 0 && open.every((r) => r.code === HOSTING_CODE.memoryUnmeasured);
112
+ return open.some((r) => r.code === HOSTING_CODE.memoryUnmeasured) && open.every((r) => r.code === HOSTING_CODE.memoryUnmeasured || r.rating === "conditional");
110
113
  }
111
114
 
112
- /** Whether a record's outcome calls for the isolated step: rated `conditional` by some host, a memory gap and nothing else, or a probe that crashed or hung. */
115
+ /** Whether a record's outcome calls for the isolated step: rated `conditional` by some host, a memory gap and nothing else unanswered, or a probe that crashed or hung. */
113
116
  export function owesIsolated(record: QualificationRecord): boolean {
114
117
  return PROBE_FAILED_HARD.has(record.probe.outcome) || record.verdicts.some((v) => v.measured && (v.rating === "conditional" || memoryGapOnly(v)));
115
118
  }
@@ -128,6 +131,21 @@ export function pinFromRecord(record: QualificationRecord): PinEntry | undefined
128
131
  return pinAfterStep({ uri, integrity: { files: sorted, layout } }, step);
129
132
  }
130
133
 
134
+ /**
135
+ * Whether a step of the isolated kind is a verdict. A refusal (`isolated.silent-output`, `isolated.not-added`) is one: the
136
+ * plugin was tried and the answer is no. A step that could not run decided nothing, and neither did silence before the
137
+ * notes were played where a plugin could hear them: a step of that tool does not say how many inputs it linked. Those two
138
+ * are owed, and a pass tries them again.
139
+ */
140
+ export function isolatedDecided(s: RunStep): boolean {
141
+ return s.step === RUN_STEP.isolated && s.result !== ISOLATED_RESULT.notRun && !(s.code === ISOLATED_CODE.silent && s.figures?.midiInputsLinked === undefined);
142
+ }
143
+
144
+ /** Whether the record holds a finished isolated step, of whatever answer: nothing about it is owed. */
145
+ export function isolatedFinished(record: QualificationRecord): boolean {
146
+ return record.run?.steps.some(isolatedDecided) === true;
147
+ }
148
+
131
149
  /** Whether the isolated step saw this plugin running alone. A record's word alone never says so. */
132
150
  export function provenAlone(record: QualificationRecord): boolean {
133
151
  return record.run?.steps.some((s) => s.step === RUN_STEP.isolated && s.result === ISOLATED_RESULT.isolated) === true;
@@ -180,7 +198,7 @@ export function spawnedIsolatedRunner(jack: JackStarter = startPrivateJack): Iso
180
198
  encoding: "utf8",
181
199
  timeout: ISOLATED_WINDOW_MS + ISOLATED_STEP_SLACK_MS,
182
200
  killSignal: "SIGKILL",
183
- env: { ...env, JACK_DEFAULT_SERVER: server.name, [ISOLATED_SIGNAL_TOOL_ENV]: signal },
201
+ env: { ...withRunHome(env), JACK_DEFAULT_SERVER: server.name, [ISOLATED_SIGNAL_TOOL_ENV]: signal },
184
202
  });
185
203
  if (r.error !== undefined || r.status !== 0) return { started: false, problem: `isolated-step: ${r.error?.message ?? `exit ${String(r.status)}`}: ${(r.stderr ?? "").trim()}` };
186
204
  return JSON.parse(r.stdout) as IsolatedRunFacts;
@@ -223,10 +241,7 @@ export function runIdOf(record: QualificationRecord, dir: string): string | unde
223
241
  */
224
242
  export function completeIsolated(path: string, record: QualificationRecord, uri: string, cell: GridCell, runner: IsolatedRunner, env: Readonly<Record<string, string | undefined>>): { readonly record: QualificationRecord; readonly step: RunStep } | undefined {
225
243
  const isStep = (s: RunStep): boolean => s.step === RUN_STEP.isolated;
226
- // a step that could not run decided nothing, and neither did silence before the notes were played where a plugin could
227
- // hear them: a step of that tool does not say how many inputs it linked. A pass tries these again, and it takes their place
228
- const decided = (s: RunStep): boolean => s.result !== ISOLATED_RESULT.notRun && !(s.code === ISOLATED_CODE.silent && s.figures?.midiInputsLinked === undefined);
229
- if (!owesIsolated(record) || record.run?.steps.some((s) => isStep(s) && decided(s)) === true) return undefined;
244
+ if (!owesIsolated(record) || isolatedFinished(record)) return undefined;
230
245
  const id = runIdOf(record, join(path, ".."));
231
246
  if (id === undefined) return undefined;
232
247
  const step = judgeIsolatedRun(runner(uri, cell, env));
@@ -30,6 +30,7 @@ import { existsSync } from "node:fs";
30
30
  import { availableParallelism } from "node:os";
31
31
  import { join } from "node:path";
32
32
 
33
+ import { withRunHome } from "./run-home.js";
33
34
  import type { GridCell } from "./measurement-grid.js";
34
35
  import type { PinEntry, PinPool } from "./pins-file.js";
35
36
  import type { PoolFigures, QualificationRecord, RunBlock, RunStep } from "./qualification-record.js";
@@ -203,7 +204,7 @@ const POOL_STEP_SLACK_MS = 120000;
203
204
  export function asksForPool(prober: string, uri: string, env: Readonly<Record<string, string | undefined>>): boolean | undefined {
204
205
  const m = /^clap:(.*)#([^#]*)$/s.exec(uri);
205
206
  if (m === null) return undefined;
206
- const r = spawnSync(prober, ["--asks-thread-pool", m[1]!, m[2]!], { encoding: "utf8", timeout: 60000, killSignal: "SIGKILL", env: { ...env } });
207
+ const r = spawnSync(prober, ["--asks-thread-pool", m[1]!, m[2]!], { encoding: "utf8", timeout: 60000, killSignal: "SIGKILL", env: withRunHome(env) });
207
208
  if (r.status !== 0) return undefined;
208
209
  try {
209
210
  return (JSON.parse(r.stdout) as { threadPool?: unknown }).threadPool === true;
@@ -233,7 +234,7 @@ export function watchOnHelpers(prober: string, interposer: string, uri: string,
233
234
  encoding: "utf8",
234
235
  timeout: 120000,
235
236
  killSignal: "SIGKILL",
236
- env: { ...env, LD_PRELOAD: preload },
237
+ env: { ...withRunHome(env), LD_PRELOAD: preload },
237
238
  });
238
239
  if (r.error !== undefined || r.status !== 0) return { problem: `${CLAP_QUALIFY_TOOL_NAME} --pool: ${r.error?.message ?? `exit ${String(r.status)}`}` };
239
240
  const line = r.stdout.split("\n").filter((l) => l.startsWith("{")).pop();
@@ -273,7 +274,7 @@ export function spawnedPoolRunner(jack: JackStarter = startPrivateJack): PoolRun
273
274
  encoding: "utf8",
274
275
  timeout: 2 * POOL_ROUNDS * POOL_WINDOW_MS + POOL_STEP_SLACK_MS,
275
276
  killSignal: "SIGKILL",
276
- env: { ...env, JACK_DEFAULT_SERVER: server.name, [ISOLATED_SIGNAL_TOOL_ENV]: signal },
277
+ env: { ...withRunHome(env), JACK_DEFAULT_SERVER: server.name, [ISOLATED_SIGNAL_TOOL_ENV]: signal },
277
278
  });
278
279
  if (r.error !== undefined || r.status !== 0) return { started: false, problem: `pool-step: ${r.error?.message ?? `exit ${String(r.status)}`}: ${(r.stderr ?? "").trim()}` };
279
280
  return { ...(JSON.parse(r.stdout) as PoolRunFacts), asks: true, rt: watched.rt };
@@ -48,7 +48,7 @@ import {
48
48
  type RecordVerdict,
49
49
  type RunBlock,
50
50
  } from "./qualification-record.js";
51
- import { ISOLATED_RESULT, RUN_STEP, pinAfterStep, provenAlone } from "./qualify-isolated.js";
51
+ import { ISOLATED_RESULT, RUN_STEP, isolatedFinished, pinAfterStep } from "./qualify-isolated.js";
52
52
  import { pinAfterPool, poolStepOf } from "./qualify-pool.js";
53
53
  import { PI_CELLS, PROBE_MEMORY_ONLY_ENV, PROBE_NOTES_ENV } from "./qualify-declarations.js";
54
54
  import { SYNC_STATE_SCHEMA, digestFiles, recordStem, type SyncContext, type SyncEntry, type SyncPlugin, type SyncState } from "./qualify-sync.js";
@@ -198,9 +198,9 @@ export function rejudgeRecord(record: QualificationRecord, probeBytes: Uint8Arra
198
198
  const { profiles, kept } = rejudgeProfiles(record, output, options.host);
199
199
  const { reasons, owes: runs } = runsOwed(record, output, profiles);
200
200
  const judgment = judgeProbeOutput(output, profiles, { admissions: admissionsOf(record), owed: reasons });
201
- // decides no verdict: a record from before the steps that reads isolated stays offered, runs alone, and is not proven by the step
201
+ // decides no verdict: a record from before the steps that reads isolated stays offered and runs alone, and owes the step until one has finished; a refusal is finished: it is the step's verdict, not debt
202
202
  const readsIsolated = judgment.hosts.some((h) => h.verdict.measured && h.verdict.rating === "conditional");
203
- const owes = readsIsolated && !provenAlone(record) ? [...runs, { measurement: "isolated-step" as const, code: OWED_CODE.isolatedStep }] : runs;
203
+ const owes = readsIsolated && !isolatedFinished(record) ? [...runs, { measurement: "isolated-step" as const, code: OWED_CODE.isolatedStep }] : runs;
204
204
  const memory = record.memoryRun === undefined ? {} : { memory: { tool: record.memoryRun.tool, measuredAt: record.memoryRun.measuredAt } };
205
205
  const a = record.adapter;
206
206
  const adapterTaken = a?.versions === undefined || a.measuredAt === undefined ? {} : { adapter: { versions: a.versions, measuredAt: a.measuredAt } };
@@ -23,23 +23,26 @@ import { createConnection } from "node:net";
23
23
  import { homedir, tmpdir } from "node:os";
24
24
  import { basename, dirname, join, resolve, sep } from "node:path";
25
25
 
26
+ import { withRunHome } from "./run-home.js";
26
27
  import { clapQualifyTools } from "./clap-qualify.js";
27
- import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, outcomeOfVerdict, matchDistributed, readBoardModel, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
28
- import { clapSearchPath, parseClapScan } from "./clap-loader.js";
28
+ import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, hostClassOfMachine, outcomeOfVerdict, matchDistributed, readBoardModel, readMachine, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
29
+ import { parseClapScan } from "./clap-loader.js";
30
+ import { SPAWNED_SEARCH_PATH, hostdSearchPath, searchPathWarnings, type SearchPathRun } from "./hostd-search-path.js";
29
31
  import { HOSTING_CODE } from "./hosting-codes.js";
30
32
  import { shippedHostProfile, type HostProfile } from "./host-profiles.js";
31
- import { lv2BundlesOnPath, manifestPlugins } from "./lv2-loader.js";
33
+ import { NO_JACK_SERVER } from "./host-admission.js";
34
+ import { iriPath, lv2BundlesOnPath, manifestPlugins } from "./lv2-loader.js";
32
35
  import { judgmentPinEntries, pinClearLine, pinLineUri, pinsFile, type PinEntry } from "./pins-file.js";
33
36
  import { POOL_RESULT, completePool, judgePoolRun, pinAfterPool, pinFromRecordWithPool, poolEnv, spawnedPoolRunner, type PoolRunner } from "./qualify-pool.js";
34
37
  import { hostdPinFiles, type PinFilesRun } from "./hostd-pin-files.js";
35
38
  import type { Judgment } from "./probe-judge.js";
36
39
  import { qualifyTargets, type ProbeRun } from "./probe-runner.js";
37
- import { GRID_FULL, parseGridSpec, type GridSpec } from "./measurement-grid.js";
40
+ import { GRID_FULL, hostCells, parseGridSpec, type GridCell, type GridSpec } from "./measurement-grid.js";
38
41
  import { adapterPass, type AdapterPassOptions, type AdapterPassResult } from "./qualify-adapter-pass.js";
39
42
  import { ISOLATED_RESULT, baseSteps, completeIsolated, isolatedCell, missingSteps, judgeIsolatedRun, owesIsolated, pinAfterStep, spawnedIsolatedRunner, type IsolatedRunner } from "./qualify-isolated.js";
40
43
  import { lv2World, rejudgeRecord, rejudgeState, rejudgeToolName, memoryPass, type MemoryPassResult, type RejudgeResult } from "./qualify-rejudge.js";
41
44
  import { memoryBoundOf, parseMemoryLimit, type MemoryBound } from "./memory-bound.js";
42
- import { QUALIFICATION_RECORD_SCHEMA, latencyNeverTaken, rewriteRecord, writeQualification, type NotMeasured, type QualificationRecord } from "./qualification-record.js";
45
+ import { QUALIFICATION_RECORD_SCHEMA, descriptorNamePart, latencyNeverTaken, rewriteRecord, writeQualification, type NotMeasured, type QualificationRecord } from "./qualification-record.js";
43
46
  import { ADMIT_STEP_MS_ENV, DESCRIPTOR_DIR_STATE_SUBDIR, DESCRIPTOR_DIR_SYSTEM, MEMORY_PASS_PROBE_NOTES, QUALIFY_QUANTA, QUALIFY_RATES } from "./qualify-declarations.js";
44
47
  import { toolsDir } from "./tool-paths.js";
45
48
  import { writeFileWhole } from "./whole-file.js";
@@ -49,12 +52,6 @@ import { writeFileWhole } from "./whole-file.js";
49
52
  /** The settings file a system service reads, `key value` per line. */
50
53
  export const SYNC_CONF = "/etc/omx/qualify.conf";
51
54
 
52
- /** The LV2 directories plugin-hostd searches when `LV2_PATH` is unset (its `lv2_path` default). */
53
- export const HOSTD_LV2_PATH = ["/usr/lib64/lv2", "/usr/lib/lv2", "/usr/local/lib/lv2"];
54
-
55
- /** Zynthian's own LV2 directories: the image's plugins and the ones its user installs. */
56
- export const ZYNTHIAN_LV2_DIRS = ["/zynthian/zynthian-plugins/lv2", "/zynthian/zynthian-my-data/plugins/lv2"];
57
-
58
55
  export interface SyncSetting {
59
56
  readonly key: string;
60
57
  readonly meaning: string;
@@ -64,10 +61,7 @@ export interface SyncSetting {
64
61
 
65
62
  /** Every setting, declared once: the settings file, the flags (`--pin-file` for `pin_file`) and `--help` read this. */
66
63
  export const SYNC_SETTINGS: readonly SyncSetting[] = [
67
- { key: "lv2_path", meaning: "the LV2 directories, colon-separated, in the order plugin-hostd searches them", default: "$LV2_PATH, else " + HOSTD_LV2_PATH.join(":") },
68
- { key: "lv2_extra", meaning: "more LV2 directories, searched after lv2_path", default: ZYNTHIAN_LV2_DIRS.join(":") },
69
- { key: "clap_path", meaning: "the CLAP directories, colon-separated", default: "$CLAP_PATH, then ~/.clap, then /usr/lib/clap" },
70
- { key: "clap_extra", meaning: "more CLAP directories, searched after clap_path", default: "" },
64
+ { key: "hostd_conf", meaning: "plugin-hostd's settings file: the LV2 and CLAP directories scanned are the ones plugin-hostd --search-path -c <file> prints", default: "plugin-hostd's own (its XDG_CONFIG_HOME/plugin-hostd.conf, else ~/.config/plugin-hostd.conf)" },
71
65
  { key: "pin_file", meaning: "the pin file plugin-hostd loads as pin_file", default: "/var/lib/omx/qualified.pins (system), else $XDG_STATE_HOME/omx/qualified.pins" },
72
66
  { key: "qualification_dir", meaning: "where the qualification records go", default: "/var/lib/omx/qualification (system), else $XDG_STATE_HOME/omx/qualification" },
73
67
  { key: "descriptor_dir", meaning: "where the scanned descriptors go", default: `${DESCRIPTOR_DIR_SYSTEM} (system), else $XDG_STATE_HOME/${DESCRIPTOR_DIR_STATE_SUBDIR}` },
@@ -85,8 +79,16 @@ export const SYNC_SETTINGS: readonly SyncSetting[] = [
85
79
  { key: "distributed_dir", meaning: "the directories of distributed records, colon-separated, each holding <host class>/<set>/", default: `${DISTRIBUTED_DIR_SYSTEM} (system), else $XDG_DATA_HOME/omx/qualification and each $XDG_DATA_DIRS/omx/qualification` },
86
80
  ];
87
81
 
82
+ /** The search path settings that moved to plugin-hostd's settings file (plugin-qualify#9): refused here, with where they went. */
83
+ export const MOVED_TO_HOSTD = ["lv2_path", "lv2_extra", "clap_path", "clap_extra"] as const;
84
+
85
+ const movedText = (key: string): string =>
86
+ `${key} is plugin-hostd's setting now: write it in plugin-hostd's settings file (plugin-hostd.conf, named here by hostd_conf); the directories scanned are the ones plugin-hostd --search-path prints${key === "clap_extra" ? " (plugin-hostd has no clap_extra: add the directories to clap_path)" : ""}`;
87
+
88
88
  export interface SyncSettings {
89
+ /** The LV2 directories, as plugin-hostd searches them with hostd_conf (plugin-hostd --search-path). */
89
90
  readonly lv2Dirs: readonly string[];
91
+ /** The CLAP directories, as plugin-hostd searches them with hostd_conf. */
90
92
  readonly clapDirs: readonly string[];
91
93
  readonly pinFile: string;
92
94
  readonly qualificationDir: string;
@@ -121,6 +123,7 @@ export function parseSyncConf(text: string, source = SYNC_CONF): { ok: true; val
121
123
  if (line === "") continue;
122
124
  const m = /^(\S+)(?:\s+(.*))?$/.exec(line)!;
123
125
  const key = m[1]!;
126
+ if ((MOVED_TO_HOSTD as readonly string[]).includes(key)) return { ok: false, error: `${source}:${String(n + 1)}: ${movedText(key)}` };
124
127
  if (!known.has(key)) return { ok: false, error: `${source}:${String(n + 1)}: ${key} is not a setting` };
125
128
  values.set(key, (m[2] ?? "").trim());
126
129
  }
@@ -186,7 +189,8 @@ export function parseSyncArgs(argv: readonly string[]): { ok: true; args: SyncAr
186
189
  if (v === undefined) return { ok: false, error: `${a} needs a value` };
187
190
  if (a === "--config") config = v;
188
191
  else values.set(byFlag.get(a)!, v);
189
- } else return { ok: false, error: `unknown argument ${a}` };
192
+ } else if (MOVED_TO_HOSTD.some((k) => flagOf(k) === a)) return { ok: false, error: `${a}: ${movedText(a.slice(2).replace(/-/g, "_"))}` };
193
+ else return { ok: false, error: `unknown argument ${a}` };
190
194
  }
191
195
  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
196
  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" };
@@ -228,17 +232,19 @@ export function resolveSyncSettings(
228
232
  env: Readonly<Record<string, string | undefined>>,
229
233
  home: string = homedir(),
230
234
  boardModel: () => string | undefined = readBoardModel,
231
- ): { ok: true; settings: SyncSettings } | { ok: false; error: string } {
235
+ machineArch: () => string | undefined = readMachine,
236
+ searchPathRun: SearchPathRun = SPAWNED_SEARCH_PATH,
237
+ ): { ok: true; settings: SyncSettings; warnings: string[] } | { ok: false; error: string } {
232
238
  const get = (key: string): string | undefined => args.values.get(key) ?? file.get(key);
233
239
  const state = env["XDG_STATE_HOME"] !== undefined && env["XDG_STATE_HOME"] !== "" ? env["XDG_STATE_HOME"] : join(home, ".local", "state");
234
240
  const place = (key: string, system: string, user: string): string => {
235
241
  const v = get(key);
236
242
  return v !== undefined && v !== "" ? v : args.system ? system : join(state, user);
237
243
  };
238
- const lv2Named = get("lv2_path");
239
- const lv2Base = lv2Named !== undefined && lv2Named !== "" ? dirList(lv2Named) : dirList(env["LV2_PATH"] ?? "").length > 0 ? dirList(env["LV2_PATH"]!) : HOSTD_LV2_PATH;
240
- const clapNamed = get("clap_path");
241
- const clapBase = clapNamed !== undefined && clapNamed !== "" ? dirList(clapNamed) : clapSearchPath(env, home);
244
+ const hostdConfNamed = get("hostd_conf");
245
+ const hostdConf = hostdConfNamed !== undefined && hostdConfNamed !== "" ? hostdConfNamed : undefined;
246
+ const search = hostdSearchPath(hostdConf, env, searchPathRun);
247
+ if (!search.ok) return { ok: false, error: `the directories to scan: ${search.error}` };
242
248
  const number = (key: string, min: number): number | undefined => {
243
249
  const v = Number(get(key) ?? SYNC_SETTINGS.find((s) => s.key === key)!.default);
244
250
  return Number.isFinite(v) && v >= min ? v : undefined;
@@ -276,13 +282,14 @@ export function resolveSyncSettings(
276
282
  ? [DISTRIBUTED_DIR_SYSTEM]
277
283
  : [dataHome, ...dataDirs].map((d) => join(d, "omx", "qualification"));
278
284
  const classNamed = get("host_class");
279
- const hostClass = classNamed !== undefined && classNamed !== "" ? classNamed : hostClassOfBoard(boardModel());
285
+ const hostClass = classNamed !== undefined && classNamed !== "" ? classNamed : hostClassOfBoard(boardModel()) ?? hostClassOfMachine(machineArch());
280
286
  if (hostClass !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(hostClass)) return { ok: false, error: `host_class ${hostClass} is not a directory name` };
281
287
  return {
282
288
  ok: true,
289
+ warnings: searchPathWarnings(search.path, hostdConf),
283
290
  settings: {
284
- lv2Dirs: once([...lv2Base, ...dirList(get("lv2_extra") ?? ZYNTHIAN_LV2_DIRS.join(":"))]),
285
- clapDirs: once([...clapBase, ...dirList(get("clap_extra") ?? "")]),
291
+ lv2Dirs: once(search.path.lv2),
292
+ clapDirs: once(search.path.clap),
286
293
  pinFile: place("pin_file", "/var/lib/omx/qualified.pins", join("omx", "qualified.pins")),
287
294
  qualificationDir: place("qualification_dir", "/var/lib/omx/qualification", join("omx", "qualification")),
288
295
  descriptorDir: place("descriptor_dir", DESCRIPTOR_DIR_SYSTEM, DESCRIPTOR_DIR_STATE_SUBDIR),
@@ -441,7 +448,7 @@ export function discoverSyncItems(
441
448
  const binary = lilv?.binary ?? (p.binary === undefined ? undefined : join(bundle, p.binary));
442
449
  const files = ["manifest.ttl", ...p.files.filter((f) => f !== "manifest.ttl")];
443
450
  // a binary named by an absolute IRI (a complement's) is tracked through the bundle's link to it, when it has one
444
- if (binary !== undefined && !files.includes(basename(binary)) && linksTo(join(bundle, basename(binary)), binary)) files.push(basename(binary));
451
+ if (binary !== undefined && !files.some((f) => iriPath(f) === basename(binary)) && linksTo(join(bundle, basename(binary)), binary)) files.push(basename(binary));
445
452
  items.push({ key: `lv2:${p.uri}`, format: "lv2", root: bundle, files, uri: p.uri, ...(binary === undefined ? {} : { binary }) });
446
453
  }
447
454
  }
@@ -481,7 +488,10 @@ export function fileSha256(path: string): string {
481
488
  export function digestFiles(root: string, files: readonly string[], known: readonly FileDigest[] = []): FileDigest[] | undefined {
482
489
  const out: FileDigest[] = [];
483
490
  for (const path of [...new Set(files)].sort()) {
484
- const full = join(root, path);
491
+ // a name may be an IRI's word (the pin keeps `libSurge%20XT.so`); the file is the path it decodes to, and a name
492
+ // that is a file as written (a .clap with a % in it) is read as written when the decoded one is not there
493
+ const decoded = join(root, iriPath(path));
494
+ const full = existsSync(decoded) ? decoded : join(root, path);
485
495
  try {
486
496
  const st = statSync(full);
487
497
  if (!st.isFile()) return undefined;
@@ -525,6 +535,7 @@ export function machineReason(p: Pick<SyncPlugin, "outcome" | "code" | "detail">
525
535
  if (p.outcome !== "unmeasured" || p.code !== SYNC_CODE.notMeasured) return undefined;
526
536
  const errno = /\b(EACCES|EPERM|ENOSPC)\b/.exec(p.detail ?? "")?.[1];
527
537
  if (errno !== undefined) return errno;
538
+ if ((p.detail ?? "").startsWith(NO_JACK_SERVER)) return NO_JACK_SERVER;
528
539
  return /wrote no document|cannot run the probe runner/.test(p.detail ?? "") ? "the measurement wrote no document" : undefined;
529
540
  }
530
541
 
@@ -622,7 +633,7 @@ export function recordedAsIs(item: SyncItem, entry: SyncEntry | undefined): { sa
622
633
  */
623
634
  export function sameBinaryElsewhere(item: SyncItem, entry: SyncEntry | undefined): SyncEntry | undefined {
624
635
  if (item.format !== "lv2" || entry === undefined || item.binary === undefined || resolve(entry.root) === resolve(item.root)) return undefined;
625
- const was = entry.files.find((f) => basename(f.path) === basename(item.binary!));
636
+ const was = entry.files.find((f) => basename(iriPath(f.path)) === basename(item.binary!));
626
637
  if (was === undefined) return undefined;
627
638
  let now: string;
628
639
  try {
@@ -681,7 +692,10 @@ export function refreshPins(ctx: SyncContext, state: SyncState): { state: SyncSt
681
692
  const pin = plugin.pin;
682
693
  if (pin === undefined || pin.integrity.files === undefined) return plugin;
683
694
  const r = hostdPinFiles(pin.uri, ctx.settings.lv2Dirs, ctx.env, ...(ctx.pinFilesRun === undefined ? [] : [ctx.pinFilesRun]));
684
- if (!r.ok) return plugin;
695
+ if (!r.ok) {
696
+ ctx.log(`${pin.uri}: pin files not refreshed: ${r.detail}`);
697
+ return plugin;
698
+ }
685
699
  const old = new Map(pin.integrity.files.map((f) => [f.path, f.sha256]));
686
700
  const changed = r.files.filter((f) => old.has(f.path) && old.get(f.path) !== f.sha256).map((f) => f.path);
687
701
  const same = r.files.length === old.size && r.files.every((f) => old.get(f.path) === f.sha256);
@@ -711,7 +725,7 @@ function outcomeOf(judgment: Judgment, host: string): { outcome: SyncOutcome; co
711
725
  return { outcome: v.rating === "unsuitable" ? "refused" : "unmeasured", ...(code === undefined ? {} : { code }) };
712
726
  }
713
727
 
714
- const safe = (text: string): string => text.replace(/[^A-Za-z0-9._-]/g, "_");
728
+ const safe = descriptorNamePart;
715
729
 
716
730
  /** The record of a plugin no probe output exists for: what it is, the bytes it was, and why nothing was measured. */
717
731
  function writeUnmeasuredRecord(ctx: SyncContext, item: SyncItem, files: readonly FileDigest[], plugin: Omit<SyncPlugin, "record">): string {
@@ -731,8 +745,19 @@ function writeUnmeasuredRecord(ctx: SyncContext, item: SyncItem, files: readonly
731
745
  return path;
732
746
  }
733
747
 
734
- /** The step's environment: each of its commands has the sync's budget. */
735
- const isolatedEnv = (ctx: SyncContext): Readonly<Record<string, string | undefined>> => ({ ...ctx.env, [ADMIT_STEP_MS_ENV]: String(ctx.settings.budgetS * 1000) });
748
+ /** The directories the sync scanned, as the plugin-hostd a step starts reads them: with neither variable set it searches
749
+ * only its own defaults, and every plugin of `lv2_extra` or of a directory the settings add is refused at add. */
750
+ export const hostdSearchEnv = (settings: Pick<SyncSettings, "lv2Dirs" | "clapDirs">): Record<string, string> => ({
751
+ LV2_PATH: settings.lv2Dirs.join(":"),
752
+ CLAP_PATH: settings.clapDirs.join(":"),
753
+ });
754
+
755
+ /** The step's environment: the sync's directories, and each of its commands has the sync's budget. */
756
+ export const isolatedEnv = (ctx: Pick<SyncContext, "env" | "settings">): Readonly<Record<string, string | undefined>> => ({
757
+ ...ctx.env,
758
+ ...hostdSearchEnv(ctx.settings),
759
+ [ADMIT_STEP_MS_ENV]: String(ctx.settings.budgetS * 1000),
760
+ });
736
761
 
737
762
  /** Each judged run of a pass as the plugins it qualified: the record written, the pin taken, the outcome. */
738
763
  function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgment: Judgment; stderr?: readonly Buffer[] }[], describeProblem?: string): SyncPlugin[] {
@@ -768,7 +793,7 @@ function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgmen
768
793
  // the pool is for a CLAP plugin that runs: the thread pool is granted by measuring it, here and nowhere else
769
794
  if (judgment.output.plugin.format === "clap" && pin !== undefined && pin.verdict !== "refused" && outcome === "qualified") {
770
795
  const runner = ctx.poolRunner ?? spawnedPoolRunner();
771
- const step = judgePoolRun(runner(pin.uri, isolatedCell(record, host), poolEnv(ctx.env, ctx.settings.budgetS)));
796
+ const step = judgePoolRun(runner(pin.uri, isolatedCell(record, host), poolEnv({ ...ctx.env, ...hostdSearchEnv(ctx.settings) }, ctx.settings.budgetS)));
772
797
  steps.push(step);
773
798
  pin = pinAfterPool(pin, step);
774
799
  }
@@ -841,7 +866,7 @@ function qualifyClap(ctx: SyncContext, item: SyncItem): SyncPlugin[] {
841
866
  return [{ uri: fileUri, outcome: "unmeasured", code: SYNC_CODE.notMeasured, detail: (e as Error).message }];
842
867
  }
843
868
  // the scanner lists the plugins of the file in a child of its own, and says how that child ended
844
- const r = spawnSync(tool, ["--json", file], { encoding: "utf8", timeout: Math.max(ctx.settings.budgetS, 35) * 1000, killSignal: "SIGKILL", maxBuffer: 64 << 20 });
869
+ const r = spawnSync(tool, ["--json", file], { encoding: "utf8", timeout: Math.max(ctx.settings.budgetS, 35) * 1000, killSignal: "SIGKILL", maxBuffer: 64 << 20, env: withRunHome(process.env) });
845
870
  let scanned;
846
871
  try {
847
872
  if (r.error !== undefined) throw r.error;
@@ -931,7 +956,9 @@ export function isolatedPass(ctx: SyncContext, state: SyncState): { readonly sta
931
956
  return rest.outcome === "qualified" && rebuilt.verdict === "refused" ? { ...rest, outcome: "refused" as const, pin: rebuilt } : { ...rest, pin: rebuilt };
932
957
  }
933
958
  ctx.log(`${p.uri}: isolated step ${done.step.result}${done.step.code === undefined ? "" : ` (${done.step.code})`}`);
934
- const pin = pinAfterStep(p.pin, done.step);
959
+ // a plugin the state holds no pin for is pinned from the record the step now completes: the bytes it names and the word
960
+ // the step earned (plugin-qualify#316: 38 plugins read hosting.memory.unmeasured and had none)
961
+ const pin = p.pin !== undefined ? pinAfterStep(p.pin, done.step) : withHostdFiles(ctx, pinFromRecordWithPool(done.record));
935
962
  const { pin: _pin, code: _code, ...rest } = p;
936
963
  if (done.step.result === ISOLATED_RESULT.isolated) {
937
964
  count.isolated++;
@@ -969,7 +996,7 @@ export function poolPass(ctx: SyncContext, state: SyncState): { readonly state:
969
996
  ctx.log(`${p.uri}: its record does not read; no pool step`);
970
997
  return p;
971
998
  }
972
- const done = completePool(path, record, p.pin.uri, isolatedCell(record, host), runner, poolEnv(ctx.env, ctx.settings.budgetS));
999
+ const done = completePool(path, record, p.pin.uri, isolatedCell(record, host), runner, poolEnv({ ...ctx.env, ...hostdSearchEnv(ctx.settings) }, ctx.settings.budgetS));
973
1000
  if (done === undefined) return p;
974
1001
  ctx.log(`${p.uri}: pool step ${done.step.result}${done.step.code === undefined ? "" : ` (${done.step.code})`}`);
975
1002
  if (done.step.result === POOL_RESULT.cores) count.cores++;
@@ -985,6 +1012,14 @@ export function poolPass(ctx: SyncContext, state: SyncState): { readonly state:
985
1012
 
986
1013
  // --- the distributed records ----------------------------------------------------------------------------------------
987
1014
 
1015
+ /**
1016
+ * The cells a sync runs at, which a distributed set must hold to apply (#317): the grid's own cells when it names them
1017
+ * (`grid 48000/512`), else the cells the host profile judges cost and memory at.
1018
+ */
1019
+ export function syncCells(settings: Pick<SyncSettings, "grid" | "hostProfile">): GridCell[] {
1020
+ return settings.grid.kind === "cells" ? [...settings.grid.cells] : hostCells(settings.hostProfile);
1021
+ }
1022
+
988
1023
  /** The layer a run reads: none under `--force` or `--no-distributed`, or without a host class. */
989
1024
  function distributedLayerOf(settings: SyncSettings, force: boolean, log: (line: string) => void): DistributedLayer {
990
1025
  if (force || !settings.distributed) return EMPTY_LAYER;
@@ -1007,12 +1042,10 @@ function takeDistributed(ctx: SyncContext, layer: DistributedLayer, item: SyncIt
1007
1042
  if (layer.records === 0) return undefined;
1008
1043
  const started = Date.now();
1009
1044
  const host = ctx.settings.hostProfile.id;
1010
- const match = matchDistributed(layer, item, host, (paths) => digestFiles(item.root, paths, files));
1045
+ const match = matchDistributed(layer, item, host, (paths) => digestFiles(item.root, paths, files), syncCells(ctx.settings));
1011
1046
  if (match.status === "none") return undefined;
1012
- if (match.status === "ignored") {
1013
- for (const line of match.lines) ctx.log(line);
1014
- return undefined;
1015
- }
1047
+ for (const line of match.lines) ctx.log(line);
1048
+ if (match.status === "ignored") return undefined;
1016
1049
  for (const r of match.records) {
1017
1050
  const missing = missingSteps(r.record);
1018
1051
  if (missing.length > 0) {
@@ -1067,7 +1100,7 @@ function scanClapDescriptors(ctx: SyncContext, item: SyncItem): void {
1067
1100
  ctx.log(`${file}: descriptors not written: ${(e as Error).message}`);
1068
1101
  return;
1069
1102
  }
1070
- const r = spawnSync(tool, ["--json", file], { encoding: "utf8", timeout: Math.max(ctx.settings.budgetS, 35) * 1000, killSignal: "SIGKILL", maxBuffer: 64 << 20 });
1103
+ const r = spawnSync(tool, ["--json", file], { encoding: "utf8", timeout: Math.max(ctx.settings.budgetS, 35) * 1000, killSignal: "SIGKILL", maxBuffer: 64 << 20, env: withRunHome(process.env) });
1071
1104
  if (r.error !== undefined || r.status !== 0) {
1072
1105
  ctx.log(`${file}: descriptors not written: the scan ended ${r.error?.message ?? `with exit ${String(r.status)}`}`);
1073
1106
  return;