@reddoorla/maintenance 0.90.1 → 0.92.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 (161) hide show
  1. package/dist/{announce-NDTLRQCN.js → announce-6OS4C57D.js} +3 -3
  2. package/dist/{blux-YGBGS24U.js → blux-NXQLMS4W.js} +4 -4
  3. package/dist/blux-NXQLMS4W.js.map +1 -0
  4. package/dist/{bump-deps-KGH5UYSF.js → bump-deps-GI7PHMOC.js} +6 -6
  5. package/dist/{chunk-WS6NU475.js → chunk-22GKJO5F.js} +2 -2
  6. package/dist/{chunk-A6T2R63Z.js → chunk-3HS6LPFX.js} +2 -2
  7. package/dist/{chunk-UXJZU23T.js → chunk-4NUKMPOO.js} +9 -9
  8. package/dist/{chunk-BQRFQSGY.js → chunk-4R3YAQNV.js} +2 -2
  9. package/dist/{chunk-JRHIFA5R.js → chunk-5XMKJX74.js} +10 -4
  10. package/dist/chunk-5XMKJX74.js.map +1 -0
  11. package/dist/{chunk-XIDUYLSR.js → chunk-7CJ3DVZW.js} +2 -2
  12. package/dist/{chunk-NYUYNYQL.js → chunk-7MKT4I5T.js} +15 -1
  13. package/dist/chunk-7MKT4I5T.js.map +1 -0
  14. package/dist/{chunk-Z3KYFUN6.js → chunk-B6NUZ2ZX.js} +2 -2
  15. package/dist/chunk-BJQCIJOY.js +3534 -0
  16. package/dist/chunk-BJQCIJOY.js.map +1 -0
  17. package/dist/{chunk-Y53WOQIJ.js → chunk-CVJF2IC5.js} +2 -2
  18. package/dist/{chunk-W45FZEGE.js → chunk-DMPHP7UP.js} +2 -2
  19. package/dist/{chunk-KGUKL3CV.js → chunk-E22WAIF2.js} +2 -2
  20. package/dist/{chunk-VBEKL445.js → chunk-FCS36FDT.js} +2 -2
  21. package/dist/{chunk-QD427NIO.js → chunk-JB4Q3C3M.js} +7 -7
  22. package/dist/{chunk-IYSEC543.js → chunk-LS6QPANO.js} +2 -2
  23. package/dist/{chunk-SJOXWJ5Y.js → chunk-M2XF6JAI.js} +80 -9
  24. package/dist/chunk-M2XF6JAI.js.map +1 -0
  25. package/dist/{chunk-ZOCJBJQV.js → chunk-MVDMKKBN.js} +2 -2
  26. package/dist/{chunk-7RJ2HMMJ.js → chunk-NTLSMJHW.js} +2 -2
  27. package/dist/{chunk-MFHS7IQ2.js → chunk-OCNSIBMP.js} +27 -1
  28. package/dist/{chunk-MFHS7IQ2.js.map → chunk-OCNSIBMP.js.map} +1 -1
  29. package/dist/chunk-PJ23RDHT.js +48 -0
  30. package/dist/chunk-PJ23RDHT.js.map +1 -0
  31. package/dist/{chunk-2NJQLSEU.js → chunk-PPIZHDJL.js} +2 -2
  32. package/dist/chunk-T7ZUUOZ2.js +496 -0
  33. package/dist/chunk-T7ZUUOZ2.js.map +1 -0
  34. package/dist/{chunk-TVL5MVUF.js → chunk-TCF6S7AD.js} +4 -4
  35. package/dist/{chunk-GSWTT2J6.js → chunk-WHWVYL7B.js} +4 -4
  36. package/dist/{chunk-3G25KIWW.js → chunk-ZTPPCHDB.js} +4 -4
  37. package/dist/cli/bin.js +30 -27
  38. package/dist/cli/bin.js.map +1 -1
  39. package/dist/cli/commands/audit.js +14 -14
  40. package/dist/client-M2V6FHH5.js +11 -0
  41. package/dist/{convert-to-pnpm-NWB5FCQ2.js → convert-to-pnpm-ONFP3JKF.js} +6 -6
  42. package/dist/{db-LOSJ74ZJ.js → db-DJVW6ZSE.js} +11 -11
  43. package/dist/{digest-CS6KLCZB.js → digest-EOA4JLTV.js} +13 -13
  44. package/dist/{digest-collectors-NI2M3UTC.js → digest-collectors-ZA3BTS3R.js} +7 -7
  45. package/dist/{email-LJQT5J7F.js → email-I4BCETI4.js} +16 -64
  46. package/dist/email-I4BCETI4.js.map +1 -0
  47. package/dist/{ensure-site-FFDUMY6E.js → ensure-site-SF6KICTN.js} +2 -2
  48. package/dist/forms/index.d.ts +5 -1
  49. package/dist/forms/index.js +1 -1
  50. package/dist/forms/index.js.map +1 -1
  51. package/dist/forms/prismic.d.ts +69 -0
  52. package/dist/forms/prismic.js +75 -0
  53. package/dist/forms/prismic.js.map +1 -0
  54. package/dist/{forms-notify-target-IHJX7NWA.js → forms-notify-target-MNNOGSJ7.js} +5 -4
  55. package/dist/{forms-notify-target-IHJX7NWA.js.map → forms-notify-target-MNNOGSJ7.js.map} +1 -1
  56. package/dist/{github-signals-FCLNHKMF.js → github-signals-3WLIJ5RR.js} +5 -5
  57. package/dist/{header-image-7A7XLVEU.js → header-image-DOCDFFRO.js} +2 -2
  58. package/dist/{health-endpoint-UVX6R5N5.js → health-endpoint-DHKJUPRM.js} +6 -6
  59. package/dist/{health-mirror-GMSF3Z5R.js → health-mirror-55RRU6BI.js} +3 -3
  60. package/dist/index.js +36 -29
  61. package/dist/index.js.map +1 -1
  62. package/dist/{init-NXZ4NXTL.js → init-ZRKWYCKA.js} +15 -15
  63. package/dist/{launch-5V7RLUV7.js → launch-6X5OXPF2.js} +13 -13
  64. package/dist/migrate-K4JETR36.js +7 -0
  65. package/dist/{notify-4HP4GZ2H.js → notify-FYSOWKMJ.js} +4 -3
  66. package/dist/{onboard-BKCYBAVW.js → onboard-6NHGCKGB.js} +6 -6
  67. package/dist/{orchestrate-6HROYTM6.js → orchestrate-KMQIUP3H.js} +5 -5
  68. package/dist/pipeline-AQBFG5WM.js +31 -0
  69. package/dist/{preflight-LIN5NCYU.js → preflight-VPHQYWTY.js} +6 -6
  70. package/dist/{prismic-ci-6YGRIOHT.js → prismic-ci-A2HI7NNJ.js} +8 -8
  71. package/dist/{prismic-models-VNGIKS2B.js → prismic-models-QTKIMFBC.js} +5 -5
  72. package/dist/prospect/types.d.ts +809 -2
  73. package/dist/prospect/types.js +8 -0
  74. package/dist/{prospect-audit-JCWTKJTS.js → prospect-audit-RKUVSTLN.js} +27 -6
  75. package/dist/prospect-audit-RKUVSTLN.js.map +1 -0
  76. package/dist/{prospect-audits-3PMIO73D.js → prospect-audits-RVYEA4KH.js} +6 -4
  77. package/dist/recipes/sync-configs.js +3 -3
  78. package/dist/{chunk-5PWB3JHJ.js → render-7G43BSK5.js} +28 -9
  79. package/dist/render-7G43BSK5.js.map +1 -0
  80. package/dist/{replay-4S6ENYCH.js → replay-HV7MGKBU.js} +3 -2
  81. package/dist/{replay-4S6ENYCH.js.map → replay-HV7MGKBU.js.map} +1 -1
  82. package/dist/{report-AAY2HZZR.js → report-Y4LXDHRV.js} +11 -11
  83. package/dist/{report-mirror-GMNSH5HF.js → report-mirror-6ZDCAQZB.js} +3 -3
  84. package/dist/{schema-75PJECAE.js → schema-2V7BGYVT.js} +1 -1
  85. package/dist/{schema-75PJECAE.js.map → schema-2V7BGYVT.js.map} +1 -1
  86. package/dist/{self-updating-VQIW24RZ.js → self-updating-D5XOWS7V.js} +5 -5
  87. package/dist/{selftest-CUX2FIQD.js → selftest-IFGSJP4X.js} +5 -5
  88. package/dist/{site-mirror-GCFLBBWK.js → site-mirror-3E7JYTJG.js} +3 -3
  89. package/dist/{smoke-suite-4JFZZYVW.js → smoke-suite-GXC5JZPH.js} +6 -6
  90. package/dist/{submissions-7LGJJSDL.js → submissions-UMJS3YST.js} +2 -2
  91. package/dist/{svelte-codemods-QAPDZC6F.js → svelte-codemods-A3EARSRP.js} +6 -6
  92. package/dist/{sync-configs-PVRLZRKL.js → sync-configs-KSESOHFF.js} +6 -6
  93. package/dist/{upgrade-IR2OO3KP.js → upgrade-ZLNVEIEA.js} +6 -6
  94. package/dist/{webflow-624FEBFD.js → webflow-IRKOIT7S.js} +4 -4
  95. package/package.json +6 -1
  96. package/dist/blux-YGBGS24U.js.map +0 -1
  97. package/dist/chunk-5PWB3JHJ.js.map +0 -1
  98. package/dist/chunk-DXKF552B.js +0 -1630
  99. package/dist/chunk-DXKF552B.js.map +0 -1
  100. package/dist/chunk-JRHIFA5R.js.map +0 -1
  101. package/dist/chunk-NYUYNYQL.js.map +0 -1
  102. package/dist/chunk-SJOXWJ5Y.js.map +0 -1
  103. package/dist/client-JYKPVCJX.js +0 -11
  104. package/dist/email-LJQT5J7F.js.map +0 -1
  105. package/dist/migrate-YITCXBLS.js +0 -7
  106. package/dist/pipeline-MQJD4CKN.js +0 -16
  107. package/dist/prospect-audit-JCWTKJTS.js.map +0 -1
  108. package/dist/render-P3SBUFVB.js +0 -10
  109. package/dist/render-P3SBUFVB.js.map +0 -1
  110. /package/dist/{announce-NDTLRQCN.js.map → announce-6OS4C57D.js.map} +0 -0
  111. /package/dist/{bump-deps-KGH5UYSF.js.map → bump-deps-GI7PHMOC.js.map} +0 -0
  112. /package/dist/{chunk-WS6NU475.js.map → chunk-22GKJO5F.js.map} +0 -0
  113. /package/dist/{chunk-A6T2R63Z.js.map → chunk-3HS6LPFX.js.map} +0 -0
  114. /package/dist/{chunk-UXJZU23T.js.map → chunk-4NUKMPOO.js.map} +0 -0
  115. /package/dist/{chunk-BQRFQSGY.js.map → chunk-4R3YAQNV.js.map} +0 -0
  116. /package/dist/{chunk-XIDUYLSR.js.map → chunk-7CJ3DVZW.js.map} +0 -0
  117. /package/dist/{chunk-Z3KYFUN6.js.map → chunk-B6NUZ2ZX.js.map} +0 -0
  118. /package/dist/{chunk-Y53WOQIJ.js.map → chunk-CVJF2IC5.js.map} +0 -0
  119. /package/dist/{chunk-W45FZEGE.js.map → chunk-DMPHP7UP.js.map} +0 -0
  120. /package/dist/{chunk-KGUKL3CV.js.map → chunk-E22WAIF2.js.map} +0 -0
  121. /package/dist/{chunk-VBEKL445.js.map → chunk-FCS36FDT.js.map} +0 -0
  122. /package/dist/{chunk-QD427NIO.js.map → chunk-JB4Q3C3M.js.map} +0 -0
  123. /package/dist/{chunk-IYSEC543.js.map → chunk-LS6QPANO.js.map} +0 -0
  124. /package/dist/{chunk-ZOCJBJQV.js.map → chunk-MVDMKKBN.js.map} +0 -0
  125. /package/dist/{chunk-7RJ2HMMJ.js.map → chunk-NTLSMJHW.js.map} +0 -0
  126. /package/dist/{chunk-2NJQLSEU.js.map → chunk-PPIZHDJL.js.map} +0 -0
  127. /package/dist/{chunk-TVL5MVUF.js.map → chunk-TCF6S7AD.js.map} +0 -0
  128. /package/dist/{chunk-GSWTT2J6.js.map → chunk-WHWVYL7B.js.map} +0 -0
  129. /package/dist/{chunk-3G25KIWW.js.map → chunk-ZTPPCHDB.js.map} +0 -0
  130. /package/dist/{client-JYKPVCJX.js.map → client-M2V6FHH5.js.map} +0 -0
  131. /package/dist/{convert-to-pnpm-NWB5FCQ2.js.map → convert-to-pnpm-ONFP3JKF.js.map} +0 -0
  132. /package/dist/{db-LOSJ74ZJ.js.map → db-DJVW6ZSE.js.map} +0 -0
  133. /package/dist/{digest-CS6KLCZB.js.map → digest-EOA4JLTV.js.map} +0 -0
  134. /package/dist/{digest-collectors-NI2M3UTC.js.map → digest-collectors-ZA3BTS3R.js.map} +0 -0
  135. /package/dist/{ensure-site-FFDUMY6E.js.map → ensure-site-SF6KICTN.js.map} +0 -0
  136. /package/dist/{github-signals-FCLNHKMF.js.map → github-signals-3WLIJ5RR.js.map} +0 -0
  137. /package/dist/{header-image-7A7XLVEU.js.map → header-image-DOCDFFRO.js.map} +0 -0
  138. /package/dist/{health-endpoint-UVX6R5N5.js.map → health-endpoint-DHKJUPRM.js.map} +0 -0
  139. /package/dist/{health-mirror-GMSF3Z5R.js.map → health-mirror-55RRU6BI.js.map} +0 -0
  140. /package/dist/{init-NXZ4NXTL.js.map → init-ZRKWYCKA.js.map} +0 -0
  141. /package/dist/{launch-5V7RLUV7.js.map → launch-6X5OXPF2.js.map} +0 -0
  142. /package/dist/{migrate-YITCXBLS.js.map → migrate-K4JETR36.js.map} +0 -0
  143. /package/dist/{notify-4HP4GZ2H.js.map → notify-FYSOWKMJ.js.map} +0 -0
  144. /package/dist/{onboard-BKCYBAVW.js.map → onboard-6NHGCKGB.js.map} +0 -0
  145. /package/dist/{orchestrate-6HROYTM6.js.map → orchestrate-KMQIUP3H.js.map} +0 -0
  146. /package/dist/{pipeline-MQJD4CKN.js.map → pipeline-AQBFG5WM.js.map} +0 -0
  147. /package/dist/{preflight-LIN5NCYU.js.map → preflight-VPHQYWTY.js.map} +0 -0
  148. /package/dist/{prismic-ci-6YGRIOHT.js.map → prismic-ci-A2HI7NNJ.js.map} +0 -0
  149. /package/dist/{prismic-models-VNGIKS2B.js.map → prismic-models-QTKIMFBC.js.map} +0 -0
  150. /package/dist/{prospect-audits-3PMIO73D.js.map → prospect-audits-RVYEA4KH.js.map} +0 -0
  151. /package/dist/{report-AAY2HZZR.js.map → report-Y4LXDHRV.js.map} +0 -0
  152. /package/dist/{report-mirror-GMNSH5HF.js.map → report-mirror-6ZDCAQZB.js.map} +0 -0
  153. /package/dist/{self-updating-VQIW24RZ.js.map → self-updating-D5XOWS7V.js.map} +0 -0
  154. /package/dist/{selftest-CUX2FIQD.js.map → selftest-IFGSJP4X.js.map} +0 -0
  155. /package/dist/{site-mirror-GCFLBBWK.js.map → site-mirror-3E7JYTJG.js.map} +0 -0
  156. /package/dist/{smoke-suite-4JFZZYVW.js.map → smoke-suite-GXC5JZPH.js.map} +0 -0
  157. /package/dist/{submissions-7LGJJSDL.js.map → submissions-UMJS3YST.js.map} +0 -0
  158. /package/dist/{svelte-codemods-QAPDZC6F.js.map → svelte-codemods-A3EARSRP.js.map} +0 -0
  159. /package/dist/{sync-configs-PVRLZRKL.js.map → sync-configs-KSESOHFF.js.map} +0 -0
  160. /package/dist/{upgrade-IR2OO3KP.js.map → upgrade-ZLNVEIEA.js.map} +0 -0
  161. /package/dist/{webflow-624FEBFD.js.map → webflow-IRKOIT7S.js.map} +0 -0
@@ -1 +1,9 @@
1
+ import {
2
+ GOAL_LABELS,
3
+ orderRequirements
4
+ } from "../chunk-T7ZUUOZ2.js";
5
+ export {
6
+ GOAL_LABELS,
7
+ orderRequirements
8
+ };
1
9
  //# sourceMappingURL=types.js.map
@@ -21,6 +21,18 @@ function reportPrintUrl(token, raw = process.env.REPORT_BASE_URL) {
21
21
  }
22
22
 
23
23
  // src/cli/commands/prospect-audit.ts
24
+ var GOALS = [
25
+ "book",
26
+ "enquire",
27
+ "call",
28
+ "visit",
29
+ "buy",
30
+ "demo",
31
+ "partner"
32
+ ];
33
+ function isSiteGoal(value) {
34
+ return GOALS.includes(value);
35
+ }
24
36
  function fail(message) {
25
37
  return { output: message, code: 2 };
26
38
  }
@@ -97,17 +109,25 @@ async function runProspectAuditCommand(url, opts) {
97
109
  "No TURSO_DATABASE_URL, so the report cannot be saved or shared. Re-run with --out <file>, or set the Turso credentials."
98
110
  );
99
111
  }
100
- const { runProspectAudit } = await import("./pipeline-MQJD4CKN.js");
112
+ const { runProspectAudit } = await import("./pipeline-AQBFG5WM.js");
101
113
  const onStage = (name, status, detail) => {
102
114
  if (status === "start") console.error(`\u2026 ${name}`);
103
115
  else if (status === "ok") console.error(`\u2713 ${name}`);
104
116
  else console.error(`! ${name} \u2014 ${detail ?? "failed"}`);
105
117
  };
106
118
  const business = opts.business?.trim();
119
+ const goal = opts.goal?.trim();
120
+ if (goal !== void 0 && goal !== "" && !isSiteGoal(goal)) {
121
+ return fail(`--goal must be one of: ${GOALS.join(", ")}`);
122
+ }
107
123
  const result = await runProspectAudit(
108
124
  url,
109
125
  {
110
126
  ...business ? { business } : {},
127
+ // An empty string is what a blank dispatch field sends; it must read as
128
+ // "not supplied" so the model's own inference still runs, rather than
129
+ // overriding it with nothing.
130
+ ...goal ? { goal } : {},
111
131
  ...opts.competitors ? {
112
132
  competitors: opts.competitors.split(",").map((c) => c.trim()).filter(Boolean)
113
133
  } : {},
@@ -115,7 +135,7 @@ async function runProspectAuditCommand(url, opts) {
115
135
  },
116
136
  { ...opts.deps ?? {}, onStage }
117
137
  );
118
- const { renderProspectReport } = await import("./render-P3SBUFVB.js");
138
+ const { renderProspectReport } = await import("./render-7G43BSK5.js");
119
139
  const html = renderProspectReport(result);
120
140
  const warnings = [];
121
141
  let file = null;
@@ -132,8 +152,8 @@ async function runProspectAuditCommand(url, opts) {
132
152
  let auditId = null;
133
153
  if (canPersist) {
134
154
  try {
135
- const { openDb, readDbConfig } = await import("./client-JYKPVCJX.js");
136
- const { createProspectAudit } = await import("./prospect-audits-3PMIO73D.js");
155
+ const { openDb, readDbConfig } = await import("./client-M2V6FHH5.js");
156
+ const { createProspectAudit } = await import("./prospect-audits-RVYEA4KH.js");
137
157
  const db = await openDb(readDbConfig());
138
158
  const created = await createProspectAudit(db, {
139
159
  url: result.url,
@@ -167,7 +187,7 @@ async function runProspectAuditCommand(url, opts) {
167
187
  let email = null;
168
188
  if (opts.email) {
169
189
  try {
170
- const { sendAuditEmail, parseProspectAuditRecipients } = await import("./email-LJQT5J7F.js");
190
+ const { sendAuditEmail, parseProspectAuditRecipients } = await import("./email-I4BCETI4.js");
171
191
  const recipients = parseProspectAuditRecipients(process.env.PROSPECT_AUDIT_RECIPIENTS);
172
192
  email = await sendAuditEmail(result, { link, recipients, auditId, pdf });
173
193
  if (!email.sent) {
@@ -190,6 +210,7 @@ async function runProspectAuditCommand(url, opts) {
190
210
  return { output: summarize(result, link, file, warnings, recovery, email), code };
191
211
  }
192
212
  export {
213
+ GOALS,
193
214
  runProspectAuditCommand
194
215
  };
195
- //# sourceMappingURL=prospect-audit-JCWTKJTS.js.map
216
+ //# sourceMappingURL=prospect-audit-RKUVSTLN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/commands/prospect-audit.ts","../src/prospect/report-url.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { hostnameOf, isHttpUrl, isPrivateOrLoopbackHost } from \"../../util/url.js\";\nimport { reportUrl, reportPrintUrl } from \"../../prospect/report-url.js\";\nimport type { ProspectAuditStatus } from \"../../db/prospect-audits.js\";\nimport type { PipelineDeps, StageName } from \"../../prospect/pipeline.js\";\nimport type { ProspectAuditResult } from \"../../prospect/types.js\";\nimport type { SendAuditEmailResult } from \"../../prospect/email.js\";\nimport type { SiteGoal } from \"../../prospect/goals.js\";\n\n/** The operator-selectable goals, in the order the dispatch dropdown lists\n * them. `unknown` is deliberately absent: it is a finding the audit can reach\n * on its own, never something an operator would choose. */\nexport const GOALS = [\n \"book\",\n \"enquire\",\n \"call\",\n \"visit\",\n \"buy\",\n \"demo\",\n \"partner\",\n] as const satisfies readonly SiteGoal[];\n\nfunction isSiteGoal(value: string): value is SiteGoal {\n return (GOALS as readonly string[]).includes(value);\n}\n\nexport type ProspectAuditCliOptions = {\n business?: string;\n /**\n * The one action the site should produce from a visitor, chosen by the\n * operator instead of inferred from the site.\n *\n * Worth overriding because the operator knows things the site does not say.\n * A practice that wants online bookings but has never built one reads as\n * \"call\" from its own pages, and grading it as \"call\" would score it well for\n * doing the thing they are trying to stop doing.\n */\n goal?: string;\n /** Comma-separated competitor domains. */\n competitors?: string;\n /** cac sets this false for `--no-probes`. */\n probes?: boolean;\n out?: string;\n json?: boolean;\n /**\n * Email the internal sheet (scores, what wasn't measured and why, top\n * fixes, and the shareable link) to `PROSPECT_AUDIT_RECIPIENTS`, once the\n * report is rendered and the `--out`/Turso delivery attempts finish. A\n * missing/empty recipients var, or a Resend failure, is a warning — never a\n * crash — because by the time this runs the audit is already delivered (or\n * recovered) by the code above it.\n */\n email?: boolean;\n /** Test seam: injected pipeline deps. Never set from the CLI. */\n deps?: PipelineDeps;\n};\n\ntype RecoveryWrite = { htmlPath: string; jsonPath: string };\n\nfunction fail(message: string): { output: string; code: number } {\n return { output: message, code: 2 };\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nfunction scoreLine(label: string, value: number | null): string {\n return `${label.padEnd(14)} ${value === null ? \"not measured\" : String(value).padStart(3)}`;\n}\n\n/** Item 3: the `status` column exists to answer, without deserializing the\n * whole result_json, whether this was a clean run or a degraded one. crawl\n * is excluded — it's the one fatal stage (see pipeline.ts), so by the time a\n * ProspectAuditResult exists it was always ok. Every other stage's\n * StageResult already collapses \"failed\" and \"deliberately skipped\"\n * (--no-probes, the checks→analyze cascade) into the same `ok: false` — both\n * mean the report has a \"not measured\" section, so both count as partial\n * here too. */\nfunction auditStatus(result: ProspectAuditResult): ProspectAuditStatus {\n const allStagesOk =\n result.checks.ok && result.lighthouse.ok && result.analyze.ok && result.probes.ok;\n return allStagesOk ? \"complete\" : \"partial\";\n}\n\n/** Filesystem-safe, never empty. Collapses anything outside [a-z0-9.-] to a\n * single dash so a hostname becomes a sane filename fragment. */\nfunction slugify(s: string): string {\n const slug = s\n .toLowerCase()\n .replace(/[^a-z0-9.-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return slug || \"prospect\";\n}\n\n/**\n * Last-resort write when NEITHER `--out` nor the database persist made it —\n * the one situation a paid, already-run audit must never end in silently. Lands\n * in `reports/`, mirroring header-image.ts's own convention for CLI-written\n * artifacts (mkdir -p, resolved against cwd), rather than a bare file dropped\n * into whatever directory the operator happened to be standing in when they\n * ran the command — often unrelated to this repo at all in fleet-tool usage.\n */\nasync function writeRecoveryFiles(\n result: ProspectAuditResult,\n html: string,\n): Promise<RecoveryWrite> {\n const dir = resolve(\"reports\");\n await mkdir(dir, { recursive: true });\n const stamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const base = `prospect-audit-${slugify(hostnameOf(result.url))}-${stamp}`;\n const htmlPath = resolve(dir, `${base}.html`);\n const jsonPath = resolve(dir, `${base}.json`);\n await writeFile(htmlPath, html, \"utf-8\");\n await writeFile(jsonPath, JSON.stringify(result, null, 2), \"utf-8\");\n return { htmlPath, jsonPath };\n}\n\nfunction summarize(\n result: ProspectAuditResult,\n link: string | null,\n file: string | null,\n warnings: string[],\n recovery: RecoveryWrite | null,\n email: SendAuditEmailResult | null,\n): string {\n const lines = [\n `Prospect audit — ${result.businessName ?? result.url}`,\n \"\",\n scoreLine(\"Findability\", result.scores.findability),\n scoreLine(\"Readability\", result.scores.readability),\n scoreLine(\"Answers\", result.scores.answers),\n // No AI Visibility line: the score was removed from every presentation\n // surface (web, print, email, here) — the measurement lives on as the\n // report's receipts section, not as a number that reads as ours to move.\n ];\n for (const [name, stage] of [\n [\"checks\", result.checks],\n [\"lighthouse\", result.lighthouse],\n [\"analyze\", result.analyze],\n [\"probes\", result.probes],\n ] as const) {\n if (!stage.ok) lines.push(` ! ${name} not measured — ${stage.error}`);\n }\n if (file) lines.push(\"\", `Report written to ${file}`);\n if (link) lines.push(\"\", `Shareable link: ${link}`);\n if (email?.sent) {\n lines.push(\"\", `Emailed to ${email.recipients.join(\", \")} (${email.messageId})`);\n }\n for (const w of warnings) lines.push(\"\", `! ${w}`);\n if (recovery) {\n lines.push(\n \"\",\n \"Neither --out nor the database took this report — wrote a recovery copy instead:\",\n ` ${recovery.htmlPath}`,\n ` ${recovery.jsonPath}`,\n );\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Run one prospect audit end to end. Progress goes to stderr so `--json` stdout\n * stays pipeable. Persistence needs Turso; without it `--out` is mandatory,\n * because an audit nobody can read afterwards is just a bill.\n *\n * By the time `runProspectAudit` resolves, the money is already spent — only\n * the crawl is fatal, every other stage degrades rather than throwing. So the\n * `--out` write and the database persist below are each wrapped in their own\n * try/catch and attempted independently: a bad `--out` path must not cost a\n * working Turso persist (or vice versa), and if BOTH fail — or persist fails\n * with no `--out` given at all — a recovery copy goes to `reports/` rather\n * than the run ending with nothing anywhere. `--json` only changes the SHAPE\n * of stdout; it still runs the same `--out`/persist/recovery logic beneath it.\n */\nexport async function runProspectAuditCommand(\n url: string,\n opts: ProspectAuditCliOptions,\n): Promise<{ output: string; code: number }> {\n if (!isHttpUrl(url)) {\n return fail(`\"${url}\" is not a URL. Pass the full address, e.g. https://example.com`);\n }\n // #612 review: this validated only the SHAPE. The sole private-address guard\n // was in `triggerProspectAudit` — one layer at the far end of the chain — so\n // anyone running the CLI directly, or any future second caller, had none.\n // `crawlSite` now refuses too; this keeps the refusal a clean message instead\n // of a thrown crawl error.\n if (isPrivateOrLoopbackHost(new URL(url).hostname)) {\n return fail(`\"${url}\" is a private or loopback address — refusing to audit it.`);\n }\n const canPersist = Boolean(process.env.TURSO_DATABASE_URL);\n if (!canPersist && !opts.out) {\n return fail(\n \"No TURSO_DATABASE_URL, so the report cannot be saved or shared. Re-run with --out <file>, or set the Turso credentials.\",\n );\n }\n\n const { runProspectAudit } = await import(\"../../prospect/pipeline.js\");\n const onStage = (name: StageName, status: \"start\" | \"ok\" | \"fail\", detail?: string): void => {\n if (status === \"start\") console.error(`… ${name}`);\n else if (status === \"ok\") console.error(`✓ ${name}`);\n else console.error(`! ${name} — ${detail ?? \"failed\"}`);\n };\n\n const business = opts.business?.trim();\n const goal = opts.goal?.trim();\n if (goal !== undefined && goal !== \"\" && !isSiteGoal(goal)) {\n return fail(`--goal must be one of: ${GOALS.join(\", \")}`);\n }\n\n const result = await runProspectAudit(\n url,\n {\n ...(business ? { business } : {}),\n // An empty string is what a blank dispatch field sends; it must read as\n // \"not supplied\" so the model's own inference still runs, rather than\n // overriding it with nothing.\n ...(goal ? { goal } : {}),\n ...(opts.competitors\n ? {\n competitors: opts.competitors\n .split(\",\")\n .map((c) => c.trim())\n .filter(Boolean),\n }\n : {}),\n ...(opts.probes === false ? { probes: false } : {}),\n },\n { ...(opts.deps ?? {}), onStage },\n );\n\n const { renderProspectReport } = await import(\"../../prospect/render.js\");\n const html = renderProspectReport(result);\n\n const warnings: string[] = [];\n\n let file: string | null = null;\n if (opts.out) {\n try {\n await writeFile(opts.out, html, \"utf-8\");\n file = opts.out;\n } catch (err) {\n warnings.push(`Could not write --out (${opts.out}): ${errorMessage(err)}`);\n }\n }\n\n let link: string | null = null;\n let token: string | null = null;\n let auditId: string | null = null;\n if (canPersist) {\n try {\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { createProspectAudit } = await import(\"../../db/prospect-audits.js\");\n const db = await openDb(readDbConfig());\n const created = await createProspectAudit(db, {\n url: result.url,\n // Map at the boundary: ProspectAuditResult.businessName is the field\n // name (Item 2 — it's a resolved NAME, not a description); the\n // `prospect_audits.business` column keeps its existing name, since\n // renaming a column needs its own migration for no benefit here.\n business: result.businessName,\n status: auditStatus(result),\n resultJson: JSON.stringify(result),\n });\n auditId = created.id;\n token = created.token;\n link = reportUrl(token);\n } catch (err) {\n warnings.push(`Could not save to the database: ${errorMessage(err)}`);\n }\n }\n\n const delivered = file !== null || link !== null;\n const recovery = delivered ? null : await writeRecoveryFiles(result, html);\n const code = delivered ? 0 : 1;\n\n // The audit is already delivered (or recovered) above — an email failure\n // must never take that away. Same independent try/catch shape as the\n // --out write and the Turso persist: record a warning, keep going, never\n // touch `code`. Dynamically imported for the same reason db/client.js and\n // prospect/render.js are above: it reaches `resend` (a devDependency) and\n // must stay out of bin.js's static import graph (see bin.ts's\n // central-dep-blocker comment).\n // The PDF leave-behind, printed from the website's print route — a document\n // designed for paper, not the interactive report with its evidence folded\n // away behind disclosures.\n //\n // Best-effort by design. Every other stage in this pipeline degrades rather\n // than throwing, and an attachment is not worth losing a delivered report\n // over: if this fails the email still goes, with the link, and a warning says\n // the PDF is missing. It also needs a token — with no persisted report there\n // is no page to print.\n let pdf: Buffer | null = null;\n if (opts.email && token) {\n try {\n const { renderReportPdf } = await import(\"../../prospect/pdf.js\");\n pdf = await renderReportPdf(reportPrintUrl(token));\n } catch (err) {\n warnings.push(`Could not attach the PDF: ${errorMessage(err)}`);\n }\n }\n\n let email: SendAuditEmailResult | null = null;\n if (opts.email) {\n try {\n const { sendAuditEmail, parseProspectAuditRecipients } =\n await import(\"../../prospect/email.js\");\n const recipients = parseProspectAuditRecipients(process.env.PROSPECT_AUDIT_RECIPIENTS);\n email = await sendAuditEmail(result, { link, recipients, auditId, pdf });\n if (!email.sent) {\n warnings.push(`Could not email the audit sheet: ${email.reason}`);\n }\n } catch (err) {\n warnings.push(`Could not email the audit sheet: ${errorMessage(err)}`);\n }\n }\n\n if (opts.json) {\n return {\n output: JSON.stringify(\n { result, out: file, link, token, recovery, warnings, email },\n null,\n 2,\n ),\n code,\n };\n }\n\n return { output: summarize(result, link, file, warnings, recovery, email), code };\n}\n","/** Where the prospect-facing report lives now: a real, branded route on the\n * marketing site rather than generated HTML on the ops app's domain. */\nexport const DEFAULT_REPORT_BASE_URL = \"https://reddoorla.com\";\n\n/**\n * Resolve the public report origin.\n *\n * Deliberately its own variable rather than reusing `DASHBOARD_BASE_URL`. That\n * one addresses operators and points at the ops app; this one addresses a\n * prospect and points at reddoorla.com. They are different audiences on\n * different domains, and sharing a variable would mean one of them silently\n * moving the day the other is repointed.\n */\nexport function resolveReportBaseUrl(raw: string | undefined): string {\n return (raw?.trim() || DEFAULT_REPORT_BASE_URL).replace(/\\/$/, \"\");\n}\n\n/** The link a prospect opens. */\nexport function reportUrl(token: string, raw = process.env.REPORT_BASE_URL): string {\n return `${resolveReportBaseUrl(raw)}/audit/${token}`;\n}\n\n/** The print-designed variant, which the runner renders to a PDF. Same token,\n * same guards — a separate document, not a stylesheet over the page. */\nexport function reportPrintUrl(token: string, raw = process.env.REPORT_BASE_URL): string {\n return `${reportUrl(token, raw)}/print`;\n}\n"],"mappings":"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","names":[]}
@@ -5,8 +5,9 @@ import {
5
5
  getProspectAuditByToken,
6
6
  isValidToken,
7
7
  listRecentProspectAudits,
8
- newProspectAuditId
9
- } from "./chunk-NYUYNYQL.js";
8
+ newProspectAuditId,
9
+ siteKey
10
+ } from "./chunk-7MKT4I5T.js";
10
11
  export {
11
12
  MAX_RECENT_PROSPECT_AUDITS,
12
13
  createProspectAudit,
@@ -14,6 +15,7 @@ export {
14
15
  getProspectAuditByToken,
15
16
  isValidToken,
16
17
  listRecentProspectAudits,
17
- newProspectAuditId
18
+ newProspectAuditId,
19
+ siteKey
18
20
  };
19
- //# sourceMappingURL=prospect-audits-3PMIO73D.js.map
21
+ //# sourceMappingURL=prospect-audits-RVYEA4KH.js.map
@@ -3,11 +3,11 @@ import {
3
3
  isConfigName,
4
4
  planTemplateDiffs,
5
5
  syncConfigs
6
- } from "../chunk-A6T2R63Z.js";
7
- import "../chunk-3G25KIWW.js";
6
+ } from "../chunk-3HS6LPFX.js";
7
+ import "../chunk-ZTPPCHDB.js";
8
8
  import "../chunk-MLT4QY5X.js";
9
- import "../chunk-XTK5VIQB.js";
10
9
  import "../chunk-XXTZBPUY.js";
10
+ import "../chunk-XTK5VIQB.js";
11
11
  export {
12
12
  ALL_CONFIG_NAMES,
13
13
  isConfigName,
@@ -2,11 +2,12 @@ import {
2
2
  ANALYZE_SKIPPED,
3
3
  PROBES_SKIPPED,
4
4
  resolveBusinessName
5
- } from "./chunk-DXKF552B.js";
5
+ } from "./chunk-BJQCIJOY.js";
6
6
  import {
7
7
  escapeHtml,
8
8
  safeUrl
9
9
  } from "./chunk-BGTYPVLT.js";
10
+ import "./chunk-T7ZUUOZ2.js";
10
11
  import {
11
12
  hostnameOf
12
13
  } from "./chunk-RARREDQE.js";
@@ -55,6 +56,7 @@ var STYLES = `
55
56
  .tag { display: inline-block; font-size: 12px; letter-spacing: .06em; text-transform: uppercase;
56
57
  border-radius: 999px; padding: 2px 10px; border: 1px solid currentColor; }
57
58
  .yes { color: #14663c; } .partial { color: #8a6d00; } .no { color: ${RED}; }
59
+ .unknown { color: #8a857e; border-style: dashed; }
58
60
  ul { padding-left: 20px; } li { margin: 6px 0; }
59
61
  .muted { color: #8a857e; }
60
62
  .cta { margin-top: 56px; background: ${RED}; color: #fff; border-radius: 8px; padding: 24px 28px; }
@@ -136,22 +138,33 @@ function buildProbesSection(p, businessNameUsed) {
136
138
  function buildFindabilitySection(c) {
137
139
  const accessBlock = !c.crawlerAccessMeasured ? `<p class="muted">Crawler access: not measured \u2014 ${STAGE_FAILED_MESSAGE}.</p>` : c.crawlerAccess.blockedAi.length ? `<p><strong>Blocked AI crawlers:</strong> ${escapeHtml(
138
140
  c.crawlerAccess.blockedAi.map(describeAgent).join(", ")
139
- )}</p>` : `<p>Every AI crawler we checked can reach the site.</p>`;
141
+ )}</p>` : (
142
+ // Says what was measured, which is robots.txt, and no more. "Every AI
143
+ // crawler can reach the site" was a claim robots.txt cannot support:
144
+ // a CDN's bot management enforces its own answer and can contradict the
145
+ // file without the owner knowing. Confirmed on a live prospect: its
146
+ // robots.txt blocked nothing relevant, and its CDN returned 403 to one
147
+ // named AI crawler on every request while serving a browser and two
148
+ // other crawlers normally. The report said they were fine.
149
+ `<p>Nothing in your robots.txt blocks the AI crawlers we checked.</p>`
150
+ );
140
151
  const classical = c.crawlerAccessMeasured && c.crawlerAccess.blockedClassical.length ? `<p><strong>Blocked search crawlers:</strong> ${escapeHtml(
141
152
  c.crawlerAccess.blockedClassical.join(", ")
142
153
  )}</p>` : "";
143
154
  const sitemapLine = c.sitemapMeasured ? `<li>sitemap.xml: ${c.sitemapPresent ? "present" : "missing"}</li>` : `<li>sitemap.xml: not measured \u2014 ${STAGE_FAILED_MESSAGE}.</li>`;
144
- const llmsLine = c.llmsTxtMeasured ? `<li>llms.txt: ${c.llmsTxtPresent ? "present" : "missing"}</li>` : `<li>llms.txt: not measured \u2014 ${STAGE_FAILED_MESSAGE}.</li>`;
155
+ const llmsFootnote = `<p class="muted"><strong>A note on llms.txt.</strong> You may have been told to
156
+ add one. We check for it \u2014 this site ${!c.llmsTxtMeasured ? "could not be checked" : c.llmsTxtPresent ? "has one" : "does not"} \u2014 but we do not score it and we will not recommend it. It is a 2024 proposal that no
157
+ answer engine has committed to reading, and adding one has no measured effect on whether you
158
+ are cited. If that changes we will say so and score it then.</p>`;
145
159
  return `${accessBlock}${classical}
146
160
  <ul>
147
161
  ${sitemapLine}
148
- ${llmsLine}
149
162
  <li>Pages missing a meta description: ${c.meta.missingDescription} of ${c.meta.pageCount}</li>
150
163
  <li>Pages missing a canonical URL: ${c.meta.missingCanonical} of ${c.meta.pageCount}</li>
151
164
  <li>Pages missing share images/titles: ${c.meta.missingSocial} of ${c.meta.pageCount}</li>
152
165
  <li>Security headers missing: ${c.securityHeaders.missing.length ? escapeHtml(c.securityHeaders.missing.join(", ")) : "none"}</li>
153
166
  ${c.meta.pagesWithoutExtract > 0 ? `<li><strong>${c.meta.pagesWithoutExtract}</strong> additional page${c.meta.pagesWithoutExtract === 1 ? "" : "s"} produced no readable content at all and ${c.meta.pagesWithoutExtract === 1 ? "is" : "are"} not counted in the ${c.meta.pageCount} pages above.</li>` : ""}
154
- </ul>`;
167
+ </ul>${llmsFootnote}`;
155
168
  }
156
169
  function buildLighthouseBlock(lh) {
157
170
  return lh.ok ? `<p class="muted">Lighthouse \u2014 performance ${lh.data.performance ?? "not measured"},
@@ -168,15 +181,22 @@ function buildReadabilitySection(c) {
168
181
  ${c.meta.pagesWithoutExtract > 0 ? `<li><strong>${c.meta.pagesWithoutExtract}</strong> page${c.meta.pagesWithoutExtract === 1 ? "" : "s"} produced no extract at all \u2014 excluded from every figure above, not counted as readable or unreadable.</li>` : ""}
169
182
  </ul>`;
170
183
  }
184
+ var ANSWERED_LABEL = {
185
+ yes: "yes",
186
+ partial: "partial",
187
+ no: "no",
188
+ unknown: "not measured"
189
+ };
171
190
  function buildAnswersSection(a) {
172
191
  const rows = a.buyerQuestions.map((q) => {
173
192
  const answered = escapeHtml(q.answered);
193
+ const label = escapeHtml(ANSWERED_LABEL[q.answered] ?? q.answered);
174
194
  const evidenceText = q.evidence ? escapeHtml(q.evidence) : null;
175
195
  const evidenceHref = q.evidence && q.page ? safeUrl(q.page) : null;
176
- const evidenceCell = evidenceText === null ? '<span class="muted">no passage on the site</span>' : evidenceHref && evidenceHref !== "#" ? `<a href="${escapeHtml(evidenceHref)}" target="_blank" rel="noopener noreferrer">${evidenceText}</a>` : evidenceText;
196
+ const evidenceCell = q.answered === "unknown" ? '<span class="muted">we did not get a verdict on this question</span>' : evidenceText === null ? '<span class="muted">no passage on the site</span>' : evidenceHref && evidenceHref !== "#" ? `<a href="${escapeHtml(evidenceHref)}" target="_blank" rel="noopener noreferrer">${evidenceText}</a>` : evidenceText;
177
197
  return `<tr>
178
198
  <td>${escapeHtml(q.question)}</td>
179
- <td><span class="tag ${answered}">${answered}</span></td>
199
+ <td><span class="tag ${answered}">${label}</span></td>
180
200
  <td>${evidenceCell}</td>
181
201
  </tr>`;
182
202
  }).join("");
@@ -277,8 +297,7 @@ function renderProspectReport(result) {
277
297
  </body>
278
298
  </html>`;
279
299
  }
280
-
281
300
  export {
282
301
  renderProspectReport
283
302
  };
284
- //# sourceMappingURL=chunk-5PWB3JHJ.js.map
303
+ //# sourceMappingURL=render-7G43BSK5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/prospect/render.ts"],"sourcesContent":["import { escapeHtml, safeUrl } from \"../util/html.js\";\nimport { hostnameOf } from \"../util/url.js\";\nimport { ANALYZE_SKIPPED, PROBES_SKIPPED } from \"./pipeline.js\";\nimport { resolveBusinessName } from \"./probes.js\";\nimport type {\n AnalyzeResult,\n ChecksResult,\n Fix,\n LighthouseScores,\n ProbeAnswer,\n ProbesResult,\n ProspectAuditResult,\n StageResult,\n} from \"./types.js\";\n\nconst RED = \"#d71920\";\nconst IMPACT_ORDER: Record<Fix[\"impact\"], number> = { high: 0, medium: 1, low: 2 };\nconst KIND_LABEL: Record<ProbeAnswer[\"kind\"], string> = {\n branded: \"Branded — asked about the business by name\",\n category: \"Category — the questions a buyer would actually type\",\n competitor: \"Competitor — head-to-head comparisons\",\n};\nconst KIND_ORDER: ProbeAnswer[\"kind\"][] = [\"category\", \"branded\", \"competitor\"];\n\n/** Terse product mapping so a recipient who has never seen a crawler's raw\n * user-agent string can place it — factual labelling, not a voice change. */\nconst AI_AGENT_LABELS: Record<string, string> = {\n GPTBot: \"ChatGPT\",\n \"OAI-SearchBot\": \"ChatGPT's search results\",\n ClaudeBot: \"Claude\",\n PerplexityBot: \"Perplexity\",\n \"Google-Extended\": \"Gemini and Google's AI Overviews\",\n CCBot: \"Common Crawl, used to train many AI models\",\n};\n\nfunction describeAgent(agent: string): string {\n const label = AI_AGENT_LABELS[agent];\n return label ? `${agent} (feeds ${label})` : agent;\n}\n\n/** Every genuine stage failure collapses to this one phrase — the real error\n * (status codes, retry counts, timeouts — operator vocabulary) stays in the\n * persisted JSON and CLI output for operators, but never reaches a stranger\n * who would read it as broken software rather than a diagnostic. */\nconst STAGE_FAILED_MESSAGE = \"we could not complete this part of the analysis\";\n\nconst STYLES = `\n :root { color-scheme: light; }\n * { box-sizing: border-box; }\n body { margin: 0; background: #faf8f5; color: #1a1a1a;\n font: 16px/1.6 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif; }\n .wrap { max-width: 860px; margin: 0 auto; padding: 48px 24px 80px; }\n h1, h2, h3 { font-family: Besley, Georgia, \"Times New Roman\", serif; font-weight: 600; line-height: 1.2; }\n h1 { font-size: 40px; margin: 0 0 8px; }\n h2 { font-size: 26px; margin: 48px 0 12px; border-top: 2px solid #e6e1da; padding-top: 24px; }\n h3 { font-size: 18px; margin: 24px 0 8px; }\n .lede { color: #57544f; margin: 0 0 8px; }\n .scores { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin: 32px 0; }\n .score { background: #fff; border: 1px solid #e6e1da; border-radius: 8px; padding: 16px; }\n .score .n { font-family: Besley, Georgia, serif; font-size: 38px; line-height: 1; color: ${RED}; }\n .score .n.na { font-size: 18px; color: #8a857e; }\n .score .l { font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: #57544f; margin-top: 8px; }\n .score .hint { font-size: 12px; color: #8a857e; margin-top: 4px; }\n .card { background: #fff; border: 1px solid #e6e1da; border-radius: 8px; padding: 16px; margin: 12px 0; }\n .q { font-weight: 600; }\n .tag { display: inline-block; font-size: 12px; letter-spacing: .06em; text-transform: uppercase;\n border-radius: 999px; padding: 2px 10px; border: 1px solid currentColor; }\n .yes { color: #14663c; } .partial { color: #8a6d00; } .no { color: ${RED}; }\n .unknown { color: #8a857e; border-style: dashed; }\n ul { padding-left: 20px; } li { margin: 6px 0; }\n .muted { color: #8a857e; }\n .cta { margin-top: 56px; background: ${RED}; color: #fff; border-radius: 8px; padding: 24px 28px; }\n .cta h2 { border: 0; padding: 0; margin: 0 0 8px; color: #fff; }\n .cta a { color: #fff; }\n table { width: 100%; border-collapse: collapse; margin-top: 8px; }\n td, th { text-align: left; padding: 6px 8px; border-bottom: 1px solid #eee; vertical-align: top; }\n @media print {\n body { background: #fff; }\n .card, .score, .cta { break-inside: avoid; }\n /* Browsers default \"print background graphics\" OFF, which drops the red\n .cta background but keeps its white text — the only ask in the report\n would otherwise print white-on-white and vanish. Fall back to dark\n text on no background with a visible border instead of relying on the\n background for contrast. */\n .cta { background: none; color: #1a1a1a; border: 2px solid ${RED}; }\n .cta h2, .cta a { color: #1a1a1a; }\n }\n`;\n\n/** One of the THREE site-score cards. AI Visibility used to be a fourth and\n * was removed on purpose, matching the web report's control split: these\n * three measure the site and move when the site is fixed; visibility is\n * measured out in the world and reported with receipts in \"What the AI\n * engines said about you\" — a fourth card said it was the same kind of claim,\n * and ours to move. It is neither. `scores.aiVisibility` stays computed and\n * stored; it just is not a card, here or in the email or CLI summary. */\nfunction scoreCard(label: string, value: number | null, hint?: string): string {\n const n =\n value === null ? `<div class=\"n na\">Not measured</div>` : `<div class=\"n\">${value}</div>`;\n const hintHtml = hint ? `<div class=\"hint\">${escapeHtml(hint)}</div>` : \"\";\n return `<div class=\"score\">${n}<div class=\"l\">${escapeHtml(label)}</div>${hintHtml}</div>`;\n}\n\n/** Body for a stage that succeeded, or a uniform \"Not measured\" note when it didn't. */\nfunction stageBody<T>(stage: StageResult<T>, body: (data: T) => string): string {\n return stage.ok ? body(stage.data) : notMeasuredNote();\n}\n\n/** The stage's real error is deliberately not a parameter here — see\n * STAGE_FAILED_MESSAGE above. Every genuine failure reads the same\n * client-safe way, regardless of what actually broke. */\nfunction notMeasuredNote(): string {\n return `<p class=\"muted\">Not measured — ${STAGE_FAILED_MESSAGE}.</p>`;\n}\n\n/** A stage a human deliberately turned off (--no-probes, or analyze skipped\n * because checks failed upstream) must read as \"you asked us to skip this\" —\n * never as \"we tried and could not\", which is what a bare \"Not measured —\n * {error}\" would otherwise imply. Compares against the pipeline's exported\n * constants rather than a retyped string literal. */\nfunction skippableStageNote(error: string, skipConstant: string, skipMessage: string): string {\n if (error === skipConstant) {\n return `<p class=\"muted\">${escapeHtml(skipMessage)}</p>`;\n }\n return notMeasuredNote();\n}\n\n/** `new Date(iso).toLocaleDateString()` never throws on a bad timestamp — it\n * silently returns \"Invalid Date\" — so a try/catch here is dead code. Guard\n * on getTime() instead, and fall back to the raw ISO string so a corrupt\n * timestamp is at least visibly a timestamp rather than the literal words\n * \"Invalid Date\". */\nfunction formatIsoDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return iso;\n return d.toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n timeZone: \"UTC\",\n });\n}\n\n/** The probes section leads the report, so a branded-only run (analyze\n * failed upstream and supplied no buyer questions, or every category query\n * itself failed) must not let the branded recognition line read as a\n * positive discoverability finding on its own — an engine echoing a name\n * it was just handed proves nothing about whether a buyer who never named\n * the business would find it. */\n/** `businessNameUsed` is whether the engines were actually queried with the\n * business NAME — not merely whether one existed. `resolveBusinessName` falls\n * back to the bare domain for anything it cannot use, so a name can be present\n * and still never reach a query. Without this gate, \"even with the name handed\n * to them\" is a false claim: whenever businessName was empty, and (until the\n * caller was fixed) whenever the name was rejected and the probes searched for\n * the domain instead. Never derived from the raw ProbesResult, which has no\n * way to say \"no name was available\". */\nfunction buildProbesSection(p: ProbesResult, businessNameUsed: boolean): string {\n const byKind = new Map<ProbeAnswer[\"kind\"], ProbeAnswer[]>();\n for (const a of p.answers) {\n const bucket = byKind.get(a.kind) ?? [];\n bucket.push(a);\n byKind.set(a.kind, bucket);\n }\n\n const recognition = p.brandedRecognized\n ? \"<p>When the engines were asked about the business by name, they recognized it.</p>\"\n : businessNameUsed\n ? \"<p>When the engines were asked about the business by name, they did not recognize it — a real citation of the site never showed up, even with the name handed to them.</p>\"\n : \"<p>The engines did not recognize the business — there was no business name available to ask them with, so this reflects a harder starting point than a named query would.</p>\";\n\n const categoryCaveat = byKind.has(\"category\")\n ? \"\"\n : `<p class=\"muted\"><strong>No buyer-question (category) query was tested here</strong> — only name-recognition. An engine echoing back a name it was just given says nothing about whether a buyer who had never heard of the business would be shown it; treat the line above as a floor, not a visibility signal.</p>`;\n\n // A degraded run says so. The score now divides by what was ASKED, so failed\n // probes push it DOWN rather than silently inflating it — which is the safe\n // direction, but only if the reader is told the run was incomplete. Silence\n // here would trade one misleading number for another.\n // Bound to a local so the narrowing survives into the template — `missing > 0`\n // says nothing to the compiler about `p.categoryProbes` itself, and a stored\n // report from before the field existed genuinely does not have it.\n const tally = p.categoryProbes;\n const missing = tally ? tally.attempted - tally.answered : 0;\n const degradedNotice =\n tally && missing > 0\n ? `<p class=\"muted\"><strong>${missing} of ${tally.attempted} buyer-question searches did not come back</strong> — the engine errored on them. They are counted as \"not found\", so the visibility figure above is a floor: the real number could be higher, not lower. Worth re-running before drawing a conclusion from it.</p>`\n : \"\";\n\n const groups = KIND_ORDER.filter((k) => byKind.has(k))\n .map((kind) => {\n const answers = byKind.get(kind) ?? [];\n const cards = answers\n .map(\n (a) => `<div class=\"card\">\n <div class=\"q\">${escapeHtml(a.engine)} · “${escapeHtml(a.query)}”</div>\n <p class=\"muted\">Asked ${escapeHtml(formatIsoDate(a.askedAt))}</p>\n <p>${escapeHtml(a.snippet)}${a.truncated ? \"…\" : \"\"}</p>\n <p class=\"muted\">${\n // The scorer's own decision, recorded on the answer — NOT\n // `domainCited || brandMentioned`, which is a looser rule. Using the\n // loose one printed \"You were named in this answer\" above a card\n // contributing zero to the score beside it. Reports persisted before\n // the field existed fall back to what they already said.\n (a.countedAsVisible ?? (a.domainCited || a.brandMentioned))\n ? \"You were named in this answer.\"\n : \"You were not named in this answer.\"\n }${a.citedDomains.length ? ` Sources the engine retrieved: ${escapeHtml(a.citedDomains.join(\", \"))}` : \"\"}</p>\n </div>`,\n )\n .join(\"\");\n return `<h3>${escapeHtml(KIND_LABEL[kind])}</h3>${cards}`;\n })\n .join(\"\");\n\n const competitors = p.competitorsSeen.length\n ? `<h3>Who the engines cited instead</h3><ul>${p.competitorsSeen\n .map((c) => `<li>${escapeHtml(c.domain)} — ${c.count} time${c.count === 1 ? \"\" : \"s\"}</li>`)\n .join(\"\")}</ul>`\n : \"\";\n\n return recognition + categoryCaveat + degradedNotice + groups + competitors;\n}\n\n/** crawlerAccessMeasured is false only when the robots.txt fetch itself\n * failed, so the blocked/allowed lists are empty out of ignorance, not\n * because we confirmed access. Saying \"every crawler can reach the site\" in\n * that case would manufacture a finding from our own missing data.\n * sitemapMeasured/llmsTxtMeasured get the same treatment as\n * crawlerAccessMeasured: a failed fetch reads as \"not measured\", never as a\n * confirmed \"missing\" — that's a claim about the prospect's site we haven't\n * earned. Item 1: none of the three \"not measured\" lines below take the raw\n * fetch error as input anymore — status codes and transport vocabulary\n * (\"503 Service Unavailable\", \"ETIMEDOUT\") are operator vocabulary, same as\n * a stage failure (see STAGE_FAILED_MESSAGE); the real error still lives in\n * the persisted JSON and CLI output, never in this client-facing page. */\nfunction buildFindabilitySection(c: ChecksResult): string {\n const accessBlock = !c.crawlerAccessMeasured\n ? `<p class=\"muted\">Crawler access: not measured — ${STAGE_FAILED_MESSAGE}.</p>`\n : c.crawlerAccess.blockedAi.length\n ? `<p><strong>Blocked AI crawlers:</strong> ${escapeHtml(\n c.crawlerAccess.blockedAi.map(describeAgent).join(\", \"),\n )}</p>`\n : // Says what was measured, which is robots.txt, and no more. \"Every AI\n // crawler can reach the site\" was a claim robots.txt cannot support:\n // a CDN's bot management enforces its own answer and can contradict the\n // file without the owner knowing. Confirmed on a live prospect: its\n // robots.txt blocked nothing relevant, and its CDN returned 403 to one\n // named AI crawler on every request while serving a browser and two\n // other crawlers normally. The report said they were fine.\n `<p>Nothing in your robots.txt blocks the AI crawlers we checked.</p>`;\n const classical =\n c.crawlerAccessMeasured && c.crawlerAccess.blockedClassical.length\n ? `<p><strong>Blocked search crawlers:</strong> ${escapeHtml(\n c.crawlerAccess.blockedClassical.join(\", \"),\n )}</p>`\n : \"\";\n const sitemapLine = c.sitemapMeasured\n ? `<li>sitemap.xml: ${c.sitemapPresent ? \"present\" : \"missing\"}</li>`\n : `<li>sitemap.xml: not measured — ${STAGE_FAILED_MESSAGE}.</li>`;\n // llms.txt is no longer listed as a finding. It used to sit here as\n // \"present\"/\"missing\" beside sitemap.xml, which put the two on equal footing\n // — and \"missing\" beside a checklist reads as a job to do. Search crawlers\n // demonstrably read a sitemap; no answer engine has documented reading\n // llms.txt to build an answer. The footnote below says that outright instead\n // of leaving a prospect to infer it, or to go and build one on our say-so.\n const llmsFootnote = `<p class=\"muted\"><strong>A note on llms.txt.</strong> You may have been told to\n add one. We check for it — this site ${\n !c.llmsTxtMeasured ? \"could not be checked\" : c.llmsTxtPresent ? \"has one\" : \"does not\"\n } — but we do not score it and we will not recommend it. It is a 2024 proposal that no\n answer engine has committed to reading, and adding one has no measured effect on whether you\n are cited. If that changes we will say so and score it then.</p>`;\n return `${accessBlock}${classical}\n <ul>\n ${sitemapLine}\n <li>Pages missing a meta description: ${c.meta.missingDescription} of ${c.meta.pageCount}</li>\n <li>Pages missing a canonical URL: ${c.meta.missingCanonical} of ${c.meta.pageCount}</li>\n <li>Pages missing share images/titles: ${c.meta.missingSocial} of ${c.meta.pageCount}</li>\n <li>Security headers missing: ${\n c.securityHeaders.missing.length ? escapeHtml(c.securityHeaders.missing.join(\", \")) : \"none\"\n }</li>\n ${\n c.meta.pagesWithoutExtract > 0\n ? `<li><strong>${c.meta.pagesWithoutExtract}</strong> additional page${\n c.meta.pagesWithoutExtract === 1 ? \"\" : \"s\"\n } produced no readable content at all and ${\n c.meta.pagesWithoutExtract === 1 ? \"is\" : \"are\"\n } not counted in the ${c.meta.pageCount} pages above.</li>`\n : \"\"\n }\n </ul>${llmsFootnote}`;\n}\n\n/** Lighthouse runs as its own independent pipeline stage (see pipeline.ts) —\n * it does not depend on `checks` succeeding. Rendering it off `result.checks`\n * would throw away real, measured performance/SEO/accessibility numbers\n * whenever an unrelated stage failed, so this reads `result.lighthouse`\n * directly and is rendered unconditionally, not nested inside the checks\n * degrade path. */\nfunction buildLighthouseBlock(lh: StageResult<LighthouseScores>): string {\n return lh.ok\n ? `<p class=\"muted\">Lighthouse — performance ${lh.data.performance ?? \"not measured\"},\n SEO ${lh.data.seo ?? \"not measured\"}, accessibility ${lh.data.accessibility ?? \"not measured\"}.</p>`\n : `<p class=\"muted\">Lighthouse not measured — ${STAGE_FAILED_MESSAGE}.</p>`;\n}\n\nfunction buildReadabilitySection(c: ChecksResult): string {\n const jsLine =\n c.jsDependence.avgMissing === null\n ? `<p class=\"muted\">JavaScript-dependence: not measured — no page produced a comparable raw/rendered pair.</p>`\n : `<p><strong>${Math.round(c.jsDependence.avgMissing * 100)}%</strong> of the words a visitor reads only appear after JavaScript runs.\n Most AI crawlers do not run JavaScript, so that share of your site is invisible to them.</p>`;\n return `${jsLine}\n <ul>\n <li>Structured data found: ${c.schema.typesFound.length ? escapeHtml(c.schema.typesFound.join(\", \")) : \"none\"}</li>\n <li>Expected structured data missing: ${\n c.schema.missingExpected.length ? escapeHtml(c.schema.missingExpected.join(\", \")) : \"none\"\n }</li>\n <li>Pages without a top-level heading: ${c.headings.pagesWithoutH1} of ${c.meta.pageCount}</li>\n ${\n c.meta.pagesWithoutExtract > 0\n ? `<li><strong>${c.meta.pagesWithoutExtract}</strong> page${\n c.meta.pagesWithoutExtract === 1 ? \"\" : \"s\"\n } produced no extract at all — excluded from every figure above, not counted as readable or unreadable.</li>`\n : \"\"\n }\n </ul>`;\n}\n\n/** The report's whole premise is claims the recipient can go check — so the\n * one quoted passage of evidence links to the page it came from (via\n * safeUrl; a null/non-http page renders as plain escaped text, no dead\n * \"#\" anchor). `answered`/`impact`/`effort` are escaped even though a zod\n * enum constrains them upstream — the renderer should not depend on\n * another file staying in sync to stay safe. */\n/** What the verdict column says. \"unknown\" is OUR gap — the model returned no\n * verdict for that question — so it must not be printed as a finding about the\n * site, and must not look like one either. Matches the web report's labels. */\nconst ANSWERED_LABEL: Record<string, string> = {\n yes: \"yes\",\n partial: \"partial\",\n no: \"no\",\n unknown: \"not measured\",\n};\n\nfunction buildAnswersSection(a: AnalyzeResult): string {\n const rows = a.buyerQuestions\n .map((q) => {\n const answered = escapeHtml(q.answered);\n const label = escapeHtml(ANSWERED_LABEL[q.answered] ?? q.answered);\n const evidenceText = q.evidence ? escapeHtml(q.evidence) : null;\n const evidenceHref = q.evidence && q.page ? safeUrl(q.page) : null;\n // An unmeasured question has no evidence for the same reason it has no\n // verdict: we never got one back. Printing \"no passage on the site\" there\n // reports our own missing measurement as a defect of theirs.\n const evidenceCell =\n q.answered === \"unknown\"\n ? '<span class=\"muted\">we did not get a verdict on this question</span>'\n : evidenceText === null\n ? '<span class=\"muted\">no passage on the site</span>'\n : evidenceHref && evidenceHref !== \"#\"\n ? `<a href=\"${escapeHtml(evidenceHref)}\" target=\"_blank\" rel=\"noopener noreferrer\">${evidenceText}</a>`\n : evidenceText;\n return `<tr>\n <td>${escapeHtml(q.question)}</td>\n <td><span class=\"tag ${answered}\">${label}</span></td>\n <td>${evidenceCell}</td>\n </tr>`;\n })\n .join(\"\");\n return `<p>${escapeHtml(a.narrative.answers)}</p>\n <table><tr><th>What buyers ask</th><th>Answered</th><th>Evidence</th></tr>${rows}</table>`;\n}\n\nexport function renderProspectReport(result: ProspectAuditResult): string {\n const host = hostnameOf(result.url);\n // `businessName` is the searchable NAME (possibly \"\"), never the\n // description. An empty/missing name is itself a finding — fall back to\n // the hostname rather than printing nothing, or worse, printing \"\" as if\n // it were a verified fact about the business.\n const name = result.businessName && result.businessName.trim() ? result.businessName : host;\n const date = formatIsoDate(result.generatedAt);\n\n const findabilitySection = stageBody(result.checks, buildFindabilitySection);\n const lighthouseBlock = buildLighthouseBlock(result.lighthouse);\n const readabilitySection = stageBody(result.checks, buildReadabilitySection);\n\n // A real business name was resolved and handed to the engines only when\n // `result.businessName` is non-empty — the same value the pipeline\n // actually queried with (see pipeline.ts). Used to gate the probes\n // section's \"even with the name handed to them\" phrasing (Fix 6).\n // Whether the engines were actually queried with the NAME, which is not the\n // same as whether a name existed. resolveBusinessName falls back to the bare\n // domain for anything it cannot use, and this line used to read the\n // un-resolved value — so a report could claim the engines were given the name\n // \"even with the name handed to them\" while the probes had in fact searched\n // for \"stlouisroofing.com\". Resolve it the same way probes.ts does.\n const businessNameUsed = Boolean(\n result.businessName &&\n result.businessName.trim() &&\n resolveBusinessName(result.businessName, result.url) === result.businessName.trim(),\n );\n\n const fixes = result.analyze.ok\n ? (() => {\n const sorted = [...result.analyze.data.fixes].sort(\n (x, y) => IMPACT_ORDER[x.impact] - IMPACT_ORDER[y.impact],\n );\n if (sorted.length === 0) {\n // A genuinely well-optimized site: a bare empty list reads as a\n // rendering bug, not a compliment. Say so directly, and keep this\n // section as a pitch — it's the one the salesperson is sending\n // the report to make.\n return `<p>Nothing here reads as urgent — the essentials are already in place.\n Reddoor can still help push this further: tightening structured data, monitoring\n how the answer engines describe you, and keeping pace as they change.</p>`;\n }\n return `<ol>${sorted\n .map((f) => {\n const impact = escapeHtml(f.impact);\n const effort = escapeHtml(f.effort);\n return `<li><strong>${escapeHtml(f.title)}</strong> — ${escapeHtml(f.why)}\n <span class=\"muted\">(${impact} impact, ${effort} effort)</span></li>`;\n })\n .join(\"\")}</ol>`;\n })()\n : skippableStageNote(\n result.analyze.error,\n ANALYZE_SKIPPED,\n \"Skipped — the checks stage failed, so there was nothing to build fixes from.\",\n );\n\n const probesSectionFinal = result.probes.ok\n ? buildProbesSection(result.probes.data, businessNameUsed)\n : skippableStageNote(\n result.probes.error,\n PROBES_SKIPPED,\n \"This audit did not run the AI-visibility probes.\",\n );\n\n const answersSectionFinal = result.analyze.ok\n ? buildAnswersSection(result.analyze.data)\n : skippableStageNote(\n result.analyze.error,\n ANALYZE_SKIPPED,\n \"Skipped — the checks stage failed, so there were no buyer questions to check.\",\n );\n\n const description = result.analyze.ok ? result.analyze.data.business : null;\n const narrative = result.analyze.ok\n ? `<p class=\"lede\">${escapeHtml(description ?? \"\")}</p>\n <p class=\"lede\">${escapeHtml(result.analyze.data.narrative.findability)}\n ${escapeHtml(result.analyze.data.narrative.readability)}</p>`\n : \"\";\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<meta name=\"robots\" content=\"noindex\" />\n<title>Can AI find ${escapeHtml(name)}? — Reddoor audit</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n<link href=\"https://fonts.googleapis.com/css2?family=Besley:wght@400;600&display=swap\" rel=\"stylesheet\" />\n<style>${STYLES}</style>\n</head>\n<body>\n<div class=\"wrap\">\n <h1>Can AI and Google actually find ${escapeHtml(name)}?</h1>\n <p class=\"lede\"><a href=\"${escapeHtml(safeUrl(result.url))}\" target=\"_blank\" rel=\"noopener noreferrer\">${escapeHtml(result.url)}</a> · audited ${escapeHtml(date)} by Reddoor Creative</p>\n ${narrative}\n\n <p class=\"muted\">Each score below runs 0–100 — read it as a score, not a percentage.</p>\n <div class=\"scores\">\n ${scoreCard(\"Findability\", result.scores.findability, \"How easily AI and search crawlers can find and reach your site — crawl rules, sitemap, key metadata\")}\n ${scoreCard(\"Readability\", result.scores.readability, \"How much of your site's content those crawlers can actually read once they're in — most don't run JavaScript\")}\n ${scoreCard(\"Answers\", result.scores.answers, \"How many buyer questions your site's own content answers — separate from what the AI engines say back\")}\n </div>\n\n <h2>What the AI engines said about you</h2>\n ${probesSectionFinal}\n\n <h2>What the crawlers can reach</h2>\n ${findabilitySection}\n ${lighthouseBlock}\n\n <h2>What the crawlers can read</h2>\n ${readabilitySection}\n\n <h2>The questions your buyers ask</h2>\n ${answersSectionFinal}\n\n <h2>What to fix first</h2>\n ${fixes}\n\n <div class=\"cta\">\n <h2>Want this fixed?</h2>\n <p>Reddoor Creative rebuilds sites so answer engines can read, quote and recommend them.\n Reply to the email this link came from, or start at\n <a href=\"https://reddoorla.com/\">reddoorla.com</a>.</p>\n </div>\n</div>\n</body>\n</html>`;\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  ingestSubmission
3
- } from "./chunk-JRHIFA5R.js";
3
+ } from "./chunk-5XMKJX74.js";
4
4
  import "./chunk-NO74DXDP.js";
5
+ import "./chunk-PJ23RDHT.js";
5
6
  import "./chunk-2OP4JKIQ.js";
6
7
 
7
8
  // src/db/deadletter.ts
@@ -63,4 +64,4 @@ async function replayOne(db, deps, row) {
63
64
  export {
64
65
  replayDeadLetters
65
66
  };
66
- //# sourceMappingURL=replay-4S6ENYCH.js.map
67
+ //# sourceMappingURL=replay-HV7MGKBU.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/db/deadletter.ts","../src/forms/replay.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/** Rows not yet replayed, oldest first — replay preserves arrival order so the\n * duplicate/velocity signals see submissions in the order they happened. */\nexport async function listUnreplayedDeadLetters(db: Db): Promise<DeadLetterRow[]> {\n const rows = await db\n .selectFrom(\"submission_deadletter\")\n .select([\"id\", \"site_slug\", \"payload\", \"turnstile\", \"error\", \"received_at\"])\n .where(\"replayed_at\", \"is\", null)\n .orderBy(\"received_at\", \"asc\")\n .execute();\n return rows.map((r) => ({\n id: r.id,\n siteSlug: r.site_slug,\n payload: JSON.parse(r.payload) as unknown,\n turnstile: JSON.parse(r.turnstile) as TurnstileVerification,\n error: r.error,\n receivedAt: r.received_at,\n }));\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,\n * unknown-site). A replay whose lookup threw again is left untouched so the\n * next run retries it. */\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","import type { Db } from \"../db/client.js\";\nimport {\n listUnreplayedDeadLetters,\n markDeadLetterReplayed,\n type DeadLetterRow,\n} from \"../db/deadletter.js\";\nimport { ingestSubmission, type IngestDeps } from \"./ingest.js\";\n\nexport type ReplayResult = {\n /** Terminal outcomes this run: the row will never be picked up again. */\n replayed: Array<{ id: string; outcome: string; submissionId: string | null }>;\n /** Rows whose lookup threw AGAIN — left unreplayed for the next run. */\n stillFailing: Array<{ id: string; error: string }>;\n};\n\n/**\n * Run every unreplayed dead-letter back through `ingestSubmission` — the SAME\n * pipeline a live submission takes, so a replayed lead gets real spam\n * classification, notify, and fan-out, not a bespoke re-implementation.\n *\n * Each row replays with the Turnstile verification computed at receipt (tokens\n * expire in 300s; re-verifying is impossible by design, see deadletter.ts).\n *\n * `deps` must NOT wire `deadLetter`: a lookup that throws during replay would\n * otherwise dead-letter the SAME lead a second time while the original row stays\n * unreplayed — every retry would mint a duplicate. Enforced here by stripping the\n * key rather than trusting the caller. A thrown lookup leaves the row untouched\n * for the next run; every RESOLVED outcome (accepted, rejected, unknown-site) is\n * terminal — re-running cannot improve on an answer the store actually gave.\n *\n * Oldest-first (listUnreplayedDeadLetters orders by received_at) so the\n * duplicate/velocity spam signals see submissions in arrival order.\n */\nexport async function replayDeadLetters(\n db: Db,\n deps: Omit<IngestDeps, \"deadLetter\">,\n): Promise<ReplayResult> {\n const rows = await listUnreplayedDeadLetters(db);\n const result: ReplayResult = { replayed: [], stillFailing: [] };\n\n for (const row of rows) {\n const outcome = await replayOne(db, deps, row);\n if (outcome.terminal) {\n result.replayed.push({\n id: row.id,\n outcome: outcome.outcome,\n submissionId: outcome.submissionId,\n });\n } else {\n result.stillFailing.push({ id: row.id, error: outcome.error });\n }\n }\n return result;\n}\n\nasync function replayOne(\n db: Db,\n deps: Omit<IngestDeps, \"deadLetter\">,\n row: DeadLetterRow,\n): Promise<\n | { terminal: true; outcome: string; submissionId: string | null }\n | { terminal: false; error: string }\n> {\n // Strip deadLetter defensively even though the type forbids it — a structurally\n // wider object passed through `as` must not re-arm the duplicate loop.\n const { deadLetter: _stripped, ...safeDeps } = deps as IngestDeps;\n void _stripped;\n let res;\n try {\n res = await ingestSubmission(safeDeps, row.siteSlug, row.payload, row.turnstile);\n } catch (err) {\n return { terminal: false, error: String(err) };\n }\n const now = deps.now();\n if (res.status === \"accepted\") {\n await markDeadLetterReplayed(db, row.id, \"accepted\", res.submissionId, now);\n return { terminal: true, outcome: \"accepted\", submissionId: res.submissionId };\n }\n const outcome = res.status === \"rejected\" ? `rejected:${res.reason}` : \"unknown-site\";\n await markDeadLetterReplayed(db, row.id, outcome, null, now);\n return { terminal: true, outcome, submissionId: null };\n}\n"],"mappings":";;;;;;;AAsDA,eAAsB,0BAA0B,IAAkC;AAChF,QAAM,OAAO,MAAM,GAChB,WAAW,uBAAuB,EAClC,OAAO,CAAC,MAAM,aAAa,WAAW,aAAa,SAAS,aAAa,CAAC,EAC1E,MAAM,eAAe,MAAM,IAAI,EAC/B,QAAQ,eAAe,KAAK,EAC5B,QAAQ;AACX,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,IAAI,EAAE;AAAA,IACN,UAAU,EAAE;AAAA,IACZ,SAAS,KAAK,MAAM,EAAE,OAAO;AAAA,IAC7B,WAAW,KAAK,MAAM,EAAE,SAAS;AAAA,IACjC,OAAO,EAAE;AAAA,IACT,YAAY,EAAE;AAAA,EAChB,EAAE;AACJ;AAMA,eAAsB,uBACpB,IACA,IACA,SACA,cACA,KACe;AACf,QAAM,GACH,YAAY,uBAAuB,EACnC,IAAI;AAAA,IACH,aAAa,IAAI,YAAY;AAAA,IAC7B,gBAAgB;AAAA,IAChB,sBAAsB;AAAA,EACxB,CAAC,EACA,MAAM,MAAM,KAAK,EAAE,EACnB,QAAQ;AACb;;;AC1DA,eAAsB,kBACpB,IACA,MACuB;AACvB,QAAM,OAAO,MAAM,0BAA0B,EAAE;AAC/C,QAAM,SAAuB,EAAE,UAAU,CAAC,GAAG,cAAc,CAAC,EAAE;AAE9D,aAAW,OAAO,MAAM;AACtB,UAAM,UAAU,MAAM,UAAU,IAAI,MAAM,GAAG;AAC7C,QAAI,QAAQ,UAAU;AACpB,aAAO,SAAS,KAAK;AAAA,QACnB,IAAI,IAAI;AAAA,QACR,SAAS,QAAQ;AAAA,QACjB,cAAc,QAAQ;AAAA,MACxB,CAAC;AAAA,IACH,OAAO;AACL,aAAO,aAAa,KAAK,EAAE,IAAI,IAAI,IAAI,OAAO,QAAQ,MAAM,CAAC;AAAA,IAC/D;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,UACb,IACA,MACA,KAIA;AAGA,QAAM,EAAE,YAAY,WAAW,GAAG,SAAS,IAAI;AAC/C,OAAK;AACL,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,iBAAiB,UAAU,IAAI,UAAU,IAAI,SAAS,IAAI,SAAS;AAAA,EACjF,SAAS,KAAK;AACZ,WAAO,EAAE,UAAU,OAAO,OAAO,OAAO,GAAG,EAAE;AAAA,EAC/C;AACA,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,IAAI,WAAW,YAAY;AAC7B,UAAM,uBAAuB,IAAI,IAAI,IAAI,YAAY,IAAI,cAAc,GAAG;AAC1E,WAAO,EAAE,UAAU,MAAM,SAAS,YAAY,cAAc,IAAI,aAAa;AAAA,EAC/E;AACA,QAAM,UAAU,IAAI,WAAW,aAAa,YAAY,IAAI,MAAM,KAAK;AACvE,QAAM,uBAAuB,IAAI,IAAI,IAAI,SAAS,MAAM,GAAG;AAC3D,SAAO,EAAE,UAAU,MAAM,SAAS,cAAc,KAAK;AACvD;","names":[]}
1
+ {"version":3,"sources":["../src/db/deadletter.ts","../src/forms/replay.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/** Rows not yet replayed, oldest first — replay preserves arrival order so the\n * duplicate/velocity signals see submissions in the order they happened. */\nexport async function listUnreplayedDeadLetters(db: Db): Promise<DeadLetterRow[]> {\n const rows = await db\n .selectFrom(\"submission_deadletter\")\n .select([\"id\", \"site_slug\", \"payload\", \"turnstile\", \"error\", \"received_at\"])\n .where(\"replayed_at\", \"is\", null)\n .orderBy(\"received_at\", \"asc\")\n .execute();\n return rows.map((r) => ({\n id: r.id,\n siteSlug: r.site_slug,\n payload: JSON.parse(r.payload) as unknown,\n turnstile: JSON.parse(r.turnstile) as TurnstileVerification,\n error: r.error,\n receivedAt: r.received_at,\n }));\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,\n * unknown-site). A replay whose lookup threw again is left untouched so the\n * next run retries it. */\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","import type { Db } from \"../db/client.js\";\nimport {\n listUnreplayedDeadLetters,\n markDeadLetterReplayed,\n type DeadLetterRow,\n} from \"../db/deadletter.js\";\nimport { ingestSubmission, type IngestDeps } from \"./ingest.js\";\n\nexport type ReplayResult = {\n /** Terminal outcomes this run: the row will never be picked up again. */\n replayed: Array<{ id: string; outcome: string; submissionId: string | null }>;\n /** Rows whose lookup threw AGAIN — left unreplayed for the next run. */\n stillFailing: Array<{ id: string; error: string }>;\n};\n\n/**\n * Run every unreplayed dead-letter back through `ingestSubmission` — the SAME\n * pipeline a live submission takes, so a replayed lead gets real spam\n * classification, notify, and fan-out, not a bespoke re-implementation.\n *\n * Each row replays with the Turnstile verification computed at receipt (tokens\n * expire in 300s; re-verifying is impossible by design, see deadletter.ts).\n *\n * `deps` must NOT wire `deadLetter`: a lookup that throws during replay would\n * otherwise dead-letter the SAME lead a second time while the original row stays\n * unreplayed — every retry would mint a duplicate. Enforced here by stripping the\n * key rather than trusting the caller. A thrown lookup leaves the row untouched\n * for the next run; every RESOLVED outcome (accepted, rejected, unknown-site) is\n * terminal — re-running cannot improve on an answer the store actually gave.\n *\n * Oldest-first (listUnreplayedDeadLetters orders by received_at) so the\n * duplicate/velocity spam signals see submissions in arrival order.\n */\nexport async function replayDeadLetters(\n db: Db,\n deps: Omit<IngestDeps, \"deadLetter\">,\n): Promise<ReplayResult> {\n const rows = await listUnreplayedDeadLetters(db);\n const result: ReplayResult = { replayed: [], stillFailing: [] };\n\n for (const row of rows) {\n const outcome = await replayOne(db, deps, row);\n if (outcome.terminal) {\n result.replayed.push({\n id: row.id,\n outcome: outcome.outcome,\n submissionId: outcome.submissionId,\n });\n } else {\n result.stillFailing.push({ id: row.id, error: outcome.error });\n }\n }\n return result;\n}\n\nasync function replayOne(\n db: Db,\n deps: Omit<IngestDeps, \"deadLetter\">,\n row: DeadLetterRow,\n): Promise<\n | { terminal: true; outcome: string; submissionId: string | null }\n | { terminal: false; error: string }\n> {\n // Strip deadLetter defensively even though the type forbids it — a structurally\n // wider object passed through `as` must not re-arm the duplicate loop.\n const { deadLetter: _stripped, ...safeDeps } = deps as IngestDeps;\n void _stripped;\n let res;\n try {\n res = await ingestSubmission(safeDeps, row.siteSlug, row.payload, row.turnstile);\n } catch (err) {\n return { terminal: false, error: String(err) };\n }\n const now = deps.now();\n if (res.status === \"accepted\") {\n await markDeadLetterReplayed(db, row.id, \"accepted\", res.submissionId, now);\n return { terminal: true, outcome: \"accepted\", submissionId: res.submissionId };\n }\n const outcome = res.status === \"rejected\" ? `rejected:${res.reason}` : \"unknown-site\";\n await markDeadLetterReplayed(db, row.id, outcome, null, now);\n return { terminal: true, outcome, submissionId: null };\n}\n"],"mappings":";;;;;;;;AAsDA,eAAsB,0BAA0B,IAAkC;AAChF,QAAM,OAAO,MAAM,GAChB,WAAW,uBAAuB,EAClC,OAAO,CAAC,MAAM,aAAa,WAAW,aAAa,SAAS,aAAa,CAAC,EAC1E,MAAM,eAAe,MAAM,IAAI,EAC/B,QAAQ,eAAe,KAAK,EAC5B,QAAQ;AACX,SAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACtB,IAAI,EAAE;AAAA,IACN,UAAU,EAAE;AAAA,IACZ,SAAS,KAAK,MAAM,EAAE,OAAO;AAAA,IAC7B,WAAW,KAAK,MAAM,EAAE,SAAS;AAAA,IACjC,OAAO,EAAE;AAAA,IACT,YAAY,EAAE;AAAA,EAChB,EAAE;AACJ;AAMA,eAAsB,uBACpB,IACA,IACA,SACA,cACA,KACe;AACf,QAAM,GACH,YAAY,uBAAuB,EACnC,IAAI;AAAA,IACH,aAAa,IAAI,YAAY;AAAA,IAC7B,gBAAgB;AAAA,IAChB,sBAAsB;AAAA,EACxB,CAAC,EACA,MAAM,MAAM,KAAK,EAAE,EACnB,QAAQ;AACb;;;AC1DA,eAAsB,kBACpB,IACA,MACuB;AACvB,QAAM,OAAO,MAAM,0BAA0B,EAAE;AAC/C,QAAM,SAAuB,EAAE,UAAU,CAAC,GAAG,cAAc,CAAC,EAAE;AAE9D,aAAW,OAAO,MAAM;AACtB,UAAM,UAAU,MAAM,UAAU,IAAI,MAAM,GAAG;AAC7C,QAAI,QAAQ,UAAU;AACpB,aAAO,SAAS,KAAK;AAAA,QACnB,IAAI,IAAI;AAAA,QACR,SAAS,QAAQ;AAAA,QACjB,cAAc,QAAQ;AAAA,MACxB,CAAC;AAAA,IACH,OAAO;AACL,aAAO,aAAa,KAAK,EAAE,IAAI,IAAI,IAAI,OAAO,QAAQ,MAAM,CAAC;AAAA,IAC/D;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,UACb,IACA,MACA,KAIA;AAGA,QAAM,EAAE,YAAY,WAAW,GAAG,SAAS,IAAI;AAC/C,OAAK;AACL,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,iBAAiB,UAAU,IAAI,UAAU,IAAI,SAAS,IAAI,SAAS;AAAA,EACjF,SAAS,KAAK;AACZ,WAAO,EAAE,UAAU,OAAO,OAAO,OAAO,GAAG,EAAE;AAAA,EAC/C;AACA,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,IAAI,WAAW,YAAY;AAC7B,UAAM,uBAAuB,IAAI,IAAI,IAAI,YAAY,IAAI,cAAc,GAAG;AAC1E,WAAO,EAAE,UAAU,MAAM,SAAS,YAAY,cAAc,IAAI,aAAa;AAAA,EAC/E;AACA,QAAM,UAAU,IAAI,WAAW,aAAa,YAAY,IAAI,MAAM,KAAK;AACvE,QAAM,uBAAuB,IAAI,IAAI,IAAI,SAAS,MAAM,GAAG;AAC3D,SAAO,EAAE,UAAU,MAAM,SAAS,cAAc,KAAK;AACvD;","names":[]}
@@ -78,14 +78,14 @@ function dashboardBaseUrl() {
78
78
  }
79
79
  async function runReportCommand(slug, opts) {
80
80
  if (opts.digest) {
81
- const { runDigest } = await import("./digest-CS6KLCZB.js");
81
+ const { runDigest } = await import("./digest-EOA4JLTV.js");
82
82
  return runDigest({ baseUrl: dashboardBaseUrl() });
83
83
  }
84
84
  if (opts.sendReady) {
85
- const { sendApprovedReports } = await import("./orchestrate-6HROYTM6.js");
86
- const { makeSiteMirror } = await import("./site-mirror-GCFLBBWK.js");
85
+ const { sendApprovedReports } = await import("./orchestrate-KMQIUP3H.js");
86
+ const { makeSiteMirror } = await import("./site-mirror-3E7JYTJG.js");
87
87
  const { mirrorWrite } = await import("./freeze-A6MUCZU4.js");
88
- const { openDb, readDbConfig } = await import("./client-JYKPVCJX.js");
88
+ const { openDb, readDbConfig } = await import("./client-M2V6FHH5.js");
89
89
  const { mirrorReportPatch } = await import("./fleet-state-3LQUQHYD.js");
90
90
  return sendApprovedReports({
91
91
  siteMirror: await makeSiteMirror(),
@@ -105,7 +105,7 @@ async function runReportCommand(slug, opts) {
105
105
  if (opts.rerender) {
106
106
  const { rerenderReport, formatRerenderResult } = await import("./rerender-3FM6DNRH.js");
107
107
  const { renderReportFromRow } = await import("./render-from-row-P5CWM3MQ.js");
108
- const { openDb, readDbConfig } = await import("./client-JYKPVCJX.js");
108
+ const { openDb, readDbConfig } = await import("./client-M2V6FHH5.js");
109
109
  const { getReportById, getSiteById, storeRenderedHtml } = await import("./fleet-state-3LQUQHYD.js");
110
110
  const { loadHeaderImage } = await import("./header-images-SQBCE5OD.js");
111
111
  const { fetchAttachmentBytes } = await import("./attachments-NLZIJ5NU.js");
@@ -145,9 +145,9 @@ async function runReportCommand(slug, opts) {
145
145
  }
146
146
  async function runDueDraft() {
147
147
  const base = openBase(readAirtableConfig());
148
- const { makeScheduleMirrorBestEffort } = await import("./health-mirror-GMSF3Z5R.js");
149
- const { makeReportMirror } = await import("./report-mirror-GMNSH5HF.js");
150
- const { makeSiteMirror } = await import("./site-mirror-GCFLBBWK.js");
148
+ const { makeScheduleMirrorBestEffort } = await import("./health-mirror-55RRU6BI.js");
149
+ const { makeReportMirror } = await import("./report-mirror-6ZDCAQZB.js");
150
+ const { makeSiteMirror } = await import("./site-mirror-3E7JYTJG.js");
151
151
  const result = await draftDueReports(
152
152
  base,
153
153
  /* @__PURE__ */ new Date(),
@@ -337,8 +337,8 @@ async function runSingleSiteDraft(slug, opts) {
337
337
  if (!site) {
338
338
  throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
339
339
  }
340
- const reportMirror = opts.previewOnly ? null : await (await import("./report-mirror-GMNSH5HF.js")).makeReportMirror();
341
- const siteMirror = opts.previewOnly ? null : await (await import("./site-mirror-GCFLBBWK.js")).makeSiteMirror();
340
+ const reportMirror = opts.previewOnly ? null : await (await import("./report-mirror-6ZDCAQZB.js")).makeReportMirror();
341
+ const siteMirror = opts.previewOnly ? null : await (await import("./site-mirror-3E7JYTJG.js")).makeSiteMirror();
342
342
  const result = await draftReportForSite(opts.previewOnly ? null : base, site, opts.reportType, {
343
343
  previewOnly: opts.previewOnly,
344
344
  // Only forced on. Left undefined, draftReportForSite keeps its default
@@ -366,4 +366,4 @@ export {
366
366
  runReportCommand,
367
367
  writeNextDueDates
368
368
  };
369
- //# sourceMappingURL=report-AAY2HZZR.js.map
369
+ //# sourceMappingURL=report-Y4LXDHRV.js.map
@@ -13,8 +13,8 @@ import "./chunk-7NYN2P6I.js";
13
13
  import {
14
14
  openDb,
15
15
  readDbConfig
16
- } from "./chunk-WS6NU475.js";
17
- import "./chunk-MFHS7IQ2.js";
16
+ } from "./chunk-22GKJO5F.js";
17
+ import "./chunk-OCNSIBMP.js";
18
18
  import "./chunk-6VAL7XJT.js";
19
19
 
20
20
  // src/reports/report-mirror.ts
@@ -52,4 +52,4 @@ async function makeReportMirror(open = () => openDb(readDbConfig()), strict = TU
52
52
  export {
53
53
  makeReportMirror
54
54
  };
55
- //# sourceMappingURL=report-mirror-GMNSH5HF.js.map
55
+ //# sourceMappingURL=report-mirror-6ZDCAQZB.js.map
@@ -15,4 +15,4 @@ var DATABASE_TABLES = [
15
15
  export {
16
16
  DATABASE_TABLES
17
17
  };
18
- //# sourceMappingURL=schema-75PJECAE.js.map
18
+ //# sourceMappingURL=schema-2V7BGYVT.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/db/schema.ts"],"sourcesContent":["// The hand-written shape Kysely types every query from. SQLite stores TEXT/INTEGER\n// only, so these are the column *shapes* — the value domains (form_type, status,\n// notify_status) are still narrowed at read time by the validators in\n// src/reports/submission-row.ts. Keep this in lockstep with src/db/migrations.ts;\n// drift is caught by the in-memory tests in tests/db/, not by codegen.\n\nexport interface SubmissionsTable {\n id: string;\n submission_id: number | null;\n site_id: string;\n form_type: string;\n name: string;\n email: string;\n phone: string | null;\n message: string | null;\n extra_fields: string | null;\n source_url: string | null;\n utm: string | null;\n submitted_at: string | null;\n status: string;\n notify_status: string;\n resend_message_id: string | null;\n spam_score: number | null;\n spam_reason: string | null;\n fanout_status: string | null;\n}\n\nexport interface SpamScreenoutsTable {\n site_id: string;\n date: string;\n honeypot: number;\n too_fast: number;\n marked_spam: number;\n}\n\nexport interface FleetEventsTable {\n id: string;\n ts: string;\n type: string;\n site_id: string | null;\n site_name: string | null;\n summary: string;\n data: string | null;\n created_at: string;\n}\n\nexport interface MigrationsTable {\n id: string;\n applied_at: string;\n}\n\n/** A lead whose site lookup threw mid-ingest (migration 0006). `payload` and\n * `turnstile` are JSON — the raw wire body and the verification computed at\n * receipt — exactly what `db replay-deadletters` feeds back through\n * `ingestSubmission` once the lookup recovers. */\nexport interface SubmissionDeadletterTable {\n id: string;\n site_slug: string;\n payload: string;\n turnstile: string;\n error: string;\n received_at: string;\n replayed_at: string | null;\n replay_outcome: string | null;\n replay_submission_id: string | null;\n}\n\n/** Operator-owned fleet config (migration 0007). PK = Airtable rec id (D1).\n * `legacy` holds the 33 populated-but-code-unreferenced Airtable columns as a\n * JSON object keyed by original column name; the plaintext DNS/cms credential\n * cells never migrate (operator ruling 2026-08-23). */\nexport interface SitesTable {\n id: string;\n slug: string;\n name: string;\n url: string | null;\n status: string | null;\n point_of_contact: string | null;\n maintenance_freq: string | null;\n testing_freq: string | null;\n maintenance_day: string | null;\n testing_day: string | null;\n ga4_property_id: string | null;\n search_query: string | null;\n search_console_property: string | null;\n git_repo: string | null;\n netlify_id: string | null;\n report_recipients_to: string | null;\n report_recipients_cc: string | null;\n copy_intro: string | null;\n copy_contact: string | null;\n copy_footer: string | null;\n newsletter_webhook: string | null;\n mailchimp_api_key: string | null;\n mailchimp_audience_id: string | null;\n notify_routing: string | null;\n require_turnstile: number;\n accepted_watch_conditions: string | null;\n prismic_ack_until: string | null;\n launched_at: string | null;\n header_image: Uint8Array | null;\n header_image_filename: string | null;\n header_image_type: string | null;\n header_image_generated_at: string | null;\n legacy: string | null;\n}\n\n/** Nightly-cron-owned health/telemetry (migration 0007) — one row per site,\n * written as one batched upsert per sweep family. `analytics_soft_fail_at` is\n * the \"Analytics soft-fail at\" column code always wrote best-effort but no\n * operator ever created in Airtable. */\nexport interface SiteHealthTable {\n site_id: string;\n p_score: number | null;\n r_score: number | null;\n bp_score: number | null;\n seo_score: number | null;\n lighthouse_at: string | null;\n a11y_violations: number | null;\n deps_drifted: number | null;\n deps_major_behind: number | null;\n deps_outdated: number | null;\n deps_major_outdated: number | null;\n vulns_critical: number | null;\n vulns_high: number | null;\n vulns_moderate: number | null;\n vulns_low: number | null;\n security_audit_at: string | null;\n security_advisories: string | null;\n auto_fix_attempts: number | null;\n analytics_soft_fail_at: string | null;\n cert_days_remaining: number | null;\n domain_checked_at: string | null;\n deploy_status: string | null;\n last_deploy_at: string | null;\n deploy_log_url: string | null;\n deploy_checked_at: string | null;\n function_health: string | null;\n cms_reachable: string | null;\n turnstile_widget: string | null;\n function_health_checked_at: string | null;\n crossbrowser_ok: number | null;\n mobile_ok: number | null;\n links_ok: number | null;\n broken_links: number | null;\n browser_checked_at: string | null;\n uptime_reachable: string | null;\n titles_meta_ok: string | null;\n smoke_ok: string | null;\n last_smoke_at: string | null;\n form_e2e_ok: string | null;\n form_e2e_checked_at: string | null;\n renovate_failing_cis: number | null;\n default_branch_ci: string | null;\n last_commit_at: string | null;\n github_signals_at: string | null;\n prismic_models: string | null;\n prismic_models_checked_at: string | null;\n prismic_models_drift: string | null;\n}\n\n/** Code-derived schedule (migration 0007) — written by updateNextDueDates. */\nexport interface SiteScheduleTable {\n site_id: string;\n next_maintenance_at: string | null;\n next_testing_at: string | null;\n computed_at: string | null;\n}\n\n/** Report rows (migration 0007). PK = Airtable rec id. `checklist` is JSON\n * keyed by the STABLE checklist key (src/reports/checklist.ts), not the\n * Airtable column name; `rendered_html` is the downloaded attachment body. */\nexport interface ReportsTable {\n id: string;\n site_id: string | null;\n report_id: string | null;\n report_type: string | null;\n period: string | null;\n period_start: string | null;\n period_end: string | null;\n completed_on: string | null;\n lighthouse_performance: number | null;\n lighthouse_accessibility: number | null;\n lighthouse_best_practices: number | null;\n lighthouse_seo: number | null;\n ga_users_current: number | null;\n ga_users_previous: number | null;\n search_found_page1: number | null;\n search_position: number | null;\n last_tested_date: string | null;\n commentary: string | null;\n subject_override: string | null;\n draft_ready: number;\n approved_to_send: number;\n approved_at: string | null;\n approved_by: string | null;\n send_override: number;\n override_reason: string | null;\n override_by: string | null;\n override_at: string | null;\n sent_at: string | null;\n delivery_status: string | null;\n resend_message_id: string | null;\n checklist: string | null;\n checklist_auto_evidence: string | null;\n rendered_html: string | null;\n}\n\n/** One prospect-audit run (migration 0009). `token` is the 128-bit unguessable\n * public handle for GET /r/{token}; `result_json` holds the full\n * ProspectAuditResult (src/prospect/types.ts). */\nexport interface ProspectAuditsTable {\n id: string;\n token: string;\n url: string;\n business: string | null;\n created_at: string;\n status: string;\n result_json: string;\n}\n\n/** The digest's prior-run snapshot (migration 0011). One row, `id` = the\n * constant singleton key; `snapshot` is the whole DigestSnapshot as JSON. */\nexport interface DigestStateTable {\n id: string;\n snapshot: string;\n updated_at: string | null;\n}\n\n/** Every table the app owns, as a RUNTIME value (#612 review).\n *\n * The `Database` interface below is types-only, so nothing could assert that a\n * backup actually covered it — `tables=N` was printed by the verifier and never\n * checked, meaning a table a migration failed to create would ride green\n * forever. The compile-time check under this list makes forgetting an entry a\n * build error naming the missing table. */\nexport const DATABASE_TABLES = [\n \"submissions\",\n \"spam_screenouts\",\n \"fleet_events\",\n \"_migrations\",\n \"submission_deadletter\",\n \"sites\",\n \"site_health\",\n \"site_schedule\",\n \"reports\",\n \"prospect_audits\",\n \"digest_state\",\n] as const;\n\nexport type DatabaseTable = (typeof DATABASE_TABLES)[number];\n\nexport interface Database {\n submissions: SubmissionsTable;\n spam_screenouts: SpamScreenoutsTable;\n fleet_events: FleetEventsTable;\n _migrations: MigrationsTable;\n submission_deadletter: SubmissionDeadletterTable;\n sites: SitesTable;\n site_health: SiteHealthTable;\n site_schedule: SiteScheduleTable;\n reports: ReportsTable;\n prospect_audits: ProspectAuditsTable;\n digest_state: DigestStateTable;\n}\n\n/** Compile-time exhaustiveness: if a table is added to `Database` and not to\n * `DATABASE_TABLES`, this errors and NAMES it. */\ntype MissingFromDatabaseTables = Exclude<keyof Database, DatabaseTable>;\nconst _everyTableIsListed: MissingFromDatabaseTables extends never\n ? true\n : MissingFromDatabaseTables = true;\nvoid _everyTableIsListed;\n"],"mappings":";AA4OO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/db/schema.ts"],"sourcesContent":["// The hand-written shape Kysely types every query from. SQLite stores TEXT/INTEGER\n// only, so these are the column *shapes* — the value domains (form_type, status,\n// notify_status) are still narrowed at read time by the validators in\n// src/reports/submission-row.ts. Keep this in lockstep with src/db/migrations.ts;\n// drift is caught by the in-memory tests in tests/db/, not by codegen.\n\nexport interface SubmissionsTable {\n id: string;\n submission_id: number | null;\n site_id: string;\n form_type: string;\n name: string;\n email: string;\n phone: string | null;\n message: string | null;\n extra_fields: string | null;\n source_url: string | null;\n utm: string | null;\n submitted_at: string | null;\n status: string;\n notify_status: string;\n resend_message_id: string | null;\n spam_score: number | null;\n spam_reason: string | null;\n fanout_status: string | null;\n}\n\nexport interface SpamScreenoutsTable {\n site_id: string;\n date: string;\n honeypot: number;\n too_fast: number;\n marked_spam: number;\n}\n\nexport interface FleetEventsTable {\n id: string;\n ts: string;\n type: string;\n site_id: string | null;\n site_name: string | null;\n summary: string;\n data: string | null;\n created_at: string;\n}\n\nexport interface MigrationsTable {\n id: string;\n applied_at: string;\n}\n\n/** A lead whose site lookup threw mid-ingest (migration 0006). `payload` and\n * `turnstile` are JSON — the raw wire body and the verification computed at\n * receipt — exactly what `db replay-deadletters` feeds back through\n * `ingestSubmission` once the lookup recovers. */\nexport interface SubmissionDeadletterTable {\n id: string;\n site_slug: string;\n payload: string;\n turnstile: string;\n error: string;\n received_at: string;\n replayed_at: string | null;\n replay_outcome: string | null;\n replay_submission_id: string | null;\n}\n\n/** Operator-owned fleet config (migration 0007). PK = Airtable rec id (D1).\n * `legacy` holds the 33 populated-but-code-unreferenced Airtable columns as a\n * JSON object keyed by original column name; the plaintext DNS/cms credential\n * cells never migrate (operator ruling 2026-08-23). */\nexport interface SitesTable {\n id: string;\n slug: string;\n name: string;\n url: string | null;\n status: string | null;\n point_of_contact: string | null;\n maintenance_freq: string | null;\n testing_freq: string | null;\n maintenance_day: string | null;\n testing_day: string | null;\n ga4_property_id: string | null;\n search_query: string | null;\n search_console_property: string | null;\n git_repo: string | null;\n netlify_id: string | null;\n report_recipients_to: string | null;\n report_recipients_cc: string | null;\n copy_intro: string | null;\n copy_contact: string | null;\n copy_footer: string | null;\n newsletter_webhook: string | null;\n mailchimp_api_key: string | null;\n mailchimp_audience_id: string | null;\n notify_routing: string | null;\n require_turnstile: number;\n accepted_watch_conditions: string | null;\n prismic_ack_until: string | null;\n launched_at: string | null;\n header_image: Uint8Array | null;\n header_image_filename: string | null;\n header_image_type: string | null;\n header_image_generated_at: string | null;\n legacy: string | null;\n}\n\n/** Nightly-cron-owned health/telemetry (migration 0007) — one row per site,\n * written as one batched upsert per sweep family. `analytics_soft_fail_at` is\n * the \"Analytics soft-fail at\" column code always wrote best-effort but no\n * operator ever created in Airtable. */\nexport interface SiteHealthTable {\n site_id: string;\n p_score: number | null;\n r_score: number | null;\n bp_score: number | null;\n seo_score: number | null;\n lighthouse_at: string | null;\n a11y_violations: number | null;\n deps_drifted: number | null;\n deps_major_behind: number | null;\n deps_outdated: number | null;\n deps_major_outdated: number | null;\n vulns_critical: number | null;\n vulns_high: number | null;\n vulns_moderate: number | null;\n vulns_low: number | null;\n security_audit_at: string | null;\n security_advisories: string | null;\n auto_fix_attempts: number | null;\n analytics_soft_fail_at: string | null;\n cert_days_remaining: number | null;\n domain_checked_at: string | null;\n deploy_status: string | null;\n last_deploy_at: string | null;\n deploy_log_url: string | null;\n deploy_checked_at: string | null;\n function_health: string | null;\n cms_reachable: string | null;\n turnstile_widget: string | null;\n function_health_checked_at: string | null;\n crossbrowser_ok: number | null;\n mobile_ok: number | null;\n links_ok: number | null;\n broken_links: number | null;\n browser_checked_at: string | null;\n uptime_reachable: string | null;\n titles_meta_ok: string | null;\n smoke_ok: string | null;\n last_smoke_at: string | null;\n form_e2e_ok: string | null;\n form_e2e_checked_at: string | null;\n renovate_failing_cis: number | null;\n default_branch_ci: string | null;\n last_commit_at: string | null;\n github_signals_at: string | null;\n prismic_models: string | null;\n prismic_models_checked_at: string | null;\n prismic_models_drift: string | null;\n}\n\n/** Code-derived schedule (migration 0007) — written by updateNextDueDates. */\nexport interface SiteScheduleTable {\n site_id: string;\n next_maintenance_at: string | null;\n next_testing_at: string | null;\n computed_at: string | null;\n}\n\n/** Report rows (migration 0007). PK = Airtable rec id. `checklist` is JSON\n * keyed by the STABLE checklist key (src/reports/checklist.ts), not the\n * Airtable column name; `rendered_html` is the downloaded attachment body. */\nexport interface ReportsTable {\n id: string;\n site_id: string | null;\n report_id: string | null;\n report_type: string | null;\n period: string | null;\n period_start: string | null;\n period_end: string | null;\n completed_on: string | null;\n lighthouse_performance: number | null;\n lighthouse_accessibility: number | null;\n lighthouse_best_practices: number | null;\n lighthouse_seo: number | null;\n ga_users_current: number | null;\n ga_users_previous: number | null;\n search_found_page1: number | null;\n search_position: number | null;\n last_tested_date: string | null;\n commentary: string | null;\n subject_override: string | null;\n draft_ready: number;\n approved_to_send: number;\n approved_at: string | null;\n approved_by: string | null;\n send_override: number;\n override_reason: string | null;\n override_by: string | null;\n override_at: string | null;\n sent_at: string | null;\n delivery_status: string | null;\n resend_message_id: string | null;\n checklist: string | null;\n checklist_auto_evidence: string | null;\n rendered_html: string | null;\n}\n\n/** One prospect-audit run (migration 0009). `token` is the 128-bit unguessable\n * public handle for GET /r/{token}; `result_json` holds the full\n * ProspectAuditResult (src/prospect/types.ts). */\nexport interface ProspectAuditsTable {\n id: string;\n token: string;\n url: string;\n /** One key per site, however its address was written (migration 0013). `url`\n * records what we audited; this is what two audits of one site are grouped\n * by. Null on rows written before the column existed. See siteKey(). */\n site_key: string | null;\n business: string | null;\n created_at: string;\n status: string;\n result_json: string;\n}\n\n/** The digest's prior-run snapshot (migration 0011). One row, `id` = the\n * constant singleton key; `snapshot` is the whole DigestSnapshot as JSON. */\nexport interface DigestStateTable {\n id: string;\n snapshot: string;\n updated_at: string | null;\n}\n\n/** Every table the app owns, as a RUNTIME value (#612 review).\n *\n * The `Database` interface below is types-only, so nothing could assert that a\n * backup actually covered it — `tables=N` was printed by the verifier and never\n * checked, meaning a table a migration failed to create would ride green\n * forever. The compile-time check under this list makes forgetting an entry a\n * build error naming the missing table. */\nexport const DATABASE_TABLES = [\n \"submissions\",\n \"spam_screenouts\",\n \"fleet_events\",\n \"_migrations\",\n \"submission_deadletter\",\n \"sites\",\n \"site_health\",\n \"site_schedule\",\n \"reports\",\n \"prospect_audits\",\n \"digest_state\",\n] as const;\n\nexport type DatabaseTable = (typeof DATABASE_TABLES)[number];\n\nexport interface Database {\n submissions: SubmissionsTable;\n spam_screenouts: SpamScreenoutsTable;\n fleet_events: FleetEventsTable;\n _migrations: MigrationsTable;\n submission_deadletter: SubmissionDeadletterTable;\n sites: SitesTable;\n site_health: SiteHealthTable;\n site_schedule: SiteScheduleTable;\n reports: ReportsTable;\n prospect_audits: ProspectAuditsTable;\n digest_state: DigestStateTable;\n}\n\n/** Compile-time exhaustiveness: if a table is added to `Database` and not to\n * `DATABASE_TABLES`, this errors and NAMES it. */\ntype MissingFromDatabaseTables = Exclude<keyof Database, DatabaseTable>;\nconst _everyTableIsListed: MissingFromDatabaseTables extends never\n ? true\n : MissingFromDatabaseTables = true;\nvoid _everyTableIsListed;\n"],"mappings":";AAgPO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  selfUpdating
3
- } from "./chunk-GSWTT2J6.js";
3
+ } from "./chunk-WHWVYL7B.js";
4
4
  import "./chunk-MNDIED4M.js";
5
5
  import {
6
6
  runRecipeOverSites
@@ -9,7 +9,7 @@ import {
9
9
  appendSkipNotice,
10
10
  fleetWorkdir,
11
11
  prepareFleetSites
12
- } from "./chunk-TVL5MVUF.js";
12
+ } from "./chunk-TCF6S7AD.js";
13
13
  import {
14
14
  resolveSites
15
15
  } from "./chunk-Q47DU255.js";
@@ -17,10 +17,10 @@ import "./chunk-MLT4QY5X.js";
17
17
  import "./chunk-GASBX52O.js";
18
18
  import "./chunk-XHHMM5HX.js";
19
19
  import "./chunk-Z3JXLULD.js";
20
- import "./chunk-RARREDQE.js";
21
- import "./chunk-XTK5VIQB.js";
22
20
  import "./chunk-LBYOLBW7.js";
21
+ import "./chunk-RARREDQE.js";
23
22
  import "./chunk-XXTZBPUY.js";
23
+ import "./chunk-XTK5VIQB.js";
24
24
 
25
25
  // src/cli/commands/self-updating.ts
26
26
  import { resolve } from "path";
@@ -62,4 +62,4 @@ async function runSelfUpdatingCommand(site, opts) {
62
62
  export {
63
63
  runSelfUpdatingCommand
64
64
  };
65
- //# sourceMappingURL=self-updating-VQIW24RZ.js.map
65
+ //# sourceMappingURL=self-updating-D5XOWS7V.js.map
@@ -13,7 +13,7 @@ import {
13
13
  import {
14
14
  isProbablyEmail,
15
15
  parseAddresses
16
- } from "./chunk-VBEKL445.js";
16
+ } from "./chunk-FCS36FDT.js";
17
17
  import "./chunk-F3JWOPTG.js";
18
18
  import "./chunk-VAD43MOR.js";
19
19
  import {
@@ -40,10 +40,10 @@ import {
40
40
  import {
41
41
  defaultResendClient
42
42
  } from "./chunk-WEKL2HYJ.js";
43
- import "./chunk-W45FZEGE.js";
44
- import "./chunk-WS6NU475.js";
43
+ import "./chunk-DMPHP7UP.js";
44
+ import "./chunk-22GKJO5F.js";
45
+ import "./chunk-OCNSIBMP.js";
45
46
  import "./chunk-RARREDQE.js";
46
- import "./chunk-MFHS7IQ2.js";
47
47
  import "./chunk-6VAL7XJT.js";
48
48
 
49
49
  // src/recipes/selftest-email.ts
@@ -172,4 +172,4 @@ async function runSelftestCommand(kind, site, opts) {
172
172
  export {
173
173
  runSelftestCommand
174
174
  };
175
- //# sourceMappingURL=selftest-CUX2FIQD.js.map
175
+ //# sourceMappingURL=selftest-IFGSJP4X.js.map