@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,277 @@
1
+ // @ts-check
2
+ /**
3
+ * HTTP to a capture worker, on a connection that may stay silent for ten minutes.
4
+ *
5
+ * ## Why this is not `fetch`
6
+ *
7
+ * Node's global `fetch` is undici, and undici caps the wait for RESPONSE HEADERS at 300 s
8
+ * (`headersTimeout`). `AbortSignal.timeout()` does not govern it — it is a separate mechanism, settable
9
+ * only through a dispatcher. Measured on Node v24.7.0 against a server that withheld headers for 310 s:
10
+ *
11
+ * global fetch + AbortSignal.timeout(560s) : THREW — TypeError: fetch failed :: UND_ERR_HEADERS_TIMEOUT
12
+ * node:http request : SURVIVED — got 200
13
+ *
14
+ * That cap lands squarely inside this project's own budget ladder. `capture-pure.mjs` sets a 420 s sweep
15
+ * budget inside a 520 s hard timeout, deliberately raised so that "a real page was sampled not validated",
16
+ * and the three clients then declared 560 s, 560 s and 320 s. Every one of those numbers is above 300 s, so
17
+ * the effective ceiling was undici's — not any constant in this repo.
18
+ *
19
+ * The failure is silent and it destroys evidence. The worker writes its status and body together at the END
20
+ * of a capture (`send(res, 200, {...})`), so headers arrive last; a capture that takes 301 s therefore has
21
+ * its socket torn down by the CLIENT at the moment it finishes, and the worker writes a completed capture
22
+ * into a dead socket. The host sees `fetch failed`, classifies it transient, and retries — paying another
23
+ * full capture to reach the same cliff. Three such pages in a row on one worker trips `shouldEvictWorker`
24
+ * and removes a machine that was never faulty.
25
+ *
26
+ * `cli.ts` had the diagnosis exactly right in a comment — it names `UND_ERR_HEADERS_TIMEOUT` and the ~300 s
27
+ * cap — and then raised an `AbortSignal`, which governs a different mechanism. This repo's own "a comment
28
+ * that names an ambiguity, above code that resolves it by assumption", one step worse: the comment named
29
+ * the mechanism and the code still addressed another one.
30
+ *
31
+ * ## Why not an undici dispatcher
32
+ *
33
+ * `undici` is not a dependency here, and installing it would not help by itself: the standalone package
34
+ * keeps its own global dispatcher, so `setGlobalDispatcher` does not reach Node's built-in `fetch`. Using
35
+ * it would mean importing undici's `fetch` too. `node:http` is in core, has no client-side headers cap at
36
+ * all, and gives us one explicit deadline over the whole exchange instead of three interacting ones.
37
+ *
38
+ * ## Errors carry a CODE
39
+ *
40
+ * `node:http` reports `error.code` (`ECONNREFUSED`, `EHOSTUNREACH`, `ECONNRESET`) where `fetch` collapsed
41
+ * everything into `TypeError: fetch failed`. That is strictly better — `capture-faults.mjs` records what
42
+ * matching on prose costs — but it is a behaviour change the retry logic has to know about, so
43
+ * `isTransient` now keys on those codes. `EHOSTUNREACH` matters most: it is how a bare-metal worker's NIC
44
+ * waking from selective suspend presents, and under `fetch` it was transient only by accident, because the
45
+ * wrapper said "fetch failed".
46
+ */
47
+ import { request as httpRequest } from "node:http";
48
+ import { request as httpsRequest } from "node:https";
49
+ /**
50
+ * How long a CLIENT should wait for a capture. One definition, because five had drifted.
51
+ *
52
+ * It must exceed the worker's own hard timeout (`CAPTURE_HARD_TIMEOUT_DEFAULT_MS`, 520 s) or the client
53
+ * gives up first, and a capture the worker would have completed is reported as a client failure. Five
54
+ * clients sat at 300 s -- `compare-workers`, `bench-capture`, `evidence-check`, `repeat-capture` and
55
+ * `capture-real-pages` -- against that 520 s. On the generated corpus nothing noticed, because a 1,338-byte
56
+ * page finishes in seconds. On REAL pages it silently dropped whatever used its budget, biasing the
57
+ * real-page corpus toward small simple pages: precisely the axis that corpus exists to add.
58
+ *
59
+ * 560,000 -> 620,000 on architecture-audit.md §14.5: `runCapture` (server.mjs) spends up to
60
+ * `DESKTOP_PREPARE_TIMEOUT_MS` (60 s) clearing the desktop BEFORE the hard-timeout-wrapped capture attempt
61
+ * even starts, sequentially rather than overlapping it -- so the true worst case a worker can legitimately
62
+ * take is 60 s + 520 s = 580 s, not 520 s alone. The old 560 s ceiling sat BELOW that, so a real page that
63
+ * used the full prepare budget and the full capture budget was killed by the CLIENT first and reported as
64
+ * a client failure for work the worker would have finished -- the exact shape this constant already exists
65
+ * to prevent, one rung further out. 620,000 keeps the same 40 s margin above the new true worst case that
66
+ * the original 560,000 kept above 520,000. `budget-ladder.test.ts` asserts the full sequence, not only the
67
+ * capture attempt inside it.
68
+ *
69
+ * Deliberately NOT imported from `@a11ign/screenreader-worker`: this package runs on macOS and Linux and must
70
+ * not depend on a win32-only one. `budget-ladder.test.ts` enforces the relationship instead, over every
71
+ * client it DISCOVERS rather than a list -- which is how the 300 s clients stayed invisible while a guard
72
+ * for exactly this existed and read one hardcoded path.
73
+ *
74
+ * `DATASET_CAPTURE_TIMEOUT_MS` still overrides it in the dataset runner, which is the only client that
75
+ * wants a per-run ceiling.
76
+ */
77
+ export const CAPTURE_CLIENT_TIMEOUT_MS = 620_000;
78
+ /**
79
+ * How long a capture's silent connection may idle before the OS proves it is still there.
80
+ *
81
+ * 15 s, and RAISING IT TO 60 s WAS TRIED AND REVERTED — by this file's own test, which refuses a delay
82
+ * above ~30 s because common NAT idle timeouts start there.
83
+ *
84
+ * The reasoning for raising it was that the async path removed the long-lived connection, so nothing needs
85
+ * an aggressive value. That is true of the async path and FALSE of the `A11Y_SYNC_CAPTURE` escape hatch,
86
+ * which still holds one socket silent for a whole capture. At 60 s the first probe would fire after the
87
+ * reap, so the hatch would be unprotected while the constant looked deliberate — a guard weakened for a
88
+ * reason that did not cover the case it exists for.
89
+ *
90
+ * It stays aggressive until item D explains why THIS path reaps in seconds when the literature says
91
+ * minutes. An unexplained number is not a solved problem.
92
+ *
93
+ * EXPORTED so its test can key on the exact value. Node's own HTTP SERVER calls `setKeepAlive(true, 5000)`
94
+ * on every socket it accepts, so a test that merely looked for "keepalive with a plausible delay" matched
95
+ * the server's call and passed with this hook DELETED — found by mutation, not by reading.
96
+ */
97
+ export const KEEPALIVE_DELAY_MS = 15_000;
98
+ /**
99
+ * A worker address, validated at the BOUNDARY where it enters the program.
100
+ *
101
+ * `requestJson` already calls `new URL(url)`, which throws `ERR_INVALID_URL` on a malformed address — so an
102
+ * empty host dies in under a second, in principle. In practice it did not, and the way it did not is the
103
+ * reason this function exists.
104
+ *
105
+ * `--worker=http://:8765` reached `capture-real-pages.mjs` because nothing there did more than check the
106
+ * value was truthy, and `http://:8765` is truthy. The readiness loop then caught the resulting
107
+ * `ERR_INVALID_URL` in a bare `catch` whose only content was the comment "mid-boot or mid-restart; keep
108
+ * waiting", and so classified a permanent
109
+ * programmer error as a transient network condition: 60 attempts, 5 s apart, per page — then recorded
110
+ * "worker never became ready" as a failure of the PAGE. Four shards spent 29 minutes that way while every
111
+ * worker sat idle, and the run blamed the corpus.
112
+ *
113
+ * So the fix is two-part and both halves are needed: refuse the value here, and stop the readiness loop
114
+ * swallowing what it cannot recover from. Validating without fixing the catch leaves the next unrecoverable
115
+ * error to be absorbed the same way.
116
+ *
117
+ * Node's URL parser does the work. There is no regex here on purpose — a hand-rolled one would accept
118
+ * `http://:8765` again, since the only thing wrong with it is an empty host. Note `http:/x` and
119
+ * `http:///path` DO parse, to host `x` and host `path`; that is the parser's business and not something to
120
+ * second-guess here.
121
+ *
122
+ * @param {string | null | undefined} value the raw `--worker=` or `A11Y_WORKER` value
123
+ * @param {{ source?: string }} [options] what to name in the error, e.g. "--worker"
124
+ * @returns {string} the address, trailing slash removed
125
+ */
126
+ export function assertWorkerUrl(value, { source = "--worker" } = {}) {
127
+ if (typeof value !== "string" || !value.trim()) {
128
+ throw new Error(`${source} is required and was empty. Give a worker address, e.g. ${source}=http://192.0.2.10:8765`);
129
+ }
130
+ const raw = value.trim().replace(/\/$/, "");
131
+ let target;
132
+ try {
133
+ target = new URL(raw);
134
+ }
135
+ catch (cause) {
136
+ // A missing HOST lands here rather than in a hostname check below, and there is no such check on
137
+ // purpose: verified against Node's parser, no `http:`/`https:` URL can parse with an empty hostname —
138
+ // `http://`, `http://:8765` and `http://:/x` all throw. A `!target.hostname` branch would therefore be
139
+ // unreachable, which is this repo's own most-repeated defect, so the message that belongs to that case
140
+ // is folded in here where it can actually be read.
141
+ throw new Error(`${source}=${raw} is not a URL. Expected something like http://192.0.2.10:8765\n`
142
+ + "If the host is missing, that is what a shell variable expanding to nothing looks like: a bash "
143
+ + "array does not survive `nohup bash -c`, and zsh does not word-split a scalar. Both produce "
144
+ + "exactly this.", { cause });
145
+ }
146
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
147
+ throw new Error(`${source}=${raw} must be http: or https:, not ${target.protocol}`);
148
+ }
149
+ return raw;
150
+ }
151
+ /**
152
+ * One request, with a single deadline covering connect, headers and body.
153
+ *
154
+ * @param {string} url
155
+ * @param {{ method?: string, body?: unknown, timeoutMs?: number }} [options]
156
+ * @returns {Promise<{ status: number, ok: boolean, text: string, json: any }>}
157
+ * `json: any`, not `unknown`. This is a JSON body off the wire, and every caller reads named fields
158
+ * from it -- `body.error`, `body.fault`, `health.busy`, `body.transcript`. `unknown` makes each of
159
+ * those a cast, and a cast written to satisfy a checker asserts a shape nobody verified, which is
160
+ * strictly worse than saying the value is untyped. The SHAPE that matters is checked where it is
161
+ * defined: `capture-core`'s `Capture` typedef, and the worker's own `/health` contract.
162
+ */
163
+ export function requestJson(url, { method = "GET", body, timeoutMs = 30_000 } = {}) {
164
+ const target = new URL(url);
165
+ const send = target.protocol === "https:" ? httpsRequest : httpRequest;
166
+ const payload = body === undefined ? null : JSON.stringify(body);
167
+ return new Promise((resolve, reject) => {
168
+ const req = send(target, { method, headers: requestHeaders(payload) }, (res) => {
169
+ collect(res).then((text) => {
170
+ clearTimeout(deadline);
171
+ // `?? 0` because node types `statusCode` optional -- it is set on every client response, and a 0
172
+ // would fall out of the 2xx range exactly as an absent one should, so the fallback cannot change
173
+ // an answer. Named once rather than read three times.
174
+ const status = res.statusCode ?? 0;
175
+ resolve({ status, ok: status >= 200 && status < 300, text, json: parse(text) });
176
+ }, failWith);
177
+ });
178
+ // Our own deadline, because node:http has no equivalent of a total-request timeout: its `timeout`
179
+ // option measures socket INACTIVITY, which a worker holding a connection open while NVDA reads a page
180
+ // never trips. The message says "timed out" so the prose fallback in isTransient still classifies it,
181
+ // and the code says so too for anything that prefers codes.
182
+ const deadline = setTimeout(() => {
183
+ // Typed as an ErrnoException so the `code` this deliberately attaches is describable. Recovery in
184
+ // this repo is keyed on CODES and never on message text (`capture-faults.mjs`), so the field is
185
+ // load-bearing rather than decorative.
186
+ const error = /** @type {NodeJS.ErrnoException} */ (new Error(`Request to ${url} timed out after ${timeoutMs} ms`));
187
+ error.code = "ETIMEDOUT";
188
+ req.destroy(error);
189
+ }, timeoutMs);
190
+ /** @param {unknown} error */
191
+ function failWith(error) {
192
+ clearTimeout(deadline);
193
+ reject(error);
194
+ }
195
+ // KEEPALIVE, KEPT ON PRECAUTIONARY GROUNDS ONLY. ITS DIAGNOSIS WAS WRONG, AND THE REASON IS TOPOLOGY.
196
+ //
197
+ // The story attached to this was: a capture holds a connection SILENT for minutes, so NAT and Wi-Fi
198
+ // power-save reap it as idle. It was argued from *High Performance Browser Networking* -- "Most mobile
199
+ // carriers set a 5-30 minute NAT connection timeout" -- and that passage is about MOBILE CARRIERS AND
200
+ // THE PUBLIC INTERNET.
201
+ //
202
+ // THIS IS A LAN. The control plane and every worker sit on the same /24; the route between them is
203
+ // `link#14`, direct on the link layer with no gateway hop. NAT happens at the router on the
204
+ // way OUT. There is no translation table between these hosts, so there is nothing here that a NAT
205
+ // timeout could expire. The theory was inapplicable from the first line, and the mismatch went
206
+ // unnoticed because the quotation fitted the SYMPTOM.
207
+ //
208
+ // The measurements agree, which is how it was caught rather than believed. Keepalive explicitly OFF,
209
+ // ~26 s silent captures:
210
+ //
211
+ // 8 sequential on one box 0 lost
212
+ // 15 across five boxes at once 0 lost <- the exact condition the 9/40 was measured under
213
+ //
214
+ // 0 of 23. At the old 22% rate P(0 in 23) = 0.78^23 = 0.4%, so the rate really did change -- and these
215
+ // trials ran WITHOUT the keepalive, so it is not shown to be why. Losses went 9/40 -> 0 when it landed
216
+ // and nobody tested removing it: "a green result vouches for the mechanism", committed inside the work
217
+ // that kept finding that trap elsewhere.
218
+ //
219
+ // So: the losses were REAL and their cause is UNKNOWN. NAT is excluded by topology; the worker's own
220
+ // `keepAliveTimeout` and `requestTimeout` were excluded by measurement; the boxes' NIC power settings
221
+ // were already correct. What is left is the Wi-Fi association itself or something transient, and this
222
+ // comment will not guess again.
223
+ //
224
+ // Item A is unaffected and that is the useful part: the async path holds no long connection at all, so
225
+ // it is immune whatever the mechanism was. A STRUCTURAL FIX DOES NOT DEPEND ON THE DIAGNOSIS BEING
226
+ // RIGHT, which is the strongest argument there is for preferring one.
227
+ req.on("socket", (socket) => {
228
+ socket.setKeepAlive(true, KEEPALIVE_DELAY_MS);
229
+ // Nagle would batch the request itself; a capture POST is one small write followed by a wait, so
230
+ // there is nothing to batch and delaying it only adds latency to the request that starts the work.
231
+ socket.setNoDelay(true);
232
+ });
233
+ req.on("error", failWith);
234
+ if (payload !== null)
235
+ req.write(payload);
236
+ req.end();
237
+ });
238
+ }
239
+ /** @param {string | null} payload */
240
+ function requestHeaders(payload) {
241
+ if (payload === null)
242
+ return { accept: "application/json" };
243
+ return {
244
+ accept: "application/json",
245
+ "content-type": "application/json",
246
+ // Explicit, so the worker never has to read a chunked body. Byte length, not character count:
247
+ // a non-ASCII task string would otherwise under-declare and the worker would wait for bytes
248
+ // that never arrive.
249
+ "content-length": Buffer.byteLength(payload),
250
+ };
251
+ }
252
+ /**
253
+ * @param {import("node:http").IncomingMessage} res
254
+ * @returns {Promise<string>}
255
+ */
256
+ function collect(res) {
257
+ return new Promise((resolve, reject) => {
258
+ let text = "";
259
+ res.setEncoding("utf8");
260
+ res.on("data", (/** @type {string} */ chunk) => { text += chunk; });
261
+ res.on("end", () => resolve(text));
262
+ res.on("error", reject);
263
+ });
264
+ }
265
+ /**
266
+ * Parsed JSON, or undefined — a worker error page is not a parse failure worth throwing over.
267
+ * @param {string} text
268
+ */
269
+ function parse(text) {
270
+ try {
271
+ return JSON.parse(text);
272
+ }
273
+ catch {
274
+ return undefined;
275
+ }
276
+ }
277
+ //# sourceMappingURL=worker-http.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-http.mjs","sourceRoot":"","sources":["../src/worker-http.mjs"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /** Linear-interpolated quantile. Fine for the sample sizes here and has no dependencies. */
2
+ /** @param {number[]} values @param {number} q @returns {number|null} */
3
+ export function quantile(values: number[], q: number): number | null;
4
+ /**
5
+ * The shape of one worker's samples.
6
+ *
7
+ * `iqr` is the spread that matters: half the runs fall inside it, so two workers whose IQRs overlap are
8
+ * not distinguishable by these samples however different their medians look.
9
+ */
10
+ /**
11
+ * @param {number[]} values
12
+ * @returns {Summary|null}
13
+ */
14
+ export function describe(values: number[]): Summary | null;
15
+ /**
16
+ * Compare workers and say, conservatively, whether a difference is supported.
17
+ *
18
+ * The bar is deliberately crude and deliberately strict: medians must differ by more than the wider
19
+ * worker's IQR, and the two IQRs must not overlap. That is a weaker claim than a significance test and a
20
+ * much stronger one than "the mean was higher", which is what produced every wrong conclusion this
21
+ * module was written after. When the samples do not clear it, the verdict is "not distinguishable" —
22
+ * which is a real answer, not a failure to find one.
23
+ *
24
+ * @param {Record<string, number[]>} samplesByWorker
25
+ * @returns {{ stats: Record<string, Summary>, slowest: string|null, fastest: string|null,
26
+ * distinguishable: boolean, verdict: string }}
27
+ */
28
+ export function compareWorkers(samplesByWorker: Record<string, number[]>): {
29
+ stats: Record<string, Summary>;
30
+ slowest: string | null;
31
+ fastest: string | null;
32
+ distinguishable: boolean;
33
+ verdict: string;
34
+ };
35
+ /**
36
+ * Recovery rate per worker — the reliability statistic, as opposed to the speed one.
37
+ *
38
+ * Counted from the worker's own `vitals.recoveries` across the run, because a guest whose screen reader
39
+ * keeps dying still returns evidence and still records zero failures. Speed and reliability are separate
40
+ * questions and a fast worker can be the unreliable one.
41
+ *
42
+ * @param {Record<string, {recoveries: number, captures: number}>} deltas
43
+ * @returns {Record<string, number | null>} null where the worker captured nothing — "no idea", which
44
+ * must not be confused with a rate of zero ("perfectly reliable").
45
+ */
46
+ export function recoveryRates(deltas: Record<string, {
47
+ recoveries: number;
48
+ captures: number;
49
+ }>): Record<string, number | null>;
50
+ /**
51
+ * One worker's samples, summarised.
52
+ *
53
+ * Named as a typedef rather than repeated inline because four functions pass it around, and this module's
54
+ * whole purpose is refusing to claim a difference the samples do not support — so `q1`, `q3` and `iqr`
55
+ * travelling together, as one thing with one name, is the point rather than a formality.
56
+ */
57
+ export type Summary = {
58
+ n: number;
59
+ median: number;
60
+ q1: number;
61
+ q3: number;
62
+ iqr: number;
63
+ min: number;
64
+ max: number;
65
+ };
66
+ //# sourceMappingURL=worker-stats.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-stats.d.mts","sourceRoot":"","sources":["../src/worker-stats.mjs"],"names":[],"mappings":"AAuCA,4FAA4F;AAC5F,wEAAwE;AACxE,iCADY,MAAM,EAAE,KAAiB,MAAM,GAAc,MAAM,GAAC,IAAI,CAOnE;AAED;;;;;GAKG;AACH;;;GAGG;AACH,iCAHW,MAAM,EAAE,GACN,OAAO,GAAC,IAAI,CAexB;AAOD;;;;;;;;;;;;GAYG;AACH,gDAJW,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GACtB;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,GAAC,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,GAAC,IAAI,CAAC;IAC3E,eAAe,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAqCzD;AAED;;;;;;;;;;GAUG;AACH,sCAJW,MAAM,CAAC,MAAM,EAAE;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,CAAC,GACpD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAUzC;;;;;;;;sBAtHY;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAC/D,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAC"}
@@ -0,0 +1,143 @@
1
+ // @ts-check
2
+ /**
3
+ * Robust statistics for comparing workers, and an explicit refusal to call a difference real when the
4
+ * samples do not support it.
5
+ *
6
+ * This module exists because of a specific, repeated mistake: concluding from one measurement. Over one
7
+ * session a 2x difference between two guests was attributed, in turn, to the display being blanked, to
8
+ * in-memory accumulation, to background CPU, to Edge's launch, to the Edge profile, to the sweep, and to
9
+ * Edge's startup boost. Every one of those was a single measurement, and every one was wrong.
10
+ *
11
+ * Two things caused that, and both are fixed here rather than in a comment.
12
+ *
13
+ * **Means lied.** One 63 s outlier in an eight-run sample moved the mean by 5 s and made a healthy
14
+ * worker look broken. Capture time has a long right tail by nature — a mute screen reader costs ~86 s
15
+ * against a normal ~12 s — so the mean measures how often the tail was hit, not what a capture costs.
16
+ * The median and the interquartile range do not move when one run goes bad.
17
+ *
18
+ * **Sequential sampling confounded.** Measuring worker A for five minutes and then worker B for five
19
+ * minutes attributes any drift in the host during those ten minutes to the difference between the
20
+ * workers. Interleaving round-robin makes drift common to all of them.
21
+ */
22
+ /**
23
+ * One worker's samples, summarised.
24
+ *
25
+ * Named as a typedef rather than repeated inline because four functions pass it around, and this module's
26
+ * whole purpose is refusing to claim a difference the samples do not support — so `q1`, `q3` and `iqr`
27
+ * travelling together, as one thing with one name, is the point rather than a formality.
28
+ *
29
+ * @typedef {{n: number, median: number, q1: number, q3: number, iqr: number,
30
+ * min: number, max: number}} Summary
31
+ */
32
+ /** Below this many rounds, report the numbers but never claim a difference. */
33
+ const MIN_ROUNDS_FOR_A_VERDICT = 5;
34
+ /** @param {number[]} values @returns {number[]} */
35
+ const sorted = (values) => [...values].sort((a, b) => a - b);
36
+ /** Linear-interpolated quantile. Fine for the sample sizes here and has no dependencies. */
37
+ /** @param {number[]} values @param {number} q @returns {number|null} */
38
+ export function quantile(values, q) {
39
+ if (!values.length)
40
+ return null;
41
+ const s = sorted(values);
42
+ const position = (s.length - 1) * q;
43
+ const low = Math.floor(position), high = Math.ceil(position);
44
+ return low === high ? s[low] : s[low] + (s[high] - s[low]) * (position - low);
45
+ }
46
+ /**
47
+ * The shape of one worker's samples.
48
+ *
49
+ * `iqr` is the spread that matters: half the runs fall inside it, so two workers whose IQRs overlap are
50
+ * not distinguishable by these samples however different their medians look.
51
+ */
52
+ /**
53
+ * @param {number[]} values
54
+ * @returns {Summary|null}
55
+ */
56
+ export function describe(values) {
57
+ if (!values.length)
58
+ return null;
59
+ // Non-null by construction: `quantile` returns null only for an empty list, and the guard above has
60
+ // already excluded that. Written as a local rather than an assertion so the reason is stated once.
61
+ const q1 = /** @type {number} */ (quantile(values, 0.25));
62
+ const q3 = /** @type {number} */ (quantile(values, 0.75));
63
+ return {
64
+ n: values.length,
65
+ median: /** @type {number} */ (quantile(values, 0.5)),
66
+ q1, q3, iqr: q3 - q1,
67
+ min: Math.min(...values),
68
+ max: Math.max(...values),
69
+ };
70
+ }
71
+ /** Do two interquartile ranges overlap at all? @param {Summary} a @param {Summary} b */
72
+ function overlaps(a, b) {
73
+ return a.q1 <= b.q3 && b.q1 <= a.q3;
74
+ }
75
+ /**
76
+ * Compare workers and say, conservatively, whether a difference is supported.
77
+ *
78
+ * The bar is deliberately crude and deliberately strict: medians must differ by more than the wider
79
+ * worker's IQR, and the two IQRs must not overlap. That is a weaker claim than a significance test and a
80
+ * much stronger one than "the mean was higher", which is what produced every wrong conclusion this
81
+ * module was written after. When the samples do not clear it, the verdict is "not distinguishable" —
82
+ * which is a real answer, not a failure to find one.
83
+ *
84
+ * @param {Record<string, number[]>} samplesByWorker
85
+ * @returns {{ stats: Record<string, Summary>, slowest: string|null, fastest: string|null,
86
+ * distinguishable: boolean, verdict: string }}
87
+ */
88
+ export function compareWorkers(samplesByWorker) {
89
+ /** @type {Record<string, Summary>} */
90
+ const stats = {};
91
+ for (const [worker, values] of Object.entries(samplesByWorker)) {
92
+ const description = describe(values);
93
+ if (description)
94
+ stats[worker] = description;
95
+ }
96
+ const names = Object.keys(stats);
97
+ if (names.length < 2) {
98
+ return { stats, slowest: null, fastest: null, distinguishable: false, verdict: "need at least two workers with samples" };
99
+ }
100
+ const rounds = Math.min(...names.map((n) => stats[n].n));
101
+ const byMedian = [...names].sort((a, b) => stats[a].median - stats[b].median);
102
+ const fastest = byMedian[0], slowest = byMedian[byMedian.length - 1];
103
+ const gap = stats[slowest].median - stats[fastest].median;
104
+ const widest = Math.max(stats[fastest].iqr, stats[slowest].iqr);
105
+ if (rounds < MIN_ROUNDS_FOR_A_VERDICT) {
106
+ return {
107
+ stats, slowest, fastest, distinguishable: false,
108
+ verdict: `only ${rounds} round(s) — too few to claim anything. Run at least ${MIN_ROUNDS_FOR_A_VERDICT}.`,
109
+ };
110
+ }
111
+ if (gap <= widest || overlaps(stats[fastest], stats[slowest])) {
112
+ return {
113
+ stats, slowest, fastest, distinguishable: false,
114
+ verdict: `NOT DISTINGUISHABLE: medians differ by ${gap.toFixed(1)} but the spread is ${widest.toFixed(1)} ` +
115
+ "and the interquartile ranges overlap. These samples do not support a difference between the workers.",
116
+ };
117
+ }
118
+ return {
119
+ stats, slowest, fastest, distinguishable: true,
120
+ verdict: `${slowest} is slower than ${fastest} by ${gap.toFixed(1)} (medians), ` +
121
+ `with non-overlapping interquartile ranges over ${rounds} interleaved rounds.`,
122
+ };
123
+ }
124
+ /**
125
+ * Recovery rate per worker — the reliability statistic, as opposed to the speed one.
126
+ *
127
+ * Counted from the worker's own `vitals.recoveries` across the run, because a guest whose screen reader
128
+ * keeps dying still returns evidence and still records zero failures. Speed and reliability are separate
129
+ * questions and a fast worker can be the unreliable one.
130
+ *
131
+ * @param {Record<string, {recoveries: number, captures: number}>} deltas
132
+ * @returns {Record<string, number | null>} null where the worker captured nothing — "no idea", which
133
+ * must not be confused with a rate of zero ("perfectly reliable").
134
+ */
135
+ export function recoveryRates(deltas) {
136
+ /** @type {Record<string, number | null>} */
137
+ const rates = {};
138
+ for (const [worker, d] of Object.entries(deltas)) {
139
+ rates[worker] = d.captures > 0 ? d.recoveries / d.captures : null;
140
+ }
141
+ return rates;
142
+ }
143
+ //# sourceMappingURL=worker-stats.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-stats.mjs","sourceRoot":"","sources":["../src/worker-stats.mjs"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,99 @@
1
1
  {
2
2
  "name": "@a11ign/screenreader-fleet",
3
- "version": "0.0.0-reserved.0",
3
+ "version": "0.1.0",
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.",
4
5
  "license": "AGPL-3.0-or-later",
5
- "description": "Reserved name. Real releases are published by a11ign/screenreader-fleet's release workflow.",
6
- "repository": { "type": "git", "url": "git+https://github.com/a11ign/screenreader-fleet.git" }
7
- }
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./health": {
13
+ "default": "./dist/worker-health.mjs"
14
+ },
15
+ "./capacity": {
16
+ "default": "./dist/host-capacity.mjs"
17
+ },
18
+ "./probe-outcome": {
19
+ "types": "./dist/probe-outcome.d.mts",
20
+ "default": "./dist/probe-outcome.mjs"
21
+ },
22
+ "./worker-http": {
23
+ "types": "./dist/worker-http.d.mts",
24
+ "default": "./dist/worker-http.mjs"
25
+ },
26
+ "./cli-flags": {
27
+ "default": "./dist/cli-flags.mjs"
28
+ },
29
+ "./transient-fault": {
30
+ "types": "./dist/transient-fault.d.mts",
31
+ "default": "./dist/transient-fault.mjs"
32
+ },
33
+ "./capture-client": {
34
+ "types": "./dist/capture-client.d.mts",
35
+ "default": "./dist/capture-client.mjs"
36
+ },
37
+ "./host-address": {
38
+ "types": "./dist/host-address.d.mts",
39
+ "default": "./dist/host-address.mjs"
40
+ },
41
+ "./fleet-env": {
42
+ "types": "./dist/fleet-env.d.mts",
43
+ "default": "./dist/fleet-env.mjs"
44
+ },
45
+ "./fleet-consistency": {
46
+ "types": "./dist/fleet-consistency.d.mts",
47
+ "default": "./dist/fleet-consistency.mjs"
48
+ },
49
+ "./worker-code-check": {
50
+ "types": "./dist/worker-code-check.d.mts",
51
+ "default": "./dist/worker-code-check.mjs"
52
+ }
53
+ },
54
+ "bin": {
55
+ "a11ign-doctor": "./dist/doctor.mjs",
56
+ "a11ign-worker-code": "./dist/check-worker-code.mjs",
57
+ "a11ign-worker-compare": "./dist/compare-workers.mjs",
58
+ "a11ign-worker-ctl": "./src/local-worker/worker-ctl.sh",
59
+ "a11ign-worker-deploy": "./dist/deploy-worker.mjs"
60
+ },
61
+ "files": [
62
+ "dist",
63
+ "src/local-worker",
64
+ "src/provisioning",
65
+ "README.md",
66
+ "LICENSE"
67
+ ],
68
+ "dependencies": {
69
+ "@a11ign/judge": "0.1.0",
70
+ "@a11ign/screenreader-worker": "0.1.0"
71
+ },
72
+ "devDependencies": {
73
+ "@a11ign/evidence": "0.1.0",
74
+ "yaml": "^2.9.1",
75
+ "typescript": "^6.0.3"
76
+ },
77
+ "engines": {
78
+ "node": ">=20"
79
+ },
80
+ "publishConfig": {
81
+ "access": "public"
82
+ },
83
+ "repository": {
84
+ "type": "git",
85
+ "url": "git+https://github.com/a11ign/screenreader-fleet.git",
86
+ "directory": "packages/worker-fleet"
87
+ },
88
+ "homepage": "https://github.com/a11ign/screenreader-fleet",
89
+ "keywords": [
90
+ "accessibility",
91
+ "a11y",
92
+ "nvda",
93
+ "screen-reader",
94
+ "windows",
95
+ "utm",
96
+ "vm"
97
+ ],
98
+ "scripts": {}
99
+ }