@reddoorla/maintenance 0.68.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/dist/airtable-2PJ4NSAV.js +9 -0
  2. package/dist/{announce-72R3LRNT.js → announce-3JE5FKW7.js} +6 -6
  3. package/dist/blux-FKZYMKLF.js +2205 -0
  4. package/dist/blux-FKZYMKLF.js.map +1 -0
  5. package/dist/{bump-deps-QNM3H4GI.js → bump-deps-K3JMLMBB.js} +3 -2
  6. package/dist/bump-deps-K3JMLMBB.js.map +1 -0
  7. package/dist/{chunk-RINXPU3H.js → chunk-2PWQ6MPG.js} +2 -2
  8. package/dist/{chunk-H6YZBXBQ.js → chunk-52TDNN7K.js} +37 -15
  9. package/dist/chunk-52TDNN7K.js.map +1 -0
  10. package/dist/{chunk-XWPZ46IN.js → chunk-5QC5KJHR.js} +2 -2
  11. package/dist/{chunk-O6G4HHMF.js → chunk-6J25IIAI.js} +162 -42
  12. package/dist/chunk-6J25IIAI.js.map +1 -0
  13. package/dist/chunk-7MJBTV2R.js +145 -0
  14. package/dist/chunk-7MJBTV2R.js.map +1 -0
  15. package/dist/{chunk-6JFOAPW4.js → chunk-AYM2EFSK.js} +38 -6
  16. package/dist/chunk-AYM2EFSK.js.map +1 -0
  17. package/dist/{chunk-GKSXZIMX.js → chunk-CMQMNFFJ.js} +77 -3
  18. package/dist/chunk-CMQMNFFJ.js.map +1 -0
  19. package/dist/{chunk-6MT5W6UF.js → chunk-F6OE6MGQ.js} +6 -3
  20. package/dist/chunk-F6OE6MGQ.js.map +1 -0
  21. package/dist/{chunk-OTBA7DON.js → chunk-J2SHWT2O.js} +1 -1
  22. package/dist/chunk-J2SHWT2O.js.map +1 -0
  23. package/dist/{chunk-XVR7UM6Z.js → chunk-JNXCDUYB.js} +4 -4
  24. package/dist/{chunk-2E2X5BCG.js → chunk-KOAGMTRI.js} +3 -3
  25. package/dist/chunk-KRDLHAKK.js +17 -0
  26. package/dist/chunk-KRDLHAKK.js.map +1 -0
  27. package/dist/{chunk-LYCKN7ZR.js → chunk-LPMQXS2N.js} +8 -4
  28. package/dist/chunk-LPMQXS2N.js.map +1 -0
  29. package/dist/{chunk-WCN54744.js → chunk-M7HOQ4W7.js} +388 -8
  30. package/dist/chunk-M7HOQ4W7.js.map +1 -0
  31. package/dist/{chunk-LCKKIJ22.js → chunk-MPRLMGJU.js} +11 -3
  32. package/dist/chunk-MPRLMGJU.js.map +1 -0
  33. package/dist/chunk-PKVZMFEB.js +315 -0
  34. package/dist/chunk-PKVZMFEB.js.map +1 -0
  35. package/dist/{chunk-A5KYRGQA.js → chunk-PSJQE6T6.js} +4 -4
  36. package/dist/chunk-SL24DCGC.js +41 -0
  37. package/dist/chunk-SL24DCGC.js.map +1 -0
  38. package/dist/{chunk-MAYHLXCS.js → chunk-VR246V3G.js} +25 -5
  39. package/dist/chunk-VR246V3G.js.map +1 -0
  40. package/dist/{chunk-45AUGDFR.js → chunk-WN4NEHE7.js} +2 -7
  41. package/dist/chunk-WN4NEHE7.js.map +1 -0
  42. package/dist/{chunk-YQMYQAJX.js → chunk-Y7LZHPDF.js} +2 -2
  43. package/dist/{chunk-C3PDS623.js → chunk-YWRQEDB3.js} +53 -3
  44. package/dist/chunk-YWRQEDB3.js.map +1 -0
  45. package/dist/cli/bin.js +63 -19
  46. package/dist/cli/bin.js.map +1 -1
  47. package/dist/cli/commands/audit.d.ts +1 -1
  48. package/dist/cli/commands/audit.js +13 -11
  49. package/dist/cli/commands/audit.js.map +1 -1
  50. package/dist/client/index.d.ts +84 -0
  51. package/dist/client/index.js +47 -0
  52. package/dist/client/index.js.map +1 -0
  53. package/dist/configs/playwright-a11y.js +1 -1
  54. package/dist/{convert-to-pnpm-XH7NYRVK.js → convert-to-pnpm-6F4RAFEJ.js} +3 -2
  55. package/dist/convert-to-pnpm-6F4RAFEJ.js.map +1 -0
  56. package/dist/{digest-J7TDG62Q.js → digest-GZCZYJKC.js} +11 -11
  57. package/dist/{ensure-site-2PF522VL.js → ensure-site-H5VMBPLF.js} +2 -2
  58. package/dist/forms/index.d.ts +2 -0
  59. package/dist/forms/index.js +7 -1
  60. package/dist/forms/index.js.map +1 -1
  61. package/dist/{github-signals-PP7GWYIY.js → github-signals-SQECQNST.js} +8 -8
  62. package/dist/health-endpoint-QXWM6GW2.js +54 -0
  63. package/dist/health-endpoint-QXWM6GW2.js.map +1 -0
  64. package/dist/index.d.ts +132 -17
  65. package/dist/index.js +213 -70
  66. package/dist/index.js.map +1 -1
  67. package/dist/{init-WLR35YGN.js → init-KQOIPLVT.js} +12 -8
  68. package/dist/init-KQOIPLVT.js.map +1 -0
  69. package/dist/{launch-2HPQXWII.js → launch-RX4VI7F2.js} +13 -13
  70. package/dist/{lighthouse-airtable-UBSXKHAQ.js → lighthouse-airtable-TTXULEGU.js} +3 -3
  71. package/dist/{onboard-X6KJ5Z2V.js → onboard-TFBL62J3.js} +2 -2
  72. package/dist/{orchestrate-PVCGBH4K.js → orchestrate-DYA4XAQJ.js} +6 -6
  73. package/dist/{preflight-KWVYJ2DU.js → preflight-S3GYME3Q.js} +8 -8
  74. package/dist/probe-DJAMLVTT.js +46 -0
  75. package/dist/probe-DJAMLVTT.js.map +1 -0
  76. package/dist/recipes/sync-configs.d.ts +1 -1
  77. package/dist/recipes/sync-configs.js +2 -2
  78. package/dist/{renovate-dispatch-LG4RJGDY.js → renovate-dispatch-MZPAWVBL.js} +2 -2
  79. package/dist/{report-ZBFMTT76.js → report-BLZ67QAQ.js} +12 -12
  80. package/dist/run-migration-LJNNZWIV.js +205 -0
  81. package/dist/run-migration-LJNNZWIV.js.map +1 -0
  82. package/dist/{self-updating-JTTFOJ7Q.js → self-updating-GSH427XP.js} +2 -2
  83. package/dist/{selftest-LFZPZRJP.js → selftest-KPTL6KC4.js} +11 -11
  84. package/dist/smoke-suite-GP3W57JP.js +54 -0
  85. package/dist/smoke-suite-GP3W57JP.js.map +1 -0
  86. package/dist/{svelte-codemods-KJLNH6FQ.js → svelte-codemods-GYZZX742.js} +3 -2
  87. package/dist/svelte-codemods-GYZZX742.js.map +1 -0
  88. package/dist/{sync-configs-6GBZLGRQ.js → sync-configs-CQNOTJ2B.js} +5 -4
  89. package/dist/sync-configs-CQNOTJ2B.js.map +1 -0
  90. package/dist/{types-Dg4mHvyf.d.ts → types-0jreMCF5.d.ts} +2 -2
  91. package/dist/{upgrade-TPTE26FG.js → upgrade-RTAJZD56.js} +3 -2
  92. package/dist/upgrade-RTAJZD56.js.map +1 -0
  93. package/dist/{websites-6YTT5ZTO.js → websites-SVI4KYSH.js} +8 -2
  94. package/dist/{write-audits-to-airtable-IWOO4FFH.js → write-audits-to-airtable-3XFNWMUE.js} +4 -4
  95. package/package.json +8 -1
  96. package/dist/airtable-WGQQ3KAD.js +0 -9
  97. package/dist/bump-deps-QNM3H4GI.js.map +0 -1
  98. package/dist/chunk-45AUGDFR.js.map +0 -1
  99. package/dist/chunk-6JFOAPW4.js.map +0 -1
  100. package/dist/chunk-6MT5W6UF.js.map +0 -1
  101. package/dist/chunk-C3PDS623.js.map +0 -1
  102. package/dist/chunk-GKSXZIMX.js.map +0 -1
  103. package/dist/chunk-H6YZBXBQ.js.map +0 -1
  104. package/dist/chunk-LCKKIJ22.js.map +0 -1
  105. package/dist/chunk-LYCKN7ZR.js.map +0 -1
  106. package/dist/chunk-MAYHLXCS.js.map +0 -1
  107. package/dist/chunk-O6G4HHMF.js.map +0 -1
  108. package/dist/chunk-OTBA7DON.js.map +0 -1
  109. package/dist/chunk-WCN54744.js.map +0 -1
  110. package/dist/convert-to-pnpm-XH7NYRVK.js.map +0 -1
  111. package/dist/init-WLR35YGN.js.map +0 -1
  112. package/dist/svelte-codemods-KJLNH6FQ.js.map +0 -1
  113. package/dist/sync-configs-6GBZLGRQ.js.map +0 -1
  114. package/dist/upgrade-TPTE26FG.js.map +0 -1
  115. /package/dist/{airtable-WGQQ3KAD.js.map → airtable-2PJ4NSAV.js.map} +0 -0
  116. /package/dist/{announce-72R3LRNT.js.map → announce-3JE5FKW7.js.map} +0 -0
  117. /package/dist/{chunk-RINXPU3H.js.map → chunk-2PWQ6MPG.js.map} +0 -0
  118. /package/dist/{chunk-XWPZ46IN.js.map → chunk-5QC5KJHR.js.map} +0 -0
  119. /package/dist/{chunk-XVR7UM6Z.js.map → chunk-JNXCDUYB.js.map} +0 -0
  120. /package/dist/{chunk-2E2X5BCG.js.map → chunk-KOAGMTRI.js.map} +0 -0
  121. /package/dist/{chunk-A5KYRGQA.js.map → chunk-PSJQE6T6.js.map} +0 -0
  122. /package/dist/{chunk-YQMYQAJX.js.map → chunk-Y7LZHPDF.js.map} +0 -0
  123. /package/dist/{digest-J7TDG62Q.js.map → digest-GZCZYJKC.js.map} +0 -0
  124. /package/dist/{ensure-site-2PF522VL.js.map → ensure-site-H5VMBPLF.js.map} +0 -0
  125. /package/dist/{github-signals-PP7GWYIY.js.map → github-signals-SQECQNST.js.map} +0 -0
  126. /package/dist/{launch-2HPQXWII.js.map → launch-RX4VI7F2.js.map} +0 -0
  127. /package/dist/{lighthouse-airtable-UBSXKHAQ.js.map → lighthouse-airtable-TTXULEGU.js.map} +0 -0
  128. /package/dist/{onboard-X6KJ5Z2V.js.map → onboard-TFBL62J3.js.map} +0 -0
  129. /package/dist/{orchestrate-PVCGBH4K.js.map → orchestrate-DYA4XAQJ.js.map} +0 -0
  130. /package/dist/{preflight-KWVYJ2DU.js.map → preflight-S3GYME3Q.js.map} +0 -0
  131. /package/dist/{renovate-dispatch-LG4RJGDY.js.map → renovate-dispatch-MZPAWVBL.js.map} +0 -0
  132. /package/dist/{report-ZBFMTT76.js.map → report-BLZ67QAQ.js.map} +0 -0
  133. /package/dist/{self-updating-JTTFOJ7Q.js.map → self-updating-GSH427XP.js.map} +0 -0
  134. /package/dist/{selftest-LFZPZRJP.js.map → selftest-KPTL6KC4.js.map} +0 -0
  135. /package/dist/{websites-6YTT5ZTO.js.map → websites-SVI4KYSH.js.map} +0 -0
  136. /package/dist/{write-audits-to-airtable-IWOO4FFH.js.map → write-audits-to-airtable-3XFNWMUE.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/reports/preflight.ts"],"sourcesContent":["import { openBase, readAirtableConfig } from \"./airtable/client.js\";\nimport type { AirtableBase } from \"./airtable/client.js\";\nimport { listWebsites, siteSlug } from \"./airtable/websites.js\";\nimport type { WebsiteRow } from \"./airtable/websites.js\";\nimport { listAllReports } from \"./airtable/reports.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { parseAddresses, isProbablyEmail } from \"./send/orchestrate.js\";\nimport { ELIGIBLE_STATUSES, reportPeriodKey } from \"./due.js\";\nimport type { ReportType } from \"./types.js\";\n\nexport type PreflightLevel = \"fail\" | \"warn\" | \"info\";\n\nexport type PreflightFinding = {\n level: PreflightLevel;\n /** Stable machine-readable check id, e.g. \"recipients-missing\". */\n check: string;\n message: string;\n};\n\nexport type PreflightSiteResult = {\n site: string;\n findings: PreflightFinding[];\n};\n\n/** Domains whose addresses are the OPERATOR's, not a client's. A client site whose\n * resolved To contains one of these is almost always a test-send leftover — the\n * exact misconfiguration that would silently divert a client announcement. */\nconst OPERATOR_DOMAINS = [\"reddoorla.com\", \"tuckerlemos.com\"];\n\n/** Anchors older than this, WHEN the anchor is actually the schedule's base date\n * (no newer Sent-at — see nextDueDate's `lastSent ?? fallback`), mean the next\n * `report --due` run drafts a surprise back-dated report. */\nconst STALE_ANCHOR_DAYS = 396; // ~13 months\n\nfunction domainOf(addr: string): string {\n const at = addr.lastIndexOf(\"@\");\n return at === -1 ? \"\" : addr.slice(at + 1).toLowerCase();\n}\n\nfunction isOperatorSite(site: WebsiteRow): boolean {\n if (!site.url) return false;\n try {\n // Airtable `url` cells aren't guaranteed a scheme; retry with one before giving up.\n const host = new URL(\n /^[a-z][a-z0-9+.-]*:\\/\\//i.test(site.url) ? site.url : `https://${site.url}`,\n ).hostname.toLowerCase();\n return OPERATOR_DOMAINS.some((d) => host === d || host.endsWith(`.${d}`));\n } catch {\n return false;\n }\n}\n\nfunction lastSentForType(reports: ReportRow[], type: ReportType): string | null {\n const sent = reports\n .filter((r) => r.reportType === type && r.sentAt !== null)\n .map((r) => r.sentAt!)\n .sort();\n return sent[sent.length - 1] ?? null;\n}\n\nfunction checkFrequency(\n site: WebsiteRow,\n which: \"maintenance\" | \"testing\",\n reports: ReportRow[],\n findings: PreflightFinding[],\n now: Date,\n): void {\n // Validate the RAW Airtable cell, not the coerced Frequency: toFrequency maps any\n // unrecognized value (typo, trailing space, renamed option) to \"None\", so by the\n // time WebsiteRow is built the damage is invisible — the site just silently drops\n // off the schedule. This is the loud version.\n const raw = which === \"maintenance\" ? site.maintenanceFreqRaw : site.testingFreqRaw;\n const coerced = which === \"maintenance\" ? site.maintenanceFreq : site.testingFreq;\n if (raw !== null && raw !== \"\" && coerced === \"None\" && raw.trim() !== \"None\") {\n findings.push({\n level: \"fail\",\n check: \"frequency-unrecognized\",\n message: `${which} frequency cell is '${raw}' — not an exact Monthly/Quarterly/Yearly/None, so the scheduler silently treats it as None and the site drops off the calendar; fix the Airtable value`,\n });\n return;\n }\n if (coerced === \"None\") return;\n\n const anchor = which === \"maintenance\" ? site.maintenanceDay : site.testingDay;\n if (!anchor) return;\n // nextDueDate uses `lastSent ?? anchor`: once anything has been sent, the anchor\n // is inert and an old one is normal on every healthy mature site — only warn when\n // the anchor is genuinely the base date the scheduler would use.\n const type: ReportType = which === \"maintenance\" ? \"Maintenance\" : \"Testing\";\n const lastSent = lastSentForType(reports, type);\n if (lastSent && new Date(lastSent).getTime() >= new Date(anchor).getTime()) return;\n const ageDays = (now.getTime() - new Date(anchor).getTime()) / 86_400_000;\n if (ageDays > STALE_ANCHOR_DAYS) {\n findings.push({\n level: \"warn\",\n check: \"anchor-stale\",\n message: `${which} day anchor is ${anchor} (>13 months old) and nothing newer has been sent — \\`report --due\\` will draft a back-dated overdue report; reset the anchor or clear the schedule`,\n });\n }\n}\n\n/**\n * Pure per-site preflight: every check that, left unfixed, makes a report send fail,\n * go to the wrong inbox, or surprise the operator. Read-only over data already\n * fetched; no Airtable handle, no network — trivially testable.\n *\n * Mirrors the send/draft-time validation (recipients + header image in\n * send/orchestrate.ts, Websites-row scores in draft.ts) so problems surface BEFORE\n * draft/approve instead of exploding at `report --send-ready`, and adds the checks\n * send time cannot do: operator-address leftovers, To-override shadowing,\n * pending-draft races, schedule hygiene against the RAW Airtable values.\n */\nexport function preflightSite(\n site: WebsiteRow,\n reports: ReportRow[],\n type: ReportType,\n now: Date,\n): PreflightSiteResult {\n const findings: PreflightFinding[] = [];\n\n // A site that isn't on the checked calendar never drafts, so its send\n // requirements (recipients / header image / scores) can't hurt anyone —\n // checking them anyway would drown the fleet run in fails for legacy hosting\n // rows. Frequency hygiene and pending-draft checks still run: a typo'd cell\n // is exactly HOW a site falls off the calendar, and an already-drafted report\n // can still be approved + sent regardless of schedule.\n const scheduledForType =\n type === \"Announcement\"\n ? true\n : type === \"Maintenance\"\n ? site.maintenanceFreq !== \"None\"\n : site.testingFreq !== \"None\";\n if (!scheduledForType) {\n findings.push({\n level: \"info\",\n check: \"not-scheduled\",\n message: `no ${type} schedule (frequency is None/blank) — send-requirement checks skipped; schedule + pending-draft checks still apply`,\n });\n }\n\n // --- Recipients: same resolution order as the send path (To override, else contact).\n const explicitTo = parseAddresses(site.reportRecipientsTo);\n const fallbackTo = parseAddresses(site.pointOfContact);\n const to = scheduledForType ? (explicitTo ?? fallbackTo ?? []) : [];\n if (scheduledForType && to.length === 0) {\n findings.push({\n level: \"fail\",\n check: \"recipients-missing\",\n message:\n \"no recipients — Report recipients (To) and point of contact are both empty; the send will throw\",\n });\n }\n for (const addr of to) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `recipient '${addr}' is malformed — use bare addresses only in Report recipients (To) / point of contact`,\n });\n }\n }\n const cc = scheduledForType ? (parseAddresses(site.reportRecipientsCc) ?? []) : [];\n for (const addr of cc) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `CC '${addr}' is malformed — fix Report recipients (CC)`,\n });\n }\n }\n if (!isOperatorSite(site)) {\n const operatorAddrs = to.filter((a) => OPERATOR_DOMAINS.includes(domainOf(a)));\n if (operatorAddrs.length > 0) {\n const allOperator = operatorAddrs.length === to.length;\n findings.push({\n level: \"warn\",\n check: \"recipient-operator-address\",\n message: allOperator\n ? `resolved To is ONLY operator address(es) ${operatorAddrs.join(\", \")} — probably a test-send leftover; the client will NOT receive this report`\n : `resolved To includes operator address(es) ${operatorAddrs.join(\", \")} alongside the client's — confirm that's intentional (the ops inbox is already CC'd on every send)`,\n });\n }\n }\n if (scheduledForType && explicitTo && fallbackTo) {\n const a = [...explicitTo]\n .map((x) => x.toLowerCase())\n .sort()\n .join(\",\");\n const b = [...fallbackTo]\n .map((x) => x.toLowerCase())\n .sort()\n .join(\",\");\n if (a !== b) {\n findings.push({\n level: \"info\",\n check: \"to-override-shadows-contact\",\n message: `Report recipients (To) [${explicitTo.join(\", \")}] overrides point of contact [${fallbackTo.join(\", \")}] — the contact won't receive this unless also listed`,\n });\n }\n }\n\n // --- Hard requirements of the draft/send paths, surfaced early.\n if (scheduledForType && !site.headerImage) {\n findings.push({\n level: \"fail\",\n check: \"header-image-missing\",\n message: \"no Header image on the Websites row — the send will throw\",\n });\n } else if (scheduledForType && site.headerImage && !site.headerImage.type.startsWith(\"image/\")) {\n findings.push({\n level: \"fail\",\n check: \"header-image-not-image\",\n message: `Header image attachment is '${site.headerImage.type}' (${site.headerImage.filename}) — not a decodable image; the send throws in prepareHeaderImage`,\n });\n }\n const scores = [site.pScore, site.rScore, site.bpScore, site.seoScore];\n if (scheduledForType && scores.some((s) => s === null)) {\n if (type === \"Announcement\") {\n // announce() skips score-less sites (skipped-no-scores) rather than throwing.\n findings.push({\n level: \"warn\",\n check: \"scores-missing\",\n message:\n \"Websites row is missing Lighthouse scores — `announce` will skip this site; run `audit lighthouse --write-airtable` first\",\n });\n } else {\n // report <site> / report --due HARD-THROW here (scoresFromWebsite in draft.ts).\n findings.push({\n level: \"fail\",\n check: \"scores-missing\",\n message:\n \"Websites row is missing Lighthouse scores — drafting will throw (scoresFromWebsite); run `audit lighthouse --write-airtable` first\",\n });\n }\n }\n if (type === \"Announcement\" && site.status !== \"maintenance\") {\n findings.push({\n level: \"info\",\n check: \"status-not-maintenance\",\n message: `status is '${site.status ?? \"(blank)\"}' — \\`announce --all\\` only drafts for maintenance sites`,\n });\n }\n\n // --- Pending drafts. For an Announcement these always race (the queue tier rule\n // supersedes or blocks). For Maintenance/Testing, the CURRENT cycle's own draft\n // sitting ready on send day IS the payload — that's the steady state, not a\n // problem — so only genuinely stale/foreign drafts get the warn.\n const pending = reports.filter((r) => r.draftReady && r.sentAt === null);\n if (pending.length > 0) {\n const currentPeriod = reportPeriodKey(now);\n const isExpectedPayload = (r: ReportRow): boolean =>\n type !== \"Announcement\" && r.reportType === type && r.period === currentPeriod;\n const expected = pending.filter(isExpectedPayload);\n const stale = pending.filter((r) => !isExpectedPayload(r));\n if (expected.length > 0) {\n findings.push({\n level: \"info\",\n check: \"pending-drafts\",\n message: `current-cycle ${type} draft is queued${expected[0]!.approvedToSend ? \" and approved\" : \" (awaiting approval)\"} — this is the report \\`--send-ready\\` will deliver`,\n });\n }\n if (stale.length > 0) {\n const summary = stale\n .map(\n (r) =>\n `${r.reportType} ${r.period ?? \"(no period)\"}${r.approvedToSend ? \" [APPROVED]\" : \"\"}`,\n )\n .join(\", \");\n findings.push({\n level: \"warn\",\n check: \"pending-drafts\",\n message: `${stale.length} unsent draft(s) queued: ${summary} — resolve before sending so the client doesn't get the wrong report first`,\n });\n }\n }\n\n // --- Schedule hygiene (validates the RAW Airtable frequency cells).\n checkFrequency(site, \"maintenance\", reports, findings, now);\n checkFrequency(site, \"testing\", reports, findings, now);\n if (type === \"Announcement\") {\n const anySent = lastSentForType(reports, \"Maintenance\") ?? lastSentForType(reports, \"Testing\");\n if (!site.maintenanceDay && !site.testingDay && !anySent) {\n findings.push({\n level: \"info\",\n check: \"anchor-missing\",\n message:\n \"no maintenance/testing day anchors and nothing ever sent — set both anchors to the announcement send date (the Sonder playbook) so recurrence starts from the announcement\",\n });\n }\n }\n\n return { site: site.name, findings };\n}\n\n/**\n * Fleet-level heuristics that need the whole selection. Airtable column renames\n * don't error — the mapper reads null forever (its own header comment admits this).\n * A load-bearing column empty on EVERY selected site is far more likely a rename\n * than N coincidences; say so. Likewise two different sites resolving to one\n * recipient is worth one eyeball (it can be legitimate — same owner, two sites).\n */\nexport function preflightFleet(sites: WebsiteRow[]): PreflightFinding[] {\n if (sites.length < 3) return []; // too few rows to distinguish rename from data\n const findings: PreflightFinding[] = [];\n const allEmpty = (get: (s: WebsiteRow) => unknown, column: string): void => {\n if (sites.every((s) => get(s) === null || get(s) === \"\")) {\n findings.push({\n level: \"warn\",\n check: \"column-possibly-renamed\",\n message: `'${column}' is empty on all ${sites.length} selected sites — if that column was renamed in Airtable the code reads null silently; verify the column name`,\n });\n }\n };\n allEmpty((s) => s.pointOfContact, \"point of contact\");\n allEmpty((s) => s.maintenanceFreqRaw, \"maintenence freq\");\n allEmpty((s) => s.headerImage, \"Header image\");\n\n const byContact = new Map<string, string[]>();\n for (const s of sites) {\n const to = parseAddresses(s.reportRecipientsTo) ?? parseAddresses(s.pointOfContact) ?? [];\n for (const addr of to) {\n const key = addr.toLowerCase();\n byContact.set(key, [...(byContact.get(key) ?? []), s.name]);\n }\n }\n for (const [addr, names] of byContact) {\n if (names.length > 1 && !OPERATOR_DOMAINS.includes(domainOf(addr))) {\n findings.push({\n level: \"info\",\n check: \"duplicate-contact\",\n message: `${names.join(\" and \")} resolve to the same recipient (${addr}) — fine if one person owns both; otherwise a copy-paste error on one row`,\n });\n }\n }\n return findings;\n}\n\nexport type PreflightDeps = {\n /** Airtable handle. Defaults to opening the live base from credentials. */\n base?: AirtableBase;\n /** Slug of a single site (matched via siteSlug). Mutually exclusive with `all`. */\n site?: string;\n /** Fleet mode: for Announcement, maintenance-status sites (announce's own filter);\n * for Maintenance/Testing, everything `report --due` schedules — ELIGIBLE_STATUSES\n * plus null-status rows (due.ts treats null as active for legacy rows). */\n all?: boolean;\n type?: ReportType;\n now?: Date;\n};\n\nexport type PreflightResult = {\n results: PreflightSiteResult[];\n fleet: PreflightFinding[];\n};\n\n/**\n * Read-only rollout preflight over the live Airtable base. One Websites fetch, one\n * Reports fetch (the --due path does the same for the same rate-limit reason), then\n * {@link preflightSite} per selected site plus {@link preflightFleet} across the\n * selection. NEVER writes and NEVER sends.\n */\nexport async function preflight(deps?: PreflightDeps): Promise<PreflightResult> {\n const base = deps?.base ?? openBase(readAirtableConfig());\n const type: ReportType = deps?.type ?? \"Announcement\";\n const now = deps?.now ?? new Date();\n\n const websites = await listWebsites(base);\n const selected = deps?.site\n ? websites.filter((w) => siteSlug(w.name) === siteSlug(deps.site!))\n : type === \"Announcement\"\n ? websites.filter((w) => w.status === \"maintenance\")\n : websites.filter((w) => w.status === null || ELIGIBLE_STATUSES.has(w.status));\n\n const allReports = selected.length > 0 ? await listAllReports(base) : [];\n const results = selected.map((site) =>\n preflightSite(\n site,\n allReports.filter((r) => r.siteId === site.id),\n type,\n now,\n ),\n );\n return { results, fleet: deps?.site ? [] : preflightFleet(selected) };\n}\n\n/**\n * The send-blocking subset for ONE report, at approve time: exactly the\n * conditions that make `sendOne` throw (no recipients, malformed To/CC, no\n * header image, no report-level Lighthouse scores) plus the wrong-inbox warn\n * (operator address resolved as a client's To). PURE — the approve gate, the\n * dashboard's pending-row chip, and the digest collector all call this one\n * function so \"approvable\" can't drift from \"sendable\".\n *\n * Deliberately narrower than {@link preflightSite}: schedule hygiene and\n * pending-draft races are fleet/site concerns — blocking THIS report's approval\n * on them would be over-gating.\n */\nexport function approveBlockers(site: WebsiteRow, report: ReportRow): PreflightFinding[] {\n const findings: PreflightFinding[] = [];\n\n const explicitTo = parseAddresses(site.reportRecipientsTo);\n const fallbackTo = parseAddresses(site.pointOfContact);\n const to = explicitTo ?? fallbackTo ?? [];\n if (to.length === 0) {\n findings.push({\n level: \"fail\",\n check: \"recipients-missing\",\n message:\n \"no recipients — Report recipients (To) and point of contact are both empty; the send will throw\",\n });\n }\n for (const addr of to) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `recipient '${addr}' is malformed — fix Report recipients (To) / point of contact in Airtable`,\n });\n }\n }\n for (const addr of parseAddresses(site.reportRecipientsCc) ?? []) {\n if (!isProbablyEmail(addr)) {\n findings.push({\n level: \"fail\",\n check: \"recipients-malformed\",\n message: `CC '${addr}' is malformed — fix Report recipients (CC) in Airtable`,\n });\n }\n }\n if (!site.headerImage) {\n findings.push({\n level: \"fail\",\n check: \"header-image-missing\",\n message: \"no Header image on the Websites row — the send will throw\",\n });\n } else if (!site.headerImage.type.startsWith(\"image/\")) {\n findings.push({\n level: \"fail\",\n check: \"header-image-not-image\",\n message: `Header image attachment is '${site.headerImage.type}' (${site.headerImage.filename}) — not a decodable image; the send throws in prepareHeaderImage`,\n });\n }\n // sendOne throws on a null report.lighthouse (one blank/non-numeric cell nulls\n // all four) — the REPORT's snapshot, not the Websites-row scores.\n if (report.lighthouse === null) {\n findings.push({\n level: \"fail\",\n check: \"report-scores-missing\",\n message:\n \"the Reports row has no Lighthouse scores (one blank/non-numeric cell nulls all four) — the send will throw\",\n });\n }\n if (!isOperatorSite(site)) {\n const operatorAddrs = to.filter((a) => OPERATOR_DOMAINS.includes(domainOf(a)));\n if (operatorAddrs.length > 0 && operatorAddrs.length === to.length) {\n findings.push({\n level: \"warn\",\n check: \"recipient-operator-address\",\n message: `resolved To is ONLY operator address(es) ${operatorAddrs.join(\", \")} — the client will NOT receive this report`,\n });\n }\n }\n return findings;\n}\n\n/** Convenience: just the fail-level blockers, formatted for gate messages. */\nexport function formatBlockers(findings: PreflightFinding[]): string[] {\n return findings.filter((f) => f.level === \"fail\").map((f) => `${f.check}: ${f.message}`);\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/reports/ga/config.ts","../src/reports/draft.ts","../src/reports/auto-tick.ts","../src/reports/ga/client.ts","../src/reports/search/client.ts"],"sourcesContent":["import { dirname, join } from \"node:path\";\nimport { defaultCredentialsPath } from \"../../util/credentials.js\";\n\nexport type GaConfig = {\n /** Workspace user the service account impersonates (domain-wide delegation). */\n subject: string;\n /** Absolute path to the service-account JSON key file. */\n keyPath: string;\n};\n\n/**\n * Read GA configuration from the environment (credentials.env is already loaded into\n * process.env by the CLI entrypoint). Returns null when `GA_SUBJECT` is unset — the\n * signal that GA enrichment is simply not configured, so drafting skips it silently.\n *\n * `GA_SA_KEY_PATH` is optional; it defaults to `ga-service-account.json` alongside the\n * credentials file (e.g. ~/.config/reddoor-maint/), keeping the key out of the repo.\n */\nexport function readGaConfig(): GaConfig | null {\n const subject = process.env.GA_SUBJECT?.trim();\n if (!subject) return null;\n const keyPath =\n process.env.GA_SA_KEY_PATH?.trim() ||\n join(dirname(defaultCredentialsPath()), \"ga-service-account.json\");\n return { subject, keyPath };\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type { ReportType, LighthouseScores } from \"./types.js\";\nimport { renderReportHtml } from \"./render.js\";\nimport { siteSlug, updateAnalyticsHealth } from \"./airtable/websites.js\";\nimport { resolveCopy } from \"./copy.js\";\nimport type { WebsiteRow } from \"./airtable/websites.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { createDraft, listReportsForSite } from \"./airtable/reports.js\";\nimport { queueDraft } from \"./queue.js\";\nimport { autoTickChecklist } from \"./auto-tick.js\";\nimport { uploadAttachment } from \"./airtable/attachments.js\";\nimport type { AirtableBase } from \"./airtable/client.js\";\nimport { readGaConfig } from \"./ga/config.js\";\nimport { fetchPeriodUsers } from \"./ga/client.js\";\nimport { fetchSearchPresence } from \"./search/client.js\";\nimport type { SearchPresence } from \"./search/client.js\";\n\nexport type DraftOptions = {\n /** Where to write the local preview HTML when `previewOnly`. Defaults to `reports/<slug>/draft.html`. */\n previewPath?: string;\n /** If true: render locally only, never touch Airtable. */\n previewOnly?: boolean;\n /** UTC \"YYYY-MM\" recurrence key; falls back to periodEnd's month when omitted. */\n period?: string;\n /** Airtable record id of an EXISTING (not-ready) row to COMPLETE in place rather\n * than creating a new one. When set, we skip createDraft and only re-render →\n * upload the HTML attachment → flip Draft ready on this row. Used by the --due\n * re-draft path to finish a draft whose createDraft succeeded but whose\n * setDraftReady never ran (a crash mid-sequence wedged the period). */\n completeRowId?: string;\n /** The mapped ReportRow being completed, returned as `reportRow` from the\n * complete path so callers keep the same shape they get on the create path. */\n existingRow?: ReportRow;\n};\n\n/** An enrichment fetch that *errored* (not one that was legitimately skipped\n * because it isn't configured / the site lacks the inputs). Surfaced so a\n * fleet-wide GA/Search outage is visible in a `--due` batch summary instead of\n * hiding behind one easily-missed console.warn per site. */\nexport type SoftFailure = \"ga\" | \"search\";\n\nexport type DraftResult = {\n /** null when previewOnly. */\n reportRow: ReportRow | null;\n /** Path to the local preview file (only set when previewOnly). */\n htmlPath: string | null;\n /** Always present — the rendered HTML string. */\n html: string;\n /** Enrichment fetches that errored for this site (empty on success or skip). */\n softFailures: SoftFailure[];\n /** Whether the draft was placed in the approve queue. False when a higher-or-equal-tier\n * report is already queued for the site (single-queue rule); null on the previewOnly path. */\n queued: boolean | null;\n /** Ids of lower-tier queued reports this draft superseded (un-queued). */\n supersededIds: string[];\n};\n\nfunction scoresFromWebsite(siteRow: WebsiteRow): LighthouseScores {\n const { pScore, rScore, bpScore, seoScore } = siteRow;\n if (pScore === null || rScore === null || bpScore === null || seoScore === null) {\n throw new Error(\n `Site '${siteRow.name}' is missing one or more Lighthouse scores on the Websites row (pScore, rScore, bpScore, seoScore). ` +\n `Run 'reddoor-maint audit lighthouse' from the site's checkout and paste the four numbers into Airtable, then retry.`,\n );\n }\n return { performance: pScore, accessibility: rScore, bestPractices: bpScore, seo: seoScore };\n}\n\nfunction daysAgo(today: Date, n: number): Date {\n // UTC accessors to stay TZ-consistent with `due.ts` (and avoid landing\n // Airtable's `Period start` on a different calendar day than the operator\n // expects on late-night runs near a month boundary). See morning brief\n // 2026-05-29 (M1) for context.\n const out = new Date(today);\n out.setUTCDate(out.getUTCDate() - n);\n return out;\n}\n\n/**\n * Render and create an Airtable draft for one site.\n *\n * No idempotency guard here — the recurrence guard lives in draftDueReports\n * (cli/commands/report.ts), keyed on reportPeriodKey(dueDate). The manual\n * single-site path intentionally always drafts (an operator asking for a draft\n * gets one). findReportByPeriod (airtable/reports.ts) is the real-Airtable\n * point lookup available to dashboard/digest callers that need the same\n * idempotency guarantee outside the CLI batch loop.\n */\nexport async function draftReportForSite(\n base: AirtableBase | null,\n siteRow: WebsiteRow,\n reportType: ReportType,\n options: DraftOptions = {},\n): Promise<DraftResult> {\n const scores = scoresFromWebsite(siteRow);\n\n const today = new Date();\n const slug = siteSlug(siteRow.name);\n\n const periodStart =\n base !== null ? await derivePeriodStart(base, siteRow, reportType, today) : daysAgo(today, 30);\n\n const periodEnd = today;\n const completedOn = today;\n // \"Last Tested\" on the Maintenance email is the REAL timestamp of the most recent automated\n // Lighthouse audit — stamped live on the Websites row (`Last lighthouse audit at`) by\n // `audit lighthouse --write-airtable` every time the scores refresh. It is deliberately NOT\n // the `testing day` field: that's the recurrence anchor consumed by due.ts and is hand-set, so\n // it goes stale. Reading the audit timestamp keeps the date current with no manual upkeep.\n const lastTestedDate =\n reportType === \"Maintenance\" && siteRow.lastLighthouseAuditAt\n ? new Date(siteRow.lastLighthouseAuditAt)\n : null;\n\n // GA enrichment (real path only). Soft-fail: any GA problem leaves the numbers null so\n // the draft still proceeds (operator fills them manually) — GA is an enhancement, not a\n // gate. Rendered with the fetched numbers so the review HTML matches the Airtable fields.\n // An *error* (vs a legitimate not-configured skip) is recorded in softFailures so the\n // caller can surface a fleet-wide outage in the batch summary.\n const gaResult =\n base !== null ? await fetchGaUsers(siteRow, periodStart, periodEnd) : NO_ENRICHMENT;\n const searchResult =\n base !== null ? await fetchSearch(siteRow, periodStart, periodEnd) : NO_ENRICHMENT;\n const gaUsers = gaResult.value;\n const search = searchResult.value;\n const softFailures: SoftFailure[] = [\n ...(gaResult.softFailed ? ([\"ga\"] as const) : []),\n ...(searchResult.softFailed ? ([\"search\"] as const) : []),\n ];\n\n const cidName = `${slug}-header`;\n const { html } = await renderReportHtml({\n siteName: siteRow.name,\n siteUrl: siteRow.url,\n reportType,\n completedOn,\n lighthouse: scores,\n gaUsersCurrent: gaUsers?.current,\n gaUsersPrevious: gaUsers?.previous,\n searchPosition: search?.foundOnPage1 ? (search.position ?? undefined) : undefined,\n lastTestedDate,\n commentary: null,\n copy: resolveCopy(siteRow),\n headerImageCid: cidName,\n });\n\n if (options.previewOnly) {\n const path = options.previewPath ?? `reports/${slug}/draft.html`;\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, html, \"utf-8\");\n return { reportRow: null, htmlPath: path, html, softFailures, queued: null, supersededIds: [] };\n }\n\n if (base === null) throw new Error(\"base required when previewOnly=false\");\n\n // Record this site's GA/Search enrichment health for the per-site analytics-failure\n // signal (cockpit/digest). Only when analytics is configured for THIS site — set the\n // timestamp on a soft-fail, clear it (null) on a clean enrichment so the signal\n // self-heals. Best-effort: the `Analytics soft-fail at` column is operator-added, so\n // until it exists the write throws UNKNOWN_FIELD_NAME — which must NOT break drafting.\n if (readGaConfig() !== null && Boolean(siteRow.ga4PropertyId || siteRow.searchQuery)) {\n try {\n await updateAnalyticsHealth(\n base,\n siteRow.id,\n softFailures.length > 0 ? today.toISOString() : null,\n );\n } catch (e) {\n console.warn(`⚠ analytics-health write skipped for ${siteRow.name}: ${(e as Error).message}`);\n }\n }\n\n // \"Finish an existing row\" path (the --due re-draft wedge fix). When the caller\n // hands us a row that was created but never made Draft-ready — a crash between\n // createDraft and setDraftReady leaves exactly this — we DON'T createDraft again\n // (that would duplicate the period). We re-attach the rendered HTML and queue the\n // EXISTING row, completing the half-made draft in place. The row's other fields\n // (scores, period, dates) were already written at create time; the only pieces a\n // crash drops are the attachment + the ready flag.\n if (options.completeRowId) {\n await uploadDraftHtml(options.completeRowId, slug, periodEnd, html);\n const outcome = await queueDraft(base, {\n id: options.completeRowId,\n siteId: siteRow.id,\n reportType,\n });\n return {\n reportRow: options.existingRow ?? null,\n htmlPath: null,\n html,\n softFailures,\n queued: outcome.queued,\n supersededIds: outcome.supersededIds,\n };\n }\n\n // Auto-tick the checklist boxes the system can prove (Phase 1: Google Indexed via the\n // inline search signal). Fail-safe lives in autoTickChecklist: only `pass` entries are ticked;\n // the evidence snapshot drives the dashboard's green/amber badges. The operator's approve gate\n // and per-box override are unchanged.\n const evidence = autoTickChecklist(siteRow, reportType, completedOn, { search: searchResult });\n const checklistTicks = [...evidence.entries()]\n .filter(([, e]) => e.result === \"pass\")\n .map(([field]) => field);\n const autoEvidence = Object.fromEntries(evidence);\n\n const reportId = `${siteRow.name} — ${reportType} — ${periodEnd.toISOString().slice(0, 10)}`;\n const created = await createDraft(base, {\n reportId,\n siteId: siteRow.id,\n reportType,\n period: options.period ?? periodEnd.toISOString().slice(0, 7),\n periodStart,\n periodEnd,\n completedOn,\n lighthouse: scores,\n lastTestedDate,\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 checklistTicks,\n autoEvidence,\n });\n\n await uploadDraftHtml(created.id, slug, periodEnd, html);\n const outcome = await queueDraft(base, {\n id: created.id,\n siteId: siteRow.id,\n reportType,\n });\n\n return {\n reportRow: created,\n htmlPath: null,\n html,\n softFailures,\n queued: outcome.queued,\n supersededIds: outcome.supersededIds,\n };\n}\n\n/** Attach the rendered HTML to a Reports row. Queueing (Draft ready + the single-queue\n * reconciliation) is handled separately by queueDraft so both the create path and the\n * \"complete a half-made row\" path share the identical, re-runnable upload step. */\nasync function uploadDraftHtml(\n rowId: string,\n slug: string,\n periodEnd: Date,\n html: string,\n): Promise<void> {\n const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;\n await uploadAttachment(rowId, \"Rendered HTML\", html, htmlFilename, \"text/html\");\n}\n\n/** Result of an enrichment fetch: the value (null if unavailable) plus whether\n * it errored (`softFailed`) as opposed to being legitimately not-configured. */\ntype Enrichment<T> = { value: T | null; softFailed: boolean };\n/** A not-configured / skipped enrichment — null value, not a soft-failure. */\nconst NO_ENRICHMENT: Enrichment<never> = { value: null, softFailed: false };\n\n/**\n * Fetch GA \"Users\" for the period, soft-failing to null. Returns a null value (no enrichment)\n * when GA isn't configured (`GA_SUBJECT` unset) or the site has no GA4 property ID — those are\n * legitimate skips, `softFailed: false`. When the GA API errors it logs a one-line warning and\n * returns `softFailed: true`. Never throws, so a GA problem can never block a draft; the\n * operator can always enter the numbers by hand.\n */\nexport async function fetchGaUsers(\n siteRow: WebsiteRow,\n periodStart: Date,\n periodEnd: Date,\n): Promise<Enrichment<{ current: number; previous: number }>> {\n const cfg = readGaConfig();\n if (!cfg || !siteRow.ga4PropertyId) return NO_ENRICHMENT;\n try {\n const value = await fetchPeriodUsers(\n { propertyId: siteRow.ga4PropertyId, subject: cfg.subject, keyPath: cfg.keyPath },\n periodStart,\n periodEnd,\n );\n return { value, softFailed: false };\n } catch (e) {\n console.warn(`⚠ GA skipped for ${siteRow.name}: ${(e as Error).message}`);\n return { value: null, softFailed: true };\n }\n}\n\n/**\n * Fetch the site's Google search presence for the period, soft-failing to null. Returns a null\n * value when GA/SA isn't configured (`readGaConfig()` null — search shares the SA credentials)\n * or the site has no `searchQuery` (legitimate skips, `softFailed: false`). When the Search\n * Console API errors it logs a one-line warning and returns `softFailed: true`. Never throws,\n * so a search problem can never block a draft.\n */\nexport async function fetchSearch(\n siteRow: WebsiteRow,\n periodStart: Date,\n periodEnd: Date,\n): Promise<Enrichment<SearchPresence>> {\n const cfg = readGaConfig();\n if (!cfg || !siteRow.searchQuery) return NO_ENRICHMENT;\n try {\n const value = await fetchSearchPresence(\n {\n keyPath: cfg.keyPath,\n subject: cfg.subject,\n property: siteRow.searchConsoleProperty ?? undefined,\n host: siteRow.url,\n query: siteRow.searchQuery,\n },\n periodStart,\n periodEnd,\n );\n return { value, softFailed: false };\n } catch (e) {\n console.warn(`⚠ Search presence skipped for ${siteRow.name}: ${(e as Error).message}`);\n return { value: null, softFailed: true };\n }\n}\n\nasync function derivePeriodStart(\n base: AirtableBase,\n siteRow: WebsiteRow,\n reportType: ReportType,\n today: Date,\n): Promise<Date> {\n const prior = await listReportsForSite(base, siteRow.id);\n const sameType = prior\n .filter((r) => r.reportType === reportType && r.periodEnd)\n .map((r) => r.periodEnd!)\n .sort();\n const latest = sameType[sameType.length - 1];\n if (!latest) return daysAgo(today, 30);\n // Half-open periods. The prior report's GA/Search windows are inclusive of its\n // periodEnd, so starting this report on the *same* day double-counts that\n // boundary day across two consecutive reports (and inflates the headline Users\n // count). Start the next day instead. UTC to stay TZ-consistent with daysAgo.\n const start = new Date(latest);\n start.setUTCDate(start.getUTCDate() + 1);\n return start;\n}\n","import type { ReportType } from \"./types.js\";\nimport type { WebsiteRow } from \"./airtable/websites.js\";\nimport type { SearchPresence } from \"./search/client.js\";\nimport { checklistFor } from \"./checklist.js\";\nimport { isNetlifyAppUrl } from \"../util/url.js\";\n\n/** A persisted audit signal is only trusted within this window — a stale \"pass\" degrades to\n * \"unknown\" (no tick). Matches the github-signals staleness convention (~3 days). */\nconst STALE_DAYS = 3;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n/** True when `checkedAt` is a valid timestamp within STALE_DAYS of `now`. */\nfunction isFresh(checkedAt: string | null, now: Date): boolean {\n if (!checkedAt) return false;\n const t = new Date(checkedAt).getTime();\n if (Number.isNaN(t)) return false;\n return now.getTime() - t <= STALE_DAYS * MS_PER_DAY;\n}\n\n/** Cert-expiry buffer: a cert must have MORE than this many days left to pass (renew window).\n * Exactly 14 days does NOT pass — matches the spec's strict \">14 days\" and the domain audit's\n * own `> 14` pass status, so the two thresholds can't drift. */\nconst CERT_MIN_DAYS = 14;\n\n/** A single auto-check outcome. `pass` + fresh ⇒ the caller ticks the box. */\nexport type EvidenceResult = \"pass\" | \"fail\" | \"unknown\";\nexport type EvidenceRecord = { result: EvidenceResult; checkedAt: string | null; note: string };\n\n/**\n * Inline signals fetched during the draft. Phase 1 carries Search Console only; Phase 2/3 read\n * the remaining signals (security, domain, browser) off the Websites row passed as `site`.\n */\nexport type AutoTickSignals = {\n search: { value: SearchPresence | null; softFailed: boolean };\n};\n\n/**\n * Decide, per checklist item for this report type, an evidence record — ONLY for items that\n * currently have a signal. Items with no signal are omitted, so the caller leaves them manual.\n * PURE. The fail-safe invariant lives here: a box is only tickable when `result === \"pass\"`.\n * Google Indexed is an inline signal (fetched at draft), so its evidence is inherently fresh\n * (`checkedAt = now`).\n */\nexport function autoTickChecklist(\n site: WebsiteRow,\n reportType: ReportType,\n now: Date,\n signals: AutoTickSignals,\n): Map<string, EvidenceRecord> {\n const out = new Map<string, EvidenceRecord>();\n const fields = new Set(checklistFor(reportType).map((i) => i.field));\n\n // Google Indexed — Search Console (inline, always fresh).\n if (fields.has(\"Maint: Google Indexed\")) {\n const g = googleEvidence(now, signals.search);\n if (g) out.set(\"Maint: Google Indexed\", g);\n }\n\n // Security Updates — the nightly `security` audit's persisted vuln counts.\n if (fields.has(\"Maint: Security Updates\")) {\n const s = securityEvidence(site, now);\n if (s) out.set(\"Maint: Security Updates\", s);\n }\n\n // Domain, DNS & SSL — the nightly `domain` audit's persisted cert/resolve signal.\n if (fields.has(\"Maint: Domain, DNS & SSL\")) {\n const d = domainEvidence(site, now);\n if (d) out.set(\"Maint: Domain, DNS & SSL\", d);\n }\n\n // Desktop / Mobile / Links — the nightly `browser` audit's persisted verdicts (one timestamp\n // gates all three). Each is a stored boolean computed by the audit (multi-engine/route);\n // here we add freshness + omit-when-absent.\n if (fields.has(\"Test: Desktop Browsers\")) {\n const e = browserEvidence(\n site.crossbrowserOk,\n site,\n now,\n \"Desktop renders cleanly\",\n \"render errors\",\n );\n if (e) out.set(\"Test: Desktop Browsers\", e);\n }\n if (fields.has(\"Test: Mobile Browsers\")) {\n const e = browserEvidence(\n site.mobileOk,\n site,\n now,\n \"Mobile renders cleanly\",\n \"overflow/errors\",\n );\n if (e) out.set(\"Test: Mobile Browsers\", e);\n }\n if (fields.has(\"Test: Links & Navigation\")) {\n const broken = site.brokenLinks;\n const failNote = broken && broken > 0 ? `${broken} broken link(s)` : \"broken links / nav\";\n const e = browserEvidence(site.linksOk, site, now, \"All internal links resolve\", failNote);\n if (e) out.set(\"Test: Links & Navigation\", e);\n }\n\n return out;\n}\n\n/** One browser-audit verdict (Desktop/Mobile/Links) → evidence, gated on `Browser checked at`\n * freshness. null verdict (never run) → omit (manual); stale → unknown; true → pass; false →\n * fail (with the failure note). The verdict itself was computed by the audit (summarizeBrowser). */\nfunction browserEvidence(\n ok: boolean | null,\n site: WebsiteRow,\n now: Date,\n passNote: string,\n failNote: string,\n): EvidenceRecord | null {\n if (ok === null || !site.browserCheckedAt) return null;\n const at = site.browserCheckedAt;\n if (!isFresh(at, now)) {\n return { result: \"unknown\", checkedAt: at, note: \"Browser check is stale (>3d)\" };\n }\n return ok\n ? { result: \"pass\", checkedAt: at, note: passNote }\n : { result: \"fail\", checkedAt: at, note: failNote };\n}\n\n/**\n * Security Updates evidence from the persisted `security` audit. Omits (→ manual) when the audit\n * never ran (counts null / no timestamp); stale → unknown; 0 critical AND 0 high → pass; any\n * critical/high → fail (with the count). Honest scope: \"no known critical/high advisories in the\n * declared dependencies as of the last audit\" — moderate/low are advisory, not gating, and this\n * does not prove the fix is deployed.\n */\nfunction securityEvidence(site: WebsiteRow, now: Date): EvidenceRecord | null {\n const crit = site.securityVulnsCritical;\n const high = site.securityVulnsHigh;\n if (crit === null || high === null || !site.lastSecurityAuditAt) return null;\n const at = site.lastSecurityAuditAt;\n if (!isFresh(at, now)) {\n return { result: \"unknown\", checkedAt: at, note: \"Security audit is stale (>3d)\" };\n }\n if (crit === 0 && high === 0) {\n return { result: \"pass\", checkedAt: at, note: \"No known critical/high vulnerabilities\" };\n }\n return { result: \"fail\", checkedAt: at, note: `${crit} critical / ${high} high vuln(s)` };\n}\n\n/** Search Console → Google Indexed evidence. `null` (not configured) emits nothing so the box\n * stays manual; a soft-fail is `unknown`; page-1 is `pass` (with the position in the note). */\nfunction googleEvidence(now: Date, search: AutoTickSignals[\"search\"]): EvidenceRecord | null {\n const at = now.toISOString();\n if (search.softFailed) {\n return { result: \"unknown\", checkedAt: at, note: \"Search Console unavailable this run\" };\n }\n if (search.value === null) return null;\n if (search.value.foundOnPage1) {\n const pos = search.value.position;\n return {\n result: \"pass\",\n checkedAt: at,\n note: `Page 1 on Google${pos !== null ? ` (#${pos})` : \"\"}`,\n };\n }\n const pos = search.value.position;\n return {\n result: \"fail\",\n checkedAt: at,\n note: `Not on page 1${pos !== null ? ` (avg #${pos})` : \"\"}`,\n };\n}\n\n/**\n * Domain/DNS/SSL evidence from the persisted `domain` audit. Omits (→ manual) when there's no\n * custom domain to verify (`*.netlify.app`) or the audit never ran. A `pass` requires a fresh\n * check that resolved with a cert comfortably before expiry; stale → unknown; resolved-but-no-cert\n * or near/past expiry → fail. The honest scope is resolve + valid cert — NOT registrar expiry,\n * www↔apex redirect, or MX.\n */\nfunction domainEvidence(site: WebsiteRow, now: Date): EvidenceRecord | null {\n // No custom domain to verify — the no-domain watch signal covers `*.netlify.app` elsewhere.\n if (!site.url || isNetlifyAppUrl(site.url)) return null;\n if (!site.domainCheckedAt) return null; // never probed → leave manual\n const at = site.domainCheckedAt;\n if (!isFresh(site.domainCheckedAt, now)) {\n return { result: \"unknown\", checkedAt: at, note: \"Domain check is stale (>3d)\" };\n }\n const days = site.certDaysRemaining;\n if (days === null) {\n return { result: \"fail\", checkedAt: at, note: \"Did not resolve, or no valid TLS cert\" };\n }\n if (days <= CERT_MIN_DAYS) {\n return { result: \"fail\", checkedAt: at, note: `TLS cert expires in ${days}d` };\n }\n return { result: \"pass\", checkedAt: at, note: `Custom domain, valid cert (${days}d left)` };\n}\n","import { readFileSync } from \"node:fs\";\nimport { JWT } from \"google-auth-library\";\nimport { BetaAnalyticsDataClient } from \"@google-analytics/data\";\n\nconst ANALYTICS_READONLY = \"https://www.googleapis.com/auth/analytics.readonly\";\nconst MS_PER_DAY = 86_400_000;\n\nexport type GaQuery = {\n /** GA4 numeric property ID (e.g. \"471880366\"). */\n propertyId: string;\n /** Workspace user to impersonate via domain-wide delegation. */\n subject: string;\n /** Path to the service-account JSON key. */\n keyPath: string;\n};\n\n/** UTC YYYY-MM-DD — matches the rest of the reports pipeline's date handling. */\nfunction ymd(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Fetch GA4 `activeUsers` (\"Users\") for a report period and the equal-length window\n * immediately before it, via a domain-wide-delegation service account that impersonates\n * `subject`. Throws on any auth/API error — the caller (draftReportForSite) soft-fails.\n *\n * Previous window: same length as the current period, ending the day before `periodStart`.\n */\nexport async function fetchPeriodUsers(\n query: GaQuery,\n periodStart: Date,\n periodEnd: Date,\n): Promise<{ current: number; previous: number }> {\n const key = JSON.parse(readFileSync(query.keyPath, \"utf8\")) as {\n client_email: string;\n private_key: string;\n };\n const authClient = new JWT({\n email: key.client_email,\n key: key.private_key,\n scopes: [ANALYTICS_READONLY],\n subject: query.subject,\n });\n const client = new BetaAnalyticsDataClient({ authClient });\n\n const lengthDays = Math.round((periodEnd.getTime() - periodStart.getTime()) / MS_PER_DAY);\n const prevEnd = new Date(periodStart.getTime() - MS_PER_DAY);\n const prevStart = new Date(prevEnd.getTime() - lengthDays * MS_PER_DAY);\n\n const property = `properties/${query.propertyId}`;\n const run = async (start: Date, end: Date): Promise<number> => {\n const [resp] = await client.runReport({\n property,\n dateRanges: [{ startDate: ymd(start), endDate: ymd(end) }],\n metrics: [{ name: \"activeUsers\" }],\n });\n const raw = resp.rows?.[0]?.metricValues?.[0]?.value ?? \"0\";\n const n = Number.parseInt(raw, 10);\n return Number.isFinite(n) ? n : 0;\n };\n\n const current = await run(periodStart, periodEnd);\n const previous = await run(prevStart, prevEnd);\n return { current, previous };\n}\n","import { readFileSync } from \"node:fs\";\nimport { JWT } from \"google-auth-library\";\n\nconst WEBMASTERS_READONLY = \"https://www.googleapis.com/auth/webmasters.readonly\";\nconst SC_BASE = \"https://searchconsole.googleapis.com/webmasters/v3\";\n/** Average-position threshold for \"on page 1\" (10 organic results per page). */\nconst PAGE_1_MAX_POSITION = 10;\n/** Rows to pull for the brand `contains` match. searchAnalytics has no orderBy and returns\n * rows clicks-desc, so the exact-query row survives into the response only if it isn't paged\n * out beyond this cap. Sized with wide headroom — a brand has a handful of phrasing variants,\n * far below 100 — so the exact query (and every variant) is in practice always present. */\nexport const BRAND_QUERY_ROW_LIMIT = 100;\n\nexport type SearchPresenceQuery = {\n /** Path to the service-account JSON key (same one GA uses). */\n keyPath: string;\n /** Workspace user to impersonate via domain-wide delegation. */\n subject: string;\n /** Explicit Search Console property (`sc-domain:...` or `https://.../`). Overrides auto-resolution. */\n property?: string | undefined;\n /** Site host, used to auto-resolve the property from `sites.list` when `property` is absent. */\n host: string;\n /** Operator-supplied brand hint (e.g. the business name). Matched as a case-insensitive\n * SUBSTRING (`contains`) of the actual user query — NOT a hard exact string — so it tolerates\n * phrasing (\"red door creative\" vs \"…creative la\"). An exact-query match is preferred when\n * present; otherwise the most-searched matching query wins. */\n query: string;\n};\n\nexport type SearchPresence = {\n /** True when the average position for the query is on page 1 (<= 10). */\n foundOnPage1: boolean;\n /** Rounded average position, or null when not found / no data. */\n position: number | null;\n};\n\ntype SiteEntry = { siteUrl: string };\n\n/** Reduce any property string or URL to a bare host: no `sc-domain:`, scheme, `www.`, path, lowercased. */\nexport function bareHost(s: string): string {\n return s\n .trim()\n .replace(/^sc-domain:/i, \"\")\n .replace(/^https?:\\/\\//i, \"\")\n .split(\"/\")[0]!\n .replace(/^www\\./i, \"\")\n .toLowerCase();\n}\n\n/**\n * All Search Console properties matching `host`, ordered for query fallback: Domain\n * (`sc-domain:`) forms first (broadest coverage), then URL-prefix forms. A site can be verified\n * as both; a freshly-created Domain property has no backfilled history, so its data can be empty\n * even while a long-lived URL-prefix property has data — hence we return every match and let the\n * caller try them in order until one returns data. Empty list = nothing matches.\n */\nexport function resolvePropertyCandidates(entries: SiteEntry[], host: string): string[] {\n const target = bareHost(host);\n const matches = entries.filter((e) => bareHost(e.siteUrl) === target).map((e) => e.siteUrl);\n const domains = matches.filter((s) => s.toLowerCase().startsWith(\"sc-domain:\"));\n const prefixes = matches.filter((s) => !s.toLowerCase().startsWith(\"sc-domain:\"));\n return [...domains, ...prefixes];\n}\n\n/** UTC YYYY-MM-DD — matches the rest of the reports pipeline. */\nfunction ymd(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * From the queries matching the brand substring, pick the one the brand is actually FOUND\n * by most — the highest-impression row — and return its average position; tie-break on the\n * better (lower) position. Returns undefined when no row carries a numeric position. This is\n * what makes the reported \"brand search\" position robust to phrasing: with a `contains`\n * filter several variants can match, and we report the position of the variant real users\n * search most, not whichever the operator happened to type. Rows missing `impressions` count\n * as 0 (keeps a single-row, impression-less response — e.g. a test fixture — working). PURE.\n */\nexport function pickBrandQuery(\n rows: Array<{ position?: number; impressions?: number }>,\n): number | undefined {\n let best: { position: number; impressions: number } | undefined;\n for (const r of rows) {\n if (typeof r.position !== \"number\") continue;\n const impressions = r.impressions ?? 0;\n if (\n best === undefined ||\n impressions > best.impressions ||\n (impressions === best.impressions && r.position < best.position)\n ) {\n best = { position: r.position, impressions };\n }\n }\n return best?.position;\n}\n\n/**\n * The brand's reported position from the `contains`-matched rows, preferring an EXACT match.\n * When a returned row's query equals the operator's configured string (case-insensitive), its\n * position is used verbatim — a precisely-configured brand query is honored, with no chance a\n * longer higher-impression variant (\"…reviews\", \"…near me\") hijacks the number. Otherwise we\n * fall back to the most-searched matching query (`pickBrandQuery`). Operates only on the rows\n * actually returned: a string `contains` itself, so the exact query is among the `contains`\n * results whenever it has data AND isn't paged out beyond `BRAND_QUERY_ROW_LIMIT` (sized so a\n * brand's handful of phrasing variants always fit; if it ever were paged out, the most-searched\n * fallback still returns a valid brand position). Returns undefined when no row carries a\n * numeric position. PURE.\n */\nexport function selectBrandPosition(\n rows: Array<{ keys?: string[]; position?: number; impressions?: number }>,\n exactQuery: string,\n): number | undefined {\n const want = exactQuery.toLowerCase();\n const exact = rows.find(\n (r) => typeof r.position === \"number\" && (r.keys?.[0] ?? \"\").toLowerCase() === want,\n );\n if (exact) return exact.position;\n return pickBrandQuery(rows);\n}\n\n/**\n * Query Google Search Console for the brand's average position on the site over the report\n * period, via a domain-wide-delegation service account impersonating `subject`. `query` is a\n * case-insensitive SUBSTRING (`contains`) hint; among the matching user queries we report the\n * position of the exact-match query when present, else the most-searched one (see\n * `selectBrandPosition`) — so it doesn't depend on the operator typing the exact search string,\n * yet honors one verbatim when they do. Uses `property` verbatim when given (operator's\n * choice is final — no fallback); otherwise auto-discovers all matching properties via\n * `sites.list` and tries them in order (Domain first) until one returns data. Throws on any\n * auth/API error — the caller (draftReportForSite) soft-fails.\n */\nexport async function fetchSearchPresence(\n q: SearchPresenceQuery,\n periodStart: Date,\n periodEnd: Date,\n): Promise<SearchPresence> {\n const key = JSON.parse(readFileSync(q.keyPath, \"utf8\")) as {\n client_email: string;\n private_key: string;\n };\n const jwt = new JWT({\n email: key.client_email,\n key: key.private_key,\n scopes: [WEBMASTERS_READONLY],\n subject: q.subject,\n });\n\n const explicit = q.property?.trim();\n let candidates: string[];\n if (explicit) {\n candidates = [explicit];\n } else {\n const list = await jwt.request<{ siteEntry?: SiteEntry[] }>({\n url: `${SC_BASE}/sites`,\n method: \"GET\",\n });\n candidates = resolvePropertyCandidates(list.data.siteEntry ?? [], q.host);\n if (candidates.length === 0) return { foundOnPage1: false, position: null };\n }\n\n for (const property of candidates) {\n const res = await jwt.request<{\n rows?: Array<{ keys?: string[]; position?: number; impressions?: number }>;\n }>({\n url: `${SC_BASE}/sites/${encodeURIComponent(property)}/searchAnalytics/query`,\n method: \"POST\",\n data: {\n startDate: ymd(periodStart),\n endDate: ymd(periodEnd),\n dimensions: [\"query\"],\n dimensionFilterGroups: [\n {\n filters: [\n { dimension: \"query\", operator: \"contains\", expression: q.query.toLowerCase() },\n ],\n },\n ],\n rowLimit: BRAND_QUERY_ROW_LIMIT,\n },\n });\n const pos = selectBrandPosition(res.data.rows ?? [], q.query.toLowerCase());\n if (typeof pos === \"number\") {\n // Search Console can average below 1; floor to 1 so the template never\n // renders a nonsensical \"#0\" (positions are 1-indexed).\n return { foundOnPage1: pos <= PAGE_1_MAX_POSITION, position: Math.max(1, Math.round(pos)) };\n }\n }\n return { foundOnPage1: false, position: null };\n}\n"],"mappings":"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","names":["dirname","pos","MS_PER_DAY","readFileSync","JWT","ymd","dirname","outcome"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/db/fleet-events.ts","../src/audits/fleet-events-writer.ts"],"sourcesContent":["import type { Selectable } from \"kysely\";\nimport type { Db } from \"./client.js\";\nimport type { FleetEventsTable } from \"./schema.js\";\n\nexport type FleetEventType =\n | \"pr_automerged\"\n | \"vuln_cleared\"\n | \"ci_recovered\"\n | \"site_launched\"\n | \"fleet_swept\"\n | \"cert_renewed\";\n\n/** One recorded fleet activity event. `id` is deterministic (see the detectors)\n * so re-runs INSERT OR IGNORE without duplicating. `siteId`/`siteName` are null\n * for fleet-wide rollups; `data` is optional structured detail (e.g. the PR url). */\nexport type FleetEvent = {\n id: string;\n ts: string;\n type: FleetEventType;\n siteId: string | null;\n siteName: string | null;\n summary: string;\n data: unknown | null;\n};\n\n/** Tolerant JSON parse for the stored `data` column — a malformed value (should\n * never happen, we wrote it) degrades to null rather than throwing the read. */\nfunction parseData(raw: string | null): unknown | null {\n if (raw === null) return null;\n try {\n return JSON.parse(raw);\n } catch {\n return null;\n }\n}\n\nfunction rowToEvent(r: Selectable<FleetEventsTable>): FleetEvent {\n return {\n id: r.id,\n ts: r.ts,\n type: r.type as FleetEventType,\n siteId: r.site_id,\n siteName: r.site_name,\n summary: r.summary,\n data: parseData(r.data),\n };\n}\n\n/** Idempotent append: INSERT OR IGNORE on the deterministic primary key, so an\n * overlapping/re-run sweep never duplicates an event. The first write wins. */\nexport async function recordFleetEvent(db: Db, e: FleetEvent): Promise<void> {\n await db\n .insertInto(\"fleet_events\")\n .values({\n id: e.id,\n ts: e.ts,\n type: e.type,\n site_id: e.siteId,\n site_name: e.siteName,\n summary: e.summary,\n data: e.data == null ? null : JSON.stringify(e.data),\n created_at: new Date().toISOString(),\n })\n .onConflict((oc) => oc.column(\"id\").doNothing())\n .execute();\n}\n\n/** Recent events at/after `sinceIso`, newest-first, capped at `limit`. */\nexport async function listFleetEvents(\n db: Db,\n opts: { sinceIso: string; limit: number },\n): Promise<FleetEvent[]> {\n const rows = await db\n .selectFrom(\"fleet_events\")\n .selectAll()\n .where(\"ts\", \">=\", opts.sinceIso)\n .orderBy(\"ts\", \"desc\")\n .limit(opts.limit)\n .execute();\n return rows.map(rowToEvent);\n}\n\n/** Retention prune: delete events strictly older than `beforeIso`. */\nexport async function pruneFleetEvents(db: Db, beforeIso: string): Promise<void> {\n await db.deleteFrom(\"fleet_events\").where(\"ts\", \"<\", beforeIso).execute();\n}\n","import { openDb, readDbConfig, type Db } from \"../db/client.js\";\nimport { recordFleetEvent, pruneFleetEvents, type FleetEvent } from \"../db/fleet-events.js\";\n\nconst RETENTION_MS = 30 * 24 * 60 * 60 * 1000;\n\n/** Persist a batch of fleet events + prune the 30-day window — best-effort.\n * Opens its OWN libSQL connection (the producers carry only Airtable creds today).\n * A missing TURSO_* (creds not yet added to the workflow) or any write error is\n * swallowed with a console.error: recording fleet activity must NEVER fail the\n * sweep that produced it. The feed ships dark until the creds are present.\n * `open` is injectable for tests. */\nexport async function recordFleetEventsBestEffort(\n events: FleetEvent[],\n now: Date,\n open: () => Promise<Db> = () => openDb(readDbConfig()),\n): Promise<void> {\n if (events.length === 0) return;\n let db: Db;\n try {\n db = await open();\n } catch (e) {\n console.error(`[fleet-events] skipped ${events.length} event(s): no libSQL (${String(e)})`);\n return;\n }\n try {\n for (const e of events) await recordFleetEvent(db, e);\n await pruneFleetEvents(db, new Date(now.getTime() - RETENTION_MS).toISOString());\n } catch (e) {\n console.error(`[fleet-events] write failed: ${String(e)}`);\n }\n}\n"],"mappings":";;;;;;AAkDA,eAAsB,iBAAiB,IAAQ,GAA8B;AAC3E,QAAM,GACH,WAAW,cAAc,EACzB,OAAO;AAAA,IACN,IAAI,EAAE;AAAA,IACN,IAAI,EAAE;AAAA,IACN,MAAM,EAAE;AAAA,IACR,SAAS,EAAE;AAAA,IACX,WAAW,EAAE;AAAA,IACb,SAAS,EAAE;AAAA,IACX,MAAM,EAAE,QAAQ,OAAO,OAAO,KAAK,UAAU,EAAE,IAAI;AAAA,IACnD,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,EACrC,CAAC,EACA,WAAW,CAAC,OAAO,GAAG,OAAO,IAAI,EAAE,UAAU,CAAC,EAC9C,QAAQ;AACb;AAkBA,eAAsB,iBAAiB,IAAQ,WAAkC;AAC/E,QAAM,GAAG,WAAW,cAAc,EAAE,MAAM,MAAM,KAAK,SAAS,EAAE,QAAQ;AAC1E;;;AClFA,IAAM,eAAe,KAAK,KAAK,KAAK,KAAK;AAQzC,eAAsB,4BACpB,QACA,KACA,OAA0B,MAAM,OAAO,aAAa,CAAC,GACtC;AACf,MAAI,OAAO,WAAW,EAAG;AACzB,MAAI;AACJ,MAAI;AACF,SAAK,MAAM,KAAK;AAAA,EAClB,SAAS,GAAG;AACV,YAAQ,MAAM,0BAA0B,OAAO,MAAM,yBAAyB,OAAO,CAAC,CAAC,GAAG;AAC1F;AAAA,EACF;AACA,MAAI;AACF,eAAW,KAAK,OAAQ,OAAM,iBAAiB,IAAI,CAAC;AACpD,UAAM,iBAAiB,IAAI,IAAI,KAAK,IAAI,QAAQ,IAAI,YAAY,EAAE,YAAY,CAAC;AAAA,EACjF,SAAS,GAAG;AACV,YAAQ,MAAM,gCAAgC,OAAO,CAAC,CAAC,EAAE;AAAA,EAC3D;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/audits/deps.ts","../src/audits/deps-outdated.ts","../src/audits/lint.ts","../src/github/gh-rest.ts","../src/audits/security.ts","../src/audits/lighthouse.ts","../src/audits/util/site-config.ts","../src/util/free-port.ts","../src/audits/a11y.ts","../src/audits/domain.ts","../src/audits/route-discovery.ts","../src/audits/browser.ts","../src/audits/netlify-deploy.ts","../src/audits/index.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { baselineVersions } from \"../configs/baseline-versions.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport { scanOutdated, type OutdatedCounts } from \"./deps-outdated.js\";\n\nexport type Drift = \"same\" | \"patch\" | \"minor\" | \"major\" | \"newer\";\n\nexport type DepsDriftEntry = {\n pkg: string;\n baseline: string;\n actual: string;\n drift: Drift;\n};\n\n/** The deps audit reports TWO signals:\n * - `entries`: declared-range drift vs the canonical baseline (what the\n * package.json *asks for*, caret-stripped) — the long-standing signal.\n * - `outdated`: real installed-version drift vs the registry's latest, from\n * the committed lockfile (null when it can't be determined). Added so the\n * \"Deps Drifted\" dashboard number stops being the only — and misleading —\n * deps signal. */\nexport type DepsDetails = {\n entries: DepsDriftEntry[];\n outdated: OutdatedCounts | null;\n};\n\nfunction stripCaret(range: string): string {\n return range.replace(/^[\\^~]/, \"\");\n}\n\n/** A spec we can drift-compare against a semver baseline: a plain version or\n * caret/tilde range like \"5.55.10\", \"^5.55.10\", \"~5.0.0\". Excludes \"*\",\n * \"latest\", \"workspace:*\", \"npm:\"-aliases, and git/URL/file specs — those used\n * to parse to NaN and produce bogus drift, so they're skipped instead. */\nfunction isComparableRange(spec: string): boolean {\n return /^[\\^~]?\\d/.test(spec.trim());\n}\n\nfunction parseSemver(v: string): [number, number, number] {\n const cleaned = stripCaret(v).split(\"-\")[0] ?? \"0.0.0\";\n const parts = cleaned.split(\".\").map((n) => Number.parseInt(n, 10));\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n}\n\nfunction compareSemver(actual: string, baseline: string): Drift {\n const [aMajor, aMinor, aPatch] = parseSemver(actual);\n const [bMajor, bMinor, bPatch] = parseSemver(baseline);\n if (aMajor > bMajor) return \"newer\";\n if (aMajor < bMajor) return \"major\";\n if (aMinor > bMinor) return \"newer\";\n if (aMinor < bMinor) return \"minor\";\n if (aPatch > bPatch) return \"newer\";\n if (aPatch < bPatch) return \"patch\";\n return \"same\";\n}\n\nexport async function depsAudit(ctx: AuditContext): Promise<AuditResult> {\n const pkgPath = join(ctx.site.path, \"package.json\");\n let pkgRaw: string;\n try {\n pkgRaw = await readFile(pkgPath, \"utf-8\");\n } catch (err) {\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status: \"skip\",\n summary: `no package.json at ${pkgPath}`,\n details: { error: String(err) },\n };\n }\n\n let pkg: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };\n try {\n pkg = JSON.parse(pkgRaw) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n } catch (err) {\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status: \"fail\",\n summary: `package.json is not valid JSON: ${(err as Error).message}`,\n details: { error: String(err) },\n };\n }\n const installed: Record<string, string> = {\n ...(pkg.dependencies ?? {}),\n ...(pkg.devDependencies ?? {}),\n };\n\n const entries: DepsDriftEntry[] = [];\n for (const [name, baseline] of Object.entries(baselineVersions)) {\n const actual = installed[name];\n if (!actual) continue;\n // Skip non-semver specs (\"*\", \"workspace:*\", \"npm:\"-aliases, git/URL): they\n // can't be drift-compared and used to yield NaN-driven bogus drift (LOW-3).\n if (!isComparableRange(actual)) continue;\n entries.push({\n pkg: name,\n baseline,\n actual,\n drift: compareSemver(actual, baseline),\n });\n }\n\n const anyMajor = entries.some((d) => d.drift === \"major\");\n const anyMinor = entries.some((d) => d.drift === \"minor\");\n const anyNewer = entries.some((d) => d.drift === \"newer\");\n\n // Status stays driven by the declared-range baseline drift (unchanged\n // behavior). The outdated count is an independent, informational signal.\n const status: AuditResult[\"status\"] = anyMajor ? \"fail\" : anyMinor || anyNewer ? \"warn\" : \"pass\";\n\n const driftSummary =\n status === \"pass\"\n ? `all ${entries.length} tracked deps in line with baseline`\n : status === \"warn\"\n ? `${entries.filter((d) => d.drift !== \"same\").length} of ${entries.length} tracked deps drifted`\n : `${entries.filter((d) => d.drift === \"major\").length} deps lagging by a major version`;\n\n const outdated = await scanOutdated(ctx.site.path, ctx.spawn ?? defaultSpawn);\n const summary = outdated\n ? `${driftSummary}; ${outdated.outdated} outdated install(s) (${outdated.major} major)`\n : driftSummary;\n\n return {\n audit: \"deps\",\n site: siteLabel(ctx.site),\n status,\n summary,\n details: { entries, outdated } satisfies DepsDetails,\n };\n}\n","import { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { SpawnFn } from \"./util/spawn.js\";\n\n/** Real installed-version drift, distinct from the declared-range \"drift\" the\n * deps audit computes against the baseline: how many dependencies are behind\n * the registry's latest, per the committed lockfile. */\nexport type OutdatedCounts = { outdated: number; major: number };\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction majorOf(version: string): number {\n const head = version.replace(/^[\\^~]/, \"\").split(\".\")[0] ?? \"0\";\n const n = Number.parseInt(head, 10);\n return Number.isNaN(n) ? 0 : n;\n}\n\n/**\n * Count outdated dependencies for a site, using its committed lockfile as the\n * source of truth for \"what's installed/deployed\". Returns `null` (skip — the\n * caller degrades gracefully) when it can't determine this:\n * - no `pnpm-lock.yaml` (not a pnpm site, or never installed)\n * - the lockfile is stale vs package.json (`--frozen-lockfile` install fails)\n * - `pnpm outdated` output isn't parseable\n *\n * `pnpm outdated` exits non-zero precisely WHEN there are outdated packages, so\n * its exit code is ignored and only its JSON is parsed. `--frozen-lockfile`\n * never mutates the lockfile, so this stays read-only with respect to the repo.\n */\nexport async function scanOutdated(\n sitePath: string,\n spawn: SpawnFn,\n): Promise<OutdatedCounts | null> {\n if (!(await exists(join(sitePath, \"pnpm-lock.yaml\")))) return null;\n\n // Everything below is best-effort: a thrown spawn (timeout, `pnpm` not on\n // PATH, spawn error) must degrade to a skip (null), NOT bubble up and flip the\n // whole deps audit to a hard fail — the declared-range drift is independent of\n // pnpm and must still report. (Mirrors securityAudit's try/catch.)\n try {\n // Materialize node_modules from the lockfile, but only when it's missing —\n // an already-installed checkout skips the cold install. `--frozen-lockfile`\n // never rewrites the lockfile (read-only wrt the repo) and fails fast on a\n // lockfile out of sync with package.json → skip.\n if (!(await exists(join(sitePath, \"node_modules\")))) {\n const install = await spawn(\"pnpm\", [\"install\", \"--frozen-lockfile\"], {\n cwd: sitePath,\n timeoutMs: 180_000,\n });\n if (install.code !== 0) return null;\n }\n\n // `pnpm outdated` exits non-zero precisely WHEN there are outdated packages,\n // so its exit code is ignored and only its JSON is parsed.\n const res = await spawn(\"pnpm\", [\"outdated\", \"--json\"], {\n cwd: sitePath,\n timeoutMs: 60_000,\n });\n const parsed = JSON.parse(res.stdout || \"{}\") as Record<\n string,\n { current?: string; latest?: string }\n >;\n const entries = Object.values(parsed);\n return {\n outdated: entries.length,\n major: entries.filter((e) => e.current && e.latest && majorOf(e.latest) > majorOf(e.current))\n .length,\n };\n } catch {\n return null;\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { ESLint } from \"eslint\";\nimport { check as prettierCheck, resolveConfig as prettierResolveConfig } from \"prettier\";\nimport { glob } from \"tinyglobby\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport type { AuditContext } from \"./util/inject.js\";\n\nconst TARGET_GLOBS = [\"**/*.{ts,js,svelte}\"];\nconst IGNORE = [\"node_modules/**\", \"dist/**\", \".svelte-kit/**\", \"build/**\", \".netlify/**\"];\n\nasync function listFiles(cwd: string): Promise<string[]> {\n return glob(TARGET_GLOBS, { cwd, ignore: IGNORE, absolute: false });\n}\n\nexport async function lintAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const configPath = join(site.path, \"eslint.config.js\");\n\n if (!existsSync(configPath)) {\n return {\n audit: \"lint\",\n site: siteLabel(site),\n status: \"skip\",\n summary: \"no eslint config at site root\",\n };\n }\n\n const eslint = new ESLint({\n cwd: site.path,\n overrideConfigFile: configPath,\n errorOnUnmatchedPattern: false,\n });\n\n const relFiles = await listFiles(site.path);\n\n // Pass relative paths to ESLint; its cwd is already site.path. Avoids\n // dereferencing symlinks on pnpm workspaces.\n const eslintResults = await eslint.lintFiles(relFiles);\n const eslintErrors = eslintResults.reduce((n, r) => n + r.errorCount, 0);\n const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);\n\n const prettierUnformatted: string[] = [];\n for (const rel of relFiles) {\n const absForResolve = join(site.path, rel);\n const source = await readFile(absForResolve, \"utf-8\");\n const options = (await prettierResolveConfig(absForResolve)) ?? {};\n const ok = await prettierCheck(source, { ...options, filepath: absForResolve });\n if (!ok) prettierUnformatted.push(rel);\n }\n\n const status: AuditResult[\"status\"] =\n eslintErrors > 0 || prettierUnformatted.length > 0\n ? \"fail\"\n : eslintWarnings > 0\n ? \"warn\"\n : \"pass\";\n\n const summary =\n status === \"pass\"\n ? `lint clean across ${relFiles.length} files`\n : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;\n\n return {\n audit: \"lint\",\n site: siteLabel(site),\n status,\n summary,\n details: {\n eslintErrors,\n eslintWarnings,\n prettierUnformatted,\n files: relFiles.length,\n },\n };\n}\n","import { assertUrlSegment } from \"./gh.js\";\n\n/**\n * A `fetch`-based GitHub client covering the two operations the dashboard's\n * request-path Renovate trigger needs: resolve a repo's default branch and fire\n * a `workflow_dispatch`.\n *\n * Why not reuse `makeGitHub` (in gh.ts)? That client shells out to the `gh` CLI\n * (`spawn(\"gh\", …)`), which is the right tool inside GitHub Actions / a dev box\n * but is ABSENT in the Netlify Functions (AWS Lambda) runtime where this code\n * runs — so every `gh` call there throws `ENOENT`. The button's first live\n * dispatch surfaced exactly that as a 502. The Lambda runtime has a global\n * `fetch`, so we talk to the REST API directly.\n *\n * Mirrors the relevant subset of the `GitHub` interface so the `.mts` handler's\n * dispatch adapter is a drop-in swap. `fetch` is injectable for tests.\n */\n/** A subset of a GitHub Actions workflow run, mapped to camelCase. */\nexport type WorkflowRun = {\n id: number;\n status: string; // \"queued\" | \"in_progress\" | \"completed\" | \"requested\" | \"waiting\" | ...\n conclusion: string | null; // \"success\" | \"failure\" | \"cancelled\" | \"timed_out\" | ... | null\n createdAt: string; // ISO\n htmlUrl: string;\n};\n\n/** One GitHub Dependabot alert, mapped to the fields the security audit needs. */\nexport type DependabotAlert = {\n /** The vulnerable package name (`dependency.package.name`). */\n package: string;\n /** GitHub's raw severity vocabulary: \"low\" | \"medium\" | \"high\" | \"critical\". The audit layer\n * maps \"medium\" → the app's \"moderate\"; the rest pass through. */\n severity: string;\n /** Human advisory summary (`security_advisory.summary`). */\n summary: string;\n /** CVE id from the advisory's `cve_id`, as a 0- or 1-element array. Empty for GHSA-only\n * advisories (no CVE assigned) — those have no inline identifier here, but `url` still links\n * to the GHSA advisory page. (The GitHub `identifiers[]` array is intentionally not surfaced.) */\n cves: string[];\n /** Link to the alert (`html_url`), or null. */\n url: string | null;\n /** Dependency graph scope, or null when GitHub doesn't classify it. \"development\" marks a\n * build-time-only dependency (lower live-exploit surface for a static site). */\n scope: \"runtime\" | \"development\" | null;\n};\n\nexport type GitHubRest = {\n defaultBranch: (repo: string) => Promise<string>;\n /** Fire a `workflow_dispatch` for `<workflow>` (a filename like `renovate.yml`)\n * on `ref`. Needs the token's `actions:write` scope; a non-2xx (404 no such\n * workflow / 403 missing scope) surfaces as a thrown error carrying the status. */\n dispatchWorkflow: (repo: string, workflow: string, ref: string) => Promise<void>;\n /** List a workflow's runs, newest first, created on/after `opts.since` (ISO).\n * Used to re-find the run a prior `workflow_dispatch` started (dispatch returns\n * 204 with no id). Non-2xx surfaces as a thrown error carrying the status. */\n listWorkflowRuns: (\n repo: string,\n workflow: string,\n opts: { since: string; event?: string; perPage?: number },\n ) => Promise<WorkflowRun[]>;\n /** The name of the in-progress step of a run's in-progress job, or null if none\n * is currently running (between steps / completed). Used to show a coarse \"phase\"\n * in the refresh spinner. Non-2xx throws (callers treat it best-effort). */\n currentRunStep: (repo: string, runId: number) => Promise<string | null>;\n /** List a repo's Dependabot alerts, following pagination. `opts.state` defaults to \"open\".\n * Needs the token's Dependabot-alerts read permission (fine-grained PAT) or `repo`/\n * `security_events` (classic PAT); a non-2xx (403 missing scope / 404 alerts disabled)\n * throws carrying the status so the caller can fall back. */\n listDependabotAlerts: (\n repo: string,\n opts?: { state?: \"open\" | \"dismissed\" | \"fixed\" | \"auto_dismissed\" },\n ) => Promise<DependabotAlert[]>;\n};\n\nconst GITHUB_API = \"https://api.github.com\";\n\n/** Per-request timeout for the Dependabot fetch. It runs inside the nightly fleet sweep at\n * `--concurrency 1`, so a hung connection (half-open TCP / accept-but-no-response) would otherwise\n * stall the whole sweep — `fetch` has no default overall timeout. With this signal a hang rejects\n * with an AbortError, which the audit's try/catch converts to the pnpm-audit fallback. */\nconst DEPENDABOT_FETCH_TIMEOUT_MS = 15_000;\n\n/** Extract the `rel=\"next\"` URL from a GitHub `Link` header, or null when absent. GitHub's\n * Dependabot-alerts endpoint paginates ONLY by opaque `after` cursor exposed via this header —\n * there is no numeric `page` param (it is silently ignored) — so following rel=\"next\" is the only\n * correct way to advance pages. */\nfunction nextLink(link: string | null): string | null {\n if (!link) return null;\n for (const part of link.split(\",\")) {\n const m = part.match(/<([^>]+)>\\s*;\\s*rel=\"next\"/);\n if (m) return m[1] ?? null;\n }\n return null;\n}\n\n/** Split + validate an `owner/repo`, mirroring the shape check in gh.ts's methods. */\nfunction splitRepo(repo: string): { owner: string; name: string } {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`expected \"owner/repo\", got ${JSON.stringify(repo)}`);\n }\n return { owner, name };\n}\n\n/** Best-effort response body for an error message; capped so a huge body can't bloat logs. */\nasync function bodyText(res: Response): Promise<string> {\n try {\n return (await res.text()).slice(0, 300);\n } catch {\n return \"<no body>\";\n }\n}\n\n/** Map one raw Dependabot-alert object to {@link DependabotAlert}, or null if it carries no\n * usable package name (fail-soft — a malformed alert is dropped rather than propagated). */\nfunction mapDependabotAlert(raw: Record<string, unknown>): DependabotAlert | null {\n const dependency = (raw[\"dependency\"] ?? {}) as Record<string, unknown>;\n const pkg = (dependency[\"package\"] ?? {}) as Record<string, unknown>;\n const packageName = typeof pkg[\"name\"] === \"string\" ? (pkg[\"name\"] as string) : null;\n if (!packageName) return null;\n\n const adv = (raw[\"security_advisory\"] ?? {}) as Record<string, unknown>;\n const cveId = typeof adv[\"cve_id\"] === \"string\" ? (adv[\"cve_id\"] as string) : null;\n const rawScope = dependency[\"scope\"];\n return {\n package: packageName,\n severity: typeof adv[\"severity\"] === \"string\" ? (adv[\"severity\"] as string) : \"\",\n summary: typeof adv[\"summary\"] === \"string\" ? (adv[\"summary\"] as string) : \"\",\n cves: cveId ? [cveId] : [],\n url: typeof raw[\"html_url\"] === \"string\" ? (raw[\"html_url\"] as string) : null,\n scope: rawScope === \"runtime\" || rawScope === \"development\" ? rawScope : null,\n };\n}\n\nexport function makeGitHubRest(deps: { token: string; fetch?: typeof fetch }): GitHubRest {\n const doFetch = deps.fetch ?? fetch;\n // GitHub requires a User-Agent; the rest are the documented JSON-API headers.\n const baseHeaders: Record<string, string> = {\n authorization: `Bearer ${deps.token}`,\n accept: \"application/vnd.github+json\",\n \"x-github-api-version\": \"2022-11-28\",\n \"user-agent\": \"reddoor-maintenance-dashboard\",\n };\n\n return {\n async defaultBranch(repo) {\n const { owner, name } = splitRepo(repo);\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n const res = await doFetch(`${GITHUB_API}/repos/${owner}/${name}`, { headers: baseHeaders });\n if (!res.ok) {\n throw new Error(\n `GitHub GET repos/${owner}/${name} failed (${res.status}): ${await bodyText(res)}`,\n );\n }\n let body: { default_branch?: string };\n try {\n body = (await res.json()) as { default_branch?: string };\n } catch {\n throw new Error(`GitHub repos/${owner}/${name}: 200 with a non-JSON body`);\n }\n if (!body.default_branch) {\n throw new Error(`GitHub repos/${owner}/${name}: response had no default_branch`);\n }\n return body.default_branch;\n },\n\n async dispatchWorkflow(repo, workflow, ref) {\n const { owner, name } = splitRepo(repo);\n // Every segment interpolates into the API path — guard them all (defense in\n // depth), matching gh.ts's dispatchWorkflow. `ref` is repo-sourced.\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n assertUrlSegment(\"path\", workflow);\n assertUrlSegment(\"branch\", ref);\n const res = await doFetch(\n `${GITHUB_API}/repos/${owner}/${name}/actions/workflows/${workflow}/dispatches`,\n {\n method: \"POST\",\n headers: { ...baseHeaders, \"content-type\": \"application/json\" },\n body: JSON.stringify({ ref }),\n },\n );\n // 204 No Content on success.\n if (!res.ok) {\n throw new Error(\n `GitHub workflow_dispatch ${owner}/${name}/${workflow} failed (${res.status}): ${await bodyText(res)}`,\n );\n }\n },\n\n async listWorkflowRuns(repo, workflow, opts) {\n const { owner, name } = splitRepo(repo);\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n assertUrlSegment(\"path\", workflow);\n const qs = new URLSearchParams({\n created: `>=${opts.since}`,\n per_page: String(opts.perPage ?? 1),\n });\n if (opts.event) qs.set(\"event\", opts.event);\n const res = await doFetch(\n `${GITHUB_API}/repos/${owner}/${name}/actions/workflows/${workflow}/runs?${qs.toString()}`,\n { headers: baseHeaders },\n );\n if (!res.ok) {\n throw new Error(\n `GitHub GET runs ${owner}/${name}/${workflow} failed (${res.status}): ${await bodyText(res)}`,\n );\n }\n let body: { workflow_runs?: Array<Record<string, unknown>> };\n try {\n body = (await res.json()) as { workflow_runs?: Array<Record<string, unknown>> };\n } catch {\n throw new Error(`GitHub runs ${owner}/${name}/${workflow}: 200 with a non-JSON body`);\n }\n return (\n (body.workflow_runs ?? [])\n .map((r) => ({\n id: Number(r.id),\n status: String(r.status ?? \"\"),\n conclusion: (r.conclusion as string | null) ?? null,\n createdAt: String(r.created_at ?? \"\"),\n htmlUrl: String(r.html_url ?? \"\"),\n }))\n // Fail-soft like the other fields: a record with a missing/garbage id\n // (Number(...) → NaN) is dropped rather than propagated.\n .filter((r) => Number.isFinite(r.id))\n );\n },\n\n async currentRunStep(repo, runId) {\n const { owner, name } = splitRepo(repo);\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n if (!Number.isInteger(runId) || runId < 0) {\n throw new Error(`currentRunStep: expected a non-negative integer runId, got ${runId}`);\n }\n const res = await doFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs/${runId}/jobs`, {\n headers: baseHeaders,\n });\n if (!res.ok) {\n throw new Error(\n `GitHub GET run ${owner}/${name}/${runId} jobs failed (${res.status}): ${await bodyText(res)}`,\n );\n }\n let body: {\n jobs?: Array<{ status?: string; steps?: Array<{ name?: string; status?: string }> }>;\n };\n try {\n body = (await res.json()) as typeof body;\n } catch {\n throw new Error(`GitHub run ${owner}/${name}/${runId} jobs: 200 with a non-JSON body`);\n }\n const jobs = body.jobs ?? [];\n // Prefer the in-progress job; fall back to the first job (single-job workflows).\n const job = jobs.find((j) => j.status === \"in_progress\") ?? jobs[0];\n const step = (job?.steps ?? []).find((s) => s.status === \"in_progress\");\n return step?.name ?? null;\n },\n\n async listDependabotAlerts(repo, opts) {\n const { owner, name } = splitRepo(repo);\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n const state = opts?.state ?? \"open\";\n const out: DependabotAlert[] = [];\n // Follow the Link rel=\"next\" cursor (NOT a numeric `page` param — that is silently ignored\n // and would re-fetch page 1 forever, duplicating counts). The iteration cap is a runaway\n // backstop. Each request carries an abort timeout so a hung connection rejects (→ the\n // caller's try/catch → pnpm fallback) instead of stalling the whole sequential fleet sweep.\n let url: string | null =\n `${GITHUB_API}/repos/${owner}/${name}/dependabot/alerts?` +\n new URLSearchParams({ state, per_page: \"100\" }).toString();\n for (let i = 0; i < 100 && url; i++) {\n const res = await doFetch(url, {\n headers: baseHeaders,\n signal: AbortSignal.timeout(DEPENDABOT_FETCH_TIMEOUT_MS),\n });\n if (!res.ok) {\n throw new Error(\n `GitHub GET dependabot/alerts ${owner}/${name} failed (${res.status}): ${await bodyText(res)}`,\n );\n }\n let body: unknown;\n try {\n body = await res.json();\n } catch {\n throw new Error(`GitHub dependabot/alerts ${owner}/${name}: 200 with a non-JSON body`);\n }\n // The alerts endpoint returns a bare array; anything else means we're done.\n if (!Array.isArray(body)) break;\n for (const raw of body as Array<Record<string, unknown>>) {\n const mapped = mapDependabotAlert(raw);\n if (mapped) out.push(mapped);\n }\n url = nextLink(res.headers.get(\"link\"));\n }\n return out;\n },\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { defaultSpawn, type SpawnResult } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { makeGitHubRest, type DependabotAlert } from \"../github/gh-rest.js\";\nimport { readGitHubConfig } from \"../github/config.js\";\n\ntype Severity = \"low\" | \"moderate\" | \"high\" | \"critical\";\n\ntype Counts = { low: number; moderate: number; high: number; critical: number };\n\ntype AdvisoryEntry = {\n module: string;\n severity: Severity;\n title: string;\n cves?: string[];\n url?: string;\n /** Dependency graph scope from Dependabot (\"runtime\" | \"development\"); absent for the\n * lockfile `pnpm audit` fallback, which carries no per-package scope. Display-only. */\n scope?: \"runtime\" | \"development\";\n};\n\n// pnpm audit output (npm-compat with extra advisories map keyed by ID).\ntype PnpmAuditJson = {\n metadata?: { vulnerabilities?: Partial<Counts> };\n advisories?: Record<\n string,\n {\n id?: number;\n title?: string;\n module_name?: string;\n severity?: string;\n cves?: string[];\n url?: string;\n }\n >;\n};\n\n// npm v7+ shape (vulnerabilities keyed by package name).\ntype NpmAuditJson = {\n metadata?: { vulnerabilities?: Partial<Counts> };\n vulnerabilities?: Record<\n string,\n {\n name?: string;\n severity?: string;\n via?: unknown;\n url?: string;\n }\n >;\n};\n\nfunction classify(v: Counts) {\n if (v.critical > 0 || v.high > 0) return \"fail\" as const;\n if (v.moderate > 0 || v.low > 0) return \"warn\" as const;\n return \"pass\" as const;\n}\n\nfunction normalizeSeverity(s: unknown): Severity {\n if (s === \"low\" || s === \"moderate\" || s === \"high\" || s === \"critical\") return s;\n // npm/pnpm sometimes emit \"info\" for informational advisories. Map down\n // rather than defaulting to \"moderate\" (which would inflate severity).\n return \"low\";\n}\n\nfunction extractAdvisoriesFromPnpm(parsed: PnpmAuditJson): AdvisoryEntry[] {\n const out: AdvisoryEntry[] = [];\n for (const a of Object.values(parsed.advisories ?? {})) {\n if (!a) continue;\n out.push({\n module: a.module_name ?? \"unknown\",\n severity: normalizeSeverity(a.severity),\n title: a.title ?? \"(no title)\",\n ...(a.cves ? { cves: a.cves } : {}),\n ...(a.url ? { url: a.url } : {}),\n });\n }\n return out;\n}\n\n/** Walk an npm v7+ `via` chain to find the root entry whose `via` array\n * contains a real advisory object (rather than another package name string).\n * Returns the package name at the root and the advisory detail. */\nfunction resolveNpmAdvisoryRoot(\n startName: string,\n vulnerabilities: NonNullable<NpmAuditJson[\"vulnerabilities\"]>,\n): { rootName: string; detail?: { title?: string; url?: string } } {\n const seen = new Set<string>();\n let current = startName;\n while (!seen.has(current)) {\n seen.add(current);\n const entry = vulnerabilities[current];\n if (!entry || !Array.isArray(entry.via)) return { rootName: current };\n\n const detailed = entry.via.find(\n (e): e is { title?: string; url?: string } => typeof e === \"object\" && e !== null,\n );\n if (detailed) return { rootName: current, detail: detailed };\n\n const next = entry.via.find((e): e is string => typeof e === \"string\");\n if (!next || next === current) return { rootName: current };\n current = next;\n }\n return { rootName: current };\n}\n\nfunction extractAdvisoriesFromNpm(parsed: NpmAuditJson): AdvisoryEntry[] {\n const vulnerabilities = parsed.vulnerabilities ?? {};\n const roots = new Map<string, AdvisoryEntry>();\n\n for (const [name, v] of Object.entries(vulnerabilities)) {\n if (!v) continue;\n const { rootName, detail } = resolveNpmAdvisoryRoot(name, vulnerabilities);\n if (roots.has(rootName)) continue; // already surfaced via another transitive entry\n\n const rootEntry = vulnerabilities[rootName];\n const severity = normalizeSeverity(rootEntry?.severity ?? v.severity);\n const title = detail?.title ?? rootName;\n const url = detail?.url;\n\n roots.set(rootName, {\n module: rootEntry?.name ?? rootName,\n severity,\n title,\n ...(url ? { url } : {}),\n });\n }\n\n return [...roots.values()];\n}\n\ntype ToolResult =\n | { kind: \"missing\" }\n | { kind: \"error\"; reason: string }\n | { kind: \"ok\"; parsed: PnpmAuditJson & NpmAuditJson };\n\nasync function runAuditTool(\n spawn: (cmd: string, args: readonly string[], opts?: { cwd?: string }) => Promise<SpawnResult>,\n cmd: string,\n args: readonly string[],\n cwd: string,\n): Promise<ToolResult> {\n let raw: SpawnResult;\n try {\n raw = await spawn(cmd, args, { cwd });\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) return { kind: \"missing\" };\n return { kind: \"error\", reason: `spawn failed: ${String(err).slice(0, 200)}` };\n }\n\n // 0 = clean, 1 = vulns found. Anything else is a real error.\n if (raw.code !== 0 && raw.code !== 1) {\n return {\n kind: \"error\",\n reason: `exit ${raw.code}${raw.stderr ? `: ${raw.stderr.slice(0, 150)}` : \"\"}`,\n };\n }\n\n let parsed: PnpmAuditJson & NpmAuditJson;\n try {\n parsed = JSON.parse(raw.stdout || \"{}\") as PnpmAuditJson & NpmAuditJson;\n } catch (err) {\n return { kind: \"error\", reason: `unparseable JSON: ${String(err).slice(0, 100)}` };\n }\n\n // pnpm error envelope: { error: { code, message } }. npm sometimes emits\n // a top-level error too. Either means the audit didn't actually run.\n const errEnvelope = (parsed as unknown as { error?: { code?: string } }).error;\n if (errEnvelope && typeof errEnvelope === \"object\") {\n return { kind: \"error\", reason: errEnvelope.code ?? \"error envelope returned\" };\n }\n\n // Without metadata.vulnerabilities there are no counts to report and we\n // can't trust the result. An empty `{}` is just as suspect as a missing\n // key — counts default to 0 and we'd silently report \"pass\". Treat both\n // as a tool failure so the caller can fall through to the other audit.\n const vulnsMeta = parsed.metadata?.vulnerabilities;\n if (!vulnsMeta || Object.keys(vulnsMeta).length === 0) {\n return { kind: \"error\", reason: \"no metadata.vulnerabilities in output\" };\n }\n\n return { kind: \"ok\", parsed };\n}\n\n/** The Dependabot fetch the security audit needs, injectable for tests. The real default is\n * built from GITHUB_TOKEN; absent token (or a site with no gitRepo) → pnpm/npm audit fallback. */\nexport type DependabotDeps = {\n listAlerts: (repo: string) => Promise<DependabotAlert[]>;\n};\n\nfunction defaultDependabotDeps(): DependabotDeps | null {\n const cfg = readGitHubConfig();\n if (!cfg) return null;\n const gh = makeGitHubRest({ token: cfg.token });\n return { listAlerts: (repo) => gh.listDependabotAlerts(repo, { state: \"open\" }) };\n}\n\n/** Map GitHub's severity vocabulary onto the app's. \"medium\" → \"moderate\"; an unrecognized\n * value maps DOWN to \"low\" rather than inflating (mirrors `normalizeSeverity`). */\nfunction ghSeverityToApp(s: string): Severity {\n if (s === \"critical\" || s === \"high\" || s === \"low\") return s;\n if (s === \"medium\" || s === \"moderate\") return \"moderate\";\n return \"low\";\n}\n\n/** Build a security AuditResult from GitHub Dependabot alerts, or null if the fetch failed so\n * the caller falls back to `pnpm audit`. ALL open alerts count toward the severity tallies —\n * the cockpit's auto-patching (amber) vs Renovate-exhausted (red) bands decide urgency, so the\n * prod/dev scope is carried on each advisory for display only, not used to weight the counts. */\nasync function dependabotAudit(\n deps: DependabotDeps,\n repo: string,\n label: string,\n): Promise<AuditResult | null> {\n let alerts: DependabotAlert[];\n try {\n alerts = await deps.listAlerts(repo);\n } catch {\n return null;\n }\n\n const counts: Counts = { low: 0, moderate: 0, high: 0, critical: 0 };\n const advisories: AdvisoryEntry[] = [];\n for (const a of alerts) {\n const severity = ghSeverityToApp(a.severity);\n counts[severity] += 1;\n advisories.push({\n module: a.package,\n severity,\n title: a.summary || \"(no title)\",\n ...(a.cves.length > 0 ? { cves: a.cves } : {}),\n ...(a.url ? { url: a.url } : {}),\n ...(a.scope ? { scope: a.scope } : {}),\n });\n }\n\n const status = classify(counts);\n const total = counts.low + counts.moderate + counts.high + counts.critical;\n const summary =\n status === \"pass\"\n ? \"Dependabot: 0 alerts\"\n : `Dependabot: ${total} alert(s) (${counts.critical}C/${counts.high}H/${counts.moderate}M/${counts.low}L)`;\n\n return { audit: \"security\", site: label, status, summary, details: { counts, advisories } };\n}\n\nexport async function securityAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n // Prefer GitHub Dependabot alerts (ground truth: prod+dev, GitHub Advisory DB) for repo-backed\n // sites. The lockfile `pnpm audit` below stays the fallback for sites with no GitHub repo, no\n // token, or a transient API failure — a Dependabot hiccup must never fail an otherwise-fine site.\n if (site.gitRepo) {\n const dependabot = ctx.dependabotDeps ?? defaultDependabotDeps();\n if (dependabot) {\n const viaDependabot = await dependabotAudit(dependabot, site.gitRepo, label);\n if (viaDependabot) return viaDependabot;\n }\n }\n\n let used: \"pnpm audit\" | \"npm audit\" = \"pnpm audit\";\n let result = await runAuditTool(spawn, \"pnpm\", [\"audit\", \"--json\", \"--prod\"], site.path);\n\n // Fall through to npm if pnpm is missing OR pnpm couldn't actually\n // audit the project (e.g., no pnpm-lock.yaml). Previously we only fell\n // through on ENOENT, which meant npm-using sites silently reported \"pass\"\n // because pnpm returned an error envelope with no metadata.\n if (result.kind !== \"ok\") {\n const pnpmReason = result.kind === \"missing\" ? \"not installed\" : result.reason;\n const npmResult = await runAuditTool(\n spawn,\n \"npm\",\n [\"audit\", \"--json\", \"--omit=dev\"],\n site.path,\n );\n if (npmResult.kind === \"ok\") {\n result = npmResult;\n used = \"npm audit\";\n } else {\n const npmReason = npmResult.kind === \"missing\" ? \"not installed\" : npmResult.reason;\n return {\n audit: \"security\",\n site: label,\n status: \"skip\",\n summary: `cannot run audit — pnpm: ${pnpmReason}; npm: ${npmReason}`,\n };\n }\n }\n\n const parsed = result.parsed;\n\n const counts: Counts = {\n low: parsed.metadata?.vulnerabilities?.low ?? 0,\n moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,\n high: parsed.metadata?.vulnerabilities?.high ?? 0,\n critical: parsed.metadata?.vulnerabilities?.critical ?? 0,\n };\n\n const advisories =\n used === \"pnpm audit\" ? extractAdvisoriesFromPnpm(parsed) : extractAdvisoriesFromNpm(parsed);\n\n const status = classify(counts);\n const total = counts.low + counts.moderate + counts.high + counts.critical;\n const summary =\n status === \"pass\"\n ? `${used}: 0 vulnerabilities`\n : `${used}: ${total} vulnerabilities (${counts.critical}C/${counts.high}H/${counts.moderate}M/${counts.low}L)`;\n\n return {\n audit: \"security\",\n site: label,\n status,\n summary,\n details: { counts, advisories },\n };\n}\n","import { readFile, writeFile, mkdtemp, rm, readdir } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AuditResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { lighthouseConfig } from \"../configs/lighthouse.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { SpawnFn, SpawnResult } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { readSiteConfig } from \"./util/site-config.js\";\nimport { findFreePort, withFreePort } from \"../util/free-port.js\";\n\ntype ManifestEntry = {\n url: string;\n summary: Record<string, number>;\n htmlPath?: string;\n jsonPath?: string;\n};\n\ntype AssertionResult = {\n name: string;\n actual: number;\n expected: number;\n operator: string;\n passed: boolean;\n level: \"warn\" | \"error\";\n auditProperty?: string;\n auditId?: string;\n};\n\ntype NormalizedLhciResult = {\n summary: Record<string, number>;\n assertionsFailed: number;\n assertions: Array<{ category: string; level: \"warn\" | \"error\"; message: string }>;\n};\n\nasync function readJsonMaybe<T>(path: string): Promise<T | null> {\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\ntype LhrFile = {\n requestedUrl: string;\n finalUrl?: string;\n categories: Record<string, { score: number | null }>;\n};\n\n/**\n * Build manifest-equivalent entries by scanning the `.lighthouseci/` dir\n * for `lhr-*.json` files written by `lhci collect`. We used to read\n * `manifest.json` directly, but lhci 0.15+ no longer writes it — the\n * audit would silently return \"no manifest written\" against a perfectly\n * healthy run. Reproduced on caltex 2026-05-28 (0.10.5 dogfood).\n */\nasync function readLhrEntries(resultsDir: string): Promise<ManifestEntry[]> {\n const files = await readdir(resultsDir).catch(() => [] as string[]);\n const entries: ManifestEntry[] = [];\n for (const f of files) {\n if (!f.startsWith(\"lhr-\") || !f.endsWith(\".json\")) continue;\n const lhr = await readJsonMaybe<LhrFile>(join(resultsDir, f));\n if (!lhr || !lhr.categories) continue;\n const summary: Record<string, number> = {};\n for (const [k, v] of Object.entries(lhr.categories)) {\n if (typeof v?.score === \"number\") summary[k] = v.score;\n }\n entries.push({ url: lhr.requestedUrl, summary });\n }\n return entries;\n}\n\nfunction averageSummaries(entries: ManifestEntry[]): Record<string, number> {\n if (entries.length === 0) return {};\n const sums: Record<string, number> = {};\n const counts: Record<string, number> = {};\n for (const e of entries) {\n for (const [k, v] of Object.entries(e.summary ?? {})) {\n if (typeof v !== \"number\") continue;\n sums[k] = (sums[k] ?? 0) + v;\n counts[k] = (counts[k] ?? 0) + 1;\n }\n }\n const out: Record<string, number> = {};\n for (const k of Object.keys(sums)) {\n const total = sums[k] ?? 0;\n const count = counts[k] ?? 1;\n out[k] = total / count;\n }\n return out;\n}\n\nfunction categoryFromAssertion(a: AssertionResult): string {\n // `name` looks like \"categories:accessibility\" or \"audits:uses-http2\".\n const colonIdx = a.name.indexOf(\":\");\n return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;\n}\n\nfunction messageForAssertion(a: AssertionResult): string {\n // `a.actual` is parsed from external lhci/Lighthouse JSON; a malformed or\n // missing value (not a number) would make `.toFixed` throw and crash the whole\n // audit. Guard it and fall back to a readable string instead.\n const actual = typeof a.actual === \"number\" ? a.actual.toFixed(2) : \"n/a\";\n return `${a.name} ${a.operator} ${a.expected} (actual: ${actual})`;\n}\n\n/** Shared tail: scan `.lighthouseci/` for lhr-*.json + assertion-results.json and\n * build the AuditResult. Identical for the checkout and deployed paths. */\nasync function parseLhciResults(\n resultsDir: string,\n label: string,\n raw: SpawnResult,\n): Promise<AuditResult> {\n const manifest = await readLhrEntries(resultsDir);\n\n if (manifest.length === 0) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"fail\",\n summary: `lighthouse: no lhr-*.json written (exit ${raw.code})${\n raw.stderr ? ` — ${raw.stderr.slice(0, 200)}` : \"\"\n }`,\n };\n }\n\n const assertionResults =\n (await readJsonMaybe<AssertionResult[]>(join(resultsDir, \"assertion-results.json\"))) ?? [];\n\n const failed = assertionResults.filter((a) => !a.passed);\n const assertions = failed.map((a) => ({\n category: categoryFromAssertion(a),\n level: a.level,\n message: messageForAssertion(a),\n }));\n\n const anyError = assertions.some((a) => a.level === \"error\");\n const anyWarn = assertions.some((a) => a.level === \"warn\");\n const status: AuditResult[\"status\"] = anyError ? \"fail\" : anyWarn ? \"warn\" : \"pass\";\n\n const normalized: NormalizedLhciResult = {\n summary: averageSummaries(manifest),\n assertionsFailed: failed.length,\n assertions,\n };\n\n const summary =\n status === \"pass\"\n ? \"lighthouse: all categories passing\"\n : `lighthouse: ${failed.length} assertion(s) failed`;\n\n return { audit: \"lighthouse\", site: label, status, summary, details: normalized };\n}\n\n/** Checkout mode (unchanged behavior): boot the site's vite dev server on a\n * pinned free port and audit the local fixtures/override URL. */\nasync function checkoutLighthouse(spawn: SpawnFn, site: Site, label: string): Promise<AuditResult> {\n const siteCfg = await readSiteConfig(site.path);\n // Allocate a free port + force vite to `--strictPort` so the spawned dev\n // server either binds the port we picked or fails loudly (caltex 2026-05-28\n // zombie-vite incident).\n const port = await findFreePort();\n const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];\n const resolvedConfig = {\n ...lighthouseConfig,\n ci: {\n ...lighthouseConfig.ci,\n collect: {\n ...lighthouseConfig.ci.collect,\n url: [withFreePort(baseUrl, port)],\n startServerCommand: `npm run vite:dev -- --port ${port} --strictPort`,\n },\n },\n };\n\n const configDir = await mkdtemp(join(tmpdir(), \"reddoor-lhci-\"));\n const configPath = join(configDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(site.path, \".lighthouseci\");\n await rm(resultsDir, { recursive: true, force: true });\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: site.path,\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(configDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n await rm(configDir, { recursive: true, force: true });\n\n return parseLhciResults(resultsDir, label, raw);\n}\n\n/** Deployed mode: audit a production URL directly — no checkout, no dev server.\n * Runs in a throwaway tmp cwd; uploads to the filesystem so fleet runs never\n * push 200 public reports to temporary-public-storage. */\nasync function deployedLighthouse(\n spawn: SpawnFn,\n deployedUrl: string,\n label: string,\n): Promise<AuditResult> {\n const workDir = await mkdtemp(join(tmpdir(), \"reddoor-lh-deployed-\"));\n const resolvedConfig = {\n ci: {\n // Deliberately NOT spread from lighthouseConfig.ci.collect: deployed mode\n // must omit startServerCommand and the dev-server settings entirely.\n collect: {\n url: [deployedUrl],\n // 3 runs to damp Lighthouse's run-to-run variance; parseLhciResults\n // averages the lhr files. (Median is a tracked future refinement.)\n numberOfRuns: 3,\n // Use devtools (applied) throttling instead of the desktop preset's\n // default `simulate` (Lantern). Lantern estimates LCP from the\n // dependency graph and can't model an LCP element revealed by a\n // JS-load opacity fade (e.g. gallerysonder's GridImage): on a fast\n // observed load it finds no LCP node and throws NO_LCP, nulling the\n // perf score, which the cockpit then coerces to a misleading 0.\n // devtools reads the real LCP paint event from the throttled trace\n // instead. Trade-off: slightly noisier run-to-run, damped by the\n // numberOfRuns:3 average above.\n settings: { preset: \"desktop\", throttlingMethod: \"devtools\", skipAudits: [\"uses-http2\"] },\n },\n assert: lighthouseConfig.ci.assert,\n upload: { target: \"filesystem\", outputDir: join(workDir, \"lhci-report\") },\n },\n };\n\n const configPath = join(workDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(workDir, \".lighthouseci\");\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: workDir,\n // 3 serial cold runs of a slow deployed site (lhci's own maxWaitForLoad\n // ~45-60s each) + first-use Chrome download can plausibly exceed 3 min →\n // SIGTERM → no lhr-*.json → spurious \"no scores\". Match the 5-min budget\n // the checkout path already gives (erp-industrials nightly flake,\n // morning-brief 2026-06-10 MEDIUM-F).\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(workDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n\n try {\n return await parseLhciResults(resultsDir, label, raw);\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\nexport async function lighthouseAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n return site.deployedUrl\n ? deployedLighthouse(spawn, site.deployedUrl, label)\n : checkoutLighthouse(spawn, site, label);\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport type SiteConfig = {\n /** Override URL the lighthouse audit hits. Sites without the default\n * `/dev/a11y-fixtures` dev route set this to their homepage. */\n lighthouseUrl?: string;\n};\n\n/**\n * Read per-site overrides from `package.json#reddoor`. Returns `{}` on any\n * failure (missing file, malformed JSON, missing key, wrong type) so every\n * caller can safely fall back to its built-in default. Never throws.\n */\nexport async function readSiteConfig(sitePath: string): Promise<SiteConfig> {\n let raw: string;\n try {\n raw = await readFile(join(sitePath, \"package.json\"), \"utf-8\");\n } catch {\n return {};\n }\n let pkg: unknown;\n try {\n pkg = JSON.parse(raw);\n } catch {\n return {};\n }\n if (!pkg || typeof pkg !== \"object\") return {};\n const cfg = (pkg as { reddoor?: unknown }).reddoor;\n if (!cfg || typeof cfg !== \"object\") return {};\n\n const out: SiteConfig = {};\n const url = (cfg as { lighthouseUrl?: unknown }).lighthouseUrl;\n if (typeof url === \"string\" && url.length > 0) {\n out.lighthouseUrl = url;\n }\n return out;\n}\n","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","import { readFile, writeFile, mkdtemp, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { a11yRoutes, smokeRoutes } from \"../configs/playwright-a11y.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { findFreePort } from \"../util/free-port.js\";\n\ntype Impact = \"minor\" | \"moderate\" | \"serious\" | \"critical\";\n\ntype AxeViolation = {\n id: string;\n impact: Impact;\n route: string;\n help?: string;\n helpUrl?: string;\n nodes?: Array<{ html?: string; target?: string[] }>;\n};\n\ntype NormalizedA11y = {\n totalViolations: number;\n byImpact: Partial<Record<Impact, number>>;\n violations: AxeViolation[];\n};\n\nconst RESULTS_REL = \".reddoor-a11y/results.json\";\n\nasync function readJsonMaybe<T>(path: string): Promise<T | null> {\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\n// The audit-controlled playwright config. We synthesize it (rather than\n// rely on the site's playwright.config.ts) so we can pin the dev server\n// port + force `--strictPort` — same fix as the lighthouse audit, same\n// reason (zombie vite processes squatting on 5173 would otherwise eat\n// the audit's request and return stale 404s).\nfunction buildPlaywrightConfig(port: number, sitePath: string): string {\n return `import { defineConfig } from \"@playwright/test\";\n\nexport default defineConfig({\n testDir: \".\",\n testMatch: /.*\\\\.spec\\\\.ts$/,\n fullyParallel: true,\n forbidOnly: !!process.env.CI,\n retries: process.env.CI ? 2 : 0,\n reporter: process.env.CI ? \"github\" : \"list\",\n use: {\n baseURL: \"http://localhost:${port}\",\n trace: \"on-first-retry\",\n },\n webServer: {\n // --strictPort: refuse to bump to a different port if ours is taken,\n // so the audit fails loudly instead of probing a zombie.\n // reuseExistingServer:false: never reuse — we control the lifecycle.\n // cwd: playwright's default webServer.cwd is the config file's\n // directory. Our config lives in /tmp so without this override,\n // \"npm run vite:dev\" tries to read /tmp/.../package.json and\n // ENOENTs before vite ever starts. Caltex 2026-05-28 (0.10.5).\n command: \"npm run vite:dev -- --port ${port} --strictPort\",\n url: \"http://localhost:${port}/dev/a11y-fixtures\",\n cwd: ${JSON.stringify(sitePath)},\n reuseExistingServer: false,\n timeout: 120_000,\n },\n});\n`;\n}\n\n// The spec the audit writes runs all configured routes through axe in a single\n// test (so worker isolation doesn't fragment the collected violations) and\n// writes the structured result to <cwd>/.reddoor-a11y/results.json before\n// asserting. That way, the audit can read real axe details even when the\n// expect(...).toEqual([]) assertion fails.\nfunction buildSpec(): string {\n return `import { test, expect } from \"@playwright/test\";\nimport AxeBuilder from \"@axe-core/playwright\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\nconst pages = ${JSON.stringify(a11yRoutes)};\nconst smokePages = ${JSON.stringify(smokeRoutes)};\nconst OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;\n\n// Playwright's default per-test timeout is 30s. We loop through every\n// configured route in a single test, so the budget needs to scale.\ntest.setTimeout(5 * 60_000);\n\ntest(\"a11y + hydration across configured routes\", async ({ page }) => {\n const violations = [];\n\n // Capture uncaught client-side exceptions across every route we visit. A page\n // that builds + SSRs cleanly can still throw on hydrate and blank itself\n // (data-dynamiq: a Svelte 4->5 run() referenced a $state declared after it) --\n // axe never sees that, so we listen for it directly and tag the route in scope.\n let currentRoute = \"\";\n page.on(\"pageerror\", (err) => {\n violations.push({\n id: \"client-error\",\n impact: \"critical\",\n route: currentRoute,\n help: String(err && err.message ? err.message : err),\n });\n });\n\n for (const { path, name } of pages) {\n currentRoute = name;\n await page.goto(path);\n // Snap CSS transitions/animations to their resting state before axe runs.\n // AnimateIn-style fixtures transition opacity 0->1; sampling mid-transition\n // makes axe compute color-contrast against semi-transparent text, yielding a\n // flaky \"serious\" color-contrast violation (~1/3 of runs on /dev/animate-in).\n // Disabling transitions/animations forces the final, rendered state\n // deterministically -- which is also what users (and prefers-reduced-motion\n // users) actually see, so it's the correct thing to assert.\n await page.addStyleTag({\n content: \"*,*::before,*::after{transition:none!important;animation:none!important;}\",\n });\n const results = await new AxeBuilder({ page })\n .withTags([\"wcag2a\",\"wcag2aa\",\"wcag21a\",\"wcag21aa\",\"wcag22aa\"])\n .analyze();\n for (const v of results.violations) {\n violations.push({\n id: v.id,\n impact: v.impact ?? \"moderate\",\n route: name,\n help: v.help,\n helpUrl: v.helpUrl,\n nodes: v.nodes.map((n) => ({ html: n.html, target: n.target })),\n });\n }\n }\n\n // Hydration smoke check: load real routes (the homepage) and fail on any\n // uncaught client-side error. No axe here -- real routes carry pre-existing\n // a11y debt we don't gate on; we only assert they don't crash on hydrate.\n // HTTP/SSR errors don't fire 'pageerror', so a data-less CI homepage that\n // renders empty-but-valid won't false-fail -- only a real client crash does.\n for (const { path, name } of smokePages) {\n currentRoute = name;\n await page.goto(path);\n // Let hydration + first effects run so a TDZ/ReferenceError surfaces.\n await page.waitForTimeout(2000);\n }\n\n const byImpact = {};\n for (const v of violations) {\n byImpact[v.impact] = (byImpact[v.impact] ?? 0) + 1;\n }\n if (OUTPUT) {\n await mkdir(dirname(OUTPUT), { recursive: true });\n await writeFile(\n OUTPUT,\n JSON.stringify({ totalViolations: violations.length, byImpact, violations }, null, 2),\n );\n }\n expect(violations).toEqual([]);\n});\n`;\n}\n\nexport async function a11yAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n // specDir lives INSIDE site.path (not /tmp) so the spec's\n // `import AxeBuilder from \"@axe-core/playwright\"` resolves via Node's\n // walk-up — the site's node_modules is the nearest one. A spec written\n // to /tmp ENOENTs at module resolution before any test runs. Caltex\n // 2026-05-28 (0.10.6 dogfood), third layer of the same class as the\n // webServer.cwd bug.\n const specDir = await mkdtemp(join(site.path, \".reddoor-a11y-spec-\"));\n // Everything past mkdtemp is wrapped so the transient specDir is removed on\n // EVERY catchable exit — success, skip-return, or any throw (a failed\n // writeFile/findFreePort used to orphan it). A timeout-SIGKILL of the parent\n // can't be caught here; `.reddoor-a11y-spec-*/` is fleet-gitignored as the\n // backstop for that. (2026-06-10 MEDIUM-D; recurred from 06-05 M3.)\n try {\n const specPath = join(specDir, \"a11y.spec.ts\");\n await writeFile(specPath, buildSpec(), \"utf-8\");\n\n const port = await findFreePort();\n const configPath = join(specDir, \"playwright.config.ts\");\n await writeFile(configPath, buildPlaywrightConfig(port, site.path), \"utf-8\");\n\n const resultsPath = join(site.path, RESULTS_REL);\n // Clear stale artifacts so a failed spawn never reports old data.\n await rm(join(site.path, \".reddoor-a11y\"), { recursive: true, force: true });\n\n let raw;\n try {\n raw = await spawn(\n \"npx\",\n [\"--yes\", \"playwright\", \"test\", `--config=${configPath}`, \"--reporter=line\", specPath],\n {\n cwd: site.path,\n env: { ...process.env, REDDOOR_A11Y_OUTPUT: resultsPath },\n // playwright on a cold tree downloads Chrome, boots the site's dev\n // server, and runs axe over every configured route. The shared 30 s\n // default in runAudits is fine for deps/lint/security but starves\n // playwright (mirrors the lighthouse fix shipped earlier).\n timeoutMs: 5 * 60_000,\n },\n );\n } catch (err) {\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"a11y\",\n site: label,\n status: \"skip\",\n summary: \"npx/playwright not available\",\n };\n }\n throw err;\n }\n\n const artifact = await readJsonMaybe<NormalizedA11y>(resultsPath);\n\n if (!artifact) {\n return {\n audit: \"a11y\",\n site: label,\n status: \"fail\",\n summary: `a11y: no results written (exit ${raw.code})${\n raw.stderr ? ` — ${raw.stderr.slice(0, 200)}` : \"\"\n }`,\n };\n }\n\n const hasSerious =\n (artifact.byImpact.serious ?? 0) > 0 || (artifact.byImpact.critical ?? 0) > 0;\n const hasAny = artifact.totalViolations > 0;\n\n const status: AuditResult[\"status\"] = hasSerious ? \"fail\" : hasAny ? \"warn\" : \"pass\";\n const summary =\n status === \"pass\"\n ? `a11y: 0 violations across ${a11yRoutes.length} routes (+${smokeRoutes.length} hydration smoke)`\n : `a11y: ${artifact.totalViolations} violations`;\n\n return {\n audit: \"a11y\",\n site: label,\n status,\n summary,\n details: artifact,\n };\n } finally {\n await rm(specDir, { recursive: true, force: true });\n }\n}\n","import { promises as dnsPromises } from \"node:dns\";\nimport tls from \"node:tls\";\nimport type { AuditResult } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { siteLabel } from \"../util/site.js\";\n\n/** Result of probing a site's domain: did DNS resolve, and how many days until the TLS cert\n * expires (null when unresolved or no usable cert). The auto-tick verdict is recomputed from\n * the persisted fields — this is just the raw signal. */\nexport type DomainCheck = {\n resolved: boolean;\n certDaysRemaining: number | null;\n};\n\n/** Injected IO so the check is unit-testable without real DNS/TLS. `lookup` throws when the host\n * doesn't resolve; `certValidTo` returns the cert's notAfter date, or null when there's none. */\nexport type DomainDeps = {\n lookup: (host: string) => Promise<void>;\n certValidTo: (host: string) => Promise<Date | null>;\n now: Date;\n};\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n/**\n * Probe one URL's domain: resolve DNS, then read the TLS certificate expiry. PURE given its deps.\n * Any failure degrades safely — an unresolvable host or a missing/unreadable cert yields\n * `certDaysRemaining: null` (which the auto-tick rule treats as \"not provable\", never a pass).\n */\nexport async function checkDomain(url: string, deps: DomainDeps): Promise<DomainCheck> {\n let host: string;\n try {\n host = new URL(url).hostname;\n } catch {\n return { resolved: false, certDaysRemaining: null };\n }\n try {\n await deps.lookup(host);\n } catch {\n return { resolved: false, certDaysRemaining: null };\n }\n let validTo: Date | null;\n try {\n validTo = await deps.certValidTo(host);\n } catch {\n validTo = null;\n }\n if (!validTo || Number.isNaN(validTo.getTime()))\n return { resolved: true, certDaysRemaining: null };\n return {\n resolved: true,\n certDaysRemaining: Math.floor((validTo.getTime() - deps.now.getTime()) / MS_PER_DAY),\n };\n}\n\n/** Real DNS + TLS. `certValidTo` opens a TLS socket (SNI = host), reads the peer cert's\n * `valid_to`, and closes immediately — no HTTP request. */\nexport function defaultDomainDeps(now: Date): DomainDeps {\n return {\n lookup: async (host) => {\n await dnsPromises.lookup(host);\n },\n certValidTo: (host) =>\n new Promise<Date | null>((resolvePromise) => {\n // `rejectUnauthorized: true` is what makes this check HONEST and the green box truthful:\n // the secureConnect callback only fires on a trusted, host-matching (SNI), unexpired\n // chain — an expired / self-signed / untrusted-CA / SAN-mismatched cert fires \"error\"\n // → null → fail. The whole \"valid cert\" claim depends on this. NEVER set it false.\n // `socket.authorized` is asserted belt-and-suspenders before trusting `valid_to`.\n const socket = tls.connect(\n { host, port: 443, servername: host, timeout: 10_000, rejectUnauthorized: true },\n () => {\n const cert = socket.authorized ? socket.getPeerCertificate() : null;\n socket.end();\n const validTo = cert && cert.valid_to ? new Date(cert.valid_to) : null;\n resolvePromise(validTo);\n },\n );\n socket.on(\"error\", () => resolvePromise(null));\n socket.on(\"timeout\", () => {\n socket.destroy();\n resolvePromise(null);\n });\n }),\n now,\n };\n}\n\n/**\n * Audit a site's domain (DNS + TLS) against its deployed URL. Checkout-free — needs only\n * `site.deployedUrl`. Skips a site with no deployed URL. Writes `certDaysRemaining` + a checked-at\n * timestamp via the Airtable layer; the auto-tick rule decides pass/fail from those.\n */\nexport async function domainAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const label = siteLabel(site);\n if (!site.deployedUrl) {\n return { audit: \"domain\", site: label, status: \"skip\", summary: \"no deployed URL\" };\n }\n const now = ctx.now ?? new Date();\n const deps = ctx.domainDeps ?? defaultDomainDeps(now);\n const check = await checkDomain(site.deployedUrl, deps);\n const checkedAt = now.toISOString();\n const status: AuditResult[\"status\"] =\n check.resolved && check.certDaysRemaining !== null && check.certDaysRemaining > 14\n ? \"pass\"\n : \"warn\";\n const summary = !check.resolved\n ? \"did not resolve\"\n : check.certDaysRemaining === null\n ? \"resolved, no usable TLS cert\"\n : `resolved, cert ${check.certDaysRemaining}d remaining`;\n return {\n audit: \"domain\",\n site: label,\n status,\n summary,\n details: { resolved: check.resolved, certDaysRemaining: check.certDaysRemaining, checkedAt },\n };\n}\n","/**\n * Route discovery for the deployed-URL browser audit. Pulls the site's sitemap, then samples a\n * REPRESENTATIVE set of paths — bucketed by path family (first segment) and sampled round-robin —\n * so every page *type* is covered, including the dynamic CMS-generated templates (Prismic\n * `[uid]`/`[slug]` detail pages: blog posts, projects, portfolio items) where broken\n * images / overflowing galleries / dead links hide. Taking the first N sitemap entries would skew\n * to top-level static pages and miss them. Pure functions + one fetch-injected entry point.\n */\n\nexport type DiscoverDeps = {\n /** Fetch a URL, returning its text body, or null on any non-2xx / network error. */\n fetchText: (url: string) => Promise<string | null>;\n};\n\nexport type DiscoveredRoutes = {\n /** Absolute URLs (rebuilt on the deployed origin), always including the homepage. */\n routes: string[];\n source: \"sitemap\" | \"homepage-links\" | \"root-only\";\n /** Path-family → count, for the evidence note (\"/, /work ×4, /blog ×4\"). */\n familyCounts: Record<string, number>;\n};\n\nconst DEFAULT_CAP = 15;\n\n/** Extract `<loc>` URLs from sitemap XML. Tolerant of namespaces/whitespace; ignores junk. */\nexport function parseSitemapUrls(xml: string): string[] {\n const out: string[] = [];\n const re = /<loc>\\s*([^<\\s]+)\\s*<\\/loc>/gi;\n let m: RegExpExecArray | null;\n while ((m = re.exec(xml)) !== null) {\n const url = m[1];\n if (url) out.push(url.trim());\n }\n return out;\n}\n\n/** Same-origin `<a href>` paths from an HTML body (homepage fallback). Returns root-relative\n * pathnames; off-origin / anchor / mailto / tel links are dropped. */\nexport function parseHtmlLinks(html: string, baseUrl: string): string[] {\n const out = new Set<string>();\n const re = /<a\\b[^>]*\\bhref\\s*=\\s*[\"']([^\"']+)[\"']/gi;\n let m: RegExpExecArray | null;\n while ((m = re.exec(html)) !== null) {\n const href = m[1];\n if (!href || href.startsWith(\"#\") || /^(mailto:|tel:|javascript:)/i.test(href)) continue;\n try {\n const u = new URL(href, baseUrl);\n if (u.origin !== new URL(baseUrl).origin) continue;\n out.add(u.pathname);\n } catch {\n /* skip unparseable */\n }\n }\n return [...out];\n}\n\n/** First non-empty path segment (\"/\" → \"\", \"/work/x\" → \"work\"). The \"family\" we bucket by. */\nfunction family(pathname: string): string {\n return pathname.split(\"/\").filter(Boolean)[0] ?? \"\";\n}\n\n/**\n * Sample a representative set of PATHNAMES from a list of URLs/paths. Always includes \"/\". Buckets\n * the rest by path family and takes them round-robin (one per family per pass), so every family\n * is represented before any family gets a second page — guaranteeing CMS templates aren't skipped.\n * Caps the total. PURE.\n */\nexport function sampleRoutePaths(urlsOrPaths: string[], cap: number = DEFAULT_CAP): string[] {\n const seen = new Set<string>([\"/\"]);\n const buckets = new Map<string, string[]>();\n for (const raw of urlsOrPaths) {\n let pathname: string;\n try {\n pathname = raw.startsWith(\"/\")\n ? new URL(raw, \"https://x.invalid\").pathname\n : new URL(raw).pathname;\n } catch {\n continue;\n }\n if (pathname === \"/\") continue; // root already guaranteed\n if (seen.has(pathname)) continue;\n seen.add(pathname);\n const fam = family(pathname);\n const arr = buckets.get(fam) ?? [];\n arr.push(pathname);\n buckets.set(fam, arr);\n }\n const result = [\"/\"];\n const families = [...buckets.values()];\n let guard = 0;\n while (result.length < cap && families.some((f) => f.length > 0) && guard++ < 10_000) {\n for (const fam of families) {\n if (result.length >= cap) break;\n const next = fam.shift();\n if (next) result.push(next);\n }\n }\n return result;\n}\n\n/** Per-family counts of a sampled path list, for the evidence note. */\nexport function familyCountsOf(paths: string[]): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const p of paths) {\n const key = p === \"/\" ? \"/\" : `/${family(p)}`;\n counts[key] = (counts[key] ?? 0) + 1;\n }\n return counts;\n}\n\n/**\n * Discover a representative route set for `deployedUrl`. Tries `<origin>/sitemap.xml` first\n * (family-sampled); falls back to the homepage's same-origin links; worst case the homepage only.\n * Returns absolute URLs rebuilt on the deployed origin.\n */\nexport async function discoverRoutes(\n deployedUrl: string,\n deps: DiscoverDeps,\n cap: number = DEFAULT_CAP,\n): Promise<DiscoveredRoutes> {\n const origin = new URL(deployedUrl).origin;\n const abs = (paths: string[]) => paths.map((p) => new URL(p, origin).href);\n\n const sitemapXml = await deps.fetchText(new URL(\"/sitemap.xml\", origin).href);\n if (sitemapXml) {\n const urls = parseSitemapUrls(sitemapXml);\n if (urls.length > 0) {\n const paths = sampleRoutePaths(urls, cap);\n return { routes: abs(paths), source: \"sitemap\", familyCounts: familyCountsOf(paths) };\n }\n }\n\n const homeHtml = await deps.fetchText(origin);\n if (homeHtml) {\n const links = parseHtmlLinks(homeHtml, origin);\n if (links.length > 0) {\n const paths = sampleRoutePaths(links, cap);\n return { routes: abs(paths), source: \"homepage-links\", familyCounts: familyCountsOf(paths) };\n }\n }\n\n return { routes: [new URL(\"/\", origin).href], source: \"root-only\", familyCounts: { \"/\": 1 } };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport {\n discoverRoutes,\n familyCountsOf,\n type DiscoverDeps,\n type DiscoveredRoutes,\n} from \"./route-discovery.js\";\n\n/** One route probed across desktop engines + mobile devices, plus the internal links found on it. */\nexport type RouteResult = {\n url: string;\n /** Per desktop engine (chromium/firefox/webkit): loaded with no JS error + a visible main landmark. */\n desktop: Array<{ engine: string; ok: boolean }>;\n /** Per mobile device: loaded with no JS error and no horizontal overflow. */\n mobile: Array<{ device: string; ok: boolean }>;\n /** Same-origin links discovered on the page (absolute URLs), for the Links check. */\n links: string[];\n};\n\nexport type LinkResult = { url: string; status: number | null };\n\n/** Injected browser IO. The real impl drives Playwright; tests pass a fake. */\nexport type BrowserRunner = {\n probe: (urls: string[]) => Promise<RouteResult[]>;\n checkLinks: (urls: string[]) => Promise<LinkResult[]>;\n close?: () => Promise<void>;\n};\n\nexport type BrowserSummary = {\n desktopOk: boolean;\n mobileOk: boolean;\n linksOk: boolean;\n brokenLinks: number;\n routesChecked: number;\n note: string;\n};\n\n/** A link status counts as broken when it didn't return a 2xx/3xx (null = unreachable). */\nfunction isBroken(status: number | null): boolean {\n return status === null || status >= 400;\n}\n\n/**\n * Reduce raw per-route observations to the three checklist verdicts. PURE.\n * - desktopOk: EVERY route loaded cleanly in EVERY desktop engine.\n * - mobileOk: EVERY route loaded cleanly (no overflow / error) on EVERY mobile device.\n * - linksOk: NO internal link is broken.\n * Empty observations (a probe that produced nothing) → not ok (fail-safe: prove, don't assume).\n */\nexport function summarizeBrowser(\n routes: RouteResult[],\n links: LinkResult[],\n familyCounts: Record<string, number>,\n): BrowserSummary {\n const desktopChecks = routes.flatMap((r) => r.desktop);\n const mobileChecks = routes.flatMap((r) => r.mobile);\n // Per-route completeness: EVERY route must carry observations AND every observation must be ok.\n // (Guarding only the flattened array would excuse a route that produced no observations at all —\n // a false green.) A route with an empty desktop[]/mobile[] (never rendered) → not ok.\n const desktopOk =\n routes.length > 0 && routes.every((r) => r.desktop.length > 0 && r.desktop.every((d) => d.ok));\n const mobileOk =\n routes.length > 0 && routes.every((r) => r.mobile.length > 0 && r.mobile.every((m) => m.ok));\n const brokenLinks = links.filter((l) => isBroken(l.status)).length;\n // linksOk requires links to have ACTUALLY been checked — zero links checked is \"not proven\",\n // never a pass (e.g. a JS-rendered page whose hrefs weren't in the DOM, or a chromium evaluate\n // that threw → []). Otherwise the box would assert \"all links resolve\" having checked nothing.\n const linksOk = links.length > 0 && brokenLinks === 0;\n\n const engines = [...new Set(desktopChecks.map((d) => d.engine))];\n const devices = [...new Set(mobileChecks.map((m) => m.device))];\n const families = Object.entries(familyCounts)\n .map(([f, n]) => (f === \"/\" ? \"/\" : `${f} ×${n}`))\n .join(\", \");\n const note =\n `${routes.length} routes (${families}); ` +\n `desktop ${engines.join(\"/\") || \"—\"}; mobile ${devices.join(\"/\") || \"—\"}; ` +\n `${links.length} links, ${brokenLinks} broken`;\n\n return { desktopOk, mobileOk, linksOk, brokenLinks, routesChecked: routes.length, note };\n}\n\n/**\n * Deployed-URL browser audit: discovers a representative route set (incl. CMS templates), probes\n * each across desktop engines + mobile devices, and checks internal links — all against the LIVE\n * url (checkout-free). Skips a site with no deployed URL. Produces ONE AuditResult whose details\n * carry the three verdicts (Crossbrowser / Mobile / Links); the Airtable layer fans them out and\n * the auto-tick rule gates each on freshness.\n */\nexport async function browserAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const label = siteLabel(site);\n if (!site.deployedUrl) {\n return { audit: \"browser\", site: label, status: \"skip\", summary: \"no deployed URL\" };\n }\n const now = ctx.now ?? new Date();\n const discoverDeps = ctx.discoverDeps ?? defaultDiscoverDeps();\n const runner = ctx.browserRunner ?? (await defaultBrowserRunner());\n try {\n const discovered: DiscoveredRoutes = await discoverRoutes(site.deployedUrl, discoverDeps);\n const routeResults = await runner.probe(discovered.routes);\n const internalLinks = [...new Set(routeResults.flatMap((r) => r.links))];\n const linkResults = await runner.checkLinks(internalLinks);\n const summary = summarizeBrowser(\n routeResults,\n linkResults,\n discovered.familyCounts ?? familyCountsOf(discovered.routes),\n );\n const status: AuditResult[\"status\"] =\n summary.desktopOk && summary.mobileOk && summary.linksOk ? \"pass\" : \"warn\";\n return {\n audit: \"browser\",\n site: label,\n status,\n summary: summary.note,\n details: { ...summary, checkedAt: now.toISOString() },\n };\n } finally {\n await runner.close?.();\n }\n}\n\n/** Bound the plain fetch()es (route-discovery GET + link HEAD/GET) so a host that hangs\n * WITHOUT erroring can't stall the sequential fleet audit indefinitely. Playwright's\n * page.goto already has PAGE_TIMEOUT_MS; these fetches had no ceiling. On abort the existing\n * catch path treats it exactly like a network error (→ null), so a slow host degrades, never throws. */\nconst FETCH_TIMEOUT_MS = 10_000;\n\n/** Real route-discovery fetch: GET text, null on any non-2xx / error / timeout. */\nexport function defaultDiscoverDeps(): DiscoverDeps {\n return {\n fetchText: async (url) => {\n try {\n const res = await fetch(url, {\n redirect: \"follow\",\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n });\n if (!res.ok) return null;\n return await res.text();\n } catch {\n return null;\n }\n },\n };\n}\n\nconst DESKTOP_VIEWPORT = { width: 1366, height: 900 };\nconst PAGE_TIMEOUT_MS = 30_000;\n\n/**\n * Real Playwright runner. Launches chromium/firefox/webkit (desktop) + mobile-emulated chromium &\n * webkit, probes each route, and HEAD/GET-checks internal links. Every failure degrades to a\n * `false`/`null` (never throws past the audit), so a flaky run yields a non-pass (→ the box stays\n * manual) rather than a false green. Imported lazily so unit tests (which inject a fake runner)\n * never load Playwright.\n */\nexport async function defaultBrowserRunner(): Promise<BrowserRunner> {\n const { chromium, firefox, webkit, devices } = await import(\"@playwright/test\");\n const desktopEngines = [\n { engine: \"chromium\", type: chromium },\n { engine: \"firefox\", type: firefox },\n { engine: \"webkit\", type: webkit },\n ];\n const mobileTargets = [\n { device: \"Pixel 7\", descriptor: devices[\"Pixel 7\"] },\n { device: \"iPhone 14\", descriptor: devices[\"iPhone 14\"] },\n ];\n\n return {\n async probe(urls) {\n const results: RouteResult[] = [];\n const browsers = await Promise.all(desktopEngines.map((e) => e.type.launch()));\n const mobileBrowsers = await Promise.all(mobileTargets.map(() => chromium.launch()));\n try {\n for (const url of urls) {\n const desktop: RouteResult[\"desktop\"] = [];\n const linkSet = new Set<string>();\n for (let i = 0; i < desktopEngines.length; i++) {\n const engine = desktopEngines[i]!.engine;\n const browser = browsers[i]!;\n const ctx = await browser.newContext({ viewport: DESKTOP_VIEWPORT });\n const page = await ctx.newPage();\n const errors: string[] = [];\n page.on(\"pageerror\", (e) => errors.push(String(e)));\n let ok = false;\n try {\n const resp = await page.goto(url, {\n waitUntil: \"domcontentloaded\",\n timeout: PAGE_TIMEOUT_MS,\n });\n const hasMain = await page\n .locator(\"main, [role=main]\")\n .first()\n .isVisible()\n .catch(() => false);\n ok = !!resp && resp.ok() && errors.length === 0 && hasMain;\n if (engine === \"chromium\") {\n // Link discovery runs on chromium only (cost). Links coverage therefore depends on\n // chromium loading this route + the evaluate succeeding; if it yields nothing, the\n // Links verdict is \"not proven\" (links.length===0 ⇒ linksOk false), never a pass.\n // String-form evaluate so the browser-context code isn't type-checked against the\n // Node lib (no DOM globals in this project's tsconfig). Returns absolute hrefs.\n const hrefs = (await page\n .evaluate(\"Array.from(document.querySelectorAll('a[href]')).map((a) => a.href)\")\n .catch(() => [])) as string[];\n const origin = new URL(url).origin;\n for (const h of hrefs) {\n try {\n if (new URL(h).origin === origin) linkSet.add(new URL(h).href);\n } catch {\n /* skip */\n }\n }\n }\n } catch {\n ok = false;\n } finally {\n await ctx.close().catch(() => {});\n }\n desktop.push({ engine, ok });\n }\n\n const mobile: RouteResult[\"mobile\"] = [];\n for (let i = 0; i < mobileTargets.length; i++) {\n const { device, descriptor } = mobileTargets[i]!;\n const browser = mobileBrowsers[i]!;\n const ctx = await browser.newContext({ ...descriptor });\n const page = await ctx.newPage();\n const errors: string[] = [];\n page.on(\"pageerror\", (e) => errors.push(String(e)));\n let ok = false;\n try {\n const resp = await page.goto(url, {\n waitUntil: \"domcontentloaded\",\n timeout: PAGE_TIMEOUT_MS,\n });\n const overflow = (await page\n .evaluate(\"document.documentElement.scrollWidth > window.innerWidth + 2\")\n .catch(() => true)) as boolean;\n ok = !!resp && resp.ok() && errors.length === 0 && !overflow;\n } catch {\n ok = false;\n } finally {\n await ctx.close().catch(() => {});\n }\n mobile.push({ device, ok });\n }\n\n results.push({ url, desktop, mobile, links: [...linkSet] });\n }\n } finally {\n await Promise.all([...browsers, ...mobileBrowsers].map((b) => b.close().catch(() => {})));\n }\n return results;\n },\n\n async checkLinks(urls) {\n const out: LinkResult[] = [];\n for (const url of urls) {\n let status: number | null;\n try {\n let res = await fetch(url, {\n method: \"HEAD\",\n redirect: \"follow\",\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n });\n // Some servers reject HEAD (405/501) — retry GET before declaring it broken.\n if (res.status === 405 || res.status === 501) {\n res = await fetch(url, {\n method: \"GET\",\n redirect: \"follow\",\n signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),\n });\n }\n status = res.status;\n } catch {\n status = null;\n }\n out.push({ url, status });\n }\n return out;\n },\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { siteLabel } from \"../util/site.js\";\n\n/** Result of probing a site's latest PRODUCTION Netlify deploy. Every field is\n * nullable: a site with no production deploy yet degrades to all-nulls (a real\n * \"none\" verdict). A read FAILURE is NOT represented here — it surfaces as\n * `{ ok: false }` on the enclosing `NetlifyDeployFetch` so it can't be mistaken\n * for a verdict. See `NetlifyDeployFetch`.\n * - `state` is Netlify's raw deploy state (`ready`/`error`/`building`/…), lower-cased,\n * or null when the site has no production deploy.\n * - `deployedAt` is the deploy's publish/create time (ISO), or null.\n * - `logUrl` links to the deploy (its live URL or the Netlify admin/build-log page).\n * - `errorMessage` is Netlify's `error_message` for a failed build, or null. */\nexport type NetlifyDeployCheck = {\n state: string | null;\n deployedAt: string | null;\n logUrl: string | null;\n errorMessage: string | null;\n};\n\n/** Outcome of probing Netlify, distinguishing \"couldn't read\" from \"read OK\".\n * This distinction is load-bearing: `Deploy status` drives the cockpit's Broken\n * band (an ALARM), so a transient read failure must NOT be allowed to clear a\n * real `error` — unlike a degraded \"no deploy yet\", which is a real verdict.\n * - `{ ok: false }` — the API was unreachable/unreadable (network error, non-2xx,\n * malformed body). The caller persists nothing, leaving the prior status intact.\n * - `{ ok: true, deploy }` — the API answered. `deploy` is the latest production\n * deploy, or all-nulls when the site genuinely has no production deploy yet\n * (a real verdict, safe to persist). */\nexport type NetlifyDeployFetch = { ok: false } | { ok: true; deploy: NetlifyDeployCheck };\n\n/** Injected IO so the check is unit-testable without hitting the Netlify API.\n * `fetchLatestProductionDeploy` returns a `NetlifyDeployFetch` — `{ ok: false }`\n * when the call couldn't be read, else `{ ok: true, deploy }` with the latest\n * production deploy (or all-nulls for a site with none). `now` is the clock for\n * the checked-at stamp. */\nexport type NetlifyDeployDeps = {\n fetchLatestProductionDeploy: (siteId: string) => Promise<NetlifyDeployFetch>;\n now: Date;\n};\n\n/** The Netlify deploy states we treat as a successful production build. */\nconst READY_STATES: ReadonlySet<string> = new Set([\"ready\"]);\n/** States that mean the build actually failed — the \"needs attention\" case. */\nconst FAILED_STATES: ReadonlySet<string> = new Set([\"error\", \"failed\", \"rejected\"]);\n/** States that mean a build is still in flight — neutral, not yet a verdict. */\nconst IN_PROGRESS_STATES: ReadonlySet<string> = new Set([\n \"building\",\n \"enqueued\",\n \"new\",\n \"processing\",\n \"uploading\",\n \"preparing\",\n]);\n\nexport function isReadyState(state: string | null): boolean {\n return state !== null && READY_STATES.has(state);\n}\nexport function isFailedState(state: string | null): boolean {\n return state !== null && FAILED_STATES.has(state);\n}\nexport function isInProgressState(state: string | null): boolean {\n return state !== null && IN_PROGRESS_STATES.has(state);\n}\n\n/**\n * Probe one Netlify site's latest production deploy. PURE given its deps — the\n * real network/parse work lives in `defaultNetlifyDeployDeps`. Propagates the\n * read/no-read distinction: a deps throw is treated as `{ ok: false }` (couldn't\n * read), so the caller preserves the prior status rather than clearing it.\n */\nexport async function checkNetlifyDeploy(\n siteId: string,\n deps: NetlifyDeployDeps,\n): Promise<NetlifyDeployFetch> {\n return deps\n .fetchLatestProductionDeploy(siteId)\n .catch(() => ({ ok: false }) as NetlifyDeployFetch);\n}\n\n/** Coerce an untrusted value to a trimmed non-empty string, or null. */\nfunction str(raw: unknown): string | null {\n if (typeof raw !== \"string\") return null;\n const trimmed = raw.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n/**\n * Real Netlify API deps. Fetches the latest production deploy for `siteId` and\n * maps the raw JSON into a `NetlifyDeployFetch`. A read failure — network error,\n * non-2xx, malformed/unexpected body — resolves to `{ ok: false }` (the caller\n * preserves the prior status); an empty deploy list resolves to a real all-null\n * `{ ok: true }` verdict. NEVER throws: a fleet sweep must not red an unrelated\n * site because one site's Netlify call hiccuped.\n *\n * `fetchImpl` is injected (defaults to the global `fetch`) only so the default\n * deps themselves stay testable; production callers pass nothing.\n */\nexport function defaultNetlifyDeployDeps(\n token: string,\n now: Date,\n fetchImpl: typeof fetch = fetch,\n): NetlifyDeployDeps {\n return {\n now,\n fetchLatestProductionDeploy: async (siteId): Promise<NetlifyDeployFetch> => {\n // `production=true` + `per_page=1` asks Netlify for ONLY the newest\n // production deploy, so we read `[0]` without paging. siteId is the\n // Airtable-supplied site identity; encode it so it can't break the path.\n const url =\n `https://api.netlify.com/api/v1/sites/${encodeURIComponent(siteId)}/deploys` +\n `?per_page=1&production=true`;\n let res: Response;\n try {\n res = await fetchImpl(url, {\n headers: { Authorization: `Bearer ${token}` },\n });\n } catch {\n return { ok: false }; // network error — couldn't read; preserve prior status\n }\n if (!res.ok) return { ok: false }; // non-2xx — couldn't read\n let body: unknown;\n try {\n body = await res.json();\n } catch {\n return { ok: false }; // malformed JSON — couldn't read\n }\n if (!Array.isArray(body)) return { ok: false }; // unexpected shape — couldn't read\n if (body.length === 0) {\n // Read OK; the site genuinely has no production deploy yet. An all-null\n // verdict is real (not a read failure), so it's safe to persist.\n return {\n ok: true,\n deploy: { state: null, deployedAt: null, logUrl: null, errorMessage: null },\n };\n }\n const d = body[0] as Record<string, unknown>;\n const state = str(d[\"state\"]);\n // Prefer the publish time; fall back to create time for an in-progress deploy.\n const deployedAt = str(d[\"published_at\"]) ?? str(d[\"created_at\"]);\n // Prefer the live deploy URL, then the Netlify admin page, then a raw log link.\n const logUrl = str(d[\"deploy_ssl_url\"]) ?? str(d[\"admin_url\"]) ?? str(d[\"deploy_url\"]);\n const errorMessage = str(d[\"error_message\"]);\n return {\n ok: true,\n deploy: {\n state: state ? state.toLowerCase() : null,\n deployedAt,\n logUrl,\n errorMessage,\n },\n };\n },\n };\n}\n\n/**\n * Audit a site's latest PRODUCTION Netlify deploy health. Checkout-free — needs\n * only the site's Netlify id. Degrades gracefully fleet-wide:\n * - no `site.netlifyId` → skip (\"no netlify id\"); the site simply isn't on Netlify\n * (or hasn't been wired up) so there's nothing to check.\n * - no `NETLIFY_PAT` in the env → skip (\"no NETLIFY_PAT\"); the whole audit is\n * unconfigured, which must not red every site.\n *\n * Status mapping: `ready` → pass; a failed build (`error`/`failed`/`rejected`) →\n * fail (the \"needs attention\" status — it lands the site in the cockpit's attention\n * tier, mirroring a sub-floor Lighthouse score); an in-flight build or any\n * unknown/unreadable state → warn (neutral, no verdict yet). `details` carries\n * `{ state, deployedAt, logUrl, checkedAt }` for the Airtable writer + dashboard.\n */\nexport async function netlifyDeployAudit(ctx: AuditContext): Promise<AuditResult> {\n const { site } = ctx;\n const label = siteLabel(site);\n if (!site.netlifyId) {\n return { audit: \"netlify-deploy\", site: label, status: \"skip\", summary: \"no netlify id\" };\n }\n const token = process.env.NETLIFY_PAT;\n if (!token) {\n return { audit: \"netlify-deploy\", site: label, status: \"skip\", summary: \"no NETLIFY_PAT\" };\n }\n\n const now = ctx.now ?? new Date();\n const deps = ctx.netlifyDeployDeps ?? defaultNetlifyDeployDeps(token, now);\n const fetched = await checkNetlifyDeploy(site.netlifyId, deps);\n\n // Couldn't read Netlify (network error / non-2xx / malformed). Return WITHOUT\n // details so `hasNetlifyDeployResult` is false and the Airtable writer skips the\n // write — leaving the prior `Deploy status` intact. A `Deploy status` of `error`\n // drives the cockpit's Broken band, and a transient API hiccup during the nightly\n // sweep must NEVER silently clear that alarm to \"all clear\" while prod is down.\n if (!fetched.ok) {\n return {\n audit: \"netlify-deploy\",\n site: label,\n status: \"warn\",\n summary: \"deploy status unavailable (Netlify API unreachable)\",\n };\n }\n const check = fetched.deploy;\n const checkedAt = now.toISOString();\n\n const status: AuditResult[\"status\"] = isReadyState(check.state)\n ? \"pass\"\n : isFailedState(check.state)\n ? \"fail\"\n : \"warn\";\n\n const summary =\n check.state === null\n ? \"no deploy found\"\n : isFailedState(check.state)\n ? `deploy ${check.state}${check.errorMessage ? ` — ${check.errorMessage}` : \"\"}`\n : `deploy ${check.state}`;\n\n return {\n audit: \"netlify-deploy\",\n site: label,\n status,\n summary,\n details: {\n state: check.state,\n deployedAt: check.deployedAt,\n logUrl: check.logUrl,\n checkedAt,\n },\n };\n}\n","import type { AuditName, AuditResult, Site } from \"../types.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { SpawnFn } from \"./util/spawn.js\";\nimport { depsAudit } from \"./deps.js\";\nimport { lintAudit } from \"./lint.js\";\nimport { securityAudit } from \"./security.js\";\nimport { lighthouseAudit } from \"./lighthouse.js\";\nimport { a11yAudit } from \"./a11y.js\";\nimport { domainAudit } from \"./domain.js\";\nimport { browserAudit } from \"./browser.js\";\nimport { netlifyDeployAudit } from \"./netlify-deploy.js\";\n\nconst REGISTRY: Record<AuditName, (ctx: AuditContext) => Promise<AuditResult>> = {\n deps: depsAudit,\n lint: lintAudit,\n security: securityAudit,\n lighthouse: lighthouseAudit,\n a11y: a11yAudit,\n domain: domainAudit,\n browser: browserAudit,\n \"netlify-deploy\": netlifyDeployAudit,\n};\n\nexport const ALL_AUDIT_NAMES = Object.keys(REGISTRY) as AuditName[];\n\n/** Default per-audit spawn timeout when running via runAudits (30 s). */\nconst DEFAULT_AUDIT_TIMEOUT_MS = 30_000;\n\nfunction timedSpawn(timeoutMs: number): SpawnFn {\n return (cmd, args, opts = {}) =>\n defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });\n}\n\n/** Single-audit runner with the same error-to-result conversion that\n * `runAudits` applies. Exposed so the CLI can wrap each audit in its\n * own progress task (listr2) and surface per-audit completion timing,\n * while keeping audit implementations UI-free. */\nexport async function runOneAudit(site: Site, name: AuditName): Promise<AuditResult> {\n if (!(name in REGISTRY)) throw new Error(`unknown audit: ${name}`);\n const spawn = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);\n // `||` not `??`: an empty-string slug (Airtable Name with no slug-able chars)\n // must fall back to the path, not render a blank `AuditResult.site` that would\n // then collapse fleet write-back grouping under the \"\" key.\n const label = site.name || site.path;\n try {\n return await REGISTRY[name]({ site, spawn });\n } catch (err) {\n return {\n audit: name,\n site: label,\n status: \"fail\",\n summary: `${name}: unexpected error — ${String(err)}`,\n };\n }\n}\n\nexport async function runAudits(site: Site, which?: AuditName[]): Promise<AuditResult[]> {\n const names = which ?? ALL_AUDIT_NAMES;\n for (const n of names) {\n if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);\n }\n return Promise.all(names.map((n) => runOneAudit(site, n)));\n}\n\nexport async function runAuditsAcross(sites: Site[], which?: AuditName[]): Promise<AuditResult[]> {\n const all = await Promise.all(sites.map((s) => runAudits(s, which)));\n return all.flat();\n}\n\nexport { depsAudit, lintAudit, securityAudit, lighthouseAudit, a11yAudit };\n"],"mappings":"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","names":["join","join","readFile","join","join","readFile","readFile","join","readFile","join","readFile","join","readFile","writeFile","mkdtemp","rm","join","readJsonMaybe","readFile","mkdtemp","join","writeFile","rm"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli/commands/convert-to-pnpm.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { convertToPnpm } from \"../../recipes/convert-to-pnpm.js\";\nimport type { RecipeResult } from \"../../types.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport { prepareFleetSites, appendSkipNotice, type SkippedSite } from \"../fleet/prepare-sites.js\";\nimport { runRecipeOverSites } from \"../fleet/run-recipe-over-sites.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\n\nexport type ConvertToPnpmCommandOptions = {\n fleet?: string;\n workdir?: string;\n cwd?: string;\n};\n\nfunction formatResult(r: RecipeResult): string {\n if (r.status === \"noop\") return `[${r.site}] noop: ${r.notes ?? \"\"}`;\n if (r.status === \"failed\") return `[${r.site}] failed: ${r.notes ?? \"\"}`;\n return `[${r.site}] applied: ${r.commits.length} commit(s)\\n${r.notes ?? \"\"}`;\n}\n\nexport async function runConvertToPnpmCommand(\n site: string | undefined,\n opts: ConvertToPnpmCommandOptions,\n): Promise<{ output: string; code: number }> {\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n cwd,\n });\n\n let skipped: SkippedSite[] = [];\n if (opts.fleet) {\n const workdir = opts.workdir ?? fleetWorkdir();\n const prep = await prepareFleetSites(sites, { workdir });\n sites = prep.prepared;\n skipped = prep.skipped;\n }\n\n const results = await runRecipeOverSites(\"convert-to-pnpm\", sites, (s) => convertToPnpm(s));\n\n const output = results.map(formatResult).join(\"\\n\");\n const code = results.some((r) => r.status === \"failed\") ? 1 : 0;\n return { output: appendSkipNotice(output, skipped), code };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,eAAe;AAcxB,SAAS,aAAa,GAAyB;AAC7C,MAAI,EAAE,WAAW,OAAQ,QAAO,IAAI,EAAE,IAAI,WAAW,EAAE,SAAS,EAAE;AAClE,MAAI,EAAE,WAAW,SAAU,QAAO,IAAI,EAAE,IAAI,aAAa,EAAE,SAAS,EAAE;AACtE,SAAO,IAAI,EAAE,IAAI,cAAc,EAAE,QAAQ,MAAM;AAAA,EAAe,EAAE,SAAS,EAAE;AAC7E;AAEA,eAAsB,wBACpB,MACA,MAC2C;AAC3C,QAAM,MAAM,KAAK,MAAM,QAAQ,KAAK,GAAG,IAAI,QAAQ,IAAI;AAEvD,MAAI,QAAQ,MAAM,aAAa;AAAA,IAC7B,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IACrC,GAAI,KAAK,UAAU,SAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAAA,IACxD;AAAA,EACF,CAAC;AAED,MAAI,UAAyB,CAAC;AAC9B,MAAI,KAAK,OAAO;AACd,UAAM,UAAU,KAAK,WAAW,aAAa;AAC7C,UAAM,OAAO,MAAM,kBAAkB,OAAO,EAAE,QAAQ,CAAC;AACvD,YAAQ,KAAK;AACb,cAAU,KAAK;AAAA,EACjB;AAEA,QAAM,UAAU,MAAM,mBAAmB,mBAAmB,OAAO,CAAC,MAAM,cAAc,CAAC,CAAC;AAE1F,QAAM,SAAS,QAAQ,IAAI,YAAY,EAAE,KAAK,IAAI;AAClD,QAAM,OAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,QAAQ,IAAI,IAAI;AAC9D,SAAO,EAAE,QAAQ,iBAAiB,QAAQ,OAAO,GAAG,KAAK;AAC3D;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli/commands/init.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { init, type InitResult, type InitStepResult } from \"../../recipes/init.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport { prepareFleetSites, appendSkipNotice, type SkippedSite } from \"../fleet/prepare-sites.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\nimport { siteLabel } from \"../../util/site.js\";\n\nexport type InitCommandOptions = {\n fleet?: string;\n workdir?: string;\n cwd?: string;\n};\n\nfunction formatStep(name: string, r: InitStepResult): string {\n if (r.kind === \"error\") return `${name.padEnd(20)} error: ${r.message}`;\n if (r.kind === \"audit\") {\n const lines = r.results.map(\n (a) => ` ${a.audit.padEnd(12)} ${a.status.padEnd(5)} ${a.summary}`,\n );\n return `${name.padEnd(20)} ${r.results.length} audit(s):\\n${lines.join(\"\\n\")}`;\n }\n const rec = r.result;\n if (rec.status === \"noop\") return `${name.padEnd(20)} noop${rec.notes ? ` — ${rec.notes}` : \"\"}`;\n if (rec.status === \"failed\")\n return `${name.padEnd(20)} failed${rec.notes ? ` — ${rec.notes}` : \"\"}`;\n return `${name.padEnd(20)} applied (${rec.commits.length} commit${rec.commits.length === 1 ? \"\" : \"s\"})${rec.notes ? ` — ${rec.notes}` : \"\"}`;\n}\n\nfunction formatResult(r: InitResult): string {\n const header = `[${r.site}] init — ${r.complete ? \"complete\" : \"STOPPED\"}`;\n const body = r.steps.map((s) => formatStep(s.name, s.result)).join(\"\\n\");\n return `${header}\\n${body}`;\n}\n\nfunction exitCodeFor(r: InitResult): number {\n if (!r.complete) return 1;\n // A complete chain can still surface a failing audit at the end. Treat\n // any `fail`-status audit as exit 1 so CI signals a regression even when\n // every recipe applied cleanly.\n for (const step of r.steps) {\n if (step.result.kind === \"audit\" && step.result.results.some((a) => a.status === \"fail\")) {\n return 1;\n }\n }\n return 0;\n}\n\nexport async function runInitCommand(\n site: string | undefined,\n opts: InitCommandOptions,\n): Promise<{ output: string; code: number }> {\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n cwd,\n });\n\n let skipped: SkippedSite[] = [];\n if (opts.fleet) {\n const workdir = opts.workdir ?? fleetWorkdir();\n const prep = await prepareFleetSites(sites, { workdir });\n sites = prep.prepared;\n skipped = prep.skipped;\n }\n\n const results: InitResult[] = [];\n for (const s of sites) {\n try {\n results.push(await init(s));\n } catch (err) {\n // Isolate a per-site throw (e.g. a transient git error) so the rest of the\n // fleet still runs — the same guarantee `runRecipeOverSites` gives the other\n // fleet commands, but `init` returns an `InitResult`, so synthesize a stopped\n // one here rather than reuse the shared helper.\n results.push({\n site: siteLabel(s),\n steps: [\n {\n name: \"init\",\n result: { kind: \"error\", message: err instanceof Error ? err.message : String(err) },\n },\n ],\n complete: false,\n });\n }\n }\n\n const output = results.map(formatResult).join(\"\\n\\n\");\n const code = results.some((r) => exitCodeFor(r) !== 0) ? 1 : 0;\n return { output: appendSkipNotice(output, skipped), code };\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli/commands/svelte-codemods.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { svelteCodemods } from \"../../recipes/svelte-codemods.js\";\nimport type { RecipeResult } from \"../../types.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport { prepareFleetSites, appendSkipNotice, type SkippedSite } from \"../fleet/prepare-sites.js\";\nimport { runRecipeOverSites } from \"../fleet/run-recipe-over-sites.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\n\nexport type SvelteCodemodsCommandOptions = {\n fleet?: string;\n workdir?: string;\n cwd?: string;\n};\n\nfunction formatResult(r: RecipeResult): string {\n if (r.status === \"noop\") return `[${r.site}] noop: ${r.notes ?? \"\"}`;\n if (r.status === \"failed\") return `[${r.site}] failed: ${r.notes ?? \"\"}`;\n return `[${r.site}] applied: ${r.commits.length} commit(s)\\n${r.notes ?? \"\"}`;\n}\n\nexport async function runSvelteCodemodsCommand(\n site: string | undefined,\n opts: SvelteCodemodsCommandOptions,\n): Promise<{ output: string; code: number }> {\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n cwd,\n });\n\n let skipped: SkippedSite[] = [];\n if (opts.fleet) {\n const workdir = opts.workdir ?? fleetWorkdir();\n const prep = await prepareFleetSites(sites, { workdir });\n sites = prep.prepared;\n skipped = prep.skipped;\n }\n\n const results = await runRecipeOverSites(\"svelte-codemods\", sites, (s) => svelteCodemods(s));\n\n const output = results.map(formatResult).join(\"\\n\");\n const code = results.some((r) => r.status === \"failed\") ? 1 : 0;\n return { output: appendSkipNotice(output, skipped), code };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,eAAe;AAcxB,SAAS,aAAa,GAAyB;AAC7C,MAAI,EAAE,WAAW,OAAQ,QAAO,IAAI,EAAE,IAAI,WAAW,EAAE,SAAS,EAAE;AAClE,MAAI,EAAE,WAAW,SAAU,QAAO,IAAI,EAAE,IAAI,aAAa,EAAE,SAAS,EAAE;AACtE,SAAO,IAAI,EAAE,IAAI,cAAc,EAAE,QAAQ,MAAM;AAAA,EAAe,EAAE,SAAS,EAAE;AAC7E;AAEA,eAAsB,yBACpB,MACA,MAC2C;AAC3C,QAAM,MAAM,KAAK,MAAM,QAAQ,KAAK,GAAG,IAAI,QAAQ,IAAI;AAEvD,MAAI,QAAQ,MAAM,aAAa;AAAA,IAC7B,GAAI,SAAS,SAAY,EAAE,KAAK,IAAI,CAAC;AAAA,IACrC,GAAI,KAAK,UAAU,SAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAAA,IACxD;AAAA,EACF,CAAC;AAED,MAAI,UAAyB,CAAC;AAC9B,MAAI,KAAK,OAAO;AACd,UAAM,UAAU,KAAK,WAAW,aAAa;AAC7C,UAAM,OAAO,MAAM,kBAAkB,OAAO,EAAE,QAAQ,CAAC;AACvD,YAAQ,KAAK;AACb,cAAU,KAAK;AAAA,EACjB;AAEA,QAAM,UAAU,MAAM,mBAAmB,mBAAmB,OAAO,CAAC,MAAM,eAAe,CAAC,CAAC;AAE3F,QAAM,SAAS,QAAQ,IAAI,YAAY,EAAE,KAAK,IAAI;AAClD,QAAM,OAAO,QAAQ,KAAK,CAAC,MAAM,EAAE,WAAW,QAAQ,IAAI,IAAI;AAC9D,SAAO,EAAE,QAAQ,iBAAiB,QAAQ,OAAO,GAAG,KAAK;AAC3D;","names":[]}