@reddoorla/maintenance 0.88.2 → 0.89.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 (116) hide show
  1. package/dist/{announce-GKD5YJNB.js → announce-OJFAS5JT.js} +6 -6
  2. package/dist/{blux-DN4MI532.js → blux-EATHOHKK.js} +5 -5
  3. package/dist/{chunk-7UAYMAKR.js → chunk-3VQWSE3F.js} +4 -4
  4. package/dist/{chunk-A2UNJIKH.js → chunk-5PWB3JHJ.js} +6 -4
  5. package/dist/chunk-5PWB3JHJ.js.map +1 -0
  6. package/dist/{chunk-K6SLF6GI.js → chunk-62NYED6E.js} +6 -6
  7. package/dist/{chunk-B23FFWU4.js → chunk-AEXJDCJN.js} +96 -19
  8. package/dist/{chunk-B23FFWU4.js.map → chunk-AEXJDCJN.js.map} +1 -1
  9. package/dist/{chunk-7FFUBF4I.js → chunk-AX22DH6S.js} +2 -2
  10. package/dist/{chunk-7TMWBXX7.js → chunk-DXKF552B.js} +269 -7
  11. package/dist/chunk-DXKF552B.js.map +1 -0
  12. package/dist/{chunk-6I6L3CMX.js → chunk-KGUKL3CV.js} +2 -2
  13. package/dist/{chunk-ULHPALI5.js → chunk-NDCOOI3V.js} +2 -2
  14. package/dist/{chunk-GQDDWYU2.js → chunk-QBAAZY7W.js} +1 -1
  15. package/dist/{chunk-GQDDWYU2.js.map → chunk-QBAAZY7W.js.map} +1 -1
  16. package/dist/{chunk-KMCZA7CH.js → chunk-QLIUDTY6.js} +2 -2
  17. package/dist/{chunk-XP5WM2LV.js → chunk-SKI23ZZH.js} +4 -4
  18. package/dist/chunk-SVAAONYB.js +21 -0
  19. package/dist/chunk-SVAAONYB.js.map +1 -0
  20. package/dist/{chunk-DGZWFI2G.js → chunk-VBEKL445.js} +17 -8
  21. package/dist/chunk-VBEKL445.js.map +1 -0
  22. package/dist/{chunk-ANJNTAMM.js → chunk-ZOCJBJQV.js} +4 -2
  23. package/dist/{chunk-J3WEZGIN.js → chunk-ZRI5VRPG.js} +4 -4
  24. package/dist/cli/bin.js +27 -18
  25. package/dist/cli/bin.js.map +1 -1
  26. package/dist/cli/commands/audit.js +5 -5
  27. package/dist/{db-V6UIS433.js → db-DAJX7NNW.js} +51 -10
  28. package/dist/db-DAJX7NNW.js.map +1 -0
  29. package/dist/{digest-2JEAEJK6.js → digest-T6TAZNLF.js} +73 -16
  30. package/dist/digest-T6TAZNLF.js.map +1 -0
  31. package/dist/digest-collectors-NI2M3UTC.js +50 -0
  32. package/dist/digest-state-3KHZF7GD.js +50 -0
  33. package/dist/digest-state-3KHZF7GD.js.map +1 -0
  34. package/dist/{dump-EPL7AZFR.js → dump-QK7LNOGC.js} +14 -1
  35. package/dist/{dump-EPL7AZFR.js.map → dump-QK7LNOGC.js.map} +1 -1
  36. package/dist/{email-SL7464YI.js → email-LJQT5J7F.js} +6 -7
  37. package/dist/{email-SL7464YI.js.map → email-LJQT5J7F.js.map} +1 -1
  38. package/dist/{ensure-site-FU6527OU.js → ensure-site-NSNY2QHN.js} +2 -2
  39. package/dist/{fleet-state-R5LU7IC3.js → fleet-state-3LQUQHYD.js} +2 -2
  40. package/dist/{forms-notify-target-SGDGXXK5.js → forms-notify-target-LYSKME5L.js} +2 -2
  41. package/dist/freeze-A6MUCZU4.js +9 -0
  42. package/dist/{github-signals-GWGCKEE2.js → github-signals-4YML6NY5.js} +4 -4
  43. package/dist/{health-mirror-GCDN7BJL.js → health-mirror-7LKC7Q5Z.js} +5 -5
  44. package/dist/index.d.ts +19 -4
  45. package/dist/index.js +29 -28
  46. package/dist/index.js.map +1 -1
  47. package/dist/{init-FGTYNGUI.js → init-NHKADHB6.js} +7 -7
  48. package/dist/{launch-5H7LPEGP.js → launch-VQJ4Z35C.js} +6 -6
  49. package/dist/{orchestrate-JNKCWU5K.js → orchestrate-6HROYTM6.js} +4 -4
  50. package/dist/{parity-NBYZUBFU.js → parity-623WNFZG.js} +3 -3
  51. package/dist/{pipeline-WYTXSLHX.js → pipeline-MQJD4CKN.js} +6 -2
  52. package/dist/{preflight-IMTYDDD4.js → preflight-LIN5NCYU.js} +5 -5
  53. package/dist/{prismic-ci-MY2U6NN7.js → prismic-ci-PZ5LCPWT.js} +4 -4
  54. package/dist/{prismic-models-NJN5JORF.js → prismic-models-6OYYZUR5.js} +3 -3
  55. package/dist/prospect/types.d.ts +27 -0
  56. package/dist/{prospect-audit-JMBRVXSG.js → prospect-audit-DBBXL7EG.js} +8 -6
  57. package/dist/prospect-audit-DBBXL7EG.js.map +1 -0
  58. package/dist/recipes/sync-configs.js +2 -2
  59. package/dist/{render-VX72AESY.js → render-P3SBUFVB.js} +3 -3
  60. package/dist/{render-from-row-DOYJDO2L.js → render-from-row-P5CWM3MQ.js} +3 -3
  61. package/dist/render-from-row-P5CWM3MQ.js.map +1 -0
  62. package/dist/{report-TALY5P3L.js → report-2N4PXO3L.js} +33 -17
  63. package/dist/report-2N4PXO3L.js.map +1 -0
  64. package/dist/{report-mirror-AAK3MXHY.js → report-mirror-3ENU6OOK.js} +5 -5
  65. package/dist/{selftest-NZTR4SM2.js → selftest-CUX2FIQD.js} +7 -7
  66. package/dist/{site-mirror-Q763PLGG.js → site-mirror-RQTEAID5.js} +5 -5
  67. package/dist/{sync-GBFV45WX.js → sync-ESP5ZQUU.js} +3 -3
  68. package/dist/{sync-configs-34JIA6KZ.js → sync-configs-LVBITPLU.js} +3 -3
  69. package/dist/usage-UHKU76VK.js +147 -0
  70. package/dist/usage-UHKU76VK.js.map +1 -0
  71. package/dist/{write-audits-to-airtable-OBHGR7DQ.js → write-audits-to-airtable-K352LZQY.js} +3 -3
  72. package/dist/write-audits-to-airtable-K352LZQY.js.map +1 -0
  73. package/package.json +1 -1
  74. package/dist/chunk-7TMWBXX7.js.map +0 -1
  75. package/dist/chunk-A2UNJIKH.js.map +0 -1
  76. package/dist/chunk-DGZWFI2G.js.map +0 -1
  77. package/dist/chunk-RUPPWAD6.js +0 -7
  78. package/dist/chunk-RUPPWAD6.js.map +0 -1
  79. package/dist/db-V6UIS433.js.map +0 -1
  80. package/dist/digest-2JEAEJK6.js.map +0 -1
  81. package/dist/digest-state-CRZRNGDI.js +0 -25
  82. package/dist/digest-state-CRZRNGDI.js.map +0 -1
  83. package/dist/prospect-audit-JMBRVXSG.js.map +0 -1
  84. package/dist/report-TALY5P3L.js.map +0 -1
  85. /package/dist/{announce-GKD5YJNB.js.map → announce-OJFAS5JT.js.map} +0 -0
  86. /package/dist/{blux-DN4MI532.js.map → blux-EATHOHKK.js.map} +0 -0
  87. /package/dist/{chunk-7UAYMAKR.js.map → chunk-3VQWSE3F.js.map} +0 -0
  88. /package/dist/{chunk-K6SLF6GI.js.map → chunk-62NYED6E.js.map} +0 -0
  89. /package/dist/{chunk-7FFUBF4I.js.map → chunk-AX22DH6S.js.map} +0 -0
  90. /package/dist/{chunk-6I6L3CMX.js.map → chunk-KGUKL3CV.js.map} +0 -0
  91. /package/dist/{chunk-ULHPALI5.js.map → chunk-NDCOOI3V.js.map} +0 -0
  92. /package/dist/{chunk-KMCZA7CH.js.map → chunk-QLIUDTY6.js.map} +0 -0
  93. /package/dist/{chunk-XP5WM2LV.js.map → chunk-SKI23ZZH.js.map} +0 -0
  94. /package/dist/{chunk-ANJNTAMM.js.map → chunk-ZOCJBJQV.js.map} +0 -0
  95. /package/dist/{chunk-J3WEZGIN.js.map → chunk-ZRI5VRPG.js.map} +0 -0
  96. /package/dist/{fleet-state-R5LU7IC3.js.map → digest-collectors-NI2M3UTC.js.map} +0 -0
  97. /package/dist/{ensure-site-FU6527OU.js.map → ensure-site-NSNY2QHN.js.map} +0 -0
  98. /package/dist/{orchestrate-JNKCWU5K.js.map → fleet-state-3LQUQHYD.js.map} +0 -0
  99. /package/dist/{forms-notify-target-SGDGXXK5.js.map → forms-notify-target-LYSKME5L.js.map} +0 -0
  100. /package/dist/{parity-NBYZUBFU.js.map → freeze-A6MUCZU4.js.map} +0 -0
  101. /package/dist/{github-signals-GWGCKEE2.js.map → github-signals-4YML6NY5.js.map} +0 -0
  102. /package/dist/{health-mirror-GCDN7BJL.js.map → health-mirror-7LKC7Q5Z.js.map} +0 -0
  103. /package/dist/{init-FGTYNGUI.js.map → init-NHKADHB6.js.map} +0 -0
  104. /package/dist/{launch-5H7LPEGP.js.map → launch-VQJ4Z35C.js.map} +0 -0
  105. /package/dist/{pipeline-WYTXSLHX.js.map → orchestrate-6HROYTM6.js.map} +0 -0
  106. /package/dist/{render-VX72AESY.js.map → parity-623WNFZG.js.map} +0 -0
  107. /package/dist/{render-from-row-DOYJDO2L.js.map → pipeline-MQJD4CKN.js.map} +0 -0
  108. /package/dist/{preflight-IMTYDDD4.js.map → preflight-LIN5NCYU.js.map} +0 -0
  109. /package/dist/{prismic-ci-MY2U6NN7.js.map → prismic-ci-PZ5LCPWT.js.map} +0 -0
  110. /package/dist/{prismic-models-NJN5JORF.js.map → prismic-models-6OYYZUR5.js.map} +0 -0
  111. /package/dist/{write-audits-to-airtable-OBHGR7DQ.js.map → render-P3SBUFVB.js.map} +0 -0
  112. /package/dist/{report-mirror-AAK3MXHY.js.map → report-mirror-3ENU6OOK.js.map} +0 -0
  113. /package/dist/{selftest-NZTR4SM2.js.map → selftest-CUX2FIQD.js.map} +0 -0
  114. /package/dist/{site-mirror-Q763PLGG.js.map → site-mirror-RQTEAID5.js.map} +0 -0
  115. /package/dist/{sync-GBFV45WX.js.map → sync-ESP5ZQUU.js.map} +0 -0
  116. /package/dist/{sync-configs-34JIA6KZ.js.map → sync-configs-LVBITPLU.js.map} +0 -0
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  scoresFromRow
3
- } from "./chunk-ULHPALI5.js";
3
+ } from "./chunk-NDCOOI3V.js";
4
4
  import {
5
5
  fetchGaUsers,
6
6
  fetchSearch,
7
7
  readGaConfig,
8
8
  refreshHeaderImage
9
- } from "./chunk-J3WEZGIN.js";
9
+ } from "./chunk-ZRI5VRPG.js";
10
10
  import {
11
11
  queueDraft
12
12
  } from "./chunk-YFAXOD7Z.js";
@@ -26,12 +26,12 @@ import {
26
26
  import {
27
27
  defaultReportSubject
28
28
  } from "./chunk-GECESMVS.js";
29
- import "./chunk-5UINJNAJ.js";
30
29
  import {
31
30
  announcementSiteExtras,
32
31
  renderReportHtml,
33
32
  resolveCopy
34
33
  } from "./chunk-KJGKZLCT.js";
34
+ import "./chunk-5UINJNAJ.js";
35
35
  import "./chunk-BGTYPVLT.js";
36
36
  import {
37
37
  listWebsites,
@@ -203,8 +203,8 @@ function formatAnnounceResult(result) {
203
203
  return result.results.map(formatSiteResult).join("\n");
204
204
  }
205
205
  async function runAnnounceCommand(site, _opts) {
206
- const { makeReportMirror } = await import("./report-mirror-AAK3MXHY.js");
207
- const { makeSiteMirror } = await import("./site-mirror-Q763PLGG.js");
206
+ const { makeReportMirror } = await import("./report-mirror-3ENU6OOK.js");
207
+ const { makeSiteMirror } = await import("./site-mirror-RQTEAID5.js");
208
208
  const reportMirror = await makeReportMirror();
209
209
  const siteMirror = await makeSiteMirror();
210
210
  const result = await announce({ ...site ? { site } : {}, reportMirror, siteMirror });
@@ -215,4 +215,4 @@ export {
215
215
  formatAnnounceResult,
216
216
  runAnnounceCommand
217
217
  };
218
- //# sourceMappingURL=announce-GKD5YJNB.js.map
218
+ //# sourceMappingURL=announce-OJFAS5JT.js.map
@@ -4,14 +4,14 @@ import {
4
4
  frozenPageCustomType,
5
5
  makeIsMapMount
6
6
  } from "./chunk-DRDAEELJ.js";
7
- import {
8
- assetRef,
9
- richText
10
- } from "./chunk-UWJZXL2D.js";
11
7
  import {
12
8
  CDN_HOSTS,
13
9
  collectAssetUrls
14
10
  } from "./chunk-SL24DCGC.js";
11
+ import {
12
+ assetRef,
13
+ richText
14
+ } from "./chunk-UWJZXL2D.js";
15
15
 
16
16
  // src/cli/commands/blux.ts
17
17
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -4767,4 +4767,4 @@ ${lines.join("\n")}`,
4767
4767
  export {
4768
4768
  runBluxCommand
4769
4769
  };
4770
- //# sourceMappingURL=blux-DN4MI532.js.map
4770
+ //# sourceMappingURL=blux-EATHOHKK.js.map
@@ -1,10 +1,10 @@
1
+ import {
2
+ withRecipe
3
+ } from "./chunk-3G25KIWW.js";
1
4
  import {
2
5
  ALL_TEMPLATES,
3
6
  templatesByName
4
7
  } from "./chunk-73UFRZKY.js";
5
- import {
6
- withRecipe
7
- } from "./chunk-3G25KIWW.js";
8
8
  import {
9
9
  listTrackedFiles,
10
10
  removeFromIndex
@@ -207,4 +207,4 @@ export {
207
207
  isConfigName,
208
208
  syncConfigs
209
209
  };
210
- //# sourceMappingURL=chunk-7UAYMAKR.js.map
210
+ //# sourceMappingURL=chunk-3VQWSE3F.js.map
@@ -2,7 +2,7 @@ import {
2
2
  ANALYZE_SKIPPED,
3
3
  PROBES_SKIPPED,
4
4
  resolveBusinessName
5
- } from "./chunk-7TMWBXX7.js";
5
+ } from "./chunk-DXKF552B.js";
6
6
  import {
7
7
  escapeHtml,
8
8
  safeUrl
@@ -110,6 +110,9 @@ function buildProbesSection(p, businessNameUsed) {
110
110
  }
111
111
  const recognition = p.brandedRecognized ? "<p>When the engines were asked about the business by name, they recognized it.</p>" : businessNameUsed ? "<p>When the engines were asked about the business by name, they did not recognize it \u2014 a real citation of the site never showed up, even with the name handed to them.</p>" : "<p>The engines did not recognize the business \u2014 there was no business name available to ask them with, so this reflects a harder starting point than a named query would.</p>";
112
112
  const categoryCaveat = byKind.has("category") ? "" : `<p class="muted"><strong>No buyer-question (category) query was tested here</strong> \u2014 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>`;
113
+ const tally = p.categoryProbes;
114
+ const missing = tally ? tally.attempted - tally.answered : 0;
115
+ const degradedNotice = tally && missing > 0 ? `<p class="muted"><strong>${missing} of ${tally.attempted} buyer-question searches did not come back</strong> \u2014 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>` : "";
113
116
  const groups = KIND_ORDER.filter((k) => byKind.has(k)).map((kind) => {
114
117
  const answers = byKind.get(kind) ?? [];
115
118
  const cards = answers.map(
@@ -128,7 +131,7 @@ function buildProbesSection(p, businessNameUsed) {
128
131
  return `<h3>${escapeHtml(KIND_LABEL[kind])}</h3>${cards}`;
129
132
  }).join("");
130
133
  const competitors = p.competitorsSeen.length ? `<h3>Who the engines cited instead</h3><ul>${p.competitorsSeen.map((c) => `<li>${escapeHtml(c.domain)} \u2014 ${c.count} time${c.count === 1 ? "" : "s"}</li>`).join("")}</ul>` : "";
131
- return recognition + categoryCaveat + groups + competitors;
134
+ return recognition + categoryCaveat + degradedNotice + groups + competitors;
132
135
  }
133
136
  function buildFindabilitySection(c) {
134
137
  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(
@@ -246,7 +249,6 @@ function renderProspectReport(result) {
246
249
  ${scoreCard("Findability", result.scores.findability, "How easily AI and search crawlers can find and reach your site \u2014 crawl rules, sitemap, key metadata")}
247
250
  ${scoreCard("Readability", result.scores.readability, "How much of your site's content those crawlers can actually read once they're in \u2014 most don't run JavaScript")}
248
251
  ${scoreCard("Answers", result.scores.answers, "How many buyer questions your site's own content answers \u2014 separate from what the AI engines say back")}
249
- ${scoreCard("AI Visibility", result.scores.aiVisibility, "Based on buyer questions \u2014 not questions that name the business directly")}
250
252
  </div>
251
253
 
252
254
  <h2>What the AI engines said about you</h2>
@@ -279,4 +281,4 @@ function renderProspectReport(result) {
279
281
  export {
280
282
  renderProspectReport
281
283
  };
282
- //# sourceMappingURL=chunk-A2UNJIKH.js.map
284
+ //# sourceMappingURL=chunk-5PWB3JHJ.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 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 : `<p>Every AI crawler we checked can reach the site.</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 const llmsLine = c.llmsTxtMeasured\n ? `<li>llms.txt: ${c.llmsTxtPresent ? \"present\" : \"missing\"}</li>`\n : `<li>llms.txt: not measured — ${STAGE_FAILED_MESSAGE}.</li>`;\n return `${accessBlock}${classical}\n <ul>\n ${sitemapLine}\n ${llmsLine}\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>`;\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. */\nfunction buildAnswersSection(a: AnalyzeResult): string {\n const rows = a.buyerQuestions\n .map((q) => {\n const answered = escapeHtml(q.answered);\n const evidenceText = q.evidence ? escapeHtml(q.evidence) : null;\n const evidenceHref = q.evidence && q.page ? safeUrl(q.page) : null;\n const evidenceCell =\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}\">${answered}</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,3 +1,6 @@
1
+ import {
2
+ convertToPnpm
3
+ } from "./chunk-XIDUYLSR.js";
1
4
  import {
2
5
  svelteCodemods
3
6
  } from "./chunk-IYSEC543.js";
@@ -10,18 +13,15 @@ import {
10
13
  import {
11
14
  onboard
12
15
  } from "./chunk-7RJ2HMMJ.js";
13
- import {
14
- convertToPnpm
15
- } from "./chunk-XIDUYLSR.js";
16
16
  import {
17
17
  syncConfigs
18
- } from "./chunk-7UAYMAKR.js";
18
+ } from "./chunk-3VQWSE3F.js";
19
19
  import {
20
20
  withRecipe
21
21
  } from "./chunk-3G25KIWW.js";
22
22
  import {
23
23
  runAudits
24
- } from "./chunk-B23FFWU4.js";
24
+ } from "./chunk-AEXJDCJN.js";
25
25
  import {
26
26
  siteLabel
27
27
  } from "./chunk-XXTZBPUY.js";
@@ -143,4 +143,4 @@ export {
143
143
  DEFAULT_INIT_STEPS,
144
144
  init
145
145
  };
146
- //# sourceMappingURL=chunk-K6SLF6GI.js.map
146
+ //# sourceMappingURL=chunk-62NYED6E.js.map
@@ -1727,11 +1727,12 @@ async function formE2eAudit(ctx) {
1727
1727
  };
1728
1728
  }
1729
1729
  const thin = typeof outcome.postElapsedMs === "number" && isIngestBudgetThin(outcome.postElapsedMs) ? budgetThinSummary(outcome.postElapsedMs) : null;
1730
+ const refillNote = outcome.refilled ? " \u2014 fields were wiped by a client re-render and re-filled once" : "";
1730
1731
  return {
1731
1732
  audit: "form-e2e",
1732
1733
  site: label,
1733
1734
  status: thin ? "warn" : "pass",
1734
- summary: thin ? `form-e2e: synthetic submission succeeded \u2014 ${thin}` : "form-e2e: synthetic submission succeeded",
1735
+ summary: thin ? `form-e2e: synthetic submission succeeded \u2014 ${thin}${refillNote}` : `form-e2e: synthetic submission succeeded${refillNote}`,
1735
1736
  details
1736
1737
  };
1737
1738
  } finally {
@@ -1769,6 +1770,29 @@ async function declaresTestModeForwarding(baseUrl) {
1769
1770
  return false;
1770
1771
  }
1771
1772
  }
1773
+ function isSameSitePost(postUrl, baseUrl) {
1774
+ try {
1775
+ const host = (u) => new URL(u).hostname.replace(/^www\./, "");
1776
+ return host(postUrl) === host(baseUrl);
1777
+ } catch {
1778
+ return false;
1779
+ }
1780
+ }
1781
+ function noBannerDetail(e) {
1782
+ const bits = [
1783
+ e.post ? `POST ${e.post.status}${e.post.status >= 400 && e.post.bodyBit ? ` ${e.post.bodyBit}` : ""}` : "no same-site POST: the submission never left the page"
1784
+ ];
1785
+ if (e.alertText?.trim()) bits.push(`alert: ${e.alertText.trim().slice(0, 80)}`);
1786
+ if (e.formState) {
1787
+ if (!e.formState.formPresent) bits.push("the form is gone from the page");
1788
+ else if (e.formState.emptyRequired.length > 0)
1789
+ bits.push(`empty required: ${e.formState.emptyRequired.join(", ")}`);
1790
+ else if (e.formState.checkValidity === false) bits.push("form fails validation");
1791
+ }
1792
+ if (e.hydrationMismatch) bits.push("hydration mismatch warning seen");
1793
+ if (e.refilled) bits.push("fields were wiped and re-filled once");
1794
+ return `no success banner after submit \u2014 ${bits.join("; ")}`;
1795
+ }
1772
1796
  async function defaultFormRunner() {
1773
1797
  const { chromium } = await import("@playwright/test");
1774
1798
  return {
@@ -1780,17 +1804,14 @@ async function defaultFormRunner() {
1780
1804
  try {
1781
1805
  const ctx = await browser.newContext();
1782
1806
  const page = await ctx.newPage();
1807
+ let hydrationMismatch = false;
1808
+ page.on("console", (msg) => {
1809
+ if (msg.text().includes("hydration_mismatch")) hydrationMismatch = true;
1810
+ });
1783
1811
  const found = await findFormPath(page, baseUrl);
1784
1812
  if (!found) return { formPresent: false };
1785
- await page.fill('[name="name"]', "Reddoor Monitor").catch(() => {
1786
- });
1787
- await page.fill('[name="email"]', "monitor+e2e@reddoorla.com").catch(() => {
1788
- });
1789
- await page.fill('[name="phone"]', "5555550123").catch(() => {
1790
- });
1791
- await page.fill('[name="message"]', "Synthetic end-to-end health check \u2014 please ignore.").catch(() => {
1792
- });
1793
- await page.evaluate(`
1813
+ const tokenValue = `testmode-${testSitekey}`;
1814
+ const injectExpr = `
1794
1815
  (function () {
1795
1816
  const f = document.querySelector("form");
1796
1817
  if (!f) return;
@@ -1805,15 +1826,52 @@ async function defaultFormRunner() {
1805
1826
  el.value = value;
1806
1827
  };
1807
1828
  add("testMode", "true");
1808
- add("cf-turnstile-response", ${JSON.stringify(`testmode-${testSitekey}`)});
1829
+ add("cf-turnstile-response", ${JSON.stringify(tokenValue)});
1809
1830
  })();
1810
- `);
1831
+ `;
1832
+ const fills = [
1833
+ { selector: '[name="name"]', value: "Reddoor Monitor" },
1834
+ { selector: '[name="email"]', value: "monitor+e2e@reddoorla.com" },
1835
+ { selector: '[name="phone"]', value: "5555550123" },
1836
+ {
1837
+ selector: '[name="message"]',
1838
+ value: "Synthetic end-to-end health check \u2014 please ignore."
1839
+ }
1840
+ ];
1841
+ const filled = [];
1842
+ const fillAll = async () => {
1843
+ for (const f of fills) {
1844
+ const landed = await page.fill(f.selector, f.value).then(() => true).catch(() => false);
1845
+ if (landed && !filled.some((s) => s.selector === f.selector)) filled.push(f);
1846
+ }
1847
+ await page.evaluate(injectExpr);
1848
+ };
1849
+ await fillAll();
1811
1850
  await page.waitForTimeout(FILL_SETTLE_MS);
1851
+ const expected = [...filled, { selector: 'input[name="testMode"]', value: "true" }];
1852
+ const wipedCount = await page.evaluate(
1853
+ `
1854
+ (function () {
1855
+ const expected = ${JSON.stringify(expected)};
1856
+ let wiped = 0;
1857
+ for (const { selector, value } of expected) {
1858
+ const el = document.querySelector(selector);
1859
+ if (!el || el.value !== value) wiped++;
1860
+ }
1861
+ return wiped;
1862
+ })();
1863
+ `
1864
+ ).catch(() => 0);
1865
+ const refilled = wipedCount > 0;
1866
+ if (refilled) await fillAll();
1812
1867
  let startedAt = 0;
1813
1868
  let postElapsedMs;
1814
- const postResponse = page.waitForResponse((r) => r.request().method() === "POST", {
1815
- timeout: PAGE_TIMEOUT_MS2
1816
- }).then((r) => {
1869
+ const postResponse = page.waitForResponse(
1870
+ (r) => r.request().method() === "POST" && isSameSitePost(r.url(), baseUrl),
1871
+ {
1872
+ timeout: PAGE_TIMEOUT_MS2
1873
+ }
1874
+ ).then((r) => {
1817
1875
  if (startedAt > 0) postElapsedMs = Date.now() - startedAt;
1818
1876
  return r;
1819
1877
  }).catch(() => null);
@@ -1826,15 +1884,34 @@ async function defaultFormRunner() {
1826
1884
  formPresent: true,
1827
1885
  success: true,
1828
1886
  elapsedMs,
1829
- ...postElapsedMs !== void 0 ? { postElapsedMs } : {}
1887
+ ...postElapsedMs !== void 0 ? { postElapsedMs } : {},
1888
+ ...refilled ? { refilled } : {}
1830
1889
  };
1831
1890
  const actionResp = await postResponse;
1832
1891
  const alertText = await page.locator('[role="alert"]').first().textContent({ timeout: 1e3 }).catch(() => null);
1833
- const respBit = actionResp ? `POST ${actionResp.status()}${actionResp.status() >= 400 ? ` ${(await actionResp.text().catch(() => "")).slice(0, 80)}` : ""}` : "no POST observed";
1892
+ const formState = await page.evaluate(
1893
+ `
1894
+ (function () {
1895
+ const f = document.querySelector("form");
1896
+ if (!f) return { formPresent: false, checkValidity: null, emptyRequired: [] };
1897
+ const emptyRequired = [];
1898
+ for (const el of f.querySelectorAll("input, textarea, select")) {
1899
+ if (el.required && !String(el.value).trim())
1900
+ emptyRequired.push(el.name || el.id || el.type);
1901
+ }
1902
+ return { formPresent: true, checkValidity: f.checkValidity(), emptyRequired };
1903
+ })();
1904
+ `
1905
+ ).catch(() => null);
1906
+ const post = actionResp ? {
1907
+ status: actionResp.status(),
1908
+ ...actionResp.status() >= 400 ? { bodyBit: (await actionResp.text().catch(() => "")).slice(0, 80) } : {}
1909
+ } : null;
1834
1910
  return {
1835
1911
  formPresent: true,
1836
1912
  success: false,
1837
- detail: `no success banner after submit \u2014 ${respBit}${alertText ? `; alert: ${alertText.trim().slice(0, 80)}` : ""}`
1913
+ ...refilled ? { refilled } : {},
1914
+ detail: noBannerDetail({ post, alertText, formState, hydrationMismatch, refilled })
1838
1915
  };
1839
1916
  } catch (err) {
1840
1917
  return { formPresent: true, success: false, detail: String(err).slice(0, 120) };
@@ -1902,4 +1979,4 @@ export {
1902
1979
  runAudits,
1903
1980
  runAuditsAcross
1904
1981
  };
1905
- //# sourceMappingURL=chunk-B23FFWU4.js.map
1982
+ //# sourceMappingURL=chunk-AEXJDCJN.js.map