@a11ign/screenreader-fleet 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/{capture-client.d.mts → capture-client.d.ts} +2 -2
- package/dist/{check-worker-code.d.mts → check-worker-code.d.ts} +2 -3
- package/dist/check-worker-code.mjs +1 -1
- package/dist/{cli-flags.d.mts → cli-flags.d.ts} +5 -5
- package/dist/{code-drift.d.mts → code-drift.d.ts} +7 -7
- package/dist/{command-line-census.d.mts → command-line-census.d.ts} +3 -3
- package/dist/{compare-workers.d.mts → compare-workers.d.ts} +1 -1
- package/dist/{control-plane-isolation.d.mts → control-plane-isolation.d.ts} +1 -1
- package/dist/{doctor.d.mts → doctor.d.ts} +57 -31
- package/dist/doctor.mjs +4 -4
- package/dist/{fleet-consistency.d.mts → fleet-consistency.d.ts} +50 -85
- package/dist/fleet-consistency.mjs +4 -0
- package/dist/{fleet-env.d.mts → fleet-env.d.ts} +45 -45
- package/dist/{fleet-scripts.d.mts → fleet-scripts.d.ts} +1 -1
- package/dist/{git-safe-env.d.mts → git-safe-env.d.ts} +3 -3
- package/dist/{guest-run.d.mts → guest-run.d.ts} +5 -5
- package/dist/{host-address.d.mts → host-address.d.ts} +4 -4
- package/dist/{host-capacity.d.mts → host-capacity.d.ts} +5 -5
- package/dist/{host-metrics.d.mts → host-metrics.d.ts} +23 -15
- package/dist/index.mjs +1 -1
- package/dist/{measure-guard.d.mts → measure-guard.d.ts} +14 -14
- package/dist/{npm-cli-executable.d.mts → npm-cli-executable.d.ts} +4 -4
- package/dist/{probe-outcome.d.mts → probe-outcome.d.ts} +43 -44
- package/dist/{protocol-guard.d.mts → protocol-guard.d.ts} +3 -3
- package/dist/source-walk.d.ts +11 -0
- package/dist/{transient-fault.d.mts → transient-fault.d.ts} +1 -1
- package/dist/tsx-import.d.ts +6 -0
- package/dist/{utm-deprecated.d.mts → utm-deprecated.d.ts} +1 -1
- package/dist/worker-code-check.d.ts +95 -0
- package/dist/worker-code-check.mjs +1 -1
- package/dist/{worker-health.d.mts → worker-health.d.ts} +2 -2
- package/dist/{worker-http.d.mts → worker-http.d.ts} +51 -51
- package/dist/{worker-stats.d.mts → worker-stats.d.ts} +40 -20
- package/package.json +30 -19
- package/src/local-worker/build-vm.sh +2 -2
- package/src/local-worker/clone-worker.sh +1 -1
- package/src/local-worker/create-utm-vm.sh +1 -1
- package/src/local-worker/fetch-windows-iso.sh +5 -5
- package/src/local-worker/worker-ctl.sh +12 -12
- package/src/provisioning/apply-foreground-lock-timeout.ps1 +1 -1
- package/src/provisioning/bootstrap-windows-worker.ps1 +1 -1
- package/src/provisioning/diagnose-nvda-worker.ps1 +1 -1
- package/src/provisioning/stamp-provision-revision.ps1 +3 -3
- package/dist/source-walk.d.mts +0 -11
- package/dist/worker-code-check.d.mts +0 -54
- /package/dist/{normalise-fleet.d.mts → normalise-fleet.d.ts} +0 -0
- /package/dist/{src_fleet-scripts_mjs.mjs → src_fleet-scripts_ts.mjs} +0 -0
- /package/dist/{src_git-safe-env_mjs.mjs → src_git-safe-env_ts.mjs} +0 -0
package/README.md
CHANGED
|
@@ -93,3 +93,40 @@ the only real lifecycle — a cold boot to ready is 15–45 s, which is fine.
|
|
|
93
93
|
|
|
94
94
|
Not exported: `host-metrics`, `worker-stats`, `fleet-consistency`. They are measurement internals whose shapes
|
|
95
95
|
change every time something new gets measured.
|
|
96
|
+
|
|
97
|
+
## Working here
|
|
98
|
+
|
|
99
|
+
This repository is the package: `package.json`, `src/` and this README sit at the root, and the README is the npm page. It moved here from
|
|
100
|
+
[`a11ign/a11ign`](https://github.com/a11ign/a11ign) with its history. It depends on
|
|
101
|
+
[`@a11ign/screenreader-worker`](https://github.com/a11ign/screenreader-worker) and `@a11ign/judge` **by name, from the registry**.
|
|
102
|
+
|
|
103
|
+
**The fleet drives workers that have no authentication.** Anyone who can reach a worker's port can drive the browser and the screen
|
|
104
|
+
reader on that machine (`SECURITY.md` in `a11ign/a11ign` says what else somebody must know first). Run it only on a network you control.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pnpm install --frozen-lockfile
|
|
108
|
+
pnpm test # what the `gate` check runs on every pull request and merge-queue entry, with lint, typecheck and layout-check
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`main` takes pull requests only, each with one approving review, through the merge queue. `pnpm exec layout-check` is the repository-layout
|
|
112
|
+
check from `@a11ign/toolchain`: it fails a workspace of one package, a directory not named for its package, a second README and a leftover
|
|
113
|
+
`lerna.json`.
|
|
114
|
+
|
|
115
|
+
### What did not come across: 27 tests
|
|
116
|
+
|
|
117
|
+
The package's test directory was written for the monorepo, and 27 of its 53 test files read something that is not in this repository:
|
|
118
|
+
`packages/control`'s playbooks and inventories, `packages/lab`'s capture clients, the private `guards` package, or the root's
|
|
119
|
+
`scripts/`. Run here, they fail on a file they cannot find, so they were **not** carried into this repository; they stay in
|
|
120
|
+
`a11ign/a11ign`, beside what they read, until its row #3504 relocates them. Their names are the `COUPLED` list in
|
|
121
|
+
`packages/lab/src/packaging/screenreader-fleet-extraction.test.ts` there. The other 26 run here and are what `gate` runs, with
|
|
122
|
+
`scripts/package-boundary.test.ts` holding the package to its own directory.
|
|
123
|
+
|
|
124
|
+
### Releasing
|
|
125
|
+
|
|
126
|
+
This repository releases on its own, not with `a11ign/a11ign`. A change that should reach npm carries a changeset
|
|
127
|
+
(`pnpm exec changeset`), and **merging it to `main` is the release**: `.github/workflows/release.yml` calls the one
|
|
128
|
+
reusable per-merge workflow in `a11ign/toolchain`, which versions the merge on a detached commit, publishes, tags and
|
|
129
|
+
releases it. There is no version pull request and nothing is written to `main`, so its `package.json` version and
|
|
130
|
+
`CHANGELOG.md` lag the last tag (`.changeset/README.md`). The publish uses npm trusted publishing over OIDC with
|
|
131
|
+
provenance and no stored token. The first release, 0.1.0, was published before this workflow existed;
|
|
132
|
+
no git tag names it, so the first per-merge release bases on `main`'s 0.1.0.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @param {string} worker @param {string} captureId
|
|
15
15
|
*/
|
|
16
|
-
export function recoverCapture(worker: string, captureId: string): Promise<{
|
|
16
|
+
export declare function recoverCapture(worker: string, captureId: string): Promise<{
|
|
17
17
|
status: number;
|
|
18
18
|
ok: boolean;
|
|
19
19
|
text: string;
|
|
@@ -38,7 +38,7 @@ export function recoverCapture(worker: string, captureId: string): Promise<{
|
|
|
38
38
|
* beforeRecovery?: (error: unknown) => Promise<void>,
|
|
39
39
|
* onProgress?: (progress: object) => void, sync?: boolean }} request
|
|
40
40
|
*/
|
|
41
|
-
export function captureTolerantly({ worker, body, timeoutMs, beforeRecovery, onProgress, sync }: {
|
|
41
|
+
export declare function captureTolerantly({ worker, body, timeoutMs, beforeRecovery, onProgress, sync }: {
|
|
42
42
|
worker: string;
|
|
43
43
|
body: object;
|
|
44
44
|
timeoutMs?: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { configuredWorkers, inventoryWorkerUrls } from "./fleet-env.js";
|
|
2
3
|
/**
|
|
3
4
|
* Which workers to ask, AND WHERE THAT LIST CAME FROM — the second half is not decoration.
|
|
4
5
|
*
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
* Reading it here is safe in a way it would not be for a capture run: this probes `/health` and starts
|
|
19
20
|
* nothing, so the rule that naming workers means you are managing them does not apply.
|
|
20
21
|
*/
|
|
21
|
-
export function workerUrls({ named, local, inventory, }?: {
|
|
22
|
+
export declare function workerUrls({ named, local, inventory, }?: {
|
|
22
23
|
named?: typeof configuredWorkers | undefined;
|
|
23
24
|
local?: typeof localPoolUrls | undefined;
|
|
24
25
|
inventory?: typeof inventoryWorkerUrls | undefined;
|
|
@@ -26,8 +27,6 @@ export function workerUrls({ named, local, inventory, }?: {
|
|
|
26
27
|
urls: string[];
|
|
27
28
|
source: string;
|
|
28
29
|
};
|
|
29
|
-
import { configuredWorkers } from "./fleet-env.mjs";
|
|
30
30
|
/** The local UTM pool, or none — `utmctl` is absent on a machine that never had one. */
|
|
31
31
|
declare function localPoolUrls(): any;
|
|
32
|
-
import { inventoryWorkerUrls } from "./fleet-env.mjs";
|
|
33
32
|
export {};
|
|
@@ -3,7 +3,7 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { errorText } from "@a11ign/screenreader-worker/error-text";
|
|
6
|
-
import { fleetScriptPaths } from "./src_fleet-
|
|
6
|
+
import { fleetScriptPaths } from "./src_fleet-scripts_ts.mjs";
|
|
7
7
|
import { inventoryWorkerUrls, configuredWorkers, resolveWorkerPool } from "./fleet-env.mjs";
|
|
8
8
|
import { remedyLines, codeDrift, resolveExpectedWorkerCode } from "./worker-code-check.mjs";
|
|
9
9
|
import { refuseUnknownFlags } from "./cli-flags.mjs";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The flag name alone: `--shard=0/4` and `--shard` both name `--shard`.
|
|
3
3
|
* @param {string} argument @returns {string}
|
|
4
4
|
*/
|
|
5
|
-
export function nameOf(argument: string): string;
|
|
5
|
+
export declare function nameOf(argument: string): string;
|
|
6
6
|
/**
|
|
7
7
|
* `--name=value`'s value, or `undefined` if `--name=` was never passed. audit §9's "argv parsing" row:
|
|
8
8
|
* VALIDATION is owned here (`refuseUnknownFlags`), but 15+ files each hand-rolled this exact three-line
|
|
@@ -21,7 +21,7 @@ export function nameOf(argument: string): string;
|
|
|
21
21
|
* @param {readonly string[]} argv @param {string} name
|
|
22
22
|
* @returns {string | undefined}
|
|
23
23
|
*/
|
|
24
|
-
export function flagValue(argv: readonly string[], name: string): string | undefined;
|
|
24
|
+
export declare function flagValue(argv: readonly string[], name: string): string | undefined;
|
|
25
25
|
/**
|
|
26
26
|
* Which of `argv` are flags this command does not know. PURE, so it is testable without a process.
|
|
27
27
|
*
|
|
@@ -45,12 +45,12 @@ export function flagValue(argv: readonly string[], name: string): string | undef
|
|
|
45
45
|
/**
|
|
46
46
|
* @param {string[]} argv @param {string[]} known @returns {string[]}
|
|
47
47
|
*/
|
|
48
|
-
export function unknownFlags(argv: string[], known: string[]): string[];
|
|
48
|
+
export declare function unknownFlags(argv: string[], known: string[]): string[];
|
|
49
49
|
/**
|
|
50
50
|
* The closest known flag, when there is one close enough to be a likely typo rather than a guess.
|
|
51
51
|
* @param {string} flag @param {string[]} known @returns {string | undefined}
|
|
52
52
|
*/
|
|
53
|
-
export function didYouMean(flag: string, known: string[]): string | undefined;
|
|
53
|
+
export declare function didYouMean(flag: string, known: string[]): string | undefined;
|
|
54
54
|
/**
|
|
55
55
|
* Refuse, naming the flag and what this command does take. Exits 2; does not return on failure.
|
|
56
56
|
*
|
|
@@ -63,7 +63,7 @@ export function didYouMean(flag: string, known: string[]): string | undefined;
|
|
|
63
63
|
* `entry` is the caller's `import.meta.url`, and it is REQUIRED. `command` names the thing a HUMAN
|
|
64
64
|
* typed — the npm script, not the file — because that is what they will retype.
|
|
65
65
|
*/
|
|
66
|
-
export function refuseUnknownFlags(known: string[], { entry, argv, command }: {
|
|
66
|
+
export declare function refuseUnknownFlags(known: string[], { entry, argv, command }: {
|
|
67
67
|
entry: string;
|
|
68
68
|
argv?: string[];
|
|
69
69
|
command?: string;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* @returns {{expected: string, stale: Array<{worker: string, serving: string}>, unreachable: string[],
|
|
17
17
|
* answered: number}}
|
|
18
18
|
*/
|
|
19
|
-
export function codeDrift(expected: string, readings: Array<{
|
|
19
|
+
export declare function codeDrift(expected: string, readings: Array<{
|
|
20
20
|
worker: string;
|
|
21
21
|
code: string | null | undefined;
|
|
22
22
|
}>): {
|
|
@@ -49,7 +49,7 @@ export function codeDrift(expected: string, readings: Array<{
|
|
|
49
49
|
* @param {string[]} bareMetalUrls
|
|
50
50
|
* @returns {string[]}
|
|
51
51
|
*/
|
|
52
|
-
export function remedyLines(staleUrls: string[], bareMetalUrls: string[]): string[];
|
|
52
|
+
export declare function remedyLines(staleUrls: string[], bareMetalUrls: string[]): string[];
|
|
53
53
|
/**
|
|
54
54
|
* The refusal text, or `null` when the fleet is running this checkout. PURE.
|
|
55
55
|
*
|
|
@@ -61,7 +61,7 @@ export function remedyLines(staleUrls: string[], bareMetalUrls: string[]): strin
|
|
|
61
61
|
* @param {{when?: string, bareMetalUrls?: string[], sourceDirty?: string}} options
|
|
62
62
|
* @returns {string|null}
|
|
63
63
|
*/
|
|
64
|
-
export function describeCodeDrift(drift: {
|
|
64
|
+
export declare function describeCodeDrift(drift: {
|
|
65
65
|
expected: string;
|
|
66
66
|
stale: Array<{
|
|
67
67
|
worker: string;
|
|
@@ -76,7 +76,7 @@ export function describeCodeDrift(drift: {
|
|
|
76
76
|
}): string | null;
|
|
77
77
|
/** What a single worker is serving, or `null` when it did not answer. */
|
|
78
78
|
/** @param {string} url */
|
|
79
|
-
export function readWorkerCode(url: string): Promise<any>;
|
|
79
|
+
export declare function readWorkerCode(url: string): Promise<any>;
|
|
80
80
|
/**
|
|
81
81
|
* Is the worker source in `sourceDir` modified against HEAD?
|
|
82
82
|
*
|
|
@@ -89,7 +89,7 @@ export function readWorkerCode(url: string): Promise<any>;
|
|
|
89
89
|
*
|
|
90
90
|
* @param {string} sourceDir
|
|
91
91
|
*/
|
|
92
|
-
export function workerSourceDirty(sourceDir: string): string;
|
|
92
|
+
export declare function workerSourceDirty(sourceDir: string): string;
|
|
93
93
|
/**
|
|
94
94
|
* AN EMPTY POOL IS NOT A CLEAN FLEET — the refusal, as a value, so it can be tested without exiting.
|
|
95
95
|
*
|
|
@@ -110,7 +110,7 @@ export function workerSourceDirty(sourceDir: string): string;
|
|
|
110
110
|
* @param {string} expected
|
|
111
111
|
* @returns {string | null} the refusal, or null when there is a pool to check
|
|
112
112
|
*/
|
|
113
|
-
export function describeEmptyPool(workers: string[] | undefined, expected: string): string | null;
|
|
113
|
+
export declare function describeEmptyPool(workers: string[] | undefined, expected: string): string | null;
|
|
114
114
|
/**
|
|
115
115
|
* Refuse to run against a fleet that is not serving `expected` — the shared body of
|
|
116
116
|
* `assertFleetRunsThisCheckout`, taking the hash as a parameter rather than computing it.
|
|
@@ -131,7 +131,7 @@ export function describeEmptyPool(workers: string[] | undefined, expected: strin
|
|
|
131
131
|
*
|
|
132
132
|
* @param {{when?: string, allow?: boolean, read?: (url: string) => Promise<string|null>, bareMetalUrls?: string[], sourceDir: string}} options
|
|
133
133
|
*/
|
|
134
|
-
export function assertWorkersServe(expected: string, workers: string[], options: {
|
|
134
|
+
export declare function assertWorkersServe(expected: string, workers: string[], options: {
|
|
135
135
|
when?: string;
|
|
136
136
|
allow?: boolean;
|
|
137
137
|
read?: (url: string) => Promise<string | null>;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @param {string} repoRoot absolute path to the repository root
|
|
6
6
|
* @returns {string[]} repo-relative paths, unsorted (discovery order)
|
|
7
7
|
*/
|
|
8
|
-
export function allMjsFiles(repoRoot: string): string[];
|
|
8
|
+
export declare function allMjsFiles(repoRoot: string): string[];
|
|
9
9
|
/**
|
|
10
10
|
* Does this file read argv? Comments stripped first, and that is not a nicety: matching raw source
|
|
11
11
|
* classified a module as a CLI merely because a COMMENT mentioned `process.argv` — measured on
|
|
@@ -20,7 +20,7 @@ export function allMjsFiles(repoRoot: string): string[];
|
|
|
20
20
|
* @param {string} repoRoot absolute path to the repository root
|
|
21
21
|
* @returns {boolean}
|
|
22
22
|
*/
|
|
23
|
-
export function readsArgv(rel: string, repoRoot: string): boolean;
|
|
23
|
+
export declare function readsArgv(rel: string, repoRoot: string): boolean;
|
|
24
24
|
/**
|
|
25
25
|
* `allMjsFiles()` filtered to the ones that read argv — the population `cli-flags.test.ts`'s flag-guard
|
|
26
26
|
* census classifies. NOT the same question as "is this a runnable command with an entry-point guard";
|
|
@@ -29,4 +29,4 @@ export function readsArgv(rel: string, repoRoot: string): boolean;
|
|
|
29
29
|
* @param {string} repoRoot absolute path to the repository root
|
|
30
30
|
* @returns {string[]}
|
|
31
31
|
*/
|
|
32
|
-
export function commandLineModules(repoRoot: string): string[];
|
|
32
|
+
export declare function commandLineModules(repoRoot: string): string[];
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @param {{ argv?: readonly string[], baseDir?: string }} [options]
|
|
13
13
|
* @returns {string}
|
|
14
14
|
*/
|
|
15
|
-
export function outDirFor({ argv, baseDir }?: {
|
|
15
|
+
export declare function outDirFor({ argv, baseDir }?: {
|
|
16
16
|
argv?: readonly string[];
|
|
17
17
|
baseDir?: string;
|
|
18
18
|
}): string;
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* @param {{ hasNodeModules: boolean, hasFleetKey: boolean, packages?: number, isWorkspace?: boolean }} found
|
|
34
34
|
* @returns {{ violated: boolean, why: string }}
|
|
35
35
|
*/
|
|
36
|
-
export function controlPlaneIsolation({ hasNodeModules, hasFleetKey, packages, isWorkspace }: {
|
|
36
|
+
export declare function controlPlaneIsolation({ hasNodeModules, hasFleetKey, packages, isWorkspace }: {
|
|
37
37
|
hasNodeModules: boolean;
|
|
38
38
|
hasFleetKey: boolean;
|
|
39
39
|
packages?: number;
|
|
@@ -12,10 +12,47 @@
|
|
|
12
12
|
* @param {{ argv?: readonly string[], baseDir?: string }} [options]
|
|
13
13
|
* @returns {string}
|
|
14
14
|
*/
|
|
15
|
-
export function runsDirFor({ argv, baseDir }?: {
|
|
15
|
+
export declare function runsDirFor({ argv, baseDir }?: {
|
|
16
16
|
argv?: readonly string[];
|
|
17
17
|
baseDir?: string;
|
|
18
18
|
}): string;
|
|
19
|
+
/** What the checks have recorded so far, for a test that drives one check through the real recorder. */
|
|
20
|
+
export declare const recordedChecks: () => {
|
|
21
|
+
name: string;
|
|
22
|
+
id: string;
|
|
23
|
+
ok: boolean;
|
|
24
|
+
detail: string;
|
|
25
|
+
fix: string | null;
|
|
26
|
+
note?: string | null;
|
|
27
|
+
advisory?: boolean;
|
|
28
|
+
}[];
|
|
29
|
+
/**
|
|
30
|
+
* One check's verdict, and its REMEDY. Every parameter is typed here rather than inferred, because
|
|
31
|
+
* `remedy` defaulting to `null` infers as exactly `null` -- so the argument that matters most, what a
|
|
32
|
+
* reader is to do about a failed check, was the one the compiler refused.
|
|
33
|
+
*
|
|
34
|
+
* `id` is always `name` -- #256 is the first check queried individually by `--json`
|
|
35
|
+
* (`.checks[] | select(.id=="dist-freshness")`), and every check already has a unique `name`, so a
|
|
36
|
+
* separate parameter here would be a second spelling of the same fact rather than a new one.
|
|
37
|
+
*
|
|
38
|
+
* #1059: `fix` IS A COMMAND OR IT IS `null`, and human advice goes in `note`.
|
|
39
|
+
*
|
|
40
|
+
* CLAUDE.md tells an agent to read `next_command` and do that, and `next_command` is `fix`. A sentence
|
|
41
|
+
* there ("unlock the Mac if it is locked, then re-run …") is not something anything can run, so an
|
|
42
|
+
* automated reader loops. The advice is not lost -- it moves to the field nothing executes.
|
|
43
|
+
* ONE `remedy` ARGUMENT, not two: a bare string stays a command (which is what every existing call site
|
|
44
|
+
* passes), and the object form carries the advice beside it. Four parameters is the house limit and
|
|
45
|
+
* bundling cohesive arguments is the house answer to it.
|
|
46
|
+
* @param {string} name
|
|
47
|
+
* @param {boolean} ok
|
|
48
|
+
* @param {string} detail
|
|
49
|
+
* @param {string|null|{fix: string|null, note?: string|null}} [remedy] a runnable command, `null` when
|
|
50
|
+
* none can be constructed, or `{ fix, note }` when there is advice a shell cannot run
|
|
51
|
+
*/
|
|
52
|
+
export declare const addCheck: (name: string, ok: boolean, detail: string, remedy?: string | null | {
|
|
53
|
+
fix: string | null;
|
|
54
|
+
note?: string | null;
|
|
55
|
+
}) => void;
|
|
19
56
|
/**
|
|
20
57
|
* The remedy for a failed local-pool query, as a runnable command plus the advice that is not one.
|
|
21
58
|
*
|
|
@@ -27,7 +64,7 @@ export function runsDirFor({ argv, baseDir }?: {
|
|
|
27
64
|
* @param {string} observed
|
|
28
65
|
* @returns {{ fix: string|null, note: string|null }}
|
|
29
66
|
*/
|
|
30
|
-
export function workerControlFix(observed: string): {
|
|
67
|
+
export declare function workerControlFix(observed: string): {
|
|
31
68
|
fix: string | null;
|
|
32
69
|
note: string | null;
|
|
33
70
|
};
|
|
@@ -64,10 +101,10 @@ export function workerControlFix(observed: string): {
|
|
|
64
101
|
* and telling four of those five to mark themselves would be the #198 defect wearing an advisory's
|
|
65
102
|
* clothes. It states what is true and names the command; the operator decides.
|
|
66
103
|
*/
|
|
67
|
-
export function checkPrimaryCheckoutMark({ baseDir }?: {
|
|
104
|
+
export declare function checkPrimaryCheckoutMark({ baseDir }?: {
|
|
68
105
|
baseDir?: string | undefined;
|
|
69
106
|
}): void;
|
|
70
|
-
export function checkControlPlaneIsolation({ baseDir }?: {
|
|
107
|
+
export declare function checkControlPlaneIsolation({ baseDir }?: {
|
|
71
108
|
baseDir?: string | undefined;
|
|
72
109
|
}): void;
|
|
73
110
|
/**
|
|
@@ -80,7 +117,7 @@ export function checkControlPlaneIsolation({ baseDir }?: {
|
|
|
80
117
|
* @param {string} thisCheckoutRootReal
|
|
81
118
|
* @returns {boolean}
|
|
82
119
|
*/
|
|
83
|
-
export function resolvesToThisCheckout(resolvedRealPath: string, thisCheckoutRootReal: string): boolean;
|
|
120
|
+
export declare function resolvesToThisCheckout(resolvedRealPath: string, thisCheckoutRootReal: string): boolean;
|
|
84
121
|
/**
|
|
85
122
|
* Pure: which checkout root does a resolved `packages/<name>/dist/...` path belong to? Everything before
|
|
86
123
|
* the first `/packages/` -- this repo's own, fixed layout, not a guess. `null` when the path does not look
|
|
@@ -89,7 +126,7 @@ export function resolvesToThisCheckout(resolvedRealPath: string, thisCheckoutRoo
|
|
|
89
126
|
* @param {string} resolvedRealPath
|
|
90
127
|
* @returns {string | null}
|
|
91
128
|
*/
|
|
92
|
-
export function checkoutRootFor(resolvedRealPath: string): string | null;
|
|
129
|
+
export declare function checkoutRootFor(resolvedRealPath: string): string | null;
|
|
93
130
|
/**
|
|
94
131
|
* The `exports` targets of `packageDir` that are NOT on disk, or `null` when the package's manifest could not be
|
|
95
132
|
* read or declares none -- "could not tell" and "missing" need opposite responses (investigate vs. rebuild), and a
|
|
@@ -106,7 +143,7 @@ export function checkoutRootFor(resolvedRealPath: string): string | null;
|
|
|
106
143
|
* @param {string} packageDir
|
|
107
144
|
* @returns {string[] | null}
|
|
108
145
|
*/
|
|
109
|
-
export function missingExportTargets(packageDir: string): string[] | null;
|
|
146
|
+
export declare function missingExportTargets(packageDir: string): string[] | null;
|
|
110
147
|
/**
|
|
111
148
|
* WHOSE dist a cross-package import actually resolves to, and is IT stale (#256) -- both computed from
|
|
112
149
|
* the exact SPECIFIER a real import site in this repo uses, never the bare package name. CLAUDE.md's own
|
|
@@ -120,7 +157,7 @@ export function missingExportTargets(packageDir: string): string[] | null;
|
|
|
120
157
|
* environmental fact would be ignored, which is how a guard gets switched off. It is reported every run
|
|
121
158
|
* so a stale answer is a known condition, not a silent one.
|
|
122
159
|
*/
|
|
123
|
-
export function checkCrossPackageDist({ baseDir }?: {
|
|
160
|
+
export declare function checkCrossPackageDist({ baseDir }?: {
|
|
124
161
|
baseDir?: string | undefined;
|
|
125
162
|
}): number | void;
|
|
126
163
|
/**
|
|
@@ -135,10 +172,10 @@ export function checkCrossPackageDist({ baseDir }?: {
|
|
|
135
172
|
* @param {{ from?: string }} [options] a path or file URL to resolve `@a11ign/judge` from
|
|
136
173
|
* @returns {Promise<string>}
|
|
137
174
|
*/
|
|
138
|
-
export function scorerWeightsFor({ from }?: {
|
|
175
|
+
export declare function scorerWeightsFor({ from }?: {
|
|
139
176
|
from?: string;
|
|
140
177
|
}): Promise<string>;
|
|
141
|
-
export function checkJudge({ from }?: {
|
|
178
|
+
export declare function checkJudge({ from }?: {
|
|
142
179
|
from?: string | undefined;
|
|
143
180
|
}): Promise<void>;
|
|
144
181
|
/**
|
|
@@ -184,7 +221,7 @@ export function checkJudge({ from }?: {
|
|
|
184
221
|
* coverage?: { field: string, reported: number, asked: number }[] } }} input
|
|
185
222
|
* @returns {string}
|
|
186
223
|
*/
|
|
187
|
-
export function fleetAgreementLine({ agreeing, configured, fields }: {
|
|
224
|
+
export declare function fleetAgreementLine({ agreeing, configured, fields }: {
|
|
188
225
|
agreeing: number;
|
|
189
226
|
configured: number;
|
|
190
227
|
fields: {
|
|
@@ -213,7 +250,7 @@ export function fleetAgreementLine({ agreeing, configured, fields }: {
|
|
|
213
250
|
* @param {{ok: boolean, fix: string|null}[]} [checkList]
|
|
214
251
|
* @returns {string|null}
|
|
215
252
|
*/
|
|
216
|
-
export function nextCommand(checkList?: {
|
|
253
|
+
export declare function nextCommand(checkList?: {
|
|
217
254
|
ok: boolean;
|
|
218
255
|
fix: string | null;
|
|
219
256
|
}[]): string | null;
|
|
@@ -227,7 +264,7 @@ export function nextCommand(checkList?: {
|
|
|
227
264
|
* @param {string|null} line
|
|
228
265
|
* @returns {boolean}
|
|
229
266
|
*/
|
|
230
|
-
export function isRunnableCommand(line: string | null): boolean;
|
|
267
|
+
export declare function isRunnableCommand(line: string | null): boolean;
|
|
231
268
|
/**
|
|
232
269
|
* Only when RUN, never on import.
|
|
233
270
|
*
|
|
@@ -246,10 +283,14 @@ export function isRunnableCommand(line: string | null): boolean;
|
|
|
246
283
|
* @param {{name: string, ok: boolean}[]} checkList
|
|
247
284
|
* @returns {boolean}
|
|
248
285
|
*/
|
|
249
|
-
export function readyFrom(checkList: {
|
|
286
|
+
export declare function readyFrom(checkList: {
|
|
250
287
|
name: string;
|
|
251
288
|
ok: boolean;
|
|
252
289
|
}[]): boolean;
|
|
290
|
+
/** Every DECLARED check name, gating or not -- so a test can compare the two sets without a literal. */
|
|
291
|
+
export declare const allChecks: () => string[];
|
|
292
|
+
/** Which checks decide `ready`, for a test that must not retype the list. */
|
|
293
|
+
export declare const gatingChecks: () => string[];
|
|
253
294
|
/**
|
|
254
295
|
* #1082: A `--json` RUN THAT CANNOT PRODUCE JSON STILL PRODUCES JSON.
|
|
255
296
|
*
|
|
@@ -271,7 +312,7 @@ export function readyFrom(checkList: {
|
|
|
271
312
|
* @param {unknown} error
|
|
272
313
|
* @returns {{ready: false, error: string, checks: never[]}}
|
|
273
314
|
*/
|
|
274
|
-
export function errorDocument(error: unknown): {
|
|
315
|
+
export declare function errorDocument(error: unknown): {
|
|
275
316
|
ready: false;
|
|
276
317
|
error: string;
|
|
277
318
|
checks: never[];
|
|
@@ -283,25 +324,10 @@ export function errorDocument(error: unknown): {
|
|
|
283
324
|
* err?: (line: string) => void, runsDir?: () => string }} [deps]
|
|
284
325
|
* @returns {Promise<number>}
|
|
285
326
|
*/
|
|
286
|
-
export function doctorRun(deps?: {
|
|
327
|
+
export declare function doctorRun(deps?: {
|
|
287
328
|
steps?: (() => unknown)[];
|
|
288
329
|
json?: boolean;
|
|
289
330
|
out?: (line: string) => void;
|
|
290
331
|
err?: (line: string) => void;
|
|
291
332
|
runsDir?: () => string;
|
|
292
333
|
}): Promise<number>;
|
|
293
|
-
export function recordedChecks(): {
|
|
294
|
-
name: string;
|
|
295
|
-
id: string;
|
|
296
|
-
ok: boolean;
|
|
297
|
-
detail: string;
|
|
298
|
-
fix: string | null;
|
|
299
|
-
note?: string | null;
|
|
300
|
-
advisory?: boolean;
|
|
301
|
-
}[];
|
|
302
|
-
export function addCheck(name: string, ok: boolean, detail: string, remedy?: string | null | {
|
|
303
|
-
fix: string | null;
|
|
304
|
-
note?: string | null;
|
|
305
|
-
}): void;
|
|
306
|
-
export function allChecks(): string[];
|
|
307
|
-
export function gatingChecks(): string[];
|
package/dist/doctor.mjs
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
import { execFile, execFileSync } from "node:child_process";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
4
|
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
5
|
-
import { resolve } from "node:path";
|
|
5
|
+
import { join, 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";
|
|
9
9
|
import { availableHostMemoryMb, workersHostCanRun } from "./host-capacity.mjs";
|
|
10
10
|
import { fleetConsistency, describeMismatches } from "./fleet-consistency.mjs";
|
|
11
|
-
import { fleetScriptPaths } from "./src_fleet-
|
|
11
|
+
import { fleetScriptPaths } from "./src_fleet-scripts_ts.mjs";
|
|
12
12
|
import { namedInventoryWorkers, configuredWorkers } from "./fleet-env.mjs";
|
|
13
13
|
import { refuseUnknownFlags, flagValue } from "./cli-flags.mjs";
|
|
14
14
|
import { requestJson } from "./worker-http.mjs";
|
|
15
|
-
import { sandboxGitEnv } from "./src_git-safe-
|
|
15
|
+
import { sandboxGitEnv } from "./src_git-safe-env_ts.mjs";
|
|
16
16
|
import { assessWorker } from "./worker-health.mjs";
|
|
17
17
|
function controlPlaneIsolation({ hasNodeModules, hasFleetKey, packages, isWorkspace = false }) {
|
|
18
18
|
if (!hasNodeModules || !hasFleetKey) return {
|
|
@@ -361,7 +361,7 @@ async function checkDegradedWorkers(probed) {
|
|
|
361
361
|
for (const w of probed){
|
|
362
362
|
if (!w.health) continue;
|
|
363
363
|
const { degraded, reason } = assessWorker(w.health.vitals);
|
|
364
|
-
if (degraded) add(`worker ${w.name}`, true, `DEGRADED — ${reason}`, `re-provision ${w.name}:
|
|
364
|
+
if (degraded) add(`worker ${w.name}`, true, `DEGRADED — ${reason}`, `re-provision ${w.name}: ${join(fleetScriptPaths().provisioning, "provision-nvda-worker.ps1")}, elevated, in the interactive session`);
|
|
365
365
|
}
|
|
366
366
|
}
|
|
367
367
|
function checkFleetConsistency(probed, configured) {
|
|
@@ -1,42 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compare guests field by field.
|
|
3
|
-
*
|
|
4
|
-
* @param {Guest[]} guests
|
|
5
|
-
* @returns {{consistent: boolean, mismatches: Mismatch[], compared: number, fields: FieldCoverage,
|
|
6
|
-
* reportedOnly: ReportedDrift[]}}
|
|
7
|
-
* `compared` is how many guests the verdict is actually ABOUT -- #920. A guest with no `environment`
|
|
8
|
-
* and no `policy` is dropped below before comparing, so it is not the length of what was passed in,
|
|
9
|
-
* and a caller that reports `consistent` without it is stating agreement over a set it cannot name.
|
|
10
|
-
* `fields` is that same question one axis over: WHICH fields the verdict is about -- #1997.
|
|
11
|
-
* `reportedOnly` is the third channel (#2063): compared, named, and part of NEITHER of the two above,
|
|
12
|
-
* which is what makes it something to report rather than something to refuse.
|
|
13
|
-
*/
|
|
14
|
-
export function fleetConsistency(guests: Guest[]): {
|
|
15
|
-
consistent: boolean;
|
|
16
|
-
mismatches: Mismatch[];
|
|
17
|
-
compared: number;
|
|
18
|
-
fields: FieldCoverage;
|
|
19
|
-
reportedOnly: ReportedDrift[];
|
|
20
|
-
};
|
|
21
|
-
/**
|
|
22
|
-
* One line per reported-only field that has something to say, in `describeMismatches`'s own shape.
|
|
23
|
-
*
|
|
24
|
-
* SEPARATE FROM `describeMismatches` rather than a flag on it, because the two lines make different
|
|
25
|
-
* claims and a reader has to be able to tell them apart at a glance: that one says the fleet is not
|
|
26
|
-
* usable for a capture run, this one says the fleet is not identical and the run may proceed anyway.
|
|
27
|
-
* Folding them would put the ruling's own distinction behind a boolean argument.
|
|
28
|
-
*
|
|
29
|
-
* @param {ReportedDrift[]} drifts
|
|
30
|
-
* @returns {string[]}
|
|
31
|
-
*/
|
|
32
|
-
export function describeReportedOnly(drifts: ReportedDrift[]): string[];
|
|
33
|
-
/**
|
|
34
|
-
* One line per mismatch, naming the guests, so the report is actionable rather than just alarming.
|
|
35
|
-
*
|
|
36
|
-
* @param {Mismatch[]} mismatches
|
|
37
|
-
* @returns {string[]}
|
|
38
|
-
*/
|
|
39
|
-
export function describeMismatches(mismatches: Mismatch[]): string[];
|
|
40
1
|
/**
|
|
41
2
|
* Are the guests actually interchangeable?
|
|
42
3
|
*
|
|
@@ -65,7 +26,7 @@ export function describeMismatches(mismatches: Mismatch[]): string[];
|
|
|
65
26
|
* `workerCode` is absent on purpose: it changes when a comment changes, and the deploy tooling already
|
|
66
27
|
* verifies it against the checkout. Flagging it here would cry wolf on every reworded line.
|
|
67
28
|
*/
|
|
68
|
-
export const MUST_MATCH: {
|
|
29
|
+
export declare const MUST_MATCH: {
|
|
69
30
|
path: string;
|
|
70
31
|
why: string;
|
|
71
32
|
}[];
|
|
@@ -98,7 +59,7 @@ export const MUST_MATCH: {
|
|
|
98
59
|
* until the worker carrying it was deployed on 2026-09-23, and the state any field added here starts in,
|
|
99
60
|
* and it must be readable as such without refusing anything.
|
|
100
61
|
*/
|
|
101
|
-
export const REPORTED_ONLY: {
|
|
62
|
+
export declare const REPORTED_ONLY: {
|
|
102
63
|
path: string;
|
|
103
64
|
why: string;
|
|
104
65
|
}[];
|
|
@@ -110,11 +71,14 @@ export const REPORTED_ONLY: {
|
|
|
110
71
|
* `Record<string, unknown>` on the other, which typecheck caught in the test that calls them in sequence.
|
|
111
72
|
* Two spellings of one shape is the duplication this repo names as its most expensive recurring defect,
|
|
112
73
|
* and a typedef is the cheapest form of "delete a copy".
|
|
113
|
-
*
|
|
114
|
-
* @typedef {{field: string, why: string, values: Record<string, unknown>}} Mismatch
|
|
115
74
|
*/
|
|
75
|
+
export type Mismatch = {
|
|
76
|
+
field: string;
|
|
77
|
+
why: string;
|
|
78
|
+
values: Record<string, unknown>;
|
|
79
|
+
};
|
|
116
80
|
/** Edge policy values every guest must agree on, checked separately because they come from /diagnostics. */
|
|
117
|
-
export const POLICY_MUST_MATCH: string[];
|
|
81
|
+
export declare const POLICY_MUST_MATCH: string[];
|
|
118
82
|
/**
|
|
119
83
|
* Which fields the verdict is actually ABOUT -- #1997.
|
|
120
84
|
*
|
|
@@ -141,27 +105,6 @@ export type FieldReporters = {
|
|
|
141
105
|
reported: number;
|
|
142
106
|
asked: number;
|
|
143
107
|
};
|
|
144
|
-
/**
|
|
145
|
-
* Which fields the verdict is actually ABOUT -- #1997.
|
|
146
|
-
*
|
|
147
|
-
* `compared` NAMES the fields at least one guest reported a value for; `unchecked` names the ones that
|
|
148
|
-
* drew a value from NOBODY. The second list exists because `check()` below skips an absent value, which
|
|
149
|
-
* is right (a rolling deploy must not flag the guest it has not reached yet) and has a cost: a field no
|
|
150
|
-
* guest reports contributes no values, `new Set([]).size > 1` is false, and it reads as agreement.
|
|
151
|
-
* A field compared on nobody and a field equal on everybody produced the IDENTICAL verdict.
|
|
152
|
-
*
|
|
153
|
-
* Measured 2026-09-22T20:09Z on the live fleet, at the merge of #1953: nine fields at 10/10 guests and
|
|
154
|
-
* `displayMode` at 0/10, and `fleet:status` printed `fleet CONSISTENT across 10 of 10 -- these workers
|
|
155
|
-
* are interchangeable for capture`. The display was still not compared. Naming the two lists is what
|
|
156
|
-
* lets a reader tell "they agree" from "nobody was asked".
|
|
157
|
-
*
|
|
158
|
-
* `coverage` IS THE SAME QUESTION WITHOUT THE THRESHOLD -- #2019. The two lists above are a PARTITION on
|
|
159
|
-
* "did anybody report it", which answers 0-of-10 and says nothing about 1-of-10; `fleet:status` read the
|
|
160
|
-
* second as agreement about ten guests on the strength of one. So every asked field also carries how many
|
|
161
|
-
* of the asked guests actually reported it, and the verdict draws its own line. `compared`/`unchecked`
|
|
162
|
-
* stay because they are what a caller greps for the REMEDY -- a field at 0 sends a reader to the field,
|
|
163
|
-
* a field at k sends them to the boxes -- and `coverage` is the measurement both are derived from.
|
|
164
|
-
*/
|
|
165
108
|
export type FieldCoverage = {
|
|
166
109
|
compared: string[];
|
|
167
110
|
unchecked: string[];
|
|
@@ -195,22 +138,19 @@ export type ReportedDrift = {
|
|
|
195
138
|
* that gates nothing. One reader means the two channels cannot drift apart in how they compare — a
|
|
196
139
|
* second copy of this loop is how a "reported-only" field would end up counted differently from a gating
|
|
197
140
|
* one and nobody would know which was right.
|
|
141
|
+
*
|
|
142
|
+
* @param {Guest[]} present
|
|
143
|
+
* @param {{field: string, why: string, source: (guest: Guest) => Record<string, unknown> | undefined,
|
|
144
|
+
* key: string}} ask `source` is the BLOCK this field lives in, not the value: coverage has to tell
|
|
145
|
+
* "the guest did not report this field" from "this caller never collected that block at all", and only
|
|
146
|
+
* the block answers the second
|
|
147
|
+
* @returns {Reading}
|
|
198
148
|
*/
|
|
199
149
|
export type Guest = {
|
|
200
150
|
worker: string;
|
|
201
151
|
environment?: Record<string, unknown>;
|
|
202
152
|
policy?: Record<string, unknown>;
|
|
203
153
|
};
|
|
204
|
-
/**
|
|
205
|
-
* ONE FIELD READ ACROSS THE FLEET, before anybody decides what it means.
|
|
206
|
-
*
|
|
207
|
-
* Split out from `fleetConsistency` when the third channel arrived (#2063), because the READING and the
|
|
208
|
-
* CONSEQUENCE are two things and only the second differs between the channels: `MUST_MATCH` turns a
|
|
209
|
-
* reading into a mismatch and a coverage row, `REPORTED_ONLY` turns the same reading into a named drift
|
|
210
|
-
* that gates nothing. One reader means the two channels cannot drift apart in how they compare — a
|
|
211
|
-
* second copy of this loop is how a "reported-only" field would end up counted differently from a gating
|
|
212
|
-
* one and nobody would know which was right.
|
|
213
|
-
*/
|
|
214
154
|
export type Reading = {
|
|
215
155
|
field: string;
|
|
216
156
|
why: string;
|
|
@@ -219,16 +159,41 @@ export type Reading = {
|
|
|
219
159
|
asked: number;
|
|
220
160
|
};
|
|
221
161
|
/**
|
|
222
|
-
*
|
|
162
|
+
* Compare guests field by field.
|
|
223
163
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
164
|
+
* @param {Guest[]} guests
|
|
165
|
+
* @returns {{consistent: boolean, mismatches: Mismatch[], compared: number, fields: FieldCoverage,
|
|
166
|
+
* reportedOnly: ReportedDrift[]}}
|
|
167
|
+
* `compared` is how many guests the verdict is actually ABOUT -- #920. A guest with no `environment`
|
|
168
|
+
* and no `policy` is dropped below before comparing, so it is not the length of what was passed in,
|
|
169
|
+
* and a caller that reports `consistent` without it is stating agreement over a set it cannot name.
|
|
170
|
+
* `fields` is that same question one axis over: WHICH fields the verdict is about -- #1997.
|
|
171
|
+
* `reportedOnly` is the third channel (#2063): compared, named, and part of NEITHER of the two above,
|
|
172
|
+
* which is what makes it something to report rather than something to refuse.
|
|
229
173
|
*/
|
|
230
|
-
export
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
174
|
+
export declare function fleetConsistency(guests: Guest[]): {
|
|
175
|
+
consistent: boolean;
|
|
176
|
+
mismatches: Mismatch[];
|
|
177
|
+
compared: number;
|
|
178
|
+
fields: FieldCoverage;
|
|
179
|
+
reportedOnly: ReportedDrift[];
|
|
234
180
|
};
|
|
181
|
+
/**
|
|
182
|
+
* One line per reported-only field that has something to say, in `describeMismatches`'s own shape.
|
|
183
|
+
*
|
|
184
|
+
* SEPARATE FROM `describeMismatches` rather than a flag on it, because the two lines make different
|
|
185
|
+
* claims and a reader has to be able to tell them apart at a glance: that one says the fleet is not
|
|
186
|
+
* usable for a capture run, this one says the fleet is not identical and the run may proceed anyway.
|
|
187
|
+
* Folding them would put the ruling's own distinction behind a boolean argument.
|
|
188
|
+
*
|
|
189
|
+
* @param {ReportedDrift[]} drifts
|
|
190
|
+
* @returns {string[]}
|
|
191
|
+
*/
|
|
192
|
+
export declare function describeReportedOnly(drifts: ReportedDrift[]): string[];
|
|
193
|
+
/**
|
|
194
|
+
* One line per mismatch, naming the guests, so the report is actionable rather than just alarming.
|
|
195
|
+
*
|
|
196
|
+
* @param {Mismatch[]} mismatches
|
|
197
|
+
* @returns {string[]}
|
|
198
|
+
*/
|
|
199
|
+
export declare function describeMismatches(mismatches: Mismatch[]): string[];
|