@reddoorla/maintenance 0.97.0 → 0.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/{announce-5ZD2SBGD.js → announce-2FH2KBQQ.js} +4 -4
  2. package/dist/{bump-deps-775VLDYB.js → bump-deps-54K4ECP3.js} +2 -2
  3. package/dist/{chunk-YXWBM26P.js → chunk-3YALLSBF.js} +2 -2
  4. package/dist/{chunk-LAJ3FF54.js → chunk-5UI4FERC.js} +16 -5
  5. package/dist/chunk-5UI4FERC.js.map +1 -0
  6. package/dist/{chunk-VMGHQUTV.js → chunk-B2BRFWJY.js} +2 -2
  7. package/dist/{chunk-IOTOOTII.js → chunk-CP6MVZ5X.js} +16 -1
  8. package/dist/{chunk-IOTOOTII.js.map → chunk-CP6MVZ5X.js.map} +1 -1
  9. package/dist/{chunk-L3S3QZXL.js → chunk-DXZRWW2F.js} +64 -9
  10. package/dist/chunk-DXZRWW2F.js.map +1 -0
  11. package/dist/chunk-DZZ5KRTB.js +23 -0
  12. package/dist/chunk-DZZ5KRTB.js.map +1 -0
  13. package/dist/{chunk-U2ZI6G7H.js → chunk-GRF2RYEO.js} +2 -2
  14. package/dist/{chunk-QFSL2L2B.js → chunk-L5F3WYIY.js} +81 -31
  15. package/dist/chunk-L5F3WYIY.js.map +1 -0
  16. package/dist/{chunk-MMSMZC7S.js → chunk-MNRWHSFS.js} +2 -2
  17. package/dist/{chunk-PARKXDXI.js → chunk-ORTOMEYR.js} +2 -2
  18. package/dist/{chunk-FP6HOW6J.js → chunk-S2PLX4XK.js} +2 -2
  19. package/dist/{chunk-A5QKNOXW.js → chunk-TTT6HYPW.js} +47 -10
  20. package/dist/chunk-TTT6HYPW.js.map +1 -0
  21. package/dist/{chunk-BIYQN7FH.js → chunk-VCPTOZ56.js} +31 -1
  22. package/dist/{chunk-BIYQN7FH.js.map → chunk-VCPTOZ56.js.map} +1 -1
  23. package/dist/{chunk-LDHRIQUW.js → chunk-ZEE5WQYU.js} +2 -2
  24. package/dist/cli/bin.js +27 -27
  25. package/dist/cli/commands/audit.js +10 -10
  26. package/dist/client-3XGSZMLU.js +11 -0
  27. package/dist/{convert-to-pnpm-KLWA53N5.js → convert-to-pnpm-U2CRNKNY.js} +2 -2
  28. package/dist/{db-II5WAK4D.js → db-7XYTBIPH.js} +20 -13
  29. package/dist/db-7XYTBIPH.js.map +1 -0
  30. package/dist/{deadletter-G4M3HSVM.js → deadletter-A7RQVNG3.js} +8 -4
  31. package/dist/{digest-CWKK2DMR.js → digest-AMXRGWZA.js} +20 -20
  32. package/dist/{digest-collectors-CYTN2EUX.js → digest-collectors-CX2NCGS7.js} +7 -7
  33. package/dist/{ensure-site-2QZHFVQ3.js → ensure-site-F2WFCWWA.js} +2 -2
  34. package/dist/{forms-notify-target-X6UEOI2M.js → forms-notify-target-DK5R326N.js} +3 -3
  35. package/dist/{github-signals-QIRTHHET.js → github-signals-RGRPYTUB.js} +6 -6
  36. package/dist/{header-image-H7ZJAV26.js → header-image-43ZIEJKP.js} +3 -3
  37. package/dist/{health-endpoint-BJ6BKRHF.js → health-endpoint-K5LXDPGE.js} +2 -2
  38. package/dist/{health-mirror-KU3G7V2Y.js → health-mirror-WZUHWCCV.js} +3 -3
  39. package/dist/index.js +10 -11
  40. package/dist/index.js.map +1 -1
  41. package/dist/{init-GOWDA5QS.js → init-OFITGMOP.js} +5 -5
  42. package/dist/{launch-7N3X2MXB.js → launch-AJY3TGTQ.js} +7 -7
  43. package/dist/{lighthouse-52WSPZ5D.js → lighthouse-RFMFREEX.js} +2 -2
  44. package/dist/{match-harness-GBCDSVPS.js → match-harness-CFELJA57.js} +2 -2
  45. package/dist/migrate-EZL55AWA.js +7 -0
  46. package/dist/{onboard-JQQIMQW7.js → onboard-F7RTLMDZ.js} +2 -2
  47. package/dist/{orchestrate-L2A5BZFY.js → orchestrate-SAIFXZIW.js} +5 -5
  48. package/dist/{pipeline-UUPXOS2J.js → pipeline-TCSNVDQZ.js} +3 -3
  49. package/dist/{preflight-O7Q6FWHU.js → preflight-6XGVVO2U.js} +7 -7
  50. package/dist/{prismic-ci-NAPWOXXD.js → prismic-ci-XMX7A5E4.js} +2 -2
  51. package/dist/{prismic-models-LL6ORTQ2.js → prismic-models-7GKFAAFA.js} +4 -4
  52. package/dist/prospect/types.d.ts +44 -2
  53. package/dist/prospect/types.js +1 -1
  54. package/dist/{prospect-audit-FMPGQIRQ.js → prospect-audit-KWP5P5U2.js} +37 -5
  55. package/dist/prospect-audit-KWP5P5U2.js.map +1 -0
  56. package/dist/{render-MHUUHPLN.js → render-NWUAX6RN.js} +3 -3
  57. package/dist/{renovate-dispatch-2IHAZDVA.js → renovate-dispatch-ZZQXT7DA.js} +2 -2
  58. package/dist/replay-HB44TAII.js +77 -0
  59. package/dist/replay-HB44TAII.js.map +1 -0
  60. package/dist/{report-IAHXN5CW.js → report-MVPWS7WP.js} +13 -13
  61. package/dist/{report-mirror-4QM57LMS.js → report-mirror-O6IDFDWU.js} +3 -3
  62. package/dist/{roster-GDNSIRAK.js → roster-EJ3ILR52.js} +2 -2
  63. package/dist/{screenouts-7P5GOZMV.js → screenouts-7NQUWHOJ.js} +14 -1
  64. package/dist/screenouts-7NQUWHOJ.js.map +1 -0
  65. package/dist/{self-updating-ZUWPGYUP.js → self-updating-KX5FMGEO.js} +2 -2
  66. package/dist/{selftest-G4W3LIBQ.js → selftest-KABY2CYJ.js} +5 -5
  67. package/dist/{site-mirror-WR7ACXOV.js → site-mirror-TKTDWYP3.js} +3 -3
  68. package/dist/{smoke-suite-WAUPI6EJ.js → smoke-suite-QQMV7TNA.js} +2 -2
  69. package/dist/{submissions-JDZP35JM.js → submissions-4I66W6QK.js} +11 -1
  70. package/dist/{submissions-JDZP35JM.js.map → submissions-4I66W6QK.js.map} +1 -1
  71. package/dist/{submissions-2SHCFOKD.js → submissions-K5UXGZUN.js} +4 -4
  72. package/dist/{svelte-codemods-VOLOKKFI.js → svelte-codemods-YFN4TEGV.js} +2 -2
  73. package/dist/{sync-configs-OXSWKVZ2.js → sync-configs-HBXS3D7C.js} +2 -2
  74. package/dist/{upgrade-EVAL3JTO.js → upgrade-XOJMDIT4.js} +2 -2
  75. package/package.json +1 -1
  76. package/dist/chunk-A5QKNOXW.js.map +0 -1
  77. package/dist/chunk-L3S3QZXL.js.map +0 -1
  78. package/dist/chunk-LAJ3FF54.js.map +0 -1
  79. package/dist/chunk-QFSL2L2B.js.map +0 -1
  80. package/dist/client-DGTO65WB.js +0 -11
  81. package/dist/db-II5WAK4D.js.map +0 -1
  82. package/dist/migrate-XZ7U5DFC.js +0 -7
  83. package/dist/prospect-audit-FMPGQIRQ.js.map +0 -1
  84. package/dist/replay-4FVFOMHH.js +0 -54
  85. package/dist/replay-4FVFOMHH.js.map +0 -1
  86. package/dist/screenouts-7P5GOZMV.js.map +0 -1
  87. /package/dist/{announce-5ZD2SBGD.js.map → announce-2FH2KBQQ.js.map} +0 -0
  88. /package/dist/{bump-deps-775VLDYB.js.map → bump-deps-54K4ECP3.js.map} +0 -0
  89. /package/dist/{chunk-YXWBM26P.js.map → chunk-3YALLSBF.js.map} +0 -0
  90. /package/dist/{chunk-VMGHQUTV.js.map → chunk-B2BRFWJY.js.map} +0 -0
  91. /package/dist/{chunk-U2ZI6G7H.js.map → chunk-GRF2RYEO.js.map} +0 -0
  92. /package/dist/{chunk-MMSMZC7S.js.map → chunk-MNRWHSFS.js.map} +0 -0
  93. /package/dist/{chunk-PARKXDXI.js.map → chunk-ORTOMEYR.js.map} +0 -0
  94. /package/dist/{chunk-FP6HOW6J.js.map → chunk-S2PLX4XK.js.map} +0 -0
  95. /package/dist/{chunk-LDHRIQUW.js.map → chunk-ZEE5WQYU.js.map} +0 -0
  96. /package/dist/{client-DGTO65WB.js.map → client-3XGSZMLU.js.map} +0 -0
  97. /package/dist/{convert-to-pnpm-KLWA53N5.js.map → convert-to-pnpm-U2CRNKNY.js.map} +0 -0
  98. /package/dist/{deadletter-G4M3HSVM.js.map → deadletter-A7RQVNG3.js.map} +0 -0
  99. /package/dist/{digest-CWKK2DMR.js.map → digest-AMXRGWZA.js.map} +0 -0
  100. /package/dist/{digest-collectors-CYTN2EUX.js.map → digest-collectors-CX2NCGS7.js.map} +0 -0
  101. /package/dist/{ensure-site-2QZHFVQ3.js.map → ensure-site-F2WFCWWA.js.map} +0 -0
  102. /package/dist/{forms-notify-target-X6UEOI2M.js.map → forms-notify-target-DK5R326N.js.map} +0 -0
  103. /package/dist/{github-signals-QIRTHHET.js.map → github-signals-RGRPYTUB.js.map} +0 -0
  104. /package/dist/{header-image-H7ZJAV26.js.map → header-image-43ZIEJKP.js.map} +0 -0
  105. /package/dist/{health-endpoint-BJ6BKRHF.js.map → health-endpoint-K5LXDPGE.js.map} +0 -0
  106. /package/dist/{health-mirror-KU3G7V2Y.js.map → health-mirror-WZUHWCCV.js.map} +0 -0
  107. /package/dist/{init-GOWDA5QS.js.map → init-OFITGMOP.js.map} +0 -0
  108. /package/dist/{launch-7N3X2MXB.js.map → launch-AJY3TGTQ.js.map} +0 -0
  109. /package/dist/{lighthouse-52WSPZ5D.js.map → lighthouse-RFMFREEX.js.map} +0 -0
  110. /package/dist/{match-harness-GBCDSVPS.js.map → match-harness-CFELJA57.js.map} +0 -0
  111. /package/dist/{migrate-XZ7U5DFC.js.map → migrate-EZL55AWA.js.map} +0 -0
  112. /package/dist/{onboard-JQQIMQW7.js.map → onboard-F7RTLMDZ.js.map} +0 -0
  113. /package/dist/{orchestrate-L2A5BZFY.js.map → orchestrate-SAIFXZIW.js.map} +0 -0
  114. /package/dist/{pipeline-UUPXOS2J.js.map → pipeline-TCSNVDQZ.js.map} +0 -0
  115. /package/dist/{preflight-O7Q6FWHU.js.map → preflight-6XGVVO2U.js.map} +0 -0
  116. /package/dist/{prismic-ci-NAPWOXXD.js.map → prismic-ci-XMX7A5E4.js.map} +0 -0
  117. /package/dist/{prismic-models-LL6ORTQ2.js.map → prismic-models-7GKFAAFA.js.map} +0 -0
  118. /package/dist/{render-MHUUHPLN.js.map → render-NWUAX6RN.js.map} +0 -0
  119. /package/dist/{renovate-dispatch-2IHAZDVA.js.map → renovate-dispatch-ZZQXT7DA.js.map} +0 -0
  120. /package/dist/{report-IAHXN5CW.js.map → report-MVPWS7WP.js.map} +0 -0
  121. /package/dist/{report-mirror-4QM57LMS.js.map → report-mirror-O6IDFDWU.js.map} +0 -0
  122. /package/dist/{roster-GDNSIRAK.js.map → roster-EJ3ILR52.js.map} +0 -0
  123. /package/dist/{self-updating-ZUWPGYUP.js.map → self-updating-KX5FMGEO.js.map} +0 -0
  124. /package/dist/{selftest-G4W3LIBQ.js.map → selftest-KABY2CYJ.js.map} +0 -0
  125. /package/dist/{site-mirror-WR7ACXOV.js.map → site-mirror-TKTDWYP3.js.map} +0 -0
  126. /package/dist/{smoke-suite-WAUPI6EJ.js.map → smoke-suite-QQMV7TNA.js.map} +0 -0
  127. /package/dist/{submissions-2SHCFOKD.js.map → submissions-K5UXGZUN.js.map} +0 -0
  128. /package/dist/{svelte-codemods-VOLOKKFI.js.map → svelte-codemods-YFN4TEGV.js.map} +0 -0
  129. /package/dist/{sync-configs-OXSWKVZ2.js.map → sync-configs-HBXS3D7C.js.map} +0 -0
  130. /package/dist/{upgrade-EVAL3JTO.js.map → upgrade-XOJMDIT4.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runMigrations
3
- } from "./chunk-IOTOOTII.js";
3
+ } from "./chunk-CP6MVZ5X.js";
4
4
  import {
5
5
  defaultCredentialsPath
6
6
  } from "./chunk-6VAL7XJT.js";
@@ -48,4 +48,4 @@ export {
48
48
  readDbConfig,
49
49
  openDb
50
50
  };
51
- //# sourceMappingURL=chunk-MMSMZC7S.js.map
51
+ //# sourceMappingURL=chunk-MNRWHSFS.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  approveBlockers
3
- } from "./chunk-VMGHQUTV.js";
3
+ } from "./chunk-B2BRFWJY.js";
4
4
  import {
5
5
  ACTIVE_STATUSES,
6
6
  allActionableVulnsTransitive,
@@ -337,4 +337,4 @@ export {
337
337
  prismicAckIsLive,
338
338
  collectPrismicDriftAlerts
339
339
  };
340
- //# sourceMappingURL=chunk-PARKXDXI.js.map
340
+ //# sourceMappingURL=chunk-ORTOMEYR.js.map
@@ -33,7 +33,7 @@ async function resolveSites(input) {
33
33
  }
34
34
  if (input.fleet === FLEET_KEYWORD || input.fleet === DEPRECATED_FLEET_KEYWORD) {
35
35
  if (input.fleet === DEPRECATED_FLEET_KEYWORD) console.warn(DEPRECATED_FLEET_KEYWORD_WARNING);
36
- const { openDb, readDbConfig } = await import("./client-DGTO65WB.js");
36
+ const { openDb, readDbConfig } = await import("./client-3XGSZMLU.js");
37
37
  const { fromTursoDb } = await import("./turso-KQKBBQCK.js");
38
38
  const open = input.openDb ?? (() => openDb(readDbConfig()));
39
39
  return fromTursoDb(open, input.workdir ? { workdir: input.workdir } : {})();
@@ -64,4 +64,4 @@ async function resolveSites(input) {
64
64
  export {
65
65
  resolveSites
66
66
  };
67
- //# sourceMappingURL=chunk-FP6HOW6J.js.map
67
+ //# sourceMappingURL=chunk-S2PLX4XK.js.map
@@ -17,16 +17,47 @@ async function createDeadLetter(db, input) {
17
17
  }).execute();
18
18
  return { id };
19
19
  }
20
+ async function readUnreplayedDeadLetters(db) {
21
+ const raw = await db.selectFrom("submission_deadletter").select(["id", "site_slug", "payload", "turnstile", "error", "received_at"]).where("replayed_at", "is", null).where("abandoned_at", "is", null).orderBy("received_at", "asc").execute();
22
+ const queue = { rows: [], unreadable: [] };
23
+ for (const r of raw) {
24
+ let payload;
25
+ try {
26
+ payload = JSON.parse(r.payload);
27
+ } catch (err) {
28
+ queue.unreadable.push({
29
+ id: r.id,
30
+ siteSlug: r.site_slug,
31
+ error: `payload is not decodable JSON: ${String(err)}`,
32
+ receivedAt: r.received_at
33
+ });
34
+ continue;
35
+ }
36
+ let turnstile;
37
+ try {
38
+ turnstile = JSON.parse(r.turnstile);
39
+ } catch (err) {
40
+ queue.unreadable.push({
41
+ id: r.id,
42
+ siteSlug: r.site_slug,
43
+ error: `turnstile is not decodable JSON: ${String(err)}`,
44
+ receivedAt: r.received_at
45
+ });
46
+ continue;
47
+ }
48
+ queue.rows.push({
49
+ id: r.id,
50
+ siteSlug: r.site_slug,
51
+ payload,
52
+ turnstile,
53
+ error: r.error,
54
+ receivedAt: r.received_at
55
+ });
56
+ }
57
+ return queue;
58
+ }
20
59
  async function listUnreplayedDeadLetters(db) {
21
- const rows = await db.selectFrom("submission_deadletter").select(["id", "site_slug", "payload", "turnstile", "error", "received_at"]).where("replayed_at", "is", null).where("abandoned_at", "is", null).orderBy("received_at", "asc").execute();
22
- return rows.map((r) => ({
23
- id: r.id,
24
- siteSlug: r.site_slug,
25
- payload: JSON.parse(r.payload),
26
- turnstile: JSON.parse(r.turnstile),
27
- error: r.error,
28
- receivedAt: r.received_at
29
- }));
60
+ return (await readUnreplayedDeadLetters(db)).rows;
30
61
  }
31
62
  async function markDeadLetterReplayed(db, id, outcome, submissionId, now) {
32
63
  await db.updateTable("submission_deadletter").set({
@@ -39,6 +70,10 @@ async function countUnreplayedDeadLettersBySlug(db) {
39
70
  const rows = await db.selectFrom("submission_deadletter").select((eb) => ["site_slug", eb.fn.countAll().as("n")]).where("replayed_at", "is", null).where("abandoned_at", "is", null).groupBy("site_slug").execute();
40
71
  return new Map(rows.map((r) => [r.site_slug, Number(r.n)]));
41
72
  }
73
+ async function countUnreplayedDeadLettersForSlug(db, slug) {
74
+ const row = await db.selectFrom("submission_deadletter").select((eb) => eb.fn.countAll().as("n")).where("site_slug", "=", slug).where("replayed_at", "is", null).where("abandoned_at", "is", null).executeTakeFirst();
75
+ return Number(row?.n) || 0;
76
+ }
42
77
  async function abandonDeadLetters(db, opts) {
43
78
  const bySlug = opts.slug !== void 0;
44
79
  const byId = opts.id !== void 0;
@@ -64,9 +99,11 @@ async function abandonDeadLetters(db, opts) {
64
99
  export {
65
100
  newDeadLetterId,
66
101
  createDeadLetter,
102
+ readUnreplayedDeadLetters,
67
103
  listUnreplayedDeadLetters,
68
104
  markDeadLetterReplayed,
69
105
  countUnreplayedDeadLettersBySlug,
106
+ countUnreplayedDeadLettersForSlug,
70
107
  abandonDeadLetters
71
108
  };
72
- //# sourceMappingURL=chunk-A5QKNOXW.js.map
109
+ //# sourceMappingURL=chunk-TTT6HYPW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/deadletter.ts"],"sourcesContent":["import type { Db } from \"./client.js\";\nimport type { TurnstileVerification } from \"../forms/turnstile.js\";\n\n/** One captured-but-unresolved lead: the site lookup threw before the normal\n * pipeline could run (Phase 0 of #539 — persist before enrich). */\nexport type DeadLetterRow = {\n id: string;\n siteSlug: string;\n /** The raw wire payload, exactly as the handler received it. */\n payload: unknown;\n /** The Turnstile verification computed at receipt — tokens expire in 300s,\n * so replay reuses this answer rather than re-verifying. */\n turnstile: TurnstileVerification;\n error: string;\n receivedAt: string;\n};\n\nexport type DeadLetterInput = {\n siteSlug: string;\n payload: unknown;\n turnstile: TurnstileVerification;\n error: string;\n receivedAt: Date;\n};\n\n/** Opaque, collision-free id (mirrors newSubmissionId). */\nexport function newDeadLetterId(): string {\n return `dl_${crypto.randomUUID()}`;\n}\n\n/** Persist a lead the normal pipeline could not place. This is the LAST writer\n * on the ingest path — if it throws too, both stores are down and the caller's\n * 502 is honest. */\nexport async function createDeadLetter(db: Db, input: DeadLetterInput): Promise<{ id: string }> {\n const id = newDeadLetterId();\n await db\n .insertInto(\"submission_deadletter\")\n .values({\n id,\n site_slug: input.siteSlug,\n payload: JSON.stringify(input.payload),\n turnstile: JSON.stringify(input.turnstile),\n error: input.error,\n received_at: input.receivedAt.toISOString(),\n replayed_at: null,\n replay_outcome: null,\n replay_submission_id: null,\n })\n .execute();\n return { id };\n}\n\n/** A queued row whose stored JSON could not be decoded. Carries no payload by\n * construction: the bytes that failed to parse are exactly the bytes nothing\n * should be guessing at, and they may hold a client's PII. */\nexport type UnreadableDeadLetterRow = {\n id: string;\n siteSlug: string;\n /** Which column failed, and the parser's own message. */\n error: string;\n receivedAt: string;\n};\n\nexport type DeadLetterQueue = {\n /** Rows that decoded — the ones replay can actually run. */\n rows: DeadLetterRow[];\n /** Rows that did not. Reported, never dropped: they stay queued, they still\n * count toward the cockpit alarm (which is computed in SQL and never touches\n * these columns), and #786's `--abandon` is the only way to retire one. */\n unreadable: UnreadableDeadLetterRow[];\n};\n\n/** Rows not yet replayed, oldest first — replay preserves arrival order so the\n * duplicate/velocity signals see submissions in the order they happened.\n *\n * MED-10(c). Decoding is PER ROW. This used to `JSON.parse` inside a `.map`\n * over the whole result set, so a single undecodable payload threw before any\n * row was returned and wedged replay for every other lead in the queue — the\n * `import-reap` shape, where one bad record froze the sync permanently. A row\n * that cannot be decoded is now separated out and handed back for the caller\n * to surface; the rows around it replay normally.\n */\nexport async function readUnreplayedDeadLetters(db: Db): Promise<DeadLetterQueue> {\n const raw = await db\n .selectFrom(\"submission_deadletter\")\n .select([\"id\", \"site_slug\", \"payload\", \"turnstile\", \"error\", \"received_at\"])\n .where(\"replayed_at\", \"is\", null)\n // #786: an abandoned row is terminal by decision — it must leave the replay\n // queue, or the command it was meant to release stays at exit 1 forever.\n .where(\"abandoned_at\", \"is\", null)\n .orderBy(\"received_at\", \"asc\")\n .execute();\n\n const queue: DeadLetterQueue = { rows: [], unreadable: [] };\n for (const r of raw) {\n let payload: unknown;\n try {\n payload = JSON.parse(r.payload) as unknown;\n } catch (err) {\n queue.unreadable.push({\n id: r.id,\n siteSlug: r.site_slug,\n error: `payload is not decodable JSON: ${String(err)}`,\n receivedAt: r.received_at,\n });\n continue;\n }\n let turnstile: TurnstileVerification;\n try {\n turnstile = JSON.parse(r.turnstile) as TurnstileVerification;\n } catch (err) {\n queue.unreadable.push({\n id: r.id,\n siteSlug: r.site_slug,\n error: `turnstile is not decodable JSON: ${String(err)}`,\n receivedAt: r.received_at,\n });\n continue;\n }\n queue.rows.push({\n id: r.id,\n siteSlug: r.site_slug,\n payload,\n turnstile,\n error: r.error,\n receivedAt: r.received_at,\n });\n }\n return queue;\n}\n\n/** The decodable rows only. Kept for callers that have no way to act on an\n * undecodable row; anything that REPLAYS must use `readUnreplayedDeadLetters`\n * so the rows it skipped are reported rather than silently missing. */\nexport async function listUnreplayedDeadLetters(db: Db): Promise<DeadLetterRow[]> {\n return (await readUnreplayedDeadLetters(db)).rows;\n}\n\n/** Mark a row's replay TERMINAL — it will never be picked up again. Only call\n * for outcomes that re-running cannot improve (accepted, rejected). A replay\n * whose lookup threw again is left untouched so the next run retries it — and\n * since #645 so is `unknown-site`, because `ensure-site` can heal the missing\n * Turso row and make the same replay succeed. */\nexport async function markDeadLetterReplayed(\n db: Db,\n id: string,\n outcome: string,\n submissionId: string | null,\n now: Date,\n): Promise<void> {\n await db\n .updateTable(\"submission_deadletter\")\n .set({\n replayed_at: now.toISOString(),\n replay_outcome: outcome,\n replay_submission_id: submissionId,\n })\n .where(\"id\", \"=\", id)\n .execute();\n}\n\n/** Unreplayed rows per site slug — the input to the `deadletter` attention\n * collector (#645). Grouped in SQL rather than by listing and counting in JS:\n * the rows carry full lead payloads, and an alarm must never pull a client's\n * PII into a dashboard request just to learn how many rows there are. */\nexport async function countUnreplayedDeadLettersBySlug(\n db: Db,\n): Promise<ReadonlyMap<string, number>> {\n const rows = await db\n .selectFrom(\"submission_deadletter\")\n .select((eb) => [\"site_slug\", eb.fn.countAll<number>().as(\"n\")])\n .where(\"replayed_at\", \"is\", null)\n // #786: and out of the alarm count, which is the standing CRITICAL item.\n .where(\"abandoned_at\", \"is\", null)\n .groupBy(\"site_slug\")\n .execute();\n return new Map(rows.map((r) => [r.site_slug, Number(r.n)]));\n}\n\n/** The same count for ONE slug — the `/s/:slug` dead-letter chip (MED-11).\n *\n * The fleet homepage genuinely needs the whole map (it renders every site), so\n * `countUnreplayedDeadLettersBySlug` stays. The site page did not: it built the\n * fleet-wide map and read one key out of it.\n *\n * Needs `idx_deadletter_slug_unreplayed` (0028) to be a per-slug read rather\n * than a per-slug filter. Without it SQLite serves the predicate from\n * `idx_deadletter_unreplayed (replayed_at)` and visits every unreplayed row in\n * the fleet, then a rowid lookup into the payload-bearing table for each — the\n * exact cost the grouped version's docblock says it exists to avoid. The plan\n * still reads `SEARCH`, which is why the query-plan gate cannot see the\n * difference and the index has to be a deliberate choice. */\nexport async function countUnreplayedDeadLettersForSlug(db: Db, slug: string): Promise<number> {\n const row = await db\n .selectFrom(\"submission_deadletter\")\n .select((eb) => eb.fn.countAll<number>().as(\"n\"))\n .where(\"site_slug\", \"=\", slug)\n .where(\"replayed_at\", \"is\", null)\n .where(\"abandoned_at\", \"is\", null)\n .executeTakeFirst();\n return Number(row?.n) || 0;\n}\n\n/** Abandon dead letters as resolved-by-DECISION (#786).\n *\n * The escape hatch #785 left open. `unknown-site` is deliberately non-terminal\n * on replay so that healing and replaying are safe in either order — but that\n * means a slug which is genuinely dead, and still deployed and posting, grows\n * the queue without bound, holds `db replay-deadletters` at exit 1, and leaves\n * a standing CRITICAL cockpit item. The only alternative today is deleting\n * rows by hand, which destroys the very leads the decision was made about.\n *\n * Targets ONE slug or ONE row id, never \"everything\": a mistyped invocation\n * must not be able to write off the whole queue. Only rows that are still live\n * (neither replayed nor already abandoned) are touched, which makes the call\n * idempotent and keeps the FIRST decision — the one actually made — in place.\n * `reason` is required and non-blank, because an undocumented write-off is the\n * thing this exists to stop being necessary.\n *\n * Returns the ids it abandoned, so the CLI can name them rather than print a\n * bare count.\n */\nexport async function abandonDeadLetters(\n db: Db,\n opts: { slug?: string; id?: string; by: string; reason: string; now: Date },\n): Promise<string[]> {\n const bySlug = opts.slug !== undefined;\n const byId = opts.id !== undefined;\n if (bySlug === byId) {\n throw new Error(\"abandonDeadLetters: pass exactly one of slug or id\");\n }\n const reason = opts.reason.trim();\n if (reason === \"\") {\n throw new Error(\"abandonDeadLetters: a non-blank reason is required\");\n }\n\n // Read the target ids first so the caller can be told exactly what was\n // abandoned; the same predicate then guards the write.\n let q = db\n .selectFrom(\"submission_deadletter\")\n .select(\"id\")\n .where(\"replayed_at\", \"is\", null)\n .where(\"abandoned_at\", \"is\", null);\n q = bySlug ? q.where(\"site_slug\", \"=\", opts.slug!) : q.where(\"id\", \"=\", opts.id!);\n const ids = (await q.execute()).map((r) => r.id);\n if (ids.length === 0) return [];\n\n await db\n .updateTable(\"submission_deadletter\")\n .set({\n abandoned_at: opts.now.toISOString(),\n abandoned_by: opts.by,\n abandoned_reason: reason,\n })\n .where(\"id\", \"in\", ids)\n // Re-asserted on the write: between the read and here nothing else runs\n // (libSQL is single-writer), but the guard costs nothing and keeps the\n // write's meaning legible on its own.\n .where(\"replayed_at\", \"is\", null)\n .where(\"abandoned_at\", \"is\", null)\n .execute();\n return ids;\n}\n"],"mappings":"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","names":[]}
@@ -50,6 +50,35 @@ async function readSiteConfig(sitePath) {
50
50
  if (gate === "dev" || gate === "preview") out.gateServer = gate;
51
51
  return out;
52
52
  }
53
+ var PLACEHOLDER_PRISMIC_REPO = "your-prismic-repo-name";
54
+ var PRISMIC_CONFIG_FILES = ["slicemachine.config.json", "prismic.config.json"];
55
+ async function readsPlaceholderPrismicRepo(sitePath) {
56
+ let sawPlaceholder = false;
57
+ for (const name of PRISMIC_CONFIG_FILES) {
58
+ let raw;
59
+ try {
60
+ raw = await readFile(join(sitePath, name), "utf-8");
61
+ } catch {
62
+ continue;
63
+ }
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(raw);
67
+ } catch {
68
+ continue;
69
+ }
70
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
71
+ const repo = parsed.repositoryName;
72
+ if (typeof repo !== "string") continue;
73
+ const trimmed = repo.trim();
74
+ if (trimmed === PLACEHOLDER_PRISMIC_REPO) {
75
+ sawPlaceholder = true;
76
+ continue;
77
+ }
78
+ if (trimmed.length > 0) return false;
79
+ }
80
+ return sawPlaceholder;
81
+ }
53
82
 
54
83
  // src/audits/lighthouse.ts
55
84
  async function readJsonMaybe(path) {
@@ -240,6 +269,7 @@ async function lighthouseAudit(ctx) {
240
269
 
241
270
  export {
242
271
  readSiteConfig,
272
+ readsPlaceholderPrismicRepo,
243
273
  lighthouseAudit
244
274
  };
245
- //# sourceMappingURL=chunk-BIYQN7FH.js.map
275
+ //# sourceMappingURL=chunk-VCPTOZ56.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/audits/lighthouse.ts","../src/audits/util/site-config.ts"],"sourcesContent":["import { readFile, writeFile, mkdtemp, rm, readdir } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AuditResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { lighthouseConfig } from \"../configs/lighthouse.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { SpawnFn, SpawnResult } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { readSiteConfig } from \"./util/site-config.js\";\nimport { findFreePort, withFreePort } from \"../util/free-port.js\";\n\ntype ManifestEntry = {\n url: string;\n summary: Record<string, number>;\n htmlPath?: string;\n jsonPath?: string;\n};\n\ntype AssertionResult = {\n name: string;\n actual: number;\n expected: number;\n operator: string;\n passed: boolean;\n level: \"warn\" | \"error\";\n auditProperty?: string;\n auditId?: string;\n};\n\ntype NormalizedLhciResult = {\n summary: Record<string, number>;\n assertionsFailed: number;\n assertions: Array<{ category: string; level: \"warn\" | \"error\"; message: string }>;\n};\n\nasync function readJsonMaybe<T>(path: string): Promise<T | null> {\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\ntype LhrFile = {\n requestedUrl: string;\n finalUrl?: string;\n categories: Record<string, { score: number | null }>;\n};\n\n/**\n * Build manifest-equivalent entries by scanning the `.lighthouseci/` dir\n * for `lhr-*.json` files written by `lhci collect`. We used to read\n * `manifest.json` directly, but lhci 0.15+ no longer writes it — the\n * audit would silently return \"no manifest written\" against a perfectly\n * healthy run. Reproduced on caltex 2026-05-28 (0.10.5 dogfood).\n */\nasync function readLhrEntries(resultsDir: string): Promise<ManifestEntry[]> {\n const files = await readdir(resultsDir).catch(() => [] as string[]);\n const entries: ManifestEntry[] = [];\n for (const f of files) {\n if (!f.startsWith(\"lhr-\") || !f.endsWith(\".json\")) continue;\n const lhr = await readJsonMaybe<LhrFile>(join(resultsDir, f));\n if (!lhr || !lhr.categories) continue;\n const summary: Record<string, number> = {};\n for (const [k, v] of Object.entries(lhr.categories)) {\n if (typeof v?.score === \"number\") summary[k] = v.score;\n }\n entries.push({ url: lhr.requestedUrl, summary });\n }\n return entries;\n}\n\nfunction averageSummaries(entries: ManifestEntry[]): Record<string, number> {\n if (entries.length === 0) return {};\n const sums: Record<string, number> = {};\n const counts: Record<string, number> = {};\n for (const e of entries) {\n for (const [k, v] of Object.entries(e.summary ?? {})) {\n if (typeof v !== \"number\") continue;\n sums[k] = (sums[k] ?? 0) + v;\n counts[k] = (counts[k] ?? 0) + 1;\n }\n }\n const out: Record<string, number> = {};\n for (const k of Object.keys(sums)) {\n const total = sums[k] ?? 0;\n const count = counts[k] ?? 1;\n out[k] = total / count;\n }\n return out;\n}\n\nfunction categoryFromAssertion(a: AssertionResult): string {\n // `name` looks like \"categories:accessibility\" or \"audits:uses-http2\".\n const colonIdx = a.name.indexOf(\":\");\n return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;\n}\n\nfunction messageForAssertion(a: AssertionResult): string {\n // `a.actual` is parsed from external lhci/Lighthouse JSON; a malformed or\n // missing value (not a number) would make `.toFixed` throw and crash the whole\n // audit. Guard it and fall back to a readable string instead.\n const actual = typeof a.actual === \"number\" ? a.actual.toFixed(2) : \"n/a\";\n return `${a.name} ${a.operator} ${a.expected} (actual: ${actual})`;\n}\n\n/** Shared tail: scan `.lighthouseci/` for lhr-*.json + assertion-results.json and\n * build the AuditResult. Identical for the checkout and deployed paths. */\nasync function parseLhciResults(\n resultsDir: string,\n label: string,\n raw: SpawnResult,\n): Promise<AuditResult> {\n const manifest = await readLhrEntries(resultsDir);\n\n if (manifest.length === 0) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"fail\",\n summary: `lighthouse: no lhr-*.json written (exit ${raw.code})${\n raw.stderr ? ` — ${raw.stderr.slice(0, 200)}` : \"\"\n }`,\n };\n }\n\n const assertionResults =\n (await readJsonMaybe<AssertionResult[]>(join(resultsDir, \"assertion-results.json\"))) ?? [];\n\n const failed = assertionResults.filter((a) => !a.passed);\n const assertions = failed.map((a) => ({\n category: categoryFromAssertion(a),\n level: a.level,\n message: messageForAssertion(a),\n }));\n\n const anyError = assertions.some((a) => a.level === \"error\");\n const anyWarn = assertions.some((a) => a.level === \"warn\");\n const status: AuditResult[\"status\"] = anyError ? \"fail\" : anyWarn ? \"warn\" : \"pass\";\n\n const normalized: NormalizedLhciResult = {\n summary: averageSummaries(manifest),\n assertionsFailed: failed.length,\n assertions,\n };\n\n const summary =\n status === \"pass\"\n ? \"lighthouse: all categories passing\"\n : `lighthouse: ${failed.length} assertion(s) failed`;\n\n return { audit: \"lighthouse\", site: label, status, summary, details: normalized };\n}\n\n/** Checkout mode (unchanged behavior): boot the site's vite dev server on a\n * pinned free port and audit the local fixtures/override URL. */\nasync function checkoutLighthouse(spawn: SpawnFn, site: Site, label: string): Promise<AuditResult> {\n const siteCfg = await readSiteConfig(site.path);\n // Allocate a free port + force vite to `--strictPort` so the spawned dev\n // server either binds the port we picked or fails loudly (caltex 2026-05-28\n // zombie-vite incident).\n const port = await findFreePort();\n const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];\n const resolvedConfig = {\n ...lighthouseConfig,\n ci: {\n ...lighthouseConfig.ci,\n collect: {\n ...lighthouseConfig.ci.collect,\n url: [withFreePort(baseUrl, port)],\n startServerCommand: `npm run vite:dev -- --port ${port} --strictPort`,\n },\n },\n };\n\n const configDir = await mkdtemp(join(tmpdir(), \"reddoor-lhci-\"));\n const configPath = join(configDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(site.path, \".lighthouseci\");\n await rm(resultsDir, { recursive: true, force: true });\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: site.path,\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(configDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n await rm(configDir, { recursive: true, force: true });\n\n return parseLhciResults(resultsDir, label, raw);\n}\n\n/** Deployed mode: audit a production URL directly — no checkout, no dev server.\n * Runs in a throwaway tmp cwd; uploads to the filesystem so fleet runs never\n * push 200 public reports to temporary-public-storage. */\nasync function deployedLighthouse(\n spawn: SpawnFn,\n deployedUrl: string,\n label: string,\n): Promise<AuditResult> {\n const workDir = await mkdtemp(join(tmpdir(), \"reddoor-lh-deployed-\"));\n const resolvedConfig = {\n ci: {\n // Deliberately NOT spread from lighthouseConfig.ci.collect: deployed mode\n // must omit startServerCommand and the dev-server settings entirely.\n collect: {\n url: [deployedUrl],\n // 3 runs to damp Lighthouse's run-to-run variance; parseLhciResults\n // averages the lhr files. (Median is a tracked future refinement.)\n numberOfRuns: 3,\n // Use devtools (applied) throttling instead of the desktop preset's\n // default `simulate` (Lantern). Lantern estimates LCP from the\n // dependency graph and can't model an LCP element revealed by a\n // JS-load opacity fade (e.g. gallerysonder's GridImage): on a fast\n // observed load it finds no LCP node and throws NO_LCP, nulling the\n // perf score, which the cockpit then coerces to a misleading 0.\n // devtools reads the real LCP paint event from the throttled trace\n // instead. Trade-off: slightly noisier run-to-run, damped by the\n // numberOfRuns:3 average above.\n settings: { preset: \"desktop\", throttlingMethod: \"devtools\", skipAudits: [\"uses-http2\"] },\n },\n assert: lighthouseConfig.ci.assert,\n upload: { target: \"filesystem\", outputDir: join(workDir, \"lhci-report\") },\n },\n };\n\n const configPath = join(workDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(workDir, \".lighthouseci\");\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: workDir,\n // 3 serial cold runs of a slow deployed site (lhci's own maxWaitForLoad\n // ~45-60s each) + first-use Chrome download can plausibly exceed 3 min →\n // SIGTERM → no lhr-*.json → spurious \"no scores\". Match the 5-min budget\n // the checkout path already gives (erp-industrials nightly flake,\n // morning-brief 2026-06-10 MEDIUM-F).\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(workDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n\n try {\n return await parseLhciResults(resultsDir, label, raw);\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\nexport async function lighthouseAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n return site.deployedUrl\n ? deployedLighthouse(spawn, site.deployedUrl, label)\n : checkoutLighthouse(spawn, site, label);\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport type SiteConfig = {\n /** Override URL the lighthouse audit hits. Sites without the default\n * `/dev/a11y-fixtures` dev route set this to their homepage. */\n lighthouseUrl?: string;\n /**\n * Real site routes the a11y audit should axe-scan, IN ADDITION to the two\n * synthetic fixture pages it always covers. Opt-in per site and absent by\n * default: the audit runs with `--fail-on-violations` in the shared CI\n * workflow, and most of the fleet carries pre-existing a11y debt, so scanning\n * real routes everywhere at once would red every repo. A site adopts this once\n * its routes are clean. Omitted (never `[]`) when unset or unusable.\n */\n a11yRoutes?: string[];\n /**\n * Which server the browser gates run against: `\"dev\"` (the default — `vite\n * dev`) or `\"preview\"` (a real `vite build`, served by `vite preview`).\n *\n * Both gates measured the dev server, so the production bundle was built in\n * CI and then never opened by a browser — and dev does not merely fail to\n * reproduce some defects, it hides them. Module graph, code splitting,\n * minification and asset hashing are most of what \"hydration works\" means,\n * and a stylesheet can even be fetched under a different CSP directive in\n * each (#700).\n *\n * Opt-in, because a preview costs a `pnpm build` per run and because the\n * `/dev/*` fixture routes the axe scan targets are not guaranteed to survive\n * one — a site that flipped this without checking would trade a working gate\n * for one reporting every fixture as a missing route. Omitted (never\n * defaulted here) so each caller states its own default.\n *\n * Requires a `preview` script in the site's package.json; the starter has one.\n */\n gateServer?: \"dev\" | \"preview\";\n};\n\n/**\n * Read per-site overrides from `package.json#reddoor`. Returns `{}` on any\n * failure (missing file, malformed JSON, missing key, wrong type) so every\n * caller can safely fall back to its built-in default. Never throws.\n */\nexport async function readSiteConfig(sitePath: string): Promise<SiteConfig> {\n let raw: string;\n try {\n raw = await readFile(join(sitePath, \"package.json\"), \"utf-8\");\n } catch {\n return {};\n }\n let pkg: unknown;\n try {\n pkg = JSON.parse(raw);\n } catch {\n return {};\n }\n if (!pkg || typeof pkg !== \"object\") return {};\n const cfg = (pkg as { reddoor?: unknown }).reddoor;\n if (!cfg || typeof cfg !== \"object\") return {};\n\n const out: SiteConfig = {};\n const url = (cfg as { lighthouseUrl?: unknown }).lighthouseUrl;\n if (typeof url === \"string\" && url.length > 0) {\n out.lighthouseUrl = url;\n }\n const routes = (cfg as { a11yRoutes?: unknown }).a11yRoutes;\n if (Array.isArray(routes)) {\n // Junk entries are dropped rather than passed through — a `null` in the list\n // would become `page.goto(null)` inside the generated spec and fail the whole\n // audit for a typo. An all-junk list leaves the key omitted, so the caller's\n // \"no routes configured\" branch is the same shape as \"key absent\".\n const clean = routes\n .filter((r): r is string => typeof r === \"string\")\n .map((r) => r.trim())\n .filter((r) => r.length > 0);\n if (clean.length > 0) out.a11yRoutes = clean;\n }\n // Anything but the two known values reads as absent. This string reaches a\n // shell as part of a `webServer.command`, so a typo must fall back to the\n // default rather than be forwarded — `\"prod\"` would become `npm run prod`.\n const gate = (cfg as { gateServer?: unknown }).gateServer;\n if (gate === \"dev\" || gate === \"preview\") out.gateServer = gate;\n\n return out;\n}\n"],"mappings":"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","names":["readFile","join","readFile","join"]}
1
+ {"version":3,"sources":["../src/audits/lighthouse.ts","../src/audits/util/site-config.ts"],"sourcesContent":["import { readFile, writeFile, mkdtemp, rm, readdir } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AuditResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { lighthouseConfig } from \"../configs/lighthouse.js\";\nimport { defaultSpawn } from \"./util/spawn.js\";\nimport type { SpawnFn, SpawnResult } from \"./util/spawn.js\";\nimport type { AuditContext } from \"./util/inject.js\";\nimport { readSiteConfig } from \"./util/site-config.js\";\nimport { findFreePort, withFreePort } from \"../util/free-port.js\";\n\ntype ManifestEntry = {\n url: string;\n summary: Record<string, number>;\n htmlPath?: string;\n jsonPath?: string;\n};\n\ntype AssertionResult = {\n name: string;\n actual: number;\n expected: number;\n operator: string;\n passed: boolean;\n level: \"warn\" | \"error\";\n auditProperty?: string;\n auditId?: string;\n};\n\ntype NormalizedLhciResult = {\n summary: Record<string, number>;\n assertionsFailed: number;\n assertions: Array<{ category: string; level: \"warn\" | \"error\"; message: string }>;\n};\n\nasync function readJsonMaybe<T>(path: string): Promise<T | null> {\n try {\n const raw = await readFile(path, \"utf-8\");\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n}\n\ntype LhrFile = {\n requestedUrl: string;\n finalUrl?: string;\n categories: Record<string, { score: number | null }>;\n};\n\n/**\n * Build manifest-equivalent entries by scanning the `.lighthouseci/` dir\n * for `lhr-*.json` files written by `lhci collect`. We used to read\n * `manifest.json` directly, but lhci 0.15+ no longer writes it — the\n * audit would silently return \"no manifest written\" against a perfectly\n * healthy run. Reproduced on caltex 2026-05-28 (0.10.5 dogfood).\n */\nasync function readLhrEntries(resultsDir: string): Promise<ManifestEntry[]> {\n const files = await readdir(resultsDir).catch(() => [] as string[]);\n const entries: ManifestEntry[] = [];\n for (const f of files) {\n if (!f.startsWith(\"lhr-\") || !f.endsWith(\".json\")) continue;\n const lhr = await readJsonMaybe<LhrFile>(join(resultsDir, f));\n if (!lhr || !lhr.categories) continue;\n const summary: Record<string, number> = {};\n for (const [k, v] of Object.entries(lhr.categories)) {\n if (typeof v?.score === \"number\") summary[k] = v.score;\n }\n entries.push({ url: lhr.requestedUrl, summary });\n }\n return entries;\n}\n\nfunction averageSummaries(entries: ManifestEntry[]): Record<string, number> {\n if (entries.length === 0) return {};\n const sums: Record<string, number> = {};\n const counts: Record<string, number> = {};\n for (const e of entries) {\n for (const [k, v] of Object.entries(e.summary ?? {})) {\n if (typeof v !== \"number\") continue;\n sums[k] = (sums[k] ?? 0) + v;\n counts[k] = (counts[k] ?? 0) + 1;\n }\n }\n const out: Record<string, number> = {};\n for (const k of Object.keys(sums)) {\n const total = sums[k] ?? 0;\n const count = counts[k] ?? 1;\n out[k] = total / count;\n }\n return out;\n}\n\nfunction categoryFromAssertion(a: AssertionResult): string {\n // `name` looks like \"categories:accessibility\" or \"audits:uses-http2\".\n const colonIdx = a.name.indexOf(\":\");\n return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;\n}\n\nfunction messageForAssertion(a: AssertionResult): string {\n // `a.actual` is parsed from external lhci/Lighthouse JSON; a malformed or\n // missing value (not a number) would make `.toFixed` throw and crash the whole\n // audit. Guard it and fall back to a readable string instead.\n const actual = typeof a.actual === \"number\" ? a.actual.toFixed(2) : \"n/a\";\n return `${a.name} ${a.operator} ${a.expected} (actual: ${actual})`;\n}\n\n/** Shared tail: scan `.lighthouseci/` for lhr-*.json + assertion-results.json and\n * build the AuditResult. Identical for the checkout and deployed paths. */\nasync function parseLhciResults(\n resultsDir: string,\n label: string,\n raw: SpawnResult,\n): Promise<AuditResult> {\n const manifest = await readLhrEntries(resultsDir);\n\n if (manifest.length === 0) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"fail\",\n summary: `lighthouse: no lhr-*.json written (exit ${raw.code})${\n raw.stderr ? ` — ${raw.stderr.slice(0, 200)}` : \"\"\n }`,\n };\n }\n\n const assertionResults =\n (await readJsonMaybe<AssertionResult[]>(join(resultsDir, \"assertion-results.json\"))) ?? [];\n\n const failed = assertionResults.filter((a) => !a.passed);\n const assertions = failed.map((a) => ({\n category: categoryFromAssertion(a),\n level: a.level,\n message: messageForAssertion(a),\n }));\n\n const anyError = assertions.some((a) => a.level === \"error\");\n const anyWarn = assertions.some((a) => a.level === \"warn\");\n const status: AuditResult[\"status\"] = anyError ? \"fail\" : anyWarn ? \"warn\" : \"pass\";\n\n const normalized: NormalizedLhciResult = {\n summary: averageSummaries(manifest),\n assertionsFailed: failed.length,\n assertions,\n };\n\n const summary =\n status === \"pass\"\n ? \"lighthouse: all categories passing\"\n : `lighthouse: ${failed.length} assertion(s) failed`;\n\n return { audit: \"lighthouse\", site: label, status, summary, details: normalized };\n}\n\n/** Checkout mode (unchanged behavior): boot the site's vite dev server on a\n * pinned free port and audit the local fixtures/override URL. */\nasync function checkoutLighthouse(spawn: SpawnFn, site: Site, label: string): Promise<AuditResult> {\n const siteCfg = await readSiteConfig(site.path);\n // Allocate a free port + force vite to `--strictPort` so the spawned dev\n // server either binds the port we picked or fails loudly (caltex 2026-05-28\n // zombie-vite incident).\n const port = await findFreePort();\n const baseUrl = siteCfg.lighthouseUrl ?? lighthouseConfig.ci.collect.url[0];\n const resolvedConfig = {\n ...lighthouseConfig,\n ci: {\n ...lighthouseConfig.ci,\n collect: {\n ...lighthouseConfig.ci.collect,\n url: [withFreePort(baseUrl, port)],\n startServerCommand: `npm run vite:dev -- --port ${port} --strictPort`,\n },\n },\n };\n\n const configDir = await mkdtemp(join(tmpdir(), \"reddoor-lhci-\"));\n const configPath = join(configDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(site.path, \".lighthouseci\");\n await rm(resultsDir, { recursive: true, force: true });\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: site.path,\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(configDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n await rm(configDir, { recursive: true, force: true });\n\n return parseLhciResults(resultsDir, label, raw);\n}\n\n/** Deployed mode: audit a production URL directly — no checkout, no dev server.\n * Runs in a throwaway tmp cwd; uploads to the filesystem so fleet runs never\n * push 200 public reports to temporary-public-storage. */\nasync function deployedLighthouse(\n spawn: SpawnFn,\n deployedUrl: string,\n label: string,\n): Promise<AuditResult> {\n const workDir = await mkdtemp(join(tmpdir(), \"reddoor-lh-deployed-\"));\n const resolvedConfig = {\n ci: {\n // Deliberately NOT spread from lighthouseConfig.ci.collect: deployed mode\n // must omit startServerCommand and the dev-server settings entirely.\n collect: {\n url: [deployedUrl],\n // 3 runs to damp Lighthouse's run-to-run variance; parseLhciResults\n // averages the lhr files. (Median is a tracked future refinement.)\n numberOfRuns: 3,\n // Use devtools (applied) throttling instead of the desktop preset's\n // default `simulate` (Lantern). Lantern estimates LCP from the\n // dependency graph and can't model an LCP element revealed by a\n // JS-load opacity fade (e.g. gallerysonder's GridImage): on a fast\n // observed load it finds no LCP node and throws NO_LCP, nulling the\n // perf score, which the cockpit then coerces to a misleading 0.\n // devtools reads the real LCP paint event from the throttled trace\n // instead. Trade-off: slightly noisier run-to-run, damped by the\n // numberOfRuns:3 average above.\n settings: { preset: \"desktop\", throttlingMethod: \"devtools\", skipAudits: [\"uses-http2\"] },\n },\n assert: lighthouseConfig.ci.assert,\n upload: { target: \"filesystem\", outputDir: join(workDir, \"lhci-report\") },\n },\n };\n\n const configPath = join(workDir, \"lighthouserc.json\");\n await writeFile(configPath, JSON.stringify(resolvedConfig), \"utf-8\");\n\n const resultsDir = join(workDir, \".lighthouseci\");\n\n let raw: SpawnResult;\n try {\n raw = await spawn(\"npx\", [\"--yes\", \"@lhci/cli\", \"autorun\", `--config=${configPath}`], {\n cwd: workDir,\n // 3 serial cold runs of a slow deployed site (lhci's own maxWaitForLoad\n // ~45-60s each) + first-use Chrome download can plausibly exceed 3 min →\n // SIGTERM → no lhr-*.json → spurious \"no scores\". Match the 5-min budget\n // the checkout path already gives (erp-industrials nightly flake,\n // morning-brief 2026-06-10 MEDIUM-F).\n timeoutMs: 5 * 60_000,\n });\n } catch (err) {\n await rm(workDir, { recursive: true, force: true });\n const e = err as NodeJS.ErrnoException;\n if (e.code === \"ENOENT\" || /ENOENT/.test(String(err))) {\n return {\n audit: \"lighthouse\",\n site: label,\n status: \"skip\",\n summary: \"npx/@lhci/cli not available\",\n };\n }\n throw err;\n }\n\n try {\n return await parseLhciResults(resultsDir, label, raw);\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\nexport async function lighthouseAudit(ctx: AuditContext): Promise<AuditResult> {\n const spawn = ctx.spawn ?? defaultSpawn;\n const site = ctx.site;\n const label = siteLabel(site);\n\n return site.deployedUrl\n ? deployedLighthouse(spawn, site.deployedUrl, label)\n : checkoutLighthouse(spawn, site, label);\n}\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nexport type SiteConfig = {\n /** Override URL the lighthouse audit hits. Sites without the default\n * `/dev/a11y-fixtures` dev route set this to their homepage. */\n lighthouseUrl?: string;\n /**\n * Real site routes the a11y audit should axe-scan, IN ADDITION to the two\n * synthetic fixture pages it always covers. Opt-in per site and absent by\n * default: the audit runs with `--fail-on-violations` in the shared CI\n * workflow, and most of the fleet carries pre-existing a11y debt, so scanning\n * real routes everywhere at once would red every repo. A site adopts this once\n * its routes are clean. Omitted (never `[]`) when unset or unusable.\n */\n a11yRoutes?: string[];\n /**\n * Which server the browser gates run against: `\"dev\"` (the default — `vite\n * dev`) or `\"preview\"` (a real `vite build`, served by `vite preview`).\n *\n * Both gates measured the dev server, so the production bundle was built in\n * CI and then never opened by a browser — and dev does not merely fail to\n * reproduce some defects, it hides them. Module graph, code splitting,\n * minification and asset hashing are most of what \"hydration works\" means,\n * and a stylesheet can even be fetched under a different CSP directive in\n * each (#700).\n *\n * Opt-in, because a preview costs a `pnpm build` per run and because the\n * `/dev/*` fixture routes the axe scan targets are not guaranteed to survive\n * one — a site that flipped this without checking would trade a working gate\n * for one reporting every fixture as a missing route. Omitted (never\n * defaulted here) so each caller states its own default.\n *\n * Requires a `preview` script in the site's package.json; the starter has one.\n */\n gateServer?: \"dev\" | \"preview\";\n};\n\n/**\n * Read per-site overrides from `package.json#reddoor`. Returns `{}` on any\n * failure (missing file, malformed JSON, missing key, wrong type) so every\n * caller can safely fall back to its built-in default. Never throws.\n */\nexport async function readSiteConfig(sitePath: string): Promise<SiteConfig> {\n let raw: string;\n try {\n raw = await readFile(join(sitePath, \"package.json\"), \"utf-8\");\n } catch {\n return {};\n }\n let pkg: unknown;\n try {\n pkg = JSON.parse(raw);\n } catch {\n return {};\n }\n if (!pkg || typeof pkg !== \"object\") return {};\n const cfg = (pkg as { reddoor?: unknown }).reddoor;\n if (!cfg || typeof cfg !== \"object\") return {};\n\n const out: SiteConfig = {};\n const url = (cfg as { lighthouseUrl?: unknown }).lighthouseUrl;\n if (typeof url === \"string\" && url.length > 0) {\n out.lighthouseUrl = url;\n }\n const routes = (cfg as { a11yRoutes?: unknown }).a11yRoutes;\n if (Array.isArray(routes)) {\n // Junk entries are dropped rather than passed through — a `null` in the list\n // would become `page.goto(null)` inside the generated spec and fail the whole\n // audit for a typo. An all-junk list leaves the key omitted, so the caller's\n // \"no routes configured\" branch is the same shape as \"key absent\".\n const clean = routes\n .filter((r): r is string => typeof r === \"string\")\n .map((r) => r.trim())\n .filter((r) => r.length > 0);\n if (clean.length > 0) out.a11yRoutes = clean;\n }\n // Anything but the two known values reads as absent. This string reaches a\n // shell as part of a `webServer.command`, so a typo must fall back to the\n // default rather than be forwarded — `\"prod\"` would become `npm run prod`.\n const gate = (cfg as { gateServer?: unknown }).gateServer;\n if (gate === \"dev\" || gate === \"preview\") out.gateServer = gate;\n\n return out;\n}\n\n/**\n * The starter's unreplaced Prismic repository name. A clone still carrying it\n * has no content model of its own: every Prismic-backed route resolves nothing\n * and `getByUID(\"page\",\"home\")` throws, so the site's own `/` answers **404 by\n * design** until `/new-site` step 6 swaps this for a real repository.\n *\n * Exactly the same string the site side already reads in four places —\n * `src/lib/prismicio.ts` (`isPlaceholderRepo`), `svelte.config.js`, the home\n * route's `entries()` prerender guard, and `tests/smoke/routes.ts`, whose\n * committed smoke manifest flips `/`'s expected status on it. This is that fact\n * arriving in the audit, not a new convention.\n */\nexport const PLACEHOLDER_PRISMIC_REPO = \"your-prismic-repo-name\";\n\n/** Slice Machine's filename, and the name the Prismic CLI's migration renames\n * it to, in the same preference order `src/prismic/models/config.ts` reads\n * them. Both are checked so a half-migrated repo is read correctly. */\nconst PRISMIC_CONFIG_FILES = [\"slicemachine.config.json\", \"prismic.config.json\"] as const;\n\n/**\n * True when this site is still pointed at the starter placeholder — i.e. when a\n * 404 on one of its own content routes is the designed answer rather than a\n * broken route.\n *\n * DELIBERATELY NARROWER than `PLACEHOLDER_REPOSITORY_NAMES` in\n * `src/prismic/models/config.ts`, and this is the trap worth naming. That list\n * also holds `reddoor-wireframer`, which is there precisely because IT\n * RESOLVES: data-dynamiq names it, the repository really exists with published\n * documents, and the site really renders from it. It earns a place on a list\n * that means \"mint no token for this\" and must never reach a list that means\n * \"a 404 here is expected\" — folding the two together would buy a LIVE\n * production site permanent, silent tolerance of a broken homepage. Only the\n * starter sentinel, which 404s at Prismic itself and so can never serve a page,\n * belongs here.\n *\n * Reads the COMMITTED config only, never `VITE_PRISMIC_ENVIRONMENT`. The site\n * side honours that override, but `tests/smoke/routes.ts` throws when it names\n * the sentinel under CI — \"it would make this smoke run expect no home page and\n * pass\" — and an audit that honoured it would hand that same false green to any\n * real site whose environment happened to carry it.\n *\n * Never throws, and every unclear answer is `false`: a missing, unreadable or\n * malformed Prismic config buys a site no 404 tolerance at all. The failure\n * this returns to is the one that reports too much, not the one that reports\n * nothing (#680).\n */\nexport async function readsPlaceholderPrismicRepo(sitePath: string): Promise<boolean> {\n let sawPlaceholder = false;\n for (const name of PRISMIC_CONFIG_FILES) {\n let raw: string;\n try {\n raw = await readFile(join(sitePath, name), \"utf-8\");\n } catch {\n continue;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n continue;\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n const repo = (parsed as { repositoryName?: unknown }).repositoryName;\n if (typeof repo !== \"string\") continue;\n const trimmed = repo.trim();\n if (trimmed === PLACEHOLDER_PRISMIC_REPO) {\n // Not an early return: the CLI migration renames slicemachine.config.json\n // to prismic.config.json, so a half-migrated repo can hold a stale\n // sentinel in the first file and its real repository in the second. A\n // real name anywhere wins.\n sawPlaceholder = true;\n continue;\n }\n if (trimmed.length > 0) return false;\n }\n return sawPlaceholder;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAAA,SAAS,YAAAA,WAAU,WAAW,SAAS,IAAI,eAAe;AAC1D,SAAS,cAAc;AACvB,SAAS,QAAAC,aAAY;;;ACFrB,SAAS,gBAAgB;AACzB,SAAS,YAAY;AA0CrB,eAAsB,eAAe,UAAuC;AAC1E,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,SAAS,KAAK,UAAU,cAAc,GAAG,OAAO;AAAA,EAC9D,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,GAAG;AAAA,EACtB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,CAAC;AAC7C,QAAM,MAAO,IAA8B;AAC3C,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,CAAC;AAE7C,QAAM,MAAkB,CAAC;AACzB,QAAM,MAAO,IAAoC;AACjD,MAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,GAAG;AAC7C,QAAI,gBAAgB;AAAA,EACtB;AACA,QAAM,SAAU,IAAiC;AACjD,MAAI,MAAM,QAAQ,MAAM,GAAG;AAKzB,UAAM,QAAQ,OACX,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,EAChD,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC7B,QAAI,MAAM,SAAS,EAAG,KAAI,aAAa;AAAA,EACzC;AAIA,QAAM,OAAQ,IAAiC;AAC/C,MAAI,SAAS,SAAS,SAAS,UAAW,KAAI,aAAa;AAE3D,SAAO;AACT;AAcO,IAAM,2BAA2B;AAKxC,IAAM,uBAAuB,CAAC,4BAA4B,qBAAqB;AA6B/E,eAAsB,4BAA4B,UAAoC;AACpF,MAAI,iBAAiB;AACrB,aAAW,QAAQ,sBAAsB;AACvC,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,SAAS,KAAK,UAAU,IAAI,GAAG,OAAO;AAAA,IACpD,QAAQ;AACN;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,GAAG;AAAA,IACzB,QAAQ;AACN;AAAA,IACF;AACA,QAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,EAAG;AACpE,UAAM,OAAQ,OAAwC;AACtD,QAAI,OAAO,SAAS,SAAU;AAC9B,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,YAAY,0BAA0B;AAKxC,uBAAiB;AACjB;AAAA,IACF;AACA,QAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,EACjC;AACA,SAAO;AACT;;;AD9HA,eAAe,cAAiB,MAAiC;AAC/D,MAAI;AACF,UAAM,MAAM,MAAMC,UAAS,MAAM,OAAO;AACxC,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAeA,eAAe,eAAe,YAA8C;AAC1E,QAAM,QAAQ,MAAM,QAAQ,UAAU,EAAE,MAAM,MAAM,CAAC,CAAa;AAClE,QAAM,UAA2B,CAAC;AAClC,aAAW,KAAK,OAAO;AACrB,QAAI,CAAC,EAAE,WAAW,MAAM,KAAK,CAAC,EAAE,SAAS,OAAO,EAAG;AACnD,UAAM,MAAM,MAAM,cAAuBC,MAAK,YAAY,CAAC,CAAC;AAC5D,QAAI,CAAC,OAAO,CAAC,IAAI,WAAY;AAC7B,UAAM,UAAkC,CAAC;AACzC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,IAAI,UAAU,GAAG;AACnD,UAAI,OAAO,GAAG,UAAU,SAAU,SAAQ,CAAC,IAAI,EAAE;AAAA,IACnD;AACA,YAAQ,KAAK,EAAE,KAAK,IAAI,cAAc,QAAQ,CAAC;AAAA,EACjD;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,SAAkD;AAC1E,MAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAClC,QAAM,OAA+B,CAAC;AACtC,QAAM,SAAiC,CAAC;AACxC,aAAW,KAAK,SAAS;AACvB,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,EAAE,WAAW,CAAC,CAAC,GAAG;AACpD,UAAI,OAAO,MAAM,SAAU;AAC3B,WAAK,CAAC,KAAK,KAAK,CAAC,KAAK,KAAK;AAC3B,aAAO,CAAC,KAAK,OAAO,CAAC,KAAK,KAAK;AAAA,IACjC;AAAA,EACF;AACA,QAAM,MAA8B,CAAC;AACrC,aAAW,KAAK,OAAO,KAAK,IAAI,GAAG;AACjC,UAAM,QAAQ,KAAK,CAAC,KAAK;AACzB,UAAM,QAAQ,OAAO,CAAC,KAAK;AAC3B,QAAI,CAAC,IAAI,QAAQ;AAAA,EACnB;AACA,SAAO;AACT;AAEA,SAAS,sBAAsB,GAA4B;AAEzD,QAAM,WAAW,EAAE,KAAK,QAAQ,GAAG;AACnC,SAAO,YAAY,IAAI,EAAE,KAAK,MAAM,WAAW,CAAC,IAAI,EAAE;AACxD;AAEA,SAAS,oBAAoB,GAA4B;AAIvD,QAAM,SAAS,OAAO,EAAE,WAAW,WAAW,EAAE,OAAO,QAAQ,CAAC,IAAI;AACpE,SAAO,GAAG,EAAE,IAAI,IAAI,EAAE,QAAQ,IAAI,EAAE,QAAQ,aAAa,MAAM;AACjE;AAIA,eAAe,iBACb,YACA,OACA,KACsB;AACtB,QAAM,WAAW,MAAM,eAAe,UAAU;AAEhD,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,SAAS,2CAA2C,IAAI,IAAI,IAC1D,IAAI,SAAS,WAAM,IAAI,OAAO,MAAM,GAAG,GAAG,CAAC,KAAK,EAClD;AAAA,IACF;AAAA,EACF;AAEA,QAAM,mBACH,MAAM,cAAiCA,MAAK,YAAY,wBAAwB,CAAC,KAAM,CAAC;AAE3F,QAAM,SAAS,iBAAiB,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM;AACvD,QAAM,aAAa,OAAO,IAAI,CAAC,OAAO;AAAA,IACpC,UAAU,sBAAsB,CAAC;AAAA,IACjC,OAAO,EAAE;AAAA,IACT,SAAS,oBAAoB,CAAC;AAAA,EAChC,EAAE;AAEF,QAAM,WAAW,WAAW,KAAK,CAAC,MAAM,EAAE,UAAU,OAAO;AAC3D,QAAM,UAAU,WAAW,KAAK,CAAC,MAAM,EAAE,UAAU,MAAM;AACzD,QAAM,SAAgC,WAAW,SAAS,UAAU,SAAS;AAE7E,QAAM,aAAmC;AAAA,IACvC,SAAS,iBAAiB,QAAQ;AAAA,IAClC,kBAAkB,OAAO;AAAA,IACzB;AAAA,EACF;AAEA,QAAM,UACJ,WAAW,SACP,uCACA,eAAe,OAAO,MAAM;AAElC,SAAO,EAAE,OAAO,cAAc,MAAM,OAAO,QAAQ,SAAS,SAAS,WAAW;AAClF;AAIA,eAAe,mBAAmB,OAAgB,MAAY,OAAqC;AACjG,QAAM,UAAU,MAAM,eAAe,KAAK,IAAI;AAI9C,QAAM,OAAO,MAAM,aAAa;AAChC,QAAM,UAAU,QAAQ,iBAAiB,iBAAiB,GAAG,QAAQ,IAAI,CAAC;AAC1E,QAAM,iBAAiB;AAAA,IACrB,GAAG;AAAA,IACH,IAAI;AAAA,MACF,GAAG,iBAAiB;AAAA,MACpB,SAAS;AAAA,QACP,GAAG,iBAAiB,GAAG;AAAA,QACvB,KAAK,CAAC,aAAa,SAAS,IAAI,CAAC;AAAA,QACjC,oBAAoB,8BAA8B,IAAI;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,QAAQA,MAAK,OAAO,GAAG,eAAe,CAAC;AAC/D,QAAM,aAAaA,MAAK,WAAW,mBAAmB;AACtD,QAAM,UAAU,YAAY,KAAK,UAAU,cAAc,GAAG,OAAO;AAEnE,QAAM,aAAaA,MAAK,KAAK,MAAM,eAAe;AAClD,QAAM,GAAG,YAAY,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAErD,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,CAAC,SAAS,aAAa,WAAW,YAAY,UAAU,EAAE,GAAG;AAAA,MACpF,KAAK,KAAK;AAAA,MACV,WAAW,IAAI;AAAA,IACjB,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,UAAM,GAAG,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACpD,UAAM,IAAI;AACV,QAAI,EAAE,SAAS,YAAY,SAAS,KAAK,OAAO,GAAG,CAAC,GAAG;AACrD,aAAO;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,MACX;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACA,QAAM,GAAG,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAEpD,SAAO,iBAAiB,YAAY,OAAO,GAAG;AAChD;AAKA,eAAe,mBACb,OACA,aACA,OACsB;AACtB,QAAM,UAAU,MAAM,QAAQA,MAAK,OAAO,GAAG,sBAAsB,CAAC;AACpE,QAAM,iBAAiB;AAAA,IACrB,IAAI;AAAA;AAAA;AAAA,MAGF,SAAS;AAAA,QACP,KAAK,CAAC,WAAW;AAAA;AAAA;AAAA,QAGjB,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAUd,UAAU,EAAE,QAAQ,WAAW,kBAAkB,YAAY,YAAY,CAAC,YAAY,EAAE;AAAA,MAC1F;AAAA,MACA,QAAQ,iBAAiB,GAAG;AAAA,MAC5B,QAAQ,EAAE,QAAQ,cAAc,WAAWA,MAAK,SAAS,aAAa,EAAE;AAAA,IAC1E;AAAA,EACF;AAEA,QAAM,aAAaA,MAAK,SAAS,mBAAmB;AACpD,QAAM,UAAU,YAAY,KAAK,UAAU,cAAc,GAAG,OAAO;AAEnE,QAAM,aAAaA,MAAK,SAAS,eAAe;AAEhD,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,CAAC,SAAS,aAAa,WAAW,YAAY,UAAU,EAAE,GAAG;AAAA,MACpF,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAML,WAAW,IAAI;AAAA,IACjB,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,UAAM,GAAG,SAAS,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAClD,UAAM,IAAI;AACV,QAAI,EAAE,SAAS,YAAY,SAAS,KAAK,OAAO,GAAG,CAAC,GAAG;AACrD,aAAO;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,MACX;AAAA,IACF;AACA,UAAM;AAAA,EACR;AAEA,MAAI;AACF,WAAO,MAAM,iBAAiB,YAAY,OAAO,GAAG;AAAA,EACtD,UAAE;AACA,UAAM,GAAG,SAAS,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EACpD;AACF;AAEA,eAAsB,gBAAgB,KAAyC;AAC7E,QAAM,QAAQ,IAAI,SAAS;AAC3B,QAAM,OAAO,IAAI;AACjB,QAAM,QAAQ,UAAU,IAAI;AAE5B,SAAO,KAAK,cACR,mBAAmB,OAAO,KAAK,aAAa,KAAK,IACjD,mBAAmB,OAAO,MAAM,KAAK;AAC3C;","names":["readFile","join","readFile","join"]}
@@ -28,7 +28,7 @@ import {
28
28
  } from "./chunk-WEKL2HYJ.js";
29
29
  import {
30
30
  recordFleetEventsBestEffort
31
- } from "./chunk-YXWBM26P.js";
31
+ } from "./chunk-3YALLSBF.js";
32
32
 
33
33
  // src/reports/send/idempotency.ts
34
34
  function isIdempotencyConflict(err) {
@@ -241,4 +241,4 @@ export {
241
241
  parseAddresses,
242
242
  isProbablyEmail
243
243
  };
244
- //# sourceMappingURL=chunk-LDHRIQUW.js.map
244
+ //# sourceMappingURL=chunk-ZEE5WQYU.js.map
package/dist/cli/bin.js CHANGED
@@ -142,7 +142,7 @@ cli.command("sync-configs [site]", "Sync canonical configs into a site.").option
142
142
  'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
143
143
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
144
144
  async (site, opts) => runOrExit(
145
- async () => (await import("../sync-configs-OXSWKVZ2.js")).runSyncConfigsCommand(site, opts),
145
+ async () => (await import("../sync-configs-HBXS3D7C.js")).runSyncConfigsCommand(site, opts),
146
146
  opts
147
147
  )
148
148
  );
@@ -151,7 +151,7 @@ cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>",
151
151
  'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
152
152
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
153
153
  async (site, opts) => runOrExit(
154
- async () => (await import("../bump-deps-775VLDYB.js")).runBumpDepsCommand(site, opts),
154
+ async () => (await import("../bump-deps-54K4ECP3.js")).runBumpDepsCommand(site, opts),
155
155
  opts
156
156
  )
157
157
  );
@@ -163,7 +163,7 @@ cli.command(
163
163
  'Inventory file (.json or .mjs/.js), or "turso" ("airtable" is a deprecated alias)'
164
164
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
165
165
  async (site, opts) => runOrExit(
166
- async () => (await import("../self-updating-ZUWPGYUP.js")).runSelfUpdatingCommand(site, opts),
166
+ async () => (await import("../self-updating-KX5FMGEO.js")).runSelfUpdatingCommand(site, opts),
167
167
  opts
168
168
  )
169
169
  );
@@ -175,7 +175,7 @@ cli.command(
175
175
  'Inventory file (.json or .mjs/.js), or "turso" ("airtable" is a deprecated alias)'
176
176
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
177
177
  async (site, opts) => runOrExit(
178
- async () => (await import("../prismic-ci-NAPWOXXD.js")).runPrismicCiCommand(site, opts),
178
+ async () => (await import("../prismic-ci-XMX7A5E4.js")).runPrismicCiCommand(site, opts),
179
179
  opts
180
180
  )
181
181
  );
@@ -193,7 +193,7 @@ cli.command(
193
193
  "Write the report to this file (the CI workflow posts it as a PR comment)"
194
194
  ).action(
195
195
  async (site, opts) => runOrExit(
196
- async () => (await import("../prismic-models-LL6ORTQ2.js")).runPrismicModelsCommand(site, opts),
196
+ async () => (await import("../prismic-models-7GKFAAFA.js")).runPrismicModelsCommand(site, opts),
197
197
  opts
198
198
  )
199
199
  );
@@ -202,7 +202,7 @@ cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to
202
202
  'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
203
203
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
204
204
  async (upgrade, site, opts) => runOrExit(
205
- async () => (await import("../upgrade-EVAL3JTO.js")).runUpgradeCommand(upgrade, site, opts),
205
+ async () => (await import("../upgrade-XOJMDIT4.js")).runUpgradeCommand(upgrade, site, opts),
206
206
  opts
207
207
  )
208
208
  );
@@ -214,7 +214,7 @@ cli.command(
214
214
  'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
215
215
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
216
216
  async (site, opts) => runOrExit(
217
- async () => (await import("../convert-to-pnpm-KLWA53N5.js")).runConvertToPnpmCommand(site, opts),
217
+ async () => (await import("../convert-to-pnpm-U2CRNKNY.js")).runConvertToPnpmCommand(site, opts),
218
218
  opts
219
219
  )
220
220
  );
@@ -223,7 +223,7 @@ cli.command("svelte-codemods [site]", "Apply Svelte 5 gotcha codemods to an alre
223
223
  'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
224
224
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
225
225
  async (site, opts) => runOrExit(
226
- async () => (await import("../svelte-codemods-VOLOKKFI.js")).runSvelteCodemodsCommand(site, opts),
226
+ async () => (await import("../svelte-codemods-YFN4TEGV.js")).runSvelteCodemodsCommand(site, opts),
227
227
  opts
228
228
  )
229
229
  );
@@ -235,7 +235,7 @@ cli.command(
235
235
  'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
236
236
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
237
237
  async (site, opts) => runOrExit(
238
- async () => (await import("../health-endpoint-BJ6BKRHF.js")).runHealthEndpointCommand(site, opts),
238
+ async () => (await import("../health-endpoint-K5LXDPGE.js")).runHealthEndpointCommand(site, opts),
239
239
  opts
240
240
  )
241
241
  );
@@ -247,7 +247,7 @@ cli.command(
247
247
  'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
248
248
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
249
249
  async (site, opts) => runOrExit(
250
- async () => (await import("../match-harness-GBCDSVPS.js")).runMatchHarnessCommand(site, opts),
250
+ async () => (await import("../match-harness-CFELJA57.js")).runMatchHarnessCommand(site, opts),
251
251
  opts
252
252
  )
253
253
  );
@@ -259,7 +259,7 @@ cli.command(
259
259
  'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
260
260
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
261
261
  async (site, opts) => runOrExit(
262
- async () => (await import("../smoke-suite-WAUPI6EJ.js")).runSmokeSuiteCommand(site, opts),
262
+ async () => (await import("../smoke-suite-QQMV7TNA.js")).runSmokeSuiteCommand(site, opts),
263
263
  opts
264
264
  )
265
265
  );
@@ -271,7 +271,7 @@ cli.command(
271
271
  'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
272
272
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
273
273
  async (site, opts) => runOrExit(
274
- async () => (await import("../onboard-JQQIMQW7.js")).runOnboardCommand(site, opts),
274
+ async () => (await import("../onboard-F7RTLMDZ.js")).runOnboardCommand(site, opts),
275
275
  opts
276
276
  )
277
277
  );
@@ -282,14 +282,14 @@ cli.command(
282
282
  "--fleet <inventory>",
283
283
  'Inventory file (.json or .mjs/.js), or "turso" to read the fleet roster from the database ("airtable" is a deprecated alias)'
284
284
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
285
- async (site, opts) => runOrExit(async () => (await import("../init-GOWDA5QS.js")).runInitCommand(site, opts), opts)
285
+ async (site, opts) => runOrExit(async () => (await import("../init-OFITGMOP.js")).runInitCommand(site, opts), opts)
286
286
  );
287
287
  cli.command(
288
288
  "launch <site>",
289
289
  "Bootstrap + first-audit a site, then draft its launch email for approval."
290
290
  ).action(
291
291
  async (site, opts) => runOrExit(
292
- async () => (await import("../launch-7N3X2MXB.js")).runLaunchCommand(site, opts),
292
+ async () => (await import("../launch-AJY3TGTQ.js")).runLaunchCommand(site, opts),
293
293
  opts
294
294
  )
295
295
  );
@@ -298,7 +298,7 @@ cli.command("header-image [site]", "Generate a site's report header image from i
298
298
  "Extra CSS selector(s) to hide before the shutter (consent UI the heuristic misses)"
299
299
  ).action(
300
300
  async (site, opts) => runOrExit(
301
- async () => (await import("../header-image-H7ZJAV26.js")).runHeaderImageCommand(site, opts),
301
+ async () => (await import("../header-image-43ZIEJKP.js")).runHeaderImageCommand(site, opts),
302
302
  opts
303
303
  )
304
304
  );
@@ -307,7 +307,7 @@ cli.command(
307
307
  "Draft the monthly-report announcement email for maintenance sites (all, or one) for approval."
308
308
  ).action(
309
309
  async (site, opts) => runOrExit(
310
- async () => (await import("../announce-5ZD2SBGD.js")).runAnnounceCommand(site, opts),
310
+ async () => (await import("../announce-2FH2KBQQ.js")).runAnnounceCommand(site, opts),
311
311
  opts
312
312
  )
313
313
  );
@@ -316,7 +316,7 @@ cli.command(
316
316
  "Show who a form submission would email, and optionally flip the pre-launch guard (read-back confirmed)."
317
317
  ).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(
318
318
  async (site, opts) => runOrExit(
319
- async () => (await import("../forms-notify-target-X6UEOI2M.js")).runFormsNotifyTargetCommand(
319
+ async () => (await import("../forms-notify-target-DK5R326N.js")).runFormsNotifyTargetCommand(
320
320
  site,
321
321
  opts
322
322
  ),
@@ -328,7 +328,7 @@ cli.command(
328
328
  "Operator self-tests. kind=email: preview a report email for a site (or --all) to yourself."
329
329
  ).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(
330
330
  async (kind, site, opts) => runOrExit(
331
- async () => (await import("../selftest-G4W3LIBQ.js")).runSelftestCommand(kind, site, opts),
331
+ async () => (await import("../selftest-KABY2CYJ.js")).runSelftestCommand(kind, site, opts),
332
332
  opts
333
333
  )
334
334
  );
@@ -340,7 +340,7 @@ cli.command(
340
340
  "Fleet mode: announcement\u2192maintenance sites; maintenance/testing\u2192all schedulable sites."
341
341
  ).action(
342
342
  async (site, opts) => runOrExit(
343
- async () => (await import("../preflight-O7Q6FWHU.js")).runPreflightCommand(site, opts),
343
+ async () => (await import("../preflight-6XGVVO2U.js")).runPreflightCommand(site, opts),
344
344
  opts
345
345
  )
346
346
  );
@@ -349,7 +349,7 @@ cli.command(
349
349
  "Create/verify a site's fleet row in Turso (new sites get a site_<ULID> id)."
350
350
  ).option("--name <display name>", "Human Name for the row (client-facing copy uses it verbatim).").option("--url <url>", "Deployed URL (e.g. the Netlify site URL).").option("--contact <email>", "point of contact \u2014 the client address reports resolve to.").option("--git-repo <owner/repo>", "GitHub identity. Default on create: reddoorla/<slug>.").action(
351
351
  async (slug, opts) => runOrExit(
352
- async () => (await import("../ensure-site-2QZHFVQ3.js")).runEnsureSiteCommand(slug, opts),
352
+ async () => (await import("../ensure-site-F2WFCWWA.js")).runEnsureSiteCommand(slug, opts),
353
353
  opts
354
354
  )
355
355
  );
@@ -376,7 +376,7 @@ cli.command(
376
376
  "Comma-separated buyer questions, chosen by hand. Blank asks the goal's fixed set. A chosen set gets its own version key, so comparisons never mix sets."
377
377
  ).action(
378
378
  async (url, opts) => runOrExit(
379
- async () => (await import("../prospect-audit-FMPGQIRQ.js")).runProspectAuditCommand(url, opts),
379
+ async () => (await import("../prospect-audit-KWP5P5U2.js")).runProspectAuditCommand(url, opts),
380
380
  opts
381
381
  )
382
382
  );
@@ -397,7 +397,7 @@ cli.command("report [site]", "Draft or send maintenance/testing reports.").optio
397
397
  "Regenerate one UNSENT report's stored HTML from its current row, so the console preview shows edited commentary. Refuses a sent report."
398
398
  ).action(
399
399
  async (site, opts) => runOrExit(
400
- async () => (await import("../report-IAHXN5CW.js")).runReportCommand(site, opts),
400
+ async () => (await import("../report-MVPWS7WP.js")).runReportCommand(site, opts),
401
401
  opts
402
402
  )
403
403
  );
@@ -406,7 +406,7 @@ cli.command(
406
406
  "Sweep the fleet for GitHub signals (Renovate-failing/CI/last-commit) and write each site's row."
407
407
  ).option("--fleet", "Run across every site in the Airtable inventory.").option("--write-back", "Write each site's signals back to its Websites row.").action(
408
408
  async (opts) => runOrExit(
409
- async () => (await import("../github-signals-QIRTHHET.js")).runGitHubSignalsCommand({
409
+ async () => (await import("../github-signals-RGRPYTUB.js")).runGitHubSignalsCommand({
410
410
  fleet: opts.fleet,
411
411
  writeBack: opts.writeBack
412
412
  }),
@@ -446,7 +446,7 @@ cli.command(
446
446
  "--by <who>",
447
447
  "replay-deadletters --abandon: who decided (default: OPERATOR_EMAIL, else 'operator')."
448
448
  ).action(
449
- async (action, opts) => runOrExit(async () => (await import("../db-II5WAK4D.js")).runDbCommand(action, opts), opts)
449
+ async (action, opts) => runOrExit(async () => (await import("../db-7XYTBIPH.js")).runDbCommand(action, opts), opts)
450
450
  );
451
451
  cli.command(
452
452
  "submissions <action>",
@@ -456,7 +456,7 @@ cli.command(
456
456
  "Write the re-buckets: status \u2192 spam_auto, spam_score/spam_reason replaced with the new verdict + 'retro-rescore' marker."
457
457
  ).action(
458
458
  async (action, opts) => runOrExit(
459
- async () => (await import("../submissions-2SHCFOKD.js")).runSubmissionsCommand(action, opts),
459
+ async () => (await import("../submissions-K5UXGZUN.js")).runSubmissionsCommand(action, opts),
460
460
  opts
461
461
  )
462
462
  );
@@ -465,10 +465,10 @@ cli.command(
465
465
  "Trigger Renovate on fleet sites the security sweep flagged with critical/high vulns."
466
466
  ).option("--fleet", "Run across every active, repo-backed site in the Airtable inventory.").action(
467
467
  async (opts) => runOrExit(
468
- async () => (await import("../renovate-dispatch-2IHAZDVA.js")).runRenovateDispatchCommand({
468
+ async () => (await import("../renovate-dispatch-ZZQXT7DA.js")).runRenovateDispatchCommand({
469
469
  fleet: opts.fleet,
470
470
  // #539 Phase 5: mirror the auto-fix counter into site_health.
471
- siteMirror: await (await import("../site-mirror-WR7ACXOV.js")).makeSiteMirror()
471
+ siteMirror: await (await import("../site-mirror-TKTDWYP3.js")).makeSiteMirror()
472
472
  }),
473
473
  opts
474
474
  )
@@ -8,24 +8,24 @@ import {
8
8
  } from "../../chunk-DYTYPKFN.js";
9
9
  import {
10
10
  resolveSites
11
- } from "../../chunk-FP6HOW6J.js";
11
+ } from "../../chunk-S2PLX4XK.js";
12
12
  import {
13
13
  ALL_AUDIT_NAMES,
14
14
  runOneAudit
15
- } from "../../chunk-L3S3QZXL.js";
15
+ } from "../../chunk-DXZRWW2F.js";
16
16
  import "../../chunk-LKGVSM2O.js";
17
17
  import "../../chunk-MVKNU4QD.js";
18
18
  import "../../chunk-43SW3FGW.js";
19
19
  import "../../chunk-Z3JXLULD.js";
20
20
  import {
21
21
  recordFleetEventsBestEffort
22
- } from "../../chunk-YXWBM26P.js";
23
- import "../../chunk-MMSMZC7S.js";
24
- import "../../chunk-BIYQN7FH.js";
22
+ } from "../../chunk-3YALLSBF.js";
23
+ import "../../chunk-MNRWHSFS.js";
24
+ import "../../chunk-VCPTOZ56.js";
25
25
  import "../../chunk-UDRHAQT7.js";
26
26
  import "../../chunk-PTDMVGKL.js";
27
27
  import "../../chunk-LBYOLBW7.js";
28
- import "../../chunk-IOTOOTII.js";
28
+ import "../../chunk-CP6MVZ5X.js";
29
29
  import "../../chunk-A3WDWNN5.js";
30
30
  import "../../chunk-2OP4JKIQ.js";
31
31
  import {
@@ -262,12 +262,12 @@ ${wb.summary}`;
262
262
  task: async (_ctx, task) => {
263
263
  const base = openBase(readAirtableConfig());
264
264
  task.output = "loading the fleet roster\u2026";
265
- const { readFleetRoster } = await import("../../roster-GDNSIRAK.js");
265
+ const { readFleetRoster } = await import("../../roster-EJ3ILR52.js");
266
266
  const websites = await readFleetRoster();
267
267
  task.output = "writing scores\u2026";
268
268
  writeSummary = await writeAuditsToAirtable({ base, websites, slug, results });
269
269
  if (writeSummary.siteId && writeSummary.fields) {
270
- const { makeSiteMirror } = await import("../../site-mirror-WR7ACXOV.js");
270
+ const { makeSiteMirror } = await import("../../site-mirror-TKTDWYP3.js");
271
271
  await (await makeSiteMirror()).health(writeSummary.siteId, writeSummary.fields);
272
272
  }
273
273
  task.title = `Wrote to Websites[${writeSummary.siteName}] (${writeSummary.writes.length} audit type${writeSummary.writes.length === 1 ? "" : "s"})`;
@@ -302,12 +302,12 @@ async function runFleetWriteBack(args) {
302
302
  base = openBase(readAirtableConfig());
303
303
  }
304
304
  const roster = deps.roster ?? (async () => {
305
- const { readFleetRoster } = await import("../../roster-GDNSIRAK.js");
305
+ const { readFleetRoster } = await import("../../roster-EJ3ILR52.js");
306
306
  return readFleetRoster();
307
307
  });
308
308
  const websites = await roster();
309
309
  const makeMirror = deps.makeMirror ?? (async () => {
310
- const { makeHealthMirrorBestEffort } = await import("../../health-mirror-KU3G7V2Y.js");
310
+ const { makeHealthMirrorBestEffort } = await import("../../health-mirror-WZUHWCCV.js");
311
311
  return makeHealthMirrorBestEffort();
312
312
  });
313
313
  const mirror = await makeMirror();
@@ -0,0 +1,11 @@
1
+ import {
2
+ openDb,
3
+ readDbConfig
4
+ } from "./chunk-MNRWHSFS.js";
5
+ import "./chunk-CP6MVZ5X.js";
6
+ import "./chunk-6VAL7XJT.js";
7
+ export {
8
+ openDb,
9
+ readDbConfig
10
+ };
11
+ //# sourceMappingURL=client-3XGSZMLU.js.map