@a11ign/screenreader-fleet 0.0.0-reserved.0 → 0.1.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.
Files changed (147) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +94 -2
  3. package/dist/capture-client.d.mts +49 -0
  4. package/dist/capture-client.d.mts.map +1 -0
  5. package/dist/capture-client.mjs +352 -0
  6. package/dist/capture-client.mjs.map +1 -0
  7. package/dist/check-worker-code.d.mts +34 -0
  8. package/dist/check-worker-code.d.mts.map +1 -0
  9. package/dist/check-worker-code.mjs +173 -0
  10. package/dist/check-worker-code.mjs.map +1 -0
  11. package/dist/cli-flags.d.mts +71 -0
  12. package/dist/cli-flags.d.mts.map +1 -0
  13. package/dist/cli-flags.mjs +207 -0
  14. package/dist/cli-flags.mjs.map +1 -0
  15. package/dist/code-drift.d.mts +140 -0
  16. package/dist/code-drift.d.mts.map +1 -0
  17. package/dist/code-drift.mjs +284 -0
  18. package/dist/code-drift.mjs.map +1 -0
  19. package/dist/command-line-census.d.mts +33 -0
  20. package/dist/command-line-census.d.mts.map +1 -0
  21. package/dist/command-line-census.mjs +96 -0
  22. package/dist/command-line-census.mjs.map +1 -0
  23. package/dist/compare-workers.d.mts +3 -0
  24. package/dist/compare-workers.d.mts.map +1 -0
  25. package/dist/compare-workers.mjs +332 -0
  26. package/dist/compare-workers.mjs.map +1 -0
  27. package/dist/control-plane-isolation.d.mts +45 -0
  28. package/dist/control-plane-isolation.d.mts.map +1 -0
  29. package/dist/control-plane-isolation.mjs +67 -0
  30. package/dist/control-plane-isolation.mjs.map +1 -0
  31. package/dist/deploy-worker.d.mts +3 -0
  32. package/dist/deploy-worker.d.mts.map +1 -0
  33. package/dist/deploy-worker.mjs +333 -0
  34. package/dist/deploy-worker.mjs.map +1 -0
  35. package/dist/doctor.d.mts +216 -0
  36. package/dist/doctor.d.mts.map +1 -0
  37. package/dist/doctor.mjs +962 -0
  38. package/dist/doctor.mjs.map +1 -0
  39. package/dist/fleet-consistency.d.mts +235 -0
  40. package/dist/fleet-consistency.d.mts.map +1 -0
  41. package/dist/fleet-consistency.mjs +436 -0
  42. package/dist/fleet-consistency.mjs.map +1 -0
  43. package/dist/fleet-env.d.mts +228 -0
  44. package/dist/fleet-env.d.mts.map +1 -0
  45. package/dist/fleet-env.mjs +509 -0
  46. package/dist/fleet-env.mjs.map +1 -0
  47. package/dist/fleet-scripts.d.mts +11 -0
  48. package/dist/fleet-scripts.d.mts.map +1 -0
  49. package/dist/fleet-scripts.mjs +41 -0
  50. package/dist/fleet-scripts.mjs.map +1 -0
  51. package/dist/git-safe-env.d.mts +10 -0
  52. package/dist/git-safe-env.d.mts.map +1 -0
  53. package/dist/git-safe-env.mjs +44 -0
  54. package/dist/git-safe-env.mjs.map +1 -0
  55. package/dist/guest-run.d.mts +26 -0
  56. package/dist/guest-run.d.mts.map +1 -0
  57. package/dist/guest-run.mjs +164 -0
  58. package/dist/guest-run.mjs.map +1 -0
  59. package/dist/host-address.d.mts +33 -0
  60. package/dist/host-address.d.mts.map +1 -0
  61. package/dist/host-address.mjs +105 -0
  62. package/dist/host-address.mjs.map +1 -0
  63. package/dist/host-capacity.d.mts +64 -0
  64. package/dist/host-capacity.d.mts.map +1 -0
  65. package/dist/host-capacity.mjs +152 -0
  66. package/dist/host-capacity.mjs.map +1 -0
  67. package/dist/host-metrics.d.mts +116 -0
  68. package/dist/host-metrics.d.mts.map +1 -0
  69. package/dist/host-metrics.mjs +201 -0
  70. package/dist/host-metrics.mjs.map +1 -0
  71. package/dist/index.d.ts +23 -0
  72. package/dist/index.d.ts.map +1 -0
  73. package/dist/index.js +25 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/local-vm.d.ts +125 -0
  76. package/dist/local-vm.d.ts.map +1 -0
  77. package/dist/local-vm.js +360 -0
  78. package/dist/local-vm.js.map +1 -0
  79. package/dist/measure-guard.d.mts +34 -0
  80. package/dist/measure-guard.d.mts.map +1 -0
  81. package/dist/measure-guard.mjs +73 -0
  82. package/dist/measure-guard.mjs.map +1 -0
  83. package/dist/normalise-fleet.d.mts +2 -0
  84. package/dist/normalise-fleet.d.mts.map +1 -0
  85. package/dist/normalise-fleet.mjs +76 -0
  86. package/dist/normalise-fleet.mjs.map +1 -0
  87. package/dist/npm-cli-executable.d.mts +42 -0
  88. package/dist/npm-cli-executable.d.mts.map +1 -0
  89. package/dist/npm-cli-executable.mjs +159 -0
  90. package/dist/npm-cli-executable.mjs.map +1 -0
  91. package/dist/probe-outcome.d.mts +89 -0
  92. package/dist/probe-outcome.d.mts.map +1 -0
  93. package/dist/probe-outcome.mjs +104 -0
  94. package/dist/probe-outcome.mjs.map +1 -0
  95. package/dist/protocol-guard.d.mts +34 -0
  96. package/dist/protocol-guard.d.mts.map +1 -0
  97. package/dist/protocol-guard.mjs +121 -0
  98. package/dist/protocol-guard.mjs.map +1 -0
  99. package/dist/source-walk.d.mts +12 -0
  100. package/dist/source-walk.d.mts.map +1 -0
  101. package/dist/source-walk.mjs +56 -0
  102. package/dist/source-walk.mjs.map +1 -0
  103. package/dist/transient-fault.d.mts +6 -0
  104. package/dist/transient-fault.d.mts.map +1 -0
  105. package/dist/transient-fault.mjs +86 -0
  106. package/dist/transient-fault.mjs.map +1 -0
  107. package/dist/utm-deprecated.d.mts +6 -0
  108. package/dist/utm-deprecated.d.mts.map +1 -0
  109. package/dist/utm-deprecated.mjs +23 -0
  110. package/dist/utm-deprecated.mjs.map +1 -0
  111. package/dist/worker-code-check.d.mts +29 -0
  112. package/dist/worker-code-check.d.mts.map +1 -0
  113. package/dist/worker-code-check.mjs +78 -0
  114. package/dist/worker-code-check.mjs.map +1 -0
  115. package/dist/worker-health.d.mts +56 -0
  116. package/dist/worker-health.d.mts.map +1 -0
  117. package/dist/worker-health.mjs +73 -0
  118. package/dist/worker-health.mjs.map +1 -0
  119. package/dist/worker-http.d.mts +103 -0
  120. package/dist/worker-http.d.mts.map +1 -0
  121. package/dist/worker-http.mjs +277 -0
  122. package/dist/worker-http.mjs.map +1 -0
  123. package/dist/worker-stats.d.mts +66 -0
  124. package/dist/worker-stats.d.mts.map +1 -0
  125. package/dist/worker-stats.mjs +143 -0
  126. package/dist/worker-stats.mjs.map +1 -0
  127. package/package.json +96 -4
  128. package/src/local-worker/autounattend.xml +280 -0
  129. package/src/local-worker/build-vm.sh +218 -0
  130. package/src/local-worker/clone-worker.sh +141 -0
  131. package/src/local-worker/create-utm-vm.sh +202 -0
  132. package/src/local-worker/fetch-windows-iso.sh +238 -0
  133. package/src/local-worker/first-boot.cmd +58 -0
  134. package/src/local-worker/worker-ctl.sh +442 -0
  135. package/src/provisioning/README.md +28 -0
  136. package/src/provisioning/apply-foreground-lock-timeout.ps1 +71 -0
  137. package/src/provisioning/bare-metal/README.md +213 -0
  138. package/src/provisioning/bare-metal/a11y-bootstrap.service +58 -0
  139. package/src/provisioning/bare-metal/autounattend.xml +428 -0
  140. package/src/provisioning/bare-metal/serve-bootstrap.sh +86 -0
  141. package/src/provisioning/bootstrap-control-plane.sh +463 -0
  142. package/src/provisioning/bootstrap-windows-worker.ps1 +649 -0
  143. package/src/provisioning/build-lean-worker-image.ps1 +275 -0
  144. package/src/provisioning/diagnose-nvda-worker.ps1 +174 -0
  145. package/src/provisioning/provision-nvda-worker.ps1 +827 -0
  146. package/src/provisioning/set-display-mode.ps1 +411 -0
  147. package/src/provisioning/stamp-provision-revision.ps1 +184 -0
@@ -0,0 +1,509 @@
1
+ // @ts-check
2
+ /**
3
+ * `A11Y_WORKERS`, derived from the Ansible inventory — so a machine is added in ONE place.
4
+ *
5
+ * eval "$(npm run --silent fleet:env)"
6
+ * npm run fleet:env -- --list # just the URLs, one per line
7
+ *
8
+ * ## Why derive rather than maintain both
9
+ *
10
+ * The fleet was a comma-separated string in an environment variable, which was fine for two VMs and is the
11
+ * wrong shape for twelve boxes. Ansible needs an inventory regardless, so the choice is not "one format or
12
+ * two" but "one source of truth or two" — and two is how a box comes to be provisioned but never dispatched
13
+ * to, or dispatched to but never updated. Both of those are silent: the run simply never sends that box a
14
+ * case, and nothing reports a machine it does not know about.
15
+ *
16
+ * ## Why this reads the file rather than shelling out to `ansible-inventory`
17
+ *
18
+ * `ansible-inventory --list` is authoritative and would be the better answer if Ansible were always
19
+ * present. It is not: the control plane runs captures, and requiring an Ansible install before a capture
20
+ * run could start would make the fleet tooling a dependency of the thing it exists to serve.
21
+ *
22
+ * ## Strict on purpose
23
+ *
24
+ * A hand-rolled reader for a subset of YAML is exactly the sort of thing that quietly returns four hosts
25
+ * out of twelve after somebody reformats the file — and a SHORT fleet list is invisible, because a run with
26
+ * eight workers looks like a run with eight workers. So this refuses rather than guesses: anything that
27
+ * looks like a host entry but does not parse is an error naming the line, and finding no hosts at all is an
28
+ * error too.
29
+ */
30
+ import { readFileSync } from "node:fs";
31
+ import { assertWorkerUrl } from "./worker-http.mjs";
32
+ import { fileURLToPath, pathToFileURL } from "node:url";
33
+ import { refuseUnknownFlags } from "./cli-flags.mjs";
34
+ /**
35
+ * its output is `eval`-ed by a shell, so a wrong shape is executed rather than read.
36
+ *
37
+ * An unrecognised flag is otherwise IGNORED, so it runs the default and reports success.
38
+ */
39
+ refuseUnknownFlags(["--list"], { entry: import.meta.url, command: "npm run fleet:env" });
40
+ export const DEFAULT_WORKER_PORT = 8765;
41
+ /**
42
+ * The fleet named by the environment — ONE parser, because there were three that did not agree.
43
+ *
44
+ * `doctor.mjs` preferred `A11Y_WORKERS`, `check-worker-code.mjs` preferred `A11Y_WORKER`, and the
45
+ * dataset runner read only `A11Y_WORKERS`. With both variables set, `doctor` and `worker:code` reported
46
+ * on **different machines** — so "doctor says the fleet is fine" and "worker:code says a worker is
47
+ * stale" could be statements about two disjoint sets, with nothing to say so.
48
+ *
49
+ * `A11Y_WORKERS` wins, matching the dataset runner: the plural names the pool a run dispatches across,
50
+ * and a diagnostic that describes a different set from the one that will do the work is worse than no
51
+ * diagnostic. Each entry is trimmed and de-slashed, because `A11Y_WORKERS=a, b` otherwise yields a URL
52
+ * with a leading space — a configuration typo wearing a dead-machine costume.
53
+ *
54
+ * Returns `[]` rather than null when neither is set: "no worker was named" is a normal state that means
55
+ * "find the local VMs", and every caller already branches on emptiness.
56
+ *
57
+ * @returns {Array<{ name: string, url: string }>}
58
+ */
59
+ export function configuredWorkers() {
60
+ const raw = process.env.A11Y_WORKERS ?? process.env.A11Y_WORKER ?? "";
61
+ const named = process.env.A11Y_WORKERS !== undefined ? "A11Y_WORKERS" : "A11Y_WORKER";
62
+ return raw.split(",")
63
+ .map((w) => w.trim())
64
+ .filter(Boolean)
65
+ // Validated, because this is the ENV route into the same defect the `--worker=` clients now refuse.
66
+ // `A11Y_WORKERS=http://:8765` used to pass straight through to every consumer of this function --
67
+ // `doctor`, `worker:code`, `fleet:status`, the dataset runner -- each of which would then report a
68
+ // machine that cannot be addressed as one that is not answering. Note the empty case is untouched:
69
+ // "no worker was named" is a normal state meaning "find the local VMs", and every caller branches on it.
70
+ .map((url) => assertWorkerUrl(url, { source: named }))
71
+ .map((url) => ({ name: url.replace(/^https?:\/\//, ""), url }));
72
+ }
73
+ // THE INVENTORY LIVES IN `packages/control`, because it describes the machines the CONTROL PLANE drives
74
+ // and it is read by the ansible that runs there. `packages/worker-fleet` is PUBLISHED, and `control` is
75
+ // never published (ADR 0012), so this constant is a real cycle -- audit §3.2 -- and NOT the sanctioned
76
+ // direction: `control` reaching `worker-fleet` by relative import is fine (control has no
77
+ // `node_modules`); this file, reaching back into a package that will not exist in an installed
78
+ // `node_modules/@a11ign/screenreader-fleet`, is what the audit calls "ships code whose data file lives in
79
+ // a package that is never published".
80
+ //
81
+ // FIXED 2026-09-06 by injection, not by moving this module: `doctor.mjs` and `check-worker-code.mjs` are
82
+ // PUBLISHED bins that must keep resolving a bare-metal fleet correctly when run as `npm run doctor` from
83
+ // this checkout, so the functions below take the path as an optional PARAMETER, defaulting to this
84
+ // constant. The default is not a fix in itself -- an installed tarball still ships one that points at a
85
+ // package it will never find -- but `inventoryWorkerUrls`/`namedInventoryWorkers` already catch that and
86
+ // return `[]`, which is this project's own supported "no bare-metal fleet declared here" answer (see their
87
+ // own comments). What injection buys is that the assumption is now a NAMED, overridable default rather
88
+ // than a hidden module constant -- a caller outside this monorepo (or a test) can supply its own path
89
+ // instead of silently inheriting one that can only ever resolve here.
90
+ const INVENTORY = fileURLToPath(new URL("../../control/ansible/inventory.yml", import.meta.url));
91
+ const GROUP_VARS = fileURLToPath(new URL("../../control/ansible/group_vars/a11y_workers.yml", import.meta.url));
92
+ /** A line that declares a host address, ignoring anything commented out. */
93
+ const HOST_LINE = /^\s*ansible_host\s*:\s*(\S+)\s*$/;
94
+ /** Anything that mentions the key but does not parse — a reformat, a quoted value, a list. */
95
+ const SUSPECT = /ansible_host\s*:/;
96
+ /** A mapping key, with or without an inline value. Indentation is the only thing that says where it sits. */
97
+ const KEY_LINE = /^(\s*)([A-Za-z_][\w.-]*)\s*:/;
98
+ /**
99
+ * A host var declaring whether the host is in the CAPTURE SET. Absent means in: the default is unchanged, so
100
+ * a host that says nothing needs no inventory edit.
101
+ */
102
+ const CAPTURE_KEY = "a11y_capture";
103
+ const CAPTURE_LINE = new RegExp(`^(\\s*)${CAPTURE_KEY}\\s*:\\s*(\\S+)\\s*$`);
104
+ const CAPTURE_SUSPECT = new RegExp(`${CAPTURE_KEY}\\s*:`);
105
+ /**
106
+ * The inventory group whose hosts are capture workers.
107
+ *
108
+ * This reader was GROUPLESS until 2026-08-21, and that was a live hazard rather than an untidiness: every
109
+ * `ansible_host:` in the file became `http://<addr>:8765`, so adding any non-worker host to `inventory.yml`
110
+ * -- the lab container, the control container, a switch -- would have silently added a phantom worker to
111
+ * `A11Y_WORKERS`, and a run would have dispatched capture cases to it. That is the exact failure this
112
+ * module's own header says it exists to prevent ("dispatched to but never updated", "both of those are
113
+ * silent"), arriving through the door nobody had shut.
114
+ */
115
+ export const WORKER_GROUP = "a11y_workers";
116
+ /**
117
+ * One frame of the indentation stack: a key and the column it started at.
118
+ *
119
+ * @typedef {{indent: number, key: string}} Frame
120
+ */
121
+ /**
122
+ * A host as the inventory declares it — the ADDRESS and the NAME together.
123
+ *
124
+ * They travel as one value because separating them is what sent `fleet:sleep` at the wrong machine: every
125
+ * tool that ACTS on a worker takes the name, every tool that REPORTS on one printed the address, and
126
+ * nothing mapped between them. `collectHost` explains the incident.
127
+ *
128
+ * `capture` is false for a host that declares `a11y_capture: false`: enrolled, served, deployed to, and not
129
+ * handed corpus cases.
130
+ *
131
+ * @typedef {{name: string|undefined, host: string, capture: boolean}} Host
132
+ */
133
+ /**
134
+ * The group a host sits in: the key directly beneath `children`.
135
+ *
136
+ * Ansible nests as `all.children.<group>.hosts.<name>`, so the group is positional rather than something
137
+ * to pattern-match on a name. Reading it from the path means a group added later needs no change here.
138
+ */
139
+ /** @param {Array<string|undefined>} path @returns {string|undefined} */
140
+ function groupOf(path) {
141
+ const children = path.indexOf("children");
142
+ return children === -1 ? undefined : path[children + 1];
143
+ }
144
+ /**
145
+ * Track the YAML path by indentation — a stack, not a parser.
146
+ *
147
+ * Deliberately not a YAML library, for the reason the header gives about `ansible-inventory`: the control
148
+ * plane must be able to read the fleet without installing anything. Indentation is sufficient because the
149
+ * only question asked of the path is which group a host is in.
150
+ */
151
+ /** @param {Frame[]} stack @param {string} line */
152
+ function descend(stack, line) {
153
+ const match = line.match(KEY_LINE);
154
+ if (!match)
155
+ return;
156
+ const indent = match[1].length;
157
+ while (stack.length && stack[stack.length - 1].indent >= indent)
158
+ stack.pop();
159
+ stack.push({ indent, key: match[2] });
160
+ }
161
+ /**
162
+ * Which group each line of the inventory sits in, index-aligned with `text.split(/\r?\n/)`.
163
+ *
164
+ * Exported so there is ONE group implementation. `fleet-discover.mjs` has its own host reader with its own
165
+ * regexes -- two readers of one file, which this repo's notes call its most expensive recurring shape -- and
166
+ * when this module became group-aware that one did not, so `fleet:discover` probed the lab container on
167
+ * :8765 and reported it "ASLEEP?". A phantom worker in the diagnostic instead of in the dispatch list is
168
+ * still a phantom worker. Rather than teach a second parser about groups, both now ask this.
169
+ *
170
+ * @param {string} text
171
+ * @returns {Array<string | undefined>}
172
+ */
173
+ export function groupPerLine(text) {
174
+ /** @type {Frame[]} */
175
+ const stack = [];
176
+ return text.split(/\r?\n/).map((line) => {
177
+ if (!line.trim() || line.trimStart().startsWith("#"))
178
+ return groupOf(stack.map((f) => f.key));
179
+ if (!HOST_LINE.test(line))
180
+ descend(stack, line);
181
+ return groupOf(stack.map((f) => f.key));
182
+ });
183
+ }
184
+ /**
185
+ * The hosts of one inventory group, each with whether it is in the capture set.
186
+ *
187
+ * ONE reader for both questions ("the whole fleet" and "the capture set"), because a second pass over the
188
+ * file is a second parser, and this repo has paid for that shape already (`fleet-discover.mjs`).
189
+ *
190
+ * @param {string} text
191
+ * @param {string} group
192
+ * @returns {Host[]}
193
+ */
194
+ function hostsInGroup(text, group) {
195
+ /** @type {Host[]} */
196
+ const hosts = [];
197
+ /** @type {Frame[]} */
198
+ const stack = [];
199
+ /** @type {Map<string, {capture: boolean, line: number}>} */
200
+ const declared = new Map();
201
+ text.split(/\r?\n/).forEach((line, index) => {
202
+ if (!line.trim() || line.trimStart().startsWith("#"))
203
+ return;
204
+ const match = line.match(HOST_LINE);
205
+ if (match) {
206
+ collectHost(hosts, { match, index, stack, group });
207
+ return;
208
+ }
209
+ if (CAPTURE_SUSPECT.test(line)) {
210
+ readCaptureDeclaration(declared, { line, index, stack });
211
+ return;
212
+ }
213
+ if (SUSPECT.test(line)) {
214
+ throw new Error(`inventory.yml:${index + 1} looks like a host entry but does not parse: ${line.trim()}\n`
215
+ + "This reader understands `ansible_host: <address>` and nothing else, deliberately — a fleet list "
216
+ + "that silently comes up short is invisible, because a run with fewer workers looks normal.");
217
+ }
218
+ descend(stack, line);
219
+ });
220
+ if (!hosts.length) {
221
+ throw new Error(`no hosts found under ${group}.hosts in inventory.yml. Add one before running.`);
222
+ }
223
+ return applyCaptureDeclarations(hosts, declared, group);
224
+ }
225
+ /**
226
+ * Record `a11y_capture: true|false` against the host whose var it is — the nearest enclosing key.
227
+ *
228
+ * Strict for the reason the header gives: `a11y_capture: no` or `"false"` that this reader took for "not
229
+ * declared" would leave a cold host in the capture set, and a capture guard would then refuse the run
230
+ * naming the wrong cause. Only the two literals are read; anything else names its line.
231
+ *
232
+ * @param {Map<string, {capture: boolean, line: number}>} declared
233
+ * @param {{line: string, index: number, stack: Frame[]}} at
234
+ */
235
+ function readCaptureDeclaration(declared, { line, index, stack }) {
236
+ const match = line.match(CAPTURE_LINE);
237
+ const value = match?.[2];
238
+ if (value !== "true" && value !== "false") {
239
+ throw new Error(`inventory.yml:${index + 1} declares ${CAPTURE_KEY} but not as \`true\` or \`false\`: ${line.trim()}\n`
240
+ + "This reader takes those two literals and nothing else, so a host is never left in or out of the "
241
+ + "capture set by a spelling it did not understand.");
242
+ }
243
+ const indent = /** @type {RegExpMatchArray} */ (match)[1].length;
244
+ const owner = [...stack].reverse().find((frame) => frame.indent < indent)?.key;
245
+ declared.set(owner ?? "", { capture: value === "true", line: index + 1 });
246
+ }
247
+ /**
248
+ * Attach each declaration to its host, and refuse one that belongs to no host of the group.
249
+ *
250
+ * A declaration on a group or on a host in another group would otherwise be read and dropped, which is the
251
+ * silent shape this module exists to refuse: somebody believes a machine is out of the capture set and it is
252
+ * not.
253
+ *
254
+ * @param {Host[]} hosts
255
+ * @param {Map<string, {capture: boolean, line: number}>} declared
256
+ * @param {string} group
257
+ * @returns {Host[]}
258
+ */
259
+ function applyCaptureDeclarations(hosts, declared, group) {
260
+ const names = new Set(hosts.map((host) => host.name));
261
+ for (const [owner, { line }] of declared) {
262
+ if (!names.has(owner)) {
263
+ throw new Error(`inventory.yml:${line} declares ${CAPTURE_KEY} on \`${owner}\`, which is not a host in ${group}.\n`
264
+ + `Declare it directly under the host (\`${group}.hosts.<name>.${CAPTURE_KEY}\`); it is not read anywhere else.`);
265
+ }
266
+ }
267
+ return hosts.map((host) => ({ ...host, capture: declared.get(host.name ?? "")?.capture ?? true }));
268
+ }
269
+ /**
270
+ * Worker URLs from the text of an inventory file.
271
+ *
272
+ * `scope` says which question is asked. `"fleet"` (the default) is every enrolled worker -- what `doctor`,
273
+ * `worker:code`, `fleet:status` and the deploy tooling mean, and they must keep seeing a worker that is
274
+ * serving but not capturing. `"capture"` is the fleet minus every host that declares `a11y_capture: false`
275
+ * -- what `A11Y_WORKERS` means. It is a string and not a flag because a boolean argument does not say
276
+ * which of the two you got.
277
+ *
278
+ * Exported and pure so the strictness above is testable: a reader that has never been shown to reject
279
+ * anything is a reader nobody knows the limits of.
280
+ *
281
+ * @param {string} text
282
+ * @param {{ port?: number, group?: string, scope?: "fleet" | "capture" }} [options]
283
+ * @returns {string[]}
284
+ */
285
+ export function workersFromInventory(text, { port = DEFAULT_WORKER_PORT, group = WORKER_GROUP, scope = "fleet" } = {}) {
286
+ const hosts = hostsInGroup(text, group);
287
+ const chosen = scope === "capture" ? hosts.filter((host) => host.capture) : hosts;
288
+ if (!chosen.length) {
289
+ // An empty A11Y_WORKERS means "find the local VMs", which do not exist on the control plane -- so an
290
+ // inventory with every host excluded must fail HERE, naming the cause, not as something unrelated.
291
+ throw new Error(`every host under ${group}.hosts declares ${CAPTURE_KEY}: false, so the capture set is empty.`);
292
+ }
293
+ return chosen.map(({ host }) => `http://${host}:${port}`);
294
+ }
295
+ /**
296
+ * The hosts left OUT of the capture set, so the command that leaves them out can say so.
297
+ *
298
+ * @param {string} text
299
+ * @param {{ port?: number, group?: string }} [options]
300
+ * @returns {{name: string, url: string}[]}
301
+ */
302
+ export function hostsOutOfCaptureSet(text, { port = DEFAULT_WORKER_PORT, group = WORKER_GROUP } = {}) {
303
+ return hostsInGroup(text, group)
304
+ .filter((host) => !host.capture)
305
+ .map(({ name, host }) => ({ name: name ?? host, url: `http://${host}:${port}` }));
306
+ }
307
+ /**
308
+ * The inventory as `{ url -> name }`, so a report can say which machine a command would act on.
309
+ *
310
+ * Same parser, same group rules — deliberately not a second reader of the same file, which this repo
311
+ * calls its most expensive recurring shape and has already paid for once in `fleet-discover.mjs`.
312
+ *
313
+ * @param {string} text
314
+ * @param {{ port?: number, group?: string }} [options]
315
+ * @returns {Record<string, string>}
316
+ */
317
+ export function workerNamesFromInventory(text, { port = DEFAULT_WORKER_PORT, group = WORKER_GROUP } = {}) {
318
+ /** @type {Host[]} */
319
+ const hosts = [];
320
+ /** @type {Frame[]} */
321
+ const stack = [];
322
+ text.split(/\r?\n/).forEach((line, index) => {
323
+ if (!line.trim() || line.trimStart().startsWith("#"))
324
+ return;
325
+ const match = line.match(HOST_LINE);
326
+ if (match)
327
+ return collectHost(hosts, { match, index, stack, group });
328
+ descend(stack, line);
329
+ });
330
+ // Falls back to the ADDRESS when the inventory nests a host without a name. That cannot happen in a
331
+ // well-formed inventory — Ansible keys hosts by name — but `undefined` reaching a report would print as
332
+ // the string "undefined", which is this repo's worst failure shape: a value that looks like an answer.
333
+ // An address is a worse label than a name and still identifies the machine, which is what this map is for.
334
+ return Object.fromEntries(hosts.map(({ name, host }) => [`http://${host}:${port}`, name ?? host]));
335
+ }
336
+ /**
337
+ * Keep this host if it is in the worker group; refuse it if it is in no group at all.
338
+ *
339
+ * A host outside every group is the ambiguous case, and it gets an error rather than a default. Including
340
+ * it recreates the phantom-worker bug; dropping it silently is how a fleet list comes up short — and this
341
+ * module exists because both of those are invisible. So it says which line, and which group it expected.
342
+ *
343
+ * @param {Host[]} hosts
344
+ * @param {{match: RegExpMatchArray, index: number, stack: Frame[], group: string}} found
345
+ */
346
+ function collectHost(hosts, { match, index, stack, group }) {
347
+ const found = groupOf(stack.map((frame) => frame.key));
348
+ if (found === group) {
349
+ // The inventory NAME comes along with the address. Ansible nests the host as
350
+ // `...hosts.<name>.ansible_host`, so the name is the innermost frame the stack is still holding.
351
+ //
352
+ // Carried because every tool that ACTS on a worker takes the name (`-l a11y-worker-4`) while every
353
+ // tool that REPORTS on one printed only the address — and nothing mapped them. On 2026-08-24 that
354
+ // sent `fleet:sleep` at the wrong machine: `fleet:status` named .224 as the Edge-drifted box, and
355
+ // .224 is a11y-worker-FIVE. Two commands, one fleet, no shared vocabulary.
356
+ hosts.push({ name: stack[stack.length - 1]?.key, host: match[1].replace(/^["']|["']$/g, ""), capture: true });
357
+ return;
358
+ }
359
+ if (found === undefined) {
360
+ throw new Error(`inventory.yml:${index + 1} declares a host outside any group: ${match[0].trim()}\n`
361
+ + `This reader takes workers from \`${group}\` only, so it cannot tell whether an ungrouped host is a `
362
+ + "capture worker or something else on the network. Nest it under `all.children.<group>.hosts`.");
363
+ }
364
+ }
365
+ /** The port the group vars declare, so it is stated once and not guessed here. */
366
+ /** @param {string} text @returns {number} */
367
+ export function portFromGroupVars(text) {
368
+ const match = text.match(/^\s*a11y_port\s*:\s*(\d+)\s*$/m);
369
+ return match ? Number(match[1]) : DEFAULT_WORKER_PORT;
370
+ }
371
+ /**
372
+ * The workers declared in `inventory.yml` — i.e. the BARE-METAL fleet.
373
+ *
374
+ * Exists so a caller can tell a physical box from a local UTM VM, which decides how it is deployed to and
375
+ * therefore what remedy to print. `worker:code` used to tell every stale worker to run `utmctl` and
376
+ * `npm run worker:deploy`, which CANNOT reach a bare-metal box — it is a `utmctl file push` keyed on a VM
377
+ * UUID and fails immediately off macOS. Following that advice on this fleet wastes the time it takes to
378
+ * discover the tool was describing a different kind of machine.
379
+ *
380
+ * Reads the same file through the same parser as `main()`, rather than a second copy of the knowledge.
381
+ *
382
+ * `inventoryPath`/`groupVarsPath` are INJECTED, defaulting to this monorepo's own control-plane files —
383
+ * see the comment above `INVENTORY` for why the default exists and what it does not fix on its own.
384
+ *
385
+ * @param {{ inventoryPath?: string, groupVarsPath?: string }} [paths]
386
+ */
387
+ export function inventoryWorkerUrls({ inventoryPath = INVENTORY, groupVarsPath = GROUP_VARS } = {}) {
388
+ try {
389
+ const port = portFromGroupVars(readFileSync(groupVarsPath, "utf8"));
390
+ return workersFromInventory(readFileSync(inventoryPath, "utf8"), { port });
391
+ }
392
+ catch {
393
+ // No inventory, or one that does not parse, means "no bare-metal fleet declared here" — a local-VM-only
394
+ // checkout is a supported setup. Rethrowing would make a hint fail the command it is only advising.
395
+ return [];
396
+ }
397
+ }
398
+ /**
399
+ * The bare-metal fleet as `{name, url}` — the shape a REPORT needs.
400
+ *
401
+ * `fleet-status.mjs` already pairs the two, and its comment records what the address alone cost: "this
402
+ * table named .224 as the box whose Edge had drifted, and .224 is a11y-worker-FIVE — so
403
+ * `fleet:sleep --limit=a11y-worker-4` put a healthy machine to sleep and left the drifted one serving. A
404
+ * report and a command that cannot be matched up is a report you have to translate, and translation is
405
+ * where the mistake goes."
406
+ *
407
+ * Here rather than in each reporter, because that is the same pairing and a second copy would drift.
408
+ *
409
+ * `inventoryPath`/`groupVarsPath` are INJECTED, same reason and same default as `inventoryWorkerUrls`.
410
+ *
411
+ * @param {{ inventoryPath?: string, groupVarsPath?: string }} [paths]
412
+ * @returns {{name: string, url: string}[]} empty when no inventory is declared, like `inventoryWorkerUrls`
413
+ */
414
+ export function namedInventoryWorkers({ inventoryPath = INVENTORY, groupVarsPath = GROUP_VARS } = {}) {
415
+ try {
416
+ const port = portFromGroupVars(readFileSync(groupVarsPath, "utf8"));
417
+ const inventory = readFileSync(inventoryPath, "utf8");
418
+ const names = workerNamesFromInventory(inventory, { port });
419
+ return workersFromInventory(inventory, { port })
420
+ .map((url) => ({ name: names[url] ?? url.replace(/^https?:\/\//, ""), url }));
421
+ }
422
+ catch {
423
+ return [];
424
+ }
425
+ }
426
+ /**
427
+ * WHICH WORKERS TO USE, AND WHERE THAT LIST CAME FROM — the one precedence, in one place.
428
+ *
429
+ * THREE MODULES HELD THREE DIFFERENT ANSWERS, and one of them carried a comment saying they had been
430
+ * unified. Measured 2026-08-29:
431
+ *
432
+ * doctor.mjs named -> inventory
433
+ * check-worker-code.mjs named -> LOCAL UTM POOL -> inventory
434
+ * capture-screenreader-dataset.mjs named -> LOCAL UTM POOL -> single-VM lease (never reads inventory)
435
+ *
436
+ * The corpus capture path's own comment reads "One parser, in fleet-env.mjs. This copy and doctor's and
437
+ * check-worker-code's had drifted apart on precedence, which meant a diagnostic could describe a different
438
+ * fleet from the one about to run." That unification covered the NAMED half only; the fallback order below
439
+ * it stayed three separate answers, so the sentence describes a fix that was half applied.
440
+ *
441
+ * The consequence is the one the comment predicted: on a Mac with any registered UTM guest, `worker:code`
442
+ * reports the local VM while `doctor` reports the five bare-metal boxes, and a capture dispatches to
443
+ * whichever the entry point happened to prefer.
444
+ *
445
+ * ## THE LOCAL UTM POOL IS LAST, and that is a deprecation, not a preference
446
+ *
447
+ * The local guests were a testing arrangement and are deprecated; `inventory.yml` is the fleet, and ADR
448
+ * 0012 already calls it the single source of truth. So the pool is a fallback for a checkout with NO
449
+ * inventory — a supported setup for an outside contributor with one Mac and no hardware — and never a
450
+ * contender with one. Any other order reproduces the divergence above on every Mac that still has a bundle
451
+ * registered.
452
+ *
453
+ * `local` is injected rather than imported: reading the UTM pool means shelling out to `utmctl`, and this
454
+ * module is imported by everything that needs a worker list, including on Linux. A caller that has no
455
+ * local-pool reader simply omits it.
456
+ *
457
+ * @param {{ named?: () => {url: string}[], inventory?: () => string[], local?: () => string[] }} readers
458
+ * @returns {{ urls: string[], source: string }}
459
+ */
460
+ export function resolveWorkerPool({ named = configuredWorkers, inventory = inventoryWorkerUrls, local = () => [], } = {}) {
461
+ const configured = named();
462
+ // Naming workers means you are managing them — nothing is started or stopped for you — so an explicit
463
+ // list always wins. That half was already consistent everywhere; it is the fallback below that was not.
464
+ if (configured.length)
465
+ return { urls: configured.map((w) => w.url), source: "A11Y_WORKER(S)" };
466
+ const fleet = inventory();
467
+ if (fleet.length)
468
+ return { urls: fleet, source: "inventory.yml" };
469
+ const pool = local();
470
+ if (pool.length)
471
+ return { urls: pool, source: "the local UTM pool (DEPRECATED — see inventory.yml)" };
472
+ // Names what it LOOKED IN, never a bare "nothing found". `lab:inventory`'s rule: "'none here' and 'none
473
+ // anywhere' are different answers, and it now refuses to turn the first into the second."
474
+ return { urls: [], source: "A11Y_WORKER(S), inventory.yml and the local UTM pool — all empty" };
475
+ }
476
+ /**
477
+ * What `fleet:env` prints, as data: `stdout` is what a shell reads, `stderr` is what the operator reads.
478
+ *
479
+ * `mode: "list"` is the WHOLE fleet, one URL per line, and leaves nobody out. The default is the capture
480
+ * set as an `export`, and it NAMES every host it left out on stderr -- an omission with no line is a
481
+ * machine nothing reports, which is the failure this module's header is about. stderr, because stdout is
482
+ * `eval`-ed.
483
+ *
484
+ * @param {string} text the inventory
485
+ * @param {{ port?: number, mode?: "env" | "list" }} [options]
486
+ * @returns {{stdout: string, stderr: string}}
487
+ */
488
+ export function fleetEnvOutput(text, { port = DEFAULT_WORKER_PORT, mode = "env" } = {}) {
489
+ if (mode === "list") {
490
+ return { stdout: `${workersFromInventory(text, { port }).join("\n")}\n`, stderr: "" };
491
+ }
492
+ const workers = workersFromInventory(text, { port, scope: "capture" });
493
+ const stderr = hostsOutOfCaptureSet(text, { port })
494
+ .map(({ name, url }) => `fleet:env: ${name} (${url}) is NOT in A11Y_WORKERS -- its inventory entry declares ${CAPTURE_KEY}: false. `
495
+ + "It is still enrolled: `--list`, `doctor` and `fleet:status` name it.\n")
496
+ .join("");
497
+ // Shell-quoted, so `eval "$(npm run --silent fleet:env)"` is safe even if a hostname ever contains
498
+ // something the shell would otherwise split on.
499
+ return { stdout: `export A11Y_WORKERS='${workers.join(",")}'\n`, stderr };
500
+ }
501
+ function main() {
502
+ const port = portFromGroupVars(readFileSync(GROUP_VARS, "utf8"));
503
+ const { stdout, stderr } = fleetEnvOutput(readFileSync(INVENTORY, "utf8"), { port, mode: process.argv.includes("--list") ? "list" : "env" });
504
+ process.stderr.write(stderr);
505
+ process.stdout.write(stdout);
506
+ }
507
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href)
508
+ main();
509
+ //# sourceMappingURL=fleet-env.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fleet-env.mjs","sourceRoot":"","sources":["../src/fleet-env.mjs"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /** Absolute paths to the provisioning and lifecycle scripts. Absolute, because a consumer spawns them. */
2
+ export function fleetScriptPaths(): {
3
+ dir: string;
4
+ workerCtl: string;
5
+ buildVm: string;
6
+ cloneWorker: string;
7
+ createUtmVm: string;
8
+ fetchWindowsIso: string;
9
+ provisioning: string;
10
+ };
11
+ //# sourceMappingURL=fleet-scripts.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fleet-scripts.d.mts","sourceRoot":"","sources":["../src/fleet-scripts.mjs"],"names":[],"mappings":"AA6BA,0GAA0G;AAC1G;;;;;;;;EAWC"}
@@ -0,0 +1,41 @@
1
+ // @ts-check
2
+ /**
3
+ * Where the fleet's shell, PowerShell and XML assets live — ONE definition.
4
+ *
5
+ * `worker-ctl.sh` was resolved independently in four modules: `local-vm.ts`, `doctor.mjs`,
6
+ * `deploy-worker.mjs` and `check-worker-code.mjs`. Three of them agreed and the fourth did not —
7
+ * `local-vm.ts` said `../../scripts/local-worker/…`, which after M6 pointed at `packages/scripts/…`, a
8
+ * directory that does not exist. So `leaseWorker`, the primary export of this package, could not find the
9
+ * script it drives.
10
+ *
11
+ * It went unnoticed because every check that exercised a lease set `A11Y_WORKER`, which short-circuits VM
12
+ * discovery — so the DEFAULT path, the one CLAUDE.md documents as "with no A11Y_WORKER set the run finds the
13
+ * local VM", was the one path never run. Four copies of a fact is three chances to be wrong about it.
14
+ *
15
+ * `.mjs` rather than `.ts` on purpose: the fleet's own scripts are `.mjs` and are run directly with
16
+ * `node packages/worker-fleet/src/doctor.mjs`, with nothing compiled first, so they can only import `.mjs`
17
+ * siblings.
18
+ */
19
+ import { fileURLToPath } from "node:url";
20
+ import { join } from "node:path";
21
+ /**
22
+ * `../src/local-worker/`, NOT `./local-worker/`. These assets are not JavaScript, so tsc does not copy them
23
+ * into `dist` — they ship from `src`. Resolving via the package root works from `src/…` under tsx and from
24
+ * `dist/…` when installed, because the two are siblings one level below it. Getting this wrong is not subtle:
25
+ * the isolation gate failed with `dist/local-worker/` missing.
26
+ */
27
+ const assetDir = () => fileURLToPath(new URL("../src/local-worker/", import.meta.url));
28
+ /** Absolute paths to the provisioning and lifecycle scripts. Absolute, because a consumer spawns them. */
29
+ export function fleetScriptPaths() {
30
+ const dir = assetDir();
31
+ return {
32
+ dir,
33
+ workerCtl: join(dir, "worker-ctl.sh"),
34
+ buildVm: join(dir, "build-vm.sh"),
35
+ cloneWorker: join(dir, "clone-worker.sh"),
36
+ createUtmVm: join(dir, "create-utm-vm.sh"),
37
+ fetchWindowsIso: join(dir, "fetch-windows-iso.sh"),
38
+ provisioning: fileURLToPath(new URL("../src/provisioning/", import.meta.url)),
39
+ };
40
+ }
41
+ //# sourceMappingURL=fleet-scripts.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fleet-scripts.mjs","sourceRoot":"","sources":["../src/fleet-scripts.mjs"],"names":[],"mappings":"AAAA,YAAY;AACZ;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,sBAAsB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAEvF,0GAA0G;AAC1G,MAAM,UAAU,gBAAgB;IAC9B,MAAM,GAAG,GAAG,QAAQ,EAAE,CAAC;IACvB,OAAO;QACL,GAAG;QACH,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC;QACrC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC;QACjC,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC;QACzC,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC;QAC1C,eAAe,EAAE,IAAI,CAAC,GAAG,EAAE,sBAAsB,CAAC;QAClD,YAAY,EAAE,aAAa,CAAC,IAAI,GAAG,CAAC,sBAAsB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;KAC9E,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `process.env` with every `GIT_*` key removed, `extra` applied on top. A prefix strip, never a list
3
+ * lookup, so a new `GIT_*` variable is caught without this file needing to know its name.
4
+ * @param {Record<string, string>} [extra]
5
+ * @returns {Record<string, string | undefined>}
6
+ */
7
+ export function sandboxGitEnv(extra?: Record<string, string>): Record<string, string | undefined>;
8
+ /** @type {readonly string[]} */
9
+ export const KNOWN_GIT_REDIRECT_VARS: readonly string[];
10
+ //# sourceMappingURL=git-safe-env.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-safe-env.d.mts","sourceRoot":"","sources":["../src/git-safe-env.mjs"],"names":[],"mappings":"AA8BA;;;;;GAKG;AACH,sCAHW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAS9C;AAxBD,gCAAgC;AAChC,sCADW,SAAS,MAAM,EAAE,CAS1B"}
@@ -0,0 +1,44 @@
1
+ // A DELIBERATE, DISCLOSED DUPLICATE of `packages/guards/src/git-env.mjs` at the repo root.
2
+ //
3
+ // git EXPORTS `GIT_DIR`/`GIT_WORK_TREE`/`GIT_INDEX_FILE` into every hook environment, and a process that
4
+ // spawns `git` with an inherited `env` operates on whatever `GIT_DIR` names, not on `cwd` -- see
5
+ // `packages/guards/src/git-env.mjs`'s header for the incident that proved it (a pre-push-hook test forged 15 commits
6
+ // across all refs of the real repo).
7
+ //
8
+ // This package publishes `check-worker-code.mjs` and `deploy-worker.mjs` as `bin` entries
9
+ // (`package.json`), so every file they import -- `code-drift.mjs` among them -- ships in the published
10
+ // tarball and can only import from INSIDE `@a11ign/screenreader-fleet`. The repo-root `scripts/` directory
11
+ // does not exist once this package is installed from npm, so importing it here would work in this
12
+ // monorepo and break for every real consumer. That is the entire reason this file exists rather than a
13
+ // relative import to the root: not stylistic preference, a publish-boundary constraint (see ADR 0004).
14
+ //
15
+ // Kept textually identical to `packages/guards/src/git-env.mjs` and pinned equal to it by
16
+ // `git-safe-env.test.ts`, which is this repo's own remedy #3 ("pin them equal with a test") for a fact
17
+ // that CANNOT be stated once because the two copies cross a package-publishing boundary neither can
18
+ // import through.
19
+ /** @type {readonly string[]} */
20
+ export const KNOWN_GIT_REDIRECT_VARS = [
21
+ "GIT_DIR",
22
+ "GIT_WORK_TREE",
23
+ "GIT_INDEX_FILE",
24
+ "GIT_CEILING_DIRECTORIES",
25
+ "GIT_OBJECT_DIRECTORY",
26
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
27
+ "GIT_COMMON_DIR",
28
+ ];
29
+ /**
30
+ * `process.env` with every `GIT_*` key removed, `extra` applied on top. A prefix strip, never a list
31
+ * lookup, so a new `GIT_*` variable is caught without this file needing to know its name.
32
+ * @param {Record<string, string>} [extra]
33
+ * @returns {Record<string, string | undefined>}
34
+ */
35
+ export function sandboxGitEnv(extra = {}) {
36
+ /** @type {Record<string, string | undefined>} */
37
+ const scrubbed = {};
38
+ for (const [key, value] of Object.entries(process.env)) {
39
+ if (!key.startsWith("GIT_"))
40
+ scrubbed[key] = value;
41
+ }
42
+ return { ...scrubbed, ...extra };
43
+ }
44
+ //# sourceMappingURL=git-safe-env.mjs.map