@reddoorla/maintenance 0.95.1 → 0.97.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.
- package/README.md +41 -7
- package/dist/{announce-CLQYX5I4.js → announce-5ZD2SBGD.js} +39 -31
- package/dist/announce-5ZD2SBGD.js.map +1 -0
- package/dist/attachments-JFGFQRXQ.js +10 -0
- package/dist/{blux-4KNXUP2K.js → blux-NXQLMS4W.js} +1 -1
- package/dist/{bump-deps-C5G2EFBB.js → bump-deps-775VLDYB.js} +6 -6
- package/dist/{chunk-FUWFLAWQ.js → chunk-3RLDX3Q2.js} +5 -5
- package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
- package/dist/chunk-43SW3FGW.js.map +1 -0
- package/dist/{chunk-R53VL3RI.js → chunk-5CJVUPJU.js} +1 -1
- package/dist/{chunk-R53VL3RI.js.map → chunk-5CJVUPJU.js.map} +1 -1
- package/dist/{chunk-S7RYFUIK.js → chunk-6PGXKN6F.js} +2 -2
- package/dist/{chunk-HTABUCF4.js → chunk-6THEZQLA.js} +3 -3
- package/dist/chunk-A5QKNOXW.js +72 -0
- package/dist/chunk-A5QKNOXW.js.map +1 -0
- package/dist/chunk-ASLDCWBF.js +41 -0
- package/dist/chunk-ASLDCWBF.js.map +1 -0
- package/dist/{chunk-CWXTNPNZ.js → chunk-AYDQQBNG.js} +3 -3
- package/dist/chunk-AYDQQBNG.js.map +1 -0
- package/dist/{chunk-WVNDEYXL.js → chunk-BINFZQ2T.js} +57 -6
- package/dist/chunk-BINFZQ2T.js.map +1 -0
- package/dist/{chunk-2NQUCS7O.js → chunk-BIYQN7FH.js} +3 -1
- package/dist/{chunk-2NQUCS7O.js.map → chunk-BIYQN7FH.js.map} +1 -1
- package/dist/{chunk-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
- package/dist/chunk-CXICCLNM.js.map +1 -0
- package/dist/{chunk-BYGPUUCT.js → chunk-DRNZVORX.js} +66 -30
- package/dist/chunk-DRNZVORX.js.map +1 -0
- package/dist/{chunk-LM2NHRCJ.js → chunk-DYTYPKFN.js} +4 -4
- package/dist/chunk-DYTYPKFN.js.map +1 -0
- package/dist/{chunk-DFKBTBO3.js → chunk-EN6CTBUN.js} +35 -6
- package/dist/chunk-EN6CTBUN.js.map +1 -0
- package/dist/{chunk-5EEMTJMT.js → chunk-FHFXJ3EM.js} +8 -6
- package/dist/chunk-FHFXJ3EM.js.map +1 -0
- package/dist/{chunk-Z2K4JPQ3.js → chunk-FP6HOW6J.js} +10 -7
- package/dist/chunk-FP6HOW6J.js.map +1 -0
- package/dist/{chunk-2LYOAB2E.js → chunk-HSMN35LU.js} +2 -2
- package/dist/chunk-ICSRWW5Q.js +148 -0
- package/dist/chunk-ICSRWW5Q.js.map +1 -0
- package/dist/{chunk-FSGICFXB.js → chunk-IDBPII6G.js} +17 -5
- package/dist/chunk-IDBPII6G.js.map +1 -0
- package/dist/{chunk-C3IQQKXL.js → chunk-IOTOOTII.js} +121 -1
- package/dist/{chunk-C3IQQKXL.js.map → chunk-IOTOOTII.js.map} +1 -1
- package/dist/{chunk-BCKEP5S6.js → chunk-JHJNNJLG.js} +2 -2
- package/dist/{chunk-TO7DJYLZ.js → chunk-L3S3QZXL.js} +178 -39
- package/dist/chunk-L3S3QZXL.js.map +1 -0
- package/dist/{chunk-WSJ7RO54.js → chunk-LAG6IP7Y.js} +23 -5
- package/dist/chunk-LAG6IP7Y.js.map +1 -0
- package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
- package/dist/chunk-LAJ3FF54.js.map +1 -0
- package/dist/{chunk-2ZAZ7ETT.js → chunk-LDHRIQUW.js} +32 -18
- package/dist/chunk-LDHRIQUW.js.map +1 -0
- package/dist/{chunk-6R56OKJZ.js → chunk-LF2TJN4X.js} +2 -2
- package/dist/chunk-LF2TJN4X.js.map +1 -0
- package/dist/{chunk-ZQV7UQNE.js → chunk-LUF52EIY.js} +2 -2
- package/dist/{chunk-5UINJNAJ.js → chunk-LZMAG73U.js} +34 -21
- package/dist/chunk-LZMAG73U.js.map +1 -0
- package/dist/{chunk-VSGMCCGQ.js → chunk-MMSMZC7S.js} +2 -2
- package/dist/chunk-MVKNU4QD.js +37 -0
- package/dist/chunk-MVKNU4QD.js.map +1 -0
- package/dist/{chunk-XPJQXSC4.js → chunk-P7323YVW.js} +27 -5
- package/dist/chunk-P7323YVW.js.map +1 -0
- package/dist/{chunk-KJRVMMM3.js → chunk-PARKXDXI.js} +36 -5
- package/dist/chunk-PARKXDXI.js.map +1 -0
- package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
- package/dist/{chunk-WMBLR5VA.js → chunk-PZZ6HM5T.js} +3 -3
- package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
- package/dist/chunk-QFSL2L2B.js.map +1 -0
- package/dist/chunk-RQJFGK3A.js +51 -0
- package/dist/chunk-RQJFGK3A.js.map +1 -0
- package/dist/{chunk-3Z6X3BHU.js → chunk-TC4GANMX.js} +53 -150
- package/dist/chunk-TC4GANMX.js.map +1 -0
- package/dist/{chunk-F3JWOPTG.js → chunk-TEMQPSCM.js} +45 -105
- package/dist/chunk-TEMQPSCM.js.map +1 -0
- package/dist/chunk-TJLKWQOX.js +52 -0
- package/dist/chunk-TJLKWQOX.js.map +1 -0
- package/dist/{chunk-CJ2RMTT7.js → chunk-TV76A6GN.js} +2 -2
- package/dist/{chunk-KZRR4NFG.js → chunk-TVZ6QCOE.js} +3 -3
- package/dist/{chunk-BVEDKMNG.js → chunk-U2ZI6G7H.js} +29 -11
- package/dist/chunk-U2ZI6G7H.js.map +1 -0
- package/dist/chunk-UPE6D4AT.js +69 -0
- package/dist/chunk-UPE6D4AT.js.map +1 -0
- package/dist/{chunk-R5IMFAFR.js → chunk-VMGHQUTV.js} +7 -16
- package/dist/chunk-VMGHQUTV.js.map +1 -0
- package/dist/chunk-VQMAAQ5M.js +53 -0
- package/dist/chunk-VQMAAQ5M.js.map +1 -0
- package/dist/{chunk-AVEQUR44.js → chunk-WMF5S2MB.js} +60 -17
- package/dist/chunk-WMF5S2MB.js.map +1 -0
- package/dist/{chunk-SVAAONYB.js → chunk-WQLNUEZL.js} +12 -2
- package/dist/{chunk-SVAAONYB.js.map → chunk-WQLNUEZL.js.map} +1 -1
- package/dist/{chunk-XN37VTDB.js → chunk-X56LAKPR.js} +3 -3
- package/dist/chunk-X56LAKPR.js.map +1 -0
- package/dist/{chunk-ZCL7C5LH.js → chunk-Y6FCXRIC.js} +1661 -387
- package/dist/chunk-Y6FCXRIC.js.map +1 -0
- package/dist/{chunk-2SLED3NB.js → chunk-YDZ7XGPZ.js} +1 -1
- package/dist/{chunk-2SLED3NB.js.map → chunk-YDZ7XGPZ.js.map} +1 -1
- package/dist/{chunk-IY6256GK.js → chunk-YJBSB4LP.js} +6 -1
- package/dist/chunk-YJBSB4LP.js.map +1 -0
- package/dist/{chunk-BI2VI6EM.js → chunk-YXWBM26P.js} +2 -2
- package/dist/{chunk-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
- package/dist/chunk-Z7FDJ35L.js.map +1 -0
- package/dist/cli/bin.js +111 -51
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +33 -3
- package/dist/cli/commands/audit.js +28 -25
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-DGTO65WB.js +11 -0
- package/dist/configs/playwright-a11y.d.ts +11 -1
- package/dist/configs/playwright-a11y.js +3 -1
- package/dist/{convert-to-pnpm-YTFREUJK.js → convert-to-pnpm-KLWA53N5.js} +6 -6
- package/dist/{db-XKZYDCKM.js → db-II5WAK4D.js} +45 -20
- package/dist/db-II5WAK4D.js.map +1 -0
- package/dist/deadletter-G4M3HSVM.js +17 -0
- package/dist/{digest-7UBGUBRK.js → digest-CWKK2DMR.js} +56 -32
- package/dist/digest-CWKK2DMR.js.map +1 -0
- package/dist/{digest-collectors-DJFN5ZDK.js → digest-collectors-CYTN2EUX.js} +17 -12
- package/dist/{digest-state-3KHZF7GD.js → digest-state-ZDPKR7MW.js} +18 -2
- package/dist/digest-state-ZDPKR7MW.js.map +1 -0
- package/dist/ensure-site-2QZHFVQ3.js +70 -0
- package/dist/ensure-site-2QZHFVQ3.js.map +1 -0
- package/dist/ensure-site-JEKV2ODE.js +105 -0
- package/dist/ensure-site-JEKV2ODE.js.map +1 -0
- package/dist/{ensure-site-OAAE5YE2.js → ensure-site-OO4UPWNQ.js} +49 -44
- package/dist/ensure-site-OO4UPWNQ.js.map +1 -0
- package/dist/fleet-events-CkOZpMVC.d.ts +435 -0
- package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-RJROYXKO.js} +19 -7
- package/dist/{forms-notify-target-UC6PF3AE.js → forms-notify-target-X6UEOI2M.js} +19 -15
- package/dist/forms-notify-target-X6UEOI2M.js.map +1 -0
- package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
- package/dist/{github-signals-TX5FPYP6.js → github-signals-QIRTHHET.js} +22 -17
- package/dist/github-signals-QIRTHHET.js.map +1 -0
- package/dist/{header-image-PSPT7CUM.js → header-image-H7ZJAV26.js} +15 -18
- package/dist/header-image-H7ZJAV26.js.map +1 -0
- package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-BJ6BKRHF.js} +8 -7
- package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-BJ6BKRHF.js.map} +1 -1
- package/dist/{health-mirror-XJAKA3EY.js → health-mirror-KU3G7V2Y.js} +10 -10
- package/dist/{import-airtable-TAKSN4NV.js → import-airtable-UBF5GYAL.js} +3 -3
- package/dist/index.d.ts +158 -419
- package/dist/index.js +91 -46
- package/dist/index.js.map +1 -1
- package/dist/{init-FFE4FCO2.js → init-GOWDA5QS.js} +19 -18
- package/dist/{init-FFE4FCO2.js.map → init-GOWDA5QS.js.map} +1 -1
- package/dist/{launch-UD36AHUN.js → launch-7N3X2MXB.js} +229 -79
- package/dist/launch-7N3X2MXB.js.map +1 -0
- package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
- package/dist/lighthouse-airtable-M45OPVRV.js +14 -0
- package/dist/match-harness-GBCDSVPS.js +417 -0
- package/dist/match-harness-GBCDSVPS.js.map +1 -0
- package/dist/migrate-XZ7U5DFC.js +7 -0
- package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +4 -4
- package/dist/{onboard-TM2KESGF.js → onboard-JQQIMQW7.js} +6 -6
- package/dist/{orchestrate-HYVQPRCE.js → orchestrate-L2A5BZFY.js} +13 -10
- package/dist/parity-2JGVUCZD.js +14 -0
- package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
- package/dist/{preflight-RYIP54NY.js → preflight-O7Q6FWHU.js} +35 -11
- package/dist/preflight-O7Q6FWHU.js.map +1 -0
- package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-NAPWOXXD.js} +39 -9
- package/dist/prismic-ci-NAPWOXXD.js.map +1 -0
- package/dist/{prismic-models-P6FWWVRS.js → prismic-models-LL6ORTQ2.js} +20 -17
- package/dist/prismic-models-LL6ORTQ2.js.map +1 -0
- package/dist/prospect/types.d.ts +72 -0
- package/dist/prospect/types.js +1 -1
- package/dist/{prospect-audit-PRHU53HS.js → prospect-audit-FMPGQIRQ.js} +16 -7
- package/dist/prospect-audit-FMPGQIRQ.js.map +1 -0
- package/dist/{prospect-audits-R3LWF6U7.js → prospect-audits-IZVQY3OQ.js} +4 -2
- package/dist/protection-audit-YXJKFU6O.js +413 -0
- package/dist/protection-audit-YXJKFU6O.js.map +1 -0
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/{render-YGNGTX3W.js → render-MHUUHPLN.js} +36 -9
- package/dist/render-MHUUHPLN.js.map +1 -0
- package/dist/{render-from-row-IFSBHWDH.js → render-from-row-75JTED2Z.js} +6 -4
- package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-2IHAZDVA.js} +14 -9
- package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-2IHAZDVA.js.map} +1 -1
- package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
- package/dist/replay-4FVFOMHH.js.map +1 -0
- package/dist/{report-LQS4ECTM.js → report-IAHXN5CW.js} +103 -57
- package/dist/report-IAHXN5CW.js.map +1 -0
- package/dist/{report-mirror-HDTOJE64.js → report-mirror-4QM57LMS.js} +34 -12
- package/dist/report-mirror-4QM57LMS.js.map +1 -0
- package/dist/roster-GDNSIRAK.js +19 -0
- package/dist/roster-GDNSIRAK.js.map +1 -0
- package/dist/{schema-A4WCPIF4.js → schema-KWUQEZ5O.js} +1 -1
- package/dist/schema-KWUQEZ5O.js.map +1 -0
- package/dist/{self-updating-MWXIZLL7.js → self-updating-ZUWPGYUP.js} +8 -7
- package/dist/{self-updating-MWXIZLL7.js.map → self-updating-ZUWPGYUP.js.map} +1 -1
- package/dist/{selftest-VXTHRPRN.js → selftest-G4W3LIBQ.js} +21 -17
- package/dist/{selftest-VXTHRPRN.js.map → selftest-G4W3LIBQ.js.map} +1 -1
- package/dist/site-create-ZU3ACBTO.js +45 -0
- package/dist/site-create-ZU3ACBTO.js.map +1 -0
- package/dist/site-lookup-DTOLJ4EA.js +27 -0
- package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
- package/dist/{site-mirror-WON4FTJ2.js → site-mirror-WR7ACXOV.js} +26 -12
- package/dist/site-mirror-WR7ACXOV.js.map +1 -0
- package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-WAUPI6EJ.js} +8 -7
- package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-WAUPI6EJ.js.map} +1 -1
- package/dist/{submissions-OVZPVBYG.js → submissions-2SHCFOKD.js} +12 -7
- package/dist/submissions-2SHCFOKD.js.map +1 -0
- package/dist/{submissions-3I3GAQAX.js → submissions-JDZP35JM.js} +49 -9
- package/dist/submissions-JDZP35JM.js.map +1 -0
- package/dist/{svelte-codemods-WSV2FGNG.js → svelte-codemods-VOLOKKFI.js} +6 -6
- package/dist/{sync-K3PKHVLH.js → sync-F4USL4K5.js} +6 -6
- package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-OXSWKVZ2.js} +6 -6
- package/dist/turso-KQKBBQCK.js +24 -0
- package/dist/turso-KQKBBQCK.js.map +1 -0
- package/dist/{upgrade-VTD6V4HS.js → upgrade-EVAL3JTO.js} +6 -6
- package/dist/util/git.d.ts +49 -10
- package/dist/util/git.js +1 -1
- package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
- package/dist/{websites-TB4TCAWM.js → websites-Y6FN2XFD.js} +27 -20
- package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-TI3JMWSY.js} +7 -5
- package/package.json +3 -2
- package/dist/airtable-JGCPS7JH.js +0 -9
- package/dist/announce-CLQYX5I4.js.map +0 -1
- package/dist/attachments-NLZIJ5NU.js +0 -9
- package/dist/chunk-2ZAZ7ETT.js.map +0 -1
- package/dist/chunk-3Z6X3BHU.js.map +0 -1
- package/dist/chunk-5EEMTJMT.js.map +0 -1
- package/dist/chunk-5GUJ36TL.js +0 -53
- package/dist/chunk-5GUJ36TL.js.map +0 -1
- package/dist/chunk-5UINJNAJ.js.map +0 -1
- package/dist/chunk-6R56OKJZ.js.map +0 -1
- package/dist/chunk-AVEQUR44.js.map +0 -1
- package/dist/chunk-BVEDKMNG.js.map +0 -1
- package/dist/chunk-BYGPUUCT.js.map +0 -1
- package/dist/chunk-CWXTNPNZ.js.map +0 -1
- package/dist/chunk-DFKBTBO3.js.map +0 -1
- package/dist/chunk-F3JWOPTG.js.map +0 -1
- package/dist/chunk-FSGICFXB.js.map +0 -1
- package/dist/chunk-GASBX52O.js +0 -12
- package/dist/chunk-GASBX52O.js.map +0 -1
- package/dist/chunk-IY6256GK.js.map +0 -1
- package/dist/chunk-KJRVMMM3.js.map +0 -1
- package/dist/chunk-LM2NHRCJ.js.map +0 -1
- package/dist/chunk-QF6IQQFP.js.map +0 -1
- package/dist/chunk-R5IMFAFR.js.map +0 -1
- package/dist/chunk-TO7DJYLZ.js.map +0 -1
- package/dist/chunk-TR3HFH6J.js.map +0 -1
- package/dist/chunk-WJMPCVOR.js.map +0 -1
- package/dist/chunk-WSJ7RO54.js.map +0 -1
- package/dist/chunk-WVNDEYXL.js.map +0 -1
- package/dist/chunk-XHHMM5HX.js.map +0 -1
- package/dist/chunk-XN37VTDB.js.map +0 -1
- package/dist/chunk-XPJQXSC4.js.map +0 -1
- package/dist/chunk-YFAXOD7Z.js +0 -42
- package/dist/chunk-YFAXOD7Z.js.map +0 -1
- package/dist/chunk-YLYXW5PH.js.map +0 -1
- package/dist/chunk-Z2K4JPQ3.js.map +0 -1
- package/dist/chunk-ZCL7C5LH.js.map +0 -1
- package/dist/client-G2WGWPCA.js +0 -11
- package/dist/db-XKZYDCKM.js.map +0 -1
- package/dist/digest-7UBGUBRK.js.map +0 -1
- package/dist/digest-state-3KHZF7GD.js.map +0 -1
- package/dist/ensure-site-OAAE5YE2.js.map +0 -1
- package/dist/fleet-events-cF2Om88w.d.ts +0 -24
- package/dist/forms-notify-target-UC6PF3AE.js.map +0 -1
- package/dist/github-signals-TX5FPYP6.js.map +0 -1
- package/dist/header-image-PSPT7CUM.js.map +0 -1
- package/dist/launch-UD36AHUN.js.map +0 -1
- package/dist/lighthouse-airtable-YMH4UA7R.js +0 -12
- package/dist/match-harness-QRG6XZAR.js +0 -70
- package/dist/match-harness-QRG6XZAR.js.map +0 -1
- package/dist/migrate-Y7R4CESL.js +0 -7
- package/dist/parity-CAJ477PS.js +0 -14
- package/dist/preflight-RYIP54NY.js.map +0 -1
- package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
- package/dist/prismic-models-P6FWWVRS.js.map +0 -1
- package/dist/prospect-audit-PRHU53HS.js.map +0 -1
- package/dist/protection-audit-SJO53MEQ.js +0 -168
- package/dist/protection-audit-SJO53MEQ.js.map +0 -1
- package/dist/render-YGNGTX3W.js.map +0 -1
- package/dist/replay-PCSQOGCZ.js.map +0 -1
- package/dist/report-LQS4ECTM.js.map +0 -1
- package/dist/report-mirror-HDTOJE64.js.map +0 -1
- package/dist/schema-A4WCPIF4.js.map +0 -1
- package/dist/site-mirror-WON4FTJ2.js.map +0 -1
- package/dist/submissions-3I3GAQAX.js.map +0 -1
- package/dist/submissions-OVZPVBYG.js.map +0 -1
- /package/dist/{airtable-JGCPS7JH.js.map → attachments-JFGFQRXQ.js.map} +0 -0
- /package/dist/{blux-4KNXUP2K.js.map → blux-NXQLMS4W.js.map} +0 -0
- /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-775VLDYB.js.map} +0 -0
- /package/dist/{chunk-FUWFLAWQ.js.map → chunk-3RLDX3Q2.js.map} +0 -0
- /package/dist/{chunk-S7RYFUIK.js.map → chunk-6PGXKN6F.js.map} +0 -0
- /package/dist/{chunk-HTABUCF4.js.map → chunk-6THEZQLA.js.map} +0 -0
- /package/dist/{chunk-2LYOAB2E.js.map → chunk-HSMN35LU.js.map} +0 -0
- /package/dist/{chunk-BCKEP5S6.js.map → chunk-JHJNNJLG.js.map} +0 -0
- /package/dist/{chunk-ZQV7UQNE.js.map → chunk-LUF52EIY.js.map} +0 -0
- /package/dist/{chunk-VSGMCCGQ.js.map → chunk-MMSMZC7S.js.map} +0 -0
- /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
- /package/dist/{chunk-WMBLR5VA.js.map → chunk-PZZ6HM5T.js.map} +0 -0
- /package/dist/{chunk-CJ2RMTT7.js.map → chunk-TV76A6GN.js.map} +0 -0
- /package/dist/{chunk-KZRR4NFG.js.map → chunk-TVZ6QCOE.js.map} +0 -0
- /package/dist/{chunk-BI2VI6EM.js.map → chunk-YXWBM26P.js.map} +0 -0
- /package/dist/{attachments-NLZIJ5NU.js.map → client-DGTO65WB.js.map} +0 -0
- /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-KLWA53N5.js.map} +0 -0
- /package/dist/{client-G2WGWPCA.js.map → deadletter-G4M3HSVM.js.map} +0 -0
- /package/dist/{digest-collectors-DJFN5ZDK.js.map → digest-collectors-CYTN2EUX.js.map} +0 -0
- /package/dist/{fleet-state-UEVZ3X3J.js.map → fleet-state-RJROYXKO.js.map} +0 -0
- /package/dist/{freeze-A6MUCZU4.js.map → freeze-GUQ43EAU.js.map} +0 -0
- /package/dist/{health-mirror-XJAKA3EY.js.map → health-mirror-KU3G7V2Y.js.map} +0 -0
- /package/dist/{import-airtable-TAKSN4NV.js.map → import-airtable-UBF5GYAL.js.map} +0 -0
- /package/dist/{lighthouse-VHI77PFB.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
- /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-airtable-M45OPVRV.js.map} +0 -0
- /package/dist/{migrate-Y7R4CESL.js.map → migrate-XZ7U5DFC.js.map} +0 -0
- /package/dist/{notify-IISTHNBR.js.map → notify-K3WVH3DM.js.map} +0 -0
- /package/dist/{onboard-TM2KESGF.js.map → onboard-JQQIMQW7.js.map} +0 -0
- /package/dist/{orchestrate-HYVQPRCE.js.map → orchestrate-L2A5BZFY.js.map} +0 -0
- /package/dist/{parity-CAJ477PS.js.map → parity-2JGVUCZD.js.map} +0 -0
- /package/dist/{pipeline-YROAY6DT.js.map → pipeline-UUPXOS2J.js.map} +0 -0
- /package/dist/{prospect-audits-R3LWF6U7.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
- /package/dist/{render-from-row-IFSBHWDH.js.map → render-from-row-75JTED2Z.js.map} +0 -0
- /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-VOLOKKFI.js.map} +0 -0
- /package/dist/{sync-K3PKHVLH.js.map → sync-F4USL4K5.js.map} +0 -0
- /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-OXSWKVZ2.js.map} +0 -0
- /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-EVAL3JTO.js.map} +0 -0
- /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
- /package/dist/{websites-TB4TCAWM.js.map → websites-Y6FN2XFD.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → write-audits-to-airtable-TI3JMWSY.js.map} +0 -0
|
@@ -1,23 +1,3 @@
|
|
|
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
1
|
// src/recipes/match-harness/template.ts
|
|
22
2
|
var HARNESS_MJS_RELATIVE = "matching/harness.mjs";
|
|
23
3
|
var HARNESS_MJS_TEMPLATE = `// The single source for everything the matching gates need to know about this
|
|
@@ -62,10 +42,31 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
62
42
|
|
|
63
43
|
// fileURLToPath, not URL#pathname: pathname is percent-encoded, so a checkout
|
|
64
44
|
// under a directory with a space in it would resolve to a path that does not
|
|
65
|
-
// exist.
|
|
66
|
-
|
|
45
|
+
// exist. EXPORTED so next.mjs, strikes.mjs and build-spec.mjs take the resolved
|
|
46
|
+
// directory from here instead of each recomputing it \u2014 three of them did, all
|
|
47
|
+
// with the rejected form, and the one with teeth was strikes.mjs's PAUSED path:
|
|
48
|
+
// under a percent-encoding checkout \`existsSync\` said false and the pause
|
|
49
|
+
// switch failed OPEN (#47).
|
|
50
|
+
export const DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
67
51
|
const CFG = JSON.parse(readFileSync(join(DIR, "harness.json"), "utf8"));
|
|
68
52
|
|
|
53
|
+
// A page key is interpolated into a RegExp (specHeadingRe, below) and into
|
|
54
|
+
// gate.sh's \`grep -qE\` \u2014 unescaped, in both. Measured: \`specHeadingRe("a.c")\`
|
|
55
|
+
// matched \`## abc\` (a WRONG page's section satisfies the check) and
|
|
56
|
+
// \`specHeadingRe("a(b")\` threw. So the vocabulary is refused ONCE, here, where
|
|
57
|
+
// every consumer reads the table \u2014 including gate.sh, which only ever sees keys
|
|
58
|
+
// through \`--table\` \u2014 and a bad key is a load-time error naming itself rather
|
|
59
|
+
// than a fail-open pattern three files away (#50). The set is what a gate key
|
|
60
|
+
// already has to be: it names a run dir, a SPEC heading and a spec-sections
|
|
61
|
+
// file.
|
|
62
|
+
const KEY_RE = /^[a-z0-9-]+$/;
|
|
63
|
+
for (const key of Object.keys(CFG.pages ?? {})) {
|
|
64
|
+
if (!KEY_RE.test(key))
|
|
65
|
+
throw new Error(
|
|
66
|
+
\`harness.json: page key \${JSON.stringify(key)} must match \${KEY_RE} \u2014 keys are interpolated unescaped into specHeadingRe and gate.sh's grep\`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
69
70
|
// Env overrides exist for one-off probes only. They are NOT how a site is
|
|
70
71
|
// configured \u2014 harness.json is, so that what a gate ran against is committed.
|
|
71
72
|
export const REF = process.env.MATCH_REF ?? CFG.ref;
|
|
@@ -143,6 +144,27 @@ export const TOTALS = Object.fromEntries(
|
|
|
143
144
|
PAGES.map((p) => [p.key, scorable(p.key) ? (p.anchors.length + 1) * MATRIX.length : null]),
|
|
144
145
|
);
|
|
145
146
|
|
|
147
|
+
/** WHY a report's region count is not the one TOTALS predicts for \`page\` \u2014
|
|
148
|
+
* as a string \u2014 or null when it is, or when the page is not scorable and so
|
|
149
|
+
* has no identity to hold (an unanchored page's count is page-diff's own;
|
|
150
|
+
* see TOTALS).
|
|
151
|
+
*
|
|
152
|
+
* ONE predicate for two callers, because the identity lived only in checkRun
|
|
153
|
+
* and only \`gate.sh --check-run\` asked it: next.mjs gated a report through
|
|
154
|
+
* \`uncountable(meta)\` alone, none of whose arms looks at how many regions a
|
|
155
|
+
* report carries, so a page whose report came up SHORT \u2014 an anchor whose text
|
|
156
|
+
* had gone from the rendered page, the ordinary way, is what a slice rename
|
|
157
|
+
* does \u2014 contributed no failing region, fell through to "Backlog is empty" and
|
|
158
|
+
* exited 0 over three regions nobody measured. Same sentence as the
|
|
159
|
+
* \`unmeasured\` guard beside it, one level down (#756). */
|
|
160
|
+
export function regionCountWhy(page, report) {
|
|
161
|
+
const expected = TOTALS[page];
|
|
162
|
+
if (expected == null) return null;
|
|
163
|
+
const n = Array.isArray(report?.regions) ? report.regions.length : 0;
|
|
164
|
+
if (n === expected) return null;
|
|
165
|
+
return \`\${n} region(s), expected \${expected} = (\${byKey[page].anchors.length} anchors + 1) x \${MATRIX.length} viewport(s)\`;
|
|
166
|
+
}
|
|
167
|
+
|
|
146
168
|
/** The SPEC.md heading predicate, shared by gate.sh's preflight and
|
|
147
169
|
* build-spec.mjs so a section can never build fine and then refuse at the
|
|
148
170
|
* gate. Matches the key followed by any non-key character (\`## team\` matches,
|
|
@@ -333,8 +355,11 @@ export function checkRun(page, dir, startedAt) {
|
|
|
333
355
|
// gate-shaped runs in the corpus this harness was cut from, all of them
|
|
334
356
|
// anchored: regions.length === (sections + 1) * viewports holds 298/298,
|
|
335
357
|
// while regions.length === TOTALS[page] holds only 260/298 \u2014 the 38 are
|
|
336
|
-
// legitimately narrower HAND rounds. So the
|
|
337
|
-
//
|
|
358
|
+
// legitimately narrower HAND rounds. So the two arms ABOVE hold the run's own
|
|
359
|
+
// matrix and anchors to the table, and only then is the count asked of
|
|
360
|
+
// regionCountWhy, whose prediction is the table's: at this point the two
|
|
361
|
+
// derivations are the same number, and a HAND round has already been refused
|
|
362
|
+
// by name for its narrower matrix or anchors, not for the count that follows.
|
|
338
363
|
//
|
|
339
364
|
// WITHOUT anchors there is no such identity, and asserting one is a FALSE
|
|
340
365
|
// REFUSAL of the shape every new site starts in. page-diff falls back to each
|
|
@@ -344,14 +369,10 @@ export function checkRun(page, dir, startedAt) {
|
|
|
344
369
|
// page-diff on a seed harness (anchors: [], matrix of 4): 16 regions labelled
|
|
345
370
|
// grid-0-0 \u2026 grid-3-0, not the 4 this identity predicted. The 298/298 above
|
|
346
371
|
// was measured over anchored runs only and never covered this case.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
ok: false,
|
|
352
|
-
why: \`\${path}: \${report.regions.length} region(s), expected \${expected} = (\${secs.length} anchors + 1) x \${vws.length} viewport(s)\`,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
372
|
+
// regionCountWhy returns null for such a page (TOTALS is null there), which
|
|
373
|
+
// is the \`if (secs.length)\` guard this used to carry, kept in one place.
|
|
374
|
+
const count = regionCountWhy(page, report);
|
|
375
|
+
if (count) return { ok: false, why: \`\${path}: \${count}\` };
|
|
355
376
|
|
|
356
377
|
// A green that STATES what it is made of, so a green over nothing reads
|
|
357
378
|
// differently from a green over the matrix (census.sh:219's habit).
|
|
@@ -664,6 +685,20 @@ var CENSUS_SH_TEMPLATE = `#!/usr/bin/env bash
|
|
|
664
685
|
# demands an artefact only a working census produces; none of them can turn a
|
|
665
686
|
# red green.
|
|
666
687
|
set -uo pipefail
|
|
688
|
+
|
|
689
|
+
# PAUSE SWITCH \u2014 the same early exit next.mjs and strikes.mjs take, for the same
|
|
690
|
+
# reason (see next.mjs). This gate is the third door into a round: measured on
|
|
691
|
+
# the site this was cut from, PAUSED since 2026-09-01, a run here would have
|
|
692
|
+
# spent 27 browser pairs against the live reference during a declared pause. A
|
|
693
|
+
# switch two of three gates honour is not a switch
|
|
694
|
+
# (reddoorla/reddoor-maintenance#735).
|
|
695
|
+
if [ -f "$(dirname "$0")/PAUSED" ]; then
|
|
696
|
+
echo "MATCHING PAUSED \u2014 no agenda, and none is to be inferred."
|
|
697
|
+
echo
|
|
698
|
+
cat "$(dirname "$0")/PAUSED"
|
|
699
|
+
exit 0
|
|
700
|
+
fi
|
|
701
|
+
|
|
667
702
|
# Resolved BEFORE the cd. $0 is the path as TYPED, so a relatively invoked
|
|
668
703
|
# census.sh that resolved the harness afterwards would look for it under
|
|
669
704
|
# whatever root the cd landed on.
|
|
@@ -874,8 +909,10 @@ var NEXT_MJS_TEMPLATE = `// What is still broken, ranked. Exits 1 while work rem
|
|
|
874
909
|
// reflects HEAD rather than history (that is strikes.mjs's job).
|
|
875
910
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
876
911
|
import { join } from "node:path";
|
|
877
|
-
|
|
878
|
-
|
|
912
|
+
// DIR comes from harness.mjs (imported below \u2014 ESM hoists it, so it is bound
|
|
913
|
+
// before the pause check runs). It was \`new URL(".", import.meta.url).pathname\`
|
|
914
|
+
// here, which percent-encodes, so under a checkout path with a space in it this
|
|
915
|
+
// read a directory that does not exist (#47).
|
|
879
916
|
|
|
880
917
|
// PAUSE SWITCH. While matching/PAUSED exists this hands out no agenda and
|
|
881
918
|
// exits 0. It is deliberately the FIRST thing that runs: no report is read, no
|
|
@@ -895,6 +932,7 @@ if (existsSync(PAUSE)) {
|
|
|
895
932
|
|
|
896
933
|
import { FLOORS, ACCEPTED } from "./floors.mjs";
|
|
897
934
|
import {
|
|
935
|
+
DIR,
|
|
898
936
|
TOTALS,
|
|
899
937
|
THRESHOLD,
|
|
900
938
|
MAX_HEIGHT_DELTA,
|
|
@@ -902,6 +940,7 @@ import {
|
|
|
902
940
|
uncountable,
|
|
903
941
|
scorable,
|
|
904
942
|
unscorableWhy,
|
|
943
|
+
regionCountWhy,
|
|
905
944
|
byKey,
|
|
906
945
|
} from "./harness.mjs";
|
|
907
946
|
|
|
@@ -1025,6 +1064,20 @@ const scored = [...latest.entries()]
|
|
|
1025
1064
|
const unmeasured = Object.keys(TOTALS)
|
|
1026
1065
|
.filter((p) => !latest.has(p))
|
|
1027
1066
|
.sort();
|
|
1067
|
+
|
|
1068
|
+
// Pages that REPORTED, are scorable, and whose newest report carries FEWER
|
|
1069
|
+
// regions than TOTALS predicts. \`uncountable()\` cannot see this \u2014 none of its
|
|
1070
|
+
// arms counts regions \u2014 so a short report was fully countable, contributed no
|
|
1071
|
+
// failing region, and fell through to "Backlog is empty", exit 0, over regions
|
|
1072
|
+
// nobody had measured. The score line stayed honest (\`3/6\`), which is what made
|
|
1073
|
+
// it easy to walk past. The identity is harness.mjs's regionCountWhy, the same
|
|
1074
|
+
// one gate.sh's --check-run refuses a run with \u2014 asked here of the report being
|
|
1075
|
+
// SCORED, because a report can reach \`latest\` without ever passing through the
|
|
1076
|
+
// gate (#756).
|
|
1077
|
+
const short = scored
|
|
1078
|
+
.map((s) => ({ p: s.p, why: regionCountWhy(s.p, latest.get(s.p).report) }))
|
|
1079
|
+
.filter((s) => s.why)
|
|
1080
|
+
.sort((a, b) => a.p.localeCompare(b.p));
|
|
1028
1081
|
const sum = scored.reduce((a, s) => a + s.pass, 0);
|
|
1029
1082
|
// Summed EXPLICITLY over the scorable pages rather than over TOTALS' values:
|
|
1030
1083
|
// \`a + null\` is silently \`a\`, and a denominator that is right only because of a
|
|
@@ -1097,6 +1150,17 @@ if (unscorable.length) {
|
|
|
1097
1150
|
);
|
|
1098
1151
|
}
|
|
1099
1152
|
|
|
1153
|
+
if (short.length) {
|
|
1154
|
+
console.error(
|
|
1155
|
+
\`\\nnext: \${short.length} page(s) reported fewer regions than their anchors predict, so their\\n\` +
|
|
1156
|
+
\` score has regions nobody measured \u2014 \` +
|
|
1157
|
+
short.map((s) => \`\${s.p}: \${s.why}\`).join("; ") +
|
|
1158
|
+
\`.\\n An anchor whose text is no longer on the page cuts no region: check the anchors in\\n\` +
|
|
1159
|
+
\` matching/harness.json against BOTH renders, then re-run: bash matching/gate.sh <tag> \` +
|
|
1160
|
+
short.map((s) => s.p).join(" "),
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1100
1164
|
if (unmeasured.length) {
|
|
1101
1165
|
console.error(
|
|
1102
1166
|
\`\\nnext: \${unmeasured.length} page(s) have no countable gate run \u2014 \${unmeasured.join(", ")}.\\n\` +
|
|
@@ -1105,8 +1169,10 @@ if (unmeasured.length) {
|
|
|
1105
1169
|
process.exit(2);
|
|
1106
1170
|
}
|
|
1107
1171
|
|
|
1108
|
-
// See above: deliberately
|
|
1172
|
+
// See above: deliberately separate statements, not an \`else\` chain \u2014 every
|
|
1173
|
+
// refusal is PRINTED before the first one EXITS.
|
|
1109
1174
|
if (unscorable.length) process.exit(2);
|
|
1175
|
+
if (short.length) process.exit(2);
|
|
1110
1176
|
|
|
1111
1177
|
if (!rows.length) {
|
|
1112
1178
|
console.log(
|
|
@@ -1148,10 +1214,11 @@ for (const r of rows.filter((r) => r.page === worst)) {
|
|
|
1148
1214
|
\` @\${String(r.vw).padEnd(5)} \${r.label.slice(0, 44).padEnd(45)} \${why.join(" + ")}\`,
|
|
1149
1215
|
);
|
|
1150
1216
|
}
|
|
1217
|
+
// Only scripts the match-harness recipe INSTALLS may be named here: this is the
|
|
1218
|
+
// first thing an operator is told to run on a failing agenda, and on every site
|
|
1219
|
+
// but this one \`probe-anchor-parity.mjs\` (a site-specific probe, never shipped)
|
|
1220
|
+
// failed module-not-found \u2014 reddoorla/reddoor-maintenance#732, #767.
|
|
1151
1221
|
console.log(\`\\nBefore treating any of these as geometry:\`);
|
|
1152
|
-
console.log(
|
|
1153
|
-
\` node matching/probe-anchor-parity.mjs \${worst} # is the gate cutting comparably?\`,
|
|
1154
|
-
);
|
|
1155
1222
|
console.log(
|
|
1156
1223
|
\` node matching/strikes.mjs \${worst} # has it stalled? then change the MODEL\`,
|
|
1157
1224
|
);
|
|
@@ -1185,8 +1252,9 @@ var STRIKES_MJS_TEMPLATE = `// The 3-strike rule, made countable.
|
|
|
1185
1252
|
// upper bound. Improvement resets the count: real progress earns a fresh start.
|
|
1186
1253
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
1187
1254
|
import { join } from "node:path";
|
|
1188
|
-
import { FLOORS } from "./floors.mjs";
|
|
1255
|
+
import { FLOORS, ACCEPTED } from "./floors.mjs";
|
|
1189
1256
|
import {
|
|
1257
|
+
DIR,
|
|
1190
1258
|
PAGES,
|
|
1191
1259
|
THRESHOLD,
|
|
1192
1260
|
MAX_HEIGHT_DELTA,
|
|
@@ -1197,7 +1265,11 @@ import {
|
|
|
1197
1265
|
// protocol step 1 is "run strikes.mjs; the stalled regions are the agenda", so
|
|
1198
1266
|
// this is the second door into the backlog and has to be shut too. Exits 0:
|
|
1199
1267
|
// there are no strikes to spend when no round is being played.
|
|
1200
|
-
|
|
1268
|
+
//
|
|
1269
|
+
// DIR is harness.mjs's fileURLToPath'd directory, not URL#pathname: pathname
|
|
1270
|
+
// percent-encodes, so under a checkout path with a space this \`existsSync\` was
|
|
1271
|
+
// false and the pause switch failed OPEN \u2014 an agenda handed out mid-pause (#47).
|
|
1272
|
+
const PAUSED = join(DIR, "PAUSED");
|
|
1201
1273
|
if (existsSync(PAUSED)) {
|
|
1202
1274
|
console.log("MATCHING PAUSED \u2014 no agenda, and none is to be inferred.\\n");
|
|
1203
1275
|
console.log(readFileSync(PAUSED, "utf8").trimEnd());
|
|
@@ -1206,7 +1278,6 @@ if (existsSync(PAUSED)) {
|
|
|
1206
1278
|
|
|
1207
1279
|
const IMPROVE_PP = 0.01; // 1 percentage point = the smallest move worth calling progress
|
|
1208
1280
|
const MAX_STRIKES = 3;
|
|
1209
|
-
const ROOT = new URL(".", import.meta.url).pathname;
|
|
1210
1281
|
const only = process.argv[2] ?? null;
|
|
1211
1282
|
|
|
1212
1283
|
// The gate key for a report, from its ref path. Deriving it by string surgery
|
|
@@ -1220,10 +1291,10 @@ const pageOf = (ref) => {
|
|
|
1220
1291
|
|
|
1221
1292
|
const runs = [];
|
|
1222
1293
|
const unreadable = [];
|
|
1223
|
-
for (const dir of readdirSync(
|
|
1294
|
+
for (const dir of readdirSync(DIR).filter((d) => d.startsWith("out-"))) {
|
|
1224
1295
|
let report;
|
|
1225
1296
|
try {
|
|
1226
|
-
report = JSON.parse(readFileSync(join(
|
|
1297
|
+
report = JSON.parse(readFileSync(join(DIR, dir, "report.json"), "utf8"));
|
|
1227
1298
|
} catch {
|
|
1228
1299
|
continue; // not a completed run dir
|
|
1229
1300
|
}
|
|
@@ -1279,18 +1350,27 @@ const keyOf = (dir) => /^out-[^-]+-(.+)$/.exec(dir)?.[1] ?? null;
|
|
|
1279
1350
|
|
|
1280
1351
|
// key -> chronological list of {dir, at, mm, pass, masked}
|
|
1281
1352
|
const history = new Map();
|
|
1282
|
-
|
|
1353
|
+
// Regions the OPERATOR accepted, by key \u2014 counted so the skip is visible, never
|
|
1354
|
+
// silent (next.mjs prints them under their own heading for the same reason).
|
|
1355
|
+
const acceptedKeys = new Set();
|
|
1283
1356
|
for (const run of runs) {
|
|
1284
1357
|
const page = pageOf(run.meta.ref) ?? keyOf(run.dir) ?? "unknown";
|
|
1285
1358
|
const gateKey = keyOf(run.dir);
|
|
1286
|
-
seenNames.add(page);
|
|
1287
|
-
if (gateKey) seenNames.add(gateKey);
|
|
1288
1359
|
if (only && page !== only && gateKey !== only) continue;
|
|
1289
1360
|
for (const r of run.regions) {
|
|
1290
1361
|
// A DECLARED FLOOR is flat by definition \u2014 reporting it as stalled is noise
|
|
1291
1362
|
// that hides a real stall. It stays in the LEDGER; it does not belong here.
|
|
1292
1363
|
if (FLOORS.some((fl) => fl.match(r, gateKey || page))) continue;
|
|
1293
1364
|
const key = \`\${page}|\${r.viewport}|\${r.label}\`;
|
|
1365
|
+
// An operator-ACCEPTED region will not move either \u2014 that is what accepting
|
|
1366
|
+
// it meant \u2014 so it is flat by definition too. Left in, it tripped this
|
|
1367
|
+
// detector on every run from the decision onward, and the one lever that
|
|
1368
|
+
// silenced it was the reclassification rule 3 forbids: moving the entry to
|
|
1369
|
+
// FLOORS (reddoorla/reddoor-maintenance#772).
|
|
1370
|
+
if (ACCEPTED.some((a) => a.match(r, gateKey || page))) {
|
|
1371
|
+
acceptedKeys.add(key);
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1294
1374
|
if (!history.has(key)) history.set(key, []);
|
|
1295
1375
|
history.get(key).push({
|
|
1296
1376
|
dir: run.dir,
|
|
@@ -1310,15 +1390,25 @@ for (const run of runs) {
|
|
|
1310
1390
|
// A name that matches no run must NOT report "clear". This check exists to stop
|
|
1311
1391
|
// work on a stalled region, so failing open is the one thing it may never do \u2014
|
|
1312
1392
|
// a typo'd or wrong-vocabulary page silently greened rule 3 for six of the nine
|
|
1313
|
-
// pages. Fail loud instead, and say what the vocabulary is
|
|
1393
|
+
// pages. Fail loud instead, and say what the vocabulary is \u2014 the TABLE's keys,
|
|
1394
|
+
// which is all a user should ever type. This used to list every name seen in
|
|
1395
|
+
// the corpus (pageOf AND keyOf of every run dir): 26 names on this site, of
|
|
1396
|
+
// which 9 were pages, 3 were a retired vocabulary each backed by one legacy run
|
|
1397
|
+
// that read as *clear*, and 14 were probe dirs. keyOf stays as the LOOKUP
|
|
1398
|
+
// fallback for exactly those dirs; it was never the vocabulary (#48).
|
|
1314
1399
|
if (only && history.size === 0) {
|
|
1315
1400
|
console.error(
|
|
1316
1401
|
\`strikes: "\${only}" matches no gate run \u2014 refusing to report "clear".\\n\` +
|
|
1317
|
-
\` known pages: \${
|
|
1402
|
+
\` known pages: \${PAGES.map((p) => p.key).sort().join(", ")}\`,
|
|
1318
1403
|
);
|
|
1319
1404
|
process.exit(2);
|
|
1320
1405
|
}
|
|
1321
1406
|
|
|
1407
|
+
if (acceptedKeys.size)
|
|
1408
|
+
console.log(
|
|
1409
|
+
\`strikes: \${acceptedKeys.size} region(s) skipped as operator-accepted (floors.mjs ACCEPTED).\`,
|
|
1410
|
+
);
|
|
1411
|
+
|
|
1322
1412
|
const stuck = [];
|
|
1323
1413
|
for (const [key, all] of history) {
|
|
1324
1414
|
const h = all.filter((e) => !e.dirty); // compare like with like
|
|
@@ -1407,9 +1497,10 @@ var BUILD_SPEC_MJS_TEMPLATE = `// Assemble matching/SPEC.md from the per-page se
|
|
|
1407
1497
|
// no longer build fine here and then re-block its own page at the gate.
|
|
1408
1498
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
1409
1499
|
import { join } from "node:path";
|
|
1410
|
-
|
|
1500
|
+
// DIR too: harness.mjs resolves it with fileURLToPath, and the URL#pathname
|
|
1501
|
+
// form this file used percent-encodes a space in the checkout path (#47).
|
|
1502
|
+
import { DIR, PAGES, specHeadingRe } from "./harness.mjs";
|
|
1411
1503
|
|
|
1412
|
-
const DIR = new URL(".", import.meta.url).pathname;
|
|
1413
1504
|
const SECTIONS = join(DIR, "spec-sections");
|
|
1414
1505
|
|
|
1415
1506
|
// Order = live's nav order, then the detail templates. Both fall out of the
|
|
@@ -1637,13 +1728,16 @@ var MATCH_ROUTE_SERVER_TEMPLATE = `import { error } from "@sveltejs/kit";
|
|
|
1637
1728
|
import { dev } from "$app/environment";
|
|
1638
1729
|
import { documents } from "$lib/site-pages.js";
|
|
1639
1730
|
|
|
1640
|
-
// Local matching surface: renders the
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
1731
|
+
// Local matching surface: renders the assemblies in $lib/site-pages.js \u2014 the
|
|
1732
|
+
// same module a Prismic Migration API script publishes from (start from the
|
|
1733
|
+
// starter's scripts/import/migrate.example.ts; no \`reddoor-maint\` command does
|
|
1734
|
+
// this, the seed is per-site work) \u2014 so a fix made to pass a gate is a fix to
|
|
1735
|
+
// what ships. Not prerendered, SSR-on-demand, dev-only.
|
|
1643
1736
|
export const prerender = false;
|
|
1644
1737
|
|
|
1645
|
-
//
|
|
1646
|
-
// are nominal \u2014 slices size their own
|
|
1738
|
+
// A migration script resolves images to asset ids (migration.createAsset);
|
|
1739
|
+
// here they only need a URL. Dimensions are nominal \u2014 slices size their own
|
|
1740
|
+
// image boxes in CSS.
|
|
1647
1741
|
const devImg = (u: string) => ({
|
|
1648
1742
|
url: u,
|
|
1649
1743
|
alt: null,
|
|
@@ -1661,9 +1755,14 @@ export async function load({ params }) {
|
|
|
1661
1755
|
|
|
1662
1756
|
const docs = documents(devImg) as Array<{ uid: string; data: { slices?: unknown[] } }>;
|
|
1663
1757
|
const doc = docs.find((d) => d.uid === params.uid);
|
|
1758
|
+
// The leading token is a MACHINE tell for the launch recipe's dev-guard. This
|
|
1759
|
+
// 404 renders through the site's own +error.svelte exactly like a guarded
|
|
1760
|
+
// route's does, so on a site whose uids lack "home" an UNGUARDED twin would
|
|
1761
|
+
// pass the deployed check forever; the token is what tells the two apart.
|
|
1762
|
+
// Reword the prose freely \u2014 the token is the contract (#719).
|
|
1664
1763
|
if (!doc)
|
|
1665
1764
|
error(404, {
|
|
1666
|
-
message: \`no assembly for "\${params.uid}" (have: \${docs.map((d) => d.uid).join(", ") || "none"})\`,
|
|
1765
|
+
message: \`reddoor-match-twin:no-assembly: no assembly for "\${params.uid}" (have: \${docs.map((d) => d.uid).join(", ") || "none"})\`,
|
|
1667
1766
|
});
|
|
1668
1767
|
|
|
1669
1768
|
return { uid: params.uid, slices: doc.data.slices ?? [] };
|
|
@@ -1681,10 +1780,11 @@ var MATCH_ROUTE_PAGE_TEMPLATE = `<script lang="ts">
|
|
|
1681
1780
|
`;
|
|
1682
1781
|
var SITE_PAGES_JS_RELATIVE = "src/lib/site-pages.js";
|
|
1683
1782
|
var SITE_PAGES_JS_TEMPLATE = `// The page assemblies for this site \u2014 the SINGLE source of truth for both
|
|
1684
|
-
// consumers:
|
|
1685
|
-
//
|
|
1686
|
-
//
|
|
1687
|
-
//
|
|
1783
|
+
// consumers: a Prismic Migration API script, which publishes them (start from
|
|
1784
|
+
// the starter's scripts/import/migrate.example.ts \u2014 no \`reddoor-maint\` command
|
|
1785
|
+
// does this; the seed is per-site work), and src/routes/dev/match/[uid], the
|
|
1786
|
+
// local matching surface. Because both read from here, any fix made to pass a
|
|
1787
|
+
// gate is a fix to what ships.
|
|
1688
1788
|
//
|
|
1689
1789
|
// THE MIGRATION API DROPS SILENTLY. It validates against the slice models
|
|
1690
1790
|
// registered in Prismic and discards every field the model does not declare \u2014
|
|
@@ -1710,7 +1810,9 @@ export function documents(img) {
|
|
|
1710
1810
|
var SITE_PAGES_TEST_RELATIVE = "src/lib/site-pages.test.ts";
|
|
1711
1811
|
var SITE_PAGES_TEST_TEMPLATE = `// The page assemblies in src/lib/site-pages.js are the SINGLE source of truth
|
|
1712
1812
|
// shared by two consumers: the local matching route (src/routes/dev/match/[uid])
|
|
1713
|
-
// and
|
|
1813
|
+
// and a Prismic Migration API script, which publishes them (start from the
|
|
1814
|
+
// starter's scripts/import/migrate.example.ts \u2014 no \`reddoor-maint\` command does
|
|
1815
|
+
// this; the seed is per-site work).
|
|
1714
1816
|
//
|
|
1715
1817
|
// Those two consumers do NOT validate the same way. The dev route hands the
|
|
1716
1818
|
// object straight to the slice components, so any field a fixture sets is
|
|
@@ -2323,120 +2425,1497 @@ console.log(
|
|
|
2323
2425
|
);
|
|
2324
2426
|
process.exit(1);
|
|
2325
2427
|
`;
|
|
2326
|
-
var
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
matching
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
matching
|
|
2356
|
-
matching
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
# prettier config differs must not reformat them: a reformatted file is
|
|
2366
|
-
# indistinguishable from a hand edit, and the recipe refuses to overwrite a hand
|
|
2367
|
-
# edit. Measured on a \`useTabs\`/\`singleQuote\` site, all three src/ entries
|
|
2368
|
-
# below are rewritten by \`prettier --write .\`, and the next upgrade is refused
|
|
2369
|
-
# for all three.
|
|
2370
|
-
#
|
|
2371
|
-
# The bracketed route segment MUST stay backslash-escaped. This file is
|
|
2372
|
-
# gitignore glob syntax, in which \`[uid]\` is a CHARACTER CLASS matching one of
|
|
2373
|
-
# \`u\`, \`i\`, \`d\` \u2014 an unescaped entry silently ignores nothing at all.
|
|
2374
|
-
#
|
|
2375
|
-
# \`matching/*.mjs\` also catches two SITE-owned records (floors.mjs,
|
|
2376
|
-
# census-deviations.mjs). Accepted, not overlooked: eslint lints .mjs, so those
|
|
2377
|
-
# two keep a style check either way, and naming seven scripts instead of one
|
|
2378
|
-
# glob goes stale on the next script the harness gains.
|
|
2379
|
-
#
|
|
2380
|
-
# harness.json is here because it is a TABLE, not prose: it carries a
|
|
2381
|
-
# hand-chosen row layout that prettier reflows.
|
|
2382
|
-
#
|
|
2383
|
-
# Everything else under matching/ (SPEC.md, LEDGER.md, spec-sections/,
|
|
2384
|
-
# states/*.mjs) stays inside \`prettier --check .\`. That is deliberately narrow:
|
|
2385
|
-
# eslint does not lint Markdown, so for the records prettier is the ONLY style
|
|
2386
|
-
# check there is, and a blanket \`matching/\` entry would leave them with none.
|
|
2387
|
-
matching/*.mjs
|
|
2388
|
-
matching/*.sh
|
|
2389
|
-
matching/harness.json
|
|
2390
|
-
src/routes/dev/match/\\[uid\\]/+page.server.ts
|
|
2391
|
-
src/routes/dev/match/\\[uid\\]/+page.svelte
|
|
2392
|
-
src/lib/site-pages.test.ts
|
|
2393
|
-
`;
|
|
2394
|
-
var CLAUDE_MD_MARKER = "## Matching rules (installed by reddoor-maint match-harness)";
|
|
2395
|
-
var CLAUDE_MD_BLOCK = `
|
|
2396
|
-
The \`matching-a-page\` skill governs a live-reference rebuild. These five rules
|
|
2397
|
-
exist because the skill alone did not hold on the project this harness came
|
|
2398
|
-
from \u2014 each one is a drift that actually happened, with the mechanical check
|
|
2399
|
-
that now catches it. \`matching/harness.json\` is this site's configuration;
|
|
2400
|
-
\`matching/LEDGER.md\` is the dated record of every deviation, floor and mask.
|
|
2401
|
-
|
|
2402
|
-
### 1. Source prescribes, rects only verify
|
|
2403
|
-
|
|
2404
|
-
Every geometry fix must cite the rule it came from: a line in the captured
|
|
2405
|
-
reference stylesheet under \`matching/spec/\`, or the reference's HTML. "The probe
|
|
2406
|
-
says the gap is 40px" is not a source. If you cannot name the line you are
|
|
2407
|
-
guessing \u2014 go read the stylesheet first.
|
|
2408
|
-
|
|
2409
|
-
**Check:** the commit body must name the file:line for each fix.
|
|
2410
|
-
**Operator's challenge:** _"which line of the reference stylesheet says that?"_
|
|
2411
|
-
|
|
2412
|
-
### 2. Phase 1 before Phase 4
|
|
2413
|
-
|
|
2414
|
-
A page gets its section census and per-section spec in \`matching/SPEC.md\` BEFORE
|
|
2415
|
-
its geometry is touched. No SPEC section, no geometry round. The census is the
|
|
2416
|
-
coverage denominator; skipping it is how a reference's root-font ladder and its
|
|
2417
|
-
per-component height ladders get discovered reactively, after the region has
|
|
2418
|
-
already failed several rounds.
|
|
2428
|
+
var HARNESS_MJS_PREV_0_95_1 = `// The single source for everything the matching gates need to know about this
|
|
2429
|
+
// site. DATA lives in matching/harness.json (site-edited); this file is the
|
|
2430
|
+
// READ LAYER \u2014 edit harness.json, not this. (It is installed and upgraded by
|
|
2431
|
+
// the \`reddoor-maint match-harness\` recipe, which owns these bytes: a hand
|
|
2432
|
+
// edit here is flagged on the next run and never silently overwritten.)
|
|
2433
|
+
//
|
|
2434
|
+
// It exists because the page table, the two hosts, the matrix, the threshold
|
|
2435
|
+
// and the skill path were hand-copied all over matching/. Re-measured
|
|
2436
|
+
// 2026-09-09 AFTER the six-probe conversion, over the 216 tracked scripts under
|
|
2437
|
+
// matching/ (214 top-level \u2014 212 .mjs + 2 .sh \u2014 plus 2 in states/):
|
|
2438
|
+
//
|
|
2439
|
+
// \u2022 a hand-typed copy of the page table (three or more gate keys sitting next
|
|
2440
|
+
// to their route): 8 files. Exactly ONE of them, probe-chrome-count.mjs,
|
|
2441
|
+
// still carries all nine rows; probe-anchors.mjs carries five; the other
|
|
2442
|
+
// six are three-row detail triples (team/svc/qa).
|
|
2443
|
+
// \u2022 the skill path (~/.claude/skills/matching-a-page): 193 copies
|
|
2444
|
+
// \u2022 the viewport matrix (1440/834/390): 51 copies
|
|
2445
|
+
// \u2022 REF pointed at a host listed in selfHosts \u2014 i.e. comparing the candidate
|
|
2446
|
+
// with itself: 12 scripts, one of which (probe-chrome-count.mjs) is also
|
|
2447
|
+
// the last nine-row table carrier
|
|
2448
|
+
//
|
|
2449
|
+
// The first bullet read "the nine-row page table: 5 copies \u2014 gate.sh,
|
|
2450
|
+
// probe-anchor-parity.mjs, sweep-all10.sh, sweep-all16.sh, sweep-final.sh" when
|
|
2451
|
+
// it was written here at 922dde3. It was wrong within the hour and wrong on two
|
|
2452
|
+
// counts: 4e2cd7b took the table out of gate.sh, and the list never named
|
|
2453
|
+
// states/index.mjs or probe-chrome-count.mjs, which were both carrying nine-row
|
|
2454
|
+
// copies at the time. A census is a claim about code; it has to be measured
|
|
2455
|
+
// against the tree, not recalled.
|
|
2456
|
+
//
|
|
2457
|
+
// node matching/harness.mjs --env shell-safe KEY='value' lines
|
|
2458
|
+
// node matching/harness.mjs --table key<TAB>ref<TAB>cand<TAB>anchors
|
|
2459
|
+
// node matching/harness.mjs --check-ref the D11 preflight; exit 2 on failure
|
|
2460
|
+
// node matching/harness.mjs --check-run <page> <out-dir> <startedAt-iso>
|
|
2461
|
+
// did THIS run leave a countable report?
|
|
2462
|
+
// exit 2 when it did not
|
|
2463
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
2464
|
+
import { homedir } from "node:os";
|
|
2465
|
+
import { join } from "node:path";
|
|
2466
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
2419
2467
|
|
|
2420
|
-
|
|
2421
|
-
|
|
2468
|
+
// fileURLToPath, not URL#pathname: pathname is percent-encoded, so a checkout
|
|
2469
|
+
// under a directory with a space in it would resolve to a path that does not
|
|
2470
|
+
// exist.
|
|
2471
|
+
const DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
2472
|
+
const CFG = JSON.parse(readFileSync(join(DIR, "harness.json"), "utf8"));
|
|
2422
2473
|
|
|
2423
|
-
|
|
2474
|
+
// Env overrides exist for one-off probes only. They are NOT how a site is
|
|
2475
|
+
// configured \u2014 harness.json is, so that what a gate ran against is committed.
|
|
2476
|
+
export const REF = process.env.MATCH_REF ?? CFG.ref;
|
|
2477
|
+
export const CAND = process.env.MATCH_CAND ?? process.env.CAND_BASE ?? CFG.cand;
|
|
2478
|
+
export const MATRIX = CFG.matrix;
|
|
2479
|
+
export const THRESHOLD = CFG.threshold;
|
|
2480
|
+
export const MAX_HEIGHT_DELTA = CFG.maxHeightDelta;
|
|
2481
|
+
export const REF_MARK = CFG.refMark;
|
|
2482
|
+
export const CAND_MARK = CFG.candMark;
|
|
2483
|
+
export const SELF_HOSTS = CFG.selfHosts ?? [];
|
|
2424
2484
|
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2485
|
+
/** One record per gated page, in harness.json order. \`key\` is the gate key
|
|
2486
|
+
* (out-<TAG>-<key>, the SPEC heading, spec-sections/<key>.md); \`uid\` is the
|
|
2487
|
+
* Prismic uid or null where the page has no /dev/match twin. */
|
|
2488
|
+
export const PAGES = Object.entries(CFG.pages).map(([key, p]) => ({ key, ...p }));
|
|
2489
|
+
export const byKey = Object.fromEntries(PAGES.map((p) => [p.key, p]));
|
|
2428
2490
|
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2491
|
+
/** Can this page's region count be PREDICTED at all? It needs anchors \u2014 see
|
|
2492
|
+
* TOTALS below \u2014 AND a matrix to measure them at. Exported beside TOTALS
|
|
2493
|
+
* rather than left for each consumer to re-derive, because "remember to ask
|
|
2494
|
+
* first" is exactly what failed: checkRun remembered, next.mjs did not, for
|
|
2495
|
+
* two releases.
|
|
2496
|
+
*
|
|
2497
|
+
* \`MATRIX.length > 0\` is not defensive padding. TOTALS is
|
|
2498
|
+
* \`(anchors + 1) * MATRIX.length\`, so an ANCHORED page with \`matrix: []\`
|
|
2499
|
+
* yields 0 \u2014 truthy-adjacent, arithmetically fatal. Measured on a page with 3
|
|
2500
|
+
* anchors, an empty matrix and 8 passing regions: without this clause the
|
|
2501
|
+
* scorer printed \`SCORE 8/0 regions passing\` and \`Backlog is empty \u2014 Phases 5
|
|
2502
|
+
* and 6 are what is left\`, exit 0. The absurd fraction would be questioned;
|
|
2503
|
+
* the sentence would not. \`pass/0\` is also Infinity, so such a page sorts
|
|
2504
|
+
* BEST and can never be named \`worst\` \u2014 the same ranking bug this change set
|
|
2505
|
+
* removed for unanchored pages, one input along. */
|
|
2506
|
+
export const scorable = (key) =>
|
|
2507
|
+
(byKey[key]?.anchors?.length ?? 0) > 0 && MATRIX.length > 0;
|
|
2432
2508
|
|
|
2433
|
-
|
|
2509
|
+
/** WHY a page is not scorable, in the words of the thing that is actually
|
|
2510
|
+
* missing. A refusal that states a cause it did not check is the shape
|
|
2511
|
+
* CLAUDE.md names: a field must never be named after something it cannot
|
|
2512
|
+
* observe. "no anchors" printed for a page carrying three of them sends the
|
|
2513
|
+
* operator to edit the one part of harness.json that was already right. */
|
|
2514
|
+
export const unscorableWhy = (key) =>
|
|
2515
|
+
(byKey[key]?.anchors?.length ?? 0) === 0
|
|
2516
|
+
? "no anchors"
|
|
2517
|
+
: MATRIX.length === 0
|
|
2518
|
+
? "matrix is empty"
|
|
2519
|
+
: null;
|
|
2434
2520
|
|
|
2435
|
-
|
|
2436
|
-
|
|
2521
|
+
// DERIVED, never hand-typed: page-diff cuts one region before the first anchor
|
|
2522
|
+
// ("top") plus one per anchor, at every viewport. The old hand-written map went
|
|
2523
|
+
// stale the moment an anchor list changed, and a wrong denominator makes the
|
|
2524
|
+
// score a lie in the flattering direction.
|
|
2525
|
+
//
|
|
2526
|
+
// THAT IDENTITY HOLDS ONLY WITH ANCHORS. \`splitRegions\` (page-diff.mjs:103-110)
|
|
2527
|
+
// only cuts by anchor when there are anchors to cut by; with none it falls back
|
|
2528
|
+
// to the page's own <section> boxes, and with none of those to an even four-row
|
|
2529
|
+
// grid (lib/regions.mjs:27-35, gridRows = 4, labels \`grid-<r>-<c>\`). So the
|
|
2530
|
+
// count is DATA-DEPENDENT, the two pages need not even agree, and no formula
|
|
2531
|
+
// over anchors can predict it.
|
|
2532
|
+
//
|
|
2533
|
+
// \`checkRun\` below already reaches this conclusion \u2014 its \`if (secs.length)\`
|
|
2534
|
+
// guard declines to assert a region count without anchors, and says why. That
|
|
2535
|
+
// fix was applied to the VALIDATOR and never carried to the DENOMINATOR, so
|
|
2536
|
+
// next.mjs went on dividing a real pass count by an imaginary total. Measured
|
|
2537
|
+
// 2026-09-09 on a seed harness (anchors: [], matrix of 3): page-diff produced
|
|
2538
|
+
// 12 grid regions, all passing, and next.mjs printed \`SCORE 12/3 regions
|
|
2539
|
+
// passing\` followed by "Backlog is empty \u2014 Phases 5 and 6 are what is left",
|
|
2540
|
+
// exit 0. On a matrix of 4 the same seed prints \`SCORE 16/4\`. The absurd
|
|
2541
|
+
// fraction would have been questioned; the sentence would not.
|
|
2542
|
+
//
|
|
2543
|
+
// So an unpredictable page gets NO NUMBER \u2014 \`null\`, not a plausible-looking
|
|
2544
|
+
// integer. That is a SIGNAL, not a barrier: \`a + null\` is \`a\`, so a consumer
|
|
2545
|
+
// that sums TOTALS without asking \`scorable()\` still gets a too-small
|
|
2546
|
+
// denominator. The barrier is \`scorable()\` plus next.mjs's exit-2 refusal.
|
|
2547
|
+
export const TOTALS = Object.fromEntries(
|
|
2548
|
+
PAGES.map((p) => [p.key, scorable(p.key) ? (p.anchors.length + 1) * MATRIX.length : null]),
|
|
2549
|
+
);
|
|
2550
|
+
|
|
2551
|
+
/** The SPEC.md heading predicate, shared by gate.sh's preflight and
|
|
2552
|
+
* build-spec.mjs so a section can never build fine and then refuse at the
|
|
2553
|
+
* gate. Matches the key followed by any non-key character (\`## team\` matches,
|
|
2554
|
+
* \`## teamfoo\` does not, \`## our-team\` cannot match \`team\`). */
|
|
2555
|
+
export const specHeadingRe = (key) => new RegExp(\`^##+ +\${key}([^A-Za-z0-9_-]|$)\`, "m");
|
|
2556
|
+
|
|
2557
|
+
export const SKILL_DIR =
|
|
2558
|
+
process.env.MATCHING_SKILL_DIR ?? join(homedir(), ".claude/skills/matching-a-page");
|
|
2559
|
+
export const PD = join(SKILL_DIR, "page-diff.mjs");
|
|
2560
|
+
export const SC = join(SKILL_DIR, "style-census.mjs");
|
|
2561
|
+
export const PLAYWRIGHT = pathToFileURL(join(SKILL_DIR, "node_modules/playwright/index.mjs")).href;
|
|
2562
|
+
|
|
2563
|
+
/** The report format this site's scripts can read, so gate.sh can compare it
|
|
2564
|
+
* with \`page-diff --version\` before spending a run and next.mjs can refuse
|
|
2565
|
+
* rather than quietly drop a page whose newest report came from another
|
|
2566
|
+
* schema. Both do that now \u2014 gate.sh preflights \`page-diff --version\` against
|
|
2567
|
+
* this value before spending a run, and next.mjs counts a foreign-schema
|
|
2568
|
+
* report as MISSING rather than skipping it. (This said "neither does that
|
|
2569
|
+
* yet" \u2014 true at 922dde3 where it was written, false from 4e2cd7b, which
|
|
2570
|
+
* gave gate.sh the preflight and did not come back here.) Checked 2026-09-09
|
|
2571
|
+
* against the installed skill: \`page-diff --version\` \u2192 \`page-diff 0.1.0
|
|
2572
|
+
* report-schema 1\`. */
|
|
2573
|
+
export const REPORT_SCHEMA = 1;
|
|
2574
|
+
|
|
2575
|
+
/**
|
|
2576
|
+
* Fail-closed reference preflight. A 200 is NOT evidence: a host that has been
|
|
2577
|
+
* repointed at our own build answers 200, and so does a staging host serving a
|
|
2578
|
+
* 404 page. Both have happened on a real site \u2014 see the dated measurement in
|
|
2579
|
+
* LEDGER.md. A pass here requires an artefact only the reference produces.
|
|
2580
|
+
*/
|
|
2581
|
+
export async function checkRef() {
|
|
2582
|
+
if (!REF_MARK) {
|
|
2583
|
+
return {
|
|
2584
|
+
ok: false,
|
|
2585
|
+
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.",
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
const host = new URL(REF).host;
|
|
2589
|
+
if (SELF_HOSTS.includes(host)) return { ok: false, why: \`REF host \${host} is in selfHosts\` };
|
|
2590
|
+
if (host === new URL(CAND).host) return { ok: false, why: \`REF host \${host} equals CAND's host\` };
|
|
2591
|
+
let res;
|
|
2592
|
+
try {
|
|
2593
|
+
res = await fetch(\`\${REF}/\`, { redirect: "manual" });
|
|
2594
|
+
} catch (e) {
|
|
2595
|
+
return { ok: false, why: \`GET \${REF}/ failed: \${e.message}\` };
|
|
2596
|
+
}
|
|
2597
|
+
if (res.status !== 200)
|
|
2598
|
+
return { ok: false, why: \`GET \${REF}/ \u2192 HTTP \${res.status}, expected 200\` };
|
|
2599
|
+
const loc = res.headers.get("location");
|
|
2600
|
+
if (loc) return { ok: false, why: \`GET \${REF}/ \u2192 \${res.status} redirect to \${loc}\` };
|
|
2601
|
+
const body = await res.text();
|
|
2602
|
+
if (!body.includes(REF_MARK))
|
|
2603
|
+
return {
|
|
2604
|
+
ok: false,
|
|
2605
|
+
why: \`\${REF}/ served 200 but WITHOUT refMark \${JSON.stringify(REF_MARK)} \u2014 that is not the reference\`,
|
|
2606
|
+
};
|
|
2607
|
+
if (CAND_MARK && body.includes(CAND_MARK))
|
|
2608
|
+
return {
|
|
2609
|
+
ok: false,
|
|
2610
|
+
why: \`\${REF}/ contains candMark \${JSON.stringify(CAND_MARK)} \u2014 REF is serving OUR build\`,
|
|
2611
|
+
};
|
|
2612
|
+
return { ok: true, why: \`\${REF}/ \u2192 200, no redirect, refMark present, candMark absent\` };
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
/**
|
|
2616
|
+
* Would next.mjs COUNT a run with this meta? Returns the reason it would not,
|
|
2617
|
+
* as a string, or null when it would.
|
|
2618
|
+
*
|
|
2619
|
+
* A reason string and not a boolean, because the two callers must tell the
|
|
2620
|
+
* cases apart: next.mjs treats "schema" as a page BLANKED (it has its own
|
|
2621
|
+
* message and its own exit) and merely skips the rest of the diagnostics,
|
|
2622
|
+
* while gate.sh prints whatever this says.
|
|
2623
|
+
*
|
|
2624
|
+
* It lives HERE rather than inside next.mjs because gate.sh now asks the same
|
|
2625
|
+
* question, and a question asked twice drifts: a gate that greens a run
|
|
2626
|
+
* next.mjs then drops is the same false green one step along. census.sh's
|
|
2627
|
+
* GUARD 2c (census.sh:153-168) records exactly that drift between
|
|
2628
|
+
* style-census's printer and census-count.mjs's parser \u2014 a COMPLETE census
|
|
2629
|
+
* reported as 0 mismatches because the two had versioned apart.
|
|
2630
|
+
*/
|
|
2631
|
+
export function uncountable(m) {
|
|
2632
|
+
// Missing schemaVersion means "written before the field existed" = 0. It is
|
|
2633
|
+
// not an error on its own; it is only fatal when it would blank a page,
|
|
2634
|
+
// which is next.mjs's call to make, not this predicate's.
|
|
2635
|
+
if ((m.schemaVersion ?? 0) !== REPORT_SCHEMA) return "schema";
|
|
2636
|
+
// A masked / media-neutralised run is a DIAGNOSTIC, never the state of the
|
|
2637
|
+
// page. An --mask-photos probe of yfv made \`top\` @834 read 43.9% while the
|
|
2638
|
+
// real gate had it passing at 1.3%.
|
|
2639
|
+
if ((m.mask?.length ?? 0) > 0) return \`mask=[\${m.mask.join(", ")}]\`;
|
|
2640
|
+
if (m.neutralizeMedia) return "neutralize-media";
|
|
2641
|
+
if (m.maskPhotos) return "mask-photos";
|
|
2642
|
+
if (m.truncated) return "truncated";
|
|
2643
|
+
if (m.threshold !== THRESHOLD) return \`threshold \${m.threshold} != \${THRESHOLD}\`;
|
|
2644
|
+
return null;
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
/**
|
|
2648
|
+
* Did THIS run of page-diff leave a report the scorer will actually count?
|
|
2649
|
+
*
|
|
2650
|
+
* gate.sh cannot use page-diff's exit status for this. page-diff exits 1 for a
|
|
2651
|
+
* region that legitimately FAILED (page-diff.mjs:227) and node exits 1 for the
|
|
2652
|
+
* bare \`throw e\` one line below it, so the status cannot tell a finding from a
|
|
2653
|
+
* crash-before-looking \u2014 census.sh:17-32 records the same shape for
|
|
2654
|
+
* style-census. Measured on 29 Navy with the reference alive and no dev
|
|
2655
|
+
* server: every page-diff died in \`page.goto\`, the gate printed \`home exit=1\`
|
|
2656
|
+
* and \`ALL DONE\`, exited 0, and wrote no report at all.
|
|
2657
|
+
*
|
|
2658
|
+
* So the evidence is the artefact only a completed run leaves: the report
|
|
2659
|
+
* next.mjs will read, fresh, over the matrix and anchors the table declares.
|
|
2660
|
+
* Cheapest and most specific arm first.
|
|
2661
|
+
*/
|
|
2662
|
+
export function checkRun(page, dir, startedAt) {
|
|
2663
|
+
const path = join(dir, "report.json");
|
|
2664
|
+
let report;
|
|
2665
|
+
try {
|
|
2666
|
+
report = JSON.parse(readFileSync(path, "utf8"));
|
|
2667
|
+
} catch (e) {
|
|
2668
|
+
if (e.code === "ENOENT")
|
|
2669
|
+
return { ok: false, why: \`\${path}: no report.json \u2014 the run wrote nothing\` };
|
|
2670
|
+
return { ok: false, why: \`\${path}: \${e.message}\` };
|
|
2671
|
+
}
|
|
2672
|
+
const meta = report.meta ?? {};
|
|
2673
|
+
|
|
2674
|
+
const uncount = uncountable(meta);
|
|
2675
|
+
if (uncount)
|
|
2676
|
+
return { ok: false, why: \`\${path}: next.mjs would not count this run (\${uncount})\` };
|
|
2677
|
+
|
|
2678
|
+
// FRESHNESS. lib/report.mjs:45 is \`mkdirSync(outDir, {recursive:true})\` and
|
|
2679
|
+
// nothing ever clears the directory, so a crashed re-run under a tag used
|
|
2680
|
+
// before leaves the PREVIOUS round's report exactly where it was \u2014 measured
|
|
2681
|
+
// 2026-09-09, sha unchanged across the crash. Requiring report.json without
|
|
2682
|
+
// this arm reproduces the green one step along. meta.generatedAt is built
|
|
2683
|
+
// after \`finally { await browser.close() }\` (page-diff.mjs:163-176), so it
|
|
2684
|
+
// is an artefact of the run that wrote it and not of the file's mtime.
|
|
2685
|
+
const since = Date.parse(startedAt);
|
|
2686
|
+
// NOT skipped when startedAt is unusable: a fail-open default is the exact
|
|
2687
|
+
// shape this guard exists to stop.
|
|
2688
|
+
if (!Number.isFinite(since))
|
|
2689
|
+
return {
|
|
2690
|
+
ok: false,
|
|
2691
|
+
why: \`startedAt \${JSON.stringify(startedAt)} is not a timestamp \u2014 cannot tell this run's report from a previous round's\`,
|
|
2692
|
+
};
|
|
2693
|
+
const at = Date.parse(meta.generatedAt ?? "");
|
|
2694
|
+
if (!Number.isFinite(at))
|
|
2695
|
+
return {
|
|
2696
|
+
ok: false,
|
|
2697
|
+
why: \`\${path}: no usable meta.generatedAt \u2014 cannot tell this run's report from a previous round's\`,
|
|
2698
|
+
};
|
|
2699
|
+
if (at < since)
|
|
2700
|
+
return {
|
|
2701
|
+
ok: false,
|
|
2702
|
+
why: \`\${path}: STALE \u2014 written \${meta.generatedAt}, this run started \${startedAt}. page-diff never cleared the directory.\`,
|
|
2703
|
+
};
|
|
2704
|
+
|
|
2705
|
+
// COVERAGE \u2014 what the run was ASKED for, against the table.
|
|
2706
|
+
const vws = meta.viewports ?? [];
|
|
2707
|
+
if (vws.join(",") !== MATRIX.join(","))
|
|
2708
|
+
return {
|
|
2709
|
+
ok: false,
|
|
2710
|
+
why: \`\${path}: ran viewports [\${vws.join(",")}], harness.json matrix is [\${MATRIX.join(",")}]\`,
|
|
2711
|
+
};
|
|
2712
|
+
const secs = meta.sections ?? [];
|
|
2713
|
+
const want = byKey[page]?.anchors ?? [];
|
|
2714
|
+
if (secs.join("\\0") !== want.join("\\0"))
|
|
2715
|
+
return {
|
|
2716
|
+
ok: false,
|
|
2717
|
+
why: \`\${path}: ran sections [\${secs.join(" | ")}], harness.json anchors are [\${want.join(" | ")}]\`,
|
|
2718
|
+
};
|
|
2719
|
+
|
|
2720
|
+
// ...and what it actually PRODUCED. Every viewport the run says it covered
|
|
2721
|
+
// has to appear in the regions. Deliberately compared against the run's own
|
|
2722
|
+
// meta.viewports and not against MATRIX: the arm above owns "the run used the
|
|
2723
|
+
// wrong matrix", and folding the two together would make either one
|
|
2724
|
+
// unfalsifiable on its own.
|
|
2725
|
+
if (!Array.isArray(report.regions) || report.regions.length === 0)
|
|
2726
|
+
return { ok: false, why: \`\${path}: no regions \u2014 nothing was compared\` };
|
|
2727
|
+
const seen = new Set(report.regions.map((r) => r.viewport));
|
|
2728
|
+
const missing = vws.filter((v) => !seen.has(v));
|
|
2729
|
+
if (missing.length)
|
|
2730
|
+
return {
|
|
2731
|
+
ok: false,
|
|
2732
|
+
why: \`\${path}: no region at viewport(s) [\${missing.join(",")}] \u2014 the run covered [\${[...seen].join(",")}]\`,
|
|
2733
|
+
};
|
|
2734
|
+
|
|
2735
|
+
// With anchors the region count is an exact identity: page-diff cuts one
|
|
2736
|
+
// region before the first anchor plus one per anchor, at every viewport
|
|
2737
|
+
// (regionsFromAnchors, page-diff.mjs:105-109). Measured over the 298 clean
|
|
2738
|
+
// gate-shaped runs in the corpus this harness was cut from, all of them
|
|
2739
|
+
// anchored: regions.length === (sections + 1) * viewports holds 298/298,
|
|
2740
|
+
// while regions.length === TOTALS[page] holds only 260/298 \u2014 the 38 are
|
|
2741
|
+
// legitimately narrower HAND rounds. So the identity is checked against the
|
|
2742
|
+
// run's OWN meta and the matrix/anchors are checked against the table above.
|
|
2743
|
+
//
|
|
2744
|
+
// WITHOUT anchors there is no such identity, and asserting one is a FALSE
|
|
2745
|
+
// REFUSAL of the shape every new site starts in. page-diff falls back to each
|
|
2746
|
+
// page's own <section> boxes and, with none, an even four-row grid
|
|
2747
|
+
// (splitRegions, page-diff.mjs:103-110), so the count is data-dependent and
|
|
2748
|
+
// the two pages need not even agree. Measured 2026-09-09 against the real
|
|
2749
|
+
// page-diff on a seed harness (anchors: [], matrix of 4): 16 regions labelled
|
|
2750
|
+
// grid-0-0 \u2026 grid-3-0, not the 4 this identity predicted. The 298/298 above
|
|
2751
|
+
// was measured over anchored runs only and never covered this case.
|
|
2752
|
+
if (secs.length) {
|
|
2753
|
+
const expected = (secs.length + 1) * vws.length;
|
|
2754
|
+
if (report.regions.length !== expected)
|
|
2755
|
+
return {
|
|
2756
|
+
ok: false,
|
|
2757
|
+
why: \`\${path}: \${report.regions.length} region(s), expected \${expected} = (\${secs.length} anchors + 1) x \${vws.length} viewport(s)\`,
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
// A green that STATES what it is made of, so a green over nothing reads
|
|
2762
|
+
// differently from a green over the matrix (census.sh:219's habit).
|
|
2763
|
+
return {
|
|
2764
|
+
ok: true,
|
|
2765
|
+
why: \`\${report.regions.length} region(s) over \${vws.length} viewport(s), written \${meta.generatedAt}\`,
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
// CLI. Both sides go through realpathSync. \`import.meta.url\` is ALREADY the
|
|
2770
|
+
// resolved real path (node resolves symlinks unless --preserve-symlinks) while
|
|
2771
|
+
// process.argv[1] is the path as typed, so a plain pathToFileURL compare goes
|
|
2772
|
+
// false the moment any component of the invoked path is a symlink \u2014 and then
|
|
2773
|
+
// the CLI prints nothing and exits 0, which every caller reads as success.
|
|
2774
|
+
// page-diff.mjs:184-189 records exactly that defect and the same fix: "The
|
|
2775
|
+
// pathToFileURL compare that replaced the old template string is still false
|
|
2776
|
+
// whenever ANY component of the invoked path is a symlink \u2014 which is how this
|
|
2777
|
+
// skill is installed now (~/.claude/skills/matching-a-page -> the claude-skills
|
|
2778
|
+
// checkout). isMain() resolves the real path on both sides."
|
|
2779
|
+
const isMain = () => {
|
|
2780
|
+
if (!process.argv[1]) return false;
|
|
2781
|
+
try {
|
|
2782
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
2783
|
+
} catch {
|
|
2784
|
+
return false;
|
|
2785
|
+
}
|
|
2786
|
+
};
|
|
2787
|
+
|
|
2788
|
+
if (isMain()) {
|
|
2789
|
+
const mode = process.argv[2];
|
|
2790
|
+
const q = (v) => \`'\${String(v).replace(/'/g, \`'\\\\''\`)}'\`;
|
|
2791
|
+
if (mode === "--env") {
|
|
2792
|
+
const pairs = [
|
|
2793
|
+
["REF", REF],
|
|
2794
|
+
["CAND", CAND],
|
|
2795
|
+
["MATRIX", MATRIX.join(",")],
|
|
2796
|
+
["VIEWPORTS_SP", MATRIX.join(" ")],
|
|
2797
|
+
["THRESHOLD", THRESHOLD],
|
|
2798
|
+
["MAX_HEIGHT_DELTA", MAX_HEIGHT_DELTA],
|
|
2799
|
+
["PD", PD],
|
|
2800
|
+
["SC", SC],
|
|
2801
|
+
["REPORT_SCHEMA", REPORT_SCHEMA],
|
|
2802
|
+
];
|
|
2803
|
+
for (const [k, v] of pairs) console.log(\`\${k}=\${q(v)}\`);
|
|
2804
|
+
} else if (mode === "--table") {
|
|
2805
|
+
for (const p of PAGES) console.log([p.key, p.ref, p.cand, p.anchors.join(",")].join("\\t"));
|
|
2806
|
+
} else if (mode === "--check-ref") {
|
|
2807
|
+
const r = await checkRef();
|
|
2808
|
+
console.log(\`\${r.ok ? "REF OK" : "REF REFUSED"} \u2014 \${r.why}\`);
|
|
2809
|
+
process.exit(r.ok ? 0 : 2);
|
|
2810
|
+
} else if (mode === "--check-run") {
|
|
2811
|
+
// startedAt is REQUIRED, never defaulted: an optional one is a fail-open
|
|
2812
|
+
// door in the one guard that decides whether a page was measured at all.
|
|
2813
|
+
const [page, dir, startedAt] = process.argv.slice(3);
|
|
2814
|
+
if (!page || !dir || !startedAt) {
|
|
2815
|
+
console.error("usage: harness.mjs --check-run <page> <out-dir> <startedAt-iso>");
|
|
2816
|
+
process.exit(2);
|
|
2817
|
+
}
|
|
2818
|
+
const r = checkRun(page, dir, startedAt);
|
|
2819
|
+
console.log(\`\${r.ok ? "RUN OK" : "NO RUN"} \u2014 \${r.why}\`);
|
|
2820
|
+
process.exit(r.ok ? 0 : 2);
|
|
2821
|
+
} else {
|
|
2822
|
+
console.error("usage: harness.mjs --env | --table | --check-ref | --check-run");
|
|
2823
|
+
process.exit(2);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
`;
|
|
2827
|
+
var CENSUS_SH_PREV_0_95_1 = `#!/usr/bin/env bash
|
|
2828
|
+
# Phase 3 style gate \u2014 the mechanical net for the type spec.
|
|
2829
|
+
#
|
|
2830
|
+
# bash matching/census.sh # all 9 pages, all 3 viewports
|
|
2831
|
+
# bash matching/census.sh home yfv # just these
|
|
2832
|
+
#
|
|
2833
|
+
# style-census diffs the computed type tuple (family, weight, size,
|
|
2834
|
+
# line-height, letter-spacing, transform, colour) of every text snippet present
|
|
2835
|
+
# on BOTH pages. It is the only gate that catches the 11px footer line and the
|
|
2836
|
+
# cyan-vs-teal link that page-diff is structurally blind to \u2014 a region can be
|
|
2837
|
+
# pixel-clean at 0.10 and still be wearing the wrong colour on small text.
|
|
2838
|
+
#
|
|
2839
|
+
# Prints a per-page/per-viewport mismatch count and leaves the full runs in
|
|
2840
|
+
# matching/census-<page>-<vw>.log. Exits 1 while any count is non-zero, and 2
|
|
2841
|
+
# when it cannot honestly report a count at all.
|
|
2842
|
+
#
|
|
2843
|
+
# EVIDENCE \u2014 read before adding a guard here, or removing one. A green here
|
|
2844
|
+
# has to be produced by a census that RAN. It was not: with style-census.mjs
|
|
2845
|
+
# absent this gate printed "Phase 3 CLEAN \u2014 0 undeclared type mismatches" and
|
|
2846
|
+
# exited 0. Every run died on module resolution, census-count.mjs read each
|
|
2847
|
+
# crash log as "0 0 0" (it counts \` y=\` rows and a stack trace has none \u2014
|
|
2848
|
+
# census-count.mjs:35-56), and no rows was reported as no mismatches. Measured
|
|
2849
|
+
# again with style-census.mjs PRESENT and the candidate host down: identical
|
|
2850
|
+
# output, because style-census exits 1 both when it finds mismatches
|
|
2851
|
+
# (style-census.mjs:187) and when it dies before it looks \u2014 so the exit status
|
|
2852
|
+
# cannot tell a finding from a crash and may not grant a green either.
|
|
2853
|
+
#
|
|
2854
|
+
# The guards below are what next.mjs:83-89 (no parseable gate run \u2014 refusing to
|
|
2855
|
+
# report a score), strikes.mjs:87-97 (refusing to report "clear") and
|
|
2856
|
+
# gate.sh:37-42 (preflight page-diff before spending a run) already do. Each
|
|
2857
|
+
# demands an artefact only a working census produces; none of them can turn a
|
|
2858
|
+
# red green.
|
|
2859
|
+
set -uo pipefail
|
|
2860
|
+
# Resolved BEFORE the cd. $0 is the path as TYPED, so a relatively invoked
|
|
2861
|
+
# census.sh that resolved the harness afterwards would look for it under
|
|
2862
|
+
# whatever root the cd landed on.
|
|
2863
|
+
HARNESS="$(cd "$(dirname "$0")" && pwd)/harness.mjs"
|
|
2864
|
+
cd "$(dirname "$0")/.."
|
|
2865
|
+
|
|
2866
|
+
# Everything configurable lives in matching/harness.json; harness.mjs is the one
|
|
2867
|
+
# reader. --env supplies REF, CAND, SC and VIEWPORTS_SP \u2014 this gate held its own
|
|
2868
|
+
# copy of all four, and its REF went stale when production cut over to our own
|
|
2869
|
+
# build (LEDGER "REFERENCE MOVED", verified 2026-08-10) while gate.sh's was
|
|
2870
|
+
# repointed the same day.
|
|
2871
|
+
eval "$(node "$HARNESS" --env)"
|
|
2872
|
+
NODE="\${NODE:-node}"
|
|
2873
|
+
|
|
2874
|
+
# The matrix is DATA, read as VIEWPORTS_SP, the way gate.sh reads MATRIX. The
|
|
2875
|
+
# old VIEWPORTS override is retired rather than silently ignored: a census run
|
|
2876
|
+
# over a matrix harness.json does not name is a column set the pixel gate never
|
|
2877
|
+
# measured, and it would look exactly like a clean one.
|
|
2878
|
+
if [ -n "\${VIEWPORTS:-}" ]; then
|
|
2879
|
+
echo "census.sh: the VIEWPORTS override is retired \u2014 the matrix is data now." >&2
|
|
2880
|
+
echo " Edit \\"matrix\\" in matching/harness.json, then unset it." >&2
|
|
2881
|
+
exit 2
|
|
2882
|
+
fi
|
|
2883
|
+
|
|
2884
|
+
# GUARD 1 \u2014 the tool is there and it starts. gate.sh compares \`page-diff
|
|
2885
|
+
# --version\` with the harness's REPORT_SCHEMA before spending a run
|
|
2886
|
+
# (gate.sh:37-42); style-census has no --version to compare (page-diff.mjs
|
|
2887
|
+
# defines one at 191-195; nothing in style-census.mjs writes one), so the
|
|
2888
|
+
# artefact demanded here is the other one only it produces: the usage banner it
|
|
2889
|
+
# prints on exit 2 when --ref/--cand are missing (style-census.mjs:151-157).
|
|
2890
|
+
# That banner is not free \u2014 \`import { chromium } from "playwright"\` runs first
|
|
2891
|
+
# (style-census.mjs:19) \u2014 so a skill directory with no playwright installed
|
|
2892
|
+
# fails HERE, once, instead of writing one crash log per page per viewport that
|
|
2893
|
+
# every reader downstream counts as zero.
|
|
2894
|
+
if [ ! -f "$SC" ]; then
|
|
2895
|
+
echo "census.sh: no style-census at $SC \u2014 refusing to report a count." >&2
|
|
2896
|
+
echo " Install the matching-a-page skill, or point MATCHING_SKILL_DIR" >&2
|
|
2897
|
+
echo " at a checkout that has style-census.mjs." >&2
|
|
2898
|
+
exit 2
|
|
2899
|
+
fi
|
|
2900
|
+
SC_USAGE="$("$NODE" "$SC" 2>&1)"
|
|
2901
|
+
case "$SC_USAGE" in
|
|
2902
|
+
*"style-census"*"--ref"*"--cand"*"--vw"*) ;;
|
|
2903
|
+
*)
|
|
2904
|
+
echo "census.sh: $SC did not answer with a style-census --ref/--cand/--vw usage" >&2
|
|
2905
|
+
echo " banner \u2014 refusing to report a count. It printed:" >&2
|
|
2906
|
+
echo " \${SC_USAGE:-(nothing at all)}" >&2
|
|
2907
|
+
exit 2
|
|
2908
|
+
;;
|
|
2909
|
+
esac
|
|
2910
|
+
|
|
2911
|
+
declare -a WANT=("$@")
|
|
2912
|
+
|
|
2913
|
+
# page -> "refpath candpath": the same table gate.sh drives, from the same file.
|
|
2914
|
+
# cut drops the anchors column, which the style census has no argument for
|
|
2915
|
+
# (style-census.mjs: --ref <url> --cand <url> [--vw 1440]).
|
|
2916
|
+
pages() { node "$HARNESS" --table | cut -f1-3; }
|
|
2917
|
+
|
|
2918
|
+
TOTAL=0
|
|
2919
|
+
AMB=0
|
|
2920
|
+
DECL=0
|
|
2921
|
+
ROWS=0
|
|
2922
|
+
RUNS=0
|
|
2923
|
+
BROKEN=0
|
|
2924
|
+
SEEN=""
|
|
2925
|
+
# One column per viewport, from the matrix. Flush with the data rows below, at
|
|
2926
|
+
# last: the old header hardcoded four fixed fields separated by a literal space,
|
|
2927
|
+
# which the rows do not have, so its labels sat 1/2/3 columns right of them.
|
|
2928
|
+
printf '%-10s' page
|
|
2929
|
+
for vw in $VIEWPORTS_SP; do printf '%8s' "$vw"; done
|
|
2930
|
+
printf '\\n'
|
|
2931
|
+
while read -r page refpath candpath; do
|
|
2932
|
+
[ -z "$page" ] && continue
|
|
2933
|
+
SEEN="$SEEN $page"
|
|
2934
|
+
if [ \${#WANT[@]} -gt 0 ]; then
|
|
2935
|
+
hit=0
|
|
2936
|
+
for w in "\${WANT[@]}"; do [ "$w" = "$page" ] && hit=1; done
|
|
2937
|
+
[ $hit -eq 1 ] || continue
|
|
2938
|
+
fi
|
|
2939
|
+
ROWS=$((ROWS + 1))
|
|
2940
|
+
line=$(printf '%-10s' "$page")
|
|
2941
|
+
for vw in $VIEWPORTS_SP; do
|
|
2942
|
+
log="matching/census-$page-$vw.log"
|
|
2943
|
+
RUNS=$((RUNS + 1))
|
|
2944
|
+
"$NODE" "$SC" --ref "$REF$refpath" --cand "$CAND$candpath" --vw "$vw" >"$log" 2>&1
|
|
2945
|
+
# GUARD 2 \u2014 this run produced a census. The header and the counts line are
|
|
2946
|
+
# written together at style-census.mjs:166-169 and only after both pages
|
|
2947
|
+
# have been walked, so they are the artefact a completed run leaves and a
|
|
2948
|
+
# crashed one cannot. The viewport is read back out of the header because a
|
|
2949
|
+
# --vw that did not take would otherwise fill three differently named logs
|
|
2950
|
+
# with the same 1440 census. \`ref runs\`/\`cand runs\` must be non-zero: two
|
|
2951
|
+
# pages that rendered no text at all agree perfectly and mean nothing.
|
|
2952
|
+
if ! grep -qE "^=== style census, viewport $vw ===\\$" "$log" ||
|
|
2953
|
+
! grep -qE "^ref runs: [1-9][0-9]* +cand runs: [1-9][0-9]* +mismatches: [0-9]+ +ambiguous: [0-9]+\\$" "$log"; then
|
|
2954
|
+
BROKEN=$((BROKEN + 1))
|
|
2955
|
+
echo "census.sh: $page @$vw produced no usable census \u2014 see $log" >&2
|
|
2956
|
+
line="$line$(printf '%8s' '!!')"
|
|
2957
|
+
continue
|
|
2958
|
+
fi
|
|
2959
|
+
# census-count splits the log three ways: REAL mismatches, AMBIGUOUS
|
|
2960
|
+
# same-text collisions (style-census's own split), and DECLARED rows the
|
|
2961
|
+
# operator has already ruled on (matching/census-deviations.mjs, the same
|
|
2962
|
+
# contract as floors.mjs). Only the first is outstanding work \u2014 without the
|
|
2963
|
+
# third, this gate can never reach zero and its number means nothing.
|
|
2964
|
+
read -r n a d <<<"$("$NODE" matching/census-count.mjs "$log")"
|
|
2965
|
+
# GUARD 2b \u2014 three integers, or the counter did not count. census-count.mjs
|
|
2966
|
+
# is the reader that turned a stack trace into "0 0 0"; a run of it that
|
|
2967
|
+
# dies (a broken census-deviations.mjs, a bad path) prints nothing at all,
|
|
2968
|
+
# and bash reads an empty $n as zero in the arithmetic below.
|
|
2969
|
+
counted=1
|
|
2970
|
+
for v in "\${n:-}" "\${a:-}" "\${d:-}"; do
|
|
2971
|
+
case "$v" in "" | *[!0-9]*) counted=0 ;; esac
|
|
2972
|
+
done
|
|
2973
|
+
if [ "$counted" -eq 0 ]; then
|
|
2974
|
+
BROKEN=$((BROKEN + 1))
|
|
2975
|
+
echo "census.sh: census-count.mjs did not return three counts for $log" >&2
|
|
2976
|
+
line="$line$(printf '%8s' '!!')"
|
|
2977
|
+
continue
|
|
2978
|
+
fi
|
|
2979
|
+
# GUARD 2c \u2014 the number reported is the number the census MEASURED. GUARD 2
|
|
2980
|
+
# matched the counts line for PRESENCE and threw its numbers away, so the
|
|
2981
|
+
# count printed below came from census-count's row parse and nothing ever
|
|
2982
|
+
# compared the two. Measured 2026-09-09: a COMPLETE census whose counts line
|
|
2983
|
+
# said "mismatches: 3", whose \`y=\` rows carried one leading space instead of
|
|
2984
|
+
# two (census-count.mjs:39 matches /^ {2}y=/), was reported as 0 and this
|
|
2985
|
+
# gate printed "Phase 3 CLEAN \u2014 0 undeclared type mismatches", exit 0. The
|
|
2986
|
+
# printer lives in the SKILL (style-census.mjs:171-173) and the parser is
|
|
2987
|
+
# copied into every site, so the two version independently and drift needs
|
|
2988
|
+
# nobody to touch either repo.
|
|
2989
|
+
#
|
|
2990
|
+
# The identity is exact for mismatches: style-census prints at most 100 rows
|
|
2991
|
+
# and states the remainder on its own line (style-census.mjs:175-177), so
|
|
2992
|
+
# parsed + remainder == reported. Ambiguous rows are capped the same way
|
|
2993
|
+
# (:184) but the remainder is NOT stated, so only a floor is checkable
|
|
2994
|
+
# there \u2014 any is not none, and the parse may never exceed the report.
|
|
2995
|
+
counts=$(grep -E "^ref runs: [1-9][0-9]* +cand runs: [1-9][0-9]* +mismatches: [0-9]+ +ambiguous: [0-9]+$" "$log" | tail -1)
|
|
2996
|
+
said_m=\${counts##*mismatches: }
|
|
2997
|
+
said_m=\${said_m%% *}
|
|
2998
|
+
said_a=\${counts##*ambiguous: }
|
|
2999
|
+
more=$(sed -n 's/^ *\u2026 and \\([0-9][0-9]*\\) more (truncated print, all counted)$/\\1/p' "$log" | tail -1)
|
|
3000
|
+
if [ $((n + d + \${more:-0})) -ne "$said_m" ] ||
|
|
3001
|
+
[ "$a" -gt "$said_a" ] ||
|
|
3002
|
+
{ [ "$said_a" -gt 0 ] && [ "$a" -eq 0 ]; }; then
|
|
3003
|
+
BROKEN=$((BROKEN + 1))
|
|
3004
|
+
echo "census.sh: $log reports mismatches: $said_m ambiguous: $said_a, but" >&2
|
|
3005
|
+
echo " census-count.mjs read $((n + d)) mismatch row(s)\${more:+ (+ $more truncated)} and $a ambiguous." >&2
|
|
3006
|
+
echo " The census and its reader disagree; there is no count to report." >&2
|
|
3007
|
+
line="$line$(printf '%8s' '!!')"
|
|
3008
|
+
continue
|
|
3009
|
+
fi
|
|
3010
|
+
AMB=$((AMB + a))
|
|
3011
|
+
DECL=$((DECL + d))
|
|
3012
|
+
TOTAL=$((TOTAL + n))
|
|
3013
|
+
line="$line$(printf '%8s' "$n")"
|
|
3014
|
+
done
|
|
3015
|
+
echo "$line"
|
|
3016
|
+
done < <(pages)
|
|
3017
|
+
|
|
3018
|
+
echo
|
|
3019
|
+
# GUARD 3 \u2014 something was actually censused. Process substitution above, NOT a
|
|
3020
|
+
# pipe: a pipe runs the loop in a subshell and every counter here would still
|
|
3021
|
+
# read 0, which is exactly the green this file exists to stop (gate.sh:123-125
|
|
3022
|
+
# records the same trap for FAILED_PREFLIGHT). A page name that matches nothing
|
|
3023
|
+
# must not report CLEAN either \u2014 strikes.mjs:147-156 is the same refusal, and
|
|
3024
|
+
# it was written because a typo silently greened rule 3 on six of nine pages.
|
|
3025
|
+
if [ "$ROWS" -eq 0 ]; then
|
|
3026
|
+
if [ -n "$SEEN" ]; then
|
|
3027
|
+
echo "census.sh: \\"\${WANT[*]:-}\\" matches no page \u2014 refusing to report CLEAN." >&2
|
|
3028
|
+
echo " known pages:$SEEN" >&2
|
|
3029
|
+
else
|
|
3030
|
+
echo "census.sh: the page table is empty \u2014 refusing to report CLEAN." >&2
|
|
3031
|
+
echo " node matching/harness.mjs --table printed no rows; check" >&2
|
|
3032
|
+
echo " \\"pages\\" in matching/harness.json." >&2
|
|
3033
|
+
fi
|
|
3034
|
+
exit 2
|
|
3035
|
+
fi
|
|
3036
|
+
if [ "$BROKEN" -gt 0 ]; then
|
|
3037
|
+
echo "CENSUS INCOMPLETE \u2014 $BROKEN of $RUNS run(s) produced no usable census (marked !!)."
|
|
3038
|
+
echo "Those page/viewport pairs have NOT been measured; there is no count to"
|
|
3039
|
+
echo "report for them, and the totals below would be a floor, not a score."
|
|
3040
|
+
echo "Full runs: matching/census-<page>-<vw>.log"
|
|
3041
|
+
exit 2
|
|
3042
|
+
fi
|
|
3043
|
+
if [ "$TOTAL" -eq 0 ]; then
|
|
3044
|
+
echo "Phase 3 CLEAN \u2014 0 undeclared type mismatches ($DECL declared, $AMB ambiguous)."
|
|
3045
|
+
echo "($RUNS censused run(s) over $ROWS page(s), each one counted.)"
|
|
3046
|
+
[ "$AMB" -gt 0 ] && echo "($AMB ambiguous same-text rows remain: each needs ADJUDICATING," &&
|
|
3047
|
+
echo " not fixing \u2014 our element matches, another sharing its text does not.)"
|
|
3048
|
+
exit 0
|
|
3049
|
+
fi
|
|
3050
|
+
echo "$TOTAL type mismatch(es) remain (+ $AMB ambiguous, $DECL declared)."
|
|
3051
|
+
echo "Full runs: matching/census-<page>-<vw>.log"
|
|
3052
|
+
echo "A mismatch is a defect or a ledgered deviation. An ambiguous row is neither"
|
|
3053
|
+
echo "until you look: the census keys on TEXT, so two different elements sharing a"
|
|
3054
|
+
echo "string land under one key and only one of them may be wrong."
|
|
3055
|
+
exit 1
|
|
3056
|
+
`;
|
|
3057
|
+
var NEXT_MJS_PREV_0_95_1 = `// What is still broken, ranked. Exits 1 while work remains.
|
|
3058
|
+
//
|
|
3059
|
+
// node matching/next.mjs
|
|
3060
|
+
//
|
|
3061
|
+
// Round protocol step 0 (repo CLAUDE.md rule 5). A commit is a CHECKPOINT, not
|
|
3062
|
+
// a stopping point: after committing, run this. If it exits 1 there is a named
|
|
3063
|
+
// next action and the round continues without handing control back.
|
|
3064
|
+
//
|
|
3065
|
+
// Reads the most recent gate log per page rather than the whole corpus, so it
|
|
3066
|
+
// reflects HEAD rather than history (that is strikes.mjs's job).
|
|
3067
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3068
|
+
import { join } from "node:path";
|
|
3069
|
+
|
|
3070
|
+
const DIR = new URL(".", import.meta.url).pathname;
|
|
3071
|
+
|
|
3072
|
+
// PAUSE SWITCH. While matching/PAUSED exists this hands out no agenda and
|
|
3073
|
+
// exits 0. It is deliberately the FIRST thing that runs: no report is read, no
|
|
3074
|
+
// score is printed, nothing tempting is put on screen to argue with.
|
|
3075
|
+
//
|
|
3076
|
+
// Why an exit code and not a note somewhere: rule 5 is a LOOP \u2014 "after
|
|
3077
|
+
// committing, run next.mjs; while it exits 1 there is a named next action and
|
|
3078
|
+
// the round continues". A pause written as prose loses to that loop, because
|
|
3079
|
+
// the loop is mechanical and the prose is not. Exiting 0 satisfies rule 5
|
|
3080
|
+
// truthfully rather than suspending it: there is no next action.
|
|
3081
|
+
const PAUSE = join(DIR, "PAUSED");
|
|
3082
|
+
if (existsSync(PAUSE)) {
|
|
3083
|
+
console.log("MATCHING PAUSED \u2014 no agenda, and none is to be inferred.\\n");
|
|
3084
|
+
console.log(readFileSync(PAUSE, "utf8").trimEnd());
|
|
3085
|
+
process.exit(0);
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
import { FLOORS, ACCEPTED } from "./floors.mjs";
|
|
3089
|
+
import {
|
|
3090
|
+
TOTALS,
|
|
3091
|
+
THRESHOLD,
|
|
3092
|
+
MAX_HEIGHT_DELTA,
|
|
3093
|
+
REPORT_SCHEMA,
|
|
3094
|
+
uncountable,
|
|
3095
|
+
scorable,
|
|
3096
|
+
unscorableWhy,
|
|
3097
|
+
byKey,
|
|
3098
|
+
} from "./harness.mjs";
|
|
3099
|
+
|
|
3100
|
+
// Reports written by a different page-diff, by page key. Kept rather than
|
|
3101
|
+
// dropped: silently ignoring them is how a page vanishes from the score.
|
|
3102
|
+
const schemaMismatch = new Set();
|
|
3103
|
+
|
|
3104
|
+
const latest = new Map();
|
|
3105
|
+
for (const d of readdirSync(DIR).filter((d) => d.startsWith("out-"))) {
|
|
3106
|
+
const m = /^out-[^-]+-(.+)$/.exec(d);
|
|
3107
|
+
// Membership in the page table is asked of the TABLE. \`!TOTALS[m[1]]\` was a
|
|
3108
|
+
// NUMBER standing in for a fact: it is null for an unanchored page and 0 for
|
|
3109
|
+
// an empty matrix, and either dropped the page out of \`latest\` while
|
|
3110
|
+
// \`Object.keys(TOTALS)\` still listed it \u2014 so a gate run that had just
|
|
3111
|
+
// SUCCEEDED came back as "no countable gate run".
|
|
3112
|
+
if (!m || !byKey[m[1]]) continue;
|
|
3113
|
+
let report, mtime;
|
|
3114
|
+
try {
|
|
3115
|
+
const p = join(DIR, d, "report.json");
|
|
3116
|
+
report = JSON.parse(readFileSync(p, "utf8"));
|
|
3117
|
+
mtime = statSync(p).mtimeMs;
|
|
3118
|
+
} catch {
|
|
3119
|
+
continue;
|
|
3120
|
+
}
|
|
3121
|
+
// A masked / media-neutralised run is a DIAGNOSTIC, never the state of the
|
|
3122
|
+
// page. Picking one up as "latest" silently reports scores nobody can ship \u2014
|
|
3123
|
+
// it happened immediately: an --mask-photos probe of yfv made \`top\` @834 read
|
|
3124
|
+
// 43.9% here while the real gate had it passing at 1.3%. Missing
|
|
3125
|
+
// schemaVersion means "written before the field existed" = 0; it is not an
|
|
3126
|
+
// error on its own, only when it would blank a page.
|
|
3127
|
+
//
|
|
3128
|
+
// The predicate itself lives in harness.mjs now, because gate.sh asks the
|
|
3129
|
+
// same question per run and two copies of one question drift \u2014 a gate that
|
|
3130
|
+
// greens a run this file then drops is the same false green one step along.
|
|
3131
|
+
const why = uncountable(report.meta ?? {});
|
|
3132
|
+
if (why === "schema") {
|
|
3133
|
+
schemaMismatch.add(m[1]);
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
if (why) continue;
|
|
3137
|
+
const prev = latest.get(m[1]);
|
|
3138
|
+
if (!prev || mtime > prev.mtime) latest.set(m[1], { dir: d, mtime, report });
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
const blanked = [...schemaMismatch].filter((p) => !latest.has(p));
|
|
3142
|
+
if (blanked.length) {
|
|
3143
|
+
console.error(
|
|
3144
|
+
\`next: \${blanked.length} page(s) have no run at report schema \${REPORT_SCHEMA} \u2014 \` +
|
|
3145
|
+
\`their newest reports came from a different page-diff (\${blanked.sort().join(", ")}).\\n\` +
|
|
3146
|
+
\` Re-run: bash matching/gate.sh <tag> \${blanked.sort().join(" ")}\`,
|
|
3147
|
+
);
|
|
3148
|
+
process.exit(2);
|
|
3149
|
+
}
|
|
3150
|
+
if (latest.size === 0) {
|
|
3151
|
+
console.error(
|
|
3152
|
+
"next: no parseable gate run under matching/ \u2014 refusing to report a score.\\n" +
|
|
3153
|
+
" Run bash matching/gate.sh <tag> first.",
|
|
3154
|
+
);
|
|
3155
|
+
process.exit(2);
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
const rows = [];
|
|
3159
|
+
const accepted = [];
|
|
3160
|
+
let openTotal = 0;
|
|
3161
|
+
let floorTotal = 0;
|
|
3162
|
+
for (const [page, { dir, report }] of latest) {
|
|
3163
|
+
const fails = report.regions.filter((r) => !r.pass);
|
|
3164
|
+
for (const f of fails) {
|
|
3165
|
+
const floor = FLOORS.find((fl) => fl.match(f, page));
|
|
3166
|
+
if (floor) {
|
|
3167
|
+
floorTotal++;
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
const ack = ACCEPTED.find((a) => a.match(f, page));
|
|
3171
|
+
if (ack) {
|
|
3172
|
+
accepted.push({ page, vw: f.viewport, label: f.label, why: ack.why });
|
|
3173
|
+
continue;
|
|
3174
|
+
}
|
|
3175
|
+
openTotal++;
|
|
3176
|
+
rows.push({
|
|
3177
|
+
page,
|
|
3178
|
+
vw: f.viewport,
|
|
3179
|
+
label: f.label,
|
|
3180
|
+
mm: f.mismatchFraction,
|
|
3181
|
+
dh: f.heightDeltaFraction ?? 0,
|
|
3182
|
+
dir,
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
// Pages that REPORTED but cannot be scored: no anchors, so their regions are
|
|
3188
|
+
// whatever page-diff's fallback cut and there is no model for one to be right
|
|
3189
|
+
// against. Kept separate from \`scored\` and never ranked with it.
|
|
3190
|
+
const unscorable = [...latest.entries()]
|
|
3191
|
+
.filter(([p]) => !scorable(p))
|
|
3192
|
+
.map(([p, v]) => ({ p, regions: v.report.regions.length }))
|
|
3193
|
+
.sort((a, b) => a.p.localeCompare(b.p));
|
|
3194
|
+
|
|
3195
|
+
// \`.filter(scorable)\` before \`.map\`, because the sort below divides by \`total\`
|
|
3196
|
+
// and an unanchored page's ratio is NOT BOUNDED BY 1. A passing one scored
|
|
3197
|
+
// 16/4 = 4.0 and sorted LAST, i.e. best, so \`worst\` could never name the one
|
|
3198
|
+
// page whose Phase 1 was not done; a failing one scored 0/4 = 0.0, sorted
|
|
3199
|
+
// FIRST, and put \`grid-0-0\`, \`grid-1-0\` \u2026 on the agenda \u2014 an instruction to fix
|
|
3200
|
+
// geometry against regions page-diff invented.
|
|
3201
|
+
const scored = [...latest.entries()]
|
|
3202
|
+
.filter(([p]) => scorable(p))
|
|
3203
|
+
.map(([p, v]) => ({
|
|
3204
|
+
p,
|
|
3205
|
+
pass: v.report.regions.filter((r) => r.pass).length,
|
|
3206
|
+
total: TOTALS[p],
|
|
3207
|
+
}))
|
|
3208
|
+
.sort((a, b) => a.pass / a.total - b.pass / b.total);
|
|
3209
|
+
|
|
3210
|
+
// The denominator is the DECLARED site, not the pages that happened to report.
|
|
3211
|
+
// Summed over \`scored\` it shrank to match the numerator: 8 of 9 pages reporting
|
|
3212
|
+
// read SCORE 160/160 while the ninth, whose page-diff had crashed, was in
|
|
3213
|
+
// neither the numerator nor the denominator nor the list below. harness.mjs:61-67
|
|
3214
|
+
// already gives the reason \u2014 "a wrong denominator makes the score a lie in the
|
|
3215
|
+
// flattering direction" \u2014 and that fix was applied per REGION (\`total:
|
|
3216
|
+
// TOTALS[p]\`) and never per PAGE.
|
|
3217
|
+
const unmeasured = Object.keys(TOTALS)
|
|
3218
|
+
.filter((p) => !latest.has(p))
|
|
3219
|
+
.sort();
|
|
3220
|
+
const sum = scored.reduce((a, s) => a + s.pass, 0);
|
|
3221
|
+
// Summed EXPLICITLY over the scorable pages rather than over TOTALS' values:
|
|
3222
|
+
// \`a + null\` is silently \`a\`, and a denominator that is right only because of a
|
|
3223
|
+
// coercion is the same lie one refactor away.
|
|
3224
|
+
const pagesAll = Object.keys(TOTALS).length;
|
|
3225
|
+
const pagesScorable = Object.keys(TOTALS).filter((p) => scorable(p)).length;
|
|
3226
|
+
const max = Object.keys(TOTALS).reduce((a, p) => a + (scorable(p) ? TOTALS[p] : 0), 0);
|
|
3227
|
+
|
|
3228
|
+
// A score is printed only if SOMETHING can carry one. \`SCORE 0/0\` is a third
|
|
3229
|
+
// lie and the one that reads best of all, so it is never printed: with nothing
|
|
3230
|
+
// scorable the line says so in words and gives no fraction to quote.
|
|
3231
|
+
console.log(
|
|
3232
|
+
(pagesScorable === 0
|
|
3233
|
+
? \`NO SCORE \u2014 0 of \${pagesAll} page(s) can carry one.\`
|
|
3234
|
+
: \`SCORE \${sum}/\${max} regions passing over \${pagesScorable} of \${pagesAll} page(s)\` +
|
|
3235
|
+
(unmeasured.length ? \` \u2014 \${unmeasured.length} page(s) NOT MEASURED\` : "")) + "\\n",
|
|
3236
|
+
);
|
|
3237
|
+
console.log(
|
|
3238
|
+
[
|
|
3239
|
+
...scored.map((s) => \` \${s.p.padEnd(9)} \${String(s.pass).padStart(2)}/\${s.total}\`),
|
|
3240
|
+
// The run's OWN region count, as EVIDENCE FOR THE REFUSAL \u2014 it proves a run
|
|
3241
|
+
// happened and explains why it cannot be scored, so a refusal is not
|
|
3242
|
+
// mistaken for a crash. The pass FRACTION is deliberately withheld: it is
|
|
3243
|
+
// the number with no referent, and the number that gets quoted.
|
|
3244
|
+
...unscorable.map(
|
|
3245
|
+
(u) =>
|
|
3246
|
+
\` \${u.p.padEnd(9)} \${String(u.regions).padStart(2)} region(s) NOT SCORABLE \u2014 \${unscorableWhy(u.p)}\`,
|
|
3247
|
+
),
|
|
3248
|
+
// \`?/N\`, never \`0/N\`: an unmeasured page is not a page that scored zero,
|
|
3249
|
+
// and printing zero would be a different lie. \`?/?\` when the page is ALSO
|
|
3250
|
+
// unanchored \u2014 \`?/null\` would name the denominator "null", which is worse
|
|
3251
|
+
// than the number it replaced.
|
|
3252
|
+
...unmeasured.map((p) => \` \${p.padEnd(9)} ?/\${TOTALS[p] ?? "?"} NOT MEASURED\`),
|
|
3253
|
+
].join("\\n"),
|
|
3254
|
+
);
|
|
3255
|
+
|
|
3256
|
+
if (accepted.length) {
|
|
3257
|
+
console.log(\`\\nOperator-ACCEPTED failures (left failing on purpose):\`);
|
|
3258
|
+
for (const a of accepted)
|
|
3259
|
+
console.log(\` \${a.page} @\${a.vw} "\${a.label}" \u2014 \${a.why.slice(0, 96)}\u2026\`);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
// BEFORE the \`!rows.length\` branch, and deliberately so: an unmeasured page
|
|
3263
|
+
// contributes no failing region, so that branch would print "Backlog is empty"
|
|
3264
|
+
// and exit 0 over a page nobody had looked at. Exit 2 matches the two guards
|
|
3265
|
+
// above (\`blanked\`, \`latest.size === 0\`) \u2014 neither "clean" nor "work remains"
|
|
3266
|
+
// but "this cannot be scored", the one answer rule 5's while-it-exits-1 loop
|
|
3267
|
+
// cannot swallow. The score print stays above it so the partial state is still
|
|
3268
|
+
// visible.
|
|
3269
|
+
// A page with no anchors has not finished Phase 1, so a score has no referent
|
|
3270
|
+
// and this refuses to invent one \u2014 the same answer \`refMark: ""\` gets from
|
|
3271
|
+
// checkRef and an absent \`## <page>\` SPEC section gets from gate.sh. All three
|
|
3272
|
+
// are seed sentinels, and this was the one that failed OPEN. Repo CLAUDE.md
|
|
3273
|
+
// rule 1's corollary is the whole argument: a field that can only observe
|
|
3274
|
+
// configuration must never be named after the thing it cannot observe, and
|
|
3275
|
+
// "12 of 12 grid rows passed" observes a screenshot cut into quarters, not a
|
|
3276
|
+
// design anyone specced.
|
|
3277
|
+
//
|
|
3278
|
+
// PRINTED here, EXITED below: \`unmeasured\` and \`unscorable\` are different pages
|
|
3279
|
+
// with different remedies, and exiting inside the first block would hide the
|
|
3280
|
+
// second from an operator who then fixes only what was printed.
|
|
3281
|
+
if (unscorable.length) {
|
|
3282
|
+
console.error(
|
|
3283
|
+
\`\\nnext: \${unscorable.length} page(s) have no anchors, so their regions are page-diff's own\\n\` +
|
|
3284
|
+
\` fallback cut and cannot be scored \u2014 \` +
|
|
3285
|
+
unscorable.map((u) => \`\${u.p} (\${u.regions} region(s))\`).join(", ") +
|
|
3286
|
+
\`.\\n Set "anchors" for them in matching/harness.json to section texts that exist on\\n\` +
|
|
3287
|
+
\` BOTH the reference and the candidate, then re-run: bash matching/gate.sh <tag> \` +
|
|
3288
|
+
unscorable.map((u) => u.p).join(" "),
|
|
3289
|
+
);
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
if (unmeasured.length) {
|
|
3293
|
+
console.error(
|
|
3294
|
+
\`\\nnext: \${unmeasured.length} page(s) have no countable gate run \u2014 \${unmeasured.join(", ")}.\\n\` +
|
|
3295
|
+
\` Re-run: bash matching/gate.sh <tag> \${unmeasured.join(" ")}\`,
|
|
3296
|
+
);
|
|
3297
|
+
process.exit(2);
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// See above: deliberately a second statement, not an \`else\`.
|
|
3301
|
+
if (unscorable.length) process.exit(2);
|
|
3302
|
+
|
|
3303
|
+
if (!rows.length) {
|
|
3304
|
+
console.log(
|
|
3305
|
+
\`\\nNo open geometry failures. \${floorTotal} declared floor(s) remain.\`,
|
|
3306
|
+
);
|
|
3307
|
+
console.log(
|
|
3308
|
+
"Backlog is empty \u2014 Phases 5 (states) and 6 (adversarial review) are what is left.",
|
|
3309
|
+
);
|
|
3310
|
+
process.exit(0);
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
// Worst page first, then worst region inside it: fix where the model is most wrong.
|
|
3314
|
+
//
|
|
3315
|
+
// INVARIANT, documented rather than guarded because no test could redden a
|
|
3316
|
+
// guard here: \`scored[0]\` is safe because \`scored\` is empty only when every
|
|
3317
|
+
// page in \`latest\` is unanchored \u2014 and that means \`unscorable.length > 0\`, so
|
|
3318
|
+
// the exit above already fired. (\`latest\` being empty is caught further up.)
|
|
3319
|
+
// \`rows\` likewise still contains unanchored pages' failing regions; they are
|
|
3320
|
+
// never printed because that same exit fires first. Both facts depend on the
|
|
3321
|
+
// exit staying ABOVE this line \u2014 a second filter here would be a second place
|
|
3322
|
+
// to keep in sync, which is how checkRun and next.mjs drifted apart to begin
|
|
3323
|
+
// with. If that exit ever moves, this becomes a TypeError.
|
|
3324
|
+
const worst = scored[0].p;
|
|
3325
|
+
rows.sort(
|
|
3326
|
+
(a, b) =>
|
|
3327
|
+
(a.page === worst ? -1 : 0) - (b.page === worst ? -1 : 0) || b.mm - a.mm,
|
|
3328
|
+
);
|
|
3329
|
+
|
|
3330
|
+
console.log(
|
|
3331
|
+
\`\\n\${openTotal} open failure(s) + \${floorTotal} declared floor(s).\`,
|
|
3332
|
+
);
|
|
3333
|
+
console.log(\`\\nNEXT: \${worst} \u2014 worst page. Its open regions:\\n\`);
|
|
3334
|
+
for (const r of rows.filter((r) => r.page === worst)) {
|
|
3335
|
+
const why = [];
|
|
3336
|
+
if (r.mm > THRESHOLD) why.push(\`pixels \${(r.mm * 100).toFixed(1)}%\`);
|
|
3337
|
+
if (Math.abs(r.dh) > MAX_HEIGHT_DELTA)
|
|
3338
|
+
why.push(\`height \${(r.dh * 100).toFixed(1)}%\`);
|
|
3339
|
+
console.log(
|
|
3340
|
+
\` @\${String(r.vw).padEnd(5)} \${r.label.slice(0, 44).padEnd(45)} \${why.join(" + ")}\`,
|
|
3341
|
+
);
|
|
3342
|
+
}
|
|
3343
|
+
console.log(\`\\nBefore treating any of these as geometry:\`);
|
|
3344
|
+
console.log(
|
|
3345
|
+
\` node matching/probe-anchor-parity.mjs \${worst} # is the gate cutting comparably?\`,
|
|
3346
|
+
);
|
|
3347
|
+
console.log(
|
|
3348
|
+
\` node matching/strikes.mjs \${worst} # has it stalled? then change the MODEL\`,
|
|
3349
|
+
);
|
|
3350
|
+
console.log(
|
|
3351
|
+
\`\\nRound continues. Do not hand back control with work outstanding.\`,
|
|
3352
|
+
);
|
|
3353
|
+
process.exit(1);
|
|
3354
|
+
`;
|
|
3355
|
+
var STRIKES_MJS_PREV_0_95_1 = `// The 3-strike rule, made countable.
|
|
3356
|
+
//
|
|
3357
|
+
// node matching/strikes.mjs [page]
|
|
3358
|
+
//
|
|
3359
|
+
// The matching skill says: "A region that has not improved after 3 fix ->
|
|
3360
|
+
// re-run cycles: stop. Present the three attempts. Do not widen the threshold,
|
|
3361
|
+
// mask it, or reclassify it \u2014 a stuck region means your model of it is wrong."
|
|
3362
|
+
//
|
|
3363
|
+
// That rule was blown twice on this project (services \`top\`, yfv \`top\`) purely
|
|
3364
|
+
// because nobody was counting. This reads every matching/out-*/report.json,
|
|
3365
|
+
// reconstructs each region's history in chronological order, and prints the
|
|
3366
|
+
// regions that are OUT OF STRIKES. Exit 1 when any exist, so a round that
|
|
3367
|
+
// starts by running this cannot quietly attempt a fourth cycle.
|
|
3368
|
+
//
|
|
3369
|
+
// WHAT THIS ACTUALLY MEASURES (read before trusting a number): report.json
|
|
3370
|
+
// cannot record INTENT, so a "cycle" here is any completed gate run in which
|
|
3371
|
+
// the region was still failing and did not improve by IMPROVE_PP. Runs aimed at
|
|
3372
|
+
// a different region on the same page therefore count too. That makes this a
|
|
3373
|
+
// STALL detector, not a literal attempt counter \u2014 "this has been failing,
|
|
3374
|
+
// unchanged, across N gate runs while you were working on this page." That is
|
|
3375
|
+
// the more useful discipline number anyway, and it is honest about being an
|
|
3376
|
+
// upper bound. Improvement resets the count: real progress earns a fresh start.
|
|
3377
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3378
|
+
import { join } from "node:path";
|
|
3379
|
+
import { FLOORS } from "./floors.mjs";
|
|
3380
|
+
import {
|
|
3381
|
+
PAGES,
|
|
3382
|
+
THRESHOLD,
|
|
3383
|
+
MAX_HEIGHT_DELTA,
|
|
3384
|
+
REPORT_SCHEMA,
|
|
3385
|
+
} from "./harness.mjs";
|
|
3386
|
+
|
|
3387
|
+
// PAUSE SWITCH \u2014 see matching/next.mjs for why this is an exit code. Round
|
|
3388
|
+
// protocol step 1 is "run strikes.mjs; the stalled regions are the agenda", so
|
|
3389
|
+
// this is the second door into the backlog and has to be shut too. Exits 0:
|
|
3390
|
+
// there are no strikes to spend when no round is being played.
|
|
3391
|
+
const PAUSED = join(new URL(".", import.meta.url).pathname, "PAUSED");
|
|
3392
|
+
if (existsSync(PAUSED)) {
|
|
3393
|
+
console.log("MATCHING PAUSED \u2014 no agenda, and none is to be inferred.\\n");
|
|
3394
|
+
console.log(readFileSync(PAUSED, "utf8").trimEnd());
|
|
3395
|
+
process.exit(0);
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
const IMPROVE_PP = 0.01; // 1 percentage point = the smallest move worth calling progress
|
|
3399
|
+
const MAX_STRIKES = 3;
|
|
3400
|
+
const ROOT = new URL(".", import.meta.url).pathname;
|
|
3401
|
+
const only = process.argv[2] ?? null;
|
|
3402
|
+
|
|
3403
|
+
// The gate key for a report, from its ref path. Deriving it by string surgery
|
|
3404
|
+
// disagreed with the gate on exactly six of the nine rows \u2014 yfv, contact, atd,
|
|
3405
|
+
// svc, qa and team (measured 2026-09-09 against harness.json); the table knows.
|
|
3406
|
+
// keyOf (below) stays as the fallback for a run whose ref was rewritten.
|
|
3407
|
+
const pageOf = (ref) => {
|
|
3408
|
+
const path = new URL(ref).pathname.replace(/\\/$/, "") || "/";
|
|
3409
|
+
return PAGES.find((p) => p.ref === path)?.key ?? null;
|
|
3410
|
+
};
|
|
3411
|
+
|
|
3412
|
+
const runs = [];
|
|
3413
|
+
const unreadable = [];
|
|
3414
|
+
for (const dir of readdirSync(ROOT).filter((d) => d.startsWith("out-"))) {
|
|
3415
|
+
let report;
|
|
3416
|
+
try {
|
|
3417
|
+
report = JSON.parse(readFileSync(join(ROOT, dir, "report.json"), "utf8"));
|
|
3418
|
+
} catch {
|
|
3419
|
+
continue; // not a completed run dir
|
|
3420
|
+
}
|
|
3421
|
+
if (!report.meta?.ref || !Array.isArray(report.regions)) continue;
|
|
3422
|
+
// A report from another schema is still usable for a STALL count as long as
|
|
3423
|
+
// it carries the two fields this reads. Anything else is counted and named,
|
|
3424
|
+
// never silently dropped \u2014 an under-counted history reads as "clear".
|
|
3425
|
+
const usable = report.regions.every(
|
|
3426
|
+
(r) =>
|
|
3427
|
+
typeof r.mismatchFraction === "number" && typeof r.pass === "boolean",
|
|
3428
|
+
);
|
|
3429
|
+
if (!usable) {
|
|
3430
|
+
unreadable.push(\`\${dir} (schema \${report.meta.schemaVersion ?? 0})\`);
|
|
3431
|
+
continue;
|
|
3432
|
+
}
|
|
3433
|
+
runs.push({
|
|
3434
|
+
dir,
|
|
3435
|
+
at: report.meta.generatedAt,
|
|
3436
|
+
meta: report.meta,
|
|
3437
|
+
regions: report.regions,
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
// The failure mode of a stall detector is always "clear", which is exactly the
|
|
3442
|
+
// answer that stops nobody. A corpus this could not read is not a clean one.
|
|
3443
|
+
if (runs.length === 0) {
|
|
3444
|
+
console.error(
|
|
3445
|
+
\`strikes: no parseable gate run under matching/ \u2014 refusing to report "clear".\` +
|
|
3446
|
+
(unreadable.length
|
|
3447
|
+
? \`\\n \${unreadable.length} report(s) unreadable at schema \${REPORT_SCHEMA}: \${unreadable.slice(0, 5).join(", ")}\`
|
|
3448
|
+
: ""),
|
|
3449
|
+
);
|
|
3450
|
+
process.exit(2);
|
|
3451
|
+
}
|
|
3452
|
+
if (unreadable.length) {
|
|
3453
|
+
console.error(
|
|
3454
|
+
\`strikes: ignored \${unreadable.length} unreadable report(s) \u2014 the history below is incomplete.\` +
|
|
3455
|
+
\`\\n \${unreadable.slice(0, 5).join(", ")}\`,
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
runs.sort((a, b) => a.at.localeCompare(b.at));
|
|
3460
|
+
|
|
3461
|
+
// The gate page KEY, recovered from the run dir the way next.mjs does it
|
|
3462
|
+
// (out-<TAG>-<page>, split on the first hyphen \u2014 the tag is hyphen-free by
|
|
3463
|
+
// gate.sh's own preflight). \`pageOf\` now returns that same key off the table,
|
|
3464
|
+
// so the two vocabularies agree and the round protocol in CLAUDE.md \u2014 "run
|
|
3465
|
+
// \`strikes.mjs <page>\` with the key you just passed to gate.sh", which until
|
|
3466
|
+
// 2026-08-13 silently matched nothing \u2014 works either way. This stays because
|
|
3467
|
+
// a dir key is not always a gate key: 69 of this corpus's 377 run dirs are
|
|
3468
|
+
// hand-named probes (out-band3, out-390masked), and FLOORS matches on it.
|
|
3469
|
+
const keyOf = (dir) => /^out-[^-]+-(.+)$/.exec(dir)?.[1] ?? null;
|
|
3470
|
+
|
|
3471
|
+
// key -> chronological list of {dir, at, mm, pass, masked}
|
|
3472
|
+
const history = new Map();
|
|
3473
|
+
const seenNames = new Set();
|
|
3474
|
+
for (const run of runs) {
|
|
3475
|
+
const page = pageOf(run.meta.ref) ?? keyOf(run.dir) ?? "unknown";
|
|
3476
|
+
const gateKey = keyOf(run.dir);
|
|
3477
|
+
seenNames.add(page);
|
|
3478
|
+
if (gateKey) seenNames.add(gateKey);
|
|
3479
|
+
if (only && page !== only && gateKey !== only) continue;
|
|
3480
|
+
for (const r of run.regions) {
|
|
3481
|
+
// A DECLARED FLOOR is flat by definition \u2014 reporting it as stalled is noise
|
|
3482
|
+
// that hides a real stall. It stays in the LEDGER; it does not belong here.
|
|
3483
|
+
if (FLOORS.some((fl) => fl.match(r, gateKey || page))) continue;
|
|
3484
|
+
const key = \`\${page}|\${r.viewport}|\${r.label}\`;
|
|
3485
|
+
if (!history.has(key)) history.set(key, []);
|
|
3486
|
+
history.get(key).push({
|
|
3487
|
+
dir: run.dir,
|
|
3488
|
+
at: run.at,
|
|
3489
|
+
mm: r.mismatchFraction,
|
|
3490
|
+
dh: r.heightDeltaFraction,
|
|
3491
|
+
pass: r.pass,
|
|
3492
|
+
// a run with masks/neutralised media is not comparable to a clean one
|
|
3493
|
+
dirty:
|
|
3494
|
+
(run.meta.mask?.length ?? 0) > 0 ||
|
|
3495
|
+
run.meta.neutralizeMedia ||
|
|
3496
|
+
run.meta.maskPhotos,
|
|
3497
|
+
});
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
// A name that matches no run must NOT report "clear". This check exists to stop
|
|
3502
|
+
// work on a stalled region, so failing open is the one thing it may never do \u2014
|
|
3503
|
+
// a typo'd or wrong-vocabulary page silently greened rule 3 for six of the nine
|
|
3504
|
+
// pages. Fail loud instead, and say what the vocabulary is.
|
|
3505
|
+
if (only && history.size === 0) {
|
|
3506
|
+
console.error(
|
|
3507
|
+
\`strikes: "\${only}" matches no gate run \u2014 refusing to report "clear".\\n\` +
|
|
3508
|
+
\` known pages: \${[...seenNames].sort().join(", ")}\`,
|
|
3509
|
+
);
|
|
3510
|
+
process.exit(2);
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
const stuck = [];
|
|
3514
|
+
for (const [key, all] of history) {
|
|
3515
|
+
const h = all.filter((e) => !e.dirty); // compare like with like
|
|
3516
|
+
if (h.length === 0) continue;
|
|
3517
|
+
const last = h[h.length - 1];
|
|
3518
|
+
if (last.pass) continue;
|
|
3519
|
+
|
|
3520
|
+
let best = Infinity;
|
|
3521
|
+
let strikes = 0;
|
|
3522
|
+
let sinceIdx = 0;
|
|
3523
|
+
for (let i = 0; i < h.length; i++) {
|
|
3524
|
+
if (h[i].mm < best - IMPROVE_PP) {
|
|
3525
|
+
best = Math.min(best, h[i].mm);
|
|
3526
|
+
strikes = 0;
|
|
3527
|
+
sinceIdx = i;
|
|
3528
|
+
} else {
|
|
3529
|
+
best = Math.min(best, h[i].mm);
|
|
3530
|
+
strikes++;
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
if (strikes >= MAX_STRIKES) stuck.push({ key, strikes, h, sinceIdx, last });
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
// Triage order: the worst-matching region that has never moved is the one whose
|
|
3537
|
+
// model is most wrong. Sorting by strike count instead would put a 0.3% region
|
|
3538
|
+
// that only fails on height above a 76% region that fails on everything.
|
|
3539
|
+
stuck.sort((a, b) => b.last.mm - a.last.mm);
|
|
3540
|
+
|
|
3541
|
+
if (stuck.length === 0) {
|
|
3542
|
+
console.log(
|
|
3543
|
+
\`strikes: clear \u2014 no failing region has stalled for \${MAX_STRIKES}+ gate runs\` +
|
|
3544
|
+
(only ? \` on \${only}\` : "") +
|
|
3545
|
+
".",
|
|
3546
|
+
);
|
|
3547
|
+
process.exit(0);
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
const why = (e) => {
|
|
3551
|
+
const reasons = [];
|
|
3552
|
+
if (e.mm > THRESHOLD) reasons.push(\`pixels \${(e.mm * 100).toFixed(1)}%\`);
|
|
3553
|
+
if (Math.abs(e.dh ?? 0) > MAX_HEIGHT_DELTA)
|
|
3554
|
+
reasons.push(\`height \${((e.dh ?? 0) * 100).toFixed(1)}%\`);
|
|
3555
|
+
return reasons.join(" + ") || "marginal";
|
|
3556
|
+
};
|
|
3557
|
+
|
|
3558
|
+
console.log(
|
|
3559
|
+
\`STALLED \u2014 \${stuck.length} failing region(s) have not moved in \${MAX_STRIKES}+ gate runs.\\n\` +
|
|
3560
|
+
\`These need a new model or the operator, not another attempt. Worst first:\\n\`,
|
|
3561
|
+
);
|
|
3562
|
+
for (const { key, strikes, h, sinceIdx, last } of stuck.slice(0, 20)) {
|
|
3563
|
+
const [page, vw, label] = key.split("|");
|
|
3564
|
+
console.log(
|
|
3565
|
+
\`\${page} @\${vw} "\${label}" \u2014 \${why(last)}, flat across \${strikes} runs\`,
|
|
3566
|
+
);
|
|
3567
|
+
const window = h.slice(Math.max(0, sinceIdx));
|
|
3568
|
+
const shown = window.length > 4 ? [window[0], ...window.slice(-3)] : window;
|
|
3569
|
+
for (const [i, e] of shown.entries()) {
|
|
3570
|
+
const gap =
|
|
3571
|
+
window.length > 4 && i === 1
|
|
3572
|
+
? \` ... \${window.length - 4} more ...\\n\`
|
|
3573
|
+
: "";
|
|
3574
|
+
console.log(
|
|
3575
|
+
gap +
|
|
3576
|
+
\` \${(e.mm * 100).toFixed(1).padStart(5)}% \${e.at.slice(0, 16).replace("T", " ")} \${e.dir}\`,
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
console.log();
|
|
3580
|
+
}
|
|
3581
|
+
if (stuck.length > 20) console.log(\`... and \${stuck.length - 20} more.\\n\`);
|
|
3582
|
+
console.log(
|
|
3583
|
+
"Per the matching skill: present the attempts to the operator.\\n" +
|
|
3584
|
+
"Do NOT widen the threshold, add a mask, or reclassify them as a floor.",
|
|
3585
|
+
);
|
|
3586
|
+
process.exit(1);
|
|
3587
|
+
`;
|
|
3588
|
+
var BUILD_SPEC_MJS_PREV_0_95_1 = `// Assemble matching/SPEC.md from the per-page sections in spec-sections/.
|
|
3589
|
+
//
|
|
3590
|
+
// node matching/build-spec.mjs
|
|
3591
|
+
//
|
|
3592
|
+
// SPEC.md is the Phase 1 deliverable and the thing matching/gate.sh preflights
|
|
3593
|
+
// against, so it is GENERATED rather than hand-maintained: edit the section
|
|
3594
|
+
// file, rebuild. The page list comes from matching/harness.mjs (the same
|
|
3595
|
+
// reader gate.sh's \`--table\` loop uses) and the heading test from its
|
|
3596
|
+
// specHeadingRe, whose pattern is has_spec's grep verbatim \u2014 so a section can
|
|
3597
|
+
// no longer build fine here and then re-block its own page at the gate.
|
|
3598
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
3599
|
+
import { join } from "node:path";
|
|
3600
|
+
import { PAGES, specHeadingRe } from "./harness.mjs";
|
|
3601
|
+
|
|
3602
|
+
const DIR = new URL(".", import.meta.url).pathname;
|
|
3603
|
+
const SECTIONS = join(DIR, "spec-sections");
|
|
3604
|
+
|
|
3605
|
+
// Order = live's nav order, then the detail templates. Both fall out of the
|
|
3606
|
+
// table's own order within each group, so this is not a second list to keep.
|
|
3607
|
+
const KEYS = [
|
|
3608
|
+
...PAGES.filter((p) => p.group === "nav"),
|
|
3609
|
+
...PAGES.filter((p) => p.group !== "nav"),
|
|
3610
|
+
].map((p) => p.spec ?? p.key);
|
|
3611
|
+
|
|
3612
|
+
const parts = [];
|
|
3613
|
+
const missing = [];
|
|
3614
|
+
|
|
3615
|
+
const chrome = join(SECTIONS, "_chrome.md");
|
|
3616
|
+
if (existsSync(chrome)) parts.push(readFileSync(chrome, "utf8").trim());
|
|
3617
|
+
else missing.push("_chrome");
|
|
3618
|
+
|
|
3619
|
+
for (const page of KEYS) {
|
|
3620
|
+
const file = join(SECTIONS, \`\${page}.md\`);
|
|
3621
|
+
if (!existsSync(file)) {
|
|
3622
|
+
missing.push(page);
|
|
3623
|
+
continue;
|
|
3624
|
+
}
|
|
3625
|
+
const body = readFileSync(file, "utf8").trim();
|
|
3626
|
+
// The preflight greps \`^##+ +<page>\`; a section that does not open with its
|
|
3627
|
+
// own key would build fine here and then refuse at the gate, which is the
|
|
3628
|
+
// most confusing possible failure. Catch it now.
|
|
3629
|
+
if (!specHeadingRe(page).test(body)) {
|
|
3630
|
+
console.error(
|
|
3631
|
+
\`WARNING: \${page}.md does not open with a '## \${page}' heading \u2014 gate.sh will still refuse this page.\`,
|
|
3632
|
+
);
|
|
3633
|
+
}
|
|
3634
|
+
parts.push(body);
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
// The header is a spec-section like any other, so a site can say what a
|
|
3638
|
+
// citation looks like there and what its own ladders are. DEFAULT_HEADER is
|
|
3639
|
+
// what a site gets before it writes one: the two paragraphs that are true of
|
|
3640
|
+
// every project, and nothing that is not.
|
|
3641
|
+
const DEFAULT_HEADER = \`# Reference spec
|
|
3642
|
+
|
|
3643
|
+
GENERATED by \\\`node matching/build-spec.mjs\\\` from \\\`matching/spec-sections/\\\`.
|
|
3644
|
+
Edit the section files, not this one.
|
|
3645
|
+
|
|
3646
|
+
This is the Phase 1 deliverable of the \\\`matching-a-page\\\` skill and the
|
|
3647
|
+
precondition \\\`matching/gate.sh\\\` enforces: a page with no section here does not
|
|
3648
|
+
get a geometry round (see the matching rules in CLAUDE.md).
|
|
3649
|
+
|
|
3650
|
+
\`;
|
|
3651
|
+
const headerFile = join(SECTIONS, "_header.md");
|
|
3652
|
+
const header = existsSync(headerFile)
|
|
3653
|
+
? readFileSync(headerFile, "utf8").trimEnd() + "\\n\\n"
|
|
3654
|
+
: DEFAULT_HEADER;
|
|
3655
|
+
|
|
3656
|
+
writeFileSync(join(DIR, "SPEC.md"), header + parts.join("\\n\\n---\\n\\n") + "\\n");
|
|
3657
|
+
|
|
3658
|
+
const built = KEYS.length - missing.filter((m) => m !== "_chrome").length;
|
|
3659
|
+
console.log(
|
|
3660
|
+
\`SPEC.md built \u2014 \${built}/\${KEYS.length} pages\${missing.length ? \`, MISSING: \${missing.join(", ")}\` : ""}\`,
|
|
3661
|
+
);
|
|
3662
|
+
process.exit(missing.length ? 1 : 0);
|
|
3663
|
+
`;
|
|
3664
|
+
var MATCH_ROUTE_SERVER_PREV_0_95_1 = `import { error } from "@sveltejs/kit";
|
|
3665
|
+
import { dev } from "$app/environment";
|
|
3666
|
+
import { documents } from "$lib/site-pages.js";
|
|
3667
|
+
|
|
3668
|
+
// Local matching surface: renders the EXACT assembly \`reddoor-maint
|
|
3669
|
+
// prismic-seed\` publishes, from the same module, so a fix made to pass a gate
|
|
3670
|
+
// is a fix to what ships. Not prerendered, SSR-on-demand, dev-only.
|
|
3671
|
+
export const prerender = false;
|
|
3672
|
+
|
|
3673
|
+
// The seed resolves images to asset ids; here they only need a URL. Dimensions
|
|
3674
|
+
// are nominal \u2014 slices size their own image boxes in CSS.
|
|
3675
|
+
const devImg = (u: string) => ({
|
|
3676
|
+
url: u,
|
|
3677
|
+
alt: null,
|
|
3678
|
+
copyright: null,
|
|
3679
|
+
dimensions: { width: 1600, height: 1067 },
|
|
3680
|
+
edit: { x: 0, y: 0, zoom: 1, background: "transparent" },
|
|
3681
|
+
id: u,
|
|
3682
|
+
});
|
|
3683
|
+
|
|
3684
|
+
export async function load({ params }) {
|
|
3685
|
+
// FIRST statement: everything below reads fixtures that must not be reachable
|
|
3686
|
+
// from a production build. The launch recipe asserts this route 404s on the
|
|
3687
|
+
// deployed URL, with /dev/a11y-fixtures as the 200 control.
|
|
3688
|
+
if (!dev) error(404, { message: "Not found" });
|
|
3689
|
+
|
|
3690
|
+
const docs = documents(devImg) as Array<{ uid: string; data: { slices?: unknown[] } }>;
|
|
3691
|
+
const doc = docs.find((d) => d.uid === params.uid);
|
|
3692
|
+
if (!doc)
|
|
3693
|
+
error(404, {
|
|
3694
|
+
message: \`no assembly for "\${params.uid}" (have: \${docs.map((d) => d.uid).join(", ") || "none"})\`,
|
|
3695
|
+
});
|
|
3696
|
+
|
|
3697
|
+
return { uid: params.uid, slices: doc.data.slices ?? [] };
|
|
3698
|
+
}
|
|
3699
|
+
`;
|
|
3700
|
+
var SITE_PAGES_TEST_PREV_0_95_1 = `// The page assemblies in src/lib/site-pages.js are the SINGLE source of truth
|
|
3701
|
+
// shared by two consumers: the local matching route (src/routes/dev/match/[uid])
|
|
3702
|
+
// and \`reddoor-maint prismic-seed\`, which publishes them to Prismic.
|
|
3703
|
+
//
|
|
3704
|
+
// Those two consumers do NOT validate the same way. The dev route hands the
|
|
3705
|
+
// object straight to the slice components, so any field a fixture sets is
|
|
3706
|
+
// simply there. The Migration API validates against the slice models registered
|
|
3707
|
+
// in Prismic and SILENTLY DROPS every field the model does not declare \u2014 no
|
|
3708
|
+
// error, no warning, a 200. A page can gate green locally and publish wrong.
|
|
3709
|
+
//
|
|
3710
|
+
// This test is the mechanical check. It fails the moment a fixture carries a
|
|
3711
|
+
// field its slice model does not declare \u2014 before the seed runs, not after the
|
|
3712
|
+
// content is published. Run it before every seed.
|
|
3713
|
+
import { describe, expect, it } from "vitest";
|
|
3714
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
3715
|
+
import { fileURLToPath } from "node:url";
|
|
3716
|
+
import { dirname, join } from "node:path";
|
|
3717
|
+
import { documents } from "./site-pages.js";
|
|
3718
|
+
|
|
3719
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
3720
|
+
const SLICES = join(HERE, "slices");
|
|
3721
|
+
|
|
3722
|
+
type Variation = { primary: string[]; items: string[] };
|
|
3723
|
+
type Slice = {
|
|
3724
|
+
slice_type: string;
|
|
3725
|
+
variation: string;
|
|
3726
|
+
primary?: Record<string, unknown>;
|
|
3727
|
+
items?: Array<Record<string, unknown>>;
|
|
3728
|
+
};
|
|
3729
|
+
|
|
3730
|
+
/** Every slice model in src/lib/slices, indexed by its Prismic slice id. */
|
|
3731
|
+
function loadModels(): Record<string, Record<string, Variation>> {
|
|
3732
|
+
const out: Record<string, Record<string, Variation>> = {};
|
|
3733
|
+
if (!existsSync(SLICES)) return out;
|
|
3734
|
+
for (const dir of readdirSync(SLICES)) {
|
|
3735
|
+
const file = join(SLICES, dir, "model.json");
|
|
3736
|
+
if (!existsSync(file)) continue;
|
|
3737
|
+
const model = JSON.parse(readFileSync(file, "utf8"));
|
|
3738
|
+
out[model.id] = Object.fromEntries(
|
|
3739
|
+
(model.variations ?? []).map((v: Record<string, unknown>) => [
|
|
3740
|
+
v.id,
|
|
3741
|
+
{
|
|
3742
|
+
primary: Object.keys((v.primary as object) ?? {}),
|
|
3743
|
+
items: Object.keys((v.items as object) ?? {}),
|
|
3744
|
+
},
|
|
3745
|
+
]),
|
|
3746
|
+
);
|
|
3747
|
+
}
|
|
3748
|
+
return out;
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
/** Image resolver stub \u2014 shape only; this test never reads image values. */
|
|
3752
|
+
const stubImg = () => ({ url: "https://example.test/x.jpg" });
|
|
3753
|
+
|
|
3754
|
+
describe("site-pages documents vs slice models", () => {
|
|
3755
|
+
const models = loadModels();
|
|
3756
|
+
const docs = documents(stubImg) as Array<{ uid: string; data: { slices?: Slice[] } }>;
|
|
3757
|
+
const pages: Array<[string, Slice[]]> = docs.map((d) => [d.uid, d.data.slices ?? []]);
|
|
3758
|
+
|
|
3759
|
+
it("declares every slice type the documents use", () => {
|
|
3760
|
+
const missing = new Set<string>();
|
|
3761
|
+
for (const [, slices] of pages)
|
|
3762
|
+
for (const s of slices) if (!models[s.slice_type]) missing.add(s.slice_type);
|
|
3763
|
+
expect([...missing]).toEqual([]);
|
|
3764
|
+
});
|
|
3765
|
+
|
|
3766
|
+
it("declares every variation the documents use", () => {
|
|
3767
|
+
const missing: string[] = [];
|
|
3768
|
+
for (const [uid, slices] of pages)
|
|
3769
|
+
for (const s of slices) {
|
|
3770
|
+
const model = models[s.slice_type];
|
|
3771
|
+
if (model && !model[s.variation]) missing.push(\`\${uid}: \${s.slice_type}/\${s.variation}\`);
|
|
3772
|
+
}
|
|
3773
|
+
expect(missing).toEqual([]);
|
|
3774
|
+
});
|
|
3775
|
+
|
|
3776
|
+
// The one that catches a silent Migration-API drop.
|
|
3777
|
+
it("declares every field the documents set, so Prismic strips nothing", () => {
|
|
3778
|
+
const stripped: string[] = [];
|
|
3779
|
+
for (const [uid, slices] of pages)
|
|
3780
|
+
for (const s of slices) {
|
|
3781
|
+
const variation = models[s.slice_type]?.[s.variation];
|
|
3782
|
+
if (!variation) continue;
|
|
3783
|
+
for (const key of Object.keys(s.primary ?? {}))
|
|
3784
|
+
if (!variation.primary.includes(key))
|
|
3785
|
+
stripped.push(\`\${uid} \${s.slice_type}/\${s.variation} primary.\${key}\`);
|
|
3786
|
+
const itemKeys = new Set((s.items ?? []).flatMap((i) => Object.keys(i)));
|
|
3787
|
+
for (const key of itemKeys)
|
|
3788
|
+
if (!variation.items.includes(key))
|
|
3789
|
+
stripped.push(\`\${uid} \${s.slice_type}/\${s.variation} items.\${key}\`);
|
|
3790
|
+
}
|
|
3791
|
+
expect(stripped).toEqual([]);
|
|
3792
|
+
});
|
|
3793
|
+
});
|
|
3794
|
+
`;
|
|
3795
|
+
var MATCH_HARNESS_PREVIOUS = {
|
|
3796
|
+
"matching/harness.mjs": [HARNESS_MJS_PREV_0_95_0, HARNESS_MJS_PREV_0_95_1],
|
|
3797
|
+
"matching/gate.sh": [GATE_SH_PREV_0_95_0],
|
|
3798
|
+
"matching/next.mjs": [NEXT_MJS_PREV_0_95_0, NEXT_MJS_PREV_0_95_1],
|
|
3799
|
+
"matching/census.sh": [CENSUS_SH_PREV_0_95_1],
|
|
3800
|
+
"matching/strikes.mjs": [STRIKES_MJS_PREV_0_95_1],
|
|
3801
|
+
"matching/build-spec.mjs": [BUILD_SPEC_MJS_PREV_0_95_1],
|
|
3802
|
+
"src/routes/dev/match/[uid]/+page.server.ts": [MATCH_ROUTE_SERVER_PREV_0_95_1],
|
|
3803
|
+
"src/lib/site-pages.test.ts": [SITE_PAGES_TEST_PREV_0_95_1]
|
|
3804
|
+
};
|
|
3805
|
+
var MATCH_HARNESS_COUPLED = [
|
|
3806
|
+
"matching/harness.mjs",
|
|
3807
|
+
"matching/gate.sh",
|
|
3808
|
+
"matching/census.sh",
|
|
3809
|
+
"matching/next.mjs",
|
|
3810
|
+
"matching/strikes.mjs",
|
|
3811
|
+
"matching/build-spec.mjs"
|
|
3812
|
+
];
|
|
3813
|
+
var UNGUARDED_TWIN_TELL = "reddoor-match-twin:no-assembly";
|
|
3814
|
+
var GITIGNORE_MARKER = "# reddoor-maint match-harness: scripts + records tracked, workspace ignored";
|
|
3815
|
+
var GITIGNORE_BLOCK = `scratch-diff*/
|
|
3816
|
+
matching/*
|
|
3817
|
+
!matching/*.sh
|
|
3818
|
+
!matching/*.mjs
|
|
3819
|
+
!matching/*.md
|
|
3820
|
+
# The pause switch. Extensionless on purpose (it is a sentinel, not a doc), so
|
|
3821
|
+
# the whitelist above misses it \u2014 and an ignored switch is not a switch: it
|
|
3822
|
+
# would work on one machine and be absent from every fresh clone, CI run and
|
|
3823
|
+
# agent that most needs to see it.
|
|
3824
|
+
!matching/PAUSED
|
|
3825
|
+
!matching/spec-sections/
|
|
3826
|
+
!matching/states/
|
|
3827
|
+
matching/states/*
|
|
3828
|
+
!matching/states/*.mjs
|
|
3829
|
+
# derived artefacts, even where the extensions above would catch them
|
|
3830
|
+
matching/*.log
|
|
3831
|
+
matching/*.json
|
|
3832
|
+
# ...except the page table itself, which is the harness's configuration and the
|
|
3833
|
+
# one thing a fresh clone cannot reconstruct.
|
|
3834
|
+
!matching/harness.json
|
|
3835
|
+
matching/*.png
|
|
3836
|
+
`;
|
|
3837
|
+
var PRETTIERIGNORE_MARKER = "# reddoor-maint match-harness";
|
|
3838
|
+
var PRETTIERIGNORE_BLOCK = `# Matching harness \u2014 every file the \`match-harness\` recipe OWNS. It installs
|
|
3839
|
+
# them byte-for-byte and byte-compares them on the next install, so a site whose
|
|
3840
|
+
# prettier config differs must not reformat them: a reformatted file is
|
|
3841
|
+
# indistinguishable from a hand edit, and the recipe refuses to overwrite a hand
|
|
3842
|
+
# edit. Measured on a \`useTabs\`/\`singleQuote\` site, all three src/ entries
|
|
3843
|
+
# below are rewritten by \`prettier --write .\`, and the next upgrade is refused
|
|
3844
|
+
# for all three.
|
|
3845
|
+
#
|
|
3846
|
+
# The bracketed route segment MUST stay backslash-escaped. This file is
|
|
3847
|
+
# gitignore glob syntax, in which \`[uid]\` is a CHARACTER CLASS matching one of
|
|
3848
|
+
# \`u\`, \`i\`, \`d\` \u2014 an unescaped entry silently ignores nothing at all.
|
|
3849
|
+
#
|
|
3850
|
+
# \`matching/*.mjs\` also catches two SITE-owned records (floors.mjs,
|
|
3851
|
+
# census-deviations.mjs). Accepted, not overlooked: eslint lints .mjs, so those
|
|
3852
|
+
# two keep a style check either way, and naming seven scripts instead of one
|
|
3853
|
+
# glob goes stale on the next script the harness gains.
|
|
3854
|
+
#
|
|
3855
|
+
# harness.json is here because it is a TABLE, not prose: it carries a
|
|
3856
|
+
# hand-chosen row layout that prettier reflows.
|
|
3857
|
+
#
|
|
3858
|
+
# Everything else under matching/ (SPEC.md, LEDGER.md, spec-sections/,
|
|
3859
|
+
# states/*.mjs) stays inside \`prettier --check .\`. That is deliberately narrow:
|
|
3860
|
+
# eslint does not lint Markdown, so for the records prettier is the ONLY style
|
|
3861
|
+
# check there is, and a blanket \`matching/\` entry would leave them with none.
|
|
3862
|
+
matching/*.mjs
|
|
3863
|
+
matching/*.sh
|
|
3864
|
+
matching/harness.json
|
|
3865
|
+
src/routes/dev/match/\\[uid\\]/+page.server.ts
|
|
3866
|
+
src/routes/dev/match/\\[uid\\]/+page.svelte
|
|
3867
|
+
src/lib/site-pages.test.ts
|
|
3868
|
+
`;
|
|
3869
|
+
var CLAUDE_MD_MARKER = "## Matching rules (installed by reddoor-maint match-harness)";
|
|
3870
|
+
var CLAUDE_MD_BLOCK = `
|
|
3871
|
+
The \`matching-a-page\` skill governs a live-reference rebuild. These five rules
|
|
3872
|
+
exist because the skill alone did not hold on the project this harness came
|
|
3873
|
+
from \u2014 each one is a drift that actually happened, with the mechanical check
|
|
3874
|
+
that now catches it. \`matching/harness.json\` is this site's configuration;
|
|
3875
|
+
\`matching/LEDGER.md\` is the dated record of every deviation, floor and mask.
|
|
3876
|
+
|
|
3877
|
+
### 1. Source prescribes, rects only verify
|
|
3878
|
+
|
|
3879
|
+
Every geometry fix must cite the rule it came from: a line in the captured
|
|
3880
|
+
reference stylesheet under \`matching/spec/\`, or the reference's HTML. "The probe
|
|
3881
|
+
says the gap is 40px" is not a source. If you cannot name the line you are
|
|
3882
|
+
guessing \u2014 go read the stylesheet first.
|
|
3883
|
+
|
|
3884
|
+
**Check:** the commit body must name the file:line for each fix.
|
|
3885
|
+
**Operator's challenge:** _"which line of the reference stylesheet says that?"_
|
|
3886
|
+
|
|
3887
|
+
### 2. Phase 1 before Phase 4
|
|
3888
|
+
|
|
3889
|
+
A page gets its section census and per-section spec in \`matching/SPEC.md\` BEFORE
|
|
3890
|
+
its geometry is touched. No SPEC section, no geometry round. The census is the
|
|
3891
|
+
coverage denominator; skipping it is how a reference's root-font ladder and its
|
|
3892
|
+
per-component height ladders get discovered reactively, after the region has
|
|
3893
|
+
already failed several rounds.
|
|
3894
|
+
|
|
3895
|
+
**Check:** \`matching/gate.sh\` refuses to run a page with no \`SPEC.md\` section.
|
|
3896
|
+
**Operator's challenge:** _"show me the SPEC section for that region."_
|
|
3897
|
+
|
|
3898
|
+
### 3. Three strikes, then stop
|
|
3899
|
+
|
|
3900
|
+
A failing region that has not improved across 3+ gate runs does not get a fourth
|
|
3901
|
+
attempt. Present the attempts. Never widen the threshold, add a mask, or
|
|
3902
|
+
reclassify it as a floor to make it go away.
|
|
3903
|
+
|
|
3904
|
+
**Check:** \`node matching/strikes.mjs <page>\` \u2014 exits 1 while any region is
|
|
3905
|
+
stalled, and is the first thing a geometry round runs.
|
|
3906
|
+
**Operator's challenge:** _"how many runs has that region been flat?"_
|
|
3907
|
+
|
|
3908
|
+
### 4. A gate closes an item, nothing else
|
|
3909
|
+
|
|
3910
|
+
No fix is "done" because the code changed. Paste the gate header \u2014 it is
|
|
3911
|
+
self-describing, so a nonstandard threshold or an undisclosed mask is visible.
|
|
2437
3912
|
The threshold and the matrix are whatever \`matching/harness.json\` says, on every
|
|
2438
3913
|
page, never a subset.
|
|
2439
3914
|
|
|
3915
|
+
**Check:** \`bash matching/census.sh <page>\` exits 0 \u2014 the Phase 3 style gate,
|
|
3916
|
+
and the only one that sees an 11px footer line or a cyan-vs-teal link the pixel
|
|
3917
|
+
diff is structurally blind to. A remaining row is fixed at its source or
|
|
3918
|
+
declared in \`matching/census-deviations.mjs\` with a LEDGER line, never ignored.
|
|
2440
3919
|
**Operator's challenge:** _"paste the gate header."_
|
|
2441
3920
|
|
|
2442
3921
|
### 5. A commit is a checkpoint, not a stopping point
|
|
@@ -2470,229 +3949,24 @@ operator's call.
|
|
|
2470
3949
|
not.
|
|
2471
3950
|
3. Fix, each change citing its source line.
|
|
2472
3951
|
4. \`bash matching/gate.sh <tag> <page>\` \u2014 paste the header.
|
|
2473
|
-
5.
|
|
3952
|
+
5. \`bash matching/census.sh <page>\` \u2014 exits 0 or the round is not closed; a
|
|
3953
|
+
remaining row is fixed at its source or declared with a LEDGER line.
|
|
3954
|
+
6. Append to \`matching/LEDGER.md\` at the moment a deviation, floor or mask is
|
|
2474
3955
|
decided, not reconstructed at the end.
|
|
2475
|
-
|
|
2476
|
-
`;
|
|
2477
|
-
|
|
2478
|
-
// src/recipes/match-harness/previous.ts
|
|
2479
|
-
var MATCH_HARNESS_BLOCK_PREVIOUS = {};
|
|
2480
|
-
|
|
2481
|
-
// src/recipes/match-harness/index.ts
|
|
2482
|
-
var PRETTIER_TIMEOUT_MS = 6e4;
|
|
2483
|
-
async function readIfExists(path) {
|
|
2484
|
-
try {
|
|
2485
|
-
return await readFile(path, "utf-8");
|
|
2486
|
-
} catch {
|
|
2487
|
-
return null;
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
function normalize(s) {
|
|
2491
|
-
return s.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").trim();
|
|
2492
|
-
}
|
|
2493
|
-
function planBlockWrite(existing, marker, endMarker, block, previous) {
|
|
2494
|
-
const region = `${marker}
|
|
2495
|
-
${block}
|
|
2496
|
-
${endMarker}`;
|
|
2497
|
-
if (existing === null) return { action: "write", content: `${region}
|
|
2498
|
-
` };
|
|
2499
|
-
const start = existing.indexOf(marker);
|
|
2500
|
-
if (start === -1) {
|
|
2501
|
-
const base = existing.endsWith("\n") ? existing : `${existing}
|
|
3956
|
+
7. \`pnpm verify\`, then commit and push.
|
|
2502
3957
|
`;
|
|
2503
|
-
return { action: "write", content: `${base}
|
|
2504
|
-
${region}
|
|
2505
|
-
` };
|
|
2506
|
-
}
|
|
2507
|
-
const head = existing.slice(0, start);
|
|
2508
|
-
const bodyStart = start + marker.length + 1;
|
|
2509
|
-
const endAt = existing.indexOf(endMarker, bodyStart);
|
|
2510
|
-
if (endAt !== -1) {
|
|
2511
|
-
const body = existing.slice(bodyStart, endAt);
|
|
2512
|
-
if (normalize(body) === normalize(block)) return { action: "skip" };
|
|
2513
|
-
if (previous.some((p) => normalize(body) === normalize(p)))
|
|
2514
|
-
return {
|
|
2515
|
-
action: "replace",
|
|
2516
|
-
content: head + region + existing.slice(endAt + endMarker.length)
|
|
2517
|
-
};
|
|
2518
|
-
return { action: "flag" };
|
|
2519
|
-
}
|
|
2520
|
-
for (const candidate of [block, ...previous]) {
|
|
2521
|
-
if (!existing.startsWith(candidate, bodyStart)) continue;
|
|
2522
|
-
const content = `${head}${region}
|
|
2523
|
-
${existing.slice(bodyStart + candidate.length)}`;
|
|
2524
|
-
return candidate === block ? { action: "terminate", content } : { action: "replace", content };
|
|
2525
|
-
}
|
|
2526
|
-
return { action: "flag" };
|
|
2527
|
-
}
|
|
2528
|
-
function planFileWrite(existing, template, owner, previous) {
|
|
2529
|
-
if (existing === null) return "write";
|
|
2530
|
-
if (normalize(existing) === normalize(template)) return "skip";
|
|
2531
|
-
if (owner === "site") return "skip";
|
|
2532
|
-
if (previous.some((p) => normalize(existing) === normalize(p))) return "replace";
|
|
2533
|
-
return "flag";
|
|
2534
|
-
}
|
|
2535
|
-
var GITIGNORE_END_MARKER = "# end reddoor-maint match-harness";
|
|
2536
|
-
var PRETTIERIGNORE_END_MARKER = "# end reddoor-maint match-harness";
|
|
2537
|
-
var CLAUDE_MD_END_MARKER = "<!-- end reddoor-maint match-harness -->";
|
|
2538
|
-
var APPENDED_BLOCKS = [
|
|
2539
|
-
[".gitignore", GITIGNORE_MARKER, GITIGNORE_END_MARKER, GITIGNORE_BLOCK],
|
|
2540
|
-
[".prettierignore", PRETTIERIGNORE_MARKER, PRETTIERIGNORE_END_MARKER, PRETTIERIGNORE_BLOCK],
|
|
2541
|
-
["CLAUDE.md", CLAUDE_MD_MARKER, CLAUDE_MD_END_MARKER, CLAUDE_MD_BLOCK]
|
|
2542
|
-
];
|
|
2543
|
-
var MATCH_HARNESS_INSTALLED_PATHS = [
|
|
2544
|
-
...MATCH_HARNESS_FILES.map((f) => f.rel),
|
|
2545
|
-
...APPENDED_BLOCKS.map(([rel]) => rel)
|
|
2546
|
-
];
|
|
2547
|
-
async function matchHarness(site, opts, deps = { spawn: defaultSpawn }) {
|
|
2548
|
-
return withRecipe({
|
|
2549
|
-
name: "match-harness",
|
|
2550
|
-
site,
|
|
2551
|
-
plan: async () => {
|
|
2552
|
-
if (!opts.ref || !/^https?:\/\//.test(opts.ref)) {
|
|
2553
|
-
return {
|
|
2554
|
-
kind: "failed",
|
|
2555
|
-
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"
|
|
2556
|
-
};
|
|
2557
|
-
}
|
|
2558
|
-
return {
|
|
2559
|
-
kind: "apply",
|
|
2560
|
-
plan: {
|
|
2561
|
-
ref: opts.ref.replace(/\/$/, ""),
|
|
2562
|
-
cand: opts.cand ?? "http://localhost:5173",
|
|
2563
|
-
matrix: opts.matrix ?? [1440, 834, 390]
|
|
2564
|
-
}
|
|
2565
|
-
};
|
|
2566
|
-
},
|
|
2567
|
-
apply: async (planned, { commit, cwd }) => {
|
|
2568
|
-
const notes = [];
|
|
2569
|
-
const written = [];
|
|
2570
|
-
const before = /* @__PURE__ */ new Map();
|
|
2571
|
-
const previousAll = deps.previous ?? MATCH_HARNESS_PREVIOUS;
|
|
2572
|
-
const blockPreviousAll = deps.blockPrevious ?? MATCH_HARNESS_BLOCK_PREVIOUS;
|
|
2573
|
-
const plans = [];
|
|
2574
|
-
for (const f of MATCH_HARNESS_FILES) {
|
|
2575
|
-
const target = join(cwd, f.rel);
|
|
2576
|
-
let template = f.template;
|
|
2577
|
-
if (f.rel === HARNESS_JSON_RELATIVE) {
|
|
2578
|
-
const seed = JSON.parse(template);
|
|
2579
|
-
seed.ref = planned.ref;
|
|
2580
|
-
seed.cand = planned.cand;
|
|
2581
|
-
seed.matrix = planned.matrix;
|
|
2582
|
-
template = JSON.stringify(seed, null, 2) + "\n";
|
|
2583
|
-
}
|
|
2584
|
-
const existing = await readIfExists(target);
|
|
2585
|
-
plans.push({
|
|
2586
|
-
f,
|
|
2587
|
-
template,
|
|
2588
|
-
existing,
|
|
2589
|
-
action: planFileWrite(existing, template, f.owner, previousAll[f.rel] ?? [])
|
|
2590
|
-
});
|
|
2591
|
-
}
|
|
2592
|
-
const coupled = new Set(MATCH_HARNESS_COUPLED);
|
|
2593
|
-
const blockedBy = plans.filter((p) => coupled.has(p.f.rel) && p.action === "flag").map((p) => p.f.rel);
|
|
2594
|
-
const demoted = plans.filter(
|
|
2595
|
-
(p) => coupled.has(p.f.rel) && p.action === "replace" && !blockedBy.includes(p.f.rel)
|
|
2596
|
-
);
|
|
2597
|
-
if (blockedBy.length > 0 && demoted.length > 0) {
|
|
2598
|
-
for (const p of demoted) p.action = "flag";
|
|
2599
|
-
notes.push(
|
|
2600
|
-
`${blockedBy.join(", ")} differs from the shipped template, so the whole coupled set was left alone: ${MATCH_HARNESS_COUPLED.join(", ")} upgrade together. matching/gate.sh calls \`harness.mjs --check-run\` and matching/next.mjs imports from harness.mjs, so upgrading one without the others leaves a harness that cannot run.`
|
|
2601
|
-
);
|
|
2602
|
-
}
|
|
2603
|
-
const demotedRels = new Set(demoted.map((p) => p.f.rel));
|
|
2604
|
-
for (const { f, template, existing, action } of plans) {
|
|
2605
|
-
if (action === "flag") {
|
|
2606
|
-
if (demotedRels.has(f.rel)) continue;
|
|
2607
|
-
notes.push(
|
|
2608
|
-
`${f.rel} differs from the shipped template and was left alone (hand-edited?)`
|
|
2609
|
-
);
|
|
2610
|
-
continue;
|
|
2611
|
-
}
|
|
2612
|
-
if (action === "skip") continue;
|
|
2613
|
-
const target = join(cwd, f.rel);
|
|
2614
|
-
await mkdir(dirname(target), { recursive: true });
|
|
2615
|
-
before.set(f.rel, existing);
|
|
2616
|
-
await writeFile(target, template, "utf-8");
|
|
2617
|
-
written.push(f.rel);
|
|
2618
|
-
if (action === "replace") notes.push(`${f.rel} upgraded from a previous version`);
|
|
2619
|
-
}
|
|
2620
|
-
for (const [rel, marker, endMarker, block] of APPENDED_BLOCKS) {
|
|
2621
|
-
const path = join(cwd, rel);
|
|
2622
|
-
const existing = await readIfExists(path);
|
|
2623
|
-
const plan = planBlockWrite(
|
|
2624
|
-
existing,
|
|
2625
|
-
marker,
|
|
2626
|
-
endMarker,
|
|
2627
|
-
block,
|
|
2628
|
-
blockPreviousAll[rel] ?? []
|
|
2629
|
-
);
|
|
2630
|
-
if (plan.action === "skip") continue;
|
|
2631
|
-
if (plan.action === "flag") {
|
|
2632
|
-
notes.push(
|
|
2633
|
-
`${rel} carries a match-harness block that differs from every block this recipe has shipped, and was left alone (hand-edited?)`
|
|
2634
|
-
);
|
|
2635
|
-
continue;
|
|
2636
|
-
}
|
|
2637
|
-
before.set(rel, existing);
|
|
2638
|
-
await writeFile(path, plan.content, "utf-8");
|
|
2639
|
-
written.push(rel);
|
|
2640
|
-
if (plan.action === "replace")
|
|
2641
|
-
notes.push(`${rel}'s match-harness block upgraded from a previous version`);
|
|
2642
|
-
if (plan.action === "terminate")
|
|
2643
|
-
notes.push(
|
|
2644
|
-
`${rel}'s match-harness block region was terminated so a future version can update it`
|
|
2645
|
-
);
|
|
2646
|
-
}
|
|
2647
|
-
const siteOwned = new Set(
|
|
2648
|
-
MATCH_HARNESS_FILES.filter((f) => f.owner === "site").map((f) => f.rel)
|
|
2649
|
-
);
|
|
2650
|
-
const toFormat = written.filter((p) => siteOwned.has(p) || p === "CLAUDE.md");
|
|
2651
|
-
if (toFormat.length > 0) {
|
|
2652
|
-
const bin = await (deps.resolvePrettier ?? resolveTargetPrettier)(cwd);
|
|
2653
|
-
if (bin === null) {
|
|
2654
|
-
notes.push(PRETTIER_FLAG_NOTE);
|
|
2655
|
-
} else if (!await formatWithPrettier(deps.spawn, cwd, toFormat, {
|
|
2656
|
-
bin,
|
|
2657
|
-
timeoutMs: PRETTIER_TIMEOUT_MS
|
|
2658
|
-
})) {
|
|
2659
|
-
notes.push(PRETTIER_FLAG_NOTE);
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
await commit(
|
|
2663
|
-
"feat: install the matching harness (/dev/match route + matching/ gate scripts)"
|
|
2664
|
-
);
|
|
2665
|
-
const missing = await pathsMissingFromHead(cwd, MATCH_HARNESS_INSTALLED_PATHS);
|
|
2666
|
-
if (missing.length > 0) {
|
|
2667
|
-
const rules = await ignoreRulesFor(cwd, missing);
|
|
2668
|
-
const sources = [...new Set(rules.map((r) => r.split(" ")[0]))];
|
|
2669
|
-
const explained = new Set(rules.map((r) => r.split(" ")[1] ?? ""));
|
|
2670
|
-
const unexplained = missing.filter((p) => !explained.has(p));
|
|
2671
|
-
const removed = [];
|
|
2672
|
-
for (const rel of written.filter((p) => missing.includes(p))) {
|
|
2673
|
-
const prev = before.get(rel);
|
|
2674
|
-
if (prev === void 0) continue;
|
|
2675
|
-
if (prev === null) {
|
|
2676
|
-
await rm(join(cwd, rel), { force: true });
|
|
2677
|
-
removed.push(rel);
|
|
2678
|
-
} else {
|
|
2679
|
-
await writeFile(join(cwd, rel), prev, "utf-8");
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
for (const d of [...new Set(removed.map(dirname))].sort((a, b) => b.length - a.length)) {
|
|
2683
|
-
if (d !== ".") await rmdir(join(cwd, d)).catch(() => void 0);
|
|
2684
|
-
}
|
|
2685
|
-
return {
|
|
2686
|
-
kind: "failed",
|
|
2687
|
-
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."
|
|
2688
|
-
};
|
|
2689
|
-
}
|
|
2690
|
-
return notes.length > 0 ? { kind: "ok", notes: notes.join("; ") } : { kind: "ok" };
|
|
2691
|
-
}
|
|
2692
|
-
});
|
|
2693
|
-
}
|
|
2694
3958
|
|
|
2695
3959
|
export {
|
|
2696
|
-
|
|
3960
|
+
HARNESS_JSON_RELATIVE,
|
|
3961
|
+
MATCH_HARNESS_FILES,
|
|
3962
|
+
MATCH_HARNESS_PREVIOUS,
|
|
3963
|
+
MATCH_HARNESS_COUPLED,
|
|
3964
|
+
UNGUARDED_TWIN_TELL,
|
|
3965
|
+
GITIGNORE_MARKER,
|
|
3966
|
+
GITIGNORE_BLOCK,
|
|
3967
|
+
PRETTIERIGNORE_MARKER,
|
|
3968
|
+
PRETTIERIGNORE_BLOCK,
|
|
3969
|
+
CLAUDE_MD_MARKER,
|
|
3970
|
+
CLAUDE_MD_BLOCK
|
|
2697
3971
|
};
|
|
2698
|
-
//# sourceMappingURL=chunk-
|
|
3972
|
+
//# sourceMappingURL=chunk-Y6FCXRIC.js.map
|