@setsquare/review-sdk 0.0.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 (181) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +57 -0
  3. package/README.md +338 -0
  4. package/THIRD-PARTY-NOTICES.md +61 -0
  5. package/dist/autoscan.d.ts +25 -0
  6. package/dist/autoscan.d.ts.map +1 -0
  7. package/dist/autoscan.js +332 -0
  8. package/dist/autoscan.js.map +1 -0
  9. package/dist/axe.d.ts +96 -0
  10. package/dist/axe.d.ts.map +1 -0
  11. package/dist/axe.js +210 -0
  12. package/dist/axe.js.map +1 -0
  13. package/dist/bin/setsquare.d.ts +3 -0
  14. package/dist/bin/setsquare.d.ts.map +1 -0
  15. package/dist/bin/setsquare.js +21 -0
  16. package/dist/bin/setsquare.js.map +1 -0
  17. package/dist/brand.d.ts +84 -0
  18. package/dist/brand.d.ts.map +1 -0
  19. package/dist/brand.js +335 -0
  20. package/dist/brand.js.map +1 -0
  21. package/dist/checks/dynamic.d.ts +57 -0
  22. package/dist/checks/dynamic.d.ts.map +1 -0
  23. package/dist/checks/dynamic.js +184 -0
  24. package/dist/checks/dynamic.js.map +1 -0
  25. package/dist/checks/rule-pack.d.ts +34 -0
  26. package/dist/checks/rule-pack.d.ts.map +1 -0
  27. package/dist/checks/rule-pack.js +270 -0
  28. package/dist/checks/rule-pack.js.map +1 -0
  29. package/dist/checks/versions.d.ts +28 -0
  30. package/dist/checks/versions.d.ts.map +1 -0
  31. package/dist/checks/versions.js +45 -0
  32. package/dist/checks/versions.js.map +1 -0
  33. package/dist/ci.d.ts +37 -0
  34. package/dist/ci.d.ts.map +1 -0
  35. package/dist/ci.js +199 -0
  36. package/dist/ci.js.map +1 -0
  37. package/dist/contract.d.ts +334 -0
  38. package/dist/contract.d.ts.map +1 -0
  39. package/dist/contract.js +35 -0
  40. package/dist/contract.js.map +1 -0
  41. package/dist/dedupe.d.ts +79 -0
  42. package/dist/dedupe.d.ts.map +1 -0
  43. package/dist/dedupe.js +102 -0
  44. package/dist/dedupe.js.map +1 -0
  45. package/dist/document.d.ts +29 -0
  46. package/dist/document.d.ts.map +1 -0
  47. package/dist/document.js +94 -0
  48. package/dist/document.js.map +1 -0
  49. package/dist/envelope.d.ts +124 -0
  50. package/dist/envelope.d.ts.map +1 -0
  51. package/dist/envelope.js +337 -0
  52. package/dist/envelope.js.map +1 -0
  53. package/dist/escape.d.ts +14 -0
  54. package/dist/escape.d.ts.map +1 -0
  55. package/dist/escape.js +20 -0
  56. package/dist/escape.js.map +1 -0
  57. package/dist/findings.d.ts +47 -0
  58. package/dist/findings.d.ts.map +1 -0
  59. package/dist/findings.js +229 -0
  60. package/dist/findings.js.map +1 -0
  61. package/dist/fixture.d.ts +7 -0
  62. package/dist/fixture.d.ts.map +1 -0
  63. package/dist/fixture.js +237 -0
  64. package/dist/fixture.js.map +1 -0
  65. package/dist/group.d.ts +214 -0
  66. package/dist/group.d.ts.map +1 -0
  67. package/dist/group.js +709 -0
  68. package/dist/group.js.map +1 -0
  69. package/dist/html-report.d.ts +68 -0
  70. package/dist/html-report.d.ts.map +1 -0
  71. package/dist/html-report.js +649 -0
  72. package/dist/html-report.js.map +1 -0
  73. package/dist/human.d.ts +65 -0
  74. package/dist/human.d.ts.map +1 -0
  75. package/dist/human.js +111 -0
  76. package/dist/human.js.map +1 -0
  77. package/dist/index.d.ts +34 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +33 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/infer.d.ts +65 -0
  82. package/dist/infer.d.ts.map +1 -0
  83. package/dist/infer.js +67 -0
  84. package/dist/infer.js.map +1 -0
  85. package/dist/insights.d.ts +140 -0
  86. package/dist/insights.d.ts.map +1 -0
  87. package/dist/insights.js +483 -0
  88. package/dist/insights.js.map +1 -0
  89. package/dist/install.d.ts +8 -0
  90. package/dist/install.d.ts.map +1 -0
  91. package/dist/install.js +114 -0
  92. package/dist/install.js.map +1 -0
  93. package/dist/links.d.ts +45 -0
  94. package/dist/links.d.ts.map +1 -0
  95. package/dist/links.js +45 -0
  96. package/dist/links.js.map +1 -0
  97. package/dist/mask.d.ts +85 -0
  98. package/dist/mask.d.ts.map +1 -0
  99. package/dist/mask.js +98 -0
  100. package/dist/mask.js.map +1 -0
  101. package/dist/observe.d.ts +51 -0
  102. package/dist/observe.d.ts.map +1 -0
  103. package/dist/observe.js +233 -0
  104. package/dist/observe.js.map +1 -0
  105. package/dist/options.d.ts +249 -0
  106. package/dist/options.d.ts.map +1 -0
  107. package/dist/options.js +185 -0
  108. package/dist/options.js.map +1 -0
  109. package/dist/output.d.ts +113 -0
  110. package/dist/output.d.ts.map +1 -0
  111. package/dist/output.js +231 -0
  112. package/dist/output.js.map +1 -0
  113. package/dist/perf.d.ts +11 -0
  114. package/dist/perf.d.ts.map +1 -0
  115. package/dist/perf.js +150 -0
  116. package/dist/perf.js.map +1 -0
  117. package/dist/reading-level.d.ts +73 -0
  118. package/dist/reading-level.d.ts.map +1 -0
  119. package/dist/reading-level.js +189 -0
  120. package/dist/reading-level.js.map +1 -0
  121. package/dist/reporter.d.ts +94 -0
  122. package/dist/reporter.d.ts.map +1 -0
  123. package/dist/reporter.js +709 -0
  124. package/dist/reporter.js.map +1 -0
  125. package/dist/rules.d.ts +55 -0
  126. package/dist/rules.d.ts.map +1 -0
  127. package/dist/rules.js +77 -0
  128. package/dist/rules.js.map +1 -0
  129. package/dist/runtime.d.ts +32 -0
  130. package/dist/runtime.d.ts.map +1 -0
  131. package/dist/runtime.js +67 -0
  132. package/dist/runtime.js.map +1 -0
  133. package/dist/sanitise.d.ts +116 -0
  134. package/dist/sanitise.d.ts.map +1 -0
  135. package/dist/sanitise.js +247 -0
  136. package/dist/sanitise.js.map +1 -0
  137. package/dist/scan.d.ts +60 -0
  138. package/dist/scan.d.ts.map +1 -0
  139. package/dist/scan.js +246 -0
  140. package/dist/scan.js.map +1 -0
  141. package/dist/screens.d.ts +98 -0
  142. package/dist/screens.d.ts.map +1 -0
  143. package/dist/screens.js +301 -0
  144. package/dist/screens.js.map +1 -0
  145. package/dist/settle.d.ts +65 -0
  146. package/dist/settle.d.ts.map +1 -0
  147. package/dist/settle.js +162 -0
  148. package/dist/settle.js.map +1 -0
  149. package/dist/shots.d.ts +109 -0
  150. package/dist/shots.d.ts.map +1 -0
  151. package/dist/shots.js +113 -0
  152. package/dist/shots.js.map +1 -0
  153. package/dist/style.d.ts +47 -0
  154. package/dist/style.d.ts.map +1 -0
  155. package/dist/style.js +154 -0
  156. package/dist/style.js.map +1 -0
  157. package/dist/suite.d.ts +218 -0
  158. package/dist/suite.d.ts.map +1 -0
  159. package/dist/suite.js +219 -0
  160. package/dist/suite.js.map +1 -0
  161. package/dist/summary.d.ts +268 -0
  162. package/dist/summary.d.ts.map +1 -0
  163. package/dist/summary.js +1308 -0
  164. package/dist/summary.js.map +1 -0
  165. package/dist/unscannable.d.ts +75 -0
  166. package/dist/unscannable.d.ts.map +1 -0
  167. package/dist/unscannable.js +111 -0
  168. package/dist/unscannable.js.map +1 -0
  169. package/dist/versions.d.ts +13 -0
  170. package/dist/versions.d.ts.map +1 -0
  171. package/dist/versions.js +39 -0
  172. package/dist/versions.js.map +1 -0
  173. package/dist/wcag-map.d.ts +19 -0
  174. package/dist/wcag-map.d.ts.map +1 -0
  175. package/dist/wcag-map.js +52 -0
  176. package/dist/wcag-map.js.map +1 -0
  177. package/dist/with-setsquare.d.ts +26 -0
  178. package/dist/with-setsquare.d.ts.map +1 -0
  179. package/dist/with-setsquare.js +70 -0
  180. package/dist/with-setsquare.js.map +1 -0
  181. package/package.json +81 -0
@@ -0,0 +1,94 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { sanitiseLabel } from './sanitise.js';
3
+ /**
4
+ * Hash the title to 16 hex characters.
5
+ *
6
+ * A page title routinely names a person — `Candidate: Jane Doe — Review` — and
7
+ * the finding needs only equality, never the text. Hashing in the worker means
8
+ * the title is never written into a record, an attachment, or Playwright's trace.
9
+ *
10
+ * Stated plainly because it would otherwise be overclaimed: a title is
11
+ * low-entropy text, so this is DATA MINIMISATION, not a privacy guarantee. Anyone
12
+ * holding a candidate list could confirm a guess. What it does buy is that we
13
+ * never store or transmit the string, and the platform never needs to.
14
+ */
15
+ function hashTitle(title) {
16
+ return createHash('sha256').update(title, 'utf8').digest('hex').slice(0, 16);
17
+ }
18
+ /**
19
+ * Read theme and document facts from the live page.
20
+ *
21
+ * Returns `{}` on any failure. Every field is independently optional, and absence
22
+ * always means "not captured", never a negative finding — `documentConsistency`
23
+ * relies on that to keep an uncaptured surface out of `skipLinkAbsent`, which
24
+ * would otherwise manufacture findings out of our own gaps.
25
+ */
26
+ export async function readDocumentFacts(page) {
27
+ let raw;
28
+ try {
29
+ raw = await page.evaluate(() => {
30
+ const root = document.documentElement;
31
+ const result = {};
32
+ // Our own attribute first: a customer who has told us explicitly should not
33
+ // be second-guessed by a heuristic. `data-theme` is the widespread
34
+ // convention and is what the reference implementation uses.
35
+ const theme = root.getAttribute('data-setsquare-theme') ?? root.getAttribute('data-theme') ?? undefined;
36
+ if (theme)
37
+ result.theme = theme;
38
+ const title = document.title;
39
+ if (typeof title === 'string' && title.length > 0)
40
+ result.title = title;
41
+ const lang = root.getAttribute('lang');
42
+ if (lang)
43
+ result.lang = lang;
44
+ /*
45
+ * A skip link is the first in-page anchor that lands on a real target.
46
+ *
47
+ * Deliberately structural rather than text-matching: looking for the words
48
+ * "skip to content" is English-only and would report every localised site as
49
+ * having no skip link. Only the first few anchors are considered, because a
50
+ * skip link that is not near the top of the document is not doing its job —
51
+ * and `bypass` is axe's rule for the conformance question. This value exists
52
+ * to compare surfaces against each other, not to adjudicate 2.4.1.
53
+ */
54
+ const anchors = Array.from(document.querySelectorAll('a[href^="#"]')).slice(0, 5);
55
+ result.hasSkipLink = anchors.some((anchor) => {
56
+ const href = anchor.getAttribute('href');
57
+ if (!href || href === '#')
58
+ return false;
59
+ try {
60
+ return document.querySelector(`[id="${CSS.escape(href.slice(1))}"]`) !== null;
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ });
66
+ return result;
67
+ });
68
+ }
69
+ catch {
70
+ // Navigating away mid-read, a closed page, a policy that blocked evaluation.
71
+ // None of these is a finding about the page.
72
+ return {};
73
+ }
74
+ const facts = {};
75
+ const theme = sanitiseLabel(raw.theme);
76
+ if (theme)
77
+ facts.theme = theme;
78
+ const document_ = {};
79
+ if (raw.title !== undefined)
80
+ document_.titleHash = hashTitle(raw.title);
81
+ // BCP-47's practical maximum. A `lang` longer than this is not a language tag,
82
+ // so it is dropped rather than truncated into something that looks like one.
83
+ if (raw.lang !== undefined && raw.lang.length <= 35) {
84
+ const lang = sanitiseLabel(raw.lang);
85
+ if (lang)
86
+ document_.lang = lang;
87
+ }
88
+ if (raw.hasSkipLink !== undefined)
89
+ document_.hasSkipLink = raw.hasSkipLink;
90
+ if (Object.keys(document_).length > 0)
91
+ facts.document = document_;
92
+ return facts;
93
+ }
94
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.js","sourceRoot":"","sources":["../src/document.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ import type { RunIngestRequest, TestResult } from '@setsquare/schema';
2
+ import type { ScanRecord } from './contract.js';
3
+ import type { SlowStep, TestFailureFacts } from './suite.js';
4
+ import type { Shot } from './shots.js';
5
+ import { UNKNOWN } from './ci.js';
6
+ import { type InferenceConfig } from './infer.js';
7
+ /**
8
+ * Assemble the wire body from what the workers observed.
9
+ *
10
+ * This file is the reporter's half of the seam: everything here is
11
+ * interpretation, and none of it happens in a worker. It produces the ingest
12
+ * request body EXACTLY as `packages/schema` defines it — no local-only fields,
13
+ * no helpful extras. Local facts go to the sidecar. That separation is what makes
14
+ * "the same artifact the platform would have received, minus nothing" a claim the
15
+ * test suite can check rather than a sentence in a README.
16
+ *
17
+ * The envelope has two arrays because a test and a scan are different things with
18
+ * different cardinalities — a test visits many surfaces, or none. Stage 1 of P1a
19
+ * is what proved that: the old single-array shape silently discarded every
20
+ * surface after the first.
21
+ */
22
+ /** One finished test, as the reporter saw it. */
23
+ export interface TestOutcome {
24
+ testId: string;
25
+ title: string;
26
+ project?: string;
27
+ retry: number;
28
+ status: TestResult['status'];
29
+ durationMs: number;
30
+ records: ScanRecord[];
31
+ /**
32
+ * Which Playwright worker ran this test.
33
+ *
34
+ * Absent when Playwright reported `-1` (a test that never got a worker), which
35
+ * must not become `0` — worker zero is a real worker, and conflating the two
36
+ * would invent an imbalance out of skipped tests.
37
+ */
38
+ workerIndex?: number;
39
+ /** Why it failed. Absent on a pass, and on a failure whose result said nothing. */
40
+ failure?: TestFailureFacts;
41
+ /**
42
+ * The slowest leaf steps in this test.
43
+ *
44
+ * Local-only, and a REDUCTION rather than the tree: a full step tree is
45
+ * unbounded and the useful artefact is a short list, so the truncation happens
46
+ * in the worker where the tree already exists.
47
+ */
48
+ slowestSteps?: SlowStep[];
49
+ /**
50
+ * Whether this test's project prepares the suite rather than tests it,
51
+ * resolved against ITS OWN invocation's dependency graph.
52
+ *
53
+ * Set on outcomes that crossed an invocation boundary (run groups). A merged
54
+ * run holds outcomes from several Playwright configs, and a project name that
55
+ * is infrastructure in one config's graph can be a real test project in
56
+ * another's — so the flag must be resolved where the graph is, not against a
57
+ * union of sets that flattens the distinction. Absent means "resolve against
58
+ * `infrastructureProjects`", which is the single-invocation path unchanged.
59
+ */
60
+ infrastructure?: boolean;
61
+ /**
62
+ * The inference config of the invocation this outcome came from, for the
63
+ * same reason: surface/persona/theme maps are per-config, and resolving one
64
+ * lane's scans with another lane's maps would mislabel them quietly.
65
+ */
66
+ inference?: InferenceConfig;
67
+ /**
68
+ * The run-group lane label this outcome came from. Set only by the merge;
69
+ * flows onto the wire result so a consumer can query it without parsing the
70
+ * testId prefix back apart.
71
+ */
72
+ lane?: string;
73
+ /**
74
+ * Where Playwright put the screenshots this test produced.
75
+ *
76
+ * Paths only — never pixels, and never a file we wrote. Local to the sidecar
77
+ * and the contact sheet, and deliberately absent from the wire envelope: a
78
+ * filesystem path is meaningless to the platform and `run.json` is strict, so
79
+ * `buildEnvelope` reads this field for nothing. It travels through run-group
80
+ * parts, which is what lets a merged sheet show every lane's captures.
81
+ */
82
+ shots?: Shot[];
83
+ }
84
+ export interface EnvelopeInputs {
85
+ outcomes: TestOutcome[];
86
+ /** Projects that prepare the suite rather than test it — derived, not configured. */
87
+ infrastructureProjects?: ReadonlySet<string>;
88
+ inference?: InferenceConfig;
89
+ sdkVersion: string;
90
+ axeVersion: string;
91
+ branch: string;
92
+ commit: string;
93
+ runSource: 'ci' | 'local';
94
+ ci?: RunIngestRequest['source']['ci'];
95
+ /** Present only when this envelope is a run-group merge. See the schema. */
96
+ runGroup?: RunIngestRequest['source']['runGroup'];
97
+ generatedAt: string;
98
+ }
99
+ /**
100
+ * Re-exported so the envelope, the reporter and their tests cannot disagree on
101
+ * what 'we could not determine this' looks like.
102
+ */
103
+ export { UNKNOWN };
104
+ export interface EnvelopeBuild {
105
+ envelope: RunIngestRequest;
106
+ unknowns: string[];
107
+ warnings: string[];
108
+ }
109
+ /** Re-exported so the envelope's own tests and the reporter agree on one value. */
110
+ export declare function buildEnvelope(inputs: EnvelopeInputs): EnvelopeBuild;
111
+ /**
112
+ * Pin the detection engine.
113
+ *
114
+ * Stage 1 digests the ruleset's IDENTITY (the axe-core version), which is
115
+ * reproducible and honest but coarse: it does not change when rules are enabled
116
+ * or disabled. Once scanning exists this digests the actual resolved rule set,
117
+ * which is what "evidence claims are relative to a specific ruleset" requires.
118
+ *
119
+ * Deliberately a real hash of a stated input rather than a placeholder constant.
120
+ * A fake digest that looks like a digest is worse than no digest, because it
121
+ * would be trusted.
122
+ */
123
+ export declare function rulesetDigest(axeVersion: string): string;
124
+ //# sourceMappingURL=envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAW,gBAAgB,EAAE,UAAU,EAAgB,MAAM,mBAAmB,CAAC;AAQ7F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,YAAY,CAAC;AAGpB;;;;;;;;;;;;;;GAcG;AAEH,iDAAiD;AACjD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAC;IAC1B;;;;;;;;;;OAUG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,qFAAqF;IACrF,sBAAsB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7C,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,IAAI,GAAG,OAAO,CAAC;IAC1B,EAAE,CAAC,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC;IACtC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAC,UAAU,CAAC,CAAC;IAClD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,OAAO,EAAE,OAAO,EAAE,CAAC;AAEnB,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,mFAAmF;AAEnF,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,CAyKnE;AA2HD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGxD"}
@@ -0,0 +1,337 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { FORMAT_VERSION } from '@setsquare/schema/format-version';
3
+ import { MAX_SURFACE_LENGTH, MAX_TEST_ID_LENGTH, MAX_TEST_TITLE_LENGTH, } from '@setsquare/schema/limits';
4
+ import { UNKNOWN } from './ci.js';
5
+ import { dedupeScans } from './dedupe.js';
6
+ import { inferSurface, resolvePersona, resolveTheme, } from './infer.js';
7
+ import { capLength, sanitiseTestText, toUrlPattern } from './sanitise.js';
8
+ /**
9
+ * Re-exported so the envelope, the reporter and their tests cannot disagree on
10
+ * what 'we could not determine this' looks like.
11
+ */
12
+ export { UNKNOWN };
13
+ /** Re-exported so the envelope's own tests and the reporter agree on one value. */
14
+ export function buildEnvelope(inputs) {
15
+ const unknowns = [];
16
+ const warnings = [];
17
+ if (inputs.branch === UNKNOWN)
18
+ unknowns.push('branch');
19
+ if (inputs.commit === UNKNOWN)
20
+ unknowns.push('commit');
21
+ const results = [];
22
+ const candidates = [];
23
+ /*
24
+ * The unscannable ledger, summed across every page in the run.
25
+ *
26
+ * Aggregated here rather than per record because a route pattern that failed to
27
+ * settle in four different tests is one fact with a count of four, not four
28
+ * facts. Summing is also what makes a merged run group correct — parts carry
29
+ * their own rows and the merge must add them, not concatenate duplicates.
30
+ */
31
+ const ledger = new Map();
32
+ let unscannedOmitted = 0;
33
+ for (const outcome of inputs.outcomes) {
34
+ results.push(toResult(outcome));
35
+ for (const record of outcome.records) {
36
+ if (record.kind !== 'observation')
37
+ continue;
38
+ unscannedOmitted += record.unscannedOmitted ?? 0;
39
+ for (const row of record.unscanned ?? []) {
40
+ addLedgerRow(ledger, row.urlPattern, row.reason, row.count);
41
+ }
42
+ }
43
+ for (const scan of toScans(outcome, outcome.inference ?? inputs.inference ?? {})) {
44
+ const project = outcome.project ?? '';
45
+ candidates.push({
46
+ scan,
47
+ project,
48
+ infrastructure: outcome.infrastructure ?? inputs.infrastructureProjects?.has(project) ?? false,
49
+ index: candidates.length,
50
+ });
51
+ }
52
+ }
53
+ /*
54
+ * Best-wins, keyed on the full context tuple.
55
+ *
56
+ * The reference implementation's warmup project reaches every route in the app
57
+ * FIRST, so a first-wins rule would have filled the grid with evidence from a
58
+ * pass that only ever waited for a heading. See `dedupe.ts`.
59
+ */
60
+ const { kept: scans, dropped } = dedupeScans(candidates);
61
+ /*
62
+ * Surfaces the suite VISITED but nothing SCANNED.
63
+ *
64
+ * Auto-scan fires when a page settles, and a test that clicks through three
65
+ * routes faster than the quiet window leaves two of them unscanned — the
66
+ * teardown sweep only ever sees the last one. Measured on the fixture suite:
67
+ * the SPA spec visits /spa, /spa/one and /spa/two, and /spa/two is gone.
68
+ *
69
+ * This is the honest limit of a heuristic that must not scan mid-render, and it
70
+ * is reported rather than hidden. A customer looking at a grid missing a surface
71
+ * their suite definitely visits deserves to be told which one and why — the
72
+ * alternative is that they conclude the surface is fine, which is the one thing
73
+ * this product must never let them conclude by accident.
74
+ *
75
+ * The observation records exist for exactly this: the fixture watches every
76
+ * navigation whether or not a scan follows.
77
+ */
78
+ const scannedPatterns = new Set(scans.map((scan) => scan.context.urlPattern ?? ''));
79
+ const visited = new Set();
80
+ for (const outcome of inputs.outcomes) {
81
+ for (const record of outcome.records) {
82
+ if (record.kind !== 'observation')
83
+ continue;
84
+ for (const navigation of record.observed.navigations) {
85
+ visited.add(toUrlPattern(navigation.url).pattern);
86
+ }
87
+ }
88
+ }
89
+ const unscanned = [...visited].filter((pattern) => !scannedPatterns.has(pattern)).sort();
90
+ for (const pattern of unscanned) {
91
+ /*
92
+ * The same fact as the sentence below, as a countable row.
93
+ *
94
+ * Both are kept deliberately: the prose says WHY in a way a figure cannot,
95
+ * and the row is what lets coverage state its own denominator. Dropping the
96
+ * sentence when the ledger arrived would have been a regression in
97
+ * explanation dressed up as progress.
98
+ */
99
+ addLedgerRow(ledger, pattern, 'settle-timed-out-empty', 1);
100
+ }
101
+ if (unscanned.length > 0) {
102
+ warnings.push(
103
+ // "surface(s) were" is the sound a form makes, not a sentence. This line is
104
+ // read by someone deciding whether their run covered what they think it
105
+ // did, and a tool that cannot conjugate reads as a tool that was not
106
+ // finished.
107
+ `${unscanned.length} ${unscanned.length === 1 ? 'surface was' : 'surfaces were'} ` +
108
+ 'visited but never scanned, because the test moved on before the page ' +
109
+ `settled: ${unscanned.slice(0, 10).join(', ')}${unscanned.length > 10 ? ', …' : ''}. ` +
110
+ `${unscanned.length === 1 ? 'It is' : 'They are'} absent from this run rather ` +
111
+ 'than reported as clean.');
112
+ }
113
+ /*
114
+ * The ruleset actually used, taken from the scans rather than from the
115
+ * installed package version.
116
+ *
117
+ * These differ in a way that matters. The installed version says which axe is
118
+ * on disk; the digest of a real run says which rules were evaluated inside the
119
+ * browser — which is the thing an evidence claim is relative to. Falling back
120
+ * to the version-only digest happens only when nothing was scanned, and it is
121
+ * honest there because there is no ruleset to describe.
122
+ */
123
+ const pinned = pinnedRuleset(inputs.outcomes);
124
+ if (pinned && pinned.disagreements > 0) {
125
+ /*
126
+ * Scans within one run disagreeing on the ruleset is not a hypothetical: a
127
+ * sharded run can be produced by runners whose dependencies drifted. The run
128
+ * envelope has one digest, so this is stated out loud rather than resolved by
129
+ * picking a winner quietly — findings from two rulesets are not comparable,
130
+ * and pretending otherwise is exactly the kind of claim this product exists
131
+ * not to make.
132
+ */
133
+ warnings.push(`Scans in this run used ${pinned.disagreements + 1} different axe rulesets. The run ` +
134
+ `envelope records one, so findings across this run are not strictly comparable. ` +
135
+ `This usually means runners with drifted dependencies.`);
136
+ }
137
+ const envelope = {
138
+ formatVersion: FORMAT_VERSION,
139
+ kind: 'run',
140
+ generatedAt: inputs.generatedAt,
141
+ source: {
142
+ runSource: inputs.runSource,
143
+ // The client never claims canonicality. The server decides, and in the
144
+ // local path there is no server — so this is false, always, and honestly.
145
+ canonical: false,
146
+ branch: inputs.branch,
147
+ commit: inputs.commit,
148
+ sdkVersion: inputs.sdkVersion,
149
+ axeVersion: pinned?.axeVersion ?? inputs.axeVersion,
150
+ rulesetDigest: pinned?.rulesetDigest ?? rulesetDigest(inputs.axeVersion),
151
+ ...(inputs.ci ? { ci: inputs.ci } : {}),
152
+ ...(inputs.runGroup ? { runGroup: inputs.runGroup } : {}),
153
+ },
154
+ results,
155
+ scans,
156
+ // Absent when nothing went unscanned. An empty array would be a claim that
157
+ // the run reached everything, which is a stronger statement than we have.
158
+ ...(ledger.size > 0 ? { unscanned: [...ledger.values()] } : {}),
159
+ };
160
+ if (unscannedOmitted > 0) {
161
+ warnings.push(`${unscannedOmitted} unscannable-surface events were not recorded because the run hit ` +
162
+ `the ledger's row cap. The counts below are a floor, not a total.`);
163
+ }
164
+ return { envelope, unknowns, warnings };
165
+ }
166
+ function toResult(outcome) {
167
+ const result = {
168
+ testId: sanitiseTestText(outcome.testId, MAX_TEST_ID_LENGTH),
169
+ title: sanitiseTestText(outcome.title, MAX_TEST_TITLE_LENGTH),
170
+ // A test runs in exactly one engine, because a Playwright project has one
171
+ // browserName. Its scans inherit it through `testId` rather than repeating it.
172
+ engine: engineOf(outcome),
173
+ retry: outcome.retry,
174
+ status: outcome.status,
175
+ durationMs: Math.max(0, Math.round(outcome.durationMs)),
176
+ };
177
+ if (outcome.project)
178
+ result.project = outcome.project;
179
+ if (outcome.lane)
180
+ result.lane = outcome.lane;
181
+ if (outcome.workerIndex !== undefined)
182
+ result.workerIndex = outcome.workerIndex;
183
+ if (outcome.failure)
184
+ result.failure = outcome.failure;
185
+ return result;
186
+ }
187
+ /**
188
+ * Turn a test's scan records into wire-format scans.
189
+ *
190
+ * Both kinds of scan arrive here as `kind: 'scan'` records and differ in one
191
+ * thing: whether the customer named the surface. An explicit call carries a name
192
+ * we could never have worked out; an automatic one carries an empty name and
193
+ * gets the route's own shape instead. The payload always says which, so a grid
194
+ * can be honest about how confident its row labels are.
195
+ *
196
+ * Observations produce NO scan rows. Under the reshaped schema a scan row IS the
197
+ * evidence that scanning happened — a row with no findings means "scanned, found
198
+ * nothing", and no row means "never scanned". Emitting a row for a page the suite
199
+ * merely walked past would assert a clean result for something nothing looked at.
200
+ */
201
+ /**
202
+ * Add to an aggregated ledger, summing rather than appending.
203
+ *
204
+ * Joined on U+0000, which the contract bans from every string it accepts, so a
205
+ * route pattern containing the separator cannot forge a collision.
206
+ */
207
+ function addLedgerRow(ledger, urlPattern, reason, count) {
208
+ const key = `${urlPattern}\u0000${reason}`;
209
+ const existing = ledger.get(key);
210
+ if (existing)
211
+ existing.count += count;
212
+ else
213
+ ledger.set(key, { urlPattern, reason, count });
214
+ }
215
+ function toScans(outcome, inference) {
216
+ const scans = [];
217
+ for (const record of outcome.records) {
218
+ if (record.kind !== 'scan')
219
+ continue;
220
+ const { pattern } = toUrlPattern(record.page.url);
221
+ const explicit = record.declared.surface.length > 0;
222
+ const context = {
223
+ project: record.page.project,
224
+ urlPattern: pattern,
225
+ ...(record.page.observedTheme ? { observedTheme: record.page.observedTheme } : {}),
226
+ };
227
+ // A declared value always outranks a resolved one: the customer telling us
228
+ // beats us inferring, on every axis, everywhere.
229
+ const theme = record.declared.theme ?? resolveTheme(context, inference);
230
+ const persona = record.declared.persona ?? resolvePersona(context, inference);
231
+ const surface = explicit
232
+ ? capLength(record.declared.surface, MAX_SURFACE_LENGTH)
233
+ : inferSurface(pattern, inference);
234
+ const scan = {
235
+ testId: sanitiseTestText(outcome.testId, MAX_TEST_ID_LENGTH),
236
+ visitRef: record.visitRef,
237
+ context: {
238
+ surface,
239
+ surfaceSource: explicit ? 'explicit' : 'inferred',
240
+ urlPattern: pattern,
241
+ },
242
+ // Stated by the scan, never inferred from what it found: a scan with no AAA
243
+ // findings might be an AA scan that never looked.
244
+ wcagTarget: record.wcagTarget,
245
+ findings: record.findings,
246
+ passes: record.passes,
247
+ ...(record.inapplicable && record.inapplicable.length > 0
248
+ ? { inapplicable: record.inapplicable }
249
+ : {}),
250
+ };
251
+ if (theme)
252
+ scan.context.theme = theme;
253
+ if (persona)
254
+ scan.context.persona = persona;
255
+ if (record.declared.state)
256
+ scan.context.state = record.declared.state;
257
+ if (record.page.viewport)
258
+ scan.context.viewport = record.page.viewport;
259
+ // Only the first scan of a page load carries timings, so this is absent far
260
+ // more often than it is present — which is exactly right.
261
+ if (record.perf)
262
+ scan.perf = record.perf;
263
+ // Same grain as perf, for the same reason: these describe the page LOAD.
264
+ // Absent on later scans of one visit, so one 500 is reported once.
265
+ if (record.runtime)
266
+ scan.runtime = record.runtime;
267
+ if (record.document)
268
+ scan.document = record.document;
269
+ // Absent on explicit scans: the customer chose the moment, so no heuristic
270
+ // ran and there is nothing to be suspicious of.
271
+ if (record.settle)
272
+ scan.settle = record.settle;
273
+ scans.push(scan);
274
+ }
275
+ return scans;
276
+ }
277
+ /** Stable within a run, meaningless outside it — which is all the server needs. */
278
+ function visitRef(testId, atMs) {
279
+ return createHash('sha256').update(`${testId}#${atMs}`, 'utf8').digest('hex').slice(0, 16);
280
+ }
281
+ /**
282
+ * Pin the detection engine.
283
+ *
284
+ * Stage 1 digests the ruleset's IDENTITY (the axe-core version), which is
285
+ * reproducible and honest but coarse: it does not change when rules are enabled
286
+ * or disabled. Once scanning exists this digests the actual resolved rule set,
287
+ * which is what "evidence claims are relative to a specific ruleset" requires.
288
+ *
289
+ * Deliberately a real hash of a stated input rather than a placeholder constant.
290
+ * A fake digest that looks like a digest is worse than no digest, because it
291
+ * would be trusted.
292
+ */
293
+ export function rulesetDigest(axeVersion) {
294
+ const digest = createHash('sha256').update(`axe-core@${axeVersion}`, 'utf8').digest('hex');
295
+ return `sha256:${digest}`;
296
+ }
297
+ /**
298
+ * The engine a test ran in, from whichever record kind is available.
299
+ *
300
+ * A Playwright project has one browserName, so every record for a test agrees;
301
+ * this only needs to find one of them. Defaults to chromium when a test opened no
302
+ * browser at all — the field is required, and for a test with no page the value
303
+ * is a formality rather than a claim about anything.
304
+ */
305
+ function engineOf(outcome) {
306
+ for (const record of outcome.records) {
307
+ if (record.kind === 'observation')
308
+ return record.observed.engine;
309
+ return record.page.engine;
310
+ }
311
+ return 'chromium';
312
+ }
313
+ /**
314
+ * The ruleset the run's scans actually used.
315
+ *
316
+ * Returns the first scan's pinning and a count of how many others disagreed with
317
+ * it. Scans are visited in order so the answer is deterministic — two runs of the
318
+ * same suite must produce byte-identical envelopes wherever the inputs match.
319
+ */
320
+ function pinnedRuleset(outcomes) {
321
+ let pinned;
322
+ let disagreements = 0;
323
+ for (const outcome of outcomes) {
324
+ for (const record of outcome.records) {
325
+ if (record.kind !== 'scan')
326
+ continue;
327
+ if (!pinned) {
328
+ pinned = { axeVersion: record.axeVersion, rulesetDigest: record.rulesetDigest };
329
+ }
330
+ else if (record.rulesetDigest !== pinned.rulesetDigest) {
331
+ disagreements += 1;
332
+ }
333
+ }
334
+ }
335
+ return pinned ? { ...pinned, disagreements } : undefined;
336
+ }
337
+ //# sourceMappingURL=envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.js","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * HTML escaping, in one place because there is more than one renderer.
3
+ *
4
+ * Both HTML artifacts — the accessibility report and the visual contact sheet —
5
+ * interpolate strings the customer's own page, config and filesystem supplied.
6
+ * Two copies of this function is two chances for one of them to be the weaker
7
+ * copy, and the weaker copy is the one an injected selector finds. The hostile
8
+ * fixture exists to prove a page full of injection attempts renders as inert
9
+ * text; that proof has to cover every sink, so every sink shares this function.
10
+ *
11
+ * `&` goes first, or the escapes escape each other's output.
12
+ */
13
+ export declare function esc(value: string): string;
14
+ //# sourceMappingURL=escape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMzC"}
package/dist/escape.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * HTML escaping, in one place because there is more than one renderer.
3
+ *
4
+ * Both HTML artifacts — the accessibility report and the visual contact sheet —
5
+ * interpolate strings the customer's own page, config and filesystem supplied.
6
+ * Two copies of this function is two chances for one of them to be the weaker
7
+ * copy, and the weaker copy is the one an injected selector finds. The hostile
8
+ * fixture exists to prove a page full of injection attempts renders as inert
9
+ * text; that proof has to cover every sink, so every sink shares this function.
10
+ *
11
+ * `&` goes first, or the escapes escape each other's output.
12
+ */
13
+ export function esc(value) {
14
+ return value
15
+ .replaceAll('&', '&amp;')
16
+ .replaceAll('<', '&lt;')
17
+ .replaceAll('>', '&gt;')
18
+ .replaceAll('"', '&quot;');
19
+ }
20
+ //# sourceMappingURL=escape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escape.js","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,GAAG,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;SACxB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,MAAM,CAAC;SACvB,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AAC/B,CAAC"}