@canonry/canonry 4.138.0 → 4.139.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 (39) hide show
  1. package/README.md +1 -1
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +9 -13
  3. package/assets/assets/{AuditHistoryPanel-CIdoSKjP.js → AuditHistoryPanel-DL9chF3F.js} +1 -1
  4. package/assets/assets/BacklinksPage-sA_tgQJv.js +1 -0
  5. package/assets/assets/ChartPrimitives-D7nLxIbe.js +1 -0
  6. package/assets/assets/HistoryPage-CLWbcu5m.js +1 -0
  7. package/assets/assets/ProjectPage-C_oXMCNU.js +8 -0
  8. package/assets/assets/{RunRow-D6W6D1BX.js → RunRow-DQ4icEiv.js} +1 -1
  9. package/assets/assets/RunsPage-D5sizm9T.js +1 -0
  10. package/assets/assets/SettingsPage-XdQjvWsg.js +1 -0
  11. package/assets/assets/TrafficPage-C12osXDa.js +1 -0
  12. package/assets/assets/TrafficSourceDetailPage-CXAiNK8Y.js +1 -0
  13. package/assets/assets/{arrow-left-ClC65OiR.js → arrow-left-LaI5a16g.js} +1 -1
  14. package/assets/assets/extract-error-message-CYWaTM3m.js +1 -0
  15. package/assets/assets/index-CguwySA5.js +213 -0
  16. package/assets/assets/index-CntuM6Th.css +1 -0
  17. package/assets/assets/trash-2-DoH00q7m.js +1 -0
  18. package/assets/index.html +2 -2
  19. package/dist/{chunk-3KGJ7BXW.js → chunk-DLKUDNDA.js} +531 -435
  20. package/dist/{chunk-GO24URVP.js → chunk-IGU67QFM.js} +1607 -1149
  21. package/dist/{chunk-R3GAWKHM.js → chunk-IYTHIAAE.js} +5 -25
  22. package/dist/{chunk-RJ7OW6W6.js → chunk-OA2EGCUS.js} +2973 -2914
  23. package/dist/cli.js +10 -38
  24. package/dist/index.js +4 -4
  25. package/dist/{intelligence-service-5RJHJLYA.js → intelligence-service-SMYVN3KN.js} +2 -2
  26. package/dist/mcp.js +2 -2
  27. package/package.json +10 -10
  28. package/assets/assets/BacklinksPage-BYnzQLFq.js +0 -1
  29. package/assets/assets/ChartPrimitives-bx4p5MDx.js +0 -1
  30. package/assets/assets/HistoryPage-CjLb8TKY.js +0 -1
  31. package/assets/assets/ProjectPage-C_bSGiLs.js +0 -8
  32. package/assets/assets/RunsPage-CQT4lXFp.js +0 -1
  33. package/assets/assets/SettingsPage-DVQKzz6s.js +0 -1
  34. package/assets/assets/TrafficPage-BdocLgWw.js +0 -1
  35. package/assets/assets/TrafficSourceDetailPage-CTZF_R9C.js +0 -1
  36. package/assets/assets/extract-error-message-CsgTjzyj.js +0 -1
  37. package/assets/assets/index-BZgknVrZ.js +0 -213
  38. package/assets/assets/index-DY7GziwY.css +0 -1
  39. package/assets/assets/trash-2-nr8RRH47.js +0 -1
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-R3GAWKHM.js";
13
+ } from "./chunk-IYTHIAAE.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -50,6 +50,7 @@ import {
50
50
  deliverWebhook,
51
51
  determineCitationState,
52
52
  discoverySessions,
53
+ doctorHealthState,
53
54
  downloadFile,
54
55
  dropLegacyCredentialColumns,
55
56
  executeDiscovery,
@@ -70,11 +71,9 @@ import {
70
71
  gbpPlaceDetails,
71
72
  getAttributes,
72
73
  getCrawlIssues,
73
- getLinkCounts,
74
74
  getLodging,
75
75
  getPlaceDetails,
76
76
  getUrlInfo,
77
- getUrlLinks,
78
77
  groupRunsByCreatedAt,
79
78
  gscCoverageSnapshots,
80
79
  gscDailyTotals,
@@ -113,13 +112,16 @@ import {
113
112
  refreshAccessToken,
114
113
  researchRunQueries,
115
114
  researchRuns,
115
+ resolveDestination,
116
+ resolveVercelSyncDeadlineMs,
116
117
  resolveWebhookTarget,
117
118
  runs,
118
119
  schedules,
119
120
  siteAuditPages,
120
121
  siteAuditSnapshots,
122
+ toAlertView,
121
123
  usageCounters
122
- } from "./chunk-GO24URVP.js";
124
+ } from "./chunk-IGU67QFM.js";
123
125
  import {
124
126
  AGENT_MEMORY_VALUE_MAX_BYTES,
125
127
  AGENT_PROVIDER_IDS,
@@ -206,7 +208,7 @@ import {
206
208
  validationError,
207
209
  winnabilityClassLabel,
208
210
  withRetry
209
- } from "./chunk-RJ7OW6W6.js";
211
+ } from "./chunk-OA2EGCUS.js";
210
212
 
211
213
  // src/telemetry.ts
212
214
  import crypto from "crypto";
@@ -537,11 +539,11 @@ function checkLatestVersionForServer(opts) {
537
539
 
538
540
  // src/server.ts
539
541
  import { createRequire as createRequire3 } from "module";
540
- import crypto23 from "crypto";
542
+ import crypto22 from "crypto";
541
543
  import fs8 from "fs";
542
544
  import path9 from "path";
543
545
  import { fileURLToPath as fileURLToPath3 } from "url";
544
- import { and as and16, eq as eq22 } from "drizzle-orm";
546
+ import { and as and15, eq as eq21 } from "drizzle-orm";
545
547
  import Fastify from "fastify";
546
548
  import os5 from "os";
547
549
 
@@ -958,7 +960,14 @@ var geminiAdapter = {
958
960
  model: raw.model,
959
961
  servedModel: raw.servedModel,
960
962
  groundingSources: raw.groundingSources,
961
- searchQueries: raw.searchQueries
963
+ searchQueries: raw.searchQueries,
964
+ // Retrieval detection is not implemented for this provider. Its candidate
965
+ // marker is present on 100% of stored rows, so it has never been shown to
966
+ // discriminate a non-retrieving answer and wiring it up would hardcode
967
+ // `used`. `unknown` states what we actually know. The contract is a
968
+ // declaration about how we build the request, so it is always knowable.
969
+ retrievalStatus: "unknown",
970
+ retrievalContract: "native-auto-v1"
962
971
  };
963
972
  },
964
973
  normalizeResult(raw) {
@@ -975,7 +984,8 @@ var geminiAdapter = {
975
984
  answerText: normalized.answerText,
976
985
  citedDomains: normalized.citedDomains,
977
986
  groundingSources: normalized.groundingSources,
978
- searchQueries: normalized.searchQueries
987
+ searchQueries: normalized.searchQueries,
988
+ retrievalStatus: "unknown"
979
989
  };
980
990
  },
981
991
  async generateText(prompt, config) {
@@ -1300,7 +1310,14 @@ var openaiAdapter = {
1300
1310
  model: raw.model,
1301
1311
  servedModel: raw.servedModel,
1302
1312
  groundingSources: raw.groundingSources,
1303
- searchQueries: raw.searchQueries
1313
+ searchQueries: raw.searchQueries,
1314
+ // Retrieval detection is not implemented for this provider. Its candidate
1315
+ // marker is present on 100% of stored rows, so it has never been shown to
1316
+ // discriminate a non-retrieving answer and wiring it up would hardcode
1317
+ // `used`. `unknown` states what we actually know. The contract is a
1318
+ // declaration about how we build the request, so it is always knowable.
1319
+ retrievalStatus: "unknown",
1320
+ retrievalContract: "native-auto-v1"
1304
1321
  };
1305
1322
  },
1306
1323
  normalizeResult(raw) {
@@ -1317,7 +1334,8 @@ var openaiAdapter = {
1317
1334
  answerText: normalized.answerText,
1318
1335
  citedDomains: normalized.citedDomains,
1319
1336
  groundingSources: normalized.groundingSources,
1320
- searchQueries: normalized.searchQueries
1337
+ searchQueries: normalized.searchQueries,
1338
+ retrievalStatus: "unknown"
1321
1339
  };
1322
1340
  },
1323
1341
  async generateText(prompt, config) {
@@ -1350,6 +1368,7 @@ async function withRetry4(fn, options = {}) {
1350
1368
  // ../provider-claude/src/normalize.ts
1351
1369
  var DEFAULT_MODEL3 = "claude-sonnet-4-6";
1352
1370
  var VALIDATION_PATTERN = /^claude-/;
1371
+ var CLAUDE_RETRIEVAL_CONTRACT = "search-required-v1";
1353
1372
  function resolveModel2(config) {
1354
1373
  const m = config.model;
1355
1374
  if (!m) return DEFAULT_MODEL3;
@@ -1424,6 +1443,9 @@ async function executeTrackedQuery3(input) {
1424
1443
  model,
1425
1444
  max_tokens: 4096,
1426
1445
  tools: [webSearchTool],
1446
+ // search-required-v1: retrieval is guaranteed by the API control, so the
1447
+ // query text and answer substance stay untouched. See the contract note.
1448
+ tool_choice: { type: "tool", name: "web_search" },
1427
1449
  messages: [{ role: "user", content: input.query }]
1428
1450
  })
1429
1451
  );
@@ -1438,7 +1460,9 @@ async function executeTrackedQuery3(input) {
1438
1460
  model,
1439
1461
  servedModel: extractServedModel3(rawResponse),
1440
1462
  groundingSources: parsed.groundingSources,
1441
- searchQueries: parsed.searchQueries
1463
+ searchQueries: parsed.searchQueries,
1464
+ retrievalStatus: parsed.retrievalStatus,
1465
+ retrievalContract: CLAUDE_RETRIEVAL_CONTRACT
1442
1466
  };
1443
1467
  } catch (err) {
1444
1468
  const msg = err instanceof Error ? err.message : String(err);
@@ -1456,7 +1480,8 @@ function normalizeResult3(raw) {
1456
1480
  answerText: parsed.answerText,
1457
1481
  citedDomains,
1458
1482
  groundingSources,
1459
- searchQueries
1483
+ searchQueries,
1484
+ retrievalStatus: useParsed ? parsed.retrievalStatus : raw.retrievalStatus
1460
1485
  };
1461
1486
  }
1462
1487
  function hasParsedResponseContent3(rawResponse) {
@@ -1475,6 +1500,7 @@ function reparseStoredResult3(rawResponse) {
1475
1500
  citedDomains: extractCitedDomainsFromSources3(groundingSources),
1476
1501
  groundingSources,
1477
1502
  searchQueries,
1503
+ retrievalStatus: extractRetrievalStatusFromRaw(rawResponse),
1478
1504
  ...providerErrors.length > 0 ? { providerError: `web_search tool error: ${providerErrors.join(", ")}` } : {}
1479
1505
  };
1480
1506
  }
@@ -1514,6 +1540,17 @@ function extractGroundingSourcesFromRaw2(rawResponse) {
1514
1540
  }
1515
1541
  return sources;
1516
1542
  }
1543
+ function extractRetrievalStatusFromRaw(rawResponse) {
1544
+ try {
1545
+ const content = rawResponse.content;
1546
+ if (!Array.isArray(content) || content.length === 0) return "unknown";
1547
+ return content.some(
1548
+ (block) => block.type === "server_tool_use" && block.name === "web_search"
1549
+ ) ? "used" : "not-used";
1550
+ } catch {
1551
+ return "unknown";
1552
+ }
1553
+ }
1517
1554
  function extractSearchQueriesFromRaw3(rawResponse) {
1518
1555
  const queries2 = /* @__PURE__ */ new Set();
1519
1556
  try {
@@ -1661,7 +1698,9 @@ var claudeAdapter = {
1661
1698
  model: raw.model,
1662
1699
  servedModel: raw.servedModel,
1663
1700
  groundingSources: raw.groundingSources,
1664
- searchQueries: raw.searchQueries
1701
+ searchQueries: raw.searchQueries,
1702
+ retrievalStatus: raw.retrievalStatus,
1703
+ retrievalContract: raw.retrievalContract
1665
1704
  };
1666
1705
  },
1667
1706
  normalizeResult(raw) {
@@ -1670,7 +1709,12 @@ var claudeAdapter = {
1670
1709
  rawResponse: raw.rawResponse,
1671
1710
  model: raw.model,
1672
1711
  groundingSources: raw.groundingSources,
1673
- searchQueries: raw.searchQueries
1712
+ searchQueries: raw.searchQueries,
1713
+ // The shared RawQueryResult now carries retrieval, so nothing is lost
1714
+ // across this boundary. A reconstruction that predates the field falls
1715
+ // back to `unknown` rather than asserting an absence.
1716
+ retrievalStatus: raw.retrievalStatus ?? "unknown",
1717
+ retrievalContract: raw.retrievalContract ?? CLAUDE_RETRIEVAL_CONTRACT
1674
1718
  };
1675
1719
  const normalized = normalizeResult3(claudeRaw);
1676
1720
  return {
@@ -1678,7 +1722,8 @@ var claudeAdapter = {
1678
1722
  answerText: normalized.answerText,
1679
1723
  citedDomains: normalized.citedDomains,
1680
1724
  groundingSources: normalized.groundingSources,
1681
- searchQueries: normalized.searchQueries
1725
+ searchQueries: normalized.searchQueries,
1726
+ retrievalStatus: normalized.retrievalStatus
1682
1727
  };
1683
1728
  },
1684
1729
  async generateText(prompt, config) {
@@ -1894,7 +1939,11 @@ var localAdapter = {
1894
1939
  model: raw.model,
1895
1940
  servedModel: raw.servedModel,
1896
1941
  groundingSources: raw.groundingSources,
1897
- searchQueries: raw.searchQueries
1942
+ searchQueries: raw.searchQueries,
1943
+ // A plain chat completion with no web access: there is no retrieval step
1944
+ // to report, which is what `not-applicable` means.
1945
+ retrievalStatus: "not-applicable",
1946
+ retrievalContract: "native-auto-v1"
1898
1947
  };
1899
1948
  },
1900
1949
  normalizeResult(raw) {
@@ -1911,7 +1960,8 @@ var localAdapter = {
1911
1960
  answerText: normalized.answerText,
1912
1961
  citedDomains: normalized.citedDomains,
1913
1962
  groundingSources: normalized.groundingSources,
1914
- searchQueries: normalized.searchQueries
1963
+ searchQueries: normalized.searchQueries,
1964
+ retrievalStatus: "not-applicable"
1915
1965
  };
1916
1966
  },
1917
1967
  async generateText(prompt, config) {
@@ -2360,7 +2410,8 @@ function normalizeResult5(raw) {
2360
2410
  answerText,
2361
2411
  citedDomains: extractCitedDomains(raw.groundingSources),
2362
2412
  groundingSources: raw.groundingSources,
2363
- searchQueries: raw.searchQueries
2413
+ searchQueries: raw.searchQueries,
2414
+ retrievalStatus: raw.retrievalStatus ?? "unknown"
2364
2415
  };
2365
2416
  }
2366
2417
 
@@ -2470,6 +2521,12 @@ var cdpChatgptAdapter = {
2470
2521
  model: "chatgpt-web",
2471
2522
  groundingSources,
2472
2523
  searchQueries: [input.query],
2524
+ // This surface is scraped from the rendered web UI, which exposes no
2525
+ // reliable signal for whether a retrieval step ran behind the answer.
2526
+ // `searchQueries` here is the query we submitted, not evidence of a
2527
+ // search the product performed.
2528
+ retrievalStatus: "unknown",
2529
+ retrievalContract: "native-auto-v1",
2473
2530
  screenshotPath: capturedScreenshotPath
2474
2531
  };
2475
2532
  } catch (err) {
@@ -2784,7 +2841,14 @@ var perplexityAdapter = {
2784
2841
  model: raw.model,
2785
2842
  servedModel: raw.servedModel,
2786
2843
  groundingSources: raw.groundingSources,
2787
- searchQueries: raw.searchQueries
2844
+ searchQueries: raw.searchQueries,
2845
+ // Retrieval detection is not implemented for this provider. Its candidate
2846
+ // marker is present on 100% of stored rows, so it has never been shown to
2847
+ // discriminate a non-retrieving answer and wiring it up would hardcode
2848
+ // `used`. `unknown` states what we actually know. The contract is a
2849
+ // declaration about how we build the request, so it is always knowable.
2850
+ retrievalStatus: "unknown",
2851
+ retrievalContract: "native-auto-v1"
2788
2852
  };
2789
2853
  },
2790
2854
  normalizeResult(raw) {
@@ -2801,7 +2865,8 @@ var perplexityAdapter = {
2801
2865
  answerText: normalized.answerText,
2802
2866
  citedDomains: normalized.citedDomains,
2803
2867
  groundingSources: normalized.groundingSources,
2804
- searchQueries: normalized.searchQueries
2868
+ searchQueries: normalized.searchQueries,
2869
+ retrievalStatus: "unknown"
2805
2870
  };
2806
2871
  },
2807
2872
  async generateText(prompt, config) {
@@ -3620,6 +3685,11 @@ var JobRunner = class {
3620
3685
  sourceCount: citedUrlCapture.sourceCount,
3621
3686
  resolvedCount: citedUrlCapture.resolvedCount,
3622
3687
  captureVersion: citedUrlCapture.captureVersion,
3688
+ // Retrieval is the adapter's own observation, never inferred
3689
+ // from citation counts. Both branches record it so no snapshot
3690
+ // can be written unmarked.
3691
+ retrievalStatus: normalized.retrievalStatus,
3692
+ retrievalContract: raw.retrievalContract,
3623
3693
  competitorOverlap: overlap,
3624
3694
  recommendedCompetitors: extractedCompetitors,
3625
3695
  location: runLocation?.label ?? null,
@@ -3651,6 +3721,11 @@ var JobRunner = class {
3651
3721
  sourceCount: citedUrlCapture.sourceCount,
3652
3722
  resolvedCount: citedUrlCapture.resolvedCount,
3653
3723
  captureVersion: citedUrlCapture.captureVersion,
3724
+ // Retrieval is the adapter's own observation, never inferred
3725
+ // from citation counts. Both branches record it so no snapshot
3726
+ // can be written unmarked.
3727
+ retrievalStatus: normalized.retrievalStatus,
3728
+ retrievalContract: raw.retrievalContract,
3654
3729
  competitorOverlap: overlap,
3655
3730
  recommendedCompetitors: extractedCompetitors,
3656
3731
  location: runLocation?.label ?? null,
@@ -6087,155 +6162,10 @@ function computeSummary2(rows) {
6087
6162
  return computeBacklinkSummaryMetrics(rows);
6088
6163
  }
6089
6164
 
6090
- // src/bing-backlinks-sync.ts
6165
+ // src/discovery-run.ts
6091
6166
  import crypto13 from "crypto";
6092
6167
  import { and as and9, eq as eq11 } from "drizzle-orm";
6093
- var log11 = createLogger("BingBacklinkSync");
6094
- var DEFAULT_MAX_PAGES = 200;
6095
- var LINK_COUNTS_MAX_PAGES = 50;
6096
- var URL_LINKS_MAX_PAGES = 20;
6097
- function bingReleaseId(date) {
6098
- return `bing-${date.toISOString().slice(0, 10)}`;
6099
- }
6100
- function aggregateInboundLinksByDomain(links, targetDomain) {
6101
- const target = hostOf(targetDomain);
6102
- const byHost = /* @__PURE__ */ new Map();
6103
- for (const link of links) {
6104
- const host = hostOf(link.Url);
6105
- if (!host) continue;
6106
- if (target && hostMatchesDomain(host, target)) continue;
6107
- let urls = byHost.get(host);
6108
- if (!urls) {
6109
- urls = /* @__PURE__ */ new Set();
6110
- byHost.set(host, urls);
6111
- }
6112
- urls.add(link.Url);
6113
- }
6114
- return [...byHost.entries()].map(([linkingDomain, urls]) => ({ linkingDomain, numHosts: urls.size })).sort((a, b) => b.numHosts - a.numHosts || a.linkingDomain.localeCompare(b.linkingDomain));
6115
- }
6116
- function computeBingSummary(rows) {
6117
- return computeBacklinkSummaryMetrics(rows);
6118
- }
6119
- function defaultDeps4() {
6120
- return { getLinkCounts, getUrlLinks, now: () => /* @__PURE__ */ new Date() };
6121
- }
6122
- async function executeBingBacklinkSync(db, runId, projectId, opts) {
6123
- const deps = { ...defaultDeps4(), ...opts.deps };
6124
- const startedAt = deps.now().toISOString();
6125
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq11(runs.id, runId)).run();
6126
- try {
6127
- const project = db.select().from(projects).where(eq11(projects.id, projectId)).get();
6128
- if (!project) throw new Error(`Project not found: ${projectId}`);
6129
- const conn = opts.resolveConnection(project.canonicalDomain);
6130
- if (!conn) throw new Error(`No Bing Webmaster connection for ${project.canonicalDomain}`);
6131
- const siteUrl = conn.siteUrl;
6132
- if (!siteUrl) {
6133
- throw new Error(`Bing connection for ${project.canonicalDomain} has no verified site selected`);
6134
- }
6135
- const pages = await deps.getLinkCounts(conn.apiKey, siteUrl, { maxPages: LINK_COUNTS_MAX_PAGES });
6136
- const maxPages = Math.max(1, opts.maxPages ?? DEFAULT_MAX_PAGES);
6137
- const targetPages = [...pages].sort((a, b) => b.Count - a.Count).slice(0, maxPages);
6138
- if (pages.length > targetPages.length) {
6139
- log11.info("bing-sync.pages-capped", {
6140
- runId,
6141
- projectId,
6142
- sitePagesFound: pages.length,
6143
- sitePagesPulled: targetPages.length
6144
- });
6145
- }
6146
- const allLinks = [];
6147
- let pageFailures = 0;
6148
- for (const page of targetPages) {
6149
- try {
6150
- const links = await deps.getUrlLinks(conn.apiKey, siteUrl, page.Url, { maxPages: URL_LINKS_MAX_PAGES });
6151
- allLinks.push(...links);
6152
- } catch (err) {
6153
- pageFailures++;
6154
- log11.warn("bing-sync.page-failed", {
6155
- runId,
6156
- projectId,
6157
- page: page.Url,
6158
- error: err instanceof Error ? err.message : String(err)
6159
- });
6160
- }
6161
- }
6162
- if (targetPages.length > 0 && pageFailures === targetPages.length) {
6163
- throw new Error(`All ${targetPages.length} Bing inbound-link page fetch(es) failed`);
6164
- }
6165
- const rows = aggregateInboundLinksByDomain(allLinks, project.canonicalDomain);
6166
- const summary = computeBingSummary(rows);
6167
- const queriedAt = deps.now().toISOString();
6168
- const release = bingReleaseId(deps.now());
6169
- const targetDomain = project.canonicalDomain;
6170
- const source = BacklinkSources["bing-webmaster"];
6171
- const existing = db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and9(
6172
- eq11(backlinkSummaries.projectId, projectId),
6173
- eq11(backlinkSummaries.source, source),
6174
- eq11(backlinkSummaries.release, release)
6175
- )).get();
6176
- const preserveExisting = pageFailures > 0 && !!existing;
6177
- if (!preserveExisting) {
6178
- db.transaction((tx) => {
6179
- tx.delete(backlinkDomains).where(and9(
6180
- eq11(backlinkDomains.projectId, projectId),
6181
- eq11(backlinkDomains.source, source),
6182
- eq11(backlinkDomains.release, release)
6183
- )).run();
6184
- if (rows.length > 0) {
6185
- tx.insert(backlinkDomains).values(rows.map((r) => ({
6186
- id: crypto13.randomUUID(),
6187
- projectId,
6188
- releaseSyncId: null,
6189
- source,
6190
- release,
6191
- targetDomain,
6192
- linkingDomain: r.linkingDomain,
6193
- numHosts: r.numHosts,
6194
- createdAt: queriedAt
6195
- }))).run();
6196
- }
6197
- tx.insert(backlinkSummaries).values({
6198
- id: crypto13.randomUUID(),
6199
- projectId,
6200
- releaseSyncId: null,
6201
- source,
6202
- release,
6203
- targetDomain,
6204
- totalLinkingDomains: summary.totalLinkingDomains,
6205
- totalHosts: summary.totalHosts,
6206
- top10HostsShare: summary.top10HostsShare,
6207
- queriedAt,
6208
- createdAt: queriedAt
6209
- }).onConflictDoUpdate({
6210
- target: [backlinkSummaries.projectId, backlinkSummaries.source, backlinkSummaries.release],
6211
- set: {
6212
- targetDomain,
6213
- totalLinkingDomains: summary.totalLinkingDomains,
6214
- totalHosts: summary.totalHosts,
6215
- top10HostsShare: summary.top10HostsShare,
6216
- queriedAt
6217
- }
6218
- }).run();
6219
- });
6220
- }
6221
- const finishedAt = deps.now().toISOString();
6222
- const status = pageFailures > 0 ? RunStatuses.partial : RunStatuses.completed;
6223
- const error = preserveExisting ? `Kept existing ${release} snapshot; ${pageFailures} of ${targetPages.length} inbound-link page fetches failed` : pageFailures > 0 ? `${pageFailures} of ${targetPages.length} inbound-link page fetches failed` : null;
6224
- db.update(runs).set({ status, error, finishedAt }).where(eq11(runs.id, runId)).run();
6225
- log11.info("bing-sync.completed", { runId, projectId, release, rows: rows.length, status, pageFailures, preserveExisting });
6226
- } catch (err) {
6227
- const errorMsg = err instanceof Error ? err.message : String(err);
6228
- const finishedAt = deps.now().toISOString();
6229
- db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt }).where(eq11(runs.id, runId)).run();
6230
- log11.error("bing-sync.failed", { runId, projectId, error: errorMsg });
6231
- throw err;
6232
- }
6233
- }
6234
-
6235
- // src/discovery-run.ts
6236
- import crypto14 from "crypto";
6237
- import { and as and10, eq as eq12 } from "drizzle-orm";
6238
- var log12 = createLogger("DiscoveryRun");
6168
+ var log11 = createLogger("DiscoveryRun");
6239
6169
  var DEFAULT_SEED_COUNT = 30;
6240
6170
  var EMBED_RETRY_MAX_RETRIES = 3;
6241
6171
  var EMBED_RETRY_MAX_DELAY_MS = 1e4;
@@ -6268,7 +6198,7 @@ async function embedWithRetry(fn, opts = {}) {
6268
6198
  maxDelayMs: EMBED_RETRY_MAX_DELAY_MS,
6269
6199
  sleep: opts.sleep,
6270
6200
  onRetry: opts.onRetry ?? (({ attempt, delayMs }) => {
6271
- log12.warn("discovery.embed.retry", { attempt, delayMs });
6201
+ log11.warn("discovery.embed.retry", { attempt, delayMs });
6272
6202
  })
6273
6203
  }
6274
6204
  );
@@ -6276,11 +6206,11 @@ async function embedWithRetry(fn, opts = {}) {
6276
6206
  var QUERIES_PER_INTENT_BUCKET = 6;
6277
6207
  async function executeDiscoveryRun(opts) {
6278
6208
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6279
- opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq12(runs.id, opts.runId)).run();
6209
+ opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq11(runs.id, opts.runId)).run();
6280
6210
  try {
6281
- const projectRow = opts.db.select().from(projects).where(eq12(projects.id, opts.projectId)).get();
6211
+ const projectRow = opts.db.select().from(projects).where(eq11(projects.id, opts.projectId)).get();
6282
6212
  if (!projectRow) throw new Error(`Project ${opts.projectId} not found`);
6283
- const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq12(competitors.projectId, opts.projectId)).all().map((r) => r.domain.toLowerCase());
6213
+ const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq11(competitors.projectId, opts.projectId)).all().map((r) => r.domain.toLowerCase());
6284
6214
  const canonicalDomains = effectiveDomains({
6285
6215
  canonicalDomain: projectRow.canonicalDomain,
6286
6216
  ownedDomains: projectRow.ownedDomains
@@ -6318,8 +6248,8 @@ async function executeDiscoveryRun(opts) {
6318
6248
  seedProvider: result.seedProvider,
6319
6249
  result
6320
6250
  });
6321
- opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq12(runs.id, opts.runId)).run();
6322
- log12.info("discovery.completed", {
6251
+ opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq11(runs.id, opts.runId)).run();
6252
+ log11.info("discovery.completed", {
6323
6253
  runId: opts.runId,
6324
6254
  sessionId: opts.sessionId,
6325
6255
  buckets: result.buckets,
@@ -6327,13 +6257,13 @@ async function executeDiscoveryRun(opts) {
6327
6257
  });
6328
6258
  } catch (err) {
6329
6259
  const errorMsg = err instanceof Error ? err.message : String(err);
6330
- log12.error("discovery.failed", { runId: opts.runId, sessionId: opts.sessionId, error: errorMsg });
6260
+ log11.error("discovery.failed", { runId: opts.runId, sessionId: opts.sessionId, error: errorMsg });
6331
6261
  markSessionFailed(opts.db, opts.sessionId, errorMsg);
6332
6262
  opts.db.update(runs).set({
6333
6263
  status: RunStatuses.failed,
6334
6264
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
6335
6265
  error: errorMsg
6336
- }).where(eq12(runs.id, opts.runId)).run();
6266
+ }).where(eq11(runs.id, opts.runId)).run();
6337
6267
  }
6338
6268
  }
6339
6269
  function buildDefaultDeps(registry) {
@@ -6582,13 +6512,13 @@ function writeDiscoveryInsight(db, input) {
6582
6512
  totalProbes
6583
6513
  });
6584
6514
  db.transaction((tx) => {
6585
- tx.update(insights).set({ dismissed: true }).where(and10(
6586
- eq12(insights.projectId, input.projectId),
6587
- eq12(insights.type, "discovery.basket-divergence"),
6588
- eq12(insights.dismissed, false)
6515
+ tx.update(insights).set({ dismissed: true }).where(and9(
6516
+ eq11(insights.projectId, input.projectId),
6517
+ eq11(insights.type, "discovery.basket-divergence"),
6518
+ eq11(insights.dismissed, false)
6589
6519
  )).run();
6590
6520
  tx.insert(insights).values({
6591
- id: crypto14.randomUUID(),
6521
+ id: crypto13.randomUUID(),
6592
6522
  projectId: input.projectId,
6593
6523
  runId: input.runId,
6594
6524
  type: "discovery.basket-divergence",
@@ -6624,10 +6554,10 @@ function buildDiscoveryInsightTitle(input) {
6624
6554
  }
6625
6555
 
6626
6556
  // src/execute-site-audit.ts
6627
- import crypto15 from "crypto";
6628
- import { eq as eq13 } from "drizzle-orm";
6557
+ import crypto14 from "crypto";
6558
+ import { eq as eq12 } from "drizzle-orm";
6629
6559
  import { runSitemapAudit } from "@ainyc/aeo-audit";
6630
- var log13 = createLogger("SiteAudit");
6560
+ var log12 = createLogger("SiteAudit");
6631
6561
  var SITE_AUDIT_DEFAULT_PAGE_LIMIT = 500;
6632
6562
  var SITE_AUDIT_MAX_PAGE_LIMIT = 2e3;
6633
6563
  function toHomepageUrl(canonicalDomain) {
@@ -6688,15 +6618,15 @@ function computeFactorAverages(pages) {
6688
6618
  }
6689
6619
  async function executeSiteAudit(db, runId, projectId, opts = {}) {
6690
6620
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6691
- db.update(runs).set({ status: "running", startedAt }).where(eq13(runs.id, runId)).run();
6621
+ db.update(runs).set({ status: "running", startedAt }).where(eq12(runs.id, runId)).run();
6692
6622
  try {
6693
- const project = db.select().from(projects).where(eq13(projects.id, projectId)).get();
6623
+ const project = db.select().from(projects).where(eq12(projects.id, projectId)).get();
6694
6624
  if (!project) {
6695
6625
  throw new Error(`Project not found: ${projectId}`);
6696
6626
  }
6697
6627
  const homepageUrl = toHomepageUrl(project.canonicalDomain);
6698
6628
  const limit = clampSiteAuditLimit(opts.limit);
6699
- log13.info("start", { runId, projectId, homepageUrl, sitemapUrl: opts.sitemapUrl ?? null, limit });
6629
+ log12.info("start", { runId, projectId, homepageUrl, sitemapUrl: opts.sitemapUrl ?? null, limit });
6700
6630
  await assertSiteAuditUrlAllowed(homepageUrl, "canonicalDomain");
6701
6631
  if (opts.sitemapUrl) await assertSiteAuditUrlAllowed(opts.sitemapUrl, "sitemapUrl");
6702
6632
  const report = await runSitemapAudit(homepageUrl, { sitemapUrl: opts.sitemapUrl, limit });
@@ -6704,7 +6634,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
6704
6634
  const pagesErrored = report.pages.filter((page) => page.status === "error").length;
6705
6635
  const auditable = report.pagesDiscovered - report.pagesSkipped;
6706
6636
  if (auditable > report.pagesAudited) {
6707
- log13.info("truncated", {
6637
+ log12.info("truncated", {
6708
6638
  runId,
6709
6639
  projectId,
6710
6640
  auditable,
@@ -6723,7 +6653,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
6723
6653
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
6724
6654
  db.transaction((tx) => {
6725
6655
  tx.insert(siteAuditSnapshots).values({
6726
- id: crypto15.randomUUID(),
6656
+ id: crypto14.randomUUID(),
6727
6657
  projectId,
6728
6658
  runId,
6729
6659
  sitemapUrl: report.sitemapUrl,
@@ -6752,7 +6682,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
6752
6682
  }).run();
6753
6683
  for (const page of report.pages) {
6754
6684
  tx.insert(siteAuditPages).values({
6755
- id: crypto15.randomUUID(),
6685
+ id: crypto14.randomUUID(),
6756
6686
  projectId,
6757
6687
  runId,
6758
6688
  url: page.url,
@@ -6763,9 +6693,9 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
6763
6693
  createdAt: finishedAt
6764
6694
  }).run();
6765
6695
  }
6766
- tx.update(runs).set({ status, finishedAt }).where(eq13(runs.id, runId)).run();
6696
+ tx.update(runs).set({ status, finishedAt }).where(eq12(runs.id, runId)).run();
6767
6697
  });
6768
- log13.info("completed", {
6698
+ log12.info("completed", {
6769
6699
  runId,
6770
6700
  projectId,
6771
6701
  status,
@@ -6775,14 +6705,14 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
6775
6705
  });
6776
6706
  } catch (err) {
6777
6707
  const errorMsg = err instanceof Error ? err.message : String(err);
6778
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq13(runs.id, runId)).run();
6779
- log13.error("failed", { runId, projectId, error: errorMsg });
6708
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq12(runs.id, runId)).run();
6709
+ log12.error("failed", { runId, projectId, error: errorMsg });
6780
6710
  throw err;
6781
6711
  }
6782
6712
  }
6783
6713
 
6784
6714
  // src/commands/backfill.ts
6785
- import { and as and11, eq as eq14, inArray as inArray4, isNull, sql as sql5 } from "drizzle-orm";
6715
+ import { and as and10, eq as eq13, inArray as inArray4, isNull, sql as sql5 } from "drizzle-orm";
6786
6716
  var SNAPSHOT_BATCH_SIZE = 500;
6787
6717
  async function backfillAnswerVisibilityCommand(opts) {
6788
6718
  const config = loadConfig();
@@ -6790,7 +6720,7 @@ async function backfillAnswerVisibilityCommand(opts) {
6790
6720
  migrate(db);
6791
6721
  const projectFilter = opts?.project?.trim();
6792
6722
  const isDryRun = opts?.dryRun === true;
6793
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq14(projects.name, projectFilter)).all() : db.select().from(projects).all();
6723
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
6794
6724
  let examined = 0;
6795
6725
  let updated = 0;
6796
6726
  let wouldUpdate = 0;
@@ -6798,10 +6728,10 @@ async function backfillAnswerVisibilityCommand(opts) {
6798
6728
  let reparsed = 0;
6799
6729
  let providerErrors = 0;
6800
6730
  if (scopedProjects.length > 0) {
6801
- const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and11(
6802
- eq14(runs.kind, RunKinds["answer-visibility"]),
6731
+ const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and10(
6732
+ eq13(runs.kind, RunKinds["answer-visibility"]),
6803
6733
  inArray4(runs.projectId, scopedProjects.map((project) => project.id))
6804
- )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq14(runs.kind, RunKinds["answer-visibility"])).all();
6734
+ )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq13(runs.kind, RunKinds["answer-visibility"])).all();
6805
6735
  const runIdsByProject = /* @__PURE__ */ new Map();
6806
6736
  for (const run of runRows) {
6807
6737
  const existing = runIdsByProject.get(run.projectId);
@@ -6809,7 +6739,7 @@ async function backfillAnswerVisibilityCommand(opts) {
6809
6739
  else runIdsByProject.set(run.projectId, [run.id]);
6810
6740
  }
6811
6741
  for (const project of scopedProjects) {
6812
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, project.id)).all().map((row) => row.domain);
6742
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, project.id)).all().map((row) => row.domain);
6813
6743
  const runIds = runIdsByProject.get(project.id) ?? [];
6814
6744
  if (runIds.length === 0) continue;
6815
6745
  const projectDomains = effectiveDomains({
@@ -6855,7 +6785,12 @@ async function backfillAnswerVisibilityCommand(opts) {
6855
6785
  answerText,
6856
6786
  citedDomains: reparsedResult.citedDomains,
6857
6787
  groundingSources: reparsedResult.groundingSources,
6858
- searchQueries: reparsedResult.searchQueries
6788
+ searchQueries: reparsedResult.searchQueries,
6789
+ // Backfill recomputes mention and citation state from stored
6790
+ // rows; it never observes retrieval and never writes it back
6791
+ // (nextPatch carries no retrieval field). `unknown` keeps this
6792
+ // reconstruction from asserting anything about a search.
6793
+ retrievalStatus: "unknown"
6859
6794
  };
6860
6795
  const nextCitationState = determineCitationState(normalized, projectDomains);
6861
6796
  const nextCitedDomains = reparsedResult.citedDomains;
@@ -6897,7 +6832,7 @@ async function backfillAnswerVisibilityCommand(opts) {
6897
6832
  } else {
6898
6833
  db.transaction((tx) => {
6899
6834
  for (const update of pendingUpdates) {
6900
- tx.update(querySnapshots).set(update.patch).where(eq14(querySnapshots.id, update.id)).run();
6835
+ tx.update(querySnapshots).set(update.patch).where(eq13(querySnapshots.id, update.id)).run();
6901
6836
  }
6902
6837
  });
6903
6838
  updated += pendingUpdates.length;
@@ -6946,13 +6881,13 @@ No DB writes performed. Re-run without --dry-run to apply.`);
6946
6881
  function backfillNormalizedPaths(db, opts) {
6947
6882
  const baseConditions = [];
6948
6883
  if (opts?.projectId) {
6949
- baseConditions.push(eq14(gaTrafficSnapshots.projectId, opts.projectId));
6884
+ baseConditions.push(eq13(gaTrafficSnapshots.projectId, opts.projectId));
6950
6885
  }
6951
6886
  const rows = db.select({
6952
6887
  id: gaTrafficSnapshots.id,
6953
6888
  landingPage: gaTrafficSnapshots.landingPage,
6954
6889
  landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
6955
- }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and11(...baseConditions) : void 0).all();
6890
+ }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and10(...baseConditions) : void 0).all();
6956
6891
  let updated = 0;
6957
6892
  let unchanged = 0;
6958
6893
  if (rows.length > 0) {
@@ -6967,7 +6902,7 @@ function backfillNormalizedPaths(db, opts) {
6967
6902
  unchanged++;
6968
6903
  continue;
6969
6904
  }
6970
- tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq14(gaTrafficSnapshots.id, row.id)).run();
6905
+ tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq13(gaTrafficSnapshots.id, row.id)).run();
6971
6906
  updated++;
6972
6907
  }
6973
6908
  });
@@ -6981,7 +6916,7 @@ async function backfillNormalizedPathsCommand(opts) {
6981
6916
  const projectFilter = opts?.project?.trim();
6982
6917
  let projectId;
6983
6918
  if (projectFilter) {
6984
- const project = db.select({ id: projects.id }).from(projects).where(eq14(projects.name, projectFilter)).get();
6919
+ const project = db.select({ id: projects.id }).from(projects).where(eq13(projects.name, projectFilter)).get();
6985
6920
  if (!project) {
6986
6921
  const result2 = {
6987
6922
  project: projectFilter,
@@ -7018,13 +6953,13 @@ async function backfillNormalizedPathsCommand(opts) {
7018
6953
  function backfillAiReferralPaths(db, opts) {
7019
6954
  const baseConditions = [];
7020
6955
  if (opts?.projectId) {
7021
- baseConditions.push(eq14(gaAiReferrals.projectId, opts.projectId));
6956
+ baseConditions.push(eq13(gaAiReferrals.projectId, opts.projectId));
7022
6957
  }
7023
6958
  const rows = db.select({
7024
6959
  id: gaAiReferrals.id,
7025
6960
  landingPage: gaAiReferrals.landingPage,
7026
6961
  landingPageNormalized: gaAiReferrals.landingPageNormalized
7027
- }).from(gaAiReferrals).where(baseConditions.length > 0 ? and11(...baseConditions) : void 0).all();
6962
+ }).from(gaAiReferrals).where(baseConditions.length > 0 ? and10(...baseConditions) : void 0).all();
7028
6963
  let updated = 0;
7029
6964
  let unchanged = 0;
7030
6965
  if (rows.length > 0) {
@@ -7039,7 +6974,7 @@ function backfillAiReferralPaths(db, opts) {
7039
6974
  unchanged++;
7040
6975
  continue;
7041
6976
  }
7042
- tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq14(gaAiReferrals.id, row.id)).run();
6977
+ tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq13(gaAiReferrals.id, row.id)).run();
7043
6978
  updated++;
7044
6979
  }
7045
6980
  });
@@ -7053,7 +6988,7 @@ async function backfillAiReferralPathsCommand(opts) {
7053
6988
  const projectFilter = opts?.project?.trim();
7054
6989
  let projectId;
7055
6990
  if (projectFilter) {
7056
- const project = db.select({ id: projects.id }).from(projects).where(eq14(projects.name, projectFilter)).get();
6991
+ const project = db.select({ id: projects.id }).from(projects).where(eq13(projects.name, projectFilter)).get();
7057
6992
  if (!project) {
7058
6993
  const result2 = {
7059
6994
  project: projectFilter,
@@ -7089,10 +7024,10 @@ async function backfillAiReferralPathsCommand(opts) {
7089
7024
  }
7090
7025
  function backfillProjectAnswerMentions(db, projectId, opts) {
7091
7026
  const isDryRun = opts?.dryRun === true;
7092
- const project = db.select().from(projects).where(eq14(projects.id, projectId)).get();
7027
+ const project = db.select().from(projects).where(eq13(projects.id, projectId)).get();
7093
7028
  if (!project) return { examined: 0, updated: 0, mentioned: 0 };
7094
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, projectId)).all().map((row) => row.domain);
7095
- const runRows = db.select({ id: runs.id }).from(runs).where(and11(eq14(runs.kind, RunKinds["answer-visibility"]), eq14(runs.projectId, projectId))).all();
7029
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, projectId)).all().map((row) => row.domain);
7030
+ const runRows = db.select({ id: runs.id }).from(runs).where(and10(eq13(runs.kind, RunKinds["answer-visibility"]), eq13(runs.projectId, projectId))).all();
7096
7031
  const runIds = runRows.map((r) => r.id);
7097
7032
  let examined = 0;
7098
7033
  let updated = 0;
@@ -7134,6 +7069,8 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
7134
7069
  answerText,
7135
7070
  citedDomains,
7136
7071
  groundingSources,
7072
+ // See the note above: retrieval is not observed or written here.
7073
+ retrievalStatus: "unknown",
7137
7074
  searchQueries: []
7138
7075
  };
7139
7076
  const nextCompetitorOverlap = computeCompetitorOverlap(normalized, competitorDomains);
@@ -7164,7 +7101,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
7164
7101
  } else {
7165
7102
  db.transaction((tx) => {
7166
7103
  for (const update of pendingUpdates) {
7167
- tx.update(querySnapshots).set(update.patch).where(eq14(querySnapshots.id, update.id)).run();
7104
+ tx.update(querySnapshots).set(update.patch).where(eq13(querySnapshots.id, update.id)).run();
7168
7105
  }
7169
7106
  });
7170
7107
  updated += pendingUpdates.length;
@@ -7179,7 +7116,7 @@ async function backfillAnswerMentionsCommand(opts) {
7179
7116
  migrate(db);
7180
7117
  const projectFilter = opts?.project?.trim();
7181
7118
  const isDryRun = opts?.dryRun === true;
7182
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq14(projects.name, projectFilter)).all() : db.select().from(projects).all();
7119
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
7183
7120
  let examined = 0;
7184
7121
  let updated = 0;
7185
7122
  let wouldUpdate = 0;
@@ -7239,7 +7176,7 @@ function readStoredGroundingSources(rawResponse) {
7239
7176
  return result;
7240
7177
  }
7241
7178
  async function backfillInsightsCommand(project, opts) {
7242
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-5RJHJLYA.js");
7179
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-SMYVN3KN.js");
7243
7180
  const config = loadConfig();
7244
7181
  const db = createClient(config.database);
7245
7182
  migrate(db);
@@ -7398,7 +7335,7 @@ async function backfillSnapshotAttributionCommand(opts) {
7398
7335
  const config = loadConfig();
7399
7336
  const db = createClient(config.database);
7400
7337
  migrate(db);
7401
- const project = db.select().from(projects).where(eq14(projects.name, opts.project)).get();
7338
+ const project = db.select().from(projects).where(eq13(projects.name, opts.project)).get();
7402
7339
  if (!project) {
7403
7340
  throw new Error(`Project "${opts.project}" not found`);
7404
7341
  }
@@ -7409,8 +7346,8 @@ async function backfillSnapshotAttributionCommand(opts) {
7409
7346
  process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
7410
7347
  `);
7411
7348
  }
7412
- const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and11(
7413
- eq14(auditLog.projectId, project.id),
7349
+ const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and10(
7350
+ eq13(auditLog.projectId, project.id),
7414
7351
  inArray4(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
7415
7352
  )).orderBy(auditLog.createdAt).all();
7416
7353
  const history = replayQueryAuditLog(events);
@@ -7418,8 +7355,8 @@ async function backfillSnapshotAttributionCommand(opts) {
7418
7355
  runId: runs.id,
7419
7356
  createdAt: runs.createdAt,
7420
7357
  location: runs.location
7421
- }).from(runs).innerJoin(querySnapshots, eq14(querySnapshots.runId, runs.id)).where(and11(
7422
- eq14(runs.projectId, project.id),
7358
+ }).from(runs).innerJoin(querySnapshots, eq13(querySnapshots.runId, runs.id)).where(and10(
7359
+ eq13(runs.projectId, project.id),
7423
7360
  isNull(querySnapshots.queryId),
7424
7361
  isNull(querySnapshots.queryText)
7425
7362
  )).groupBy(runs.id).orderBy(runs.createdAt).all();
@@ -7441,8 +7378,8 @@ async function backfillSnapshotAttributionCommand(opts) {
7441
7378
  provider: querySnapshots.provider,
7442
7379
  createdAt: querySnapshots.createdAt,
7443
7380
  answerText: querySnapshots.answerText
7444
- }).from(querySnapshots).where(and11(
7445
- eq14(querySnapshots.runId, run.runId),
7381
+ }).from(querySnapshots).where(and10(
7382
+ eq13(querySnapshots.runId, run.runId),
7446
7383
  isNull(querySnapshots.queryId),
7447
7384
  isNull(querySnapshots.queryText)
7448
7385
  )).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
@@ -7508,7 +7445,7 @@ async function backfillSnapshotAttributionCommand(opts) {
7508
7445
  if (!isDryRun && updates.length > 0) {
7509
7446
  db.transaction((tx) => {
7510
7447
  for (const u of updates) {
7511
- tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq14(querySnapshots.id, u.id)).run();
7448
+ tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq13(querySnapshots.id, u.id)).run();
7512
7449
  }
7513
7450
  });
7514
7451
  }
@@ -7582,7 +7519,7 @@ async function backfillTrafficClassificationCommand(opts) {
7582
7519
  const projectFilter = opts?.project?.trim();
7583
7520
  const isDryRun = opts?.dryRun === true;
7584
7521
  const isJson = isMachineFormat(opts?.format);
7585
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq14(projects.name, projectFilter)).all() : db.select().from(projects).all();
7522
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq13(projects.name, projectFilter)).all() : db.select().from(projects).all();
7586
7523
  if (scopedProjects.length === 0) {
7587
7524
  if (projectFilter && !isJson) {
7588
7525
  process.stderr.write(`No project named "${projectFilter}".
@@ -7607,8 +7544,8 @@ async function backfillTrafficClassificationCommand(opts) {
7607
7544
  dryRun: isDryRun,
7608
7545
  byBot: {}
7609
7546
  };
7610
- const unknownCountRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and11(
7611
- eq14(rawEventSamples.eventType, "unknown"),
7547
+ const unknownCountRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and10(
7548
+ eq13(rawEventSamples.eventType, "unknown"),
7612
7549
  inArray4(rawEventSamples.projectId, projectIds)
7613
7550
  )).get();
7614
7551
  result.unknownBefore = Number(unknownCountRow?.n ?? 0);
@@ -7620,8 +7557,8 @@ async function backfillTrafficClassificationCommand(opts) {
7620
7557
  userAgent: rawEventSamples.userAgent,
7621
7558
  pathNormalized: rawEventSamples.pathNormalized,
7622
7559
  status: rawEventSamples.status
7623
- }).from(rawEventSamples).where(and11(
7624
- eq14(rawEventSamples.eventType, "unknown"),
7560
+ }).from(rawEventSamples).where(and10(
7561
+ eq13(rawEventSamples.eventType, "unknown"),
7625
7562
  inArray4(rawEventSamples.projectId, projectIds)
7626
7563
  )).all();
7627
7564
  result.examined = unknownSamples.length;
@@ -7660,7 +7597,7 @@ async function backfillTrafficClassificationCommand(opts) {
7660
7597
  result.reclassified++;
7661
7598
  result.byBot[classified.botId] = (result.byBot[classified.botId] ?? 0) + 1;
7662
7599
  if (isDryRun) continue;
7663
- db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq14(rawEventSamples.id, snap.id)).run();
7600
+ db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq13(rawEventSamples.id, snap.id)).run();
7664
7601
  const tsHour = new Date(snap.ts);
7665
7602
  tsHour.setUTCMinutes(0, 0, 0);
7666
7603
  if (userFetch) {
@@ -7724,8 +7661,8 @@ async function backfillTrafficClassificationCommand(opts) {
7724
7661
  }
7725
7662
  }
7726
7663
  if (!isDryRun) {
7727
- const afterRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and11(
7728
- eq14(rawEventSamples.eventType, "unknown"),
7664
+ const afterRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and10(
7665
+ eq13(rawEventSamples.eventType, "unknown"),
7729
7666
  inArray4(rawEventSamples.projectId, projectIds)
7730
7667
  )).get();
7731
7668
  result.unknownAfter = Number(afterRow?.n ?? 0);
@@ -7760,7 +7697,7 @@ No DB writes performed. Re-run without --dry-run to apply.`);
7760
7697
  }
7761
7698
 
7762
7699
  // src/commands/skills.ts
7763
- import crypto16 from "crypto";
7700
+ import crypto15 from "crypto";
7764
7701
  import fs4 from "fs";
7765
7702
  import os4 from "os";
7766
7703
  import path5 from "path";
@@ -7815,7 +7752,7 @@ function walkRelative(dir, prefix = "") {
7815
7752
  return out.sort();
7816
7753
  }
7817
7754
  function sha256File(filePath) {
7818
- return crypto16.createHash("sha256").update(fs4.readFileSync(filePath)).digest("hex");
7755
+ return crypto15.createHash("sha256").update(fs4.readFileSync(filePath)).digest("hex");
7819
7756
  }
7820
7757
  function readSkillManifest(skillDir) {
7821
7758
  try {
@@ -8157,10 +8094,11 @@ var ProviderRegistry = class {
8157
8094
  };
8158
8095
 
8159
8096
  // src/scheduler.ts
8160
- import crypto17 from "crypto";
8097
+ import crypto16 from "crypto";
8161
8098
  import cron from "node-cron";
8162
- import { and as and12, eq as eq15, inArray as inArray5 } from "drizzle-orm";
8163
- var log14 = createLogger("Scheduler");
8099
+ import { and as and11, eq as eq14, inArray as inArray5, notExists, sql as sql6 } from "drizzle-orm";
8100
+ var log13 = createLogger("Scheduler");
8101
+ var DEFAULT_HEALTH_CRON = "0 */6 * * *";
8164
8102
  function taskKey(projectId, kind) {
8165
8103
  return `${projectId}::${kind}`;
8166
8104
  }
@@ -8172,18 +8110,60 @@ var Scheduler = class {
8172
8110
  this.db = db;
8173
8111
  this.callbacks = callbacks;
8174
8112
  }
8113
+ /**
8114
+ * Give every project a health schedule if it has never had one.
8115
+ *
8116
+ * Ships the schedule with the feature. The six checks that existed before
8117
+ * this had never run on a live instance because nothing scheduled them, and
8118
+ * a health check nobody enables is indistinguishable from no health check.
8119
+ *
8120
+ * Keyed on the row EXISTING, not on it being enabled, so an operator who
8121
+ * deliberately turns this off keeps it off across restarts. Seeded here
8122
+ * rather than in a migration because migrations must stay additive to remain
8123
+ * downgrade-safe, and inserting rows is a data mutation.
8124
+ *
8125
+ * Every 6h, not daily: the cadence has to be shorter than the time-to-damage
8126
+ * it guards. A Vercel source begins discarding traffic once it is 24h behind,
8127
+ * so a daily pass could only ever observe the loss after it started.
8128
+ */
8129
+ ensureHealthSchedules() {
8130
+ const projectsWithoutHealth = this.db.select({ id: projects.id }).from(projects).where(notExists(
8131
+ this.db.select({ one: sql6`1` }).from(schedules).where(and11(
8132
+ eq14(schedules.projectId, projects.id),
8133
+ eq14(schedules.kind, SchedulableRunKinds.doctor)
8134
+ ))
8135
+ )).all();
8136
+ if (projectsWithoutHealth.length === 0) return;
8137
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8138
+ for (const project of projectsWithoutHealth) {
8139
+ this.db.insert(schedules).values({
8140
+ id: crypto16.randomUUID(),
8141
+ projectId: project.id,
8142
+ kind: SchedulableRunKinds.doctor,
8143
+ cronExpr: DEFAULT_HEALTH_CRON,
8144
+ timezone: "UTC",
8145
+ enabled: true,
8146
+ providers: [],
8147
+ nextRunAt: nextRunFromCron(DEFAULT_HEALTH_CRON, "UTC"),
8148
+ createdAt: now,
8149
+ updatedAt: now
8150
+ }).run();
8151
+ }
8152
+ log13.info("health-schedule.seeded", { projectCount: projectsWithoutHealth.length, cron: DEFAULT_HEALTH_CRON });
8153
+ }
8175
8154
  /** Load all enabled schedules from DB and register cron jobs. */
8176
8155
  start() {
8177
- const allSchedules = this.db.select().from(schedules).where(eq15(schedules.enabled, true)).all();
8156
+ this.ensureHealthSchedules();
8157
+ const allSchedules = this.db.select().from(schedules).where(eq14(schedules.enabled, true)).all();
8178
8158
  for (const schedule of allSchedules) {
8179
8159
  const missedRunAt = schedule.nextRunAt;
8180
8160
  this.registerCronTask(schedule);
8181
8161
  if (missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
8182
- log14.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
8162
+ log13.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
8183
8163
  this.triggerRun(schedule.id, schedule.projectId, schedule.kind);
8184
8164
  }
8185
8165
  }
8186
- log14.info("started", { scheduleCount: allSchedules.length });
8166
+ log13.info("started", { scheduleCount: allSchedules.length });
8187
8167
  }
8188
8168
  /** Stop all cron tasks for graceful shutdown. */
8189
8169
  stop() {
@@ -8204,7 +8184,7 @@ var Scheduler = class {
8204
8184
  this.stopTask(key, existing, "Stopped");
8205
8185
  this.tasks.delete(key);
8206
8186
  }
8207
- const schedule = this.db.select().from(schedules).where(and12(eq15(schedules.projectId, projectId), eq15(schedules.kind, kind))).get();
8187
+ const schedule = this.db.select().from(schedules).where(and11(eq14(schedules.projectId, projectId), eq14(schedules.kind, kind))).get();
8208
8188
  if (schedule && schedule.enabled) {
8209
8189
  this.registerCronTask(schedule);
8210
8190
  }
@@ -8227,13 +8207,13 @@ var Scheduler = class {
8227
8207
  stopTask(key, task, verb) {
8228
8208
  void task.stop();
8229
8209
  void task.destroy();
8230
- log14.info(`task.${verb.toLowerCase()}`, { key });
8210
+ log13.info(`task.${verb.toLowerCase()}`, { key });
8231
8211
  }
8232
8212
  registerCronTask(schedule) {
8233
8213
  const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
8234
8214
  const kind = schedule.kind;
8235
8215
  if (!cron.validate(cronExpr)) {
8236
- log14.error("cron.invalid", { projectId, kind, cronExpr });
8216
+ log13.error("cron.invalid", { projectId, kind, cronExpr });
8237
8217
  return;
8238
8218
  }
8239
8219
  const task = cron.schedule(cronExpr, () => {
@@ -8245,51 +8225,51 @@ var Scheduler = class {
8245
8225
  this.db.update(schedules).set({
8246
8226
  nextRunAt: nextRunFromCron(cronExpr, timezone),
8247
8227
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
8248
- }).where(eq15(schedules.id, scheduleId)).run();
8228
+ }).where(eq14(schedules.id, scheduleId)).run();
8249
8229
  const label = schedule.preset ?? cronExpr;
8250
- log14.info("cron.registered", { projectId, kind, schedule: label, timezone });
8230
+ log13.info("cron.registered", { projectId, kind, schedule: label, timezone });
8251
8231
  }
8252
8232
  triggerRun(scheduleId, projectId, kind) {
8253
8233
  try {
8254
8234
  const now = (/* @__PURE__ */ new Date()).toISOString();
8255
- const currentSchedule = this.db.select().from(schedules).where(eq15(schedules.id, scheduleId)).get();
8235
+ const currentSchedule = this.db.select().from(schedules).where(eq14(schedules.id, scheduleId)).get();
8256
8236
  if (!currentSchedule || !currentSchedule.enabled) {
8257
- log14.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
8237
+ log13.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
8258
8238
  this.remove(projectId, kind);
8259
8239
  return;
8260
8240
  }
8261
8241
  const nextRunAt = nextRunFromCron(currentSchedule.cronExpr, currentSchedule.timezone);
8262
- const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
8242
+ const project = this.db.select().from(projects).where(eq14(projects.id, projectId)).get();
8263
8243
  if (!project) {
8264
- log14.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
8244
+ log13.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
8265
8245
  this.remove(projectId, kind);
8266
8246
  return;
8267
8247
  }
8268
8248
  if (kind === SchedulableRunKinds["traffic-sync"]) {
8269
8249
  const sourceId = currentSchedule.sourceId;
8270
8250
  if (!sourceId) {
8271
- log14.warn("traffic-sync.missing-source", { scheduleId, projectId });
8251
+ log13.warn("traffic-sync.missing-source", { scheduleId, projectId });
8272
8252
  return;
8273
8253
  }
8274
8254
  if (!this.callbacks.onTrafficSyncRequested) {
8275
- log14.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
8255
+ log13.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
8276
8256
  return;
8277
8257
  }
8278
8258
  this.db.update(schedules).set({
8279
8259
  lastRunAt: now,
8280
8260
  nextRunAt,
8281
8261
  updatedAt: now
8282
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8283
- log14.info("traffic-sync.triggered", { projectName: project.name, sourceId });
8262
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8263
+ log13.info("traffic-sync.triggered", { projectName: project.name, sourceId });
8284
8264
  this.callbacks.onTrafficSyncRequested(project.name, sourceId);
8285
8265
  return;
8286
8266
  }
8287
8267
  if (kind === SchedulableRunKinds["gbp-sync"]) {
8288
8268
  if (!this.callbacks.onGbpSyncRequested) {
8289
- log14.warn("gbp-sync.no-callback", { scheduleId, projectId, msg: "host did not register onGbpSyncRequested" });
8269
+ log13.warn("gbp-sync.no-callback", { scheduleId, projectId, msg: "host did not register onGbpSyncRequested" });
8290
8270
  return;
8291
8271
  }
8292
- const runId2 = crypto17.randomUUID();
8272
+ const runId2 = crypto16.randomUUID();
8293
8273
  this.db.insert(runs).values({
8294
8274
  id: runId2,
8295
8275
  projectId,
@@ -8302,27 +8282,27 @@ var Scheduler = class {
8302
8282
  lastRunAt: now,
8303
8283
  nextRunAt,
8304
8284
  updatedAt: now
8305
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8306
- log14.info("gbp-sync.triggered", { runId: runId2, projectName: project.name });
8285
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8286
+ log13.info("gbp-sync.triggered", { runId: runId2, projectName: project.name });
8307
8287
  this.callbacks.onGbpSyncRequested(runId2, projectId);
8308
8288
  return;
8309
8289
  }
8310
8290
  if (kind === SchedulableRunKinds["ads-sync"]) {
8311
8291
  if (!this.callbacks.onAdsSyncRequested) {
8312
- log14.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
8292
+ log13.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
8313
8293
  return;
8314
8294
  }
8315
- const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and12(
8316
- eq15(runs.projectId, projectId),
8317
- eq15(runs.kind, RunKinds["ads-sync"]),
8295
+ const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and11(
8296
+ eq14(runs.projectId, projectId),
8297
+ eq14(runs.kind, RunKinds["ads-sync"]),
8318
8298
  inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
8319
8299
  )).get();
8320
8300
  if (activeAdsRun) {
8321
- log14.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
8322
- this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq15(schedules.id, currentSchedule.id)).run();
8301
+ log13.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
8302
+ this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq14(schedules.id, currentSchedule.id)).run();
8323
8303
  return;
8324
8304
  }
8325
- const runId2 = crypto17.randomUUID();
8305
+ const runId2 = crypto16.randomUUID();
8326
8306
  this.db.insert(runs).values({
8327
8307
  id: runId2,
8328
8308
  projectId,
@@ -8335,55 +8315,69 @@ var Scheduler = class {
8335
8315
  lastRunAt: now,
8336
8316
  nextRunAt,
8337
8317
  updatedAt: now
8338
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8339
- log14.info("ads-sync.triggered", { runId: runId2, projectName: project.name });
8318
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8319
+ log13.info("ads-sync.triggered", { runId: runId2, projectName: project.name });
8340
8320
  this.callbacks.onAdsSyncRequested(runId2, projectId);
8341
8321
  return;
8342
8322
  }
8343
8323
  if (kind === SchedulableRunKinds["data-refresh"]) {
8344
8324
  if (!this.callbacks.onDataRefreshRequested) {
8345
- log14.warn("data-refresh.no-callback", { scheduleId, projectId, msg: "host did not register onDataRefreshRequested" });
8325
+ log13.warn("data-refresh.no-callback", { scheduleId, projectId, msg: "host did not register onDataRefreshRequested" });
8346
8326
  return;
8347
8327
  }
8348
8328
  this.db.update(schedules).set({
8349
8329
  lastRunAt: now,
8350
8330
  nextRunAt,
8351
8331
  updatedAt: now
8352
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8353
- log14.info("data-refresh.triggered", { projectName: project.name });
8332
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8333
+ log13.info("data-refresh.triggered", { projectName: project.name });
8354
8334
  this.callbacks.onDataRefreshRequested(project.name);
8355
8335
  return;
8356
8336
  }
8337
+ if (kind === SchedulableRunKinds.doctor) {
8338
+ if (!this.callbacks.onDoctorRequested) {
8339
+ log13.warn("doctor.no-callback", { scheduleId, projectId, msg: "host did not register onDoctorRequested" });
8340
+ return;
8341
+ }
8342
+ this.db.update(schedules).set({
8343
+ lastRunAt: now,
8344
+ nextRunAt,
8345
+ updatedAt: now
8346
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8347
+ log13.info("doctor.triggered", { projectName: project.name });
8348
+ this.callbacks.onDoctorRequested(project.name);
8349
+ return;
8350
+ }
8357
8351
  if (kind === SchedulableRunKinds["backlinks-sync"]) {
8358
8352
  if (!this.callbacks.onBacklinksSyncRequested) {
8359
- log14.warn("backlinks-sync.no-callback", { scheduleId, projectId, msg: "host did not register onBacklinksSyncRequested" });
8353
+ log13.warn("backlinks-sync.no-callback", { scheduleId, projectId, msg: "host did not register onBacklinksSyncRequested" });
8360
8354
  return;
8361
8355
  }
8362
8356
  this.db.update(schedules).set({
8363
8357
  lastRunAt: now,
8364
8358
  nextRunAt,
8365
8359
  updatedAt: now
8366
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8367
- log14.info("backlinks-sync.triggered", { projectName: project.name });
8360
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8361
+ log13.info("backlinks-sync.triggered", { projectName: project.name });
8368
8362
  this.callbacks.onBacklinksSyncRequested(project.name);
8369
8363
  return;
8370
8364
  }
8371
8365
  if (kind === SchedulableRunKinds["site-audit"]) {
8372
8366
  if (!this.callbacks.onSiteAuditRequested) {
8373
- log14.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
8367
+ log13.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
8374
8368
  return;
8375
8369
  }
8376
- const active = this.db.select({ id: runs.id }).from(runs).where(and12(
8377
- eq15(runs.projectId, projectId),
8378
- eq15(runs.kind, RunKinds["site-audit"]),
8370
+ const active = this.db.select({ id: runs.id }).from(runs).where(and11(
8371
+ eq14(runs.projectId, projectId),
8372
+ eq14(runs.kind, RunKinds["site-audit"]),
8379
8373
  inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
8380
8374
  )).get();
8381
8375
  if (active) {
8382
- log14.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
8383
- this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq15(schedules.id, currentSchedule.id)).run();
8376
+ log13.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
8377
+ this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq14(schedules.id, currentSchedule.id)).run();
8384
8378
  return;
8385
8379
  }
8386
- const runId2 = crypto17.randomUUID();
8380
+ const runId2 = crypto16.randomUUID();
8387
8381
  this.db.insert(runs).values({
8388
8382
  id: runId2,
8389
8383
  projectId,
@@ -8396,8 +8390,8 @@ var Scheduler = class {
8396
8390
  lastRunAt: now,
8397
8391
  nextRunAt,
8398
8392
  updatedAt: now
8399
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8400
- log14.info("site-audit.triggered", { runId: runId2, projectName: project.name });
8393
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8394
+ log13.info("site-audit.triggered", { runId: runId2, projectName: project.name });
8401
8395
  this.callbacks.onSiteAuditRequested(runId2, projectId);
8402
8396
  return;
8403
8397
  }
@@ -8406,7 +8400,7 @@ var Scheduler = class {
8406
8400
  if (project.defaultLocation) {
8407
8401
  const loc = projectLocations.find((l) => l.label === project.defaultLocation);
8408
8402
  if (!loc) {
8409
- log14.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
8403
+ log13.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
8410
8404
  return;
8411
8405
  }
8412
8406
  resolvedLocation = loc;
@@ -8420,11 +8414,11 @@ var Scheduler = class {
8420
8414
  location: locationLabel
8421
8415
  });
8422
8416
  if (queueResult.conflict) {
8423
- log14.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
8417
+ log13.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
8424
8418
  this.db.update(schedules).set({
8425
8419
  nextRunAt,
8426
8420
  updatedAt: now
8427
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8421
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8428
8422
  return;
8429
8423
  }
8430
8424
  const runId = queueResult.runId;
@@ -8432,19 +8426,19 @@ var Scheduler = class {
8432
8426
  lastRunAt: now,
8433
8427
  nextRunAt,
8434
8428
  updatedAt: now
8435
- }).where(eq15(schedules.id, currentSchedule.id)).run();
8429
+ }).where(eq14(schedules.id, currentSchedule.id)).run();
8436
8430
  const scheduleProviders = currentSchedule.providers;
8437
8431
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
8438
- log14.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
8432
+ log13.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
8439
8433
  this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
8440
8434
  } catch (err) {
8441
- log14.error("trigger.error", { scheduleId, projectId, kind, error: err instanceof Error ? err.message : String(err) });
8435
+ log13.error("trigger.error", { scheduleId, projectId, kind, error: err instanceof Error ? err.message : String(err) });
8442
8436
  }
8443
8437
  }
8444
8438
  };
8445
8439
 
8446
8440
  // src/data-refresh.ts
8447
- var log15 = createLogger("DataRefresh");
8441
+ var log14 = createLogger("DataRefresh");
8448
8442
  async function refreshAllIntegrations(client, projectName) {
8449
8443
  const integrations = [
8450
8444
  { name: "gsc", run: () => client.gscSync(projectName, {}) },
@@ -8457,19 +8451,19 @@ async function refreshAllIntegrations(client, projectName) {
8457
8451
  results.forEach((result, idx) => {
8458
8452
  const integration = integrations[idx].name;
8459
8453
  if (result.status === "fulfilled") {
8460
- log15.info("integration.refreshed", { projectName, integration });
8454
+ log14.info("integration.refreshed", { projectName, integration });
8461
8455
  } else {
8462
8456
  const reason = result.reason;
8463
8457
  const message = reason instanceof Error ? reason.message : String(reason);
8464
- log15.warn("integration.refresh-failed", { projectName, integration, error: message });
8458
+ log14.warn("integration.refresh-failed", { projectName, integration, error: message });
8465
8459
  }
8466
8460
  });
8467
8461
  }
8468
8462
 
8469
8463
  // src/notifier.ts
8470
- import { eq as eq16, desc as desc5, and as and13, inArray as inArray6, or } from "drizzle-orm";
8471
- import crypto18 from "crypto";
8472
- var log16 = createLogger("Notifier");
8464
+ import { eq as eq15, desc as desc5, and as and12, inArray as inArray6, or } from "drizzle-orm";
8465
+ import crypto17 from "crypto";
8466
+ var log15 = createLogger("Notifier");
8473
8467
  var Notifier = class {
8474
8468
  db;
8475
8469
  serverUrl;
@@ -8479,26 +8473,26 @@ var Notifier = class {
8479
8473
  }
8480
8474
  /** Called after a run completes (success, partial, or failed). */
8481
8475
  async onRunCompleted(runId, projectId) {
8482
- log16.info("run.completed", { runId, projectId });
8483
- const notifs = this.db.select().from(notifications).where(eq16(notifications.projectId, projectId)).all().filter((n) => n.enabled);
8476
+ log15.info("run.completed", { runId, projectId });
8477
+ const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
8484
8478
  if (notifs.length === 0) {
8485
- log16.info("notifications.none-enabled", { projectId });
8479
+ log15.info("notifications.none-enabled", { projectId });
8486
8480
  return;
8487
8481
  }
8488
- log16.info("notifications.found", { projectId, count: notifs.length });
8489
- const run = this.db.select().from(runs).where(eq16(runs.id, runId)).get();
8482
+ log15.info("notifications.found", { projectId, count: notifs.length });
8483
+ const run = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
8490
8484
  if (!run) {
8491
- log16.error("run.not-found", { runId, msg: "skipping notification dispatch" });
8485
+ log15.error("run.not-found", { runId, msg: "skipping notification dispatch" });
8492
8486
  return;
8493
8487
  }
8494
- const project = this.db.select().from(projects).where(eq16(projects.id, projectId)).get();
8488
+ const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
8495
8489
  if (!project) {
8496
- log16.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
8490
+ log15.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
8497
8491
  return;
8498
8492
  }
8499
8493
  const transitions = this.computeTransitions(runId, projectId);
8500
8494
  const events = [];
8501
- log16.info("run.status", { runId: run.id, status: run.status, projectId });
8495
+ log15.info("run.status", { runId: run.id, status: run.status, projectId });
8502
8496
  if (run.status === "completed" || run.status === "partial") {
8503
8497
  events.push("run.completed");
8504
8498
  }
@@ -8514,7 +8508,7 @@ var Notifier = class {
8514
8508
  if (!config.url) continue;
8515
8509
  const subscribedEvents = config.events;
8516
8510
  const matchingEvents = events.filter((e) => subscribedEvents.includes(e));
8517
- log16.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
8511
+ log15.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
8518
8512
  if (matchingEvents.length === 0) continue;
8519
8513
  for (const event of matchingEvents) {
8520
8514
  const relevantTransitions = event === "citation.lost" ? lostTransitions : event === "citation.gained" ? gainedTransitions : transitions;
@@ -8530,6 +8524,101 @@ var Notifier = class {
8530
8524
  }
8531
8525
  }
8532
8526
  }
8527
+ /**
8528
+ * Record the outcome of a scheduled health pass and notify on transitions.
8529
+ *
8530
+ * Edge-triggered on `(status, code)`. A pass that finds the same problem as
8531
+ * last time is stored but not re-sent: an operator who receives the same
8532
+ * warning every day stops reading the channel, which is how a real
8533
+ * degradation goes unnoticed. A pass whose worst code CHANGES does notify,
8534
+ * even at the same severity, because a different cause is different news.
8535
+ *
8536
+ * Returns the event emitted, or null when nothing changed — the return value
8537
+ * is what tests assert against, so "stayed quiet" is observable rather than
8538
+ * inferred from the absence of a webhook.
8539
+ */
8540
+ async onHealthChecked(projectId, report) {
8541
+ const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
8542
+ if (!project) {
8543
+ log15.error("project.not-found", { projectId, msg: "skipping health notification" });
8544
+ return null;
8545
+ }
8546
+ const rank = (status2) => status2 === "fail" ? 2 : status2 === "warn" ? 1 : 0;
8547
+ const categoryRank = (category) => {
8548
+ switch (category) {
8549
+ case "database":
8550
+ return 6;
8551
+ case "integrations":
8552
+ return 5;
8553
+ case "providers":
8554
+ return 4;
8555
+ case "auth":
8556
+ return 3;
8557
+ case "schedules":
8558
+ return 2;
8559
+ case "config":
8560
+ return 1;
8561
+ default:
8562
+ return 0;
8563
+ }
8564
+ };
8565
+ const graded = report.checks.filter((c) => c.status === "fail" || c.status === "warn" || c.status === "ok");
8566
+ const failing = graded.filter((c) => c.status !== "ok").sort((a, b) => rank(b.status) - rank(a.status) || categoryRank(b.category) - categoryRank(a.category) || a.id.localeCompare(b.id));
8567
+ const worst = failing.at(0) ?? null;
8568
+ const noSignal = graded.length === 0;
8569
+ const status = noSignal ? "warn" : worst ? worst.status : "ok";
8570
+ const code = noSignal ? "health.no-signal" : worst?.code ?? "health.ok";
8571
+ const summary = noSignal ? `No health check produced a result (${report.checks.length} skipped) \u2014 health is unknown, not confirmed.` : worst?.summary ?? `All ${graded.length} health check(s) passing.`;
8572
+ const previous = this.db.select().from(doctorHealthState).where(eq15(doctorHealthState.projectId, projectId)).get();
8573
+ const previousStatus = previous?.status ?? null;
8574
+ let event = null;
8575
+ if (previous === void 0) {
8576
+ if (status !== "ok") event = "health.degraded";
8577
+ } else if (status === "ok" && previousStatus !== "ok") {
8578
+ event = "health.recovered";
8579
+ } else if (status !== "ok" && (previousStatus === "ok" || previous.code !== code)) {
8580
+ event = "health.degraded";
8581
+ }
8582
+ const now = report.checkedAt;
8583
+ const observation = { projectId, status, code, summary, checkedAt: now };
8584
+ if (previous === void 0) {
8585
+ this.db.insert(doctorHealthState).values({ ...observation, notifiedAt: null }).run();
8586
+ } else {
8587
+ this.db.update(doctorHealthState).set(observation).where(eq15(doctorHealthState.projectId, projectId)).run();
8588
+ }
8589
+ if (!event) {
8590
+ log15.info("health.unchanged", { projectId, status, code });
8591
+ return null;
8592
+ }
8593
+ const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
8594
+ const payload = {
8595
+ source: "canonry",
8596
+ event,
8597
+ project: { name: project.name, canonicalDomain: project.canonicalDomain },
8598
+ health: {
8599
+ status,
8600
+ code,
8601
+ summary,
8602
+ remediation: worst?.remediation ?? null,
8603
+ checkedAt: now,
8604
+ previousStatus,
8605
+ failing: failing.map((c) => ({ id: c.id, status: c.status, code: c.code, summary: c.summary }))
8606
+ },
8607
+ dashboardUrl: `${this.serverUrl}/projects/${project.name}`
8608
+ };
8609
+ let delivered = 0;
8610
+ for (const notif of notifs) {
8611
+ const config = notif.config;
8612
+ if (!config.url) continue;
8613
+ await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null);
8614
+ delivered += 1;
8615
+ }
8616
+ if (delivered > 0) {
8617
+ this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq15(doctorHealthState.projectId, projectId)).run();
8618
+ }
8619
+ log15.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
8620
+ return event;
8621
+ }
8533
8622
  /** Dispatch insight webhooks for critical/high severity insights after a run. */
8534
8623
  async dispatchInsightWebhooks(runId, projectId, result) {
8535
8624
  const insightEvents = [];
@@ -8538,11 +8627,11 @@ var Notifier = class {
8538
8627
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
8539
8628
  if (highInsights.length > 0) insightEvents.push("insight.high");
8540
8629
  if (insightEvents.length === 0) return;
8541
- const notifs = this.db.select().from(notifications).where(eq16(notifications.projectId, projectId)).all().filter((n) => n.enabled);
8630
+ const notifs = this.db.select().from(notifications).where(eq15(notifications.projectId, projectId)).all().filter((n) => n.enabled);
8542
8631
  if (notifs.length === 0) return;
8543
- const run = this.db.select().from(runs).where(eq16(runs.id, runId)).get();
8632
+ const run = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
8544
8633
  if (!run) return;
8545
- const project = this.db.select().from(projects).where(eq16(projects.id, projectId)).get();
8634
+ const project = this.db.select().from(projects).where(eq15(projects.id, projectId)).get();
8546
8635
  if (!project) return;
8547
8636
  for (const notif of notifs) {
8548
8637
  const config = notif.config;
@@ -8572,12 +8661,12 @@ var Notifier = class {
8572
8661
  }
8573
8662
  }
8574
8663
  computeTransitions(runId, projectId) {
8575
- const thisRun = this.db.select().from(runs).where(eq16(runs.id, runId)).get();
8664
+ const thisRun = this.db.select().from(runs).where(eq15(runs.id, runId)).get();
8576
8665
  if (!thisRun) return [];
8577
- const groupSiblings = this.db.select().from(runs).where(and13(
8578
- eq16(runs.projectId, projectId),
8579
- eq16(runs.kind, thisRun.kind),
8580
- eq16(runs.createdAt, thisRun.createdAt)
8666
+ const groupSiblings = this.db.select().from(runs).where(and12(
8667
+ eq15(runs.projectId, projectId),
8668
+ eq15(runs.kind, thisRun.kind),
8669
+ eq15(runs.createdAt, thisRun.createdAt)
8581
8670
  )).all();
8582
8671
  const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
8583
8672
  if (stillPending) return [];
@@ -8593,17 +8682,17 @@ var Notifier = class {
8593
8682
  return candidate.id > best.id ? candidate : best;
8594
8683
  });
8595
8684
  if (winner.id !== runId) return [];
8596
- const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq16(projects.id, projectId)).get();
8685
+ const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq15(projects.id, projectId)).get();
8597
8686
  const locationCount = Math.max(
8598
8687
  1,
8599
8688
  (projectLocations?.locations ?? []).length
8600
8689
  );
8601
8690
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
8602
8691
  const recentRuns = this.db.select().from(runs).where(
8603
- and13(
8604
- eq16(runs.projectId, projectId),
8605
- eq16(runs.kind, thisRun.kind),
8606
- or(eq16(runs.status, "completed"), eq16(runs.status, "partial"))
8692
+ and12(
8693
+ eq15(runs.projectId, projectId),
8694
+ eq15(runs.kind, thisRun.kind),
8695
+ or(eq15(runs.status, "completed"), eq15(runs.status, "partial"))
8607
8696
  )
8608
8697
  ).orderBy(desc5(runs.createdAt), desc5(runs.id)).limit(RECENT_FETCH_LIMIT).all();
8609
8698
  const groups = groupRunsByCreatedAt(recentRuns);
@@ -8620,7 +8709,7 @@ var Notifier = class {
8620
8709
  provider: querySnapshots.provider,
8621
8710
  location: querySnapshots.location,
8622
8711
  citationState: querySnapshots.citationState
8623
- }).from(querySnapshots).leftJoin(queries, eq16(querySnapshots.queryId, queries.id)).where(inArray6(querySnapshots.runId, currentRunIds)).all();
8712
+ }).from(querySnapshots).leftJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(inArray6(querySnapshots.runId, currentRunIds)).all();
8624
8713
  const previousSnapshots = this.db.select({
8625
8714
  queryId: querySnapshots.queryId,
8626
8715
  provider: querySnapshots.provider,
@@ -8650,26 +8739,29 @@ var Notifier = class {
8650
8739
  return transitions;
8651
8740
  }
8652
8741
  async sendWebhook(url, payload, notificationId, projectId, webhookSecret) {
8742
+ const destination = resolveDestination(url);
8743
+ const body = destination.render ? destination.render(toAlertView(payload)) : payload;
8744
+ const signingSecret = destination.signed ? webhookSecret : null;
8653
8745
  const targetLabel = redactNotificationUrl(url).urlDisplay;
8654
8746
  const targetCheck = await resolveWebhookTarget(url);
8655
8747
  if (!targetCheck.ok) {
8656
- log16.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
8748
+ log15.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
8657
8749
  this.logDelivery(projectId, notificationId, payload.event, "failed", `SSRF: ${targetCheck.message}`);
8658
8750
  return;
8659
8751
  }
8660
- log16.info("webhook.send", { event: payload.event, url: targetLabel });
8752
+ log15.info("webhook.send", { event: payload.event, url: targetLabel });
8661
8753
  const maxRetries = 3;
8662
8754
  const delays = [1e3, 4e3, 16e3];
8663
8755
  for (let attempt = 0; attempt < maxRetries; attempt++) {
8664
8756
  try {
8665
- const response = await deliverWebhook(targetCheck.target, payload, webhookSecret);
8757
+ const response = await deliverWebhook(targetCheck.target, body, signingSecret);
8666
8758
  if (response.status >= 200 && response.status < 300) {
8667
- log16.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
8759
+ log15.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
8668
8760
  this.logDelivery(projectId, notificationId, payload.event, "sent", null);
8669
8761
  return;
8670
8762
  }
8671
8763
  const errorDetail = response.error ?? `HTTP ${response.status}`;
8672
- log16.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
8764
+ log15.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
8673
8765
  if (attempt === maxRetries - 1) {
8674
8766
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
8675
8767
  }
@@ -8677,7 +8769,7 @@ var Notifier = class {
8677
8769
  const errorDetail = err instanceof Error ? err.message : String(err);
8678
8770
  if (attempt === maxRetries - 1) {
8679
8771
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
8680
- log16.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
8772
+ log15.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
8681
8773
  }
8682
8774
  }
8683
8775
  if (attempt < maxRetries - 1) {
@@ -8687,7 +8779,7 @@ var Notifier = class {
8687
8779
  }
8688
8780
  logDelivery(projectId, notificationId, event, status, error) {
8689
8781
  this.db.insert(auditLog).values({
8690
- id: crypto18.randomUUID(),
8782
+ id: crypto17.randomUUID(),
8691
8783
  projectId,
8692
8784
  actor: "scheduler",
8693
8785
  action: `notification.${status}`,
@@ -8700,8 +8792,8 @@ var Notifier = class {
8700
8792
  };
8701
8793
 
8702
8794
  // src/run-coordinator.ts
8703
- import { eq as eq17 } from "drizzle-orm";
8704
- var log17 = createLogger("RunCoordinator");
8795
+ import { eq as eq16 } from "drizzle-orm";
8796
+ var log16 = createLogger("RunCoordinator");
8705
8797
  var RunCoordinator = class {
8706
8798
  constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
8707
8799
  this.db = db;
@@ -8716,10 +8808,10 @@ var RunCoordinator = class {
8716
8808
  onInsightsGenerated;
8717
8809
  onAeroEvent;
8718
8810
  async onRunCompleted(runId, projectId) {
8719
- const runRow = this.db.select().from(runs).where(eq17(runs.id, runId)).get();
8811
+ const runRow = this.db.select().from(runs).where(eq16(runs.id, runId)).get();
8720
8812
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
8721
8813
  if (runRow?.trigger === RunTriggers.probe) {
8722
- log17.info("probe.skip-side-effects", { runId, projectId, kind });
8814
+ log16.info("probe.skip-side-effects", { runId, projectId, kind });
8723
8815
  return;
8724
8816
  }
8725
8817
  let insightCount = 0;
@@ -8736,12 +8828,12 @@ var RunCoordinator = class {
8736
8828
  try {
8737
8829
  await this.onInsightsGenerated(runId, projectId, result);
8738
8830
  } catch (err) {
8739
- log17.error("insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8831
+ log16.error("insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8740
8832
  }
8741
8833
  }
8742
8834
  }
8743
8835
  } catch (err) {
8744
- log17.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8836
+ log16.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8745
8837
  }
8746
8838
  } else if (kind === RunKinds["gbp-sync"]) {
8747
8839
  try {
@@ -8754,17 +8846,17 @@ var RunCoordinator = class {
8754
8846
  try {
8755
8847
  await this.onInsightsGenerated(runId, projectId, analysisResultFromInsights(gbpInsights));
8756
8848
  } catch (err) {
8757
- log17.error("gbp-insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8849
+ log16.error("gbp-insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8758
8850
  }
8759
8851
  }
8760
8852
  } catch (err) {
8761
- log17.error("gbp-intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8853
+ log16.error("gbp-intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8762
8854
  }
8763
8855
  }
8764
8856
  try {
8765
8857
  await this.notifier.onRunCompleted(runId, projectId);
8766
8858
  } catch (err) {
8767
- log17.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8859
+ log16.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8768
8860
  }
8769
8861
  if (this.onAeroEvent) {
8770
8862
  try {
@@ -8777,7 +8869,7 @@ var RunCoordinator = class {
8777
8869
  };
8778
8870
  await this.onAeroEvent(ctx);
8779
8871
  } catch (err) {
8780
- log17.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8872
+ log16.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
8781
8873
  }
8782
8874
  }
8783
8875
  }
@@ -8792,7 +8884,7 @@ var RunCoordinator = class {
8792
8884
  * so the Aero queue is never starved of a follow-up.
8793
8885
  */
8794
8886
  buildDiscoveryAeroContext(runId, projectId, status, error) {
8795
- const session = this.db.select().from(discoverySessions).where(eq17(discoverySessions.runId, runId)).get();
8887
+ const session = this.db.select().from(discoverySessions).where(eq16(discoverySessions.runId, runId)).get();
8796
8888
  const competitorMap = session ? session.competitorMap : [];
8797
8889
  return {
8798
8890
  kind: RunKinds["aeo-discover-probe"],
@@ -8834,8 +8926,8 @@ function analysisResultFromInsights(insights2) {
8834
8926
  }
8835
8927
 
8836
8928
  // src/agent/session-registry.ts
8837
- import crypto22 from "crypto";
8838
- import { eq as eq19 } from "drizzle-orm";
8929
+ import crypto21 from "crypto";
8930
+ import { eq as eq18 } from "drizzle-orm";
8839
8931
 
8840
8932
  // src/agent/session.ts
8841
8933
  import fs7 from "fs";
@@ -9578,7 +9670,7 @@ function buildAeroStateTools(ctx, opts = {}) {
9578
9670
  }
9579
9671
 
9580
9672
  // src/agent/llm-usage.ts
9581
- import crypto19 from "crypto";
9673
+ import crypto18 from "crypto";
9582
9674
  var AeroLlmUsageFeatures = {
9583
9675
  turn: "aero.turn"
9584
9676
  };
@@ -9597,7 +9689,7 @@ function recordLlmUsageEvent(args) {
9597
9689
  const usage = args.message.usage;
9598
9690
  const now = (/* @__PURE__ */ new Date()).toISOString();
9599
9691
  args.db.insert(llmUsageEvents).values({
9600
- id: crypto19.randomUUID(),
9692
+ id: crypto18.randomUUID(),
9601
9693
  projectId: args.projectId,
9602
9694
  runId: args.runId,
9603
9695
  agentSessionId: args.agentSessionId,
@@ -9673,7 +9765,7 @@ function splitAeroAnthropicSystemCachePayload(payload, model) {
9673
9765
  }
9674
9766
 
9675
9767
  // src/agent/tool-usage.ts
9676
- import crypto20 from "crypto";
9768
+ import crypto19 from "crypto";
9677
9769
  var AeroToolEventStatuses = {
9678
9770
  success: "success",
9679
9771
  error: "error"
@@ -9736,7 +9828,7 @@ function createAeroToolUsageHooks(args) {
9736
9828
  function recordAgentToolEvent(args) {
9737
9829
  try {
9738
9830
  args.db.insert(agentToolEvents).values({
9739
- id: crypto20.randomUUID(),
9831
+ id: crypto19.randomUUID(),
9740
9832
  projectId: args.projectId,
9741
9833
  agentSessionId: args.agentSessionId,
9742
9834
  toolCallId: args.toolCallId,
@@ -9879,7 +9971,7 @@ function resolveSessionProviderAndModel(config, opts) {
9879
9971
  import { Type as Type4 } from "@sinclair/typebox";
9880
9972
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
9881
9973
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
9882
- var log18 = createLogger("RemoteMcp");
9974
+ var log17 = createLogger("RemoteMcp");
9883
9975
  var AERO_EXCLUDED_MCP_TOOLS2 = /* @__PURE__ */ new Set([]);
9884
9976
  var MAX_TOOL_RESULT_CHARS2 = 2e4;
9885
9977
  function truncate(text) {
@@ -9932,7 +10024,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
9932
10024
  try {
9933
10025
  client = await connect(server);
9934
10026
  } catch (err) {
9935
- log18.error("external-mcp.connect-failed", {
10027
+ log17.error("external-mcp.connect-failed", {
9936
10028
  label,
9937
10029
  error: err instanceof Error ? err.message : String(err)
9938
10030
  });
@@ -9942,7 +10034,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
9942
10034
  try {
9943
10035
  listed = await client.listTools();
9944
10036
  } catch (err) {
9945
- log18.error("external-mcp.list-failed", {
10037
+ log17.error("external-mcp.list-failed", {
9946
10038
  label,
9947
10039
  error: err instanceof Error ? err.message : String(err)
9948
10040
  });
@@ -9953,7 +10045,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
9953
10045
  if (!isReadOnly(tool)) continue;
9954
10046
  tools.push(adaptRemoteTool(client, tool));
9955
10047
  }
9956
- log18.info("external-mcp.loaded", {
10048
+ log17.info("external-mcp.loaded", {
9957
10049
  label,
9958
10050
  discovered: listed.tools.length,
9959
10051
  adopted: tools.length
@@ -9963,8 +10055,8 @@ async function loadExternalMcpTools(servers, opts = {}) {
9963
10055
  }
9964
10056
 
9965
10057
  // src/agent/memory-store.ts
9966
- import crypto21 from "crypto";
9967
- import { and as and14, desc as desc6, eq as eq18, like, sql as sql6 } from "drizzle-orm";
10058
+ import crypto20 from "crypto";
10059
+ import { and as and13, desc as desc6, eq as eq17, like, sql as sql7 } from "drizzle-orm";
9968
10060
  var COMPACTION_KEY_PREFIX = "compaction:";
9969
10061
  var COMPACTION_NOTES_PER_SESSION = 3;
9970
10062
  function rowToDto(row) {
@@ -9978,7 +10070,7 @@ function rowToDto(row) {
9978
10070
  };
9979
10071
  }
9980
10072
  function listMemoryEntries(db, projectId, opts = {}) {
9981
- const query = db.select().from(agentMemory).where(eq18(agentMemory.projectId, projectId)).orderBy(desc6(agentMemory.updatedAt));
10073
+ const query = db.select().from(agentMemory).where(eq17(agentMemory.projectId, projectId)).orderBy(desc6(agentMemory.updatedAt));
9982
10074
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
9983
10075
  return rows.map(rowToDto);
9984
10076
  }
@@ -9992,7 +10084,7 @@ function upsertMemoryEntry(db, args) {
9992
10084
  throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
9993
10085
  }
9994
10086
  const now = (/* @__PURE__ */ new Date()).toISOString();
9995
- const id = crypto21.randomUUID();
10087
+ const id = crypto20.randomUUID();
9996
10088
  db.insert(agentMemory).values({
9997
10089
  id,
9998
10090
  projectId: args.projectId,
@@ -10009,12 +10101,12 @@ function upsertMemoryEntry(db, args) {
10009
10101
  updatedAt: now
10010
10102
  }
10011
10103
  }).run();
10012
- const row = db.select().from(agentMemory).where(and14(eq18(agentMemory.projectId, args.projectId), eq18(agentMemory.key, args.key))).get();
10104
+ const row = db.select().from(agentMemory).where(and13(eq17(agentMemory.projectId, args.projectId), eq17(agentMemory.key, args.key))).get();
10013
10105
  if (!row) throw new Error("memory upsert produced no row");
10014
10106
  return rowToDto(row);
10015
10107
  }
10016
10108
  function deleteMemoryEntry(db, projectId, key) {
10017
- const result = db.delete(agentMemory).where(and14(eq18(agentMemory.projectId, projectId), eq18(agentMemory.key, key))).run();
10109
+ const result = db.delete(agentMemory).where(and13(eq17(agentMemory.projectId, projectId), eq17(agentMemory.key, key))).run();
10018
10110
  const changes = result.changes ?? 0;
10019
10111
  return changes > 0;
10020
10112
  }
@@ -10029,7 +10121,7 @@ function writeCompactionNote(db, args) {
10029
10121
  }
10030
10122
  const now = (/* @__PURE__ */ new Date()).toISOString();
10031
10123
  const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
10032
- const id = crypto21.randomUUID();
10124
+ const id = crypto20.randomUUID();
10033
10125
  let inserted;
10034
10126
  db.transaction((tx) => {
10035
10127
  tx.insert(agentMemory).values({
@@ -10043,16 +10135,16 @@ function writeCompactionNote(db, args) {
10043
10135
  }).run();
10044
10136
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
10045
10137
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
10046
- and14(
10047
- eq18(agentMemory.projectId, args.projectId),
10138
+ and13(
10139
+ eq17(agentMemory.projectId, args.projectId),
10048
10140
  like(agentMemory.key, `${sessionPrefix}%`)
10049
10141
  )
10050
10142
  ).orderBy(desc6(agentMemory.updatedAt)).all();
10051
10143
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
10052
10144
  if (stale.length > 0) {
10053
- tx.delete(agentMemory).where(sql6`${agentMemory.id} IN (${sql6.join(stale.map((s) => sql6`${s}`), sql6`, `)})`).run();
10145
+ tx.delete(agentMemory).where(sql7`${agentMemory.id} IN (${sql7.join(stale.map((s) => sql7`${s}`), sql7`, `)})`).run();
10054
10146
  }
10055
- const row = tx.select().from(agentMemory).where(and14(eq18(agentMemory.projectId, args.projectId), eq18(agentMemory.key, key))).get();
10147
+ const row = tx.select().from(agentMemory).where(and13(eq17(agentMemory.projectId, args.projectId), eq17(agentMemory.key, key))).get();
10056
10148
  if (row) inserted = rowToDto(row);
10057
10149
  });
10058
10150
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -10185,7 +10277,7 @@ async function compactMessages(args) {
10185
10277
  }
10186
10278
 
10187
10279
  // src/agent/session-registry.ts
10188
- var log19 = createLogger("SessionRegistry");
10280
+ var log18 = createLogger("SessionRegistry");
10189
10281
  var MAX_HYDRATE_NOTES = 20;
10190
10282
  var MAX_HYDRATE_BYTES = 32 * 1024;
10191
10283
  function escapeMemoryFragment(value) {
@@ -10229,7 +10321,7 @@ var SessionRegistry = class {
10229
10321
  if (!this.externalToolsPromise) {
10230
10322
  this.externalToolsPromise = loadExternalMcpTools(this.opts.config.externalMcpServers).catch(
10231
10323
  (err) => {
10232
- log19.error("external-mcp.load-failed", {
10324
+ log18.error("external-mcp.load-failed", {
10233
10325
  error: err instanceof Error ? err.message : String(err)
10234
10326
  });
10235
10327
  return [];
@@ -10279,7 +10371,7 @@ var SessionRegistry = class {
10279
10371
  modelProvider: effectiveProvider,
10280
10372
  modelId: effectiveModelId,
10281
10373
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
10282
- }).where(eq19(agentSessions.projectId, projectId)).run();
10374
+ }).where(eq18(agentSessions.projectId, projectId)).run();
10283
10375
  }
10284
10376
  const agent2 = createAeroSession({
10285
10377
  projectName,
@@ -10308,7 +10400,7 @@ var SessionRegistry = class {
10308
10400
  }
10309
10401
  const { provider, modelId } = resolveSessionProviderAndModel(this.opts.config, preferences);
10310
10402
  const systemPrompt = loadAeroSystemPrompt();
10311
- const sessionId = crypto22.randomUUID();
10403
+ const sessionId = crypto21.randomUUID();
10312
10404
  const agent = createAeroSession({
10313
10405
  projectName,
10314
10406
  client: this.opts.client,
@@ -10474,13 +10566,13 @@ ${lines.join("\n")}
10474
10566
  agent.state.messages = result.messages;
10475
10567
  agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
10476
10568
  this.save(projectName);
10477
- log19.info("compaction.completed", {
10569
+ log18.info("compaction.completed", {
10478
10570
  projectName,
10479
10571
  removedCount: result.removedCount,
10480
10572
  summaryBytes: Buffer.byteLength(result.summary, "utf8")
10481
10573
  });
10482
10574
  } catch (err) {
10483
- log19.error("compaction.failed", {
10575
+ log18.error("compaction.failed", {
10484
10576
  projectName,
10485
10577
  error: err instanceof Error ? err.message : String(err)
10486
10578
  });
@@ -10513,7 +10605,7 @@ ${lines.join("\n")}
10513
10605
  modelProvider: nextProvider,
10514
10606
  modelId: nextModelId,
10515
10607
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
10516
- }).where(eq19(agentSessions.projectId, projectId)).run();
10608
+ }).where(eq18(agentSessions.projectId, projectId)).run();
10517
10609
  }
10518
10610
  /** Persist a session's transcript back to the DB. Call after any run settles. */
10519
10611
  save(projectName) {
@@ -10581,7 +10673,7 @@ ${lines.join("\n")}
10581
10673
  await agent.prompt(msgs);
10582
10674
  this.save(projectName);
10583
10675
  } catch (err) {
10584
- log19.error("drain.failed", {
10676
+ log18.error("drain.failed", {
10585
10677
  projectName,
10586
10678
  error: err instanceof Error ? err.message : String(err)
10587
10679
  });
@@ -10677,17 +10769,17 @@ ${lines.join("\n")}
10677
10769
  return id;
10678
10770
  }
10679
10771
  tryResolveProjectId(projectName) {
10680
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq19(projects.name, projectName)).get();
10772
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq18(projects.name, projectName)).get();
10681
10773
  return row?.id;
10682
10774
  }
10683
10775
  loadRow(projectId) {
10684
- const row = this.opts.db.select().from(agentSessions).where(eq19(agentSessions.projectId, projectId)).get();
10776
+ const row = this.opts.db.select().from(agentSessions).where(eq18(agentSessions.projectId, projectId)).get();
10685
10777
  return row ?? null;
10686
10778
  }
10687
10779
  insertRow(params) {
10688
10780
  const now = (/* @__PURE__ */ new Date()).toISOString();
10689
10781
  this.opts.db.insert(agentSessions).values({
10690
- id: params.id ?? crypto22.randomUUID(),
10782
+ id: params.id ?? crypto21.randomUUID(),
10691
10783
  projectId: params.projectId,
10692
10784
  systemPrompt: params.systemPrompt,
10693
10785
  modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
@@ -10700,14 +10792,14 @@ ${lines.join("\n")}
10700
10792
  }
10701
10793
  updateRow(projectId, patch) {
10702
10794
  const now = (/* @__PURE__ */ new Date()).toISOString();
10703
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq19(agentSessions.projectId, projectId)).run();
10795
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq18(agentSessions.projectId, projectId)).run();
10704
10796
  }
10705
10797
  };
10706
10798
 
10707
10799
  // src/agent/agent-routes.ts
10708
- import { eq as eq20 } from "drizzle-orm";
10800
+ import { eq as eq19 } from "drizzle-orm";
10709
10801
  function resolveProject(db, name) {
10710
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq20(projects.name, name)).get();
10802
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq19(projects.name, name)).get();
10711
10803
  if (!row) throw notFound("project", name);
10712
10804
  return row;
10713
10805
  }
@@ -10716,7 +10808,7 @@ function registerAgentRoutes(app, opts) {
10716
10808
  "/projects/:name/agent/transcript",
10717
10809
  async (request) => {
10718
10810
  const project = resolveProject(opts.db, request.params.name);
10719
- const row = opts.db.select().from(agentSessions).where(eq20(agentSessions.projectId, project.id)).get();
10811
+ const row = opts.db.select().from(agentSessions).where(eq19(agentSessions.projectId, project.id)).get();
10720
10812
  if (!row) {
10721
10813
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
10722
10814
  }
@@ -10740,7 +10832,7 @@ function registerAgentRoutes(app, opts) {
10740
10832
  async (request) => {
10741
10833
  const project = resolveProject(opts.db, request.params.name);
10742
10834
  opts.sessionRegistry.reset(project.name);
10743
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(agentSessions.projectId, project.id)).run();
10835
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(agentSessions.projectId, project.id)).run();
10744
10836
  return { status: "reset" };
10745
10837
  }
10746
10838
  );
@@ -11174,7 +11266,7 @@ function formatAuditFactorScore(factor) {
11174
11266
  }
11175
11267
 
11176
11268
  // src/snapshot-service.ts
11177
- var log20 = createLogger("Snapshot");
11269
+ var log19 = createLogger("Snapshot");
11178
11270
  var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
11179
11271
  var SNAPSHOT_QUERY_COUNT = 6;
11180
11272
  var ProviderExecutionGate2 = class {
@@ -11320,7 +11412,7 @@ var SnapshotService = class {
11320
11412
  return mapAuditReport(report);
11321
11413
  } catch (err) {
11322
11414
  const message = err instanceof Error ? err.message : String(err);
11323
- log20.warn("audit.failed", { homepageUrl, error: message });
11415
+ log19.warn("audit.failed", { homepageUrl, error: message });
11324
11416
  return {
11325
11417
  url: homepageUrl,
11326
11418
  finalUrl: homepageUrl,
@@ -11349,7 +11441,7 @@ var SnapshotService = class {
11349
11441
  queries: parsedQueries
11350
11442
  };
11351
11443
  } catch (err) {
11352
- log20.warn("profile.generation-failed", {
11444
+ log19.warn("profile.generation-failed", {
11353
11445
  domain: ctx.domain,
11354
11446
  provider: ctx.analysisProvider.adapter.name,
11355
11447
  error: err instanceof Error ? err.message : String(err)
@@ -11491,7 +11583,7 @@ var SnapshotService = class {
11491
11583
  recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
11492
11584
  };
11493
11585
  } catch (err) {
11494
- log20.warn("response.analysis-failed", {
11586
+ log19.warn("response.analysis-failed", {
11495
11587
  provider: ctx.analysisProvider.adapter.name,
11496
11588
  error: err instanceof Error ? err.message : String(err)
11497
11589
  });
@@ -11746,20 +11838,20 @@ function clipText(value, length) {
11746
11838
  }
11747
11839
 
11748
11840
  // src/research-runner.ts
11749
- import { and as and15, eq as eq21, inArray as inArray7, sql as sql7 } from "drizzle-orm";
11841
+ import { and as and14, eq as eq20, inArray as inArray7, sql as sql8 } from "drizzle-orm";
11750
11842
  var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
11751
11843
  function finalResearchRunStatus(completed, failed) {
11752
11844
  if (failed === 0) return ResearchRunStatuses.completed;
11753
11845
  return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
11754
11846
  }
11755
11847
  async function executeResearchRun(db, registry, runId, projectId) {
11756
- const run = db.select().from(researchRuns).where(and15(eq21(researchRuns.id, runId), eq21(researchRuns.projectId, projectId))).get();
11848
+ const run = db.select().from(researchRuns).where(and14(eq20(researchRuns.id, runId), eq20(researchRuns.projectId, projectId))).get();
11757
11849
  if (!run || run.status !== ResearchRunStatuses.queued) return;
11758
- const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
11850
+ const project = db.select().from(projects).where(eq20(projects.id, projectId)).get();
11759
11851
  if (!project) return;
11760
11852
  const provider = registry.get(run.provider);
11761
11853
  const now = (/* @__PURE__ */ new Date()).toISOString();
11762
- const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and15(eq21(researchRuns.id, runId), eq21(researchRuns.status, ResearchRunStatuses.queued))).run();
11854
+ const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and14(eq20(researchRuns.id, runId), eq20(researchRuns.status, ResearchRunStatuses.queued))).run();
11763
11855
  if (claim.changes !== 1) return;
11764
11856
  let reserved = 0;
11765
11857
  let dispatched = 0;
@@ -11783,8 +11875,8 @@ async function executeResearchRun(db, registry, runId, projectId) {
11783
11875
  }
11784
11876
  reserved = run.totalQueries;
11785
11877
  reservation = { scope, period };
11786
- const rows = db.select().from(researchRunQueries).where(eq21(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
11787
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq21(competitors.projectId, projectId)).all().map((row) => row.domain);
11878
+ const rows = db.select().from(researchRunQueries).where(eq20(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
11879
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq20(competitors.projectId, projectId)).all().map((row) => row.domain);
11788
11880
  const domains = effectiveDomains(project);
11789
11881
  const brands = effectiveBrandNames(project);
11790
11882
  const config = { ...provider.config, model: run.resolvedModel };
@@ -11792,7 +11884,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
11792
11884
  await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
11793
11885
  try {
11794
11886
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11795
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and15(eq21(researchRunQueries.id, row.id), eq21(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
11887
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and14(eq20(researchRunQueries.id, row.id), eq20(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
11796
11888
  const raw = await gate.run(async () => {
11797
11889
  dispatched++;
11798
11890
  return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
@@ -11819,7 +11911,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
11819
11911
  citationState: determineCitationState(normalized, domains),
11820
11912
  rawResponse: raw.rawResponse,
11821
11913
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
11822
- }).where(and15(eq21(researchRunQueries.id, row.id), eq21(researchRunQueries.status, ResearchQueryStatuses.running))).run();
11914
+ }).where(and14(eq20(researchRunQueries.id, row.id), eq20(researchRunQueries.status, ResearchQueryStatuses.running))).run();
11823
11915
  if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
11824
11916
  } catch (error) {
11825
11917
  try {
@@ -11843,18 +11935,18 @@ async function executeResearchRun(db, registry, runId, projectId) {
11843
11935
  }
11844
11936
  }
11845
11937
  function incrementResearchProgress(db, runId, column) {
11846
- db.update(researchRuns).set({ [column]: sql7`${researchRuns[column]} + 1` }).where(eq21(researchRuns.id, runId)).run();
11938
+ db.update(researchRuns).set({ [column]: sql8`${researchRuns[column]} + 1` }).where(eq20(researchRuns.id, runId)).run();
11847
11939
  }
11848
11940
  function markResearchQueryFailed(db, runId, queryId, error) {
11849
11941
  const message = error instanceof Error ? error.message : String(error);
11850
- const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and15(eq21(researchRunQueries.id, queryId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
11942
+ const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and14(eq20(researchRunQueries.id, queryId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
11851
11943
  if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
11852
11944
  }
11853
11945
  function markUnfinishedResearchQueriesFailed(db, runId, error) {
11854
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and15(eq21(researchRunQueries.researchRunId, runId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
11946
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and14(eq20(researchRunQueries.researchRunId, runId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
11855
11947
  }
11856
11948
  function finalizeResearchRun(db, runId, fatalError) {
11857
- const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq21(researchRunQueries.researchRunId, runId)).all();
11949
+ const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq20(researchRunQueries.researchRunId, runId)).all();
11858
11950
  const completed = rows.filter((row) => row.status === ResearchQueryStatuses.completed).length;
11859
11951
  const failed = rows.filter((row) => row.status === ResearchQueryStatuses.failed).length;
11860
11952
  db.update(researchRuns).set({
@@ -11863,13 +11955,13 @@ function finalizeResearchRun(db, runId, fatalError) {
11863
11955
  failedQueries: failed,
11864
11956
  error: fatalError ?? (failed === rows.length ? "Every research query failed." : null),
11865
11957
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
11866
- }).where(eq21(researchRuns.id, runId)).run();
11958
+ }).where(eq20(researchRuns.id, runId)).run();
11867
11959
  }
11868
11960
 
11869
11961
  // src/server.ts
11870
11962
  var _require3 = createRequire3(import.meta.url);
11871
11963
  var { version: PKG_VERSION2 } = _require3("../package.json");
11872
- var log21 = createLogger("Server");
11964
+ var log20 = createLogger("Server");
11873
11965
  var DEFAULT_QUOTA = {
11874
11966
  maxConcurrency: 2,
11875
11967
  maxRequestsPerMinute: 10,
@@ -11912,14 +12004,14 @@ function summarizeProviderConfig(config) {
11912
12004
  };
11913
12005
  }
11914
12006
  function hashApiKey(key) {
11915
- return crypto23.createHash("sha256").update(key).digest("hex");
12007
+ return crypto22.createHash("sha256").update(key).digest("hex");
11916
12008
  }
11917
12009
  var DASHBOARD_SCRYPT_KEYLEN = 64;
11918
12010
  var DASHBOARD_SCRYPT_COST = 1 << 15;
11919
12011
  var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
11920
12012
  function hashDashboardPassword(password) {
11921
- const salt = crypto23.randomBytes(16);
11922
- const derived = crypto23.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
12013
+ const salt = crypto22.randomBytes(16);
12014
+ const derived = crypto22.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
11923
12015
  N: DASHBOARD_SCRYPT_COST,
11924
12016
  maxmem: DASHBOARD_SCRYPT_MAXMEM
11925
12017
  });
@@ -11940,14 +12032,14 @@ function verifyDashboardPassword(password, storedHash) {
11940
12032
  } catch {
11941
12033
  return { ok: false, needsRehash: false };
11942
12034
  }
11943
- const derived = crypto23.scryptSync(password, salt, expected.length, {
12035
+ const derived = crypto22.scryptSync(password, salt, expected.length, {
11944
12036
  N: DASHBOARD_SCRYPT_COST,
11945
12037
  maxmem: DASHBOARD_SCRYPT_MAXMEM
11946
12038
  });
11947
12039
  if (derived.length !== expected.length)
11948
12040
  return { ok: false, needsRehash: false };
11949
12041
  return {
11950
- ok: crypto23.timingSafeEqual(derived, expected),
12042
+ ok: crypto22.timingSafeEqual(derived, expected),
11951
12043
  needsRehash: false
11952
12044
  };
11953
12045
  }
@@ -11956,7 +12048,7 @@ function verifyDashboardPassword(password, storedHash) {
11956
12048
  const expected = Buffer.from(storedHash, "hex");
11957
12049
  if (candidate.length !== expected.length)
11958
12050
  return { ok: false, needsRehash: false };
11959
- const ok = crypto23.timingSafeEqual(candidate, expected);
12051
+ const ok = crypto22.timingSafeEqual(candidate, expected);
11960
12052
  return { ok, needsRehash: ok };
11961
12053
  }
11962
12054
  return { ok: false, needsRehash: false };
@@ -12019,7 +12111,7 @@ function applyLegacyCredentials(rows, config) {
12019
12111
  }
12020
12112
  if (migratedGoogle > 0) {
12021
12113
  saveConfigPatch({ google: config.google });
12022
- log21.info("credentials.migrated", { type: "google", count: migratedGoogle });
12114
+ log20.info("credentials.migrated", { type: "google", count: migratedGoogle });
12023
12115
  }
12024
12116
  let migratedGa4 = 0;
12025
12117
  for (const row of rows.ga4) {
@@ -12037,7 +12129,7 @@ function applyLegacyCredentials(rows, config) {
12037
12129
  }
12038
12130
  if (migratedGa4 > 0) {
12039
12131
  saveConfigPatch({ ga4: config.ga4 });
12040
- log21.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
12132
+ log20.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
12041
12133
  }
12042
12134
  }
12043
12135
  function isLoopbackBindHost(host) {
@@ -12094,11 +12186,11 @@ async function createServer(opts) {
12094
12186
  applyLegacyCredentials(legacyRows, opts.config);
12095
12187
  dropLegacyCredentialColumns(opts.db);
12096
12188
  } catch (err) {
12097
- log21.warn("credentials.migration.failed", {
12189
+ log20.warn("credentials.migration.failed", {
12098
12190
  error: err instanceof Error ? err.message : String(err)
12099
12191
  });
12100
12192
  }
12101
- log21.info("providers.configured", {
12193
+ log20.info("providers.configured", {
12102
12194
  providers: Object.keys(providers).filter((k) => {
12103
12195
  const p = providers[k];
12104
12196
  return p?.apiKey || p?.baseUrl || p?.vertexProject;
@@ -12162,7 +12254,7 @@ async function createServer(opts) {
12162
12254
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
12163
12255
  async (ctx) => {
12164
12256
  if (!sessionRegistry) return;
12165
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq22(projects.id, ctx.projectId)).get();
12257
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq21(projects.id, ctx.projectId)).get();
12166
12258
  if (!project) return;
12167
12259
  let content;
12168
12260
  if (ctx.kind === RunKinds["aeo-discover-probe"]) {
@@ -12497,6 +12589,24 @@ async function createServer(opts) {
12497
12589
  onAdsSyncRequested: (runId, projectId) => {
12498
12590
  runAdsSync(runId, projectId);
12499
12591
  },
12592
+ onDoctorRequested: (projectName) => {
12593
+ void (async () => {
12594
+ try {
12595
+ const report = await aeroClient.runDoctor({ project: projectName });
12596
+ const project = opts.db.select().from(projects).where(eq21(projects.name, projectName)).get();
12597
+ if (!project) {
12598
+ app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
12599
+ return;
12600
+ }
12601
+ await notifier.onHealthChecked(project.id, {
12602
+ checks: report.checks,
12603
+ checkedAt: report.generatedAt
12604
+ });
12605
+ } catch (err) {
12606
+ app.log.warn({ projectName, err }, "scheduled doctor pass failed");
12607
+ }
12608
+ })();
12609
+ },
12500
12610
  onDataRefreshRequested: (projectName) => {
12501
12611
  void refreshAllIntegrations(aeroClient, projectName);
12502
12612
  },
@@ -12511,9 +12621,9 @@ async function createServer(opts) {
12511
12621
  });
12512
12622
  if (!probed) return;
12513
12623
  const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
12514
- and16(
12515
- eq22(ccReleaseSyncs.release, probed.release),
12516
- eq22(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
12624
+ and15(
12625
+ eq21(ccReleaseSyncs.release, probed.release),
12626
+ eq21(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
12517
12627
  )
12518
12628
  ).limit(1).get();
12519
12629
  if (alreadySynced) {
@@ -12534,18 +12644,6 @@ async function createServer(opts) {
12534
12644
  );
12535
12645
  });
12536
12646
  })();
12537
- const project = opts.db.select().from(projects).where(eq22(projects.name, projectName)).get();
12538
- if (project && bingConnectionStore.getConnection(project.canonicalDomain)) {
12539
- aeroClient.backlinksBingSync(projectName).catch((err) => {
12540
- app.log.error(
12541
- {
12542
- projectName,
12543
- err: err instanceof Error ? err.message : String(err)
12544
- },
12545
- "Scheduled Bing backlinks sync failed"
12546
- );
12547
- });
12548
- }
12549
12647
  },
12550
12648
  onSiteAuditRequested: (runId, projectId) => {
12551
12649
  runSiteAudit(runId, projectId);
@@ -12680,7 +12778,7 @@ async function createServer(opts) {
12680
12778
  return removed;
12681
12779
  }
12682
12780
  };
12683
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto23.randomBytes(32).toString("hex");
12781
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto22.randomBytes(32).toString("hex");
12684
12782
  const googleConnectionStore = {
12685
12783
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
12686
12784
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
@@ -12753,11 +12851,11 @@ async function createServer(opts) {
12753
12851
  const googlePublicUrl = resolveGooglePublicUrl(opts.config, basePath);
12754
12852
  if (opts.config.apiKey) {
12755
12853
  const keyHash = hashApiKey(opts.config.apiKey);
12756
- const existing = opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, keyHash)).get();
12854
+ const existing = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, keyHash)).get();
12757
12855
  if (!existing) {
12758
12856
  const prefix = opts.config.apiKey.slice(0, 12);
12759
12857
  opts.db.insert(apiKeys).values({
12760
- id: `key_${crypto23.randomBytes(8).toString("hex")}`,
12858
+ id: `key_${crypto22.randomBytes(8).toString("hex")}`,
12761
12859
  name: "default",
12762
12860
  keyHash,
12763
12861
  keyPrefix: prefix,
@@ -12781,7 +12879,7 @@ async function createServer(opts) {
12781
12879
  };
12782
12880
  const createSession = (apiKeyId) => {
12783
12881
  pruneExpiredSessions();
12784
- const sessionId = crypto23.randomBytes(32).toString("hex");
12882
+ const sessionId = crypto22.randomBytes(32).toString("hex");
12785
12883
  sessions.set(sessionId, {
12786
12884
  apiKeyId,
12787
12885
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -12805,7 +12903,7 @@ async function createServer(opts) {
12805
12903
  };
12806
12904
  const getDefaultApiKey = () => {
12807
12905
  if (!opts.config.apiKey) return void 0;
12808
- return opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
12906
+ return opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
12809
12907
  };
12810
12908
  const createPasswordSession = (reply) => {
12811
12909
  const key = getDefaultApiKey();
@@ -12829,7 +12927,7 @@ async function createServer(opts) {
12829
12927
  if (!header) return false;
12830
12928
  const parts = header.split(" ");
12831
12929
  if (parts.length !== 2 || parts[0] !== "Bearer") return false;
12832
- const key = opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, hashApiKey(parts[1]))).get();
12930
+ const key = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(parts[1]))).get();
12833
12931
  return Boolean(key && !key.revokedAt);
12834
12932
  };
12835
12933
  app.get(apiPrefix + "/session", async (request, reply) => {
@@ -12906,12 +13004,12 @@ async function createServer(opts) {
12906
13004
  return reply.send({ authenticated: true });
12907
13005
  }
12908
13006
  if (apiKey) {
12909
- const key = opts.db.select().from(apiKeys).where(eq22(apiKeys.keyHash, hashApiKey(apiKey))).get();
13007
+ const key = opts.db.select().from(apiKeys).where(eq21(apiKeys.keyHash, hashApiKey(apiKey))).get();
12910
13008
  if (!key || key.revokedAt) {
12911
13009
  const err2 = authInvalid();
12912
13010
  return reply.status(err2.statusCode).send(err2.toJSON());
12913
13011
  }
12914
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(apiKeys.id, key.id)).run();
13012
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(apiKeys.id, key.id)).run();
12915
13013
  const sessionId = createSession(key.id);
12916
13014
  reply.header(
12917
13015
  "set-cookie",
@@ -13022,6 +13120,11 @@ async function createServer(opts) {
13022
13120
  // installer; cloud deployments inherit the secure default of `false` by
13023
13121
  // not passing this option. Override with CANONRY_ALLOW_LOOPBACK_WEBHOOKS=0.
13024
13122
  allowLoopbackWebhooks: process.env.CANONRY_ALLOW_LOOPBACK_WEBHOOKS !== "0",
13123
+ // Wall-clock budget for one incremental Vercel drain. This is the only lever
13124
+ // that decides whether a source catches up or falls further behind, and it
13125
+ // was previously reachable only from tests, so a source losing ground could
13126
+ // not be rescued without shipping code. Unset keeps the built-in default.
13127
+ vercelSyncDeadlineMs: resolveVercelSyncDeadlineMs(process.env),
13025
13128
  // Local-only Aero agent routes. Registered here so they inherit api-routes'
13026
13129
  // auth plugin — bare `registerAgentRoutes(app, ...)` would skip auth.
13027
13130
  registerAuthenticatedRoutes: async (scope) => {
@@ -13104,7 +13207,7 @@ async function createServer(opts) {
13104
13207
  deps: {
13105
13208
  enqueueAutoExtract: ({ projectId, release: r }) => {
13106
13209
  const now = (/* @__PURE__ */ new Date()).toISOString();
13107
- const runId = crypto23.randomUUID();
13210
+ const runId = crypto22.randomUUID();
13108
13211
  opts.db.insert(runs).values({
13109
13212
  id: runId,
13110
13213
  projectId,
@@ -13134,13 +13237,6 @@ async function createServer(opts) {
13134
13237
  }
13135
13238
  );
13136
13239
  },
13137
- onBingBacklinkSyncRequested: (runId, projectId) => {
13138
- executeBingBacklinkSync(opts.db, runId, projectId, {
13139
- resolveConnection: (domain) => bingConnectionStore.getConnection(domain)
13140
- }).catch((err) => {
13141
- app.log.error({ runId, err }, "Bing backlink sync failed");
13142
- });
13143
- },
13144
13240
  onDiscoveryRunRequested: (input) => {
13145
13241
  executeDiscoveryRun({
13146
13242
  db: opts.db,
@@ -13236,7 +13332,7 @@ async function createServer(opts) {
13236
13332
  ...inspectOpts,
13237
13333
  config: opts.config
13238
13334
  }).then(() => {
13239
- const finished = opts.db.select({ status: runs.status }).from(runs).where(eq22(runs.id, runId)).get();
13335
+ const finished = opts.db.select({ status: runs.status }).from(runs).where(eq21(runs.id, runId)).get();
13240
13336
  if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
13241
13337
  return maybeRefreshGscCoverage(opts.db, opts.config, projectId);
13242
13338
  }
@@ -13330,7 +13426,7 @@ async function createServer(opts) {
13330
13426
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
13331
13427
  opts.db.insert(auditLog).values(
13332
13428
  targetProjectIds.map((projectId) => ({
13333
- id: crypto23.randomUUID(),
13429
+ id: crypto22.randomUUID(),
13334
13430
  projectId,
13335
13431
  actor: "api",
13336
13432
  action: existing ? "provider.updated" : "provider.created",
@@ -13585,7 +13681,7 @@ async function createServer(opts) {
13585
13681
  }
13586
13682
  checkLatestVersionForServer();
13587
13683
  scheduler.start();
13588
- for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq22(researchRuns.status, ResearchRunStatuses.queued)).all()) {
13684
+ for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq21(researchRuns.status, ResearchRunStatuses.queued)).all()) {
13589
13685
  dispatchResearchRun(run.id, run.projectId);
13590
13686
  }
13591
13687
  app.addHook("onClose", async () => {