@a11ign/screenreader-fleet 0.4.1 → 0.4.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 +82 -0
- package/dist/doctor.mjs +45 -13
- package/package.json +1 -1
package/dist/doctor.d.mts
CHANGED
|
@@ -31,6 +31,45 @@ export function workerControlFix(observed: string): {
|
|
|
31
31
|
fix: string | null;
|
|
32
32
|
note: string | null;
|
|
33
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* IS THE FLEET KEY SITTING NEXT TO 100 MB OF PACKAGES NOBODY AUDITED? — ADR 0012, checked rather than
|
|
36
|
+
* asserted.
|
|
37
|
+
*
|
|
38
|
+
* Found on 2026-08-29 to be violated on BOTH machines the ADR is about: the control plane carried 56 MB
|
|
39
|
+
* and 121 packages beside the key, and this laptop carries 103 MB beside the same key plus the lab key.
|
|
40
|
+
* The document was accurate about the intent and described a system that did not exist — which is worse
|
|
41
|
+
* than no document, because it is read as a guarantee.
|
|
42
|
+
*
|
|
43
|
+
* Reported by `doctor` because that is the command whose whole promise is that every check names its own
|
|
44
|
+
* fix, and because a check nobody runs is one this repo has learned not to write.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* IS THIS CHECKOUT MARKED AS THE PRIMARY, and does that match what it looks like?
|
|
48
|
+
*
|
|
49
|
+
* The primary-checkout guards (`pre-commit`, `post-checkout`) are OPT-IN as of #198: they fire only where
|
|
50
|
+
* `git config --local a11y.primaryCheckout` is `true`. That is the correct default — inferring it from
|
|
51
|
+
* `.git` being a directory made the hook fire on the lab, which is an ordinary clone, and broke every
|
|
52
|
+
* `lab:job -e ref=<branch>`.
|
|
53
|
+
*
|
|
54
|
+
* But an opt-in guard nobody can find the switch for is an OFF guard, and "unmarked" must not read the
|
|
55
|
+
* same as "safe". So this reports the state on every run rather than only when something is wrong — the
|
|
56
|
+
* `isolation` check above takes the same shape for the same reason: a debt that is reported every run is
|
|
57
|
+
* a known one, and a debt reported never is a forgotten one.
|
|
58
|
+
*
|
|
59
|
+
* ADVISORY, never a hard failure. `doctor` exits 0 when a RUN can proceed, and an unmarked checkout can
|
|
60
|
+
* run perfectly well — it is the fleet-driving machine's protection that is missing, not its capability.
|
|
61
|
+
* A doctor that refused READY over this would be ignored, which is how a guard gets switched off.
|
|
62
|
+
*
|
|
63
|
+
* It does not GUESS which machine deserves the mark. `doctor` runs on laptops, worktrees, the lab and CI,
|
|
64
|
+
* and telling four of those five to mark themselves would be the #198 defect wearing an advisory's
|
|
65
|
+
* clothes. It states what is true and names the command; the operator decides.
|
|
66
|
+
*/
|
|
67
|
+
export function checkPrimaryCheckoutMark({ baseDir }?: {
|
|
68
|
+
baseDir?: string | undefined;
|
|
69
|
+
}): void;
|
|
70
|
+
export function checkControlPlaneIsolation({ baseDir }?: {
|
|
71
|
+
baseDir?: string | undefined;
|
|
72
|
+
}): void;
|
|
34
73
|
/**
|
|
35
74
|
* Pure: does `resolvedRealPath` (already realpath'd) live under `thisCheckoutRoot` (also realpath'd)?
|
|
36
75
|
* Both must be realpath'd BEFORE calling this, never inside it -- comparing a symlinked path against a
|
|
@@ -76,6 +115,40 @@ export function checkoutRootFor(resolvedRealPath: string): string | null;
|
|
|
76
115
|
export function tscProjectUpToDate(tsconfigPath: string, { run }?: {
|
|
77
116
|
run?: (cmd: string, args: string[]) => string;
|
|
78
117
|
}): boolean | null;
|
|
118
|
+
/**
|
|
119
|
+
* WHOSE dist a cross-package import actually resolves to, and is IT stale (#256) -- both computed from
|
|
120
|
+
* the exact SPECIFIER a real import site in this repo uses, never the bare package name. CLAUDE.md's own
|
|
121
|
+
* recorded lesson: "resolving @a11ign/judge does not prove @a11ign/judge/rules came from your
|
|
122
|
+
* tree" -- a package can export subpaths from elsewhere, so resolving the root proves nothing about a
|
|
123
|
+
* subpath. `@a11ign/judge/rules` is a real specifier this repo imports
|
|
124
|
+
* (`packages/lab/scripts/score-rules.ts` and others), not a synthetic probe.
|
|
125
|
+
*
|
|
126
|
+
* ADVISORY, never a hard failure -- same reasoning as `isolation` above: a worktree resolving to the
|
|
127
|
+
* primary's dist can still run every command correctly today, and a doctor that refused READY over an
|
|
128
|
+
* environmental fact would be ignored, which is how a guard gets switched off. It is reported every run
|
|
129
|
+
* so a stale answer is a known condition, not a silent one.
|
|
130
|
+
*/
|
|
131
|
+
export function checkCrossPackageDist({ baseDir }?: {
|
|
132
|
+
baseDir?: string | undefined;
|
|
133
|
+
}): number | void;
|
|
134
|
+
/**
|
|
135
|
+
* Where the trained scorer's weights are, asked of `@a11ign/scorer` itself (a11ign/a11ign#3784).
|
|
136
|
+
*
|
|
137
|
+
* This was `../../scorer/models/...` from this module: the monorepo layout, which from an installed package points into
|
|
138
|
+
* `node_modules/@a11ign/` and answers "missing" under pnpm, where the scorer sits beside `@a11ign/judge` in the store and
|
|
139
|
+
* not beside this package. The scorer is a PEER of the judge, so it is resolved FROM the judge's real path, the one place
|
|
140
|
+
* both layouts agree it is reachable, and its own `scorerPaths()` says where its weights are ("the weights are the API").
|
|
141
|
+
* `from` is INJECTED so a test can stand in for either layout.
|
|
142
|
+
*
|
|
143
|
+
* @param {{ from?: string }} [options] a path or file URL to resolve `@a11ign/judge` from
|
|
144
|
+
* @returns {Promise<string>}
|
|
145
|
+
*/
|
|
146
|
+
export function scorerWeightsFor({ from }?: {
|
|
147
|
+
from?: string;
|
|
148
|
+
}): Promise<string>;
|
|
149
|
+
export function checkJudge({ from }?: {
|
|
150
|
+
from?: string | undefined;
|
|
151
|
+
}): Promise<void>;
|
|
79
152
|
/**
|
|
80
153
|
* The agreement sentence, with WHICH FIELDS AGREED DERIVED rather than retyped — #1997.
|
|
81
154
|
*
|
|
@@ -225,6 +298,15 @@ export function doctorRun(deps?: {
|
|
|
225
298
|
err?: (line: string) => void;
|
|
226
299
|
runsDir?: () => string;
|
|
227
300
|
}): Promise<number>;
|
|
301
|
+
export function recordedChecks(): {
|
|
302
|
+
name: string;
|
|
303
|
+
id: string;
|
|
304
|
+
ok: boolean;
|
|
305
|
+
detail: string;
|
|
306
|
+
fix: string | null;
|
|
307
|
+
note?: string | null;
|
|
308
|
+
advisory?: boolean;
|
|
309
|
+
}[];
|
|
228
310
|
export function addCheck(name: string, ok: boolean, detail: string, remedy?: string | null | {
|
|
229
311
|
fix: string | null;
|
|
230
312
|
note?: string | null;
|
package/dist/doctor.mjs
CHANGED
|
@@ -84,9 +84,18 @@ const REFUSED = 2;
|
|
|
84
84
|
const WORKERS_ENV = configuredWorkers();
|
|
85
85
|
const PAGES_PORT = Number(process.env.DATASET_PAGES_PORT || 5050);
|
|
86
86
|
const CTL = fleetScriptPaths().workerCtl;
|
|
87
|
-
const SCORER_MODEL_DIR = fileURLToPath(new URL("../../scorer/models/screenreader-scorer/", import.meta.url));
|
|
88
87
|
const MODULE_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
89
88
|
const isMonorepoRoot = (root)=>external_node_fs_existsSync(resolve(root, "packages")) && external_node_fs_existsSync(resolve(root, "package.json"));
|
|
89
|
+
function checkoutLayout(baseDir = MODULE_DIR) {
|
|
90
|
+
const root = resolve(baseDir, "..", "..", "..");
|
|
91
|
+
return {
|
|
92
|
+
root,
|
|
93
|
+
checkout: isMonorepoRoot(root)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const notACheckout = (names, root)=>{
|
|
97
|
+
for (const name of names)advise(name, `n/a: installed package -- ${root} is not a checkout, so there is nothing here for this check to read`);
|
|
98
|
+
};
|
|
90
99
|
function runsDirFor({ argv = process.argv.slice(2), baseDir = MODULE_DIR } = {}) {
|
|
91
100
|
const supplied = flagValue(argv, "runs-dir");
|
|
92
101
|
if (void 0 !== supplied) {
|
|
@@ -100,6 +109,7 @@ function runsDirFor({ argv = process.argv.slice(2), baseDir = MODULE_DIR } = {})
|
|
|
100
109
|
const datasetDir = ()=>resolve(runsDirFor(), "screenreader-dataset");
|
|
101
110
|
const PROBE_TIMEOUT_MS = 8000;
|
|
102
111
|
const checks = [];
|
|
112
|
+
const recordedChecks = ()=>checks;
|
|
103
113
|
const GATES = Object.freeze({
|
|
104
114
|
worker: true,
|
|
105
115
|
fleet: true,
|
|
@@ -182,8 +192,11 @@ async function httpJson(url) {
|
|
|
182
192
|
if (void 0 === response.json) throw new Error(`invalid JSON from ${url}`);
|
|
183
193
|
return response.json;
|
|
184
194
|
}
|
|
185
|
-
function checkPrimaryCheckoutMark() {
|
|
186
|
-
const root
|
|
195
|
+
function checkPrimaryCheckoutMark({ baseDir = MODULE_DIR } = {}) {
|
|
196
|
+
const { root, checkout } = checkoutLayout(baseDir);
|
|
197
|
+
if (!checkout) return notACheckout([
|
|
198
|
+
"primary checkout"
|
|
199
|
+
], root);
|
|
187
200
|
let linked;
|
|
188
201
|
try {
|
|
189
202
|
linked = !statSync(resolve(root, ".git")).isDirectory();
|
|
@@ -209,17 +222,19 @@ function checkPrimaryCheckoutMark() {
|
|
|
209
222
|
if (marked) return add("primary checkout", true, "MARKED — pre-commit refuses commits here and post-checkout keeps it detached at origin/main");
|
|
210
223
|
advise("primary checkout", "not marked, so the primary-checkout guards are INERT here. Correct for the lab, a worker or a colleague's clone; wrong for the machine that drives the fleet.", "pnpm run primary:mark -- --set (only on the fleet-driving checkout — see docs/primary-checkout.md)");
|
|
211
224
|
}
|
|
212
|
-
function checkControlPlaneIsolation() {
|
|
225
|
+
function checkControlPlaneIsolation({ baseDir = MODULE_DIR } = {}) {
|
|
226
|
+
const { root, checkout } = checkoutLayout(baseDir);
|
|
227
|
+
if (!checkout) return notACheckout([
|
|
228
|
+
"isolation"
|
|
229
|
+
], root);
|
|
213
230
|
const raw = process.env.A11Y_SSH_KEY || "~/.ssh/a11y-witness_ed25519";
|
|
214
231
|
const keyPath = raw.startsWith("~/") ? resolve(homedir(), raw.slice(2)) : raw;
|
|
215
232
|
const hasFleetKey = external_node_fs_existsSync(keyPath);
|
|
216
|
-
const root = resolve(fileURLToPath(new URL(".", import.meta.url)), "..", "..", "..");
|
|
217
233
|
const hasNodeModules = external_node_fs_existsSync(resolve(root, "node_modules"));
|
|
218
|
-
const isWorkspace = external_node_fs_existsSync(resolve(root, "packages")) && external_node_fs_existsSync(resolve(root, "package.json"));
|
|
219
234
|
const verdict = controlPlaneIsolation({
|
|
220
235
|
hasNodeModules,
|
|
221
236
|
hasFleetKey,
|
|
222
|
-
isWorkspace
|
|
237
|
+
isWorkspace: checkout
|
|
223
238
|
});
|
|
224
239
|
if (!verdict.violated) return add("isolation", true, verdict.why);
|
|
225
240
|
advise("isolation", verdict.why, "docs/control-plane-plan.md L3 — drive the control plane rather than holding its keys");
|
|
@@ -270,9 +285,13 @@ function behindOriginMainNote(otherRoot) {
|
|
|
270
285
|
return "";
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
|
-
function checkCrossPackageDist() {
|
|
288
|
+
function checkCrossPackageDist({ baseDir = MODULE_DIR } = {}) {
|
|
274
289
|
const specifier = "@a11ign/judge/rules";
|
|
275
|
-
const
|
|
290
|
+
const { root: thisCheckoutRoot, checkout } = checkoutLayout(baseDir);
|
|
291
|
+
if (!checkout) return notACheckout([
|
|
292
|
+
"dist-resolution",
|
|
293
|
+
"dist-freshness"
|
|
294
|
+
], thisCheckoutRoot);
|
|
276
295
|
let resolvedRealPath;
|
|
277
296
|
try {
|
|
278
297
|
resolvedRealPath = external_node_fs_realpathSync(createRequire(import.meta.url).resolve(specifier));
|
|
@@ -291,11 +310,24 @@ function checkCrossPackageDist() {
|
|
|
291
310
|
if (upToDate) return add("dist-freshness", true, `packages/judge under ${distRoot} is up to date (tsc --build --dry)`);
|
|
292
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");
|
|
293
312
|
}
|
|
294
|
-
async function
|
|
313
|
+
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"));
|
|
316
|
+
const { scorerPaths } = await import(pathToFileURL(scorerEntry).href);
|
|
317
|
+
return scorerPaths().weights;
|
|
318
|
+
}
|
|
319
|
+
async function checkJudge({ from = import.meta.url } = {}) {
|
|
295
320
|
const backend = (process.env.JUDGE_BACKEND || "local").toLowerCase();
|
|
296
321
|
if ("local" === backend) {
|
|
297
|
-
|
|
298
|
-
|
|
322
|
+
let weights;
|
|
323
|
+
try {
|
|
324
|
+
weights = await scorerWeightsFor({
|
|
325
|
+
from
|
|
326
|
+
});
|
|
327
|
+
} catch (error) {
|
|
328
|
+
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
|
+
}
|
|
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`);
|
|
299
331
|
}
|
|
300
332
|
if ("anthropic" === backend || "openai" === backend) {
|
|
301
333
|
const key = "anthropic" === backend ? "ANTHROPIC_API_KEY" : "JUDGE_BASE_URL";
|
|
@@ -532,4 +564,4 @@ async function main() {
|
|
|
532
564
|
process.exit(await doctorRun());
|
|
533
565
|
}
|
|
534
566
|
if (import.meta.url === pathToFileURL(process.argv[1] ? external_node_fs_realpathSync(process.argv[1]) : "").href) await main();
|
|
535
|
-
export { addCheck, allChecks, checkoutRootFor, doctorRun, errorDocument, fleetAgreementLine, gatingChecks, isRunnableCommand, nextCommand, readyFrom, resolvesToThisCheckout, runsDirFor, tscProjectUpToDate, workerControlFix };
|
|
567
|
+
export { addCheck, allChecks, checkControlPlaneIsolation, checkCrossPackageDist, checkJudge, checkPrimaryCheckoutMark, checkoutRootFor, doctorRun, errorDocument, fleetAgreementLine, gatingChecks, isRunnableCommand, nextCommand, readyFrom, recordedChecks, resolvesToThisCheckout, runsDirFor, scorerWeightsFor, tscProjectUpToDate, workerControlFix };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a11ign/screenreader-fleet",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.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",
|