@variance-authority/mcp 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.
- package/CHANGELOG.md +89 -0
- package/LICENSE +21 -0
- package/README.md +407 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.js +32 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/observability-subject.d.ts +22 -0
- package/dist/observability-subject.js +2 -0
- package/dist/observability-subject.js.map +1 -0
- package/dist/presentation.d.ts +6 -0
- package/dist/presentation.js +64 -0
- package/dist/presentation.js.map +1 -0
- package/dist/protocol.d.ts +82 -0
- package/dist/protocol.js +186 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server.d.ts +71 -0
- package/dist/server.js +103 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/adjudicate.d.ts +25 -0
- package/dist/tools/adjudicate.js +122 -0
- package/dist/tools/adjudicate.js.map +1 -0
- package/dist/tools/attention.d.ts +13 -0
- package/dist/tools/attention.js +151 -0
- package/dist/tools/attention.js.map +1 -0
- package/dist/tools/changelog.d.ts +38 -0
- package/dist/tools/changelog.js +201 -0
- package/dist/tools/changelog.js.map +1 -0
- package/dist/tools/changes.d.ts +31 -0
- package/dist/tools/changes.js +120 -0
- package/dist/tools/changes.js.map +1 -0
- package/dist/tools/composition.d.ts +13 -0
- package/dist/tools/composition.js +366 -0
- package/dist/tools/composition.js.map +1 -0
- package/dist/tools/describe.d.ts +20 -0
- package/dist/tools/describe.js +279 -0
- package/dist/tools/describe.js.map +1 -0
- package/dist/tools/diff.d.ts +20 -0
- package/dist/tools/diff.js +85 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/explain-verdict.d.ts +25 -0
- package/dist/tools/explain-verdict.js +92 -0
- package/dist/tools/explain-verdict.js.map +1 -0
- package/dist/tools/findings.d.ts +40 -0
- package/dist/tools/findings.js +130 -0
- package/dist/tools/findings.js.map +1 -0
- package/dist/tools/narrowing.d.ts +26 -0
- package/dist/tools/narrowing.js +38 -0
- package/dist/tools/narrowing.js.map +1 -0
- package/dist/tools/observability.d.ts +7 -0
- package/dist/tools/observability.js +199 -0
- package/dist/tools/observability.js.map +1 -0
- package/dist/tools/presentations.d.ts +6 -0
- package/dist/tools/presentations.js +74 -0
- package/dist/tools/presentations.js.map +1 -0
- package/dist/tools/run-signals.d.ts +12 -0
- package/dist/tools/run-signals.js +115 -0
- package/dist/tools/run-signals.js.map +1 -0
- package/dist/tools/scenarios.d.ts +6 -0
- package/dist/tools/scenarios.js +62 -0
- package/dist/tools/scenarios.js.map +1 -0
- package/dist/tools/source-tests.d.ts +5 -0
- package/dist/tools/source-tests.js +95 -0
- package/dist/tools/source-tests.js.map +1 -0
- package/dist/tools/subject.d.ts +45 -0
- package/dist/tools/subject.js +57 -0
- package/dist/tools/subject.js.map +1 -0
- package/dist/tools/summary.d.ts +31 -0
- package/dist/tools/summary.js +436 -0
- package/dist/tools/summary.js.map +1 -0
- package/dist/tools/test-signals.d.ts +16 -0
- package/dist/tools/test-signals.js +106 -0
- package/dist/tools/test-signals.js.map +1 -0
- package/dist/tools/tool.d.ts +64 -0
- package/dist/tools/tool.js +9 -0
- package/dist/tools/tool.js.map +1 -0
- package/dist/tools/trace-component.d.ts +17 -0
- package/dist/tools/trace-component.js +50 -0
- package/dist/tools/trace-component.js.map +1 -0
- package/dist/tools/vantage-lines.d.ts +44 -0
- package/dist/tools/vantage-lines.js +66 -0
- package/dist/tools/vantage-lines.js.map +1 -0
- package/dist/tools/variations.d.ts +27 -0
- package/dist/tools/variations.js +125 -0
- package/dist/tools/variations.js.map +1 -0
- package/dist/tools.d.ts +120 -0
- package/dist/tools.js +127 -0
- package/dist/tools.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +64 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { arrivalLine, carriedLine } from '@variance-authority/report';
|
|
2
|
+
import { presentationSummary } from '../presentation.js';
|
|
3
|
+
import { narrowing } from './narrowing.js';
|
|
4
|
+
import { NO_ARGS } from './tool.js';
|
|
5
|
+
/**
|
|
6
|
+
* `variance_summary`, and the sections it is assembled from.
|
|
7
|
+
*
|
|
8
|
+
* The helpers below live with it rather than somewhere shared because none of
|
|
9
|
+
* them is a general fact about a report — each is one paragraph of this one
|
|
10
|
+
* answer, and each exists to stop this answer claiming something the run never
|
|
11
|
+
* established. Coverage, order dependence, findings and the closing sentence all
|
|
12
|
+
* repeat the same rule in different words: silence about a subject is not a pass.
|
|
13
|
+
* Read them in the order `run` prints them; that order is the argument.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Overview: what happened, what needs attention, and what was never looked at.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately does not list unchanged subjects. A run over 300 subjects where
|
|
19
|
+
* two changed should print two lines — a list as long as the suite is the "100
|
|
20
|
+
* changes? merge" failure in its purest form, and the reader stops reading long
|
|
21
|
+
* before the interesting line.
|
|
22
|
+
*
|
|
23
|
+
* The coverage section is the exception to that economy and is not negotiable.
|
|
24
|
+
* A run that planned 300 subjects, failed on 50 and found the other 250
|
|
25
|
+
* unchanged has an observation list in which every entry is clean; a summary
|
|
26
|
+
* computed from observations alone therefore reports it as a clean run, and an
|
|
27
|
+
* agent acts on that. So `nothing to review` is claimed only from a report that
|
|
28
|
+
* accounted for every subject it planned — never from silence, and never over a
|
|
29
|
+
* subject the run meant to see and could not. That is the same rule the CLI's
|
|
30
|
+
* exit code applies, stated in the same words, because a human and an agent
|
|
31
|
+
* reading one artifact must not be able to disagree about whether it was green.
|
|
32
|
+
*/
|
|
33
|
+
export const summarize = {
|
|
34
|
+
name: 'variance_summary',
|
|
35
|
+
description: 'What the last visual run found: counts by verdict, then one line per subject that ' +
|
|
36
|
+
'needs attention, then which subjects were not observed at all. Unchanged subjects are ' +
|
|
37
|
+
'counted, not listed. Start here.',
|
|
38
|
+
inputSchema: NO_ARGS,
|
|
39
|
+
run(report) {
|
|
40
|
+
const counts = new Map();
|
|
41
|
+
for (const observation of report.observations) {
|
|
42
|
+
counts.set(observation.verdict, (counts.get(observation.verdict) ?? 0) + 1);
|
|
43
|
+
}
|
|
44
|
+
const header = [
|
|
45
|
+
`${observedOf(report)}, ${report.retention} run at ${report.at}`,
|
|
46
|
+
`rendered by ${describeIdentity(report)}`,
|
|
47
|
+
// The coverage state joins the verdict counts rather than only appearing in
|
|
48
|
+
// the section below, because that section is as long as the hole is and the
|
|
49
|
+
// hole is what a reader most needs in the first four lines. A run that lost
|
|
50
|
+
// 50 subjects otherwise opens with "250 unchanged" and says so 50 lines later.
|
|
51
|
+
[...counts.entries()]
|
|
52
|
+
.map(([verdict, count]) => `${count} ${verdict}`)
|
|
53
|
+
.concat(shortfall(report))
|
|
54
|
+
.join(', '),
|
|
55
|
+
...(report.intent !== undefined ? [`intent: ${report.intent}`] : []),
|
|
56
|
+
...narrowing(report),
|
|
57
|
+
];
|
|
58
|
+
// `ignored` is counted in the header and does not get a line of its own.
|
|
59
|
+
// It is a real state and it is not a finding: a shared header carrying a
|
|
60
|
+
// clock puts every subject in the suite here, and three hundred lines saying
|
|
61
|
+
// "you already decided not to look at this" is the output nobody reads —
|
|
62
|
+
// which is how the *other* lines get missed. The header count keeps it
|
|
63
|
+
// visible, and `variance run` prints the per-rule ledger beneath it.
|
|
64
|
+
//
|
|
65
|
+
// `unstable` goes the other way and joins this list even when the verdict is
|
|
66
|
+
// green, and the reason is the last line of this answer: a run whose only
|
|
67
|
+
// finding was an unstable subject printed "nothing to review" directly under
|
|
68
|
+
// a heading naming six of them. Under `--flakes` that is the *normal* case —
|
|
69
|
+
// every subject agrees with its baseline, and the whole point of the mode is
|
|
70
|
+
// what agreeing with a baseline does not say — so it was not an edge.
|
|
71
|
+
const notable = report.observations.filter((o) => (o.verdict !== 'unchanged' && o.verdict !== 'ignored') ||
|
|
72
|
+
(o.unstable !== undefined && o.unstable.absorbed === undefined));
|
|
73
|
+
return [
|
|
74
|
+
...header,
|
|
75
|
+
...(notable.length === 0
|
|
76
|
+
? []
|
|
77
|
+
: [
|
|
78
|
+
'',
|
|
79
|
+
...notable.map((observation) => {
|
|
80
|
+
const cause = observation.regions.find((region) => region.cause);
|
|
81
|
+
const lead = cause?.component !== undefined ? ` — ${cause.component}` : '';
|
|
82
|
+
// Labelled by what it *is* rather than by its verdict. The verdict
|
|
83
|
+
// stays `changed` — the pixels really did move — but a reader who
|
|
84
|
+
// acts on that word reviews a component that nothing edited. The
|
|
85
|
+
// two need opposite actions, so they get different words.
|
|
86
|
+
// An absorbed instability is not one of these. The subject declared
|
|
87
|
+
// that it does not assert on what moved, so it is neither a defect
|
|
88
|
+
// nor a different word for its verdict — it is listed further down,
|
|
89
|
+
// under a heading that says so.
|
|
90
|
+
const reported = observation.unstable !== undefined && observation.unstable.absorbed === undefined
|
|
91
|
+
? observation.unstable
|
|
92
|
+
: undefined;
|
|
93
|
+
const label = reported
|
|
94
|
+
? 'unstable'
|
|
95
|
+
: observation.alone?.reproduced === false
|
|
96
|
+
? 'order-dependent'
|
|
97
|
+
: observation.verdict;
|
|
98
|
+
const because = reported
|
|
99
|
+
? reported.because
|
|
100
|
+
: observation.alone?.reproduced === false
|
|
101
|
+
? observation.alone.because
|
|
102
|
+
: observation.because;
|
|
103
|
+
return `[${label}] ${observation.subject}${lead}: ${because}`;
|
|
104
|
+
}),
|
|
105
|
+
]),
|
|
106
|
+
'',
|
|
107
|
+
...coverage(report),
|
|
108
|
+
...drift(report),
|
|
109
|
+
...instability(report),
|
|
110
|
+
...orderDependence(report),
|
|
111
|
+
...findingsLine(report),
|
|
112
|
+
...presentationSummary(report),
|
|
113
|
+
...(notable.length === 0 ? ['', settlement(report)] : []),
|
|
114
|
+
].join('\n');
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Subjects that did not agree with themselves, and the one section here written
|
|
119
|
+
* to be *acted on* rather than reviewed.
|
|
120
|
+
*
|
|
121
|
+
* Above every other finding, because it invalidates them. A subject that reads
|
|
122
|
+
* differently twice in a row has a verdict that was decided by which of the two
|
|
123
|
+
* readings the run happened to take first, and an agent that starts reviewing
|
|
124
|
+
* its regions is reading a diff against a coin flip. Nothing else in this
|
|
125
|
+
* summary is worth doing on these subjects until this is.
|
|
126
|
+
*
|
|
127
|
+
* The component and the band are printed because they are what makes the fix
|
|
128
|
+
* bounded. "This subject is flaky" is a page to read; `Clock (content)` is a node
|
|
129
|
+
* whose text moves between two readings taken seconds apart, and the causes of
|
|
130
|
+
* that are a short list — a clock, a random seed, an id counter, a request that
|
|
131
|
+
* had not landed. The band narrows it further: `content` is data, `geometry` is
|
|
132
|
+
* layout that has not settled, `token` is a style that is still being applied.
|
|
133
|
+
*
|
|
134
|
+
* This is where the base layer hands over. Animations are pinned, GIFs are frozen
|
|
135
|
+
* on the wire, fonts and images are waited for, and every asset's bytes are in
|
|
136
|
+
* the environment key — so a subject that still disagrees with itself is past
|
|
137
|
+
* everything a recipe can do, and is a defect in the page with a name attached
|
|
138
|
+
* rather than a tolerance to widen.
|
|
139
|
+
*/
|
|
140
|
+
function instability(report) {
|
|
141
|
+
const unstable = report.observations.filter((o) => o.unstable !== undefined && o.unstable.absorbed === undefined);
|
|
142
|
+
if (unstable.length === 0)
|
|
143
|
+
return absorbedInstability(report);
|
|
144
|
+
const bands = new Set(unstable.flatMap((o) => o.unstable?.bands ?? []));
|
|
145
|
+
return [
|
|
146
|
+
'',
|
|
147
|
+
`UNSTABLE: ${unstable.length} subject(s) were read twice, seconds apart, with nothing`,
|
|
148
|
+
' changed in between, and the two readings disagreed. Their verdicts were decided by',
|
|
149
|
+
' whichever reading came first, so do not review their regions and do not accept them',
|
|
150
|
+
' (`accept` refuses these). Fix what moves between two readings of the same page:',
|
|
151
|
+
...unstable.flatMap((observation) => {
|
|
152
|
+
const moved = observation.unstable;
|
|
153
|
+
const named = (moved?.components ?? [])
|
|
154
|
+
.map((component) => component.file === undefined ? component.name : `${component.name} ${component.file}`)
|
|
155
|
+
.join(', ');
|
|
156
|
+
const where = named === '' ? '' : ` — ${named}`;
|
|
157
|
+
const inBands = moved === undefined || moved.bands.length === 0 ? '' : ` (${moved.bands.join(', ')})`;
|
|
158
|
+
return [` ${observation.subject}${where}${inBands}`, ...recurrence(report, observation.subject)];
|
|
159
|
+
}),
|
|
160
|
+
...remedies(bands),
|
|
161
|
+
// The loop closes here. Everything above tells a reader what moved and how
|
|
162
|
+
// often; this is the one line that says how to find out whether the edit
|
|
163
|
+
// they are about to make worked — without re-running three hundred subjects
|
|
164
|
+
// and without waiting for tomorrow's build to be the experiment.
|
|
165
|
+
' Check a fix by reading the same subject twice again, and nothing else:',
|
|
166
|
+
...unstable.map((observation) => ` variance run --subjects '${observation.subject}' --flakes`),
|
|
167
|
+
' It exits 1 while the two readings still disagree, even with every verdict green.',
|
|
168
|
+
...absorbedInstability(report),
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Tokens whose value moved in this run, and how far they have travelled.
|
|
173
|
+
*
|
|
174
|
+
* The one finding on this page that no comparison could have produced. Every
|
|
175
|
+
* other line answers *what moved*; this answers *how much it has moved
|
|
176
|
+
* altogether*, which is a sum across approvals and is therefore invisible to
|
|
177
|
+
* every review that approved one of them. Eleven correct approvals of 2px each
|
|
178
|
+
* are eleven correct decisions and one 22px change nobody made.
|
|
179
|
+
*
|
|
180
|
+
* Placed above the instability section deliberately: it is rarer, it is never
|
|
181
|
+
* noise, and it is the finding a reader would most regret scrolling past.
|
|
182
|
+
*/
|
|
183
|
+
function drift(report) {
|
|
184
|
+
const moved = Object.entries(report.drift ?? {});
|
|
185
|
+
if (moved.length === 0)
|
|
186
|
+
return [];
|
|
187
|
+
return [
|
|
188
|
+
'',
|
|
189
|
+
`DRIFT: ${moved.length} token(s) moved in this run, and the record says what they have`,
|
|
190
|
+
' drifted to across every approved change in the window. No single review saw these',
|
|
191
|
+
' totals, because each of them approved one step:',
|
|
192
|
+
...moved.map(([, record]) => ` ${record.because}`),
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* What the record says about a subject that just read differently.
|
|
197
|
+
*
|
|
198
|
+
* Two readings are a **lower bound** and can never be more: a subject that flakes
|
|
199
|
+
* one time in fifty passes that check forty-nine runs out of fifty. This is the
|
|
200
|
+
* only line in the answer that can distinguish *a fixture that has been bad for a
|
|
201
|
+
* month* from *something that started today*, and those need different people.
|
|
202
|
+
*
|
|
203
|
+
* The absent case gets a line too, and it is the one worth being careful about.
|
|
204
|
+
* Silence here would read as "first time", which is a claim — and it is the claim
|
|
205
|
+
* a reader most wants to be true.
|
|
206
|
+
*/
|
|
207
|
+
function recurrence(report, subject) {
|
|
208
|
+
const record = report.flakiness?.[subject];
|
|
209
|
+
if (record === undefined) {
|
|
210
|
+
return [
|
|
211
|
+
' no history record answered for this subject, so nothing here says whether it has',
|
|
212
|
+
' happened before. That is silence, not a first occurrence.',
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
const shape = record.sweepsSince > 0 && record.occurrences > 1
|
|
216
|
+
? ' — it has been quiet since, so check whether a fix already landed before writing one'
|
|
217
|
+
: record.occurrences > 1
|
|
218
|
+
? ' — recurring, and the most recent sweep still saw it: the fixture is the bug'
|
|
219
|
+
: ' — the record has not seen this before';
|
|
220
|
+
return [` ${record.because}`, shape];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Subjects that moved between two readings, in bands they do not claim to assert
|
|
224
|
+
* on.
|
|
225
|
+
*
|
|
226
|
+
* Counted and named, never silent — the same rule `ignored` follows for pixels,
|
|
227
|
+
* one level up and about kinds. A route declared `layout` with a live clock in it
|
|
228
|
+
* is *working as declared*, and reporting it as a defect would make every
|
|
229
|
+
* route-level test red for exactly the reason its level was written. But a
|
|
230
|
+
* declaration that is quietly absorbing movement is also how a suite ends up
|
|
231
|
+
* green over a surface nobody watches, so it gets a line and names the rule that
|
|
232
|
+
* did it — which is what makes it auditable later.
|
|
233
|
+
*/
|
|
234
|
+
function absorbedInstability(report) {
|
|
235
|
+
const absorbed = report.observations.filter((o) => o.unstable?.absorbed !== undefined);
|
|
236
|
+
if (absorbed.length === 0)
|
|
237
|
+
return [];
|
|
238
|
+
return [
|
|
239
|
+
'',
|
|
240
|
+
`not asserted on: ${absorbed.length} subject(s) read differently between two readings,`,
|
|
241
|
+
' entirely in bands their declared level does not assert on. Working as declared, and',
|
|
242
|
+
' listed because a declaration nobody re-reads is how a suite stops watching something:',
|
|
243
|
+
...absorbed.map((observation) => {
|
|
244
|
+
const bands = observation.unstable?.bands ?? [];
|
|
245
|
+
const rule = observation.unstable?.absorbed?.rule ?? 'a sensitivity rule';
|
|
246
|
+
const level = observation.unstable?.absorbed?.level ?? 'its level';
|
|
247
|
+
return ` ${observation.subject} — ${bands.join(', ')}, absorbed by \`${rule}\` (asserts on ${level})`;
|
|
248
|
+
}),
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* What the band means for the fix, which is the whole reason a band is reported.
|
|
253
|
+
*
|
|
254
|
+
* A frequency band is not a severity — it is a statement about *what kind of
|
|
255
|
+
* thing* moved, and each kind has a short list of causes. An agent handed "this
|
|
256
|
+
* subject is flaky" has a page to read; one handed `content` has four candidates
|
|
257
|
+
* and can check all of them in a minute.
|
|
258
|
+
*
|
|
259
|
+
* Masking is listed last and hedged, deliberately. A clock genuinely is a clock
|
|
260
|
+
* and an ignore is the right answer for it — but reaching for one first is how a
|
|
261
|
+
* suite ends up green over a surface nobody watches, and the same mask that hides
|
|
262
|
+
* this hides the regression that later lands in the same place.
|
|
263
|
+
*/
|
|
264
|
+
function remedies(bands) {
|
|
265
|
+
const lines = [...BAND_REMEDY].filter(([band]) => bands.has(band)).map(([, hint]) => ` ${hint}`);
|
|
266
|
+
if (lines.length === 0)
|
|
267
|
+
return [];
|
|
268
|
+
return [
|
|
269
|
+
' What each band that moved usually means:',
|
|
270
|
+
...lines,
|
|
271
|
+
' If the movement is genuinely inherent to the subject — a real clock, a live feed —',
|
|
272
|
+
' mask the *element* rather than accept the subject: the component named above is it,',
|
|
273
|
+
' and an element-scoped ignore follows it when layout moves. Reach for that second, not',
|
|
274
|
+
' first: a mask hides the next regression that lands in the same place.',
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
const BAND_REMEDY = [
|
|
278
|
+
['content', 'content — text or data moved: a clock, a random seed, an id counter, a request that had not landed'],
|
|
279
|
+
['geometry', 'geometry — the tree or its boxes moved: layout that had not settled, a measurement taken during a transition, a late-arriving image with no intrinsic size'],
|
|
280
|
+
['token', 'token — a declared style moved: a theme applied after first paint, a CSS-in-JS class name that carries a counter'],
|
|
281
|
+
['a11y', 'a11y — a role, name or state moved: focus landing somewhere between readings, an aria-live region updating itself'],
|
|
282
|
+
['texture', 'texture — the painted surface moved with nothing structural behind it'],
|
|
283
|
+
];
|
|
284
|
+
/**
|
|
285
|
+
* Subjects whose change vanished when nothing else had run.
|
|
286
|
+
*
|
|
287
|
+
* Separated from the verdict counts because it is a different kind of work.
|
|
288
|
+
* Every other line in this summary is about a component; these are about the
|
|
289
|
+
* *suite* — some earlier subject left shared state behind, and this one read it.
|
|
290
|
+
* Filed under `changed` they read as a backlog of reviews, and a reviewer who
|
|
291
|
+
* opens one finds a component nobody touched.
|
|
292
|
+
*
|
|
293
|
+
* What this section deliberately does not print is who poisoned them. The run
|
|
294
|
+
* has no evidence for that: a leak that lives in module scope — a singleton
|
|
295
|
+
* store, a cached client, a memoized selector — is invisible to anything a
|
|
296
|
+
* document can observe about itself. The honest handoff is the difference,
|
|
297
|
+
* already resolved to a region and a component and a file, plus the fact that a
|
|
298
|
+
* clean world does not show it. Narrowing to the writer from there is a
|
|
299
|
+
* bisection over run order, which is work for whoever reads this, and cheap
|
|
300
|
+
* once they know it is the answer they are looking for.
|
|
301
|
+
*/
|
|
302
|
+
function orderDependence(report) {
|
|
303
|
+
const leaked = report.observations.filter((o) => o.alone?.reproduced === false);
|
|
304
|
+
if (leaked.length === 0)
|
|
305
|
+
return [];
|
|
306
|
+
return [
|
|
307
|
+
'',
|
|
308
|
+
`order dependence: ${leaked.length} subject(s) changed under the shared session and`,
|
|
309
|
+
' matched the baseline when re-collected alone. These are not component changes and',
|
|
310
|
+
' `accept` refuses them. The writer is not named — module-level state is outside',
|
|
311
|
+
' anything a render can see — so bisect run order over:',
|
|
312
|
+
...leaked.map((observation) => ` ${observation.subject}`),
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The coverage section, in three states — and the third is why this is not
|
|
317
|
+
* simply a list.
|
|
318
|
+
*
|
|
319
|
+
* `absent` means the report's writer never said what it skipped. That is not the
|
|
320
|
+
* same sentence as "it skipped nothing" and must not be printed as one: a reader
|
|
321
|
+
* shown a clean summary over a report that never counted its subjects has been
|
|
322
|
+
* told the suite is green by something that never looked at the suite. Costs
|
|
323
|
+
* three lines of output on every report a `variance run` did not write, which is
|
|
324
|
+
* the price of not collapsing "unknown" into "fine".
|
|
325
|
+
*/
|
|
326
|
+
function coverage(report) {
|
|
327
|
+
const entries = report.notObserved;
|
|
328
|
+
if (entries === undefined) {
|
|
329
|
+
return [
|
|
330
|
+
'coverage: unknown — this report does not state which subjects were not observed.',
|
|
331
|
+
' It was not written by `variance run`, so silence about a subject here cannot be',
|
|
332
|
+
' read as a pass.',
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
if (entries.length === 0)
|
|
336
|
+
return ['coverage: every planned subject was observed.'];
|
|
337
|
+
const failed = entries.filter((entry) => entry.kind === 'failed');
|
|
338
|
+
const excluded = entries.filter((entry) => entry.kind === 'excluded');
|
|
339
|
+
const unreached = entries.filter((entry) => entry.kind === 'unreached');
|
|
340
|
+
// Every entry is named, however many there are. A count alone leaves an agent
|
|
341
|
+
// unable to act, and a capped list reads as complete coverage — the failure
|
|
342
|
+
// `truncated` exists to prevent, applied to the list that matters most.
|
|
343
|
+
return [
|
|
344
|
+
`not observed: ${entries.length} subject(s) — ` +
|
|
345
|
+
`${failed.length} the run could not see, ${excluded.length} excluded by configuration, ` +
|
|
346
|
+
`${unreached.length} not reached by this change`,
|
|
347
|
+
...failed.map(coverageLine),
|
|
348
|
+
...excluded.map(coverageLine),
|
|
349
|
+
...unreached.map(coverageLine),
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* How many subjects were observed, and how many there were to observe.
|
|
354
|
+
*
|
|
355
|
+
* The denominator is the point. A run that narrowed twenty subjects to two opens
|
|
356
|
+
* with `2 subject(s) observed` and reads as a suite of two — the eighteen it
|
|
357
|
+
* reasoned its way out of rendering are the work, and a bare numerator throws
|
|
358
|
+
* them away. Absent when the report never counted what it skipped, because
|
|
359
|
+
* `2 of 2` over a report that did not say is a denominator invented to fill the
|
|
360
|
+
* slot.
|
|
361
|
+
*/
|
|
362
|
+
function observedOf(report) {
|
|
363
|
+
const seen = report.observations.length;
|
|
364
|
+
const planned = seen + (report.notObserved?.length ?? 0);
|
|
365
|
+
const of = report.notObserved === undefined || planned === seen ? '' : ` of ${planned}`;
|
|
366
|
+
return `${seen}${of} subject(s) observed`;
|
|
367
|
+
}
|
|
368
|
+
function coverageLine(entry) {
|
|
369
|
+
return ` [${entry.kind}] ${entry.subject}: ${entry.because}`;
|
|
370
|
+
}
|
|
371
|
+
/** The coverage state as one clause, for the counts line. Empty when there is none to state. */
|
|
372
|
+
function shortfall(report) {
|
|
373
|
+
if (report.notObserved === undefined)
|
|
374
|
+
return ['coverage unknown'];
|
|
375
|
+
return report.notObserved.length === 0
|
|
376
|
+
? []
|
|
377
|
+
: [`${report.notObserved.length} not observed`];
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* One line, and only when there is something to say.
|
|
381
|
+
*
|
|
382
|
+
* Findings do not change the verdict, so they must not be able to make a clean
|
|
383
|
+
* run read as dirty — but a run that found fourteen controls with no accessible
|
|
384
|
+
* name and mentioned none of them has withheld the only thing it knew that a
|
|
385
|
+
* comparison could not have told it.
|
|
386
|
+
*/
|
|
387
|
+
function findingsLine(report) {
|
|
388
|
+
const inspected = report.observations.filter((o) => o.findings !== undefined);
|
|
389
|
+
const subjects = inspected.filter((o) => o.findings.length > 0);
|
|
390
|
+
const found = subjects.flatMap((o) => o.findings ?? []);
|
|
391
|
+
if (found.length > 0) {
|
|
392
|
+
// The dating leads the line rather than riding it, and rather than waiting
|
|
393
|
+
// for `variance_findings`. A reader who stops here stops on the first clause,
|
|
394
|
+
// and a bare count is the one shape of this that reads as *you have introduced
|
|
395
|
+
// fourteen defects* whether or not anything in the run said so.
|
|
396
|
+
const carried = carriedLine(found);
|
|
397
|
+
return [
|
|
398
|
+
`findings: ${arrivalLine(found)}` +
|
|
399
|
+
(carried === undefined ? '' : ` · ${carried}`) +
|
|
400
|
+
` · ${found.length} in ${subjects.length} subject(s), found without a baseline` +
|
|
401
|
+
' — call variance_findings. These do not affect the verdict.',
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
// "Inspected and clean" is worth one line; "nobody inspected anything" is
|
|
405
|
+
// worth nothing here and is said by `variance_findings` when asked, because a
|
|
406
|
+
// reader who did not ask must not be told either way.
|
|
407
|
+
return inspected.length === 0
|
|
408
|
+
? []
|
|
409
|
+
: [`findings: none in ${inspected.length} inspected subject(s).`];
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* The closing sentence when no observation is notable — the one an agent stops
|
|
413
|
+
* reading at, and therefore the one that must not overstate.
|
|
414
|
+
*
|
|
415
|
+
* Only the third branch may say "nothing to review". The first two are the cases
|
|
416
|
+
* where the observations are all clean and the run still is not: an unaccounted
|
|
417
|
+
* report, and a hole the run meant to fill. Both mirror `exitFor`'s `1`.
|
|
418
|
+
*/
|
|
419
|
+
function settlement(report) {
|
|
420
|
+
const entries = report.notObserved;
|
|
421
|
+
if (entries === undefined) {
|
|
422
|
+
return ('no observed subject needs review, but this report never stated what it skipped — ' +
|
|
423
|
+
'it cannot be read as a clean run');
|
|
424
|
+
}
|
|
425
|
+
const failed = entries.filter((entry) => entry.kind === 'failed').length;
|
|
426
|
+
if (failed > 0) {
|
|
427
|
+
return (`no observed subject needs review, but ${failed} subject(s) the run meant to see were ` +
|
|
428
|
+
'not observed — an absent observation is not an unchanged one');
|
|
429
|
+
}
|
|
430
|
+
return 'nothing to review';
|
|
431
|
+
}
|
|
432
|
+
function describeIdentity(report) {
|
|
433
|
+
const { renderer, engine, platform, deviceScaleFactor } = report.identity;
|
|
434
|
+
return `${renderer} (${engine}, ${platform}, ${deviceScaleFactor}x)`;
|
|
435
|
+
}
|
|
436
|
+
//# sourceMappingURL=summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summary.js","sourceRoot":"","sources":["../../src/tools/summary.ts"],"names":[],"mappings":"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","sourcesContent":["import { arrivalLine, carriedLine } from '@variance-authority/report';\nimport type { NotObserved, RunReport } from '@variance-authority/report';\nimport { presentationSummary } from '../presentation.js';\nimport { narrowing } from './narrowing.js';\nimport { NO_ARGS, type Tool } from './tool.js';\n\n/**\n * `variance_summary`, and the sections it is assembled from.\n *\n * The helpers below live with it rather than somewhere shared because none of\n * them is a general fact about a report — each is one paragraph of this one\n * answer, and each exists to stop this answer claiming something the run never\n * established. Coverage, order dependence, findings and the closing sentence all\n * repeat the same rule in different words: silence about a subject is not a pass.\n * Read them in the order `run` prints them; that order is the argument.\n */\n\n/**\n * Overview: what happened, what needs attention, and what was never looked at.\n *\n * Deliberately does not list unchanged subjects. A run over 300 subjects where\n * two changed should print two lines — a list as long as the suite is the \"100\n * changes? merge\" failure in its purest form, and the reader stops reading long\n * before the interesting line.\n *\n * The coverage section is the exception to that economy and is not negotiable.\n * A run that planned 300 subjects, failed on 50 and found the other 250\n * unchanged has an observation list in which every entry is clean; a summary\n * computed from observations alone therefore reports it as a clean run, and an\n * agent acts on that. So `nothing to review` is claimed only from a report that\n * accounted for every subject it planned — never from silence, and never over a\n * subject the run meant to see and could not. That is the same rule the CLI's\n * exit code applies, stated in the same words, because a human and an agent\n * reading one artifact must not be able to disagree about whether it was green.\n */\nexport const summarize: Tool = {\n name: 'variance_summary',\n description:\n 'What the last visual run found: counts by verdict, then one line per subject that ' +\n 'needs attention, then which subjects were not observed at all. Unchanged subjects are ' +\n 'counted, not listed. Start here.',\n inputSchema: NO_ARGS,\n\n run(report) {\n const counts = new Map<string, number>();\n for (const observation of report.observations) {\n counts.set(observation.verdict, (counts.get(observation.verdict) ?? 0) + 1);\n }\n\n const header = [\n `${observedOf(report)}, ${report.retention} run at ${report.at}`,\n `rendered by ${describeIdentity(report)}`,\n // The coverage state joins the verdict counts rather than only appearing in\n // the section below, because that section is as long as the hole is and the\n // hole is what a reader most needs in the first four lines. A run that lost\n // 50 subjects otherwise opens with \"250 unchanged\" and says so 50 lines later.\n [...counts.entries()]\n .map(([verdict, count]) => `${count} ${verdict}`)\n .concat(shortfall(report))\n .join(', '),\n ...(report.intent !== undefined ? [`intent: ${report.intent}`] : []),\n ...narrowing(report),\n ];\n\n // `ignored` is counted in the header and does not get a line of its own.\n // It is a real state and it is not a finding: a shared header carrying a\n // clock puts every subject in the suite here, and three hundred lines saying\n // \"you already decided not to look at this\" is the output nobody reads —\n // which is how the *other* lines get missed. The header count keeps it\n // visible, and `variance run` prints the per-rule ledger beneath it.\n //\n // `unstable` goes the other way and joins this list even when the verdict is\n // green, and the reason is the last line of this answer: a run whose only\n // finding was an unstable subject printed \"nothing to review\" directly under\n // a heading naming six of them. Under `--flakes` that is the *normal* case —\n // every subject agrees with its baseline, and the whole point of the mode is\n // what agreeing with a baseline does not say — so it was not an edge.\n const notable = report.observations.filter(\n (o) =>\n (o.verdict !== 'unchanged' && o.verdict !== 'ignored') ||\n (o.unstable !== undefined && o.unstable.absorbed === undefined),\n );\n\n return [\n ...header,\n ...(notable.length === 0\n ? []\n : [\n '',\n ...notable.map((observation) => {\n const cause = observation.regions.find((region) => region.cause);\n const lead = cause?.component !== undefined ? ` — ${cause.component}` : '';\n // Labelled by what it *is* rather than by its verdict. The verdict\n // stays `changed` — the pixels really did move — but a reader who\n // acts on that word reviews a component that nothing edited. The\n // two need opposite actions, so they get different words.\n // An absorbed instability is not one of these. The subject declared\n // that it does not assert on what moved, so it is neither a defect\n // nor a different word for its verdict — it is listed further down,\n // under a heading that says so.\n const reported =\n observation.unstable !== undefined && observation.unstable.absorbed === undefined\n ? observation.unstable\n : undefined;\n const label = reported\n ? 'unstable'\n : observation.alone?.reproduced === false\n ? 'order-dependent'\n : observation.verdict;\n const because = reported\n ? reported.because\n : observation.alone?.reproduced === false\n ? observation.alone.because\n : observation.because;\n return `[${label}] ${observation.subject}${lead}: ${because}`;\n }),\n ]),\n '',\n ...coverage(report),\n ...drift(report),\n ...instability(report),\n ...orderDependence(report),\n ...findingsLine(report),\n ...presentationSummary(report),\n ...(notable.length === 0 ? ['', settlement(report)] : []),\n ].join('\\n');\n },\n};\n\n/**\n * Subjects that did not agree with themselves, and the one section here written\n * to be *acted on* rather than reviewed.\n *\n * Above every other finding, because it invalidates them. A subject that reads\n * differently twice in a row has a verdict that was decided by which of the two\n * readings the run happened to take first, and an agent that starts reviewing\n * its regions is reading a diff against a coin flip. Nothing else in this\n * summary is worth doing on these subjects until this is.\n *\n * The component and the band are printed because they are what makes the fix\n * bounded. \"This subject is flaky\" is a page to read; `Clock (content)` is a node\n * whose text moves between two readings taken seconds apart, and the causes of\n * that are a short list — a clock, a random seed, an id counter, a request that\n * had not landed. The band narrows it further: `content` is data, `geometry` is\n * layout that has not settled, `token` is a style that is still being applied.\n *\n * This is where the base layer hands over. Animations are pinned, GIFs are frozen\n * on the wire, fonts and images are waited for, and every asset's bytes are in\n * the environment key — so a subject that still disagrees with itself is past\n * everything a recipe can do, and is a defect in the page with a name attached\n * rather than a tolerance to widen.\n */\nfunction instability(report: RunReport): readonly string[] {\n const unstable = report.observations.filter(\n (o) => o.unstable !== undefined && o.unstable.absorbed === undefined,\n );\n if (unstable.length === 0) return absorbedInstability(report);\n\n const bands = new Set(unstable.flatMap((o) => o.unstable?.bands ?? []));\n\n return [\n '',\n `UNSTABLE: ${unstable.length} subject(s) were read twice, seconds apart, with nothing`,\n ' changed in between, and the two readings disagreed. Their verdicts were decided by',\n ' whichever reading came first, so do not review their regions and do not accept them',\n ' (`accept` refuses these). Fix what moves between two readings of the same page:',\n ...unstable.flatMap((observation) => {\n const moved = observation.unstable;\n const named = (moved?.components ?? [])\n .map((component) =>\n component.file === undefined ? component.name : `${component.name} ${component.file}`,\n )\n .join(', ');\n const where = named === '' ? '' : ` — ${named}`;\n const inBands = moved === undefined || moved.bands.length === 0 ? '' : ` (${moved.bands.join(', ')})`;\n return [` ${observation.subject}${where}${inBands}`, ...recurrence(report, observation.subject)];\n }),\n ...remedies(bands),\n // The loop closes here. Everything above tells a reader what moved and how\n // often; this is the one line that says how to find out whether the edit\n // they are about to make worked — without re-running three hundred subjects\n // and without waiting for tomorrow's build to be the experiment.\n ' Check a fix by reading the same subject twice again, and nothing else:',\n ...unstable.map((observation) => ` variance run --subjects '${observation.subject}' --flakes`),\n ' It exits 1 while the two readings still disagree, even with every verdict green.',\n ...absorbedInstability(report),\n ];\n}\n\n/**\n * Tokens whose value moved in this run, and how far they have travelled.\n *\n * The one finding on this page that no comparison could have produced. Every\n * other line answers *what moved*; this answers *how much it has moved\n * altogether*, which is a sum across approvals and is therefore invisible to\n * every review that approved one of them. Eleven correct approvals of 2px each\n * are eleven correct decisions and one 22px change nobody made.\n *\n * Placed above the instability section deliberately: it is rarer, it is never\n * noise, and it is the finding a reader would most regret scrolling past.\n */\nfunction drift(report: RunReport): readonly string[] {\n const moved = Object.entries(report.drift ?? {});\n if (moved.length === 0) return [];\n\n return [\n '',\n `DRIFT: ${moved.length} token(s) moved in this run, and the record says what they have`,\n ' drifted to across every approved change in the window. No single review saw these',\n ' totals, because each of them approved one step:',\n ...moved.map(([, record]) => ` ${record.because}`),\n ];\n}\n\n/**\n * What the record says about a subject that just read differently.\n *\n * Two readings are a **lower bound** and can never be more: a subject that flakes\n * one time in fifty passes that check forty-nine runs out of fifty. This is the\n * only line in the answer that can distinguish *a fixture that has been bad for a\n * month* from *something that started today*, and those need different people.\n *\n * The absent case gets a line too, and it is the one worth being careful about.\n * Silence here would read as \"first time\", which is a claim — and it is the claim\n * a reader most wants to be true.\n */\nfunction recurrence(report: RunReport, subject: string): readonly string[] {\n const record = report.flakiness?.[subject];\n\n if (record === undefined) {\n return [\n ' no history record answered for this subject, so nothing here says whether it has',\n ' happened before. That is silence, not a first occurrence.',\n ];\n }\n\n const shape =\n record.sweepsSince > 0 && record.occurrences > 1\n ? ' — it has been quiet since, so check whether a fix already landed before writing one'\n : record.occurrences > 1\n ? ' — recurring, and the most recent sweep still saw it: the fixture is the bug'\n : ' — the record has not seen this before';\n\n return [` ${record.because}`, shape];\n}\n\n/**\n * Subjects that moved between two readings, in bands they do not claim to assert\n * on.\n *\n * Counted and named, never silent — the same rule `ignored` follows for pixels,\n * one level up and about kinds. A route declared `layout` with a live clock in it\n * is *working as declared*, and reporting it as a defect would make every\n * route-level test red for exactly the reason its level was written. But a\n * declaration that is quietly absorbing movement is also how a suite ends up\n * green over a surface nobody watches, so it gets a line and names the rule that\n * did it — which is what makes it auditable later.\n */\nfunction absorbedInstability(report: RunReport): readonly string[] {\n const absorbed = report.observations.filter((o) => o.unstable?.absorbed !== undefined);\n if (absorbed.length === 0) return [];\n\n return [\n '',\n `not asserted on: ${absorbed.length} subject(s) read differently between two readings,`,\n ' entirely in bands their declared level does not assert on. Working as declared, and',\n ' listed because a declaration nobody re-reads is how a suite stops watching something:',\n ...absorbed.map((observation) => {\n const bands = observation.unstable?.bands ?? [];\n const rule = observation.unstable?.absorbed?.rule ?? 'a sensitivity rule';\n const level = observation.unstable?.absorbed?.level ?? 'its level';\n return ` ${observation.subject} — ${bands.join(', ')}, absorbed by \\`${rule}\\` (asserts on ${level})`;\n }),\n ];\n}\n\n/**\n * What the band means for the fix, which is the whole reason a band is reported.\n *\n * A frequency band is not a severity — it is a statement about *what kind of\n * thing* moved, and each kind has a short list of causes. An agent handed \"this\n * subject is flaky\" has a page to read; one handed `content` has four candidates\n * and can check all of them in a minute.\n *\n * Masking is listed last and hedged, deliberately. A clock genuinely is a clock\n * and an ignore is the right answer for it — but reaching for one first is how a\n * suite ends up green over a surface nobody watches, and the same mask that hides\n * this hides the regression that later lands in the same place.\n */\nfunction remedies(bands: ReadonlySet<string>): readonly string[] {\n const lines = [...BAND_REMEDY].filter(([band]) => bands.has(band)).map(([, hint]) => ` ${hint}`);\n if (lines.length === 0) return [];\n\n return [\n ' What each band that moved usually means:',\n ...lines,\n ' If the movement is genuinely inherent to the subject — a real clock, a live feed —',\n ' mask the *element* rather than accept the subject: the component named above is it,',\n ' and an element-scoped ignore follows it when layout moves. Reach for that second, not',\n ' first: a mask hides the next regression that lands in the same place.',\n ];\n}\n\nconst BAND_REMEDY: readonly (readonly [string, string])[] = [\n ['content', 'content — text or data moved: a clock, a random seed, an id counter, a request that had not landed'],\n ['geometry', 'geometry — the tree or its boxes moved: layout that had not settled, a measurement taken during a transition, a late-arriving image with no intrinsic size'],\n ['token', 'token — a declared style moved: a theme applied after first paint, a CSS-in-JS class name that carries a counter'],\n ['a11y', 'a11y — a role, name or state moved: focus landing somewhere between readings, an aria-live region updating itself'],\n ['texture', 'texture — the painted surface moved with nothing structural behind it'],\n];\n\n/**\n * Subjects whose change vanished when nothing else had run.\n *\n * Separated from the verdict counts because it is a different kind of work.\n * Every other line in this summary is about a component; these are about the\n * *suite* — some earlier subject left shared state behind, and this one read it.\n * Filed under `changed` they read as a backlog of reviews, and a reviewer who\n * opens one finds a component nobody touched.\n *\n * What this section deliberately does not print is who poisoned them. The run\n * has no evidence for that: a leak that lives in module scope — a singleton\n * store, a cached client, a memoized selector — is invisible to anything a\n * document can observe about itself. The honest handoff is the difference,\n * already resolved to a region and a component and a file, plus the fact that a\n * clean world does not show it. Narrowing to the writer from there is a\n * bisection over run order, which is work for whoever reads this, and cheap\n * once they know it is the answer they are looking for.\n */\nfunction orderDependence(report: RunReport): readonly string[] {\n const leaked = report.observations.filter((o) => o.alone?.reproduced === false);\n if (leaked.length === 0) return [];\n\n return [\n '',\n `order dependence: ${leaked.length} subject(s) changed under the shared session and`,\n ' matched the baseline when re-collected alone. These are not component changes and',\n ' `accept` refuses them. The writer is not named — module-level state is outside',\n ' anything a render can see — so bisect run order over:',\n ...leaked.map((observation) => ` ${observation.subject}`),\n ];\n}\n\n/**\n * The coverage section, in three states — and the third is why this is not\n * simply a list.\n *\n * `absent` means the report's writer never said what it skipped. That is not the\n * same sentence as \"it skipped nothing\" and must not be printed as one: a reader\n * shown a clean summary over a report that never counted its subjects has been\n * told the suite is green by something that never looked at the suite. Costs\n * three lines of output on every report a `variance run` did not write, which is\n * the price of not collapsing \"unknown\" into \"fine\".\n */\nfunction coverage(report: RunReport): readonly string[] {\n const entries = report.notObserved;\n\n if (entries === undefined) {\n return [\n 'coverage: unknown — this report does not state which subjects were not observed.',\n ' It was not written by `variance run`, so silence about a subject here cannot be',\n ' read as a pass.',\n ];\n }\n\n if (entries.length === 0) return ['coverage: every planned subject was observed.'];\n\n const failed = entries.filter((entry) => entry.kind === 'failed');\n const excluded = entries.filter((entry) => entry.kind === 'excluded');\n const unreached = entries.filter((entry) => entry.kind === 'unreached');\n\n // Every entry is named, however many there are. A count alone leaves an agent\n // unable to act, and a capped list reads as complete coverage — the failure\n // `truncated` exists to prevent, applied to the list that matters most.\n return [\n `not observed: ${entries.length} subject(s) — ` +\n `${failed.length} the run could not see, ${excluded.length} excluded by configuration, ` +\n `${unreached.length} not reached by this change`,\n ...failed.map(coverageLine),\n ...excluded.map(coverageLine),\n ...unreached.map(coverageLine),\n ];\n}\n\n/**\n * How many subjects were observed, and how many there were to observe.\n *\n * The denominator is the point. A run that narrowed twenty subjects to two opens\n * with `2 subject(s) observed` and reads as a suite of two — the eighteen it\n * reasoned its way out of rendering are the work, and a bare numerator throws\n * them away. Absent when the report never counted what it skipped, because\n * `2 of 2` over a report that did not say is a denominator invented to fill the\n * slot.\n */\nfunction observedOf(report: RunReport): string {\n const seen = report.observations.length;\n const planned = seen + (report.notObserved?.length ?? 0);\n const of = report.notObserved === undefined || planned === seen ? '' : ` of ${planned}`;\n return `${seen}${of} subject(s) observed`;\n}\n\nfunction coverageLine(entry: NotObserved): string {\n return ` [${entry.kind}] ${entry.subject}: ${entry.because}`;\n}\n\n/** The coverage state as one clause, for the counts line. Empty when there is none to state. */\nfunction shortfall(report: RunReport): readonly string[] {\n if (report.notObserved === undefined) return ['coverage unknown'];\n return report.notObserved.length === 0\n ? []\n : [`${report.notObserved.length} not observed`];\n}\n\n/**\n * One line, and only when there is something to say.\n *\n * Findings do not change the verdict, so they must not be able to make a clean\n * run read as dirty — but a run that found fourteen controls with no accessible\n * name and mentioned none of them has withheld the only thing it knew that a\n * comparison could not have told it.\n */\nfunction findingsLine(report: RunReport): readonly string[] {\n const inspected = report.observations.filter((o) => o.findings !== undefined);\n const subjects = inspected.filter((o) => o.findings!.length > 0);\n const found = subjects.flatMap((o) => o.findings ?? []);\n\n if (found.length > 0) {\n // The dating leads the line rather than riding it, and rather than waiting\n // for `variance_findings`. A reader who stops here stops on the first clause,\n // and a bare count is the one shape of this that reads as *you have introduced\n // fourteen defects* whether or not anything in the run said so.\n const carried = carriedLine(found);\n\n return [\n `findings: ${arrivalLine(found)}` +\n (carried === undefined ? '' : ` · ${carried}`) +\n ` · ${found.length} in ${subjects.length} subject(s), found without a baseline` +\n ' — call variance_findings. These do not affect the verdict.',\n ];\n }\n\n // \"Inspected and clean\" is worth one line; \"nobody inspected anything\" is\n // worth nothing here and is said by `variance_findings` when asked, because a\n // reader who did not ask must not be told either way.\n return inspected.length === 0\n ? []\n : [`findings: none in ${inspected.length} inspected subject(s).`];\n}\n\n/**\n * The closing sentence when no observation is notable — the one an agent stops\n * reading at, and therefore the one that must not overstate.\n *\n * Only the third branch may say \"nothing to review\". The first two are the cases\n * where the observations are all clean and the run still is not: an unaccounted\n * report, and a hole the run meant to fill. Both mirror `exitFor`'s `1`.\n */\nfunction settlement(report: RunReport): string {\n const entries = report.notObserved;\n\n if (entries === undefined) {\n return (\n 'no observed subject needs review, but this report never stated what it skipped — ' +\n 'it cannot be read as a clean run'\n );\n }\n\n const failed = entries.filter((entry) => entry.kind === 'failed').length;\n if (failed > 0) {\n return (\n `no observed subject needs review, but ${failed} subject(s) the run meant to see were ` +\n 'not observed — an absent observation is not an unchanged one'\n );\n }\n\n return 'nothing to review';\n}\n\nfunction describeIdentity(report: RunReport): string {\n const { renderer, engine, platform, deviceScaleFactor } = report.identity;\n return `${renderer} (${engine}, ${platform}, ${deviceScaleFactor}x)`;\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { VantageState } from '@variance-authority/vantage';
|
|
2
|
+
import { type Tool } from './tool.js';
|
|
3
|
+
/**
|
|
4
|
+
* Everything one test has announced, in order, whatever realm said it.
|
|
5
|
+
*
|
|
6
|
+
* This is the answer a timeout cannot give from outside the worker. A test that
|
|
7
|
+
* is hanging has usually heard *something*, and which realm stopped talking is
|
|
8
|
+
* the whole of the diagnosis: nothing at all means no listener or no `vae` call,
|
|
9
|
+
* and a page that spoke while a service did not means the request never reached
|
|
10
|
+
* the service or never came back.
|
|
11
|
+
*
|
|
12
|
+
* Asked while the test is still running, which is the point. The answer is a
|
|
13
|
+
* snapshot and says so.
|
|
14
|
+
*/
|
|
15
|
+
export declare const testSignals: Tool<VantageState>;
|
|
16
|
+
//# sourceMappingURL=test-signals.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { stringArg } from './tool.js';
|
|
2
|
+
import { announcement, heading, unattached } from './vantage-lines.js';
|
|
3
|
+
/**
|
|
4
|
+
* Everything one test has announced, in order, whatever realm said it.
|
|
5
|
+
*
|
|
6
|
+
* This is the answer a timeout cannot give from outside the worker. A test that
|
|
7
|
+
* is hanging has usually heard *something*, and which realm stopped talking is
|
|
8
|
+
* the whole of the diagnosis: nothing at all means no listener or no `vae` call,
|
|
9
|
+
* and a page that spoke while a service did not means the request never reached
|
|
10
|
+
* the service or never came back.
|
|
11
|
+
*
|
|
12
|
+
* Asked while the test is still running, which is the point. The answer is a
|
|
13
|
+
* snapshot and says so.
|
|
14
|
+
*/
|
|
15
|
+
export const testSignals = {
|
|
16
|
+
name: 'variance_test_signals',
|
|
17
|
+
description: 'Everything one test has announced, in order, with the realm that said each — plus work that started and never ended. Answers while the test is still running.',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
test: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'A test id from variance_run_signals, or part of its title.',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ['test'],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
run(state, input) {
|
|
30
|
+
if (state.tests.length === 0)
|
|
31
|
+
return unattached(state);
|
|
32
|
+
const asked = stringArg(input, 'test');
|
|
33
|
+
const found = locate(state, asked);
|
|
34
|
+
return typeof found === 'string' ? found : described(found);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
/** The one test meant, or the sentence explaining why it is not one test. */
|
|
38
|
+
function locate(state, asked) {
|
|
39
|
+
const byId = state.tests.find((test) => test.id === asked);
|
|
40
|
+
if (byId !== undefined)
|
|
41
|
+
return byId;
|
|
42
|
+
const byTitle = state.tests.filter((test) => test.title === asked);
|
|
43
|
+
if (byTitle.length === 1 && byTitle[0] !== undefined)
|
|
44
|
+
return byTitle[0];
|
|
45
|
+
const lowered = asked.toLowerCase();
|
|
46
|
+
const partial = state.tests.filter((test) => test.title.toLowerCase().includes(lowered));
|
|
47
|
+
if (partial.length === 1 && partial[0] !== undefined)
|
|
48
|
+
return partial[0];
|
|
49
|
+
if (partial.length === 0) {
|
|
50
|
+
return [
|
|
51
|
+
`No test here matches ${asked}. ${state.tests.length} test(s) have reported:`,
|
|
52
|
+
...state.tests.map((test) => ` ${test.title} [${test.id}]`),
|
|
53
|
+
].join('\n');
|
|
54
|
+
}
|
|
55
|
+
return [
|
|
56
|
+
`${partial.length} tests match ${asked}; ask for one by id:`,
|
|
57
|
+
...partial.map((test) => ` ${test.title} [${test.id}]`),
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
function described(test) {
|
|
61
|
+
return [
|
|
62
|
+
`${heading(test)} — ${test.state}`,
|
|
63
|
+
'',
|
|
64
|
+
...heard(test),
|
|
65
|
+
...pending(test),
|
|
66
|
+
...remarks(test),
|
|
67
|
+
...ended(test),
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
function heard(test) {
|
|
71
|
+
if (test.heard.length === 0) {
|
|
72
|
+
return [
|
|
73
|
+
'Nothing has been announced in this execution, by any realm. Either no ' +
|
|
74
|
+
'listener is installed for it — the test destructures no `events` ' +
|
|
75
|
+
'fixture — or the code it drives does not call `vae` yet.',
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
return [
|
|
79
|
+
test.forgotten === 0
|
|
80
|
+
? 'Heard, in order:'
|
|
81
|
+
: `Heard, in order (the first ${test.forgotten} were dropped to stay bounded):`,
|
|
82
|
+
...test.heard.map((event) => ` ${announcement(event)}`),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
function pending(test) {
|
|
86
|
+
if (test.pending.length === 0)
|
|
87
|
+
return [];
|
|
88
|
+
return [
|
|
89
|
+
'',
|
|
90
|
+
'Started and never ended:',
|
|
91
|
+
...test.pending.map((event) => ` ${event.realm} ${event.location} / ${event.subject} / ${event.action}`),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
function remarks(test) {
|
|
95
|
+
if (test.remarks.length === 0)
|
|
96
|
+
return [];
|
|
97
|
+
return ['', 'The listener also knows:', ...test.remarks.map((line) => ` ${line}`)];
|
|
98
|
+
}
|
|
99
|
+
function ended(test) {
|
|
100
|
+
if (test.state === 'running')
|
|
101
|
+
return ['', 'Still running; this is where it had got to.'];
|
|
102
|
+
if (test.error === undefined)
|
|
103
|
+
return ['', `Ended: ${test.state}.`];
|
|
104
|
+
return ['', `Ended: ${test.state}.`, ...test.error.split('\n').map((line) => ` ${line}`)];
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=test-signals.js.map
|