@a11ign/screenreader-fleet 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/doctor.d.mts CHANGED
@@ -91,30 +91,22 @@ export function resolvesToThisCheckout(resolvedRealPath: string, thisCheckoutRoo
91
91
  */
92
92
  export function checkoutRootFor(resolvedRealPath: string): string | null;
93
93
  /**
94
- * Whether `tsc --build --dry` -- the SAME authority the real build uses, deferred to rather than
95
- * reimplemented -- considers `tsconfigPath`'s own outputs up to date.
96
- *
97
- * NOT a timestamp comparison, and that correction cost a wrong first version of this check (#256, live-
98
- * measured): a directory's mtime does not move on rewrite; a file's mtime moves on `git checkout` with no
99
- * content change at all, which is the ordinary case of switching branches; and `tsc --build` itself is
100
- * content-addressed for the files it actually recompiles, so a real build can be legitimately up to date
101
- * with source files whose mtimes are newer than its outputs. Measured on two live worktrees straight
102
- * after a branch switch: several source files newer than `dist/index.js`, and `tsc --build --dry` still
103
- * correctly reported "is up to date". A raw mtime comparison would have flagged both as stale --
104
- * permanently, on every worktree, the moment `git checkout` runs -- which is exactly the "readiness
105
- * command that cries wolf" this file's own `advise` doc warns against.
106
- *
107
- * `null`, not `false`, when the run failed outright or its report never mentioned this project at all --
108
- * "could not tell" and "not up to date" need opposite responses (investigate vs. rebuild), and this repo's
109
- * own rule is that a lookup failure is never silently read as a clean answer.
110
- *
111
- * @param {string} tsconfigPath
112
- * @param {{ run?: (cmd: string, args: string[]) => string }} [deps]
113
- * @returns {boolean | null}
94
+ * The `exports` targets of `packageDir` that are NOT on disk, or `null` when the package's manifest could not be
95
+ * read or declares none -- "could not tell" and "missing" need opposite responses (investigate vs. rebuild), and a
96
+ * lookup failure is never read as a clean answer.
97
+ *
98
+ * WHY EXISTENCE AND NOT FRESHNESS. This asked `tsc --build --dry` whether the package's outputs were current, and
99
+ * a11ign builds `judge` with Rslib now (a11ign/a11ign#3580): its `tsconfig.json` is `noEmit`, so tsc has no output to
100
+ * be up to date about and answered wrongly on every run (a11ign/a11ign#3810). Rslib writes no stamp of the source it
101
+ * built from, and a timestamp comparison is the wrong first version this check once had (#256: `git checkout` moves
102
+ * source mtimes with no content change, so every worktree would read stale the moment it switched branches). So what
103
+ * is read is what Rslib's output actually IS: the files `exports` promises. A present but out-of-date `dist` is
104
+ * therefore NOT caught here, and the check says "present", never "up to date".
105
+ *
106
+ * @param {string} packageDir
107
+ * @returns {string[] | null}
114
108
  */
115
- export function tscProjectUpToDate(tsconfigPath: string, { run }?: {
116
- run?: (cmd: string, args: string[]) => string;
117
- }): boolean | null;
109
+ export function missingExportTargets(packageDir: string): string[] | null;
118
110
  /**
119
111
  * WHOSE dist a cross-package import actually resolves to, and is IT stale (#256) -- both computed from
120
112
  * the exact SPECIFIER a real import site in this repo uses, never the bare package name. CLAUDE.md's own
package/dist/doctor.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile, execFileSync } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
- import { existsSync as external_node_fs_existsSync, readFileSync, realpathSync as external_node_fs_realpathSync, statSync } from "node:fs";
5
- import { delimiter as external_node_path_delimiter, dirname as external_node_path_dirname, join as external_node_path_join, resolve } from "node:path";
4
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  import { homedir } from "node:os";
8
8
  import { createRequire } from "node:module";
@@ -26,51 +26,6 @@ function controlPlaneIsolation({ hasNodeModules, hasFleetKey, packages, isWorksp
26
26
  why: `ADR 0012 VIOLATED: node_modules${count} sits beside the fleet SSH key. A compromised transitive dependency here can reach the key that reconfigures every Windows worker, each of which auto-logs-in to an unlocked desktop. ${remedy}`
27
27
  };
28
28
  }
29
- function pnpmCliInvocation(args) {
30
- const fromParent = process.env.npm_execpath ?? "";
31
- if (/pnpm\.c?js$/.test(fromParent) && external_node_fs_existsSync(fromParent)) return {
32
- command: process.execPath,
33
- args: [
34
- fromParent,
35
- ...args
36
- ]
37
- };
38
- const shim = onPath("pnpm");
39
- if (null !== shim) return shimInvocation(shim, external_node_path_join("node_modules", "pnpm", "bin", "pnpm.cjs"), args);
40
- const corepack = onPath("corepack");
41
- if (null !== corepack) return shimInvocation(corepack, external_node_path_join("node_modules", "corepack", "dist", "corepack.js"), [
42
- "pnpm",
43
- ...args
44
- ]);
45
- throw new Error("could not find pnpm: `npm_execpath` is not a pnpm script, and neither `pnpm` nor `corepack` is on PATH. `packageManager` in the root package.json names the version -- `corepack enable` or `pnpm/action-setup` provides it.");
46
- }
47
- function onPath(name) {
48
- for (const dir of (process.env.PATH ?? "").split(external_node_path_delimiter))if ("" !== dir) {
49
- for (const candidate of [
50
- external_node_path_join(dir, name),
51
- external_node_path_join(dir, `${name}.cmd`)
52
- ])if (external_node_fs_existsSync(candidate)) return candidate;
53
- }
54
- return null;
55
- }
56
- function shimInvocation(shim, script, args) {
57
- if (!shim.endsWith(".cmd")) return {
58
- command: shim,
59
- args
60
- };
61
- const found = [
62
- external_node_path_join(external_node_path_dirname(shim), script),
63
- external_node_path_join(external_node_path_dirname(process.execPath), script)
64
- ].find((path)=>external_node_fs_existsSync(path));
65
- if (void 0 === found) throw new Error(`${shim} is a .cmd shim and its script ${script} is not beside it or beside node`);
66
- return {
67
- command: process.execPath,
68
- args: [
69
- found,
70
- ...args
71
- ]
72
- };
73
- }
74
29
  refuseUnknownFlags([
75
30
  "--json",
76
31
  "--runs-dir="
@@ -78,14 +33,14 @@ refuseUnknownFlags([
78
33
  entry: import.meta.url,
79
34
  command: "pnpm run doctor"
80
35
  });
81
- const doctor_run = promisify(execFile);
36
+ const run = promisify(execFile);
82
37
  const JSON_OUT = process.argv.includes("--json");
83
38
  const REFUSED = 2;
84
39
  const WORKERS_ENV = configuredWorkers();
85
40
  const PAGES_PORT = Number(process.env.DATASET_PAGES_PORT || 5050);
86
41
  const CTL = fleetScriptPaths().workerCtl;
87
42
  const MODULE_DIR = fileURLToPath(new URL(".", import.meta.url));
88
- const isMonorepoRoot = (root)=>external_node_fs_existsSync(resolve(root, "packages")) && external_node_fs_existsSync(resolve(root, "package.json"));
43
+ const isMonorepoRoot = (root)=>existsSync(resolve(root, "packages")) && existsSync(resolve(root, "package.json"));
89
44
  function checkoutLayout(baseDir = MODULE_DIR) {
90
45
  const root = resolve(baseDir, "..", "..", "..");
91
46
  return {
@@ -99,7 +54,7 @@ const notACheckout = (names, root)=>{
99
54
  function runsDirFor({ argv = process.argv.slice(2), baseDir = MODULE_DIR } = {}) {
100
55
  const supplied = flagValue(argv, "runs-dir");
101
56
  if (void 0 !== supplied) {
102
- if (external_node_fs_existsSync(supplied)) return resolve(supplied);
57
+ if (existsSync(supplied)) return resolve(supplied);
103
58
  throw new Error(`doctor: --runs-dir=${supplied} does not exist. Pass --runs-dir=<the runs directory>.`);
104
59
  }
105
60
  const root = resolve(baseDir, "..", "..", "..");
@@ -169,7 +124,7 @@ function workerControlFix(observed) {
169
124
  fix: `${CTL} pool`,
170
125
  note: "UTM has no registered worker VM; re-register the existing a11y-worker*.utm bundles in UTM first"
171
126
  };
172
- return external_node_fs_existsSync("/Applications/UTM.app") ? {
127
+ return existsSync("/Applications/UTM.app") ? {
173
128
  fix: `${CTL} pool`,
174
129
  note: "unlock the Mac first if it is locked"
175
130
  } : {
@@ -178,7 +133,7 @@ function workerControlFix(observed) {
178
133
  };
179
134
  }
180
135
  async function shell(cmd, args, timeout = 30000) {
181
- const { stdout } = await doctor_run(cmd, args, {
136
+ const { stdout } = await run(cmd, args, {
182
137
  timeout,
183
138
  encoding: "utf8"
184
139
  });
@@ -229,8 +184,8 @@ function checkControlPlaneIsolation({ baseDir = MODULE_DIR } = {}) {
229
184
  ], root);
230
185
  const raw = process.env.A11Y_SSH_KEY || "~/.ssh/a11y-witness_ed25519";
231
186
  const keyPath = raw.startsWith("~/") ? resolve(homedir(), raw.slice(2)) : raw;
232
- const hasFleetKey = external_node_fs_existsSync(keyPath);
233
- const hasNodeModules = external_node_fs_existsSync(resolve(root, "node_modules"));
187
+ const hasFleetKey = existsSync(keyPath);
188
+ const hasNodeModules = existsSync(resolve(root, "node_modules"));
234
189
  const verdict = controlPlaneIsolation({
235
190
  hasNodeModules,
236
191
  hasFleetKey,
@@ -246,27 +201,23 @@ function checkoutRootFor(resolvedRealPath) {
246
201
  const idx = resolvedRealPath.indexOf("/packages/");
247
202
  return -1 === idx ? null : resolvedRealPath.slice(0, idx);
248
203
  }
249
- const defaultTscRun = (cmd, args)=>execFileSync(cmd, args, {
250
- encoding: "utf8"
251
- });
252
- function tscProjectUpToDate(tsconfigPath, { run = defaultTscRun } = {}) {
253
- let output;
204
+ function exportTargets(exportsField) {
205
+ if ("string" == typeof exportsField) return [
206
+ exportsField
207
+ ];
208
+ if (null === exportsField || "object" != typeof exportsField) return [];
209
+ return Object.values(exportsField).flatMap(exportTargets);
210
+ }
211
+ function missingExportTargets(packageDir) {
212
+ let manifest;
254
213
  try {
255
- const tsc = pnpmCliInvocation([
256
- "exec",
257
- "tsc",
258
- "--build",
259
- "--dry",
260
- tsconfigPath
261
- ]);
262
- output = run(tsc.command, tsc.args);
263
- } catch (error) {
264
- output = error?.stdout ?? "";
265
- if (!output) return null;
214
+ manifest = JSON.parse(readFileSync(resolve(packageDir, "package.json"), "utf8"));
215
+ } catch {
216
+ return null;
266
217
  }
267
- const line = output.split("\n").find((l)=>l.includes(tsconfigPath));
268
- if (!line) return null;
269
- return /is up to date/.test(line);
218
+ const targets = exportTargets(manifest?.exports);
219
+ if (0 === targets.length) return null;
220
+ return targets.filter((target)=>!existsSync(resolve(packageDir, target)));
270
221
  }
271
222
  function behindOriginMainNote(otherRoot) {
272
223
  if (!otherRoot) return "";
@@ -294,25 +245,25 @@ function checkCrossPackageDist({ baseDir = MODULE_DIR } = {}) {
294
245
  ], thisCheckoutRoot);
295
246
  let resolvedRealPath;
296
247
  try {
297
- resolvedRealPath = external_node_fs_realpathSync(createRequire(import.meta.url).resolve(specifier));
248
+ resolvedRealPath = realpathSync(createRequire(import.meta.url).resolve(specifier));
298
249
  } catch (error) {
299
250
  return advise("dist-resolution", `could not resolve ${specifier} to check whose dist it comes from -- ${error.message}`, "pnpm run build");
300
251
  }
301
- if (resolvesToThisCheckout(resolvedRealPath, external_node_fs_realpathSync(thisCheckoutRoot))) add("dist-resolution", true, `${specifier} resolves to this checkout's own dist`);
252
+ if (resolvesToThisCheckout(resolvedRealPath, realpathSync(thisCheckoutRoot))) add("dist-resolution", true, `${specifier} resolves to this checkout's own dist`);
302
253
  else {
303
254
  const behindNote = behindOriginMainNote(checkoutRootFor(resolvedRealPath));
304
255
  advise("dist-resolution", `${specifier} resolves to ${resolvedRealPath} (NOT this checkout${behindNote})`, "pnpm run primary:update && pnpm run build # if that is the primary checkout");
305
256
  }
306
257
  const distRoot = checkoutRootFor(resolvedRealPath) ?? thisCheckoutRoot;
307
- const tsconfigPath = resolve(distRoot, "packages/judge/tsconfig.json");
308
- const upToDate = tscProjectUpToDate(tsconfigPath);
309
- if (null === upToDate) return advise("dist-freshness", `could not ask tsc whether ${tsconfigPath} is up to date`, "pnpm run build");
310
- if (upToDate) return add("dist-freshness", true, `packages/judge under ${distRoot} is up to date (tsc --build --dry)`);
311
- advise("dist-freshness", `packages/judge under ${distRoot} is NOT up to date (tsc --build --dry) -- a build compiled before the source it now reflects`, "pnpm run build # in that checkout");
258
+ const judgeDir = resolve(distRoot, "packages/judge");
259
+ const missing = missingExportTargets(judgeDir);
260
+ if (null === missing) return advise("dist-freshness", `could not read the exports of ${judgeDir}/package.json to see whether its dist is built`, "pnpm run build");
261
+ if (0 === missing.length) return add("dist-freshness", true, `every exports target of packages/judge under ${distRoot} is present (existence only: Rslib leaves no stamp to read freshness from)`);
262
+ advise("dist-freshness", `packages/judge under ${distRoot} is missing ${missing.length} exports target(s): ${missing.join(", ")}`, "pnpm run build # in that checkout");
312
263
  }
313
264
  async function scorerWeightsFor({ from = import.meta.url } = {}) {
314
- const judgeEntry = external_node_fs_realpathSync(createRequire(from).resolve("@a11ign/judge"));
315
- const scorerEntry = external_node_fs_realpathSync(createRequire(judgeEntry).resolve("@a11ign/scorer"));
265
+ const judgeEntry = realpathSync(createRequire(from).resolve("@a11ign/judge"));
266
+ const scorerEntry = realpathSync(createRequire(judgeEntry).resolve("@a11ign/scorer"));
316
267
  const { scorerPaths } = await import(pathToFileURL(scorerEntry).href);
317
268
  return scorerPaths().weights;
318
269
  }
@@ -327,7 +278,7 @@ async function checkJudge({ from = import.meta.url } = {}) {
327
278
  } catch (error) {
328
279
  return add("judge", false, `backend=local, but @a11ign/scorer could not be resolved from @a11ign/judge: ${error.message}`, "install @a11ign/scorer (a peer dependency of @a11ign/judge) beside @a11ign/judge");
329
280
  }
330
- return add("judge", external_node_fs_existsSync(weights), external_node_fs_existsSync(weights) ? "backend=local, trained scorer present" : "backend=local, but the trained scorer is missing", `expected weights at ${weights} (where @a11ign/scorer says they are) — reinstall @a11ign/scorer`);
281
+ return add("judge", existsSync(weights), existsSync(weights) ? "backend=local, trained scorer present" : "backend=local, but the trained scorer is missing", `expected weights at ${weights} (where @a11ign/scorer says they are) — reinstall @a11ign/scorer`);
331
282
  }
332
283
  if ("anthropic" === backend || "openai" === backend) {
333
284
  const key = "anthropic" === backend ? "ANTHROPIC_API_KEY" : "JUDGE_BASE_URL";
@@ -339,7 +290,7 @@ async function checkWorker() {
339
290
  if (WORKERS_ENV.length) return checkConfiguredFleet(WORKERS_ENV);
340
291
  const inventory = namedInventoryWorkers();
341
292
  if (inventory.length) return checkConfiguredFleet(inventory);
342
- if ("darwin" !== process.platform || !external_node_fs_existsSync(CTL)) return add("worker", false, "no A11Y_WORKERS set, no inventory.yml fleet, and no local VM tooling here", "set A11Y_WORKERS=http://host:8765[,http://host2:8765], or see docs/getting-started.md");
293
+ if ("darwin" !== process.platform || !existsSync(CTL)) return add("worker", false, "no A11Y_WORKERS set, no inventory.yml fleet, and no local VM tooling here", "set A11Y_WORKERS=http://host:8765[,http://host2:8765], or see docs/getting-started.md");
343
294
  let pool;
344
295
  try {
345
296
  pool = JSON.parse(await shell(CTL, [
@@ -466,7 +417,7 @@ function checkHostCapacity(pool) {
466
417
  }
467
418
  async function checkDatasetPages() {
468
419
  const manifestPath = resolve(datasetDir(), "manifest.json");
469
- if (!external_node_fs_existsSync(manifestPath)) return add("dataset", false, "no manifest — the dataset has not been generated", "pnpm run training:generate");
420
+ if (!existsSync(manifestPath)) return add("dataset", false, "no manifest — the dataset has not been generated", "pnpm run training:generate");
470
421
  const sample = JSON.parse(readFileSync(manifestPath, "utf8")).cases?.[0]?.id;
471
422
  const probe = sample ? `${sample}/good.html` : "";
472
423
  try {
@@ -481,7 +432,7 @@ async function checkDatasetPages() {
481
432
  }
482
433
  function checkRunState() {
483
434
  const progress = resolve(datasetDir(), "capture-progress.json");
484
- if (!external_node_fs_existsSync(progress)) return add("run", true, "no capture run recorded");
435
+ if (!existsSync(progress)) return add("run", true, "no capture run recorded");
485
436
  const p = JSON.parse(readFileSync(progress, "utf8"));
486
437
  if (!p.startedAt) return add("run", true, "no capture run recorded");
487
438
  if (!p.finishedAt) return add("run", false, `a run is UNFINISHED (started ${p.startedAt})`, "pnpm run training:wait, or pnpm run training:capture -- --resume --no-cache");
@@ -563,5 +514,5 @@ function renderDoctor({ json, out }) {
563
514
  async function main() {
564
515
  process.exit(await doctorRun());
565
516
  }
566
- if (import.meta.url === pathToFileURL(process.argv[1] ? external_node_fs_realpathSync(process.argv[1]) : "").href) await main();
567
- export { addCheck, allChecks, checkControlPlaneIsolation, checkCrossPackageDist, checkJudge, checkPrimaryCheckoutMark, checkoutRootFor, doctorRun, errorDocument, fleetAgreementLine, gatingChecks, isRunnableCommand, nextCommand, readyFrom, recordedChecks, resolvesToThisCheckout, runsDirFor, scorerWeightsFor, tscProjectUpToDate, workerControlFix };
517
+ if (import.meta.url === pathToFileURL(process.argv[1] ? realpathSync(process.argv[1]) : "").href) await main();
518
+ export { addCheck, allChecks, checkControlPlaneIsolation, checkCrossPackageDist, checkJudge, checkPrimaryCheckoutMark, checkoutRootFor, doctorRun, errorDocument, fleetAgreementLine, gatingChecks, isRunnableCommand, missingExportTargets, nextCommand, readyFrom, recordedChecks, resolvesToThisCheckout, runsDirFor, scorerWeightsFor, workerControlFix };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a11ign/screenreader-fleet",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Host-side lifecycle, health and capacity for a fleet of Windows NVDA capture workers: lease one, judge whether it is degrading, and know how many the host can afford.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -73,7 +73,7 @@
73
73
  },
74
74
  "devDependencies": {
75
75
  "@a11ign/evidence": "0.1.0",
76
- "@a11ign/toolchain": "0.1.2",
76
+ "@a11ign/toolchain": "0.1.4",
77
77
  "@rslib/core": "1.0.3",
78
78
  "typescript": "^6.0.3",
79
79
  "yaml": "^2.9.1"