@reddoorla/maintenance 0.95.0 → 0.96.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 (272) hide show
  1. package/README.md +37 -2
  2. package/dist/airtable-SJV4ZSMS.js +9 -0
  3. package/dist/{announce-PGT4SZZB.js → announce-45HVP4J4.js} +7 -7
  4. package/dist/{blux-4KNXUP2K.js → blux-RUFMNOUJ.js} +5 -5
  5. package/dist/{bump-deps-C5G2EFBB.js → bump-deps-EL3TKMIL.js} +6 -6
  6. package/dist/{chunk-XN37VTDB.js → chunk-37B4LVB3.js} +2 -2
  7. package/dist/{chunk-BVEDKMNG.js → chunk-3EA43S3R.js} +28 -10
  8. package/dist/chunk-3EA43S3R.js.map +1 -0
  9. package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
  10. package/dist/chunk-43SW3FGW.js.map +1 -0
  11. package/dist/{chunk-WMBLR5VA.js → chunk-44SDGPGQ.js} +3 -3
  12. package/dist/chunk-4UTJ4G6P.js +52 -0
  13. package/dist/chunk-4UTJ4G6P.js.map +1 -0
  14. package/dist/{chunk-R53VL3RI.js → chunk-5CJVUPJU.js} +1 -1
  15. package/dist/{chunk-R53VL3RI.js.map → chunk-5CJVUPJU.js.map} +1 -1
  16. package/dist/{chunk-4AIYWWKV.js → chunk-62YWOFVN.js} +36 -5
  17. package/dist/chunk-62YWOFVN.js.map +1 -0
  18. package/dist/{chunk-22GKJO5F.js → chunk-6INOBNRO.js} +2 -2
  19. package/dist/{chunk-BYGPUUCT.js → chunk-72FJZ6LO.js} +43 -11
  20. package/dist/chunk-72FJZ6LO.js.map +1 -0
  21. package/dist/{chunk-5GUJ36TL.js → chunk-74QRWE6G.js} +2 -2
  22. package/dist/chunk-A5QKNOXW.js +72 -0
  23. package/dist/chunk-A5QKNOXW.js.map +1 -0
  24. package/dist/{chunk-2NQUCS7O.js → chunk-BIYQN7FH.js} +3 -1
  25. package/dist/{chunk-2NQUCS7O.js.map → chunk-BIYQN7FH.js.map} +1 -1
  26. package/dist/{chunk-S7RYFUIK.js → chunk-CA6ADGOC.js} +2 -2
  27. package/dist/chunk-CGQZFQAY.js +38 -0
  28. package/dist/chunk-CGQZFQAY.js.map +1 -0
  29. package/dist/{chunk-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
  30. package/dist/chunk-CXICCLNM.js.map +1 -0
  31. package/dist/{chunk-KZRR4NFG.js → chunk-D3CTOCUR.js} +3 -3
  32. package/dist/{chunk-CWXTNPNZ.js → chunk-DVJNYPFR.js} +3 -3
  33. package/dist/chunk-DVJNYPFR.js.map +1 -0
  34. package/dist/{chunk-AVEQUR44.js → chunk-EASYCD6F.js} +60 -17
  35. package/dist/chunk-EASYCD6F.js.map +1 -0
  36. package/dist/{chunk-BCKEP5S6.js → chunk-ECDJDHKT.js} +2 -2
  37. package/dist/{chunk-DMPHP7UP.js → chunk-EQQ4H6L3.js} +2 -2
  38. package/dist/{chunk-WVNDEYXL.js → chunk-GEUC5VGM.js} +11 -5
  39. package/dist/chunk-GEUC5VGM.js.map +1 -0
  40. package/dist/{chunk-Z2K4JPQ3.js → chunk-H445EH33.js} +2 -2
  41. package/dist/chunk-IHHAYXKY.js +351 -0
  42. package/dist/chunk-IHHAYXKY.js.map +1 -0
  43. package/dist/{chunk-FUWFLAWQ.js → chunk-IMYL67JV.js} +3 -3
  44. package/dist/{chunk-3Z6X3BHU.js → chunk-KN52PM63.js} +7 -2
  45. package/dist/{chunk-3Z6X3BHU.js.map → chunk-KN52PM63.js.map} +1 -1
  46. package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
  47. package/dist/chunk-LAJ3FF54.js.map +1 -0
  48. package/dist/{chunk-5UINJNAJ.js → chunk-MZZDMBDT.js} +16 -3
  49. package/dist/chunk-MZZDMBDT.js.map +1 -0
  50. package/dist/{chunk-DFKBTBO3.js → chunk-NVLREB5R.js} +35 -6
  51. package/dist/chunk-NVLREB5R.js.map +1 -0
  52. package/dist/{chunk-AEUABPAJ.js → chunk-OTEVKRAH.js} +3 -3
  53. package/dist/{chunk-KQB2BX4V.js → chunk-P4A6D6HO.js} +4 -4
  54. package/dist/chunk-P7323YVW.js +134 -0
  55. package/dist/chunk-P7323YVW.js.map +1 -0
  56. package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
  57. package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
  58. package/dist/chunk-QFSL2L2B.js.map +1 -0
  59. package/dist/{chunk-2LYOAB2E.js → chunk-RRE74VWI.js} +2 -2
  60. package/dist/{chunk-LM2NHRCJ.js → chunk-SHGIDTYF.js} +4 -4
  61. package/dist/chunk-SHGIDTYF.js.map +1 -0
  62. package/dist/{chunk-ZQV7UQNE.js → chunk-SQ4FSNYL.js} +2 -2
  63. package/dist/{chunk-OCNSIBMP.js → chunk-T76MA2FH.js} +135 -1
  64. package/dist/{chunk-OCNSIBMP.js.map → chunk-T76MA2FH.js.map} +1 -1
  65. package/dist/{chunk-6R56OKJZ.js → chunk-UIITN37J.js} +2 -2
  66. package/dist/{chunk-CJ2RMTT7.js → chunk-VEIIUCYL.js} +2 -2
  67. package/dist/{chunk-SVAAONYB.js → chunk-WQLNUEZL.js} +12 -2
  68. package/dist/{chunk-SVAAONYB.js.map → chunk-WQLNUEZL.js.map} +1 -1
  69. package/dist/{chunk-WSJ7RO54.js → chunk-XED2DNAI.js} +23 -5
  70. package/dist/chunk-XED2DNAI.js.map +1 -0
  71. package/dist/{chunk-FSGICFXB.js → chunk-XG2A6GIG.js} +17 -5
  72. package/dist/chunk-XG2A6GIG.js.map +1 -0
  73. package/dist/{chunk-TO7DJYLZ.js → chunk-XY7YV2YQ.js} +178 -39
  74. package/dist/chunk-XY7YV2YQ.js.map +1 -0
  75. package/dist/{chunk-HTABUCF4.js → chunk-Y5BVR4HW.js} +3 -3
  76. package/dist/chunk-Y6FCXRIC.js +3972 -0
  77. package/dist/chunk-Y6FCXRIC.js.map +1 -0
  78. package/dist/{chunk-2SLED3NB.js → chunk-YDZ7XGPZ.js} +1 -1
  79. package/dist/{chunk-2SLED3NB.js.map → chunk-YDZ7XGPZ.js.map} +1 -1
  80. package/dist/{chunk-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
  81. package/dist/chunk-Z7FDJ35L.js.map +1 -0
  82. package/dist/{chunk-5EEMTJMT.js → chunk-ZH5LOQET.js} +4 -4
  83. package/dist/cli/bin.js +49 -31
  84. package/dist/cli/bin.js.map +1 -1
  85. package/dist/cli/commands/audit.js +18 -18
  86. package/dist/client-L7NAUG6J.js +11 -0
  87. package/dist/configs/eslint.js +27 -1
  88. package/dist/configs/eslint.js.map +1 -1
  89. package/dist/configs/playwright-a11y.d.ts +11 -1
  90. package/dist/configs/playwright-a11y.js +3 -1
  91. package/dist/{convert-to-pnpm-YTFREUJK.js → convert-to-pnpm-4DON7IFS.js} +6 -6
  92. package/dist/{db-BBEM7CBX.js → db-5K4CDEGJ.js} +45 -20
  93. package/dist/db-5K4CDEGJ.js.map +1 -0
  94. package/dist/deadletter-G4M3HSVM.js +17 -0
  95. package/dist/{digest-65BOZ7AT.js → digest-5AJGWS33.js} +45 -22
  96. package/dist/digest-5AJGWS33.js.map +1 -0
  97. package/dist/{digest-collectors-EHFY2OQC.js → digest-collectors-MC5NWEUA.js} +12 -10
  98. package/dist/{digest-state-3KHZF7GD.js → digest-state-ZDPKR7MW.js} +18 -2
  99. package/dist/digest-state-ZDPKR7MW.js.map +1 -0
  100. package/dist/{ensure-site-WIDO6LGU.js → ensure-site-XESZHZEB.js} +39 -11
  101. package/dist/ensure-site-XESZHZEB.js.map +1 -0
  102. package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-GJWGKTLD.js} +6 -4
  103. package/dist/{forms-notify-target-2LWF5C5B.js → forms-notify-target-4NPLX2UG.js} +5 -5
  104. package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
  105. package/dist/{github-signals-GAHNOAHS.js → github-signals-DMU74SAX.js} +10 -10
  106. package/dist/{header-image-YN3AQRY3.js → header-image-BRTOLHJF.js} +6 -5
  107. package/dist/header-image-BRTOLHJF.js.map +1 -0
  108. package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-UCLMSSCN.js} +8 -7
  109. package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-UCLMSSCN.js.map} +1 -1
  110. package/dist/{health-mirror-EB76EERV.js → health-mirror-ONRT4DQ5.js} +7 -7
  111. package/dist/{import-airtable-TAKSN4NV.js → import-airtable-IIK2QUUF.js} +3 -3
  112. package/dist/index.d.ts +135 -4
  113. package/dist/index.js +68 -34
  114. package/dist/index.js.map +1 -1
  115. package/dist/{init-FFE4FCO2.js → init-3LMZTCVD.js} +19 -18
  116. package/dist/{init-FFE4FCO2.js.map → init-3LMZTCVD.js.map} +1 -1
  117. package/dist/{launch-4RVWPZGI.js → launch-XTDMC6LD.js} +209 -61
  118. package/dist/launch-XTDMC6LD.js.map +1 -0
  119. package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
  120. package/dist/{lighthouse-airtable-YMH4UA7R.js → lighthouse-airtable-3NOFB42B.js} +3 -3
  121. package/dist/{match-harness-2Q6ISEES.js → match-harness-OW45CB5H.js} +14 -7
  122. package/dist/match-harness-OW45CB5H.js.map +1 -0
  123. package/dist/migrate-NE3DUMTM.js +7 -0
  124. package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +3 -3
  125. package/dist/{onboard-TM2KESGF.js → onboard-CZYBWVWH.js} +6 -6
  126. package/dist/{orchestrate-MWXENQOS.js → orchestrate-TGDFGY52.js} +8 -8
  127. package/dist/{parity-CAJ477PS.js → parity-AEKYSLN6.js} +5 -5
  128. package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
  129. package/dist/{preflight-4VQP7JJL.js → preflight-POZZI3GN.js} +9 -9
  130. package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-AHY5ULJR.js} +38 -8
  131. package/dist/prismic-ci-AHY5ULJR.js.map +1 -0
  132. package/dist/{prismic-models-W6CNLTWH.js → prismic-models-URYGODNY.js} +9 -9
  133. package/dist/prospect/types.d.ts +72 -0
  134. package/dist/prospect/types.js +1 -1
  135. package/dist/{prospect-audit-X4Z67LWW.js → prospect-audit-AJEE3Z4L.js} +16 -7
  136. package/dist/prospect-audit-AJEE3Z4L.js.map +1 -0
  137. package/dist/{prospect-audits-RVYEA4KH.js → prospect-audits-IZVQY3OQ.js} +12 -4
  138. package/dist/protection-audit-3J3UEOVT.js +413 -0
  139. package/dist/protection-audit-3J3UEOVT.js.map +1 -0
  140. package/dist/recipes/sync-configs.js +3 -3
  141. package/dist/{render-YGNGTX3W.js → render-MHUUHPLN.js} +36 -9
  142. package/dist/render-MHUUHPLN.js.map +1 -0
  143. package/dist/{render-from-row-IFSBHWDH.js → render-from-row-65DC6ZP3.js} +4 -4
  144. package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-ONJMBUHJ.js} +3 -3
  145. package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
  146. package/dist/replay-4FVFOMHH.js.map +1 -0
  147. package/dist/{report-4GFD4MW2.js → report-RC5LGPU3.js} +22 -20
  148. package/dist/report-RC5LGPU3.js.map +1 -0
  149. package/dist/{report-mirror-IP64R7B3.js → report-mirror-KKZZ6RMZ.js} +13 -9
  150. package/dist/{report-mirror-IP64R7B3.js.map → report-mirror-KKZZ6RMZ.js.map} +1 -1
  151. package/dist/{schema-2V7BGYVT.js → schema-KWUQEZ5O.js} +1 -1
  152. package/dist/schema-KWUQEZ5O.js.map +1 -0
  153. package/dist/{self-updating-MWXIZLL7.js → self-updating-RKKIYNV4.js} +8 -7
  154. package/dist/{self-updating-MWXIZLL7.js.map → self-updating-RKKIYNV4.js.map} +1 -1
  155. package/dist/{selftest-MAB2PBXQ.js → selftest-AFLR7YV7.js} +10 -10
  156. package/dist/site-lookup-DTOLJ4EA.js +27 -0
  157. package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
  158. package/dist/{site-mirror-UKHREAD6.js → site-mirror-H2FYMN55.js} +23 -9
  159. package/dist/site-mirror-H2FYMN55.js.map +1 -0
  160. package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-BWHR3XX5.js} +8 -7
  161. package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-BWHR3XX5.js.map} +1 -1
  162. package/dist/{submissions-5GKJPCIH.js → submissions-II5J35QM.js} +4 -4
  163. package/dist/{submissions-3I3GAQAX.js → submissions-JDZP35JM.js} +49 -9
  164. package/dist/submissions-JDZP35JM.js.map +1 -0
  165. package/dist/{svelte-codemods-WSV2FGNG.js → svelte-codemods-QWGIRNN6.js} +6 -6
  166. package/dist/{sync-K3PKHVLH.js → sync-NKPDCMYW.js} +5 -5
  167. package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-HGLQV55H.js} +6 -6
  168. package/dist/{upgrade-VTD6V4HS.js → upgrade-WZ44IARG.js} +6 -6
  169. package/dist/util/git.d.ts +48 -9
  170. package/dist/util/git.js +1 -1
  171. package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
  172. package/dist/{websites-TB4TCAWM.js → websites-6DHIHJ2D.js} +4 -2
  173. package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-BIK6LNVC.js} +5 -5
  174. package/dist/write-audits-to-airtable-BIK6LNVC.js.map +1 -0
  175. package/package.json +3 -2
  176. package/dist/airtable-JGCPS7JH.js +0 -9
  177. package/dist/chunk-4AIYWWKV.js.map +0 -1
  178. package/dist/chunk-5UINJNAJ.js.map +0 -1
  179. package/dist/chunk-6VKYQATC.js +0 -1708
  180. package/dist/chunk-6VKYQATC.js.map +0 -1
  181. package/dist/chunk-7MKT4I5T.js +0 -62
  182. package/dist/chunk-7MKT4I5T.js.map +0 -1
  183. package/dist/chunk-AVEQUR44.js.map +0 -1
  184. package/dist/chunk-BVEDKMNG.js.map +0 -1
  185. package/dist/chunk-BYGPUUCT.js.map +0 -1
  186. package/dist/chunk-CWXTNPNZ.js.map +0 -1
  187. package/dist/chunk-DFKBTBO3.js.map +0 -1
  188. package/dist/chunk-FSGICFXB.js.map +0 -1
  189. package/dist/chunk-GASBX52O.js +0 -12
  190. package/dist/chunk-GASBX52O.js.map +0 -1
  191. package/dist/chunk-LM2NHRCJ.js.map +0 -1
  192. package/dist/chunk-QF6IQQFP.js.map +0 -1
  193. package/dist/chunk-TO7DJYLZ.js.map +0 -1
  194. package/dist/chunk-TR3HFH6J.js.map +0 -1
  195. package/dist/chunk-WJMPCVOR.js.map +0 -1
  196. package/dist/chunk-WSJ7RO54.js.map +0 -1
  197. package/dist/chunk-WVNDEYXL.js.map +0 -1
  198. package/dist/chunk-XHHMM5HX.js.map +0 -1
  199. package/dist/chunk-YLYXW5PH.js.map +0 -1
  200. package/dist/client-M2V6FHH5.js +0 -11
  201. package/dist/db-BBEM7CBX.js.map +0 -1
  202. package/dist/digest-65BOZ7AT.js.map +0 -1
  203. package/dist/digest-state-3KHZF7GD.js.map +0 -1
  204. package/dist/ensure-site-WIDO6LGU.js.map +0 -1
  205. package/dist/header-image-YN3AQRY3.js.map +0 -1
  206. package/dist/launch-4RVWPZGI.js.map +0 -1
  207. package/dist/match-harness-2Q6ISEES.js.map +0 -1
  208. package/dist/migrate-K4JETR36.js +0 -7
  209. package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
  210. package/dist/prospect-audit-X4Z67LWW.js.map +0 -1
  211. package/dist/protection-audit-SJO53MEQ.js +0 -168
  212. package/dist/protection-audit-SJO53MEQ.js.map +0 -1
  213. package/dist/render-YGNGTX3W.js.map +0 -1
  214. package/dist/replay-PCSQOGCZ.js.map +0 -1
  215. package/dist/report-4GFD4MW2.js.map +0 -1
  216. package/dist/schema-2V7BGYVT.js.map +0 -1
  217. package/dist/site-mirror-UKHREAD6.js.map +0 -1
  218. package/dist/submissions-3I3GAQAX.js.map +0 -1
  219. /package/dist/{airtable-JGCPS7JH.js.map → airtable-SJV4ZSMS.js.map} +0 -0
  220. /package/dist/{announce-PGT4SZZB.js.map → announce-45HVP4J4.js.map} +0 -0
  221. /package/dist/{blux-4KNXUP2K.js.map → blux-RUFMNOUJ.js.map} +0 -0
  222. /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-EL3TKMIL.js.map} +0 -0
  223. /package/dist/{chunk-XN37VTDB.js.map → chunk-37B4LVB3.js.map} +0 -0
  224. /package/dist/{chunk-WMBLR5VA.js.map → chunk-44SDGPGQ.js.map} +0 -0
  225. /package/dist/{chunk-22GKJO5F.js.map → chunk-6INOBNRO.js.map} +0 -0
  226. /package/dist/{chunk-5GUJ36TL.js.map → chunk-74QRWE6G.js.map} +0 -0
  227. /package/dist/{chunk-S7RYFUIK.js.map → chunk-CA6ADGOC.js.map} +0 -0
  228. /package/dist/{chunk-KZRR4NFG.js.map → chunk-D3CTOCUR.js.map} +0 -0
  229. /package/dist/{chunk-BCKEP5S6.js.map → chunk-ECDJDHKT.js.map} +0 -0
  230. /package/dist/{chunk-DMPHP7UP.js.map → chunk-EQQ4H6L3.js.map} +0 -0
  231. /package/dist/{chunk-Z2K4JPQ3.js.map → chunk-H445EH33.js.map} +0 -0
  232. /package/dist/{chunk-FUWFLAWQ.js.map → chunk-IMYL67JV.js.map} +0 -0
  233. /package/dist/{chunk-AEUABPAJ.js.map → chunk-OTEVKRAH.js.map} +0 -0
  234. /package/dist/{chunk-KQB2BX4V.js.map → chunk-P4A6D6HO.js.map} +0 -0
  235. /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
  236. /package/dist/{chunk-2LYOAB2E.js.map → chunk-RRE74VWI.js.map} +0 -0
  237. /package/dist/{chunk-ZQV7UQNE.js.map → chunk-SQ4FSNYL.js.map} +0 -0
  238. /package/dist/{chunk-6R56OKJZ.js.map → chunk-UIITN37J.js.map} +0 -0
  239. /package/dist/{chunk-CJ2RMTT7.js.map → chunk-VEIIUCYL.js.map} +0 -0
  240. /package/dist/{chunk-HTABUCF4.js.map → chunk-Y5BVR4HW.js.map} +0 -0
  241. /package/dist/{chunk-5EEMTJMT.js.map → chunk-ZH5LOQET.js.map} +0 -0
  242. /package/dist/{client-M2V6FHH5.js.map → client-L7NAUG6J.js.map} +0 -0
  243. /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-4DON7IFS.js.map} +0 -0
  244. /package/dist/{digest-collectors-EHFY2OQC.js.map → deadletter-G4M3HSVM.js.map} +0 -0
  245. /package/dist/{fleet-state-UEVZ3X3J.js.map → digest-collectors-MC5NWEUA.js.map} +0 -0
  246. /package/dist/{freeze-A6MUCZU4.js.map → fleet-state-GJWGKTLD.js.map} +0 -0
  247. /package/dist/{forms-notify-target-2LWF5C5B.js.map → forms-notify-target-4NPLX2UG.js.map} +0 -0
  248. /package/dist/{import-airtable-TAKSN4NV.js.map → freeze-GUQ43EAU.js.map} +0 -0
  249. /package/dist/{github-signals-GAHNOAHS.js.map → github-signals-DMU74SAX.js.map} +0 -0
  250. /package/dist/{health-mirror-EB76EERV.js.map → health-mirror-ONRT4DQ5.js.map} +0 -0
  251. /package/dist/{lighthouse-VHI77PFB.js.map → import-airtable-IIK2QUUF.js.map} +0 -0
  252. /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
  253. /package/dist/{migrate-K4JETR36.js.map → lighthouse-airtable-3NOFB42B.js.map} +0 -0
  254. /package/dist/{notify-IISTHNBR.js.map → migrate-NE3DUMTM.js.map} +0 -0
  255. /package/dist/{orchestrate-MWXENQOS.js.map → notify-K3WVH3DM.js.map} +0 -0
  256. /package/dist/{onboard-TM2KESGF.js.map → onboard-CZYBWVWH.js.map} +0 -0
  257. /package/dist/{parity-CAJ477PS.js.map → orchestrate-TGDFGY52.js.map} +0 -0
  258. /package/dist/{pipeline-YROAY6DT.js.map → parity-AEKYSLN6.js.map} +0 -0
  259. /package/dist/{prospect-audits-RVYEA4KH.js.map → pipeline-UUPXOS2J.js.map} +0 -0
  260. /package/dist/{preflight-4VQP7JJL.js.map → preflight-POZZI3GN.js.map} +0 -0
  261. /package/dist/{prismic-models-W6CNLTWH.js.map → prismic-models-URYGODNY.js.map} +0 -0
  262. /package/dist/{render-from-row-IFSBHWDH.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
  263. /package/dist/{websites-TB4TCAWM.js.map → render-from-row-65DC6ZP3.js.map} +0 -0
  264. /package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-ONJMBUHJ.js.map} +0 -0
  265. /package/dist/{selftest-MAB2PBXQ.js.map → selftest-AFLR7YV7.js.map} +0 -0
  266. /package/dist/{submissions-5GKJPCIH.js.map → submissions-II5J35QM.js.map} +0 -0
  267. /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-QWGIRNN6.js.map} +0 -0
  268. /package/dist/{sync-K3PKHVLH.js.map → sync-NKPDCMYW.js.map} +0 -0
  269. /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-HGLQV55H.js.map} +0 -0
  270. /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-WZ44IARG.js.map} +0 -0
  271. /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
  272. /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → websites-6DHIHJ2D.js.map} +0 -0
@@ -1,1708 +0,0 @@
1
- import {
2
- PRETTIER_FLAG_NOTE,
3
- formatWithPrettier,
4
- resolveTargetPrettier
5
- } from "./chunk-R53VL3RI.js";
6
- import {
7
- withRecipe
8
- } from "./chunk-ZQV7UQNE.js";
9
- import {
10
- defaultSpawn
11
- } from "./chunk-LBYOLBW7.js";
12
- import {
13
- ignoreRulesFor,
14
- pathsMissingFromHead
15
- } from "./chunk-AVEQUR44.js";
16
-
17
- // src/recipes/match-harness/index.ts
18
- import { mkdir, readFile, rm, rmdir, writeFile } from "fs/promises";
19
- import { dirname, join } from "path";
20
-
21
- // src/recipes/match-harness/template.ts
22
- var HARNESS_MJS_RELATIVE = "matching/harness.mjs";
23
- var HARNESS_MJS_TEMPLATE = `// The single source for everything the matching gates need to know about this
24
- // site. DATA lives in matching/harness.json (site-edited); this file is the
25
- // READ LAYER \u2014 edit harness.json, not this. (It is installed and upgraded by
26
- // the \`reddoor-maint match-harness\` recipe, which owns these bytes: a hand
27
- // edit here is flagged on the next run and never silently overwritten.)
28
- //
29
- // It exists because the page table, the two hosts, the matrix, the threshold
30
- // and the skill path were hand-copied all over matching/. Re-measured
31
- // 2026-09-09 AFTER the six-probe conversion, over the 216 tracked scripts under
32
- // matching/ (214 top-level \u2014 212 .mjs + 2 .sh \u2014 plus 2 in states/):
33
- //
34
- // \u2022 a hand-typed copy of the page table (three or more gate keys sitting next
35
- // to their route): 8 files. Exactly ONE of them, probe-chrome-count.mjs,
36
- // still carries all nine rows; probe-anchors.mjs carries five; the other
37
- // six are three-row detail triples (team/svc/qa).
38
- // \u2022 the skill path (~/.claude/skills/matching-a-page): 193 copies
39
- // \u2022 the viewport matrix (1440/834/390): 51 copies
40
- // \u2022 REF pointed at a host listed in selfHosts \u2014 i.e. comparing the candidate
41
- // with itself: 12 scripts, one of which (probe-chrome-count.mjs) is also
42
- // the last nine-row table carrier
43
- //
44
- // The first bullet read "the nine-row page table: 5 copies \u2014 gate.sh,
45
- // probe-anchor-parity.mjs, sweep-all10.sh, sweep-all16.sh, sweep-final.sh" when
46
- // it was written here at 922dde3. It was wrong within the hour and wrong on two
47
- // counts: 4e2cd7b took the table out of gate.sh, and the list never named
48
- // states/index.mjs or probe-chrome-count.mjs, which were both carrying nine-row
49
- // copies at the time. A census is a claim about code; it has to be measured
50
- // against the tree, not recalled.
51
- //
52
- // node matching/harness.mjs --env shell-safe KEY='value' lines
53
- // node matching/harness.mjs --table key<TAB>ref<TAB>cand<TAB>anchors
54
- // node matching/harness.mjs --check-ref the D11 preflight; exit 2 on failure
55
- import { readFileSync, realpathSync } from "node:fs";
56
- import { homedir } from "node:os";
57
- import { join } from "node:path";
58
- import { fileURLToPath, pathToFileURL } from "node:url";
59
-
60
- // fileURLToPath, not URL#pathname: pathname is percent-encoded, so a checkout
61
- // under a directory with a space in it would resolve to a path that does not
62
- // exist.
63
- const DIR = fileURLToPath(new URL(".", import.meta.url));
64
- const CFG = JSON.parse(readFileSync(join(DIR, "harness.json"), "utf8"));
65
-
66
- // Env overrides exist for one-off probes only. They are NOT how a site is
67
- // configured \u2014 harness.json is, so that what a gate ran against is committed.
68
- export const REF = process.env.MATCH_REF ?? CFG.ref;
69
- export const CAND = process.env.MATCH_CAND ?? process.env.CAND_BASE ?? CFG.cand;
70
- export const MATRIX = CFG.matrix;
71
- export const THRESHOLD = CFG.threshold;
72
- export const MAX_HEIGHT_DELTA = CFG.maxHeightDelta;
73
- export const REF_MARK = CFG.refMark;
74
- export const CAND_MARK = CFG.candMark;
75
- export const SELF_HOSTS = CFG.selfHosts ?? [];
76
-
77
- /** One record per gated page, in harness.json order. \`key\` is the gate key
78
- * (out-<TAG>-<key>, the SPEC heading, spec-sections/<key>.md); \`uid\` is the
79
- * Prismic uid or null where the page has no /dev/match twin. */
80
- export const PAGES = Object.entries(CFG.pages).map(([key, p]) => ({ key, ...p }));
81
- export const byKey = Object.fromEntries(PAGES.map((p) => [p.key, p]));
82
-
83
- // DERIVED, never hand-typed: page-diff cuts one region before the first anchor
84
- // ("top") plus one per anchor, at every viewport. The old hand-written map went
85
- // stale the moment an anchor list changed, and a wrong denominator makes the
86
- // score a lie in the flattering direction.
87
- export const TOTALS = Object.fromEntries(
88
- PAGES.map((p) => [p.key, (p.anchors.length + 1) * MATRIX.length]),
89
- );
90
-
91
- /** The SPEC.md heading predicate, shared by gate.sh's preflight and
92
- * build-spec.mjs so a section can never build fine and then refuse at the
93
- * gate. Matches the key followed by any non-key character (\`## team\` matches,
94
- * \`## teamfoo\` does not, \`## our-team\` cannot match \`team\`). */
95
- export const specHeadingRe = (key) => new RegExp(\`^##+ +\${key}([^A-Za-z0-9_-]|$)\`, "m");
96
-
97
- export const SKILL_DIR =
98
- process.env.MATCHING_SKILL_DIR ?? join(homedir(), ".claude/skills/matching-a-page");
99
- export const PD = join(SKILL_DIR, "page-diff.mjs");
100
- export const SC = join(SKILL_DIR, "style-census.mjs");
101
- export const PLAYWRIGHT = pathToFileURL(join(SKILL_DIR, "node_modules/playwright/index.mjs")).href;
102
-
103
- /** The report format this site's scripts can read, so gate.sh can compare it
104
- * with \`page-diff --version\` before spending a run and next.mjs can refuse
105
- * rather than quietly drop a page whose newest report came from another
106
- * schema. Both do that now \u2014 gate.sh preflights \`page-diff --version\` against
107
- * this value before spending a run, and next.mjs counts a foreign-schema
108
- * report as MISSING rather than skipping it. (This said "neither does that
109
- * yet" \u2014 true at 922dde3 where it was written, false from 4e2cd7b, which
110
- * gave gate.sh the preflight and did not come back here.) Checked 2026-09-09
111
- * against the installed skill: \`page-diff --version\` \u2192 \`page-diff 0.1.0
112
- * report-schema 1\`. */
113
- export const REPORT_SCHEMA = 1;
114
-
115
- /**
116
- * Fail-closed reference preflight. A 200 is NOT evidence: a host that has been
117
- * repointed at our own build answers 200, and so does a staging host serving a
118
- * 404 page. Both have happened on a real site \u2014 see the dated measurement in
119
- * LEDGER.md. A pass here requires an artefact only the reference produces.
120
- */
121
- export async function checkRef() {
122
- if (!REF_MARK) {
123
- return {
124
- ok: false,
125
- why: "harness.json refMark is empty \u2014 set it to a string only the reference serves (a Webflow site id, a build hash). A 200 is not evidence.",
126
- };
127
- }
128
- const host = new URL(REF).host;
129
- if (SELF_HOSTS.includes(host)) return { ok: false, why: \`REF host \${host} is in selfHosts\` };
130
- if (host === new URL(CAND).host) return { ok: false, why: \`REF host \${host} equals CAND's host\` };
131
- let res;
132
- try {
133
- res = await fetch(\`\${REF}/\`, { redirect: "manual" });
134
- } catch (e) {
135
- return { ok: false, why: \`GET \${REF}/ failed: \${e.message}\` };
136
- }
137
- if (res.status !== 200)
138
- return { ok: false, why: \`GET \${REF}/ \u2192 HTTP \${res.status}, expected 200\` };
139
- const loc = res.headers.get("location");
140
- if (loc) return { ok: false, why: \`GET \${REF}/ \u2192 \${res.status} redirect to \${loc}\` };
141
- const body = await res.text();
142
- if (!body.includes(REF_MARK))
143
- return {
144
- ok: false,
145
- why: \`\${REF}/ served 200 but WITHOUT refMark \${JSON.stringify(REF_MARK)} \u2014 that is not the reference\`,
146
- };
147
- if (CAND_MARK && body.includes(CAND_MARK))
148
- return {
149
- ok: false,
150
- why: \`\${REF}/ contains candMark \${JSON.stringify(CAND_MARK)} \u2014 REF is serving OUR build\`,
151
- };
152
- return { ok: true, why: \`\${REF}/ \u2192 200, no redirect, refMark present, candMark absent\` };
153
- }
154
-
155
- // CLI. Both sides go through realpathSync. \`import.meta.url\` is ALREADY the
156
- // resolved real path (node resolves symlinks unless --preserve-symlinks) while
157
- // process.argv[1] is the path as typed, so a plain pathToFileURL compare goes
158
- // false the moment any component of the invoked path is a symlink \u2014 and then
159
- // the CLI prints nothing and exits 0, which every caller reads as success.
160
- // page-diff.mjs:184-189 records exactly that defect and the same fix: "The
161
- // pathToFileURL compare that replaced the old template string is still false
162
- // whenever ANY component of the invoked path is a symlink \u2014 which is how this
163
- // skill is installed now (~/.claude/skills/matching-a-page -> the claude-skills
164
- // checkout). isMain() resolves the real path on both sides."
165
- const isMain = () => {
166
- if (!process.argv[1]) return false;
167
- try {
168
- return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
169
- } catch {
170
- return false;
171
- }
172
- };
173
-
174
- if (isMain()) {
175
- const mode = process.argv[2];
176
- const q = (v) => \`'\${String(v).replace(/'/g, \`'\\\\''\`)}'\`;
177
- if (mode === "--env") {
178
- const pairs = [
179
- ["REF", REF],
180
- ["CAND", CAND],
181
- ["MATRIX", MATRIX.join(",")],
182
- ["VIEWPORTS_SP", MATRIX.join(" ")],
183
- ["THRESHOLD", THRESHOLD],
184
- ["MAX_HEIGHT_DELTA", MAX_HEIGHT_DELTA],
185
- ["PD", PD],
186
- ["SC", SC],
187
- ["REPORT_SCHEMA", REPORT_SCHEMA],
188
- ];
189
- for (const [k, v] of pairs) console.log(\`\${k}=\${q(v)}\`);
190
- } else if (mode === "--table") {
191
- for (const p of PAGES) console.log([p.key, p.ref, p.cand, p.anchors.join(",")].join("\\t"));
192
- } else if (mode === "--check-ref") {
193
- const r = await checkRef();
194
- console.log(\`\${r.ok ? "REF OK" : "REF REFUSED"} \u2014 \${r.why}\`);
195
- process.exit(r.ok ? 0 : 2);
196
- } else {
197
- console.error("usage: harness.mjs --env | --table | --check-ref");
198
- process.exit(2);
199
- }
200
- }
201
- `;
202
- var GATE_SH_RELATIVE = "matching/gate.sh";
203
- var GATE_SH_TEMPLATE = `#!/usr/bin/env bash
204
- # The matching gate. This file is generic \u2014 everything specific to a site lives
205
- # in matching/harness.json (the data) and matching/LEDGER.md (the why). It is
206
- # installed and upgraded by the \`reddoor-maint match-harness\` recipe, which
207
- # owns these bytes: a hand edit here is flagged on the next run, never
208
- # silently overwritten.
209
- #
210
- # bash matching/gate.sh <round-tag> [page ...]
211
- #
212
- # Runs page-diff for every page in the table (or just the named ones) at the
213
- # full breakpoint matrix and writes matching/out-<round-tag>-<page>/.
214
- #
215
- # The matrix, the anchor lists, the reference and the candidate are DATA. Why a
216
- # site chose them \u2014 which live breakpoint band hid what, why an anchor is a
217
- # heading and not a button label \u2014 belongs in matching/LEDGER.md, which is dated
218
- # and append-only, because JSON holds no comments.
219
- #
220
- # NO MASKS and the threshold from harness.json everywhere: the numbers stay
221
- # honest and a known floor stays visible as its own region. A media-neutralised
222
- # secondary read is \`node "$PD" ... --neutralize-media\`; next.mjs ignores such
223
- # runs on purpose (next.mjs:57-64).
224
- #
225
- # PREFLIGHT (see the matching rules in CLAUDE.md): a page with no section in
226
- # matching/SPEC.md has not had Phase 1 done, and its geometry must not be
227
- # touched. Skipping the spec is how a reference's root-font ladder and its
228
- # per-component height ladders get found reactively, after the region has
229
- # already failed several rounds. This refuses the run instead of trusting anyone
230
- # to remember.
231
- set -u
232
- # Everything configurable lives in matching/harness.json; harness.mjs is the one
233
- # reader. --env emits shell-safe assignments (REF, CAND, MATRIX, VIEWPORTS_SP,
234
- # THRESHOLD, MAX_HEIGHT_DELTA, PD, SC, REPORT_SCHEMA).
235
- eval "$(node "$(dirname "$0")/harness.mjs" --env)"
236
-
237
- # The skill must be able to write reports this site's scripts can read. Cheap,
238
- # local, and it fails before any browser starts.
239
- PD_SCHEMA="$(node "$PD" --version 2>/dev/null | awk '{print $4}')"
240
- if [ "$PD_SCHEMA" != "$REPORT_SCHEMA" ]; then
241
- echo "gate.sh: page-diff writes report schema '\${PD_SCHEMA:-none}', this harness reads $REPORT_SCHEMA." >&2
242
- echo " Update matching/harness.mjs REPORT_SCHEMA or the matching-a-page skill." >&2
243
- exit 2
244
- fi
245
- SPEC="$(dirname "$0")/SPEC.md"
246
- TAG="\${1:?usage: gate.sh <round-tag> [page ...]}"
247
- # The tag must not contain a hyphen. Output dirs are "out-<TAG>-<page>", and
248
- # next.mjs recovers the page with /^out-[^-]+-(.+)$/ \u2014 it splits on the FIRST
249
- # hyphen, so a tag like "r-forms-2026-08-07" yields the page key
250
- # "forms-2026-08-07-yfv" and that run is silently never counted. It cannot split
251
- # on the last hyphen instead, because page keys have hyphens of their own
252
- # ("our-team", "ask-the-doctor"). Failing here is the cheap end of that: a
253
- # mis-tagged round otherwise LOOKS green because next.mjs keeps reading an older
254
- # report for the page you just changed. (Cost this once, 2026-08-07.)
255
- case "$TAG" in
256
- *-*)
257
- echo "gate.sh: round tag must not contain a hyphen (got '$TAG')." >&2
258
- echo " out-<TAG>-<page> is parsed on the first hyphen, so a" >&2
259
- echo " hyphenated tag hides the run from next.mjs. Try '\${TAG//-/}'." >&2
260
- exit 2
261
- ;;
262
- esac
263
- shift || true
264
- WANT=("$@")
265
-
266
- # Fail closed on the reference before spending a single run. A 200 is NOT
267
- # evidence: a production host that has cut over to OUR build answers 200, and
268
- # every region then scores near zero against itself. --check-ref requires an
269
- # artefact only the reference serves (harness.json refMark) and refuses a
270
- # redirect, a host listed in selfHosts, and a body carrying candMark. Measured
271
- # on the site this harness was cut from, 2026-09-09 AFTER the consolidation:
272
- # 12 of its 214 top-level matching scripts still assign REF a host listed in
273
- # selfHosts \u2014 i.e. compare the candidate with itself. It was 33 of 229 before.
274
- # (This comment read "33 of its 230" while sitting in the post-consolidation
275
- # tree, contradicting harness.mjs's 12 three files away.)
276
- if ! node "$(dirname "$0")/harness.mjs" --check-ref; then
277
- echo "gate.sh: refusing to gate against an unverified reference." >&2
278
- exit 2
279
- fi
280
-
281
- # Set SPEC_OPTIONAL=1 only for a read-only baseline sweep of pages you are not
282
- # about to edit. It is recorded in the round tag so the exemption is visible.
283
- SPEC_OPTIONAL="\${SPEC_OPTIONAL:-0}"
284
-
285
- has_spec() { # page
286
- # NB: the obvious \`( |$|\\b)\` guard is rejected by ugrep as an empty
287
- # subexpression, and a preflight that errors out fails CLOSED \u2014 it refused all
288
- # 9 pages while SPEC.md was complete. Match the page key followed by any
289
- # non-key character (so \`## team\` matches but \`## teamfoo\` does not; \`##
290
- # our-team\` cannot match \`team\` because the key must follow the spaces).
291
- [ -f "$SPEC" ] && grep -qE "^##+ +$1([^A-Za-z0-9_-]|$)" "$SPEC"
292
- }
293
-
294
- run() { # tag refpath candpath sections
295
- local page="$1" refpath="$2" candpath="$3" sections="$4"
296
- if [ \${#WANT[@]} -gt 0 ]; then
297
- local hit=0
298
- for w in "\${WANT[@]}"; do [ "$w" = "$page" ] && hit=1; done
299
- [ $hit -eq 1 ] || return 0
300
- fi
301
- if ! has_spec "$page"; then
302
- if [ "$SPEC_OPTIONAL" = "1" ]; then
303
- echo "########## $page ##########"
304
- echo "WARNING: no '## $page' section in matching/SPEC.md \u2014 Phase 1 not done."
305
- echo " Running anyway because SPEC_OPTIONAL=1 (baseline read only)."
306
- echo " Do NOT apply geometry fixes off this run."
307
- else
308
- echo "########## $page ##########"
309
- echo "REFUSED: no '## $page' section in matching/SPEC.md."
310
- echo " Phase 1 (section census + per-section spec, read from"
311
- echo " matching/spec/) comes before geometry."
312
- echo " See CLAUDE.md rule 2. SPEC_OPTIONAL=1 for a baseline read."
313
- FAILED_PREFLIGHT=1
314
- return 0
315
- fi
316
- fi
317
- echo "########## $page ##########"
318
- node "$PD" --ref "$REF$refpath" --cand "$CAND$candpath" \\
319
- --viewports "$MATRIX" --threshold "$THRESHOLD" \\
320
- --sections "$sections" --out "matching/out-$TAG-$page" \\
321
- > "matching/out-$TAG-$page.log" 2>&1
322
- echo "$page exit=$?"
323
- }
324
-
325
- # The page table is matching/harness.json. Process substitution, NOT a pipe:
326
- # a pipe would run this loop in a subshell and FAILED_PREFLIGHT would not
327
- # survive to the check below, so a refused page would exit 0.
328
- while IFS=$'\\t' read -r key refpath candpath anchors; do
329
- run "$key" "$refpath" "$candpath" "$anchors"
330
- done < <(node "$(dirname "$0")/harness.mjs" --table)
331
-
332
- if [ "\${FAILED_PREFLIGHT:-0}" = "1" ]; then
333
- echo
334
- echo "GATE INCOMPLETE ($TAG) \u2014 one or more pages were refused for a missing"
335
- echo "SPEC.md section. Those pages have NOT been measured; do not report a"
336
- echo "score for them."
337
- exit 2
338
- fi
339
- echo "ALL DONE ($TAG)"
340
- `;
341
- var CENSUS_SH_RELATIVE = "matching/census.sh";
342
- var CENSUS_SH_TEMPLATE = `#!/usr/bin/env bash
343
- # Phase 3 style gate \u2014 the mechanical net for the type spec.
344
- #
345
- # bash matching/census.sh # all 9 pages, all 3 viewports
346
- # bash matching/census.sh home yfv # just these
347
- #
348
- # style-census diffs the computed type tuple (family, weight, size,
349
- # line-height, letter-spacing, transform, colour) of every text snippet present
350
- # on BOTH pages. It is the only gate that catches the 11px footer line and the
351
- # cyan-vs-teal link that page-diff is structurally blind to \u2014 a region can be
352
- # pixel-clean at 0.10 and still be wearing the wrong colour on small text.
353
- #
354
- # Prints a per-page/per-viewport mismatch count and leaves the full runs in
355
- # matching/census-<page>-<vw>.log. Exits 1 while any count is non-zero, and 2
356
- # when it cannot honestly report a count at all.
357
- #
358
- # EVIDENCE \u2014 read before adding a guard here, or removing one. A green here
359
- # has to be produced by a census that RAN. It was not: with style-census.mjs
360
- # absent this gate printed "Phase 3 CLEAN \u2014 0 undeclared type mismatches" and
361
- # exited 0. Every run died on module resolution, census-count.mjs read each
362
- # crash log as "0 0 0" (it counts \` y=\` rows and a stack trace has none \u2014
363
- # census-count.mjs:35-56), and no rows was reported as no mismatches. Measured
364
- # again with style-census.mjs PRESENT and the candidate host down: identical
365
- # output, because style-census exits 1 both when it finds mismatches
366
- # (style-census.mjs:187) and when it dies before it looks \u2014 so the exit status
367
- # cannot tell a finding from a crash and may not grant a green either.
368
- #
369
- # The guards below are what next.mjs:83-89 (no parseable gate run \u2014 refusing to
370
- # report a score), strikes.mjs:87-97 (refusing to report "clear") and
371
- # gate.sh:37-42 (preflight page-diff before spending a run) already do. Each
372
- # demands an artefact only a working census produces; none of them can turn a
373
- # red green.
374
- set -uo pipefail
375
- # Resolved BEFORE the cd. $0 is the path as TYPED, so a relatively invoked
376
- # census.sh that resolved the harness afterwards would look for it under
377
- # whatever root the cd landed on.
378
- HARNESS="$(cd "$(dirname "$0")" && pwd)/harness.mjs"
379
- cd "$(dirname "$0")/.."
380
-
381
- # Everything configurable lives in matching/harness.json; harness.mjs is the one
382
- # reader. --env supplies REF, CAND, SC and VIEWPORTS_SP \u2014 this gate held its own
383
- # copy of all four, and its REF went stale when production cut over to our own
384
- # build (LEDGER "REFERENCE MOVED", verified 2026-08-10) while gate.sh's was
385
- # repointed the same day.
386
- eval "$(node "$HARNESS" --env)"
387
- NODE="\${NODE:-node}"
388
-
389
- # The matrix is DATA, read as VIEWPORTS_SP, the way gate.sh reads MATRIX. The
390
- # old VIEWPORTS override is retired rather than silently ignored: a census run
391
- # over a matrix harness.json does not name is a column set the pixel gate never
392
- # measured, and it would look exactly like a clean one.
393
- if [ -n "\${VIEWPORTS:-}" ]; then
394
- echo "census.sh: the VIEWPORTS override is retired \u2014 the matrix is data now." >&2
395
- echo " Edit \\"matrix\\" in matching/harness.json, then unset it." >&2
396
- exit 2
397
- fi
398
-
399
- # GUARD 1 \u2014 the tool is there and it starts. gate.sh compares \`page-diff
400
- # --version\` with the harness's REPORT_SCHEMA before spending a run
401
- # (gate.sh:37-42); style-census has no --version to compare (page-diff.mjs
402
- # defines one at 191-195; nothing in style-census.mjs writes one), so the
403
- # artefact demanded here is the other one only it produces: the usage banner it
404
- # prints on exit 2 when --ref/--cand are missing (style-census.mjs:151-157).
405
- # That banner is not free \u2014 \`import { chromium } from "playwright"\` runs first
406
- # (style-census.mjs:19) \u2014 so a skill directory with no playwright installed
407
- # fails HERE, once, instead of writing one crash log per page per viewport that
408
- # every reader downstream counts as zero.
409
- if [ ! -f "$SC" ]; then
410
- echo "census.sh: no style-census at $SC \u2014 refusing to report a count." >&2
411
- echo " Install the matching-a-page skill, or point MATCHING_SKILL_DIR" >&2
412
- echo " at a checkout that has style-census.mjs." >&2
413
- exit 2
414
- fi
415
- SC_USAGE="$("$NODE" "$SC" 2>&1)"
416
- case "$SC_USAGE" in
417
- *"style-census"*"--ref"*"--cand"*"--vw"*) ;;
418
- *)
419
- echo "census.sh: $SC did not answer with a style-census --ref/--cand/--vw usage" >&2
420
- echo " banner \u2014 refusing to report a count. It printed:" >&2
421
- echo " \${SC_USAGE:-(nothing at all)}" >&2
422
- exit 2
423
- ;;
424
- esac
425
-
426
- declare -a WANT=("$@")
427
-
428
- # page -> "refpath candpath": the same table gate.sh drives, from the same file.
429
- # cut drops the anchors column, which the style census has no argument for
430
- # (style-census.mjs: --ref <url> --cand <url> [--vw 1440]).
431
- pages() { node "$HARNESS" --table | cut -f1-3; }
432
-
433
- TOTAL=0
434
- AMB=0
435
- DECL=0
436
- ROWS=0
437
- RUNS=0
438
- BROKEN=0
439
- SEEN=""
440
- # One column per viewport, from the matrix. Flush with the data rows below, at
441
- # last: the old header hardcoded four fixed fields separated by a literal space,
442
- # which the rows do not have, so its labels sat 1/2/3 columns right of them.
443
- printf '%-10s' page
444
- for vw in $VIEWPORTS_SP; do printf '%8s' "$vw"; done
445
- printf '\\n'
446
- while read -r page refpath candpath; do
447
- [ -z "$page" ] && continue
448
- SEEN="$SEEN $page"
449
- if [ \${#WANT[@]} -gt 0 ]; then
450
- hit=0
451
- for w in "\${WANT[@]}"; do [ "$w" = "$page" ] && hit=1; done
452
- [ $hit -eq 1 ] || continue
453
- fi
454
- ROWS=$((ROWS + 1))
455
- line=$(printf '%-10s' "$page")
456
- for vw in $VIEWPORTS_SP; do
457
- log="matching/census-$page-$vw.log"
458
- RUNS=$((RUNS + 1))
459
- "$NODE" "$SC" --ref "$REF$refpath" --cand "$CAND$candpath" --vw "$vw" >"$log" 2>&1
460
- # GUARD 2 \u2014 this run produced a census. The header and the counts line are
461
- # written together at style-census.mjs:166-169 and only after both pages
462
- # have been walked, so they are the artefact a completed run leaves and a
463
- # crashed one cannot. The viewport is read back out of the header because a
464
- # --vw that did not take would otherwise fill three differently named logs
465
- # with the same 1440 census. \`ref runs\`/\`cand runs\` must be non-zero: two
466
- # pages that rendered no text at all agree perfectly and mean nothing.
467
- if ! grep -qE "^=== style census, viewport $vw ===\\$" "$log" ||
468
- ! grep -qE "^ref runs: [1-9][0-9]* +cand runs: [1-9][0-9]* +mismatches: [0-9]+ +ambiguous: [0-9]+\\$" "$log"; then
469
- BROKEN=$((BROKEN + 1))
470
- echo "census.sh: $page @$vw produced no usable census \u2014 see $log" >&2
471
- line="$line$(printf '%8s' '!!')"
472
- continue
473
- fi
474
- # census-count splits the log three ways: REAL mismatches, AMBIGUOUS
475
- # same-text collisions (style-census's own split), and DECLARED rows the
476
- # operator has already ruled on (matching/census-deviations.mjs, the same
477
- # contract as floors.mjs). Only the first is outstanding work \u2014 without the
478
- # third, this gate can never reach zero and its number means nothing.
479
- read -r n a d <<<"$("$NODE" matching/census-count.mjs "$log")"
480
- # GUARD 2b \u2014 three integers, or the counter did not count. census-count.mjs
481
- # is the reader that turned a stack trace into "0 0 0"; a run of it that
482
- # dies (a broken census-deviations.mjs, a bad path) prints nothing at all,
483
- # and bash reads an empty $n as zero in the arithmetic below.
484
- counted=1
485
- for v in "\${n:-}" "\${a:-}" "\${d:-}"; do
486
- case "$v" in "" | *[!0-9]*) counted=0 ;; esac
487
- done
488
- if [ "$counted" -eq 0 ]; then
489
- BROKEN=$((BROKEN + 1))
490
- echo "census.sh: census-count.mjs did not return three counts for $log" >&2
491
- line="$line$(printf '%8s' '!!')"
492
- continue
493
- fi
494
- # GUARD 2c \u2014 the number reported is the number the census MEASURED. GUARD 2
495
- # matched the counts line for PRESENCE and threw its numbers away, so the
496
- # count printed below came from census-count's row parse and nothing ever
497
- # compared the two. Measured 2026-09-09: a COMPLETE census whose counts line
498
- # said "mismatches: 3", whose \`y=\` rows carried one leading space instead of
499
- # two (census-count.mjs:39 matches /^ {2}y=/), was reported as 0 and this
500
- # gate printed "Phase 3 CLEAN \u2014 0 undeclared type mismatches", exit 0. The
501
- # printer lives in the SKILL (style-census.mjs:171-173) and the parser is
502
- # copied into every site, so the two version independently and drift needs
503
- # nobody to touch either repo.
504
- #
505
- # The identity is exact for mismatches: style-census prints at most 100 rows
506
- # and states the remainder on its own line (style-census.mjs:175-177), so
507
- # parsed + remainder == reported. Ambiguous rows are capped the same way
508
- # (:184) but the remainder is NOT stated, so only a floor is checkable
509
- # there \u2014 any is not none, and the parse may never exceed the report.
510
- counts=$(grep -E "^ref runs: [1-9][0-9]* +cand runs: [1-9][0-9]* +mismatches: [0-9]+ +ambiguous: [0-9]+$" "$log" | tail -1)
511
- said_m=\${counts##*mismatches: }
512
- said_m=\${said_m%% *}
513
- said_a=\${counts##*ambiguous: }
514
- more=$(sed -n 's/^ *\u2026 and \\([0-9][0-9]*\\) more (truncated print, all counted)$/\\1/p' "$log" | tail -1)
515
- if [ $((n + d + \${more:-0})) -ne "$said_m" ] ||
516
- [ "$a" -gt "$said_a" ] ||
517
- { [ "$said_a" -gt 0 ] && [ "$a" -eq 0 ]; }; then
518
- BROKEN=$((BROKEN + 1))
519
- echo "census.sh: $log reports mismatches: $said_m ambiguous: $said_a, but" >&2
520
- echo " census-count.mjs read $((n + d)) mismatch row(s)\${more:+ (+ $more truncated)} and $a ambiguous." >&2
521
- echo " The census and its reader disagree; there is no count to report." >&2
522
- line="$line$(printf '%8s' '!!')"
523
- continue
524
- fi
525
- AMB=$((AMB + a))
526
- DECL=$((DECL + d))
527
- TOTAL=$((TOTAL + n))
528
- line="$line$(printf '%8s' "$n")"
529
- done
530
- echo "$line"
531
- done < <(pages)
532
-
533
- echo
534
- # GUARD 3 \u2014 something was actually censused. Process substitution above, NOT a
535
- # pipe: a pipe runs the loop in a subshell and every counter here would still
536
- # read 0, which is exactly the green this file exists to stop (gate.sh:123-125
537
- # records the same trap for FAILED_PREFLIGHT). A page name that matches nothing
538
- # must not report CLEAN either \u2014 strikes.mjs:147-156 is the same refusal, and
539
- # it was written because a typo silently greened rule 3 on six of nine pages.
540
- if [ "$ROWS" -eq 0 ]; then
541
- if [ -n "$SEEN" ]; then
542
- echo "census.sh: \\"\${WANT[*]:-}\\" matches no page \u2014 refusing to report CLEAN." >&2
543
- echo " known pages:$SEEN" >&2
544
- else
545
- echo "census.sh: the page table is empty \u2014 refusing to report CLEAN." >&2
546
- echo " node matching/harness.mjs --table printed no rows; check" >&2
547
- echo " \\"pages\\" in matching/harness.json." >&2
548
- fi
549
- exit 2
550
- fi
551
- if [ "$BROKEN" -gt 0 ]; then
552
- echo "CENSUS INCOMPLETE \u2014 $BROKEN of $RUNS run(s) produced no usable census (marked !!)."
553
- echo "Those page/viewport pairs have NOT been measured; there is no count to"
554
- echo "report for them, and the totals below would be a floor, not a score."
555
- echo "Full runs: matching/census-<page>-<vw>.log"
556
- exit 2
557
- fi
558
- if [ "$TOTAL" -eq 0 ]; then
559
- echo "Phase 3 CLEAN \u2014 0 undeclared type mismatches ($DECL declared, $AMB ambiguous)."
560
- echo "($RUNS censused run(s) over $ROWS page(s), each one counted.)"
561
- [ "$AMB" -gt 0 ] && echo "($AMB ambiguous same-text rows remain: each needs ADJUDICATING," &&
562
- echo " not fixing \u2014 our element matches, another sharing its text does not.)"
563
- exit 0
564
- fi
565
- echo "$TOTAL type mismatch(es) remain (+ $AMB ambiguous, $DECL declared)."
566
- echo "Full runs: matching/census-<page>-<vw>.log"
567
- echo "A mismatch is a defect or a ledgered deviation. An ambiguous row is neither"
568
- echo "until you look: the census keys on TEXT, so two different elements sharing a"
569
- echo "string land under one key and only one of them may be wrong."
570
- exit 1
571
- `;
572
- var NEXT_MJS_RELATIVE = "matching/next.mjs";
573
- var NEXT_MJS_TEMPLATE = `// What is still broken, ranked. Exits 1 while work remains.
574
- //
575
- // node matching/next.mjs
576
- //
577
- // Round protocol step 0 (repo CLAUDE.md rule 5). A commit is a CHECKPOINT, not
578
- // a stopping point: after committing, run this. If it exits 1 there is a named
579
- // next action and the round continues without handing control back.
580
- //
581
- // Reads the most recent gate log per page rather than the whole corpus, so it
582
- // reflects HEAD rather than history (that is strikes.mjs's job).
583
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
584
- import { join } from "node:path";
585
-
586
- const DIR = new URL(".", import.meta.url).pathname;
587
-
588
- // PAUSE SWITCH. While matching/PAUSED exists this hands out no agenda and
589
- // exits 0. It is deliberately the FIRST thing that runs: no report is read, no
590
- // score is printed, nothing tempting is put on screen to argue with.
591
- //
592
- // Why an exit code and not a note somewhere: rule 5 is a LOOP \u2014 "after
593
- // committing, run next.mjs; while it exits 1 there is a named next action and
594
- // the round continues". A pause written as prose loses to that loop, because
595
- // the loop is mechanical and the prose is not. Exiting 0 satisfies rule 5
596
- // truthfully rather than suspending it: there is no next action.
597
- const PAUSE = join(DIR, "PAUSED");
598
- if (existsSync(PAUSE)) {
599
- console.log("MATCHING PAUSED \u2014 no agenda, and none is to be inferred.\\n");
600
- console.log(readFileSync(PAUSE, "utf8").trimEnd());
601
- process.exit(0);
602
- }
603
-
604
- import { FLOORS, ACCEPTED } from "./floors.mjs";
605
- import { TOTALS, THRESHOLD, MAX_HEIGHT_DELTA, REPORT_SCHEMA } from "./harness.mjs";
606
-
607
- // Reports written by a different page-diff, by page key. Kept rather than
608
- // dropped: silently ignoring them is how a page vanishes from the score.
609
- const schemaMismatch = new Set();
610
-
611
- const latest = new Map();
612
- for (const d of readdirSync(DIR).filter((d) => d.startsWith("out-"))) {
613
- const m = /^out-[^-]+-(.+)$/.exec(d);
614
- if (!m || !TOTALS[m[1]]) continue;
615
- let report, mtime;
616
- try {
617
- const p = join(DIR, d, "report.json");
618
- report = JSON.parse(readFileSync(p, "utf8"));
619
- mtime = statSync(p).mtimeMs;
620
- } catch {
621
- continue;
622
- }
623
- // A masked / media-neutralised run is a DIAGNOSTIC, never the state of the
624
- // page. Picking one up as "latest" silently reports scores nobody can ship \u2014
625
- // it happened immediately: an --mask-photos probe of yfv made \`top\` @834 read
626
- // 43.9% here while the real gate had it passing at 1.3%.
627
- const meta = report.meta ?? {};
628
- // Missing schemaVersion means "written before the field existed" = 0. It is
629
- // not an error on its own; it is only fatal when it would blank a page.
630
- if ((meta.schemaVersion ?? 0) !== REPORT_SCHEMA) {
631
- schemaMismatch.add(m[1]);
632
- continue;
633
- }
634
- if (
635
- (meta.mask?.length ?? 0) > 0 ||
636
- meta.neutralizeMedia ||
637
- meta.maskPhotos ||
638
- meta.truncated
639
- )
640
- continue;
641
- if (meta.threshold !== THRESHOLD) continue;
642
- const prev = latest.get(m[1]);
643
- if (!prev || mtime > prev.mtime) latest.set(m[1], { dir: d, mtime, report });
644
- }
645
-
646
- const blanked = [...schemaMismatch].filter((p) => !latest.has(p));
647
- if (blanked.length) {
648
- console.error(
649
- \`next: \${blanked.length} page(s) have no run at report schema \${REPORT_SCHEMA} \u2014 \` +
650
- \`their newest reports came from a different page-diff (\${blanked.sort().join(", ")}).\\n\` +
651
- \` Re-run: bash matching/gate.sh <tag> \${blanked.sort().join(" ")}\`,
652
- );
653
- process.exit(2);
654
- }
655
- if (latest.size === 0) {
656
- console.error(
657
- "next: no parseable gate run under matching/ \u2014 refusing to report a score.\\n" +
658
- " Run bash matching/gate.sh <tag> first.",
659
- );
660
- process.exit(2);
661
- }
662
-
663
- const rows = [];
664
- const accepted = [];
665
- let openTotal = 0;
666
- let floorTotal = 0;
667
- for (const [page, { dir, report }] of latest) {
668
- const fails = report.regions.filter((r) => !r.pass);
669
- for (const f of fails) {
670
- const floor = FLOORS.find((fl) => fl.match(f, page));
671
- if (floor) {
672
- floorTotal++;
673
- continue;
674
- }
675
- const ack = ACCEPTED.find((a) => a.match(f, page));
676
- if (ack) {
677
- accepted.push({ page, vw: f.viewport, label: f.label, why: ack.why });
678
- continue;
679
- }
680
- openTotal++;
681
- rows.push({
682
- page,
683
- vw: f.viewport,
684
- label: f.label,
685
- mm: f.mismatchFraction,
686
- dh: f.heightDeltaFraction ?? 0,
687
- dir,
688
- });
689
- }
690
- }
691
-
692
- const scored = [...latest.entries()]
693
- .map(([p, v]) => ({
694
- p,
695
- pass: v.report.regions.filter((r) => r.pass).length,
696
- total: TOTALS[p],
697
- }))
698
- .sort((a, b) => a.pass / a.total - b.pass / b.total);
699
-
700
- const sum = scored.reduce((a, s) => a + s.pass, 0);
701
- const max = scored.reduce((a, s) => a + s.total, 0);
702
- console.log(\`SCORE \${sum}/\${max} regions passing\\n\`);
703
- console.log(
704
- scored
705
- .map((s) => \` \${s.p.padEnd(9)} \${String(s.pass).padStart(2)}/\${s.total}\`)
706
- .join("\\n"),
707
- );
708
-
709
- if (accepted.length) {
710
- console.log(\`\\nOperator-ACCEPTED failures (left failing on purpose):\`);
711
- for (const a of accepted)
712
- console.log(\` \${a.page} @\${a.vw} "\${a.label}" \u2014 \${a.why.slice(0, 96)}\u2026\`);
713
- }
714
-
715
- if (!rows.length) {
716
- console.log(
717
- \`\\nNo open geometry failures. \${floorTotal} declared floor(s) remain.\`,
718
- );
719
- console.log(
720
- "Backlog is empty \u2014 Phases 5 (states) and 6 (adversarial review) are what is left.",
721
- );
722
- process.exit(0);
723
- }
724
-
725
- // Worst page first, then worst region inside it: fix where the model is most wrong.
726
- const worst = scored[0].p;
727
- rows.sort(
728
- (a, b) =>
729
- (a.page === worst ? -1 : 0) - (b.page === worst ? -1 : 0) || b.mm - a.mm,
730
- );
731
-
732
- console.log(
733
- \`\\n\${openTotal} open failure(s) + \${floorTotal} declared floor(s).\`,
734
- );
735
- console.log(\`\\nNEXT: \${worst} \u2014 worst page. Its open regions:\\n\`);
736
- for (const r of rows.filter((r) => r.page === worst)) {
737
- const why = [];
738
- if (r.mm > THRESHOLD) why.push(\`pixels \${(r.mm * 100).toFixed(1)}%\`);
739
- if (Math.abs(r.dh) > MAX_HEIGHT_DELTA)
740
- why.push(\`height \${(r.dh * 100).toFixed(1)}%\`);
741
- console.log(
742
- \` @\${String(r.vw).padEnd(5)} \${r.label.slice(0, 44).padEnd(45)} \${why.join(" + ")}\`,
743
- );
744
- }
745
- console.log(\`\\nBefore treating any of these as geometry:\`);
746
- console.log(
747
- \` node matching/probe-anchor-parity.mjs \${worst} # is the gate cutting comparably?\`,
748
- );
749
- console.log(
750
- \` node matching/strikes.mjs \${worst} # has it stalled? then change the MODEL\`,
751
- );
752
- console.log(
753
- \`\\nRound continues. Do not hand back control with work outstanding.\`,
754
- );
755
- process.exit(1);
756
- `;
757
- var STRIKES_MJS_RELATIVE = "matching/strikes.mjs";
758
- var STRIKES_MJS_TEMPLATE = `// The 3-strike rule, made countable.
759
- //
760
- // node matching/strikes.mjs [page]
761
- //
762
- // The matching skill says: "A region that has not improved after 3 fix ->
763
- // re-run cycles: stop. Present the three attempts. Do not widen the threshold,
764
- // mask it, or reclassify it \u2014 a stuck region means your model of it is wrong."
765
- //
766
- // That rule was blown twice on this project (services \`top\`, yfv \`top\`) purely
767
- // because nobody was counting. This reads every matching/out-*/report.json,
768
- // reconstructs each region's history in chronological order, and prints the
769
- // regions that are OUT OF STRIKES. Exit 1 when any exist, so a round that
770
- // starts by running this cannot quietly attempt a fourth cycle.
771
- //
772
- // WHAT THIS ACTUALLY MEASURES (read before trusting a number): report.json
773
- // cannot record INTENT, so a "cycle" here is any completed gate run in which
774
- // the region was still failing and did not improve by IMPROVE_PP. Runs aimed at
775
- // a different region on the same page therefore count too. That makes this a
776
- // STALL detector, not a literal attempt counter \u2014 "this has been failing,
777
- // unchanged, across N gate runs while you were working on this page." That is
778
- // the more useful discipline number anyway, and it is honest about being an
779
- // upper bound. Improvement resets the count: real progress earns a fresh start.
780
- import { existsSync, readdirSync, readFileSync } from "node:fs";
781
- import { join } from "node:path";
782
- import { FLOORS } from "./floors.mjs";
783
- import {
784
- PAGES,
785
- THRESHOLD,
786
- MAX_HEIGHT_DELTA,
787
- REPORT_SCHEMA,
788
- } from "./harness.mjs";
789
-
790
- // PAUSE SWITCH \u2014 see matching/next.mjs for why this is an exit code. Round
791
- // protocol step 1 is "run strikes.mjs; the stalled regions are the agenda", so
792
- // this is the second door into the backlog and has to be shut too. Exits 0:
793
- // there are no strikes to spend when no round is being played.
794
- const PAUSED = join(new URL(".", import.meta.url).pathname, "PAUSED");
795
- if (existsSync(PAUSED)) {
796
- console.log("MATCHING PAUSED \u2014 no agenda, and none is to be inferred.\\n");
797
- console.log(readFileSync(PAUSED, "utf8").trimEnd());
798
- process.exit(0);
799
- }
800
-
801
- const IMPROVE_PP = 0.01; // 1 percentage point = the smallest move worth calling progress
802
- const MAX_STRIKES = 3;
803
- const ROOT = new URL(".", import.meta.url).pathname;
804
- const only = process.argv[2] ?? null;
805
-
806
- // The gate key for a report, from its ref path. Deriving it by string surgery
807
- // disagreed with the gate on exactly six of the nine rows \u2014 yfv, contact, atd,
808
- // svc, qa and team (measured 2026-09-09 against harness.json); the table knows.
809
- // keyOf (below) stays as the fallback for a run whose ref was rewritten.
810
- const pageOf = (ref) => {
811
- const path = new URL(ref).pathname.replace(/\\/$/, "") || "/";
812
- return PAGES.find((p) => p.ref === path)?.key ?? null;
813
- };
814
-
815
- const runs = [];
816
- const unreadable = [];
817
- for (const dir of readdirSync(ROOT).filter((d) => d.startsWith("out-"))) {
818
- let report;
819
- try {
820
- report = JSON.parse(readFileSync(join(ROOT, dir, "report.json"), "utf8"));
821
- } catch {
822
- continue; // not a completed run dir
823
- }
824
- if (!report.meta?.ref || !Array.isArray(report.regions)) continue;
825
- // A report from another schema is still usable for a STALL count as long as
826
- // it carries the two fields this reads. Anything else is counted and named,
827
- // never silently dropped \u2014 an under-counted history reads as "clear".
828
- const usable = report.regions.every(
829
- (r) =>
830
- typeof r.mismatchFraction === "number" && typeof r.pass === "boolean",
831
- );
832
- if (!usable) {
833
- unreadable.push(\`\${dir} (schema \${report.meta.schemaVersion ?? 0})\`);
834
- continue;
835
- }
836
- runs.push({
837
- dir,
838
- at: report.meta.generatedAt,
839
- meta: report.meta,
840
- regions: report.regions,
841
- });
842
- }
843
-
844
- // The failure mode of a stall detector is always "clear", which is exactly the
845
- // answer that stops nobody. A corpus this could not read is not a clean one.
846
- if (runs.length === 0) {
847
- console.error(
848
- \`strikes: no parseable gate run under matching/ \u2014 refusing to report "clear".\` +
849
- (unreadable.length
850
- ? \`\\n \${unreadable.length} report(s) unreadable at schema \${REPORT_SCHEMA}: \${unreadable.slice(0, 5).join(", ")}\`
851
- : ""),
852
- );
853
- process.exit(2);
854
- }
855
- if (unreadable.length) {
856
- console.error(
857
- \`strikes: ignored \${unreadable.length} unreadable report(s) \u2014 the history below is incomplete.\` +
858
- \`\\n \${unreadable.slice(0, 5).join(", ")}\`,
859
- );
860
- }
861
-
862
- runs.sort((a, b) => a.at.localeCompare(b.at));
863
-
864
- // The gate page KEY, recovered from the run dir the way next.mjs does it
865
- // (out-<TAG>-<page>, split on the first hyphen \u2014 the tag is hyphen-free by
866
- // gate.sh's own preflight). \`pageOf\` now returns that same key off the table,
867
- // so the two vocabularies agree and the round protocol in CLAUDE.md \u2014 "run
868
- // \`strikes.mjs <page>\` with the key you just passed to gate.sh", which until
869
- // 2026-08-13 silently matched nothing \u2014 works either way. This stays because
870
- // a dir key is not always a gate key: 69 of this corpus's 377 run dirs are
871
- // hand-named probes (out-band3, out-390masked), and FLOORS matches on it.
872
- const keyOf = (dir) => /^out-[^-]+-(.+)$/.exec(dir)?.[1] ?? null;
873
-
874
- // key -> chronological list of {dir, at, mm, pass, masked}
875
- const history = new Map();
876
- const seenNames = new Set();
877
- for (const run of runs) {
878
- const page = pageOf(run.meta.ref) ?? keyOf(run.dir) ?? "unknown";
879
- const gateKey = keyOf(run.dir);
880
- seenNames.add(page);
881
- if (gateKey) seenNames.add(gateKey);
882
- if (only && page !== only && gateKey !== only) continue;
883
- for (const r of run.regions) {
884
- // A DECLARED FLOOR is flat by definition \u2014 reporting it as stalled is noise
885
- // that hides a real stall. It stays in the LEDGER; it does not belong here.
886
- if (FLOORS.some((fl) => fl.match(r, gateKey || page))) continue;
887
- const key = \`\${page}|\${r.viewport}|\${r.label}\`;
888
- if (!history.has(key)) history.set(key, []);
889
- history.get(key).push({
890
- dir: run.dir,
891
- at: run.at,
892
- mm: r.mismatchFraction,
893
- dh: r.heightDeltaFraction,
894
- pass: r.pass,
895
- // a run with masks/neutralised media is not comparable to a clean one
896
- dirty:
897
- (run.meta.mask?.length ?? 0) > 0 ||
898
- run.meta.neutralizeMedia ||
899
- run.meta.maskPhotos,
900
- });
901
- }
902
- }
903
-
904
- // A name that matches no run must NOT report "clear". This check exists to stop
905
- // work on a stalled region, so failing open is the one thing it may never do \u2014
906
- // a typo'd or wrong-vocabulary page silently greened rule 3 for six of the nine
907
- // pages. Fail loud instead, and say what the vocabulary is.
908
- if (only && history.size === 0) {
909
- console.error(
910
- \`strikes: "\${only}" matches no gate run \u2014 refusing to report "clear".\\n\` +
911
- \` known pages: \${[...seenNames].sort().join(", ")}\`,
912
- );
913
- process.exit(2);
914
- }
915
-
916
- const stuck = [];
917
- for (const [key, all] of history) {
918
- const h = all.filter((e) => !e.dirty); // compare like with like
919
- if (h.length === 0) continue;
920
- const last = h[h.length - 1];
921
- if (last.pass) continue;
922
-
923
- let best = Infinity;
924
- let strikes = 0;
925
- let sinceIdx = 0;
926
- for (let i = 0; i < h.length; i++) {
927
- if (h[i].mm < best - IMPROVE_PP) {
928
- best = Math.min(best, h[i].mm);
929
- strikes = 0;
930
- sinceIdx = i;
931
- } else {
932
- best = Math.min(best, h[i].mm);
933
- strikes++;
934
- }
935
- }
936
- if (strikes >= MAX_STRIKES) stuck.push({ key, strikes, h, sinceIdx, last });
937
- }
938
-
939
- // Triage order: the worst-matching region that has never moved is the one whose
940
- // model is most wrong. Sorting by strike count instead would put a 0.3% region
941
- // that only fails on height above a 76% region that fails on everything.
942
- stuck.sort((a, b) => b.last.mm - a.last.mm);
943
-
944
- if (stuck.length === 0) {
945
- console.log(
946
- \`strikes: clear \u2014 no failing region has stalled for \${MAX_STRIKES}+ gate runs\` +
947
- (only ? \` on \${only}\` : "") +
948
- ".",
949
- );
950
- process.exit(0);
951
- }
952
-
953
- const why = (e) => {
954
- const reasons = [];
955
- if (e.mm > THRESHOLD) reasons.push(\`pixels \${(e.mm * 100).toFixed(1)}%\`);
956
- if (Math.abs(e.dh ?? 0) > MAX_HEIGHT_DELTA)
957
- reasons.push(\`height \${((e.dh ?? 0) * 100).toFixed(1)}%\`);
958
- return reasons.join(" + ") || "marginal";
959
- };
960
-
961
- console.log(
962
- \`STALLED \u2014 \${stuck.length} failing region(s) have not moved in \${MAX_STRIKES}+ gate runs.\\n\` +
963
- \`These need a new model or the operator, not another attempt. Worst first:\\n\`,
964
- );
965
- for (const { key, strikes, h, sinceIdx, last } of stuck.slice(0, 20)) {
966
- const [page, vw, label] = key.split("|");
967
- console.log(
968
- \`\${page} @\${vw} "\${label}" \u2014 \${why(last)}, flat across \${strikes} runs\`,
969
- );
970
- const window = h.slice(Math.max(0, sinceIdx));
971
- const shown = window.length > 4 ? [window[0], ...window.slice(-3)] : window;
972
- for (const [i, e] of shown.entries()) {
973
- const gap =
974
- window.length > 4 && i === 1
975
- ? \` ... \${window.length - 4} more ...\\n\`
976
- : "";
977
- console.log(
978
- gap +
979
- \` \${(e.mm * 100).toFixed(1).padStart(5)}% \${e.at.slice(0, 16).replace("T", " ")} \${e.dir}\`,
980
- );
981
- }
982
- console.log();
983
- }
984
- if (stuck.length > 20) console.log(\`... and \${stuck.length - 20} more.\\n\`);
985
- console.log(
986
- "Per the matching skill: present the attempts to the operator.\\n" +
987
- "Do NOT widen the threshold, add a mask, or reclassify them as a floor.",
988
- );
989
- process.exit(1);
990
- `;
991
- var BUILD_SPEC_MJS_RELATIVE = "matching/build-spec.mjs";
992
- var BUILD_SPEC_MJS_TEMPLATE = `// Assemble matching/SPEC.md from the per-page sections in spec-sections/.
993
- //
994
- // node matching/build-spec.mjs
995
- //
996
- // SPEC.md is the Phase 1 deliverable and the thing matching/gate.sh preflights
997
- // against, so it is GENERATED rather than hand-maintained: edit the section
998
- // file, rebuild. The page list comes from matching/harness.mjs (the same
999
- // reader gate.sh's \`--table\` loop uses) and the heading test from its
1000
- // specHeadingRe, whose pattern is has_spec's grep verbatim \u2014 so a section can
1001
- // no longer build fine here and then re-block its own page at the gate.
1002
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
1003
- import { join } from "node:path";
1004
- import { PAGES, specHeadingRe } from "./harness.mjs";
1005
-
1006
- const DIR = new URL(".", import.meta.url).pathname;
1007
- const SECTIONS = join(DIR, "spec-sections");
1008
-
1009
- // Order = live's nav order, then the detail templates. Both fall out of the
1010
- // table's own order within each group, so this is not a second list to keep.
1011
- const KEYS = [
1012
- ...PAGES.filter((p) => p.group === "nav"),
1013
- ...PAGES.filter((p) => p.group !== "nav"),
1014
- ].map((p) => p.spec ?? p.key);
1015
-
1016
- const parts = [];
1017
- const missing = [];
1018
-
1019
- const chrome = join(SECTIONS, "_chrome.md");
1020
- if (existsSync(chrome)) parts.push(readFileSync(chrome, "utf8").trim());
1021
- else missing.push("_chrome");
1022
-
1023
- for (const page of KEYS) {
1024
- const file = join(SECTIONS, \`\${page}.md\`);
1025
- if (!existsSync(file)) {
1026
- missing.push(page);
1027
- continue;
1028
- }
1029
- const body = readFileSync(file, "utf8").trim();
1030
- // The preflight greps \`^##+ +<page>\`; a section that does not open with its
1031
- // own key would build fine here and then refuse at the gate, which is the
1032
- // most confusing possible failure. Catch it now.
1033
- if (!specHeadingRe(page).test(body)) {
1034
- console.error(
1035
- \`WARNING: \${page}.md does not open with a '## \${page}' heading \u2014 gate.sh will still refuse this page.\`,
1036
- );
1037
- }
1038
- parts.push(body);
1039
- }
1040
-
1041
- // The header is a spec-section like any other, so a site can say what a
1042
- // citation looks like there and what its own ladders are. DEFAULT_HEADER is
1043
- // what a site gets before it writes one: the two paragraphs that are true of
1044
- // every project, and nothing that is not.
1045
- const DEFAULT_HEADER = \`# Reference spec
1046
-
1047
- GENERATED by \\\`node matching/build-spec.mjs\\\` from \\\`matching/spec-sections/\\\`.
1048
- Edit the section files, not this one.
1049
-
1050
- This is the Phase 1 deliverable of the \\\`matching-a-page\\\` skill and the
1051
- precondition \\\`matching/gate.sh\\\` enforces: a page with no section here does not
1052
- get a geometry round (see the matching rules in CLAUDE.md).
1053
-
1054
- \`;
1055
- const headerFile = join(SECTIONS, "_header.md");
1056
- const header = existsSync(headerFile)
1057
- ? readFileSync(headerFile, "utf8").trimEnd() + "\\n\\n"
1058
- : DEFAULT_HEADER;
1059
-
1060
- writeFileSync(join(DIR, "SPEC.md"), header + parts.join("\\n\\n---\\n\\n") + "\\n");
1061
-
1062
- const built = KEYS.length - missing.filter((m) => m !== "_chrome").length;
1063
- console.log(
1064
- \`SPEC.md built \u2014 \${built}/\${KEYS.length} pages\${missing.length ? \`, MISSING: \${missing.join(", ")}\` : ""}\`,
1065
- );
1066
- process.exit(missing.length ? 1 : 0);
1067
- `;
1068
- var CENSUS_COUNT_MJS_RELATIVE = "matching/census-count.mjs";
1069
- var CENSUS_COUNT_MJS_TEMPLATE = `// Count one style-census log, splitting declared deviations out of the total.
1070
- //
1071
- // node matching/census-count.mjs matching/census-home-1440.log
1072
- // -> "<real> <ambiguous> <declared>"
1073
- //
1074
- // census.sh calls this per page/viewport. Parsing the log rather than importing
1075
- // style-census keeps the project's declared-deviation policy out of the shared
1076
- // skill \u2014 the skill reports what it sees; the project decides what it has
1077
- // already agreed to.
1078
- import { readFileSync } from "node:fs";
1079
- import { DECLARED } from "./census-deviations.mjs";
1080
-
1081
- const path = process.argv[2];
1082
- if (!path) {
1083
- console.error("usage: census-count.mjs <census log>");
1084
- process.exit(2);
1085
- }
1086
-
1087
- // A log that cannot be READ is not a log with nothing IN it. This printed
1088
- // "0 0 0" and exited 0, and census.sh added that to its total \u2014 the same green
1089
- // it used to grant for a style-census that never ran. There is no count to
1090
- // give here; say so and let the caller decide, which is never zero.
1091
- let text = "";
1092
- try {
1093
- text = readFileSync(path, "utf8");
1094
- } catch (e) {
1095
- console.error(\`census-count: cannot read \${path} \u2014 \${e.message}\`);
1096
- console.error(" a missing log is not a census with no findings.");
1097
- process.exit(2);
1098
- }
1099
-
1100
- const [head, tail = ""] = text.split("--- AMBIGUOUS");
1101
-
1102
- /** Pull \`{label, ref, cand}\` rows out of a census section. */
1103
- function rows(section) {
1104
- const out = [];
1105
- const lines = section.split("\\n");
1106
- for (let i = 0; i < lines.length; i++) {
1107
- if (!/^ {2}y=/.test(lines[i])) continue;
1108
- const label = lines[i].replace(/^\\s*y=\\s*\\d+\\s*/, "").trim();
1109
- const refs = [];
1110
- const cands = [];
1111
- for (
1112
- let j = i + 1;
1113
- j < lines.length && /^ {4}(ref|cand):/.test(lines[j]);
1114
- j++
1115
- ) {
1116
- const v = lines[j].replace(/^\\s*(ref|cand):\\s*/, "").trim();
1117
- (lines[j].trim().startsWith("ref:") ? refs : cands).push(v);
1118
- }
1119
- if (refs.length && cands.length)
1120
- out.push({ label, ref: refs[0], cand: cands[0] });
1121
- else out.push({ label, ref: refs[0] ?? "", cand: cands[0] ?? "" });
1122
- }
1123
- return out;
1124
- }
1125
-
1126
- const mism = rows(head);
1127
- const amb = rows(tail);
1128
- const declared = mism.filter((r) => DECLARED.some((d) => d.match(r)));
1129
- console.log(
1130
- \`\${mism.length - declared.length} \${amb.length} \${declared.length}\`,
1131
- );
1132
- `;
1133
- var HARNESS_JSON_RELATIVE = "matching/harness.json";
1134
- var HARNESS_JSON_TEMPLATE = `{
1135
- "ref": "__REF__",
1136
- "cand": "http://localhost:5173",
1137
- "matrix": [
1138
- 1440,
1139
- 834,
1140
- 390
1141
- ],
1142
- "threshold": 0.1,
1143
- "maxHeightDelta": 0.05,
1144
- "refMark": "",
1145
- "candMark": "_app/immutable",
1146
- "selfHosts": [],
1147
- "pages": {
1148
- "home": {
1149
- "uid": "home",
1150
- "ref": "/",
1151
- "cand": "/dev/match/home",
1152
- "published": "/",
1153
- "anchors": [],
1154
- "spec": "home",
1155
- "group": "nav"
1156
- }
1157
- }
1158
- }
1159
- `;
1160
- var FLOORS_MJS_RELATIVE = "matching/floors.mjs";
1161
- var FLOORS_MJS_TEMPLATE = `// The failures we have already established are NOT geometry. Shared by
1162
- // next.mjs (which reports them separately from the backlog) and strikes.mjs
1163
- // (which must not report them as stalled \u2014 a region that cannot move is flat
1164
- // by definition, and three strikes on one is noise that hides a real stall).
1165
- // Every entry needs a matching LEDGER entry to stay honest.
1166
- //
1167
- // NOTE the second matcher argument. Region records carry \`label\` and
1168
- // \`viewport\` but not the page, so an entry keyed on label+viewport alone is
1169
- // safe only where the label is unique site-wide. "top" is not \u2014 every page has
1170
- // one \u2014 so such an entry would silently accept the \`top\` failure on EVERY page
1171
- // and hide every real one. next.mjs and strikes.mjs pass the page key as the
1172
- // second argument: take \`(r, page)\` unless you have checked the label.
1173
-
1174
- /** Reference behaviour we cannot reproduce (declared floors). */
1175
- export const FLOORS = [];
1176
-
1177
- /** Regions the OPERATOR has looked at and chosen to leave failing. These are
1178
- * not floors and are not silently dropped: next.mjs prints them under their
1179
- * own heading with the decision, so the number never quietly improves. */
1180
- export const ACCEPTED = [];
1181
- `;
1182
- var CENSUS_DEVIATIONS_MJS_RELATIVE = "matching/census-deviations.mjs";
1183
- var CENSUS_DEVIATIONS_MJS_TEMPLATE = `// Style-census rows the OPERATOR has looked at and chosen to keep. Same
1184
- // contract as matching/floors.mjs: nothing is hidden, everything is counted
1185
- // under its own heading, and every entry needs a LEDGER entry to stay honest.
1186
- //
1187
- // node matching/census-count.mjs <log> -> "<real> <ambiguous> <declared>"
1188
- //
1189
- // Without this, \`census.sh\` can never reach zero and an "N remaining" figure
1190
- // says nothing about how much work is left \u2014 one permanent decision is counted
1191
- // once per page per viewport, so a single ACK can account for dozens of rows.
1192
-
1193
- /** A row is \`{ label, ref, cand }\`, each of ref/cand a "fam | wt | size | lh |
1194
- * ls | transform | colour" tuple string. */
1195
- export const DECLARED = [];
1196
- `;
1197
- var SPEC_CHROME_MD_RELATIVE = "matching/spec-sections/_chrome.md";
1198
- var SPEC_CHROME_MD_TEMPLATE = `## shared chrome
1199
-
1200
- Site-wide furniture that renders identically on every page \u2014 header, footer, and
1201
- anything else the per-page sections would otherwise restate. Phase 1 of the
1202
- \`matching-a-page\` skill fills this in; until it does, this section is a
1203
- placeholder and \`matching/SPEC.md\` has no shared-chrome contract to cite.
1204
- `;
1205
- var SPEC_HEADER_MD_RELATIVE = "matching/spec-sections/_header.md";
1206
- var SPEC_HEADER_MD_TEMPLATE = `# Reference spec
1207
-
1208
- GENERATED by \`node matching/build-spec.mjs\` from \`matching/spec-sections/\`.
1209
- Edit the section files, not this one.
1210
-
1211
- This is the Phase 1 deliverable of the \`matching-a-page\` skill and the
1212
- precondition \`matching/gate.sh\` enforces: a page with no section here does not
1213
- get a geometry round (see the matching rules in CLAUDE.md).
1214
- `;
1215
- var LEDGER_MD_RELATIVE = "matching/LEDGER.md";
1216
- var LEDGER_MD_TEMPLATE = `# Deviations / masks / floors ledger
1217
-
1218
- Append-only, and written at the moment a decision is made \u2014 not reconstructed at
1219
- the end of a round, when the reason has already been lost. An entry is never
1220
- edited to be right: a later entry corrects an earlier one and says which.
1221
-
1222
- Every entry in \`matching/floors.mjs\` and \`matching/census-deviations.mjs\`, and
1223
- every mask in \`matching/harness.json\`, needs a line here. Without one the gate
1224
- has been quietly widened and nothing records who widened it, or why.
1225
-
1226
- - [deviation | floor | mask | a11y] \`<region or selector>\` \u2014 what differs, why
1227
- it is accepted, and the evidence: a spec citation, a census row, a gate run.
1228
- `;
1229
- var MATCH_ROUTE_SERVER_RELATIVE = "src/routes/dev/match/[uid]/+page.server.ts";
1230
- var MATCH_ROUTE_SERVER_TEMPLATE = `import { error } from "@sveltejs/kit";
1231
- import { dev } from "$app/environment";
1232
- import { documents } from "$lib/site-pages.js";
1233
-
1234
- // Local matching surface: renders the EXACT assembly \`reddoor-maint
1235
- // prismic-seed\` publishes, from the same module, so a fix made to pass a gate
1236
- // is a fix to what ships. Not prerendered, SSR-on-demand, dev-only.
1237
- export const prerender = false;
1238
-
1239
- // The seed resolves images to asset ids; here they only need a URL. Dimensions
1240
- // are nominal \u2014 slices size their own image boxes in CSS.
1241
- const devImg = (u: string) => ({
1242
- url: u,
1243
- alt: null,
1244
- copyright: null,
1245
- dimensions: { width: 1600, height: 1067 },
1246
- edit: { x: 0, y: 0, zoom: 1, background: "transparent" },
1247
- id: u,
1248
- });
1249
-
1250
- export async function load({ params }) {
1251
- // FIRST statement: everything below reads fixtures that must not be reachable
1252
- // from a production build. The launch recipe asserts this route 404s on the
1253
- // deployed URL, with /dev/a11y-fixtures as the 200 control.
1254
- if (!dev) error(404, { message: "Not found" });
1255
-
1256
- const docs = documents(devImg) as Array<{ uid: string; data: { slices?: unknown[] } }>;
1257
- const doc = docs.find((d) => d.uid === params.uid);
1258
- if (!doc)
1259
- error(404, {
1260
- message: \`no assembly for "\${params.uid}" (have: \${docs.map((d) => d.uid).join(", ") || "none"})\`,
1261
- });
1262
-
1263
- return { uid: params.uid, slices: doc.data.slices ?? [] };
1264
- }
1265
- `;
1266
- var MATCH_ROUTE_PAGE_RELATIVE = "src/routes/dev/match/[uid]/+page.svelte";
1267
- var MATCH_ROUTE_PAGE_TEMPLATE = `<script lang="ts">
1268
- import { SliceZone } from "@prismicio/svelte";
1269
- import { components } from "$lib/slices";
1270
-
1271
- let { data } = $props();
1272
- </script>
1273
-
1274
- <SliceZone slices={data.slices as never} {components} />
1275
- `;
1276
- var SITE_PAGES_JS_RELATIVE = "src/lib/site-pages.js";
1277
- var SITE_PAGES_JS_TEMPLATE = `// The page assemblies for this site \u2014 the SINGLE source of truth for both
1278
- // consumers: \`reddoor-maint prismic-seed\`, which publishes them through the
1279
- // Migration API, and src/routes/dev/match/[uid], the local matching surface.
1280
- // Because both read from here, any fix made to pass a gate is a fix to what
1281
- // ships.
1282
- //
1283
- // THE MIGRATION API DROPS SILENTLY. It validates against the slice models
1284
- // registered in Prismic and discards every field the model does not declare \u2014
1285
- // HTTP 200, no warning. A fixture field with no model behind it renders here and
1286
- // vanishes on the published route. src/lib/site-pages.test.ts is the mechanical
1287
- // check; run it before every seed.
1288
- //
1289
- // PURE by contract: no node:*, no fetch, no token, no side effects at import,
1290
- // so Vite can bundle it into the dev route and node can import it into the seed.
1291
-
1292
- export const lang = "en-us";
1293
-
1294
- /**
1295
- * @param {(url: string) => unknown} img resolves an image URL to whatever the
1296
- * caller needs \u2014 an asset \`{id}\` for the seed, a \`{url}\` for the dev route.
1297
- * @returns {Array<{type: string, uid: string, title: string, data: Record<string, unknown>}>}
1298
- */
1299
- export function documents(img) {
1300
- void img;
1301
- return [];
1302
- }
1303
- `;
1304
- var SITE_PAGES_TEST_RELATIVE = "src/lib/site-pages.test.ts";
1305
- var SITE_PAGES_TEST_TEMPLATE = `// The page assemblies in src/lib/site-pages.js are the SINGLE source of truth
1306
- // shared by two consumers: the local matching route (src/routes/dev/match/[uid])
1307
- // and \`reddoor-maint prismic-seed\`, which publishes them to Prismic.
1308
- //
1309
- // Those two consumers do NOT validate the same way. The dev route hands the
1310
- // object straight to the slice components, so any field a fixture sets is
1311
- // simply there. The Migration API validates against the slice models registered
1312
- // in Prismic and SILENTLY DROPS every field the model does not declare \u2014 no
1313
- // error, no warning, a 200. A page can gate green locally and publish wrong.
1314
- //
1315
- // This test is the mechanical check. It fails the moment a fixture carries a
1316
- // field its slice model does not declare \u2014 before the seed runs, not after the
1317
- // content is published. Run it before every seed.
1318
- import { describe, expect, it } from "vitest";
1319
- import { readFileSync, readdirSync, existsSync } from "node:fs";
1320
- import { fileURLToPath } from "node:url";
1321
- import { dirname, join } from "node:path";
1322
- import { documents } from "./site-pages.js";
1323
-
1324
- const HERE = dirname(fileURLToPath(import.meta.url));
1325
- const SLICES = join(HERE, "slices");
1326
-
1327
- type Variation = { primary: string[]; items: string[] };
1328
- type Slice = {
1329
- slice_type: string;
1330
- variation: string;
1331
- primary?: Record<string, unknown>;
1332
- items?: Array<Record<string, unknown>>;
1333
- };
1334
-
1335
- /** Every slice model in src/lib/slices, indexed by its Prismic slice id. */
1336
- function loadModels(): Record<string, Record<string, Variation>> {
1337
- const out: Record<string, Record<string, Variation>> = {};
1338
- if (!existsSync(SLICES)) return out;
1339
- for (const dir of readdirSync(SLICES)) {
1340
- const file = join(SLICES, dir, "model.json");
1341
- if (!existsSync(file)) continue;
1342
- const model = JSON.parse(readFileSync(file, "utf8"));
1343
- out[model.id] = Object.fromEntries(
1344
- (model.variations ?? []).map((v: Record<string, unknown>) => [
1345
- v.id,
1346
- {
1347
- primary: Object.keys((v.primary as object) ?? {}),
1348
- items: Object.keys((v.items as object) ?? {}),
1349
- },
1350
- ]),
1351
- );
1352
- }
1353
- return out;
1354
- }
1355
-
1356
- /** Image resolver stub \u2014 shape only; this test never reads image values. */
1357
- const stubImg = () => ({ url: "https://example.test/x.jpg" });
1358
-
1359
- describe("site-pages documents vs slice models", () => {
1360
- const models = loadModels();
1361
- const docs = documents(stubImg) as Array<{ uid: string; data: { slices?: Slice[] } }>;
1362
- const pages: Array<[string, Slice[]]> = docs.map((d) => [d.uid, d.data.slices ?? []]);
1363
-
1364
- it("declares every slice type the documents use", () => {
1365
- const missing = new Set<string>();
1366
- for (const [, slices] of pages)
1367
- for (const s of slices) if (!models[s.slice_type]) missing.add(s.slice_type);
1368
- expect([...missing]).toEqual([]);
1369
- });
1370
-
1371
- it("declares every variation the documents use", () => {
1372
- const missing: string[] = [];
1373
- for (const [uid, slices] of pages)
1374
- for (const s of slices) {
1375
- const model = models[s.slice_type];
1376
- if (model && !model[s.variation]) missing.push(\`\${uid}: \${s.slice_type}/\${s.variation}\`);
1377
- }
1378
- expect(missing).toEqual([]);
1379
- });
1380
-
1381
- // The one that catches a silent Migration-API drop.
1382
- it("declares every field the documents set, so Prismic strips nothing", () => {
1383
- const stripped: string[] = [];
1384
- for (const [uid, slices] of pages)
1385
- for (const s of slices) {
1386
- const variation = models[s.slice_type]?.[s.variation];
1387
- if (!variation) continue;
1388
- for (const key of Object.keys(s.primary ?? {}))
1389
- if (!variation.primary.includes(key))
1390
- stripped.push(\`\${uid} \${s.slice_type}/\${s.variation} primary.\${key}\`);
1391
- const itemKeys = new Set((s.items ?? []).flatMap((i) => Object.keys(i)));
1392
- for (const key of itemKeys)
1393
- if (!variation.items.includes(key))
1394
- stripped.push(\`\${uid} \${s.slice_type}/\${s.variation} items.\${key}\`);
1395
- }
1396
- expect(stripped).toEqual([]);
1397
- });
1398
- });
1399
- `;
1400
- var MATCH_HARNESS_FILES = [
1401
- { rel: HARNESS_MJS_RELATIVE, template: HARNESS_MJS_TEMPLATE, owner: "recipe" },
1402
- { rel: GATE_SH_RELATIVE, template: GATE_SH_TEMPLATE, owner: "recipe" },
1403
- { rel: CENSUS_SH_RELATIVE, template: CENSUS_SH_TEMPLATE, owner: "recipe" },
1404
- { rel: NEXT_MJS_RELATIVE, template: NEXT_MJS_TEMPLATE, owner: "recipe" },
1405
- { rel: STRIKES_MJS_RELATIVE, template: STRIKES_MJS_TEMPLATE, owner: "recipe" },
1406
- { rel: BUILD_SPEC_MJS_RELATIVE, template: BUILD_SPEC_MJS_TEMPLATE, owner: "recipe" },
1407
- { rel: CENSUS_COUNT_MJS_RELATIVE, template: CENSUS_COUNT_MJS_TEMPLATE, owner: "recipe" },
1408
- { rel: HARNESS_JSON_RELATIVE, template: HARNESS_JSON_TEMPLATE, owner: "site" },
1409
- { rel: FLOORS_MJS_RELATIVE, template: FLOORS_MJS_TEMPLATE, owner: "site" },
1410
- { rel: CENSUS_DEVIATIONS_MJS_RELATIVE, template: CENSUS_DEVIATIONS_MJS_TEMPLATE, owner: "site" },
1411
- { rel: SPEC_CHROME_MD_RELATIVE, template: SPEC_CHROME_MD_TEMPLATE, owner: "site" },
1412
- { rel: SPEC_HEADER_MD_RELATIVE, template: SPEC_HEADER_MD_TEMPLATE, owner: "site" },
1413
- { rel: LEDGER_MD_RELATIVE, template: LEDGER_MD_TEMPLATE, owner: "site" },
1414
- { rel: MATCH_ROUTE_SERVER_RELATIVE, template: MATCH_ROUTE_SERVER_TEMPLATE, owner: "recipe" },
1415
- { rel: MATCH_ROUTE_PAGE_RELATIVE, template: MATCH_ROUTE_PAGE_TEMPLATE, owner: "recipe" },
1416
- { rel: SITE_PAGES_JS_RELATIVE, template: SITE_PAGES_JS_TEMPLATE, owner: "site" },
1417
- { rel: SITE_PAGES_TEST_RELATIVE, template: SITE_PAGES_TEST_TEMPLATE, owner: "recipe" }
1418
- ];
1419
- var MATCH_HARNESS_PREVIOUS = {};
1420
- var GITIGNORE_MARKER = "# reddoor-maint match-harness: scripts + records tracked, workspace ignored";
1421
- var GITIGNORE_BLOCK = `scratch-diff*/
1422
- matching/*
1423
- !matching/*.sh
1424
- !matching/*.mjs
1425
- !matching/*.md
1426
- # The pause switch. Extensionless on purpose (it is a sentinel, not a doc), so
1427
- # the whitelist above misses it \u2014 and an ignored switch is not a switch: it
1428
- # would work on one machine and be absent from every fresh clone, CI run and
1429
- # agent that most needs to see it.
1430
- !matching/PAUSED
1431
- !matching/spec-sections/
1432
- !matching/states/
1433
- matching/states/*
1434
- !matching/states/*.mjs
1435
- # derived artefacts, even where the extensions above would catch them
1436
- matching/*.log
1437
- matching/*.json
1438
- # ...except the page table itself, which is the harness's configuration and the
1439
- # one thing a fresh clone cannot reconstruct.
1440
- !matching/harness.json
1441
- matching/*.png
1442
- `;
1443
- var PRETTIERIGNORE_MARKER = "# reddoor-maint match-harness";
1444
- var PRETTIERIGNORE_BLOCK = `# Matching harness \u2014 every file the \`match-harness\` recipe OWNS. It installs
1445
- # them byte-for-byte and byte-compares them on the next install, so a site whose
1446
- # prettier config differs must not reformat them: a reformatted file is
1447
- # indistinguishable from a hand edit, and the recipe refuses to overwrite a hand
1448
- # edit. Measured on a \`useTabs\`/\`singleQuote\` site, all three src/ entries
1449
- # below are rewritten by \`prettier --write .\`, and the next upgrade is refused
1450
- # for all three.
1451
- #
1452
- # The bracketed route segment MUST stay backslash-escaped. This file is
1453
- # gitignore glob syntax, in which \`[uid]\` is a CHARACTER CLASS matching one of
1454
- # \`u\`, \`i\`, \`d\` \u2014 an unescaped entry silently ignores nothing at all.
1455
- #
1456
- # \`matching/*.mjs\` also catches two SITE-owned records (floors.mjs,
1457
- # census-deviations.mjs). Accepted, not overlooked: eslint lints .mjs, so those
1458
- # two keep a style check either way, and naming seven scripts instead of one
1459
- # glob goes stale on the next script the harness gains.
1460
- #
1461
- # harness.json is here because it is a TABLE, not prose: it carries a
1462
- # hand-chosen row layout that prettier reflows.
1463
- #
1464
- # Everything else under matching/ (SPEC.md, LEDGER.md, spec-sections/,
1465
- # states/*.mjs) stays inside \`prettier --check .\`. That is deliberately narrow:
1466
- # eslint does not lint Markdown, so for the records prettier is the ONLY style
1467
- # check there is, and a blanket \`matching/\` entry would leave them with none.
1468
- matching/*.mjs
1469
- matching/*.sh
1470
- matching/harness.json
1471
- src/routes/dev/match/\\[uid\\]/+page.server.ts
1472
- src/routes/dev/match/\\[uid\\]/+page.svelte
1473
- src/lib/site-pages.test.ts
1474
- `;
1475
- var CLAUDE_MD_MARKER = "## Matching rules (installed by reddoor-maint match-harness)";
1476
- var CLAUDE_MD_BLOCK = `
1477
- The \`matching-a-page\` skill governs a live-reference rebuild. These five rules
1478
- exist because the skill alone did not hold on the project this harness came
1479
- from \u2014 each one is a drift that actually happened, with the mechanical check
1480
- that now catches it. \`matching/harness.json\` is this site's configuration;
1481
- \`matching/LEDGER.md\` is the dated record of every deviation, floor and mask.
1482
-
1483
- ### 1. Source prescribes, rects only verify
1484
-
1485
- Every geometry fix must cite the rule it came from: a line in the captured
1486
- reference stylesheet under \`matching/spec/\`, or the reference's HTML. "The probe
1487
- says the gap is 40px" is not a source. If you cannot name the line you are
1488
- guessing \u2014 go read the stylesheet first.
1489
-
1490
- **Check:** the commit body must name the file:line for each fix.
1491
- **Operator's challenge:** _"which line of the reference stylesheet says that?"_
1492
-
1493
- ### 2. Phase 1 before Phase 4
1494
-
1495
- A page gets its section census and per-section spec in \`matching/SPEC.md\` BEFORE
1496
- its geometry is touched. No SPEC section, no geometry round. The census is the
1497
- coverage denominator; skipping it is how a reference's root-font ladder and its
1498
- per-component height ladders get discovered reactively, after the region has
1499
- already failed several rounds.
1500
-
1501
- **Check:** \`matching/gate.sh\` refuses to run a page with no \`SPEC.md\` section.
1502
- **Operator's challenge:** _"show me the SPEC section for that region."_
1503
-
1504
- ### 3. Three strikes, then stop
1505
-
1506
- A failing region that has not improved across 3+ gate runs does not get a fourth
1507
- attempt. Present the attempts. Never widen the threshold, add a mask, or
1508
- reclassify it as a floor to make it go away.
1509
-
1510
- **Check:** \`node matching/strikes.mjs <page>\` \u2014 exits 1 while any region is
1511
- stalled, and is the first thing a geometry round runs.
1512
- **Operator's challenge:** _"how many runs has that region been flat?"_
1513
-
1514
- ### 4. A gate closes an item, nothing else
1515
-
1516
- No fix is "done" because the code changed. Paste the gate header \u2014 it is
1517
- self-describing, so a nonstandard threshold or an undisclosed mask is visible.
1518
- The threshold and the matrix are whatever \`matching/harness.json\` says, on every
1519
- page, never a subset.
1520
-
1521
- **Operator's challenge:** _"paste the gate header."_
1522
-
1523
- ### 5. A commit is a checkpoint, not a stopping point
1524
-
1525
- Do not hand control back between rounds. After committing, run
1526
- \`node matching/next.mjs\`: while it exits 1 there is a named next action, and the
1527
- round continues. Report when the backlog is empty, when a decision is genuinely
1528
- the operator's (three strikes, a novel floor, a threshold change), or when
1529
- asked \u2014 not because a commit felt like a natural place to summarise.
1530
-
1531
- This exists because the failure was habitual, not deliberate: a turn ends when
1532
- user-facing prose gets written, and "I just committed something good" is the
1533
- moment that invites writing it. The gate stops incorrect work; this stops
1534
- premature stopping.
1535
-
1536
- **Pausing:** create \`matching/PAUSED\` holding a note that says what the pause
1537
- means; \`next.mjs\` and \`strikes.mjs\` then exit 0 while it exists. This rule says
1538
- "do not stop while work is named"; it never said "find work to name". An empty
1539
- agenda and a paused one end the round the same way. Deleting the switch is the
1540
- operator's call.
1541
-
1542
- **Check:** \`node matching/next.mjs\` \u2014 exits 1 while any non-floor region fails.
1543
- **Operator's challenge:** _"what does next.mjs say?"_
1544
-
1545
- ### Round protocol
1546
-
1547
- 1. \`node matching/strikes.mjs <page>\` \u2014 if it exits 1, the stalled regions are
1548
- the agenda, and stalled ones get escalated rather than re-attempted.
1549
- 2. Confirm the page has a \`matching/SPEC.md\` section; write
1550
- \`matching/spec-sections/<page>.md\` and run \`node matching/build-spec.mjs\` if
1551
- not.
1552
- 3. Fix, each change citing its source line.
1553
- 4. \`bash matching/gate.sh <tag> <page>\` \u2014 paste the header.
1554
- 5. Append to \`matching/LEDGER.md\` at the moment a deviation, floor or mask is
1555
- decided, not reconstructed at the end.
1556
- 6. \`pnpm verify\`, then commit and push.
1557
- `;
1558
-
1559
- // src/recipes/match-harness/index.ts
1560
- var PRETTIER_TIMEOUT_MS = 6e4;
1561
- async function readIfExists(path) {
1562
- try {
1563
- return await readFile(path, "utf-8");
1564
- } catch {
1565
- return null;
1566
- }
1567
- }
1568
- function normalize(s) {
1569
- return s.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").trim();
1570
- }
1571
- function mergeBlock(existing, marker, block) {
1572
- if (existing === null) return `${marker}
1573
- ${block}`;
1574
- if (existing.includes(marker)) return null;
1575
- const base = existing.endsWith("\n") ? existing : `${existing}
1576
- `;
1577
- return `${base}
1578
- ${marker}
1579
- ${block}`;
1580
- }
1581
- function planFileWrite(existing, template, owner, previous) {
1582
- if (existing === null) return "write";
1583
- if (normalize(existing) === normalize(template)) return "skip";
1584
- if (owner === "site") return "skip";
1585
- if (previous.some((p) => normalize(existing) === normalize(p))) return "replace";
1586
- return "flag";
1587
- }
1588
- var APPENDED_BLOCKS = [
1589
- [".gitignore", GITIGNORE_MARKER, GITIGNORE_BLOCK],
1590
- [".prettierignore", PRETTIERIGNORE_MARKER, PRETTIERIGNORE_BLOCK],
1591
- ["CLAUDE.md", CLAUDE_MD_MARKER, CLAUDE_MD_BLOCK]
1592
- ];
1593
- var MATCH_HARNESS_INSTALLED_PATHS = [
1594
- ...MATCH_HARNESS_FILES.map((f) => f.rel),
1595
- ...APPENDED_BLOCKS.map(([rel]) => rel)
1596
- ];
1597
- async function matchHarness(site, opts, deps = { spawn: defaultSpawn }) {
1598
- return withRecipe({
1599
- name: "match-harness",
1600
- site,
1601
- plan: async () => {
1602
- if (!opts.ref || !/^https?:\/\//.test(opts.ref)) {
1603
- return {
1604
- kind: "failed",
1605
- notes: "--ref <url> is required: the harness gates against a live reference, and a harness with no reference can only ever report an absence of errors"
1606
- };
1607
- }
1608
- return {
1609
- kind: "apply",
1610
- plan: {
1611
- ref: opts.ref.replace(/\/$/, ""),
1612
- cand: opts.cand ?? "http://localhost:5173",
1613
- matrix: opts.matrix ?? [1440, 834, 390]
1614
- }
1615
- };
1616
- },
1617
- apply: async (planned, { commit, cwd }) => {
1618
- const notes = [];
1619
- const written = [];
1620
- const before = /* @__PURE__ */ new Map();
1621
- const previousAll = deps.previous ?? MATCH_HARNESS_PREVIOUS;
1622
- for (const f of MATCH_HARNESS_FILES) {
1623
- const target = join(cwd, f.rel);
1624
- let template = f.template;
1625
- if (f.rel === HARNESS_JSON_RELATIVE) {
1626
- const seed = JSON.parse(template);
1627
- seed.ref = planned.ref;
1628
- seed.cand = planned.cand;
1629
- seed.matrix = planned.matrix;
1630
- template = JSON.stringify(seed, null, 2) + "\n";
1631
- }
1632
- const existing = await readIfExists(target);
1633
- const action = planFileWrite(existing, template, f.owner, previousAll[f.rel] ?? []);
1634
- if (action === "flag") {
1635
- notes.push(
1636
- `${f.rel} differs from the shipped template and was left alone (hand-edited?)`
1637
- );
1638
- continue;
1639
- }
1640
- if (action === "skip") continue;
1641
- await mkdir(dirname(target), { recursive: true });
1642
- before.set(f.rel, existing);
1643
- await writeFile(target, template, "utf-8");
1644
- written.push(f.rel);
1645
- if (action === "replace") notes.push(`${f.rel} upgraded from a previous version`);
1646
- }
1647
- for (const [rel, marker, block] of APPENDED_BLOCKS) {
1648
- const path = join(cwd, rel);
1649
- const existing = await readIfExists(path);
1650
- const merged = mergeBlock(existing, marker, block);
1651
- if (merged !== null) {
1652
- before.set(rel, existing);
1653
- await writeFile(path, merged, "utf-8");
1654
- written.push(rel);
1655
- }
1656
- }
1657
- const siteOwned = new Set(
1658
- MATCH_HARNESS_FILES.filter((f) => f.owner === "site").map((f) => f.rel)
1659
- );
1660
- const toFormat = written.filter((p) => siteOwned.has(p) || p === "CLAUDE.md");
1661
- if (toFormat.length > 0) {
1662
- const bin = await (deps.resolvePrettier ?? resolveTargetPrettier)(cwd);
1663
- if (bin === null) {
1664
- notes.push(PRETTIER_FLAG_NOTE);
1665
- } else if (!await formatWithPrettier(deps.spawn, cwd, toFormat, {
1666
- bin,
1667
- timeoutMs: PRETTIER_TIMEOUT_MS
1668
- })) {
1669
- notes.push(PRETTIER_FLAG_NOTE);
1670
- }
1671
- }
1672
- await commit(
1673
- "feat: install the matching harness (/dev/match route + matching/ gate scripts)"
1674
- );
1675
- const missing = await pathsMissingFromHead(cwd, MATCH_HARNESS_INSTALLED_PATHS);
1676
- if (missing.length > 0) {
1677
- const rules = await ignoreRulesFor(cwd, missing);
1678
- const sources = [...new Set(rules.map((r) => r.split(" ")[0]))];
1679
- const explained = new Set(rules.map((r) => r.split(" ")[1] ?? ""));
1680
- const unexplained = missing.filter((p) => !explained.has(p));
1681
- const removed = [];
1682
- for (const rel of written.filter((p) => missing.includes(p))) {
1683
- const prev = before.get(rel);
1684
- if (prev === void 0) continue;
1685
- if (prev === null) {
1686
- await rm(join(cwd, rel), { force: true });
1687
- removed.push(rel);
1688
- } else {
1689
- await writeFile(join(cwd, rel), prev, "utf-8");
1690
- }
1691
- }
1692
- for (const d of [...new Set(removed.map(dirname))].sort((a, b) => b.length - a.length)) {
1693
- if (d !== ".") await rmdir(join(cwd, d)).catch(() => void 0);
1694
- }
1695
- return {
1696
- kind: "failed",
1697
- notes: `the harness was NOT installed: ${missing.length} of ${MATCH_HARNESS_INSTALLED_PATHS.length} paths git refused to track, so a fresh clone, CI and the next agent get a harness that does not exist. Absent from the commit: ${missing.join(", ")}. ` + (sources.length > 0 ? `Excluded by ${sources.join(", ")} \u2014 narrow or remove those rules and re-run. ` : "") + (unexplained.length > 0 ? `No ignore rule matched ${unexplained.join(", ")} \u2014 check .git/info/exclude, core.excludesFile, and whether the directory is a nested repository. ` : "") + "Everything this run wrote to those paths has been put back as it was."
1698
- };
1699
- }
1700
- return notes.length > 0 ? { kind: "ok", notes: notes.join("; ") } : { kind: "ok" };
1701
- }
1702
- });
1703
- }
1704
-
1705
- export {
1706
- matchHarness
1707
- };
1708
- //# sourceMappingURL=chunk-6VKYQATC.js.map