testaro 78.0.8 → 78.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/.gitattributes +94 -0
  2. package/.github/workflows/ci.yml +58 -0
  3. package/.github/workflows/publish.yml +50 -0
  4. package/.github/workflows/typescript.yml +35 -0
  5. package/AGENTS.md +2 -2
  6. package/CLAUDE.md +24 -18
  7. package/CONTAINERS.md +9 -6
  8. package/CONTRIBUTING.md +29 -3
  9. package/Dockerfile +1 -1
  10. package/README.md +143 -19
  11. package/UPGRADES.md +4 -0
  12. package/actSpecs-doc.md +13 -1
  13. package/actSpecs.js +29 -5
  14. package/call.js +6 -5
  15. package/dist/nameComputation.js +1 -0
  16. package/docker-compose.yml +8 -2
  17. package/docs/checkpoint-scanning.md +199 -0
  18. package/docs/standard-result-outcome.md +165 -0
  19. package/env.example +49 -10
  20. package/eslint.config.mjs +101 -0
  21. package/netWatch.js +372 -222
  22. package/package.json +46 -31
  23. package/pour/README.md +47 -0
  24. package/pour/pour.min.js +8 -0
  25. package/procs/actDo.js +808 -0
  26. package/procs/catalog.d.ts +17 -0
  27. package/procs/catalog.js +312 -221
  28. package/procs/catalog.ts +398 -0
  29. package/procs/checkpoint.js +110 -0
  30. package/procs/config.d.ts +41 -0
  31. package/procs/config.js +98 -0
  32. package/procs/dateTime.js +2 -1
  33. package/procs/doActs.js +442 -849
  34. package/procs/doTestAct.js +27 -121
  35. package/procs/flow.js +221 -0
  36. package/procs/generateRuleRegistry.js +94 -0
  37. package/procs/getSource.d.ts +26 -0
  38. package/procs/job.js +115 -1
  39. package/procs/launch.d.ts +40 -0
  40. package/procs/launch.js +243 -108
  41. package/procs/nu.d.ts +48 -0
  42. package/procs/scope.js +196 -0
  43. package/procs/shoot.d.ts +33 -0
  44. package/procs/standard.d.ts +18 -0
  45. package/procs/standard.js +83 -0
  46. package/procs/standard.ts +126 -0
  47. package/procs/testAct.js +145 -0
  48. package/procs/testaro.d.ts +20 -0
  49. package/procs/testaro.js +236 -189
  50. package/procs/testaro.ts +321 -0
  51. package/procs/userPath.js +126 -0
  52. package/procs/xPath.d.ts +4 -0
  53. package/procs/xPath.js +81 -69
  54. package/procs/xPath.ts +106 -0
  55. package/procs/xPathScript.d.ts +6 -0
  56. package/procs/xPathScript.js +70 -0
  57. package/run.js +3 -2
  58. package/surea11y/README.md +44 -0
  59. package/surea11y/surea11y.browser.js +14 -0
  60. package/testaro/adbID.d.ts +3 -0
  61. package/testaro/adbID.js +38 -41
  62. package/testaro/adbID.ts +66 -0
  63. package/testaro/allCapStyle.d.ts +3 -0
  64. package/testaro/allCapStyle.js +31 -34
  65. package/testaro/allCapStyle.ts +55 -0
  66. package/testaro/allCaps.d.ts +16 -0
  67. package/testaro/allCaps.js +179 -151
  68. package/testaro/allCaps.ts +228 -0
  69. package/testaro/allHidden.d.ts +13 -0
  70. package/testaro/allHidden.js +29 -30
  71. package/testaro/allHidden.ts +50 -0
  72. package/testaro/allSlanted.d.ts +3 -0
  73. package/testaro/allSlanted.js +30 -33
  74. package/testaro/allSlanted.ts +54 -0
  75. package/testaro/altScheme.d.ts +3 -0
  76. package/testaro/altScheme.js +27 -30
  77. package/testaro/altScheme.ts +50 -0
  78. package/testaro/attVal.d.ts +3 -0
  79. package/testaro/attVal.js +20 -21
  80. package/testaro/attVal.ts +52 -0
  81. package/testaro/autocomplete.d.ts +3 -0
  82. package/testaro/autocomplete.js +59 -75
  83. package/testaro/autocomplete.ts +101 -0
  84. package/testaro/bulk.d.ts +13 -0
  85. package/testaro/bulk.js +32 -33
  86. package/testaro/bulk.ts +55 -0
  87. package/testaro/buttonMenu.d.ts +9 -0
  88. package/testaro/buttonMenu.js +317 -319
  89. package/testaro/buttonMenu.ts +391 -0
  90. package/testaro/captionLoc.d.ts +3 -0
  91. package/testaro/captionLoc.js +17 -20
  92. package/testaro/captionLoc.ts +40 -0
  93. package/testaro/datalistRef.d.ts +3 -0
  94. package/testaro/datalistRef.js +33 -36
  95. package/testaro/datalistRef.ts +55 -0
  96. package/testaro/distortion.d.ts +3 -0
  97. package/testaro/distortion.js +57 -26
  98. package/testaro/distortion.ts +81 -0
  99. package/testaro/docType.d.ts +15 -0
  100. package/testaro/docType.js +25 -25
  101. package/testaro/docType.ts +45 -0
  102. package/testaro/dupAtt.d.ts +16 -0
  103. package/testaro/dupAtt.js +113 -104
  104. package/testaro/dupAtt.ts +144 -0
  105. package/testaro/elements.d.ts +6 -0
  106. package/testaro/elements.js +153 -153
  107. package/testaro/elements.ts +215 -0
  108. package/testaro/embAc.d.ts +3 -0
  109. package/testaro/embAc.js +19 -20
  110. package/testaro/embAc.ts +40 -0
  111. package/testaro/focAll.d.ts +13 -0
  112. package/testaro/focAll.js +185 -191
  113. package/testaro/focAll.ts +217 -0
  114. package/testaro/focAndOp.d.ts +3 -0
  115. package/testaro/focAndOp.js +100 -104
  116. package/testaro/focAndOp.ts +128 -0
  117. package/testaro/focInd.d.ts +3 -0
  118. package/testaro/focInd.js +64 -66
  119. package/testaro/focInd.ts +96 -0
  120. package/testaro/focVis.d.ts +3 -0
  121. package/testaro/focVis.js +28 -30
  122. package/testaro/focVis.ts +52 -0
  123. package/testaro/headEl.d.ts +10 -0
  124. package/testaro/headEl.js +61 -62
  125. package/testaro/headEl.ts +82 -0
  126. package/testaro/headingAmb.d.ts +3 -0
  127. package/testaro/headingAmb.js +54 -63
  128. package/testaro/headingAmb.ts +72 -0
  129. package/testaro/hovInd.d.ts +12 -0
  130. package/testaro/hovInd.js +163 -130
  131. package/testaro/hovInd.ts +199 -0
  132. package/testaro/hover.d.ts +3 -0
  133. package/testaro/hover.js +155 -126
  134. package/testaro/hover.ts +154 -0
  135. package/testaro/hr.d.ts +3 -0
  136. package/testaro/hr.js +14 -17
  137. package/testaro/hr.ts +36 -0
  138. package/testaro/imageLink.d.ts +3 -0
  139. package/testaro/imageLink.js +17 -20
  140. package/testaro/imageLink.ts +42 -0
  141. package/testaro/labClash.d.ts +3 -0
  142. package/testaro/labClash.js +31 -33
  143. package/testaro/labClash.ts +54 -0
  144. package/testaro/legendLoc.d.ts +3 -0
  145. package/testaro/legendLoc.js +17 -20
  146. package/testaro/legendLoc.ts +42 -0
  147. package/testaro/lineHeight.d.ts +3 -0
  148. package/testaro/lineHeight.js +43 -48
  149. package/testaro/lineHeight.ts +70 -0
  150. package/testaro/linkAmb.d.ts +7 -0
  151. package/testaro/linkAmb.js +79 -80
  152. package/testaro/linkAmb.ts +105 -0
  153. package/testaro/linkExt.d.ts +3 -0
  154. package/testaro/linkExt.js +13 -16
  155. package/testaro/linkExt.ts +35 -0
  156. package/testaro/linkOldAtt.d.ts +3 -0
  157. package/testaro/linkOldAtt.js +25 -28
  158. package/testaro/linkOldAtt.ts +48 -0
  159. package/testaro/linkTo.d.ts +3 -0
  160. package/testaro/linkTo.js +22 -22
  161. package/testaro/linkTo.ts +43 -0
  162. package/testaro/linkUl.d.ts +3 -0
  163. package/testaro/linkUl.js +30 -34
  164. package/testaro/linkUl.ts +54 -0
  165. package/testaro/miniText.d.ts +3 -0
  166. package/testaro/miniText.js +41 -44
  167. package/testaro/miniText.ts +68 -0
  168. package/testaro/motion.d.ts +10 -0
  169. package/testaro/motion.js +92 -96
  170. package/testaro/motion.ts +125 -0
  171. package/testaro/nonTable.d.ts +3 -0
  172. package/testaro/nonTable.js +39 -45
  173. package/testaro/nonTable.ts +66 -0
  174. package/testaro/optRoleSel.d.ts +3 -0
  175. package/testaro/optRoleSel.js +16 -19
  176. package/testaro/optRoleSel.ts +41 -0
  177. package/testaro/phOnly.d.ts +3 -0
  178. package/testaro/phOnly.js +18 -21
  179. package/testaro/phOnly.ts +43 -0
  180. package/testaro/pseudoP.d.ts +3 -0
  181. package/testaro/pseudoP.js +36 -38
  182. package/testaro/pseudoP.ts +59 -0
  183. package/testaro/radioSet.d.ts +3 -0
  184. package/testaro/radioSet.js +57 -59
  185. package/testaro/radioSet.ts +79 -0
  186. package/testaro/registry.d.ts +63 -0
  187. package/testaro/registry.js +67 -0
  188. package/testaro/registry.ts +141 -0
  189. package/testaro/role.d.ts +3 -0
  190. package/testaro/role.js +27 -29
  191. package/testaro/role.ts +53 -0
  192. package/testaro/secHeading.d.ts +3 -0
  193. package/testaro/secHeading.js +30 -33
  194. package/testaro/secHeading.ts +53 -0
  195. package/testaro/styleDiff.d.ts +25 -0
  196. package/testaro/styleDiff.js +248 -252
  197. package/testaro/styleDiff.ts +303 -0
  198. package/testaro/tabNav.d.ts +33 -0
  199. package/testaro/tabNav.js +272 -342
  200. package/testaro/tabNav.ts +454 -0
  201. package/testaro/targetsNear.d.ts +9 -0
  202. package/testaro/targetsNear.js +130 -132
  203. package/testaro/targetsNear.ts +160 -0
  204. package/testaro/textNodes.d.ts +6 -0
  205. package/testaro/textNodes.js +139 -135
  206. package/testaro/textNodes.ts +185 -0
  207. package/testaro/textSem.d.ts +3 -0
  208. package/testaro/textSem.js +25 -28
  209. package/testaro/textSem.ts +47 -0
  210. package/testaro/title.d.ts +9 -0
  211. package/testaro/title.js +16 -13
  212. package/testaro/title.ts +31 -0
  213. package/testaro/titledEl.d.ts +3 -0
  214. package/testaro/titledEl.js +15 -18
  215. package/testaro/titledEl.ts +38 -0
  216. package/testaro/zIndex.d.ts +3 -0
  217. package/testaro/zIndex.js +19 -22
  218. package/testaro/zIndex.ts +42 -0
  219. package/tests/alfa.d.ts +45 -0
  220. package/tests/alfa.js +138 -141
  221. package/tests/alfa.ts +214 -0
  222. package/tests/aslint.d.ts +33 -0
  223. package/tests/aslint.js +272 -249
  224. package/tests/aslint.ts +301 -0
  225. package/tests/axe.d.ts +27 -0
  226. package/tests/axe.js +199 -200
  227. package/tests/axe.ts +277 -0
  228. package/tests/ed11y.d.ts +28 -0
  229. package/tests/ed11y.js +141 -99
  230. package/tests/ed11y.ts +178 -0
  231. package/tests/htmlcs.d.ts +21 -0
  232. package/tests/htmlcs.js +174 -140
  233. package/tests/htmlcs.ts +181 -0
  234. package/tests/ibm.d.ts +57 -0
  235. package/tests/ibm.js +182 -175
  236. package/tests/ibm.ts +287 -0
  237. package/tests/nuVal.d.ts +13 -0
  238. package/tests/nuVal.js +107 -112
  239. package/tests/nuVal.ts +145 -0
  240. package/tests/nuVnu.d.ts +15 -0
  241. package/tests/nuVnu.js +141 -111
  242. package/tests/nuVnu.ts +144 -0
  243. package/tests/pour.d.ts +31 -0
  244. package/tests/pour.js +242 -0
  245. package/tests/pour.ts +273 -0
  246. package/tests/qualWeb.d.ts +39 -0
  247. package/tests/qualWeb.js +262 -273
  248. package/tests/qualWeb.ts +344 -0
  249. package/tests/surea11y.d.ts +33 -0
  250. package/tests/surea11y.js +288 -0
  251. package/tests/surea11y.ts +334 -0
  252. package/tests/testaro.d.ts +25 -0
  253. package/tests/testaro.js +745 -652
  254. package/tests/testaro.ts +862 -0
  255. package/tests/wave.d.ts +46 -0
  256. package/tests/wave.js +166 -177
  257. package/tests/wave.ts +252 -0
  258. package/tsconfig.json +17 -0
  259. package/types.d.ts +243 -0
  260. package/types.js +10 -0
  261. package/types.ts +376 -0
  262. package/validation/act/README.md +46 -0
  263. package/validation/act/capture.js +877 -0
  264. package/validation/act/chromium-issue-draft.md +66 -0
  265. package/validation/act/fp-triage-2026-08-22.md +86 -0
  266. package/validation/act/isolation-notes.md +159 -0
  267. package/validation/act/playwright-issue-draft.md +73 -0
  268. package/validation/act/propose-mappings.js +0 -0
  269. package/validation/act/repro-cdp-raw.js +105 -0
  270. package/validation/act/repro-metarefresh.js +65 -0
  271. package/validation/act/score.js +184 -0
  272. package/validation/act/stage3a-stress-report.md +69 -0
  273. package/validation/act/stage3b-mapping-proposals.md +94 -0
  274. package/validation/act/stage3b-triage-draft.md +138 -0
  275. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  276. package/validation/executors/netWatch.js +180 -90
  277. package/validation/executors/test.js +17 -2
  278. package/validation/executors/tests.js +118 -10
  279. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  280. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  281. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  282. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  283. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  284. package/validation/knownFailures.json +1 -0
  285. package/validation/tests/jobProperties/adbID.json +27 -2
  286. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  287. package/validation/tests/jobProperties/allCaps.json +25 -0
  288. package/validation/tests/jobProperties/allHidden.json +133 -13
  289. package/validation/tests/jobProperties/allSlanted.json +3 -3
  290. package/validation/tests/jobProperties/altScheme.json +23 -8
  291. package/validation/tests/jobProperties/attVal.json +57 -57
  292. package/validation/tests/jobProperties/autocomplete.json +11 -1
  293. package/validation/tests/jobProperties/bulk.json +6 -1
  294. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  295. package/validation/tests/jobProperties/captionLoc.json +1 -6
  296. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  297. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  298. package/validation/tests/jobProperties/checkpoint.json +395 -0
  299. package/validation/tests/jobProperties/datalistRef.json +20 -5
  300. package/validation/tests/jobProperties/distortion.json +28 -3
  301. package/validation/tests/jobProperties/docType.json +2 -2
  302. package/validation/tests/jobProperties/dupAtt.json +48 -33
  303. package/validation/tests/jobProperties/elements.json +28 -28
  304. package/validation/tests/jobProperties/embAc.json +36 -31
  305. package/validation/tests/jobProperties/focAndOp.json +284 -0
  306. package/validation/tests/jobProperties/focInd.json +34 -34
  307. package/validation/tests/jobProperties/focVis.json +1 -1
  308. package/validation/tests/jobProperties/hover.json +39 -37
  309. package/validation/tests/jobProperties/hr.json +13 -3
  310. package/validation/tests/jobProperties/imageLink.json +0 -5
  311. package/validation/tests/jobProperties/labClash.json +68 -33
  312. package/validation/tests/jobProperties/legendLoc.json +1 -6
  313. package/validation/tests/jobProperties/lineHeight.json +2 -2
  314. package/validation/tests/jobProperties/linkAmb.json +13 -18
  315. package/validation/tests/jobProperties/linkExt.json +1 -1
  316. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  317. package/validation/tests/jobProperties/linkTo.json +1 -1
  318. package/validation/tests/jobProperties/linkUl.json +65 -65
  319. package/validation/tests/jobProperties/miniText.json +7 -2
  320. package/validation/tests/jobProperties/motion.json +4 -50
  321. package/validation/tests/jobProperties/nonTable.json +48 -3
  322. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  323. package/validation/tests/jobProperties/phOnly.json +6 -16
  324. package/validation/tests/jobProperties/pseudoP.json +17 -2
  325. package/validation/tests/jobProperties/radioSet.json +30 -30
  326. package/validation/tests/jobProperties/role.json +25 -5
  327. package/validation/tests/jobProperties/secHeading.json +26 -21
  328. package/validation/tests/jobProperties/styleDiff.json +35 -5
  329. package/validation/tests/jobProperties/tabNav.json +3 -1
  330. package/validation/tests/jobProperties/targetsNear.json +237 -0
  331. package/validation/tests/jobProperties/textNodes.json +29 -29
  332. package/validation/tests/jobProperties/textSem.json +26 -1
  333. package/validation/tests/jobProperties/title.json +12 -2
  334. package/validation/tests/jobProperties/titledEl.json +39 -9
  335. package/validation/tests/jobProperties/userPath.json +317 -0
  336. package/validation/tests/jobProperties/zIndex.json +40 -35
  337. package/validation/tests/targets/allCapStyle/index.html +26 -0
  338. package/validation/tests/targets/checkpoint/index.html +38 -0
  339. package/validation/tests/targets/datalistRef/index.html +1 -1
  340. package/validation/tests/targets/focAndOp/bad.html +29 -0
  341. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  342. package/validation/tests/targets/focInd/bad.html +2 -1
  343. package/validation/tests/targets/headEl/index.html +10 -1
  344. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  345. package/validation/tests/targets/userPath/index.html +42 -0
  346. package/validation/validateTest.js +61 -9
  347. package/.claude/settings.local.json +0 -11
  348. package/.eslintrc.json +0 -41
  349. package/htmlcs/.eslintrc.json +0 -67
  350. package/validation/tests/jobProperties/linkTitle.json +0 -127
  351. package/validation/tests/jobProperties/opFoc.json +0 -164
  352. package/validation/tests/jobProperties/targetSmall.json +0 -152
  353. package/validation/tests/jobProperties/targetTiny.json +0 -142
  354. package/validation/tests/targets/focOp/bad.html +0 -25
  355. package/validation/tests/targets/linkTitle/index.html +0 -24
  356. package/validation/tests/targets/opFoc/bad.html +0 -26
  357. package/validation/tests/targets/opFoc/good.html +0 -23
@@ -0,0 +1,184 @@
1
+ /*
2
+ © 2026 Jeff Witt.
3
+
4
+ Licensed under the MIT License. See LICENSE file at the project root or
5
+ https://opensource.org/license/mit/ for details.
6
+
7
+ SPDX-License-Identifier: MIT
8
+ */
9
+
10
+ /*
11
+ score.js
12
+ Track-A scoring: computes a per-engine, per-ACT-rule confusion matrix from a
13
+ capture JSONL (capture.js output).
14
+
15
+ Usage:
16
+ node validation/act/score.js --in results/act-....jsonl [--band asserted|review|both]
17
+ [--json out.json]
18
+
19
+ Scoring policy (criterion-level comparability layer, v1):
20
+ - An ACT rule's positive criteria are its forConformance WCAG 2.x success
21
+ criteria from the testcase feed. Rules with none (technique/ARIA-only
22
+ rules) are reported separately as unscoreable.
23
+ - An engine FLAGS a testcase when it reports ≥1 finding on any of the rule's
24
+ criteria. Band `asserted` (default) counts definite failures only (standard
25
+ instance outcome `failed`); `review` counts engine-flagged uncertainty
26
+ (outcome `cantTell`); `both` counts either.
27
+ - failed testcases are the positive class; passed + inapplicable are the
28
+ negative class. Sensitivity = TP/(TP+FN); specificity = TN/(TN+FP).
29
+ Known generosities/strictnesses, stated wherever results publish: a finding
30
+ on the right criterion from an unrelated check still counts (generous); a
31
+ real finding reported under a different criterion does not (strict).
32
+ */
33
+
34
+ // IMPORTS
35
+
36
+ const fs = require('fs');
37
+ const path = require('path');
38
+
39
+ // FUNCTIONS
40
+
41
+ const parseArgs = argv => {
42
+ const args = {};
43
+ for (let i = 2; i < argv.length; i += 2) {
44
+ args[argv[i].replace(/^--/, '')] = argv[i + 1];
45
+ }
46
+ return args;
47
+ };
48
+
49
+ const percent = (numerator, denominator) =>
50
+ denominator ? `${(100 * numerator / denominator).toFixed(1)}%` : '—';
51
+
52
+ // OPERATION
53
+
54
+ const args = parseArgs(process.argv);
55
+ if (! args.in) {
56
+ console.log('Usage: node validation/act/score.js --in <capture.jsonl> [--band asserted|review|both]');
57
+ process.exit(1);
58
+ }
59
+ const band = args.band || 'asserted';
60
+ const rows = fs.readFileSync(args.in, 'utf8')
61
+ .split('\n')
62
+ .filter(Boolean)
63
+ .map(line => JSON.parse(line));
64
+
65
+ // ACT-rule criterion sets from the cached feed.
66
+ const feed = JSON.parse(
67
+ fs.readFileSync(path.join(__dirname, 'cache', 'testcases.json'), 'utf8')
68
+ );
69
+ const ruleCriteria = {};
70
+ const ruleNames = {};
71
+ feed.testcases.forEach(testcase => {
72
+ ruleNames[testcase.ruleId] = testcase.ruleName;
73
+ if (! ruleCriteria[testcase.ruleId]) {
74
+ const criteria = Object.entries(testcase.ruleAccessibilityRequirements || {})
75
+ .filter(([key, value]) => /^wcag2\d:/.test(key) && value && value.forConformance)
76
+ .map(([key]) => key.split(':')[1]);
77
+ ruleCriteria[testcase.ruleId] = new Set(criteria);
78
+ }
79
+ });
80
+
81
+ // Whether a capture row flags its testcase under the scoring policy. Rows
82
+ // with exact ACT-rule maps (actAsserted/actReview — qualWeb) are scored on
83
+ // those; rows scored at the exact level bypass the unscoreable-criteria gate
84
+ // since the mapping needs no WCAG SC bridge.
85
+ const flags = row => {
86
+ if (row.actAsserted || row.actReview) {
87
+ const buckets = [];
88
+ if (band === 'asserted' || band === 'both') {
89
+ buckets.push(row.actAsserted || {});
90
+ }
91
+ if (band === 'review' || band === 'both') {
92
+ buckets.push(row.actReview || {});
93
+ }
94
+ return buckets.some(bucket => !! bucket[row.ruleId]);
95
+ }
96
+ const criteria = ruleCriteria[row.ruleId];
97
+ if (! criteria || ! criteria.size) {
98
+ return null;
99
+ }
100
+ const buckets = [];
101
+ if (band === 'asserted' || band === 'both') {
102
+ buckets.push(row.asserted || {});
103
+ }
104
+ if (band === 'review' || band === 'both') {
105
+ buckets.push(row.review || {});
106
+ }
107
+ return buckets.some(
108
+ bucket => Object.entries(bucket).some(([criterion, count]) => count && criteria.has(criterion))
109
+ );
110
+ };
111
+
112
+ // Tally per engine × rule.
113
+ const tallies = {};
114
+ const unscoreable = new Set();
115
+ const errored = {};
116
+ rows.forEach(row => {
117
+ if (row.prevented) {
118
+ errored[row.engine] = (errored[row.engine] || 0) + 1;
119
+ return;
120
+ }
121
+ const flagged = flags(row);
122
+ if (flagged === null) {
123
+ unscoreable.add(row.ruleId);
124
+ return;
125
+ }
126
+ tallies[row.engine] ??= {};
127
+ const tally = tallies[row.engine][row.ruleId] ??= {TP: 0, FP: 0, TN: 0, FN: 0};
128
+ if (row.expected === 'failed') {
129
+ tally[flagged ? 'TP' : 'FN']++;
130
+ }
131
+ else {
132
+ tally[flagged ? 'FP' : 'TN']++;
133
+ }
134
+ });
135
+
136
+ // Report.
137
+ const report = {band, engines: {}};
138
+ Object.entries(tallies).forEach(([engine, ruleTallies]) => {
139
+ console.log(`\n## ${engine} (band: ${band})\n`);
140
+ console.log('| ACT rule | Name | TP | FN | FP | TN | Sens | Spec |');
141
+ console.log('| --- | --- | --- | --- | --- | --- | --- | --- |');
142
+ const totals = {TP: 0, FP: 0, TN: 0, FN: 0};
143
+ const perRule = {};
144
+ Object.entries(ruleTallies)
145
+ .sort(([, a], [, b]) => (b.TP + b.FN) - (a.TP + a.FN))
146
+ .forEach(([ruleId, tally]) => {
147
+ ['TP', 'FP', 'TN', 'FN'].forEach(key => {
148
+ totals[key] += tally[key];
149
+ });
150
+ perRule[ruleId] = {
151
+ ...tally,
152
+ sensitivity: tally.TP + tally.FN ? tally.TP / (tally.TP + tally.FN) : null,
153
+ specificity: tally.TN + tally.FP ? tally.TN / (tally.TN + tally.FP) : null
154
+ };
155
+ // Rows where the engine saw nothing at all and nothing was expected are
156
+ // uninformative for display; keep them in totals and JSON regardless.
157
+ if (tally.TP + tally.FN === 0 && tally.FP === 0) {
158
+ return;
159
+ }
160
+ console.log(
161
+ `| ${ruleId} | ${ruleNames[ruleId].slice(0, 45)} | ${tally.TP} | ${tally.FN} | ${tally.FP} `
162
+ + `| ${tally.TN} | ${percent(tally.TP, tally.TP + tally.FN)} `
163
+ + `| ${percent(tally.TN, tally.TN + tally.FP)} |`
164
+ );
165
+ });
166
+ console.log(
167
+ `| **all** | | ${totals.TP} | ${totals.FN} | ${totals.FP} | ${totals.TN} `
168
+ + `| ${percent(totals.TP, totals.TP + totals.FN)} `
169
+ + `| ${percent(totals.TN, totals.TN + totals.FP)} |`
170
+ );
171
+ report.engines[engine] = {totals, perRule, errored: errored[engine] || 0};
172
+ });
173
+ if (unscoreable.size) {
174
+ console.log(
175
+ `\nUnscoreable (no forConformance WCAG 2.x SC): ${[...unscoreable].join(', ')}`
176
+ );
177
+ }
178
+ Object.entries(errored).forEach(([engine, count]) => {
179
+ console.log(`Errored/prevented rows for ${engine}: ${count}`);
180
+ });
181
+ if (args.json) {
182
+ fs.writeFileSync(args.json, JSON.stringify(report, null, 2));
183
+ console.log(`\nJSON → ${args.json}`);
184
+ }
@@ -0,0 +1,69 @@
1
+ # Stage-3a stress report — pour on 300 real pages (2026-08-22)
2
+
3
+ Capture: `results/stage3a-2026-08-22.jsonl` — 300 pages (250 seeded-random US CrUX tail + 50
4
+ top-1000 head; `yra-100k/data/stage3a-sample-300.txt`, seed 42) × pour, axe, ed11y, qualWeb, via
5
+ the hardened URL-mode harness (per-instance element paths captured for Stage 3b). The run
6
+ completed unattended: 4 canary-triggered browser replacements, zero stalls.
7
+
8
+ ## Gate verdict: PASS
9
+
10
+ | Engine | Raw ran-rate | Effective ran-rate¹ | p50 | p95 | Instances |
11
+ | --- | --- | --- | --- | --- | --- |
12
+ | pour | 96.3% | **99.0%** | 2.0 s | 7.7 s | 50,123 |
13
+ | axe | 97.0% | 99.7% | 1.7 s | 6.5 s | 32,071 |
14
+ | ed11y | 96.7% | 99.3% | 1.0 s | 4.3 s | 6,029 |
15
+ | qualWeb | 94.7% | 97.3% | 4.1 s | 13.4 s | 113,656 |
16
+
17
+ ¹ Excluding the 8 pages unreachable for **every** engine (dead/hostile sites — page problems,
18
+ not engine problems). Pour's gate was ≥95%: passed on either denominator. Latency is well inside
19
+ the worker budget. No trigger-happy rule: nothing asserts on >90% of pages (max:
20
+ `target-size-enhanced`, 86% — an AAA rule where near-ubiquity is plausible; flagged for tic
21
+ quality review, not a defect).
22
+
23
+ ## Prevention taxonomy (engine-specific residue is small)
24
+
25
+ Navigation failures/timeouts dominate (6–7 per engine — the dead-site class). Residue:
26
+ `navigated-during-eval` 1–3/engine (meta-refresh-class pages; results legitimately unobtainable);
27
+ reporter timeouts on 2 extremely heavy pages (pour 1, ed11y 3); qualWeb "No DOM" ×7 (its own
28
+ HTML ingestion fails on some pages — an incumbent robustness datum, not a candidate problem);
29
+ one axe adapter TypeError on one page (page-specific, pre-existing adapter code). Zero CSP
30
+ preventions — the nonce passthrough works in the wild.
31
+
32
+ ## Volume anatomy — the kitchen-sink effect
33
+
34
+ Pour's headline 50k instances is dominated by AAA and best-practice rules (~30k: contrast-
35
+ enhanced 13.1k, target-size-enhanced 9.2k, region 8.1k), a direct consequence of the adapter's
36
+ run-every-rule setting (matching axe's). The A/AA-comparable picture is much closer, and on the
37
+ single biggest A/AA category the two engines nearly agree:
38
+
39
+ - **color-contrast (1.4.3): pour 8,829 instances / 74% of pages vs axe 8,795 / 76%** — near-
40
+ identical at scale.
41
+ - link-name: pour 536/30% vs axe 660/31%. heading-order: 153/30% vs 269/32%.
42
+ - Divergences to watch in Stage 3b: `region` (pour 8.1k vs axe 2.7k — counting granularity) and
43
+ axe's `hidden-content` (11.3k, 87% of pages — its own kitchen-sink review rule).
44
+
45
+ ## Assertion-band inversion (the vendor claim, reproduced)
46
+
47
+ Of pour's instances, **72% are asserted violations** (severity 2–3) and 28% review; axe inverts:
48
+ **34% violations, 66% review/incomplete**. This independently reproduces pour.dev's "twice the
49
+ failing elements with fewer check-by-eye verdicts" claim — and it makes Stage-4 triage of pour's
50
+ *asserted* band the critical validation: pour stakes much more on definite assertions than axe
51
+ does.
52
+
53
+ ## ACT-identified FP classes at real-world scale
54
+
55
+ - **Contrast** (both rules): 21.9k instances = 44% of pour's volume — the FP mechanisms found on
56
+ ACT fixtures (symbol-only text, letter-as-icon glyphs) live inside this mass. Highest-priority
57
+ triage sample for Stage 4.
58
+ - **Language applicability** (`valid-lang-parts`): fired on **zero** real pages — the ACT FP
59
+ class is real but rare in the wild. `html-lang` asserts on 31 pages (10.7%) — consistent with
60
+ WebAIM's ~13% missing-language prevalence, so likely mostly true positives.
61
+ - `button-name` 111 instances / 32 pages, `link-name` 536 / 88 pages — moderate volumes; the
62
+ known name-computation gap (descendant `aria-labelledby`) warrants a targeted triage slice.
63
+
64
+ ## Next (Stage 3b)
65
+
66
+ Per-instance element paths are already captured for all four engines. Run
67
+ `propose-tic-mappings`-style co-occurrence between pour rule IDs and the mapped incumbents'
68
+ issues over this same JSONL; disposition all 70 pour rules that fired (plus the ~16 that
69
+ didn't); then Stage-4 triage sampling with the contrast and link/button-name slices prioritized.
@@ -0,0 +1,94 @@
1
+ # Stage-3b mapping proposals — pour
2
+
3
+ Capture: `validation/act/results/stage3a-2026-08-22.jsonl` · taxonomy: `tic50.js` · thresholds: rate ≥ 0.25, ≥ 5 elements on ≥ 3 pages. Exact-path co-occurrence is a floor; proposals require human accept/reject/split review.
4
+
5
+ | Candidate rule | Elements | Pages | Proposed issue | Co-rate | Lift | Runners-up | Suggestion | Verdict |
6
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- |
7
+ | color-contrast-enhanced | 12965 | 222 | contrastAAA | 43% (5611) | 4.5 | contrastAA (5058, lift 4.1); contrastRisk (1746, lift 3.0) | map | |
8
+ | target-size-enhanced | 8992 | 248 | — | 2% (171) | 5.1 | linkTextsSame (667, lift 3.4); newTabSurprise (565, lift 3.5) | new-issue candidate | |
9
+ | color-contrast | 8775 | 213 | contrastAA | 49% (4287) | 5.1 | contrastRisk (1639, lift 4.2); linkVaguenessRisk (27, lift 8.0) | map | |
10
+ | region | 8013 | 150 | — | 0% (28) | 9.0 | contentBeyondLandmarks (722, lift 3.1); imageNoText (141, lift 3.5) | new-issue candidate | |
11
+ | new-window-link | 1697 | 196 | newTabSurprise | 63% (1064) | 34.4 | nonWebLink (18, lift 20.4); linkIndication (31, lift 16.2) | map | |
12
+ | svg-img-alt | 818 | 87 | — | 24% (194) | 8.4 | svgImageNoText (1, lift 11.1); eventKeyboardRisk (2, lift 6.2) | new-issue candidate | |
13
+ | target-size | 678 | 58 | — | 1% (8) | 26.7 | ignorable (551, lift 1.2); linkConfusionRisk (8, lift 3.2) | new-issue candidate | |
14
+ | text-spacing | 580 | 61 | — | 2% (10) | 22.3 | dialogNoText (1, lift 31.2); inputNoText (1, lift 22.3) | new-issue candidate | |
15
+ | link-name | 536 | 88 | imageLinkNoText | 60% (319) | 115.4 | linkNoText (509, lift 91.1); newTabSurprise (127, lift 13.0) | map | |
16
+ | redundant-aria-label | 510 | 63 | — | 2% (9) | 53.3 | linkTextsSame (47, lift 4.3); ariaAttributeBad (16, lift 5.8) | new-issue candidate | |
17
+ | image-alt | 484 | 69 | imageNoText | 67% (322) | 133.9 | imageTextBad (31, lift 175.8); linkPair (7, lift 13.2) | map | |
18
+ | landmark-unique | 480 | 75 | bannerNotTop | 27% (131) | 177.9 | mainNotTop (111, lift 179.0); landmarkConfusion (91, lift 177.0) | map | |
19
+ | redundant-tabindex | 479 | 60 | — | 3% (16) | 10.9 | ariaAttributeBad (23, lift 8.9); visibleLabelNotInName (19, lift 4.7) | new-issue candidate | |
20
+ | redundant-aria | 337 | 61 | — | 15% (52) | 57.8 | activeEmbedding (8, lift 76.8); focusableDescendants (8, lift 21.7) | new-issue candidate | |
21
+ | landmark-top-level | 302 | 37 | bannerNotTop | 45% (135) | 291.3 | mainNotTop (112, lift 287.1); footerNotTop (6, lift 257.1) | map | |
22
+ | link-text-generic | 279 | 77 | — | 12% (33) | 306.1 | linkTextsSame (172, lift 28.5); linkConfusionRisk (36, lift 34.9) | new-issue candidate | |
23
+ | link-text-generic-only | 279 | 77 | — | 12% (33) | 306.1 | linkTextsSame (172, lift 28.5); linkConfusionRisk (36, lift 34.9) | new-issue candidate | |
24
+ | aria-hidden-focus | 231 | 27 | focusableHidden | 88% (204) | 377.4 | ignorable (120, lift 0.8); boxSizeAbsolute (5, lift 0.8) | map | |
25
+ | non-text-contrast | 190 | 90 | — | 3% (6) | 476.8 | labelConfusionRisk (86, lift 121.7); controlLabelInvisible (2, lift 476.8) | new-issue candidate | |
26
+ | nested-interactive | 183 | 29 | — | 14% (25) | 442.0 | focusableDescendants (20, lift 100.0); ariaAttributeBad (17, lift 17.1) | new-issue candidate | |
27
+ | aria-allowed-attr | 170 | 30 | ariaAttributeBad | 48% (81) | 87.9 | duplicateID (1, lift 5.5); landmarkConfusion (1, lift 5.5) | map | |
28
+ | focus-visible | 167 | 167 | — | 21% (35) | 287.7 | mainNone (48, lift 252.8); skipRepeatedContent (6, lift 191.4) | new-issue candidate | |
29
+ | aria-valid-refs | 167 | 36 | — | 22% (36) | 39.8 | activeEmbedding (4, lift 77.5); dialogNoText (1, lift 108.5) | new-issue candidate | |
30
+ | heading-order | 153 | 87 | headingLevelSkip | 95% (146) | 313.2 | headingEmpty (2, lift 16.9); headingLength (1, lift 20.4) | map | |
31
+ | redundant-role | 135 | 72 | — | 5% (7) | 48.4 | asideNotTop (3, lift 15.0); labelConfusionRisk (4, lift 8.0) | new-issue candidate | |
32
+ | landmark-one-main | 133 | 133 | mainNone | 77% (102) | 674.5 | pageLanguage (29, lift 564.3); h1Not1 (44, lift 454.1) | map | |
33
+ | redundant-image-alt | 120 | 19 | imageTextRedundant | 69% (83) | 482.0 | ignorable (74, lift 0.9) | map | |
34
+ | button-name | 111 | 32 | buttonNoText | 97% (108) | 318.2 | linkNoText (13, lift 11.2); ignorable (90, lift 1.2) | map | |
35
+ | focus-not-obscured | 108 | 8 | — | 16% (17) | 51.5 | newTabSurprise (16, lift 8.1); linkConfusionRisk (2, lift 5.0) | new-issue candidate | |
36
+ | control-contrast | 103 | 52 | labelConfusionRisk | 57% (59) | 154.0 | labelNot1 (4, lift 390.9); selectNoText (2, lift 293.2) | map | |
37
+ | form-label | 88 | 55 | — | 2% (2) | 1029.4 | selectNoText (4, lift 686.3); labelConfusionRisk (66, lift 201.6) | new-issue candidate | |
38
+ | redundant-alt-phrase | 87 | 27 | — | 1% (1) | 69.4 | ignorable (85, lift 1.4); contentBeyondLandmarks (7, lift 2.8) | new-issue candidate | |
39
+ | listitem-parent | 86 | 7 | listItemOrphan | 45% (39) | 708.3 | ignorable (2, lift 0.0) | map | |
40
+ | page-heading-one | 80 | 80 | h1Not1 | 71% (57) | 977.9 | pageTitle (5, lift 1132.3); skipRepeatedContent (10, lift 666.1) | map | |
41
+ | pause-stop-hide | 79 | 21 | — | 1% (1) | 11.8 | footerNot1 (1, lift 11.4); contrastAA (16, lift 2.1) | new-issue candidate | |
42
+ | aria-label-misuse | 77 | 16 | attributeBad | 95% (73) | 825.8 | ariaAttributeBad (69, lift 165.3); contentBeyondLandmarks (5, lift 2.3) | map | |
43
+ | link-in-text-block | 61 | 25 | linkIndication | 57% (35) | 509.6 | linkVaguenessRisk (2, lift 84.9); linkFileName (1, lift 40.1) | map | |
44
+ | empty-heading | 47 | 24 | headingEmpty | 100% (47) | 1294.1 | headingLevelSkip (3, lift 20.9); ignorable (36, lift 1.1) | map | |
45
+ | area-alt | 47 | 1 | — | 100% (47) | 1927.4 | linkNoText (47, lift 96.0); ignorable (47, lift 1.5) | low support | |
46
+ | meta-viewport | 38 | 37 | metaBansZoom | 95% (36) | 1751.4 | — | map | |
47
+ | aria-field-name | 38 | 7 | — | 82% (31) | 2383.9 | inputNoText (6, lift 2043.3); controlNoText (6, lift 185.8) | new-issue candidate | |
48
+ | media-captions | 38 | 4 | — | 21% (8) | 346.7 | videoNoTranscript (3, lift 255.4); videoNoAudioTrack (3, lift 255.4) | new-issue candidate | |
49
+ | scrollable-region-focusable | 32 | 12 | keyboardScroll | 28% (9) | 849.3 | contentBeyondLandmarks (1, lift 1.1) | map | |
50
+ | html-lang | 31 | 31 | pageLanguage | 100% (31) | 2588.2 | pageTitle (5, lift 2922.2); mainNone (26, lift 737.6) | map | |
51
+ | frame-title | 26 | 18 | iframeTitleBad | 73% (19) | 1085.2 | boxSizeAbsolute (4, lift 5.5); ignorable (19, lift 1.1) | map | |
52
+ | meta-refresh | 24 | 24 | refresh | 96% (23) | 3617.2 | — | map | |
53
+ | p-as-heading | 24 | 14 | — | 17% (4) | 112.7 | — | new-issue candidate | |
54
+ | definition-list | 24 | 1 | — | 100% (24) | 1.5 | — | low support | |
55
+ | video-loop-motion | 23 | 23 | videoNoAudioDescription | 83% (19) | 2078.7 | audioNoText (19, lift 1626.8); videoNoTranscript (13, lift 1828.6) | map | |
56
+ | list-structure | 20 | 12 | listChild | 95% (19) | 4098.0 | descendantMissing (13, lift 550.3); ignorable (19, lift 1.4) | map | |
57
+ | button-type | 17 | 10 | — | 12% (2) | 38.5 | visibleLabelNotInName (3, lift 21.1); ariaAttributeBad (1, lift 10.9) | new-issue candidate | |
58
+ | positive-tabindex | 14 | 7 | tabIndexPositive | 100% (14) | 6470.5 | skipRepeatedContent (1, lift 380.6); labelConfusionRisk (4, lift 76.8) | map | |
59
+ | reflow | 12 | 12 | h1Not1 | 42% (5) | 571.9 | mainNone (3, lift 219.9); iframeTitleBad (1, lift 123.8) | map | |
60
+ | aria-attr-valid | 9 | 4 | ariaAttributeBad | 100% (9) | 184.5 | ignorable (4, lift 0.7) | map | |
61
+ | canvas-alt | 8 | 5 | — | 50% (4) | 17.8 | ignorable (3, lift 0.6) | new-issue candidate | |
62
+ | no-autofocus | 8 | 7 | — | 25% (2) | 134.0 | labelConfusionRisk (3, lift 100.8); ignorable (8, lift 1.5) | new-issue candidate | |
63
+ | bypass-blocks | 6 | 6 | skipRepeatedContent | 100% (6) | 5328.6 | h1Not1 (3, lift 686.3); pageLanguage (2, lift 862.7) | map | |
64
+ | document-title | 6 | 6 | pageTitle | 83% (5) | 15097.8 | pageLanguage (5, lift 2156.8); h1Not1 (6, lift 1372.5) | map | |
65
+ | aria-required-children | 5 | 4 | — | 20% (1) | 2588.2 | descendantMissing (5, lift 846.6); controlNoText (1, lift 235.3) | new-issue candidate | |
66
+ | multiple-labels | 5 | 4 | labelNot1 | 100% (5) | 10065.2 | duplicateID (4, lift 747.1); ariaMissing (1, lift 862.7) | map | |
67
+ | autocomplete-valid | 5 | 3 | autocompleteBad | 100% (5) | 15097.8 | ignorable (5, lift 1.5); contrastAA (1, lift 2.1) | map | |
68
+ | dialog-name | 4 | 4 | — | 100% (4) | 18117.4 | ariaAttributeBad (1, lift 46.1) | low support | |
69
+ | fieldset-legend | 4 | 4 | — | 25% (1) | 2516.3 | controlNoText (1, lift 294.1); duplicateID (1, lift 233.5) | low support | |
70
+ | aria-required-parent | 4 | 1 | — | 0% (0) | 0.0 | — | low support | |
71
+ | label-for-valid | 3 | 3 | — | 33% (1) | 11.6 | contrastAA (2, lift 7.0); contrastAAA (2, lift 6.9) | low support | |
72
+ | table-headers | 3 | 3 | — | 100% (3) | 2264.7 | tableHeaderless (2, lift 1830.0); tabularTableless (2, lift 1776.2) | low support | |
73
+ | visual-order-divergence | 3 | 3 | — | 33% (1) | 3.4 | ignorable (1, lift 0.5) | low support | |
74
+ | auth-field-obstruction | 3 | 1 | — | 100% (3) | 1.5 | — | low support | |
75
+ | composite-widget-name | 2 | 2 | — | 50% (1) | 6470.5 | ariaAttributeBad (2, lift 184.5) | low support | |
76
+ | valid-role | 1 | 1 | — | 100% (1) | 536.0 | focusableRole (1, lift 374.3); ignorable (1, lift 1.5) | low support | |
77
+
78
+ Summary: 70 fired rules — 33 mapping proposals, 26 new-issue candidates, 11 low-support.
79
+
80
+ Incumbent instances with no tic mapping (top 10 — taxonomy coverage gaps):
81
+ - qualWeb:QW-BP30: 807
82
+ - ed11y:altURLLinked: 107
83
+ - axe:meta-refresh: 24
84
+ - ed11y:altMeaningless: 21
85
+ - ed11y:altMeaninglessLinked: 12
86
+ - axe:aria-tab-name: 1
87
+
88
+ ## Reading notes for the triage pass
89
+
90
+ - **High-confidence accepts** (semantic match + dominant co-rate + high lift): empty-heading→headingEmpty, button-name→buttonNoText, heading-order→headingLevelSkip, landmark-one-main→mainNone, page-heading-one→h1Not1, image-alt→imageNoText, aria-hidden-focus→focusableHidden, new-window-link→newTabSurprise, document-title→pageTitle, bypass-blocks→skipRepeatedContent, autocomplete-valid→autocompleteBad, multiple-labels→labelNot1, aria-label-misuse→attributeBad, link-in-text-block→linkIndication, listitem-parent→listItemOrphan, redundant-image-alt→imageTextRedundant, color-contrast→contrastAA, color-contrast-enhanced→contrastAAA.
91
+ - **Split candidates**: link-name (linkNoText vs imageLinkNoText both co-fire strongly — tic distinguishes text links from image links; pour's one rule covers both); landmark-unique / landmark-top-level (bannerNotTop / mainNotTop / footerNotTop family).
92
+ - **Likely genuine pour novelty** (new-issue candidates with no plausible runner-up): text-spacing (1.4.12 — no incumbent equivalent fired anywhere), target-size / target-size-enhanced (2.5.8/2.5.5 — tic appears to lack target-size issues), non-text-contrast (1.4.11), focus-visible, focus-not-obscured (2.4.11), and the redundant-* best-practice family (pour's "overdoing it" rules).
93
+ - **Known ranking quirk**: extreme-lift/tiny-support pairs can outscore the right answer when a rule's true match has moderate lift (see form-label, where labelConfusionRisk — 66 elements, lift 202 — is the plausible mapping but a 2-element pair scored higher and then failed thresholds). The runners-up column carries the correction; this is why proposals are review inputs, not auto-accepts.
94
+ - **Byproduct — taxonomy coverage gaps** (incumbent rules with no tic home; upstream-filing material): qualWeb QW-BP30 (807 instances), ed11y altURLLinked/altMeaningless/altMeaninglessLinked, axe meta-refresh, axe aria-tab-name.
@@ -0,0 +1,138 @@
1
+ # Stage-3b triage draft — pour (for human review)
2
+
3
+ Drafted 2026-09-01 by Claude from `stage3b-mapping-proposals.md` + the reading notes. Every
4
+ verdict below is a **draft for Jeff's accept/override** — semantic judgment calls are marked
5
+ ⚠️ where the co-occurrence evidence and the rule semantics disagree (the known ranking quirk).
6
+ Review workflow: scan the ⚠️ rows first (9 of them), then rubber-stamp or override the rest.
7
+ Accepted verdicts become tic entries (initial `quality: 1`) in the next tic version.
8
+
9
+ ## A. Mapping proposals — draft ACCEPT as proposed (24)
10
+
11
+ The 18 high-confidence accepts from the reading notes, plus 6 where semantics and evidence
12
+ agree cleanly:
13
+
14
+ | Pour rule | tic issue | Note |
15
+ | --- | --- | --- |
16
+ | empty-heading | headingEmpty | 100% co-rate |
17
+ | button-name | buttonNoText | 97% |
18
+ | heading-order | headingLevelSkip | 95% |
19
+ | landmark-one-main | mainNone | 77% |
20
+ | page-heading-one | h1Not1 | 71% |
21
+ | image-alt | imageNoText | 67% |
22
+ | aria-hidden-focus | focusableHidden | 88% |
23
+ | new-window-link | newTabSurprise | 63% |
24
+ | document-title | pageTitle | 83% |
25
+ | bypass-blocks | skipRepeatedContent | 100% |
26
+ | autocomplete-valid | autocompleteBad | 100% |
27
+ | multiple-labels | labelNot1 | 100% |
28
+ | aria-label-misuse | attributeBad | 95% |
29
+ | link-in-text-block | linkIndication | 57% |
30
+ | listitem-parent | listItemOrphan | 45%, no plausible rival |
31
+ | redundant-image-alt | imageTextRedundant | 69% |
32
+ | color-contrast | contrastAA | 49% — plus contrastRisk co-fire is expected (bands) |
33
+ | color-contrast-enhanced | contrastAAA | 43% |
34
+ | aria-allowed-attr | ariaAttributeBad | 48%, semantic exact |
35
+ | meta-viewport | metaBansZoom | 95%, semantic exact |
36
+ | scrollable-region-focusable | keyboardScroll | 28% co-rate but semantic exact — incumbents fire rarely |
37
+ | html-lang | pageLanguage | 100% |
38
+ | frame-title | iframeTitleBad | 73% |
39
+ | meta-refresh | refresh | 96% — also fills the axe:meta-refresh coverage gap |
40
+ | list-structure | listChild | 95% |
41
+ | aria-attr-valid | ariaAttributeBad | 100% (9 el) |
42
+ | positive-tabindex | tabIndexPositive | 100% |
43
+
44
+ (27 rows — three of the "33 proposals" move to §B/§C below instead.)
45
+
46
+ ## B. Mapping proposals — draft SPLIT (3)
47
+
48
+ | Pour rule | Draft | Rationale |
49
+ | --- | --- | --- |
50
+ | link-name | **split → imageLinkNoText / linkNoText** by flagged-element content (contains img/svg → imageLinkNoText, else linkNoText) | Both co-fire strongly (60% / 509-element runner-up); tic distinguishes text links from image links, pour's one rule covers both. Split at mapping time on the instance's element, not the rule. |
51
+ | landmark-top-level | **split → bannerNotTop / mainNotTop / footerNotTop / asideNotTop** by landmark role | The tic *NotTop family is per-landmark; pour's one rule covers all. Runner-up evidence shows exactly this family. |
52
+ | landmark-unique | ⚠️ **remap → landmarkConfusion** | Proposed bannerNotTop (27%) is a co-occurrence artifact — landmark-unique is about *duplicate/undifferentiated* landmarks, which is landmarkConfusion's semantics (91-element runner-up, lift 177). |
53
+
54
+ ## C. Mapping proposals — draft REJECT the proposed issue, re-disposition (3) — all ⚠️
55
+
56
+ | Pour rule | Proposed (rejected) | Draft disposition | Rationale |
57
+ | --- | --- | --- | --- |
58
+ | video-loop-motion | videoNoAudioDescription (83%) | **new issue** `motionNoControl` (2.2.2), shared with pause-stop-hide | Rule is about looping/auto-playing motion, not audio description — co-occurrence artifact (pages with video have both defects). |
59
+ | reflow | h1Not1 (42%) | **new issue** `reflowBroken` (1.4.10) | Semantic nonsense as a mapping; 1.4.10 has no incumbent coverage — this is novelty, mis-filed by base-rate. |
60
+ | control-contrast | labelConfusionRisk (57%) | **fold into** the `nonTextContrast` new issue (1.4.11) with non-text-contrast | 1.4.11 control contrast ≠ label confusion; co-occurrence artifact. |
61
+
62
+ ## D. New-issue candidates — draft REMAP to an existing tic issue instead (8) — all ⚠️ except where noted
63
+
64
+ The ranking quirk in reverse: the right mapping existed but lost on lift/threshold.
65
+
66
+ | Pour rule | Draft map | Rationale |
67
+ | --- | --- | --- |
68
+ | region | contentBeyondLandmarks | Semantic exact; 722-element runner-up (lift 3.1) lost on co-rate only because region fires far more broadly than the incumbent. |
69
+ | svg-img-alt | svgImageNoText | Semantic exact; incumbent fires so rarely (1 element) the co-rate is meaningless. |
70
+ | nested-interactive | focusableDescendants | Semantic exact (interactive inside interactive), lift 100. |
71
+ | aria-required-children | descendantMissing | Semantic exact (role's required owned children absent), lift 847. |
72
+ | aria-field-name | inputNoText | 82% co-rate, lift 2043 — likely fell out of "proposed" on a threshold technicality. Not ⚠️: evidence and semantics agree. |
73
+ | link-text-generic (+ -only) | linkVaguenessRisk | "Click here"-class generic text is exactly linkVaguenessRisk; runner-up linkTextsSame is a different defect (identical texts, different targets). Map both variants to the same issue; instance dedup absorbs the -only subset. This is pour's ACT sole-detector rule (aizyf1) — do not lose it. |
74
+ | form-label | labelConfusionRisk | Per the reading notes: the 66-element, lift-202 runner-up is the plausible mapping; the 2-element top pair was the quirk example. |
75
+ | media-captions | ⚠️ videoNoTranscript *or* new `mediaNoCaptions` (1.2.2) | Runner-up is close but captions ≠ transcript; Jeff's call whether tic's existing media family covers 1.2.2 or needs the new issue. |
76
+
77
+ ## E. New-issue candidates — draft CONFIRM as genuinely new tic issues (11 rules → 8 issues)
78
+
79
+ The novelty pour was screened for. Suggested issue IDs follow tic naming style:
80
+
81
+ | New tic issue | Pour rule(s) | WCAG | Evidence |
82
+ | --- | --- | --- | --- |
83
+ | textSpacingRigid | text-spacing | 1.4.12 | No incumbent equivalent fired anywhere (580 el / 61 pages) |
84
+ | targetSmall | target-size | 2.5.8 AA | tic has no target-size issues (678 el) |
85
+ | targetSmallEnhanced | target-size-enhanced | 2.5.5 AAA | Mirror the contrastAA/AAA banding pattern (8,992 el — volume driver, watch quality) |
86
+ | nonTextContrast | non-text-contrast, control-contrast | 1.4.11 | No incumbent; control-contrast folded in from §C |
87
+ | focusIndicatorMissing | focus-visible | 2.4.7 | 167 pages — page-level rule |
88
+ | focusObscured | focus-not-obscured | 2.4.11 | New-in-2.2 criterion, no incumbent |
89
+ | motionNoControl | pause-stop-hide, video-loop-motion | 2.2.2 | Both halves of the auto-playing/looping motion defect |
90
+ | reflowBroken | reflow | 1.4.10 | From §C |
91
+ | pseudoHeading | p-as-heading | 1.3.1 | Visual heading not marked up; no incumbent fired |
92
+ | canvasNoText | canvas-alt | 1.1.1 | Canvas is not an img; imageNoText would blur element semantics |
93
+ | ariaRedundant | redundant-aria, redundant-aria-label, redundant-tabindex, redundant-role | best practice | Pour's "overdoing it" family as one issue, `quality: 1`; per-rule granularity preserved in instances |
94
+
95
+ Also: redundant-alt-phrase → new issue `altPhraseRedundant`, and note it likely also homes
96
+ the unmapped incumbents ed11y:altMeaningless/altMeaninglessLinked (kills two coverage-gap
97
+ birds). aria-valid-refs → new issue `ariaRefBroken` (broken ID references) unless Jeff reads
98
+ tic's ariaMissing as covering it.
99
+
100
+ ## F. New-issue candidates — draft QUALITY-0 / park (2)
101
+
102
+ | Pour rule | Draft | Rationale |
103
+ | --- | --- | --- |
104
+ | button-type | quality-0 | Implicit-submit is a behavior bug more than an a11y defect; weak evidence (17 el) |
105
+ | no-autofocus | quality-0 | Best-practice; genuine debate whether autofocus is a defect at all; 8 el |
106
+
107
+ ## G. Low-support (11) — draft: park all except three semantic gifts
108
+
109
+ Park (insufficient evidence, revisit if Stage 4 surfaces them): definition-list,
110
+ aria-required-parent, label-for-valid, visual-order-divergence, auth-field-obstruction,
111
+ composite-widget-name, valid-role, fieldset-legend.
112
+
113
+ Map now despite low support (semantics are exact and unambiguous):
114
+
115
+ | Pour rule | tic issue | Note |
116
+ | --- | --- | --- |
117
+ | area-alt | linkNoText | Image-map areas are links; 100% co-rate, 47 el (one page) |
118
+ | dialog-name | dialogNoText | 100% co-rate, semantic exact |
119
+ | table-headers | tableHeaderless | Runner-up is semantically exact |
120
+
121
+ ## Tally
122
+
123
+ - 24 accepts + 3 splits + 8 remaps + 3 low-support maps = **38 mapped pour rules**
124
+ - 13 rules → **~13 new tic issues** (8 novel families incl. multi-rule groupings)
125
+ - 2 quality-0, 8 parked
126
+ - ⚠️ rows needing real human judgment: **9** (landmark-unique, the three §C rejects,
127
+ region/svg-img-alt/nested-interactive/aria-required-children remaps if the co-rate
128
+ bothers you, media-captions)
129
+
130
+ ## After sign-off
131
+
132
+ 1. Author tic entries (next tic version) in the testilo fork: mapped rules at `quality: 1`;
133
+ new issues with WCAG + weight; splits implemented in the mapping layer per §B.
134
+ 2. Re-run the evaluation slice as the Stage-4 `study_runs` row with mappings live
135
+ (this is also the app-tier load-baseline measured window).
136
+ 3. Byproduct filings: taxonomy coverage gaps (qualWeb QW-BP30 × 807, ed11y alt* family,
137
+ axe meta-refresh → homed by the §A/§E work above), pour FP classes with rates
138
+ (fp-triage-2026-08-22.md).
@@ -0,0 +1,50 @@
1
+ # Track A — surea11y, full ACT run (2026-09-01)
2
+
3
+ Capture: `results/surea11y-full-2026-09-01.jsonl` (1,213 testcases, 1,196 ran, 17 prevented —
4
+ the known redirecting-fixture class). Scored `--band asserted`;
5
+ `results/surea11y-full-2026-09-01-asserted.json`. Comparison figures are the merged
6
+ 2026-08-22 full run (`merged-full-2026-08-22-asserted.json`), same fixtures, same
7
+ criterion-level scoring layer for pour/axe/surea11y (qualWeb keeps its native-ACT scoring
8
+ advantage).
9
+
10
+ ## Aggregate (any-engine detection subset — 47 rules with surea11y included, was 43)
11
+
12
+ | Engine | Sensitivity | Specificity | TP | FN | FP | TN |
13
+ | --- | --- | --- | --- | --- | --- | --- |
14
+ | qualWeb | 85.9% | 99.1% | 213 | 35 | 5 | 527 |
15
+ | **surea11y** | **68.6%** | **92.0%** | 151 | 69 | 37 | 425 |
16
+ | pour | 51.1% | 91.7% | 112 | 107 | 37 | 407 |
17
+ | axe | 33.2% | 96.0% | 73 | 147 | 19 | 453 |
18
+
19
+ surea11y clears the Stage-2 gate: specificity ≥ incumbent median, and sensitivity is the
20
+ best of any criterion-layer engine — +17.5 points over pour at equal specificity.
21
+
22
+ ## Net-new coverage
23
+
24
+ Sole detector across all four engines: `e88epe` (image not in accessibility tree is
25
+ decorative — 20% sens, but nobody else scores at all).
26
+
27
+ Detects where pour misses (7): the three **1.4.12 text-spacing** rules (24afc2 / 78fd32 /
28
+ 9e45ec, all 100/100 — ACT-fixture confirmation of the same novelty pour's Stage-3b flagged
29
+ in the wild), **b33eff orientation lock** (100/100), **bisz58 meta-refresh no-exception**
30
+ (100/100), akn7bn iframe tab-order, e88epe.
31
+
32
+ Misses where pour detects (3): aizyf1 (link is descriptive — pour's marquee sole-source
33
+ stays unique to pour), 6cfa84 (aria-hidden focusable content), 80f0bf (auto-playing audio).
34
+ **The engines are complementary, not redundant** — each keeps sole-source value if both
35
+ were adopted.
36
+
37
+ ## FP hotspots (Stage-3 should measure their in-the-wild rates)
38
+
39
+ | ACT rule | FP | Note |
40
+ | --- | --- | --- |
41
+ | 4b1c6c iframe identical accessible names | 11 (TP 2) | Dominant FP class — over-asserts equivalence judgments the fixture set treats as `cantTell` territory |
42
+ | ucwvc8 page-language subtag matches content | 4 (TP 0) | Same lang-heuristic FP class pour had; pour's Stage-3a showed it ~absent in the wild |
43
+ | e086e5 / 2t702h / akn7bn / qt1vmo / bc4a75 / e88epe | 2–3 each | Small; triage with Stage-3 rates |
44
+
45
+ ## Verdict
46
+
47
+ Stage-2 gate **passed**. Next per pipeline: Stage-3 capture over the evaluation slice
48
+ (`--urls yra-100k/data/stage3a-sample-300.txt`, engines surea11y + incumbents) — ran-rate,
49
+ latency distribution, trigger-happy screen, and the 4b1c6c FP class's real-world rate; then
50
+ 3b mapping bootstrap off the same capture.