@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.
- package/README.md +8 -0
- package/dist/airtable-G7WBQQMI.js +9 -0
- package/dist/{announce-GIAYN67S.js → announce-MXTUQ6C7.js} +26 -19
- package/dist/announce-MXTUQ6C7.js.map +1 -0
- package/dist/{bump-deps-SWJB6H5V.js → bump-deps-EC74CPLS.js} +9 -9
- package/dist/chunk-2EVEFOZY.js +129 -0
- package/dist/chunk-2EVEFOZY.js.map +1 -0
- package/dist/{chunk-COZPL62X.js → chunk-2NDRHXMR.js} +25 -17
- package/dist/chunk-2NDRHXMR.js.map +1 -0
- package/dist/{chunk-SDUJIANJ.js → chunk-2NJQLSEU.js} +3 -3
- package/dist/chunk-2NQUCS7O.js +243 -0
- package/dist/chunk-2NQUCS7O.js.map +1 -0
- package/dist/{chunk-DTTIFYAM.js → chunk-2ZPT7GLV.js} +42 -403
- package/dist/chunk-2ZPT7GLV.js.map +1 -0
- package/dist/{chunk-ZTPPCHDB.js → chunk-3G25KIWW.js} +4 -4
- package/dist/chunk-3SF7D4DY.js +359 -0
- package/dist/chunk-3SF7D4DY.js.map +1 -0
- package/dist/{chunk-Q276MZ7E.js → chunk-3VQWSE3F.js} +4 -4
- package/dist/{chunk-37S6L6G4.js → chunk-5UINJNAJ.js} +72 -2
- package/dist/chunk-5UINJNAJ.js.map +1 -0
- package/dist/{chunk-THEFTZ7F.js → chunk-62CMMGEO.js} +5 -5
- package/dist/{chunk-FDIFTIXC.js → chunk-6TWWY7ZU.js} +2 -2
- package/dist/{chunk-UYCAF77E.js → chunk-7RJ2HMMJ.js} +3 -3
- package/dist/chunk-ACBRLLFT.js +198 -0
- package/dist/chunk-ACBRLLFT.js.map +1 -0
- package/dist/chunk-BNM5POSJ.js +284 -0
- package/dist/chunk-BNM5POSJ.js.map +1 -0
- package/dist/{chunk-R6FPFMOS.js → chunk-BQRFQSGY.js} +3 -3
- package/dist/{chunk-MGR2H7F4.js → chunk-BYK66EWO.js} +19 -12
- package/dist/chunk-BYK66EWO.js.map +1 -0
- package/dist/{chunk-6QRLRT3P.js → chunk-CQR4LIM7.js} +12 -265
- package/dist/chunk-CQR4LIM7.js.map +1 -0
- package/dist/{chunk-SEEA6RXF.js → chunk-DJZWYIPP.js} +2 -2
- package/dist/chunk-DJZWYIPP.js.map +1 -0
- package/dist/chunk-EVBCK2KC.js +68 -0
- package/dist/chunk-EVBCK2KC.js.map +1 -0
- package/dist/{chunk-TP5M2NQB.js → chunk-F5KM4LEQ.js} +3 -3
- package/dist/{chunk-62VF32IC.js → chunk-ILUNZHYV.js} +2 -2
- package/dist/chunk-ITIOFN7A.js +396 -0
- package/dist/chunk-ITIOFN7A.js.map +1 -0
- package/dist/{chunk-LS6QPANO.js → chunk-IYSEC543.js} +2 -2
- package/dist/{chunk-DC5H7KEQ.js → chunk-JHGHLKFS.js} +2 -2
- package/dist/{chunk-PBWDDDQH.js → chunk-KTH5ZODA.js} +2 -2
- package/dist/{chunk-4DHQCN5H.js → chunk-LBYOLBW7.js} +17 -2
- package/dist/chunk-LBYOLBW7.js.map +1 -0
- package/dist/{chunk-577HCGZO.js → chunk-LDWZSLSU.js} +38 -23
- package/dist/chunk-LDWZSLSU.js.map +1 -0
- package/dist/{chunk-LTB6KVJW.js → chunk-N45M3FDK.js} +13 -11
- package/dist/{chunk-LTB6KVJW.js.map → chunk-N45M3FDK.js.map} +1 -1
- package/dist/chunk-NYUYNYQL.js +48 -0
- package/dist/chunk-NYUYNYQL.js.map +1 -0
- package/dist/{chunk-7J53SUT6.js → chunk-P2FE5AI6.js} +4 -4
- package/dist/{chunk-3YXDSCM6.js → chunk-PPE5KZJK.js} +29 -11
- package/dist/chunk-PPE5KZJK.js.map +1 -0
- package/dist/chunk-PTDMVGKL.js +31 -0
- package/dist/chunk-PTDMVGKL.js.map +1 -0
- package/dist/{chunk-D4FVAIEJ.js → chunk-PZR4EBPY.js} +5 -2
- package/dist/chunk-PZR4EBPY.js.map +1 -0
- package/dist/{chunk-NWB4O5EY.js → chunk-SLUQ7R6I.js} +3 -3
- package/dist/chunk-SLUQ7R6I.js.map +1 -0
- package/dist/{chunk-GZNBCHAZ.js → chunk-TERE5SDF.js} +4 -4
- package/dist/{chunk-ZMIPG6TR.js → chunk-TVL5MVUF.js} +4 -4
- package/dist/chunk-UDRHAQT7.js +172 -0
- package/dist/chunk-UDRHAQT7.js.map +1 -0
- package/dist/chunk-VAD43MOR.js +72 -0
- package/dist/chunk-VAD43MOR.js.map +1 -0
- package/dist/chunk-VSBFM7DS.js +229 -0
- package/dist/chunk-VSBFM7DS.js.map +1 -0
- package/dist/chunk-W7Z3BGCI.js +274 -0
- package/dist/chunk-W7Z3BGCI.js.map +1 -0
- package/dist/chunk-X3B723J4.js +1209 -0
- package/dist/chunk-X3B723J4.js.map +1 -0
- package/dist/{chunk-VRWUL4TR.js → chunk-XHHMM5HX.js} +2 -2
- package/dist/{chunk-RJB3QUSP.js → chunk-XIDUYLSR.js} +3 -3
- package/dist/{chunk-QG6OSL4M.js → chunk-XUGSZP7Z.js} +2 -2
- package/dist/{chunk-S56JQJ4F.js → chunk-Y53WOQIJ.js} +3 -3
- package/dist/{chunk-ICXV25N7.js → chunk-Z3KYFUN6.js} +3 -3
- package/dist/{chunk-MPBXOJ3N.js → chunk-Z7AXYKQB.js} +33 -2
- package/dist/chunk-Z7AXYKQB.js.map +1 -0
- package/dist/{chunk-WYYQK4D3.js → chunk-ZKXSX4LO.js} +11 -11
- package/dist/cli/bin.js +46 -28
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +30 -1
- package/dist/cli/commands/audit.js +62 -30
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-AUE3XUPO.js +11 -0
- package/dist/{convert-to-pnpm-AT52NWZB.js → convert-to-pnpm-6L6P74WR.js} +9 -9
- package/dist/db-B35OGV3G.js +217 -0
- package/dist/db-B35OGV3G.js.map +1 -0
- package/dist/{digest-UVHYZUYE.js → digest-BL6UJYKH.js} +23 -20
- package/dist/{digest-UVHYZUYE.js.map → digest-BL6UJYKH.js.map} +1 -1
- package/dist/dump-P6EBSIOH.js +68 -0
- package/dist/dump-P6EBSIOH.js.map +1 -0
- package/dist/email-VTTTKKL5.js +195 -0
- package/dist/email-VTTTKKL5.js.map +1 -0
- package/dist/{ensure-site-S25MDDHW.js → ensure-site-DFTB7MN3.js} +8 -7
- package/dist/ensure-site-DFTB7MN3.js.map +1 -0
- package/dist/fleet-events-cF2Om88w.d.ts +24 -0
- package/dist/{forms-notify-target-TWN4EH6K.js → forms-notify-target-75U6OU7L.js} +38 -132
- package/dist/forms-notify-target-75U6OU7L.js.map +1 -0
- package/dist/{github-signals-G4ILZAJ6.js → github-signals-IJUPGCUX.js} +40 -19
- package/dist/github-signals-IJUPGCUX.js.map +1 -0
- package/dist/{header-image-T7XD37JL.js → header-image-AWLLQA4L.js} +34 -11
- package/dist/header-image-AWLLQA4L.js.map +1 -0
- package/dist/header-images-335L2AKB.js +68 -0
- package/dist/header-images-335L2AKB.js.map +1 -0
- package/dist/{health-endpoint-242WG2OJ.js → health-endpoint-WJC2UMYS.js} +9 -9
- package/dist/health-mirror-4B2L7T2Y.js +75 -0
- package/dist/health-mirror-4B2L7T2Y.js.map +1 -0
- package/dist/import-airtable-HE43FB4H.js +35 -0
- package/dist/index.d.ts +175 -21
- package/dist/index.js +115 -73
- package/dist/index.js.map +1 -1
- package/dist/{init-JN26NTWF.js → init-NJEOEO7I.js} +20 -17
- package/dist/{init-JN26NTWF.js.map → init-NJEOEO7I.js.map} +1 -1
- package/dist/{launch-JA7Z4CIM.js → launch-XNAX427U.js} +31 -25
- package/dist/{launch-JA7Z4CIM.js.map → launch-XNAX427U.js.map} +1 -1
- package/dist/lighthouse-VHI77PFB.js +11 -0
- package/dist/{lighthouse-airtable-BIPM26SZ.js → lighthouse-airtable-LBVIYYP5.js} +3 -3
- package/dist/mailchimp-XYCTEEJY.js +72 -0
- package/dist/mailchimp-XYCTEEJY.js.map +1 -0
- package/dist/migrate-GQH237A6.js +7 -0
- package/dist/notify-C645PGD5.js +22 -0
- package/dist/{onboard-53CITIM7.js → onboard-GK4PHPNI.js} +9 -9
- package/dist/orchestrate-77UO4BYJ.js +30 -0
- package/dist/orchestrate-77UO4BYJ.js.map +1 -0
- package/dist/parity-MWS24A3H.js +12 -0
- package/dist/parity-MWS24A3H.js.map +1 -0
- package/dist/pipeline-S6CVHDGB.js +12 -0
- package/dist/pipeline-S6CVHDGB.js.map +1 -0
- package/dist/{preflight-3XPDUHFJ.js → preflight-6WBNU2GZ.js} +16 -13
- package/dist/{preflight-3XPDUHFJ.js.map → preflight-6WBNU2GZ.js.map} +1 -1
- package/dist/{prismic-ci-UTQ6LRR3.js → prismic-ci-3RHOZUUT.js} +15 -15
- package/dist/{prismic-models-PVES6R5V.js → prismic-models-QBGUT2Q3.js} +10 -10
- package/dist/prospect-audit-IUNCJ2LV.js +174 -0
- package/dist/prospect-audit-IUNCJ2LV.js.map +1 -0
- package/dist/prospect-audits-3PMIO73D.js +19 -0
- package/dist/prospect-audits-3PMIO73D.js.map +1 -0
- package/dist/{protection-audit-64PLLBLI.js → protection-audit-SJO53MEQ.js} +3 -3
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/render-AQAVQDPZ.js +10 -0
- package/dist/render-AQAVQDPZ.js.map +1 -0
- package/dist/{renovate-dispatch-ICJGSFCE.js → renovate-dispatch-QJY75ODR.js} +8 -8
- package/dist/replay-C2LXMJZP.js +66 -0
- package/dist/replay-C2LXMJZP.js.map +1 -0
- package/dist/{report-6CKSWJNG.js → report-4EVNDHOZ.js} +55 -29
- package/dist/report-4EVNDHOZ.js.map +1 -0
- package/dist/reports/header-image/assets/headline-announcement.png +0 -0
- package/dist/reports/header-image/assets/headline-launch.png +0 -0
- package/dist/reports/header-image/assets/headline-maintenance.png +0 -0
- package/dist/reports/header-image/assets/headline-testing.png +0 -0
- package/dist/reports/header-image/assets/plate-clean.png +0 -0
- package/dist/{self-updating-PPQVV6UA.js → self-updating-6DWI2Z56.js} +9 -9
- package/dist/{selftest-CQ6NOX52.js → selftest-RC36MTKP.js} +25 -20
- package/dist/selftest-RC36MTKP.js.map +1 -0
- package/dist/smoke-B2GQOSIZ.js +20 -0
- package/dist/smoke-B2GQOSIZ.js.map +1 -0
- package/dist/{smoke-suite-TMCSOYRK.js → smoke-suite-VYY2LXM2.js} +9 -9
- package/dist/{submissions-GJ6ITBRH.js → submissions-Q7DRFOCD.js} +3 -3
- package/dist/{svelte-codemods-ZOBDHIES.js → svelte-codemods-25J2FO22.js} +9 -9
- package/dist/sync-34HNZZ56.js +44 -0
- package/dist/sync-34HNZZ56.js.map +1 -0
- package/dist/{sync-configs-CJXUX6MD.js → sync-configs-3SE6PSCO.js} +9 -9
- package/dist/{upgrade-WLX27UPC.js → upgrade-WBRQ62FX.js} +9 -9
- package/dist/{webflow-624FEBFD.js → webflow-IRKOIT7S.js} +4 -4
- package/dist/webhook-VOGOW3W6.js +38 -0
- package/dist/webhook-VOGOW3W6.js.map +1 -0
- package/dist/{websites-SADZR4XQ.js → websites-SXWTP72S.js} +6 -2
- package/dist/websites-SXWTP72S.js.map +1 -0
- package/dist/{write-audits-to-airtable-7PH75YWY.js → write-audits-to-airtable-MBEILLPU.js} +4 -4
- package/dist/write-audits-to-airtable-MBEILLPU.js.map +1 -0
- package/package.json +5 -2
- package/dist/airtable-HRKSA7V4.js +0 -9
- package/dist/announce-GIAYN67S.js.map +0 -1
- package/dist/chunk-37S6L6G4.js.map +0 -1
- package/dist/chunk-3YXDSCM6.js.map +0 -1
- package/dist/chunk-4DHQCN5H.js.map +0 -1
- package/dist/chunk-4SOCPK7A.js +0 -108
- package/dist/chunk-4SOCPK7A.js.map +0 -1
- package/dist/chunk-577HCGZO.js.map +0 -1
- package/dist/chunk-6QRLRT3P.js.map +0 -1
- package/dist/chunk-COZPL62X.js.map +0 -1
- package/dist/chunk-D4FVAIEJ.js.map +0 -1
- package/dist/chunk-DTTIFYAM.js.map +0 -1
- package/dist/chunk-MGR2H7F4.js.map +0 -1
- package/dist/chunk-MPBXOJ3N.js.map +0 -1
- package/dist/chunk-NWB4O5EY.js.map +0 -1
- package/dist/chunk-SEEA6RXF.js.map +0 -1
- package/dist/chunk-U5SZ4FOP.js +0 -26
- package/dist/chunk-U5SZ4FOP.js.map +0 -1
- package/dist/client-X5WUJWDE.js +0 -11
- package/dist/db-JSIZYT23.js +0 -23
- package/dist/db-JSIZYT23.js.map +0 -1
- package/dist/ensure-site-S25MDDHW.js.map +0 -1
- package/dist/forms-notify-target-TWN4EH6K.js.map +0 -1
- package/dist/github-signals-G4ILZAJ6.js.map +0 -1
- package/dist/header-image-T7XD37JL.js.map +0 -1
- package/dist/migrate-LJUFGXQZ.js +0 -7
- package/dist/orchestrate-KDNXZQX7.js +0 -27
- package/dist/report-6CKSWJNG.js.map +0 -1
- package/dist/reports/header-image/assets/plate.png +0 -0
- package/dist/selftest-CQ6NOX52.js.map +0 -1
- /package/dist/{airtable-HRKSA7V4.js.map → airtable-G7WBQQMI.js.map} +0 -0
- /package/dist/{bump-deps-SWJB6H5V.js.map → bump-deps-EC74CPLS.js.map} +0 -0
- /package/dist/{chunk-SDUJIANJ.js.map → chunk-2NJQLSEU.js.map} +0 -0
- /package/dist/{chunk-ZTPPCHDB.js.map → chunk-3G25KIWW.js.map} +0 -0
- /package/dist/{chunk-Q276MZ7E.js.map → chunk-3VQWSE3F.js.map} +0 -0
- /package/dist/{chunk-THEFTZ7F.js.map → chunk-62CMMGEO.js.map} +0 -0
- /package/dist/{chunk-FDIFTIXC.js.map → chunk-6TWWY7ZU.js.map} +0 -0
- /package/dist/{chunk-UYCAF77E.js.map → chunk-7RJ2HMMJ.js.map} +0 -0
- /package/dist/{chunk-R6FPFMOS.js.map → chunk-BQRFQSGY.js.map} +0 -0
- /package/dist/{chunk-TP5M2NQB.js.map → chunk-F5KM4LEQ.js.map} +0 -0
- /package/dist/{chunk-62VF32IC.js.map → chunk-ILUNZHYV.js.map} +0 -0
- /package/dist/{chunk-LS6QPANO.js.map → chunk-IYSEC543.js.map} +0 -0
- /package/dist/{chunk-DC5H7KEQ.js.map → chunk-JHGHLKFS.js.map} +0 -0
- /package/dist/{chunk-PBWDDDQH.js.map → chunk-KTH5ZODA.js.map} +0 -0
- /package/dist/{chunk-7J53SUT6.js.map → chunk-P2FE5AI6.js.map} +0 -0
- /package/dist/{chunk-GZNBCHAZ.js.map → chunk-TERE5SDF.js.map} +0 -0
- /package/dist/{chunk-ZMIPG6TR.js.map → chunk-TVL5MVUF.js.map} +0 -0
- /package/dist/{chunk-VRWUL4TR.js.map → chunk-XHHMM5HX.js.map} +0 -0
- /package/dist/{chunk-RJB3QUSP.js.map → chunk-XIDUYLSR.js.map} +0 -0
- /package/dist/{chunk-QG6OSL4M.js.map → chunk-XUGSZP7Z.js.map} +0 -0
- /package/dist/{chunk-S56JQJ4F.js.map → chunk-Y53WOQIJ.js.map} +0 -0
- /package/dist/{chunk-ICXV25N7.js.map → chunk-Z3KYFUN6.js.map} +0 -0
- /package/dist/{chunk-WYYQK4D3.js.map → chunk-ZKXSX4LO.js.map} +0 -0
- /package/dist/{client-X5WUJWDE.js.map → client-AUE3XUPO.js.map} +0 -0
- /package/dist/{convert-to-pnpm-AT52NWZB.js.map → convert-to-pnpm-6L6P74WR.js.map} +0 -0
- /package/dist/{health-endpoint-242WG2OJ.js.map → health-endpoint-WJC2UMYS.js.map} +0 -0
- /package/dist/{lighthouse-airtable-BIPM26SZ.js.map → import-airtable-HE43FB4H.js.map} +0 -0
- /package/dist/{migrate-LJUFGXQZ.js.map → lighthouse-VHI77PFB.js.map} +0 -0
- /package/dist/{orchestrate-KDNXZQX7.js.map → lighthouse-airtable-LBVIYYP5.js.map} +0 -0
- /package/dist/{websites-SADZR4XQ.js.map → migrate-GQH237A6.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-7PH75YWY.js.map → notify-C645PGD5.js.map} +0 -0
- /package/dist/{onboard-53CITIM7.js.map → onboard-GK4PHPNI.js.map} +0 -0
- /package/dist/{prismic-ci-UTQ6LRR3.js.map → prismic-ci-3RHOZUUT.js.map} +0 -0
- /package/dist/{prismic-models-PVES6R5V.js.map → prismic-models-QBGUT2Q3.js.map} +0 -0
- /package/dist/{protection-audit-64PLLBLI.js.map → protection-audit-SJO53MEQ.js.map} +0 -0
- /package/dist/{renovate-dispatch-ICJGSFCE.js.map → renovate-dispatch-QJY75ODR.js.map} +0 -0
- /package/dist/{self-updating-PPQVV6UA.js.map → self-updating-6DWI2Z56.js.map} +0 -0
- /package/dist/{smoke-suite-TMCSOYRK.js.map → smoke-suite-VYY2LXM2.js.map} +0 -0
- /package/dist/{submissions-GJ6ITBRH.js.map → submissions-Q7DRFOCD.js.map} +0 -0
- /package/dist/{svelte-codemods-ZOBDHIES.js.map → svelte-codemods-25J2FO22.js.map} +0 -0
- /package/dist/{sync-configs-CJXUX6MD.js.map → sync-configs-3SE6PSCO.js.map} +0 -0
- /package/dist/{upgrade-WLX27UPC.js.map → upgrade-WBRQ62FX.js.map} +0 -0
- /package/dist/{webflow-624FEBFD.js.map → webflow-IRKOIT7S.js.map} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fromJsonFile,
|
|
3
3
|
localPath
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6TWWY7ZU.js";
|
|
5
5
|
import {
|
|
6
6
|
isHttpUrl
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-EVBCK2KC.js";
|
|
8
8
|
|
|
9
9
|
// src/cli/fleet/resolve-sites.ts
|
|
10
10
|
import { pathToFileURL } from "url";
|
|
@@ -30,7 +30,7 @@ async function resolveSites(input) {
|
|
|
30
30
|
}
|
|
31
31
|
if (input.fleet === "airtable") {
|
|
32
32
|
const { openBase, readAirtableConfig } = await import("./client-EPVO3FEH.js");
|
|
33
|
-
const { fromAirtableBase } = await import("./airtable-
|
|
33
|
+
const { fromAirtableBase } = await import("./airtable-G7WBQQMI.js");
|
|
34
34
|
const base = openBase(readAirtableConfig());
|
|
35
35
|
const provider = fromAirtableBase(base, input.workdir ? { workdir: input.workdir } : {});
|
|
36
36
|
return provider();
|
|
@@ -61,4 +61,4 @@ async function resolveSites(input) {
|
|
|
61
61
|
export {
|
|
62
62
|
resolveSites
|
|
63
63
|
};
|
|
64
|
-
//# sourceMappingURL=chunk-
|
|
64
|
+
//# sourceMappingURL=chunk-P2FE5AI6.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
hasRealScores,
|
|
3
3
|
lighthouseScoresFromResult
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KTH5ZODA.js";
|
|
5
5
|
import {
|
|
6
6
|
detectAuditEvents
|
|
7
7
|
} from "./chunk-5L3FPQIJ.js";
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
normalizeSecurityAdvisory,
|
|
10
10
|
siteSlug,
|
|
11
11
|
updateAuditFields
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-LDWZSLSU.js";
|
|
13
13
|
|
|
14
14
|
// src/audits/a11y-airtable.ts
|
|
15
15
|
function hasA11yCounts(result) {
|
|
@@ -227,9 +227,9 @@ async function writeAuditsToAirtable(args) {
|
|
|
227
227
|
}
|
|
228
228
|
const browser = results.find((r) => r.audit === "browser");
|
|
229
229
|
if (browser && hasBrowserResult(browser)) {
|
|
230
|
-
const
|
|
231
|
-
audits.browser =
|
|
232
|
-
writes.push({ audit: "browser", counts:
|
|
230
|
+
const fields2 = browserFieldsFromAudit(browser);
|
|
231
|
+
audits.browser = fields2;
|
|
232
|
+
writes.push({ audit: "browser", counts: fields2 });
|
|
233
233
|
}
|
|
234
234
|
const netlifyDeploy = results.find((r) => r.audit === "netlify-deploy");
|
|
235
235
|
if (netlifyDeploy && hasNetlifyDeployResult(netlifyDeploy)) {
|
|
@@ -255,8 +255,9 @@ async function writeAuditsToAirtable(args) {
|
|
|
255
255
|
audits.formE2e = result;
|
|
256
256
|
writes.push({ audit: "form-e2e", counts: result });
|
|
257
257
|
}
|
|
258
|
+
let fields = {};
|
|
258
259
|
if (Object.keys(audits).length > 0) {
|
|
259
|
-
await updateAuditFields(base, target.id, audits);
|
|
260
|
+
fields = await updateAuditFields(base, target.id, audits);
|
|
260
261
|
}
|
|
261
262
|
const events = detectAuditEvents(
|
|
262
263
|
target,
|
|
@@ -275,7 +276,7 @@ async function writeAuditsToAirtable(args) {
|
|
|
275
276
|
{ exitCode: 1 }
|
|
276
277
|
);
|
|
277
278
|
}
|
|
278
|
-
return { siteName: target.name, writes, events };
|
|
279
|
+
return { siteName: target.name, writes, events, siteId: target.id, fields };
|
|
279
280
|
}
|
|
280
281
|
function formatFleetWriteSummary(result) {
|
|
281
282
|
const wrote = result.written.length;
|
|
@@ -288,10 +289,14 @@ function formatFleetWriteSummary(result) {
|
|
|
288
289
|
}
|
|
289
290
|
out += `
|
|
290
291
|
FLEET_WRITE_SUMMARY wrote=${wrote} failed=${failed} total=${total}`;
|
|
292
|
+
if (result.mirrored !== void 0) {
|
|
293
|
+
out += ` mirrored=${result.mirrored} mirror_failed=${result.mirrorFailed ?? 0}`;
|
|
294
|
+
out += ` mirror_missed=${result.mirrorMissed ?? 0}`;
|
|
295
|
+
}
|
|
291
296
|
return out;
|
|
292
297
|
}
|
|
293
298
|
async function writeFleetAuditsToAirtable(args) {
|
|
294
|
-
const { base, websites, results } = args;
|
|
299
|
+
const { base, websites, results, mirror } = args;
|
|
295
300
|
const bySlug = /* @__PURE__ */ new Map();
|
|
296
301
|
for (const r of results) {
|
|
297
302
|
const arr = bySlug.get(r.site) ?? [];
|
|
@@ -300,14 +305,27 @@ async function writeFleetAuditsToAirtable(args) {
|
|
|
300
305
|
}
|
|
301
306
|
const written = [];
|
|
302
307
|
const failed = [];
|
|
308
|
+
let mirrored = 0;
|
|
309
|
+
let mirrorFailed = 0;
|
|
310
|
+
let mirrorMissed = 0;
|
|
303
311
|
for (const [slug, siteResults] of bySlug) {
|
|
304
312
|
try {
|
|
305
|
-
|
|
313
|
+
const summary = await writeAuditsToAirtable({ base, websites, slug, results: siteResults });
|
|
314
|
+
written.push(summary);
|
|
315
|
+
if (mirror && summary.siteId && summary.fields && Object.keys(summary.fields).length > 0) {
|
|
316
|
+
try {
|
|
317
|
+
if (await mirror(summary.siteId, summary.fields)) mirrored++;
|
|
318
|
+
else mirrorMissed++;
|
|
319
|
+
} catch (e) {
|
|
320
|
+
mirrorFailed++;
|
|
321
|
+
console.error(`[health-mirror] ${slug}: ${e.message}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
306
324
|
} catch (e) {
|
|
307
325
|
failed.push({ slug, error: e.message });
|
|
308
326
|
}
|
|
309
327
|
}
|
|
310
|
-
return { written, failed };
|
|
328
|
+
return { written, failed, ...mirror ? { mirrored, mirrorFailed, mirrorMissed } : {} };
|
|
311
329
|
}
|
|
312
330
|
|
|
313
331
|
export {
|
|
@@ -315,4 +333,4 @@ export {
|
|
|
315
333
|
formatFleetWriteSummary,
|
|
316
334
|
writeFleetAuditsToAirtable
|
|
317
335
|
};
|
|
318
|
-
//# sourceMappingURL=chunk-
|
|
336
|
+
//# sourceMappingURL=chunk-PPE5KZJK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/audits/a11y-airtable.ts","../src/audits/deps-airtable.ts","../src/audits/security-airtable.ts","../src/audits/domain-airtable.ts","../src/audits/browser-airtable.ts","../src/audits/netlify-deploy-airtable.ts","../src/audits/function-health-airtable.ts","../src/audits/smoke-airtable.ts","../src/audits/form-e2e-airtable.ts","../src/audits/write-audits-to-airtable.ts"],"sourcesContent":["import type { AuditResult } from \"../types.js\";\n\ntype A11yDetails = {\n totalViolations: number;\n byImpact: Partial<Record<\"minor\" | \"moderate\" | \"serious\" | \"critical\", number>>;\n};\n\n/** True when an a11y AuditResult carries real counts worth persisting.\n * Mirrors the `hasRealScores` policy on lighthouse: write whenever real\n * data exists, regardless of status (a \"warn\" or \"fail\" with concrete\n * violation counts is exactly what the dashboard needs to track). */\nexport function hasA11yCounts(result: AuditResult): boolean {\n if (result.audit !== \"a11y\") return false;\n const details = result.details as A11yDetails | undefined;\n return typeof details?.totalViolations === \"number\";\n}\n\nexport function a11yCountsFromResult(result: AuditResult): { violations: number } {\n if (result.audit !== \"a11y\") {\n throw new Error(`Expected an 'a11y' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as A11yDetails | undefined;\n return { violations: details?.totalViolations ?? 0 };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { DepsDetails, DepsDriftEntry } from \"./deps.js\";\n\n/** True when a deps AuditResult carries the structured details (an entries\n * array, per {@link DepsDetails}). */\nexport function hasDepsCounts(result: AuditResult): boolean {\n if (result.audit !== \"deps\") return false;\n const d = result.details as Partial<DepsDetails> | undefined;\n return Array.isArray(d?.entries);\n}\n\nexport function depsCountsFromResult(result: AuditResult): {\n drifted: number;\n majorBehind: number;\n outdated: number | null;\n majorOutdated: number | null;\n} {\n if (result.audit !== \"deps\") {\n throw new Error(`Expected a 'deps' AuditResult, got '${result.audit}'`);\n }\n const details = (result.details ?? {}) as Partial<DepsDetails>;\n const entries = (details.entries ?? []) as DepsDriftEntry[];\n // \"drifted\" = drift !== \"same\", which intentionally includes \"newer\" (ahead of\n // baseline) — parity with the deps audit summary text.\n const drifted = entries.filter((e) => e.drift !== \"same\").length;\n const majorBehind = entries.filter((e) => e.drift === \"major\").length;\n // The real installed-version drift, distinct from declared-range drift. Null\n // when the audit couldn't determine it (no/stale lockfile) — kept null (not 0)\n // so the dashboard shows \"—\" rather than a misleading \"clean\".\n const outdated = details.outdated?.outdated ?? null;\n // How many of those outdated installs are a *major* behind the registry's\n // latest — surfaced separately from the total so the cockpit can show\n // \"N major\" available, distinct from the baseline-drift \"major\" count. Shares\n // the outdated signal's null-when-undetermined semantics.\n const majorOutdated = details.outdated?.major ?? null;\n return { drifted, majorBehind, outdated, majorOutdated };\n}\n","import type { AuditResult } from \"../types.js\";\nimport { normalizeSecurityAdvisory, type SecurityAdvisory } from \"../reports/airtable/websites.js\";\n\ntype SecurityDetails = {\n counts: { low: number; moderate: number; high: number; critical: number };\n advisories?: unknown;\n};\n\n/** True when a security AuditResult carries a counts object. Skipped runs\n * (no pnpm + no npm) have no details and return false. */\nexport function hasSecurityCounts(result: AuditResult): boolean {\n if (result.audit !== \"security\") return false;\n const details = result.details as SecurityDetails | undefined;\n return !!details && typeof details.counts === \"object\";\n}\n\nexport function securityCountsFromResult(result: AuditResult): {\n critical: number;\n high: number;\n moderate: number;\n low: number;\n} {\n if (result.audit !== \"security\") {\n throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as SecurityDetails | undefined;\n const c = details?.counts ?? { low: 0, moderate: 0, high: 0, critical: 0 };\n return { critical: c.critical, high: c.high, moderate: c.moderate, low: c.low };\n}\n\n/** The known advisories behind the counts, normalized for persistence. Empty when the\n * run was clean or carried no advisory detail (npm/pnpm sometimes omit it). Malformed\n * entries are dropped via the shared `normalizeSecurityAdvisory`. */\nexport function advisoriesFromResult(result: AuditResult): SecurityAdvisory[] {\n if (result.audit !== \"security\") {\n throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as SecurityDetails | undefined;\n const raw = details?.advisories;\n if (!Array.isArray(raw)) return [];\n return raw.map(normalizeSecurityAdvisory).filter((a): a is SecurityAdvisory => a !== null);\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { DomainResult } from \"../reports/airtable/websites.js\";\n\ntype DomainDetails = { resolved: boolean; certDaysRemaining: number | null; checkedAt: string };\n\n/** True when an AuditResult is a `domain` audit carrying a usable details payload (i.e. it\n * actually ran — a \"skip\" for a site with no deployed URL has no details). */\nexport function hasDomainResult(result: AuditResult): boolean {\n if (result.audit !== \"domain\") return false;\n const d = result.details as DomainDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable domain result (cert days + checked-at) from a `domain` audit. */\nexport function domainResultFromAudit(result: AuditResult): DomainResult {\n if (result.audit !== \"domain\") {\n throw new Error(`Expected a 'domain' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as DomainDetails | undefined;\n return {\n certDaysRemaining: typeof d?.certDaysRemaining === \"number\" ? d.certDaysRemaining : null,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { BrowserAuditFields } from \"../reports/airtable/websites.js\";\n\ntype BrowserDetails = {\n desktopOk: boolean;\n mobileOk: boolean;\n linksOk: boolean;\n reachableOk: boolean;\n titleMetaOk: boolean;\n brokenLinks: number;\n checkedAt: string;\n};\n\n/** True when an AuditResult is a `browser` audit that actually ran (has a details payload — a\n * \"skip\" for a site with no deployed URL has none). */\nexport function hasBrowserResult(result: AuditResult): boolean {\n if (result.audit !== \"browser\") return false;\n const d = result.details as BrowserDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable browser verdicts from a `browser` audit. */\nexport function browserFieldsFromAudit(result: AuditResult): BrowserAuditFields {\n if (result.audit !== \"browser\") {\n throw new Error(`Expected a 'browser' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as BrowserDetails | undefined;\n return {\n desktopOk: d?.desktopOk === true,\n mobileOk: d?.mobileOk === true,\n linksOk: d?.linksOk === true,\n reachableOk: d?.reachableOk === true,\n titleMetaOk: d?.titleMetaOk === true,\n brokenLinks: typeof d?.brokenLinks === \"number\" ? d.brokenLinks : 0,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { NetlifyDeployResult } from \"../reports/airtable/websites.js\";\n\ntype NetlifyDeployDetails = {\n state: string | null;\n deployedAt: string | null;\n logUrl: string | null;\n checkedAt: string;\n};\n\n/** True when an AuditResult is a `netlify-deploy` audit carrying a usable details\n * payload (i.e. it actually ran — a \"skip\" for a site with no Netlify id / no\n * token has no details). */\nexport function hasNetlifyDeployResult(result: AuditResult): boolean {\n if (result.audit !== \"netlify-deploy\") return false;\n const d = result.details as NetlifyDeployDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable deploy result (state + deployedAt + logUrl +\n * checkedAt) from a `netlify-deploy` audit. */\nexport function netlifyDeployResultFromAudit(result: AuditResult): NetlifyDeployResult {\n if (result.audit !== \"netlify-deploy\") {\n throw new Error(`Expected a 'netlify-deploy' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as NetlifyDeployDetails | undefined;\n return {\n state: typeof d?.state === \"string\" ? d.state : null,\n deployedAt: typeof d?.deployedAt === \"string\" ? d.deployedAt : null,\n logUrl: typeof d?.logUrl === \"string\" ? d.logUrl : null,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { FunctionHealthResult } from \"../reports/airtable/websites.js\";\n\ntype FunctionHealthDetails = {\n ok: boolean;\n prismic: \"ok\" | \"error\" | \"skipped\" | null;\n forms: unknown;\n checkedAt: string;\n};\n\n/** True when an AuditResult is a `function-health` audit carrying a usable details payload (i.e. it\n * actually ran — a self-skip for an unreachable / non-JSON `/health`, or a site with no deployed\n * URL, has no details, so the writer preserves the prior verdict). */\nexport function hasFunctionHealthResult(result: AuditResult): boolean {\n if (result.audit !== \"function-health\") return false;\n const d = result.details as FunctionHealthDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable function-health verdicts. `functionHealth` from `ok`; `cmsReachable`\n * from the same body's `prismic` sub-status (R2.2): `\"ok\"` → `\"pass\"`, `\"error\"` → `\"fail\"`, and\n * anything else (`\"skipped\"` — a placeholder repo, or a raw `null` from a \"deployed but erroring\"\n * body / an unrecognized value) → `null` — the CMS probe never actually ran, so it must not red CMS\n * reachability for a site that simply hasn't wired Prismic yet. Only `functionHealth` keys off\n * `ok`. */\nexport function functionHealthResultFromAudit(result: AuditResult): FunctionHealthResult {\n if (result.audit !== \"function-health\") {\n throw new Error(`Expected a 'function-health' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as FunctionHealthDetails | undefined;\n const cmsReachable: \"pass\" | \"fail\" | null =\n d?.prismic === \"ok\" ? \"pass\" : d?.prismic === \"error\" ? \"fail\" : null;\n // `forms` is untyped on the details payload (unknown) — validate defensively. A boolean\n // `turnstile` inside an object → concrete verdict; anything else (null forms from an older\n // site package or a \"deployed but erroring\" body, a malformed shape) → null: the widget\n // state is simply unknown this run, never a fail. Powers the Require-Turnstile guardrail.\n const formsRaw = d?.forms;\n const turnstileFlag =\n formsRaw && typeof formsRaw === \"object\"\n ? (formsRaw as Record<string, unknown>)[\"turnstile\"]\n : undefined;\n const turnstileWidget: \"pass\" | \"fail\" | null =\n typeof turnstileFlag === \"boolean\" ? (turnstileFlag ? \"pass\" : \"fail\") : null;\n return {\n functionHealth: d?.ok === true ? \"pass\" : \"fail\",\n cmsReachable,\n turnstileWidget,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { SmokeResult } from \"../reports/airtable/websites.js\";\n\ntype SmokeDetails = { ok: \"pass\" | \"fail\"; checkedAt: string };\n\n/** True when an AuditResult is a `smoke` audit that actually ran (has details with\n * a checkedAt — a \"skip\" for a missing `pnpm`/`test:smoke` script has none, so the\n * writer preserves the prior verdict). */\nexport function hasSmokeResult(result: AuditResult): boolean {\n if (result.audit !== \"smoke\") return false;\n const d = result.details as SmokeDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable smoke verdict (pass/fail + checked-at). */\nexport function smokeResultFromAudit(result: AuditResult): SmokeResult {\n if (result.audit !== \"smoke\") {\n throw new Error(`Expected a 'smoke' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as SmokeDetails | undefined;\n return {\n ok: d?.ok === \"fail\" ? \"fail\" : \"pass\",\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { FormE2eResult } from \"../reports/airtable/websites.js\";\n\ntype FormE2eDetails = { ok: \"pass\" | \"fail\" | null; formPresent: boolean; checkedAt: string };\n\n/** True when an AuditResult is a `form-e2e` audit that actually RAN (has details\n * with a checkedAt). Both a real verdict AND the no-form n/a case carry a checkedAt,\n * so both persist; only a no-deployed-URL skip (no details) is preserved-prior. */\nexport function hasFormE2eResult(result: AuditResult): boolean {\n if (result.audit !== \"form-e2e\") return false;\n const d = result.details as FormE2eDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable form-e2e verdict (pass/fail/null + checked-at). */\nexport function formE2eResultFromAudit(result: AuditResult): FormE2eResult {\n if (result.audit !== \"form-e2e\") {\n throw new Error(`Expected a 'form-e2e' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as FormE2eDetails | undefined;\n const ok = d?.ok === \"pass\" ? \"pass\" : d?.ok === \"fail\" ? \"fail\" : null;\n return {\n ok,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { FieldSet } from \"airtable\";\nimport type { AuditResult } from \"../types.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport type { HealthMirror } from \"./health-mirror.js\";\nimport { type WebsiteRow, siteSlug, updateAuditFields } from \"../reports/airtable/websites.js\";\nimport type {\n A11yCounts,\n DepsCounts,\n SecurityCounts,\n SecurityAdvisory,\n DomainResult,\n BrowserAuditFields,\n NetlifyDeployResult,\n FunctionHealthResult,\n SmokeResult,\n FormE2eResult,\n} from \"../reports/airtable/websites.js\";\nimport type { LighthouseScoreWriteback } from \"../reports/types.js\";\nimport { hasRealScores, lighthouseScoresFromResult } from \"./lighthouse-airtable.js\";\nimport { hasA11yCounts, a11yCountsFromResult } from \"./a11y-airtable.js\";\nimport { hasDepsCounts, depsCountsFromResult } from \"./deps-airtable.js\";\nimport {\n hasSecurityCounts,\n securityCountsFromResult,\n advisoriesFromResult,\n} from \"./security-airtable.js\";\nimport { hasDomainResult, domainResultFromAudit } from \"./domain-airtable.js\";\nimport { hasBrowserResult, browserFieldsFromAudit } from \"./browser-airtable.js\";\nimport { hasNetlifyDeployResult, netlifyDeployResultFromAudit } from \"./netlify-deploy-airtable.js\";\nimport {\n hasFunctionHealthResult,\n functionHealthResultFromAudit,\n} from \"./function-health-airtable.js\";\nimport { hasSmokeResult, smokeResultFromAudit } from \"./smoke-airtable.js\";\nimport { hasFormE2eResult, formE2eResultFromAudit } from \"./form-e2e-airtable.js\";\nimport { detectAuditEvents } from \"./fleet-event-detectors.js\";\nimport type { FleetEvent } from \"../db/fleet-events.js\";\n\ntype WriteSummary = {\n siteName: string;\n writes: Array<{\n audit:\n | \"lighthouse\"\n | \"a11y\"\n | \"deps\"\n | \"security\"\n | \"github-signals\"\n | \"domain\"\n | \"browser\"\n | \"netlify-deploy\"\n | \"function-health\"\n | \"smoke\"\n | \"form-e2e\"\n // Not an audit: the nightly Prismic model sweep reuses this result shape so\n // its write-back reports through the same `FLEET_WRITE_SUMMARY` line every\n // other fleet writer emits, rather than inventing a second contract for CI\n // to grep.\n | \"prismic-models\";\n counts: object;\n }>;\n /** Fleet-activity events detected from this site's prior row vs the fresh audits.\n * Optional: only the fleet path records them; the single-site path ignores them. */\n events?: FleetEvent[];\n /** The Websites rec id + the merged FieldSet actually written — what the\n * Phase 3 Turso mirror consumes, so mirror and Airtable write share ONE\n * payload. Populated by writeAuditsToAirtable; other WriteSummary\n * producers (prismic-models, github-signals) don't carry them. */\n siteId?: string;\n fields?: FieldSet;\n};\n\n/** Orchestrates the per-audit Airtable writes for `audit --write-airtable`.\n * Extracted from the CLI command so it can be unit-tested with a fake base\n * and so adding new audit types is a one-line addition here rather than\n * growing the CLI handler.\n *\n * Throws (with .exitCode set) on the failure modes the CLI surfaces today:\n * - 2: --only ran without lighthouse, or no Websites row matched the slug\n * - 1: lighthouse ran but produced no real scores (infrastructure failure).\n * The a11y/deps/security writes still complete FIRST — a Lighthouse\n * miss flags the site without discarding its other audit data.\n *\n * Precedence note: the Websites-row lookup (exitCode 2) is checked BEFORE the\n * no-scores gate (exitCode 1), so the rare no-row + no-scores combo surfaces\n * as exitCode 2. */\nexport async function writeAuditsToAirtable(args: {\n base: AirtableBase;\n websites: WebsiteRow[];\n slug: string;\n results: AuditResult[];\n}): Promise<WriteSummary> {\n const { base, websites, slug, results } = args;\n\n // Lighthouse is OPTIONAL. The checkout-free `--only lighthouse,domain,browser` nightly includes\n // it, but a standalone `--only security` (checkout-ful) sweep legitimately has none. We write\n // whatever audits produced values; only the lighthouse-MISS flag below is lighthouse-specific\n // (so a real Lighthouse infra failure still reds the run without discarding other audit data).\n const lhResult = results.find((r) => r.audit === \"lighthouse\");\n const target = websites.find((w) => siteSlug(w.name) === slug);\n if (!target) {\n throw Object.assign(new Error(`No Websites row matched slug \"${slug}\"`), { exitCode: 2 });\n }\n\n const writes: WriteSummary[\"writes\"] = [];\n const audits: {\n scores?: LighthouseScoreWriteback;\n a11y?: A11yCounts;\n deps?: DepsCounts;\n security?: SecurityCounts;\n securityAdvisories?: SecurityAdvisory[];\n domain?: DomainResult;\n browser?: BrowserAuditFields;\n netlifyDeploy?: NetlifyDeployResult;\n functionHealth?: FunctionHealthResult;\n smoke?: SmokeResult;\n formE2e?: FormE2eResult;\n } = {};\n\n // Collect every audit that produced real values into ONE merged input, then do a\n // SINGLE atomic Airtable update (was: up to four sequential updates on the same\n // row — a mid-sequence failure left it half-written yet reported fully failed, at\n // 4× the request volume). Lighthouse is the most timeout-prone audit: a Lighthouse\n // miss must NOT discard the site's valid a11y/deps/security results (morning-brief\n // 2026-06-10 MEDIUM-E). So include Lighthouse scores only when real, include the\n // other audits whenever present, write all of them in one update, then — if\n // Lighthouse missed — throw AFTER that atomic write so the site is still flagged\n // (exitCode 1 / collected in FleetWriteResult.failed) without losing its other data.\n const lhHasScores = lhResult ? hasRealScores(lhResult) : false;\n if (lhResult && lhHasScores) {\n const scores = lighthouseScoresFromResult(lhResult);\n audits.scores = scores;\n writes.push({ audit: \"lighthouse\", counts: scores });\n }\n\n const a11y = results.find((r) => r.audit === \"a11y\");\n if (a11y && hasA11yCounts(a11y)) {\n const counts = a11yCountsFromResult(a11y);\n audits.a11y = counts;\n writes.push({ audit: \"a11y\", counts });\n }\n\n const deps = results.find((r) => r.audit === \"deps\");\n if (deps && hasDepsCounts(deps)) {\n const counts = depsCountsFromResult(deps);\n audits.deps = counts;\n writes.push({ audit: \"deps\", counts });\n }\n\n const sec = results.find((r) => r.audit === \"security\");\n if (sec && hasSecurityCounts(sec)) {\n const counts = securityCountsFromResult(sec);\n audits.security = counts;\n // Persist the advisory list alongside the counts so the dashboard can show which packages\n // are vulnerable. An empty array (clean run) clears any stale list.\n audits.securityAdvisories = advisoriesFromResult(sec);\n writes.push({ audit: \"security\", counts });\n }\n\n const dom = results.find((r) => r.audit === \"domain\");\n if (dom && hasDomainResult(dom)) {\n const result = domainResultFromAudit(dom);\n audits.domain = result;\n writes.push({ audit: \"domain\", counts: result });\n }\n\n const browser = results.find((r) => r.audit === \"browser\");\n if (browser && hasBrowserResult(browser)) {\n const fields = browserFieldsFromAudit(browser);\n audits.browser = fields;\n writes.push({ audit: \"browser\", counts: fields });\n }\n\n const netlifyDeploy = results.find((r) => r.audit === \"netlify-deploy\");\n if (netlifyDeploy && hasNetlifyDeployResult(netlifyDeploy)) {\n const result = netlifyDeployResultFromAudit(netlifyDeploy);\n audits.netlifyDeploy = result;\n writes.push({ audit: \"netlify-deploy\", counts: result });\n }\n\n const functionHealth = results.find((r) => r.audit === \"function-health\");\n if (functionHealth && hasFunctionHealthResult(functionHealth)) {\n const result = functionHealthResultFromAudit(functionHealth);\n audits.functionHealth = result;\n writes.push({ audit: \"function-health\", counts: result });\n }\n\n const smoke = results.find((r) => r.audit === \"smoke\");\n if (smoke && hasSmokeResult(smoke)) {\n const result = smokeResultFromAudit(smoke);\n audits.smoke = result;\n writes.push({ audit: \"smoke\", counts: result });\n }\n\n const formE2e = results.find((r) => r.audit === \"form-e2e\");\n if (formE2e && hasFormE2eResult(formE2e)) {\n const result = formE2eResultFromAudit(formE2e);\n audits.formE2e = result;\n writes.push({ audit: \"form-e2e\", counts: result });\n }\n\n // One atomic write of everything that ran. Skip the call only if there is nothing\n // to write at all (no real scores AND no other audit produced values) — an empty\n // update is a wasted request.\n let fields: FieldSet = {};\n if (Object.keys(audits).length > 0) {\n fields = await updateAuditFields(base, target.id, audits);\n }\n\n // Detect fleet-activity transitions from the prior row (`target`, loaded before this\n // write) vs the fresh audits. Computed here where both are in hand; recorded by the\n // caller (fleet path) — single-site callers simply ignore `events`.\n const events = detectAuditEvents(\n target,\n {\n ...(audits.security !== undefined ? { security: audits.security } : {}),\n ...(audits.domain !== undefined ? { domain: audits.domain } : {}),\n },\n new Date().toISOString(),\n );\n\n // Lighthouse-miss flag: only when lighthouse WAS requested (in results) but produced no scores —\n // an infra failure worth reding the run, AFTER persisting the other audits. A sweep that never\n // ran lighthouse (e.g. `--only security`) skips this entirely.\n if (lhResult && !lhHasScores) {\n // Enumerate what WAS persisted so the failure (surfaced to the single-site\n // CLI operator via console.error) reads as a partial write, not a total one.\n const persisted = writes.map((w) => w.audit);\n throw Object.assign(\n new Error(\n `Lighthouse audit produced no scores; ${\n persisted.length ? `wrote ${persisted.join(\"/\")} but refused Lighthouse` : \"wrote nothing\"\n }. Summary: ${lhResult.summary}`,\n ),\n { exitCode: 1 },\n );\n }\n\n return { siteName: target.name, writes, events, siteId: target.id, fields };\n}\n\nexport type FleetWriteResult = {\n written: WriteSummary[];\n failed: Array<{ slug: string; error: string }>;\n /** Turso write-through counts (#539 Phase 3 dual-write). Present ONLY when a\n * mirror was wired — absent means mirroring was not attempted (no libSQL\n * creds), which must stay distinguishable from `mirrored=0` (a wired mirror\n * that landed nothing = the fleet-wide outage alarm). */\n mirrored?: number;\n mirrorFailed?: number;\n /** Mirror UPDATEs that matched no site_health row (site created in Airtable\n * after the last hourly import). Not a failure — the row converges on the\n * next sync — but not mirrored either: counted apart so `mirrored=` stays\n * an honest Phase 5 cutover signal instead of overcounting by the no-row\n * sites. */\n mirrorMissed?: number;\n};\n\n/** Render the fleet write-back outcome for the CLI/CI. Beyond the human-readable\n * lines, it emits a single deterministic, machine-parseable line —\n * `FLEET_WRITE_SUMMARY wrote=N failed=M total=T` — that the nightly workflow\n * greps to decide pass/fail. Keying CI on this line (not the prose, and not a\n * \"wrote ≥ 1\" heuristic) lets the gate tolerate a single known flake while\n * still reding on a total or mass write-back failure. */\nexport function formatFleetWriteSummary(result: FleetWriteResult): string {\n const wrote = result.written.length;\n const failed = result.failed.length;\n const total = wrote + failed;\n let out = `→ wrote ${wrote} site(s) to Airtable`;\n if (failed > 0) {\n out += `\\n⚠ ${failed} site(s) not written: ${result.failed\n .map((f) => `${f.slug} (${f.error})`)\n .join(\"; \")}`;\n }\n out += `\\nFLEET_WRITE_SUMMARY wrote=${wrote} failed=${failed} total=${total}`;\n // Mirror counts APPEND so the fleet workflows' `grep -oE \"FLEET_WRITE_SUMMARY\n // wrote=... total=[0-9]+\"` extraction still matches its prefix untouched.\n if (result.mirrored !== undefined) {\n out += ` mirrored=${result.mirrored} mirror_failed=${result.mirrorFailed ?? 0}`;\n out += ` mirror_missed=${result.mirrorMissed ?? 0}`;\n }\n return out;\n}\n\n/** Write each site's pooled audit results back to its own Websites row,\n * best-effort. Results are grouped by `result.site` (the slug the fleet\n * inventory stamped as Site.name). A per-site failure (no scores, no matching\n * row) is collected — not thrown — so one bad site never aborts the batch. */\nexport async function writeFleetAuditsToAirtable(args: {\n base: AirtableBase;\n websites: WebsiteRow[];\n results: AuditResult[];\n /** Optional Turso write-through (#539 Phase 3 dual-write). Each site's\n * just-written FieldSet mirrors into site_health right after its Airtable\n * write; a mirror failure is counted, never thrown — Airtable stays\n * authoritative and the hourly sync converges what the mirror missed.\n * (The lighthouse-miss partial write lands in `failed` and is deliberately\n * NOT mirrored — same convergence path.) */\n mirror?: HealthMirror;\n}): Promise<FleetWriteResult> {\n const { base, websites, results, mirror } = args;\n\n const bySlug = new Map<string, AuditResult[]>();\n for (const r of results) {\n const arr = bySlug.get(r.site) ?? [];\n arr.push(r);\n bySlug.set(r.site, arr);\n }\n\n const written: WriteSummary[] = [];\n const failed: FleetWriteResult[\"failed\"] = [];\n let mirrored = 0;\n let mirrorFailed = 0;\n let mirrorMissed = 0;\n // Serial on purpose: even at one (now atomic) update call per site, Airtable's\n // ~5 req/sec limit means a Promise.all fan-out across the fleet would burst and\n // trip 429s (silently filed as failures). Below a few dozen sites, serial trades\n // wall-clock for safety. (morning-brief 2026-06-09 MEDIUM-3.) Add a bounded pool\n // when the fleet grows.\n for (const [slug, siteResults] of bySlug) {\n try {\n const summary = await writeAuditsToAirtable({ base, websites, slug, results: siteResults });\n written.push(summary);\n if (mirror && summary.siteId && summary.fields && Object.keys(summary.fields).length > 0) {\n try {\n if (await mirror(summary.siteId, summary.fields)) mirrored++;\n else mirrorMissed++;\n } catch (e) {\n mirrorFailed++;\n console.error(`[health-mirror] ${slug}: ${(e as Error).message}`);\n }\n }\n } catch (e) {\n failed.push({ slug, error: (e as Error).message });\n }\n }\n return { written, failed, ...(mirror ? { mirrored, mirrorFailed, mirrorMissed } : {}) };\n}\n"],"mappings":"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","names":["fields"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/util/free-port.ts
|
|
2
|
+
import { createServer } from "net";
|
|
3
|
+
async function findFreePort() {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const server = createServer();
|
|
6
|
+
server.unref();
|
|
7
|
+
server.on("error", reject);
|
|
8
|
+
server.listen(0, "127.0.0.1", () => {
|
|
9
|
+
const addr = server.address();
|
|
10
|
+
if (typeof addr === "object" && addr) {
|
|
11
|
+
const port = addr.port;
|
|
12
|
+
server.close(() => resolve(port));
|
|
13
|
+
} else {
|
|
14
|
+
server.close();
|
|
15
|
+
reject(new Error("findFreePort: could not determine assigned port from socket"));
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function withFreePort(url, port) {
|
|
21
|
+
const u = new URL(url);
|
|
22
|
+
u.hostname = "localhost";
|
|
23
|
+
u.port = String(port);
|
|
24
|
+
return u.toString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
findFreePort,
|
|
29
|
+
withFreePort
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=chunk-PTDMVGKL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/util/free-port.ts"],"sourcesContent":["import { createServer } from \"node:net\";\n\n/**\n * Bind an ephemeral TCP port, capture it, release it, and return it. Used\n * by the lighthouse and a11y audits to pick a port the audit's own dev\n * server will then bind via `--strictPort`.\n *\n * Why: vite's default behavior on a busy port is to bump to the next free\n * one (5173 → 5174 → …). When zombie vite processes (or any squatter) are\n * already on 5173, the audit's spawned vite lands on a higher port, but\n * the audit tooling (lhci, playwright) still probes 5173 — hits the\n * zombie — gets stale 404s — fails with \"no manifest written\" / \"no\n * results written (exit 1)\". Reproduced on caltex 2026-05-28 with 10\n * orphaned vite processes accumulated across this repo, the reports repo,\n * and caltex itself. Allocating a free port up front + `--strictPort`\n * makes the audit immune to port collisions.\n *\n * TOCTOU note: the small window between close() and the spawned vite\n * binding is theoretically racy, but in practice we run one audit at a\n * time and the OS keeps the port free for re-use. If vite still fails to\n * bind under `--strictPort`, the audit fails loudly — that's the correct\n * outcome (vs. silently auditing the wrong server).\n */\nexport async function findFreePort(): Promise<number> {\n return new Promise((resolve, reject) => {\n const server = createServer();\n server.unref();\n server.on(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const addr = server.address();\n if (typeof addr === \"object\" && addr) {\n const port = addr.port;\n server.close(() => resolve(port));\n } else {\n server.close();\n reject(new Error(\"findFreePort: could not determine assigned port from socket\"));\n }\n });\n });\n}\n\n/**\n * Swap the port (and force `localhost` host) on a URL so it points at the\n * audit's freshly-allocated dev server. Preserves the path + any query.\n * Used to rewrite the lighthouse `url` so lhci probes the correct port.\n */\nexport function withFreePort(url: string, port: number): string {\n const u = new URL(url);\n u.hostname = \"localhost\";\n u.port = String(port);\n return u.toString();\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAuB7B,eAAsB,eAAgC;AACpD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAS,aAAa;AAC5B,WAAO,MAAM;AACb,WAAO,GAAG,SAAS,MAAM;AACzB,WAAO,OAAO,GAAG,aAAa,MAAM;AAClC,YAAM,OAAO,OAAO,QAAQ;AAC5B,UAAI,OAAO,SAAS,YAAY,MAAM;AACpC,cAAM,OAAO,KAAK;AAClB,eAAO,MAAM,MAAM,QAAQ,IAAI,CAAC;AAAA,MAClC,OAAO;AACL,eAAO,MAAM;AACb,eAAO,IAAI,MAAM,6DAA6D,CAAC;AAAA,MACjF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAOO,SAAS,aAAa,KAAa,MAAsB;AAC9D,QAAM,IAAI,IAAI,IAAI,GAAG;AACrB,IAAE,WAAW;AACb,IAAE,OAAO,OAAO,IAAI;AACpB,SAAO,EAAE,SAAS;AACpB;","names":[]}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// src/reports/due.ts
|
|
2
|
-
var ELIGIBLE_STATUSES = /* @__PURE__ */ new Set([
|
|
2
|
+
var ELIGIBLE_STATUSES = /* @__PURE__ */ new Set([
|
|
3
|
+
"maintained",
|
|
4
|
+
"hosted-only"
|
|
5
|
+
]);
|
|
3
6
|
var MONTHS = {
|
|
4
7
|
Monthly: 1,
|
|
5
8
|
Quarterly: 3,
|
|
@@ -65,4 +68,4 @@ export {
|
|
|
65
68
|
findDueReports,
|
|
66
69
|
reportPeriodKey
|
|
67
70
|
};
|
|
68
|
-
//# sourceMappingURL=chunk-
|
|
71
|
+
//# sourceMappingURL=chunk-PZR4EBPY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/reports/due.ts"],"sourcesContent":["import type { WebsiteRow, Frequency, Status } from \"./airtable/websites.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport type { ReportType } from \"./types.js\";\n\n/** Statuses where recurring Maintenance/Testing reports are appropriate. Only\n * LIVE sites: \"maintained\" and \"hosted-only\". Pre-launch\n * stages (\"building\" / \"launching\") are excluded — a not-yet-live site\n * must not be drafted a recurring Maintenance/Testing report (that's what was\n * emailing/alarming pre-launch sites); it starts its report cadence when a Launch\n * report flips its Status to \"maintained\". Launch reports themselves are a\n * separate manual flow (recipes/launch.ts), never scheduled here. \"archived\" /\n * \"external\" are dropped too. Sites with status=null pass through\n * (partial data; better to surface than silently skip). */\nexport const ELIGIBLE_STATUSES: ReadonlySet<Status> = new Set<Status>([\n \"maintained\",\n \"hosted-only\",\n]);\n\nexport type DueItem = {\n site: WebsiteRow;\n reportType: ReportType;\n /** Inclusive: the day the next report became due. */\n dueDate: Date;\n /** ISO date of the last `Sent at` for this (site, type), or null if there's never been one. */\n lastSent: string | null;\n};\n\nconst MONTHS: Record<Exclude<Frequency, \"None\">, number> = {\n Monthly: 1,\n Quarterly: 3,\n Yearly: 12,\n};\n\n/**\n * Add `n` calendar months in UTC, clamped to the last day of the target month.\n * Jan 31 + 1 month = Feb 28 (not Mar 3, which is what naive setMonth produces).\n * All-UTC accessors mean the result is timezone-independent.\n */\nfunction addMonths(d: Date, n: number): Date {\n const out = new Date(d);\n const day = out.getUTCDate();\n out.setUTCDate(1);\n out.setUTCMonth(out.getUTCMonth() + n);\n const lastDayOfTargetMonth = new Date(\n Date.UTC(out.getUTCFullYear(), out.getUTCMonth() + 1, 0),\n ).getUTCDate();\n out.setUTCDate(Math.min(day, lastDayOfTargetMonth));\n return out;\n}\n\n/** Truncate to UTC midnight. Avoids local-TZ skew when comparing Airtable date-only fields. */\nfunction startOfDay(d: Date): Date {\n const out = new Date(d);\n out.setUTCHours(0, 0, 0, 0);\n return out;\n}\n\nfunction lastSentForType(reports: ReportRow[], siteId: string, type: ReportType): string | null {\n const candidates = reports\n .filter((r) => r.siteId === siteId && r.reportType === type && r.sentAt !== null)\n .map((r) => r.sentAt!)\n .sort();\n return candidates[candidates.length - 1] ?? null;\n}\n\n/**\n * The next-due date for one (site, type): the date the next report of that type is\n * scheduled to draft, whether or not it's due yet. `null` when there's no schedule —\n * an ineligible status or a \"None\" frequency. Unrecognized/blank raw Airtable values\n * never reach here: mapRow's `toFrequency` trims, warns LOUDLY, and coerces them to\n * \"None\" at the read boundary.\n *\n * baseDate = the last `Sent at` for this (site, type), else the site's\n * `maintenance day`/`testing day` anchor. With no baseDate at all the next report is\n * due now (returns `today` at UTC midnight). Otherwise baseDate + frequency.\n *\n * Shared with {@link findDueReports} so the scheduler and any schedule display can't\n * drift on what \"next\" means.\n */\nexport function nextDueDate(\n site: WebsiteRow,\n reports: ReportRow[],\n type: ReportType,\n today: Date,\n): Date | null {\n if (site.status !== null && !ELIGIBLE_STATUSES.has(site.status)) return null;\n const freq = type === \"Maintenance\" ? site.maintenanceFreq : site.testingFreq;\n if (freq === \"None\") return null;\n const lastSent = lastSentForType(reports, site.id, type);\n const fallback = type === \"Maintenance\" ? site.maintenanceDay : site.testingDay;\n const baseIso = lastSent ?? fallback;\n if (!baseIso) return startOfDay(today);\n return addMonths(new Date(baseIso), MONTHS[freq]);\n}\n\n/**\n * Computes which (site, type) pairs are due as of `today`.\n *\n * Algorithm per (site, type):\n * 1. If freq === \"None\", skip.\n * 2. baseDate = max(last Sent at for this type, site's `maintenance/testing day` fallback).\n * 3. If no baseDate exists at all, the site is due now.\n * 4. dueDate = baseDate + frequency months.\n * 5. Due iff startOfDay(today) >= startOfDay(dueDate).\n */\nexport function findDueReports(\n websites: WebsiteRow[],\n reports: ReportRow[],\n today: Date,\n): DueItem[] {\n const out: DueItem[] = [];\n const todayStart = startOfDay(today);\n\n for (const site of websites) {\n // Skip explicitly-non-active statuses (\"archived\", \"external\").\n // Null status is treated as active for backwards compat with rows that pre-date\n // the Status convention.\n if (site.status !== null && !ELIGIBLE_STATUSES.has(site.status)) continue;\n\n for (const type of [\"Maintenance\", \"Testing\"] as const) {\n const freq = type === \"Maintenance\" ? site.maintenanceFreq : site.testingFreq;\n // Intentional silent skip — \"None\" (also the coerced default for blank cells)\n // means \"no schedule\", not a mistake. A trailing-space or typo'd raw Airtable\n // value never reaches here: mapRow's `toFrequency` trims (so \"Quarterly \"\n // schedules) and warns LOUDLY on anything still unrecognized before coercing\n // it to \"None\" — the guard lives at the read boundary, not in this loop.\n if (freq === \"None\") continue;\n\n const lastSent = lastSentForType(reports, site.id, type);\n // Same computation as the schedule write-back uses. The guards above already\n // proved a schedule exists, so dueDate is non-null; a no-anchor site returns\n // `today`, so the comparison below pushes it as due-now (the prior behavior).\n const dueDate = nextDueDate(site, reports, type, today);\n if (dueDate !== null && todayStart.getTime() >= startOfDay(dueDate).getTime()) {\n out.push({ site, reportType: type, dueDate, lastSent });\n }\n }\n }\n\n return out;\n}\n\n/**\n * The UTC `YYYY-MM` of a `dueDate` from {@link findDueReports} — the per-recurrence\n * idempotency key for drafting. Monthly recurrences land in distinct months; quarterly\n * and yearly land in distinct due-months too, so this uniquely names one draft per cycle.\n * UTC accessors keep it timezone-independent, consistent with the rest of this module.\n */\nexport function reportPeriodKey(dueDate: Date): string {\n if (Number.isNaN(dueDate.getTime())) throw new TypeError(\"reportPeriodKey: invalid Date\");\n const year = dueDate.getUTCFullYear();\n const month = String(dueDate.getUTCMonth() + 1).padStart(2, \"0\");\n return `${year}-${month}`;\n}\n"],"mappings":";AAaO,IAAM,oBAAyC,oBAAI,IAAY;AAAA,EACpE;AAAA,EACA;AACF,CAAC;AAWD,IAAM,SAAqD;AAAA,EACzD,SAAS;AAAA,EACT,WAAW;AAAA,EACX,QAAQ;AACV;AAOA,SAAS,UAAU,GAAS,GAAiB;AAC3C,QAAM,MAAM,IAAI,KAAK,CAAC;AACtB,QAAM,MAAM,IAAI,WAAW;AAC3B,MAAI,WAAW,CAAC;AAChB,MAAI,YAAY,IAAI,YAAY,IAAI,CAAC;AACrC,QAAM,uBAAuB,IAAI;AAAA,IAC/B,KAAK,IAAI,IAAI,eAAe,GAAG,IAAI,YAAY,IAAI,GAAG,CAAC;AAAA,EACzD,EAAE,WAAW;AACb,MAAI,WAAW,KAAK,IAAI,KAAK,oBAAoB,CAAC;AAClD,SAAO;AACT;AAGA,SAAS,WAAW,GAAe;AACjC,QAAM,MAAM,IAAI,KAAK,CAAC;AACtB,MAAI,YAAY,GAAG,GAAG,GAAG,CAAC;AAC1B,SAAO;AACT;AAEA,SAAS,gBAAgB,SAAsB,QAAgB,MAAiC;AAC9F,QAAM,aAAa,QAChB,OAAO,CAAC,MAAM,EAAE,WAAW,UAAU,EAAE,eAAe,QAAQ,EAAE,WAAW,IAAI,EAC/E,IAAI,CAAC,MAAM,EAAE,MAAO,EACpB,KAAK;AACR,SAAO,WAAW,WAAW,SAAS,CAAC,KAAK;AAC9C;AAgBO,SAAS,YACd,MACA,SACA,MACA,OACa;AACb,MAAI,KAAK,WAAW,QAAQ,CAAC,kBAAkB,IAAI,KAAK,MAAM,EAAG,QAAO;AACxE,QAAM,OAAO,SAAS,gBAAgB,KAAK,kBAAkB,KAAK;AAClE,MAAI,SAAS,OAAQ,QAAO;AAC5B,QAAM,WAAW,gBAAgB,SAAS,KAAK,IAAI,IAAI;AACvD,QAAM,WAAW,SAAS,gBAAgB,KAAK,iBAAiB,KAAK;AACrE,QAAM,UAAU,YAAY;AAC5B,MAAI,CAAC,QAAS,QAAO,WAAW,KAAK;AACrC,SAAO,UAAU,IAAI,KAAK,OAAO,GAAG,OAAO,IAAI,CAAC;AAClD;AAYO,SAAS,eACd,UACA,SACA,OACW;AACX,QAAM,MAAiB,CAAC;AACxB,QAAM,aAAa,WAAW,KAAK;AAEnC,aAAW,QAAQ,UAAU;AAI3B,QAAI,KAAK,WAAW,QAAQ,CAAC,kBAAkB,IAAI,KAAK,MAAM,EAAG;AAEjE,eAAW,QAAQ,CAAC,eAAe,SAAS,GAAY;AACtD,YAAM,OAAO,SAAS,gBAAgB,KAAK,kBAAkB,KAAK;AAMlE,UAAI,SAAS,OAAQ;AAErB,YAAM,WAAW,gBAAgB,SAAS,KAAK,IAAI,IAAI;AAIvD,YAAM,UAAU,YAAY,MAAM,SAAS,MAAM,KAAK;AACtD,UAAI,YAAY,QAAQ,WAAW,QAAQ,KAAK,WAAW,OAAO,EAAE,QAAQ,GAAG;AAC7E,YAAI,KAAK,EAAE,MAAM,YAAY,MAAM,SAAS,SAAS,CAAC;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAQO,SAAS,gBAAgB,SAAuB;AACrD,MAAI,OAAO,MAAM,QAAQ,QAAQ,CAAC,EAAG,OAAM,IAAI,UAAU,+BAA+B;AACxF,QAAM,OAAO,QAAQ,eAAe;AACpC,QAAM,QAAQ,OAAO,QAAQ,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AAC/D,SAAO,GAAG,IAAI,IAAI,KAAK;AACzB;","names":[]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isHttpUrl
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-EVBCK2KC.js";
|
|
4
4
|
import {
|
|
5
5
|
ACTIVE_STATUSES,
|
|
6
6
|
isPreLaunch,
|
|
7
7
|
listWebsites,
|
|
8
8
|
siteSlug
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LDWZSLSU.js";
|
|
10
10
|
|
|
11
11
|
// src/inventory/airtable.ts
|
|
12
12
|
function fromAirtableBase(base, opts = {}) {
|
|
@@ -50,4 +50,4 @@ function fromAirtableBase(base, opts = {}) {
|
|
|
50
50
|
export {
|
|
51
51
|
fromAirtableBase
|
|
52
52
|
};
|
|
53
|
-
//# sourceMappingURL=chunk-
|
|
53
|
+
//# sourceMappingURL=chunk-SLUQ7R6I.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/inventory/airtable.ts"],"sourcesContent":["import type { Site, InventoryProvider } from \"../types.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport {\n listWebsites,\n siteSlug,\n ACTIVE_STATUSES,\n isPreLaunch,\n} from \"../reports/airtable/websites.js\";\nimport { isHttpUrl } from \"../util/url.js\";\n\nexport type AirtableInventoryOptions = {\n /**\n * Local workdir to compute each site's path as `{workdir}/{slug}`.\n * Defaults to REDDOOR_FLEET_WORKDIR env var if not provided.\n * Airtable doesn't store local checkout paths, so this is required.\n */\n workdir?: string;\n};\n\n/**\n * Read sites from the Airtable Websites table as an InventoryProvider.\n * Each row becomes one Site; `path` is computed as `{workdir}/{slug}`.\n * Only LIVE `maintenance` sites that have a `url` are included — pre-launch\n * stages (\"launching\" / \"building\", via isPreLaunch) are excluded so a\n * not-yet-live site is never audited as production (its deploy/domain/uptime/CMS\n * audits would fail against nothing and red its Airtable row) nor swept by other\n * `--fleet airtable` ops; it re-enters the fleet when a Launch report flips its\n * Status to \"maintained\". The production URL is exposed as `Site.deployedUrl` so\n * the lighthouse audit can run against it with no checkout. `repoUrl` is\n * intentionally NOT set from `url` — a clone source must come from `gitRepo`\n * (`owner/repo`), never the production URL.\n */\nexport function fromAirtableBase(\n base: AirtableBase,\n opts: AirtableInventoryOptions = {},\n): InventoryProvider {\n return async (): Promise<Site[]> => {\n const workdir = opts.workdir ?? process.env.REDDOOR_FLEET_WORKDIR;\n if (!workdir) {\n throw new Error(\n \"fromAirtableBase requires `workdir` option or REDDOOR_FLEET_WORKDIR env (sites need a local path)\",\n );\n }\n const websites = await listWebsites(base);\n return websites\n .filter(\n (w) =>\n w.status !== null &&\n ACTIVE_STATUSES.has(w.status) &&\n !isPreLaunch(w.status) &&\n w.url.length > 0,\n )\n .flatMap((w) => {\n const slug = siteSlug(w.name);\n // An empty slug (a Name with no slug-able characters) can't form a stable\n // path and — fatally — can't be matched back to its Websites row on\n // write-back: every empty-slug site would collapse under the \"\" key and\n // mis-write or fail. Skip it loudly rather than silently mis-map it.\n if (slug.length === 0) {\n console.warn(\n `[inventory] skipping \"${w.name}\" (row ${w.id}): Name has no slug-able characters (empty slug)`,\n );\n return [];\n }\n const site: Site = {\n path: `${workdir}/${slug}`,\n name: slug,\n meta: { airtableRowId: w.id, displayName: w.name },\n };\n // Scheme-allowlist the Airtable `url` before exposing it as the\n // deployed-audit target (it's handed straight to Chrome/lhci). A\n // `file://`/`gopher://`/internal-host value would be a local-file read\n // or SSRF — skip the deployed audit for that site rather than trust it.\n if (isHttpUrl(w.url)) {\n site.deployedUrl = w.url;\n } else {\n console.warn(\n `[inventory] skipping deployed audit for \"${w.name}\": url is not http(s): ${JSON.stringify(w.url)}`,\n );\n }\n if (w.gitRepo) site.gitRepo = w.gitRepo;\n // Expose the Netlify site id (operator-set identity column) so the\n // netlify-deploy audit can query the API with no checkout. Absent → that\n // audit skips for this site. Not derived from the URL.\n if (w.netlifyId) site.netlifyId = w.netlifyId;\n return [site];\n });\n };\n}\n"],"mappings":";;;;;;;;;;;AAgCO,SAAS,iBACd,MACA,OAAiC,CAAC,GACf;AACnB,SAAO,YAA6B;AAClC,UAAM,UAAU,KAAK,WAAW,QAAQ,IAAI;AAC5C,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,UAAM,WAAW,MAAM,aAAa,IAAI;AACxC,WAAO,SACJ;AAAA,MACC,CAAC,MACC,EAAE,WAAW,QACb,gBAAgB,IAAI,EAAE,MAAM,KAC5B,CAAC,YAAY,EAAE,MAAM,KACrB,EAAE,IAAI,SAAS;AAAA,IACnB,EACC,QAAQ,CAAC,MAAM;AACd,YAAM,OAAO,SAAS,EAAE,IAAI;AAK5B,UAAI,KAAK,WAAW,GAAG;AACrB,gBAAQ;AAAA,UACN,yBAAyB,EAAE,IAAI,UAAU,EAAE,EAAE;AAAA,QAC/C;AACA,eAAO,CAAC;AAAA,MACV;AACA,YAAM,OAAa;AAAA,QACjB,MAAM,GAAG,OAAO,IAAI,IAAI;AAAA,QACxB,MAAM;AAAA,QACN,MAAM,EAAE,eAAe,EAAE,IAAI,aAAa,EAAE,KAAK;AAAA,MACnD;AAKA,UAAI,UAAU,EAAE,GAAG,GAAG;AACpB,aAAK,cAAc,EAAE;AAAA,MACvB,OAAO;AACL,gBAAQ;AAAA,UACN,4CAA4C,EAAE,IAAI,0BAA0B,KAAK,UAAU,EAAE,GAAG,CAAC;AAAA,QACnG;AAAA,MACF;AACA,UAAI,EAAE,QAAS,MAAK,UAAU,EAAE;AAIhC,UAAI,EAAE,UAAW,MAAK,YAAY,EAAE;AACpC,aAAO,CAAC,IAAI;AAAA,IACd,CAAC;AAAA,EACL;AACF;","names":[]}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fetchGaUsers,
|
|
3
3
|
fetchSearch
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-BYK66EWO.js";
|
|
5
5
|
import {
|
|
6
6
|
announcementSiteExtras,
|
|
7
7
|
resolveCopy
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-CQR4LIM7.js";
|
|
9
9
|
import {
|
|
10
10
|
siteSlug
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-LDWZSLSU.js";
|
|
12
12
|
|
|
13
13
|
// src/reports/report-data.ts
|
|
14
14
|
var PREVIEW_WINDOW_DAYS = 30;
|
|
@@ -60,4 +60,4 @@ export {
|
|
|
60
60
|
scoresFromRow,
|
|
61
61
|
buildReportDataForSite
|
|
62
62
|
};
|
|
63
|
-
//# sourceMappingURL=chunk-
|
|
63
|
+
//# sourceMappingURL=chunk-TERE5SDF.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defaultSpawn
|
|
3
|
-
} from "./chunk-4DHQCN5H.js";
|
|
4
1
|
import {
|
|
5
2
|
sameOwnerRepo
|
|
6
3
|
} from "./chunk-XTK5VIQB.js";
|
|
4
|
+
import {
|
|
5
|
+
defaultSpawn
|
|
6
|
+
} from "./chunk-LBYOLBW7.js";
|
|
7
7
|
|
|
8
8
|
// src/cli/fleet/clone-if-needed.ts
|
|
9
9
|
import { stat, readdir, mkdir } from "fs/promises";
|
|
@@ -150,4 +150,4 @@ export {
|
|
|
150
150
|
appendSkipNotice,
|
|
151
151
|
fleetWorkdir
|
|
152
152
|
};
|
|
153
|
-
//# sourceMappingURL=chunk-
|
|
153
|
+
//# sourceMappingURL=chunk-TVL5MVUF.js.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findFreePort
|
|
3
|
+
} from "./chunk-PTDMVGKL.js";
|
|
4
|
+
import {
|
|
5
|
+
defaultSpawn,
|
|
6
|
+
isSpawnTimeout
|
|
7
|
+
} from "./chunk-LBYOLBW7.js";
|
|
8
|
+
import {
|
|
9
|
+
siteLabel
|
|
10
|
+
} from "./chunk-XXTZBPUY.js";
|
|
11
|
+
|
|
12
|
+
// src/audits/smoke.ts
|
|
13
|
+
import { access, readFile } from "fs/promises";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
var SMOKE_TIMEOUT_MS = 15 * 6e4;
|
|
16
|
+
var SMOKE_UNMEASURED_PREFIX = "smoke: NOT MEASURED";
|
|
17
|
+
function isUnmeasuredSmoke(result) {
|
|
18
|
+
return result.audit === "smoke" && result.summary.startsWith(SMOKE_UNMEASURED_PREFIX);
|
|
19
|
+
}
|
|
20
|
+
function formatUnmeasuredSmokeSummary(results) {
|
|
21
|
+
const unmeasured = results.filter(isUnmeasuredSmoke).map((r) => r.site);
|
|
22
|
+
let out = "";
|
|
23
|
+
if (unmeasured.length > 0) {
|
|
24
|
+
out += `\u26A0 ${unmeasured.length} site(s) never measured: ${unmeasured.join("; ")}
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
out += `FLEET_SMOKE_UNMEASURED count=${unmeasured.length} sites=${unmeasured.join(",")}`;
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
31
|
+
function summarizeSmokeFailure(stdout, stderr) {
|
|
32
|
+
const lines = stdout.replace(ANSI, "").split("\n").map((l) => l.trim());
|
|
33
|
+
const failing = [];
|
|
34
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35
|
+
const line = lines[i];
|
|
36
|
+
if (line === void 0) continue;
|
|
37
|
+
if (/^\d+\)\s/.test(line)) {
|
|
38
|
+
failing.push(line);
|
|
39
|
+
let taken = 0;
|
|
40
|
+
for (let j = i + 1; j < lines.length && taken < 3; j++) {
|
|
41
|
+
const next = lines[j];
|
|
42
|
+
if (!next) continue;
|
|
43
|
+
if (/^\d+\)\s/.test(next)) break;
|
|
44
|
+
failing.push(next);
|
|
45
|
+
taken++;
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const tally = lines.find((l) => l !== void 0 && /\b\d+\s+failed\b/.test(l));
|
|
51
|
+
const distilled = [tally, ...failing].filter(Boolean).join(" | ");
|
|
52
|
+
if (distilled) return distilled.slice(0, 300);
|
|
53
|
+
const err = stderr.replace(ANSI, "").trim();
|
|
54
|
+
return err ? err.slice(0, 200) : "no reporter output";
|
|
55
|
+
}
|
|
56
|
+
async function hasTestSmokeScript(sitePath) {
|
|
57
|
+
try {
|
|
58
|
+
const raw = await readFile(join(sitePath, "package.json"), "utf-8");
|
|
59
|
+
const pkg = JSON.parse(raw);
|
|
60
|
+
return typeof pkg.scripts?.["test:smoke"] === "string";
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function hasNodeModules(sitePath) {
|
|
66
|
+
try {
|
|
67
|
+
await access(join(sitePath, "node_modules"));
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function smokeAudit(ctx) {
|
|
74
|
+
const spawn = ctx.spawn ?? defaultSpawn;
|
|
75
|
+
const site = ctx.site;
|
|
76
|
+
const label = siteLabel(site);
|
|
77
|
+
const now = ctx.now ?? /* @__PURE__ */ new Date();
|
|
78
|
+
const checkedAt = now.toISOString();
|
|
79
|
+
if (!await hasTestSmokeScript(site.path)) {
|
|
80
|
+
return {
|
|
81
|
+
audit: "smoke",
|
|
82
|
+
site: label,
|
|
83
|
+
status: "skip",
|
|
84
|
+
summary: "no test:smoke script"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (!await hasNodeModules(site.path)) {
|
|
88
|
+
let install;
|
|
89
|
+
try {
|
|
90
|
+
install = await spawn("pnpm", ["install", "--frozen-lockfile"], {
|
|
91
|
+
cwd: site.path,
|
|
92
|
+
timeoutMs: 5 * 6e4
|
|
93
|
+
});
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const e = err;
|
|
96
|
+
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
|
|
97
|
+
return { audit: "smoke", site: label, status: "skip", summary: "pnpm not available" };
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
if (install.code !== 0) {
|
|
102
|
+
const retry = await spawn("pnpm", ["install"], { cwd: site.path, timeoutMs: 5 * 6e4 });
|
|
103
|
+
if (retry.code !== 0) {
|
|
104
|
+
return {
|
|
105
|
+
audit: "smoke",
|
|
106
|
+
site: label,
|
|
107
|
+
status: "skip",
|
|
108
|
+
summary: `smoke: pnpm install failed (exit ${retry.code}) \u2014 deps unavailable`
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
await spawn("pnpm", ["exec", "svelte-kit", "sync"], {
|
|
115
|
+
cwd: site.path,
|
|
116
|
+
timeoutMs: 6e4
|
|
117
|
+
});
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
const port = await findFreePort();
|
|
121
|
+
let raw;
|
|
122
|
+
try {
|
|
123
|
+
raw = await spawn("pnpm", ["test:smoke"], {
|
|
124
|
+
cwd: site.path,
|
|
125
|
+
env: { ...process.env, REDDOOR_SMOKE_PORT: String(port) },
|
|
126
|
+
// Playwright on a cold tree installs chromium, boots the site's dev server,
|
|
127
|
+
// and runs the smoke specs — the shared 30s default starves it (mirrors a11y).
|
|
128
|
+
timeoutMs: SMOKE_TIMEOUT_MS
|
|
129
|
+
});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
const e = err;
|
|
132
|
+
if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
|
|
133
|
+
return { audit: "smoke", site: label, status: "skip", summary: "pnpm not available" };
|
|
134
|
+
}
|
|
135
|
+
if (isSpawnTimeout(err)) {
|
|
136
|
+
const minutes = Math.round(SMOKE_TIMEOUT_MS / 6e4);
|
|
137
|
+
return {
|
|
138
|
+
audit: "smoke",
|
|
139
|
+
site: label,
|
|
140
|
+
status: "fail",
|
|
141
|
+
summary: `${SMOKE_UNMEASURED_PREFIX} \u2014 \`pnpm test:smoke\` exceeded its ${minutes}m budget; no verdict, prior Airtable value preserved`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
if (raw.code === 0) {
|
|
147
|
+
return {
|
|
148
|
+
audit: "smoke",
|
|
149
|
+
site: label,
|
|
150
|
+
status: "pass",
|
|
151
|
+
summary: "smoke: suite green",
|
|
152
|
+
details: { ok: "pass", checkedAt }
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
audit: "smoke",
|
|
157
|
+
site: label,
|
|
158
|
+
status: "fail",
|
|
159
|
+
summary: `smoke: suite failed (exit ${raw.code}) \u2014 ${summarizeSmokeFailure(raw.stdout, raw.stderr)}`,
|
|
160
|
+
details: { ok: "fail", checkedAt }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export {
|
|
165
|
+
SMOKE_TIMEOUT_MS,
|
|
166
|
+
SMOKE_UNMEASURED_PREFIX,
|
|
167
|
+
isUnmeasuredSmoke,
|
|
168
|
+
formatUnmeasuredSmokeSummary,
|
|
169
|
+
summarizeSmokeFailure,
|
|
170
|
+
smokeAudit
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=chunk-UDRHAQT7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/audits/smoke.ts"],"sourcesContent":["import { access, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { defaultSpawn, isSpawnTimeout } from \"./util/spawn.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { findFreePort } from \"../util/free-port.js\";\n\n/** Persisted smoke verdict: the site's own `test:smoke` suite passed or failed. */\nexport type SmokeDetails = { ok: \"pass\" | \"fail\"; checkedAt: string };\n\n/** Wall-clock budget for a site's own `test:smoke` run.\n *\n * Measured, not guessed. reddoor-website's `Smoke test` step takes **4m57s** on a\n * 2-core GitHub runner with chromium already installed and `node_modules` warm\n * (run 32413378638, 2026-08-20). The old budget was 5m00s — three seconds of\n * headroom — and the fleet path is strictly heavier than that step: the site's\n * `test:smoke` is `playwright install chromium && playwright test`, so the browser\n * install lands INSIDE this budget, on a fresh clone, after `svelte-kit sync`.\n *\n * So the budget was already effectively negative, and the medtech release (#133)\n * pushed the suite past it: reddoor was killed at 5m03s and beachfront-dentistry\n * at 5m04s — both the wall, not their suites.\n *\n * 15 minutes is ~3x the measured suite, leaving room for a site to grow a quarter's\n * worth of specs before anyone has to think about this again, while still killing a\n * genuinely wedged run far inside fleet-smoke.yml's 90-minute step backstop. */\nexport const SMOKE_TIMEOUT_MS = 15 * 60_000;\n\n/** Summary prefix for a smoke run that produced no verdict at all.\n *\n * A timeout is NOT a failing suite — it means the measurement never happened. The\n * two must stay distinguishable: a failing suite is a finding about the site, an\n * unmeasured one is a finding about this audit. Exported so the CI summary keys on\n * the same constant the audit stamps, instead of re-describing it in a grep that\n * can drift out of sync. */\nexport const SMOKE_UNMEASURED_PREFIX = \"smoke: NOT MEASURED\";\n\n/** True when a smoke audit never reached a verdict (timed out mid-suite).\n *\n * Deliberately distinct from {@link hasSmokeResult}: an unmeasured run carries no\n * `details`, so the Airtable writer already preserves the prior verdict rather than\n * recording a false fail. That is correct — and it is also why nothing surfaced it.\n * This predicate is what makes it visible to CI. */\nexport function isUnmeasuredSmoke(result: { audit: string; summary: string }): boolean {\n return result.audit === \"smoke\" && result.summary.startsWith(SMOKE_UNMEASURED_PREFIX);\n}\n\n/** Render the \"did every site actually get measured?\" line for CI.\n *\n * Emits `FLEET_SMOKE_UNMEASURED count=N sites=a,b` on EVERY run, count=0 included —\n * deliberately, and for the same reason `FLEET_WRITE_SUMMARY` does. A gate that only\n * prints on failure cannot be told apart from a gate that never ran, so the workflow\n * treats an ABSENT line as a crash and a `count=0` line as a proven-clean sweep. That\n * distinction is the whole point: this alarm exists because the previous one was\n * structurally incapable of firing, and an alarm nobody has seen pass is not evidence.\n *\n * Only `smoke` results are considered; other audits in the same pool are ignored. */\nexport function formatUnmeasuredSmokeSummary(\n results: ReadonlyArray<{ audit: string; site: string; summary: string }>,\n): string {\n const unmeasured = results.filter(isUnmeasuredSmoke).map((r) => r.site);\n let out = \"\";\n if (unmeasured.length > 0) {\n out += `⚠ ${unmeasured.length} site(s) never measured: ${unmeasured.join(\"; \")}\\n`;\n }\n out += `FLEET_SMOKE_UNMEASURED count=${unmeasured.length} sites=${unmeasured.join(\",\")}`;\n return out;\n}\n\n// ESC built from a char code so the regex source carries no literal control\n// char (keeps `no-control-regex` quiet). Matches the SGR color codes Playwright emits.\nconst ANSI = new RegExp(`${String.fromCharCode(27)}\\\\[[0-9;]*m`, \"g\");\n\n/**\n * Distill the actionable failure out of a Playwright run. The list reporter writes\n * its failing-test list — \"N) [chromium] › file:line › title\" followed by the\n * Error/Expected/Received head — plus an \"N failed\" tally to STDOUT; STDERR only\n * carries dev-server/npm noise (e.g. `[WebServer] npm warn …`). So summarize stdout\n * first (which test, and why) and fall back to stderr only when stdout yielded\n * nothing useful (a crash before the reporter ran). Capped so a runaway report\n * can't bloat the CLI/Airtable summary.\n */\nexport function summarizeSmokeFailure(stdout: string, stderr: string): string {\n const lines = stdout\n .replace(ANSI, \"\")\n .split(\"\\n\")\n .map((l) => l.trim());\n const failing: string[] = [];\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line === undefined) continue;\n // The list reporter numbers each failing test: \"1) [chromium] › file › title\".\n if (/^\\d+\\)\\s/.test(line)) {\n failing.push(line);\n // Grab the next 3 NON-BLANK lines — the Error:/Expected:/Received: head.\n // Skip blanks without spending the budget, and stop at the next failing block.\n let taken = 0;\n for (let j = i + 1; j < lines.length && taken < 3; j++) {\n const next = lines[j];\n if (!next) continue;\n if (/^\\d+\\)\\s/.test(next)) break;\n failing.push(next);\n taken++;\n }\n break;\n }\n }\n const tally = lines.find((l) => l !== undefined && /\\b\\d+\\s+failed\\b/.test(l));\n const distilled = [tally, ...failing].filter(Boolean).join(\" | \");\n if (distilled) return distilled.slice(0, 300);\n const err = stderr.replace(ANSI, \"\").trim();\n return err ? err.slice(0, 200) : \"no reporter output\";\n}\n\n/**\n * R3.2: a site whose `package.json` has no `test:smoke` script (or no\n * `package.json` at all) has simply not adopted the suite yet — treat both\n * the same as \"can't tell\", NOT as a failure. Read BEFORE spawning so a\n * not-yet-adopted site never pays a real `pnpm` invocation.\n */\nasync function hasTestSmokeScript(sitePath: string): Promise<boolean> {\n try {\n const raw = await readFile(join(sitePath, \"package.json\"), \"utf-8\");\n const pkg = JSON.parse(raw) as { scripts?: Record<string, string> };\n return typeof pkg.scripts?.[\"test:smoke\"] === \"string\";\n } catch {\n return false;\n }\n}\n\nasync function hasNodeModules(sitePath: string): Promise<boolean> {\n try {\n await access(join(sitePath, \"node_modules\"));\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Run a site's own `pnpm test:smoke` suite in its checkout and reduce the exit\n * code to a verdict. Clone-based: the CLI (`prepareFleetSites`) has already put a\n * real checkout at `site.path` (smoke is NOT in CHECKOUT_FREE_AUDITS). Reuses the\n * a11y harness treatment: a {@link SMOKE_TIMEOUT_MS} budget (Playwright cold-boots the\n * dev server + installs chromium) and a freshly-allocated free port passed as REDDOOR_SMOKE_PORT\n * so the site's smoke playwright config can bind `--port <n> --strictPort` and stay\n * immune to a zombie-vite squatting 5173 (see free-port.ts).\n *\n * A site that hasn't adopted `test:smoke` yet (no script, or no package.json) →\n * skip (R3.2), same bucket as `pnpm` itself being unavailable. exit 0 → pass;\n * non-zero → fail (only reached once the suite is known to exist). A skip never\n * carries details, so the Airtable writer preserves the prior verdict.\n *\n * A suite that exceeds the budget is a THIRD outcome: it produced no verdict at all,\n * so it is reported as {@link SMOKE_UNMEASURED_PREFIX} and likewise carries no details.\n * That keeps Airtable on the prior value (right — nothing was learned) while letting\n * fleet-smoke.yml red the run, which is what the write-back gate alone cannot do.\n */\nexport async function smokeAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n const now = ctx.now ?? new Date();\n const checkedAt = now.toISOString();\n\n if (!(await hasTestSmokeScript(site.path))) {\n return {\n audit: \"smoke\",\n site: label,\n status: \"skip\",\n summary: \"no test:smoke script\",\n };\n }\n\n // The nightly fleet producer runs against FRESH clones (cloneIfNeeded does a\n // bare `git clone`, no install), so the site's own playwright/vite aren't on\n // PATH yet — without this, `pnpm test:smoke` exits non-zero and we'd persist a\n // FALSE Smoke OK=fail. Install only when node_modules is absent (a local\n // already-installed checkout is untouched). Any install failure → skip (NO\n // details), so the Airtable writer preserves the prior verdict rather than\n // recording a false fail. Mirrors deps-outdated.ts.\n if (!(await hasNodeModules(site.path))) {\n let install;\n try {\n install = await spawn(\"pnpm\", [\"install\", \"--frozen-lockfile\"], {\n cwd: site.path,\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return { audit: \"smoke\", site: label, status: \"skip\", summary: \"pnpm not available\" };\n }\n throw err;\n }\n if (install.code !== 0) {\n // Lockfile drift shouldn't red a site — retry unfrozen once (the clone is\n // ephemeral, so a rewritten lockfile is harmless). Still failing → skip.\n const retry = await spawn(\"pnpm\", [\"install\"], { cwd: site.path, timeoutMs: 5 * 60_000 });\n if (retry.code !== 0) {\n return {\n audit: \"smoke\",\n site: label,\n status: \"skip\",\n summary: `smoke: pnpm install failed (exit ${retry.code}) — deps unavailable`,\n };\n }\n }\n }\n\n // `playwright.config.ts` resolves `tsconfig.json`, which extends the GENERATED\n // `./.svelte-kit/tsconfig.json`. A fresh clone has no such file, and no fleet\n // repo carries a `prepare` script to write one, so Playwright aborted while\n // loading its own config — before a single test ran:\n //\n // Error: Failed to load tsconfig file at <site>/tsconfig.json:\n // Failed to resolve \"extends\" path \"./.svelte-kit/tsconfig.json\"\n //\n // That silently failed 9 of 11 live sites (found 2026-07-31). It stayed hidden\n // because fleet-smoke.yml gates on FLEET_WRITE_SUMMARY, which counts rows\n // WRITTEN, not rows passing — a failing site is still written, so the workflow\n // reported success.\n //\n // Fixing it here rather than per-repo: one change covers every site, and\n // AUTONOMY.md makes multi-repo mutations a human-reviewed operation. `sync` is\n // idempotent and fast, so run it unconditionally instead of probing for the\n // file. Best-effort by design — a non-SvelteKit site or a missing binary must\n // never downgrade a working suite; let the suite itself be the verdict.\n try {\n await spawn(\"pnpm\", [\"exec\", \"svelte-kit\", \"sync\"], {\n cwd: site.path,\n timeoutMs: 60_000,\n });\n } catch {\n // ENOENT (no pnpm) or a spawn failure — the `test:smoke` call below reports\n // the real outcome, including its own ENOENT skip.\n }\n\n const port = await findFreePort();\n\n let raw;\n try {\n raw = await spawn(\"pnpm\", [\"test:smoke\"], {\n cwd: site.path,\n env: { ...process.env, REDDOOR_SMOKE_PORT: String(port) },\n // Playwright on a cold tree installs chromium, boots the site's dev server,\n // and runs the smoke specs — the shared 30s default starves it (mirrors a11y).\n timeoutMs: SMOKE_TIMEOUT_MS,\n });\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return { audit: \"smoke\", site: label, status: \"skip\", summary: \"pnpm not available\" };\n }\n // A timeout is not a verdict. Rethrowing sent it to runOneAudit's catch-all,\n // which stringified it into `smoke: unexpected error — Error: spawn timeout…`:\n // technically a `fail`, carrying no details, so Airtable correctly preserved the\n // prior verdict — and therefore kept showing GREEN for a site that had not been\n // measured in days, while the workflow (gated on write-back) exited 0. Name it\n // instead, and leave `details` unset so the write-back behavior is unchanged.\n if (isSpawnTimeout(err)) {\n const minutes = Math.round(SMOKE_TIMEOUT_MS / 60_000);\n return {\n audit: \"smoke\",\n site: label,\n status: \"fail\",\n summary: `${SMOKE_UNMEASURED_PREFIX} — \\`pnpm test:smoke\\` exceeded its ${minutes}m budget; no verdict, prior Airtable value preserved`,\n };\n }\n throw err;\n }\n\n if (raw.code === 0) {\n return {\n audit: \"smoke\",\n site: label,\n status: \"pass\",\n summary: \"smoke: suite green\",\n details: { ok: \"pass\", checkedAt } satisfies SmokeDetails,\n };\n }\n return {\n audit: \"smoke\",\n site: label,\n status: \"fail\",\n summary: `smoke: suite failed (exit ${raw.code}) — ${summarizeSmokeFailure(raw.stdout, raw.stderr)}`,\n details: { ok: \"fail\", checkedAt } satisfies SmokeDetails,\n };\n}\n"],"mappings":"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","names":[]}
|