@setsquare/review-sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -16
- package/dist/autoscan.d.ts +9 -1
- package/dist/autoscan.d.ts.map +1 -1
- package/dist/autoscan.js +57 -7
- package/dist/autoscan.js.map +1 -1
- package/dist/axe.d.ts.map +1 -1
- package/dist/axe.js +25 -3
- package/dist/axe.js.map +1 -1
- package/dist/checks/context-change.d.ts.map +1 -1
- package/dist/checks/context-change.js +3 -0
- package/dist/checks/context-change.js.map +1 -1
- package/dist/checks/dynamic.d.ts.map +1 -1
- package/dist/checks/dynamic.js +3 -0
- package/dist/checks/dynamic.js.map +1 -1
- package/dist/checks/focus.d.ts +25 -5
- package/dist/checks/focus.d.ts.map +1 -1
- package/dist/checks/focus.js +203 -15
- package/dist/checks/focus.js.map +1 -1
- package/dist/checks/heuristics.d.ts +6 -5
- package/dist/checks/heuristics.d.ts.map +1 -1
- package/dist/checks/heuristics.js +37 -1
- package/dist/checks/heuristics.js.map +1 -1
- package/dist/checks/keyboard.js +12 -0
- package/dist/checks/keyboard.js.map +1 -1
- package/dist/checks/reflow.d.ts.map +1 -1
- package/dist/checks/reflow.js +4 -0
- package/dist/checks/reflow.js.map +1 -1
- package/dist/checks/shortcuts.d.ts.map +1 -1
- package/dist/checks/shortcuts.js +3 -0
- package/dist/checks/shortcuts.js.map +1 -1
- package/dist/colour-shim.d.ts +77 -0
- package/dist/colour-shim.d.ts.map +1 -0
- package/dist/colour-shim.js +192 -0
- package/dist/colour-shim.js.map +1 -0
- package/dist/contract.d.ts +14 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +78 -16
- package/dist/envelope.js.map +1 -1
- package/dist/findings.d.ts.map +1 -1
- package/dist/findings.js +57 -0
- package/dist/findings.js.map +1 -1
- package/dist/fixture.d.ts.map +1 -1
- package/dist/fixture.js +21 -4
- package/dist/fixture.js.map +1 -1
- package/dist/html-report.d.ts.map +1 -1
- package/dist/html-report.js +54 -12
- package/dist/html-report.js.map +1 -1
- package/dist/insights.d.ts.map +1 -1
- package/dist/insights.js +9 -12
- package/dist/insights.js.map +1 -1
- package/dist/options.d.ts +20 -9
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +23 -1
- package/dist/options.js.map +1 -1
- package/dist/reporter.js +2 -2
- package/dist/reporter.js.map +1 -1
- package/dist/screens.d.ts.map +1 -1
- package/dist/screens.js +34 -1
- package/dist/screens.js.map +1 -1
- package/dist/settle.d.ts +35 -0
- package/dist/settle.d.ts.map +1 -1
- package/dist/settle.js +43 -2
- package/dist/settle.js.map +1 -1
- package/dist/summary.d.ts +33 -1
- package/dist/summary.d.ts.map +1 -1
- package/dist/summary.js +271 -45
- package/dist/summary.js.map +1 -1
- package/dist/unscannable-reasons.d.ts +30 -0
- package/dist/unscannable-reasons.d.ts.map +1 -0
- package/dist/unscannable-reasons.js +53 -0
- package/dist/unscannable-reasons.js.map +1 -0
- package/package.json +4 -4
package/dist/summary.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { conformanceGrid, conformanceSummary, ruleSurfaceShare, stateCoverage, } from '@setsquare/schema/conformance';
|
|
2
|
-
import {
|
|
2
|
+
import { ruleHelpUrl } from './links.js';
|
|
3
3
|
import { continuationLines, createStyle, figureBlock, figureLine, heading, } from './style.js';
|
|
4
4
|
import { flakeNotes, insightBlocks } from './insights.js';
|
|
5
5
|
/**
|
|
@@ -120,6 +120,7 @@ export function groupFindings(envelope) {
|
|
|
120
120
|
ruleId: finding.ruleId,
|
|
121
121
|
impact: finding.impact ?? 'unknown',
|
|
122
122
|
status: finding.status,
|
|
123
|
+
...(finding.undecidable ? { undecidable: finding.undecidable } : {}),
|
|
123
124
|
nodes: finding.targets.nodeCount,
|
|
124
125
|
contexts: [label],
|
|
125
126
|
contextNodes: [{ label, nodes: finding.targets.nodeCount }],
|
|
@@ -230,6 +231,8 @@ export function evidenceQuality(envelope) {
|
|
|
230
231
|
let quiet = 0;
|
|
231
232
|
let teardown = 0;
|
|
232
233
|
let ceiling = 0;
|
|
234
|
+
let ceilingWaitedMs = 0;
|
|
235
|
+
let ceilingMutations = 0;
|
|
233
236
|
for (const scan of envelope.scans) {
|
|
234
237
|
for (const pass of scan.passes ?? [])
|
|
235
238
|
rules.add(pass.ruleId);
|
|
@@ -246,12 +249,17 @@ export function evidenceQuality(envelope) {
|
|
|
246
249
|
break;
|
|
247
250
|
case 'ceiling':
|
|
248
251
|
ceiling += 1;
|
|
252
|
+
// The worst case rather than an average: one page pinned at the ceiling
|
|
253
|
+
// is the thing worth knowing about, and a mean over a healthy run would
|
|
254
|
+
// hide it.
|
|
255
|
+
ceilingWaitedMs = Math.max(ceilingWaitedMs, scan.settle.sinceNavMs);
|
|
256
|
+
ceilingMutations = Math.max(ceilingMutations, scan.settle.mutations);
|
|
249
257
|
break;
|
|
250
258
|
default:
|
|
251
259
|
break;
|
|
252
260
|
}
|
|
253
261
|
}
|
|
254
|
-
return { rulesAssessed: rules.size, quiet, teardown, ceiling };
|
|
262
|
+
return { rulesAssessed: rules.size, quiet, teardown, ceiling, ceilingWaitedMs, ceilingMutations };
|
|
255
263
|
}
|
|
256
264
|
/*
|
|
257
265
|
* Google's "good" thresholds for the two Core Web Vitals we can measure in a lab.
|
|
@@ -366,6 +374,8 @@ export function totals(envelope) {
|
|
|
366
374
|
const themes = new Set();
|
|
367
375
|
let violations = 0;
|
|
368
376
|
let incomplete = 0;
|
|
377
|
+
let judgement = 0;
|
|
378
|
+
let unmeasured = 0;
|
|
369
379
|
let passes = 0;
|
|
370
380
|
let clean = 0;
|
|
371
381
|
for (const scan of envelope.scans) {
|
|
@@ -374,9 +384,17 @@ export function totals(envelope) {
|
|
|
374
384
|
themes.add(scan.context.theme);
|
|
375
385
|
const scanViolations = scan.findings.filter((f) => f.status === 'violation');
|
|
376
386
|
violations += scanViolations.reduce((sum, f) => sum + f.targets.nodeCount, 0);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
387
|
+
for (const f of scan.findings) {
|
|
388
|
+
if (f.status !== 'incomplete')
|
|
389
|
+
continue;
|
|
390
|
+
incomplete += f.targets.nodeCount;
|
|
391
|
+
// Unclassified rows count as a judgement: absence is not "measurement",
|
|
392
|
+
// and inflating our own debt figure would make it useless as a measure.
|
|
393
|
+
if (f.undecidable === 'measurement')
|
|
394
|
+
unmeasured += f.targets.nodeCount;
|
|
395
|
+
else
|
|
396
|
+
judgement += f.targets.nodeCount;
|
|
397
|
+
}
|
|
380
398
|
passes += scan.passes.reduce((sum, p) => sum + p.nodeCount, 0);
|
|
381
399
|
if (scanViolations.length === 0)
|
|
382
400
|
clean += 1;
|
|
@@ -387,6 +405,8 @@ export function totals(envelope) {
|
|
|
387
405
|
scans: envelope.scans.length,
|
|
388
406
|
violations,
|
|
389
407
|
incomplete,
|
|
408
|
+
judgement,
|
|
409
|
+
unmeasured,
|
|
390
410
|
passes,
|
|
391
411
|
clean,
|
|
392
412
|
};
|
|
@@ -448,14 +468,31 @@ export function renderText(input, columns) {
|
|
|
448
468
|
* strip before a word is read.
|
|
449
469
|
*/
|
|
450
470
|
/**
|
|
451
|
-
* Where the run's own artefacts are, and
|
|
471
|
+
* Where the run's own artefacts are, and the closing block.
|
|
472
|
+
*
|
|
473
|
+
* There used to be a five-line pitch here, on by default, switchable off with
|
|
474
|
+
* `promo: false`. It is gone, and the reasoning is worth keeping because it
|
|
475
|
+
* will be proposed again.
|
|
476
|
+
*
|
|
477
|
+
* The design answered two questions at once and could not be right about both.
|
|
478
|
+
* It read as an advert — a product name, a benefit list, a URL — but every
|
|
479
|
+
* line of it was TRUE ABOUT THEIR RUN, and it was switchable, which only makes
|
|
480
|
+
* sense for an advert. A disclosure is not optional; you cannot turn off a
|
|
481
|
+
* limitation.
|
|
452
482
|
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
483
|
+
* This repository's own rule about `doctor`'s exit code settled it: *we do not
|
|
484
|
+
* fail a customer's build, and the promise is weaker if it has an opt-out*.
|
|
485
|
+
* "We do not advertise in your build output" is a stronger promise than "we
|
|
486
|
+
* advertise unless you ask us not to" — and the flag was a fiction anyway,
|
|
487
|
+
* governing one of three places the pitch appeared while `verify` and the
|
|
488
|
+
* SARIF caveat carried it regardless, the second of those into the customer's
|
|
489
|
+
* own code-scanning UI.
|
|
490
|
+
*
|
|
491
|
+
* What survives is the half that was a fact about their run, as a caveat
|
|
492
|
+
* rather than a pitch: no product name, no URL, and not switchable, because
|
|
493
|
+
* "this run only" is a real limit on what the reader is holding.
|
|
457
494
|
*/
|
|
458
|
-
const trailer = (
|
|
495
|
+
const trailer = (scanned = true) => [
|
|
459
496
|
...(input.warnings.length > 0 ? [''] : []),
|
|
460
497
|
...warningLines(input.warnings, width, evidenceQuality(envelope), envelope),
|
|
461
498
|
'',
|
|
@@ -465,7 +502,21 @@ export function renderText(input, columns) {
|
|
|
465
502
|
? [figureLine(style, '', `${input.htmlReportPath} ${style.dim('(open in a browser)')}`)]
|
|
466
503
|
: []),
|
|
467
504
|
...(input.screensPath
|
|
468
|
-
? [
|
|
505
|
+
? [
|
|
506
|
+
figureLine(style, '', `${input.screensPath} ${style.dim('(screenshots, one page)')}`),
|
|
507
|
+
/*
|
|
508
|
+
* Said where the path is, because that is where someone decides
|
|
509
|
+
* whether to keep it — and measured rather than assumed: Playwright
|
|
510
|
+
* clears its whole output directory at the start of a run, including
|
|
511
|
+
* captures belonging to tests that run does not execute.
|
|
512
|
+
*
|
|
513
|
+
* The sheet indexes those images rather than copying them, which is a
|
|
514
|
+
* privacy guarantee and not tidiness: Playwright's own captures are
|
|
515
|
+
* unmasked, and this package writes no images at all.
|
|
516
|
+
*/
|
|
517
|
+
...continuationLines(style, "That page links to Playwright's images rather than copying them, and Playwright " +
|
|
518
|
+
'clears its output directory on its next run — so copy it now if this run matters.', width),
|
|
519
|
+
]
|
|
469
520
|
: []),
|
|
470
521
|
...(input.reportPath
|
|
471
522
|
? [figureLine(style, '', `${input.reportPath} ${style.dim('(Playwright report)')}`)]
|
|
@@ -477,7 +528,13 @@ export function renderText(input, columns) {
|
|
|
477
528
|
...groupLines(input.group).flatMap((line) => wrap(line, Math.max(40, width - 8), '').map((wrapped) => figureLine(style, '', wrapped))),
|
|
478
529
|
]
|
|
479
530
|
: []),
|
|
480
|
-
|
|
531
|
+
/*
|
|
532
|
+
* A run that scanned nothing gets no caveat, for the reason the pitch used
|
|
533
|
+
* to be suppressed there: the figures are all zero, and a line about what
|
|
534
|
+
* this run does not cover reads as noise bolted onto a failure. The block
|
|
535
|
+
* above already says nothing was looked at, which is the stronger statement.
|
|
536
|
+
*/
|
|
537
|
+
...(scanned ? historyCaveat(input, style, width) : ['']),
|
|
481
538
|
];
|
|
482
539
|
lines.push('');
|
|
483
540
|
lines.push(` ${style.accent('Setsquare')} — accessibility evidence`);
|
|
@@ -533,8 +590,27 @@ export function renderText(input, columns) {
|
|
|
533
590
|
*/
|
|
534
591
|
lines.push(heading(style, 'Accessibility'));
|
|
535
592
|
lines.push(figureLine(style, 0, 'surfaces scanned'));
|
|
536
|
-
|
|
537
|
-
|
|
593
|
+
/*
|
|
594
|
+
* Name the cause when the run knows it, and only then.
|
|
595
|
+
*
|
|
596
|
+
* "Usually the suite never opened a page, or every test finished before its
|
|
597
|
+
* page settled" is a guess offered in place of a fact the envelope is
|
|
598
|
+
* holding. On a lane run with `SETSQUARE_AUTOSCAN=0` both halves of that
|
|
599
|
+
* sentence are false and both describe a broken suite, which is how a
|
|
600
|
+
* perfectly healthy functional lane came to read as a failure.
|
|
601
|
+
*
|
|
602
|
+
* "This is not a clean result" stays either way. Nothing was looked at, and
|
|
603
|
+
* that is true whether or not the reason is reassuring.
|
|
604
|
+
*/
|
|
605
|
+
const offRows = (envelope.unscanned ?? []).filter((row) => row.reason === 'autoscan-disabled');
|
|
606
|
+
const allOff = offRows.length > 0 && offRows.length === (envelope.unscanned ?? []).length;
|
|
607
|
+
lines.push(...continuationLines(style, allOff
|
|
608
|
+
? 'This is not a clean result — it means nothing was looked at. Automatic ' +
|
|
609
|
+
'scanning was off for this run (SETSQUARE_AUTOSCAN=0), so the surfaces ' +
|
|
610
|
+
'below were visited and never scanned. That is the configuration working ' +
|
|
611
|
+
'as asked; it is not a fault in the suite.'
|
|
612
|
+
: 'This is not a clean result — it means nothing was looked at. Usually the ' +
|
|
613
|
+
'suite never opened a page, or every test finished before its page settled.', width));
|
|
538
614
|
lines.push(...trailer(false));
|
|
539
615
|
return join(lines);
|
|
540
616
|
}
|
|
@@ -581,10 +657,54 @@ export function renderText(input, columns) {
|
|
|
581
657
|
lines.push(figureLine(style, failingRules, `rule${failingRules === 1 ? '' : 's'} failing, on ${n(count.violations)} ` +
|
|
582
658
|
`element${count.violations === 1 ? '' : 's'}`));
|
|
583
659
|
if (count.incomplete > 0) {
|
|
584
|
-
|
|
660
|
+
/*
|
|
661
|
+
* Two figures, because two different things were sharing one word — and the
|
|
662
|
+
* old line credited axe for both, which stopped being true when most of the
|
|
663
|
+
* undecided rows came from our own checks.
|
|
664
|
+
*/
|
|
665
|
+
if (count.judgement > 0) {
|
|
666
|
+
lines.push(figureLine(style, count.judgement, 'needing a judgement — a person decides'));
|
|
667
|
+
}
|
|
668
|
+
if (count.unmeasured > 0) {
|
|
669
|
+
lines.push(figureLine(style, count.unmeasured, 'we could not measure — our debt, not yours'));
|
|
670
|
+
}
|
|
585
671
|
}
|
|
586
672
|
lines.push(figureLine(style, count.passes, style.dim(`checks passed, across ${quality.rulesAssessed} distinct rules`)));
|
|
673
|
+
/*
|
|
674
|
+
* The element figure holds still only in the sense that the rule count above it
|
|
675
|
+
* does. Said once, here, where someone is about to quote a number.
|
|
676
|
+
*/
|
|
677
|
+
if (count.violations > 0) {
|
|
678
|
+
lines.push(...continuationLines(style, 'Rule and surface counts are the stable pair. Element counts follow how much data ' +
|
|
679
|
+
'is on the page, so they move between runs of unchanged code — measured at 1,712 ' +
|
|
680
|
+
'and then 1,626 on two runs of one commit, while rules × surfaces held at 230.', width));
|
|
681
|
+
}
|
|
587
682
|
lines.push(...conformanceLines(input, style, width));
|
|
683
|
+
/*
|
|
684
|
+
* THE CUT. Everything below is the report's job unless asked for.
|
|
685
|
+
*
|
|
686
|
+
* Friction record run 4 measured 1,080 lines on Eos, and said the useful part
|
|
687
|
+
* was the first forty. Both halves of that matter: a summary nobody reads is
|
|
688
|
+
* not merely long, it takes the part that WAS worth reading down with it,
|
|
689
|
+
* because by the end of a run those lines have scrolled away.
|
|
690
|
+
*
|
|
691
|
+
* Measured on the fixture suite at 26 surfaces — where the enumerations are
|
|
692
|
+
* the bulk, and the criteria table is the last thing a person reads:
|
|
693
|
+
*
|
|
694
|
+
* RULES FAILING 152 · NEEDS A JUDGEMENT 82 · CONFORMANCE 62 · SURFACES 36
|
|
695
|
+
*
|
|
696
|
+
* What still prints below the cut is anything that QUALIFIES the figures
|
|
697
|
+
* above — what could not be checked, what is worth knowing. Dropping those
|
|
698
|
+
* would make the short summary a MORE confident one than the long summary,
|
|
699
|
+
* which is exactly the wrong direction for an evidence product: the reader
|
|
700
|
+
* would be left with the numbers and without the reasons to distrust them.
|
|
701
|
+
*/
|
|
702
|
+
if (!input.verbose) {
|
|
703
|
+
lines.push(...qualifyingBlocks(input, style, width));
|
|
704
|
+
lines.push(...moreInTheReport(input, style, width));
|
|
705
|
+
lines.push(...trailer());
|
|
706
|
+
return join(lines);
|
|
707
|
+
}
|
|
588
708
|
lines.push(...gridLines(input, style, width));
|
|
589
709
|
lines.push('');
|
|
590
710
|
/*
|
|
@@ -711,24 +831,46 @@ export function renderText(input, columns) {
|
|
|
711
831
|
for (const group of failingRuleGroups)
|
|
712
832
|
lines.push(...ruleEntry(group));
|
|
713
833
|
}
|
|
714
|
-
|
|
834
|
+
/*
|
|
835
|
+
* TWO sections, because two different things were sharing one word.
|
|
836
|
+
*
|
|
837
|
+
* Friction record run 4 measured what that cost: roughly 273 of Eos's 374
|
|
838
|
+
* needs-review rows were measurements we had not taken rather than judgements
|
|
839
|
+
* a person must make. A reader asked to work through a bucket where most rows
|
|
840
|
+
* cannot be acted on without a colour picker learns to skip the bucket — and
|
|
841
|
+
* the rows they skip include the ones an auditor actually wants.
|
|
842
|
+
*
|
|
843
|
+
* So the judgements come first and keep the section a person is meant to read,
|
|
844
|
+
* and our own debt is stated separately, as a figure, under our own name.
|
|
845
|
+
*/
|
|
846
|
+
const judgement = review.filter((group) => group.undecidable !== 'measurement');
|
|
847
|
+
const measurement = review.filter((group) => group.undecidable === 'measurement');
|
|
848
|
+
if (judgement.length > 0) {
|
|
715
849
|
/*
|
|
716
|
-
* A section of its own, not a parenthesis on the failure list.
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
* uncomputable from the DOM. These are neither passes nor failures, and
|
|
721
|
-
* listing them beside violations invites both of the available mistakes:
|
|
722
|
-
* treating them as broken and panicking, or treating them as fine and
|
|
723
|
-
* shipping. They are the one category that REQUIRES a person, which is worth
|
|
724
|
-
* saying plainly given we sell evidence.
|
|
850
|
+
* A section of its own, not a parenthesis on the failure list. These are
|
|
851
|
+
* neither passes nor failures, and listing them beside violations invites
|
|
852
|
+
* both available mistakes: treating them as broken and panicking, or
|
|
853
|
+
* treating them as fine and shipping.
|
|
725
854
|
*/
|
|
726
855
|
lines.push('');
|
|
727
|
-
lines.push(heading(style, 'Needs
|
|
728
|
-
lines.push(...continuationLines(style, '
|
|
729
|
-
'
|
|
856
|
+
lines.push(heading(style, 'Needs a judgement'));
|
|
857
|
+
lines.push(...continuationLines(style, 'Measured as far as measuring goes, and what is left is not mechanical — ' +
|
|
858
|
+
'does this heading describe what it heads, is this error announced. A person ' +
|
|
859
|
+
'settles these by looking, with no tools.', width));
|
|
860
|
+
lines.push('');
|
|
861
|
+
for (const group of judgement)
|
|
862
|
+
lines.push(...ruleEntry(group));
|
|
863
|
+
}
|
|
864
|
+
if (measurement.length > 0) {
|
|
865
|
+
const elements = measurement.reduce((sum, group) => sum + group.nodes, 0);
|
|
866
|
+
lines.push('');
|
|
867
|
+
lines.push(heading(style, 'Could not measure'));
|
|
868
|
+
lines.push(...continuationLines(style, `${elements} element${elements === 1 ? '' : 's'} where we did not get a number — a ` +
|
|
869
|
+
'gradient with no single colour behind it, a value we could not resolve. ' +
|
|
870
|
+
'This is our debt rather than your work, and it is here so it can be seen ' +
|
|
871
|
+
'falling rather than sitting inside a bucket labelled as yours.', width));
|
|
730
872
|
lines.push('');
|
|
731
|
-
for (const group of
|
|
873
|
+
for (const group of measurement)
|
|
732
874
|
lines.push(...ruleEntry(group));
|
|
733
875
|
}
|
|
734
876
|
/*
|
|
@@ -755,6 +897,52 @@ export function renderText(input, columns) {
|
|
|
755
897
|
lines.push(...trailer());
|
|
756
898
|
return join(lines);
|
|
757
899
|
}
|
|
900
|
+
/**
|
|
901
|
+
* The blocks that qualify the figures, and print above the cut as well as below.
|
|
902
|
+
*
|
|
903
|
+
* Deliberately a short list rather than "the insight blocks". `Could not be
|
|
904
|
+
* checked` is the surfaces the run reached and could not scan, and `Worth
|
|
905
|
+
* knowing` carries the scans the settle ceiling cut short — both are reasons to
|
|
906
|
+
* trust the numbers above LESS, and a concise summary that dropped them would be
|
|
907
|
+
* more confident than the full one. `Page runtime`, `Across surfaces`,
|
|
908
|
+
* `Coverage` and `Suite timing` are additional findings rather than
|
|
909
|
+
* qualifications, so they wait for the report or `--verbose`.
|
|
910
|
+
*/
|
|
911
|
+
const QUALIFYING = new Set(['Could not be checked', 'Worth knowing']);
|
|
912
|
+
function qualifyingBlocks(input, style, width) {
|
|
913
|
+
const lines = [];
|
|
914
|
+
for (const block of insightBlocks(input.envelope, {
|
|
915
|
+
...(input.declaredAxes ? { declaredAxes: input.declaredAxes } : {}),
|
|
916
|
+
...(input.unmeasuredAxes ? { unmeasuredAxes: input.unmeasuredAxes } : {}),
|
|
917
|
+
...(input.suite ? { suite: input.suite } : {}),
|
|
918
|
+
...(input.overhead ? { overhead: input.overhead } : {}),
|
|
919
|
+
})) {
|
|
920
|
+
if (!QUALIFYING.has(block.heading))
|
|
921
|
+
continue;
|
|
922
|
+
lines.push(...figureBlock(style, block.heading, block.figures, block.notes, width));
|
|
923
|
+
}
|
|
924
|
+
return lines;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* One line naming what was left out, and how to see it.
|
|
928
|
+
*
|
|
929
|
+
* "See the report" on its own is a dead end — a reader cannot tell whether the
|
|
930
|
+
* thing they want is in there. So the sections are named, and so is the way to
|
|
931
|
+
* get them back in a terminal, because a CI log is a place where scrollback IS
|
|
932
|
+
* the artifact and there is no report to open.
|
|
933
|
+
*/
|
|
934
|
+
function moreInTheReport(input, style, width) {
|
|
935
|
+
const count = totals(input.envelope);
|
|
936
|
+
const rules = new Set(input.envelope.scans.flatMap((scan) => scan.findings.filter((f) => f.status === 'violation').map((f) => f.ruleId))).size;
|
|
937
|
+
if (rules === 0 && count.incomplete === 0)
|
|
938
|
+
return [];
|
|
939
|
+
return [
|
|
940
|
+
'',
|
|
941
|
+
...continuationLines(style, `Which rules, on which surfaces, with the selectors and the criteria grid: in the ` +
|
|
942
|
+
`report below. SETSQUARE_VERBOSE=1 prints all of it here instead, for a CI log with ` +
|
|
943
|
+
`no report to open.`, width),
|
|
944
|
+
];
|
|
945
|
+
}
|
|
758
946
|
/**
|
|
759
947
|
* Join the assembled lines, collapsing any run of blank lines to one.
|
|
760
948
|
*
|
|
@@ -996,7 +1184,19 @@ function conformanceLines(input, style, width) {
|
|
|
996
1184
|
lines.push(figureLine(style, rulesPassed, 'criteria: every automated rule passed — evidence, not conformance'));
|
|
997
1185
|
}
|
|
998
1186
|
if (cantTell > 0) {
|
|
999
|
-
|
|
1187
|
+
/*
|
|
1188
|
+
* A criterion, not a row — so it stays one state.
|
|
1189
|
+
*
|
|
1190
|
+
* A cell rolls up many findings and can hold both kinds at once, and there is
|
|
1191
|
+
* no defensible precedence between them: a criterion with one unmeasured row
|
|
1192
|
+
* and one genuine judgement is not more one than the other. "Automation could
|
|
1193
|
+
* not decide" is true of the cell whichever it holds, and the two sections
|
|
1194
|
+
* above carry the why at the level where a row is the unit.
|
|
1195
|
+
*
|
|
1196
|
+
* The old wording — "criteria need a person" — was the one that stopped being
|
|
1197
|
+
* true, because a cell can now be undecided entirely because of our debt.
|
|
1198
|
+
*/
|
|
1199
|
+
lines.push(figureLine(style, cantTell, 'criteria automation could not decide — see the two sections above'));
|
|
1000
1200
|
}
|
|
1001
1201
|
if (inapplicable > 0) {
|
|
1002
1202
|
// EARL's inapplicable, spelled out: the checks RAN and the pages contain
|
|
@@ -1159,7 +1359,7 @@ function gridLines(input, style, width) {
|
|
|
1159
1359
|
lines.push((indent + label + cells).trimEnd());
|
|
1160
1360
|
}
|
|
1161
1361
|
lines.push('');
|
|
1162
|
-
lines.push(...continuationLines(style, `${glyph.failed} failed ${glyph.cantTell}
|
|
1362
|
+
lines.push(...continuationLines(style, `${glyph.failed} failed ${glyph.cantTell} automation could not decide ` +
|
|
1163
1363
|
`${glyph.rulesPassed} every automated rule passed ` +
|
|
1164
1364
|
`${glyph.inapplicable} nothing to check here ` +
|
|
1165
1365
|
`${glyph.assertedBySuite} covered by your own test ` +
|
|
@@ -1198,9 +1398,28 @@ function warningLines(warnings, width, quality, envelope) {
|
|
|
1198
1398
|
* dying in the envelope.
|
|
1199
1399
|
*/
|
|
1200
1400
|
if (quality && quality.ceiling > 0) {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1401
|
+
/*
|
|
1402
|
+
* Say WHOSE window it was, and WHAT the page was doing in it.
|
|
1403
|
+
*
|
|
1404
|
+
* The old sentence said "after waiting 5s", which is the default — a suite
|
|
1405
|
+
* that raised `settleTimeoutMs` was quoted a number it had changed. And it
|
|
1406
|
+
* described "a page that was still changing" without the one figure that
|
|
1407
|
+
* tells a reader which of the two available actions is theirs: a page a few
|
|
1408
|
+
* mutations from quiet settles if given longer, and a page with a poller
|
|
1409
|
+
* never will, and no timeout helps the second.
|
|
1410
|
+
*
|
|
1411
|
+
* Both numbers are measured off the scans themselves rather than read from
|
|
1412
|
+
* configuration, which also means they stay true if the window is changed
|
|
1413
|
+
* per project.
|
|
1414
|
+
*/
|
|
1415
|
+
const waited = `${(quality.ceilingWaitedMs / 1000).toFixed(1)}s`;
|
|
1416
|
+
notes.push(`${quality.ceiling} scan${quality.ceiling === 1 ? ' was' : 's were'} cut short by our ` +
|
|
1417
|
+
`settle window — the longest waited ${waited} and the page was still changing, with ` +
|
|
1418
|
+
`${quality.ceilingMutations.toLocaleString('en-GB')} DOM mutation` +
|
|
1419
|
+
`${quality.ceilingMutations === 1 ? '' : 's'} counted since it loaded. Treat a clean ` +
|
|
1420
|
+
'result from those as weaker evidence. If the page settles and simply needs longer, ' +
|
|
1421
|
+
'raise `settleTimeoutMs`; if something polls or animates forever, no window will help ' +
|
|
1422
|
+
'and the scan is of a moving page by nature.');
|
|
1204
1423
|
}
|
|
1205
1424
|
if (warnings.length === 0 && notes.length === 0)
|
|
1206
1425
|
return [];
|
|
@@ -1232,22 +1451,29 @@ function warningLines(warnings, width, quality, envelope) {
|
|
|
1232
1451
|
* only things the free layer structurally cannot do. Nothing is removed from the
|
|
1233
1452
|
* local tool to create the paid one, and this block must never imply otherwise.
|
|
1234
1453
|
*/
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1454
|
+
/**
|
|
1455
|
+
* What this run is, and what it is not, in one line.
|
|
1456
|
+
*
|
|
1457
|
+
* Derived from the run's own numbers, which is the only thing that licensed the
|
|
1458
|
+
* block it replaces. No product name and no URL: the reader is running the tool,
|
|
1459
|
+
* so they know where it comes from, and a limit stated as a limit does not need
|
|
1460
|
+
* a destination attached to be useful.
|
|
1461
|
+
*
|
|
1462
|
+
* Not switchable. "Local runs keep no history, baselines or comparison" is a
|
|
1463
|
+
* fact about the artefact in front of them — the dev plan holds it as a
|
|
1464
|
+
* permanent property of the free layer rather than a gap — so it belongs beside
|
|
1465
|
+
* the figures whether or not anyone would prefer it quiet.
|
|
1466
|
+
*/
|
|
1467
|
+
function historyCaveat(input, style, width) {
|
|
1468
|
+
const count = totals(input.envelope);
|
|
1238
1469
|
const failingRules = new Set(input.envelope.scans.flatMap((scan) => scan.findings.filter((f) => f.status === 'violation').map((f) => f.ruleId))).size;
|
|
1239
1470
|
const subject = failingRules > 0
|
|
1240
1471
|
? `${failingRules} rule${failingRules === 1 ? '' : 's'} failing across ${count.surfaces} surface${count.surfaces === 1 ? '' : 's'}`
|
|
1241
1472
|
: `${count.surfaces} clean surface${count.surfaces === 1 ? '' : 's'}`;
|
|
1242
1473
|
return [
|
|
1243
1474
|
'',
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
...wrap(`${subject}, this run only. Setsquare Cloud adds what a local tool cannot:`, width - 4, '').map((line) => ` ${line}`),
|
|
1247
|
-
' · is this better or worse than last week? (local runs keep no history)',
|
|
1248
|
-
' · which change introduced each one, on the PR that did it',
|
|
1249
|
-
' · the root cause behind repeats, grouped instead of repeated',
|
|
1250
|
-
` ${PLATFORM_URL}`,
|
|
1475
|
+
...continuationLines(style, `${subject}, this run only — local runs keep no history, no baselines and no ` +
|
|
1476
|
+
'comparison with a previous run, by design.', width),
|
|
1251
1477
|
'',
|
|
1252
1478
|
];
|
|
1253
1479
|
}
|