@reddoorla/maintenance 0.85.0 → 0.85.2

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 (37) hide show
  1. package/dist/chunk-5ML7FMBD.js +10 -0
  2. package/dist/chunk-5ML7FMBD.js.map +1 -0
  3. package/dist/{chunk-N4MOEF32.js → chunk-C6B4RHXR.js} +36 -8
  4. package/dist/chunk-C6B4RHXR.js.map +1 -0
  5. package/dist/{chunk-QRDB4NLB.js → chunk-DTTIFYAM.js} +2 -2
  6. package/dist/{chunk-R4AB6GCT.js → chunk-S56JQJ4F.js} +5 -2
  7. package/dist/{chunk-R4AB6GCT.js.map → chunk-S56JQJ4F.js.map} +1 -1
  8. package/dist/{chunk-6V7565O2.js → chunk-WYYQK4D3.js} +3 -3
  9. package/dist/cli/bin.js +6 -6
  10. package/dist/cli/commands/audit.js +2 -2
  11. package/dist/configs/playwright-a11y.js +1 -1
  12. package/dist/configs/svelte.d.ts +30 -1
  13. package/dist/configs/svelte.js +9 -1
  14. package/dist/configs/svelte.js.map +1 -1
  15. package/dist/{digest-SQ5U6SN2.js → digest-UVHYZUYE.js} +5 -3
  16. package/dist/digest-UVHYZUYE.js.map +1 -0
  17. package/dist/{forms-notify-target-SMKDKNT2.js → forms-notify-target-TWN4EH6K.js} +4 -5
  18. package/dist/forms-notify-target-TWN4EH6K.js.map +1 -0
  19. package/dist/index.js +4 -4
  20. package/dist/{init-TFVAN5OY.js → init-JN26NTWF.js} +5 -5
  21. package/dist/{launch-DWY2MF54.js → launch-JA7Z4CIM.js} +3 -3
  22. package/dist/{report-WG6RHA2X.js → report-6CKSWJNG.js} +6 -3
  23. package/dist/report-6CKSWJNG.js.map +1 -0
  24. package/dist/{selftest-BVCLJRNI.js → selftest-CQ6NOX52.js} +5 -2
  25. package/dist/selftest-CQ6NOX52.js.map +1 -0
  26. package/dist/{smoke-suite-OBWNPALQ.js → smoke-suite-TMCSOYRK.js} +2 -2
  27. package/package.json +1 -1
  28. package/dist/chunk-N4MOEF32.js.map +0 -1
  29. package/dist/digest-SQ5U6SN2.js.map +0 -1
  30. package/dist/forms-notify-target-SMKDKNT2.js.map +0 -1
  31. package/dist/report-WG6RHA2X.js.map +0 -1
  32. package/dist/selftest-BVCLJRNI.js.map +0 -1
  33. /package/dist/{chunk-QRDB4NLB.js.map → chunk-DTTIFYAM.js.map} +0 -0
  34. /package/dist/{chunk-6V7565O2.js.map → chunk-WYYQK4D3.js.map} +0 -0
  35. /package/dist/{init-TFVAN5OY.js.map → init-JN26NTWF.js.map} +0 -0
  36. /package/dist/{launch-DWY2MF54.js.map → launch-JA7Z4CIM.js.map} +0 -0
  37. /package/dist/{smoke-suite-OBWNPALQ.js.map → smoke-suite-TMCSOYRK.js.map} +0 -0
@@ -0,0 +1,10 @@
1
+ // src/util/operator.ts
2
+ var OPERATOR_FALLBACK = "tucker@reddoorla.com";
3
+ function operatorEmail() {
4
+ return process.env.OPERATOR_EMAIL?.trim() || OPERATOR_FALLBACK;
5
+ }
6
+
7
+ export {
8
+ operatorEmail
9
+ };
10
+ //# sourceMappingURL=chunk-5ML7FMBD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/operator.ts"],"sourcesContent":["/**\n * The operator inbox for INTERNAL fleet mail — the daily digest, the fleet\n * analytics-failure alert, selftest sends, and the pre-launch lead guard.\n *\n * Deliberately the monitored personal inbox, never `info@reddoorla.com`: that\n * alias is the client-facing shared inbox other staff read and reply to clients\n * from. Internal mail landing there is both noise for them and a silent miss for\n * the operator.\n *\n * This exists because the fallback used to be spelled out at each call site, and\n * they disagreed: the lead guard already used the personal inbox while the\n * digest, the analytics alert, and selftest all used `info@`. On 2026-08-17 the\n * scheduled daily-reports run failed before its digest step, a recovery run of\n * `report --digest` went out from a laptop where OPERATOR_EMAIL was unset, and\n * the fleet digest landed in the client inbox — where a colleague found it and\n * forwarded it on. `OPERATOR_EMAIL` was set as a GitHub Actions repo variable\n * and nowhere else, so CI was correct and every local run was not.\n *\n * One definition, so a missing env var degrades to a watched inbox everywhere.\n */\nexport const OPERATOR_FALLBACK = \"tucker@reddoorla.com\";\n\n/** `OPERATOR_EMAIL` when set and non-blank, else {@link OPERATOR_FALLBACK}.\n * Read at CALL time, not module load, so a process that sets the variable late\n * (and the tests) see the current value. */\nexport function operatorEmail(): string {\n return process.env.OPERATOR_EMAIL?.trim() || OPERATOR_FALLBACK;\n}\n"],"mappings":";AAoBO,IAAM,oBAAoB;AAK1B,SAAS,gBAAwB;AACtC,SAAO,QAAQ,IAAI,gBAAgB,KAAK,KAAK;AAC/C;","names":[]}
@@ -1,4 +1,5 @@
1
1
  // src/configs/playwright-a11y.ts
2
+ import { execFileSync } from "child_process";
2
3
  import { defineConfig, devices } from "@playwright/test";
3
4
  var a11yRoutes = [
4
5
  { path: "/dev/a11y-fixtures", name: "a11y fixtures" },
@@ -6,7 +7,22 @@ var a11yRoutes = [
6
7
  ];
7
8
  var smokeRoutes = [{ path: "/", name: "home" }];
8
9
  var smokePort = process.env.REDDOOR_SMOKE_PORT;
9
- var port = smokePort || "5173";
10
+ function allocateFreePortSync() {
11
+ try {
12
+ const out = execFileSync(
13
+ process.execPath,
14
+ [
15
+ "-e",
16
+ 'const s=require("node:net").createServer();s.on("error",()=>process.exit(1));s.listen(0,"127.0.0.1",()=>{const p=s.address().port;s.close(()=>process.stdout.write(String(p)))});'
17
+ ],
18
+ { encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
19
+ ).trim();
20
+ return /^\d+$/.test(out) ? out : null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ var port = smokePort || allocateFreePortSync() || "5173";
10
26
  var playwrightA11yConfig = defineConfig({
11
27
  testDir: "tests",
12
28
  testMatch: /.*\.spec\.ts$/,
@@ -41,14 +57,26 @@ var playwrightA11yConfig = defineConfig({
41
57
  // nothing, naming neither the port nor the squatter. With --strictPort it
42
58
  // is an immediate "Port 5173 is already in use".
43
59
  //
44
- // This does NOT overlap with `reuseExistingServer`: that check runs first,
45
- // so a dev server already serving the probe URL is still reused and the
46
- // command never executes. --strictPort only bites when 5173 is held by
47
- // something that is not the server under test, which is exactly the case
48
- // worth failing on.
60
+ // --strictPort now only bites if the allocated port is taken in the window
61
+ // between releasing and binding it, which is exactly the case worth failing
62
+ // on.
49
63
  command: `npm run vite:dev -- --port ${port} --strictPort`,
50
64
  url: `http://localhost:${port}/dev/a11y-fixtures`,
51
- reuseExistingServer: !process.env.CI,
65
+ // NEVER reuse (#524). This used to be `!process.env.CI`, so local runs
66
+ // reused whatever answered the probe URL. The probe only asks "does this
67
+ // respond?" — never "is this serving the code I am about to test?" — so a
68
+ // dev server left open, or one whose tree changed under it after a
69
+ // checkout, silently became the system under test. That fails in both
70
+ // directions: a false red blamed on the code (beachfront 2026-08-12, where
71
+ // it was investigated as a macOS-vs-Linux difference and written up as one
72
+ // before being caught), and a false green where a passing suite ran against
73
+ // an old build. CI already had it false, and that asymmetry is precisely
74
+ // what made the failure read as a platform bug.
75
+ //
76
+ // The cost is a fresh vite boot per run (~10-20s against a ~2min suite).
77
+ // Because the port above is allocated rather than fixed, your own dev
78
+ // server on 5173 keeps running untouched.
79
+ reuseExistingServer: false,
52
80
  timeout: 12e4
53
81
  }
54
82
  });
@@ -59,4 +87,4 @@ export {
59
87
  smokeRoutes,
60
88
  playwright_a11y_default
61
89
  };
62
- //# sourceMappingURL=chunk-N4MOEF32.js.map
90
+ //# sourceMappingURL=chunk-C6B4RHXR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/configs/playwright-a11y.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { defineConfig, devices, type PlaywrightTestConfig } from \"@playwright/test\";\n\nexport type A11yRoute = { path: string; name: string };\n\nexport const a11yRoutes: A11yRoute[] = [\n { path: \"/dev/a11y-fixtures\", name: \"a11y fixtures\" },\n { path: \"/dev/animate-in\", name: \"animate-in demo\" },\n];\n\n// Routes smoke-loaded for client-side (hydration) errors only — NOT axe-scanned.\n// Catches the class of bug where build + SSR succeed but client hydration throws\n// and blanks the page (data-dynamiq 2026-06-09: a Svelte 4->5 `run()` referenced\n// a `$state` declared after it → TDZ ReferenceError on hydrate). `/` is the one\n// route every site has; real routes carry a11y debt we don't gate on here, so we\n// assert only that they don't crash on hydrate.\nexport const smokeRoutes: A11yRoute[] = [{ path: \"/\", name: \"home\" }];\n\n// R1.1 (health-gate): the central `smoke` audit (src/audits/smoke.ts) allocates\n// a free port and passes it as REDDOOR_SMOKE_PORT so a zombie vite already\n// squatting the default 5173 can't silently hijack the run and green a stale\n// build. The per-site R1.1 config template honors it, but sites whose\n// playwright.config.ts merely re-exports this shared base (pre-R1.1 adopters\n// the smoke-suite recipe flags-but-never-rewrites) would otherwise ignore it —\n// so honor it here too and every re-exporter inherits the port binding on its\n// next package bump. Unset (local `pnpm test:smoke`) → the fixed 5173.\nconst smokePort = process.env.REDDOOR_SMOKE_PORT;\n\n/**\n * Allocate a free port SYNCHRONOUSLY, for the local path where nothing handed\n * us one. Same trick as src/util/free-port.ts (bind :0, read the assigned port,\n * release it) — but that is async, and this value is needed at module scope\n * while Playwright is still building the config object.\n *\n * It cannot be an async default export instead: sites consume this base by\n * SPREADING it (`{ ...base, use: { ...base.use } }` — see the smoke-suite\n * recipe template). Spreading a Promise yields none of its properties, so the\n * site would get a silently empty config — the exact false-green this whole\n * change exists to remove. The export must stay a plain object.\n *\n * A subprocess is the cost of that constraint: ~30-50ms, once per Playwright\n * run. On any failure we return null and the caller falls back to 5173, which\n * (with reuseExistingServer now false) degrades to a loud \"port already in use\"\n * rather than a silent wrong-server run.\n */\nfunction allocateFreePortSync(): string | null {\n try {\n const out = execFileSync(\n process.execPath,\n [\n \"-e\",\n 'const s=require(\"node:net\").createServer();s.on(\"error\",()=>process.exit(1));' +\n 's.listen(0,\"127.0.0.1\",()=>{const p=s.address().port;s.close(()=>process.stdout.write(String(p)))});',\n ],\n { encoding: \"utf8\", timeout: 5_000, stdio: [\"ignore\", \"pipe\", \"ignore\"] },\n ).trim();\n return /^\\d+$/.test(out) ? out : null;\n } catch {\n return null;\n }\n}\n\n// REDDOOR_SMOKE_PORT (the central audit already allocated one) wins; otherwise\n// this run allocates its own. The old fallback was the fixed 5173 — the same\n// port a dev server sits on, which is what let `reuseExistingServer` silently\n// hijack local runs (#524).\nconst port = smokePort || allocateFreePortSync() || \"5173\";\n\n// NOTE: default export only — sites consume this as `import base from\n// \"@reddoorla/maintenance/configs/playwright-a11y\"` (or re-export the default).\n// The old `playwrightA11yConfig` named alias had zero importers and was removed.\nconst playwrightA11yConfig: PlaywrightTestConfig = defineConfig({\n testDir: \"tests\",\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 projects: [\n {\n name: \"chromium\",\n use: { ...devices[\"Desktop Chrome\"] },\n },\n ],\n webServer: {\n // Portable across pnpm and npm sites — pnpm respects `npm run` too.\n //\n // `--port ... --strictPort` in BOTH cases. It used to be applied only when\n // REDDOOR_SMOKE_PORT allocated one, on the reasoning that we should \"fail\n // loudly rather than let vite drift to a free port the baseURL doesn't\n // point at\" — but that argument covers the unset case just as well. 5173 is\n // equally a fixed port that `baseURL` and the readiness probe below are\n // pinned to, and vite left to itself drifts off it whenever something else\n // holds it.\n //\n // The symptom that exposed this: a non-vite process on 5173 sends vite to\n // 5174 while the probe keeps polling 5173, so the run dies on\n // \"Timed out waiting 120000ms from config.webServer\" — 120 seconds of\n // nothing, naming neither the port nor the squatter. With --strictPort it\n // is an immediate \"Port 5173 is already in use\".\n //\n // --strictPort now only bites if the allocated port is taken in the window\n // between releasing and binding it, which is exactly the case worth failing\n // on.\n command: `npm run vite:dev -- --port ${port} --strictPort`,\n url: `http://localhost:${port}/dev/a11y-fixtures`,\n // NEVER reuse (#524). This used to be `!process.env.CI`, so local runs\n // reused whatever answered the probe URL. The probe only asks \"does this\n // respond?\" — never \"is this serving the code I am about to test?\" — so a\n // dev server left open, or one whose tree changed under it after a\n // checkout, silently became the system under test. That fails in both\n // directions: a false red blamed on the code (beachfront 2026-08-12, where\n // it was investigated as a macOS-vs-Linux difference and written up as one\n // before being caught), and a false green where a passing suite ran against\n // an old build. CI already had it false, and that asymmetry is precisely\n // what made the failure read as a platform bug.\n //\n // The cost is a fresh vite boot per run (~10-20s against a ~2min suite).\n // Because the port above is allocated rather than fixed, your own dev\n // server on 5173 keeps running untouched.\n reuseExistingServer: false,\n timeout: 120_000,\n },\n});\n\nexport default playwrightA11yConfig;\n"],"mappings":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,cAAc,eAA0C;AAI1D,IAAM,aAA0B;AAAA,EACrC,EAAE,MAAM,sBAAsB,MAAM,gBAAgB;AAAA,EACpD,EAAE,MAAM,mBAAmB,MAAM,kBAAkB;AACrD;AAQO,IAAM,cAA2B,CAAC,EAAE,MAAM,KAAK,MAAM,OAAO,CAAC;AAUpE,IAAM,YAAY,QAAQ,IAAI;AAmB9B,SAAS,uBAAsC;AAC7C,MAAI;AACF,UAAM,MAAM;AAAA,MACV,QAAQ;AAAA,MACR;AAAA,QACE;AAAA,QACA;AAAA,MAEF;AAAA,MACA,EAAE,UAAU,QAAQ,SAAS,KAAO,OAAO,CAAC,UAAU,QAAQ,QAAQ,EAAE;AAAA,IAC1E,EAAE,KAAK;AACP,WAAO,QAAQ,KAAK,GAAG,IAAI,MAAM;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,IAAM,OAAO,aAAa,qBAAqB,KAAK;AAKpD,IAAM,uBAA6C,aAAa;AAAA,EAC9D,SAAS;AAAA,EACT,WAAW;AAAA,EACX,eAAe;AAAA,EACf,YAAY,CAAC,CAAC,QAAQ,IAAI;AAAA,EAC1B,SAAS,QAAQ,IAAI,KAAK,IAAI;AAAA,EAC9B,UAAU,QAAQ,IAAI,KAAK,WAAW;AAAA,EACtC,KAAK;AAAA,IACH,SAAS,oBAAoB,IAAI;AAAA,IACjC,OAAO;AAAA,EACT;AAAA,EACA,UAAU;AAAA,IACR;AAAA,MACE,MAAM;AAAA,MACN,KAAK,EAAE,GAAG,QAAQ,gBAAgB,EAAE;AAAA,IACtC;AAAA,EACF;AAAA,EACA,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAoBT,SAAS,8BAA8B,IAAI;AAAA,IAC3C,KAAK,oBAAoB,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAe7B,qBAAqB;AAAA,IACrB,SAAS;AAAA,EACX;AACF,CAAC;AAED,IAAO,0BAAQ;","names":[]}
@@ -22,7 +22,7 @@ import {
22
22
  import {
23
23
  a11yRoutes,
24
24
  smokeRoutes
25
- } from "./chunk-N4MOEF32.js";
25
+ } from "./chunk-C6B4RHXR.js";
26
26
 
27
27
  // src/audits/deps.ts
28
28
  import { readFile } from "fs/promises";
@@ -2263,4 +2263,4 @@ export {
2263
2263
  runAudits,
2264
2264
  runAuditsAcross
2265
2265
  };
2266
- //# sourceMappingURL=chunk-QRDB4NLB.js.map
2266
+ //# sourceMappingURL=chunk-DTTIFYAM.js.map
@@ -144,7 +144,10 @@ export default defineConfig({
144
144
  webServer: {
145
145
  command: \`npm run vite:dev -- --port \${smokePort} --strictPort\`,
146
146
  url: \`http://localhost:\${smokePort}/dev/a11y-fixtures\`,
147
- reuseExistingServer: !process.env.CI,
147
+ // Never reuse: the readiness probe cannot tell the server under test
148
+ // from any other server answering that URL. The shared base does the
149
+ // same, and allocates its own port when REDDOOR_SMOKE_PORT is unset.
150
+ reuseExistingServer: false,
148
151
  timeout: 120_000,
149
152
  },
150
153
  }
@@ -312,4 +315,4 @@ async function smokeSuite(site, deps = { spawn: defaultSpawn }) {
312
315
  export {
313
316
  smokeSuite
314
317
  };
315
- //# sourceMappingURL=chunk-R4AB6GCT.js.map
318
+ //# sourceMappingURL=chunk-S56JQJ4F.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/recipes/smoke-suite/index.ts","../src/recipes/smoke-suite/template.ts"],"sourcesContent":["import { access, mkdir, readdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { withRecipe } from \"../_with-recipe.js\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\nimport { formatWithPrettier, PRETTIER_FLAG_NOTE } from \"../_prettier.js\";\nimport {\n SMOKE_ROUTES_RELATIVE,\n SMOKE_ROUTES_TEMPLATE,\n SMOKE_SPEC_RELATIVE,\n SMOKE_SPEC_TEMPLATE,\n PLAYWRIGHT_CONFIG_RELATIVE,\n PLAYWRIGHT_CONFIG_TEMPLATE,\n PLAYWRIGHT_CONFIG_PRE_R11,\n} from \"./template.js\";\n\nexport type SmokeSuiteDeps = { spawn: SpawnFn };\n\ntype PackageJson = {\n scripts?: Record<string, string>;\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n};\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function readIfExists(path: string): Promise<string | null> {\n try {\n return await readFile(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\n/** The template sentence explaining the default `footer` marker — swapped for a\n * fallback explanation when the marker deviates. Must match template.ts. */\nconst FOOTER_MARKER_SENTENCE =\n \"The hydration marker `footer` is the shared\\n// layout footer, present on every page including the error page.\";\n\nfunction fallbackMarkerSentence(marker: string): string {\n return (\n `The hydration marker \\`${marker}\\` is a\\n` +\n \"// fallback: no <footer> element exists in this site's Svelte source. Add a\\n\" +\n \"// semantic <footer> landmark and point the marker back at it when possible.\"\n );\n}\n\n/** The starter's default `footer` hydration marker needs the element to exist,\n * or EVERY route check false-fails (la-homelessness-initiative red'd the first\n * fleet-smoke run exactly this way). Deviate only on positive evidence of a\n * bespoke build: svelte files exist under src/ and none renders a literal\n * lowercase `<footer` element (a capital-F `<Footer` component tag proves\n * nothing — the element the browser paints lives inside that component). No\n * svelte files at all → no signal → keep the starter default. */\nasync function detectHydrationMarker(cwd: string): Promise<\"footer\" | \"main\" | \"body\"> {\n let entries: string[];\n try {\n entries = (await readdir(join(cwd, \"src\"), { recursive: true })) as string[];\n } catch {\n return \"footer\";\n }\n const svelteFiles = entries.filter((p) => p.endsWith(\".svelte\"));\n if (svelteFiles.length === 0) return \"footer\";\n let sawMain = false;\n for (const rel of svelteFiles) {\n const text = (await readIfExists(join(cwd, \"src\", rel))) ?? \"\";\n if (/<footer[\\s>/]/.test(text)) return \"footer\";\n if (/<main[\\s>/]/.test(text)) sawMain = true;\n }\n return sawMain ? \"main\" : \"body\";\n}\n\n/**\n * Adds the smoke suite to a site: the `tests/smoke/*` specs, a `test:smoke` /\n * `test:unit` script split, `@playwright/test`, and a `playwright.config.ts`\n * that honors `REDDOOR_SMOKE_PORT` (R1.1). Conservative + partial-apply — every\n * file is noop-if-exists, every `package.json` script/dep is add-if-absent, and\n * an existing config that isn't a recognizable shared-base shape is left\n * untouched and flagged for a manual R1.1 patch (never a destructive write).\n * A site with no `package.json` noops (not a node project). Branch+commit per site.\n */\nexport async function smokeSuite(\n site: Site,\n deps: SmokeSuiteDeps = { spawn: defaultSpawn },\n): Promise<RecipeResult> {\n const pkgPath = join(site.path, \"package.json\");\n return withRecipe<{ pkgPath: string; pkg: PackageJson }>({\n name: \"smoke-suite\",\n site,\n plan: async () => {\n // Parse in the read-only plan phase so a missing OR unparseable\n // package.json noops cleanly (spec: structurally unrecognizable → noop),\n // BEFORE apply writes any file. A bad JSON that only threw in apply would\n // leave the (safely-restored) branch as a `failed`, not the graceful noop.\n const raw = await readIfExists(pkgPath);\n if (raw === null) {\n return { kind: \"noop\", notes: \"no package.json (not a node project)\" };\n }\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(raw) as PackageJson;\n } catch {\n return { kind: \"noop\", notes: \"unparseable package.json — skipped\" };\n }\n return { kind: \"apply\", plan: { pkgPath, pkg } };\n },\n apply: async (planned, { commit, cwd }) => {\n const notes: string[] = [];\n // Relative paths this run actually wrote/changed — prettier-formatted to the\n // site's own config before committing (never operator files we left alone).\n const written: string[] = [];\n\n // 1. Spec files — write if absent (never clobber operator edits). The\n // routes manifest ships the starter-verbatim `footer` marker only when\n // the site actually renders one; bespoke builds fall back to `main`,\n // then `body`, so the suite proves paint instead of false-failing.\n let routesTemplate = SMOKE_ROUTES_TEMPLATE;\n if (!(await fileExists(join(cwd, SMOKE_ROUTES_RELATIVE)))) {\n const marker = await detectHydrationMarker(cwd);\n if (marker !== \"footer\") {\n routesTemplate = SMOKE_ROUTES_TEMPLATE.replace(\n 'hydrationMarker: \"footer\"',\n `hydrationMarker: \"${marker}\"`,\n ).replace(FOOTER_MARKER_SENTENCE, fallbackMarkerSentence(marker));\n notes.push(\n `no <footer> element in src/**/*.svelte — hydration marker set to \"${marker}\" ` +\n \"(add a semantic <footer> landmark to restore the default)\",\n );\n }\n }\n const specFiles: Array<[string, string]> = [\n [SMOKE_ROUTES_RELATIVE, routesTemplate],\n [SMOKE_SPEC_RELATIVE, SMOKE_SPEC_TEMPLATE],\n ];\n for (const [rel, tmpl] of specFiles) {\n const target = join(cwd, rel);\n if (!(await fileExists(target))) {\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, tmpl, \"utf-8\");\n written.push(rel);\n }\n }\n\n // 2. playwright.config.ts — four cases, never an in-place edit: absent →\n // write R1.1; already has REDDOOR_SMOKE_PORT → leave; exact pre-R1.1\n // shared-base → safe-replace wholesale; anything else → flag for manual.\n const cfgPath = join(cwd, PLAYWRIGHT_CONFIG_RELATIVE);\n const existingCfg = await readIfExists(cfgPath);\n if (existingCfg === null) {\n await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, \"utf-8\");\n written.push(PLAYWRIGHT_CONFIG_RELATIVE);\n } else if (existingCfg.includes(\"REDDOOR_SMOKE_PORT\")) {\n // Already R1.1-aware; leave it.\n } else if (existingCfg.trim() === PLAYWRIGHT_CONFIG_PRE_R11.trim()) {\n await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, \"utf-8\");\n written.push(PLAYWRIGHT_CONFIG_RELATIVE);\n } else {\n notes.push(\n \"playwright.config.ts exists without REDDOOR_SMOKE_PORT — add the R1.1 port block manually\",\n );\n }\n\n // 3. package.json — add-if-absent scripts + @playwright/test. Only rewrite\n // when something changed, so a re-run of a fully-adopted site noops\n // instead of churning the file. (Parsed in plan; see above.)\n const pkg = planned.pkg;\n let pkgChanged = false;\n pkg.scripts ??= {};\n if (!pkg.scripts[\"test:smoke\"]) {\n pkg.scripts[\"test:smoke\"] = \"playwright install chromium && playwright test\";\n pkgChanged = true;\n }\n // Unit-test scripts (`test:unit` / `test` → \"vitest run\") only when the site\n // actually runs vitest — either it depends on vitest, or it already defines a\n // `test` script. Adding `test: \"vitest run\"` to a site WITHOUT vitest makes\n // the shared CI's \"run the test script if present\" step fail with\n // `vitest: not found` (the whole fleet-smoke batch red'd on exactly this).\n const hasVitest = !!(pkg.devDependencies?.vitest || pkg.dependencies?.vitest);\n const existingTest = pkg.scripts[\"test\"];\n if (hasVitest || existingTest !== undefined) {\n if (!pkg.scripts[\"test:unit\"]) {\n pkg.scripts[\"test:unit\"] = existingTest ?? \"vitest run\";\n pkgChanged = true;\n }\n if (pkg.scripts[\"test\"] === undefined) {\n pkg.scripts[\"test\"] = \"vitest run\";\n pkgChanged = true;\n }\n }\n let depsChanged = false;\n const hasPlaywright =\n !!pkg.devDependencies?.[\"@playwright/test\"] || !!pkg.dependencies?.[\"@playwright/test\"];\n if (!hasPlaywright) {\n pkg.devDependencies ??= {};\n pkg.devDependencies[\"@playwright/test\"] = \"^1.60.0\";\n depsChanged = true;\n pkgChanged = true;\n }\n if (pkgChanged) {\n await writeFile(planned.pkgPath, JSON.stringify(pkg, null, 2) + \"\\n\", \"utf-8\");\n written.push(\"package.json\");\n }\n\n // 4. Install only when a dep was added (streaming, so the operator sees\n // progress; matches bump-deps). A failed install aborts the recipe.\n if (depsChanged) {\n const res = await deps.spawn(\"pnpm\", [\"install\"], { cwd, streaming: true });\n if (res.code !== 0) {\n return { kind: \"failed\", notes: `pnpm install failed (exit ${res.code})` };\n }\n }\n\n // 5. Format everything this run wrote to the site's own prettier config, so\n // fleet CI's format check stays green across heterogeneous configs\n // (quotes/tabs/printWidth vary). Best-effort — a site without prettier\n // just commits unformatted with a flag note.\n if (!(await formatWithPrettier(deps.spawn, cwd, written))) {\n notes.push(PRETTIER_FLAG_NOTE);\n }\n\n // 6. Commit. If nothing was written/changed the commit stages nothing and\n // withRecipe reports noop (the flag note, if any, is still surfaced).\n await commit(\"feat: add smoke suite (test:smoke + playwright config + /health smoke routes)\");\n return notes.length > 0 ? { kind: \"ok\", notes: notes.join(\"; \") } : { kind: \"ok\" };\n },\n });\n}\n","// GENERATED verbatim from the reddoor-starter working tree (tests/smoke/*,\n// playwright.config.ts). Byte-fidelity matters — the smoke spec + config are\n// written into each site unchanged. Regenerate with scratchpad/gen-smoke-template.mjs\n// if the starter's smoke suite changes. Do NOT hand-edit the string bodies.\n\nexport const SMOKE_ROUTES_RELATIVE = \"tests/smoke/routes.ts\";\nexport const SMOKE_ROUTES_TEMPLATE = `// Committed per-site smoke manifest. \\`tests/smoke/pages.spec.ts\\` iterates this\n// list, asserting each route returns its expected status and paints a hydration\n// marker with no console errors. This ships the SAFE DEFAULT every reddoor-starter\n// clone inherits; each site's figma-slices build grows the list as real routes\n// land (add \\`{ path, name, hydrationMarker }\\` entries).\n//\n// NOTE on the default \\`/\\` entry: it expects 200, which holds once the clone is\n// wired to a real Prismic repo (getByUID(\"page\",\"home\") resolves). On the bare\n// placeholder starter, \\`/\\` returns 404 (the Prismic lookup throws → error(404)),\n// so the \\`/\\` case only goes green after Prismic is wired — by design, since the\n// gate is about real site health. The hydration marker \\`footer\\` is the shared\n// layout footer, present on every page including the error page.\n\nexport type SmokeRoute = {\n /** Route path to visit, e.g. \"/\" or \"/about\". */\n path: string;\n /** Human-readable label used in the test title. */\n name: string;\n /** CSS selector asserted visible after load (hydration proof). Default: skip. */\n hydrationMarker?: string;\n /** Expected HTTP status. Default: 200. */\n expectStatus?: number;\n};\n\nexport const smokeRoutes: SmokeRoute[] = [\n { path: \"/\", name: \"home\", hydrationMarker: \"footer\" },\n];\n`;\n\nexport const SMOKE_SPEC_RELATIVE = \"tests/smoke/pages.spec.ts\";\nexport const SMOKE_SPEC_TEMPLATE = `import { test, expect, type Page, type ConsoleMessage } from \"@playwright/test\";\nimport { smokeRoutes } from \"./routes\";\n\n// Console messages we don't care about. Add patterns here only after seeing them\n// in CI and confirming they aren't actionable. Patterns are matched against both\n// the message text and the offending resource URL — Chromium's \"Failed to load\n// resource\" text omits the URL, so URL matching catches third-party network noise.\nconst ALLOWED_CONSOLE_PATTERNS: RegExp[] = [\n // Vimeo iframe embeds + their CDN telemetry endpoints occasionally 403 from\n // cloud IPs due to bot detection.\n /vimeo/i,\n // Turnstile (Cloudflare) telemetry occasionally surfaces in console.\n /turnstile|challenges\\\\.cloudflare/i,\n];\n\nfunction attachConsoleWatcher(page: Page, extraAllowed: RegExp[] = []) {\n const errors: string[] = [];\n const allowed = [...ALLOWED_CONSOLE_PATTERNS, ...extraAllowed];\n const isAllowed = (s: string) => !!s && allowed.some((re) => re.test(s));\n\n page.on(\"console\", (msg: ConsoleMessage) => {\n if (msg.type() !== \"error\") return;\n const text = msg.text();\n const url = msg.location()?.url ?? \"\";\n if (isAllowed(text) || isAllowed(url)) return;\n errors.push(\\`[console.error] \\${text}\\${url ? \\` (\\${url})\\` : \"\"}\\`);\n });\n\n page.on(\"pageerror\", (err) => {\n if (isAllowed(err.message)) return;\n errors.push(\\`[pageerror] \\${err.message}\\`);\n });\n\n return errors;\n}\n\nfor (const route of smokeRoutes) {\n test(\\`\\${route.path} (\\${route.name}) loads with no console errors\\`, async ({\n page,\n }) => {\n const errors = attachConsoleWatcher(page);\n const response = await page.goto(route.path, {\n waitUntil: \"domcontentloaded\",\n });\n expect(response?.status(), \\`HTTP status for \\${route.path}\\`).toBe(\n route.expectStatus ?? 200,\n );\n if (route.hydrationMarker) {\n await expect(\n page.locator(route.hydrationMarker),\n \\`hydration marker \"\\${route.hydrationMarker}\" on \\${route.path}\\`,\n ).toBeVisible();\n }\n expect(errors, \\`console errors on \\${route.path}\\`).toEqual([]);\n });\n}\n\ntest(\"404 page renders the custom error component\", async ({ page }) => {\n // The browser logs a top-level \"Failed to load resource: 404\" for the page\n // itself — expected on a 404 route, not a bug. Allow it here.\n const errors = attachConsoleWatcher(page, [/Failed to load resource.*404/i]);\n const response = await page.goto(\"/this-uid-does-not-exist\", {\n waitUntil: \"domcontentloaded\",\n });\n expect(response?.status()).toBe(404);\n // src/routes/+error.svelte renders \\`<h1>{page.status}</h1>\\` → \"404\".\n await expect(page.getByText(\"404\", { exact: false }).first()).toBeVisible();\n expect(errors).toEqual([]);\n});\n`;\n\nexport const PLAYWRIGHT_CONFIG_RELATIVE = \"playwright.config.ts\";\n\n/** The R1.1 config: reads REDDOOR_SMOKE_PORT and binds --strictPort. Written\n * when a site has no playwright.config.ts. */\nexport const PLAYWRIGHT_CONFIG_TEMPLATE = `import { defineConfig } from \"@playwright/test\";\nimport base from \"@reddoorla/maintenance/configs/playwright-a11y\";\n\n// Emulate reduced motion in tests: instant scrollIntoView (no long animated\n// smooth-scroll that flakes Playwright's actionability checks under parallel\n// load) and view transitions fall back to instant. Pairs with the\n// prefers-reduced-motion gate on scroll-behavior in src/app.css.\n//\n// R1.1 (health-gate): the central \\`smoke\\` audit (reddoor-maintenance\n// src/audits/smoke.ts) allocates a free port and passes it as\n// REDDOOR_SMOKE_PORT so a zombie vite already squatting the default 5173 can't\n// silently hijack the run and green a stale build. When it's set, bind vite to\n// exactly that port with --strictPort (forwarded through \\`npm run vite:dev\\` so\n// it stays portable across pnpm/npm) and aim Playwright's baseURL + readiness\n// probe at it. Unset (local \\`pnpm test:smoke\\`) → the shared base's fixed 5173.\nconst smokePort = process.env.REDDOOR_SMOKE_PORT;\n\nexport default defineConfig({\n ...base,\n use: {\n ...base.use,\n reducedMotion: \"reduce\",\n ...(smokePort ? { baseURL: \\`http://localhost:\\${smokePort}\\` } : {}),\n },\n ...(smokePort\n ? {\n webServer: {\n command: \\`npm run vite:dev -- --port \\${smokePort} --strictPort\\`,\n url: \\`http://localhost:\\${smokePort}/dev/a11y-fixtures\\`,\n reuseExistingServer: !process.env.CI,\n timeout: 120_000,\n },\n }\n : {}),\n});\n`;\n\n/** The pre-R1.1 shared-base config (no port block). Used ONLY to recognize a\n * site that adopted the shared base before R1.1, so it can be safely replaced\n * wholesale with PLAYWRIGHT_CONFIG_TEMPLATE. Any other existing config is left\n * untouched and flagged for manual patch. */\nexport const PLAYWRIGHT_CONFIG_PRE_R11 = `import { defineConfig } from \"@playwright/test\";\nimport base from \"@reddoorla/maintenance/configs/playwright-a11y\";\n\n// Emulate reduced motion in tests: instant scrollIntoView (no long animated\n// smooth-scroll that flakes Playwright's actionability checks under parallel\n// load) and view transitions fall back to instant. Pairs with the\n// prefers-reduced-motion gate on scroll-behavior in src/app.css.\nexport default defineConfig({\n ...base,\n use: { ...base.use, reducedMotion: \"reduce\" },\n});\n`;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/recipes/smoke-suite/index.ts","../src/recipes/smoke-suite/template.ts"],"sourcesContent":["import { access, mkdir, readdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { withRecipe } from \"../_with-recipe.js\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\nimport { formatWithPrettier, PRETTIER_FLAG_NOTE } from \"../_prettier.js\";\nimport {\n SMOKE_ROUTES_RELATIVE,\n SMOKE_ROUTES_TEMPLATE,\n SMOKE_SPEC_RELATIVE,\n SMOKE_SPEC_TEMPLATE,\n PLAYWRIGHT_CONFIG_RELATIVE,\n PLAYWRIGHT_CONFIG_TEMPLATE,\n PLAYWRIGHT_CONFIG_PRE_R11,\n} from \"./template.js\";\n\nexport type SmokeSuiteDeps = { spawn: SpawnFn };\n\ntype PackageJson = {\n scripts?: Record<string, string>;\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n};\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function readIfExists(path: string): Promise<string | null> {\n try {\n return await readFile(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\n/** The template sentence explaining the default `footer` marker — swapped for a\n * fallback explanation when the marker deviates. Must match template.ts. */\nconst FOOTER_MARKER_SENTENCE =\n \"The hydration marker `footer` is the shared\\n// layout footer, present on every page including the error page.\";\n\nfunction fallbackMarkerSentence(marker: string): string {\n return (\n `The hydration marker \\`${marker}\\` is a\\n` +\n \"// fallback: no <footer> element exists in this site's Svelte source. Add a\\n\" +\n \"// semantic <footer> landmark and point the marker back at it when possible.\"\n );\n}\n\n/** The starter's default `footer` hydration marker needs the element to exist,\n * or EVERY route check false-fails (la-homelessness-initiative red'd the first\n * fleet-smoke run exactly this way). Deviate only on positive evidence of a\n * bespoke build: svelte files exist under src/ and none renders a literal\n * lowercase `<footer` element (a capital-F `<Footer` component tag proves\n * nothing — the element the browser paints lives inside that component). No\n * svelte files at all → no signal → keep the starter default. */\nasync function detectHydrationMarker(cwd: string): Promise<\"footer\" | \"main\" | \"body\"> {\n let entries: string[];\n try {\n entries = (await readdir(join(cwd, \"src\"), { recursive: true })) as string[];\n } catch {\n return \"footer\";\n }\n const svelteFiles = entries.filter((p) => p.endsWith(\".svelte\"));\n if (svelteFiles.length === 0) return \"footer\";\n let sawMain = false;\n for (const rel of svelteFiles) {\n const text = (await readIfExists(join(cwd, \"src\", rel))) ?? \"\";\n if (/<footer[\\s>/]/.test(text)) return \"footer\";\n if (/<main[\\s>/]/.test(text)) sawMain = true;\n }\n return sawMain ? \"main\" : \"body\";\n}\n\n/**\n * Adds the smoke suite to a site: the `tests/smoke/*` specs, a `test:smoke` /\n * `test:unit` script split, `@playwright/test`, and a `playwright.config.ts`\n * that honors `REDDOOR_SMOKE_PORT` (R1.1). Conservative + partial-apply — every\n * file is noop-if-exists, every `package.json` script/dep is add-if-absent, and\n * an existing config that isn't a recognizable shared-base shape is left\n * untouched and flagged for a manual R1.1 patch (never a destructive write).\n * A site with no `package.json` noops (not a node project). Branch+commit per site.\n */\nexport async function smokeSuite(\n site: Site,\n deps: SmokeSuiteDeps = { spawn: defaultSpawn },\n): Promise<RecipeResult> {\n const pkgPath = join(site.path, \"package.json\");\n return withRecipe<{ pkgPath: string; pkg: PackageJson }>({\n name: \"smoke-suite\",\n site,\n plan: async () => {\n // Parse in the read-only plan phase so a missing OR unparseable\n // package.json noops cleanly (spec: structurally unrecognizable → noop),\n // BEFORE apply writes any file. A bad JSON that only threw in apply would\n // leave the (safely-restored) branch as a `failed`, not the graceful noop.\n const raw = await readIfExists(pkgPath);\n if (raw === null) {\n return { kind: \"noop\", notes: \"no package.json (not a node project)\" };\n }\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(raw) as PackageJson;\n } catch {\n return { kind: \"noop\", notes: \"unparseable package.json — skipped\" };\n }\n return { kind: \"apply\", plan: { pkgPath, pkg } };\n },\n apply: async (planned, { commit, cwd }) => {\n const notes: string[] = [];\n // Relative paths this run actually wrote/changed — prettier-formatted to the\n // site's own config before committing (never operator files we left alone).\n const written: string[] = [];\n\n // 1. Spec files — write if absent (never clobber operator edits). The\n // routes manifest ships the starter-verbatim `footer` marker only when\n // the site actually renders one; bespoke builds fall back to `main`,\n // then `body`, so the suite proves paint instead of false-failing.\n let routesTemplate = SMOKE_ROUTES_TEMPLATE;\n if (!(await fileExists(join(cwd, SMOKE_ROUTES_RELATIVE)))) {\n const marker = await detectHydrationMarker(cwd);\n if (marker !== \"footer\") {\n routesTemplate = SMOKE_ROUTES_TEMPLATE.replace(\n 'hydrationMarker: \"footer\"',\n `hydrationMarker: \"${marker}\"`,\n ).replace(FOOTER_MARKER_SENTENCE, fallbackMarkerSentence(marker));\n notes.push(\n `no <footer> element in src/**/*.svelte — hydration marker set to \"${marker}\" ` +\n \"(add a semantic <footer> landmark to restore the default)\",\n );\n }\n }\n const specFiles: Array<[string, string]> = [\n [SMOKE_ROUTES_RELATIVE, routesTemplate],\n [SMOKE_SPEC_RELATIVE, SMOKE_SPEC_TEMPLATE],\n ];\n for (const [rel, tmpl] of specFiles) {\n const target = join(cwd, rel);\n if (!(await fileExists(target))) {\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, tmpl, \"utf-8\");\n written.push(rel);\n }\n }\n\n // 2. playwright.config.ts — four cases, never an in-place edit: absent →\n // write R1.1; already has REDDOOR_SMOKE_PORT → leave; exact pre-R1.1\n // shared-base → safe-replace wholesale; anything else → flag for manual.\n const cfgPath = join(cwd, PLAYWRIGHT_CONFIG_RELATIVE);\n const existingCfg = await readIfExists(cfgPath);\n if (existingCfg === null) {\n await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, \"utf-8\");\n written.push(PLAYWRIGHT_CONFIG_RELATIVE);\n } else if (existingCfg.includes(\"REDDOOR_SMOKE_PORT\")) {\n // Already R1.1-aware; leave it.\n } else if (existingCfg.trim() === PLAYWRIGHT_CONFIG_PRE_R11.trim()) {\n await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, \"utf-8\");\n written.push(PLAYWRIGHT_CONFIG_RELATIVE);\n } else {\n notes.push(\n \"playwright.config.ts exists without REDDOOR_SMOKE_PORT — add the R1.1 port block manually\",\n );\n }\n\n // 3. package.json — add-if-absent scripts + @playwright/test. Only rewrite\n // when something changed, so a re-run of a fully-adopted site noops\n // instead of churning the file. (Parsed in plan; see above.)\n const pkg = planned.pkg;\n let pkgChanged = false;\n pkg.scripts ??= {};\n if (!pkg.scripts[\"test:smoke\"]) {\n pkg.scripts[\"test:smoke\"] = \"playwright install chromium && playwright test\";\n pkgChanged = true;\n }\n // Unit-test scripts (`test:unit` / `test` → \"vitest run\") only when the site\n // actually runs vitest — either it depends on vitest, or it already defines a\n // `test` script. Adding `test: \"vitest run\"` to a site WITHOUT vitest makes\n // the shared CI's \"run the test script if present\" step fail with\n // `vitest: not found` (the whole fleet-smoke batch red'd on exactly this).\n const hasVitest = !!(pkg.devDependencies?.vitest || pkg.dependencies?.vitest);\n const existingTest = pkg.scripts[\"test\"];\n if (hasVitest || existingTest !== undefined) {\n if (!pkg.scripts[\"test:unit\"]) {\n pkg.scripts[\"test:unit\"] = existingTest ?? \"vitest run\";\n pkgChanged = true;\n }\n if (pkg.scripts[\"test\"] === undefined) {\n pkg.scripts[\"test\"] = \"vitest run\";\n pkgChanged = true;\n }\n }\n let depsChanged = false;\n const hasPlaywright =\n !!pkg.devDependencies?.[\"@playwright/test\"] || !!pkg.dependencies?.[\"@playwright/test\"];\n if (!hasPlaywright) {\n pkg.devDependencies ??= {};\n pkg.devDependencies[\"@playwright/test\"] = \"^1.60.0\";\n depsChanged = true;\n pkgChanged = true;\n }\n if (pkgChanged) {\n await writeFile(planned.pkgPath, JSON.stringify(pkg, null, 2) + \"\\n\", \"utf-8\");\n written.push(\"package.json\");\n }\n\n // 4. Install only when a dep was added (streaming, so the operator sees\n // progress; matches bump-deps). A failed install aborts the recipe.\n if (depsChanged) {\n const res = await deps.spawn(\"pnpm\", [\"install\"], { cwd, streaming: true });\n if (res.code !== 0) {\n return { kind: \"failed\", notes: `pnpm install failed (exit ${res.code})` };\n }\n }\n\n // 5. Format everything this run wrote to the site's own prettier config, so\n // fleet CI's format check stays green across heterogeneous configs\n // (quotes/tabs/printWidth vary). Best-effort — a site without prettier\n // just commits unformatted with a flag note.\n if (!(await formatWithPrettier(deps.spawn, cwd, written))) {\n notes.push(PRETTIER_FLAG_NOTE);\n }\n\n // 6. Commit. If nothing was written/changed the commit stages nothing and\n // withRecipe reports noop (the flag note, if any, is still surfaced).\n await commit(\"feat: add smoke suite (test:smoke + playwright config + /health smoke routes)\");\n return notes.length > 0 ? { kind: \"ok\", notes: notes.join(\"; \") } : { kind: \"ok\" };\n },\n });\n}\n","// GENERATED verbatim from the reddoor-starter working tree (tests/smoke/*,\n// playwright.config.ts). Byte-fidelity matters — the smoke spec + config are\n// written into each site unchanged. Regenerate with scratchpad/gen-smoke-template.mjs\n// if the starter's smoke suite changes. Do NOT hand-edit the string bodies.\n\nexport const SMOKE_ROUTES_RELATIVE = \"tests/smoke/routes.ts\";\nexport const SMOKE_ROUTES_TEMPLATE = `// Committed per-site smoke manifest. \\`tests/smoke/pages.spec.ts\\` iterates this\n// list, asserting each route returns its expected status and paints a hydration\n// marker with no console errors. This ships the SAFE DEFAULT every reddoor-starter\n// clone inherits; each site's figma-slices build grows the list as real routes\n// land (add \\`{ path, name, hydrationMarker }\\` entries).\n//\n// NOTE on the default \\`/\\` entry: it expects 200, which holds once the clone is\n// wired to a real Prismic repo (getByUID(\"page\",\"home\") resolves). On the bare\n// placeholder starter, \\`/\\` returns 404 (the Prismic lookup throws → error(404)),\n// so the \\`/\\` case only goes green after Prismic is wired — by design, since the\n// gate is about real site health. The hydration marker \\`footer\\` is the shared\n// layout footer, present on every page including the error page.\n\nexport type SmokeRoute = {\n /** Route path to visit, e.g. \"/\" or \"/about\". */\n path: string;\n /** Human-readable label used in the test title. */\n name: string;\n /** CSS selector asserted visible after load (hydration proof). Default: skip. */\n hydrationMarker?: string;\n /** Expected HTTP status. Default: 200. */\n expectStatus?: number;\n};\n\nexport const smokeRoutes: SmokeRoute[] = [\n { path: \"/\", name: \"home\", hydrationMarker: \"footer\" },\n];\n`;\n\nexport const SMOKE_SPEC_RELATIVE = \"tests/smoke/pages.spec.ts\";\nexport const SMOKE_SPEC_TEMPLATE = `import { test, expect, type Page, type ConsoleMessage } from \"@playwright/test\";\nimport { smokeRoutes } from \"./routes\";\n\n// Console messages we don't care about. Add patterns here only after seeing them\n// in CI and confirming they aren't actionable. Patterns are matched against both\n// the message text and the offending resource URL — Chromium's \"Failed to load\n// resource\" text omits the URL, so URL matching catches third-party network noise.\nconst ALLOWED_CONSOLE_PATTERNS: RegExp[] = [\n // Vimeo iframe embeds + their CDN telemetry endpoints occasionally 403 from\n // cloud IPs due to bot detection.\n /vimeo/i,\n // Turnstile (Cloudflare) telemetry occasionally surfaces in console.\n /turnstile|challenges\\\\.cloudflare/i,\n];\n\nfunction attachConsoleWatcher(page: Page, extraAllowed: RegExp[] = []) {\n const errors: string[] = [];\n const allowed = [...ALLOWED_CONSOLE_PATTERNS, ...extraAllowed];\n const isAllowed = (s: string) => !!s && allowed.some((re) => re.test(s));\n\n page.on(\"console\", (msg: ConsoleMessage) => {\n if (msg.type() !== \"error\") return;\n const text = msg.text();\n const url = msg.location()?.url ?? \"\";\n if (isAllowed(text) || isAllowed(url)) return;\n errors.push(\\`[console.error] \\${text}\\${url ? \\` (\\${url})\\` : \"\"}\\`);\n });\n\n page.on(\"pageerror\", (err) => {\n if (isAllowed(err.message)) return;\n errors.push(\\`[pageerror] \\${err.message}\\`);\n });\n\n return errors;\n}\n\nfor (const route of smokeRoutes) {\n test(\\`\\${route.path} (\\${route.name}) loads with no console errors\\`, async ({\n page,\n }) => {\n const errors = attachConsoleWatcher(page);\n const response = await page.goto(route.path, {\n waitUntil: \"domcontentloaded\",\n });\n expect(response?.status(), \\`HTTP status for \\${route.path}\\`).toBe(\n route.expectStatus ?? 200,\n );\n if (route.hydrationMarker) {\n await expect(\n page.locator(route.hydrationMarker),\n \\`hydration marker \"\\${route.hydrationMarker}\" on \\${route.path}\\`,\n ).toBeVisible();\n }\n expect(errors, \\`console errors on \\${route.path}\\`).toEqual([]);\n });\n}\n\ntest(\"404 page renders the custom error component\", async ({ page }) => {\n // The browser logs a top-level \"Failed to load resource: 404\" for the page\n // itself — expected on a 404 route, not a bug. Allow it here.\n const errors = attachConsoleWatcher(page, [/Failed to load resource.*404/i]);\n const response = await page.goto(\"/this-uid-does-not-exist\", {\n waitUntil: \"domcontentloaded\",\n });\n expect(response?.status()).toBe(404);\n // src/routes/+error.svelte renders \\`<h1>{page.status}</h1>\\` → \"404\".\n await expect(page.getByText(\"404\", { exact: false }).first()).toBeVisible();\n expect(errors).toEqual([]);\n});\n`;\n\nexport const PLAYWRIGHT_CONFIG_RELATIVE = \"playwright.config.ts\";\n\n/** The R1.1 config: reads REDDOOR_SMOKE_PORT and binds --strictPort. Written\n * when a site has no playwright.config.ts. */\nexport const PLAYWRIGHT_CONFIG_TEMPLATE = `import { defineConfig } from \"@playwright/test\";\nimport base from \"@reddoorla/maintenance/configs/playwright-a11y\";\n\n// Emulate reduced motion in tests: instant scrollIntoView (no long animated\n// smooth-scroll that flakes Playwright's actionability checks under parallel\n// load) and view transitions fall back to instant. Pairs with the\n// prefers-reduced-motion gate on scroll-behavior in src/app.css.\n//\n// R1.1 (health-gate): the central \\`smoke\\` audit (reddoor-maintenance\n// src/audits/smoke.ts) allocates a free port and passes it as\n// REDDOOR_SMOKE_PORT so a zombie vite already squatting the default 5173 can't\n// silently hijack the run and green a stale build. When it's set, bind vite to\n// exactly that port with --strictPort (forwarded through \\`npm run vite:dev\\` so\n// it stays portable across pnpm/npm) and aim Playwright's baseURL + readiness\n// probe at it. Unset (local \\`pnpm test:smoke\\`) → the shared base's fixed 5173.\nconst smokePort = process.env.REDDOOR_SMOKE_PORT;\n\nexport default defineConfig({\n ...base,\n use: {\n ...base.use,\n reducedMotion: \"reduce\",\n ...(smokePort ? { baseURL: \\`http://localhost:\\${smokePort}\\` } : {}),\n },\n ...(smokePort\n ? {\n webServer: {\n command: \\`npm run vite:dev -- --port \\${smokePort} --strictPort\\`,\n url: \\`http://localhost:\\${smokePort}/dev/a11y-fixtures\\`,\n // Never reuse: the readiness probe cannot tell the server under test\n // from any other server answering that URL. The shared base does the\n // same, and allocates its own port when REDDOOR_SMOKE_PORT is unset.\n reuseExistingServer: false,\n timeout: 120_000,\n },\n }\n : {}),\n});\n`;\n\n/** The pre-R1.1 shared-base config (no port block). Used ONLY to recognize a\n * site that adopted the shared base before R1.1, so it can be safely replaced\n * wholesale with PLAYWRIGHT_CONFIG_TEMPLATE. Any other existing config is left\n * untouched and flagged for manual patch. */\nexport const PLAYWRIGHT_CONFIG_PRE_R11 = `import { defineConfig } from \"@playwright/test\";\nimport base from \"@reddoorla/maintenance/configs/playwright-a11y\";\n\n// Emulate reduced motion in tests: instant scrollIntoView (no long animated\n// smooth-scroll that flakes Playwright's actionability checks under parallel\n// load) and view transitions fall back to instant. Pairs with the\n// prefers-reduced-motion gate on scroll-behavior in src/app.css.\nexport default defineConfig({\n ...base,\n use: { ...base.use, reducedMotion: \"reduce\" },\n});\n`;\n"],"mappings":"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","names":[]}
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-R6FPFMOS.js";
13
13
  import {
14
14
  smokeSuite
15
- } from "./chunk-R4AB6GCT.js";
15
+ } from "./chunk-S56JQJ4F.js";
16
16
  import {
17
17
  syncConfigs
18
18
  } from "./chunk-Q276MZ7E.js";
@@ -21,7 +21,7 @@ import {
21
21
  } from "./chunk-ZTPPCHDB.js";
22
22
  import {
23
23
  runAudits
24
- } from "./chunk-QRDB4NLB.js";
24
+ } from "./chunk-DTTIFYAM.js";
25
25
  import {
26
26
  siteLabel
27
27
  } from "./chunk-XXTZBPUY.js";
@@ -143,4 +143,4 @@ export {
143
143
  DEFAULT_INIT_STEPS,
144
144
  init
145
145
  };
146
- //# sourceMappingURL=chunk-6V7565O2.js.map
146
+ //# sourceMappingURL=chunk-WYYQK4D3.js.map
package/dist/cli/bin.js CHANGED
@@ -207,7 +207,7 @@ cli.command(
207
207
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
208
208
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
209
209
  async (site, opts) => runOrExit(
210
- async () => (await import("../smoke-suite-OBWNPALQ.js")).runSmokeSuiteCommand(site, opts),
210
+ async () => (await import("../smoke-suite-TMCSOYRK.js")).runSmokeSuiteCommand(site, opts),
211
211
  opts
212
212
  )
213
213
  );
@@ -230,14 +230,14 @@ cli.command(
230
230
  "--fleet <inventory>",
231
231
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
232
232
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
233
- async (site, opts) => runOrExit(async () => (await import("../init-TFVAN5OY.js")).runInitCommand(site, opts), opts)
233
+ async (site, opts) => runOrExit(async () => (await import("../init-JN26NTWF.js")).runInitCommand(site, opts), opts)
234
234
  );
235
235
  cli.command(
236
236
  "launch <site>",
237
237
  "Bootstrap + first-audit a site, then draft its launch email for approval."
238
238
  ).action(
239
239
  async (site, opts) => runOrExit(
240
- async () => (await import("../launch-DWY2MF54.js")).runLaunchCommand(site, opts),
240
+ async () => (await import("../launch-JA7Z4CIM.js")).runLaunchCommand(site, opts),
241
241
  opts
242
242
  )
243
243
  );
@@ -261,7 +261,7 @@ cli.command(
261
261
  "Show who a form submission would email, and optionally flip the pre-launch guard (read-back confirmed)."
262
262
  ).option("--set <on|off>", "Flip the guard: on = route to operator, off = restore.").option("--restore <status>", "Status to restore with --set off. Required; never inferred.").action(
263
263
  async (site, opts) => runOrExit(
264
- async () => (await import("../forms-notify-target-SMKDKNT2.js")).runFormsNotifyTargetCommand(
264
+ async () => (await import("../forms-notify-target-TWN4EH6K.js")).runFormsNotifyTargetCommand(
265
265
  site,
266
266
  opts
267
267
  ),
@@ -273,7 +273,7 @@ cli.command(
273
273
  "Operator self-tests. kind=email: preview a report email for a site (or --all) to yourself."
274
274
  ).option("--type <type>", "Report type: announcement (default) | maintenance | testing | launch").option("--to <addr>", "Recipient(s), comma-separated. Default: OPERATOR_EMAIL.").option("--all", "Send a preview for every maintenance site (to --to/operator).").option("--dry-run", "Render only; write reports/<slug>/selftest-<type>.html; do not send.").action(
275
275
  async (kind, site, opts) => runOrExit(
276
- async () => (await import("../selftest-BVCLJRNI.js")).runSelftestCommand(kind, site, opts),
276
+ async () => (await import("../selftest-CQ6NOX52.js")).runSelftestCommand(kind, site, opts),
277
277
  opts
278
278
  )
279
279
  );
@@ -309,7 +309,7 @@ cli.command("report [site]", "Draft or send maintenance/testing reports.").optio
309
309
  "Email the operator one daily digest of reports ready for approval (skips when empty)."
310
310
  ).action(
311
311
  async (site, opts) => runOrExit(
312
- async () => (await import("../report-WG6RHA2X.js")).runReportCommand(site, opts),
312
+ async () => (await import("../report-6CKSWJNG.js")).runReportCommand(site, opts),
313
313
  opts
314
314
  )
315
315
  );
@@ -12,7 +12,7 @@ import {
12
12
  import {
13
13
  ALL_AUDIT_NAMES,
14
14
  runOneAudit
15
- } from "../../chunk-QRDB4NLB.js";
15
+ } from "../../chunk-DTTIFYAM.js";
16
16
  import "../../chunk-LKGVSM2O.js";
17
17
  import "../../chunk-GASBX52O.js";
18
18
  import "../../chunk-VRWUL4TR.js";
@@ -32,7 +32,7 @@ import {
32
32
  } from "../../chunk-U5SZ4FOP.js";
33
33
  import "../../chunk-6VAL7XJT.js";
34
34
  import "../../chunk-NMAWXBJA.js";
35
- import "../../chunk-N4MOEF32.js";
35
+ import "../../chunk-C6B4RHXR.js";
36
36
 
37
37
  // src/cli/commands/audit.ts
38
38
  import { resolve } from "path";
@@ -2,7 +2,7 @@ import {
2
2
  a11yRoutes,
3
3
  playwright_a11y_default,
4
4
  smokeRoutes
5
- } from "../chunk-N4MOEF32.js";
5
+ } from "../chunk-C6B4RHXR.js";
6
6
  export {
7
7
  a11yRoutes,
8
8
  playwright_a11y_default as default,
@@ -23,6 +23,35 @@ type CspObject = {
23
23
  directives?: CspDirectives;
24
24
  [k: string]: unknown;
25
25
  };
26
+ /**
27
+ * SHA-256 of Svelte's SSR event-replay stub, `this.__e=event`.
28
+ *
29
+ * Svelte emits `onload`/`onerror="this.__e=event"` on any load/error element
30
+ * (`<img>`, `<iframe>`, …) that carries a spread attribute or a `use:`
31
+ * directive — i.e. every `<img {...getImageProps(field)} />` the Prismic
32
+ * helpers produce. The stub stashes an event that fires BEFORE hydration so the
33
+ * component can replay it once it is alive.
34
+ *
35
+ * Hashes do not apply to inline event handlers unless `'unsafe-hashes'` is
36
+ * present, so without both of these the browser refuses to run the stub: the
37
+ * pre-hydration `load`/`error` is silently dropped (anything keyed on it — a
38
+ * fade-in, a fallback swap — can strand) and a `script-src-attr` violation is
39
+ * reported per image on every page view, burying real violations and hammering
40
+ * the report endpoint. Measured on beachfront-dentistry 2026-08-13: 12
41
+ * violations on `/` alone.
42
+ *
43
+ * `'unsafe-hashes'` widens hash matching to event handlers; it does NOT permit
44
+ * arbitrary inline handlers, so only this exact one-liner is allowed. Pair it
45
+ * with `'unsafe-inline'` and that guarantee is gone — the test asserts we do
46
+ * not. The stub itself only assigns the event to a property, so an injected
47
+ * element carrying identical text achieves nothing.
48
+ *
49
+ * Verified in Chrome 2026-08-17 against a page served with this policy: the
50
+ * handler runs and `img.__e.type === "error"`; under the previous baseline it
51
+ * did not run at all. Re-derive with:
52
+ * node -e 'console.log(require("crypto").createHash("sha256").update("this.__e=event").digest("base64"))'
53
+ */
54
+ declare const SVELTE_EVENT_REPLAY_HASH = "sha256-7dQwUgLau1NFCCGjfn9FsYptB6ZtWxJin6VohGIu20I=";
26
55
  /** reddoor-specific options that get transformed into `kit.*`, not passed through. */
27
56
  type ReddoorSvelteOptions = {
28
57
  /** Inject the baseline CSP (`true`) or the baseline extended with these fields. */
@@ -65,4 +94,4 @@ declare function createSvelteConfig(siteConfig?: SvelteConfigLike & ReddoorSvelt
65
94
  compilerOptions: NonNullable<SvelteConfigLike["compilerOptions"]>;
66
95
  };
67
96
 
68
- export { type ReddoorSvelteOptions, type SvelteConfigLike, createSvelteConfig, createSvelteConfig as default };
97
+ export { type ReddoorSvelteOptions, SVELTE_EVENT_REPLAY_HASH, type SvelteConfigLike, createSvelteConfig, createSvelteConfig as default };
@@ -18,11 +18,18 @@ var CANONICAL_ALIASES = {
18
18
  $assets: "src/lib/assets",
19
19
  "$assets/*": "src/lib/assets/*"
20
20
  };
21
+ var SVELTE_EVENT_REPLAY_HASH = "sha256-7dQwUgLau1NFCCGjfn9FsYptB6ZtWxJin6VohGIu20I=";
21
22
  var BASELINE_CSP = {
22
23
  mode: "auto",
23
24
  directives: {
24
25
  "default-src": ["self"],
25
- "script-src": ["self", "https://static.cdn.prismic.io", "https://player.vimeo.com"],
26
+ "script-src": [
27
+ "self",
28
+ "https://static.cdn.prismic.io",
29
+ "https://player.vimeo.com",
30
+ "unsafe-hashes",
31
+ SVELTE_EVENT_REPLAY_HASH
32
+ ],
26
33
  "style-src": ["self", "unsafe-inline"],
27
34
  "img-src": ["self", "data:", "https://images.prismic.io", "https://*.prismic.io"],
28
35
  "media-src": ["self", "https://*.vimeocdn.com"],
@@ -97,6 +104,7 @@ function createSvelteConfig(siteConfig = {}) {
97
104
  }
98
105
  var svelte_default = createSvelteConfig;
99
106
  export {
107
+ SVELTE_EVENT_REPLAY_HASH,
100
108
  createSvelteConfig,
101
109
  svelte_default as default
102
110
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/configs/svelte.ts"],"sourcesContent":["/**\n * Minimal shape we touch — the full type lives in @sveltejs/kit's `Config`.\n * We don't import it directly to avoid a peer dependency for what amounts\n * to a small config helper. Sites get full type-checking from their own\n * `@sveltejs/kit` install when they invoke createSvelteConfig.\n */\ntype WarningFilter = (warning: { code?: string; message?: string }) => boolean;\n\nexport type SvelteConfigLike = {\n kit?: unknown;\n preprocess?: unknown;\n compilerOptions?: {\n warningFilter?: WarningFilter;\n [k: string]: unknown;\n };\n [k: string]: unknown;\n};\n\n/** Compiler-level warnings the canonical reddoor stack treats as noise. */\nconst SILENCED_WARNING_CODES = new Set<string>([\n // `<div ... />` shorthand is widely used across reddoor codebases; the\n // Svelte 5 strictness change for non-void self-closing tags would flood\n // dev logs with no actionable signal.\n \"element_invalid_self_closing_tag\",\n]);\n\nfunction isSilenced(warning: { code?: string }): boolean {\n return warning.code !== undefined && SILENCED_WARNING_CODES.has(warning.code);\n}\n\n/**\n * Canonical `$lib` aliases shared across the reddoor fleet. Injecting these as\n * defaults means a synced site no longer has to redeclare them (and the\n * sync-configs svelte template no longer clobbers them with a thinner config).\n * Sites can override any entry or add their own — see the merge in\n * createSvelteConfig. Override an alias's bare and `/*` forms together: setting\n * only `$components` while leaving the canonical `$components/*` in place would\n * split `import \"$components\"` and `import \"$components/Foo\"` across two roots.\n */\nconst CANONICAL_ALIASES: Record<string, string> = {\n $components: \"src/lib/components\",\n \"$components/*\": \"src/lib/components/*\",\n $utils: \"src/lib/utils\",\n \"$utils/*\": \"src/lib/utils/*\",\n $stores: \"src/lib/stores\",\n \"$stores/*\": \"src/lib/stores/*\",\n $assets: \"src/lib/assets\",\n \"$assets/*\": \"src/lib/assets/*\",\n};\n\ntype CspDirectives = Record<string, string[]>;\ntype CspObject = { mode?: string; directives?: CspDirectives; [k: string]: unknown };\n\n/**\n * Baseline CSP for the reddoor stack (Prismic + Vimeo). Opt-in only — a CSP is\n * breakage-prone, so it is never injected unless a site asks via `csp`. Extend\n * per project by passing `csp: { directives: { ... } }`; named directives\n * replace the baseline entry, unnamed ones are kept. SvelteKit adds\n * nonces/hashes for the inline scripts/styles it emits.\n */\nconst BASELINE_CSP = {\n mode: \"auto\",\n directives: {\n \"default-src\": [\"self\"],\n \"script-src\": [\"self\", \"https://static.cdn.prismic.io\", \"https://player.vimeo.com\"],\n \"style-src\": [\"self\", \"unsafe-inline\"],\n \"img-src\": [\"self\", \"data:\", \"https://images.prismic.io\", \"https://*.prismic.io\"],\n \"media-src\": [\"self\", \"https://*.vimeocdn.com\"],\n \"frame-src\": [\"self\", \"https://player.vimeo.com\"],\n \"connect-src\": [\"self\", \"https://*.prismic.io\", \"https://static.cdn.prismic.io\"],\n \"font-src\": [\"self\", \"data:\"],\n \"base-uri\": [\"self\"],\n \"form-action\": [\"self\"],\n \"frame-ancestors\": [\"self\"],\n \"report-uri\": [\"/api/csp-report\"],\n },\n} satisfies CspObject;\n\n/**\n * Copy a directives map and each of its arrays, so the returned config shares\n * no array reference with the module-level BASELINE_CSP (or with a caller's\n * input). Without this, mutating one config's directive array would poison the\n * shared baseline for every later call.\n */\nfunction cloneDirectives(directives: CspDirectives): CspDirectives {\n return Object.fromEntries(\n Object.entries(directives).map(([k, v]) => [k, Array.isArray(v) ? [...v] : v]),\n );\n}\n\n/** Build a `kit.csp` block from the `csp` option, layering over the baseline. */\nfunction buildCsp(option: true | CspObject): CspObject {\n const baseDirectives = BASELINE_CSP.directives ?? {};\n if (option === true) {\n return { mode: BASELINE_CSP.mode, directives: cloneDirectives(baseDirectives) };\n }\n const { directives: siteDirectives, ...rest } = option;\n return {\n mode: BASELINE_CSP.mode,\n ...rest, // allow `mode` override, `reportOnly`, etc.\n directives: cloneDirectives({ ...baseDirectives, ...(siteDirectives ?? {}) }),\n };\n}\n\ntype PrerenderErrorDetails = {\n path?: string;\n status?: number;\n message?: string;\n referrer?: string;\n};\n\n/**\n * Prerender error handler for an un-wired placeholder clone: every Prismic-backed\n * route 404s until the clone points at a real repo, so tolerate 404 to let\n * `pnpm build` / Netlify CI pass. Any other status still throws loudly, and a\n * real site never opts in (so its 404s fail the build as they should).\n */\nfunction placeholderHttpErrorHandler({\n path,\n status,\n message,\n referrer,\n}: PrerenderErrorDetails): void {\n if (status === 404) return;\n throw new Error(`${status} ${path}${referrer ? ` (linked from ${referrer})` : \"\"}: ${message}`);\n}\n\n/** reddoor-specific options that get transformed into `kit.*`, not passed through. */\nexport type ReddoorSvelteOptions = {\n /** Inject the baseline CSP (`true`) or the baseline extended with these fields. */\n csp?: true | CspObject;\n /** Tolerate 404s during prerender — for un-wired placeholder clones only. */\n placeholder?: boolean;\n};\n\n/**\n * Compose a Svelte/Kit config with the reddoor fleet's canonical pieces layered\n * in. Sites pass their site-specific bits (`kit.adapter`, `preprocess`, etc.)\n * and get back a complete config.\n *\n * Always applied:\n * - the canonical `compilerOptions.warningFilter` (composes with a site's own:\n * a warning shows only when both filters allow it);\n * - the canonical `$components/$utils/$stores/$assets` `kit.alias` entries\n * (a site's own `kit.alias` overrides per key and may add more).\n *\n * Opt-in (NOT applied unless requested, so adoption never silently changes a\n * site's behavior):\n * - `csp: true` injects the baseline Prismic+Vimeo CSP; `csp: { directives }`\n * extends it per-directive. A CSP is breakage-prone, so it is opt-in — a site\n * that wants the starter's CSP parity must pass `csp`. An explicit `kit.csp`\n * always wins as an escape hatch.\n * - `placeholder: true` tolerates 404s during prerender (for an un-wired\n * placeholder clone only) — like `csp`, a site that wants the starter's\n * prerender-tolerance parity must pass it; the site computes the signal itself.\n *\n * @example\n * import { createSvelteConfig } from \"@reddoorla/maintenance/configs/svelte\";\n * import adapter from \"@sveltejs/adapter-netlify\";\n *\n * export default createSvelteConfig({\n * kit: { adapter: adapter() },\n * csp: true,\n * placeholder: process.env.VITE_PRISMIC_ENVIRONMENT === \"your-prismic-repo-name\",\n * });\n */\n\nexport function createSvelteConfig(\n siteConfig: SvelteConfigLike & ReddoorSvelteOptions = {},\n): SvelteConfigLike & {\n compilerOptions: NonNullable<SvelteConfigLike[\"compilerOptions\"]>;\n} {\n // Strip the reddoor-only options so they never leak onto the returned config.\n const { csp, placeholder, ...rest } = siteConfig;\n\n const siteCompiler = rest.compilerOptions ?? {};\n const siteFilter = siteCompiler.warningFilter;\n\n const siteKit = (rest.kit ?? {}) as Record<string, unknown>;\n const siteAlias = (siteKit.alias ?? {}) as Record<string, string>;\n\n const kit: Record<string, unknown> = {\n ...siteKit,\n // Canonical aliases first, site entries last so a site can override any\n // single alias or add its own without losing the fleet defaults.\n alias: { ...CANONICAL_ALIASES, ...siteAlias },\n };\n\n // CSP: opt-in. An explicit `kit.csp` always wins as an escape hatch.\n if (siteKit.csp !== undefined) {\n kit.csp = siteKit.csp;\n } else if (csp) {\n kit.csp = buildCsp(csp);\n }\n\n // Prerender placeholder tolerance: opt-in. A site-provided handler wins.\n if (placeholder) {\n const sitePrerender = (siteKit.prerender ?? {}) as Record<string, unknown>;\n kit.prerender = { handleHttpError: placeholderHttpErrorHandler, ...sitePrerender };\n }\n\n return {\n ...rest,\n kit,\n compilerOptions: {\n ...siteCompiler,\n warningFilter: (warning) => {\n if (isSilenced(warning)) return false;\n return siteFilter ? siteFilter(warning) : true;\n },\n },\n };\n}\n\nexport default createSvelteConfig;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/configs/svelte.ts"],"sourcesContent":["/**\n * Minimal shape we touch — the full type lives in @sveltejs/kit's `Config`.\n * We don't import it directly to avoid a peer dependency for what amounts\n * to a small config helper. Sites get full type-checking from their own\n * `@sveltejs/kit` install when they invoke createSvelteConfig.\n */\ntype WarningFilter = (warning: { code?: string; message?: string }) => boolean;\n\nexport type SvelteConfigLike = {\n kit?: unknown;\n preprocess?: unknown;\n compilerOptions?: {\n warningFilter?: WarningFilter;\n [k: string]: unknown;\n };\n [k: string]: unknown;\n};\n\n/** Compiler-level warnings the canonical reddoor stack treats as noise. */\nconst SILENCED_WARNING_CODES = new Set<string>([\n // `<div ... />` shorthand is widely used across reddoor codebases; the\n // Svelte 5 strictness change for non-void self-closing tags would flood\n // dev logs with no actionable signal.\n \"element_invalid_self_closing_tag\",\n]);\n\nfunction isSilenced(warning: { code?: string }): boolean {\n return warning.code !== undefined && SILENCED_WARNING_CODES.has(warning.code);\n}\n\n/**\n * Canonical `$lib` aliases shared across the reddoor fleet. Injecting these as\n * defaults means a synced site no longer has to redeclare them (and the\n * sync-configs svelte template no longer clobbers them with a thinner config).\n * Sites can override any entry or add their own — see the merge in\n * createSvelteConfig. Override an alias's bare and `/*` forms together: setting\n * only `$components` while leaving the canonical `$components/*` in place would\n * split `import \"$components\"` and `import \"$components/Foo\"` across two roots.\n */\nconst CANONICAL_ALIASES: Record<string, string> = {\n $components: \"src/lib/components\",\n \"$components/*\": \"src/lib/components/*\",\n $utils: \"src/lib/utils\",\n \"$utils/*\": \"src/lib/utils/*\",\n $stores: \"src/lib/stores\",\n \"$stores/*\": \"src/lib/stores/*\",\n $assets: \"src/lib/assets\",\n \"$assets/*\": \"src/lib/assets/*\",\n};\n\ntype CspDirectives = Record<string, string[]>;\ntype CspObject = { mode?: string; directives?: CspDirectives; [k: string]: unknown };\n\n/**\n * SHA-256 of Svelte's SSR event-replay stub, `this.__e=event`.\n *\n * Svelte emits `onload`/`onerror=\"this.__e=event\"` on any load/error element\n * (`<img>`, `<iframe>`, …) that carries a spread attribute or a `use:`\n * directive — i.e. every `<img {...getImageProps(field)} />` the Prismic\n * helpers produce. The stub stashes an event that fires BEFORE hydration so the\n * component can replay it once it is alive.\n *\n * Hashes do not apply to inline event handlers unless `'unsafe-hashes'` is\n * present, so without both of these the browser refuses to run the stub: the\n * pre-hydration `load`/`error` is silently dropped (anything keyed on it — a\n * fade-in, a fallback swap — can strand) and a `script-src-attr` violation is\n * reported per image on every page view, burying real violations and hammering\n * the report endpoint. Measured on beachfront-dentistry 2026-08-13: 12\n * violations on `/` alone.\n *\n * `'unsafe-hashes'` widens hash matching to event handlers; it does NOT permit\n * arbitrary inline handlers, so only this exact one-liner is allowed. Pair it\n * with `'unsafe-inline'` and that guarantee is gone — the test asserts we do\n * not. The stub itself only assigns the event to a property, so an injected\n * element carrying identical text achieves nothing.\n *\n * Verified in Chrome 2026-08-17 against a page served with this policy: the\n * handler runs and `img.__e.type === \"error\"`; under the previous baseline it\n * did not run at all. Re-derive with:\n * node -e 'console.log(require(\"crypto\").createHash(\"sha256\").update(\"this.__e=event\").digest(\"base64\"))'\n */\nexport const SVELTE_EVENT_REPLAY_HASH = \"sha256-7dQwUgLau1NFCCGjfn9FsYptB6ZtWxJin6VohGIu20I=\";\n\n/**\n * Baseline CSP for the reddoor stack (Prismic + Vimeo). Opt-in only — a CSP is\n * breakage-prone, so it is never injected unless a site asks via `csp`. Extend\n * per project by passing `csp: { directives: { ... } }`; named directives\n * replace the baseline entry, unnamed ones are kept. SvelteKit adds\n * nonces/hashes for the inline scripts/styles it emits.\n *\n * NOTE: a site overriding `script-src` replaces this entry wholesale, so it must\n * carry `'unsafe-hashes'` + SVELTE_EVENT_REPLAY_HASH itself or it reintroduces\n * the violation above.\n */\nconst BASELINE_CSP = {\n mode: \"auto\",\n directives: {\n \"default-src\": [\"self\"],\n \"script-src\": [\n \"self\",\n \"https://static.cdn.prismic.io\",\n \"https://player.vimeo.com\",\n \"unsafe-hashes\",\n SVELTE_EVENT_REPLAY_HASH,\n ],\n \"style-src\": [\"self\", \"unsafe-inline\"],\n \"img-src\": [\"self\", \"data:\", \"https://images.prismic.io\", \"https://*.prismic.io\"],\n \"media-src\": [\"self\", \"https://*.vimeocdn.com\"],\n \"frame-src\": [\"self\", \"https://player.vimeo.com\"],\n \"connect-src\": [\"self\", \"https://*.prismic.io\", \"https://static.cdn.prismic.io\"],\n \"font-src\": [\"self\", \"data:\"],\n \"base-uri\": [\"self\"],\n \"form-action\": [\"self\"],\n \"frame-ancestors\": [\"self\"],\n \"report-uri\": [\"/api/csp-report\"],\n },\n} satisfies CspObject;\n\n/**\n * Copy a directives map and each of its arrays, so the returned config shares\n * no array reference with the module-level BASELINE_CSP (or with a caller's\n * input). Without this, mutating one config's directive array would poison the\n * shared baseline for every later call.\n */\nfunction cloneDirectives(directives: CspDirectives): CspDirectives {\n return Object.fromEntries(\n Object.entries(directives).map(([k, v]) => [k, Array.isArray(v) ? [...v] : v]),\n );\n}\n\n/** Build a `kit.csp` block from the `csp` option, layering over the baseline. */\nfunction buildCsp(option: true | CspObject): CspObject {\n const baseDirectives = BASELINE_CSP.directives ?? {};\n if (option === true) {\n return { mode: BASELINE_CSP.mode, directives: cloneDirectives(baseDirectives) };\n }\n const { directives: siteDirectives, ...rest } = option;\n return {\n mode: BASELINE_CSP.mode,\n ...rest, // allow `mode` override, `reportOnly`, etc.\n directives: cloneDirectives({ ...baseDirectives, ...(siteDirectives ?? {}) }),\n };\n}\n\ntype PrerenderErrorDetails = {\n path?: string;\n status?: number;\n message?: string;\n referrer?: string;\n};\n\n/**\n * Prerender error handler for an un-wired placeholder clone: every Prismic-backed\n * route 404s until the clone points at a real repo, so tolerate 404 to let\n * `pnpm build` / Netlify CI pass. Any other status still throws loudly, and a\n * real site never opts in (so its 404s fail the build as they should).\n */\nfunction placeholderHttpErrorHandler({\n path,\n status,\n message,\n referrer,\n}: PrerenderErrorDetails): void {\n if (status === 404) return;\n throw new Error(`${status} ${path}${referrer ? ` (linked from ${referrer})` : \"\"}: ${message}`);\n}\n\n/** reddoor-specific options that get transformed into `kit.*`, not passed through. */\nexport type ReddoorSvelteOptions = {\n /** Inject the baseline CSP (`true`) or the baseline extended with these fields. */\n csp?: true | CspObject;\n /** Tolerate 404s during prerender — for un-wired placeholder clones only. */\n placeholder?: boolean;\n};\n\n/**\n * Compose a Svelte/Kit config with the reddoor fleet's canonical pieces layered\n * in. Sites pass their site-specific bits (`kit.adapter`, `preprocess`, etc.)\n * and get back a complete config.\n *\n * Always applied:\n * - the canonical `compilerOptions.warningFilter` (composes with a site's own:\n * a warning shows only when both filters allow it);\n * - the canonical `$components/$utils/$stores/$assets` `kit.alias` entries\n * (a site's own `kit.alias` overrides per key and may add more).\n *\n * Opt-in (NOT applied unless requested, so adoption never silently changes a\n * site's behavior):\n * - `csp: true` injects the baseline Prismic+Vimeo CSP; `csp: { directives }`\n * extends it per-directive. A CSP is breakage-prone, so it is opt-in — a site\n * that wants the starter's CSP parity must pass `csp`. An explicit `kit.csp`\n * always wins as an escape hatch.\n * - `placeholder: true` tolerates 404s during prerender (for an un-wired\n * placeholder clone only) — like `csp`, a site that wants the starter's\n * prerender-tolerance parity must pass it; the site computes the signal itself.\n *\n * @example\n * import { createSvelteConfig } from \"@reddoorla/maintenance/configs/svelte\";\n * import adapter from \"@sveltejs/adapter-netlify\";\n *\n * export default createSvelteConfig({\n * kit: { adapter: adapter() },\n * csp: true,\n * placeholder: process.env.VITE_PRISMIC_ENVIRONMENT === \"your-prismic-repo-name\",\n * });\n */\n\nexport function createSvelteConfig(\n siteConfig: SvelteConfigLike & ReddoorSvelteOptions = {},\n): SvelteConfigLike & {\n compilerOptions: NonNullable<SvelteConfigLike[\"compilerOptions\"]>;\n} {\n // Strip the reddoor-only options so they never leak onto the returned config.\n const { csp, placeholder, ...rest } = siteConfig;\n\n const siteCompiler = rest.compilerOptions ?? {};\n const siteFilter = siteCompiler.warningFilter;\n\n const siteKit = (rest.kit ?? {}) as Record<string, unknown>;\n const siteAlias = (siteKit.alias ?? {}) as Record<string, string>;\n\n const kit: Record<string, unknown> = {\n ...siteKit,\n // Canonical aliases first, site entries last so a site can override any\n // single alias or add its own without losing the fleet defaults.\n alias: { ...CANONICAL_ALIASES, ...siteAlias },\n };\n\n // CSP: opt-in. An explicit `kit.csp` always wins as an escape hatch.\n if (siteKit.csp !== undefined) {\n kit.csp = siteKit.csp;\n } else if (csp) {\n kit.csp = buildCsp(csp);\n }\n\n // Prerender placeholder tolerance: opt-in. A site-provided handler wins.\n if (placeholder) {\n const sitePrerender = (siteKit.prerender ?? {}) as Record<string, unknown>;\n kit.prerender = { handleHttpError: placeholderHttpErrorHandler, ...sitePrerender };\n }\n\n return {\n ...rest,\n kit,\n compilerOptions: {\n ...siteCompiler,\n warningFilter: (warning) => {\n if (isSilenced(warning)) return false;\n return siteFilter ? siteFilter(warning) : true;\n },\n },\n };\n}\n\nexport default createSvelteConfig;\n"],"mappings":"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","names":[]}
@@ -1,3 +1,6 @@
1
+ import {
2
+ operatorEmail
3
+ } from "./chunk-5ML7FMBD.js";
1
4
  import {
2
5
  NOTIFY_BOUNCE_WINDOW_DAYS,
3
6
  collectAnalyticsFailures,
@@ -132,7 +135,6 @@ function submissionsSection(s) {
132
135
  return `${heading}${totalLine}<table role="presentation" style="border-collapse:collapse;margin:8px 0 0">${rows}</table>`;
133
136
  }
134
137
  var FROM_ADDRESS = "Reddoor Reports <reports@reddoorla.com>";
135
- var DIGEST_OPERATOR_FALLBACK = "info@reddoorla.com";
136
138
  function digestDateKey(d) {
137
139
  return d.toISOString().slice(0, 10);
138
140
  }
@@ -271,7 +273,7 @@ async function runDigest(options) {
271
273
  const submissions = buildSubmissionsDigestSection(counts, sites);
272
274
  const html = renderDigestHtml({ readyForYourYes, needsAttention, submissions });
273
275
  const client = options.resend ?? defaultResendClient();
274
- const to = [process.env.OPERATOR_EMAIL?.trim() || DIGEST_OPERATOR_FALLBACK];
276
+ const to = [operatorEmail()];
275
277
  const n = readyForYourYes.length;
276
278
  const reportWord = n === 1 ? "report" : "reports";
277
279
  let result;
@@ -337,4 +339,4 @@ export {
337
339
  renderDigestHtml,
338
340
  runDigest
339
341
  };
340
- //# sourceMappingURL=digest-SQ5U6SN2.js.map
342
+ //# sourceMappingURL=digest-UVHYZUYE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/digest.ts"],"sourcesContent":["// src/reports/digest.ts\nimport { openBase, readAirtableConfig, type AirtableBase } from \"./airtable/client.js\";\nimport { listAllReports, isPendingApproval } from \"./airtable/reports.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { listWebsites, siteSlug, type WebsiteRow } from \"./airtable/websites.js\";\nimport { defaultResendClient, type ResendClient } from \"./send/resend.js\";\nimport { isIdempotencyConflict } from \"./send/idempotency.js\";\nimport {\n collectVulnAlerts,\n collectDeliveryFailures,\n collectPreflightBlocked,\n collectLighthouseAlerts,\n collectRenovateAlerts,\n collectCiAlerts,\n collectAnalyticsFailures,\n collectTurnstileGuardrailAlerts,\n collectNotifyBounceAlerts,\n collectPrismicDriftAlerts,\n NOTIFY_BOUNCE_WINDOW_DAYS,\n} from \"../alerts/digest-collectors.js\";\nimport { diffAttention, readDigestState, writeDigestState } from \"../alerts/digest-state.js\";\nimport { escapeHtml as esc } from \"../util/html.js\";\nimport { operatorEmail } from \"../util/operator.js\";\nimport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n SubmissionsDigestSection,\n} from \"../alerts/attention.js\";\n// Type-only: erased at compile time, so the kysely/libsql devDependency never loads\n// in a consuming fleet site (the runtime read is the dynamic import in\n// fetchSubmissionsDigestCounts, same rule as fetchNotifyBounceCounts).\nimport type { SiteSubmissionCounts } from \"../db/submissions.js\";\n\n// The attention/digest contract lives in `../alerts/attention.ts` (a dependency-free\n// types module) so the `alerts/*` collectors can depend on it without importing back\n// from this renderer/IO module — see attention.ts for the cycle it breaks. Re-exported\n// here so existing `from \"./digest.js\"` type importers keep resolving.\nexport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n SubmissionsDigestSection,\n} from \"../alerts/attention.js\";\n\nconst GREY = \"#757575\";\nconst RED = \"#C00\";\n\n/** Shared anchor style — Gmail does not inherit font-family into <a> tags. */\nconst ANCHOR_STYLE = `color:${RED};font-family:helvetica,sans-serif`;\n\nfunction readySection(items: ReadyItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Ready for your yes</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">Nothing waiting on you.</p>`;\n }\n const rows = items\n .map((it) => {\n const safeUrl = it.dashboardUrl.startsWith(\"https://\") ? it.dashboardUrl : undefined;\n const link = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">review &amp; approve</a>`\n : `review &amp; approve`;\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(it.siteName)}</strong> — ${esc(it.reportType)} (${esc(it.period)})\n — ${link}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${rows}</table>`;\n}\n\nconst SEVERITY_ORDER: Record<AttentionSeverity, number> = { critical: 0, warning: 1 };\n\n/** Render the per-item status badge (\"NEW\"/\"WORSE\"); standing items get nothing. */\nfunction attentionBadge(status?: AttentionStatus): string {\n if (status === \"new\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">NEW</strong> `;\n if (status === \"worse\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">WORSE</strong> `;\n return \"\";\n}\n\nfunction attentionSection(items: AttentionItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Needs attention</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">All clear — nothing needs attention.</p>`;\n }\n\n // Group by siteName, preserving first-seen site order; sort within a site by\n // severity (critical first).\n const bySite = new Map<string, AttentionItem[]>();\n for (const it of items) {\n const bucket = bySite.get(it.siteName);\n if (bucket) bucket.push(it);\n else bySite.set(it.siteName, [it]);\n }\n\n const groups = [...bySite.entries()]\n .map(([siteName, siteItems]) => {\n const sorted = [...siteItems].sort(\n (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity],\n );\n const rows = sorted\n .map((it) => {\n const safeUrl = it.url?.startsWith(\"https://\") ? it.url : undefined;\n const titleHtml = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">${esc(it.title)}</a>`\n : esc(it.title);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">${attentionBadge(it.status)}${titleHtml}</td>\n </tr>`;\n })\n .join(\"\");\n return `\n <tr>\n <td style=\"color:#222;font-family:helvetica,sans-serif;font-size:16px;font-weight:700;padding:8px 0 4px\">${esc(siteName)}</td>\n </tr>\n ${rows}`;\n })\n .join(\"\");\n\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${groups}</table>`;\n}\n\n/** The \"Submissions (24h)\" telemetry block: fleet totals + a per-site glance line.\n * Context, not an alarm — absent/null section or all-zero totals render nothing\n * (no-noise), and the section never triggers a send on its own (see runDigest). */\nfunction submissionsSection(s: SubmissionsDigestSection | null | undefined): string {\n if (!s) return \"\";\n if (s.leads === 0 && s.signups === 0 && s.spamAuto === 0) return \"\";\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Submissions (24h)</h2>`;\n // All three terms always render together — omitting a zero term next to a nonzero\n // one reads as \"not measured\" rather than \"none\"; simplest honest form.\n const totals = [\n `${s.leads} new lead${s.leads === 1 ? \"\" : \"s\"}`,\n `${s.signups} newsletter/RSVP signup${s.signups === 1 ? \"\" : \"s\"}`,\n `${s.spamAuto} auto-filtered spam`,\n ].join(\" · \");\n const totalLine = `<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">${totals}</p>`;\n if (s.bySite.length === 0) return `${heading}${totalLine}`;\n const rows = s.bySite\n .map((site) => {\n const parts: string[] = [];\n if (site.leads > 0) parts.push(`${site.leads} lead${site.leads === 1 ? \"\" : \"s\"}`);\n if (site.signups > 0) parts.push(`${site.signups} signup${site.signups === 1 ? \"\" : \"s\"}`);\n if (site.spamAuto > 0) parts.push(`${site.spamAuto} auto-filtered`);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(site.siteName)}</strong> — ${parts.join(\" · \")}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}${totalLine}<table role=\"presentation\" style=\"border-collapse:collapse;margin:8px 0 0\">${rows}</table>`;\n}\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\n\n/** UTC \"YYYY-MM-DD\" — the Resend idempotency key suffix, so a same-day cron re-fire dedupes. */\nfunction digestDateKey(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * The gate for \"Ready for your yes\": Draft ready ∧ ¬Approved to send ∧ Sent at BLANK.\n *\n * Implemented as `listAllReports(base).filter(...)` (the authorized deviation from the plan's\n * draft, which pre-dated Slice 1's merged fix): `listAllReports` already calls `mapRow`, which\n * handles `Period` correctly, so no local `rawToReportRow` duplicate is needed. The JS filter\n * does real work — the test fake does NOT evaluate `filterByFormula` — so correctness is\n * test-provable here.\n *\n * Exported: the fleet homepage (Task 3.5b) reuses it for the pending-approval count.\n */\nexport async function listPendingApproval(base: AirtableBase): Promise<ReportRow[]> {\n return (await listAllReports(base)).filter(isPendingApproval);\n}\n\n// ── collectAttention (IO wrapper, sibling to runDigest) ──────────────────────\n\nexport type CollectAttentionDeps = {\n base: AirtableBase;\n /** Same baseUrl value runDigest threads; used for the /s/<slug> links. */\n baseUrl: string;\n /** Pre-fetched Websites rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listWebsites`. */\n websites?: WebsiteRow[];\n /** Pre-fetched Reports rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listAllReports`. */\n reports?: ReportRow[];\n /** Clock for the GitHub-signals staleness gate (collectCiAlerts /\n * collectRenovateAlerts skip a >3-day-stale sweep). Defaults to wall-clock;\n * runDigest threads its run-start `today` so a quiet repo's frozen CI/Renovate\n * signal stops badging once its sweep goes stale. */\n now?: Date;\n /** Pre-fetched per-site bounced-lead-notification counts (tests, or a caller with\n * an open db). When omitted, collectAttention reads them from libSQL itself —\n * defensively, so a missing TURSO env or a Turso blip drops just this signal. */\n notifyBounces?: ReadonlyMap<string, number>;\n};\n\n/** Per-site bounced-notification counts for the notify-bounce collector, read from\n * libSQL over the collector's own window. The db modules are imported DYNAMICALLY:\n * src/db/submissions.ts pulls kysely at top level, and kysely/libsql live in\n * devDependencies consuming fleet sites don't install — a static import here would\n * crash their CLI at require time (same rule as openDb's own lazy loads). Any\n * failure (no TURSO env, Turso down) logs and yields an empty map: the digest\n * must never blank over a missing optional signal. */\nasync function fetchNotifyBounceCounts(now: Date): Promise<ReadonlyMap<string, number>> {\n try {\n const [{ openDb, readDbConfig }, { countNotifyBouncedBySite }, { screenOutsSince }] =\n await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/submissions.js\"),\n import(\"../db/screenouts.js\"),\n ]);\n const db = await openDb(readDbConfig());\n return await countNotifyBouncedBySite(db, screenOutsSince(now, NOTIFY_BOUNCE_WINDOW_DAYS));\n } catch (e) {\n console.warn(`⚠ notify-bounce counts unavailable (libSQL): ${(e as Error).message}`);\n return new Map();\n }\n}\n\n/** The \"Submissions (24h)\" telemetry window — a precise 24h ISO-timestamp compare\n * (unlike screenOutsSince's date-only strings). */\nconst SUBMISSIONS_DIGEST_WINDOW_MS = 24 * 60 * 60 * 1000;\n\n/** Per-site submission counts for the digest telemetry section, read from libSQL.\n * Dynamic imports + fail-soft for the same reason as fetchNotifyBounceCounts:\n * kysely/libsql are devDependencies consuming fleet sites don't install, and a\n * Turso blip must drop just this section, never the digest. Returns null (not an\n * empty map) on failure so the renderer can omit the section rather than claim a\n * quiet fleet. */\nasync function fetchSubmissionsDigestCounts(\n now: Date,\n): Promise<ReadonlyMap<string, SiteSubmissionCounts> | null> {\n try {\n const [{ openDb, readDbConfig }, { countSubmissionsSinceBySite }] = await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/submissions.js\"),\n ]);\n const db = await openDb(readDbConfig());\n return await countSubmissionsSinceBySite(\n db,\n new Date(now.getTime() - SUBMISSIONS_DIGEST_WINDOW_MS).toISOString(),\n );\n } catch (e) {\n console.warn(`⚠ submissions telemetry unavailable (libSQL): ${(e as Error).message}`);\n return null;\n }\n}\n\n/** Assemble the \"Submissions (24h)\" section from the per-site counts. PURE (exported\n * for direct tests). Totals sum ALL map entries — an orphan site id still counts in\n * the fleet totals. bySite lists only ids that resolve in `sitesById` AND carry any\n * nonzero count, sorted by total volume desc then siteName A-Z. null counts (libSQL\n * unavailable) → null (section omitted). */\nexport function buildSubmissionsDigestSection(\n counts: ReadonlyMap<string, SiteSubmissionCounts> | null,\n sitesById: ReadonlyMap<string, WebsiteRow>,\n): SubmissionsDigestSection | null {\n if (counts === null) return null;\n let leads = 0;\n let signups = 0;\n let spamAuto = 0;\n const bySite: SubmissionsDigestSection[\"bySite\"] = [];\n for (const [siteId, c] of counts) {\n leads += c.leads;\n signups += c.signups;\n spamAuto += c.spamAuto;\n const site = sitesById.get(siteId);\n if (site && c.leads + c.signups + c.spamAuto > 0) {\n bySite.push({ siteName: site.name, ...c });\n }\n }\n bySite.sort(\n (a, b) =>\n b.leads + b.signups + b.spamAuto - (a.leads + a.signups + a.spamAuto) ||\n a.siteName.localeCompare(b.siteName),\n );\n return { leads, signups, spamAuto, bySite };\n}\n\n/** Run a single collector under a try/catch: a thrown collector logs and yields []\n * so one broken signal never blanks the whole \"Needs attention\" section. */\nfunction runCollector(label: string, fn: () => AttentionItem[]): AttentionItem[] {\n try {\n return fn();\n } catch (e) {\n console.warn(`⚠ attention collector \"${label}\" failed: ${(e as Error).message}`);\n return [];\n }\n}\n\n/**\n * Fetch the free signals once (listAllReports + listWebsites) — or reuse the\n * `reports`/`websites` arrays runDigest already read, so a single run reads each\n * table once — build the sitesById map the delivery collector needs, and run each\n * pure collector isolated. Returns the union of items; diffing/badging happens in\n * runDigest.\n *\n * The Renovate + CI signals come from the SAME persisted collectors the operator\n * cockpit (`buildCockpitModel`) runs — `collectRenovateAlerts` (key\n * `renovate:<siteId>`) and `collectCiAlerts` (key `ci:<siteId>`), reading the\n * nightly-persisted `renovateFailingCis`/`defaultBranchCi` fields. This is the\n * key-space unification: because the digest writes the shared Digest State\n * snapshot with these same keys, the cockpit's NEW/WORSE diff finds them and the\n * two surfaces agree (the prior live per-PR `renovate:<repo>#<n>` sweep never\n * matched the cockpit's keys, so its cards badged NEW forever).\n */\nexport async function collectAttention(deps: CollectAttentionDeps): Promise<AttentionItem[]> {\n const reports = deps.reports ?? (await listAllReports(deps.base));\n const websites = deps.websites ?? (await listWebsites(deps.base));\n const now = deps.now ?? new Date();\n const notifyBounces = deps.notifyBounces ?? (await fetchNotifyBounceCounts(now));\n const sitesById = new Map<string, WebsiteRow>(websites.map((w) => [w.id, w]));\n return [\n ...runCollector(\"vuln\", () => collectVulnAlerts(websites, deps.baseUrl)),\n ...runCollector(\"delivery\", () => collectDeliveryFailures(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"preflight\", () => collectPreflightBlocked(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"lighthouse\", () => collectLighthouseAlerts(websites, deps.baseUrl)),\n ...runCollector(\"renovate\", () => collectRenovateAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"ci\", () => collectCiAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"analytics\", () => collectAnalyticsFailures(websites, deps.baseUrl, now)),\n ...runCollector(\"turnstile\", () =>\n collectTurnstileGuardrailAlerts(websites, deps.baseUrl, now),\n ),\n ...runCollector(\"notify-bounce\", () =>\n collectNotifyBounceAlerts(websites, notifyBounces, deps.baseUrl),\n ),\n // Same collector the cockpit runs, over the same persisted `Prismic Models`\n // columns and the same keys — so the snapshot this digest writes is the one\n // the cockpit diffs NEW/WORSE against, and the two surfaces agree.\n ...runCollector(\"prismic-drift\", () => collectPrismicDriftAlerts(websites, deps.baseUrl, now)),\n ];\n}\n\nexport type DigestRunOptions = {\n resend?: ResendClient;\n /** Dashboard origin for the /s/<slug> links, e.g. \"https://reddoor-maintenance.netlify.app\". */\n baseUrl: string;\n /**\n * Inject a pre-opened Airtable base (tests, server handlers).\n * When omitted, `openBase(readAirtableConfig())` is called from the environment.\n */\n base?: AirtableBase;\n /** Per-site submission counts for the \"Submissions (24h)\" section. undefined =\n * fetch from libSQL; null = simulate unavailable (section omitted); a Map =\n * injected (tests). */\n submissionCounts?: ReadonlyMap<string, SiteSubmissionCounts> | null;\n /**\n * The run clock. Production omits it and gets `new Date()`.\n *\n * Injectable because several collectors this run feeds are AGE-SENSITIVE —\n * `collectPrismicDriftAlerts` alone changes an item's key and its wording once\n * a verdict crosses `PRISMIC_DRIFT_STALE_DAYS` — so a digest test that cannot\n * fix the clock is asserting on the day it happens to run. Two did, and both\n * went red four days after they were written, reporting the staleness wording\n * as if the drift wiring had broken. `collectAttention` already takes a `now`\n * for exactly this reason; this threads the same lever one layer out.\n */\n now?: Date;\n};\n\nexport async function runDigest(\n options: DigestRunOptions,\n): Promise<{ output: string; code: number }> {\n // Capture clock BEFORE any await so the idempotency key can't roll past midnight mid-run.\n const today = options.now ?? new Date();\n try {\n const base = options.base ?? openBase(readAirtableConfig());\n // Read each table ONCE for the whole run, then thread the arrays into\n // collectAttention so it doesn't re-fetch (was: listWebsites ×2, listAllReports\n // ×2). Pending is derived in-line with listPendingApproval's exact predicate.\n const reports = await listAllReports(base);\n const websites = await listWebsites(base);\n const sites = new Map(websites.map((w) => [w.id, w]));\n\n const pending = reports.filter(isPendingApproval);\n\n const readyForYourYes: ReadyItem[] = [];\n const baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n for (const r of pending) {\n const site = sites.get(r.siteId);\n if (!site) continue; // orphan report → skip rather than render a broken link\n // An empty Name slugs to \"\" → `/s/` is a dead link (getWebsiteBySlug can't\n // match it). Fall back to the fleet homepage so the operator still lands\n // somewhere usable instead of a 404.\n const slug = siteSlug(site.name);\n readyForYourYes.push({\n siteName: site.name,\n reportType: r.reportType,\n period: r.period ?? \"—\",\n dashboardUrl: slug ? `${baseUrl}/s/${slug}` : baseUrl,\n });\n }\n\n // M5: collect the free signals (isolated), diff against yesterday's snapshot.\n // Renovate + CI come from the persisted collectors (the same ones the cockpit\n // runs), so the snapshot this digest writes carries the `renovate:<siteId>` /\n // `ci:<siteId>` keys the cockpit diffs against — no live GitHub sweep here.\n const collected = await collectAttention({\n base,\n baseUrl: options.baseUrl,\n websites,\n reports,\n now: today,\n });\n const prior = await readDigestState(base);\n const { tagged, next } = diffAttention(collected, prior, digestDateKey(today));\n // The operator only HEARS about a vuln once Renovate's auto-fix is exhausted\n // (tried and failed a couple of nightly cycles) — before that the fleet is\n // still self-patching and the cockpit's amber Watch band is the only surface.\n // Filter the EMAIL list only: `next` (written below) must keep every vuln key\n // so the cockpit's diff against this same snapshot stays consistent, and so\n // the exhausted-flip diffs as WORSE instead of arriving pre-badged-away.\n const needsAttention = tagged.filter(\n (it) => it.kind !== \"vuln\" || it.autoFixExhausted === true,\n );\n\n // No-noise default: skip entirely when there's nothing to report.\n if (readyForYourYes.length === 0 && needsAttention.length === 0) {\n // Persist `next` even on a skip: a key that resolved on a quiet day clears so a\n // later recurrence diffs as NEW (spec §10), and — since the skip can now fire\n // with muted pre-exhaustion vulns collected — their keys keep snapshotting so\n // the eventual exhausted-flip diffs as WORSE. Wrapped: a write failure can't\n // fail the skip.\n try {\n await writeDigestState(base, next);\n } catch (e) {\n console.warn(`⚠ digest state write failed: ${(e as Error).message}`);\n }\n return { output: \"Digest skipped (nothing ready, nothing needs attention).\", code: 0 };\n }\n\n // Submissions telemetry rides only on a digest that is ALREADY sending — the\n // skip predicate above is deliberately unchanged (leads already fire their own\n // ingest-time notification; this is context, not an alarm), and fetching AFTER\n // the skip check keeps quiet days from touching libSQL at all.\n const counts =\n options.submissionCounts !== undefined\n ? options.submissionCounts\n : await fetchSubmissionsDigestCounts(today);\n const submissions = buildSubmissionsDigestSection(counts, sites);\n\n const html = renderDigestHtml({ readyForYourYes, needsAttention, submissions });\n const client = options.resend ?? defaultResendClient();\n const to = [operatorEmail()];\n const n = readyForYourYes.length;\n const reportWord = n === 1 ? \"report\" : \"reports\";\n let result: Awaited<ReturnType<typeof client.send>>;\n try {\n result = await client.send({\n from: FROM_ADDRESS,\n to,\n subject: `Your fleet — ${digestDateKey(today)}: ${n} ${reportWord} ready for your yes`,\n html,\n idempotencyKey: `digest-${digestDateKey(today)}`,\n });\n } catch (err) {\n // A same-UTC-day re-run whose content changed re-sends with the same\n // `digest-<date>` idempotency key but a DIFFERENT body. Resend rejects that\n // with a 409 (`invalid_idempotent_request`) — \"This idempotency key has been\n // used ... but the request body was modified ...\". The operator already got\n // today's digest on the first send, so re-sending a changed version same-day\n // would just be a duplicate: treat it as an \"already sent today\" no-op.\n //\n // ResendClient (send/resend.ts) wraps the API error as a plain Error and only\n // preserves the *message* string (no name/statusCode), so the message\n // substring is the only reliable discriminator — match defensively on it.\n // Any OTHER send error re-throws to the outer catch → {code:1}, so a genuine\n // Resend/network failure still fails loudly.\n if (isIdempotencyConflict(err)) {\n // Do NOT write the snapshot: the first send already persisted it; writing\n // this run's `next` would diff against the first run's snapshot and mis-badge.\n return {\n output:\n \"Digest already sent today (content changed since the first send) — skipped to avoid a duplicate.\",\n code: 0,\n };\n }\n throw err;\n }\n // Persist the next snapshot AFTER a successful send. A write failure is caught +\n // logged: the digest already went out, tomorrow re-news at worst. (The send-FAILURE\n // path never reaches here — the outer catch returns code 1 with no write, preserving\n // the NEW badge for the retry.)\n try {\n await writeDigestState(base, next);\n } catch (e) {\n console.warn(`⚠ digest state write failed: ${(e as Error).message}`);\n }\n return { output: `Digest sent to ${to.join(\", \")} (${result.messageId})`, code: 0 };\n } catch (err) {\n // Re-throw config errors (exitCode=2: missing env vars, bad config) so runOrExit\n // surfaces them with the correct process exit code rather than collapsing to 1.\n if (typeof (err as { exitCode?: unknown }).exitCode === \"number\") {\n throw err;\n }\n const message = err instanceof Error ? err.message : String(err);\n return { output: `digest failed: ${message}`, code: 1 };\n }\n}\n\n/** Pure render of the unified daily operator digest. No IO — the caller (runDigest)\n * collects the rows and decides whether to send. */\nexport function renderDigestHtml(sections: DigestSections): string {\n return `<!doctype html>\n<html>\n <head><meta charset=\"utf-8\"></head>\n <body style=\"margin:0;padding:0;background:#ffffff\">\n <table width=\"100%\" style=\"border-collapse:collapse\">\n <tr>\n <td align=\"center\" style=\"padding:24px\">\n <table width=\"600\" style=\"border-collapse:collapse\">\n <tr>\n <td>\n <h1 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:24px;font-weight:700;margin:0 0 8px\">Your fleet today</h1>\n ${readySection(sections.readyForYourYes)}\n ${attentionSection(sections.needsAttention)}\n ${submissionsSection(sections.submissions)}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>`;\n}\n"],"mappings":"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","names":[]}