@a11ign/screenreader-fleet 0.0.0-reserved.0 → 0.2.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/LICENSE +661 -0
- package/README.md +94 -2
- package/dist/capture-client.d.mts +49 -0
- package/dist/capture-client.d.mts.map +1 -0
- package/dist/capture-client.mjs +352 -0
- package/dist/capture-client.mjs.map +1 -0
- package/dist/check-worker-code.d.mts +34 -0
- package/dist/check-worker-code.d.mts.map +1 -0
- package/dist/check-worker-code.mjs +142 -0
- package/dist/check-worker-code.mjs.map +1 -0
- package/dist/cli-flags.d.mts +71 -0
- package/dist/cli-flags.d.mts.map +1 -0
- package/dist/cli-flags.mjs +207 -0
- package/dist/cli-flags.mjs.map +1 -0
- package/dist/code-drift.d.mts +140 -0
- package/dist/code-drift.d.mts.map +1 -0
- package/dist/code-drift.mjs +284 -0
- package/dist/code-drift.mjs.map +1 -0
- package/dist/command-line-census.d.mts +33 -0
- package/dist/command-line-census.d.mts.map +1 -0
- package/dist/command-line-census.mjs +96 -0
- package/dist/command-line-census.mjs.map +1 -0
- package/dist/compare-workers.d.mts +3 -0
- package/dist/compare-workers.d.mts.map +1 -0
- package/dist/compare-workers.mjs +332 -0
- package/dist/compare-workers.mjs.map +1 -0
- package/dist/control-plane-isolation.d.mts +45 -0
- package/dist/control-plane-isolation.d.mts.map +1 -0
- package/dist/control-plane-isolation.mjs +67 -0
- package/dist/control-plane-isolation.mjs.map +1 -0
- package/dist/deploy-worker.d.mts +3 -0
- package/dist/deploy-worker.d.mts.map +1 -0
- package/dist/deploy-worker.mjs +333 -0
- package/dist/deploy-worker.mjs.map +1 -0
- package/dist/doctor.d.mts +216 -0
- package/dist/doctor.d.mts.map +1 -0
- package/dist/doctor.mjs +962 -0
- package/dist/doctor.mjs.map +1 -0
- package/dist/fleet-consistency.d.mts +235 -0
- package/dist/fleet-consistency.d.mts.map +1 -0
- package/dist/fleet-consistency.mjs +436 -0
- package/dist/fleet-consistency.mjs.map +1 -0
- package/dist/fleet-env.d.mts +228 -0
- package/dist/fleet-env.d.mts.map +1 -0
- package/dist/fleet-env.mjs +509 -0
- package/dist/fleet-env.mjs.map +1 -0
- package/dist/fleet-scripts.d.mts +11 -0
- package/dist/fleet-scripts.d.mts.map +1 -0
- package/dist/fleet-scripts.mjs +41 -0
- package/dist/fleet-scripts.mjs.map +1 -0
- package/dist/git-safe-env.d.mts +10 -0
- package/dist/git-safe-env.d.mts.map +1 -0
- package/dist/git-safe-env.mjs +44 -0
- package/dist/git-safe-env.mjs.map +1 -0
- package/dist/guest-run.d.mts +26 -0
- package/dist/guest-run.d.mts.map +1 -0
- package/dist/guest-run.mjs +164 -0
- package/dist/guest-run.mjs.map +1 -0
- package/dist/host-address.d.mts +33 -0
- package/dist/host-address.d.mts.map +1 -0
- package/dist/host-address.mjs +105 -0
- package/dist/host-address.mjs.map +1 -0
- package/dist/host-capacity.d.mts +64 -0
- package/dist/host-capacity.d.mts.map +1 -0
- package/dist/host-capacity.mjs +152 -0
- package/dist/host-capacity.mjs.map +1 -0
- package/dist/host-metrics.d.mts +116 -0
- package/dist/host-metrics.d.mts.map +1 -0
- package/dist/host-metrics.mjs +201 -0
- package/dist/host-metrics.mjs.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/local-vm.d.ts +125 -0
- package/dist/local-vm.d.ts.map +1 -0
- package/dist/local-vm.js +360 -0
- package/dist/local-vm.js.map +1 -0
- package/dist/measure-guard.d.mts +34 -0
- package/dist/measure-guard.d.mts.map +1 -0
- package/dist/measure-guard.mjs +73 -0
- package/dist/measure-guard.mjs.map +1 -0
- package/dist/normalise-fleet.d.mts +2 -0
- package/dist/normalise-fleet.d.mts.map +1 -0
- package/dist/normalise-fleet.mjs +76 -0
- package/dist/normalise-fleet.mjs.map +1 -0
- package/dist/npm-cli-executable.d.mts +42 -0
- package/dist/npm-cli-executable.d.mts.map +1 -0
- package/dist/npm-cli-executable.mjs +159 -0
- package/dist/npm-cli-executable.mjs.map +1 -0
- package/dist/probe-outcome.d.mts +89 -0
- package/dist/probe-outcome.d.mts.map +1 -0
- package/dist/probe-outcome.mjs +104 -0
- package/dist/probe-outcome.mjs.map +1 -0
- package/dist/protocol-guard.d.mts +34 -0
- package/dist/protocol-guard.d.mts.map +1 -0
- package/dist/protocol-guard.mjs +121 -0
- package/dist/protocol-guard.mjs.map +1 -0
- package/dist/source-walk.d.mts +12 -0
- package/dist/source-walk.d.mts.map +1 -0
- package/dist/source-walk.mjs +56 -0
- package/dist/source-walk.mjs.map +1 -0
- package/dist/transient-fault.d.mts +6 -0
- package/dist/transient-fault.d.mts.map +1 -0
- package/dist/transient-fault.mjs +86 -0
- package/dist/transient-fault.mjs.map +1 -0
- package/dist/utm-deprecated.d.mts +6 -0
- package/dist/utm-deprecated.d.mts.map +1 -0
- package/dist/utm-deprecated.mjs +23 -0
- package/dist/utm-deprecated.mjs.map +1 -0
- package/dist/worker-code-check.d.mts +29 -0
- package/dist/worker-code-check.d.mts.map +1 -0
- package/dist/worker-code-check.mjs +85 -0
- package/dist/worker-code-check.mjs.map +1 -0
- package/dist/worker-health.d.mts +56 -0
- package/dist/worker-health.d.mts.map +1 -0
- package/dist/worker-health.mjs +73 -0
- package/dist/worker-health.mjs.map +1 -0
- package/dist/worker-http.d.mts +103 -0
- package/dist/worker-http.d.mts.map +1 -0
- package/dist/worker-http.mjs +277 -0
- package/dist/worker-http.mjs.map +1 -0
- package/dist/worker-stats.d.mts +66 -0
- package/dist/worker-stats.d.mts.map +1 -0
- package/dist/worker-stats.mjs +143 -0
- package/dist/worker-stats.mjs.map +1 -0
- package/package.json +96 -4
- package/src/local-worker/autounattend.xml +280 -0
- package/src/local-worker/build-vm.sh +218 -0
- package/src/local-worker/clone-worker.sh +141 -0
- package/src/local-worker/create-utm-vm.sh +202 -0
- package/src/local-worker/fetch-windows-iso.sh +238 -0
- package/src/local-worker/first-boot.cmd +58 -0
- package/src/local-worker/worker-ctl.sh +442 -0
- package/src/provisioning/README.md +28 -0
- package/src/provisioning/apply-foreground-lock-timeout.ps1 +71 -0
- package/src/provisioning/bare-metal/README.md +213 -0
- package/src/provisioning/bare-metal/a11y-bootstrap.service +58 -0
- package/src/provisioning/bare-metal/autounattend.xml +428 -0
- package/src/provisioning/bare-metal/serve-bootstrap.sh +86 -0
- package/src/provisioning/bootstrap-control-plane.sh +463 -0
- package/src/provisioning/bootstrap-windows-worker.ps1 +649 -0
- package/src/provisioning/build-lean-worker-image.ps1 +275 -0
- package/src/provisioning/diagnose-nvda-worker.ps1 +174 -0
- package/src/provisioning/provision-nvda-worker.ps1 +827 -0
- package/src/provisioning/set-display-mode.ps1 +411 -0
- package/src/provisioning/stamp-provision-revision.ps1 +184 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Are the guests actually interchangeable?
|
|
4
|
+
*
|
|
5
|
+
* The pool assumes they are. `captureAcrossPool` dispatches a case to whichever worker is free, the
|
|
6
|
+
* cache lets any guest reuse another's evidence, and a good/bad pair is only comparable because both
|
|
7
|
+
* halves came from equivalent machines. Every one of those assumptions is silently false the moment
|
|
8
|
+
* two guests differ.
|
|
9
|
+
*
|
|
10
|
+
* They did differ, twice in one day, and neither was caught by tooling:
|
|
11
|
+
*
|
|
12
|
+
* - Edge auto-updated to 151 on one guest while the others stayed on 150, despite the updater being
|
|
13
|
+
* policy-disabled. The cache key covers `browserVersion`, so evidence was not corrupted — but hit
|
|
14
|
+
* rates halved and the corpus became heterogeneous.
|
|
15
|
+
* - `StartupBoostEnabled` read 1 on two guests and 0 on a third. Nothing keys on that at all.
|
|
16
|
+
*
|
|
17
|
+
* The first was noticed by reading a boot log by eye; the second by a human looking at a screenshot of
|
|
18
|
+
* a guest console. Neither is a detection mechanism. This is.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT fatal. A run on slightly mismatched guests is worse than one on matched guests and
|
|
21
|
+
* far better than no run, and this project's rule is that a diagnostic must never be the thing that
|
|
22
|
+
* takes the pool offline. It reports; the operator decides.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Fields that must match across the fleet, and why each one matters.
|
|
26
|
+
*
|
|
27
|
+
* `workerCode` is absent on purpose: it changes when a comment changes, and the deploy tooling already
|
|
28
|
+
* verifies it against the checkout. Flagging it here would cry wolf on every reworded line.
|
|
29
|
+
*/
|
|
30
|
+
export const MUST_MATCH = [
|
|
31
|
+
{ path: "browserVersion", why: "Edge renders and announces differently across releases; it is in the cache key" },
|
|
32
|
+
{ path: "screenReaderVersion", why: "NVDA's wording changes between releases; it is in the cache key" },
|
|
33
|
+
{ path: "guidepupVersion", why: "guidepup parses NVDA's speech before we see it — 0.29.2 emitted an " +
|
|
34
|
+
"intermittent U+FFFC where 0.31.0 emits a consistent empty segment, for the same page" },
|
|
35
|
+
{ path: "windowsVersion", why: "a second OS image would blend two corpora into one" },
|
|
36
|
+
{ path: "architecture", why: "ARM64 and x64 guests are not interchangeable evidence" },
|
|
37
|
+
{ path: "captureProtocol", why: "a guest on an older protocol produces evidence that means something else" },
|
|
38
|
+
{ path: "browserProfile", why: "a COLD browser profile is not the same evidence as a warm one: a fresh " +
|
|
39
|
+
"`--user-data-dir` shows Edge's first-run surface, which NVDA's quick-nav escapes into and records " +
|
|
40
|
+
"as phantom page content, and a learning profile is what drove the U+FFFC artefact from 3% to 31% " +
|
|
41
|
+
"of affected captures. Two guests on different profiles are not interchangeable, and it is in the " +
|
|
42
|
+
"cache key for the same reason. `adopted` on both sides means both predate the stamp, which is a " +
|
|
43
|
+
"match rather than an unknown" },
|
|
44
|
+
{ path: "screenReaderSettings", why: "a guest capturing with `reportLanguage` off is blind to 3.1.2 and " +
|
|
45
|
+
"one with it on is not — the same page yields different transcripts, so the two are not " +
|
|
46
|
+
"interchangeable evidence. It is in the cache key for the same reason" },
|
|
47
|
+
// A CACHE KEY that was not a consistency field, which is the worst combination.
|
|
48
|
+
//
|
|
49
|
+
// `provisionRevision` records what the guest actually has -- NVDA's config, Edge's policies,
|
|
50
|
+
// ForegroundLockTimeout -- all of which change the evidence. It is already in the cache key
|
|
51
|
+
// (`capture-cache.mjs`), so a fleet where one guest has been re-provisioned and the others report
|
|
52
|
+
// `"unstamped"` produces two evidence populations. Nothing warned: the cache merely stopped hitting,
|
|
53
|
+
// which reads as ordinary churn rather than as a split fleet. Re-provision the pool together.
|
|
54
|
+
{ path: "provisionRevision", why: "a re-provisioned guest has different NVDA/Edge configuration, and " +
|
|
55
|
+
"it is already a cache key -- a split fleet shows up only as unexplained cache misses" },
|
|
56
|
+
// A UNIFORMITY CLAIM PRINTED OVER A PROPERTY NOTHING CHECKED, which is the worst failure this file
|
|
57
|
+
// can have -- worse than missing a field, because the operator was told the opposite.
|
|
58
|
+
//
|
|
59
|
+
// `fleet:status` printed "fleet CONSISTENT across 10 of 10 -- these workers are interchangeable for
|
|
60
|
+
// capture" at 2026-09-22T18:21Z over a fleet running 1024x768 on five guests and 640x480 on the other
|
|
61
|
+
// five (#1953; measured read-only on the live fleet, #1567/#1955). The nine fields above were all
|
|
62
|
+
// matched and all true; the sentence they produced was not.
|
|
63
|
+
//
|
|
64
|
+
// `provisionRevision` does not already cover it, and this is the point. That stamp is written by the
|
|
65
|
+
// provisioning script itself, so it records WHICH PROVISIONING RAN rather than what it achieved: a
|
|
66
|
+
// guest whose display-driver install failed still gets the new stamp. All ten guests can report an
|
|
67
|
+
// identical revision while five of them sit at 640x480 on a fallback adapter. A stamp cannot fail
|
|
68
|
+
// closed on a thing it does not read.
|
|
69
|
+
{ path: "displayMode", why: "a guest capturing on a 640x480 desktop and one on 1024x768 are not " +
|
|
70
|
+
"interchangeable evidence: the window is what a real display holds (#1561), and a page's CSS can " +
|
|
71
|
+
"hide content below a width -- metoffice hides its h1 under 1280px. NOT a cache key: adding one " +
|
|
72
|
+
"invalidates every cached capture, which is a separate and far more expensive decision than making " +
|
|
73
|
+
"this sentence honest" },
|
|
74
|
+
// The PIN, which is not the same field as the DESKTOP above (#1561). `displayMode` is what the screen
|
|
75
|
+
// holds; this is what the worker asks Edge for, and a guest still running the pre-pin code asks for
|
|
76
|
+
// nothing at all and gets `--start-maximized`. Both guests report the same `captureProtocol`, so the
|
|
77
|
+
// protocol cannot separate them and a rolling deploy would otherwise split the corpus in silence --
|
|
78
|
+
// which is the shape `provisionRevision` two entries up is here for, one deploy later.
|
|
79
|
+
//
|
|
80
|
+
// UNLIKE `displayMode`, this one IS a cache key (`environmentKey`), and that is a deliberate asymmetry
|
|
81
|
+
// rather than an oversight: pinning the window is what makes the width a property of the capture rather
|
|
82
|
+
// than of the box it ran on, so it is the value a stored capture can honestly be keyed by.
|
|
83
|
+
{ path: "windowSize", why: "a capture taken in a pinned 1024x768 window and one taken maximized on " +
|
|
84
|
+
"whatever the desktop gave are not interchangeable evidence -- a page's CSS can hide content below " +
|
|
85
|
+
"a width, and metoffice hides its h1 under 1280px. It is in the cache key (#1561), so a split " +
|
|
86
|
+
"fleet also writes two evidence populations" },
|
|
87
|
+
// THE RUNTIME THE EVIDENCE WAS PRODUCED BY -- and it is here as the THIRD STEP of `ceo`'s ruling on
|
|
88
|
+
// #2063 rather than as a field arriving fresh: report it, pin provisioning so the fleet converges, and
|
|
89
|
+
// only then may it gate (#2170). The first two are done. The pin is `worker_node_version` in the worker
|
|
90
|
+
// role's `defaults/main.yml`, and the fleet converged on v24.20.0 at 2026-09-23T18:02Z -- read off all
|
|
91
|
+
// ten guests' own `/health`, 10 of 10 reporting one value, against the 5/5 split measured at 06:55Z
|
|
92
|
+
// that morning (`orchestrator`, #2170).
|
|
93
|
+
//
|
|
94
|
+
// WHY THE PRECONDITION WAS A `/health` READING AND NOT `provisionRevision`, which is the part worth
|
|
95
|
+
// keeping: at 17:57Z, immediately after a CLEAN provision, three of the five upgraded guests still
|
|
96
|
+
// reported v24.19.0. `/health` reports the runtime of the RUNNING worker process, not the installed
|
|
97
|
+
// one, and only the deploy's restart moved them. That makes the reading stricter than "the pin
|
|
98
|
+
// landed" -- which is the property this field needs, because the value it compares is written into
|
|
99
|
+
// every corpus record BY that running process.
|
|
100
|
+
{ path: "nodeVersion", why: "the guest's Node runtime is recorded into every corpus record " +
|
|
101
|
+
"(`export-screenreader-dataset.mjs`), so two guests on different runtimes write two populations " +
|
|
102
|
+
"into one corpus and a good/bad pair can straddle them -- the comparison that pair exists to make " +
|
|
103
|
+
"then carries a runtime difference nobody asked for. NOT a cache key: adding one invalidates every " +
|
|
104
|
+
"capture stamped before it, which is the same separate and far more expensive decision " +
|
|
105
|
+
"`displayMode` two entries up records" },
|
|
106
|
+
];
|
|
107
|
+
/**
|
|
108
|
+
* THE THIRD CHANNEL: fields that are COMPARED AND NAMED, and that gate nothing — #2063.
|
|
109
|
+
*
|
|
110
|
+
* `MUST_MATCH` above is a capture gate in both its channels. A disagreement there sets `consistent:
|
|
111
|
+
* false` and `capture-fleet-guard` exits 3; a field any compared guest fails to report lands in
|
|
112
|
+
* `fields.coverage` and, since #2047, exits 3 as well. So a field that belongs in the REPORT but must not
|
|
113
|
+
* refuse a run has nowhere to go in either — and adding it to one of them anyway is not a small
|
|
114
|
+
* mis-filing: `displayAdapter` reads one value on nine guests and another on the tenth, one letter apart,
|
|
115
|
+
* so a gate on it would refuse every capture on this fleet for ever.
|
|
116
|
+
*
|
|
117
|
+
* THIS CHANNEL IS A WAITING ROOM FOR SOME FIELDS AND A PERMANENT HOME FOR OTHERS, and the two must not be
|
|
118
|
+
* confused. `nodeVersion` was the first kind: `ceo` ruled the order on #2063, 2026-09-23 — **report it,
|
|
119
|
+
* then pin provisioning so the fleet converges, and only then may it join `MUST_MATCH`** — and #2170 is
|
|
120
|
+
* that last step, taken once the fleet read one runtime on all ten guests. `displayAdapter` is the
|
|
121
|
+
* second kind: the difference it reports is HARDWARE (`Intel(R) UHD Graphics 630` against
|
|
122
|
+
* `Intel(R) HD Graphics 630` on one box), so no provisioning run converges it and it can never graduate
|
|
123
|
+
* the way `nodeVersion` did. A field arriving here needs its exit named, or "not yet a gate" decays into
|
|
124
|
+
* "never looked at again".
|
|
125
|
+
*
|
|
126
|
+
* So these fields are compared exactly as the gating ones are, and their drift is named on the verdict
|
|
127
|
+
* line with each guest's value — but they contribute to NEITHER `mismatches` NOR `fields.coverage`, which
|
|
128
|
+
* are the only two things any gate reads. A fleet differing on one of them is `consistent: true`.
|
|
129
|
+
*
|
|
130
|
+
* A field NOBODY reports is `unreported` here rather than silent, which is the same distinction #1997
|
|
131
|
+
* drew for the gating fields: a field compared on nobody draws no values to disagree about and would
|
|
132
|
+
* otherwise read exactly like a field every guest agrees on. It is the state `displayAdapter` sat in
|
|
133
|
+
* until the worker carrying it was deployed on 2026-09-23, and the state any field added here starts in,
|
|
134
|
+
* and it must be readable as such without refusing anything.
|
|
135
|
+
*/
|
|
136
|
+
export const REPORTED_ONLY = [
|
|
137
|
+
// `ceo`'s fold-in on the same row: "nothing in this repo reports the display ADAPTER either ... whatever
|
|
138
|
+
// shape this row lands for 'reported, visible, not yet a gate', the adapter belongs in it rather than in
|
|
139
|
+
// a fourth row." It is the seam `displayMode` sat on -- on 2026-09-22, workers 2-6 on the Intel adapter,
|
|
140
|
+
// 7-11 on Microsoft's Basic Display Adapter after a driver install failed rc 1014 -- and it is a DIFFERENT field
|
|
141
|
+
// from the driver VERSION, which `ceo` ruled on #1567 does not split the fleet.
|
|
142
|
+
{ path: "displayAdapter", why: "the adapter is what decides whether a pinned display mode can be held " +
|
|
143
|
+
"at all -- on 2026-09-22 workers 7-11 were on Microsoft's Basic Display Adapter and captured at " +
|
|
144
|
+
"640x480 under a `provisionRevision` identical to their peers' (#1955); the pinned Intel driver " +
|
|
145
|
+
"install resolved that, and all ten read an Intel adapter at CM_PROB_NONE on 2026-09-23. NOT a " +
|
|
146
|
+
"gate, and no longer because nobody reports it: 10 of 10 guests report it since " +
|
|
147
|
+
"2026-09-23T18:02Z, reading `Intel(R) UHD Graphics 630` on nine and `Intel(R) HD Graphics 630` " +
|
|
148
|
+
"on one as of 2026-09-23. That single letter is a real hardware difference rather than a driver " +
|
|
149
|
+
"fallback, so no provisioning run can converge it and a gate would refuse that guest for ever " +
|
|
150
|
+
"(#2063, reading on #2170)" },
|
|
151
|
+
];
|
|
152
|
+
/**
|
|
153
|
+
* One field the guests disagree about, and the guests' values for it.
|
|
154
|
+
*
|
|
155
|
+
* Named once because it is produced by `fleetConsistency` and consumed by `describeMismatches`, and the
|
|
156
|
+
* two had already drifted apart the moment either was annotated — `object` on one side against
|
|
157
|
+
* `Record<string, unknown>` on the other, which typecheck caught in the test that calls them in sequence.
|
|
158
|
+
* Two spellings of one shape is the duplication this repo names as its most expensive recurring defect,
|
|
159
|
+
* and a typedef is the cheapest form of "delete a copy".
|
|
160
|
+
*
|
|
161
|
+
* @typedef {{field: string, why: string, values: Record<string, unknown>}} Mismatch
|
|
162
|
+
*/
|
|
163
|
+
/** Edge policy values every guest must agree on, checked separately because they come from /diagnostics. */
|
|
164
|
+
export const POLICY_MUST_MATCH = ["StartupBoostEnabled", "BackgroundModeEnabled"];
|
|
165
|
+
/**
|
|
166
|
+
* Which fields the verdict is actually ABOUT -- #1997.
|
|
167
|
+
*
|
|
168
|
+
* `compared` NAMES the fields at least one guest reported a value for; `unchecked` names the ones that
|
|
169
|
+
* drew a value from NOBODY. The second list exists because `check()` below skips an absent value, which
|
|
170
|
+
* is right (a rolling deploy must not flag the guest it has not reached yet) and has a cost: a field no
|
|
171
|
+
* guest reports contributes no values, `new Set([]).size > 1` is false, and it reads as agreement.
|
|
172
|
+
* A field compared on nobody and a field equal on everybody produced the IDENTICAL verdict.
|
|
173
|
+
*
|
|
174
|
+
* Measured 2026-09-22T20:09Z on the live fleet, at the merge of #1953: nine fields at 10/10 guests and
|
|
175
|
+
* `displayMode` at 0/10, and `fleet:status` printed `fleet CONSISTENT across 10 of 10 -- these workers
|
|
176
|
+
* are interchangeable for capture`. The display was still not compared. Naming the two lists is what
|
|
177
|
+
* lets a reader tell "they agree" from "nobody was asked".
|
|
178
|
+
*
|
|
179
|
+
* `coverage` IS THE SAME QUESTION WITHOUT THE THRESHOLD -- #2019. The two lists above are a PARTITION on
|
|
180
|
+
* "did anybody report it", which answers 0-of-10 and says nothing about 1-of-10; `fleet:status` read the
|
|
181
|
+
* second as agreement about ten guests on the strength of one. So every asked field also carries how many
|
|
182
|
+
* of the asked guests actually reported it, and the verdict draws its own line. `compared`/`unchecked`
|
|
183
|
+
* stay because they are what a caller greps for the REMEDY -- a field at 0 sends a reader to the field,
|
|
184
|
+
* a field at k sends them to the boxes -- and `coverage` is the measurement both are derived from.
|
|
185
|
+
*
|
|
186
|
+
* @typedef {{field: string, reported: number, asked: number}} FieldReporters
|
|
187
|
+
* @typedef {{compared: string[], unchecked: string[], coverage: FieldReporters[]}} FieldCoverage
|
|
188
|
+
*/
|
|
189
|
+
/**
|
|
190
|
+
* One reported-only field that has something to say, and WHICH thing it is saying — #2063.
|
|
191
|
+
*
|
|
192
|
+
* `drifted` is the guests giving more than one value; `unreported` is some or all of them giving none.
|
|
193
|
+
* Two states rather than a boolean because the REMEDY differs and the report has to name it: drift sends
|
|
194
|
+
* a reader to the provisioning pin, silence sends them to the worker deploy. Neither is a refusal.
|
|
195
|
+
*
|
|
196
|
+
* It carries `reported`/`asked` for the same reason `FieldReporters` does — a count is what lets a reader
|
|
197
|
+
* tell 0 of 10 from 9 of 10 — and the values map for the reason `Mismatch` does: drift detected and not
|
|
198
|
+
* located is not actionable.
|
|
199
|
+
*
|
|
200
|
+
* @typedef {{field: string, why: string, values: Record<string, unknown>, reported: number,
|
|
201
|
+
* asked: number, state: "drifted" | "unreported"}} ReportedDrift
|
|
202
|
+
*/
|
|
203
|
+
/**
|
|
204
|
+
* ONE FIELD READ ACROSS THE FLEET, before anybody decides what it means.
|
|
205
|
+
*
|
|
206
|
+
* Split out from `fleetConsistency` when the third channel arrived (#2063), because the READING and the
|
|
207
|
+
* CONSEQUENCE are two things and only the second differs between the channels: `MUST_MATCH` turns a
|
|
208
|
+
* reading into a mismatch and a coverage row, `REPORTED_ONLY` turns the same reading into a named drift
|
|
209
|
+
* that gates nothing. One reader means the two channels cannot drift apart in how they compare — a
|
|
210
|
+
* second copy of this loop is how a "reported-only" field would end up counted differently from a gating
|
|
211
|
+
* one and nobody would know which was right.
|
|
212
|
+
*
|
|
213
|
+
* @typedef {{worker: string, environment?: Record<string, unknown>, policy?: Record<string, unknown>}} Guest
|
|
214
|
+
* @typedef {{field: string, why: string, values: Record<string, unknown>, reported: number, asked: number}} Reading
|
|
215
|
+
*
|
|
216
|
+
* @param {Guest[]} present
|
|
217
|
+
* @param {{field: string, why: string, source: (guest: Guest) => Record<string, unknown> | undefined,
|
|
218
|
+
* key: string}} ask `source` is the BLOCK this field lives in, not the value: coverage has to tell
|
|
219
|
+
* "the guest did not report this field" from "this caller never collected that block at all", and only
|
|
220
|
+
* the block answers the second
|
|
221
|
+
* @returns {Reading}
|
|
222
|
+
*/
|
|
223
|
+
function readField(present, { field, why, source, key }) {
|
|
224
|
+
/** @type {Record<string, unknown>} */
|
|
225
|
+
const values = {};
|
|
226
|
+
// THE REPORTER COUNT IS COUNTED, NEVER READ OFF `values` -- #2019, and #2018 is why. The map is keyed
|
|
227
|
+
// by worker name, and a caller that supplies guests without one collapses every guest onto a single
|
|
228
|
+
// `undefined` key -- which `capture-real-pages.mjs` does -- so `Object.keys(values).length` reads 1
|
|
229
|
+
// for any number of reporting guests, understating coverage in exactly the cases a coverage number
|
|
230
|
+
// exists for. `asked` counts the guests that carried the BLOCK; `reported` counts the ones that
|
|
231
|
+
// carried a VALUE in it; both are incremented on the guest, so neither can be collapsed by a key.
|
|
232
|
+
let asked = 0;
|
|
233
|
+
let reported = 0;
|
|
234
|
+
for (const guest of present) {
|
|
235
|
+
const block = source(guest);
|
|
236
|
+
// A BLOCK THIS CALLER DID NOT COLLECT IS A FACT ABOUT THE PROBE, NOT THE FLEET. `/health` carries
|
|
237
|
+
// no policy, so every production caller passes `policy: undefined`; calling those fields
|
|
238
|
+
// "compared on nobody" would report a permanent gap on an axis nobody asked about, and drown the
|
|
239
|
+
// one this list exists to surface.
|
|
240
|
+
if (block === undefined || block === null)
|
|
241
|
+
continue;
|
|
242
|
+
asked += 1;
|
|
243
|
+
const value = block[key];
|
|
244
|
+
// Absent is not a mismatch: an older worker that does not report a field must not be flagged
|
|
245
|
+
// against newer ones. Only DIFFERING known values are evidence of drift.
|
|
246
|
+
if (value !== undefined && value !== null) {
|
|
247
|
+
reported += 1;
|
|
248
|
+
values[guest.worker] = value;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { field, why, values, reported, asked };
|
|
252
|
+
}
|
|
253
|
+
/** How many distinct values the guests actually gave for one field. @param {Reading} reading */
|
|
254
|
+
function distinctValues({ values }) {
|
|
255
|
+
return new Set(Object.values(values)).size;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* THE GATING CONSEQUENCE of a set of readings: what disagrees, and what nobody was asked.
|
|
259
|
+
*
|
|
260
|
+
* @param {Reading[]} readings
|
|
261
|
+
* @returns {{mismatches: Mismatch[], fields: FieldCoverage}}
|
|
262
|
+
*/
|
|
263
|
+
function gateOn(readings) {
|
|
264
|
+
/** @type {Mismatch[]} */
|
|
265
|
+
const mismatches = [];
|
|
266
|
+
/** @type {FieldCoverage} */
|
|
267
|
+
const fields = { compared: [], unchecked: [], coverage: [] };
|
|
268
|
+
for (const reading of readings) {
|
|
269
|
+
const { field, why, values, reported, asked } = reading;
|
|
270
|
+
// A FIELD NO GUEST REPORTED IS CANNOT ASK, NOT ALL AGREE. One value from one guest still counts as
|
|
271
|
+
// compared -- that is the rolling-deploy case the skip above is for, and it is a different claim
|
|
272
|
+
// from nobody having been asked at all. How MANY reported it is a third claim again, and it goes on
|
|
273
|
+
// `coverage` rather than into this partition: the lists answer the remedy, the count answers the
|
|
274
|
+
// verdict (#2019).
|
|
275
|
+
if (asked > 0) {
|
|
276
|
+
(reported > 0 ? fields.compared : fields.unchecked).push(field);
|
|
277
|
+
fields.coverage.push({ field, reported, asked });
|
|
278
|
+
}
|
|
279
|
+
if (distinctValues(reading) > 1)
|
|
280
|
+
mismatches.push({ field, why, values });
|
|
281
|
+
}
|
|
282
|
+
return { mismatches, fields };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* THE REPORTED-ONLY CONSEQUENCE: the same readings, as something to SAY rather than something to refuse.
|
|
286
|
+
*
|
|
287
|
+
* A field the guests agree on yields NOTHING, and that is the half of this that a test can most easily
|
|
288
|
+
* lose. `drifted` and `unreported` are both findings; agreement is not, and an implementation that
|
|
289
|
+
* returned every reported-only field regardless would satisfy "the drift is named" while naming a fleet
|
|
290
|
+
* that has none.
|
|
291
|
+
*
|
|
292
|
+
* `asked === 0` yields nothing either, for the reason `gateOn` skips it: a block this caller never
|
|
293
|
+
* collected is a fact about the probe.
|
|
294
|
+
*
|
|
295
|
+
* @param {Reading[]} readings
|
|
296
|
+
* @returns {ReportedDrift[]}
|
|
297
|
+
*/
|
|
298
|
+
function driftOf(readings) {
|
|
299
|
+
return readings.flatMap((reading) => {
|
|
300
|
+
const state = driftState(reading);
|
|
301
|
+
return state === null ? [] : [{ ...reading, state }];
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* @param {Reading} reading
|
|
306
|
+
* @returns {"drifted" | "unreported" | null}
|
|
307
|
+
*/
|
|
308
|
+
function driftState(reading) {
|
|
309
|
+
if (distinctValues(reading) > 1)
|
|
310
|
+
return "drifted";
|
|
311
|
+
// NOT REPORTED BY EVERYBODY WHO WAS ASKED, which covers #1997's nobody and #2019's some in one line --
|
|
312
|
+
// for a GATING field those are two refusals with different remedies, and here they are one sentence
|
|
313
|
+
// with the counts in it, because nothing is being refused.
|
|
314
|
+
if (reading.asked > 0 && reading.reported < reading.asked)
|
|
315
|
+
return "unreported";
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Compare guests field by field.
|
|
320
|
+
*
|
|
321
|
+
* @param {Guest[]} guests
|
|
322
|
+
* @returns {{consistent: boolean, mismatches: Mismatch[], compared: number, fields: FieldCoverage,
|
|
323
|
+
* reportedOnly: ReportedDrift[]}}
|
|
324
|
+
* `compared` is how many guests the verdict is actually ABOUT -- #920. A guest with no `environment`
|
|
325
|
+
* and no `policy` is dropped below before comparing, so it is not the length of what was passed in,
|
|
326
|
+
* and a caller that reports `consistent` without it is stating agreement over a set it cannot name.
|
|
327
|
+
* `fields` is that same question one axis over: WHICH fields the verdict is about -- #1997.
|
|
328
|
+
* `reportedOnly` is the third channel (#2063): compared, named, and part of NEITHER of the two above,
|
|
329
|
+
* which is what makes it something to report rather than something to refuse.
|
|
330
|
+
*/
|
|
331
|
+
export function fleetConsistency(guests) {
|
|
332
|
+
const present = (guests ?? []).filter((g) => g && (g.environment || g.policy));
|
|
333
|
+
// One guest is trivially consistent with itself, and zero is not a fleet. Neither is a finding, and
|
|
334
|
+
// neither is a field nothing compared: with nobody to compare against, coverage is not a question yet.
|
|
335
|
+
if (present.length < 2) {
|
|
336
|
+
// A FRESH object rather than a shared constant: this is returned to a caller, and one shared literal
|
|
337
|
+
// is one `push` away from a coverage list that grows across unrelated readings.
|
|
338
|
+
return { consistent: true, mismatches: [], compared: present.length,
|
|
339
|
+
fields: { compared: [], unchecked: [], coverage: [] }, reportedOnly: [] };
|
|
340
|
+
}
|
|
341
|
+
/** @param {Guest} guest */
|
|
342
|
+
const environmentOf = (guest) => guest.environment;
|
|
343
|
+
const gated = [
|
|
344
|
+
...MUST_MATCH.map(({ path, why }) => readField(present, { field: path, why, source: environmentOf, key: path })),
|
|
345
|
+
...POLICY_MUST_MATCH.map((name) => readField(present, { field: `edgePolicy.${name}`,
|
|
346
|
+
why: "guests with different browser behaviour are not interchangeable",
|
|
347
|
+
source: (guest) => guest.policy, key: name })),
|
|
348
|
+
];
|
|
349
|
+
const { mismatches, fields } = gateOn(gated);
|
|
350
|
+
const reportedOnly = driftOf(REPORTED_ONLY.map(({ path, why }) => readField(present, { field: path, why, source: environmentOf, key: path })));
|
|
351
|
+
return { consistent: mismatches.length === 0, mismatches, compared: present.length, fields,
|
|
352
|
+
reportedOnly };
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* One line per reported-only field that has something to say, in `describeMismatches`'s own shape.
|
|
356
|
+
*
|
|
357
|
+
* SEPARATE FROM `describeMismatches` rather than a flag on it, because the two lines make different
|
|
358
|
+
* claims and a reader has to be able to tell them apart at a glance: that one says the fleet is not
|
|
359
|
+
* usable for a capture run, this one says the fleet is not identical and the run may proceed anyway.
|
|
360
|
+
* Folding them would put the ruling's own distinction behind a boolean argument.
|
|
361
|
+
*
|
|
362
|
+
* @param {ReportedDrift[]} drifts
|
|
363
|
+
* @returns {string[]}
|
|
364
|
+
*/
|
|
365
|
+
export function describeReportedOnly(drifts) {
|
|
366
|
+
return (drifts ?? []).map((drift) => `${drift.field}: ${reportedDetail(drift)} — ${drift.why}`);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* NAMED WITH EACH GUEST'S VALUE where there is one, and with the count where there is not.
|
|
370
|
+
*
|
|
371
|
+
* `not reported by any of 10 guests` is a different finding from `.2=v24.19.0 .7=v24.20.0` and from
|
|
372
|
+
* `.2=v24.19.0 (1 of 10 guests reported it)`, and a reader acts differently on each: the first sends
|
|
373
|
+
* them to the worker code, the second to the boxes, the third to the deploy that has not finished.
|
|
374
|
+
*
|
|
375
|
+
* @param {ReportedDrift} drift
|
|
376
|
+
*/
|
|
377
|
+
function reportedDetail({ values, reported, asked, state }) {
|
|
378
|
+
const label = labelWorkers(Object.keys(values));
|
|
379
|
+
const detail = Object.entries(values).map(([worker, value]) => `${label.get(worker)}=${value}`).join(" ");
|
|
380
|
+
if (state === "drifted")
|
|
381
|
+
return detail;
|
|
382
|
+
if (reported === 0)
|
|
383
|
+
return `not reported by any of ${asked} guests`;
|
|
384
|
+
return `${detail} (${reported} of ${asked} guests reported it)`;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* One line per mismatch, naming the guests, so the report is actionable rather than just alarming.
|
|
388
|
+
*
|
|
389
|
+
* @param {Mismatch[]} mismatches
|
|
390
|
+
* @returns {string[]}
|
|
391
|
+
*/
|
|
392
|
+
export function describeMismatches(mismatches) {
|
|
393
|
+
return (mismatches ?? []).map(({ field, values, why }) => {
|
|
394
|
+
const label = labelWorkers(Object.keys(values));
|
|
395
|
+
const detail = Object.entries(values).map(([worker, value]) => `${label.get(worker)}=${value}`).join(" ");
|
|
396
|
+
return `${field}: ${detail} — ${why}`;
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* A short, UNAMBIGUOUS name per worker.
|
|
401
|
+
*
|
|
402
|
+
* `.4` is the right label for a guest's full `http://<address>:8765` in a table, and it is the wrong one the moment
|
|
403
|
+
* two workers share a last octet — two boxes on one host with different ports, or two subnets that
|
|
404
|
+
* happen to meet. The report then says which FIELD drifted and gives two identically-named values, which
|
|
405
|
+
* is drift detected and not located: `browserVersion: .1=151.0.1 .1=150.0.9`.
|
|
406
|
+
*
|
|
407
|
+
* So the short form is used only while it distinguishes, and the full host:port otherwise. Shortening is
|
|
408
|
+
* a readability optimisation, and it must never cost the thing the line exists to convey.
|
|
409
|
+
*/
|
|
410
|
+
/**
|
|
411
|
+
* @param {string[]} workers
|
|
412
|
+
* @returns {Map<string, string>}
|
|
413
|
+
*/
|
|
414
|
+
function labelWorkers(workers) {
|
|
415
|
+
const short = new Map(workers.map((w) => [w, shortWorker(w)]));
|
|
416
|
+
const counts = new Map();
|
|
417
|
+
for (const name of short.values())
|
|
418
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
419
|
+
return new Map(workers.map((w) => {
|
|
420
|
+
// `short.get(w)` cannot miss — every worker was put in the map on the line above — but TypeScript
|
|
421
|
+
// cannot know that, and `?? w` is the honest fallback rather than a non-null assertion: an unlabelled
|
|
422
|
+
// worker printed as its own URL is still a located mismatch, which is what this function is for.
|
|
423
|
+
const name = short.get(w) ?? w;
|
|
424
|
+
return [w, (counts.get(name) ?? 0) > 1 ? hostAndPort(w) : name];
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
/** a guest's full `http://<address>:8765` is noise in a table; `.4` is not. @param {string} worker */
|
|
428
|
+
function shortWorker(worker) {
|
|
429
|
+
const host = /\/\/([^:/]+)/.exec(worker)?.[1] ?? worker;
|
|
430
|
+
return host.includes(".") ? `.${host.split(".").pop()}` : host;
|
|
431
|
+
}
|
|
432
|
+
/** @param {string} worker */
|
|
433
|
+
function hostAndPort(worker) {
|
|
434
|
+
return /\/\/(.+?)\/?$/.exec(worker)?.[1] ?? worker;
|
|
435
|
+
}
|
|
436
|
+
//# sourceMappingURL=fleet-consistency.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fleet-consistency.mjs","sourceRoot":"","sources":["../src/fleet-consistency.mjs"],"names":[],"mappings":"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"}
|