@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/LICENSE +21 -0
  3. package/README.md +407 -0
  4. package/dist/bin.d.ts +3 -0
  5. package/dist/bin.js +32 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/index.d.ts +35 -0
  8. package/dist/index.js +31 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/observability-subject.d.ts +22 -0
  11. package/dist/observability-subject.js +2 -0
  12. package/dist/observability-subject.js.map +1 -0
  13. package/dist/presentation.d.ts +6 -0
  14. package/dist/presentation.js +64 -0
  15. package/dist/presentation.js.map +1 -0
  16. package/dist/protocol.d.ts +82 -0
  17. package/dist/protocol.js +186 -0
  18. package/dist/protocol.js.map +1 -0
  19. package/dist/server.d.ts +71 -0
  20. package/dist/server.js +103 -0
  21. package/dist/server.js.map +1 -0
  22. package/dist/tools/adjudicate.d.ts +25 -0
  23. package/dist/tools/adjudicate.js +122 -0
  24. package/dist/tools/adjudicate.js.map +1 -0
  25. package/dist/tools/attention.d.ts +13 -0
  26. package/dist/tools/attention.js +151 -0
  27. package/dist/tools/attention.js.map +1 -0
  28. package/dist/tools/changelog.d.ts +38 -0
  29. package/dist/tools/changelog.js +201 -0
  30. package/dist/tools/changelog.js.map +1 -0
  31. package/dist/tools/changes.d.ts +31 -0
  32. package/dist/tools/changes.js +120 -0
  33. package/dist/tools/changes.js.map +1 -0
  34. package/dist/tools/composition.d.ts +13 -0
  35. package/dist/tools/composition.js +366 -0
  36. package/dist/tools/composition.js.map +1 -0
  37. package/dist/tools/describe.d.ts +20 -0
  38. package/dist/tools/describe.js +279 -0
  39. package/dist/tools/describe.js.map +1 -0
  40. package/dist/tools/diff.d.ts +20 -0
  41. package/dist/tools/diff.js +85 -0
  42. package/dist/tools/diff.js.map +1 -0
  43. package/dist/tools/explain-verdict.d.ts +25 -0
  44. package/dist/tools/explain-verdict.js +92 -0
  45. package/dist/tools/explain-verdict.js.map +1 -0
  46. package/dist/tools/findings.d.ts +40 -0
  47. package/dist/tools/findings.js +130 -0
  48. package/dist/tools/findings.js.map +1 -0
  49. package/dist/tools/narrowing.d.ts +26 -0
  50. package/dist/tools/narrowing.js +38 -0
  51. package/dist/tools/narrowing.js.map +1 -0
  52. package/dist/tools/observability.d.ts +7 -0
  53. package/dist/tools/observability.js +199 -0
  54. package/dist/tools/observability.js.map +1 -0
  55. package/dist/tools/presentations.d.ts +6 -0
  56. package/dist/tools/presentations.js +74 -0
  57. package/dist/tools/presentations.js.map +1 -0
  58. package/dist/tools/run-signals.d.ts +12 -0
  59. package/dist/tools/run-signals.js +115 -0
  60. package/dist/tools/run-signals.js.map +1 -0
  61. package/dist/tools/scenarios.d.ts +6 -0
  62. package/dist/tools/scenarios.js +62 -0
  63. package/dist/tools/scenarios.js.map +1 -0
  64. package/dist/tools/source-tests.d.ts +5 -0
  65. package/dist/tools/source-tests.js +95 -0
  66. package/dist/tools/source-tests.js.map +1 -0
  67. package/dist/tools/subject.d.ts +45 -0
  68. package/dist/tools/subject.js +57 -0
  69. package/dist/tools/subject.js.map +1 -0
  70. package/dist/tools/summary.d.ts +31 -0
  71. package/dist/tools/summary.js +436 -0
  72. package/dist/tools/summary.js.map +1 -0
  73. package/dist/tools/test-signals.d.ts +16 -0
  74. package/dist/tools/test-signals.js +106 -0
  75. package/dist/tools/test-signals.js.map +1 -0
  76. package/dist/tools/tool.d.ts +64 -0
  77. package/dist/tools/tool.js +9 -0
  78. package/dist/tools/tool.js.map +1 -0
  79. package/dist/tools/trace-component.d.ts +17 -0
  80. package/dist/tools/trace-component.js +50 -0
  81. package/dist/tools/trace-component.js.map +1 -0
  82. package/dist/tools/vantage-lines.d.ts +44 -0
  83. package/dist/tools/vantage-lines.js +66 -0
  84. package/dist/tools/vantage-lines.js.map +1 -0
  85. package/dist/tools/variations.d.ts +27 -0
  86. package/dist/tools/variations.js +125 -0
  87. package/dist/tools/variations.js.map +1 -0
  88. package/dist/tools.d.ts +120 -0
  89. package/dist/tools.js +127 -0
  90. package/dist/tools.js.map +1 -0
  91. package/mark.svg +30 -0
  92. 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