@reddoorla/maintenance 0.57.0 → 0.59.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 (70) hide show
  1. package/dist/airtable-YZ6VMSQZ.js +9 -0
  2. package/dist/{announce-JEVCDDKL.js → announce-Q2DXP5OO.js} +3 -3
  3. package/dist/{bump-deps-O3SOLQYY.js → bump-deps-RYHQIKW4.js} +2 -2
  4. package/dist/{chunk-4PY4I5OR.js → chunk-5PHCZ6L5.js} +11 -2
  5. package/dist/chunk-5PHCZ6L5.js.map +1 -0
  6. package/dist/{chunk-J3OPWFZE.js → chunk-6CSCLBAA.js} +2 -2
  7. package/dist/{chunk-XO2RUYVM.js → chunk-HB7QPECR.js} +2 -2
  8. package/dist/{chunk-IMIR7SC2.js → chunk-HPSGCDWY.js} +3 -3
  9. package/dist/chunk-HPSGCDWY.js.map +1 -0
  10. package/dist/{chunk-5L2QXUTU.js → chunk-KC4HU7BC.js} +2 -2
  11. package/dist/{chunk-CLVTVDOU.js → chunk-L4ULXHIN.js} +14 -2
  12. package/dist/chunk-L4ULXHIN.js.map +1 -0
  13. package/dist/{chunk-5WL3ET24.js → chunk-LMXIZITU.js} +3 -3
  14. package/dist/chunk-LMXIZITU.js.map +1 -0
  15. package/dist/{chunk-JQG222CI.js → chunk-TLZ4V2BE.js} +2 -2
  16. package/dist/{chunk-W3G7XFBZ.js → chunk-TZN2G6FD.js} +2 -2
  17. package/dist/cli/bin.js +13 -13
  18. package/dist/cli/commands/audit.js +6 -6
  19. package/dist/{convert-to-pnpm-UR64X3OX.js → convert-to-pnpm-KNCXU6FM.js} +2 -2
  20. package/dist/{digest-YTO73BEO.js → digest-RVIOHUXR.js} +10 -5
  21. package/dist/digest-RVIOHUXR.js.map +1 -0
  22. package/dist/{github-signals-XLQXXH6Q.js → github-signals-NCDBYSVV.js} +4 -4
  23. package/dist/index.d.ts +12 -0
  24. package/dist/index.js +114 -17
  25. package/dist/index.js.map +1 -1
  26. package/dist/{init-JITQXFVL.js → init-KD56UENX.js} +4 -4
  27. package/dist/{launch-XW6PEKUL.js → launch-35KSDHYO.js} +13 -7
  28. package/dist/launch-35KSDHYO.js.map +1 -0
  29. package/dist/{lighthouse-airtable-4FKYKFH6.js → lighthouse-airtable-N3KCBPVA.js} +3 -3
  30. package/dist/{onboard-TCQOP2W4.js → onboard-SCZ3UKZ2.js} +2 -2
  31. package/dist/{orchestrate-LHH3LQNP.js → orchestrate-D72PB6NG.js} +3 -3
  32. package/dist/{renovate-dispatch-YVBTBJJY.js → renovate-dispatch-P2FAJTQK.js} +42 -3
  33. package/dist/renovate-dispatch-P2FAJTQK.js.map +1 -0
  34. package/dist/{report-WK3SKWJI.js → report-CN56TGMK.js} +5 -5
  35. package/dist/{self-updating-BYUBUNO4.js → self-updating-HLMF7KEG.js} +2 -2
  36. package/dist/{svelte-codemods-BYRJAU7I.js → svelte-codemods-HNITLWQM.js} +2 -2
  37. package/dist/{sync-configs-JPRLAW5J.js → sync-configs-BZXBYKTI.js} +2 -2
  38. package/dist/{upgrade-ERA45EVR.js → upgrade-6YDUELFA.js} +2 -2
  39. package/dist/{websites-X5DLC2PC.js → websites-IYP7IGLU.js} +8 -4
  40. package/dist/{write-audits-to-airtable-CE4POBAF.js → write-audits-to-airtable-VDGHCZGQ.js} +4 -4
  41. package/package.json +1 -1
  42. package/dist/airtable-2BJX4ATJ.js +0 -9
  43. package/dist/chunk-4PY4I5OR.js.map +0 -1
  44. package/dist/chunk-5WL3ET24.js.map +0 -1
  45. package/dist/chunk-CLVTVDOU.js.map +0 -1
  46. package/dist/chunk-IMIR7SC2.js.map +0 -1
  47. package/dist/digest-YTO73BEO.js.map +0 -1
  48. package/dist/launch-XW6PEKUL.js.map +0 -1
  49. package/dist/renovate-dispatch-YVBTBJJY.js.map +0 -1
  50. /package/dist/{airtable-2BJX4ATJ.js.map → airtable-YZ6VMSQZ.js.map} +0 -0
  51. /package/dist/{announce-JEVCDDKL.js.map → announce-Q2DXP5OO.js.map} +0 -0
  52. /package/dist/{bump-deps-O3SOLQYY.js.map → bump-deps-RYHQIKW4.js.map} +0 -0
  53. /package/dist/{chunk-J3OPWFZE.js.map → chunk-6CSCLBAA.js.map} +0 -0
  54. /package/dist/{chunk-XO2RUYVM.js.map → chunk-HB7QPECR.js.map} +0 -0
  55. /package/dist/{chunk-5L2QXUTU.js.map → chunk-KC4HU7BC.js.map} +0 -0
  56. /package/dist/{chunk-JQG222CI.js.map → chunk-TLZ4V2BE.js.map} +0 -0
  57. /package/dist/{chunk-W3G7XFBZ.js.map → chunk-TZN2G6FD.js.map} +0 -0
  58. /package/dist/{convert-to-pnpm-UR64X3OX.js.map → convert-to-pnpm-KNCXU6FM.js.map} +0 -0
  59. /package/dist/{github-signals-XLQXXH6Q.js.map → github-signals-NCDBYSVV.js.map} +0 -0
  60. /package/dist/{init-JITQXFVL.js.map → init-KD56UENX.js.map} +0 -0
  61. /package/dist/{lighthouse-airtable-4FKYKFH6.js.map → lighthouse-airtable-N3KCBPVA.js.map} +0 -0
  62. /package/dist/{onboard-TCQOP2W4.js.map → onboard-SCZ3UKZ2.js.map} +0 -0
  63. /package/dist/{orchestrate-LHH3LQNP.js.map → orchestrate-D72PB6NG.js.map} +0 -0
  64. /package/dist/{report-WK3SKWJI.js.map → report-CN56TGMK.js.map} +0 -0
  65. /package/dist/{self-updating-BYUBUNO4.js.map → self-updating-HLMF7KEG.js.map} +0 -0
  66. /package/dist/{svelte-codemods-BYRJAU7I.js.map → svelte-codemods-HNITLWQM.js.map} +0 -0
  67. /package/dist/{sync-configs-JPRLAW5J.js.map → sync-configs-BZXBYKTI.js.map} +0 -0
  68. /package/dist/{upgrade-ERA45EVR.js.map → upgrade-6YDUELFA.js.map} +0 -0
  69. /package/dist/{websites-X5DLC2PC.js.map → websites-IYP7IGLU.js.map} +0 -0
  70. /package/dist/{write-audits-to-airtable-CE4POBAF.js.map → write-audits-to-airtable-VDGHCZGQ.js.map} +0 -0
@@ -7,8 +7,9 @@ import {
7
7
  import "./chunk-HVOOCK6L.js";
8
8
  import {
9
9
  isDashboardVisible,
10
- listWebsites
11
- } from "./chunk-CLVTVDOU.js";
10
+ listWebsites,
11
+ updateAutoFixAttempts
12
+ } from "./chunk-L4ULXHIN.js";
12
13
  import {
13
14
  openBase,
14
15
  readAirtableConfig
@@ -55,6 +56,38 @@ async function dispatchRenovateAcross(targets, deps) {
55
56
  function formatRenovateDispatchSummary(result) {
56
57
  return `RENOVATE_DISPATCH_SUMMARY dispatched=${result.dispatched.length} skipped=${result.skipped.length} failed=${result.failed.length}`;
57
58
  }
59
+ function computeAutoFixAttemptUpdates(sites, result) {
60
+ const dispatched = new Set(result.dispatched);
61
+ const updates = [];
62
+ for (const s of sites) {
63
+ if (!isDashboardVisible(s)) continue;
64
+ const repo = s.gitRepo?.trim();
65
+ if (!repo) continue;
66
+ const current = s.securityAutoFixAttempts ?? 0;
67
+ const vulns = (s.securityVulnsCritical ?? 0) + (s.securityVulnsHigh ?? 0);
68
+ let next = current;
69
+ if (vulns === 0) next = 0;
70
+ else if (dispatched.has(repo)) next = current + 1;
71
+ if (next !== current) updates.push({ id: s.id, attempts: next });
72
+ }
73
+ return updates;
74
+ }
75
+ async function applyAutoFixAttemptUpdates(updates, write) {
76
+ let written = 0;
77
+ let failed = 0;
78
+ for (const u of updates) {
79
+ try {
80
+ await write(u.id, u.attempts);
81
+ written++;
82
+ } catch {
83
+ failed++;
84
+ }
85
+ }
86
+ return { written, failed };
87
+ }
88
+ function formatAutoFixAttemptsSummary(tally) {
89
+ return `AUTO_FIX_ATTEMPTS_SUMMARY written=${tally.written} failed=${tally.failed}`;
90
+ }
58
91
 
59
92
  // src/cli/commands/renovate-dispatch.ts
60
93
  async function runRenovateDispatchCommand(opts) {
@@ -91,10 +124,16 @@ No active repo-backed sites with critical/high vulnerabilities \u2014 nothing to
91
124
  return `${status} ${t.repo} (critical=${t.critical} high=${t.high}) \u2014 ${t.siteName}`;
92
125
  });
93
126
  for (const f of result.failed) lines.push(` \u21B3 ${f.repo}: ${f.error}`);
127
+ const attemptUpdates = computeAutoFixAttemptUpdates(websites, result);
128
+ const attemptTally = await applyAutoFixAttemptUpdates(
129
+ attemptUpdates,
130
+ (id, attempts) => updateAutoFixAttempts(base, id, attempts)
131
+ );
94
132
  lines.push(formatRenovateDispatchSummary(result));
133
+ lines.push(formatAutoFixAttemptsSummary(attemptTally));
95
134
  return { output: lines.join("\n"), code: 0 };
96
135
  }
97
136
  export {
98
137
  runRenovateDispatchCommand
99
138
  };
100
- //# sourceMappingURL=renovate-dispatch-YVBTBJJY.js.map
139
+ //# sourceMappingURL=renovate-dispatch-P2FAJTQK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/github/renovate-dispatch.ts","../src/cli/commands/renovate-dispatch.ts"],"sourcesContent":["import { isDashboardVisible, type WebsiteRow } from \"../reports/airtable/websites.js\";\nimport { isRenovatePR } from \"../alerts/renovate.js\";\nimport type { PullRequestSummary } from \"./gh.js\";\n\n/**\n * On-demand Renovate trigger for fleet sites.\n *\n * The nightly security sweep (`fleet-security.yml`) writes each active site's\n * vulnerability counts to Airtable. This module turns that into action: for the\n * sites the sweep just flagged with *actionable* (critical or high) vulns, it\n * dispatches that repo's `renovate.yml` `workflow_dispatch` so Renovate runs off\n * its weekly schedule. Renovate's OSV vulnerability alerts bypass the schedule,\n * so the remediation PR opens immediately (then auto-merges per the shared\n * preset). Moderate/low vulns are left to the normal weekly cadence — not urgent\n * enough to dispatch + churn CI nightly.\n */\n\n/** The per-repo workflow file the fleet uses for its self-hosted Renovate run. */\nexport const RENOVATE_WORKFLOW_FILE = \"renovate.yml\";\n\n/**\n * Does the repo have a HEALTHY open Renovate PR — one that is NOT conflicting?\n *\n * A healthy PR means remediation is genuinely in flight (moving toward merge), so\n * re-dispatching would just churn CI — skip it. A CONFLICTING Renovate PR is STUCK\n * (its branch fell behind the base after another PR merged the same lockfile), so\n * it does NOT count as healthy: re-dispatching triggers Renovate to rebase it.\n * `UNKNOWN` mergeability (GitHub still computing it) is treated as healthy — don't\n * churn on uncertainty. Reuses {@link isRenovatePR} for the branch-prefix check.\n */\nexport function hasHealthyRenovatePr(prs: PullRequestSummary[]): boolean {\n return prs.some((pr) => isRenovatePR(pr) && pr.mergeable !== \"CONFLICTING\");\n}\n\n/** A site selected for an on-demand Renovate run, with the reason (vuln counts). */\nexport type RenovateDispatchTarget = {\n repo: string; // owner/repo, from the Websites \"Git repo\" field\n siteName: string;\n critical: number;\n high: number;\n};\n\n/**\n * Pick the active, repo-backed sites whose latest security sweep found a\n * critical or high vulnerability. A `null` vuln count is treated as zero (a site\n * that was never swept, or had its counts cleared, is not dispatched). Inactive\n * sites and sites without a `Git repo` are excluded — there's nothing to kick.\n *\n * Counts are only guaranteed fresh for sites the sweep actually audited (active +\n * URL present); a site whose write-back failed/was skipped this run is selected on\n * its last-known counts. That's safe — a stale-high false dispatch is an idempotent\n * Renovate no-op, and the open-PR skip in `dispatchRenovateAcross` avoids re-firing\n * when remediation is already in flight.\n */\nexport function selectRenovateTargets(sites: WebsiteRow[]): RenovateDispatchTarget[] {\n const targets: RenovateDispatchTarget[] = [];\n for (const s of sites) {\n if (!isDashboardVisible(s)) continue; // active (maintenance / launch period) only\n const repo = s.gitRepo?.trim();\n if (!repo) continue; // repo-backed only — the dispatch target is its renovate.yml\n const critical = s.securityVulnsCritical ?? 0;\n const high = s.securityVulnsHigh ?? 0;\n if (critical + high <= 0) continue; // actionable (critical/high) vulns only\n targets.push({ repo, siteName: s.name, critical, high });\n }\n return targets;\n}\n\n/** Injected GitHub operations — real ones come from `makeGitHub`, fakes from tests. */\nexport type RenovateDispatchDeps = {\n /** True iff the repo has a HEALTHY (non-conflicting) open Renovate PR —\n * remediation in flight, so don't re-dispatch. A conflicting/stuck PR returns\n * false so the dispatch goes through and Renovate rebases it. */\n hasHealthyOpenRenovatePr: (repo: string) => Promise<boolean>;\n defaultBranch: (repo: string) => Promise<string>;\n dispatch: (repo: string, workflow: string, ref: string) => Promise<void>;\n};\n\nexport type RenovateDispatchResult = {\n dispatched: string[];\n /** Repos skipped because a Renovate PR is already open — no point re-firing. */\n skipped: string[];\n failed: { repo: string; error: string }[];\n};\n\n/**\n * Dispatch `renovate.yml` for each target on its default branch.\n *\n * Skips a repo with a HEALTHY (non-conflicting) open Renovate PR — remediation is\n * in flight, so re-dispatching would just churn CI (keeps a persistent vuln from\n * re-firing every nightly run while its fix PR waits, e.g. a human-gated major\n * bump). A repo whose open Renovate PR is CONFLICTING/stuck is NOT skipped:\n * dispatching triggers Renovate to rebase it. Best-effort and isolated: a repo\n * that lacks `renovate.yml`, or whose\n * dispatch/PR-check is refused (the token's `actions:write` scope), is recorded in\n * `failed` and the rest still run. Never throws — the caller (a follow-up step on\n * the security sweep) must not be able to fail the sweep over a dispatch hiccup.\n */\nexport async function dispatchRenovateAcross(\n targets: RenovateDispatchTarget[],\n deps: RenovateDispatchDeps,\n): Promise<RenovateDispatchResult> {\n const dispatched: string[] = [];\n const skipped: string[] = [];\n const failed: { repo: string; error: string }[] = [];\n for (const t of targets) {\n try {\n if (await deps.hasHealthyOpenRenovatePr(t.repo)) {\n skipped.push(t.repo);\n continue;\n }\n const ref = await deps.defaultBranch(t.repo);\n await deps.dispatch(t.repo, RENOVATE_WORKFLOW_FILE, ref);\n dispatched.push(t.repo);\n } catch (e) {\n failed.push({ repo: t.repo, error: e instanceof Error ? e.message : String(e) });\n }\n }\n return { dispatched, skipped, failed };\n}\n\n/** Machine-readable counts line the workflow greps to annotate partial failures. */\nexport function formatRenovateDispatchSummary(result: RenovateDispatchResult): string {\n return `RENOVATE_DISPATCH_SUMMARY dispatched=${result.dispatched.length} skipped=${result.skipped.length} failed=${result.failed.length}`;\n}\n\n/**\n * Plan the per-site auto-fix-attempt counter writes from a dispatch result. PURE.\n * For each active, repo-backed site:\n * - vulns now 0 → reset to 0 (episode resolved)\n * - else dispatched this run → attempts + 1 (a fresh failed-so-far attempt)\n * - else (skipped / failed / untouched) → unchanged\n * Returns only the rows whose value CHANGES (a steady fleet writes nothing). A\n * skipped repo (healthy Renovate PR in flight) is NOT a failed attempt — a fix is\n * genuinely moving toward merge — so its counter holds.\n */\nexport function computeAutoFixAttemptUpdates(\n sites: WebsiteRow[],\n result: RenovateDispatchResult,\n): { id: string; attempts: number }[] {\n const dispatched = new Set(result.dispatched);\n const updates: { id: string; attempts: number }[] = [];\n for (const s of sites) {\n if (!isDashboardVisible(s)) continue;\n const repo = s.gitRepo?.trim();\n if (!repo) continue;\n const current = s.securityAutoFixAttempts ?? 0;\n const vulns = (s.securityVulnsCritical ?? 0) + (s.securityVulnsHigh ?? 0);\n let next = current;\n if (vulns === 0) next = 0;\n else if (dispatched.has(repo)) next = current + 1;\n if (next !== current) updates.push({ id: s.id, attempts: next });\n }\n return updates;\n}\n\n/**\n * Apply the planned counter updates with an injected writer, BEST-EFFORT: a writer\n * that throws (e.g. the Airtable field not yet created, or a transient error) is\n * counted in `failed` and never propagates — the security sweep must not fail over\n * counter bookkeeping. Returns the applied/failed tallies for the summary line.\n */\nexport async function applyAutoFixAttemptUpdates(\n updates: { id: string; attempts: number }[],\n write: (id: string, attempts: number) => Promise<void>,\n): Promise<{ written: number; failed: number }> {\n let written = 0;\n let failed = 0;\n for (const u of updates) {\n try {\n await write(u.id, u.attempts);\n written++;\n } catch {\n failed++;\n }\n }\n return { written, failed };\n}\n\n/** Machine-readable counts line the workflow can grep, mirroring RENOVATE_DISPATCH_SUMMARY. */\nexport function formatAutoFixAttemptsSummary(tally: { written: number; failed: number }): string {\n return `AUTO_FIX_ATTEMPTS_SUMMARY written=${tally.written} failed=${tally.failed}`;\n}\n","import { openBase, readAirtableConfig } from \"../../reports/airtable/client.js\";\nimport { listWebsites, updateAutoFixAttempts } from \"../../reports/airtable/websites.js\";\nimport { makeGitHub } from \"../../github/gh.js\";\nimport {\n selectRenovateTargets,\n dispatchRenovateAcross,\n formatRenovateDispatchSummary,\n hasHealthyRenovatePr,\n computeAutoFixAttemptUpdates,\n applyAutoFixAttemptUpdates,\n formatAutoFixAttemptsSummary,\n} from \"../../github/renovate-dispatch.js\";\n\n/**\n * `renovate-dispatch --fleet`: read the Websites table, pick the active,\n * repo-backed sites the latest security sweep flagged with critical/high vulns,\n * and fire each one's `renovate.yml` `workflow_dispatch` so Renovate's OSV\n * vulnerability alerts open the remediation PR now (instead of waiting for the\n * weekly schedule). Designed to run as a best-effort follow-up step on\n * `fleet-security.yml` AFTER the sweep has written fresh counts to Airtable.\n *\n * Always exits 0: a missing token is a clean skip; a partial dispatch failure\n * (a repo without `renovate.yml`, or a token lacking `actions:write`) is reported\n * in the summary for the workflow to annotate, never failing the security sweep.\n */\nexport async function runRenovateDispatchCommand(opts: {\n fleet?: boolean | undefined;\n}): Promise<{ output: string; code: number }> {\n if (!opts.fleet) {\n return { output: \"renovate-dispatch currently supports only --fleet\", code: 2 };\n }\n const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();\n if (!token) {\n return {\n output: \"renovate-dispatch skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet dispatch) configured.\",\n code: 0,\n };\n }\n\n const base = openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const targets = selectRenovateTargets(websites);\n\n if (targets.length === 0) {\n return {\n output:\n `${formatRenovateDispatchSummary({ dispatched: [], skipped: [], failed: [] })}\\n` +\n \"No active repo-backed sites with critical/high vulnerabilities — nothing to dispatch.\",\n code: 0,\n };\n }\n\n const gh = makeGitHub({ token });\n const result = await dispatchRenovateAcross(targets, {\n hasHealthyOpenRenovatePr: async (repo) => hasHealthyRenovatePr(await gh.openPullRequests(repo)),\n defaultBranch: (repo) => gh.defaultBranch(repo),\n dispatch: (repo, workflow, ref) => gh.dispatchWorkflow(repo, workflow, ref),\n });\n\n const failedRepos = new Set(result.failed.map((f) => f.repo));\n const skippedRepos = new Set(result.skipped);\n const lines = targets.map((t) => {\n const status = failedRepos.has(t.repo)\n ? \"FAILED \"\n : skippedRepos.has(t.repo)\n ? \"skipped (open Renovate PR) \"\n : \"dispatched \";\n return `${status} ${t.repo} (critical=${t.critical} high=${t.high}) — ${t.siteName}`;\n });\n for (const f of result.failed) lines.push(` ↳ ${f.repo}: ${f.error}`);\n\n // Update the auto-fix attempt counters from this run, best-effort: a write that\n // fails (e.g. the Airtable field not yet created) is tallied, never thrown — the\n // sweep must still exit 0. Uses the full `websites` list so 0-vuln sites reset.\n const attemptUpdates = computeAutoFixAttemptUpdates(websites, result);\n const attemptTally = await applyAutoFixAttemptUpdates(attemptUpdates, (id, attempts) =>\n updateAutoFixAttempts(base, id, attempts),\n );\n\n lines.push(formatRenovateDispatchSummary(result));\n lines.push(formatAutoFixAttemptsSummary(attemptTally));\n\n return { output: lines.join(\"\\n\"), code: 0 };\n}\n"],"mappings":"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","names":[]}
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  draftReportForSite,
7
7
  readGaConfig
8
- } from "./chunk-W3G7XFBZ.js";
8
+ } from "./chunk-TZN2G6FD.js";
9
9
  import {
10
10
  reportTier
11
11
  } from "./chunk-6LIWDIXU.js";
@@ -18,7 +18,7 @@ import "./chunk-U5SZ4FOP.js";
18
18
  import {
19
19
  listWebsites,
20
20
  siteSlug
21
- } from "./chunk-CLVTVDOU.js";
21
+ } from "./chunk-L4ULXHIN.js";
22
22
  import {
23
23
  openBase,
24
24
  readAirtableConfig
@@ -68,11 +68,11 @@ function dashboardBaseUrl() {
68
68
  }
69
69
  async function runReportCommand(slug, opts) {
70
70
  if (opts.digest) {
71
- const { runDigest } = await import("./digest-YTO73BEO.js");
71
+ const { runDigest } = await import("./digest-RVIOHUXR.js");
72
72
  return runDigest({ baseUrl: dashboardBaseUrl() });
73
73
  }
74
74
  if (opts.sendReady) {
75
- const { sendApprovedReports } = await import("./orchestrate-LHH3LQNP.js");
75
+ const { sendApprovedReports } = await import("./orchestrate-D72PB6NG.js");
76
76
  return sendApprovedReports();
77
77
  }
78
78
  if (opts.due) {
@@ -237,4 +237,4 @@ export {
237
237
  parseSingleSiteReportType,
238
238
  runReportCommand
239
239
  };
240
- //# sourceMappingURL=report-WK3SKWJI.js.map
240
+ //# sourceMappingURL=report-CN56TGMK.js.map
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-4NAOR44V.js";
10
10
  import {
11
11
  resolveSites
12
- } from "./chunk-J3OPWFZE.js";
12
+ } from "./chunk-6CSCLBAA.js";
13
13
  import "./chunk-CASCWQQX.js";
14
14
  import "./chunk-FDIFTIXC.js";
15
15
  import "./chunk-HVOOCK6L.js";
@@ -58,4 +58,4 @@ async function runSelfUpdatingCommand(site, opts) {
58
58
  export {
59
59
  runSelfUpdatingCommand
60
60
  };
61
- //# sourceMappingURL=self-updating-BYUBUNO4.js.map
61
+ //# sourceMappingURL=self-updating-HLMF7KEG.js.map
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-4NAOR44V.js";
6
6
  import {
7
7
  resolveSites
8
- } from "./chunk-J3OPWFZE.js";
8
+ } from "./chunk-6CSCLBAA.js";
9
9
  import {
10
10
  svelteCodemods
11
11
  } from "./chunk-FUD4I2ZW.js";
@@ -48,4 +48,4 @@ async function runSvelteCodemodsCommand(site, opts) {
48
48
  export {
49
49
  runSvelteCodemodsCommand
50
50
  };
51
- //# sourceMappingURL=svelte-codemods-BYRJAU7I.js.map
51
+ //# sourceMappingURL=svelte-codemods-HNITLWQM.js.map
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-4NAOR44V.js";
6
6
  import {
7
7
  resolveSites
8
- } from "./chunk-J3OPWFZE.js";
8
+ } from "./chunk-6CSCLBAA.js";
9
9
  import {
10
10
  ALL_CONFIG_NAMES,
11
11
  CANONICAL_GITIGNORE_ENTRIES,
@@ -106,4 +106,4 @@ async function runSyncConfigsCommand(site, opts) {
106
106
  export {
107
107
  runSyncConfigsCommand
108
108
  };
109
- //# sourceMappingURL=sync-configs-JPRLAW5J.js.map
109
+ //# sourceMappingURL=sync-configs-BZXBYKTI.js.map
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-4NAOR44V.js";
6
6
  import {
7
7
  resolveSites
8
- } from "./chunk-J3OPWFZE.js";
8
+ } from "./chunk-6CSCLBAA.js";
9
9
  import {
10
10
  upgradeSvelte4to5
11
11
  } from "./chunk-O65I7G7G.js";
@@ -61,4 +61,4 @@ async function runUpgradeCommand(upgradeName, site, opts = {}) {
61
61
  export {
62
62
  runUpgradeCommand
63
63
  };
64
- //# sourceMappingURL=upgrade-ERA45EVR.js.map
64
+ //# sourceMappingURL=upgrade-6YDUELFA.js.map
@@ -13,12 +13,14 @@ import {
13
13
  updateA11yCounts,
14
14
  updateAnalyticsHealth,
15
15
  updateAuditFields,
16
+ updateAutoFixAttempts,
16
17
  updateDepsCounts,
17
18
  updateGitHubSignals,
18
19
  updateLaunched,
19
20
  updateScores,
20
- updateSecurityCounts
21
- } from "./chunk-CLVTVDOU.js";
21
+ updateSecurityCounts,
22
+ updateSiteField
23
+ } from "./chunk-L4ULXHIN.js";
22
24
  export {
23
25
  ACTIVE_STATUSES,
24
26
  SEVERITY_RANK,
@@ -34,10 +36,12 @@ export {
34
36
  updateA11yCounts,
35
37
  updateAnalyticsHealth,
36
38
  updateAuditFields,
39
+ updateAutoFixAttempts,
37
40
  updateDepsCounts,
38
41
  updateGitHubSignals,
39
42
  updateLaunched,
40
43
  updateScores,
41
- updateSecurityCounts
44
+ updateSecurityCounts,
45
+ updateSiteField
42
46
  };
43
- //# sourceMappingURL=websites-X5DLC2PC.js.map
47
+ //# sourceMappingURL=websites-IYP7IGLU.js.map
@@ -2,12 +2,12 @@ import {
2
2
  formatFleetWriteSummary,
3
3
  writeAuditsToAirtable,
4
4
  writeFleetAuditsToAirtable
5
- } from "./chunk-5WL3ET24.js";
6
- import "./chunk-IMIR7SC2.js";
7
- import "./chunk-CLVTVDOU.js";
5
+ } from "./chunk-LMXIZITU.js";
6
+ import "./chunk-HPSGCDWY.js";
7
+ import "./chunk-L4ULXHIN.js";
8
8
  export {
9
9
  formatFleetWriteSummary,
10
10
  writeAuditsToAirtable,
11
11
  writeFleetAuditsToAirtable
12
12
  };
13
- //# sourceMappingURL=write-audits-to-airtable-CE4POBAF.js.map
13
+ //# sourceMappingURL=write-audits-to-airtable-VDGHCZGQ.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reddoorla/maintenance",
3
- "version": "0.57.0",
3
+ "version": "0.59.0",
4
4
  "description": "Canonical maintenance configs, audits, and recipes for the reddoor stack.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,9 +0,0 @@
1
- import {
2
- fromAirtableBase
3
- } from "./chunk-JQG222CI.js";
4
- import "./chunk-U5SZ4FOP.js";
5
- import "./chunk-CLVTVDOU.js";
6
- export {
7
- fromAirtableBase
8
- };
9
- //# sourceMappingURL=airtable-2BJX4ATJ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/audits/deps.ts","../src/audits/deps-outdated.ts","../src/audits/lint.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/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 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\";\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};\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\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 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 settings: { preset: \"desktop\", 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 { 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\";\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};\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":";;;;;;;;;;;;;;;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,QAAAA,aAAY;;;ACDrB,SAAS,YAAY;AACrB,SAAS,YAAY;AAQrB,eAAe,OAAO,MAAgC;AACpD,MAAI;AACF,UAAM,KAAK,IAAI;AACf,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,QAAQ,SAAyB;AACxC,QAAM,OAAO,QAAQ,QAAQ,UAAU,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK;AAC5D,QAAM,IAAI,OAAO,SAAS,MAAM,EAAE;AAClC,SAAO,OAAO,MAAM,CAAC,IAAI,IAAI;AAC/B;AAcA,eAAsB,aACpB,UACA,OACgC;AAChC,MAAI,CAAE,MAAM,OAAO,KAAK,UAAU,gBAAgB,CAAC,EAAI,QAAO;AAM9D,MAAI;AAKF,QAAI,CAAE,MAAM,OAAO,KAAK,UAAU,cAAc,CAAC,GAAI;AACnD,YAAM,UAAU,MAAM,MAAM,QAAQ,CAAC,WAAW,mBAAmB,GAAG;AAAA,QACpE,KAAK;AAAA,QACL,WAAW;AAAA,MACb,CAAC;AACD,UAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,IACjC;AAIA,UAAM,MAAM,MAAM,MAAM,QAAQ,CAAC,YAAY,QAAQ,GAAG;AAAA,MACtD,KAAK;AAAA,MACL,WAAW;AAAA,IACb,CAAC;AACD,UAAM,SAAS,KAAK,MAAM,IAAI,UAAU,IAAI;AAI5C,UAAM,UAAU,OAAO,OAAO,MAAM;AACpC,WAAO;AAAA,MACL,UAAU,QAAQ;AAAA,MAClB,OAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,UAAU,QAAQ,EAAE,MAAM,IAAI,QAAQ,EAAE,OAAO,CAAC,EACzF;AAAA,IACL;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ADhDA,SAAS,WAAW,OAAuB;AACzC,SAAO,MAAM,QAAQ,UAAU,EAAE;AACnC;AAMA,SAAS,kBAAkB,MAAuB;AAChD,SAAO,YAAY,KAAK,KAAK,KAAK,CAAC;AACrC;AAEA,SAAS,YAAY,GAAqC;AACxD,QAAM,UAAU,WAAW,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK;AAC/C,QAAM,QAAQ,QAAQ,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,OAAO,SAAS,GAAG,EAAE,CAAC;AAClE,SAAO,CAAC,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;AACrD;AAEA,SAAS,cAAc,QAAgB,UAAyB;AAC9D,QAAM,CAAC,QAAQ,QAAQ,MAAM,IAAI,YAAY,MAAM;AACnD,QAAM,CAAC,QAAQ,QAAQ,MAAM,IAAI,YAAY,QAAQ;AACrD,MAAI,SAAS,OAAQ,QAAO;AAC5B,MAAI,SAAS,OAAQ,QAAO;AAC5B,MAAI,SAAS,OAAQ,QAAO;AAC5B,MAAI,SAAS,OAAQ,QAAO;AAC5B,MAAI,SAAS,OAAQ,QAAO;AAC5B,MAAI,SAAS,OAAQ,QAAO;AAC5B,SAAO;AACT;AAEA,eAAsB,UAAU,KAAyC;AACvE,QAAM,UAAUC,MAAK,IAAI,KAAK,MAAM,cAAc;AAClD,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,SAAS,SAAS,OAAO;AAAA,EAC1C,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,OAAO;AAAA,MACP,MAAM,UAAU,IAAI,IAAI;AAAA,MACxB,QAAQ;AAAA,MACR,SAAS,sBAAsB,OAAO;AAAA,MACtC,SAAS,EAAE,OAAO,OAAO,GAAG,EAAE;AAAA,IAChC;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,MAAM;AAAA,EAIzB,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,OAAO;AAAA,MACP,MAAM,UAAU,IAAI,IAAI;AAAA,MACxB,QAAQ;AAAA,MACR,SAAS,mCAAoC,IAAc,OAAO;AAAA,MAClE,SAAS,EAAE,OAAO,OAAO,GAAG,EAAE;AAAA,IAChC;AAAA,EACF;AACA,QAAM,YAAoC;AAAA,IACxC,GAAI,IAAI,gBAAgB,CAAC;AAAA,IACzB,GAAI,IAAI,mBAAmB,CAAC;AAAA,EAC9B;AAEA,QAAM,UAA4B,CAAC;AACnC,aAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,QAAQ,gBAAgB,GAAG;AAC/D,UAAM,SAAS,UAAU,IAAI;AAC7B,QAAI,CAAC,OAAQ;AAGb,QAAI,CAAC,kBAAkB,MAAM,EAAG;AAChC,YAAQ,KAAK;AAAA,MACX,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,OAAO,cAAc,QAAQ,QAAQ;AAAA,IACvC,CAAC;AAAA,EACH;AAEA,QAAM,WAAW,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,OAAO;AACxD,QAAM,WAAW,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,OAAO;AACxD,QAAM,WAAW,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,OAAO;AAIxD,QAAM,SAAgC,WAAW,SAAS,YAAY,WAAW,SAAS;AAE1F,QAAM,eACJ,WAAW,SACP,OAAO,QAAQ,MAAM,wCACrB,WAAW,SACT,GAAG,QAAQ,OAAO,CAAC,MAAM,EAAE,UAAU,MAAM,EAAE,MAAM,OAAO,QAAQ,MAAM,0BACxE,GAAG,QAAQ,OAAO,CAAC,MAAM,EAAE,UAAU,OAAO,EAAE,MAAM;AAE5D,QAAM,WAAW,MAAM,aAAa,IAAI,KAAK,MAAM,IAAI,SAAS,YAAY;AAC5E,QAAM,UAAU,WACZ,GAAG,YAAY,KAAK,SAAS,QAAQ,yBAAyB,SAAS,KAAK,YAC5E;AAEJ,SAAO;AAAA,IACL,OAAO;AAAA,IACP,MAAM,UAAU,IAAI,IAAI;AAAA,IACxB;AAAA,IACA;AAAA,IACA,SAAS,EAAE,SAAS,SAAS;AAAA,EAC/B;AACF;;;AEzIA,SAAS,kBAAkB;AAC3B,SAAS,YAAAC,iBAAgB;AACzB,SAAS,QAAAC,aAAY;AACrB,SAAS,cAAc;AACvB,SAAS,SAAS,eAAe,iBAAiB,6BAA6B;AAC/E,SAAS,YAAY;AAKrB,IAAM,eAAe,CAAC,qBAAqB;AAC3C,IAAM,SAAS,CAAC,mBAAmB,WAAW,kBAAkB,YAAY,aAAa;AAEzF,eAAe,UAAU,KAAgC;AACvD,SAAO,KAAK,cAAc,EAAE,KAAK,QAAQ,QAAQ,UAAU,MAAM,CAAC;AACpE;AAEA,eAAsB,UAAU,KAAyC;AACvE,QAAM,EAAE,KAAK,IAAI;AACjB,QAAM,aAAaC,MAAK,KAAK,MAAM,kBAAkB;AAErD,MAAI,CAAC,WAAW,UAAU,GAAG;AAC3B,WAAO;AAAA,MACL,OAAO;AAAA,MACP,MAAM,UAAU,IAAI;AAAA,MACpB,QAAQ;AAAA,MACR,SAAS;AAAA,IACX;AAAA,EACF;AAEA,QAAM,SAAS,IAAI,OAAO;AAAA,IACxB,KAAK,KAAK;AAAA,IACV,oBAAoB;AAAA,IACpB,yBAAyB;AAAA,EAC3B,CAAC;AAED,QAAM,WAAW,MAAM,UAAU,KAAK,IAAI;AAI1C,QAAM,gBAAgB,MAAM,OAAO,UAAU,QAAQ;AACrD,QAAM,eAAe,cAAc,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,YAAY,CAAC;AACvE,QAAM,iBAAiB,cAAc,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,cAAc,CAAC;AAE3E,QAAM,sBAAgC,CAAC;AACvC,aAAW,OAAO,UAAU;AAC1B,UAAM,gBAAgBA,MAAK,KAAK,MAAM,GAAG;AACzC,UAAM,SAAS,MAAMC,UAAS,eAAe,OAAO;AACpD,UAAM,UAAW,MAAM,sBAAsB,aAAa,KAAM,CAAC;AACjE,UAAM,KAAK,MAAM,cAAc,QAAQ,EAAE,GAAG,SAAS,UAAU,cAAc,CAAC;AAC9E,QAAI,CAAC,GAAI,qBAAoB,KAAK,GAAG;AAAA,EACvC;AAEA,QAAM,SACJ,eAAe,KAAK,oBAAoB,SAAS,IAC7C,SACA,iBAAiB,IACf,SACA;AAER,QAAM,UACJ,WAAW,SACP,qBAAqB,SAAS,MAAM,WACpC,GAAG,YAAY,mBAAmB,cAAc,cAAc,oBAAoB,MAAM;AAE9F,SAAO;AAAA,IACL,OAAO;AAAA,IACP,MAAM,UAAU,IAAI;AAAA,IACpB;AAAA,IACA;AAAA,IACA,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,SAAS;AAAA,IAClB;AAAA,EACF;AACF;;;AC9BA,SAAS,SAAS,GAAW;AAC3B,MAAI,EAAE,WAAW,KAAK,EAAE,OAAO,EAAG,QAAO;AACzC,MAAI,EAAE,WAAW,KAAK,EAAE,MAAM,EAAG,QAAO;AACxC,SAAO;AACT;AAEA,SAAS,kBAAkB,GAAsB;AAC/C,MAAI,MAAM,SAAS,MAAM,cAAc,MAAM,UAAU,MAAM,WAAY,QAAO;AAGhF,SAAO;AACT;AAEA,SAAS,0BAA0B,QAAwC;AACzE,QAAM,MAAuB,CAAC;AAC9B,aAAW,KAAK,OAAO,OAAO,OAAO,cAAc,CAAC,CAAC,GAAG;AACtD,QAAI,CAAC,EAAG;AACR,QAAI,KAAK;AAAA,MACP,QAAQ,EAAE,eAAe;AAAA,MACzB,UAAU,kBAAkB,EAAE,QAAQ;AAAA,MACtC,OAAO,EAAE,SAAS;AAAA,MAClB,GAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;AAAA,MACjC,GAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,IAAI,CAAC;AAAA,IAChC,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAKA,SAAS,uBACP,WACA,iBACiE;AACjE,QAAM,OAAO,oBAAI,IAAY;AAC7B,MAAI,UAAU;AACd,SAAO,CAAC,KAAK,IAAI,OAAO,GAAG;AACzB,SAAK,IAAI,OAAO;AAChB,UAAM,QAAQ,gBAAgB,OAAO;AACrC,QAAI,CAAC,SAAS,CAAC,MAAM,QAAQ,MAAM,GAAG,EAAG,QAAO,EAAE,UAAU,QAAQ;AAEpE,UAAM,WAAW,MAAM,IAAI;AAAA,MACzB,CAAC,MAA6C,OAAO,MAAM,YAAY,MAAM;AAAA,IAC/E;AACA,QAAI,SAAU,QAAO,EAAE,UAAU,SAAS,QAAQ,SAAS;AAE3D,UAAM,OAAO,MAAM,IAAI,KAAK,CAAC,MAAmB,OAAO,MAAM,QAAQ;AACrE,QAAI,CAAC,QAAQ,SAAS,QAAS,QAAO,EAAE,UAAU,QAAQ;AAC1D,cAAU;AAAA,EACZ;AACA,SAAO,EAAE,UAAU,QAAQ;AAC7B;AAEA,SAAS,yBAAyB,QAAuC;AACvE,QAAM,kBAAkB,OAAO,mBAAmB,CAAC;AACnD,QAAM,QAAQ,oBAAI,IAA2B;AAE7C,aAAW,CAAC,MAAM,CAAC,KAAK,OAAO,QAAQ,eAAe,GAAG;AACvD,QAAI,CAAC,EAAG;AACR,UAAM,EAAE,UAAU,OAAO,IAAI,uBAAuB,MAAM,eAAe;AACzE,QAAI,MAAM,IAAI,QAAQ,EAAG;AAEzB,UAAM,YAAY,gBAAgB,QAAQ;AAC1C,UAAM,WAAW,kBAAkB,WAAW,YAAY,EAAE,QAAQ;AACpE,UAAM,QAAQ,QAAQ,SAAS;AAC/B,UAAM,MAAM,QAAQ;AAEpB,UAAM,IAAI,UAAU;AAAA,MAClB,QAAQ,WAAW,QAAQ;AAAA,MAC3B;AAAA,MACA;AAAA,MACA,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;AAAA,IACvB,CAAC;AAAA,EACH;AAEA,SAAO,CAAC,GAAG,MAAM,OAAO,CAAC;AAC3B;AAOA,eAAe,aACb,OACA,KACA,MACA,KACqB;AACrB,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,KAAK,MAAM,EAAE,IAAI,CAAC;AAAA,EACtC,SAAS,KAAK;AACZ,UAAM,IAAI;AACV,QAAI,EAAE,SAAS,YAAY,SAAS,KAAK,OAAO,GAAG,CAAC,EAAG,QAAO,EAAE,MAAM,UAAU;AAChF,WAAO,EAAE,MAAM,SAAS,QAAQ,iBAAiB,OAAO,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,GAAG;AAAA,EAC/E;AAGA,MAAI,IAAI,SAAS,KAAK,IAAI,SAAS,GAAG;AACpC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,QAAQ,QAAQ,IAAI,IAAI,GAAG,IAAI,SAAS,KAAK,IAAI,OAAO,MAAM,GAAG,GAAG,CAAC,KAAK,EAAE;AAAA,IAC9E;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,IAAI,UAAU,IAAI;AAAA,EACxC,SAAS,KAAK;AACZ,WAAO,EAAE,MAAM,SAAS,QAAQ,qBAAqB,OAAO,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,GAAG;AAAA,EACnF;AAIA,QAAM,cAAe,OAAoD;AACzE,MAAI,eAAe,OAAO,gBAAgB,UAAU;AAClD,WAAO,EAAE,MAAM,SAAS,QAAQ,YAAY,QAAQ,0BAA0B;AAAA,EAChF;AAMA,QAAM,YAAY,OAAO,UAAU;AACnC,MAAI,CAAC,aAAa,OAAO,KAAK,SAAS,EAAE,WAAW,GAAG;AACrD,WAAO,EAAE,MAAM,SAAS,QAAQ,wCAAwC;AAAA,EAC1E;AAEA,SAAO,EAAE,MAAM,MAAM,OAAO;AAC9B;AAEA,eAAsB,cAAc,KAAyC;AAC3E,QAAM,QAAQ,IAAI,SAAS;AAC3B,QAAM,OAAO,IAAI;AACjB,QAAM,QAAQ,UAAU,IAAI;AAE5B,MAAI,OAAmC;AACvC,MAAI,SAAS,MAAM,aAAa,OAAO,QAAQ,CAAC,SAAS,UAAU,QAAQ,GAAG,KAAK,IAAI;AAMvF,MAAI,OAAO,SAAS,MAAM;AACxB,UAAM,aAAa,OAAO,SAAS,YAAY,kBAAkB,OAAO;AACxE,UAAM,YAAY,MAAM;AAAA,MACtB;AAAA,MACA;AAAA,MACA,CAAC,SAAS,UAAU,YAAY;AAAA,MAChC,KAAK;AAAA,IACP;AACA,QAAI,UAAU,SAAS,MAAM;AAC3B,eAAS;AACT,aAAO;AAAA,IACT,OAAO;AACL,YAAM,YAAY,UAAU,SAAS,YAAY,kBAAkB,UAAU;AAC7E,aAAO;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS,iCAA4B,UAAU,UAAU,SAAS;AAAA,MACpE;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,OAAO;AAEtB,QAAM,SAAiB;AAAA,IACrB,KAAK,OAAO,UAAU,iBAAiB,OAAO;AAAA,IAC9C,UAAU,OAAO,UAAU,iBAAiB,YAAY;AAAA,IACxD,MAAM,OAAO,UAAU,iBAAiB,QAAQ;AAAA,IAChD,UAAU,OAAO,UAAU,iBAAiB,YAAY;AAAA,EAC1D;AAEA,QAAM,aACJ,SAAS,eAAe,0BAA0B,MAAM,IAAI,yBAAyB,MAAM;AAE7F,QAAM,SAAS,SAAS,MAAM;AAC9B,QAAM,QAAQ,OAAO,MAAM,OAAO,WAAW,OAAO,OAAO,OAAO;AAClE,QAAM,UACJ,WAAW,SACP,GAAG,IAAI,wBACP,GAAG,IAAI,KAAK,KAAK,qBAAqB,OAAO,QAAQ,KAAK,OAAO,IAAI,KAAK,OAAO,QAAQ,KAAK,OAAO,GAAG;AAE9G,SAAO;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,SAAS,EAAE,QAAQ,WAAW;AAAA,EAChC;AACF;;;AChPA,SAAS,YAAAC,WAAU,WAAW,SAAS,IAAI,eAAe;AAC1D,SAAS,cAAc;AACvB,SAAS,QAAAC,aAAY;;;ACFrB,SAAS,YAAAC,iBAAgB;AACzB,SAAS,QAAAC,aAAY;AAarB,eAAsB,eAAe,UAAuC;AAC1E,MAAI;AACJ,MAAI;AACF,UAAM,MAAMD,UAASC,MAAK,UAAU,cAAc,GAAG,OAAO;AAAA,EAC9D,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,GAAG;AAAA,EACtB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,CAAC;AAC7C,QAAM,MAAO,IAA8B;AAC3C,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,CAAC;AAE7C,QAAM,MAAkB,CAAC;AACzB,QAAM,MAAO,IAAoC;AACjD,MAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,GAAG;AAC7C,QAAI,gBAAgB;AAAA,EACtB;AACA,SAAO;AACT;;;ACrCA,SAAS,oBAAoB;AAuB7B,eAAsB,eAAgC;AACpD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,SAAS,aAAa;AAC5B,WAAO,MAAM;AACb,WAAO,GAAG,SAAS,MAAM;AACzB,WAAO,OAAO,GAAG,aAAa,MAAM;AAClC,YAAM,OAAO,OAAO,QAAQ;AAC5B,UAAI,OAAO,SAAS,YAAY,MAAM;AACpC,cAAM,OAAO,KAAK;AAClB,eAAO,MAAM,MAAM,QAAQ,IAAI,CAAC;AAAA,MAClC,OAAO;AACL,eAAO,MAAM;AACb,eAAO,IAAI,MAAM,6DAA6D,CAAC;AAAA,MACjF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAOO,SAAS,aAAa,KAAa,MAAsB;AAC9D,QAAM,IAAI,IAAI,IAAI,GAAG;AACrB,IAAE,WAAW;AACb,IAAE,OAAO,OAAO,IAAI;AACpB,SAAO,EAAE,SAAS;AACpB;;;AFfA,eAAe,cAAiB,MAAiC;AAC/D,MAAI;AACF,UAAM,MAAM,MAAMC,UAAS,MAAM,OAAO;AACxC,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAeA,eAAe,eAAe,YAA8C;AAC1E,QAAM,QAAQ,MAAM,QAAQ,UAAU,EAAE,MAAM,MAAM,CAAC,CAAa;AAClE,QAAM,UAA2B,CAAC;AAClC,aAAW,KAAK,OAAO;AACrB,QAAI,CAAC,EAAE,WAAW,MAAM,KAAK,CAAC,EAAE,SAAS,OAAO,EAAG;AACnD,UAAM,MAAM,MAAM,cAAuBC,MAAK,YAAY,CAAC,CAAC;AAC5D,QAAI,CAAC,OAAO,CAAC,IAAI,WAAY;AAC7B,UAAM,UAAkC,CAAC;AACzC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,IAAI,UAAU,GAAG;AACnD,UAAI,OAAO,GAAG,UAAU,SAAU,SAAQ,CAAC,IAAI,EAAE;AAAA,IACnD;AACA,YAAQ,KAAK,EAAE,KAAK,IAAI,cAAc,QAAQ,CAAC;AAAA,EACjD;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,SAAkD;AAC1E,MAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAClC,QAAM,OAA+B,CAAC;AACtC,QAAM,SAAiC,CAAC;AACxC,aAAW,KAAK,SAAS;AACvB,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,EAAE,WAAW,CAAC,CAAC,GAAG;AACpD,UAAI,OAAO,MAAM,SAAU;AAC3B,WAAK,CAAC,KAAK,KAAK,CAAC,KAAK,KAAK;AAC3B,aAAO,CAAC,KAAK,OAAO,CAAC,KAAK,KAAK;AAAA,IACjC;AAAA,EACF;AACA,QAAM,MAA8B,CAAC;AACrC,aAAW,KAAK,OAAO,KAAK,IAAI,GAAG;AACjC,UAAM,QAAQ,KAAK,CAAC,KAAK;AACzB,UAAM,QAAQ,OAAO,CAAC,KAAK;AAC3B,QAAI,CAAC,IAAI,QAAQ;AAAA,EACnB;AACA,SAAO;AACT;AAEA,SAAS,sBAAsB,GAA4B;AAEzD,QAAM,WAAW,EAAE,KAAK,QAAQ,GAAG;AACnC,SAAO,YAAY,IAAI,EAAE,KAAK,MAAM,WAAW,CAAC,IAAI,EAAE;AACxD;AAEA,SAAS,oBAAoB,GAA4B;AAIvD,QAAM,SAAS,OAAO,EAAE,WAAW,WAAW,EAAE,OAAO,QAAQ,CAAC,IAAI;AACpE,SAAO,GAAG,EAAE,IAAI,IAAI,EAAE,QAAQ,IAAI,EAAE,QAAQ,aAAa,MAAM;AACjE;AAIA,eAAe,iBACb,YACA,OACA,KACsB;AACtB,QAAM,WAAW,MAAM,eAAe,UAAU;AAEhD,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS,2CAA2C,IAAI,IAAI,IAC1D,IAAI,SAAS,WAAM,IAAI,OAAO,MAAM,GAAG,GAAG,CAAC,KAAK,EAClD;AAAA,IACF;AAAA,EACF;AAEA,QAAM,mBACH,MAAM,cAAiCA,MAAK,YAAY,wBAAwB,CAAC,KAAM,CAAC;AAE3F,QAAM,SAAS,iBAAiB,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM;AACvD,QAAM,aAAa,OAAO,IAAI,CAAC,OAAO;AAAA,IACpC,UAAU,sBAAsB,CAAC;AAAA,IACjC,OAAO,EAAE;AAAA,IACT,SAAS,oBAAoB,CAAC;AAAA,EAChC,EAAE;AAEF,QAAM,WAAW,WAAW,KAAK,CAAC,MAAM,EAAE,UAAU,OAAO;AAC3D,QAAM,UAAU,WAAW,KAAK,CAAC,MAAM,EAAE,UAAU,MAAM;AACzD,QAAM,SAAgC,WAAW,SAAS,UAAU,SAAS;AAE7E,QAAM,aAAmC;AAAA,IACvC,SAAS,iBAAiB,QAAQ;AAAA,IAClC,kBAAkB,OAAO;AAAA,IACzB;AAAA,EACF;AAEA,QAAM,UACJ,WAAW,SACP,uCACA,eAAe,OAAO,MAAM;AAElC,SAAO,EAAE,OAAO,cAAc,MAAM,OAAO,QAAQ,SAAS,SAAS,WAAW;AAClF;AAIA,eAAe,mBAAmB,OAAgB,MAAY,OAAqC;AACjG,QAAM,UAAU,MAAM,eAAe,KAAK,IAAI;AAI9C,QAAM,OAAO,MAAM,aAAa;AAChC,QAAM,UAAU,QAAQ,iBAAiB,iBAAiB,GAAG,QAAQ,IAAI,CAAC;AAC1E,QAAM,iBAAiB;AAAA,IACrB,GAAG;AAAA,IACH,IAAI;AAAA,MACF,GAAG,iBAAiB;AAAA,MACpB,SAAS;AAAA,QACP,GAAG,iBAAiB,GAAG;AAAA,QACvB,KAAK,CAAC,aAAa,SAAS,IAAI,CAAC;AAAA,QACjC,oBAAoB,8BAA8B,IAAI;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,QAAQA,MAAK,OAAO,GAAG,eAAe,CAAC;AAC/D,QAAM,aAAaA,MAAK,WAAW,mBAAmB;AACtD,QAAM,UAAU,YAAY,KAAK,UAAU,cAAc,GAAG,OAAO;AAEnE,QAAM,aAAaA,MAAK,KAAK,MAAM,eAAe;AAClD,QAAM,GAAG,YAAY,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAErD,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,CAAC,SAAS,aAAa,WAAW,YAAY,UAAU,EAAE,GAAG;AAAA,MACpF,KAAK,KAAK;AAAA,MACV,WAAW,IAAI;AAAA,IACjB,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,UAAM,GAAG,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACpD,UAAM,IAAI;AACV,QAAI,EAAE,SAAS,YAAY,SAAS,KAAK,OAAO,GAAG,CAAC,GAAG;AACrD,aAAO;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,MACX;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACA,QAAM,GAAG,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAEpD,SAAO,iBAAiB,YAAY,OAAO,GAAG;AAChD;AAKA,eAAe,mBACb,OACA,aACA,OACsB;AACtB,QAAM,UAAU,MAAM,QAAQA,MAAK,OAAO,GAAG,sBAAsB,CAAC;AACpE,QAAM,iBAAiB;AAAA,IACrB,IAAI;AAAA;AAAA;AAAA,MAGF,SAAS;AAAA,QACP,KAAK,CAAC,WAAW;AAAA;AAAA;AAAA,QAGjB,cAAc;AAAA,QACd,UAAU,EAAE,QAAQ,WAAW,YAAY,CAAC,YAAY,EAAE;AAAA,MAC5D;AAAA,MACA,QAAQ,iBAAiB,GAAG;AAAA,MAC5B,QAAQ,EAAE,QAAQ,cAAc,WAAWA,MAAK,SAAS,aAAa,EAAE;AAAA,IAC1E;AAAA,EACF;AAEA,QAAM,aAAaA,MAAK,SAAS,mBAAmB;AACpD,QAAM,UAAU,YAAY,KAAK,UAAU,cAAc,GAAG,OAAO;AAEnE,QAAM,aAAaA,MAAK,SAAS,eAAe;AAEhD,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,CAAC,SAAS,aAAa,WAAW,YAAY,UAAU,EAAE,GAAG;AAAA,MACpF,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAML,WAAW,IAAI;AAAA,IACjB,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,UAAM,GAAG,SAAS,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAClD,UAAM,IAAI;AACV,QAAI,EAAE,SAAS,YAAY,SAAS,KAAK,OAAO,GAAG,CAAC,GAAG;AACrD,aAAO;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,MACX;AAAA,IACF;AACA,UAAM;AAAA,EACR;AAEA,MAAI;AACF,WAAO,MAAM,iBAAiB,YAAY,OAAO,GAAG;AAAA,EACtD,UAAE;AACA,UAAM,GAAG,SAAS,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EACpD;AACF;AAEA,eAAsB,gBAAgB,KAAyC;AAC7E,QAAM,QAAQ,IAAI,SAAS;AAC3B,QAAM,OAAO,IAAI;AACjB,QAAM,QAAQ,UAAU,IAAI;AAE5B,SAAO,KAAK,cACR,mBAAmB,OAAO,KAAK,aAAa,KAAK,IACjD,mBAAmB,OAAO,MAAM,KAAK;AAC3C;;;AGtRA,SAAS,YAAAC,WAAU,aAAAC,YAAW,WAAAC,UAAS,MAAAC,WAAU;AACjD,SAAS,QAAAC,aAAY;AAyBrB,IAAM,cAAc;AAEpB,eAAeC,eAAiB,MAAiC;AAC/D,MAAI;AACF,UAAM,MAAM,MAAMC,UAAS,MAAM,OAAO;AACxC,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOA,SAAS,sBAAsB,MAAc,UAA0B;AACrE,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iCAUwB,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2CAWM,IAAI;AAAA,6BAClB,IAAI;AAAA,WACtB,KAAK,UAAU,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAMnC;AAOA,SAAS,YAAoB;AAC3B,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,gBAKO,KAAK,UAAU,UAAU,CAAC;AAAA,qBACrB,KAAK,UAAU,WAAW,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8EhD;AAEA,eAAsB,UAAU,KAAyC;AACvE,QAAM,QAAQ,IAAI,SAAS;AAC3B,QAAM,OAAO,IAAI;AACjB,QAAM,QAAQ,UAAU,IAAI;AAQ5B,QAAM,UAAU,MAAMC,SAAQC,MAAK,KAAK,MAAM,qBAAqB,CAAC;AAMpE,MAAI;AACF,UAAM,WAAWA,MAAK,SAAS,cAAc;AAC7C,UAAMC,WAAU,UAAU,UAAU,GAAG,OAAO;AAE9C,UAAM,OAAO,MAAM,aAAa;AAChC,UAAM,aAAaD,MAAK,SAAS,sBAAsB;AACvD,UAAMC,WAAU,YAAY,sBAAsB,MAAM,KAAK,IAAI,GAAG,OAAO;AAE3E,UAAM,cAAcD,MAAK,KAAK,MAAM,WAAW;AAE/C,UAAME,IAAGF,MAAK,KAAK,MAAM,eAAe,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAE3E,QAAI;AACJ,QAAI;AACF,YAAM,MAAM;AAAA,QACV;AAAA,QACA,CAAC,SAAS,cAAc,QAAQ,YAAY,UAAU,IAAI,mBAAmB,QAAQ;AAAA,QACrF;AAAA,UACE,KAAK,KAAK;AAAA,UACV,KAAK,EAAE,GAAG,QAAQ,KAAK,qBAAqB,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,UAKxD,WAAW,IAAI;AAAA,QACjB;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,IAAI;AACV,UAAI,EAAE,SAAS,YAAY,SAAS,KAAK,OAAO,GAAG,CAAC,GAAG;AACrD,eAAO;AAAA,UACL,OAAO;AAAA,UACP,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,SAAS;AAAA,QACX;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAEA,UAAM,WAAW,MAAMH,eAA8B,WAAW;AAEhE,QAAI,CAAC,UAAU;AACb,aAAO;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS,kCAAkC,IAAI,IAAI,IACjD,IAAI,SAAS,WAAM,IAAI,OAAO,MAAM,GAAG,GAAG,CAAC,KAAK,EAClD;AAAA,MACF;AAAA,IACF;AAEA,UAAM,cACH,SAAS,SAAS,WAAW,KAAK,MAAM,SAAS,SAAS,YAAY,KAAK;AAC9E,UAAM,SAAS,SAAS,kBAAkB;AAE1C,UAAM,SAAgC,aAAa,SAAS,SAAS,SAAS;AAC9E,UAAM,UACJ,WAAW,SACP,6BAA6B,WAAW,MAAM,aAAa,YAAY,MAAM,sBAC7E,SAAS,SAAS,eAAe;AAEvC,WAAO;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,SAAS;AAAA,IACX;AAAA,EACF,UAAE;AACA,UAAMK,IAAG,SAAS,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EACpD;AACF;;;AChQA,SAAS,YAAY,mBAAmB;AACxC,OAAO,SAAS;AAqBhB,IAAM,aAAa,KAAK,KAAK,KAAK;AAOlC,eAAsB,YAAY,KAAa,MAAwC;AACrF,MAAI;AACJ,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE;AAAA,EACtB,QAAQ;AACN,WAAO,EAAE,UAAU,OAAO,mBAAmB,KAAK;AAAA,EACpD;AACA,MAAI;AACF,UAAM,KAAK,OAAO,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO,EAAE,UAAU,OAAO,mBAAmB,KAAK;AAAA,EACpD;AACA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,KAAK,YAAY,IAAI;AAAA,EACvC,QAAQ;AACN,cAAU;AAAA,EACZ;AACA,MAAI,CAAC,WAAW,OAAO,MAAM,QAAQ,QAAQ,CAAC;AAC5C,WAAO,EAAE,UAAU,MAAM,mBAAmB,KAAK;AACnD,SAAO;AAAA,IACL,UAAU;AAAA,IACV,mBAAmB,KAAK,OAAO,QAAQ,QAAQ,IAAI,KAAK,IAAI,QAAQ,KAAK,UAAU;AAAA,EACrF;AACF;AAIO,SAAS,kBAAkB,KAAuB;AACvD,SAAO;AAAA,IACL,QAAQ,OAAO,SAAS;AACtB,YAAM,YAAY,OAAO,IAAI;AAAA,IAC/B;AAAA,IACA,aAAa,CAAC,SACZ,IAAI,QAAqB,CAAC,mBAAmB;AAM3C,YAAM,SAAS,IAAI;AAAA,QACjB,EAAE,MAAM,MAAM,KAAK,YAAY,MAAM,SAAS,KAAQ,oBAAoB,KAAK;AAAA,QAC/E,MAAM;AACJ,gBAAM,OAAO,OAAO,aAAa,OAAO,mBAAmB,IAAI;AAC/D,iBAAO,IAAI;AACX,gBAAM,UAAU,QAAQ,KAAK,WAAW,IAAI,KAAK,KAAK,QAAQ,IAAI;AAClE,yBAAe,OAAO;AAAA,QACxB;AAAA,MACF;AACA,aAAO,GAAG,SAAS,MAAM,eAAe,IAAI,CAAC;AAC7C,aAAO,GAAG,WAAW,MAAM;AACzB,eAAO,QAAQ;AACf,uBAAe,IAAI;AAAA,MACrB,CAAC;AAAA,IACH,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAOA,eAAsB,YAAY,KAAyC;AACzE,QAAM,EAAE,KAAK,IAAI;AACjB,QAAM,QAAQ,UAAU,IAAI;AAC5B,MAAI,CAAC,KAAK,aAAa;AACrB,WAAO,EAAE,OAAO,UAAU,MAAM,OAAO,QAAQ,QAAQ,SAAS,kBAAkB;AAAA,EACpF;AACA,QAAM,MAAM,IAAI,OAAO,oBAAI,KAAK;AAChC,QAAM,OAAO,IAAI,cAAc,kBAAkB,GAAG;AACpD,QAAM,QAAQ,MAAM,YAAY,KAAK,aAAa,IAAI;AACtD,QAAM,YAAY,IAAI,YAAY;AAClC,QAAM,SACJ,MAAM,YAAY,MAAM,sBAAsB,QAAQ,MAAM,oBAAoB,KAC5E,SACA;AACN,QAAM,UAAU,CAAC,MAAM,WACnB,oBACA,MAAM,sBAAsB,OAC1B,iCACA,kBAAkB,MAAM,iBAAiB;AAC/C,SAAO;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,SAAS,EAAE,UAAU,MAAM,UAAU,mBAAmB,MAAM,mBAAmB,UAAU;AAAA,EAC7F;AACF;;;ACjGA,IAAM,cAAc;AAGb,SAAS,iBAAiB,KAAuB;AACtD,QAAM,MAAgB,CAAC;AACvB,QAAM,KAAK;AACX,MAAI;AACJ,UAAQ,IAAI,GAAG,KAAK,GAAG,OAAO,MAAM;AAClC,UAAM,MAAM,EAAE,CAAC;AACf,QAAI,IAAK,KAAI,KAAK,IAAI,KAAK,CAAC;AAAA,EAC9B;AACA,SAAO;AACT;AAIO,SAAS,eAAe,MAAc,SAA2B;AACtE,QAAM,MAAM,oBAAI,IAAY;AAC5B,QAAM,KAAK;AACX,MAAI;AACJ,UAAQ,IAAI,GAAG,KAAK,IAAI,OAAO,MAAM;AACnC,UAAM,OAAO,EAAE,CAAC;AAChB,QAAI,CAAC,QAAQ,KAAK,WAAW,GAAG,KAAK,+BAA+B,KAAK,IAAI,EAAG;AAChF,QAAI;AACF,YAAM,IAAI,IAAI,IAAI,MAAM,OAAO;AAC/B,UAAI,EAAE,WAAW,IAAI,IAAI,OAAO,EAAE,OAAQ;AAC1C,UAAI,IAAI,EAAE,QAAQ;AAAA,IACpB,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO,CAAC,GAAG,GAAG;AAChB;AAGA,SAAS,OAAO,UAA0B;AACxC,SAAO,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO,EAAE,CAAC,KAAK;AACnD;AAQO,SAAS,iBAAiB,aAAuB,MAAc,aAAuB;AAC3F,QAAM,OAAO,oBAAI,IAAY,CAAC,GAAG,CAAC;AAClC,QAAM,UAAU,oBAAI,IAAsB;AAC1C,aAAW,OAAO,aAAa;AAC7B,QAAI;AACJ,QAAI;AACF,iBAAW,IAAI,WAAW,GAAG,IACzB,IAAI,IAAI,KAAK,mBAAmB,EAAE,WAClC,IAAI,IAAI,GAAG,EAAE;AAAA,IACnB,QAAQ;AACN;AAAA,IACF;AACA,QAAI,aAAa,IAAK;AACtB,QAAI,KAAK,IAAI,QAAQ,EAAG;AACxB,SAAK,IAAI,QAAQ;AACjB,UAAM,MAAM,OAAO,QAAQ;AAC3B,UAAM,MAAM,QAAQ,IAAI,GAAG,KAAK,CAAC;AACjC,QAAI,KAAK,QAAQ;AACjB,YAAQ,IAAI,KAAK,GAAG;AAAA,EACtB;AACA,QAAM,SAAS,CAAC,GAAG;AACnB,QAAM,WAAW,CAAC,GAAG,QAAQ,OAAO,CAAC;AACrC,MAAI,QAAQ;AACZ,SAAO,OAAO,SAAS,OAAO,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,UAAU,KAAQ;AACpF,eAAW,OAAO,UAAU;AAC1B,UAAI,OAAO,UAAU,IAAK;AAC1B,YAAM,OAAO,IAAI,MAAM;AACvB,UAAI,KAAM,QAAO,KAAK,IAAI;AAAA,IAC5B;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,eAAe,OAAyC;AACtE,QAAM,SAAiC,CAAC;AACxC,aAAW,KAAK,OAAO;AACrB,UAAM,MAAM,MAAM,MAAM,MAAM,IAAI,OAAO,CAAC,CAAC;AAC3C,WAAO,GAAG,KAAK,OAAO,GAAG,KAAK,KAAK;AAAA,EACrC;AACA,SAAO;AACT;AAOA,eAAsB,eACpB,aACA,MACA,MAAc,aACa;AAC3B,QAAM,SAAS,IAAI,IAAI,WAAW,EAAE;AACpC,QAAM,MAAM,CAAC,UAAoB,MAAM,IAAI,CAAC,MAAM,IAAI,IAAI,GAAG,MAAM,EAAE,IAAI;AAEzE,QAAM,aAAa,MAAM,KAAK,UAAU,IAAI,IAAI,gBAAgB,MAAM,EAAE,IAAI;AAC5E,MAAI,YAAY;AACd,UAAM,OAAO,iBAAiB,UAAU;AACxC,QAAI,KAAK,SAAS,GAAG;AACnB,YAAM,QAAQ,iBAAiB,MAAM,GAAG;AACxC,aAAO,EAAE,QAAQ,IAAI,KAAK,GAAG,QAAQ,WAAW,cAAc,eAAe,KAAK,EAAE;AAAA,IACtF;AAAA,EACF;AAEA,QAAM,WAAW,MAAM,KAAK,UAAU,MAAM;AAC5C,MAAI,UAAU;AACZ,UAAM,QAAQ,eAAe,UAAU,MAAM;AAC7C,QAAI,MAAM,SAAS,GAAG;AACpB,YAAM,QAAQ,iBAAiB,OAAO,GAAG;AACzC,aAAO,EAAE,QAAQ,IAAI,KAAK,GAAG,QAAQ,kBAAkB,cAAc,eAAe,KAAK,EAAE;AAAA,IAC7F;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ,CAAC,IAAI,IAAI,KAAK,MAAM,EAAE,IAAI,GAAG,QAAQ,aAAa,cAAc,EAAE,KAAK,EAAE,EAAE;AAC9F;;;ACtGA,SAAS,SAAS,QAAgC;AAChD,SAAO,WAAW,QAAQ,UAAU;AACtC;AASO,SAAS,iBACd,QACA,OACA,cACgB;AAChB,QAAM,gBAAgB,OAAO,QAAQ,CAAC,MAAM,EAAE,OAAO;AACrD,QAAM,eAAe,OAAO,QAAQ,CAAC,MAAM,EAAE,MAAM;AAInD,QAAM,YACJ,OAAO,SAAS,KAAK,OAAO,MAAM,CAAC,MAAM,EAAE,QAAQ,SAAS,KAAK,EAAE,QAAQ,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;AAC/F,QAAM,WACJ,OAAO,SAAS,KAAK,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,SAAS,KAAK,EAAE,OAAO,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;AAC7F,QAAM,cAAc,MAAM,OAAO,CAAC,MAAM,SAAS,EAAE,MAAM,CAAC,EAAE;AAI5D,QAAM,UAAU,MAAM,SAAS,KAAK,gBAAgB;AAEpD,QAAM,UAAU,CAAC,GAAG,IAAI,IAAI,cAAc,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAC/D,QAAM,UAAU,CAAC,GAAG,IAAI,IAAI,aAAa,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAC9D,QAAM,WAAW,OAAO,QAAQ,YAAY,EACzC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAO,MAAM,MAAM,MAAM,GAAG,CAAC,QAAK,CAAC,EAAG,EAChD,KAAK,IAAI;AACZ,QAAM,OACJ,GAAG,OAAO,MAAM,YAAY,QAAQ,cACzB,QAAQ,KAAK,GAAG,KAAK,QAAG,YAAY,QAAQ,KAAK,GAAG,KAAK,QAAG,KACpE,MAAM,MAAM,WAAW,WAAW;AAEvC,SAAO,EAAE,WAAW,UAAU,SAAS,aAAa,eAAe,OAAO,QAAQ,KAAK;AACzF;AASA,eAAsB,aAAa,KAAyC;AAC1E,QAAM,EAAE,KAAK,IAAI;AACjB,QAAM,QAAQ,UAAU,IAAI;AAC5B,MAAI,CAAC,KAAK,aAAa;AACrB,WAAO,EAAE,OAAO,WAAW,MAAM,OAAO,QAAQ,QAAQ,SAAS,kBAAkB;AAAA,EACrF;AACA,QAAM,MAAM,IAAI,OAAO,oBAAI,KAAK;AAChC,QAAM,eAAe,IAAI,gBAAgB,oBAAoB;AAC7D,QAAM,SAAS,IAAI,iBAAkB,MAAM,qBAAqB;AAChE,MAAI;AACF,UAAM,aAA+B,MAAM,eAAe,KAAK,aAAa,YAAY;AACxF,UAAM,eAAe,MAAM,OAAO,MAAM,WAAW,MAAM;AACzD,UAAM,gBAAgB,CAAC,GAAG,IAAI,IAAI,aAAa,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AACvE,UAAM,cAAc,MAAM,OAAO,WAAW,aAAa;AACzD,UAAM,UAAU;AAAA,MACd;AAAA,MACA;AAAA,MACA,WAAW,gBAAgB,eAAe,WAAW,MAAM;AAAA,IAC7D;AACA,UAAM,SACJ,QAAQ,aAAa,QAAQ,YAAY,QAAQ,UAAU,SAAS;AACtE,WAAO;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,MACN;AAAA,MACA,SAAS,QAAQ;AAAA,MACjB,SAAS,EAAE,GAAG,SAAS,WAAW,IAAI,YAAY,EAAE;AAAA,IACtD;AAAA,EACF,UAAE;AACA,UAAM,OAAO,QAAQ;AAAA,EACvB;AACF;AAMA,IAAM,mBAAmB;AAGlB,SAAS,sBAAoC;AAClD,SAAO;AAAA,IACL,WAAW,OAAO,QAAQ;AACxB,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,KAAK;AAAA,UAC3B,UAAU;AAAA,UACV,QAAQ,YAAY,QAAQ,gBAAgB;AAAA,QAC9C,CAAC;AACD,YAAI,CAAC,IAAI,GAAI,QAAO;AACpB,eAAO,MAAM,IAAI,KAAK;AAAA,MACxB,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAM,mBAAmB,EAAE,OAAO,MAAM,QAAQ,IAAI;AACpD,IAAM,kBAAkB;AASxB,eAAsB,uBAA+C;AACnE,QAAM,EAAE,UAAU,SAAS,QAAQ,QAAQ,IAAI,MAAM,OAAO,kBAAkB;AAC9E,QAAM,iBAAiB;AAAA,IACrB,EAAE,QAAQ,YAAY,MAAM,SAAS;AAAA,IACrC,EAAE,QAAQ,WAAW,MAAM,QAAQ;AAAA,IACnC,EAAE,QAAQ,UAAU,MAAM,OAAO;AAAA,EACnC;AACA,QAAM,gBAAgB;AAAA,IACpB,EAAE,QAAQ,WAAW,YAAY,QAAQ,SAAS,EAAE;AAAA,IACpD,EAAE,QAAQ,aAAa,YAAY,QAAQ,WAAW,EAAE;AAAA,EAC1D;AAEA,SAAO;AAAA,IACL,MAAM,MAAM,MAAM;AAChB,YAAM,UAAyB,CAAC;AAChC,YAAM,WAAW,MAAM,QAAQ,IAAI,eAAe,IAAI,CAAC,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC;AAC7E,YAAM,iBAAiB,MAAM,QAAQ,IAAI,cAAc,IAAI,MAAM,SAAS,OAAO,CAAC,CAAC;AACnF,UAAI;AACF,mBAAW,OAAO,MAAM;AACtB,gBAAM,UAAkC,CAAC;AACzC,gBAAM,UAAU,oBAAI,IAAY;AAChC,mBAAS,IAAI,GAAG,IAAI,eAAe,QAAQ,KAAK;AAC9C,kBAAM,SAAS,eAAe,CAAC,EAAG;AAClC,kBAAM,UAAU,SAAS,CAAC;AAC1B,kBAAM,MAAM,MAAM,QAAQ,WAAW,EAAE,UAAU,iBAAiB,CAAC;AACnE,kBAAM,OAAO,MAAM,IAAI,QAAQ;AAC/B,kBAAM,SAAmB,CAAC;AAC1B,iBAAK,GAAG,aAAa,CAAC,MAAM,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC;AAClD,gBAAI,KAAK;AACT,gBAAI;AACF,oBAAM,OAAO,MAAM,KAAK,KAAK,KAAK;AAAA,gBAChC,WAAW;AAAA,gBACX,SAAS;AAAA,cACX,CAAC;AACD,oBAAM,UAAU,MAAM,KACnB,QAAQ,mBAAmB,EAC3B,MAAM,EACN,UAAU,EACV,MAAM,MAAM,KAAK;AACpB,mBAAK,CAAC,CAAC,QAAQ,KAAK,GAAG,KAAK,OAAO,WAAW,KAAK;AACnD,kBAAI,WAAW,YAAY;AAMzB,sBAAM,QAAS,MAAM,KAClB,SAAS,qEAAqE,EAC9E,MAAM,MAAM,CAAC,CAAC;AACjB,sBAAM,SAAS,IAAI,IAAI,GAAG,EAAE;AAC5B,2BAAW,KAAK,OAAO;AACrB,sBAAI;AACF,wBAAI,IAAI,IAAI,CAAC,EAAE,WAAW,OAAQ,SAAQ,IAAI,IAAI,IAAI,CAAC,EAAE,IAAI;AAAA,kBAC/D,QAAQ;AAAA,kBAER;AAAA,gBACF;AAAA,cACF;AAAA,YACF,QAAQ;AACN,mBAAK;AAAA,YACP,UAAE;AACA,oBAAM,IAAI,MAAM,EAAE,MAAM,MAAM;AAAA,cAAC,CAAC;AAAA,YAClC;AACA,oBAAQ,KAAK,EAAE,QAAQ,GAAG,CAAC;AAAA,UAC7B;AAEA,gBAAM,SAAgC,CAAC;AACvC,mBAAS,IAAI,GAAG,IAAI,cAAc,QAAQ,KAAK;AAC7C,kBAAM,EAAE,QAAQ,WAAW,IAAI,cAAc,CAAC;AAC9C,kBAAM,UAAU,eAAe,CAAC;AAChC,kBAAM,MAAM,MAAM,QAAQ,WAAW,EAAE,GAAG,WAAW,CAAC;AACtD,kBAAM,OAAO,MAAM,IAAI,QAAQ;AAC/B,kBAAM,SAAmB,CAAC;AAC1B,iBAAK,GAAG,aAAa,CAAC,MAAM,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC;AAClD,gBAAI,KAAK;AACT,gBAAI;AACF,oBAAM,OAAO,MAAM,KAAK,KAAK,KAAK;AAAA,gBAChC,WAAW;AAAA,gBACX,SAAS;AAAA,cACX,CAAC;AACD,oBAAM,WAAY,MAAM,KACrB,SAAS,8DAA8D,EACvE,MAAM,MAAM,IAAI;AACnB,mBAAK,CAAC,CAAC,QAAQ,KAAK,GAAG,KAAK,OAAO,WAAW,KAAK,CAAC;AAAA,YACtD,QAAQ;AACN,mBAAK;AAAA,YACP,UAAE;AACA,oBAAM,IAAI,MAAM,EAAE,MAAM,MAAM;AAAA,cAAC,CAAC;AAAA,YAClC;AACA,mBAAO,KAAK,EAAE,QAAQ,GAAG,CAAC;AAAA,UAC5B;AAEA,kBAAQ,KAAK,EAAE,KAAK,SAAS,QAAQ,OAAO,CAAC,GAAG,OAAO,EAAE,CAAC;AAAA,QAC5D;AAAA,MACF,UAAE;AACA,cAAM,QAAQ,IAAI,CAAC,GAAG,UAAU,GAAG,cAAc,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC,CAAC,CAAC;AAAA,MAC1F;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,WAAW,MAAM;AACrB,YAAM,MAAoB,CAAC;AAC3B,iBAAW,OAAO,MAAM;AACtB,YAAI;AACJ,YAAI;AACF,cAAI,MAAM,MAAM,MAAM,KAAK;AAAA,YACzB,QAAQ;AAAA,YACR,UAAU;AAAA,YACV,QAAQ,YAAY,QAAQ,gBAAgB;AAAA,UAC9C,CAAC;AAED,cAAI,IAAI,WAAW,OAAO,IAAI,WAAW,KAAK;AAC5C,kBAAM,MAAM,MAAM,KAAK;AAAA,cACrB,QAAQ;AAAA,cACR,UAAU;AAAA,cACV,QAAQ,YAAY,QAAQ,gBAAgB;AAAA,YAC9C,CAAC;AAAA,UACH;AACA,mBAAS,IAAI;AAAA,QACf,QAAQ;AACN,mBAAS;AAAA,QACX;AACA,YAAI,KAAK,EAAE,KAAK,OAAO,CAAC;AAAA,MAC1B;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;ACjRA,IAAM,WAA2E;AAAA,EAC/E,MAAM;AAAA,EACN,MAAM;AAAA,EACN,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AACX;AAEO,IAAM,kBAAkB,OAAO,KAAK,QAAQ;AAGnD,IAAM,2BAA2B;AAEjC,SAAS,WAAW,WAA4B;AAC9C,SAAO,CAAC,KAAK,MAAM,OAAO,CAAC,MACzB,aAAa,KAAK,MAAM,EAAE,GAAG,MAAM,WAAW,KAAK,aAAa,UAAU,CAAC;AAC/E;AAMA,eAAsB,YAAY,MAAY,MAAuC;AACnF,MAAI,EAAE,QAAQ,UAAW,OAAM,IAAI,MAAM,kBAAkB,IAAI,EAAE;AACjE,QAAM,QAAQ,WAAW,wBAAwB;AAIjD,QAAM,QAAQ,KAAK,QAAQ,KAAK;AAChC,MAAI;AACF,WAAO,MAAM,SAAS,IAAI,EAAE,EAAE,MAAM,MAAM,CAAC;AAAA,EAC7C,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS,GAAG,IAAI,6BAAwB,OAAO,GAAG,CAAC;AAAA,IACrD;AAAA,EACF;AACF;AAEA,eAAsB,UAAU,MAAY,OAA6C;AACvF,QAAM,QAAQ,SAAS;AACvB,aAAW,KAAK,OAAO;AACrB,QAAI,EAAE,KAAK,UAAW,OAAM,IAAI,MAAM,kBAAkB,CAAC,EAAE;AAAA,EAC7D;AACA,SAAO,QAAQ,IAAI,MAAM,IAAI,CAAC,MAAM,YAAY,MAAM,CAAC,CAAC,CAAC;AAC3D;AAEA,eAAsB,gBAAgB,OAAe,OAA6C;AAChG,QAAM,MAAM,MAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC;AACnE,SAAO,IAAI,KAAK;AAClB;","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/audits/a11y-airtable.ts","../src/audits/deps-airtable.ts","../src/audits/security-airtable.ts","../src/audits/domain-airtable.ts","../src/audits/browser-airtable.ts","../src/audits/write-audits-to-airtable.ts"],"sourcesContent":["import type { AuditResult } from \"../types.js\";\n\ntype A11yDetails = {\n totalViolations: number;\n byImpact: Partial<Record<\"minor\" | \"moderate\" | \"serious\" | \"critical\", number>>;\n};\n\n/** True when an a11y AuditResult carries real counts worth persisting.\n * Mirrors the `hasRealScores` policy on lighthouse: write whenever real\n * data exists, regardless of status (a \"warn\" or \"fail\" with concrete\n * violation counts is exactly what the dashboard needs to track). */\nexport function hasA11yCounts(result: AuditResult): boolean {\n if (result.audit !== \"a11y\") return false;\n const details = result.details as A11yDetails | undefined;\n return typeof details?.totalViolations === \"number\";\n}\n\nexport function a11yCountsFromResult(result: AuditResult): { violations: number } {\n if (result.audit !== \"a11y\") {\n throw new Error(`Expected an 'a11y' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as A11yDetails | undefined;\n return { violations: details?.totalViolations ?? 0 };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { DepsDetails, DepsDriftEntry } from \"./deps.js\";\n\n/** True when a deps AuditResult carries the structured details (an entries\n * array, per {@link DepsDetails}). */\nexport function hasDepsCounts(result: AuditResult): boolean {\n if (result.audit !== \"deps\") return false;\n const d = result.details as Partial<DepsDetails> | undefined;\n return Array.isArray(d?.entries);\n}\n\nexport function depsCountsFromResult(result: AuditResult): {\n drifted: number;\n majorBehind: number;\n outdated: number | null;\n} {\n if (result.audit !== \"deps\") {\n throw new Error(`Expected a 'deps' AuditResult, got '${result.audit}'`);\n }\n const details = (result.details ?? {}) as Partial<DepsDetails>;\n const entries = (details.entries ?? []) as DepsDriftEntry[];\n // \"drifted\" = drift !== \"same\", which intentionally includes \"newer\" (ahead of\n // baseline) — parity with the deps audit summary text.\n const drifted = entries.filter((e) => e.drift !== \"same\").length;\n const majorBehind = entries.filter((e) => e.drift === \"major\").length;\n // The real installed-version drift, distinct from declared-range drift. Null\n // when the audit couldn't determine it (no/stale lockfile) — kept null (not 0)\n // so the dashboard shows \"—\" rather than a misleading \"clean\".\n const outdated = details.outdated?.outdated ?? null;\n return { drifted, majorBehind, outdated };\n}\n","import type { AuditResult } from \"../types.js\";\nimport { normalizeSecurityAdvisory, type SecurityAdvisory } from \"../reports/airtable/websites.js\";\n\ntype SecurityDetails = {\n counts: { low: number; moderate: number; high: number; critical: number };\n advisories?: unknown;\n};\n\n/** True when a security AuditResult carries a counts object. Skipped runs\n * (no pnpm + no npm) have no details and return false. */\nexport function hasSecurityCounts(result: AuditResult): boolean {\n if (result.audit !== \"security\") return false;\n const details = result.details as SecurityDetails | undefined;\n return !!details && typeof details.counts === \"object\";\n}\n\nexport function securityCountsFromResult(result: AuditResult): {\n critical: number;\n high: number;\n moderate: number;\n low: number;\n} {\n if (result.audit !== \"security\") {\n throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as SecurityDetails | undefined;\n const c = details?.counts ?? { low: 0, moderate: 0, high: 0, critical: 0 };\n return { critical: c.critical, high: c.high, moderate: c.moderate, low: c.low };\n}\n\n/** The known advisories behind the counts, normalized for persistence. Empty when the\n * run was clean or carried no advisory detail (npm/pnpm sometimes omit it). Malformed\n * entries are dropped via the shared `normalizeSecurityAdvisory`. */\nexport function advisoriesFromResult(result: AuditResult): SecurityAdvisory[] {\n if (result.audit !== \"security\") {\n throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as SecurityDetails | undefined;\n const raw = details?.advisories;\n if (!Array.isArray(raw)) return [];\n return raw.map(normalizeSecurityAdvisory).filter((a): a is SecurityAdvisory => a !== null);\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { DomainResult } from \"../reports/airtable/websites.js\";\n\ntype DomainDetails = { resolved: boolean; certDaysRemaining: number | null; checkedAt: string };\n\n/** True when an AuditResult is a `domain` audit carrying a usable details payload (i.e. it\n * actually ran — a \"skip\" for a site with no deployed URL has no details). */\nexport function hasDomainResult(result: AuditResult): boolean {\n if (result.audit !== \"domain\") return false;\n const d = result.details as DomainDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable domain result (cert days + checked-at) from a `domain` audit. */\nexport function domainResultFromAudit(result: AuditResult): DomainResult {\n if (result.audit !== \"domain\") {\n throw new Error(`Expected a 'domain' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as DomainDetails | undefined;\n return {\n certDaysRemaining: typeof d?.certDaysRemaining === \"number\" ? d.certDaysRemaining : null,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { BrowserAuditFields } from \"../reports/airtable/websites.js\";\n\ntype BrowserDetails = {\n desktopOk: boolean;\n mobileOk: boolean;\n linksOk: boolean;\n brokenLinks: number;\n checkedAt: string;\n};\n\n/** True when an AuditResult is a `browser` audit that actually ran (has a details payload — a\n * \"skip\" for a site with no deployed URL has none). */\nexport function hasBrowserResult(result: AuditResult): boolean {\n if (result.audit !== \"browser\") return false;\n const d = result.details as BrowserDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable browser verdicts from a `browser` audit. */\nexport function browserFieldsFromAudit(result: AuditResult): BrowserAuditFields {\n if (result.audit !== \"browser\") {\n throw new Error(`Expected a 'browser' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as BrowserDetails | undefined;\n return {\n desktopOk: d?.desktopOk === true,\n mobileOk: d?.mobileOk === true,\n linksOk: d?.linksOk === true,\n brokenLinks: typeof d?.brokenLinks === \"number\" ? d.brokenLinks : 0,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport { type WebsiteRow, siteSlug, updateAuditFields } from \"../reports/airtable/websites.js\";\nimport type {\n A11yCounts,\n DepsCounts,\n SecurityCounts,\n SecurityAdvisory,\n DomainResult,\n BrowserAuditFields,\n} from \"../reports/airtable/websites.js\";\nimport type { LighthouseScores } from \"../reports/types.js\";\nimport { hasRealScores, lighthouseScoresFromResult } from \"./lighthouse-airtable.js\";\nimport { hasA11yCounts, a11yCountsFromResult } from \"./a11y-airtable.js\";\nimport { hasDepsCounts, depsCountsFromResult } from \"./deps-airtable.js\";\nimport {\n hasSecurityCounts,\n securityCountsFromResult,\n advisoriesFromResult,\n} from \"./security-airtable.js\";\nimport { hasDomainResult, domainResultFromAudit } from \"./domain-airtable.js\";\nimport { hasBrowserResult, browserFieldsFromAudit } from \"./browser-airtable.js\";\n\ntype WriteSummary = {\n siteName: string;\n writes: Array<{\n audit: \"lighthouse\" | \"a11y\" | \"deps\" | \"security\" | \"github-signals\" | \"domain\" | \"browser\";\n counts: object;\n }>;\n};\n\n/** Orchestrates the per-audit Airtable writes for `audit --write-airtable`.\n * Extracted from the CLI command so it can be unit-tested with a fake base\n * and so adding new audit types is a one-line addition here rather than\n * growing the CLI handler.\n *\n * Throws (with .exitCode set) on the failure modes the CLI surfaces today:\n * - 2: --only ran without lighthouse, or no Websites row matched the slug\n * - 1: lighthouse ran but produced no real scores (infrastructure failure).\n * The a11y/deps/security writes still complete FIRST — a Lighthouse\n * miss flags the site without discarding its other audit data.\n *\n * Precedence note: the Websites-row lookup (exitCode 2) is checked BEFORE the\n * no-scores gate (exitCode 1), so the rare no-row + no-scores combo surfaces\n * as exitCode 2. */\nexport async function writeAuditsToAirtable(args: {\n base: AirtableBase;\n websites: WebsiteRow[];\n slug: string;\n results: AuditResult[];\n}): Promise<WriteSummary> {\n const { base, websites, slug, results } = args;\n\n // Lighthouse is OPTIONAL. The checkout-free `--only lighthouse,domain,browser` nightly includes\n // it, but a standalone `--only security` (checkout-ful) sweep legitimately has none. We write\n // whatever audits produced values; only the lighthouse-MISS flag below is lighthouse-specific\n // (so a real Lighthouse infra failure still reds the run without discarding other audit data).\n const lhResult = results.find((r) => r.audit === \"lighthouse\");\n const target = websites.find((w) => siteSlug(w.name) === slug);\n if (!target) {\n throw Object.assign(new Error(`No Websites row matched slug \"${slug}\"`), { exitCode: 2 });\n }\n\n const writes: WriteSummary[\"writes\"] = [];\n const audits: {\n scores?: LighthouseScores;\n a11y?: A11yCounts;\n deps?: DepsCounts;\n security?: SecurityCounts;\n securityAdvisories?: SecurityAdvisory[];\n domain?: DomainResult;\n browser?: BrowserAuditFields;\n } = {};\n\n // Collect every audit that produced real values into ONE merged input, then do a\n // SINGLE atomic Airtable update (was: up to four sequential updates on the same\n // row — a mid-sequence failure left it half-written yet reported fully failed, at\n // 4× the request volume). Lighthouse is the most timeout-prone audit: a Lighthouse\n // miss must NOT discard the site's valid a11y/deps/security results (morning-brief\n // 2026-06-10 MEDIUM-E). So include Lighthouse scores only when real, include the\n // other audits whenever present, write all of them in one update, then — if\n // Lighthouse missed — throw AFTER that atomic write so the site is still flagged\n // (exitCode 1 / collected in FleetWriteResult.failed) without losing its other data.\n const lhHasScores = lhResult ? hasRealScores(lhResult) : false;\n if (lhResult && lhHasScores) {\n const scores = lighthouseScoresFromResult(lhResult);\n audits.scores = scores;\n writes.push({ audit: \"lighthouse\", counts: scores });\n }\n\n const a11y = results.find((r) => r.audit === \"a11y\");\n if (a11y && hasA11yCounts(a11y)) {\n const counts = a11yCountsFromResult(a11y);\n audits.a11y = counts;\n writes.push({ audit: \"a11y\", counts });\n }\n\n const deps = results.find((r) => r.audit === \"deps\");\n if (deps && hasDepsCounts(deps)) {\n const counts = depsCountsFromResult(deps);\n audits.deps = counts;\n writes.push({ audit: \"deps\", counts });\n }\n\n const sec = results.find((r) => r.audit === \"security\");\n if (sec && hasSecurityCounts(sec)) {\n const counts = securityCountsFromResult(sec);\n audits.security = counts;\n // Persist the advisory list alongside the counts so the dashboard can show which packages\n // are vulnerable. An empty array (clean run) clears any stale list.\n audits.securityAdvisories = advisoriesFromResult(sec);\n writes.push({ audit: \"security\", counts });\n }\n\n const dom = results.find((r) => r.audit === \"domain\");\n if (dom && hasDomainResult(dom)) {\n const result = domainResultFromAudit(dom);\n audits.domain = result;\n writes.push({ audit: \"domain\", counts: result });\n }\n\n const browser = results.find((r) => r.audit === \"browser\");\n if (browser && hasBrowserResult(browser)) {\n const fields = browserFieldsFromAudit(browser);\n audits.browser = fields;\n writes.push({ audit: \"browser\", counts: fields });\n }\n\n // One atomic write of everything that ran. Skip the call only if there is nothing\n // to write at all (no real scores AND no other audit produced values) — an empty\n // update is a wasted request.\n if (Object.keys(audits).length > 0) {\n await updateAuditFields(base, target.id, audits);\n }\n\n // Lighthouse-miss flag: only when lighthouse WAS requested (in results) but produced no scores —\n // an infra failure worth reding the run, AFTER persisting the other audits. A sweep that never\n // ran lighthouse (e.g. `--only security`) skips this entirely.\n if (lhResult && !lhHasScores) {\n // Enumerate what WAS persisted so the failure (surfaced to the single-site\n // CLI operator via console.error) reads as a partial write, not a total one.\n const persisted = writes.map((w) => w.audit);\n throw Object.assign(\n new Error(\n `Lighthouse audit produced no scores; ${\n persisted.length ? `wrote ${persisted.join(\"/\")} but refused Lighthouse` : \"wrote nothing\"\n }. Summary: ${lhResult.summary}`,\n ),\n { exitCode: 1 },\n );\n }\n\n return { siteName: target.name, writes };\n}\n\nexport type FleetWriteResult = {\n written: WriteSummary[];\n failed: Array<{ slug: string; error: string }>;\n};\n\n/** Render the fleet write-back outcome for the CLI/CI. Beyond the human-readable\n * lines, it emits a single deterministic, machine-parseable line —\n * `FLEET_WRITE_SUMMARY wrote=N failed=M total=T` — that the nightly workflow\n * greps to decide pass/fail. Keying CI on this line (not the prose, and not a\n * \"wrote ≥ 1\" heuristic) lets the gate tolerate a single known flake while\n * still reding on a total or mass write-back failure. */\nexport function formatFleetWriteSummary(result: FleetWriteResult): string {\n const wrote = result.written.length;\n const failed = result.failed.length;\n const total = wrote + failed;\n let out = `→ wrote ${wrote} site(s) to Airtable`;\n if (failed > 0) {\n out += `\\n⚠ ${failed} site(s) not written: ${result.failed\n .map((f) => `${f.slug} (${f.error})`)\n .join(\"; \")}`;\n }\n out += `\\nFLEET_WRITE_SUMMARY wrote=${wrote} failed=${failed} total=${total}`;\n return out;\n}\n\n/** Write each site's pooled audit results back to its own Websites row,\n * best-effort. Results are grouped by `result.site` (the slug the fleet\n * inventory stamped as Site.name). A per-site failure (no scores, no matching\n * row) is collected — not thrown — so one bad site never aborts the batch. */\nexport async function writeFleetAuditsToAirtable(args: {\n base: AirtableBase;\n websites: WebsiteRow[];\n results: AuditResult[];\n}): Promise<FleetWriteResult> {\n const { base, websites, results } = args;\n\n const bySlug = new Map<string, AuditResult[]>();\n for (const r of results) {\n const arr = bySlug.get(r.site) ?? [];\n arr.push(r);\n bySlug.set(r.site, arr);\n }\n\n const written: WriteSummary[] = [];\n const failed: FleetWriteResult[\"failed\"] = [];\n // Serial on purpose: even at one (now atomic) update call per site, Airtable's\n // ~5 req/sec limit means a Promise.all fan-out across the fleet would burst and\n // trip 429s (silently filed as failures). Below a few dozen sites, serial trades\n // wall-clock for safety. (morning-brief 2026-06-09 MEDIUM-3.) Add a bounded pool\n // when the fleet grows.\n for (const [slug, siteResults] of bySlug) {\n try {\n written.push(await writeAuditsToAirtable({ base, websites, slug, results: siteResults }));\n } catch (e) {\n failed.push({ slug, error: (e as Error).message });\n }\n }\n return { written, failed };\n}\n"],"mappings":"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","names":[]}