@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
package/dist/output.js ADDED
@@ -0,0 +1,231 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { mkdir, readdir, rename, rm, stat, unlink, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ /**
5
+ * Sortable, collision-resistant, and readable at a glance in a directory listing.
6
+ *
7
+ * Derived from `generatedAt` rather than a fresh clock so the directory name and
8
+ * the envelope's own timestamp can never disagree. Colons are stripped because
9
+ * they are illegal in filenames on Windows, and a customer on Windows finding
10
+ * that we cannot write our own output would be an unforced error.
11
+ *
12
+ * The engine used to be part of this name and deliberately is not any more. A
13
+ * single Playwright run can span three engines — the compatibility matrix
14
+ * requires it — so a directory named after one of them would be wrong for every
15
+ * mixed run, and quietly right-looking. The engine belongs to each test result,
16
+ * which is the only place it is ever singular.
17
+ */
18
+ export function runDirName(generatedAt) {
19
+ return generatedAt.replace(/[:.]/g, '-').replace(/[zZ]$/, 'Z');
20
+ }
21
+ /**
22
+ * Write a file so no reader can ever observe it half-written.
23
+ *
24
+ * A plain `writeFile` is atomic against other writers of *different* files, but
25
+ * not against a reader that lists the directory the instant the file appears —
26
+ * which is exactly what a concurrent invocation's merge does. Content goes to a
27
+ * uniquely-named sibling first, then renames into place; rename is atomic on
28
+ * every filesystem we run on, so a reader sees the old bytes or the new bytes
29
+ * and never a prefix.
30
+ *
31
+ * The rename is retried because Windows refuses to replace a file another
32
+ * process holds open without FILE_SHARE_DELETE — antivirus scanners and search
33
+ * indexers do this routinely, briefly. A short backoff outlasts almost all of
34
+ * them; persistent failure propagates, because falling back to a plain
35
+ * overwrite would reintroduce the torn read this function exists to prevent.
36
+ */
37
+ export async function writeFileAtomic(path, content) {
38
+ const tmp = join(dirname(path), `.tmp-${randomBytes(6).toString('hex')}`);
39
+ await writeFile(tmp, content, 'utf8');
40
+ let lastError;
41
+ for (let attempt = 0; attempt < 5; attempt += 1) {
42
+ try {
43
+ await rename(tmp, path);
44
+ return;
45
+ }
46
+ catch (error) {
47
+ lastError = error;
48
+ await new Promise((r) => setTimeout(r, 30 * (attempt + 1)));
49
+ }
50
+ }
51
+ await unlink(tmp).catch(() => { });
52
+ throw lastError;
53
+ }
54
+ export async function writeRun(outputDir, baseDir, envelope, meta) {
55
+ // Relative output resolves against the config's directory, never the process
56
+ // working directory: the same suite run from a monorepo root and from its own
57
+ // package must write to the same place, or the artifact moves depending on
58
+ // where the developer happened to be standing.
59
+ const root = resolve(baseDir, outputDir);
60
+ const runDir = join(root, 'runs', runDirName(envelope.generatedAt));
61
+ const result = await writeRunInto(runDir, root, envelope, meta);
62
+ await pruneRuns(join(root, 'runs'), { currentName: runDirName(envelope.generatedAt) });
63
+ return result;
64
+ }
65
+ /**
66
+ * The write itself, with the destination chosen by the caller.
67
+ *
68
+ * Split from `writeRun` because a grouped run's directory is keyed by the
69
+ * orchestrator's id rather than by this invocation's clock — but what goes IN
70
+ * the directory, and how it gets there, must not differ between the two paths.
71
+ *
72
+ * `run.json` lands before `meta.json`, deliberately: the sidecar describes the
73
+ * envelope (down to a content hash, when grouped), so a reader must never find
74
+ * a sidecar whose envelope does not exist yet.
75
+ */
76
+ export async function writeRunInto(runDir, outputRoot, envelope, meta) {
77
+ await mkdir(runDir, { recursive: true });
78
+ const envelopePath = join(runDir, 'run.json');
79
+ const metaPath = join(runDir, 'meta.json');
80
+ const fullMeta = { ...meta, outputDir: outputRoot, envelopePath };
81
+ // Pretty-printed on purpose. This file is meant to be opened, read, diffed and
82
+ // kept by the person who ran the suite — it is the free layer's artifact, not
83
+ // a wire frame. Trailing newlines because it is a text file in a git repo.
84
+ await writeFileAtomic(envelopePath, `${JSON.stringify(envelope, null, 2)}\n`);
85
+ await writeFileAtomic(metaPath, `${JSON.stringify(fullMeta, null, 2)}\n`);
86
+ return { runDir, envelopePath, metaPath };
87
+ }
88
+ /**
89
+ * Only the latest run stays on disk. P1-6.4 draws the free-layer boundary at
90
+ * "no local history, ever" — and letting timestamped run directories pile up
91
+ * would have the SDK itself building the organised time-series that boundary
92
+ * forbids, run by run, on every machine and every persistent CI box.
93
+ *
94
+ * Pruning happens after the new run is fully written, never before: a crash
95
+ * mid-write costs the customer nothing, because the previous good run is still
96
+ * there. And only *older* runs go — a directory sorting after ours belongs to a
97
+ * concurrent or clock-skewed run that is not ours to delete.
98
+ *
99
+ * Best-effort on purpose. A directory we cannot remove (permissions, a file
100
+ * held open on Windows) must never fail the run that just wrote successfully.
101
+ */
102
+ const RUN_DIR_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/;
103
+ /** A run-group directory, keyed by the orchestrator's id rather than a clock. */
104
+ export const GROUP_DIR_PATTERN = /^group-[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
105
+ /**
106
+ * A group dir is prunable only by AGE, never by looking finished.
107
+ *
108
+ * "Looks complete" is not knowable from the outside: a group between two
109
+ * sequential lanes looks exactly like a finished one, and pruning it there
110
+ * would let the next lane recreate the dir empty and merge a hollow run that
111
+ * claims nothing is missing. Staleness — no file touched for a day — is the
112
+ * only signal that cannot fire on a live group.
113
+ */
114
+ const GROUP_STALE_MS = 24 * 60 * 60 * 1000;
115
+ export async function pruneRuns(runsRoot, options) {
116
+ let entries;
117
+ try {
118
+ entries = await readdir(runsRoot, { withFileTypes: true });
119
+ }
120
+ catch {
121
+ return;
122
+ }
123
+ const now = Date.now();
124
+ const doomed = [];
125
+ for (const entry of entries) {
126
+ // Only things we recognise as our own run directories. A stray file or a
127
+ // folder the customer put there themselves is theirs, not ours to prune.
128
+ if (!entry.isDirectory())
129
+ continue;
130
+ if (RUN_DIR_PATTERN.test(entry.name)) {
131
+ /*
132
+ * Timestamp dirs keep the original rule: only *older* ones go — a
133
+ * directory sorting after ours belongs to a concurrent or clock-skewed
134
+ * run that is not ours to delete. When the current run is a group, every
135
+ * timestamp dir predates the group's logical run and is prunable once
136
+ * the group has written (`currentName` carries the merge's timestamp).
137
+ */
138
+ if (options.currentName && entry.name < options.currentName)
139
+ doomed.push(entry.name);
140
+ continue;
141
+ }
142
+ if (GROUP_DIR_PATTERN.test(entry.name)) {
143
+ if (entry.name === options.currentGroupName)
144
+ continue;
145
+ if (await groupIsStale(join(runsRoot, entry.name), now))
146
+ doomed.push(entry.name);
147
+ }
148
+ }
149
+ await Promise.all(doomed.map((name) => rm(join(runsRoot, name), { recursive: true, force: true }).catch(() => { })));
150
+ }
151
+ /** Stale means nothing in the dir (or its parts) has been touched for a day. */
152
+ async function groupIsStale(groupDir, now) {
153
+ let newest = 0;
154
+ for (const dir of [groupDir, join(groupDir, 'parts')]) {
155
+ let names;
156
+ try {
157
+ names = await readdir(dir);
158
+ }
159
+ catch {
160
+ continue;
161
+ }
162
+ for (const name of names) {
163
+ try {
164
+ const s = await stat(join(dir, name));
165
+ if (s.mtimeMs > newest)
166
+ newest = s.mtimeMs;
167
+ }
168
+ catch {
169
+ // A file vanishing mid-scan means the group is live. Do not prune it.
170
+ return false;
171
+ }
172
+ }
173
+ }
174
+ // An unreadable or empty dir is not evidence of staleness; leave it alone
175
+ // until it has an observable age.
176
+ return newest > 0 && now - newest > GROUP_STALE_MS;
177
+ }
178
+ /**
179
+ * The HTML report, written beside `run.json` in the same run directory.
180
+ *
181
+ * Separate from `writeRun` rather than folded into it, because the two must be
182
+ * allowed to fail independently: a template bug must cost the customer the
183
+ * report and nothing else — never the envelope, and never the terminal
184
+ * summary. The reporter wraps this call in its own try.
185
+ */
186
+ export async function writeHtmlReport(runDir, html) {
187
+ const reportPath = join(runDir, 'report.html');
188
+ // Atomic like the JSON artifacts: a grouped run rewrites this file while a
189
+ // person may have it open, and a browser reading a half-written report would
190
+ // render something that looks finished and is not.
191
+ await writeFileAtomic(reportPath, html);
192
+ return reportPath;
193
+ }
194
+ /**
195
+ * The visual contact sheet, written beside `report.html` in the same run
196
+ * directory.
197
+ *
198
+ * Its own file and its own failure domain, for the same reason the HTML report
199
+ * has one: a template bug in the sheet must cost the customer the sheet and
200
+ * nothing else — not the envelope, not the accessibility report, not the
201
+ * terminal summary. The reporter wraps this call in its own try.
202
+ *
203
+ * The name is stable and boring on purpose. It is a path people put in a
204
+ * `.gitignore`, a CI artifact glob and a bookmark, and renaming it later would
205
+ * break all three.
206
+ */
207
+ export async function writeScreensReport(runDir, html) {
208
+ const screensPath = join(runDir, 'screens.html');
209
+ await writeFileAtomic(screensPath, html);
210
+ return screensPath;
211
+ }
212
+ /**
213
+ * The summary as markdown, at a path the caller named.
214
+ *
215
+ * Directed by `SETSQUARE_SUMMARY_MD` and written nowhere by default, because this
216
+ * is not one of the run's artefacts — it is a second rendering of the terminal
217
+ * summary for a CI job-summary panel, and a file that appears in every
218
+ * `.setsquare/runs/` directory whether anyone asked for it or not would be one
219
+ * more thing to explain and one more thing to gitignore.
220
+ *
221
+ * `resolve` against the base directory rather than trusting the string, and its own
222
+ * failure domain like every other optional artefact here: a summary that could not
223
+ * be written must cost the summary and nothing else.
224
+ */
225
+ export async function writeSummaryMarkdown(baseDir, path, markdown) {
226
+ const full = resolve(baseDir, path);
227
+ await mkdir(dirname(full), { recursive: true });
228
+ await writeFileAtomic(full, markdown.endsWith('\n') ? markdown : `${markdown}\n`);
229
+ return full;
230
+ }
231
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"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"}
package/dist/perf.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { Perf } from '@setsquare/schema';
2
+ import type { Page } from '@playwright/test';
3
+ /**
4
+ * Capture performance for the page a scan is looking at.
5
+ *
6
+ * Returns `undefined` rather than an empty object when nothing could be measured,
7
+ * so the schema's optional block stays genuinely absent instead of arriving as a
8
+ * shell that implies a measurement took place.
9
+ */
10
+ export declare function capturePerf(page: Page): Promise<Perf | undefined>;
11
+ //# sourceMappingURL=perf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perf.d.ts","sourceRoot":"","sources":["../src/perf.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAoI7C;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC,CAmBvE"}
package/dist/perf.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Pillar 3's capture block.
3
+ *
4
+ * These are CI-LAB MEASUREMENTS, not RUM. They are trend-comparable to
5
+ * themselves over time on the same runner, and they are never comparable across
6
+ * engines — which is why the engine is structurally attached (a scan resolves to
7
+ * a test result through `testId`, and that carries the engine) rather than being
8
+ * a field that could be dropped or averaged over.
9
+ *
10
+ * Nothing displays these in P1. They are captured now because the fixture is
11
+ * already on the page at scan time so the marginal cost is near zero, and because
12
+ * history not captured is history permanently lost — the same reasoning that put
13
+ * pass rows in from the first release.
14
+ *
15
+ * The governing rule here is stronger than usual: **a number that is absent is
16
+ * better than a number that is wrong.** A perf block is optional in the schema
17
+ * precisely so an engine that cannot measure something says nothing about it.
18
+ */
19
+ /**
20
+ * Read timings from the page.
21
+ *
22
+ * Two capability traps discovered by measuring rather than reading docs, both of
23
+ * which produce fabricated evidence if you get them wrong:
24
+ *
25
+ * 1. `performance.getEntriesByType('largest-contentful-paint')` returns NOTHING,
26
+ * in every engine. LCP entries are only ever delivered to a
27
+ * `PerformanceObserver`. Reading the buffer looks like it works and silently
28
+ * yields no LCP forever.
29
+ *
30
+ * 2. `observer.observe({ type })` does NOT throw for a type the engine does not
31
+ * support — it simply never fires. So a naive CLS sum over zero entries
32
+ * returns `0`, and `0` is indistinguishable from a genuinely perfect layout.
33
+ * Firefox and WebKit do not implement `layout-shift`; both would have
34
+ * reported flawless CLS on every page forever.
35
+ *
36
+ * `PerformanceObserver.supportedEntryTypes` is the reliable gate, and it is
37
+ * honest in both directions: it correctly reports `layout-shift` as unsupported
38
+ * in Firefox and WebKit, and `largest-contentful-paint` as supported in all
39
+ * three — which measurement confirms.
40
+ */
41
+ const CAPTURE_SCRIPT = async (settleMs) => {
42
+ const supported = (type) => {
43
+ try {
44
+ return (PerformanceObserver.supportedEntryTypes ?? []).includes(type);
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ };
50
+ const navigation = performance.getEntriesByType('navigation')[0];
51
+ if (!navigation)
52
+ return null;
53
+ /*
54
+ * The navigation entry describes the DOCUMENT that was loaded, and it does not
55
+ * change when a single-page app pushes a new route. Measured: after a
56
+ * `pushState` to `/spa/one`, the entry is still named `/spa`.
57
+ *
58
+ * So if the entry's name no longer matches where we are, these timings belong
59
+ * to a different route and attributing them here would invent a page load that
60
+ * never happened. Reported as "no perf" instead, which is true.
61
+ */
62
+ if (navigation.name !== location.href)
63
+ return null;
64
+ const out = {};
65
+ const finite = (value, key) => {
66
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
67
+ out[key] = value;
68
+ };
69
+ finite(navigation.responseStart, 'ttfbMs');
70
+ finite(navigation.domContentLoadedEventEnd, 'domContentLoadedMs');
71
+ finite(navigation.loadEventEnd, 'loadMs');
72
+ const observe = (type, onEntry) => new Promise((resolve) => {
73
+ if (!supported(type))
74
+ return resolve();
75
+ let observer;
76
+ try {
77
+ observer = new PerformanceObserver((list) => {
78
+ for (const entry of list.getEntries())
79
+ onEntry(entry);
80
+ });
81
+ // `buffered` replays entries recorded before this observer existed, which
82
+ // is the only way to see an LCP that happened during page load.
83
+ observer.observe({ type, buffered: true });
84
+ }
85
+ catch {
86
+ return resolve();
87
+ }
88
+ setTimeout(() => {
89
+ try {
90
+ observer?.disconnect();
91
+ }
92
+ catch {
93
+ // Already gone. Nothing to do.
94
+ }
95
+ resolve();
96
+ }, settleMs);
97
+ });
98
+ let lcp;
99
+ await observe('largest-contentful-paint', (entry) => {
100
+ // The LAST candidate wins: LCP is redefined as larger elements render.
101
+ lcp = entry.startTime;
102
+ });
103
+ finite(lcp, 'lcpMs');
104
+ if (supported('layout-shift')) {
105
+ let cls = 0;
106
+ await observe('layout-shift', (entry) => {
107
+ const shift = entry;
108
+ // Shifts within 500ms of user input are excluded by the CLS definition —
109
+ // a layout change the user asked for is not a layout instability.
110
+ if (!shift.hadRecentInput && typeof shift.value === 'number')
111
+ cls += shift.value;
112
+ });
113
+ // Zero is a REAL measurement here, and only here: the engine supports the
114
+ // entry type, so "no shifts" is a finding rather than a silence.
115
+ out['cls'] = cls;
116
+ }
117
+ return out;
118
+ };
119
+ /** How long to wait for buffered observer entries. Long enough; not long enough to notice. */
120
+ const SETTLE_MS = 60;
121
+ /**
122
+ * Capture performance for the page a scan is looking at.
123
+ *
124
+ * Returns `undefined` rather than an empty object when nothing could be measured,
125
+ * so the schema's optional block stays genuinely absent instead of arriving as a
126
+ * shell that implies a measurement took place.
127
+ */
128
+ export async function capturePerf(page) {
129
+ try {
130
+ const raw = await page.evaluate(CAPTURE_SCRIPT, SETTLE_MS);
131
+ if (!raw || Object.keys(raw).length === 0)
132
+ return undefined;
133
+ // Round to whole milliseconds; sub-millisecond precision on a CI runner is
134
+ // noise dressed as detail. CLS is unitless and keeps three decimals.
135
+ const perf = {};
136
+ for (const [key, value] of Object.entries(raw)) {
137
+ if (key === 'cls')
138
+ perf.cls = Math.round(value * 1000) / 1000;
139
+ else
140
+ perf[key] = Math.round(value);
141
+ }
142
+ return perf;
143
+ }
144
+ catch {
145
+ // A closed page, a navigation mid-read, a CSP that blocked evaluation. None
146
+ // of it is worth a warning, and none of it justifies inventing a number.
147
+ return undefined;
148
+ }
149
+ }
150
+ //# sourceMappingURL=perf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perf.js","sourceRoot":"","sources":["../src/perf.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import type { Page } from '@playwright/test';
2
+ import type { AxeFinding } from '@setsquare/schema';
3
+ /**
4
+ * WCAG 3.1.5 Reading Level (AAA), as an honest heuristic.
5
+ *
6
+ * The SC's normative bar is text readable "without lower secondary education"
7
+ * — ISCED level 2, roughly nine years of schooling — measured *after removing
8
+ * proper names and titles*, and satisfiable by supplemental content or a
9
+ * simpler alternate version. No formula measures any of that. What formulas
10
+ * measure is word and sentence statistics, which approximate a reading grade,
11
+ * which approximates the bar. Three honesty rules follow:
12
+ *
13
+ * 1. **Never a violation.** The finding is always `incomplete` — a person
14
+ * decides 3.1.5, this check only tells them where to look.
15
+ * 2. **Fire only above a margin.** Automated scoring cannot remove proper
16
+ * names, which skews grades HIGH, so the finding fires a full grade above
17
+ * the target (FK > 10 against a target of 9; LIX > 48 against 44). A page
18
+ * at 9.4 is not a finding; a page at 11 is worth a person's time.
19
+ * 3. **Language-gated.** Flesch–Kincaid is calibrated for English; LIX is
20
+ * the conventional measure for the Nordic languages (thresholds are
21
+ * convention, not W3C-blessed: <35 easy, 45+ difficult, "lower
22
+ * secondary" ≈ 44). This is the same routing Sa11y documents publicly —
23
+ * the behaviour, never its GPL code. An unsupported or missing `lang`
24
+ * skips silently: missing `lang` is already an axe violation (3.1.1),
25
+ * and scoring text in a language the formula was never calibrated for
26
+ * would manufacture a number.
27
+ *
28
+ * Extraction happens in-page (main content only — scoring navigation chrome
29
+ * would grade the menu, not the writing); scoring happens Node-side in the
30
+ * worker, which keeps text-readability out of the injected bundle and the
31
+ * page's text out of everything: only the scores and the language ever leave
32
+ * the worker.
33
+ */
34
+ /** The one node the page-level finding points at. */
35
+ export declare const READING_LEVEL_RULE_ID = "setsquare-reading-level";
36
+ /** Versioned for the ruleset digest: bump when scoring or thresholds change. */
37
+ export declare const READING_LEVEL_CHECK_VERSION = "setsquare-reading-level@1";
38
+ export interface ExtractedText {
39
+ lang: string;
40
+ text: string;
41
+ }
42
+ export interface ReadingLevelScore {
43
+ lang: string;
44
+ formula: 'flesch-kincaid' | 'lix';
45
+ score: number;
46
+ target: number;
47
+ words: number;
48
+ flagged: boolean;
49
+ }
50
+ /**
51
+ * Main-content text and the document language, from the live page.
52
+ *
53
+ * `<main>`/`role=main` when the page declares one; otherwise the body minus
54
+ * nav/header/footer/aside, which is the same "grade the writing, not the
55
+ * chrome" heuristic every readability tool uses. Returns undefined on any
56
+ * failure — a page that cannot be read is a page that is not scored.
57
+ */
58
+ export declare function extractMainText(page: Page): Promise<ExtractedText | undefined>;
59
+ export declare function scoreReadingLevel(extracted: ExtractedText): ReadingLevelScore | undefined;
60
+ /**
61
+ * The heuristic finding, only when the score clears the margin.
62
+ *
63
+ * Page-level, so `targets` names one node — the page — with no selectors. A
64
+ * `nodeCount` of 0 would make a surface failing only this check render as
65
+ * clean, which is the false pass this product exists to avoid.
66
+ */
67
+ export declare function readingLevelFinding(score: ReadingLevelScore): AxeFinding | undefined;
68
+ /**
69
+ * The whole check: extract, gate, score, and return a finding or nothing.
70
+ * Never throws — a readability heuristic must not be able to cost a scan.
71
+ */
72
+ export declare function runReadingLevel(page: Page): Promise<AxeFinding | undefined>;
73
+ //# sourceMappingURL=reading-level.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reading-level.d.ts","sourceRoot":"","sources":["../src/reading-level.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAMpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAC/D,gFAAgF;AAChF,eAAO,MAAM,2BAA2B,8BAA+B,CAAC;AAaxE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,gBAAgB,GAAG,KAAK,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAsBpF;AAmBD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,aAAa,GAAG,iBAAiB,GAAG,SAAS,CAoCzF;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,iBAAiB,GAAG,UAAU,GAAG,SAAS,CAsCpF;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAYjF"}