@reddoorla/maintenance 0.91.0 → 0.93.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 (106) hide show
  1. package/dist/{bump-deps-KGH5UYSF.js → bump-deps-GI7PHMOC.js} +6 -6
  2. package/dist/{chunk-A6T2R63Z.js → chunk-3HS6LPFX.js} +2 -2
  3. package/dist/{chunk-SJOXWJ5Y.js → chunk-4763KQ6O.js} +184 -9
  4. package/dist/chunk-4763KQ6O.js.map +1 -0
  5. package/dist/{chunk-UXJZU23T.js → chunk-4NUKMPOO.js} +9 -9
  6. package/dist/{chunk-BQRFQSGY.js → chunk-4R3YAQNV.js} +2 -2
  7. package/dist/{chunk-XIDUYLSR.js → chunk-7CJ3DVZW.js} +2 -2
  8. package/dist/{chunk-Z3KYFUN6.js → chunk-B6NUZ2ZX.js} +2 -2
  9. package/dist/{chunk-WVVT6SJK.js → chunk-BJQCIJOY.js} +169 -17
  10. package/dist/chunk-BJQCIJOY.js.map +1 -0
  11. package/dist/{chunk-Y53WOQIJ.js → chunk-CVJF2IC5.js} +2 -2
  12. package/dist/chunk-I7NHLVVI.js +89 -0
  13. package/dist/chunk-I7NHLVVI.js.map +1 -0
  14. package/dist/{chunk-QD427NIO.js → chunk-JB4Q3C3M.js} +7 -7
  15. package/dist/{chunk-IYSEC543.js → chunk-LS6QPANO.js} +2 -2
  16. package/dist/{chunk-7RJ2HMMJ.js → chunk-NTLSMJHW.js} +2 -2
  17. package/dist/{chunk-2NJQLSEU.js → chunk-PPIZHDJL.js} +2 -2
  18. package/dist/{chunk-JRHIFA5R.js → chunk-QF6IQQFP.js} +10 -4
  19. package/dist/chunk-QF6IQQFP.js.map +1 -0
  20. package/dist/{chunk-RQLXEMEE.js → chunk-T7ZUUOZ2.js} +2 -2
  21. package/dist/chunk-T7ZUUOZ2.js.map +1 -0
  22. package/dist/{chunk-TVL5MVUF.js → chunk-TCF6S7AD.js} +4 -4
  23. package/dist/{chunk-GSWTT2J6.js → chunk-WHWVYL7B.js} +4 -4
  24. package/dist/{chunk-3G25KIWW.js → chunk-ZTPPCHDB.js} +4 -4
  25. package/dist/cli/bin.js +17 -17
  26. package/dist/cli/commands/audit.js +10 -10
  27. package/dist/{convert-to-pnpm-NWB5FCQ2.js → convert-to-pnpm-ONFP3JKF.js} +6 -6
  28. package/dist/{db-IMMQSRAB.js → db-HSZ6D5DU.js} +3 -3
  29. package/dist/digest-EOA4JLTV.js +1 -1
  30. package/dist/digest-collectors-ZA3BTS3R.js +1 -1
  31. package/dist/{email-PLPDJUDH.js → email-I4BCETI4.js} +16 -65
  32. package/dist/email-I4BCETI4.js.map +1 -0
  33. package/dist/forms/index.d.ts +5 -1
  34. package/dist/forms/index.js +1 -1
  35. package/dist/forms/index.js.map +1 -1
  36. package/dist/forms/prismic.d.ts +92 -0
  37. package/dist/forms/prismic.js +105 -0
  38. package/dist/forms/prismic.js.map +1 -0
  39. package/dist/{forms-notify-target-YCZUAHTY.js → forms-notify-target-RMZPKT6S.js} +4 -3
  40. package/dist/{forms-notify-target-YCZUAHTY.js.map → forms-notify-target-RMZPKT6S.js.map} +1 -1
  41. package/dist/{health-endpoint-UVX6R5N5.js → health-endpoint-DHKJUPRM.js} +6 -6
  42. package/dist/index.js +30 -23
  43. package/dist/index.js.map +1 -1
  44. package/dist/{init-NXZ4NXTL.js → init-ZRKWYCKA.js} +15 -15
  45. package/dist/{launch-WPEQ3TY2.js → launch-6X5OXPF2.js} +11 -11
  46. package/dist/{notify-4HP4GZ2H.js → notify-IISTHNBR.js} +4 -3
  47. package/dist/{onboard-BKCYBAVW.js → onboard-6NHGCKGB.js} +6 -6
  48. package/dist/orchestrate-KMQIUP3H.js +1 -1
  49. package/dist/{pipeline-HQJYZ443.js → pipeline-AQBFG5WM.js} +5 -3
  50. package/dist/preflight-VPHQYWTY.js +1 -1
  51. package/dist/{prismic-ci-6YGRIOHT.js → prismic-ci-A2HI7NNJ.js} +8 -8
  52. package/dist/{prismic-models-FPDSUXDO.js → prismic-models-QTKIMFBC.js} +4 -4
  53. package/dist/prospect/types.d.ts +17 -0
  54. package/dist/prospect/types.js +1 -1
  55. package/dist/{prospect-audit-TFIXSTE5.js → prospect-audit-RKUVSTLN.js} +4 -4
  56. package/dist/recipes/sync-configs.js +3 -3
  57. package/dist/{chunk-H6BLTEFN.js → render-7G43BSK5.js} +3 -3
  58. package/dist/{chunk-H6BLTEFN.js.map → render-7G43BSK5.js.map} +1 -1
  59. package/dist/{replay-4S6ENYCH.js → replay-PCSQOGCZ.js} +3 -2
  60. package/dist/{replay-4S6ENYCH.js.map → replay-PCSQOGCZ.js.map} +1 -1
  61. package/dist/{self-updating-VQIW24RZ.js → self-updating-D5XOWS7V.js} +5 -5
  62. package/dist/selftest-IFGSJP4X.js +1 -1
  63. package/dist/{smoke-suite-4JFZZYVW.js → smoke-suite-GXC5JZPH.js} +6 -6
  64. package/dist/{svelte-codemods-QAPDZC6F.js → svelte-codemods-A3EARSRP.js} +6 -6
  65. package/dist/{sync-configs-PVRLZRKL.js → sync-configs-KSESOHFF.js} +6 -6
  66. package/dist/{upgrade-IR2OO3KP.js → upgrade-ZLNVEIEA.js} +6 -6
  67. package/dist/{webflow-624FEBFD.js → webflow-IRKOIT7S.js} +4 -4
  68. package/package.json +5 -1
  69. package/dist/chunk-JRHIFA5R.js.map +0 -1
  70. package/dist/chunk-RQLXEMEE.js.map +0 -1
  71. package/dist/chunk-SJOXWJ5Y.js.map +0 -1
  72. package/dist/chunk-WVVT6SJK.js.map +0 -1
  73. package/dist/email-PLPDJUDH.js.map +0 -1
  74. package/dist/render-3GYW2J34.js +0 -11
  75. package/dist/render-3GYW2J34.js.map +0 -1
  76. /package/dist/{bump-deps-KGH5UYSF.js.map → bump-deps-GI7PHMOC.js.map} +0 -0
  77. /package/dist/{chunk-A6T2R63Z.js.map → chunk-3HS6LPFX.js.map} +0 -0
  78. /package/dist/{chunk-UXJZU23T.js.map → chunk-4NUKMPOO.js.map} +0 -0
  79. /package/dist/{chunk-BQRFQSGY.js.map → chunk-4R3YAQNV.js.map} +0 -0
  80. /package/dist/{chunk-XIDUYLSR.js.map → chunk-7CJ3DVZW.js.map} +0 -0
  81. /package/dist/{chunk-Z3KYFUN6.js.map → chunk-B6NUZ2ZX.js.map} +0 -0
  82. /package/dist/{chunk-Y53WOQIJ.js.map → chunk-CVJF2IC5.js.map} +0 -0
  83. /package/dist/{chunk-QD427NIO.js.map → chunk-JB4Q3C3M.js.map} +0 -0
  84. /package/dist/{chunk-IYSEC543.js.map → chunk-LS6QPANO.js.map} +0 -0
  85. /package/dist/{chunk-7RJ2HMMJ.js.map → chunk-NTLSMJHW.js.map} +0 -0
  86. /package/dist/{chunk-2NJQLSEU.js.map → chunk-PPIZHDJL.js.map} +0 -0
  87. /package/dist/{chunk-TVL5MVUF.js.map → chunk-TCF6S7AD.js.map} +0 -0
  88. /package/dist/{chunk-GSWTT2J6.js.map → chunk-WHWVYL7B.js.map} +0 -0
  89. /package/dist/{chunk-3G25KIWW.js.map → chunk-ZTPPCHDB.js.map} +0 -0
  90. /package/dist/{convert-to-pnpm-NWB5FCQ2.js.map → convert-to-pnpm-ONFP3JKF.js.map} +0 -0
  91. /package/dist/{db-IMMQSRAB.js.map → db-HSZ6D5DU.js.map} +0 -0
  92. /package/dist/{health-endpoint-UVX6R5N5.js.map → health-endpoint-DHKJUPRM.js.map} +0 -0
  93. /package/dist/{init-NXZ4NXTL.js.map → init-ZRKWYCKA.js.map} +0 -0
  94. /package/dist/{launch-WPEQ3TY2.js.map → launch-6X5OXPF2.js.map} +0 -0
  95. /package/dist/{notify-4HP4GZ2H.js.map → notify-IISTHNBR.js.map} +0 -0
  96. /package/dist/{onboard-BKCYBAVW.js.map → onboard-6NHGCKGB.js.map} +0 -0
  97. /package/dist/{pipeline-HQJYZ443.js.map → pipeline-AQBFG5WM.js.map} +0 -0
  98. /package/dist/{prismic-ci-6YGRIOHT.js.map → prismic-ci-A2HI7NNJ.js.map} +0 -0
  99. /package/dist/{prismic-models-FPDSUXDO.js.map → prismic-models-QTKIMFBC.js.map} +0 -0
  100. /package/dist/{prospect-audit-TFIXSTE5.js.map → prospect-audit-RKUVSTLN.js.map} +0 -0
  101. /package/dist/{self-updating-VQIW24RZ.js.map → self-updating-D5XOWS7V.js.map} +0 -0
  102. /package/dist/{smoke-suite-4JFZZYVW.js.map → smoke-suite-GXC5JZPH.js.map} +0 -0
  103. /package/dist/{svelte-codemods-QAPDZC6F.js.map → svelte-codemods-A3EARSRP.js.map} +0 -0
  104. /package/dist/{sync-configs-PVRLZRKL.js.map → sync-configs-KSESOHFF.js.map} +0 -0
  105. /package/dist/{upgrade-IR2OO3KP.js.map → upgrade-ZLNVEIEA.js.map} +0 -0
  106. /package/dist/{webflow-624FEBFD.js.map → webflow-IRKOIT7S.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  checkGoal
3
- } from "./chunk-RQLXEMEE.js";
3
+ } from "./chunk-T7ZUUOZ2.js";
4
4
  import {
5
5
  isPrivateOrLoopbackHost
6
6
  } from "./chunk-RARREDQE.js";
@@ -1273,6 +1273,10 @@ Return:
1273
1273
  at it with a pronoun has no antecedent and the engine will answer that it does not know who is meant.
1274
1274
  These are searches, not conversational questions, and they are not the buyerQuestions above.
1275
1275
  - fixes: prioritized, concrete, specific to this site. No generic SEO advice.
1276
+ Each "why" describes what a buyer or a crawler can or cannot read on the site TODAY. Never predict
1277
+ what an answer engine will cite, repeat, rank or recommend: the report states elsewhere, with
1278
+ evidence, that nothing on the site reliably moves that, and a fix that promises it contradicts the
1279
+ report. "A buyer cannot find a price" is a reason; "an engine will have nothing to cite" is not.
1276
1280
  Set addresses to the key of the measured requirement a fix would satisfy \u2014 one of the keys listed
1277
1281
  under "What we have already measured", exactly as written \u2014 or null when it answers to none of them.
1278
1282
  Null is the normal case: a heavy image, a broken link or a stale copyright year maps to no
@@ -1462,7 +1466,12 @@ async function analyzeSite(url, crawl, checks, deps = defaultAnalyzeDeps(), goal
1462
1466
  ...parsed,
1463
1467
  questionSetId: set.id,
1464
1468
  buyerQuestions: conformToSet(parsed.buyerQuestions, set),
1465
- fixes: reconcileFixes(parsed.fixes, goalFit)
1469
+ // Stamped here, at the one place model fixes enter the system, so the
1470
+ // renderer can label them as judgement and never as a finding.
1471
+ fixes: reconcileFixes(parsed.fixes, goalFit).map((f) => ({
1472
+ ...f,
1473
+ origin: "recommendation"
1474
+ }))
1466
1475
  };
1467
1476
  return verifyEvidence(conformed, crawl);
1468
1477
  }
@@ -2050,6 +2059,7 @@ async function classifyDomains(prospectUrl, prospectPhones, domains, deps) {
2050
2059
  var MAX_PAGES2 = 14;
2051
2060
  var MAX_TOTAL_CHARS = 12e4;
2052
2061
  var MAX_ASSERTIONS = 12;
2062
+ var NO_CONFLATION = { detected: false, otherNames: [], engineQuote: null };
2053
2063
  var AccuracySchema = z2.object({
2054
2064
  assertions: z2.array(
2055
2065
  z2.object({
@@ -2065,7 +2075,15 @@ var AccuracySchema = z2.object({
2065
2075
  */
2066
2076
  searchTerms: z2.array(z2.string()).min(1).max(6)
2067
2077
  })
2068
- ).max(MAX_ASSERTIONS)
2078
+ ).max(MAX_ASSERTIONS),
2079
+ /** Does the answer describe MORE THAN ONE business under this name, or a
2080
+ * different business first? Defaulted, so a model that omits it reads as
2081
+ * "not detected" rather than failing the whole stage. */
2082
+ conflation: z2.object({
2083
+ detected: z2.boolean(),
2084
+ otherNames: z2.array(z2.string()).max(6),
2085
+ engineQuote: z2.string().nullable()
2086
+ }).default({ detected: false, otherNames: [], engineQuote: null })
2069
2087
  });
2070
2088
  function makeFenceTag2() {
2071
2089
  return `data_${randomBytes2(6).toString("hex")}`;
@@ -2103,6 +2121,12 @@ For each assertion return:
2103
2121
  - searchTerms: 1-6 distinctive words or short phrases you would search the site for to find this fact \u2014
2104
2122
  proper nouns, numbers, street names. These are used to re-check "absent" verdicts across pages you were
2105
2123
  not shown, so choose terms that would actually appear if the fact were stated somewhere else on the site.
2124
+
2125
+ Separately, return "conflation": does the answer describe MORE THAN ONE business under this name, or
2126
+ describe a different business first? If so, detected is true, otherNames lists the OTHER businesses
2127
+ named (never this one), and engineQuote is the exact sentence that says the name belongs to several
2128
+ businesses \u2014 copied verbatim, it is checked against the answer character by character. Otherwise
2129
+ detected is false with an empty otherNames and a null engineQuote.
2106
2130
  Avoid generic words ("dental", "services") that appear on every page.
2107
2131
 
2108
2132
  Be conservative about "confirmed": if the site only gestures at the fact, that is "absent". Being wrong in
@@ -2268,28 +2292,40 @@ function quoteSupportsClaim(claim, quote) {
2268
2292
  for (const w of words(claim)) if (inQuote.has(w)) shared += 1;
2269
2293
  return shared >= 2;
2270
2294
  }
2271
- function verifyQuotes(raw, answerText, siteText, answerOf, prospectDomain) {
2295
+ function verifyQuotes(raw, answerText, siteText, answerOf, prospectDomain, searchTermsByClaim = /* @__PURE__ */ new Map()) {
2272
2296
  const answers = normalize(answerText);
2273
2297
  const site = normalize(siteText);
2298
+ const flat = siteText.replace(/\s+/g, " ");
2274
2299
  const out = [];
2275
2300
  for (const a of raw) {
2276
2301
  if (!answers.includes(normalize(a.engineQuote))) continue;
2302
+ let verdict = a.verdict;
2303
+ let siteQuote = a.siteQuote;
2304
+ if (a.verdict === "contradicted") {
2305
+ for (const term of distinctive(searchTermsByClaim.get(a.claim) ?? [])) {
2306
+ if (findTerm(site, term) !== "exact") continue;
2307
+ const at = site.indexOf(normalize(term));
2308
+ verdict = "confirmed";
2309
+ siteQuote = flat.slice(Math.max(0, at - 80), at + term.length + 80).trim();
2310
+ break;
2311
+ }
2312
+ }
2277
2313
  const from = answerOf.get(normalize(a.engineQuote));
2278
2314
  const sourceDomains = [
2279
2315
  ...new Set((from?.citedDomains ?? []).map(domainOf).filter((d) => d !== prospectDomain))
2280
2316
  ];
2281
- const quoteReal = a.siteQuote !== null && site.includes(normalize(a.siteQuote));
2282
- const needsQuote = a.verdict === "confirmed" || a.verdict === "contradicted";
2283
- const quoteSupports = quoteReal && a.siteQuote !== null && quoteSupportsClaim(a.claim, a.siteQuote);
2317
+ const quoteReal = siteQuote !== null && site.includes(normalize(siteQuote));
2318
+ const needsQuote = verdict === "confirmed" || verdict === "contradicted";
2319
+ const quoteSupports = quoteReal && siteQuote !== null && quoteSupportsClaim(a.claim, siteQuote);
2284
2320
  const unsupported = needsQuote && (!quoteReal || !quoteSupports);
2285
2321
  out.push({
2286
2322
  claim: a.claim,
2287
- verdict: unsupported ? "unverified" : a.verdict,
2323
+ verdict: unsupported ? "unverified" : verdict,
2288
2324
  engineQuote: a.engineQuote,
2289
2325
  // Kept when it is real, even where it does not carry the claim: the
2290
2326
  // reader can see what we looked at and judge it themselves, which is a
2291
2327
  // better position than being told we found nothing.
2292
- siteQuote: quoteReal ? a.siteQuote : null,
2328
+ siteQuote: quoteReal ? siteQuote : null,
2293
2329
  nearbyMention: null,
2294
2330
  unverifiedReason: !unsupported ? null : !quoteReal ? "We could not find the passage this was based on, so we have not stated it either way." : "The passage we found is about the same subject but does not actually say this, so we have not stated it either way.",
2295
2331
  sourceDomains,
@@ -2332,11 +2368,13 @@ async function checkAccuracy(url, crawl, answers, prospectPhones, deps) {
2332
2368
  siteFullyRead: false,
2333
2369
  pagesRead: 0,
2334
2370
  pagesTotal: crawl.pages.length,
2335
- answersRead: 0
2371
+ answersRead: 0,
2372
+ conflation: NO_CONFLATION
2336
2373
  };
2337
2374
  }
2338
2375
  const input = buildAccuracyInput(crawl, branded);
2339
2376
  const parsed = AccuracySchema.parse(await deps.run(input));
2377
+ const termsByClaim = new Map(parsed.assertions.map((a) => [a.claim, a.searchTerms]));
2340
2378
  const answerOf = /* @__PURE__ */ new Map();
2341
2379
  for (const a of branded) {
2342
2380
  const text = normalize(a.fullAnswer ?? "");
@@ -2346,14 +2384,17 @@ async function checkAccuracy(url, crawl, answers, prospectPhones, deps) {
2346
2384
  }
2347
2385
  }
2348
2386
  const siteText = fullSiteText(crawl);
2387
+ const answerText = branded.map((a) => a.fullAnswer ?? "").join("\n");
2349
2388
  const verified = verifyQuotes(
2350
2389
  parsed.assertions,
2351
- branded.map((a) => a.fullAnswer ?? "").join("\n"),
2390
+ answerText,
2352
2391
  siteText,
2353
2392
  answerOf,
2354
- domainOf(url)
2393
+ domainOf(url),
2394
+ termsByClaim
2355
2395
  );
2356
- const termsByClaim = new Map(parsed.assertions.map((a) => [a.claim, a.searchTerms]));
2396
+ const c = parsed.conflation;
2397
+ const conflation = c.detected && c.engineQuote !== null && normalize(answerText).includes(normalize(c.engineQuote)) ? { detected: true, otherNames: c.otherNames, engineQuote: c.engineQuote } : NO_CONFLATION;
2357
2398
  const sources = await classifyDomains(
2358
2399
  url,
2359
2400
  prospectPhones,
@@ -2366,7 +2407,8 @@ async function checkAccuracy(url, crawl, answers, prospectPhones, deps) {
2366
2407
  siteFullyRead: input.fullyRead,
2367
2408
  pagesRead: input.pagesRead,
2368
2409
  pagesTotal: crawl.pages.length,
2369
- answersRead: branded.length
2410
+ answersRead: branded.length,
2411
+ conflation
2370
2412
  };
2371
2413
  }
2372
2414
 
@@ -3155,6 +3197,99 @@ async function checkBasics(crawl, deps) {
3155
3197
  };
3156
3198
  }
3157
3199
 
3200
+ // src/prospect/measured-fixes.ts
3201
+ var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
3202
+ function measuredFixes(input) {
3203
+ const out = [];
3204
+ const c = input.checks;
3205
+ if (c && c.crawlerAccessMeasured && c.crawlerAccess.blockedAi.length > 0) {
3206
+ const names = c.crawlerAccess.blockedAi.join(", ");
3207
+ out.push({
3208
+ title: `Let ${names} read your site`,
3209
+ why: `Your robots.txt turns away ${names}. A crawler that is refused cannot read a page, so nothing else in this report can help for it until this changes.`,
3210
+ impact: "high",
3211
+ effort: "low",
3212
+ tier: "crawl",
3213
+ addresses: null,
3214
+ origin: "measured"
3215
+ });
3216
+ }
3217
+ for (const r of input.goalFit?.requirements ?? []) {
3218
+ if (r.status !== "missing") continue;
3219
+ out.push({
3220
+ title: r.label.charAt(0).toUpperCase() + r.label.slice(1),
3221
+ why: r.why,
3222
+ impact: "high",
3223
+ effort: r.scope === "quick" ? "low" : r.scope === "content" ? "medium" : "high",
3224
+ tier: r.scope === "quick" ? "technical" : "content",
3225
+ addresses: r.key,
3226
+ origin: "measured"
3227
+ });
3228
+ }
3229
+ const phoneJudgedByGoal = (input.goalFit?.requirements ?? []).some(
3230
+ (r) => r.key === "tappable-phone"
3231
+ );
3232
+ const plain = phoneJudgedByGoal ? 0 : (input.phones ?? []).filter((p) => p.linked === false).length;
3233
+ if (plain > 0) {
3234
+ out.push({
3235
+ title: "Make your phone number tappable",
3236
+ why: `${plural(plain, "number is", "numbers are")} written as plain text. On a phone that is something a visitor has to memorise and retype; as a link it is one tap, and it is the moment they were most likely to call.`,
3237
+ impact: "medium",
3238
+ effort: "low",
3239
+ tier: "technical",
3240
+ addresses: null,
3241
+ origin: "measured"
3242
+ });
3243
+ }
3244
+ if (input.brokenLinks !== null && input.brokenLinks > 0) {
3245
+ out.push({
3246
+ title: `Repair ${plural(input.brokenLinks, "broken link", "broken links")}`,
3247
+ why: "A visitor who follows one lands on an error page, and a crawler that follows one stops there.",
3248
+ impact: "medium",
3249
+ effort: "low",
3250
+ tier: "technical",
3251
+ addresses: null,
3252
+ origin: "measured"
3253
+ });
3254
+ }
3255
+ if (input.brokenImages !== null && input.brokenImages > 0) {
3256
+ out.push({
3257
+ title: `Replace ${plural(input.brokenImages, "broken image", "broken images")}`,
3258
+ why: "It shows as an empty box or a missing-image icon on the page a visitor is reading.",
3259
+ impact: "low",
3260
+ effort: "low",
3261
+ tier: "technical",
3262
+ addresses: null,
3263
+ origin: "measured"
3264
+ });
3265
+ }
3266
+ if (c) {
3267
+ if (c.headings.pagesWithoutH1 > 0) {
3268
+ out.push({
3269
+ title: `Give ${plural(c.headings.pagesWithoutH1, "page", "pages")} a top heading`,
3270
+ why: `${c.headings.pagesWithoutH1} of ${c.meta.pageCount} pages have no top-level heading, so a reader or a crawler arriving on them is not told what the page is about.`,
3271
+ impact: "medium",
3272
+ effort: "low",
3273
+ tier: "technical",
3274
+ addresses: null,
3275
+ origin: "measured"
3276
+ });
3277
+ }
3278
+ if (c.meta.missingCanonical > 0) {
3279
+ out.push({
3280
+ title: "Tell search engines which address is the real one for each page",
3281
+ why: `${c.meta.missingCanonical} of ${c.meta.pageCount} pages do not declare a canonical address. When the same page is reachable at more than one address, a search engine has to guess which one to keep.`,
3282
+ impact: "medium",
3283
+ effort: "low",
3284
+ tier: "technical",
3285
+ addresses: null,
3286
+ origin: "measured"
3287
+ });
3288
+ }
3289
+ }
3290
+ return out;
3291
+ }
3292
+
3158
3293
  // src/prospect/pipeline.ts
3159
3294
  function envAnalyzeDeps(factories = {
3160
3295
  api: defaultAnalyzeDeps,
@@ -3215,6 +3350,10 @@ async function stage(name, deps, fn) {
3215
3350
  return { ok: false, error };
3216
3351
  }
3217
3352
  }
3353
+ function mergeFixes(measured, model) {
3354
+ const covered = new Set(measured.map((f) => f.addresses).filter((k) => !!k));
3355
+ return [...measured, ...model.filter((f) => !(f.addresses && covered.has(f.addresses)))];
3356
+ }
3218
3357
  async function runProspectAudit(url, opts, deps = {}) {
3219
3358
  const llmAuth = llmAuthMode();
3220
3359
  const crawlDeps = deps.crawl ?? defaultCrawlDeps();
@@ -3320,9 +3459,21 @@ async function runProspectAudit(url, opts, deps = {}) {
3320
3459
  checks.ok ? checks.data : null
3321
3460
  )
3322
3461
  };
3323
- const reconciled = analyze.ok && goalFit.ok ? {
3462
+ const reconciled = analyze.ok ? {
3324
3463
  ok: true,
3325
- data: { ...analyze.data, fixes: reconcileFixes(analyze.data.fixes, goalFit.data) }
3464
+ data: {
3465
+ ...analyze.data,
3466
+ fixes: mergeFixes(
3467
+ measuredFixes({
3468
+ goalFit: goalFit.ok ? goalFit.data : null,
3469
+ checks: checks.ok ? checks.data : null,
3470
+ phones: checks.ok ? checks.data.consistency?.phones ?? null : null,
3471
+ brokenLinks: assets.ok ? assets.data.brokenLinks.length : null,
3472
+ brokenImages: assets.ok ? assets.data.brokenImages.length : null
3473
+ }),
3474
+ goalFit.ok ? reconcileFixes(analyze.data.fixes, goalFit.data) : analyze.data.fixes
3475
+ )
3476
+ }
3326
3477
  } : analyze;
3327
3478
  const brandedAnswers = probes.ok ? probes.data.answers.filter((a) => a.kind === "branded") : [];
3328
3479
  const accuracy = brandedAnswers.length === 0 ? { ok: false, error: ACCURACY_SKIPPED } : await stage("accuracy", deps, async () => {
@@ -3377,6 +3528,7 @@ export {
3377
3528
  GOAL_UNRESOLVED,
3378
3529
  ASSET_ACCEPT,
3379
3530
  defaultAssetProbe,
3531
+ mergeFixes,
3380
3532
  runProspectAudit
3381
3533
  };
3382
- //# sourceMappingURL=chunk-WVVT6SJK.js.map
3534
+ //# sourceMappingURL=chunk-BJQCIJOY.js.map