@setsquare/review-sdk 0.0.1 → 0.1.1

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 (116) hide show
  1. package/NOTICE +3 -2
  2. package/README.md +287 -3
  3. package/dist/autoscan.d.ts +23 -1
  4. package/dist/autoscan.d.ts.map +1 -1
  5. package/dist/autoscan.js +220 -15
  6. package/dist/autoscan.js.map +1 -1
  7. package/dist/brand.d.ts +51 -2
  8. package/dist/brand.d.ts.map +1 -1
  9. package/dist/brand.js +560 -5
  10. package/dist/brand.js.map +1 -1
  11. package/dist/checks/context-change.d.ts +96 -0
  12. package/dist/checks/context-change.d.ts.map +1 -0
  13. package/dist/checks/context-change.js +655 -0
  14. package/dist/checks/context-change.js.map +1 -0
  15. package/dist/checks/focus.d.ts +154 -0
  16. package/dist/checks/focus.d.ts.map +1 -0
  17. package/dist/checks/focus.js +1123 -0
  18. package/dist/checks/focus.js.map +1 -0
  19. package/dist/checks/heuristics.d.ts +117 -0
  20. package/dist/checks/heuristics.d.ts.map +1 -0
  21. package/dist/checks/heuristics.js +961 -0
  22. package/dist/checks/heuristics.js.map +1 -0
  23. package/dist/checks/keyboard.d.ts +106 -0
  24. package/dist/checks/keyboard.d.ts.map +1 -0
  25. package/dist/checks/keyboard.js +452 -0
  26. package/dist/checks/keyboard.js.map +1 -0
  27. package/dist/checks/reflow.d.ts +75 -0
  28. package/dist/checks/reflow.d.ts.map +1 -0
  29. package/dist/checks/reflow.js +285 -0
  30. package/dist/checks/reflow.js.map +1 -0
  31. package/dist/checks/rule-pack.d.ts +1 -1
  32. package/dist/checks/rule-pack.d.ts.map +1 -1
  33. package/dist/checks/rule-pack.js +350 -45
  34. package/dist/checks/rule-pack.js.map +1 -1
  35. package/dist/checks/shortcuts.d.ts +71 -0
  36. package/dist/checks/shortcuts.d.ts.map +1 -0
  37. package/dist/checks/shortcuts.js +196 -0
  38. package/dist/checks/shortcuts.js.map +1 -0
  39. package/dist/checks/versions.d.ts.map +1 -1
  40. package/dist/checks/versions.js +33 -1
  41. package/dist/checks/versions.js.map +1 -1
  42. package/dist/contract.d.ts +32 -0
  43. package/dist/contract.d.ts.map +1 -1
  44. package/dist/covers.d.ts +56 -0
  45. package/dist/covers.d.ts.map +1 -0
  46. package/dist/covers.js +135 -0
  47. package/dist/covers.js.map +1 -0
  48. package/dist/dedupe.d.ts +23 -0
  49. package/dist/dedupe.d.ts.map +1 -1
  50. package/dist/dedupe.js +96 -2
  51. package/dist/dedupe.js.map +1 -1
  52. package/dist/document.d.ts.map +1 -1
  53. package/dist/document.js +144 -0
  54. package/dist/document.js.map +1 -1
  55. package/dist/envelope.d.ts +8 -0
  56. package/dist/envelope.d.ts.map +1 -1
  57. package/dist/envelope.js +106 -16
  58. package/dist/envelope.js.map +1 -1
  59. package/dist/fixture.d.ts.map +1 -1
  60. package/dist/fixture.js +33 -4
  61. package/dist/fixture.js.map +1 -1
  62. package/dist/html-report.d.ts +24 -7
  63. package/dist/html-report.d.ts.map +1 -1
  64. package/dist/html-report.js +608 -136
  65. package/dist/html-report.js.map +1 -1
  66. package/dist/index.d.ts +13 -0
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +13 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/insights.d.ts.map +1 -1
  71. package/dist/insights.js +9 -12
  72. package/dist/insights.js.map +1 -1
  73. package/dist/links.d.ts +43 -0
  74. package/dist/links.d.ts.map +1 -1
  75. package/dist/links.js +43 -0
  76. package/dist/links.js.map +1 -1
  77. package/dist/observe.d.ts +8 -0
  78. package/dist/observe.d.ts.map +1 -1
  79. package/dist/observe.js +45 -1
  80. package/dist/observe.js.map +1 -1
  81. package/dist/options.d.ts +53 -0
  82. package/dist/options.d.ts.map +1 -1
  83. package/dist/options.js +3 -0
  84. package/dist/options.js.map +1 -1
  85. package/dist/reading-level.d.ts +22 -1
  86. package/dist/reading-level.d.ts.map +1 -1
  87. package/dist/reading-level.js +122 -19
  88. package/dist/reading-level.js.map +1 -1
  89. package/dist/reporter.d.ts.map +1 -1
  90. package/dist/reporter.js +31 -15
  91. package/dist/reporter.js.map +1 -1
  92. package/dist/scan.d.ts +36 -1
  93. package/dist/scan.d.ts.map +1 -1
  94. package/dist/scan.js +16 -2
  95. package/dist/scan.js.map +1 -1
  96. package/dist/screens.d.ts +55 -4
  97. package/dist/screens.d.ts.map +1 -1
  98. package/dist/screens.js +243 -43
  99. package/dist/screens.js.map +1 -1
  100. package/dist/settle.d.ts +35 -0
  101. package/dist/settle.d.ts.map +1 -1
  102. package/dist/settle.js +43 -2
  103. package/dist/settle.js.map +1 -1
  104. package/dist/summary.d.ts +35 -0
  105. package/dist/summary.d.ts.map +1 -1
  106. package/dist/summary.js +248 -28
  107. package/dist/summary.js.map +1 -1
  108. package/dist/unscannable-reasons.d.ts +30 -0
  109. package/dist/unscannable-reasons.d.ts.map +1 -0
  110. package/dist/unscannable-reasons.js +53 -0
  111. package/dist/unscannable-reasons.js.map +1 -0
  112. package/dist/viewports.d.ts +105 -0
  113. package/dist/viewports.d.ts.map +1 -0
  114. package/dist/viewports.js +134 -0
  115. package/dist/viewports.js.map +1 -0
  116. package/package.json +5 -4
package/dist/settle.js CHANGED
@@ -26,9 +26,44 @@
26
26
  *
27
27
  * Every scan carries its settle diagnostics, so a suspicious result is
28
28
  * identifiable in the evidence rather than silently trusted.
29
+ *
30
+ * **WHAT HAPPENS TO A PAGE THAT SETTLES LATE**, recorded here because #137 asked
31
+ * whether it is scanned or dropped and observed that it was currently neither —
32
+ * an accident of where the ceiling happened to fall. It is not an accident, and
33
+ * the behaviour is worth stating rather than changing:
34
+ *
35
+ * - Still on screen when the test ends → **scanned, twice if needed.** The
36
+ * ceiling fires and scans the skeleton, then the teardown sweep re-scans
37
+ * because the mutation counter shows the page changed since. Dedupe keeps the
38
+ * better one. Proved rather than asserted: `fixtures/.../public.spec.ts`
39
+ * drives `/slow?delay=7000`, two seconds past the ceiling, and the surviving
40
+ * evidence carries both of that route's late violations.
41
+ * - Navigated away from mid-render → **cannot be scanned by anything.** It is
42
+ * gone before teardown arrives. Recorded in the unscannable ledger rather
43
+ * than dropped silently, and raising `settleTimeoutMs` does not help, because
44
+ * the limit there is the test's pace and not our window.
45
+ *
46
+ * So the ceiling costs no evidence on its own. What costs evidence is a test
47
+ * moving on, and that is the customer's to change or accept.
29
48
  */
30
49
  /** Name of the page-side callback the SDK exposes. Namespaced; documented pollution. */
31
50
  export const MUTATION_TOTAL = '__setsquareMutationTotal';
51
+ /**
52
+ * Whether this page has EVER met the quiet definition since its last navigation.
53
+ *
54
+ * Read at teardown, where no settle signal exists to consult: the teardown sweep
55
+ * scans whatever is on screen, and until this flag there was no way to tell a
56
+ * page that had finished rendering from one caught mid-flight. Both produced a
57
+ * scan that looked identical, and the checks making whole-page ABSENCE claims —
58
+ * "nothing on this page says where you are" — were as confident on one as the
59
+ * other. Friction record run 4 watched 2.4.8 flip between two runs of unchanged
60
+ * code for exactly that reason.
61
+ *
62
+ * A flag rather than a measurement, because measuring at teardown means sampling
63
+ * mutations, waiting `quietMs`, and sampling again — half a second added to every
64
+ * page of every test, to learn something the detector already knew.
65
+ */
66
+ export const WENT_QUIET = '__setsquareWentQuiet';
32
67
  export const SETTLE_BINDING = '__setsquareSettle';
33
68
  export const DEFAULT_SETTLE = {
34
69
  /*
@@ -83,6 +118,9 @@ export function settleScript(config) {
83
118
  // Monotonic across the document lifetime, NOT reset per navigation: the
84
119
  // teardown sweep uses it to decide whether a page changed after being scanned.
85
120
  window.__setsquareMutationTotal = 0;
121
+ // Reset on every navigation below: "this page went quiet" must never be
122
+ // inherited by the next URL, which is the one way this flag could lie.
123
+ window.${WENT_QUIET} = false;
86
124
  let via = 'load';
87
125
 
88
126
  const report = (reason) => {
@@ -114,8 +152,10 @@ export function settleScript(config) {
114
152
  // readyState is checked HERE rather than as a precondition, so a page that
115
153
  // goes quiet while still loading keeps waiting instead of being scanned
116
154
  // half-built.
117
- if (document.readyState === 'complete') report('quiet');
118
- else scheduleQuiet();
155
+ if (document.readyState === 'complete') {
156
+ window.${WENT_QUIET} = true;
157
+ report('quiet');
158
+ } else scheduleQuiet();
119
159
  }, QUIET);
120
160
  };
121
161
 
@@ -124,6 +164,7 @@ export function settleScript(config) {
124
164
  fired = false;
125
165
  navigatedAt = Date.now();
126
166
  mutations = 0;
167
+ window.${WENT_QUIET} = false;
127
168
  clearTimeout(ceilingTimer);
128
169
  ceilingTimer = setTimeout(() => report('ceiling'), CEILING);
129
170
  scheduleQuiet();
@@ -1 +1 @@
1
- {"version":3,"file":"settle.js","sourceRoot":"","sources":["../src/settle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,wFAAwF;AACxF,MAAM,CAAC,MAAM,cAAc,GAAG,0BAA0B,CAAC;AAEzD,MAAM,CAAC,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAqBlD,MAAM,CAAC,MAAM,cAAc,GAAiB;IAC1C;;;;;;;OAOG;IACH,OAAO,EAAE,GAAG;IACZ;;;;;;;;OAQG;IACH,eAAe,EAAE,IAAI;CACtB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO;;;;;;;kBAOS,MAAM,CAAC,OAAO;oBACZ,MAAM,CAAC,eAAe;;;;;;;;;;;;;;;;;;;;;;0BAsBhB,cAAc;iBACvB,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6D9B,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"settle.js","sourceRoot":"","sources":["../src/settle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,wFAAwF;AACxF,MAAM,CAAC,MAAM,cAAc,GAAG,0BAA0B,CAAC;AAEzD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,sBAAsB,CAAC;AAEjD,MAAM,CAAC,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAqBlD,MAAM,CAAC,MAAM,cAAc,GAAiB;IAC1C;;;;;;;OAOG;IACH,OAAO,EAAE,GAAG;IACZ;;;;;;;;OAQG;IACH,eAAe,EAAE,IAAI;CACtB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,OAAO;;;;;;;kBAOS,MAAM,CAAC,OAAO;oBACZ,MAAM,CAAC,eAAe;;;;;;;;;;;;WAY/B,UAAU;;;;;;;;;;;;;0BAaK,cAAc;iBACvB,cAAc;;;;;;;;;;;;;;;;;;;iBAmBd,UAAU;;;;;;;;;;;aAWd,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCtB,CAAC;AACF,CAAC"}
package/dist/summary.d.ts CHANGED
@@ -157,6 +157,21 @@ export interface SurfaceRow {
157
157
  */
158
158
  incomplete: number;
159
159
  rules: string[];
160
+ /**
161
+ * The same coordinates WITHOUT the viewport.
162
+ *
163
+ * The HTML report groups these rows into a row group per band and prints the
164
+ * band's sizes on its heading, so repeating the size on all thirty-six rows
165
+ * underneath is noise the grouping was supposed to remove. It is only noise
166
+ * where the band holds ONE size, which the renderer knows and this does not —
167
+ * so both strings are carried and the renderer picks.
168
+ */
169
+ shortLabel: string;
170
+ /** The width this row was assessed at, when the run controlled one. */
171
+ viewport?: {
172
+ width: number;
173
+ height: number;
174
+ };
160
175
  }
161
176
  /**
162
177
  * One row per scanned context, worst first.
@@ -175,6 +190,19 @@ export interface EvidenceQuality {
175
190
  teardown: number;
176
191
  /** Scans the settle ceiling cut short. Weaker evidence, and named as such. */
177
192
  ceiling: number;
193
+ /**
194
+ * The longest a ceiling scan actually waited, and the most DOM mutations any
195
+ * of them counted.
196
+ *
197
+ * Both measured, neither configured. The note that reads these used to say
198
+ * "after waiting 5s", which is the DEFAULT — so a suite that raised
199
+ * `settleTimeoutMs` was told a number it had changed. And "a page that was
200
+ * still changing" without saying how much leaves the reader with one action
201
+ * ("raise the timeout") when there are two: a page 200 mutations from quiet
202
+ * will settle if given longer, and a page with a poller never will.
203
+ */
204
+ ceilingWaitedMs: number;
205
+ ceilingMutations: number;
178
206
  }
179
207
  /**
180
208
  * How much the findings are worth, as distinct from what they say.
@@ -193,6 +221,13 @@ export interface EvidenceQuality {
193
221
  export declare function evidenceQuality(envelope: RunIngestRequest): EvidenceQuality;
194
222
  export interface PerfRow {
195
223
  label: string;
224
+ /** The same coordinates without the viewport — see `SurfaceRow.shortLabel`. */
225
+ shortLabel: string;
226
+ /** The width this page load happened at, when the run controlled one. */
227
+ viewport?: {
228
+ width: number;
229
+ height: number;
230
+ };
196
231
  lcpMs?: number;
197
232
  cls?: number;
198
233
  loadMs?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../src/summary.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../src/summary.ts"],"names":[],"mappings":"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"}
package/dist/summary.js CHANGED
@@ -40,12 +40,43 @@ export function groupLines(group) {
40
40
  const IMPACT_ORDER = ['critical', 'serious', 'moderate', 'minor'];
41
41
  /** Selectors shown per rule entry in the terminal; the full set is in report.html. */
42
42
  const MAX_ENTRY_SELECTORS = 5;
43
- /** `job-editor · dark` — the coordinates a person can act on. */
44
- function contextLabel(context) {
45
- return [context.surface, context.theme, context.persona, context.state]
43
+ /**
44
+ * `job-editor · dark` — the coordinates a person can act on.
45
+ *
46
+ * The viewport joins them ONLY when the run holds more than one, and that
47
+ * condition is the whole design. Dedupe keys on the viewport unconditionally,
48
+ * because collapsing two widths into one row loses evidence; a label is read by
49
+ * a person, and `home · light · 1280×720` on every row of a suite that has one
50
+ * viewport is thirteen characters of noise repeated forty times. Where there
51
+ * genuinely are several, the label is the only thing telling two otherwise
52
+ * identical rows apart — and without it the surfaces table merges them by label
53
+ * and reports one surface's failures three times over.
54
+ */
55
+ function contextLabel(context, withViewport = false) {
56
+ const viewport = withViewport && context.viewport
57
+ ? `${context.viewport.width}\u00d7${context.viewport.height}`
58
+ : undefined;
59
+ return [context.surface, context.theme, context.persona, context.state, viewport]
46
60
  .filter(Boolean)
47
61
  .join(' · ');
48
62
  }
63
+ /**
64
+ * Whether this run looked at more than one viewport.
65
+ *
66
+ * Computed per envelope rather than carried on the scan, because it is a fact
67
+ * about the RUN: one scan cannot know whether it was the only width looked at.
68
+ */
69
+ function hasSeveralViewports(envelope) {
70
+ const seen = new Set();
71
+ for (const scan of envelope.scans) {
72
+ const viewport = scan.context.viewport;
73
+ if (viewport)
74
+ seen.add(`${viewport.width}x${viewport.height}`);
75
+ if (seen.size > 1)
76
+ return true;
77
+ }
78
+ return false;
79
+ }
49
80
  /**
50
81
  * Collapse findings to one row per (rule, status).
51
82
  *
@@ -55,11 +86,12 @@ function contextLabel(context) {
55
86
  */
56
87
  export function groupFindings(envelope) {
57
88
  const groups = new Map();
89
+ const withViewport = hasSeveralViewports(envelope);
58
90
  for (const scan of envelope.scans) {
59
91
  for (const finding of scan.findings) {
60
92
  const key = `${finding.ruleId}\u0000${finding.status}`;
61
93
  const existing = groups.get(key);
62
- const label = contextLabel(scan.context);
94
+ const label = contextLabel(scan.context, withViewport);
63
95
  if (existing) {
64
96
  existing.nodes += finding.targets.nodeCount;
65
97
  if (!existing.contexts.includes(label))
@@ -144,11 +176,25 @@ export function surfaceRows(envelope) {
144
176
  * the tool repeating itself.
145
177
  */
146
178
  const merged = new Map();
179
+ const withViewport = hasSeveralViewports(envelope);
147
180
  for (const scan of envelope.scans) {
148
- const label = contextLabel(scan.context);
181
+ const label = contextLabel(scan.context, withViewport);
149
182
  const failing = scan.findings.filter((finding) => finding.status === 'violation');
150
183
  const unresolved = scan.findings.filter((finding) => finding.status === 'incomplete');
151
- const row = merged.get(label) ?? { label, violations: 0, incomplete: 0, rules: [] };
184
+ const row = merged.get(label) ?? {
185
+ label,
186
+ shortLabel: contextLabel(scan.context),
187
+ violations: 0,
188
+ incomplete: 0,
189
+ rules: [],
190
+ /*
191
+ * Carried as data as well as inside the label, because the report GROUPS
192
+ * on it. The label is for a person to read and a string cannot be grouped
193
+ * on without parsing it back, which is the kind of thing that works until
194
+ * a surface name contains a multiplication sign.
195
+ */
196
+ ...(scan.context.viewport ? { viewport: scan.context.viewport } : {}),
197
+ };
152
198
  row.violations += failing.reduce((sum, finding) => sum + finding.targets.nodeCount, 0);
153
199
  row.incomplete += unresolved.reduce((sum, finding) => sum + finding.targets.nodeCount, 0);
154
200
  row.rules = [...new Set([...row.rules, ...failing.map((finding) => finding.ruleId)])].sort();
@@ -184,6 +230,8 @@ export function evidenceQuality(envelope) {
184
230
  let quiet = 0;
185
231
  let teardown = 0;
186
232
  let ceiling = 0;
233
+ let ceilingWaitedMs = 0;
234
+ let ceilingMutations = 0;
187
235
  for (const scan of envelope.scans) {
188
236
  for (const pass of scan.passes ?? [])
189
237
  rules.add(pass.ruleId);
@@ -200,12 +248,17 @@ export function evidenceQuality(envelope) {
200
248
  break;
201
249
  case 'ceiling':
202
250
  ceiling += 1;
251
+ // The worst case rather than an average: one page pinned at the ceiling
252
+ // is the thing worth knowing about, and a mean over a healthy run would
253
+ // hide it.
254
+ ceilingWaitedMs = Math.max(ceilingWaitedMs, scan.settle.sinceNavMs);
255
+ ceilingMutations = Math.max(ceilingMutations, scan.settle.mutations);
203
256
  break;
204
257
  default:
205
258
  break;
206
259
  }
207
260
  }
208
- return { rulesAssessed: rules.size, quiet, teardown, ceiling };
261
+ return { rulesAssessed: rules.size, quiet, teardown, ceiling, ceilingWaitedMs, ceilingMutations };
209
262
  }
210
263
  /*
211
264
  * Google's "good" thresholds for the two Core Web Vitals we can measure in a lab.
@@ -231,14 +284,17 @@ const GOOD_CLS = 0.1;
231
284
  */
232
285
  export function perfRows(envelope) {
233
286
  const byVisit = new Map();
287
+ const withViewport = hasSeveralViewports(envelope);
234
288
  for (const scan of envelope.scans) {
235
289
  if (!scan.perf)
236
290
  continue;
237
- const key = scan.visitRef ?? `${scan.testId}:${contextLabel(scan.context)}`;
291
+ const key = scan.visitRef ?? `${scan.testId}:${contextLabel(scan.context, withViewport)}`;
238
292
  if (byVisit.has(key))
239
293
  continue;
240
294
  byVisit.set(key, {
241
- label: contextLabel(scan.context),
295
+ label: contextLabel(scan.context, withViewport),
296
+ shortLabel: contextLabel(scan.context),
297
+ ...(scan.context.viewport ? { viewport: scan.context.viewport } : {}),
242
298
  ...(scan.perf.lcpMs !== undefined ? { lcpMs: scan.perf.lcpMs } : {}),
243
299
  ...(scan.perf.cls !== undefined ? { cls: scan.perf.cls } : {}),
244
300
  ...(scan.perf.loadMs !== undefined ? { loadMs: scan.perf.loadMs } : {}),
@@ -484,8 +540,27 @@ export function renderText(input, columns) {
484
540
  */
485
541
  lines.push(heading(style, 'Accessibility'));
486
542
  lines.push(figureLine(style, 0, 'surfaces scanned'));
487
- lines.push(...continuationLines(style, 'This is not a clean result — it means nothing was looked at. Usually the ' +
488
- 'suite never opened a page, or every test finished before its page settled.', width));
543
+ /*
544
+ * Name the cause when the run knows it, and only then.
545
+ *
546
+ * "Usually the suite never opened a page, or every test finished before its
547
+ * page settled" is a guess offered in place of a fact the envelope is
548
+ * holding. On a lane run with `SETSQUARE_AUTOSCAN=0` both halves of that
549
+ * sentence are false and both describe a broken suite, which is how a
550
+ * perfectly healthy functional lane came to read as a failure.
551
+ *
552
+ * "This is not a clean result" stays either way. Nothing was looked at, and
553
+ * that is true whether or not the reason is reassuring.
554
+ */
555
+ const offRows = (envelope.unscanned ?? []).filter((row) => row.reason === 'autoscan-disabled');
556
+ const allOff = offRows.length > 0 && offRows.length === (envelope.unscanned ?? []).length;
557
+ lines.push(...continuationLines(style, allOff
558
+ ? 'This is not a clean result — it means nothing was looked at. Automatic ' +
559
+ 'scanning was off for this run (SETSQUARE_AUTOSCAN=0), so the surfaces ' +
560
+ 'below were visited and never scanned. That is the configuration working ' +
561
+ 'as asked; it is not a fault in the suite.'
562
+ : 'This is not a clean result — it means nothing was looked at. Usually the ' +
563
+ 'suite never opened a page, or every test finished before its page settled.', width));
489
564
  lines.push(...trailer(false));
490
565
  return join(lines);
491
566
  }
@@ -829,6 +904,17 @@ function conformanceLines(input, style, width) {
829
904
  const criterionRow = (rollup) => [
830
905
  figureLine(style, rollup.failedSurfaces.length, `${style.bold(`${rollup.criterion} ${rollup.name}`)} ${style.dim(`FAIL on ${surfaceShare(rollup.failedSurfaces.length, rollup.totalSurfaces)}`)}`),
831
906
  ...continuationLines(style, rollup.failingRules.join(', '), width),
907
+ /*
908
+ * A cross-surface criterion has no failing RULE to name — the finding is
909
+ * the comparison itself — so the row would otherwise be a bare percentage.
910
+ * Both disagreeing surfaces are named, because "the navigation is
911
+ * inconsistent" without saying between which two pages sends the reader to
912
+ * look at everything.
913
+ */
914
+ ...(rollup.consistency?.disagreements ?? [])
915
+ .slice(0, 2)
916
+ .flatMap((row) => continuationLines(style, row.detail, width)),
917
+ ...(rollup.consistency?.note ? continuationLines(style, rollup.consistency.note, width) : []),
832
918
  ];
833
919
  for (const level of summary.levels) {
834
920
  if (level.level === 'AAA' && !level.assessed) {
@@ -849,9 +935,28 @@ function conformanceLines(input, style, width) {
849
935
  for (const rollup of level.failing)
850
936
  lines.push(...criterionRow(rollup));
851
937
  }
938
+ else if (level.assertedFailing.length > 0) {
939
+ // Not "no failures detected": a level whose own declared test is red is
940
+ // neither clean nor failed, and saying "no failures detected" over the
941
+ // top of it would be the dishonest direction.
942
+ lines.push(figureLine(style, level.assertedFailing.length, `Level ${level.level.padEnd(3)} no failures detected, but ` +
943
+ `${level.assertedFailing.length === 1 ? 'a criterion your suite covers' : `${level.assertedFailing.length} criteria your suite covers`} did not pass`));
944
+ }
852
945
  else {
853
946
  lines.push(figureLine(style, '', `Level ${level.level.padEnd(3)} no failures detected in what was scanned`));
854
947
  }
948
+ /*
949
+ * Listed whether or not the level also has detected failures. These are
950
+ * the customer's own assertions, so the row says whose claim it is and
951
+ * names the test — the only useful next action is opening that file.
952
+ */
953
+ for (const rollup of level.assertedFailing) {
954
+ lines.push(...continuationLines(style, `${rollup.criterion} ${rollup.name} — your own test for it did not pass` +
955
+ (rollup.assertions && rollup.assertions.notUpheld.length > 0
956
+ ? `: ${rollup.assertions.notUpheld.join(', ')}`
957
+ : '') +
958
+ '. Setsquare did not detect this; your suite declared it.', width));
959
+ }
855
960
  if (level.assessedScans) {
856
961
  lines.push(...continuationLines(style, `AAA was assessed on ${level.assessedScans.aaa} of ${level.assessedScans.total} scans — ` +
857
962
  'the rest looked with AA rules only.', width));
@@ -887,6 +992,15 @@ function conformanceLines(input, style, width) {
887
992
  sentence += ` Then for AAA: ${ids('AAA')}.`;
888
993
  lines.push(...continuationLines(style, sentence, width));
889
994
  }
995
+ else if (verdict.assertionsFailingAt) {
996
+ // The third outcome. Neither "we found failures" — we found none — nor
997
+ // "no failures detected", which would be publishing a clean line over the
998
+ // top of the suite's own red check.
999
+ lines.push(...continuationLines(style, `No Level ${verdict.assertionsFailingAt} violations detected, but ` +
1000
+ `${verdict.failingAssertions.length === 1 ? 'a criterion your suite declares it covers' : `${verdict.failingAssertions.length} criteria your suite declares it covers`} ` +
1001
+ `did not pass: ${verdict.failingAssertions.map((r) => r.criterion).join(', ')}. ` +
1002
+ 'That is your assertion, not our finding — but nothing here says the level is clean.', width));
1003
+ }
890
1004
  else if (verdict.noFailuresDetectedAt) {
891
1005
  lines.push(...continuationLines(style, `No Level ${verdict.noFailuresDetectedAt} violations detected across ` +
892
1006
  `${n(summary.surfacesScanned)} surface${summary.surfacesScanned === 1 ? '' : 's'} ` +
@@ -902,6 +1016,7 @@ function conformanceLines(input, style, width) {
902
1016
  const rulesPassed = counted.reduce((sum, level) => sum + level.rulesPassed.length, 0);
903
1017
  const cantTell = counted.reduce((sum, level) => sum + level.cantTell.length, 0);
904
1018
  const inapplicable = counted.reduce((sum, level) => sum + level.inapplicable.length, 0);
1019
+ const assertedBySuite = counted.flatMap((level) => level.assertedBySuite);
905
1020
  const untested = counted.flatMap((level) => level.untested);
906
1021
  if (rulesPassed > 0) {
907
1022
  lines.push(figureLine(style, rulesPassed, 'criteria: every automated rule passed — evidence, not conformance'));
@@ -914,17 +1029,69 @@ function conformanceLines(input, style, width) {
914
1029
  // nothing these criteria regulate — coverage, not a gap.
915
1030
  lines.push(figureLine(style, inapplicable, 'criteria: checks ran, nothing to check on these pages (no such content)'));
916
1031
  }
1032
+ if (assertedBySuite.length > 0) {
1033
+ /*
1034
+ * Deliberately worded as the customer's claim and placed AFTER the
1035
+ * rule-based figures, because it is the weakest positive line on the
1036
+ * summary. It is also the only line here whose strength we cannot assess:
1037
+ * it is exactly as good as the tests behind it.
1038
+ */
1039
+ lines.push(figureLine(style, assertedBySuite.length, 'criteria covered by your own tests — your assertion, no rule ran'));
1040
+ }
917
1041
  if (untested.length > 0) {
918
- if (input.criterionRules) {
919
- const noCheck = untested.filter((r) => r.untestedReason === 'notAutomatable').length;
920
- const notRun = untested.filter((r) => r.untestedReason === 'notRun').length;
921
- if (noCheck > 0)
922
- lines.push(figureLine(style, noCheck, 'criteria have no automated check in this engine'));
923
- if (notRun > 0)
924
- lines.push(figureLine(style, notRun, 'criteria have checks that did not run here'));
1042
+ const noRule = untested.filter((r) => r.untestedReason === 'noRuleCanDecide');
1043
+ if (noRule.length > 0) {
1044
+ /*
1045
+ * The most useful line on this block, and the one the product's central
1046
+ * claim rests on. "No automated rule can decide these, whatever anyone
1047
+ * builds" is a concrete floor under "automated evidence is not
1048
+ * conformance" and a customer who sees it understands what they still
1049
+ * owe an auditor, which is a better thing to have told them than a tick.
1050
+ */
1051
+ lines.push(figureLine(style, noRule.length, 'criteria no automated rule can decide — what you still owe an auditor'));
925
1052
  }
926
- else {
927
- lines.push(figureLine(style, untested.length, 'criteria not automatically checked in this run'));
1053
+ const compared = untested.filter((r) => r.untestedReason === 'singleSurface');
1054
+ if (compared.length > 0) {
1055
+ /*
1056
+ * Its own line, and never folded into "no automated check". These
1057
+ * criteria HAVE a check; fewer than two pages carried the thing it
1058
+ * compares, so there was nothing to compare. Reporting them as
1059
+ * uncheckable would be wrong in the direction that sells the product
1060
+ * short — one more page with the evidence on it is all they need.
1061
+ *
1062
+ * THE WORDING USED TO SAY "this run had one surface", AND THAT WAS A
1063
+ * CLAIM, not a description. It printed unchanged on an eighteen-surface
1064
+ * run of this repository's own site, two lines under "18 surfaces
1065
+ * scanned" — because the reason is fewer than two surfaces carrying the
1066
+ * EVIDENCE, not fewer than two surfaces. 3.2.6 needs at least two pages
1067
+ * that actually offer a help mechanism; a hundred pages without one
1068
+ * still give it nothing to compare. The schema's own note on
1069
+ * `singleSurface` had it right all along.
1070
+ */
1071
+ lines.push(figureLine(style, compared.length, 'criteria compare pages against each other — fewer than two pages carried the evidence'));
1072
+ }
1073
+ const skipped = untested.filter((r) => r.untestedReason === 'declarationSkipped').length;
1074
+ if (skipped > 0) {
1075
+ // Not folded into "no automated check": the criterion had a check, it
1076
+ // was the suite's own, and it did not run. That is a suite problem, and
1077
+ // reporting it as a tooling limitation would hide it.
1078
+ lines.push(figureLine(style, skipped, 'criteria your suite covers were skipped and did not run'));
1079
+ }
1080
+ const rest = untested.filter((r) => r.untestedReason !== 'declarationSkipped' &&
1081
+ r.untestedReason !== 'singleSurface' &&
1082
+ r.untestedReason !== 'noRuleCanDecide');
1083
+ if (rest.length > 0) {
1084
+ if (input.criterionRules) {
1085
+ const noCheck = rest.filter((r) => r.untestedReason === 'notAutomatable').length;
1086
+ const notRun = rest.filter((r) => r.untestedReason === 'notRun').length;
1087
+ if (noCheck > 0)
1088
+ lines.push(figureLine(style, noCheck, 'criteria have no automated check in this engine'));
1089
+ if (notRun > 0)
1090
+ lines.push(figureLine(style, notRun, 'criteria have checks that did not run here'));
1091
+ }
1092
+ else {
1093
+ lines.push(figureLine(style, rest.length, 'criteria not automatically checked in this run'));
1094
+ }
928
1095
  }
929
1096
  }
930
1097
  const anyFailure = summary.levels.some((level) => level.failing.length > 0);
@@ -966,9 +1133,31 @@ function gridLines(input, style, width) {
966
1133
  // ASCII glyphs whenever styling is off: the same environments that lose
967
1134
  // colour (CI, pipes, NO_COLOR) are the ones where glyph rendering is least
968
1135
  // trustworthy, and the fallback must be decided by something a test can set.
1136
+ //
1137
+ // The two declared states get their own glyphs and never borrow ✓ or ✗.
1138
+ // A cell that means "your test says so" must not look like a cell that
1139
+ // means "a rule ran" — that flattening is the exact thing the state was
1140
+ // added to prevent, and a shared glyph would reintroduce it in the one
1141
+ // rendering most people actually read.
969
1142
  const glyph = style.on
970
- ? { failed: '✗', cantTell: '?', rulesPassed: '✓', inapplicable: '–', untested: '·' }
971
- : { failed: 'F', cantTell: '?', rulesPassed: 'P', inapplicable: '-', untested: '.' };
1143
+ ? {
1144
+ failed: '',
1145
+ assertedFailing: '⊗',
1146
+ cantTell: '?',
1147
+ rulesPassed: '✓',
1148
+ inapplicable: '–',
1149
+ assertedBySuite: '⊙',
1150
+ untested: '·',
1151
+ }
1152
+ : {
1153
+ failed: 'F',
1154
+ assertedFailing: 'f',
1155
+ cantTell: '?',
1156
+ rulesPassed: 'P',
1157
+ inapplicable: '-',
1158
+ assertedBySuite: 'y',
1159
+ untested: '.',
1160
+ };
972
1161
  const indent = ' ';
973
1162
  const idWidth = Math.max(...grid.rows.map((row) => row.criterion.length));
974
1163
  const nameWidth = Math.min(Math.max(...grid.rows.map((row) => row.name.length)), 28);
@@ -998,7 +1187,10 @@ function gridLines(input, style, width) {
998
1187
  lines.push('');
999
1188
  lines.push(...continuationLines(style, `${glyph.failed} failed ${glyph.cantTell} needs a person ` +
1000
1189
  `${glyph.rulesPassed} every automated rule passed ` +
1001
- `${glyph.inapplicable} nothing to check here ${glyph.untested} untested`, width));
1190
+ `${glyph.inapplicable} nothing to check here ` +
1191
+ `${glyph.assertedBySuite} covered by your own test ` +
1192
+ `${glyph.assertedFailing} your own test for it did not pass ` +
1193
+ `${glyph.untested} untested`, width));
1002
1194
  lines.push(...continuationLines(style, grid.surfaces.map((surface, i) => `${i + 1} ${surface}`).join(' '), width));
1003
1195
  return lines;
1004
1196
  }
@@ -1032,9 +1224,28 @@ function warningLines(warnings, width, quality, envelope) {
1032
1224
  * dying in the envelope.
1033
1225
  */
1034
1226
  if (quality && quality.ceiling > 0) {
1035
- notes.push(`${quality.ceiling} scan${quality.ceiling === 1 ? ' was' : 's were'} cut short after ` +
1036
- 'waiting 5s for the page to settle, so they describe a page that was still ' +
1037
- 'changing. Treat a clean result from those as weaker evidence.');
1227
+ /*
1228
+ * Say WHOSE window it was, and WHAT the page was doing in it.
1229
+ *
1230
+ * The old sentence said "after waiting 5s", which is the default — a suite
1231
+ * that raised `settleTimeoutMs` was quoted a number it had changed. And it
1232
+ * described "a page that was still changing" without the one figure that
1233
+ * tells a reader which of the two available actions is theirs: a page a few
1234
+ * mutations from quiet settles if given longer, and a page with a poller
1235
+ * never will, and no timeout helps the second.
1236
+ *
1237
+ * Both numbers are measured off the scans themselves rather than read from
1238
+ * configuration, which also means they stay true if the window is changed
1239
+ * per project.
1240
+ */
1241
+ const waited = `${(quality.ceilingWaitedMs / 1000).toFixed(1)}s`;
1242
+ notes.push(`${quality.ceiling} scan${quality.ceiling === 1 ? ' was' : 's were'} cut short by our ` +
1243
+ `settle window — the longest waited ${waited} and the page was still changing, with ` +
1244
+ `${quality.ceilingMutations.toLocaleString('en-GB')} DOM mutation` +
1245
+ `${quality.ceilingMutations === 1 ? '' : 's'} counted since it loaded. Treat a clean ` +
1246
+ 'result from those as weaker evidence. If the page settles and simply needs longer, ' +
1247
+ 'raise `settleTimeoutMs`; if something polls or animates forever, no window will help ' +
1248
+ 'and the scan is of a moving page by nature.');
1038
1249
  }
1039
1250
  if (warnings.length === 0 && notes.length === 0)
1040
1251
  return [];
@@ -1105,16 +1316,25 @@ function markdownConformance(input) {
1105
1316
  }
1106
1317
  lines.push('');
1107
1318
  const withEvidence = summary.levels.flatMap((level) => level.level === 'AAA' && !level.assessed
1108
- ? level.failing
1109
- : [...level.failing, ...level.cantTell, ...level.rulesPassed, ...level.inapplicable]);
1319
+ ? [...level.failing, ...level.assertedFailing]
1320
+ : [
1321
+ ...level.failing,
1322
+ ...level.assertedFailing,
1323
+ ...level.cantTell,
1324
+ ...level.rulesPassed,
1325
+ ...level.inapplicable,
1326
+ ...level.assertedBySuite,
1327
+ ]);
1110
1328
  if (withEvidence.length > 0) {
1111
1329
  lines.push('| Criterion | Level | State | Surfaces |');
1112
1330
  lines.push('|---|---|---|---|');
1113
1331
  const stateWords = {
1114
1332
  failed: 'FAIL',
1333
+ assertedFailing: 'your test for it did not pass',
1115
1334
  cantTell: 'needs a person',
1116
1335
  rulesPassed: 'rules passed',
1117
1336
  inapplicable: 'nothing to check',
1337
+ assertedBySuite: 'covered by your own test',
1118
1338
  untested: 'untested',
1119
1339
  };
1120
1340
  for (const rollup of [...withEvidence].sort((a, b) => a.criterion.localeCompare(b.criterion, 'en', { numeric: true }))) {