@reddoorla/maintenance 0.96.0 → 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 +5 -6
- package/dist/{announce-45HVP4J4.js → announce-5ZD2SBGD.js} +39 -31
- package/dist/announce-5ZD2SBGD.js.map +1 -0
- package/dist/attachments-JFGFQRXQ.js +10 -0
- package/dist/{blux-RUFMNOUJ.js → blux-NXQLMS4W.js} +5 -5
- package/dist/{bump-deps-EL3TKMIL.js → bump-deps-775VLDYB.js} +6 -6
- package/dist/{chunk-IMYL67JV.js → chunk-3RLDX3Q2.js} +5 -5
- package/dist/{chunk-CA6ADGOC.js → chunk-6PGXKN6F.js} +2 -2
- package/dist/{chunk-Y5BVR4HW.js → chunk-6THEZQLA.js} +3 -3
- package/dist/chunk-ASLDCWBF.js +41 -0
- package/dist/chunk-ASLDCWBF.js.map +1 -0
- package/dist/{chunk-DVJNYPFR.js → chunk-AYDQQBNG.js} +2 -2
- package/dist/{chunk-GEUC5VGM.js → chunk-BINFZQ2T.js} +49 -4
- package/dist/chunk-BINFZQ2T.js.map +1 -0
- package/dist/{chunk-72FJZ6LO.js → chunk-DRNZVORX.js} +26 -22
- package/dist/chunk-DRNZVORX.js.map +1 -0
- package/dist/{chunk-SHGIDTYF.js → chunk-DYTYPKFN.js} +2 -2
- package/dist/{chunk-NVLREB5R.js → chunk-EN6CTBUN.js} +7 -7
- package/dist/{chunk-ZH5LOQET.js → chunk-FHFXJ3EM.js} +7 -5
- package/dist/chunk-FHFXJ3EM.js.map +1 -0
- package/dist/{chunk-H445EH33.js → chunk-FP6HOW6J.js} +10 -7
- package/dist/chunk-FP6HOW6J.js.map +1 -0
- package/dist/{chunk-RRE74VWI.js → chunk-HSMN35LU.js} +2 -2
- package/dist/chunk-ICSRWW5Q.js +148 -0
- package/dist/chunk-ICSRWW5Q.js.map +1 -0
- package/dist/{chunk-XG2A6GIG.js → chunk-IDBPII6G.js} +4 -4
- package/dist/{chunk-XG2A6GIG.js.map → chunk-IDBPII6G.js.map} +1 -1
- package/dist/{chunk-T76MA2FH.js → chunk-IOTOOTII.js} +12 -1
- package/dist/{chunk-T76MA2FH.js.map → chunk-IOTOOTII.js.map} +1 -1
- package/dist/{chunk-ECDJDHKT.js → chunk-JHJNNJLG.js} +2 -2
- package/dist/{chunk-XY7YV2YQ.js → chunk-L3S3QZXL.js} +2 -2
- package/dist/{chunk-XED2DNAI.js → chunk-LAG6IP7Y.js} +7 -7
- package/dist/{chunk-P4A6D6HO.js → chunk-LDHRIQUW.js} +32 -18
- package/dist/chunk-LDHRIQUW.js.map +1 -0
- package/dist/{chunk-UIITN37J.js → chunk-LF2TJN4X.js} +2 -2
- package/dist/chunk-LF2TJN4X.js.map +1 -0
- package/dist/{chunk-SQ4FSNYL.js → chunk-LUF52EIY.js} +2 -2
- package/dist/{chunk-MZZDMBDT.js → chunk-LZMAG73U.js} +19 -19
- package/dist/chunk-LZMAG73U.js.map +1 -0
- package/dist/{chunk-6INOBNRO.js → chunk-MMSMZC7S.js} +2 -2
- package/dist/{chunk-CGQZFQAY.js → chunk-MVKNU4QD.js} +2 -3
- package/dist/chunk-MVKNU4QD.js.map +1 -0
- package/dist/{chunk-62YWOFVN.js → chunk-PARKXDXI.js} +3 -3
- package/dist/{chunk-62YWOFVN.js.map → chunk-PARKXDXI.js.map} +1 -1
- package/dist/{chunk-44SDGPGQ.js → chunk-PZZ6HM5T.js} +3 -3
- package/dist/chunk-RQJFGK3A.js +51 -0
- package/dist/chunk-RQJFGK3A.js.map +1 -0
- package/dist/{chunk-KN52PM63.js → chunk-TC4GANMX.js} +47 -149
- 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-4UTJ4G6P.js → chunk-TJLKWQOX.js} +2 -2
- package/dist/{chunk-VEIIUCYL.js → chunk-TV76A6GN.js} +2 -2
- package/dist/{chunk-D3CTOCUR.js → chunk-TVZ6QCOE.js} +3 -3
- package/dist/{chunk-3EA43S3R.js → chunk-U2ZI6G7H.js} +12 -12
- package/dist/chunk-UPE6D4AT.js +69 -0
- package/dist/chunk-UPE6D4AT.js.map +1 -0
- package/dist/{chunk-OTEVKRAH.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-EASYCD6F.js → chunk-WMF5S2MB.js} +1 -1
- package/dist/{chunk-EASYCD6F.js.map → chunk-WMF5S2MB.js.map} +1 -1
- package/dist/{chunk-37B4LVB3.js → chunk-X56LAKPR.js} +3 -3
- package/dist/chunk-X56LAKPR.js.map +1 -0
- package/dist/{chunk-IY6256GK.js → chunk-YJBSB4LP.js} +6 -1
- package/dist/chunk-YJBSB4LP.js.map +1 -0
- package/dist/{chunk-EQQ4H6L3.js → chunk-YXWBM26P.js} +2 -2
- package/dist/cli/bin.js +92 -50
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +33 -3
- package/dist/cli/commands/audit.js +25 -22
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-DGTO65WB.js +11 -0
- package/dist/{convert-to-pnpm-4DON7IFS.js → convert-to-pnpm-KLWA53N5.js} +6 -6
- package/dist/{db-5K4CDEGJ.js → db-II5WAK4D.js} +13 -13
- package/dist/{digest-5AJGWS33.js → digest-CWKK2DMR.js} +28 -27
- package/dist/digest-CWKK2DMR.js.map +1 -0
- package/dist/{digest-collectors-MC5NWEUA.js → digest-collectors-CYTN2EUX.js} +15 -12
- 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-XESZHZEB.js → ensure-site-OO4UPWNQ.js} +19 -42
- package/dist/ensure-site-OO4UPWNQ.js.map +1 -0
- package/dist/fleet-events-CkOZpMVC.d.ts +435 -0
- package/dist/{fleet-state-GJWGKTLD.js → fleet-state-RJROYXKO.js} +17 -7
- package/dist/{forms-notify-target-4NPLX2UG.js → forms-notify-target-X6UEOI2M.js} +17 -13
- package/dist/forms-notify-target-X6UEOI2M.js.map +1 -0
- package/dist/{github-signals-DMU74SAX.js → github-signals-QIRTHHET.js} +20 -15
- package/dist/github-signals-QIRTHHET.js.map +1 -0
- package/dist/{header-image-BRTOLHJF.js → header-image-H7ZJAV26.js} +13 -17
- package/dist/header-image-H7ZJAV26.js.map +1 -0
- package/dist/{health-endpoint-UCLMSSCN.js → health-endpoint-BJ6BKRHF.js} +7 -7
- package/dist/{health-mirror-ONRT4DQ5.js → health-mirror-KU3G7V2Y.js} +10 -10
- package/dist/{import-airtable-IIK2QUUF.js → import-airtable-UBF5GYAL.js} +3 -3
- package/dist/index.d.ts +102 -494
- package/dist/index.js +60 -49
- package/dist/index.js.map +1 -1
- package/dist/{init-3LMZTCVD.js → init-GOWDA5QS.js} +15 -15
- package/dist/{launch-XTDMC6LD.js → launch-7N3X2MXB.js} +36 -34
- package/dist/launch-7N3X2MXB.js.map +1 -0
- package/dist/lighthouse-airtable-M45OPVRV.js +14 -0
- package/dist/{chunk-IHHAYXKY.js → match-harness-GBCDSVPS.js} +70 -4
- package/dist/match-harness-GBCDSVPS.js.map +1 -0
- package/dist/migrate-XZ7U5DFC.js +7 -0
- package/dist/notify-K3WVH3DM.js +1 -1
- package/dist/{onboard-CZYBWVWH.js → onboard-JQQIMQW7.js} +6 -6
- package/dist/{orchestrate-TGDFGY52.js → orchestrate-L2A5BZFY.js} +13 -10
- package/dist/parity-2JGVUCZD.js +14 -0
- package/dist/{preflight-POZZI3GN.js → preflight-O7Q6FWHU.js} +35 -11
- package/dist/preflight-O7Q6FWHU.js.map +1 -0
- package/dist/{prismic-ci-AHY5ULJR.js → prismic-ci-NAPWOXXD.js} +11 -11
- package/dist/prismic-ci-NAPWOXXD.js.map +1 -0
- package/dist/{prismic-models-URYGODNY.js → prismic-models-LL6ORTQ2.js} +19 -16
- package/dist/prismic-models-LL6ORTQ2.js.map +1 -0
- package/dist/{prospect-audit-AJEE3Z4L.js → prospect-audit-FMPGQIRQ.js} +2 -2
- package/dist/{protection-audit-3J3UEOVT.js → protection-audit-YXJKFU6O.js} +3 -3
- package/dist/{protection-audit-3J3UEOVT.js.map → protection-audit-YXJKFU6O.js.map} +1 -1
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/{render-from-row-65DC6ZP3.js → render-from-row-75JTED2Z.js} +6 -4
- package/dist/{renovate-dispatch-ONJMBUHJ.js → renovate-dispatch-2IHAZDVA.js} +13 -8
- package/dist/{renovate-dispatch-ONJMBUHJ.js.map → renovate-dispatch-2IHAZDVA.js.map} +1 -1
- package/dist/{report-RC5LGPU3.js → report-IAHXN5CW.js} +102 -58
- package/dist/report-IAHXN5CW.js.map +1 -0
- package/dist/{report-mirror-KKZZ6RMZ.js → report-mirror-4QM57LMS.js} +28 -10
- 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/{self-updating-RKKIYNV4.js → self-updating-ZUWPGYUP.js} +7 -7
- package/dist/{selftest-AFLR7YV7.js → selftest-G4W3LIBQ.js} +21 -17
- package/dist/{selftest-AFLR7YV7.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-mirror-H2FYMN55.js → site-mirror-WR7ACXOV.js} +10 -10
- package/dist/site-mirror-WR7ACXOV.js.map +1 -0
- package/dist/{smoke-suite-BWHR3XX5.js → smoke-suite-WAUPI6EJ.js} +7 -7
- package/dist/{submissions-II5J35QM.js → submissions-2SHCFOKD.js} +11 -6
- package/dist/submissions-2SHCFOKD.js.map +1 -0
- package/dist/{svelte-codemods-QWGIRNN6.js → svelte-codemods-VOLOKKFI.js} +6 -6
- package/dist/{sync-NKPDCMYW.js → sync-F4USL4K5.js} +6 -6
- package/dist/{sync-configs-HGLQV55H.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-WZ44IARG.js → upgrade-EVAL3JTO.js} +6 -6
- package/dist/util/git.d.ts +1 -1
- package/dist/util/git.js +1 -1
- package/dist/{websites-6DHIHJ2D.js → websites-Y6FN2XFD.js} +26 -21
- package/dist/{write-audits-to-airtable-BIK6LNVC.js → write-audits-to-airtable-TI3JMWSY.js} +6 -4
- package/package.json +1 -1
- package/dist/airtable-SJV4ZSMS.js +0 -9
- package/dist/announce-45HVP4J4.js.map +0 -1
- package/dist/attachments-NLZIJ5NU.js +0 -9
- package/dist/chunk-37B4LVB3.js.map +0 -1
- package/dist/chunk-72FJZ6LO.js.map +0 -1
- package/dist/chunk-74QRWE6G.js +0 -53
- package/dist/chunk-74QRWE6G.js.map +0 -1
- package/dist/chunk-CGQZFQAY.js.map +0 -1
- package/dist/chunk-F3JWOPTG.js.map +0 -1
- package/dist/chunk-GEUC5VGM.js.map +0 -1
- package/dist/chunk-H445EH33.js.map +0 -1
- package/dist/chunk-IHHAYXKY.js.map +0 -1
- package/dist/chunk-IY6256GK.js.map +0 -1
- package/dist/chunk-KN52PM63.js.map +0 -1
- package/dist/chunk-MZZDMBDT.js.map +0 -1
- package/dist/chunk-OTEVKRAH.js.map +0 -1
- package/dist/chunk-P4A6D6HO.js.map +0 -1
- package/dist/chunk-UIITN37J.js.map +0 -1
- package/dist/chunk-YFAXOD7Z.js +0 -42
- package/dist/chunk-YFAXOD7Z.js.map +0 -1
- package/dist/chunk-ZH5LOQET.js.map +0 -1
- package/dist/client-L7NAUG6J.js +0 -11
- package/dist/digest-5AJGWS33.js.map +0 -1
- package/dist/ensure-site-XESZHZEB.js.map +0 -1
- package/dist/fleet-events-cF2Om88w.d.ts +0 -24
- package/dist/forms-notify-target-4NPLX2UG.js.map +0 -1
- package/dist/github-signals-DMU74SAX.js.map +0 -1
- package/dist/header-image-BRTOLHJF.js.map +0 -1
- package/dist/launch-XTDMC6LD.js.map +0 -1
- package/dist/lighthouse-airtable-3NOFB42B.js +0 -12
- package/dist/match-harness-OW45CB5H.js +0 -77
- package/dist/match-harness-OW45CB5H.js.map +0 -1
- package/dist/migrate-NE3DUMTM.js +0 -7
- package/dist/parity-AEKYSLN6.js +0 -14
- package/dist/preflight-POZZI3GN.js.map +0 -1
- package/dist/prismic-ci-AHY5ULJR.js.map +0 -1
- package/dist/prismic-models-URYGODNY.js.map +0 -1
- package/dist/report-RC5LGPU3.js.map +0 -1
- package/dist/report-mirror-KKZZ6RMZ.js.map +0 -1
- package/dist/site-mirror-H2FYMN55.js.map +0 -1
- package/dist/submissions-II5J35QM.js.map +0 -1
- package/dist/write-audits-to-airtable-BIK6LNVC.js.map +0 -1
- /package/dist/{airtable-SJV4ZSMS.js.map → attachments-JFGFQRXQ.js.map} +0 -0
- /package/dist/{blux-RUFMNOUJ.js.map → blux-NXQLMS4W.js.map} +0 -0
- /package/dist/{bump-deps-EL3TKMIL.js.map → bump-deps-775VLDYB.js.map} +0 -0
- /package/dist/{chunk-IMYL67JV.js.map → chunk-3RLDX3Q2.js.map} +0 -0
- /package/dist/{chunk-CA6ADGOC.js.map → chunk-6PGXKN6F.js.map} +0 -0
- /package/dist/{chunk-Y5BVR4HW.js.map → chunk-6THEZQLA.js.map} +0 -0
- /package/dist/{chunk-DVJNYPFR.js.map → chunk-AYDQQBNG.js.map} +0 -0
- /package/dist/{chunk-SHGIDTYF.js.map → chunk-DYTYPKFN.js.map} +0 -0
- /package/dist/{chunk-NVLREB5R.js.map → chunk-EN6CTBUN.js.map} +0 -0
- /package/dist/{chunk-RRE74VWI.js.map → chunk-HSMN35LU.js.map} +0 -0
- /package/dist/{chunk-ECDJDHKT.js.map → chunk-JHJNNJLG.js.map} +0 -0
- /package/dist/{chunk-XY7YV2YQ.js.map → chunk-L3S3QZXL.js.map} +0 -0
- /package/dist/{chunk-XED2DNAI.js.map → chunk-LAG6IP7Y.js.map} +0 -0
- /package/dist/{chunk-SQ4FSNYL.js.map → chunk-LUF52EIY.js.map} +0 -0
- /package/dist/{chunk-6INOBNRO.js.map → chunk-MMSMZC7S.js.map} +0 -0
- /package/dist/{chunk-44SDGPGQ.js.map → chunk-PZZ6HM5T.js.map} +0 -0
- /package/dist/{chunk-4UTJ4G6P.js.map → chunk-TJLKWQOX.js.map} +0 -0
- /package/dist/{chunk-VEIIUCYL.js.map → chunk-TV76A6GN.js.map} +0 -0
- /package/dist/{chunk-D3CTOCUR.js.map → chunk-TVZ6QCOE.js.map} +0 -0
- /package/dist/{chunk-3EA43S3R.js.map → chunk-U2ZI6G7H.js.map} +0 -0
- /package/dist/{chunk-EQQ4H6L3.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-4DON7IFS.js.map → convert-to-pnpm-KLWA53N5.js.map} +0 -0
- /package/dist/{db-5K4CDEGJ.js.map → db-II5WAK4D.js.map} +0 -0
- /package/dist/{client-L7NAUG6J.js.map → digest-collectors-CYTN2EUX.js.map} +0 -0
- /package/dist/{digest-collectors-MC5NWEUA.js.map → fleet-state-RJROYXKO.js.map} +0 -0
- /package/dist/{health-endpoint-UCLMSSCN.js.map → health-endpoint-BJ6BKRHF.js.map} +0 -0
- /package/dist/{health-mirror-ONRT4DQ5.js.map → health-mirror-KU3G7V2Y.js.map} +0 -0
- /package/dist/{fleet-state-GJWGKTLD.js.map → import-airtable-UBF5GYAL.js.map} +0 -0
- /package/dist/{init-3LMZTCVD.js.map → init-GOWDA5QS.js.map} +0 -0
- /package/dist/{import-airtable-IIK2QUUF.js.map → lighthouse-airtable-M45OPVRV.js.map} +0 -0
- /package/dist/{lighthouse-airtable-3NOFB42B.js.map → migrate-XZ7U5DFC.js.map} +0 -0
- /package/dist/{onboard-CZYBWVWH.js.map → onboard-JQQIMQW7.js.map} +0 -0
- /package/dist/{migrate-NE3DUMTM.js.map → orchestrate-L2A5BZFY.js.map} +0 -0
- /package/dist/{orchestrate-TGDFGY52.js.map → parity-2JGVUCZD.js.map} +0 -0
- /package/dist/{prospect-audit-AJEE3Z4L.js.map → prospect-audit-FMPGQIRQ.js.map} +0 -0
- /package/dist/{parity-AEKYSLN6.js.map → render-from-row-75JTED2Z.js.map} +0 -0
- /package/dist/{self-updating-RKKIYNV4.js.map → self-updating-ZUWPGYUP.js.map} +0 -0
- /package/dist/{smoke-suite-BWHR3XX5.js.map → smoke-suite-WAUPI6EJ.js.map} +0 -0
- /package/dist/{svelte-codemods-QWGIRNN6.js.map → svelte-codemods-VOLOKKFI.js.map} +0 -0
- /package/dist/{sync-NKPDCMYW.js.map → sync-F4USL4K5.js.map} +0 -0
- /package/dist/{sync-configs-HGLQV55H.js.map → sync-configs-OXSWKVZ2.js.map} +0 -0
- /package/dist/{upgrade-WZ44IARG.js.map → upgrade-EVAL3JTO.js.map} +0 -0
- /package/dist/{render-from-row-65DC6ZP3.js.map → websites-Y6FN2XFD.js.map} +0 -0
- /package/dist/{websites-6DHIHJ2D.js.map → write-audits-to-airtable-TI3JMWSY.js.map} +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSiteBySlug,
|
|
3
|
+
insertSiteRows,
|
|
4
|
+
updateSiteIdentity
|
|
5
|
+
} from "./chunk-BINFZQ2T.js";
|
|
6
|
+
import "./chunk-LF2TJN4X.js";
|
|
7
|
+
import "./chunk-ASLDCWBF.js";
|
|
8
|
+
import "./chunk-VAD43MOR.js";
|
|
9
|
+
import "./chunk-ICSRWW5Q.js";
|
|
10
|
+
|
|
11
|
+
// src/db/site-create.ts
|
|
12
|
+
var SLUG_TAKEN = /UNIQUE constraint failed: sites\.slug/;
|
|
13
|
+
function makeSiteStore(db, now = () => /* @__PURE__ */ new Date()) {
|
|
14
|
+
return {
|
|
15
|
+
findBySlug: async (slug) => {
|
|
16
|
+
const row = await getSiteBySlug(db, slug);
|
|
17
|
+
if (!row) return null;
|
|
18
|
+
return {
|
|
19
|
+
id: row.id,
|
|
20
|
+
name: row.name,
|
|
21
|
+
// `rowFromJoined` maps a null url to "" — undo that, so "blank" means one thing.
|
|
22
|
+
url: row.url === "" ? null : row.url,
|
|
23
|
+
pointOfContact: row.pointOfContact,
|
|
24
|
+
gitRepo: row.gitRepo
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
create: async (site) => {
|
|
28
|
+
try {
|
|
29
|
+
await db.transaction().execute((trx) => insertSiteRows(trx, site, now().toISOString()));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32
|
+
const causeMessage = err instanceof Error && err.cause instanceof Error ? err.cause.message : "";
|
|
33
|
+
if (SLUG_TAKEN.test(message) || SLUG_TAKEN.test(causeMessage)) {
|
|
34
|
+
throw Object.assign(new Error(message, { cause: err }), { code: "SLUG_TAKEN" });
|
|
35
|
+
}
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
updateIdentity: (siteId, patch) => updateSiteIdentity(db, siteId, patch)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
makeSiteStore
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=site-create-ZU3ACBTO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/site-create.ts"],"sourcesContent":["/** #646 step 3: the Turso half of the Turso-native `ensure-site`.\n *\n * Turso is the source of truth for a new site, so this is NOT a mirror: nothing\n * shadows it and nothing converges it. It adapts `fleet-state`'s gated queries\n * to the `SiteStore` the creator (`src/fleet/ensure-site.ts`) drives, and adds\n * the one thing those queries cannot express on their own — the transaction.\n *\n * It lives in its own module, exempt from the EXPLAIN gate, for a concrete\n * reason: the gate runs every scenario against ONE shared `:memory:` libSQL\n * client, and `client.transaction()` on an in-memory client hands its connection\n * to the transaction and opens a fresh, EMPTY database for the next statement\n * (verified 2026-09-17: the statement after a committed transaction failed with\n * `no such table`). Every SQL statement this module issues is `insertSiteRows` /\n * `updateSiteIdentity` / `getSiteBySlug`, which the gate plans directly.\n *\n * The same hazard means any test driving `create` must use a `file:` database.\n */\nimport type { Db } from \"./client.js\";\nimport { getSiteBySlug, insertSiteRows, updateSiteIdentity } from \"./fleet-state.js\";\nimport type { SiteStore } from \"../fleet/ensure-site.js\";\n\n/** SQLite's message for the `sites.slug` UNIQUE index, as libSQL surfaces it. */\nconst SLUG_TAKEN = /UNIQUE constraint failed: sites\\.slug/;\n\nexport function makeSiteStore(db: Db, now: () => Date = () => new Date()): SiteStore {\n return {\n findBySlug: async (slug) => {\n const row = await getSiteBySlug(db, slug);\n if (!row) return null;\n return {\n id: row.id,\n name: row.name,\n // `rowFromJoined` maps a null url to \"\" — undo that, so \"blank\" means one thing.\n url: row.url === \"\" ? null : row.url,\n pointOfContact: row.pointOfContact,\n gitRepo: row.gitRepo,\n };\n },\n create: async (site) => {\n try {\n await db.transaction().execute((trx) => insertSiteRows(trx, site, now().toISOString()));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const causeMessage =\n err instanceof Error && err.cause instanceof Error ? err.cause.message : \"\";\n if (SLUG_TAKEN.test(message) || SLUG_TAKEN.test(causeMessage)) {\n throw Object.assign(new Error(message, { cause: err }), { code: \"SLUG_TAKEN\" });\n }\n throw err;\n }\n },\n updateIdentity: (siteId, patch) => updateSiteIdentity(db, siteId, patch),\n };\n}\n"],"mappings":";;;;;;;;;;;AAsBA,IAAM,aAAa;AAEZ,SAAS,cAAc,IAAQ,MAAkB,MAAM,oBAAI,KAAK,GAAc;AACnF,SAAO;AAAA,IACL,YAAY,OAAO,SAAS;AAC1B,YAAM,MAAM,MAAM,cAAc,IAAI,IAAI;AACxC,UAAI,CAAC,IAAK,QAAO;AACjB,aAAO;AAAA,QACL,IAAI,IAAI;AAAA,QACR,MAAM,IAAI;AAAA;AAAA,QAEV,KAAK,IAAI,QAAQ,KAAK,OAAO,IAAI;AAAA,QACjC,gBAAgB,IAAI;AAAA,QACpB,SAAS,IAAI;AAAA,MACf;AAAA,IACF;AAAA,IACA,QAAQ,OAAO,SAAS;AACtB,UAAI;AACF,cAAM,GAAG,YAAY,EAAE,QAAQ,CAAC,QAAQ,eAAe,KAAK,MAAM,IAAI,EAAE,YAAY,CAAC,CAAC;AAAA,MACxF,SAAS,KAAK;AACZ,cAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,cAAM,eACJ,eAAe,SAAS,IAAI,iBAAiB,QAAQ,IAAI,MAAM,UAAU;AAC3E,YAAI,WAAW,KAAK,OAAO,KAAK,WAAW,KAAK,YAAY,GAAG;AAC7D,gBAAM,OAAO,OAAO,IAAI,MAAM,SAAS,EAAE,OAAO,IAAI,CAAC,GAAG,EAAE,MAAM,aAAa,CAAC;AAAA,QAChF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,gBAAgB,CAAC,QAAQ,UAAU,mBAAmB,IAAI,QAAQ,KAAK;AAAA,EACzE;AACF;","names":[]}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TURSO_IS_AUTHORITATIVE
|
|
3
|
+
} from "./chunk-WQLNUEZL.js";
|
|
1
4
|
import {
|
|
2
5
|
mirrorHealthFields,
|
|
3
6
|
mirrorSiteFields,
|
|
4
7
|
mirrorSiteInsert,
|
|
5
8
|
siteRowExists
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import
|
|
9
|
-
TURSO_IS_AUTHORITATIVE
|
|
10
|
-
} from "./chunk-WQLNUEZL.js";
|
|
11
|
-
import "./chunk-F3JWOPTG.js";
|
|
9
|
+
} from "./chunk-BINFZQ2T.js";
|
|
10
|
+
import "./chunk-LF2TJN4X.js";
|
|
11
|
+
import "./chunk-ASLDCWBF.js";
|
|
12
12
|
import "./chunk-VAD43MOR.js";
|
|
13
|
-
import "./chunk-
|
|
13
|
+
import "./chunk-ICSRWW5Q.js";
|
|
14
14
|
import {
|
|
15
15
|
openDb,
|
|
16
16
|
readDbConfig
|
|
17
|
-
} from "./chunk-
|
|
18
|
-
import "./chunk-
|
|
17
|
+
} from "./chunk-MMSMZC7S.js";
|
|
18
|
+
import "./chunk-IOTOOTII.js";
|
|
19
19
|
import "./chunk-6VAL7XJT.js";
|
|
20
20
|
|
|
21
21
|
// src/db/site-mirror.ts
|
|
@@ -73,4 +73,4 @@ async function makeSiteMirror(open = () => openDb(readDbConfig()), strict = TURS
|
|
|
73
73
|
export {
|
|
74
74
|
makeSiteMirror
|
|
75
75
|
};
|
|
76
|
-
//# sourceMappingURL=site-mirror-
|
|
76
|
+
//# sourceMappingURL=site-mirror-WR7ACXOV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/site-mirror.ts"],"sourcesContent":["/** #539 Phase 5: the Turso write-through for the ONE-OFF Websites writers.\n *\n * Phase 3 mirrored the nightly sweep — the audit write-back, github-signals and\n * the next-due dates. It did not touch the writers that run on their own\n * schedule or on demand: the analytics soft-fail stamp, the Renovate\n * auto-fix counter, the prismic-models verdict, `updateLaunched`, the\n * forms notify target, and the SINGLE-SITE audit write-back (only the fleet\n * path ever passed a mirror). Each of those reached Turso solely via the hourly\n * sync, which stops existing at the freeze.\n *\n * It lives in `src/db` rather than a feature folder because its callers span\n * audits, recipes, CLI commands and the send path — no one feature owns it.\n *\n * Deliberately UNLIKE `makeHealthMirrorBestEffort`, this never returns null.\n * #585 is the reason: that factory returned null without creds and the\n * dual-write silently no-opped in production for weeks, because a dead mirror\n * and a healthy one produced identical output — the only tell was an ABSENT log\n * suffix nobody was watching for. Here creds-absent is a state the mirror\n * REPORTS, so a missing SITE_MIRROR line means the wiring is gone.\n */\nimport { openDb, readDbConfig, type Db } from \"./client.js\";\nimport {\n mirrorHealthFields,\n mirrorSiteFields,\n mirrorSiteInsert,\n siteRowExists,\n} from \"./fleet-state.js\";\nimport { TURSO_IS_AUTHORITATIVE } from \"./freeze.js\";\n\n/** Two ops because the Websites row is split across two Turso tables. Callers\n * pass the EXACT FieldSet the Airtable writer returned, so the mirror can never\n * carry a different payload than the write it shadows. */\nexport type SiteMirror = {\n /** A row Airtable just CREATED, as Airtable echoed it back. `ensure-site` used\n * this until #646 step 3; it now creates sites in Turso directly\n * (`src/fleet/ensure-site.ts`), and neither this nor `hasRow` has a caller left\n * outside tests — both go with the Airtable layer in step 6.\n * Every other op is an UPDATE, which does nothing for a row that does not\n * exist yet — so without this a bootstrapped site is invisible to Turso and\n * every mirror the rest of the bootstrap fires reports `mirrored=missed`. */\n created: (rec: { id: string; fields: Record<string, unknown> }) => Promise<void>;\n /** Columns that live in `site_health`. */\n health: (siteId: string, fields: Record<string, unknown>) => Promise<void>;\n /** Columns that live in `sites`. */\n site: (siteId: string, fields: Record<string, unknown>) => Promise<void>;\n /** #645. Does Turso hold a row for this site id? A pure READ, so it is the one\n * op here that neither logs a SITE_MIRROR line nor throws under the freeze —\n * `ensure-site` uses it to decide whether to heal, and a probe that threw\n * would sink the very command that repairs the gap. Creds-absent answers\n * `true` (\"assume present\"), so a mirror that cannot read can never provoke\n * a blind insert. */\n hasRow: (siteId: string) => Promise<boolean>;\n};\n\n/** Build the one-off writers' mirror. Never throws and never returns null:\n * Airtable is still authoritative through Phase 5, so a mirror problem must not\n * cost the write it shadows — the hourly sync converges whatever this misses.\n * `open` is injectable for tests. */\nexport async function makeSiteMirror(\n open: () => Promise<Db> = () => openDb(readDbConfig()),\n /** #612. `true` = Turso is the store that must succeed, so every failure\n * throws instead of being logged and swallowed. Defaulted from the shipped\n * constant and injected by tests, so both sides stay proven. */\n strict: boolean = TURSO_IS_AUTHORITATIVE,\n): Promise<SiteMirror> {\n let db: Db | null = null;\n let why = \"\";\n try {\n db = await open();\n } catch (e) {\n why = (e as Error).message;\n }\n // Frozen: refuse to hand back a mirror that cannot write. Failing at\n // CONSTRUCTION rather than per write matters — a caller holding a\n // working-looking mirror would run its whole batch before anyone noticed that\n // nothing had persisted.\n if (strict && !db) throw new Error(`SITE_MIRROR unavailable: ${why}`);\n\n const run = async (siteId: string, op: string, work: (db: Db) => Promise<boolean>) => {\n if (!db) {\n console.log(`SITE_MIRROR site=${siteId} op=${op} mirrored=absent reason=${why}`);\n return;\n }\n // Every path below logs EXACTLY ONE line, then strict adds a throw. Logging\n // first matters: a frozen run that fails should still leave the same trace\n // in the run log that an unfrozen one would, not just a stack.\n let matched: boolean;\n try {\n matched = await work(db);\n } catch (e) {\n console.log(`SITE_MIRROR site=${siteId} op=${op} mirrored=0 error=${(e as Error).message}`);\n if (strict) throw e;\n return;\n }\n // `missed` is its own outcome: the UPDATE matched no row. Before the freeze\n // that means the hourly sync has not imported this site yet — a transient,\n // and reporting it as mirrored=1 would claim a write that never landed.\n // After the freeze no importer exists, so an absent row stays absent: it is\n // a bug, not a wait.\n console.log(`SITE_MIRROR site=${siteId} op=${op} mirrored=${matched ? \"1\" : \"missed\"}`);\n if (strict && !matched) {\n throw new Error(`SITE_MIRROR site=${siteId} op=${op}: no such row in Turso`);\n }\n };\n\n return {\n // An INSERT always lands, so it reports 1 rather than routing a row count\n // through `missed` — there was nothing to match in the first place.\n created: (rec) =>\n run(rec.id, \"created\", async (d) => {\n await mirrorSiteInsert(d, rec, new Date().toISOString());\n return true;\n }),\n health: (siteId, fields) => run(siteId, \"health\", (d) => mirrorHealthFields(d, siteId, fields)),\n site: (siteId, fields) => run(siteId, \"site\", (d) => mirrorSiteFields(d, siteId, fields)),\n // Deliberately NOT routed through `run`: that helper logs a mirrored= line\n // and, under strict, throws on anything but a clean write — both wrong for a\n // read whose whole job is to answer a question. A failed probe answers\n // \"present\" so the caller heals nothing it cannot see.\n hasRow: async (siteId) => {\n if (!db) return true;\n try {\n return await siteRowExists(db, siteId);\n } catch (e) {\n console.log(`SITE_MIRROR site=${siteId} op=hasRow unknown error=${(e as Error).message}`);\n return true;\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA0DA,eAAsB,eACpB,OAA0B,MAAM,OAAO,aAAa,CAAC,GAIrD,SAAkB,wBACG;AACrB,MAAI,KAAgB;AACpB,MAAI,MAAM;AACV,MAAI;AACF,SAAK,MAAM,KAAK;AAAA,EAClB,SAAS,GAAG;AACV,UAAO,EAAY;AAAA,EACrB;AAKA,MAAI,UAAU,CAAC,GAAI,OAAM,IAAI,MAAM,4BAA4B,GAAG,EAAE;AAEpE,QAAM,MAAM,OAAO,QAAgB,IAAY,SAAuC;AACpF,QAAI,CAAC,IAAI;AACP,cAAQ,IAAI,oBAAoB,MAAM,OAAO,EAAE,2BAA2B,GAAG,EAAE;AAC/E;AAAA,IACF;AAIA,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,KAAK,EAAE;AAAA,IACzB,SAAS,GAAG;AACV,cAAQ,IAAI,oBAAoB,MAAM,OAAO,EAAE,qBAAsB,EAAY,OAAO,EAAE;AAC1F,UAAI,OAAQ,OAAM;AAClB;AAAA,IACF;AAMA,YAAQ,IAAI,oBAAoB,MAAM,OAAO,EAAE,aAAa,UAAU,MAAM,QAAQ,EAAE;AACtF,QAAI,UAAU,CAAC,SAAS;AACtB,YAAM,IAAI,MAAM,oBAAoB,MAAM,OAAO,EAAE,wBAAwB;AAAA,IAC7E;AAAA,EACF;AAEA,SAAO;AAAA;AAAA;AAAA,IAGL,SAAS,CAAC,QACR,IAAI,IAAI,IAAI,WAAW,OAAO,MAAM;AAClC,YAAM,iBAAiB,GAAG,MAAK,oBAAI,KAAK,GAAE,YAAY,CAAC;AACvD,aAAO;AAAA,IACT,CAAC;AAAA,IACH,QAAQ,CAAC,QAAQ,WAAW,IAAI,QAAQ,UAAU,CAAC,MAAM,mBAAmB,GAAG,QAAQ,MAAM,CAAC;AAAA,IAC9F,MAAM,CAAC,QAAQ,WAAW,IAAI,QAAQ,QAAQ,CAAC,MAAM,iBAAiB,GAAG,QAAQ,MAAM,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,IAKxF,QAAQ,OAAO,WAAW;AACxB,UAAI,CAAC,GAAI,QAAO;AAChB,UAAI;AACF,eAAO,MAAM,cAAc,IAAI,MAAM;AAAA,MACvC,SAAS,GAAG;AACV,gBAAQ,IAAI,oBAAoB,MAAM,4BAA6B,EAAY,OAAO,EAAE;AACxF,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -5,21 +5,21 @@ import {
|
|
|
5
5
|
appendSkipNotice,
|
|
6
6
|
fleetWorkdir,
|
|
7
7
|
prepareFleetSites
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-DYTYPKFN.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveSites
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FP6HOW6J.js";
|
|
12
12
|
import {
|
|
13
13
|
smokeSuite
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-EN6CTBUN.js";
|
|
15
|
+
import "./chunk-TJLKWQOX.js";
|
|
15
16
|
import "./chunk-5CJVUPJU.js";
|
|
16
|
-
import "./chunk-
|
|
17
|
-
import "./chunk-SQ4FSNYL.js";
|
|
17
|
+
import "./chunk-LUF52EIY.js";
|
|
18
18
|
import "./chunk-Z3JXLULD.js";
|
|
19
19
|
import "./chunk-LBYOLBW7.js";
|
|
20
20
|
import "./chunk-RARREDQE.js";
|
|
21
21
|
import "./chunk-XXTZBPUY.js";
|
|
22
|
-
import "./chunk-
|
|
22
|
+
import "./chunk-WMF5S2MB.js";
|
|
23
23
|
|
|
24
24
|
// src/cli/commands/smoke-suite.ts
|
|
25
25
|
import { resolve } from "path";
|
|
@@ -52,4 +52,4 @@ async function runSmokeSuiteCommand(site, opts) {
|
|
|
52
52
|
export {
|
|
53
53
|
runSmokeSuiteCommand
|
|
54
54
|
};
|
|
55
|
-
//# sourceMappingURL=smoke-suite-
|
|
55
|
+
//# sourceMappingURL=smoke-suite-WAUPI6EJ.js.map
|
|
@@ -40,10 +40,15 @@ async function rescoreNewSubmissions(db, opts) {
|
|
|
40
40
|
}
|
|
41
41
|
async function loadSiteNames() {
|
|
42
42
|
try {
|
|
43
|
-
const {
|
|
44
|
-
const {
|
|
45
|
-
const
|
|
46
|
-
|
|
43
|
+
const { openDb, readDbConfig } = await import("./client-DGTO65WB.js");
|
|
44
|
+
const { listSites } = await import("./fleet-state-RJROYXKO.js");
|
|
45
|
+
const db = await openDb(readDbConfig());
|
|
46
|
+
try {
|
|
47
|
+
const sites = await listSites(db);
|
|
48
|
+
return new Map(sites.map((s) => [s.id, s.name]));
|
|
49
|
+
} finally {
|
|
50
|
+
await db.destroy();
|
|
51
|
+
}
|
|
47
52
|
} catch (err) {
|
|
48
53
|
console.error(`[submissions] site names unavailable \u2014 printing raw site ids: ${String(err)}`);
|
|
49
54
|
return /* @__PURE__ */ new Map();
|
|
@@ -78,7 +83,7 @@ async function runSubmissionsCommand(action, opts) {
|
|
|
78
83
|
return { output: "--apply and --dry-run conflict. Pick one.", code: 2 };
|
|
79
84
|
}
|
|
80
85
|
const apply = opts.apply === true;
|
|
81
|
-
const { openDb, readDbConfig } = await import("./client-
|
|
86
|
+
const { openDb, readDbConfig } = await import("./client-DGTO65WB.js");
|
|
82
87
|
const { SPAM_THRESHOLD } = await import("./spam-classifier-T5H4VTP6.js");
|
|
83
88
|
const cfg = opts.url ? { url: opts.url } : readDbConfig();
|
|
84
89
|
const db = await openDb(cfg);
|
|
@@ -119,4 +124,4 @@ export {
|
|
|
119
124
|
rescoreNewSubmissions,
|
|
120
125
|
runSubmissionsCommand
|
|
121
126
|
};
|
|
122
|
-
//# sourceMappingURL=submissions-
|
|
127
|
+
//# sourceMappingURL=submissions-2SHCFOKD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/commands/submissions.ts"],"sourcesContent":["import type { Db } from \"../../db/client.js\";\nimport type { SubmissionRow } from \"../../reports/submission-row.js\";\n\nexport type SubmissionsCommandOptions = {\n /** Write the re-buckets. Without it the command is a DRY RUN (deliberate default —\n * this rewrites operator-visible triage state, so the table prints first). */\n apply?: boolean;\n /** Explicit dry run (the default). Conflicts with --apply. */\n dryRun?: boolean;\n /** Override the libSQL url (tests use \":memory:\"); otherwise read from env. */\n url?: string;\n cwd?: string;\n verbose?: boolean;\n};\n\n/** Provenance marker appended to the NEW classifier reasons on every re-bucketed row,\n * so a retro'd row is always distinguishable from one the classifier caught at ingest. */\nexport const RESCORE_REASON_MARKER = \"retro-rescore\";\n\nexport type RescoreFlagged = {\n row: SubmissionRow;\n newScore: number;\n /** What --apply writes to spam_reason: the new verdict's reasons + RESCORE_REASON_MARKER. */\n newReason: string;\n /** True when --apply actually flipped the row (false in dry runs, or when the row\n * stopped being 'new' between the scan and the write). */\n written: boolean;\n};\n\n/** extraFields is stored as a raw JSON string; the classifier wants the object. A\n * null/garbage/non-object value degrades to {} — same blind-spot posture as ingest\n * (classification never throws on stored data). */\nfunction parseExtraFields(raw: string | null): Record<string, unknown> {\n if (!raw || raw.trim() === \"\") return {};\n try {\n const parsed: unknown = JSON.parse(raw);\n return parsed !== null && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\n/**\n * Core of `submissions rescore`, separated from the command wrapper so tests can run it\n * against a seeded :memory: db. Scans EVERY status='new' row (the pre-2026-07-15-tuning\n * backlog these rows predate), re-runs the CURRENT classifySpam over each — turnstile\n * \"unverifiable\", exactly what ingest passes when no token reaches verification — and\n * flags rows at/above SPAM_THRESHOLD. With `apply` it re-buckets each flagged row to\n * spam_auto, REPLACING spam_score/spam_reason with the new verdict (+ marker). The\n * classifier is the single source of truth: no scoring logic is duplicated here.\n */\nexport async function rescoreNewSubmissions(\n db: Db,\n opts: { apply: boolean },\n): Promise<{ scanned: number; flagged: RescoreFlagged[] }> {\n const { listSubmissionsFiltered, rescoreSubmissionSpam } =\n await import(\"../../db/submissions.js\");\n const { classifySpam, SPAM_THRESHOLD } = await import(\"../../forms/spam-classifier.js\");\n\n // Collect the WHOLE backlog before any write: --apply mutates rows out of the\n // status='new' filter, so paging offset-wise while writing would skip rows.\n const rows: SubmissionRow[] = [];\n const PAGE = 500;\n for (let offset = 0; ; offset += PAGE) {\n const page = await listSubmissionsFiltered(db, { status: \"new\" }, { limit: PAGE, offset });\n rows.push(...page);\n if (page.length < PAGE) break;\n }\n\n const flagged: RescoreFlagged[] = [];\n for (const row of rows) {\n const verdict = classifySpam({\n name: row.name,\n email: row.email,\n // message is optional on the classifier input; exactOptionalPropertyTypes\n // forbids passing `undefined`, so spread only when present (form-ingest idiom).\n ...(row.message !== null ? { message: row.message } : {}),\n formType: row.formType,\n extraFields: parseExtraFields(row.extraFields),\n turnstile: \"unverifiable\",\n });\n if (verdict.score < SPAM_THRESHOLD) continue;\n const newReason = [...verdict.reasons, RESCORE_REASON_MARKER].join(\",\");\n const written = opts.apply\n ? await rescoreSubmissionSpam(db, row.id, verdict.score, newReason)\n : false;\n flagged.push({ row, newScore: verdict.score, newReason, written });\n }\n return { scanned: rows.length, flagged };\n}\n\n/** site_id → display name for the table. Reads the roster from Turso (#646 step 4),\n * which is also where the submissions themselves live — an Airtable read here could\n * not name a `site_<ULID>` site at all. Soft-fails to an empty map (raw ids still\n * print) so the re-score itself never depends on the name lookup. */\nasync function loadSiteNames(): Promise<Map<string, string>> {\n try {\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { listSites } = await import(\"../../db/fleet-state.js\");\n const db = await openDb(readDbConfig());\n try {\n const sites = await listSites(db);\n return new Map(sites.map((s) => [s.id, s.name] as const));\n } finally {\n await db.destroy();\n }\n } catch (err) {\n console.error(`[submissions] site names unavailable — printing raw site ids: ${String(err)}`);\n return new Map();\n }\n}\n\nfunction pad(v: string, width: number): string {\n return v.length >= width ? v : v + \" \".repeat(width - v.length);\n}\n\nfunction formatTable(flagged: RescoreFlagged[], names: Map<string, string>): string {\n const rows = flagged.map((f) => ({\n site: names.get(f.row.siteId) ?? f.row.siteId,\n email: f.row.email || \"(no email)\",\n score: `${f.row.spamScore ?? \"—\"} → ${f.newScore}`,\n reasons: f.newReason,\n }));\n const siteW = Math.max(4, ...rows.map((r) => r.site.length));\n const emailW = Math.max(5, ...rows.map((r) => r.email.length));\n const scoreW = Math.max(5, ...rows.map((r) => r.score.length));\n const lines = [\n `${pad(\"site\", siteW)} ${pad(\"email\", emailW)} ${pad(\"score\", scoreW)} reasons`,\n ...rows.map(\n (r) =>\n `${pad(r.site, siteW)} ${pad(r.email, emailW)} ${pad(r.score, scoreW)} ${r.reasons}`,\n ),\n ];\n return lines.join(\"\\n\");\n}\n\n/**\n * `submissions <action>` — operate on stored form submissions. The only action today is\n * `rescore`: re-run the CURRENT spam classifier over the status='new' backlog. Dry-run\n * by default; `--apply` writes. Only status='new' rows are ever touched.\n */\nexport async function runSubmissionsCommand(\n action: string,\n opts: SubmissionsCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (action !== \"rescore\") {\n return { output: `unknown submissions action '${action}'. Use: rescore.`, code: 1 };\n }\n if (opts.apply && opts.dryRun) {\n return { output: \"--apply and --dry-run conflict. Pick one.\", code: 2 };\n }\n const apply = opts.apply === true;\n\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { SPAM_THRESHOLD } = await import(\"../../forms/spam-classifier.js\");\n const cfg = opts.url ? { url: opts.url } : readDbConfig();\n const db = await openDb(cfg);\n\n const { scanned, flagged } = await rescoreNewSubmissions(db, { apply });\n if (flagged.length === 0) {\n return {\n output: `Scanned ${scanned} status='new' submission${scanned === 1 ? \"\" : \"s\"} — none score >= ${SPAM_THRESHOLD} under the current classifier. Nothing to re-bucket.`,\n code: 0,\n };\n }\n\n // Airtable is only consulted when there is a table to print.\n const table = formatTable(flagged, await loadSiteNames());\n const head = `Scanned ${scanned} status='new' submissions; ${flagged.length} score >= ${SPAM_THRESHOLD} under the current classifier:`;\n if (!apply) {\n return {\n output: `${head}\\n\\n${table}\\n\\nDRY RUN — nothing was written. Re-run with --apply to re-bucket these ${flagged.length} rows to spam_auto.`,\n code: 0,\n };\n }\n const written = flagged.filter((f) => f.written).length;\n const skipped = flagged.length - written;\n const skippedNote =\n skipped > 0\n ? `\\n${skipped} row${skipped === 1 ? \"\" : \"s\"} skipped — no longer status='new' at write time (operator/ingest got there first).`\n : \"\";\n return {\n output: `${head}\\n\\n${table}\\n\\nApplied: ${written} row${written === 1 ? \"\" : \"s\"} → spam_auto (new score + reasons written, '${RESCORE_REASON_MARKER}' marker appended).${skippedNote}`,\n code: 0,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -5,20 +5,20 @@ import {
|
|
|
5
5
|
appendSkipNotice,
|
|
6
6
|
fleetWorkdir,
|
|
7
7
|
prepareFleetSites
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-DYTYPKFN.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveSites
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FP6HOW6J.js";
|
|
12
12
|
import {
|
|
13
13
|
svelteCodemods
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-HSMN35LU.js";
|
|
15
15
|
import "./chunk-J7SZQUCW.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-LUF52EIY.js";
|
|
17
17
|
import "./chunk-Z3JXLULD.js";
|
|
18
18
|
import "./chunk-LBYOLBW7.js";
|
|
19
19
|
import "./chunk-RARREDQE.js";
|
|
20
20
|
import "./chunk-XXTZBPUY.js";
|
|
21
|
-
import "./chunk-
|
|
21
|
+
import "./chunk-WMF5S2MB.js";
|
|
22
22
|
|
|
23
23
|
// src/cli/commands/svelte-codemods.ts
|
|
24
24
|
import { resolve } from "path";
|
|
@@ -51,4 +51,4 @@ async function runSvelteCodemodsCommand(site, opts) {
|
|
|
51
51
|
export {
|
|
52
52
|
runSvelteCodemodsCommand
|
|
53
53
|
};
|
|
54
|
-
//# sourceMappingURL=svelte-codemods-
|
|
54
|
+
//# sourceMappingURL=svelte-codemods-VOLOKKFI.js.map
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
checkFleetParity,
|
|
3
3
|
formatParityResult
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-TVZ6QCOE.js";
|
|
5
|
+
import "./chunk-BINFZQ2T.js";
|
|
6
6
|
import {
|
|
7
7
|
formatReapSummary,
|
|
8
8
|
importFleetState
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-LF2TJN4X.js";
|
|
10
|
+
import "./chunk-ASLDCWBF.js";
|
|
11
11
|
import "./chunk-VAD43MOR.js";
|
|
12
|
-
import "./chunk-
|
|
12
|
+
import "./chunk-ICSRWW5Q.js";
|
|
13
13
|
|
|
14
14
|
// src/db/sync.ts
|
|
15
15
|
async function syncFleetState(db, io) {
|
|
@@ -43,4 +43,4 @@ export {
|
|
|
43
43
|
formatSyncResult,
|
|
44
44
|
syncFleetState
|
|
45
45
|
};
|
|
46
|
-
//# sourceMappingURL=sync-
|
|
46
|
+
//# sourceMappingURL=sync-F4USL4K5.js.map
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
appendSkipNotice,
|
|
6
6
|
fleetWorkdir,
|
|
7
7
|
prepareFleetSites
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-DYTYPKFN.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveSites
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FP6HOW6J.js";
|
|
12
12
|
import {
|
|
13
13
|
ALL_CONFIG_NAMES,
|
|
14
14
|
CANONICAL_GITIGNORE_ENTRIES,
|
|
@@ -16,17 +16,17 @@ import {
|
|
|
16
16
|
mergeGitignore,
|
|
17
17
|
planTemplateDiffs,
|
|
18
18
|
syncConfigs
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
import "./chunk-SQ4FSNYL.js";
|
|
19
|
+
} from "./chunk-3RLDX3Q2.js";
|
|
21
20
|
import {
|
|
22
21
|
ALL_TEMPLATES,
|
|
23
22
|
templatesByName
|
|
24
23
|
} from "./chunk-SLFG7Y76.js";
|
|
24
|
+
import "./chunk-LUF52EIY.js";
|
|
25
25
|
import "./chunk-Z3JXLULD.js";
|
|
26
26
|
import "./chunk-LBYOLBW7.js";
|
|
27
27
|
import "./chunk-RARREDQE.js";
|
|
28
28
|
import "./chunk-XXTZBPUY.js";
|
|
29
|
-
import "./chunk-
|
|
29
|
+
import "./chunk-WMF5S2MB.js";
|
|
30
30
|
|
|
31
31
|
// src/cli/commands/sync-configs.ts
|
|
32
32
|
import { readFile } from "fs/promises";
|
|
@@ -107,4 +107,4 @@ async function runSyncConfigsCommand(site, opts) {
|
|
|
107
107
|
export {
|
|
108
108
|
runSyncConfigsCommand
|
|
109
109
|
};
|
|
110
|
-
//# sourceMappingURL=sync-configs-
|
|
110
|
+
//# sourceMappingURL=sync-configs-OXSWKVZ2.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
requireFleetWorkdir,
|
|
3
|
+
selectFleetSites
|
|
4
|
+
} from "./chunk-VQMAAQ5M.js";
|
|
5
|
+
import "./chunk-ICSRWW5Q.js";
|
|
6
|
+
import "./chunk-RARREDQE.js";
|
|
7
|
+
|
|
8
|
+
// src/inventory/turso.ts
|
|
9
|
+
function fromTursoDb(open, opts = {}) {
|
|
10
|
+
return async () => {
|
|
11
|
+
const workdir = requireFleetWorkdir(opts.workdir, "fromTursoDb");
|
|
12
|
+
const { listSites } = await import("./fleet-state-RJROYXKO.js");
|
|
13
|
+
const db = await open();
|
|
14
|
+
try {
|
|
15
|
+
return selectFleetSites(await listSites(db), workdir);
|
|
16
|
+
} finally {
|
|
17
|
+
await db.destroy();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
fromTursoDb
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=turso-KQKBBQCK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/inventory/turso.ts"],"sourcesContent":["import type { Site, InventoryProvider } from \"../types.js\";\nimport type { Db } from \"../db/client.js\";\nimport { selectFleetSites, requireFleetWorkdir } from \"./select.js\";\n\nexport type TursoInventoryOptions = {\n /** Local workdir to compute each site's path as `{workdir}/{slug}`. Defaults to\n * REDDOOR_FLEET_WORKDIR. The database holds no checkout paths, so one is required. */\n workdir?: string;\n};\n\n/**\n * The fleet roster, read from Turso (#646 step 4) — what `--fleet turso` (and its\n * deprecated alias `--fleet airtable`) resolves through.\n *\n * Why this exists: since step 3 a site created by `ensure-site` gets a\n * `site_<ULID>` id and NO Airtable record, so an Airtable-backed roster can never\n * see it. Turso holds every site, `rec…` and `site_…` alike.\n *\n * Same selection as the Airtable provider by construction (`selectFleetSites`),\n * and the same rows by proof (`tests/inventory/selection-parity.test.ts`).\n *\n * `open` is a factory rather than a handle so the provider owns the connection it\n * opened and closes it once the roster is read — a sweep holds no Turso client\n * for its whole run. The fleet-state reader is imported lazily: `kysely` and the\n * libSQL client are devDependencies, and nothing that merely imports the\n * inventory module should pull them in.\n */\nexport function fromTursoDb(\n open: () => Promise<Db>,\n opts: TursoInventoryOptions = {},\n): InventoryProvider {\n return async (): Promise<Site[]> => {\n const workdir = requireFleetWorkdir(opts.workdir, \"fromTursoDb\");\n const { listSites } = await import(\"../db/fleet-state.js\");\n const db = await open();\n try {\n return selectFleetSites(await listSites(db), workdir);\n } finally {\n await db.destroy();\n }\n };\n}\n"],"mappings":";;;;;;;;AA2BO,SAAS,YACd,MACA,OAA8B,CAAC,GACZ;AACnB,SAAO,YAA6B;AAClC,UAAM,UAAU,oBAAoB,KAAK,SAAS,aAAa;AAC/D,UAAM,EAAE,UAAU,IAAI,MAAM,OAAO,2BAAsB;AACzD,UAAM,KAAK,MAAM,KAAK;AACtB,QAAI;AACF,aAAO,iBAAiB,MAAM,UAAU,EAAE,GAAG,OAAO;AAAA,IACtD,UAAE;AACA,YAAM,GAAG,QAAQ;AAAA,IACnB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -5,21 +5,21 @@ import {
|
|
|
5
5
|
appendSkipNotice,
|
|
6
6
|
fleetWorkdir,
|
|
7
7
|
prepareFleetSites
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-DYTYPKFN.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveSites
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FP6HOW6J.js";
|
|
12
12
|
import {
|
|
13
13
|
upgradeSvelte4to5
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-JHJNNJLG.js";
|
|
15
15
|
import "./chunk-J7SZQUCW.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-LUF52EIY.js";
|
|
17
17
|
import "./chunk-OTGB6YQO.js";
|
|
18
18
|
import "./chunk-Z3JXLULD.js";
|
|
19
19
|
import "./chunk-LBYOLBW7.js";
|
|
20
20
|
import "./chunk-RARREDQE.js";
|
|
21
21
|
import "./chunk-XXTZBPUY.js";
|
|
22
|
-
import "./chunk-
|
|
22
|
+
import "./chunk-WMF5S2MB.js";
|
|
23
23
|
|
|
24
24
|
// src/cli/commands/upgrade.ts
|
|
25
25
|
import { resolve } from "path";
|
|
@@ -60,4 +60,4 @@ async function runUpgradeCommand(upgradeName, site, opts = {}) {
|
|
|
60
60
|
export {
|
|
61
61
|
runUpgradeCommand
|
|
62
62
|
};
|
|
63
|
-
//# sourceMappingURL=upgrade-
|
|
63
|
+
//# sourceMappingURL=upgrade-EVAL3JTO.js.map
|
package/dist/util/git.d.ts
CHANGED
|
@@ -130,7 +130,7 @@ declare function getRemoteUrl(cwd: string): Promise<string>;
|
|
|
130
130
|
* read `site.gitRepo` directly and therefore refused every POSITIONAL run with
|
|
131
131
|
* "no Git repo on this site" — `localPath()` (src/inventory/local.ts:11) builds
|
|
132
132
|
* `{ path, name }` and nothing else. A site being bootstrapped is exactly the
|
|
133
|
-
* case that
|
|
133
|
+
* case that is not in the fleet roster yet (`--fleet turso` filters pre-launch
|
|
134
134
|
* statuses), so the positional path is the ONLY one `/new-site` can use.
|
|
135
135
|
*/
|
|
136
136
|
declare function resolveOwnerRepo(site: Site): Promise<string | null>;
|
package/dist/util/git.js
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WEBSITES_TABLE,
|
|
3
|
+
analyticsHealthFields,
|
|
4
|
+
getWebsiteBySlug,
|
|
5
|
+
listWebsites,
|
|
6
|
+
mapRow,
|
|
7
|
+
updateA11yCounts,
|
|
8
|
+
updateAnalyticsHealth,
|
|
9
|
+
updateAuditFields,
|
|
10
|
+
updateAutoFixAttempts,
|
|
11
|
+
updateDepsCounts,
|
|
12
|
+
updateGitHubSignals,
|
|
13
|
+
updateLaunched,
|
|
14
|
+
updateNextDueDates,
|
|
15
|
+
updatePrismicModels,
|
|
16
|
+
updateScores,
|
|
17
|
+
updateSecurityCounts,
|
|
18
|
+
updateSiteField,
|
|
19
|
+
updateSiteFields
|
|
20
|
+
} from "./chunk-TC4GANMX.js";
|
|
1
21
|
import {
|
|
2
22
|
ACTIVE_STATUSES,
|
|
3
23
|
ARCHIVED_STATUSES,
|
|
4
24
|
KNOWN_STATUSES,
|
|
5
25
|
PRE_LAUNCH_STATUSES,
|
|
6
26
|
SEVERITY_RANK,
|
|
7
|
-
WEBSITES_TABLE,
|
|
8
27
|
allActionableVulnsTransitive,
|
|
9
|
-
analyticsHealthFields,
|
|
10
|
-
getWebsiteBySlug,
|
|
11
28
|
isArchivedStatus,
|
|
12
29
|
isDashboardVisible,
|
|
13
30
|
isPreLaunch,
|
|
14
31
|
isUnrecognizedStatus,
|
|
15
|
-
listWebsites,
|
|
16
|
-
mapRow,
|
|
17
32
|
normalizeSecurityAdvisory,
|
|
18
33
|
parseNotifyRouting,
|
|
19
34
|
parseSecurityAdvisories,
|
|
@@ -21,20 +36,9 @@ import {
|
|
|
21
36
|
toFrequency,
|
|
22
37
|
toPrismicModelsVerdict,
|
|
23
38
|
toVerdict,
|
|
24
|
-
trimToNull
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
updateAuditFields,
|
|
28
|
-
updateAutoFixAttempts,
|
|
29
|
-
updateDepsCounts,
|
|
30
|
-
updateGitHubSignals,
|
|
31
|
-
updateLaunched,
|
|
32
|
-
updateNextDueDates,
|
|
33
|
-
updatePrismicModels,
|
|
34
|
-
updateScores,
|
|
35
|
-
updateSecurityCounts,
|
|
36
|
-
updateSiteField
|
|
37
|
-
} from "./chunk-KN52PM63.js";
|
|
39
|
+
trimToNull
|
|
40
|
+
} from "./chunk-ICSRWW5Q.js";
|
|
41
|
+
import "./chunk-RQJFGK3A.js";
|
|
38
42
|
export {
|
|
39
43
|
ACTIVE_STATUSES,
|
|
40
44
|
ARCHIVED_STATUSES,
|
|
@@ -70,6 +74,7 @@ export {
|
|
|
70
74
|
updatePrismicModels,
|
|
71
75
|
updateScores,
|
|
72
76
|
updateSecurityCounts,
|
|
73
|
-
updateSiteField
|
|
77
|
+
updateSiteField,
|
|
78
|
+
updateSiteFields
|
|
74
79
|
};
|
|
75
|
-
//# sourceMappingURL=websites-
|
|
80
|
+
//# sourceMappingURL=websites-Y6FN2XFD.js.map
|
|
@@ -3,15 +3,17 @@ import {
|
|
|
3
3
|
formatFleetWriteSummary,
|
|
4
4
|
writeAuditsToAirtable,
|
|
5
5
|
writeFleetAuditsToAirtable
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-FHFXJ3EM.js";
|
|
7
|
+
import "./chunk-X56LAKPR.js";
|
|
8
8
|
import "./chunk-WQLNUEZL.js";
|
|
9
9
|
import "./chunk-5L3FPQIJ.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-TC4GANMX.js";
|
|
11
|
+
import "./chunk-ICSRWW5Q.js";
|
|
12
|
+
import "./chunk-RQJFGK3A.js";
|
|
11
13
|
export {
|
|
12
14
|
fleetWriteFailed,
|
|
13
15
|
formatFleetWriteSummary,
|
|
14
16
|
writeAuditsToAirtable,
|
|
15
17
|
writeFleetAuditsToAirtable
|
|
16
18
|
};
|
|
17
|
-
//# sourceMappingURL=write-audits-to-airtable-
|
|
19
|
+
//# sourceMappingURL=write-audits-to-airtable-TI3JMWSY.js.map
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/recipes/announce.ts","../src/cli/commands/announce.ts"],"sourcesContent":["import { openBase, readAirtableConfig } from \"../reports/airtable/client.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport { listWebsites, siteSlug, updateAnalyticsHealth } from \"../reports/airtable/websites.js\";\nimport type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport { readGaConfig } from \"../reports/ga/config.js\";\nimport {\n createDraft,\n findReportByPeriod,\n updateReportScores,\n type ReportEnrichment,\n} from \"../reports/airtable/reports.js\";\nimport type { ReportMirror } from \"../reports/report-mirror.js\";\nimport type { SiteMirror } from \"../db/site-mirror.js\";\nimport { queueDraft } from \"../reports/queue.js\";\nimport { uploadAttachment } from \"../reports/airtable/attachments.js\";\nimport { renderReportHtml } from \"../reports/render.js\";\nimport { resolveCopy } from \"../reports/copy.js\";\nimport { fetchGaUsers, fetchSearch, refreshHeaderImage } from \"../reports/draft.js\";\nimport type { RefreshHeaderDeps } from \"../reports/draft.js\";\nimport { announcementSiteExtras } from \"../reports/announcement-email/template.js\";\nimport type { LighthouseScores } from \"../reports/types.js\";\nimport { defaultReportSubject } from \"../reports/subject.js\";\nimport { scoresFromRow } from \"../reports/report-data.js\";\n\nexport type AnnounceSiteResult =\n | {\n site: string;\n status: \"drafted\" | \"reused\";\n reportId: string;\n recipientMissing: boolean;\n /** False when a higher-or-equal-tier report was already queued (single-queue rule). */\n queued: boolean;\n }\n | { site: string; status: \"skipped-no-scores\" }\n | { site: string; status: \"error\"; message: string };\n\nexport type AnnounceResult = { results: AnnounceSiteResult[] };\n\n/** The traffic/search lookback window (days) the announcement reports on. The trend compares it\n * against the equal-length prior window, and the email labels it \"vs the previous N days\". */\nconst GA_WINDOW_DAYS = 30;\n\nexport type AnnounceDeps = {\n /** Airtable handle. Defaults to opening the live base from credentials. */\n base?: AirtableBase;\n /** When set, restrict to the single site whose slug matches. Default: all maintenance sites. */\n site?: string;\n /** Single timestamp driving the period key, render, draft, and preview filename. */\n now?: Date;\n /**\n * Header-image refresh, matching `draftReportForSite`'s opt-out shape: UNSET means\n * refresh (the production default), `false` skips it so unit suites don't launch a\n * browser. Announce previously never refreshed at all, so a site whose stored header\n * predated a plate change kept announcing with the old one until its next\n * Maintenance/Testing draft happened to heal it.\n */\n refreshHeader?: RefreshHeaderDeps | false;\n /** #539 Phase 5: Turso write-through for everything this recipe writes — the\n * created row (or a reused row's refreshed scores), the rendered body, and\n * the queue flag. Not defaulted here — every unit test calls `announce` with\n * a fake base, and a default would open a real libSQL handle from inside the\n * suite. The CLI composition root wires it. */\n reportMirror?: ReportMirror;\n /** #539 Phase 5: the Websites-row twin — announce stamps `Analytics soft-fail\n * at` on the SITE row, a different Turso table from the report. */\n siteMirror?: SiteMirror;\n};\n\n/**\n * Draft the monthly-report ANNOUNCEMENT email for every `maintenance` site (or one,\n * via `deps.site`). Airtable-driven and fleet-wide: unlike `launch`, it runs no audits\n * and takes no `Site`/inventory object — it reads the Lighthouse scores already stored\n * on each Websites row. DRAFTS ONLY; the M3 approve loop sends.\n *\n * One bad site must never abort the run: each site is wrapped in its own try/catch that\n * records an `error` result and continues.\n */\nexport async function announce(deps?: AnnounceDeps): Promise<AnnounceResult> {\n const base = deps?.base ?? openBase(readAirtableConfig());\n const now = deps?.now ?? new Date();\n\n const websites = await listWebsites(base);\n let targets = websites.filter((w) => w.status === \"maintained\");\n if (deps?.site) {\n const wanted = siteSlug(deps.site);\n targets = targets.filter((w) => siteSlug(w.name) === wanted);\n }\n\n const period = now.toISOString().slice(0, 7);\n const results: AnnounceSiteResult[] = [];\n\n for (const w of targets) {\n try {\n const scores = scoresFromRow(w);\n if (scores === null) {\n results.push({ site: w.name, status: \"skipped-no-scores\" });\n continue;\n }\n\n // Refresh BEFORE the render so a queued announcement always has a current header\n // stored — the send re-reads the attachment, so a stale one here reaches the\n // client. Best-effort by construction: refreshHeaderImage returns false and never\n // throws, so a capture failure leaves the stored image and the draft continues.\n if (deps?.refreshHeader !== false) {\n await refreshHeaderImage(w, deps?.refreshHeader ?? {});\n }\n\n // Traffic + search snapshot over a ~30-day window ending now (fetchPeriodUsers derives\n // the equal-length previous window for the trend). Reuses the report pipeline's\n // soft-failing enrichment: GA/search unconfigured or an API error leaves the numbers\n // null and the email simply omits the traffic section — it never blocks the draft.\n const periodEnd = now;\n const periodStart = new Date(now.getTime() - GA_WINDOW_DAYS * 24 * 60 * 60 * 1000);\n const gaResult = await fetchGaUsers(w, periodStart, periodEnd);\n const searchResult = await fetchSearch(w, periodStart, periodEnd);\n const gaUsers = gaResult.value;\n const search = searchResult.value;\n const enrichment: ReportEnrichment = {\n ...(gaUsers ? { gaUsersCurrent: gaUsers.current, gaUsersPrevious: gaUsers.previous } : {}),\n ...(search ? { searchFoundPage1: search.foundOnPage1 } : {}),\n ...(search?.foundOnPage1 && search.position !== null\n ? { searchPosition: search.position }\n : {}),\n };\n\n // Record this site's GA/Search enrichment health for the per-site analytics-failure\n // signal (cockpit/digest) — exactly as the `--due` draft path does. Without this, an\n // announcement-time GA outage hides the traffic block with NO operator signal (it reads\n // identically to \"site has no GA configured\"). Set the timestamp on a soft-fail, clear\n // it on a clean enrichment so the signal self-heals. Best-effort: the column is\n // operator-added, so until it exists the write throws — which must not break the draft.\n if (readGaConfig() !== null && Boolean(w.ga4PropertyId || w.searchQuery)) {\n try {\n const fields = await updateAnalyticsHealth(\n base,\n w.id,\n gaResult.softFailed || searchResult.softFailed ? now.toISOString() : null,\n );\n // Mirror the EXACT FieldSet Airtable got — see draft.ts.\n await deps?.siteMirror?.health(w.id, fields);\n } catch (e) {\n console.warn(`⚠ analytics-health write skipped for ${w.name}: ${(e as Error).message}`);\n }\n }\n\n // Dedupe: reuse an existing Announcement row for this (site, period) rather than\n // stacking a second draft. The reuse path refreshes the stored scores + traffic/search\n // (and Completed on) so the eventually-sent email — which reads the row — isn't stale.\n // The create path writes them via createDraft.\n let report;\n let statusKind: \"drafted\" | \"reused\";\n const existing = await findReportByPeriod(base, w.id, \"Announcement\", period);\n if (existing) {\n await updateReportScores(base, existing.id, scores, now, enrichment);\n // Mirror the SAME refresh: the reuse path exists so the eventually-sent\n // email is not stale, and the console reads those numbers too.\n await deps?.reportMirror?.patch(existing.id, {\n lighthouse_performance: scores.performance,\n lighthouse_accessibility: scores.accessibility,\n lighthouse_best_practices: scores.bestPractices,\n lighthouse_seo: scores.seo,\n completed_on: now.toISOString().slice(0, 10),\n ...(enrichment.gaUsersCurrent !== undefined\n ? { ga_users_current: enrichment.gaUsersCurrent }\n : {}),\n ...(enrichment.gaUsersPrevious !== undefined\n ? { ga_users_previous: enrichment.gaUsersPrevious }\n : {}),\n ...(enrichment.searchFoundPage1 !== undefined\n ? { search_found_page1: enrichment.searchFoundPage1 ? 1 : 0 }\n : {}),\n ...(enrichment.searchPosition !== undefined\n ? { search_position: enrichment.searchPosition }\n : {}),\n });\n report = existing;\n statusKind = \"reused\";\n } else {\n report = await createDraft(\n base,\n draftInputFor(w, scores, now, period, enrichment),\n deps?.reportMirror?.created,\n );\n statusKind = \"drafted\";\n }\n\n const slug = siteSlug(w.name);\n const { html } = await renderReportHtml({\n siteName: w.name,\n siteUrl: w.url,\n reportType: \"Announcement\",\n completedOn: now,\n lighthouse: scores,\n gaUsersCurrent: gaUsers?.current,\n gaUsersPrevious: gaUsers?.previous,\n gaPeriodDays: GA_WINDOW_DAYS,\n searchPosition: search?.foundOnPage1 ? (search.position ?? undefined) : undefined,\n lastTestedDate: null,\n commentary: null,\n copy: resolveCopy(w),\n headerImageCid: `${slug}-header`,\n // cadence (the client's go-forward pace, \"None\" omitted) + default-on improvement\n // callouts. Shared with the send re-render via announcementSiteExtras so the sent\n // email matches this reviewed preview.\n ...announcementSiteExtras(w),\n });\n\n // A preview-upload hiccup must NOT fail the site — log and continue.\n try {\n await uploadAttachment(\n report.id,\n \"Rendered HTML\",\n html,\n `${slug}-${now.toISOString().slice(0, 10)}.html`,\n \"text/html\",\n );\n // Store the same body in Turso — the console's preview route reads it\n // there, not from the Airtable attachment (whose URL expires).\n await deps?.reportMirror?.body(report.id, html);\n } catch (uploadErr) {\n console.warn(\n `⚠ Announcement preview upload skipped for ${w.name}: ${\n uploadErr instanceof Error ? uploadErr.message : String(uploadErr)\n }`,\n );\n }\n\n // Critical: NOT wrapped — without queueing, the draft never enters the approve queue,\n // so a failure here must surface as an error result for the site. queueDraft also\n // supersedes any lower-tier (Maintenance/Testing) drafts queued for this site, and\n // stands down if an equal-or-higher report is already queued (single-queue rule).\n const queue = await queueDraft(\n base,\n { id: report.id, siteId: w.id, reportType: \"Announcement\" },\n deps?.reportMirror,\n );\n\n const recipientMissing = !(w.reportRecipientsTo && w.reportRecipientsTo.trim());\n results.push({\n site: w.name,\n status: statusKind,\n reportId: report.id,\n recipientMissing,\n queued: queue.queued,\n });\n } catch (err) {\n results.push({\n site: w.name,\n status: \"error\",\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return { results };\n}\n\n/** Build the Announcement `DraftInput`. Announcements have no period window and no prior\n * maintenance test, so periodStart/periodEnd/completedOn all collapse to `now` and\n * `lastTestedDate` is null. The subject override gives the email a purpose-built line. */\nfunction draftInputFor(\n w: WebsiteRow,\n scores: LighthouseScores,\n now: Date,\n period: string,\n enrichment: ReportEnrichment,\n): Parameters<typeof createDraft>[1] {\n return {\n reportId: `${w.name} — Announcement — ${now.toISOString().slice(0, 10)}`,\n siteId: w.id,\n reportType: \"Announcement\",\n period,\n periodStart: now,\n periodEnd: now,\n completedOn: now,\n lighthouse: scores,\n lastTestedDate: null,\n subjectOverride: defaultReportSubject({\n name: w.name,\n url: w.url,\n type: \"Announcement\",\n date: now,\n }),\n ...enrichment,\n };\n}\n","import { announce, type AnnounceResult, type AnnounceSiteResult } from \"../../recipes/announce.js\";\n\nexport type AnnounceCommandOptions = {\n cwd?: string;\n};\n\nfunction formatSiteResult(r: AnnounceSiteResult): string {\n if (r.status === \"skipped-no-scores\") return `[${r.site}] skipped-no-scores`;\n if (r.status === \"error\") return `[${r.site}] error: ${r.message}`;\n const note = r.recipientMissing ? \" ⚠ recipient missing\" : \"\";\n return `[${r.site}] ${r.status}${note}`;\n}\n\nexport function formatAnnounceResult(result: AnnounceResult): string {\n if (result.results.length === 0) return \"No maintenance sites to announce.\";\n return result.results.map(formatSiteResult).join(\"\\n\");\n}\n\n/**\n * `announce [site]` — Airtable-driven and fleet-wide. Draft the monthly-report\n * announcement email for every `maintenance` site (or one, when `site` is given) into\n * the M3 approve queue. Never sends; the operator approves each draft and the next send\n * run delivers it. Reads the Lighthouse scores already stored on each Websites row —\n * no audits are run.\n */\nexport async function runAnnounceCommand(\n site: string | undefined,\n _opts: AnnounceCommandOptions,\n): Promise<{ output: string; code: number }> {\n // #539 Phase 5: the create-side Turso dual-write is wired HERE rather than\n // inside `announce`, so a unit suite calling the recipe with a fake base can\n // never open a real libSQL handle (and, with TURSO_* exported locally, write\n // into production).\n const { makeReportMirror } = await import(\"../../reports/report-mirror.js\");\n const { makeSiteMirror } = await import(\"../../db/site-mirror.js\");\n const reportMirror = await makeReportMirror();\n const siteMirror = await makeSiteMirror();\n const result = await announce({ ...(site ? { site } : {}), reportMirror, siteMirror });\n const hadError = result.results.some((r) => r.status === \"error\");\n return { output: formatAnnounceResult(result), code: hadError ? 1 : 0 };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/audits/lighthouse-airtable.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport type { LighthouseScoreWriteback } from \"../reports/types.js\";\nimport { siteSlug } from \"../reports/airtable/websites.js\";\n\nconst LIGHTHOUSE_CATEGORIES = [\"performance\", \"accessibility\", \"best-practices\", \"seo\"] as const;\n\n/**\n * True when the result carries real lighthouse scores worth persisting.\n *\n * The `audit --write-airtable` policy used to refuse on any `status: \"fail\"`,\n * but that conflates two very different failure modes:\n * 1. Infrastructure failure (no lhr-*.json written, spawn timeout, etc.)\n * → `details.summary` empty → all-zeros would corrupt the dashboard\n * 2. Assertion failure (scores below threshold, e.g. best-practices < 0.9)\n * → `details.summary` has real numbers → tracking these IS the point\n *\n * The dashboard exists to surface drift over time. Refusing to write the\n * very numbers the dashboard wants to plot — just because one assertion\n * tripped — defeats the purpose. Write whenever real scores exist.\n */\nexport function hasRealScores(result: AuditResult): boolean {\n if (result.audit !== \"lighthouse\") return false;\n const details = (result.details ?? {}) as { summary?: Record<string, number> };\n const summary = details.summary ?? {};\n return LIGHTHOUSE_CATEGORIES.some(\n (k) => typeof summary[k] === \"number\" && !Number.isNaN(summary[k]),\n );\n}\n\n/**\n * Extract the four Lighthouse scores (as integer percentages) from a\n * `lighthouse` AuditResult. LHCI manifest summaries are floats in [0,1];\n * multiply + round. A category absent from the summary — e.g. Lighthouse\n * errored its audit (NO_LCP nulls the whole performance category) — yields\n * `null`, NOT 0, so the write path can clear the Airtable cell (\"—\") instead\n * of persisting a misleading zero. (`hasRealScores` still gates the write on\n * at least one real number, so an all-null infra failure is refused upstream.)\n */\nexport function lighthouseScoresFromResult(result: AuditResult): LighthouseScoreWriteback {\n if (result.audit !== \"lighthouse\") {\n throw new Error(`Expected a 'lighthouse' AuditResult, got '${result.audit}'`);\n }\n const details = (result.details ?? {}) as { summary?: Record<string, number> };\n const summary = details.summary ?? {};\n const toPct = (n: number | undefined): number | null =>\n typeof n === \"number\" && !Number.isNaN(n) ? Math.round(n * 100) : null;\n return {\n performance: toPct(summary[\"performance\"]),\n accessibility: toPct(summary[\"accessibility\"]),\n bestPractices: toPct(summary[\"best-practices\"]),\n seo: toPct(summary[\"seo\"]),\n };\n}\n\n/**\n * Derive a site slug from the cwd's package.json#name. Used by\n * `audit lighthouse --write-airtable` when the operator doesn't pass an\n * explicit slug — the cwd is the site checkout, so package.json#name is\n * usually the canonical site name.\n */\nexport async function resolveSlugFromCwd(cwd: string): Promise<string> {\n try {\n const pkgPath = join(cwd, \"package.json\");\n const raw = await readFile(pkgPath, \"utf-8\");\n const pkg = JSON.parse(raw) as { name?: string };\n if (!pkg.name) throw new Error(\"package.json has no 'name' field\");\n return siteSlug(pkg.name);\n } catch (e) {\n throw new Error(\n `Could not derive site slug from ${cwd}/package.json: ${(e as Error).message}. ` +\n `Pass --write-airtable=<slug> explicitly.`,\n { cause: e },\n );\n }\n}\n"],"mappings":";;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,YAAY;AAKrB,IAAM,wBAAwB,CAAC,eAAe,iBAAiB,kBAAkB,KAAK;AAgB/E,SAAS,cAAc,QAA8B;AAC1D,MAAI,OAAO,UAAU,aAAc,QAAO;AAC1C,QAAM,UAAW,OAAO,WAAW,CAAC;AACpC,QAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,SAAO,sBAAsB;AAAA,IAC3B,CAAC,MAAM,OAAO,QAAQ,CAAC,MAAM,YAAY,CAAC,OAAO,MAAM,QAAQ,CAAC,CAAC;AAAA,EACnE;AACF;AAWO,SAAS,2BAA2B,QAA+C;AACxF,MAAI,OAAO,UAAU,cAAc;AACjC,UAAM,IAAI,MAAM,6CAA6C,OAAO,KAAK,GAAG;AAAA,EAC9E;AACA,QAAM,UAAW,OAAO,WAAW,CAAC;AACpC,QAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,QAAM,QAAQ,CAAC,MACb,OAAO,MAAM,YAAY,CAAC,OAAO,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,GAAG,IAAI;AACpE,SAAO;AAAA,IACL,aAAa,MAAM,QAAQ,aAAa,CAAC;AAAA,IACzC,eAAe,MAAM,QAAQ,eAAe,CAAC;AAAA,IAC7C,eAAe,MAAM,QAAQ,gBAAgB,CAAC;AAAA,IAC9C,KAAK,MAAM,QAAQ,KAAK,CAAC;AAAA,EAC3B;AACF;AAQA,eAAsB,mBAAmB,KAA8B;AACrE,MAAI;AACF,UAAM,UAAU,KAAK,KAAK,cAAc;AACxC,UAAM,MAAM,MAAM,SAAS,SAAS,OAAO;AAC3C,UAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,QAAI,CAAC,IAAI,KAAM,OAAM,IAAI,MAAM,kCAAkC;AACjE,WAAO,SAAS,IAAI,IAAI;AAAA,EAC1B,SAAS,GAAG;AACV,UAAM,IAAI;AAAA,MACR,mCAAmC,GAAG,kBAAmB,EAAY,OAAO;AAAA,MAE5E,EAAE,OAAO,EAAE;AAAA,IACb;AAAA,EACF;AACF;","names":[]}
|