testaro 78.0.8 → 78.4.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 (356) hide show
  1. package/.gitattributes +94 -0
  2. package/.github/workflows/ci.yml +58 -0
  3. package/.github/workflows/publish.yml +49 -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 +73 -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/docker-compose.yml +8 -2
  16. package/docs/checkpoint-scanning.md +199 -0
  17. package/docs/standard-result-outcome.md +165 -0
  18. package/env.example +44 -10
  19. package/eslint.config.mjs +101 -0
  20. package/netWatch.js +372 -222
  21. package/package.json +46 -31
  22. package/pour/README.md +47 -0
  23. package/pour/pour.min.js +8 -0
  24. package/procs/actDo.js +808 -0
  25. package/procs/catalog.d.ts +17 -0
  26. package/procs/catalog.js +312 -221
  27. package/procs/catalog.ts +398 -0
  28. package/procs/checkpoint.js +110 -0
  29. package/procs/config.d.ts +35 -0
  30. package/procs/config.js +62 -0
  31. package/procs/dateTime.js +2 -1
  32. package/procs/doActs.js +442 -849
  33. package/procs/doTestAct.js +27 -121
  34. package/procs/flow.js +221 -0
  35. package/procs/generateRuleRegistry.js +94 -0
  36. package/procs/getSource.d.ts +26 -0
  37. package/procs/job.js +115 -1
  38. package/procs/launch.d.ts +40 -0
  39. package/procs/launch.js +239 -108
  40. package/procs/nu.d.ts +48 -0
  41. package/procs/scope.js +196 -0
  42. package/procs/shoot.d.ts +33 -0
  43. package/procs/standard.d.ts +18 -0
  44. package/procs/standard.js +83 -0
  45. package/procs/standard.ts +126 -0
  46. package/procs/testAct.js +145 -0
  47. package/procs/testaro.d.ts +20 -0
  48. package/procs/testaro.js +236 -189
  49. package/procs/testaro.ts +321 -0
  50. package/procs/userPath.js +126 -0
  51. package/procs/xPath.d.ts +4 -0
  52. package/procs/xPath.js +81 -69
  53. package/procs/xPath.ts +106 -0
  54. package/procs/xPathScript.d.ts +6 -0
  55. package/procs/xPathScript.js +70 -0
  56. package/run.js +3 -2
  57. package/surea11y/README.md +44 -0
  58. package/surea11y/surea11y.browser.js +14 -0
  59. package/testaro/adbID.d.ts +3 -0
  60. package/testaro/adbID.js +38 -41
  61. package/testaro/adbID.ts +66 -0
  62. package/testaro/allCapStyle.d.ts +3 -0
  63. package/testaro/allCapStyle.js +31 -34
  64. package/testaro/allCapStyle.ts +55 -0
  65. package/testaro/allCaps.d.ts +16 -0
  66. package/testaro/allCaps.js +179 -151
  67. package/testaro/allCaps.ts +228 -0
  68. package/testaro/allHidden.d.ts +13 -0
  69. package/testaro/allHidden.js +29 -30
  70. package/testaro/allHidden.ts +50 -0
  71. package/testaro/allSlanted.d.ts +3 -0
  72. package/testaro/allSlanted.js +30 -33
  73. package/testaro/allSlanted.ts +54 -0
  74. package/testaro/altScheme.d.ts +3 -0
  75. package/testaro/altScheme.js +27 -30
  76. package/testaro/altScheme.ts +50 -0
  77. package/testaro/attVal.d.ts +3 -0
  78. package/testaro/attVal.js +20 -21
  79. package/testaro/attVal.ts +52 -0
  80. package/testaro/autocomplete.d.ts +3 -0
  81. package/testaro/autocomplete.js +59 -75
  82. package/testaro/autocomplete.ts +101 -0
  83. package/testaro/bulk.d.ts +13 -0
  84. package/testaro/bulk.js +32 -33
  85. package/testaro/bulk.ts +55 -0
  86. package/testaro/buttonMenu.d.ts +9 -0
  87. package/testaro/buttonMenu.js +317 -319
  88. package/testaro/buttonMenu.ts +391 -0
  89. package/testaro/captionLoc.d.ts +3 -0
  90. package/testaro/captionLoc.js +17 -20
  91. package/testaro/captionLoc.ts +40 -0
  92. package/testaro/datalistRef.d.ts +3 -0
  93. package/testaro/datalistRef.js +33 -36
  94. package/testaro/datalistRef.ts +55 -0
  95. package/testaro/distortion.d.ts +3 -0
  96. package/testaro/distortion.js +57 -26
  97. package/testaro/distortion.ts +81 -0
  98. package/testaro/docType.d.ts +15 -0
  99. package/testaro/docType.js +25 -25
  100. package/testaro/docType.ts +45 -0
  101. package/testaro/dupAtt.d.ts +16 -0
  102. package/testaro/dupAtt.js +113 -104
  103. package/testaro/dupAtt.ts +144 -0
  104. package/testaro/elements.d.ts +6 -0
  105. package/testaro/elements.js +153 -153
  106. package/testaro/elements.ts +215 -0
  107. package/testaro/embAc.d.ts +3 -0
  108. package/testaro/embAc.js +19 -20
  109. package/testaro/embAc.ts +40 -0
  110. package/testaro/focAll.d.ts +13 -0
  111. package/testaro/focAll.js +185 -191
  112. package/testaro/focAll.ts +217 -0
  113. package/testaro/focAndOp.d.ts +3 -0
  114. package/testaro/focAndOp.js +100 -104
  115. package/testaro/focAndOp.ts +128 -0
  116. package/testaro/focInd.d.ts +3 -0
  117. package/testaro/focInd.js +64 -66
  118. package/testaro/focInd.ts +96 -0
  119. package/testaro/focVis.d.ts +3 -0
  120. package/testaro/focVis.js +28 -30
  121. package/testaro/focVis.ts +52 -0
  122. package/testaro/headEl.d.ts +10 -0
  123. package/testaro/headEl.js +61 -62
  124. package/testaro/headEl.ts +82 -0
  125. package/testaro/headingAmb.d.ts +3 -0
  126. package/testaro/headingAmb.js +54 -63
  127. package/testaro/headingAmb.ts +72 -0
  128. package/testaro/hovInd.d.ts +12 -0
  129. package/testaro/hovInd.js +163 -130
  130. package/testaro/hovInd.ts +199 -0
  131. package/testaro/hover.d.ts +3 -0
  132. package/testaro/hover.js +155 -126
  133. package/testaro/hover.ts +154 -0
  134. package/testaro/hr.d.ts +3 -0
  135. package/testaro/hr.js +14 -17
  136. package/testaro/hr.ts +36 -0
  137. package/testaro/imageLink.d.ts +3 -0
  138. package/testaro/imageLink.js +17 -20
  139. package/testaro/imageLink.ts +42 -0
  140. package/testaro/labClash.d.ts +3 -0
  141. package/testaro/labClash.js +31 -33
  142. package/testaro/labClash.ts +54 -0
  143. package/testaro/legendLoc.d.ts +3 -0
  144. package/testaro/legendLoc.js +17 -20
  145. package/testaro/legendLoc.ts +42 -0
  146. package/testaro/lineHeight.d.ts +3 -0
  147. package/testaro/lineHeight.js +43 -48
  148. package/testaro/lineHeight.ts +70 -0
  149. package/testaro/linkAmb.d.ts +7 -0
  150. package/testaro/linkAmb.js +79 -80
  151. package/testaro/linkAmb.ts +105 -0
  152. package/testaro/linkExt.d.ts +3 -0
  153. package/testaro/linkExt.js +13 -16
  154. package/testaro/linkExt.ts +35 -0
  155. package/testaro/linkOldAtt.d.ts +3 -0
  156. package/testaro/linkOldAtt.js +25 -28
  157. package/testaro/linkOldAtt.ts +48 -0
  158. package/testaro/linkTo.d.ts +3 -0
  159. package/testaro/linkTo.js +22 -22
  160. package/testaro/linkTo.ts +43 -0
  161. package/testaro/linkUl.d.ts +3 -0
  162. package/testaro/linkUl.js +30 -34
  163. package/testaro/linkUl.ts +54 -0
  164. package/testaro/miniText.d.ts +3 -0
  165. package/testaro/miniText.js +41 -44
  166. package/testaro/miniText.ts +68 -0
  167. package/testaro/motion.d.ts +10 -0
  168. package/testaro/motion.js +92 -96
  169. package/testaro/motion.ts +125 -0
  170. package/testaro/nonTable.d.ts +3 -0
  171. package/testaro/nonTable.js +39 -45
  172. package/testaro/nonTable.ts +66 -0
  173. package/testaro/optRoleSel.d.ts +3 -0
  174. package/testaro/optRoleSel.js +16 -19
  175. package/testaro/optRoleSel.ts +41 -0
  176. package/testaro/phOnly.d.ts +3 -0
  177. package/testaro/phOnly.js +18 -21
  178. package/testaro/phOnly.ts +43 -0
  179. package/testaro/pseudoP.d.ts +3 -0
  180. package/testaro/pseudoP.js +36 -38
  181. package/testaro/pseudoP.ts +59 -0
  182. package/testaro/radioSet.d.ts +3 -0
  183. package/testaro/radioSet.js +57 -59
  184. package/testaro/radioSet.ts +79 -0
  185. package/testaro/registry.d.ts +63 -0
  186. package/testaro/registry.js +67 -0
  187. package/testaro/registry.ts +141 -0
  188. package/testaro/role.d.ts +3 -0
  189. package/testaro/role.js +27 -29
  190. package/testaro/role.ts +53 -0
  191. package/testaro/secHeading.d.ts +3 -0
  192. package/testaro/secHeading.js +30 -33
  193. package/testaro/secHeading.ts +53 -0
  194. package/testaro/styleDiff.d.ts +25 -0
  195. package/testaro/styleDiff.js +248 -252
  196. package/testaro/styleDiff.ts +303 -0
  197. package/testaro/tabNav.d.ts +33 -0
  198. package/testaro/tabNav.js +272 -342
  199. package/testaro/tabNav.ts +454 -0
  200. package/testaro/targetsNear.d.ts +9 -0
  201. package/testaro/targetsNear.js +130 -132
  202. package/testaro/targetsNear.ts +160 -0
  203. package/testaro/textNodes.d.ts +6 -0
  204. package/testaro/textNodes.js +139 -135
  205. package/testaro/textNodes.ts +185 -0
  206. package/testaro/textSem.d.ts +3 -0
  207. package/testaro/textSem.js +25 -28
  208. package/testaro/textSem.ts +47 -0
  209. package/testaro/title.d.ts +9 -0
  210. package/testaro/title.js +16 -13
  211. package/testaro/title.ts +31 -0
  212. package/testaro/titledEl.d.ts +3 -0
  213. package/testaro/titledEl.js +15 -18
  214. package/testaro/titledEl.ts +38 -0
  215. package/testaro/zIndex.d.ts +3 -0
  216. package/testaro/zIndex.js +19 -22
  217. package/testaro/zIndex.ts +42 -0
  218. package/tests/alfa.d.ts +45 -0
  219. package/tests/alfa.js +138 -141
  220. package/tests/alfa.ts +214 -0
  221. package/tests/aslint.d.ts +33 -0
  222. package/tests/aslint.js +272 -249
  223. package/tests/aslint.ts +301 -0
  224. package/tests/axe.d.ts +27 -0
  225. package/tests/axe.js +199 -200
  226. package/tests/axe.ts +277 -0
  227. package/tests/ed11y.d.ts +28 -0
  228. package/tests/ed11y.js +141 -99
  229. package/tests/ed11y.ts +178 -0
  230. package/tests/htmlcs.d.ts +21 -0
  231. package/tests/htmlcs.js +174 -140
  232. package/tests/htmlcs.ts +181 -0
  233. package/tests/ibm.d.ts +52 -0
  234. package/tests/ibm.js +164 -166
  235. package/tests/ibm.ts +251 -0
  236. package/tests/nuVal.d.ts +13 -0
  237. package/tests/nuVal.js +107 -112
  238. package/tests/nuVal.ts +145 -0
  239. package/tests/nuVnu.d.ts +15 -0
  240. package/tests/nuVnu.js +141 -111
  241. package/tests/nuVnu.ts +144 -0
  242. package/tests/pour.d.ts +31 -0
  243. package/tests/pour.js +242 -0
  244. package/tests/pour.ts +273 -0
  245. package/tests/qualWeb.d.ts +39 -0
  246. package/tests/qualWeb.js +302 -272
  247. package/tests/qualWeb.ts +415 -0
  248. package/tests/surea11y.d.ts +33 -0
  249. package/tests/surea11y.js +288 -0
  250. package/tests/surea11y.ts +334 -0
  251. package/tests/testaro.d.ts +25 -0
  252. package/tests/testaro.js +745 -652
  253. package/tests/testaro.ts +862 -0
  254. package/tests/wave.d.ts +46 -0
  255. package/tests/wave.js +166 -177
  256. package/tests/wave.ts +252 -0
  257. package/tsconfig.json +17 -0
  258. package/types.d.ts +243 -0
  259. package/types.js +10 -0
  260. package/types.ts +376 -0
  261. package/validation/act/README.md +46 -0
  262. package/validation/act/capture.js +424 -0
  263. package/validation/act/chromium-issue-draft.md +66 -0
  264. package/validation/act/fp-triage-2026-08-22.md +86 -0
  265. package/validation/act/isolation-notes.md +159 -0
  266. package/validation/act/playwright-issue-draft.md +73 -0
  267. package/validation/act/propose-mappings.js +0 -0
  268. package/validation/act/repro-cdp-raw.js +105 -0
  269. package/validation/act/repro-metarefresh.js +65 -0
  270. package/validation/act/score.js +184 -0
  271. package/validation/act/stage3a-stress-report.md +69 -0
  272. package/validation/act/stage3b-mapping-proposals.md +94 -0
  273. package/validation/act/stage3b-triage-draft.md +138 -0
  274. package/validation/act/surea11y-track-a-2026-09-01.md +50 -0
  275. package/validation/executors/netWatch.js +180 -90
  276. package/validation/executors/test.js +17 -2
  277. package/validation/executors/tests.js +118 -10
  278. package/validation/jobs/reports/raw/260901T1000-surea11y-validation.json +964 -0
  279. package/validation/jobs/todo/240101T1200-simple-example.json +14 -6
  280. package/validation/jobs/todo/240101T1300-shoot-example.json +2 -1
  281. package/validation/jobs/todo/260821T1900-pour-validation.json +45 -0
  282. package/validation/jobs/todo/260901T1000-surea11y-validation.json +45 -0
  283. package/validation/knownFailures.json +1 -0
  284. package/validation/tests/jobProperties/adbID.json +27 -2
  285. package/validation/tests/jobProperties/{focOp.json → allCapStyle.json} +56 -53
  286. package/validation/tests/jobProperties/allCaps.json +25 -0
  287. package/validation/tests/jobProperties/allHidden.json +133 -13
  288. package/validation/tests/jobProperties/allSlanted.json +3 -3
  289. package/validation/tests/jobProperties/altScheme.json +23 -8
  290. package/validation/tests/jobProperties/attVal.json +57 -57
  291. package/validation/tests/jobProperties/autocomplete.json +11 -1
  292. package/validation/tests/jobProperties/bulk.json +6 -1
  293. package/validation/tests/jobProperties/buttonMenu.json +63 -42
  294. package/validation/tests/jobProperties/captionLoc.json +1 -6
  295. package/validation/tests/jobProperties/checkpoint-browser.json +401 -0
  296. package/validation/tests/jobProperties/checkpoint-page.json +397 -0
  297. package/validation/tests/jobProperties/checkpoint.json +395 -0
  298. package/validation/tests/jobProperties/datalistRef.json +20 -5
  299. package/validation/tests/jobProperties/distortion.json +28 -3
  300. package/validation/tests/jobProperties/docType.json +2 -2
  301. package/validation/tests/jobProperties/dupAtt.json +48 -33
  302. package/validation/tests/jobProperties/elements.json +28 -28
  303. package/validation/tests/jobProperties/embAc.json +36 -31
  304. package/validation/tests/jobProperties/focAndOp.json +284 -0
  305. package/validation/tests/jobProperties/focInd.json +34 -34
  306. package/validation/tests/jobProperties/focVis.json +1 -1
  307. package/validation/tests/jobProperties/hover.json +39 -37
  308. package/validation/tests/jobProperties/hr.json +13 -3
  309. package/validation/tests/jobProperties/imageLink.json +0 -5
  310. package/validation/tests/jobProperties/labClash.json +68 -33
  311. package/validation/tests/jobProperties/legendLoc.json +1 -6
  312. package/validation/tests/jobProperties/lineHeight.json +2 -2
  313. package/validation/tests/jobProperties/linkAmb.json +13 -18
  314. package/validation/tests/jobProperties/linkExt.json +1 -1
  315. package/validation/tests/jobProperties/linkOldAtt.json +12 -2
  316. package/validation/tests/jobProperties/linkTo.json +1 -1
  317. package/validation/tests/jobProperties/linkUl.json +65 -65
  318. package/validation/tests/jobProperties/miniText.json +7 -2
  319. package/validation/tests/jobProperties/motion.json +4 -50
  320. package/validation/tests/jobProperties/nonTable.json +48 -3
  321. package/validation/tests/jobProperties/optRoleSel.json +12 -2
  322. package/validation/tests/jobProperties/phOnly.json +6 -16
  323. package/validation/tests/jobProperties/pseudoP.json +17 -2
  324. package/validation/tests/jobProperties/radioSet.json +30 -30
  325. package/validation/tests/jobProperties/role.json +25 -5
  326. package/validation/tests/jobProperties/secHeading.json +26 -21
  327. package/validation/tests/jobProperties/styleDiff.json +35 -5
  328. package/validation/tests/jobProperties/tabNav.json +3 -1
  329. package/validation/tests/jobProperties/targetsNear.json +237 -0
  330. package/validation/tests/jobProperties/textNodes.json +29 -29
  331. package/validation/tests/jobProperties/textSem.json +26 -1
  332. package/validation/tests/jobProperties/title.json +12 -2
  333. package/validation/tests/jobProperties/titledEl.json +39 -9
  334. package/validation/tests/jobProperties/userPath.json +317 -0
  335. package/validation/tests/jobProperties/zIndex.json +40 -35
  336. package/validation/tests/targets/allCapStyle/index.html +26 -0
  337. package/validation/tests/targets/checkpoint/index.html +38 -0
  338. package/validation/tests/targets/datalistRef/index.html +1 -1
  339. package/validation/tests/targets/focAndOp/bad.html +29 -0
  340. package/validation/tests/targets/{focOp → focAndOp}/good.html +3 -1
  341. package/validation/tests/targets/focInd/bad.html +2 -1
  342. package/validation/tests/targets/headEl/index.html +10 -1
  343. package/validation/tests/targets/{targetSmall → targetsNear}/index.html +15 -1
  344. package/validation/tests/targets/userPath/index.html +42 -0
  345. package/validation/validateTest.js +61 -9
  346. package/.claude/settings.local.json +0 -11
  347. package/.eslintrc.json +0 -41
  348. package/htmlcs/.eslintrc.json +0 -67
  349. package/validation/tests/jobProperties/linkTitle.json +0 -127
  350. package/validation/tests/jobProperties/opFoc.json +0 -164
  351. package/validation/tests/jobProperties/targetSmall.json +0 -152
  352. package/validation/tests/jobProperties/targetTiny.json +0 -142
  353. package/validation/tests/targets/focOp/bad.html +0 -25
  354. package/validation/tests/targets/linkTitle/index.html +0 -24
  355. package/validation/tests/targets/opFoc/bad.html +0 -26
  356. package/validation/tests/targets/opFoc/good.html +0 -23
@@ -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.
@@ -1,5 +1,7 @@
1
1
  /*
2
2
  © 2022–2024 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2026 Jonathan Robert Pool.
4
+ © 2026 Jeff Witt.
3
5
 
4
6
  Licensed under the MIT License. See LICENSE file at the project root or
5
7
  https://opensource.org/license/mit/ for details.
@@ -8,119 +10,207 @@
8
10
  */
9
11
 
10
12
  /*
11
- watchNet.js
12
- Validator for network watching.
13
+ netWatch.js
14
+ Validator for network watching, covering all three auth types.
13
15
  */
14
16
 
15
17
  // IMPORTS
16
18
 
17
19
  const fs = require('fs/promises');
20
+ const client = require('http');
18
21
 
19
22
  // CONSTANTS
20
23
 
21
- // Override netWatch environment variables with validation-specific ones.
22
24
  const jobDir = `${__dirname}/../jobs/todo`;
23
- process.env.JOB_URLS = 'http://localhost:3007/api/job';
24
- process.env.AGENT = 'testarauth';
25
- const {netWatch} = require('../../netWatch');
26
- const client = require('http');
27
25
  const jobID = '240101T1200-simple-example';
26
+ const workerID = 'testaro1';
27
+ const workerSecret = 'testarosecret';
28
+ // Override netWatch environment variables with validation-specific ones.
29
+ process.env.NETWATCH_URL_JOB = 'http://localhost:3007/api/job';
30
+ process.env.NETWATCH_URL_REPORT = 'http://localhost:3007/api/report';
31
+ process.env.NETWATCH_WORKER_ID = workerID;
32
+ process.env.NETWATCH_WORKER_SECRET = workerSecret;
33
+ const {netWatch} = require('../../netWatch');
34
+ /*
35
+ The authorization-header value netWatch is expected to send in header mode: per RFC 7617, Basic
36
+ followed by base64 of testaro1:testarosecret. Intentionally a literal, not derived from the
37
+ netWatch code, so an encoding regression there fails here.
38
+ */
39
+ const workerAuth = 'Basic dGVzdGFybzE6dGVzdGFyb3NlY3JldA==';
40
+ // Auth types to be validated.
41
+ const authTypes = ['header', 'pathBody', 'none'];
28
42
 
29
- // OPERATION
43
+ // FUNCTIONS
30
44
 
31
- // Start a timer.
32
- const startTime = Date.now();
33
- // Initialize the state.
34
- let jobGiven = false;
35
- // Start checking for jobs every 5 seconds in 5 seconds.
36
- setTimeout(() => {
37
- netWatch(false, 5, false);
38
- }, 5000);
39
- let server;
40
- // Handles Testaro requests to the server.
41
- const requestHandler = (request, response) => {
42
- const {method} = request;
43
- const bodyParts = [];
44
- request.on('error', err => {
45
- console.error(err);
46
- })
47
- .on('data', chunk => {
48
- bodyParts.push(chunk);
49
- })
50
- .on('end', async () => {
51
- // Remove any trailing slash from the URL.
52
- const requestURL = request.url.replace(/\/$/, '');
53
- // If the request method is GET:
54
- if (method === 'GET') {
55
- // If a job is validly requested:
56
- console.log('Server got a job request from Testaro');
57
- if (requestURL === `/api/job?agent=${process.env.AGENT}`) {
58
- // If at least 7 seconds has elapsed since timing started:
59
- if (Date.now() > startTime + 7000) {
60
- // Respond with a job.
61
- const jobJSON = await fs.readFile(`${jobDir}/${jobID}.json`);
62
- await response.end(jobJSON);
63
- console.log('Server sent job to Testaro');
64
- jobGiven = true;
45
+ // Returns an error description if a request carries wrong credentials for an auth type, else ''.
46
+ const credentialError = (authType, request, bodyObj) => {
47
+ const authHeader = request.headers.authorization;
48
+ if (authType === 'header') {
49
+ if (authHeader !== workerAuth) {
50
+ return 'authorization header missing or wrong';
51
+ }
52
+ if (bodyObj.agentPW !== undefined) {
53
+ return 'agentPW unexpectedly in the request body';
54
+ }
55
+ }
56
+ else if (authType === 'pathBody') {
57
+ if (authHeader) {
58
+ return 'authorization header unexpectedly present';
59
+ }
60
+ if (bodyObj.agentPW !== workerSecret) {
61
+ return 'agentPW missing from or wrong in the request body';
62
+ }
63
+ }
64
+ else {
65
+ if (authHeader) {
66
+ return 'authorization header unexpectedly present';
67
+ }
68
+ if (bodyObj.agentPW !== undefined) {
69
+ return 'agentPW unexpectedly in the request body';
70
+ }
71
+ }
72
+ return '';
73
+ };
74
+ // Validates netWatch with one auth type; returns whether the scenario succeeded.
75
+ const runScenario = authType => {
76
+ return new Promise(resolveScenario => {
77
+ process.env.NETWATCH_AUTH_TYPE = authType;
78
+ let jobGiven = false;
79
+ let noJobSent = false;
80
+ let reportOK = false;
81
+ let server;
82
+ // Responds to a request with a JSON object and a status code.
83
+ const respondWithObject = (object, response, statusCode = 200) => {
84
+ response.statusCode = statusCode;
85
+ response.setHeader('content-type', 'application/json; charset=utf-8');
86
+ response.end(JSON.stringify(object));
87
+ };
88
+ // Handles Testaro requests to the server.
89
+ const requestHandler = (request, response) => {
90
+ const {method} = request;
91
+ // Ignore any trailing slash on the request URL.
92
+ const url = request.url.replace(/\/$/, '');
93
+ const bodyParts = [];
94
+ request.on('error', err => {
95
+ console.error(err);
96
+ })
97
+ .on('data', chunk => {
98
+ bodyParts.push(chunk);
99
+ })
100
+ .on('end', async () => {
101
+ let bodyObj = {};
102
+ try {
103
+ bodyObj = JSON.parse(bodyParts.join('') || '{}');
65
104
  }
66
- // Otherwise, i.e. if timing started less than 7 seconds ago:
67
- else {
68
- // Send an empty-object response.
69
- await response.end('{}');
105
+ catch(error) {
106
+ console.log(`Scenario ${authType} failure: non-JSON request body (${error.message})`);
70
107
  }
71
- }
72
- else {
73
- const error = {
74
- error: 'ERROR: Job request invalid'
75
- };
76
- const errorJSON = JSON.stringify(error);
77
- await response.end(errorJSON);
78
- }
79
- }
80
- // Otherwise, if the request method is POST:
81
- else if (method === 'POST') {
82
- console.log('Server got report from Testaro');
83
- const ack = {};
84
- // If a report is validly submitted:
85
- if (requestURL === '/api') {
86
- // If a job was earlier given to Testaro:
87
- if (jobGiven) {
88
- // Respond, reporting success or failure.
89
- try {
90
- const bodyJSON = bodyParts.join('');
91
- const body = JSON.parse(bodyJSON);
108
+ // If the request carries wrong credentials for this auth type:
109
+ const credError = credentialError(authType, request, bodyObj);
110
+ if (credError) {
111
+ // Report this and reject the request; netWatch is expected to abort on this.
112
+ console.log(`Scenario ${authType} failure: ${credError}`);
113
+ respondWithObject({error: 'ERROR: Authorization invalid'}, response, 401);
114
+ server.close();
115
+ return;
116
+ }
117
+ // Otherwise, if the request is a job request:
118
+ if (method === 'POST' && url === '/api/job') {
119
+ console.log(`Server got a job request from Testaro (${authType})`);
120
+ // If this is the first job request of the scenario:
121
+ if (! noJobSent) {
122
+ // Send a no-job response, to validate the no-job branch.
123
+ noJobSent = true;
124
+ respondWithObject({}, response);
125
+ }
126
+ // Otherwise, i.e. if the no-job branch was already validated:
127
+ else {
128
+ // Respond with a job.
129
+ const jobJSON = await fs.readFile(`${jobDir}/${jobID}.json`);
130
+ response.setHeader('content-type', 'application/json; charset=utf-8');
131
+ response.end(jobJSON);
132
+ console.log(`Server sent job to Testaro (${authType})`);
133
+ jobGiven = true;
134
+ }
135
+ }
136
+ // Otherwise, if the request is a report submission:
137
+ else if (method === 'POST' && url === '/api/report') {
138
+ console.log(`Server got report from Testaro (${authType})`);
139
+ const ack = {};
140
+ // If a job was earlier given to Testaro:
141
+ if (jobGiven) {
142
+ // Check the report, including the worker identification.
143
+ const {report} = bodyObj;
92
144
  if (
93
- body.acts
94
- && body.sources
95
- && body.sources.agent
96
- && body.sources.agent === process.env.AGENT
145
+ report
146
+ && report.acts
147
+ && report.jobData
148
+ && report.sources
149
+ && report.sources.agent === workerID
97
150
  ) {
98
151
  ack.message = 'Success: Valid report submitted';
152
+ reportOK = true;
99
153
  }
100
154
  else {
101
- ack.message = 'Failure: Report invalid';
155
+ ack.message = 'Failure: Report invalid or worker not identified';
102
156
  }
103
157
  }
104
- catch(error) {
105
- ack.message = `ERROR: ${error.message}`;
158
+ else {
159
+ ack.message = 'ERROR: Report submitted before a job was given';
106
160
  }
161
+ respondWithObject(ack, response);
162
+ console.log(`Server responded: ${ack.message}`);
163
+ // This ends the scenario, so stop the server.
164
+ server.close();
165
+ console.log('Server closed');
107
166
  }
108
- }
109
- else {
110
- ack.message = 'ERROR: Report submission invalid';
111
- }
112
- const ackJSON = JSON.stringify(ack);
113
- response.end(ackJSON);
114
- console.log(`Server responded: ${ack.message}`);
115
- // This ends the validation, so stop the server.
167
+ // Otherwise, i.e. if the request is neither:
168
+ else {
169
+ // Report this and reject the request.
170
+ console.log(`Scenario ${authType} failure: unexpected request ${method} ${request.url}`);
171
+ respondWithObject({error: 'ERROR: Request invalid'}, response, 404);
172
+ server.close();
173
+ }
174
+ });
175
+ };
176
+ // Create a server.
177
+ server = client.createServer({}, requestHandler);
178
+ // Start the server listening for Testaro requests, then start a one-job watch.
179
+ server.listen(3007, async () => {
180
+ console.log(`Job and report server listening on port 3007 (auth type ${authType})`);
181
+ const watchOK = await netWatch(false, 5, false);
116
182
  server.close();
117
- console.log('Server closed');
118
- }
183
+ resolveScenario(reportOK && watchOK);
184
+ });
119
185
  });
120
186
  };
121
- // Create a server.
122
- server = client.createServer({}, requestHandler);
123
- // Start a server listening for Testaro requests.
124
- server.listen(3007, () => {
125
- console.log('Job and report server listening on port 3007');
126
- });
187
+
188
+ // OPERATION
189
+
190
+ // Impose a deadline, so a hang becomes a failure instead of a silent stall.
191
+ const watchdog = setTimeout(() => {
192
+ console.log('Failure: netWatch validation timed out');
193
+ process.exit(1);
194
+ }, 600000);
195
+ // Run the scenarios in sequence and exit with a code reporting the outcome.
196
+ (async () => {
197
+ const failures = [];
198
+ for (const authType of authTypes) {
199
+ const isOK = await runScenario(authType);
200
+ // Drop any kept-alive connections to the closed server before the next scenario.
201
+ client.globalAgent.destroy();
202
+ console.log(`Scenario ${authType}: ${isOK ? 'valid' : 'INVALID'}\n`);
203
+ if (! isOK) {
204
+ failures.push(authType);
205
+ }
206
+ }
207
+ clearTimeout(watchdog);
208
+ if (failures.length) {
209
+ console.log(`Failure: Invalid scenarios: ${failures.join(', ')}`);
210
+ process.exit(1);
211
+ }
212
+ else {
213
+ console.log('Success: Valid reports submitted for all auth types');
214
+ process.exit(0);
215
+ }
216
+ })();
@@ -1,5 +1,6 @@
1
1
  /*
2
2
  © 2022–2023 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2026 Jeff Witt.
3
4
 
4
5
  Licensed under the MIT License. See LICENSE file at the project root or
5
6
  https://opensource.org/license/mit/ for details.
@@ -13,6 +14,20 @@
13
14
  Execution example: npm test focOp
14
15
  */
15
16
 
16
- const testID = process.argv[2];
17
17
  const {validateTest} = require('../validateTest');
18
- validateTest(testID);
18
+
19
+ // Flushes the standard output and then exits, because lingering browser
20
+ // processes otherwise keep the event loop alive indefinitely.
21
+ const exitWith = code => {
22
+ process.stdout.write('', () => process.exit(code));
23
+ };
24
+
25
+ const testID = process.argv[2];
26
+ validateTest(testID)
27
+ .then(result => {
28
+ exitWith(result.success ? 0 : 1);
29
+ })
30
+ .catch(error => {
31
+ console.log(`ERROR validating test ${testID} (${error.message})`);
32
+ exitWith(1);
33
+ });