@reddoorla/maintenance 0.85.2 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +8 -0
  2. package/dist/airtable-G7WBQQMI.js +9 -0
  3. package/dist/{announce-GIAYN67S.js → announce-MXTUQ6C7.js} +26 -19
  4. package/dist/announce-MXTUQ6C7.js.map +1 -0
  5. package/dist/{bump-deps-SWJB6H5V.js → bump-deps-EC74CPLS.js} +9 -9
  6. package/dist/chunk-2EVEFOZY.js +129 -0
  7. package/dist/chunk-2EVEFOZY.js.map +1 -0
  8. package/dist/{chunk-COZPL62X.js → chunk-2NDRHXMR.js} +25 -17
  9. package/dist/chunk-2NDRHXMR.js.map +1 -0
  10. package/dist/{chunk-SDUJIANJ.js → chunk-2NJQLSEU.js} +3 -3
  11. package/dist/chunk-2NQUCS7O.js +243 -0
  12. package/dist/chunk-2NQUCS7O.js.map +1 -0
  13. package/dist/{chunk-DTTIFYAM.js → chunk-2ZPT7GLV.js} +42 -403
  14. package/dist/chunk-2ZPT7GLV.js.map +1 -0
  15. package/dist/{chunk-ZTPPCHDB.js → chunk-3G25KIWW.js} +4 -4
  16. package/dist/chunk-3SF7D4DY.js +359 -0
  17. package/dist/chunk-3SF7D4DY.js.map +1 -0
  18. package/dist/{chunk-Q276MZ7E.js → chunk-3VQWSE3F.js} +4 -4
  19. package/dist/{chunk-37S6L6G4.js → chunk-5UINJNAJ.js} +72 -2
  20. package/dist/chunk-5UINJNAJ.js.map +1 -0
  21. package/dist/{chunk-THEFTZ7F.js → chunk-62CMMGEO.js} +5 -5
  22. package/dist/{chunk-FDIFTIXC.js → chunk-6TWWY7ZU.js} +2 -2
  23. package/dist/{chunk-UYCAF77E.js → chunk-7RJ2HMMJ.js} +3 -3
  24. package/dist/chunk-ACBRLLFT.js +198 -0
  25. package/dist/chunk-ACBRLLFT.js.map +1 -0
  26. package/dist/chunk-BNM5POSJ.js +284 -0
  27. package/dist/chunk-BNM5POSJ.js.map +1 -0
  28. package/dist/{chunk-R6FPFMOS.js → chunk-BQRFQSGY.js} +3 -3
  29. package/dist/{chunk-MGR2H7F4.js → chunk-BYK66EWO.js} +19 -12
  30. package/dist/chunk-BYK66EWO.js.map +1 -0
  31. package/dist/{chunk-6QRLRT3P.js → chunk-CQR4LIM7.js} +12 -265
  32. package/dist/chunk-CQR4LIM7.js.map +1 -0
  33. package/dist/{chunk-SEEA6RXF.js → chunk-DJZWYIPP.js} +2 -2
  34. package/dist/chunk-DJZWYIPP.js.map +1 -0
  35. package/dist/chunk-EVBCK2KC.js +68 -0
  36. package/dist/chunk-EVBCK2KC.js.map +1 -0
  37. package/dist/{chunk-TP5M2NQB.js → chunk-F5KM4LEQ.js} +3 -3
  38. package/dist/{chunk-62VF32IC.js → chunk-ILUNZHYV.js} +2 -2
  39. package/dist/chunk-ITIOFN7A.js +396 -0
  40. package/dist/chunk-ITIOFN7A.js.map +1 -0
  41. package/dist/{chunk-LS6QPANO.js → chunk-IYSEC543.js} +2 -2
  42. package/dist/{chunk-DC5H7KEQ.js → chunk-JHGHLKFS.js} +2 -2
  43. package/dist/{chunk-PBWDDDQH.js → chunk-KTH5ZODA.js} +2 -2
  44. package/dist/{chunk-4DHQCN5H.js → chunk-LBYOLBW7.js} +17 -2
  45. package/dist/chunk-LBYOLBW7.js.map +1 -0
  46. package/dist/{chunk-577HCGZO.js → chunk-LDWZSLSU.js} +38 -23
  47. package/dist/chunk-LDWZSLSU.js.map +1 -0
  48. package/dist/{chunk-LTB6KVJW.js → chunk-N45M3FDK.js} +13 -11
  49. package/dist/{chunk-LTB6KVJW.js.map → chunk-N45M3FDK.js.map} +1 -1
  50. package/dist/chunk-NYUYNYQL.js +48 -0
  51. package/dist/chunk-NYUYNYQL.js.map +1 -0
  52. package/dist/{chunk-7J53SUT6.js → chunk-P2FE5AI6.js} +4 -4
  53. package/dist/{chunk-3YXDSCM6.js → chunk-PPE5KZJK.js} +29 -11
  54. package/dist/chunk-PPE5KZJK.js.map +1 -0
  55. package/dist/chunk-PTDMVGKL.js +31 -0
  56. package/dist/chunk-PTDMVGKL.js.map +1 -0
  57. package/dist/{chunk-D4FVAIEJ.js → chunk-PZR4EBPY.js} +5 -2
  58. package/dist/chunk-PZR4EBPY.js.map +1 -0
  59. package/dist/{chunk-NWB4O5EY.js → chunk-SLUQ7R6I.js} +3 -3
  60. package/dist/chunk-SLUQ7R6I.js.map +1 -0
  61. package/dist/{chunk-GZNBCHAZ.js → chunk-TERE5SDF.js} +4 -4
  62. package/dist/{chunk-ZMIPG6TR.js → chunk-TVL5MVUF.js} +4 -4
  63. package/dist/chunk-UDRHAQT7.js +172 -0
  64. package/dist/chunk-UDRHAQT7.js.map +1 -0
  65. package/dist/chunk-VAD43MOR.js +72 -0
  66. package/dist/chunk-VAD43MOR.js.map +1 -0
  67. package/dist/chunk-VSBFM7DS.js +229 -0
  68. package/dist/chunk-VSBFM7DS.js.map +1 -0
  69. package/dist/chunk-W7Z3BGCI.js +274 -0
  70. package/dist/chunk-W7Z3BGCI.js.map +1 -0
  71. package/dist/chunk-X3B723J4.js +1209 -0
  72. package/dist/chunk-X3B723J4.js.map +1 -0
  73. package/dist/{chunk-VRWUL4TR.js → chunk-XHHMM5HX.js} +2 -2
  74. package/dist/{chunk-RJB3QUSP.js → chunk-XIDUYLSR.js} +3 -3
  75. package/dist/{chunk-QG6OSL4M.js → chunk-XUGSZP7Z.js} +2 -2
  76. package/dist/{chunk-S56JQJ4F.js → chunk-Y53WOQIJ.js} +3 -3
  77. package/dist/{chunk-ICXV25N7.js → chunk-Z3KYFUN6.js} +3 -3
  78. package/dist/{chunk-MPBXOJ3N.js → chunk-Z7AXYKQB.js} +33 -2
  79. package/dist/chunk-Z7AXYKQB.js.map +1 -0
  80. package/dist/{chunk-WYYQK4D3.js → chunk-ZKXSX4LO.js} +11 -11
  81. package/dist/cli/bin.js +46 -28
  82. package/dist/cli/bin.js.map +1 -1
  83. package/dist/cli/commands/audit.d.ts +30 -1
  84. package/dist/cli/commands/audit.js +62 -30
  85. package/dist/cli/commands/audit.js.map +1 -1
  86. package/dist/client-AUE3XUPO.js +11 -0
  87. package/dist/{convert-to-pnpm-AT52NWZB.js → convert-to-pnpm-6L6P74WR.js} +9 -9
  88. package/dist/db-B35OGV3G.js +217 -0
  89. package/dist/db-B35OGV3G.js.map +1 -0
  90. package/dist/{digest-UVHYZUYE.js → digest-BL6UJYKH.js} +23 -20
  91. package/dist/{digest-UVHYZUYE.js.map → digest-BL6UJYKH.js.map} +1 -1
  92. package/dist/dump-P6EBSIOH.js +68 -0
  93. package/dist/dump-P6EBSIOH.js.map +1 -0
  94. package/dist/email-VTTTKKL5.js +195 -0
  95. package/dist/email-VTTTKKL5.js.map +1 -0
  96. package/dist/{ensure-site-S25MDDHW.js → ensure-site-DFTB7MN3.js} +8 -7
  97. package/dist/ensure-site-DFTB7MN3.js.map +1 -0
  98. package/dist/fleet-events-cF2Om88w.d.ts +24 -0
  99. package/dist/{forms-notify-target-TWN4EH6K.js → forms-notify-target-75U6OU7L.js} +38 -132
  100. package/dist/forms-notify-target-75U6OU7L.js.map +1 -0
  101. package/dist/{github-signals-G4ILZAJ6.js → github-signals-IJUPGCUX.js} +40 -19
  102. package/dist/github-signals-IJUPGCUX.js.map +1 -0
  103. package/dist/{header-image-T7XD37JL.js → header-image-AWLLQA4L.js} +34 -11
  104. package/dist/header-image-AWLLQA4L.js.map +1 -0
  105. package/dist/header-images-335L2AKB.js +68 -0
  106. package/dist/header-images-335L2AKB.js.map +1 -0
  107. package/dist/{health-endpoint-242WG2OJ.js → health-endpoint-WJC2UMYS.js} +9 -9
  108. package/dist/health-mirror-4B2L7T2Y.js +75 -0
  109. package/dist/health-mirror-4B2L7T2Y.js.map +1 -0
  110. package/dist/import-airtable-HE43FB4H.js +35 -0
  111. package/dist/index.d.ts +175 -21
  112. package/dist/index.js +115 -73
  113. package/dist/index.js.map +1 -1
  114. package/dist/{init-JN26NTWF.js → init-NJEOEO7I.js} +20 -17
  115. package/dist/{init-JN26NTWF.js.map → init-NJEOEO7I.js.map} +1 -1
  116. package/dist/{launch-JA7Z4CIM.js → launch-XNAX427U.js} +31 -25
  117. package/dist/{launch-JA7Z4CIM.js.map → launch-XNAX427U.js.map} +1 -1
  118. package/dist/lighthouse-VHI77PFB.js +11 -0
  119. package/dist/{lighthouse-airtable-BIPM26SZ.js → lighthouse-airtable-LBVIYYP5.js} +3 -3
  120. package/dist/mailchimp-XYCTEEJY.js +72 -0
  121. package/dist/mailchimp-XYCTEEJY.js.map +1 -0
  122. package/dist/migrate-GQH237A6.js +7 -0
  123. package/dist/notify-C645PGD5.js +22 -0
  124. package/dist/{onboard-53CITIM7.js → onboard-GK4PHPNI.js} +9 -9
  125. package/dist/orchestrate-77UO4BYJ.js +30 -0
  126. package/dist/orchestrate-77UO4BYJ.js.map +1 -0
  127. package/dist/parity-MWS24A3H.js +12 -0
  128. package/dist/parity-MWS24A3H.js.map +1 -0
  129. package/dist/pipeline-S6CVHDGB.js +12 -0
  130. package/dist/pipeline-S6CVHDGB.js.map +1 -0
  131. package/dist/{preflight-3XPDUHFJ.js → preflight-6WBNU2GZ.js} +16 -13
  132. package/dist/{preflight-3XPDUHFJ.js.map → preflight-6WBNU2GZ.js.map} +1 -1
  133. package/dist/{prismic-ci-UTQ6LRR3.js → prismic-ci-3RHOZUUT.js} +15 -15
  134. package/dist/{prismic-models-PVES6R5V.js → prismic-models-QBGUT2Q3.js} +10 -10
  135. package/dist/prospect-audit-IUNCJ2LV.js +174 -0
  136. package/dist/prospect-audit-IUNCJ2LV.js.map +1 -0
  137. package/dist/prospect-audits-3PMIO73D.js +19 -0
  138. package/dist/prospect-audits-3PMIO73D.js.map +1 -0
  139. package/dist/{protection-audit-64PLLBLI.js → protection-audit-SJO53MEQ.js} +3 -3
  140. package/dist/recipes/sync-configs.js +3 -3
  141. package/dist/render-AQAVQDPZ.js +10 -0
  142. package/dist/render-AQAVQDPZ.js.map +1 -0
  143. package/dist/{renovate-dispatch-ICJGSFCE.js → renovate-dispatch-QJY75ODR.js} +8 -8
  144. package/dist/replay-C2LXMJZP.js +66 -0
  145. package/dist/replay-C2LXMJZP.js.map +1 -0
  146. package/dist/{report-6CKSWJNG.js → report-4EVNDHOZ.js} +55 -29
  147. package/dist/report-4EVNDHOZ.js.map +1 -0
  148. package/dist/reports/header-image/assets/headline-announcement.png +0 -0
  149. package/dist/reports/header-image/assets/headline-launch.png +0 -0
  150. package/dist/reports/header-image/assets/headline-maintenance.png +0 -0
  151. package/dist/reports/header-image/assets/headline-testing.png +0 -0
  152. package/dist/reports/header-image/assets/plate-clean.png +0 -0
  153. package/dist/{self-updating-PPQVV6UA.js → self-updating-6DWI2Z56.js} +9 -9
  154. package/dist/{selftest-CQ6NOX52.js → selftest-RC36MTKP.js} +25 -20
  155. package/dist/selftest-RC36MTKP.js.map +1 -0
  156. package/dist/smoke-B2GQOSIZ.js +20 -0
  157. package/dist/smoke-B2GQOSIZ.js.map +1 -0
  158. package/dist/{smoke-suite-TMCSOYRK.js → smoke-suite-VYY2LXM2.js} +9 -9
  159. package/dist/{submissions-GJ6ITBRH.js → submissions-Q7DRFOCD.js} +3 -3
  160. package/dist/{svelte-codemods-ZOBDHIES.js → svelte-codemods-25J2FO22.js} +9 -9
  161. package/dist/sync-34HNZZ56.js +44 -0
  162. package/dist/sync-34HNZZ56.js.map +1 -0
  163. package/dist/{sync-configs-CJXUX6MD.js → sync-configs-3SE6PSCO.js} +9 -9
  164. package/dist/{upgrade-WLX27UPC.js → upgrade-WBRQ62FX.js} +9 -9
  165. package/dist/{webflow-624FEBFD.js → webflow-IRKOIT7S.js} +4 -4
  166. package/dist/webhook-VOGOW3W6.js +38 -0
  167. package/dist/webhook-VOGOW3W6.js.map +1 -0
  168. package/dist/{websites-SADZR4XQ.js → websites-SXWTP72S.js} +6 -2
  169. package/dist/websites-SXWTP72S.js.map +1 -0
  170. package/dist/{write-audits-to-airtable-7PH75YWY.js → write-audits-to-airtable-MBEILLPU.js} +4 -4
  171. package/dist/write-audits-to-airtable-MBEILLPU.js.map +1 -0
  172. package/package.json +5 -2
  173. package/dist/airtable-HRKSA7V4.js +0 -9
  174. package/dist/announce-GIAYN67S.js.map +0 -1
  175. package/dist/chunk-37S6L6G4.js.map +0 -1
  176. package/dist/chunk-3YXDSCM6.js.map +0 -1
  177. package/dist/chunk-4DHQCN5H.js.map +0 -1
  178. package/dist/chunk-4SOCPK7A.js +0 -108
  179. package/dist/chunk-4SOCPK7A.js.map +0 -1
  180. package/dist/chunk-577HCGZO.js.map +0 -1
  181. package/dist/chunk-6QRLRT3P.js.map +0 -1
  182. package/dist/chunk-COZPL62X.js.map +0 -1
  183. package/dist/chunk-D4FVAIEJ.js.map +0 -1
  184. package/dist/chunk-DTTIFYAM.js.map +0 -1
  185. package/dist/chunk-MGR2H7F4.js.map +0 -1
  186. package/dist/chunk-MPBXOJ3N.js.map +0 -1
  187. package/dist/chunk-NWB4O5EY.js.map +0 -1
  188. package/dist/chunk-SEEA6RXF.js.map +0 -1
  189. package/dist/chunk-U5SZ4FOP.js +0 -26
  190. package/dist/chunk-U5SZ4FOP.js.map +0 -1
  191. package/dist/client-X5WUJWDE.js +0 -11
  192. package/dist/db-JSIZYT23.js +0 -23
  193. package/dist/db-JSIZYT23.js.map +0 -1
  194. package/dist/ensure-site-S25MDDHW.js.map +0 -1
  195. package/dist/forms-notify-target-TWN4EH6K.js.map +0 -1
  196. package/dist/github-signals-G4ILZAJ6.js.map +0 -1
  197. package/dist/header-image-T7XD37JL.js.map +0 -1
  198. package/dist/migrate-LJUFGXQZ.js +0 -7
  199. package/dist/orchestrate-KDNXZQX7.js +0 -27
  200. package/dist/report-6CKSWJNG.js.map +0 -1
  201. package/dist/reports/header-image/assets/plate.png +0 -0
  202. package/dist/selftest-CQ6NOX52.js.map +0 -1
  203. /package/dist/{airtable-HRKSA7V4.js.map → airtable-G7WBQQMI.js.map} +0 -0
  204. /package/dist/{bump-deps-SWJB6H5V.js.map → bump-deps-EC74CPLS.js.map} +0 -0
  205. /package/dist/{chunk-SDUJIANJ.js.map → chunk-2NJQLSEU.js.map} +0 -0
  206. /package/dist/{chunk-ZTPPCHDB.js.map → chunk-3G25KIWW.js.map} +0 -0
  207. /package/dist/{chunk-Q276MZ7E.js.map → chunk-3VQWSE3F.js.map} +0 -0
  208. /package/dist/{chunk-THEFTZ7F.js.map → chunk-62CMMGEO.js.map} +0 -0
  209. /package/dist/{chunk-FDIFTIXC.js.map → chunk-6TWWY7ZU.js.map} +0 -0
  210. /package/dist/{chunk-UYCAF77E.js.map → chunk-7RJ2HMMJ.js.map} +0 -0
  211. /package/dist/{chunk-R6FPFMOS.js.map → chunk-BQRFQSGY.js.map} +0 -0
  212. /package/dist/{chunk-TP5M2NQB.js.map → chunk-F5KM4LEQ.js.map} +0 -0
  213. /package/dist/{chunk-62VF32IC.js.map → chunk-ILUNZHYV.js.map} +0 -0
  214. /package/dist/{chunk-LS6QPANO.js.map → chunk-IYSEC543.js.map} +0 -0
  215. /package/dist/{chunk-DC5H7KEQ.js.map → chunk-JHGHLKFS.js.map} +0 -0
  216. /package/dist/{chunk-PBWDDDQH.js.map → chunk-KTH5ZODA.js.map} +0 -0
  217. /package/dist/{chunk-7J53SUT6.js.map → chunk-P2FE5AI6.js.map} +0 -0
  218. /package/dist/{chunk-GZNBCHAZ.js.map → chunk-TERE5SDF.js.map} +0 -0
  219. /package/dist/{chunk-ZMIPG6TR.js.map → chunk-TVL5MVUF.js.map} +0 -0
  220. /package/dist/{chunk-VRWUL4TR.js.map → chunk-XHHMM5HX.js.map} +0 -0
  221. /package/dist/{chunk-RJB3QUSP.js.map → chunk-XIDUYLSR.js.map} +0 -0
  222. /package/dist/{chunk-QG6OSL4M.js.map → chunk-XUGSZP7Z.js.map} +0 -0
  223. /package/dist/{chunk-S56JQJ4F.js.map → chunk-Y53WOQIJ.js.map} +0 -0
  224. /package/dist/{chunk-ICXV25N7.js.map → chunk-Z3KYFUN6.js.map} +0 -0
  225. /package/dist/{chunk-WYYQK4D3.js.map → chunk-ZKXSX4LO.js.map} +0 -0
  226. /package/dist/{client-X5WUJWDE.js.map → client-AUE3XUPO.js.map} +0 -0
  227. /package/dist/{convert-to-pnpm-AT52NWZB.js.map → convert-to-pnpm-6L6P74WR.js.map} +0 -0
  228. /package/dist/{health-endpoint-242WG2OJ.js.map → health-endpoint-WJC2UMYS.js.map} +0 -0
  229. /package/dist/{lighthouse-airtable-BIPM26SZ.js.map → import-airtable-HE43FB4H.js.map} +0 -0
  230. /package/dist/{migrate-LJUFGXQZ.js.map → lighthouse-VHI77PFB.js.map} +0 -0
  231. /package/dist/{orchestrate-KDNXZQX7.js.map → lighthouse-airtable-LBVIYYP5.js.map} +0 -0
  232. /package/dist/{websites-SADZR4XQ.js.map → migrate-GQH237A6.js.map} +0 -0
  233. /package/dist/{write-audits-to-airtable-7PH75YWY.js.map → notify-C645PGD5.js.map} +0 -0
  234. /package/dist/{onboard-53CITIM7.js.map → onboard-GK4PHPNI.js.map} +0 -0
  235. /package/dist/{prismic-ci-UTQ6LRR3.js.map → prismic-ci-3RHOZUUT.js.map} +0 -0
  236. /package/dist/{prismic-models-PVES6R5V.js.map → prismic-models-QBGUT2Q3.js.map} +0 -0
  237. /package/dist/{protection-audit-64PLLBLI.js.map → protection-audit-SJO53MEQ.js.map} +0 -0
  238. /package/dist/{renovate-dispatch-ICJGSFCE.js.map → renovate-dispatch-QJY75ODR.js.map} +0 -0
  239. /package/dist/{self-updating-PPQVV6UA.js.map → self-updating-6DWI2Z56.js.map} +0 -0
  240. /package/dist/{smoke-suite-TMCSOYRK.js.map → smoke-suite-VYY2LXM2.js.map} +0 -0
  241. /package/dist/{submissions-GJ6ITBRH.js.map → submissions-Q7DRFOCD.js.map} +0 -0
  242. /package/dist/{svelte-codemods-ZOBDHIES.js.map → svelte-codemods-25J2FO22.js.map} +0 -0
  243. /package/dist/{sync-configs-CJXUX6MD.js.map → sync-configs-3SE6PSCO.js.map} +0 -0
  244. /package/dist/{upgrade-WLX27UPC.js.map → upgrade-WBRQ62FX.js.map} +0 -0
  245. /package/dist/{webflow-624FEBFD.js.map → webflow-IRKOIT7S.js.map} +0 -0
@@ -5,20 +5,20 @@ import {
5
5
  appendSkipNotice,
6
6
  fleetWorkdir,
7
7
  prepareFleetSites
8
- } from "./chunk-ZMIPG6TR.js";
8
+ } from "./chunk-TVL5MVUF.js";
9
9
  import {
10
10
  resolveSites
11
- } from "./chunk-7J53SUT6.js";
11
+ } from "./chunk-P2FE5AI6.js";
12
12
  import {
13
13
  healthEndpoint
14
- } from "./chunk-R6FPFMOS.js";
14
+ } from "./chunk-BQRFQSGY.js";
15
15
  import "./chunk-R53VL3RI.js";
16
- import "./chunk-ZTPPCHDB.js";
17
- import "./chunk-FDIFTIXC.js";
18
- import "./chunk-4DHQCN5H.js";
19
- import "./chunk-XXTZBPUY.js";
16
+ import "./chunk-3G25KIWW.js";
17
+ import "./chunk-6TWWY7ZU.js";
18
+ import "./chunk-EVBCK2KC.js";
20
19
  import "./chunk-XTK5VIQB.js";
21
- import "./chunk-U5SZ4FOP.js";
20
+ import "./chunk-LBYOLBW7.js";
21
+ import "./chunk-XXTZBPUY.js";
22
22
 
23
23
  // src/cli/commands/health-endpoint.ts
24
24
  import { resolve } from "path";
@@ -51,4 +51,4 @@ async function runHealthEndpointCommand(site, opts) {
51
51
  export {
52
52
  runHealthEndpointCommand
53
53
  };
54
- //# sourceMappingURL=health-endpoint-242WG2OJ.js.map
54
+ //# sourceMappingURL=health-endpoint-WJC2UMYS.js.map
@@ -0,0 +1,75 @@
1
+ import {
2
+ healthColumnFor,
3
+ scheduleColumnFor
4
+ } from "./chunk-ITIOFN7A.js";
5
+ import "./chunk-ACBRLLFT.js";
6
+ import {
7
+ MAINTENANCE_CHECKLIST,
8
+ TESTING_CHECKLIST
9
+ } from "./chunk-VAD43MOR.js";
10
+ import {
11
+ openDb,
12
+ readDbConfig
13
+ } from "./chunk-JHGHLKFS.js";
14
+ import "./chunk-3SF7D4DY.js";
15
+ import "./chunk-LDWZSLSU.js";
16
+ import "./chunk-6VAL7XJT.js";
17
+
18
+ // src/db/fleet-state.ts
19
+ async function mirrorHealthFields(db, siteId, fields) {
20
+ const patch = {};
21
+ for (const [field, value] of Object.entries(fields)) {
22
+ const m = healthColumnFor(field);
23
+ if (!m)
24
+ throw new Error(`mirrorHealthFields: importer claims no site_health column for '${field}'`);
25
+ patch[m.col] = m.coerce(value);
26
+ }
27
+ if (Object.keys(patch).length === 0) return true;
28
+ const res = await db.updateTable("site_health").set(patch).where("site_id", "=", siteId).executeTakeFirst();
29
+ return res.numUpdatedRows > 0n;
30
+ }
31
+ async function mirrorScheduleFields(db, siteId, fields, computedAt) {
32
+ const patch = {};
33
+ for (const [field, value] of Object.entries(fields)) {
34
+ const m = scheduleColumnFor(field);
35
+ if (!m)
36
+ throw new Error(
37
+ `mirrorScheduleFields: importer claims no site_schedule column for '${field}'`
38
+ );
39
+ patch[m.col] = m.coerce(value);
40
+ }
41
+ if (Object.keys(patch).length === 0) return true;
42
+ patch.computed_at = computedAt;
43
+ const res = await db.updateTable("site_schedule").set(patch).where("site_id", "=", siteId).executeTakeFirst();
44
+ return res.numUpdatedRows > 0n;
45
+ }
46
+ var CHECKLIST_FIELD_BY_KEY = new Map(
47
+ [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map((i) => [i.key, i.field])
48
+ );
49
+
50
+ // src/audits/health-mirror.ts
51
+ async function makeHealthMirrorBestEffort(open = () => openDb(readDbConfig())) {
52
+ let db;
53
+ try {
54
+ db = await open();
55
+ } catch (e) {
56
+ console.error(`[health-mirror] mirroring disabled: no libSQL (${String(e)})`);
57
+ return null;
58
+ }
59
+ return (siteId, fields) => mirrorHealthFields(db, siteId, fields);
60
+ }
61
+ async function makeScheduleMirrorBestEffort(open = () => openDb(readDbConfig())) {
62
+ let db;
63
+ try {
64
+ db = await open();
65
+ } catch (e) {
66
+ console.error(`[schedule-mirror] mirroring disabled: no libSQL (${String(e)})`);
67
+ return null;
68
+ }
69
+ return (siteId, fields, computedAt) => mirrorScheduleFields(db, siteId, fields, computedAt);
70
+ }
71
+ export {
72
+ makeHealthMirrorBestEffort,
73
+ makeScheduleMirrorBestEffort
74
+ };
75
+ //# sourceMappingURL=health-mirror-4B2L7T2Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/fleet-state.ts","../src/audits/health-mirror.ts"],"sourcesContent":["/** Phase 2 of the Airtable → Turso migration (#539): the fleet-state READ layer.\n *\n * Returns the exact `WebsiteRow` shape the Airtable module returns, so every\n * repoint is an import-only swap at a composition root — the same trick\n * listSubmissionsForSite used for the hybrid-db cutover. The raw values live\n * in `sites`/`site_health`/`site_schedule` (written by the importer / hourly\n * sync); coercion to `WebsiteRow` happens HERE, reusing the Airtable module's\n * own exported coercers (`toVerdict`, `toFrequency`, `parseNotifyRouting`,\n * `parseSecurityAdvisories`, `trimToNull`) so there is one truth for each.\n *\n * The equivalence instrument (tests/db/fleet-state.test.ts) pins this module\n * to `mapRow` field-by-field: for a fixture record, `mapRow(rec)` must deep-\n * equal the row read back through here after an import. A new WebsiteRow\n * field fails that test until this module carries it.\n *\n * `headerImage` is the ONE deliberate exception (design D5): Airtable stopped\n * being its source — the bytes belong in `sites.header_image*`. Nothing\n * writes those columns yet (verified empty across all 44 prod rows,\n * 2026-08-24), so this reads null fleet-wide today, and approve-report — the\n * only request-path consumer — stays on the Airtable reader until the\n * header-image writer moves in Phase 3. The `url` is \"\" because the bytes\n * live in the row itself, not behind a signed URL.\n */\nimport type { Selectable, Updateable } from \"kysely\";\nimport type { Db } from \"./client.js\";\nimport type { ReportsTable, SiteHealthTable, SiteScheduleTable } from \"./schema.js\";\nimport { SITE_FIELDS, healthColumnFor, scheduleColumnFor } from \"./import-airtable.js\";\nimport {\n toReportType,\n parseAutoEvidence,\n type ReportRow,\n type DeliveryStatus,\n} from \"../reports/airtable/reports.js\";\nimport { MAINTENANCE_CHECKLIST, TESTING_CHECKLIST } from \"../reports/checklist.js\";\nimport type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport { canonicalizeStatus } from \"../reports/airtable/site-status.js\";\nimport {\n parseNotifyRouting,\n parseSecurityAdvisories,\n toFrequency,\n toPrismicModelsVerdict,\n toVerdict,\n trimToNull,\n} from \"../reports/airtable/websites.js\";\n\ntype JoinedRow = Record<string, unknown>;\n\nconst str = (v: unknown): string | null => (typeof v === \"string\" ? v : null);\nconst num = (v: unknown): number | null => (typeof v === \"number\" ? v : null);\n/** Stored 1/0/null (importer's b01n) → boolean | null. */\nconst bool = (v: unknown): boolean | null => (typeof v === \"number\" ? v !== 0 : null);\n\n/** Stored JSON array (importer-normalized) → the trimmed string[] mapRow yields. */\nfunction parseAwc(raw: unknown): string[] {\n if (typeof raw !== \"string\" || raw.trim() === \"\") return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed\n .filter((x): x is string => typeof x === \"string\")\n .map((s) => s.trim())\n .filter(Boolean);\n } catch {\n return [];\n }\n}\n\nfunction rowFromJoined(r: JoinedRow): WebsiteRow {\n const name = String(r.name ?? \"\");\n const headerFilename = str(r.header_image_filename);\n const headerType = str(r.header_image_type);\n return {\n id: String(r.id),\n name,\n url: str(r.url) ?? \"\",\n // `sites.status` holds the RAW Airtable cell (the importer stores it verbatim\n // so the hourly parity check compares raw-to-raw). Canonicalizing HERE — the\n // twin of mapRow's seam — is what keeps the #558 equivalence instrument green.\n status: canonicalizeStatus(r.status),\n statusRaw: str(r.status),\n pointOfContact: str(r.point_of_contact),\n maintenanceFreq: toFrequency(r.maintenance_freq, `${name} maintenance`),\n testingFreq: toFrequency(r.testing_freq, `${name} testing`),\n maintenanceFreqRaw: str(r.maintenance_freq),\n testingFreqRaw: str(r.testing_freq),\n maintenanceDay: str(r.maintenance_day),\n testingDay: str(r.testing_day),\n ga4PropertyId: str(r.ga4_property_id),\n searchQuery: str(r.search_query),\n searchConsoleProperty: str(r.search_console_property),\n analyticsSoftFailAt: str(r.analytics_soft_fail_at),\n gitRepo: str(r.git_repo),\n reportRecipientsTo: str(r.report_recipients_to),\n reportRecipientsCc: str(r.report_recipients_cc),\n acceptedWatchConditions: parseAwc(r.accepted_watch_conditions),\n headerImage:\n headerFilename !== null && headerType !== null\n ? { url: \"\", filename: headerFilename, type: headerType }\n : null,\n pScore: num(r.p_score),\n rScore: num(r.r_score),\n bpScore: num(r.bp_score),\n seoScore: num(r.seo_score),\n lastLighthouseAuditAt: str(r.lighthouse_at),\n a11yViolations: num(r.a11y_violations),\n depsDrifted: num(r.deps_drifted),\n depsMajorBehind: num(r.deps_major_behind),\n depsOutdated: num(r.deps_outdated),\n depsMajorOutdated: num(r.deps_major_outdated),\n securityVulnsCritical: num(r.vulns_critical),\n securityVulnsHigh: num(r.vulns_high),\n securityVulnsModerate: num(r.vulns_moderate),\n securityVulnsLow: num(r.vulns_low),\n securityAutoFixAttempts: num(r.auto_fix_attempts),\n lastSecurityAuditAt: str(r.security_audit_at),\n securityAdvisories: parseSecurityAdvisories(r.security_advisories),\n certDaysRemaining: num(r.cert_days_remaining),\n domainCheckedAt: str(r.domain_checked_at),\n netlifyId: trimToNull(r.netlify_id),\n deployStatus: str(r.deploy_status),\n lastDeployAt: str(r.last_deploy_at),\n deployLogUrl: str(r.deploy_log_url),\n deployCheckedAt: str(r.deploy_checked_at),\n functionHealth: toVerdict(r.function_health),\n cmsReachable: toVerdict(r.cms_reachable),\n turnstileWidget: toVerdict(r.turnstile_widget),\n functionHealthCheckedAt: str(r.function_health_checked_at),\n crossbrowserOk: bool(r.crossbrowser_ok),\n mobileOk: bool(r.mobile_ok),\n linksOk: bool(r.links_ok),\n brokenLinks: num(r.broken_links),\n browserCheckedAt: str(r.browser_checked_at),\n reachableOk: toVerdict(r.uptime_reachable),\n titleMetaOk: toVerdict(r.titles_meta_ok),\n copyIntro: trimToNull(r.copy_intro),\n copyContact: trimToNull(r.copy_contact),\n copyFooter: trimToNull(r.copy_footer),\n launchedAt: str(r.launched_at),\n newsletterWebhook: trimToNull(r.newsletter_webhook),\n notifyRouting: parseNotifyRouting(r.notify_routing),\n notifyRoutingRaw: trimToNull(r.notify_routing),\n mailchimpApiKey: trimToNull(r.mailchimp_api_key),\n mailchimpAudienceId: trimToNull(r.mailchimp_audience_id),\n requireTurnstile: typeof r.require_turnstile === \"number\" ? r.require_turnstile !== 0 : false,\n renovateFailingCis: num(r.renovate_failing_cis),\n defaultBranchCi: str(r.default_branch_ci),\n lastCommitAt: str(r.last_commit_at),\n githubSignalsAt: str(r.github_signals_at),\n smokeOk: toVerdict(r.smoke_ok),\n lastSmokeAt: str(r.last_smoke_at),\n formE2eOk: toVerdict(r.form_e2e_ok),\n formE2eCheckedAt: str(r.form_e2e_checked_at),\n prismicModels: toPrismicModelsVerdict(r.prismic_models),\n prismicModelsCheckedAt: str(r.prismic_models_checked_at),\n prismicModelsDrift: str(r.prismic_models_drift),\n prismicAckUntil: str(r.prismic_ack_until),\n nextMaintenanceAt: str(r.next_maintenance_at),\n nextTestingAt: str(r.next_testing_at),\n };\n}\n\n/** Every sites column EXCEPT the header-image BLOB. Since the 2026-08-24\n * backfill, header_image holds 0.7–3.5 MB JPEGs — a selectAll would haul\n * megabytes into every site lookup (per LEAD on the ingest path, ×44 on the\n * fleet list) and Turso bills the bytes. WebsiteRow only needs the metadata\n * columns; the bytes have their own dedicated readers when a consumer wants\n * them. Kept in lockstep with SitesTable by the blob-exclusion test. */\nconst SITE_COLUMNS = [\n \"sites.id\",\n \"sites.slug\",\n \"sites.name\",\n \"sites.url\",\n \"sites.status\",\n \"sites.point_of_contact\",\n \"sites.maintenance_freq\",\n \"sites.testing_freq\",\n \"sites.maintenance_day\",\n \"sites.testing_day\",\n \"sites.ga4_property_id\",\n \"sites.search_query\",\n \"sites.search_console_property\",\n \"sites.git_repo\",\n \"sites.netlify_id\",\n \"sites.report_recipients_to\",\n \"sites.report_recipients_cc\",\n \"sites.copy_intro\",\n \"sites.copy_contact\",\n \"sites.copy_footer\",\n \"sites.newsletter_webhook\",\n \"sites.mailchimp_api_key\",\n \"sites.mailchimp_audience_id\",\n \"sites.notify_routing\",\n \"sites.require_turnstile\",\n \"sites.accepted_watch_conditions\",\n \"sites.prismic_ack_until\",\n \"sites.launched_at\",\n \"sites.header_image_filename\",\n \"sites.header_image_type\",\n \"sites.header_image_generated_at\",\n \"sites.legacy\",\n] as const;\n\n/** The three-table join every read composes from. site_health/site_schedule\n * rows are upserted alongside sites by the importer, but LEFT JOIN anyway —\n * a site row must never vanish from a surface because a health row is absent. */\nfunction joined(db: Db) {\n return db\n .selectFrom(\"sites\")\n .leftJoin(\"site_health\", \"site_health.site_id\", \"sites.id\")\n .leftJoin(\"site_schedule\", \"site_schedule.site_id\", \"sites.id\")\n .select(SITE_COLUMNS)\n .selectAll([\"site_health\", \"site_schedule\"]);\n}\n\n/** Write-through for the site-detail editor (Phase 2): after the Airtable\n * write (still the Phase 2 source of truth), mirror the same cell into\n * `sites` so a Turso-reading page renders the edit immediately instead of\n * waiting for the next hourly sync. The column mapping is the IMPORTER's own\n * map — one truth — and the value gets the importer's empty-clears-to-null\n * semantics. Throws on a column the importer doesn't claim (the lockstep\n * test makes that unreachable for the editor's allowlist); the caller treats\n * a mirror failure as non-fatal — the hourly sync converges it.\n *\n * Deliberately NOT applicable to `Require Turnstile` or other non-text\n * columns: every editor field is plain text today, and the lockstep test\n * pins that assumption. */\nexport async function mirrorSiteField(\n db: Db,\n siteId: string,\n airtableColumn: string,\n value: string,\n): Promise<void> {\n const col = SITE_FIELDS[airtableColumn];\n if (!col)\n throw new Error(`mirrorSiteField: importer claims no sites column for '${airtableColumn}'`);\n const stored = value.trim() === \"\" ? null : value;\n await db\n .updateTable(\"sites\")\n .set({ [col]: stored })\n .where(\"id\", \"=\", siteId)\n .execute();\n}\n\n/** Write-through mirror for the nightly health writers (#539 Phase 3). Takes\n * the EXACT FieldSet just written to Airtable (updateAuditFields /\n * updateGitHubSignals return it), so the mirror can never carry a different\n * payload than the Airtable write it shadows. Resolution + coercion come from\n * the importer's healthColumnFor — one truth; an Airtable column no\n * site_health column claims throws (same contract as mirrorSiteField).\n * Partial by design: absent fields stay untouched, matching\n * updateGitHubSignals' deliberate omission of a null lastCommitAt. Returns\n * whether a site_health row matched: a site the hourly sync hasn't imported\n * yet updates 0 rows → false, so the caller can count it honestly\n * (mirror_missed) instead of claiming it mirrored — it still converges on the\n * next sync, like every mirror. An empty FieldSet runs no SQL and returns\n * true: nothing to mirror is not a miss. */\nexport async function mirrorHealthFields(\n db: Db,\n siteId: string,\n fields: Record<string, unknown>,\n): Promise<boolean> {\n // Per-column value types (number vs text) are guaranteed by healthColumnFor's\n // coercion — the numeric/text split lives there, once — so the patch builds\n // untyped and casts at the .set() boundary (the importer's own idiom).\n const patch: Record<string, string | number | null> = {};\n for (const [field, value] of Object.entries(fields)) {\n const m = healthColumnFor(field);\n if (!m)\n throw new Error(`mirrorHealthFields: importer claims no site_health column for '${field}'`);\n patch[m.col] = m.coerce(value);\n }\n if (Object.keys(patch).length === 0) return true;\n const res = await db\n .updateTable(\"site_health\")\n .set(patch as Updateable<SiteHealthTable>)\n .where(\"site_id\", \"=\", siteId)\n .executeTakeFirst();\n // kysely/libSQL reports numUpdatedRows as a BigInt — compare in BigInt.\n return res.numUpdatedRows > 0n;\n}\n\n/** The site_schedule twin of {@link mirrorHealthFields}, for the nightly\n * next-due write-back. `computedAt` stamps when THIS computation ran — the\n * hourly sync overwrites it with its own import stamp, same as every mirrored\n * value. Empty fields → full no-op (no lone computed_at stamp for a write\n * that carried nothing). Same return contract as mirrorHealthFields: false\n * when the UPDATE matched no site_schedule row (site not yet imported), true\n * otherwise — including the empty-fields no-op. */\nexport async function mirrorScheduleFields(\n db: Db,\n siteId: string,\n fields: Record<string, unknown>,\n computedAt: string,\n): Promise<boolean> {\n const patch: Record<string, string | null> = {};\n for (const [field, value] of Object.entries(fields)) {\n const m = scheduleColumnFor(field);\n if (!m)\n throw new Error(\n `mirrorScheduleFields: importer claims no site_schedule column for '${field}'`,\n );\n patch[m.col] = m.coerce(value);\n }\n if (Object.keys(patch).length === 0) return true;\n patch.computed_at = computedAt;\n const res = await db\n .updateTable(\"site_schedule\")\n .set(patch as Updateable<SiteScheduleTable>)\n .where(\"site_id\", \"=\", siteId)\n .executeTakeFirst();\n return res.numUpdatedRows > 0n;\n}\n\n/** Same contract as the Airtable getWebsiteBySlug: slug is siteSlug(Name),\n * precomputed into the UNIQUE sites.slug column at import time. */\nexport async function getSiteBySlug(db: Db, slug: string): Promise<WebsiteRow | null> {\n const r = await joined(db).where(\"sites.slug\", \"=\", slug).executeTakeFirst();\n return r ? rowFromJoined(r as JoinedRow) : null;\n}\n\n/** By Airtable rec id (the PK) — approve-report's lookup shape. */\nexport async function getSiteById(db: Db, id: string): Promise<WebsiteRow | null> {\n const r = await joined(db).where(\"sites.id\", \"=\", id).executeTakeFirst();\n return r ? rowFromJoined(r as JoinedRow) : null;\n}\n\n/** Same contract as the Airtable listWebsites: every site, one row each.\n * Name-ordered for determinism (Airtable returned table order; no consumer\n * is order-sensitive — the cockpit groups and sorts itself). */\nexport async function listSites(db: Db): Promise<WebsiteRow[]> {\n const rows = await joined(db).orderBy(\"sites.name\").execute();\n return rows.map((r) => rowFromJoined(r as JoinedRow));\n}\n\n// ————————————————————————— reports —————————————————————————\n\n/** stable checklist key → Airtable column name. The importer stores stable keys\n * (mapReportRecord); `ReportRow.checklist` exposes Airtable column names — one\n * derived map, built from the checklist definitions themselves. */\nconst CHECKLIST_FIELD_BY_KEY: ReadonlyMap<string, string> = new Map(\n [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map((i) => [i.key, i.field]),\n);\n\nfunction checklistFromStored(raw: string | null): Record<string, boolean> {\n let stored: Record<string, unknown> = {};\n if (typeof raw === \"string\" && raw.trim() !== \"\") {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed))\n stored = parsed as Record<string, unknown>;\n } catch {\n // fall through to all-false — a bad blob must not throw the page\n }\n }\n const out: Record<string, boolean> = {};\n for (const [key, field] of CHECKLIST_FIELD_BY_KEY) out[field] = Boolean(stored[key]);\n return out;\n}\n\nfunction reportRowFromDb(r: Selectable<ReportsTable>): ReportRow {\n const p = r.lighthouse_performance;\n const a = r.lighthouse_accessibility;\n const b = r.lighthouse_best_practices;\n const seo = r.lighthouse_seo;\n const lighthouse =\n typeof p === \"number\" &&\n typeof a === \"number\" &&\n typeof b === \"number\" &&\n typeof seo === \"number\"\n ? { performance: p, accessibility: a, bestPractices: b, seo }\n : null;\n return {\n id: r.id,\n reportId: r.report_id ?? \"\",\n siteId: r.site_id ?? \"\",\n reportType: toReportType(r.report_type ?? undefined),\n period: r.period,\n periodStart: r.period_start,\n periodEnd: r.period_end,\n completedOn: r.completed_on,\n lighthouse,\n gaUsersCurrent: r.ga_users_current,\n gaUsersPrevious: r.ga_users_previous,\n searchFoundPage1: bool(r.search_found_page1),\n searchPosition: r.search_position,\n lastTestedDate: r.last_tested_date,\n commentary: r.commentary,\n subjectOverride: r.subject_override,\n draftReady: r.draft_ready !== 0,\n approvedToSend: r.approved_to_send !== 0,\n sentAt: r.sent_at,\n approvedAt: r.approved_at,\n approvedBy: r.approved_by,\n deliveryStatus: (r.delivery_status ?? \"pending\") as DeliveryStatus,\n // The body lives IN the row (rendered_html) — the link points at the\n // dashboard's own preview route instead of an EXPIRING Airtable signed URL.\n // Strictly better for the operator: the old link 404'd once the URL aged out.\n renderedHtmlAttachment:\n r.rendered_html !== null\n ? { url: `/api/reports/${r.id}/preview`, filename: `${r.report_id ?? r.id}.html` }\n : null,\n resendMessageId: r.resend_message_id,\n checklist: checklistFromStored(r.checklist),\n autoEvidence: parseAutoEvidence(r.checklist_auto_evidence),\n sendOverride: r.send_override !== 0,\n overrideReason: r.override_reason,\n overrideBy: r.override_by,\n overrideAt: r.override_at,\n };\n}\n\n/** Same contract as the Airtable listAllReports. Newest-period-first (Airtable\n * returned manual table order; every consumer filters/sorts itself). */\nexport async function listAllReports(db: Db): Promise<ReportRow[]> {\n const rows = await db\n .selectFrom(\"reports\")\n .selectAll()\n .orderBy(\"period_start\", \"desc\")\n .orderBy(\"id\")\n .execute();\n return rows.map(reportRowFromDb);\n}\n\n/** Same contract as the Airtable listReportsForSite — served by idx_reports_site. */\nexport async function listReportsForSite(db: Db, siteId: string): Promise<ReportRow[]> {\n const rows = await db\n .selectFrom(\"reports\")\n .selectAll()\n .where(\"site_id\", \"=\", siteId)\n .orderBy(\"period_start\", \"desc\")\n .orderBy(\"id\")\n .execute();\n return rows.map(reportRowFromDb);\n}\n\n/** Columns the REQUEST-PATH report writers mirror after their Airtable write\n * (same pattern as mirrorSiteField): the approve/override flow and the\n * resend-webhook's delivery status. Send-path columns (sent_at,\n * resend_message_id, rendered_html) are deliberately absent — the send is a\n * CLI batch whose writes reach Turso via the hourly sync until Phase 3. */\nexport type ReportMirrorPatch = Partial<\n Pick<\n ReportsTable,\n | \"approved_to_send\"\n | \"approved_at\"\n | \"approved_by\"\n | \"send_override\"\n | \"override_reason\"\n | \"override_by\"\n | \"override_at\"\n | \"delivery_status\"\n >\n>;\n\n/** Mirror an Airtable report write into Turso so the page re-render after an\n * approve/override/bounce shows the new state immediately instead of after\n * the next hourly sync. Callers treat a failure as non-fatal (the sync\n * converges it); an empty patch is a no-op, never invalid SQL. */\nexport async function mirrorReportPatch(\n db: Db,\n reportId: string,\n patch: ReportMirrorPatch,\n): Promise<void> {\n if (Object.keys(patch).length === 0) return;\n await db.updateTable(\"reports\").set(patch).where(\"id\", \"=\", reportId).execute();\n}\n\n/** By rec id (the PK) — approve-report's read. */\nexport async function getReportById(db: Db, id: string): Promise<ReportRow | null> {\n const r = await db.selectFrom(\"reports\").selectAll().where(\"id\", \"=\", id).executeTakeFirst();\n return r ? reportRowFromDb(r) : null;\n}\n\n/** The preview route's read: the stored rendered body, or null when the report\n * has none (imported while its signed URL was expired, or predates rendering). */\nexport async function getReportHtml(\n db: Db,\n id: string,\n): Promise<{ html: string; reportId: string | null } | null> {\n const r = await db\n .selectFrom(\"reports\")\n .select([\"rendered_html\", \"report_id\"])\n .where(\"id\", \"=\", id)\n .executeTakeFirst();\n if (!r || r.rendered_html === null) return null;\n return { html: r.rendered_html, reportId: r.report_id };\n}\n","import { openDb, readDbConfig, type Db } from \"../db/client.js\";\nimport { mirrorHealthFields, mirrorScheduleFields } from \"../db/fleet-state.js\";\n\n/** One site's just-written Airtable FieldSet, mirrored into site_health.\n * Resolves true when a site_health row matched; false when the UPDATE touched\n * 0 rows (site created in Airtable after the last hourly import) — callers\n * count that as mirror_missed, never as mirrored. */\nexport type HealthMirror = (siteId: string, fields: Record<string, unknown>) => Promise<boolean>;\n\n/** The site_schedule twin, for the nightly next-due write-back. Same\n * matched-row contract as {@link HealthMirror}: false = 0-row UPDATE →\n * mirror_missed. */\nexport type ScheduleMirror = (\n siteId: string,\n fields: Record<string, unknown>,\n computedAt: string,\n) => Promise<boolean>;\n\n/** Build the Turso write-through for the nightly writers (#539 Phase 3\n * dual-write), or null when libSQL creds are absent — mirroring NOT attempted\n * is a different outcome than mirroring failed, and the caller's summary line\n * reports mirror counts only when a mirror existed. Same contract as\n * recordFleetEventsBestEffort: the mirror must never fail the sweep that\n * produced the data (per-site failures are the caller's to count; Airtable\n * stays authoritative and the hourly sync converges whatever the mirror\n * missed). `open` is injectable for tests. */\nexport async function makeHealthMirrorBestEffort(\n open: () => Promise<Db> = () => openDb(readDbConfig()),\n): Promise<HealthMirror | null> {\n let db: Db;\n try {\n db = await open();\n } catch (e) {\n console.error(`[health-mirror] mirroring disabled: no libSQL (${String(e)})`);\n return null;\n }\n return (siteId, fields) => mirrorHealthFields(db, siteId, fields);\n}\n\n/** {@link makeHealthMirrorBestEffort}'s schedule twin — same null-without-creds\n * contract, for `writeNextDueDates`' site_schedule write-through. */\nexport async function makeScheduleMirrorBestEffort(\n open: () => Promise<Db> = () => openDb(readDbConfig()),\n): Promise<ScheduleMirror | null> {\n let db: Db;\n try {\n db = await open();\n } catch (e) {\n console.error(`[schedule-mirror] mirroring disabled: no libSQL (${String(e)})`);\n return null;\n }\n return (siteId, fields, computedAt) => mirrorScheduleFields(db, siteId, fields, computedAt);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgQA,eAAsB,mBACpB,IACA,QACA,QACkB;AAIlB,QAAM,QAAgD,CAAC;AACvD,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACnD,UAAM,IAAI,gBAAgB,KAAK;AAC/B,QAAI,CAAC;AACH,YAAM,IAAI,MAAM,kEAAkE,KAAK,GAAG;AAC5F,UAAM,EAAE,GAAG,IAAI,EAAE,OAAO,KAAK;AAAA,EAC/B;AACA,MAAI,OAAO,KAAK,KAAK,EAAE,WAAW,EAAG,QAAO;AAC5C,QAAM,MAAM,MAAM,GACf,YAAY,aAAa,EACzB,IAAI,KAAoC,EACxC,MAAM,WAAW,KAAK,MAAM,EAC5B,iBAAiB;AAEpB,SAAO,IAAI,iBAAiB;AAC9B;AASA,eAAsB,qBACpB,IACA,QACA,QACA,YACkB;AAClB,QAAM,QAAuC,CAAC;AAC9C,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACnD,UAAM,IAAI,kBAAkB,KAAK;AACjC,QAAI,CAAC;AACH,YAAM,IAAI;AAAA,QACR,sEAAsE,KAAK;AAAA,MAC7E;AACF,UAAM,EAAE,GAAG,IAAI,EAAE,OAAO,KAAK;AAAA,EAC/B;AACA,MAAI,OAAO,KAAK,KAAK,EAAE,WAAW,EAAG,QAAO;AAC5C,QAAM,cAAc;AACpB,QAAM,MAAM,MAAM,GACf,YAAY,eAAe,EAC3B,IAAI,KAAsC,EAC1C,MAAM,WAAW,KAAK,MAAM,EAC5B,iBAAiB;AACpB,SAAO,IAAI,iBAAiB;AAC9B;AA4BA,IAAM,yBAAsD,IAAI;AAAA,EAC9D,CAAC,GAAG,uBAAuB,GAAG,iBAAiB,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC;AAC9E;;;AC3TA,eAAsB,2BACpB,OAA0B,MAAM,OAAO,aAAa,CAAC,GACvB;AAC9B,MAAI;AACJ,MAAI;AACF,SAAK,MAAM,KAAK;AAAA,EAClB,SAAS,GAAG;AACV,YAAQ,MAAM,kDAAkD,OAAO,CAAC,CAAC,GAAG;AAC5E,WAAO;AAAA,EACT;AACA,SAAO,CAAC,QAAQ,WAAW,mBAAmB,IAAI,QAAQ,MAAM;AAClE;AAIA,eAAsB,6BACpB,OAA0B,MAAM,OAAO,aAAa,CAAC,GACrB;AAChC,MAAI;AACJ,MAAI;AACF,SAAK,MAAM,KAAK;AAAA,EAClB,SAAS,GAAG;AACV,YAAQ,MAAM,oDAAoD,OAAO,CAAC,CAAC,GAAG;AAC9E,WAAO;AAAA,EACT;AACA,SAAO,CAAC,QAAQ,QAAQ,eAAe,qBAAqB,IAAI,QAAQ,QAAQ,UAAU;AAC5F;","names":[]}
@@ -0,0 +1,35 @@
1
+ import {
2
+ EXCLUDED_WEBSITE_FIELDS,
3
+ HEALTH_BOOLEAN,
4
+ HEALTH_FIELDS,
5
+ SCHEDULE_FIELDS,
6
+ SITE_FIELDS,
7
+ formatReapSummary,
8
+ healthColumnFor,
9
+ importFleetState,
10
+ mapReportRecord,
11
+ mapWebsiteRecord,
12
+ reapAllowance,
13
+ reapRefusal,
14
+ renderedHtmlUrl,
15
+ scheduleColumnFor
16
+ } from "./chunk-ITIOFN7A.js";
17
+ import "./chunk-VAD43MOR.js";
18
+ import "./chunk-LDWZSLSU.js";
19
+ export {
20
+ EXCLUDED_WEBSITE_FIELDS,
21
+ HEALTH_BOOLEAN,
22
+ HEALTH_FIELDS,
23
+ SCHEDULE_FIELDS,
24
+ SITE_FIELDS,
25
+ formatReapSummary,
26
+ healthColumnFor,
27
+ importFleetState,
28
+ mapReportRecord,
29
+ mapWebsiteRecord,
30
+ reapAllowance,
31
+ reapRefusal,
32
+ renderedHtmlUrl,
33
+ scheduleColumnFor
34
+ };
35
+ //# sourceMappingURL=import-airtable-HE43FB4H.js.map
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { a as AuditResult, S as Site, A as AuditName, b as RecipeResult, R as RecipeName, I as InventoryProvider } from './types-OIQZ5hWN.js';
2
2
  export { C as ConfigName } from './types-OIQZ5hWN.js';
3
- import * as airtable_lib_airtable_base_js from 'airtable/lib/airtable_base.js';
3
+ import { A as AirtableBase, a as FleetEventType } from './fleet-events-cF2Om88w.js';
4
4
  import { F as FormType } from './types-RXY-vY-5.js';
5
5
  export { SyncConfigsOptions, syncConfigs } from './recipes/sync-configs.js';
6
+ import 'airtable/lib/airtable_base.js';
6
7
 
7
8
  type SpawnResult = {
8
9
  code: number;
@@ -119,7 +120,15 @@ type FormSubmitOutcome = {
119
120
  formPresent: true;
120
121
  success: boolean;
121
122
  detail?: string;
123
+ /** Click → success banner VISIBLE. A user-experience number: it includes the
124
+ * browser's own render of the banner, which no server budget governs. */
122
125
  elapsedMs?: number;
126
+ /** Click → the action's POST response arrives. This is the span the site's
127
+ * `INGEST_TIMEOUT_MS` abort budget actually governs (it wraps the
128
+ * site→central fetch, which is nested inside this POST), so it — not
129
+ * `elapsedMs` — is what the budget check must compare against. Undefined
130
+ * when no POST was observed. */
131
+ postElapsedMs?: number;
123
132
  } | {
124
133
  testModeUndeclared: true;
125
134
  };
@@ -388,13 +397,6 @@ declare function localPath(path: string, opts?: LocalPathOptions): InventoryProv
388
397
 
389
398
  declare function fromJsonFile(path: string): InventoryProvider;
390
399
 
391
- type AirtableConfig = {
392
- apiKey: string;
393
- baseId: string;
394
- };
395
- type AirtableBase = ReturnType<typeof openBase>;
396
- declare function openBase(cfg: AirtableConfig): airtable_lib_airtable_base_js.AirtableBase;
397
-
398
400
  type AirtableInventoryOptions = {
399
401
  /**
400
402
  * Local workdir to compute each site's path as `{workdir}/{slug}`.
@@ -407,21 +409,105 @@ type AirtableInventoryOptions = {
407
409
  * Read sites from the Airtable Websites table as an InventoryProvider.
408
410
  * Each row becomes one Site; `path` is computed as `{workdir}/{slug}`.
409
411
  * Only LIVE `maintenance` sites that have a `url` are included — pre-launch
410
- * stages ("launch period" / "in development", via isPreLaunch) are excluded so a
412
+ * stages ("launching" / "building", via isPreLaunch) are excluded so a
411
413
  * not-yet-live site is never audited as production (its deploy/domain/uptime/CMS
412
414
  * audits would fail against nothing and red its Airtable row) nor swept by other
413
415
  * `--fleet airtable` ops; it re-enters the fleet when a Launch report flips its
414
- * Status to "maintenance". The production URL is exposed as `Site.deployedUrl` so
416
+ * Status to "maintained". The production URL is exposed as `Site.deployedUrl` so
415
417
  * the lighthouse audit can run against it with no checkout. `repoUrl` is
416
418
  * intentionally NOT set from `url` — a clone source must come from `gitRepo`
417
419
  * (`owner/repo`), never the production URL.
418
420
  */
419
421
  declare function fromAirtableBase(base: AirtableBase, opts?: AirtableInventoryOptions): InventoryProvider;
420
422
 
423
+ /**
424
+ * The site-status vocabulary — ONE module owns it (#539 Phase 4).
425
+ *
426
+ * The fleet's lifecycle names were inherited from an Airtable single-select
427
+ * written by hand years ago ("probably not our problem"). The operator approved
428
+ * a canonical vocabulary, and the migration is now COMPLETE in both stores:
429
+ * Airtable's "Status" single-select carries exactly the six canonical options
430
+ * and nothing else. This module no longer knows the old names at all.
431
+ *
432
+ * The retired mapping, kept as the record of what was merged into what:
433
+ *
434
+ * old (retired 2026-08-25) new (canonical)
435
+ * ───────────────────────── ───────────────
436
+ * in development building
437
+ * launch period launching
438
+ * maintenance maintained
439
+ * hosting hosted-only
440
+ * probably not our problem external
441
+ * legacy ┐
442
+ * deprecated ┴ archived ← approved MERGE, many-to-one
443
+ *
444
+ * `legacy` and `deprecated` were already treated identically by every PREDICATE
445
+ * (ARCHIVED_STATUSES held both), so collapsing them changes no *selection* — no
446
+ * fleet op gains or loses a site. It is NOT true that collapsing them changes
447
+ * nothing at all, and one surface proved it: the cockpit's Archived lane LABELS
448
+ * each row with its Status cell, and canonicalizing there rendered all 12 live
449
+ * archived rows as "archived". That lane now labels from `statusRaw`, which is
450
+ * exactly why `WebsiteRow.statusRaw` exists. The rule this leaves behind:
451
+ * canonical values decide BEHAVIOUR; anything DISPLAYING a Status cell, or
452
+ * writing one back, uses the raw cell.
453
+ *
454
+ * The merge had NO clean reverse map while both vocabularies coexisted, which
455
+ * is why `toAirtableStatus` had to pick one archived name deliberately. With the
456
+ * old names retired there is nothing left to pick between, and no code path
457
+ * feeds operator-supplied text through it anyway (see
458
+ * `src/recipes/forms-notify-target.ts`, which writes `--restore` verbatim).
459
+ *
460
+ * THE SHAPE OF THE TRANSITION (three stages):
461
+ *
462
+ * stage 1 (DONE) code speaks the NEW vocabulary internally; reads canonicalize
463
+ * at the two Airtable/Turso seams; writes still emit the OLD
464
+ * Airtable option names. Airtable untouched. No SELECTION
465
+ * change — every predicate picks exactly the sites it picked
466
+ * before (pinned row-by-row in tests/reports/airtable/
467
+ * site-status.test.ts). Displayed labels come from `statusRaw`.
468
+ * stage 2 (DONE) the Airtable "Status" single-select gained the 6 new options
469
+ * and all 44 cells were migrated (verified live: maintained=13,
470
+ * archived=12, external=9, building=6, hosted-only=2,
471
+ * launching=2 — zero old values, zero unrecognized), `db sync`
472
+ * ran clean (mismatches=0), and AIRTABLE_USES_NEW_VOCABULARY
473
+ * flipped to true. Writers now emit canonical names verbatim.
474
+ * The alias map STAYS: the 7 old options still exist in the
475
+ * field, so a human can still pick one in the Airtable UI.
476
+ * stage 3 (DONE) the operator deleted the 7 old options from the Airtable
477
+ * "Status" field on 2026-08-25, which is what made this safe:
478
+ * the gate was always "no old value CAN BE ENTERED", not
479
+ * merely "none is stored". Verified against the live base
480
+ * before any code changed — the field carries exactly the six
481
+ * canonical choices, byte-exact, and all 44 cells still hold a
482
+ * canonical value with ZERO blanks (deleting a single-select
483
+ * option CLEARS the cells using it, so "no cell was blanked"
484
+ * was checked, not assumed). AIRTABLE_STATUS_ALIASES,
485
+ * AIRTABLE_OLD_NAMES and AIRTABLE_USES_NEW_VOCABULARY are
486
+ * gone; both functions reduce to the identity.
487
+ *
488
+ * WHAT CHANGED IN BEHAVIOUR AT STAGE 3, and it is the only thing that did: an
489
+ * old name is no longer translated. `canonicalizeStatus("maintenance")` used to
490
+ * yield `maintained`; it now yields `"maintenance"` verbatim, which
491
+ * `isUnrecognizedStatus` flags and the cockpit surfaces as a watch row. That is
492
+ * intended. The option cannot be selected any more, so a stored old value would
493
+ * mean something went wrong — a restored backup, a scripted write, an API
494
+ * caller with a stale constant — and the fleet should say so rather than absorb
495
+ * it into a status nobody chose.
496
+ *
497
+ * Canonicalization happens on READ, never at rest: `src/db/import-airtable.ts`
498
+ * still stores the raw Airtable cell verbatim in `sites.status`, so the hourly
499
+ * parity check keeps comparing raw-to-raw and the importer stays honest to its
500
+ * source. Both readers — `mapRow` (Airtable) and `rowFromJoined` (Turso) — run
501
+ * the raw value through `canonicalizeStatus`, which is what keeps the #558
502
+ * reader-equivalence instrument green.
503
+ */
504
+ /** The canonical site lifecycle vocabulary. */
505
+ type Status = "building" | "launching" | "maintained" | "hosted-only" | "external" | "archived";
506
+
421
507
  type Frequency = "None" | "Monthly" | "Quarterly" | "Yearly";
422
- type Status = "in development" | "launch period" | "maintenance" | "hosting" | "probably not our problem" | "deprecated" | "legacy";
508
+
423
509
  /**
424
- * Per-site notification routing. When present on a `maintenance` site, the form
510
+ * Per-site notification routing. When present on a `maintained` site, the form
425
511
  * notification is addressed by the value of a submission field (`field`, read from
426
512
  * `extraFields`) — e.g. route a contact form's `interest` to a different recipient
427
513
  * per option, always CC-ing a shared address. Absent (`null`) → the site keeps the
@@ -443,7 +529,17 @@ type WebsiteRow = {
443
529
  id: string;
444
530
  name: string;
445
531
  url: string;
532
+ /** Canonical lifecycle status (see ./site-status.ts). Canonicalized at the read
533
+ * boundary, so an old-vocabulary Airtable cell and a new one read identically.
534
+ * A present-but-unrecognized cell survives VERBATIM (blind-cast) rather than
535
+ * becoming null — `isUnrecognizedStatus` flags it, and null would make
536
+ * due.ts/preflight.ts treat the row as eligible-by-default. */
446
537
  status: Status | null;
538
+ /** The literal Airtable Status cell behind `status` — same pattern as
539
+ * `maintenanceFreqRaw`. The dashboard status editor round-trips THIS, so its
540
+ * dropdown offers and preselects the values Airtable actually holds while the
541
+ * two vocabularies coexist. Null = blank cell. */
542
+ statusRaw: string | null;
447
543
  pointOfContact: string | null;
448
544
  maintenanceFreq: Frequency;
449
545
  testingFreq: Frequency;
@@ -670,6 +766,18 @@ type WebsiteRow = {
670
766
  */
671
767
  prismicAckUntil: string | null;
672
768
  notifyRouting: NotifyRouting | null;
769
+ /** The RAW `Notify Routing` cell, verbatim. Same reason `statusRaw` exists: the
770
+ * dashboard editor round-trips this JSON, and re-serializing the PARSED object
771
+ * would drop any key the parser ignores and reformat what the operator typed —
772
+ * a silent rewrite of a cell they only opened to look at. */
773
+ notifyRoutingRaw: string | null;
774
+ /** Read-back of the code-owned next-due dates (date-only, written by the
775
+ * nightly `writeNextDueDates`). These exist so the writer can DIFF the
776
+ * freshly-computed dates against what the row already holds and skip the
777
+ * ~31 no-op writes a night the fleet was paying for (#539 Phase 3). Null =
778
+ * cell blank or the operator-added `Next … at` column absent. */
779
+ nextMaintenanceAt: string | null;
780
+ nextTestingAt: string | null;
673
781
  };
674
782
  /** The `Prismic Models` cell — see {@link WebsiteRow.prismicModels} for why it
675
783
  * carries a third state that no other verdict column in this table has. */
@@ -884,7 +992,9 @@ type RefreshHeaderDeps = {
884
992
  url: string;
885
993
  slug?: string;
886
994
  }) => Promise<GeneratedHeaderImage>;
887
- upload?: (recordId: string, field: string, bytes: Uint8Array, filename: string, contentType: string) => Promise<void>;
995
+ upload?: (recordId: string, field: string, bytes: Uint8Array, filename: string, contentType: string, opts?: {
996
+ replaceIn?: string;
997
+ }) => Promise<void>;
888
998
  };
889
999
  type DraftOptions = {
890
1000
  /** Where to write the local preview HTML when `previewOnly`. Defaults to `reports/<slug>/draft.html`. */
@@ -1145,8 +1255,6 @@ type AttentionItem = {
1145
1255
  autoFixExhausted?: boolean;
1146
1256
  };
1147
1257
 
1148
- type FleetEventType = "pr_automerged" | "vuln_cleared" | "ci_recovered" | "site_launched" | "fleet_swept" | "cert_renewed" | "report_sent_with_override";
1149
-
1150
1258
  type Tier = "attention" | "watch" | "healthy" | "pre-launch";
1151
1259
  type SiteCard = {
1152
1260
  site: WebsiteRow;
@@ -1191,7 +1299,7 @@ type CockpitSummary = {
1191
1299
  attention: number;
1192
1300
  watch: number;
1193
1301
  healthy: number;
1194
- /** Count of pre-live "launch period" sites still muted as expected pre-launch
1302
+ /** Count of pre-live "launching" sites still muted as expected pre-launch
1195
1303
  * conditions. A piercing alarm (piercesPreLaunchMute) re-tiers such a site to
1196
1304
  * attention, so it counts there instead — muted never means "hidden if broken". */
1197
1305
  preLaunch: number;
@@ -1221,7 +1329,15 @@ type RecentEntry = {
1221
1329
  ts: string;
1222
1330
  };
1223
1331
  /** A Websites row surfaced OUTSIDE the fleet cards: archived (legacy/deprecated)
1224
- * or holding an unrecognized Status cell. `status` is the raw cell value. */
1332
+ * or holding an unrecognized Status cell.
1333
+ *
1334
+ * `status` is the RAW Airtable cell, not the canonical status (#539 Phase 4).
1335
+ * These two lanes exist to mirror what Airtable holds so a row can never
1336
+ * silently vanish, and both of them survive on the distinction the canonical
1337
+ * vocabulary erases: `legacy` and `deprecated` both canonicalize to `archived`,
1338
+ * so labelling the lane canonically would render 12 live rows identically and
1339
+ * disagree with the cell the operator is looking at. Everything else about
1340
+ * these rows — which lane they land in — is decided canonically. */
1225
1341
  type OffFleetSiteEntry = {
1226
1342
  name: string;
1227
1343
  slug: string;
@@ -1268,7 +1384,7 @@ type SiteAlarmContext = {
1268
1384
  * fetches data, then hands it here. Easier to unit-test, easier to render
1269
1385
  * a static preview from CLI later.
1270
1386
  */
1271
- declare function renderSiteDashboardHtml(site: WebsiteRow, reports: ReportRow[], submissions?: SubmissionRow[], spamTotals?: ScreenOutTotals | null, now?: Date, alarm?: SiteAlarmContext | null): string;
1387
+ declare function renderSiteDashboardHtml(site: WebsiteRow, reports: ReportRow[], submissions?: SubmissionRow[], spamTotals?: ScreenOutTotals | null, now?: Date, alarm?: SiteAlarmContext | null, operatorEmail?: string | null): string;
1272
1388
 
1273
1389
  /**
1274
1390
  * Render the fleet cockpit as a single HTML document. Pure function: no Airtable
@@ -1277,7 +1393,7 @@ declare function renderSiteDashboardHtml(site: WebsiteRow, reports: ReportRow[],
1277
1393
  * and hands it here. Renders the doc shell + verdict bar + the per-site Needs-you
1278
1394
  * feed + the collapsed Fleet browse panel (one flat, filterable card grid).
1279
1395
  */
1280
- declare function renderCockpitHtml(model: CockpitModel): string;
1396
+ declare function renderCockpitHtml(model: CockpitModel, operatorEmail?: string | null): string;
1281
1397
 
1282
1398
  /**
1283
1399
  * Verify an `Authorization: Basic <base64>` header against the configured
@@ -1300,7 +1416,45 @@ declare function renderCockpitHtml(model: CockpitModel): string;
1300
1416
  */
1301
1417
  declare function verifyBasicAuth(authHeader: string | null | undefined, expectedPassword: string | null): boolean;
1302
1418
 
1303
- /** Read the bundled 2400x3200 plate. */
1419
+ /** Per-report-type headline overlays, stamped over the CLEAN plate at send time
1420
+ * (see orchestrate.ts). Types with no entry go out on the clean plate.
1421
+ *
1422
+ * Source: Figma "Web-Maintenance-Email" (mQ3hy2d9JnOG9ljCzbZS8j) —
1423
+ * Maintenance is node 158:15, Testing is 153:721 ("maintenance & testing",
1424
+ * which is what a Testing report actually covers). Export each at 4x; Figma
1425
+ * crops a text export to its ink box, which is why these are 1328x660 /
1426
+ * 1715x664 rather than the 1840x720 text box.
1427
+ *
1428
+ * ⚠️ A Figma MCP `download_assets` export arrives FLATTENED — never
1429
+ * transparent — and it flattens onto WHATEVER SITS BEHIND THE NODE, which is
1430
+ * not one fixed colour: Maintenance/Testing live inside a white Header frame
1431
+ * and come back on white, while Announcement/Launch sit on the bare canvas and
1432
+ * come back on Figma's canvas grey (30,30,30). Stamping either unprocessed
1433
+ * paints a solid slab over the paper texture — that, not a bad design file, is
1434
+ * what made the 2026-08-20 Testing asset unusable.
1435
+ *
1436
+ * Recover the alpha instead of re-exporting by hand: for ink C over backdrop B,
1437
+ * a = (px - B) / (C - B). DETECT B (the export's most common colour) rather
1438
+ * than assuming white, and pick the channel with the largest |C - B| — green
1439
+ * separates the brand red from white by 221 levels but from canvas grey by only
1440
+ * 4, which would quantise the alpha to noise. Verified against the known-good
1441
+ * Maintenance asset at mean abs alpha diff 0.022, 0.0037% of pixels off by
1442
+ * more than 8.
1443
+ *
1444
+ * Editing this copy needs Figma desktop, not the Plugin API: `characters`
1445
+ * requires loadFontAsync and the MCP font environment carries no Helvetica at
1446
+ * all, so the licensed Helvetica Neue LT Std can never load there. */
1447
+ declare const HEADLINE_FILES: {
1448
+ readonly Maintenance: "headline-maintenance.png";
1449
+ readonly Testing: "headline-testing.png";
1450
+ readonly Announcement: "headline-announcement.png";
1451
+ readonly Launch: "headline-launch.png";
1452
+ };
1453
+ type HeadlineKind = keyof typeof HEADLINE_FILES;
1454
+ /** Read the bundled 2400x3200 CLEAN plate (no headline — the report type's
1455
+ * headline is stamped at send time, so one stored image serves every type). */
1304
1456
  declare function loadPlate(): Promise<Uint8Array>;
1457
+ /** Read a headline overlay (transparent PNG, ink-box cropped). */
1458
+ declare function loadHeadline(kind: HeadlineKind): Promise<Uint8Array>;
1305
1459
 
1306
- export { ALL_AUDIT_NAMES, ALL_RECIPE_NAMES, type AirtableInventoryOptions, AuditName, AuditResult, BLURRED_CID, type BumpDepsOptions, type BundledImage, CHECK_CID, type ConvertToPnpmOptions, DEFAULT_INIT_STEPS, type DraftOptions, type DraftResult, type DueItem, type GenerateInput, type GeneratedHeaderImage, type HeaderImage, type InitOptions, type InitResult, type InitStep, type InitStepResult, InventoryProvider, type LighthouseScores, type LocalPathOptions, type OnboardAudit, type OnboardOptions, type OrchestrateOptions, RecipeName, RecipeResult, type RefreshHeaderDeps, type RenderResult, type ReportData, type ReportType, Site, type UpgradeSvelte4to5Options, a11yAudit, a11yFixturesPage, bumpDeps, convertToPnpm, depsAudit, domainFromUrl, draftReportForSite, findDueReports, fromAirtableBase, fromJsonFile, generateHeaderImage, init, isRecipeName, lighthouseAudit, lintAudit, loadBundledImages, loadPlate, localPath, onboard, renderCockpitHtml, renderReportHtml, renderSiteDashboardHtml, runAudits, runAuditsAcross, securityAudit, selfCaretRange, selfPackageVersion, sendApprovedReports, svelteCodemods, upgradeSvelte4to5, verifyBasicAuth };
1460
+ export { ALL_AUDIT_NAMES, ALL_RECIPE_NAMES, type AirtableInventoryOptions, AuditName, AuditResult, BLURRED_CID, type BumpDepsOptions, type BundledImage, CHECK_CID, type ConvertToPnpmOptions, DEFAULT_INIT_STEPS, type DraftOptions, type DraftResult, type DueItem, type GenerateInput, type GeneratedHeaderImage, type HeaderImage, type InitOptions, type InitResult, type InitStep, type InitStepResult, InventoryProvider, type LighthouseScores, type LocalPathOptions, type OnboardAudit, type OnboardOptions, type OrchestrateOptions, RecipeName, RecipeResult, type RefreshHeaderDeps, type RenderResult, type ReportData, type ReportType, Site, type UpgradeSvelte4to5Options, a11yAudit, a11yFixturesPage, bumpDeps, convertToPnpm, depsAudit, domainFromUrl, draftReportForSite, findDueReports, fromAirtableBase, fromJsonFile, generateHeaderImage, init, isRecipeName, lighthouseAudit, lintAudit, loadBundledImages, loadHeadline, loadPlate, localPath, onboard, renderCockpitHtml, renderReportHtml, renderSiteDashboardHtml, runAudits, runAuditsAcross, securityAudit, selfCaretRange, selfPackageVersion, sendApprovedReports, svelteCodemods, upgradeSvelte4to5, verifyBasicAuth };