@canonry/canonry 7.9.1 → 7.11.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 (34) hide show
  1. package/assets/agent-workspace/skills/aero/references/reporting.md +2 -0
  2. package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +1 -1
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +30 -4
  4. package/assets/assets/{AuditHistoryPanel-D0nukTwD.js → AuditHistoryPanel-D1WdkJKU.js} +1 -1
  5. package/assets/assets/{BacklinksPage-B525ij0g.js → BacklinksPage-Uf1djxaV.js} +1 -1
  6. package/assets/assets/{HistoryPage-DvupqQDM.js → HistoryPage-CZPxzlS_.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-CwDbi1XN.js → MeasurementPropertyPage-CaWq1f2G.js} +1 -1
  8. package/assets/assets/{ProjectPage-DmZZ2C7q.js → ProjectPage-BimakE9X.js} +7 -7
  9. package/assets/assets/{RunsPage-DHTvHcGm.js → RunsPage-CDX5RxHi.js} +1 -1
  10. package/assets/assets/{SettingsPage-BwAFzB7A.js → SettingsPage-BfzKP-IR.js} +1 -1
  11. package/assets/assets/SiteHealthSection-42_E1ueq.js +4 -0
  12. package/assets/assets/{TrafficPage-BOcXASAM.js → TrafficPage-RjkpfGMw.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-DTq0cN6g.js → TrafficSourceDetailPage-DIWDPo-8.js} +1 -1
  14. package/assets/assets/{extract-error-message-BOTDI6g5.js → extract-error-message-D_e8B2s_.js} +1 -1
  15. package/assets/assets/index-BNeiohKS.css +1 -0
  16. package/assets/assets/{index-BLJfGYUw.js → index-DKl_jsi3.js} +24 -24
  17. package/assets/assets/{react-sigma_core.esm.min-BEBdJNtD.js → react-sigma_core.esm.min-Br-N5hoB.js} +1 -1
  18. package/assets/assets/{v2-overview-adapter-BPZ-H3VY.js → v2-overview-adapter-BCSXgy2l.js} +1 -1
  19. package/assets/index.html +2 -2
  20. package/dist/{chunk-ZLB7WX5D.js → chunk-7G7YVTDA.js} +56 -6
  21. package/dist/{chunk-DO7XPDG5.js → chunk-CF7G2HGX.js} +5790 -4451
  22. package/dist/{chunk-LO56QXPO.js → chunk-GQAESNJT.js} +90 -2
  23. package/dist/{chunk-KRFTA4I7.js → chunk-HA2BAJ6Z.js} +3507 -2290
  24. package/dist/{chunk-AAMKTL6B.js → chunk-JRBQFRNT.js} +3 -3
  25. package/dist/{chunk-T2V2PESH.js → chunk-OH7E7TJD.js} +2 -2
  26. package/dist/{chunk-4BKRZGLC.js → chunk-S46DWUNR.js} +706 -245
  27. package/dist/cli.js +166 -38
  28. package/dist/{demo-server-KDMG3APY.js → demo-server-BYLZFDKI.js} +4 -4
  29. package/dist/index.js +6 -6
  30. package/dist/{intelligence-service-LAXJFE3U.js → intelligence-service-TIM4XTYH.js} +2 -2
  31. package/dist/mcp.js +4 -4
  32. package/package.json +10 -10
  33. package/assets/assets/SiteHealthSection-hi54eSIt.js +0 -4
  34. package/assets/assets/index-Rcbm8bK1.css +0 -1
@@ -20,20 +20,20 @@ import {
20
20
  saveConfig,
21
21
  saveConfigPatch,
22
22
  sentimentInstallReadiness
23
- } from "./chunk-LO56QXPO.js";
23
+ } from "./chunk-GQAESNJT.js";
24
24
  import {
25
25
  computeFactorAverages,
26
26
  deleteSiteCrawlGraphLayout,
27
27
  persistSiteCrawlGraphLayout,
28
28
  prepareSiteCrawlGraphLayout,
29
29
  recordedSharePct
30
- } from "./chunk-T2V2PESH.js";
30
+ } from "./chunk-OH7E7TJD.js";
31
31
  import {
32
32
  CanonryMcpToolNames,
33
33
  CliError,
34
34
  PACKAGE_VERSION,
35
35
  canonryMcpTools
36
- } from "./chunk-ZLB7WX5D.js";
36
+ } from "./chunk-7G7YVTDA.js";
37
37
  import {
38
38
  CC_CACHE_DIR,
39
39
  DUCKDB_SPEC,
@@ -58,12 +58,16 @@ import {
58
58
  agentMemory,
59
59
  agentSessions,
60
60
  agentToolEvents,
61
+ answerIdentityChanged,
62
+ answerIdentityFrom,
61
63
  anyUsersExist,
62
64
  apiKeys,
63
65
  apiRoutes,
66
+ applyCompetitorAutoAliases,
64
67
  assessConversionTrackingIntegrity,
65
68
  auditLog,
66
69
  backfillProjectAnswerMentions,
70
+ backfillProjectAnswerMentionsInChunks,
67
71
  backlinkDomains,
68
72
  backlinkSummaries,
69
73
  bingCoverageSnapshots,
@@ -74,6 +78,7 @@ import {
74
78
  ccReleasePaths,
75
79
  ccReleaseSyncs,
76
80
  classifyReviewsError,
81
+ competitorIdentityColumns,
77
82
  competitors,
78
83
  computeCitedCompetitorDomains,
79
84
  countAttributes,
@@ -81,6 +86,7 @@ import {
81
86
  createCredentialChecker,
82
87
  createFastifyLogger,
83
88
  createLogger,
89
+ createProjectPassQueue,
84
90
  createRunCompetitorResolver,
85
91
  createUserSession,
86
92
  deliverWebhook,
@@ -156,6 +162,7 @@ import {
156
162
  notifications,
157
163
  parseCookieHeader,
158
164
  parseJsonColumn,
165
+ previewCompetitorAutoAliases,
159
166
  probeLatestRelease,
160
167
  projects,
161
168
  providerBatchRequests,
@@ -165,7 +172,9 @@ import {
165
172
  queryBacklinks,
166
173
  querySnapshots,
167
174
  queueRunIfProjectIdle,
175
+ readAnswerIdentity,
168
176
  readInstalledVersion,
177
+ readMarketCompetitorNames,
169
178
  readNegativeReviewMaxStars,
170
179
  recordSentimentCompletion,
171
180
  redactNotificationUrl,
@@ -205,7 +214,7 @@ import {
205
214
  siteLivenessState,
206
215
  toAlertView,
207
216
  usageCounters
208
- } from "./chunk-KRFTA4I7.js";
217
+ } from "./chunk-HA2BAJ6Z.js";
209
218
  import {
210
219
  AGENT_MEMORY_VALUE_MAX_BYTES,
211
220
  AGENT_NONE,
@@ -215,12 +224,14 @@ import {
215
224
  AdsAdGroupBillingEventTypes,
216
225
  AdsCampaignBiddingTypes,
217
226
  AgentProviderIds,
227
+ AnchoredSpanSources,
218
228
  AppError,
219
229
  BacklinkSources,
220
230
  CANONRY_NPM_PACKAGE_URL,
221
231
  CITED_URL_CAPTURE_VERSION,
222
232
  CcReleaseSyncStatuses,
223
233
  CheckNotificationPolicies,
234
+ DEFAULT_COMPETITOR_AUTO_ALIAS_MODE,
224
235
  DEFAULT_VIEWER_RESEARCH_DAILY_RUN_LIMIT,
225
236
  DiscoveryCompetitorTypes,
226
237
  FEEDBACK_LIMITS,
@@ -247,6 +258,7 @@ import {
247
258
  ProviderBatchStatuses,
248
259
  ProviderBatchSubmitError,
249
260
  ProviderDispatchModes,
261
+ ProviderNames,
250
262
  RATIO_UNIT_META_KEY,
251
263
  ROUTE_CAPABLE_CITED_URL_PROVIDERS,
252
264
  RUN_FILL_PROVIDER_BREAKER,
@@ -298,6 +310,7 @@ import {
298
310
  classifyTemplateLinkEdge,
299
311
  cloudflareQueueNameSchema,
300
312
  compareSemver,
313
+ competitorIdentityAliases,
301
314
  competitorLabelFromDomain,
302
315
  compileBrandAliases,
303
316
  computeBacklinkSummaryMetrics,
@@ -364,7 +377,6 @@ import {
364
377
  missingDependency,
365
378
  nextScheduleUpdatedAt,
366
379
  normalizeAgentSlug,
367
- normalizeCompetitorAliases,
368
380
  normalizeIdTokens,
369
381
  normalizeMeasurementExecutionQueryText,
370
382
  normalizeProjectDomain,
@@ -414,7 +426,7 @@ import {
414
426
  visibilityReportRequestSchema,
415
427
  winnabilityClassLabel,
416
428
  withRetry
417
- } from "./chunk-DO7XPDG5.js";
429
+ } from "./chunk-CF7G2HGX.js";
418
430
 
419
431
  // src/runtime-context.ts
420
432
  function cliRuntimeContext(env = process.env, stdio = process) {
@@ -3545,7 +3557,8 @@ import fs9 from "fs";
3545
3557
  import path8 from "path";
3546
3558
  import { isIP } from "net";
3547
3559
  import { fileURLToPath as fileURLToPath3 } from "url";
3548
- import { and as and23, eq as eq29 } from "drizzle-orm";
3560
+ import { setImmediate as yieldToEventLoop4 } from "timers/promises";
3561
+ import { and as and23, eq as eq29, isNotNull as isNotNull4, sql as sql11 } from "drizzle-orm";
3549
3562
 
3550
3563
  // ../integration-typesafe/src/classifier.ts
3551
3564
  import { createHash } from "crypto";
@@ -4519,6 +4532,42 @@ var geminiAdapter = {
4519
4532
  }
4520
4533
  };
4521
4534
 
4535
+ // ../provider-gemini/src/anchored-spans.ts
4536
+ function isRecord2(value) {
4537
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4538
+ }
4539
+ function chunkSource(chunk) {
4540
+ if (!isRecord2(chunk) || !isRecord2(chunk.web)) return null;
4541
+ const uri = typeof chunk.web.uri === "string" ? chunk.web.uri : "";
4542
+ const uriHost = hostOf(uri);
4543
+ if (uriHost && uriHost !== VERTEX_AI_SEARCH_PROXY_DOMAIN && registrableDomain(uriHost)) return uri;
4544
+ const title = typeof chunk.web.title === "string" ? chunk.web.title.trim() : "";
4545
+ return title && registrableDomain(title) ? title : null;
4546
+ }
4547
+ function extractAnchoredSpans(rawResponse) {
4548
+ const candidates = Array.isArray(rawResponse.candidates) ? rawResponse.candidates : [];
4549
+ const candidate = candidates[0];
4550
+ if (!isRecord2(candidate) || !isRecord2(candidate.groundingMetadata)) return [];
4551
+ const metadata2 = candidate.groundingMetadata;
4552
+ const chunks = Array.isArray(metadata2.groundingChunks) ? metadata2.groundingChunks : [];
4553
+ const spans = [];
4554
+ for (const support of Array.isArray(metadata2.groundingSupports) ? metadata2.groundingSupports : []) {
4555
+ if (!isRecord2(support) || !isRecord2(support.segment)) continue;
4556
+ const text = support.segment.text;
4557
+ if (typeof text !== "string" || !text.trim()) continue;
4558
+ const sources = /* @__PURE__ */ new Set();
4559
+ for (const index of Array.isArray(support.groundingChunkIndices) ? support.groundingChunkIndices : []) {
4560
+ if (typeof index !== "number" || !Number.isInteger(index) || index < 0 || index >= chunks.length) continue;
4561
+ const source = chunkSource(chunks[index]);
4562
+ if (source) sources.add(source);
4563
+ }
4564
+ for (const source of sources) {
4565
+ spans.push({ text, source, kind: "window", via: AnchoredSpanSources["gemini-support"] });
4566
+ }
4567
+ }
4568
+ return spans;
4569
+ }
4570
+
4522
4571
  // ../provider-openai/src/normalize.ts
4523
4572
  import OpenAI from "openai";
4524
4573
 
@@ -4961,6 +5010,40 @@ var openaiAdapter = {
4961
5010
  }
4962
5011
  };
4963
5012
 
5013
+ // ../provider-openai/src/anchored-spans.ts
5014
+ function isRecord3(value) {
5015
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5016
+ }
5017
+ function extractAnchoredSpans2(rawResponse) {
5018
+ const spans = [];
5019
+ const output = Array.isArray(rawResponse.output) ? rawResponse.output : [];
5020
+ for (const item of output) {
5021
+ if (!isRecord3(item) || item.type !== "message" || !Array.isArray(item.content)) continue;
5022
+ for (const part of item.content) {
5023
+ if (!isRecord3(part) || part.type !== "output_text" || typeof part.text !== "string") continue;
5024
+ const text = part.text;
5025
+ const citations = (Array.isArray(part.annotations) ? part.annotations : []).flatMap((annotation) => {
5026
+ if (!isRecord3(annotation) || annotation.type !== "url_citation" || typeof annotation.url !== "string") return [];
5027
+ const start = annotation.start_index;
5028
+ const end = annotation.end_index;
5029
+ if (typeof start !== "number" || typeof end !== "number" || !Number.isInteger(start) || !Number.isInteger(end)) return [];
5030
+ if (start < 0 || end < start || end > text.length) return [];
5031
+ return [{ start, end, url: annotation.url }];
5032
+ }).sort((a, b) => a.start - b.start);
5033
+ let previousEnd = 0;
5034
+ for (const citation of citations) {
5035
+ const lineStart = citation.start === 0 ? 0 : text.lastIndexOf("\n", citation.start - 1) + 1;
5036
+ const from = Math.max(lineStart, previousEnd);
5037
+ if (citation.start > from) {
5038
+ spans.push({ text: text.slice(from, citation.start), source: citation.url, kind: "window", via: AnchoredSpanSources["openai-annotation"] });
5039
+ }
5040
+ previousEnd = Math.max(previousEnd, citation.end);
5041
+ }
5042
+ }
5043
+ }
5044
+ return spans;
5045
+ }
5046
+
4964
5047
  // ../provider-claude/src/normalize.ts
4965
5048
  import Anthropic from "@anthropic-ai/sdk";
4966
5049
 
@@ -5597,6 +5680,40 @@ var claudeAdapter = {
5597
5680
  }
5598
5681
  };
5599
5682
 
5683
+ // ../provider-claude/src/anchored-spans.ts
5684
+ function isRecord4(value) {
5685
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5686
+ }
5687
+ function leadIn(text) {
5688
+ const end = text.trimEnd().length;
5689
+ if (end === 0) return "";
5690
+ return text.slice(text.lastIndexOf("\n", end - 1) + 1);
5691
+ }
5692
+ function extractAnchoredSpans3(rawResponse) {
5693
+ const spans = [];
5694
+ const content = Array.isArray(rawResponse.content) ? rawResponse.content : [];
5695
+ let previousUncited = null;
5696
+ for (const block of content) {
5697
+ if (!isRecord4(block) || block.type !== "text" || typeof block.text !== "string") continue;
5698
+ const urls = /* @__PURE__ */ new Set();
5699
+ for (const citation of Array.isArray(block.citations) ? block.citations : []) {
5700
+ if (isRecord4(citation) && citation.type === "web_search_result_location" && typeof citation.url === "string") {
5701
+ urls.add(citation.url);
5702
+ }
5703
+ }
5704
+ if (urls.size === 0) {
5705
+ previousUncited = block.text;
5706
+ continue;
5707
+ }
5708
+ const window = (previousUncited === null ? "" : leadIn(previousUncited)) + block.text;
5709
+ for (const url of urls) {
5710
+ spans.push({ text: window, source: url, kind: "window", via: AnchoredSpanSources["claude-citation"] });
5711
+ }
5712
+ previousUncited = null;
5713
+ }
5714
+ return spans;
5715
+ }
5716
+
5600
5717
  // ../provider-local/src/normalize.ts
5601
5718
  import OpenAI3 from "openai";
5602
5719
 
@@ -6631,6 +6748,54 @@ function reparseStoredResult4(rawResponse) {
6631
6748
  if (agent) return parseAgentResponse(agent);
6632
6749
  return parseSonarResponse(rawResponse);
6633
6750
  }
6751
+ function extractAnchoredSpans4(rawResponse) {
6752
+ const agent = agentResponseOf(rawResponse);
6753
+ let text;
6754
+ let sourceFor;
6755
+ if (agent) {
6756
+ text = extractAgentAnswerText(agent);
6757
+ const byId = /* @__PURE__ */ new Map();
6758
+ for (const item of agentOutput(agent)) {
6759
+ if (item.type !== "search_results" || !Array.isArray(item.results)) continue;
6760
+ for (const result2 of item.results) {
6761
+ if (!isRecord5(result2) || typeof result2.id !== "number" || typeof result2.url !== "string" || !result2.url) continue;
6762
+ if (!byId.has(result2.id)) byId.set(result2.id, result2.url);
6763
+ }
6764
+ }
6765
+ sourceFor = (marker) => byId.get(marker);
6766
+ } else {
6767
+ text = extractAnswerText3(rawResponse);
6768
+ const citations = extractCitations(rawResponse);
6769
+ sourceFor = (marker) => citations[marker - 1];
6770
+ }
6771
+ const spans = [];
6772
+ if (!text.includes("[")) return spans;
6773
+ let groupStart = -1;
6774
+ let groupEnd = -1;
6775
+ let previousEnd = 0;
6776
+ let markers = [];
6777
+ const flush = () => {
6778
+ if (markers.length === 0) return;
6779
+ const lineStart = groupStart === 0 ? 0 : text.lastIndexOf("\n", groupStart - 1) + 1;
6780
+ const from = Math.max(lineStart, previousEnd);
6781
+ const window = text.slice(from, groupStart);
6782
+ if (window.trim()) {
6783
+ for (const source of new Set(markers.map(sourceFor).filter((url) => typeof url === "string" && url.length > 0))) {
6784
+ spans.push({ text: window, source, kind: "window", via: AnchoredSpanSources["perplexity-marker"] });
6785
+ }
6786
+ }
6787
+ previousEnd = groupEnd;
6788
+ markers = [];
6789
+ };
6790
+ for (const match of text.matchAll(/\[(\d{1,3})\]/g)) {
6791
+ if (markers.length > 0 && match.index !== groupEnd) flush();
6792
+ if (markers.length === 0) groupStart = match.index;
6793
+ markers.push(Number(match[1]));
6794
+ groupEnd = match.index + match[0].length;
6795
+ }
6796
+ flush();
6797
+ return spans;
6798
+ }
6634
6799
  function createClient3(apiKey) {
6635
6800
  return new OpenAI4({ apiKey, baseURL: BASE_URL });
6636
6801
  }
@@ -6642,11 +6807,11 @@ function assertUsableResponse(response) {
6642
6807
  if (status === void 0 || status === "completed") return;
6643
6808
  if (status === "incomplete") {
6644
6809
  if (extractAgentAnswerText(response).trim().length > 0) return;
6645
- const details = isRecord2(response.incomplete_details) ? response.incomplete_details : void 0;
6810
+ const details = isRecord5(response.incomplete_details) ? response.incomplete_details : void 0;
6646
6811
  const reason = typeof details?.reason === "string" && details.reason.length > 0 ? details.reason : "no reason given";
6647
6812
  throw new Error(`agent response incomplete with no answer: ${reason}`);
6648
6813
  }
6649
- const error = isRecord2(response.error) ? response.error : void 0;
6814
+ const error = isRecord5(response.error) ? response.error : void 0;
6650
6815
  const message = typeof error?.message === "string" && error.message.length > 0 ? error.message : "no error detail";
6651
6816
  const type = typeof error?.type === "string" && error.type.length > 0 ? ` (${error.type})` : "";
6652
6817
  throw new Error(`agent response ${typeof status === "string" ? status : describeError(status)}: ${message}${type}`);
@@ -6657,7 +6822,7 @@ function agentResponseOf(rawResponse) {
6657
6822
  return nested && Array.isArray(nested.output) ? nested : null;
6658
6823
  }
6659
6824
  function agentOutput(response) {
6660
- return Array.isArray(response.output) ? response.output.filter(isRecord2) : [];
6825
+ return Array.isArray(response.output) ? response.output.filter(isRecord5) : [];
6661
6826
  }
6662
6827
  function parseAgentResponse(response) {
6663
6828
  const groundingSources = extractAgentGroundingSources(response);
@@ -6675,7 +6840,7 @@ function extractAgentAnswerText(response) {
6675
6840
  for (const item of agentOutput(response)) {
6676
6841
  if (item.type !== "message" || !Array.isArray(item.content)) continue;
6677
6842
  for (const part of item.content) {
6678
- if (isRecord2(part) && part.type === "output_text" && typeof part.text === "string") parts.push(part.text);
6843
+ if (isRecord5(part) && part.type === "output_text" && typeof part.text === "string") parts.push(part.text);
6679
6844
  }
6680
6845
  }
6681
6846
  return parts.join("");
@@ -6684,7 +6849,7 @@ function extractAgentGroundingSources(response) {
6684
6849
  const sources = [];
6685
6850
  const seen = /* @__PURE__ */ new Set();
6686
6851
  const add = (entry) => {
6687
- if (!isRecord2(entry) || typeof entry.url !== "string" || entry.url.length === 0) return;
6852
+ if (!isRecord5(entry) || typeof entry.url !== "string" || entry.url.length === 0) return;
6688
6853
  if (seen.has(entry.url)) return;
6689
6854
  seen.add(entry.url);
6690
6855
  sources.push({ uri: entry.url, title: typeof entry.title === "string" ? entry.title : "" });
@@ -6696,7 +6861,7 @@ function extractAgentGroundingSources(response) {
6696
6861
  item.contents.forEach(add);
6697
6862
  } else if (item.type === "message" && Array.isArray(item.content)) {
6698
6863
  for (const part of item.content) {
6699
- if (isRecord2(part) && Array.isArray(part.annotations)) part.annotations.forEach(add);
6864
+ if (isRecord5(part) && Array.isArray(part.annotations)) part.annotations.forEach(add);
6700
6865
  }
6701
6866
  }
6702
6867
  }
@@ -6713,13 +6878,13 @@ function extractAgentSearchQueries(response) {
6713
6878
  return [...queries2];
6714
6879
  }
6715
6880
  function extractAgentUsage(response) {
6716
- const usage = isRecord2(response.usage) ? response.usage : void 0;
6881
+ const usage = isRecord5(response.usage) ? response.usage : void 0;
6717
6882
  if (!usage) return void 0;
6718
- const details = isRecord2(usage.input_tokens_details) ? usage.input_tokens_details : void 0;
6883
+ const details = isRecord5(usage.input_tokens_details) ? usage.input_tokens_details : void 0;
6719
6884
  const cachedInputTokens = usageCount(details?.cache_read_input_tokens);
6720
6885
  const cacheWriteTokens = usageCount(details?.cache_creation_input_tokens);
6721
- const toolCalls = isRecord2(usage.tool_calls_details) ? usage.tool_calls_details : void 0;
6722
- const webSearch = isRecord2(toolCalls?.web_search) ? toolCalls.web_search : void 0;
6886
+ const toolCalls = isRecord5(usage.tool_calls_details) ? usage.tool_calls_details : void 0;
6887
+ const webSearch = isRecord5(toolCalls?.web_search) ? toolCalls.web_search : void 0;
6723
6888
  const searchCount = typeof webSearch?.invocation === "number" ? usageCount(webSearch.invocation) : agentOutput(response).filter((item) => item.type === "search_results").length;
6724
6889
  return {
6725
6890
  inputTokens: Math.max(0, usageCount(usage.input_tokens) - cachedInputTokens - cacheWriteTokens),
@@ -6730,7 +6895,7 @@ function extractAgentUsage(response) {
6730
6895
  };
6731
6896
  }
6732
6897
  function extractAgentStopReason(response) {
6733
- const details = isRecord2(response.incomplete_details) ? response.incomplete_details : void 0;
6898
+ const details = isRecord5(response.incomplete_details) ? response.incomplete_details : void 0;
6734
6899
  if (typeof details?.reason === "string" && details.reason.length > 0) return details.reason;
6735
6900
  return typeof response.status === "string" && response.status.length > 0 ? response.status : void 0;
6736
6901
  }
@@ -6798,7 +6963,7 @@ function extractSearchResults(rawResponse) {
6798
6963
  function parseSearchResultsArray(value) {
6799
6964
  if (!Array.isArray(value)) return [];
6800
6965
  return value.flatMap((result2) => {
6801
- if (!isRecord2(result2)) return [];
6966
+ if (!isRecord5(result2)) return [];
6802
6967
  const url = result2.url;
6803
6968
  if (typeof url !== "string" || url.length === 0) {
6804
6969
  return [];
@@ -6825,9 +6990,9 @@ function extractAnswerText3(rawResponse) {
6825
6990
  }
6826
6991
  }
6827
6992
  function extractNestedApiResponse(rawResponse) {
6828
- return isRecord2(rawResponse.apiResponse) ? rawResponse.apiResponse : null;
6993
+ return isRecord5(rawResponse.apiResponse) ? rawResponse.apiResponse : null;
6829
6994
  }
6830
- function isRecord2(value) {
6995
+ function isRecord5(value) {
6831
6996
  return value !== null && typeof value === "object" && !Array.isArray(value);
6832
6997
  }
6833
6998
  function extractCitedDomains2(groundingSources) {
@@ -7873,7 +8038,7 @@ function nonBlank(value) {
7873
8038
  return trimmed ? trimmed : void 0;
7874
8039
  }
7875
8040
  function resolveBuildCommit(env = process.env) {
7876
- const embedded = true ? "1ef30fafb9a5781918fcb963ade6280231796cc9" : void 0;
8041
+ const embedded = true ? "df2022e0ce72be6d0666fd60f286bd9a08926e7b" : void 0;
7877
8042
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
7878
8043
  }
7879
8044
  function resolveInstanceIdentity(env = process.env) {
@@ -8295,17 +8460,15 @@ function runOutcome(inserted, providerErrors, planShortfall) {
8295
8460
  error: someFailed ? serializeRunError(buildProviderRunError(providerErrors)) : null
8296
8461
  };
8297
8462
  }
8298
- function competitorIdentityKey(rows) {
8299
- return JSON.stringify(rows.map((row) => ({ domain: row.domain, aliases: normalizeCompetitorAliases(row.aliases) })).sort((left, right) => left.domain.localeCompare(right.domain)));
8300
- }
8301
8463
  function runRecordingContext(db, input) {
8302
- const resolveCompetitors = createRunCompetitorResolver(db, input.competitors);
8464
+ const identity = answerIdentityFrom(input.project, input.competitors, input.marketNames);
8465
+ const resolveCompetitors = createRunCompetitorResolver(db, identity.competitors, identity.marketNames);
8303
8466
  return {
8304
8467
  runId: input.runId,
8305
- allDomains: effectiveDomains({ canonicalDomain: input.project.canonicalDomain, ownedDomains: input.project.ownedDomains }),
8468
+ identity: identity.fingerprint,
8469
+ allDomains: identity.projectDomains,
8306
8470
  competitorsFor: (executionId) => resolveCompetitors(input.measurementPlanVersionId, executionId),
8307
- allBrandNames: effectiveBrandNames({ displayName: input.project.displayName, aliases: input.project.aliases }),
8308
- competitorIdentity: competitorIdentityKey(input.competitors)
8471
+ allBrandNames: identity.projectBrandNames
8309
8472
  };
8310
8473
  }
8311
8474
  function resolvePlanExecution(run, projectQueries) {
@@ -8574,13 +8737,14 @@ var JobRunner = class {
8574
8737
  throw new Error("No providers configured. Add at least one provider API key.");
8575
8738
  }
8576
8739
  log2.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
8577
- const projectCompetitors = this.db.select().from(competitors).where(eq3(competitors.projectId, projectId)).all();
8740
+ const projectCompetitors = this.db.select().from(competitors).where(eq3(competitors.projectId, projectId)).orderBy(competitors.domain).all();
8578
8741
  const competitorDomains = projectCompetitors.map((c) => c.domain);
8579
8742
  recording = runRecordingContext(this.db, {
8580
8743
  runId,
8581
8744
  measurementPlanVersionId: existingRun.measurementPlanVersionId,
8582
8745
  project,
8583
- competitors: projectCompetitors
8746
+ competitors: projectCompetitors,
8747
+ marketNames: readMarketCompetitorNames(this.db, projectId)
8584
8748
  });
8585
8749
  const { allDomains, allBrandNames } = recording;
8586
8750
  const { aliases: competitorAliases } = recording.competitorsFor(null);
@@ -8808,14 +8972,17 @@ var JobRunner = class {
8808
8972
  requestedModel: config.model ?? null
8809
8973
  })),
8810
8974
  // Freeze the exact identity we actually dispatched with (domain label
8811
- // plus curated aliases) so later reporting never borrows renamed or
8812
- // newly added competitors, or later alias edits, from live project
8813
- // state. A competitor with no curated alias freezes exactly as it did
8814
- // before aliases existed, so its definition checksum is unchanged.
8975
+ // plus curated and auto-detected aliases, minus blocked names) so
8976
+ // later reporting never borrows renamed or newly added competitors,
8977
+ // or later alias edits or detections, from live project state. A
8978
+ // competitor with no alias of either kind freezes exactly as it did
8979
+ // before aliases existed, so its definition checksum is unchanged;
8980
+ // an auto-detected name moves the checksum only for runs dispatched
8981
+ // after it was applied.
8815
8982
  competitors: projectCompetitors.map((competitor) => {
8816
8983
  const label = competitorLabelFromDomain(competitor.domain);
8817
- const curated = normalizeCompetitorAliases(competitor.aliases).filter((alias) => alias.toLowerCase() !== label.toLowerCase());
8818
- return { domain: competitor.domain, label, aliases: [label, ...curated] };
8984
+ const named = competitorIdentityAliases(competitor).filter((alias) => alias.toLowerCase() !== label.toLowerCase());
8985
+ return { domain: competitor.domain, label, aliases: [label, ...named] };
8819
8986
  }),
8820
8987
  queries: projectQueries.map((query) => ({
8821
8988
  queryId: query.id,
@@ -8876,7 +9043,7 @@ var JobRunner = class {
8876
9043
  }
8877
9044
  }
8878
9045
  providerCallEnd = Date.now();
8879
- this.reconcileRunCompetitorFields(recording, projectId);
9046
+ this.reconcileRunAnswerFields(recording, projectId);
8880
9047
  this.throwIfRunCancelled(runId);
8881
9048
  if (planExecution) {
8882
9049
  for (const [provider, units] of planExecution.unitsByProvider) {
@@ -8914,7 +9081,7 @@ var JobRunner = class {
8914
9081
  });
8915
9082
  if (!finalized && this.isRunCancelled(runId)) throw new RunCancelledError(runId);
8916
9083
  } catch (err) {
8917
- if (recording) this.reconcileRunCompetitorFields(recording, projectId);
9084
+ if (recording) this.reconcileRunAnswerFields(recording, projectId);
8918
9085
  const executionContext = {
8919
9086
  providerCount: activeProviders.length,
8920
9087
  providers: activeProviders.map((provider) => provider.adapter.name),
@@ -9556,7 +9723,7 @@ var JobRunner = class {
9556
9723
  this.markBatchesCancelled([batch.id], "Cancelled because its run is no longer running.");
9557
9724
  return { kind: "cancelled" };
9558
9725
  } finally {
9559
- this.reconcileRunCompetitorFields(ctx, batch.projectId);
9726
+ this.reconcileRunAnswerFields(ctx, batch.projectId);
9560
9727
  }
9561
9728
  return this.completeBatchIngest(batch);
9562
9729
  }
@@ -9847,7 +10014,7 @@ var JobRunner = class {
9847
10014
  fatal = describeError(err);
9848
10015
  log2.error("fill.failed", { fillId, runId, error: fatal });
9849
10016
  } finally {
9850
- if (recording) this.reconcileRunCompetitorFields(recording, projectId);
10017
+ if (recording) this.reconcileRunAnswerFields(recording, projectId);
9851
10018
  this.flushProviderUsage(providerDispatchCounts, providerReservations);
9852
10019
  }
9853
10020
  const outcome = this.finalizeRunFill({ fillId, runId, filled, fatal, providerErrors, superseded });
@@ -10097,34 +10264,38 @@ var JobRunner = class {
10097
10264
  if (!run) throw new Error(`Run ${runId} not found`);
10098
10265
  const project = this.db.select().from(projects).where(eq3(projects.id, projectId)).get();
10099
10266
  if (!project) throw new Error(`Project ${projectId} not found`);
10100
- const projectCompetitors = this.db.select().from(competitors).where(eq3(competitors.projectId, projectId)).all();
10267
+ const projectCompetitors = this.db.select().from(competitors).where(eq3(competitors.projectId, projectId)).orderBy(competitors.domain).all();
10101
10268
  return runRecordingContext(this.db, {
10102
10269
  runId,
10103
10270
  measurementPlanVersionId: run.measurementPlanVersionId,
10104
10271
  project,
10105
- competitors: projectCompetitors
10272
+ competitors: projectCompetitors,
10273
+ marketNames: readMarketCompetitorNames(this.db, projectId)
10106
10274
  });
10107
10275
  }
10108
10276
  /**
10109
- * Rescore a run's stored competitor columns when the project's competitors
10110
- * changed after `recording` read them.
10277
+ * Rescore a run's stored answer fields when the identity they are scored
10278
+ * against changed after `recording` read it (`answerIdentityChanged`).
10111
10279
  *
10112
- * Saving a competitor alias rescores the stored answers once
10113
- * (`onCompetitorAliasesChanged`). A writer that read the names before the
10114
- * edit keeps scoring with them, so an answer it stores afterwards would
10115
- * disagree with an identical one the edit rescored. Each writer calls this
10116
- * after its last write: when its names are no longer current, the whole run
10117
- * gets the same competitor-only pass the edit ran. A failure is logged and
10118
- * never fails the writer.
10280
+ * Saving a competitor alias, a detection pass that changes a competitor's
10281
+ * auto-detected names, or a project alias edit rescores the stored answers
10282
+ * once (`onCompetitorAliasesChanged`, `onAliasesChanged`). A writer that read
10283
+ * the identity before the edit keeps scoring with it, so an answer it stores
10284
+ * afterwards would disagree with an identical one the edit rescored. Each
10285
+ * writer calls this after its last write: when its identity is no longer
10286
+ * current, the whole run gets the pass the edit ran, the competitor fields
10287
+ * only, or `answer_mentioned` too when the project's own identity moved. A
10288
+ * failure is logged and never fails the writer.
10119
10289
  */
10120
- reconcileRunCompetitorFields(recording, projectId) {
10290
+ reconcileRunAnswerFields(recording, projectId) {
10121
10291
  try {
10122
- const current = this.db.select({ domain: competitors.domain, aliases: competitors.aliases }).from(competitors).where(eq3(competitors.projectId, projectId)).all();
10123
- if (competitorIdentityKey(current) === recording.competitorIdentity) return;
10124
- const result2 = backfillProjectAnswerMentions(this.db, projectId, { competitorFieldsOnly: true, runId: recording.runId });
10125
- log2.info("run.competitor-fields-rescored", { runId: recording.runId, projectId, ...result2 });
10292
+ const current = readAnswerIdentity(this.db, projectId);
10293
+ if (!current || !answerIdentityChanged(recording.identity, current.fingerprint)) return;
10294
+ const full = recording.identity.project !== current.fingerprint.project;
10295
+ const result2 = backfillProjectAnswerMentions(this.db, projectId, { competitorFieldsOnly: !full, runId: recording.runId });
10296
+ log2.info("run.answer-fields-rescored", { runId: recording.runId, projectId, full, ...result2 });
10126
10297
  } catch (err) {
10127
- log2.error("run.competitor-fields-rescore-failed", { runId: recording.runId, projectId, error: describeError(err) });
10298
+ log2.error("run.answer-fields-rescore-failed", { runId: recording.runId, projectId, error: describeError(err) });
10128
10299
  }
10129
10300
  }
10130
10301
  incrementUsage(scope, metric, count3) {
@@ -11142,11 +11313,11 @@ function validateGeoSearchLimit(limit) {
11142
11313
  );
11143
11314
  }
11144
11315
  }
11145
- function isRecord3(value) {
11316
+ function isRecord6(value) {
11146
11317
  return typeof value === "object" && value !== null && !Array.isArray(value);
11147
11318
  }
11148
11319
  function validateRequestObject(value, label) {
11149
- if (!isRecord3(value)) {
11320
+ if (!isRecord6(value)) {
11150
11321
  throw new OpenAiAdsApiError(`${label} must be a JSON object`, 400);
11151
11322
  }
11152
11323
  }
@@ -11305,7 +11476,7 @@ function validateLandingPageConfiguration(value, label) {
11305
11476
  }
11306
11477
  }
11307
11478
  function parseLandingPageQueryStringTemplate(value) {
11308
- if (!isRecord3(value)) return null;
11479
+ if (!isRecord6(value)) return null;
11309
11480
  const template = value.query_string_template;
11310
11481
  return typeof template === "string" && template.trim().length > 0 ? template : null;
11311
11482
  }
@@ -12899,7 +13070,7 @@ async function executeDiscoveryRun(opts) {
12899
13070
  try {
12900
13071
  const projectRow = opts.db.select().from(projects).where(eq14(projects.id, opts.projectId)).get();
12901
13072
  if (!projectRow) throw new Error(`Project ${opts.projectId} not found`);
12902
- const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, opts.projectId)).all().map((r) => r.domain.toLowerCase());
13073
+ const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, opts.projectId)).orderBy(competitors.domain).all().map((r) => r.domain.toLowerCase());
12903
13074
  const canonicalDomains = effectiveDomains({
12904
13075
  canonicalDomain: projectRow.canonicalDomain,
12905
13076
  ownedDomains: projectRow.ownedDomains
@@ -14242,6 +14413,115 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
14242
14413
  }
14243
14414
  }
14244
14415
 
14416
+ // src/answer-field-recompute.ts
14417
+ import { setImmediate as yieldToEventLoop2 } from "timers/promises";
14418
+ function createAnswerFieldRecomputeQueue(opts) {
14419
+ const pendingFull = /* @__PURE__ */ new Map();
14420
+ const queue = createProjectPassQueue(async (projectId) => {
14421
+ const full = pendingFull.get(projectId) ?? false;
14422
+ pendingFull.delete(projectId);
14423
+ await yieldToEventLoop2();
14424
+ const result2 = await backfillProjectAnswerMentionsInChunks(opts.db, projectId, { competitorFieldsOnly: !full });
14425
+ return { full, result: result2 };
14426
+ });
14427
+ return {
14428
+ request(projectId, scope) {
14429
+ pendingFull.set(projectId, (pendingFull.get(projectId) ?? false) || scope.full === true);
14430
+ return queue.request(projectId);
14431
+ },
14432
+ settled: () => queue.settled()
14433
+ };
14434
+ }
14435
+
14436
+ // src/competitor-auto-alias-runner.ts
14437
+ import { setImmediate as yieldToEventLoop3 } from "timers/promises";
14438
+ var log15 = createLogger("CompetitorAutoAliases");
14439
+ function createCompetitorAutoAliasRunner(opts) {
14440
+ const reasons = /* @__PURE__ */ new Map();
14441
+ const queue = createProjectPassQueue(async (projectId) => {
14442
+ await yieldToEventLoop3();
14443
+ const reason = (reasons.get(projectId) ?? []).join(",");
14444
+ reasons.delete(projectId);
14445
+ const result2 = await applyCompetitorAutoAliases(opts.db, projectId, { readAnchors: opts.readAnchors });
14446
+ if (!result2) return null;
14447
+ const added = result2.detection.competitors.reduce((sum, competitor) => sum + competitor.added.length, 0);
14448
+ const removed = result2.detection.competitors.reduce((sum, competitor) => sum + competitor.removed.length, 0);
14449
+ log15.info("detection.completed", {
14450
+ projectId,
14451
+ reason,
14452
+ runs: result2.detection.scan.runs,
14453
+ snapshots: result2.detection.scan.snapshots,
14454
+ added,
14455
+ removed,
14456
+ namesChanged: result2.namesChanged
14457
+ });
14458
+ if (result2.namesChanged) {
14459
+ try {
14460
+ opts.onNamesChanged(projectId, result2.detection.project);
14461
+ } catch (err) {
14462
+ log15.error("names-changed.failed", { projectId, error: describeError(err) });
14463
+ }
14464
+ }
14465
+ return result2;
14466
+ });
14467
+ const enqueue = (projectId, reason) => {
14468
+ reasons.set(projectId, [...reasons.get(projectId) ?? [], reason]);
14469
+ return queue.request(projectId);
14470
+ };
14471
+ return {
14472
+ schedule(projectId, reason) {
14473
+ enqueue(projectId, reason).catch((err) => {
14474
+ log15.error("detection.failed", { projectId, reason, error: describeError(err) });
14475
+ });
14476
+ },
14477
+ async request(projectId, reason) {
14478
+ try {
14479
+ return await enqueue(projectId, reason);
14480
+ } catch (err) {
14481
+ log15.error("detection.failed", { projectId, reason, error: describeError(err) });
14482
+ throw err;
14483
+ }
14484
+ },
14485
+ settled: () => queue.settled()
14486
+ };
14487
+ }
14488
+
14489
+ // src/stored-answer-anchors.ts
14490
+ function resolveStoredApiResponse(parsed) {
14491
+ const nested = parsed.apiResponse;
14492
+ if (nested !== null && typeof nested === "object" && !Array.isArray(nested)) {
14493
+ return nested;
14494
+ }
14495
+ if (looksLikeProviderApiResponse(parsed)) {
14496
+ return parsed;
14497
+ }
14498
+ return null;
14499
+ }
14500
+ function looksLikeProviderApiResponse(value) {
14501
+ return Array.isArray(value.output) || Array.isArray(value.content) || Array.isArray(value.candidates) || Array.isArray(value.choices);
14502
+ }
14503
+ function extractStoredAnswerAnchors(provider, rawResponse) {
14504
+ if (!rawResponse) return [];
14505
+ const apiResponse = resolveStoredApiResponse(parseJsonColumn(rawResponse, {}));
14506
+ if (!apiResponse) return [];
14507
+ try {
14508
+ switch (provider) {
14509
+ case ProviderNames.openai:
14510
+ return extractAnchoredSpans2(apiResponse);
14511
+ case ProviderNames.claude:
14512
+ return extractAnchoredSpans3(apiResponse);
14513
+ case ProviderNames.gemini:
14514
+ return extractAnchoredSpans(apiResponse);
14515
+ case ProviderNames.perplexity:
14516
+ return extractAnchoredSpans4(apiResponse);
14517
+ default:
14518
+ return [];
14519
+ }
14520
+ } catch {
14521
+ return [];
14522
+ }
14523
+ }
14524
+
14245
14525
  // src/provider-registry.ts
14246
14526
  var ProviderRegistry = class {
14247
14527
  providers = /* @__PURE__ */ new Map();
@@ -14358,7 +14638,7 @@ function createProviderModelCatalog(registry) {
14358
14638
  import crypto21 from "crypto";
14359
14639
  import cron from "node-cron";
14360
14640
  import { and as and15, eq as eq17, inArray as inArray7, notExists, sql as sql7 } from "drizzle-orm";
14361
- var log15 = createLogger("Scheduler");
14641
+ var log16 = createLogger("Scheduler");
14362
14642
  var DEFAULT_HEALTH_CRON = "0 */6 * * *";
14363
14643
  function ensureDefaultHealthSchedule(db, projectId, now = (/* @__PURE__ */ new Date()).toISOString()) {
14364
14644
  const result2 = db.insert(schedules).values({
@@ -14420,7 +14700,7 @@ var Scheduler = class {
14420
14700
  if (ensureDefaultHealthSchedule(this.db, project.id, now)) seeded += 1;
14421
14701
  }
14422
14702
  if (seeded > 0) {
14423
- log15.info("health-schedule.seeded", { projectCount: seeded, cron: DEFAULT_HEALTH_CRON });
14703
+ log16.info("health-schedule.seeded", { projectCount: seeded, cron: DEFAULT_HEALTH_CRON });
14424
14704
  }
14425
14705
  }
14426
14706
  /**
@@ -14444,10 +14724,10 @@ var Scheduler = class {
14444
14724
  const current = ensureCurrentQueryBasketRevision(this.db, project.id);
14445
14725
  if (current !== null && current.revision !== previous) minted += 1;
14446
14726
  } catch (err) {
14447
- log15.warn("query-basket.mint-failed", { projectId: project.id, err: String(err) });
14727
+ log16.warn("query-basket.mint-failed", { projectId: project.id, err: String(err) });
14448
14728
  }
14449
14729
  }
14450
- if (minted > 0) log15.info("query-basket.ensured", { projectCount: minted });
14730
+ if (minted > 0) log16.info("query-basket.ensured", { projectCount: minted });
14451
14731
  }
14452
14732
  /** Load all enabled schedules from DB and register cron jobs. */
14453
14733
  start() {
@@ -14461,12 +14741,12 @@ var Scheduler = class {
14461
14741
  if (registered && missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
14462
14742
  const answerRecurrence = recurrence && schedule.kind === SchedulableRunKinds["answer-visibility"];
14463
14743
  if (!recurrence || answerRecurrence || this.claimCalendarOccurrence(schedule, missedRunAt, /* @__PURE__ */ new Date())) {
14464
- log15.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
14744
+ log16.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
14465
14745
  this.triggerRun(schedule.id, schedule.projectId, schedule.kind, answerRecurrence ? missedRunAt : void 0);
14466
14746
  }
14467
14747
  }
14468
14748
  }
14469
- log15.info("started", { scheduleCount: allSchedules.length });
14749
+ log16.info("started", { scheduleCount: allSchedules.length });
14470
14750
  }
14471
14751
  /** Stop all cron tasks for graceful shutdown. */
14472
14752
  stop() {
@@ -14510,7 +14790,7 @@ var Scheduler = class {
14510
14790
  stopTask(key, task, verb) {
14511
14791
  void task.stop();
14512
14792
  void task.destroy();
14513
- log15.info(`task.${verb.toLowerCase()}`, { key });
14793
+ log16.info(`task.${verb.toLowerCase()}`, { key });
14514
14794
  }
14515
14795
  /** Runtime timing writes must not restore a version already issued to a settings caller. */
14516
14796
  updateScheduleTiming(scheduleId, timing) {
@@ -14530,13 +14810,13 @@ var Scheduler = class {
14530
14810
  return true;
14531
14811
  }
14532
14812
  if (schedule.recurrence) {
14533
- log15.error("calendar.invalid", { projectId: schedule.projectId, kind: schedule.kind });
14813
+ log16.error("calendar.invalid", { projectId: schedule.projectId, kind: schedule.kind });
14534
14814
  return false;
14535
14815
  }
14536
14816
  const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
14537
14817
  const kind = schedule.kind;
14538
14818
  if (!cron.validate(cronExpr)) {
14539
- log15.error("cron.invalid", { projectId, kind, cronExpr });
14819
+ log16.error("cron.invalid", { projectId, kind, cronExpr });
14540
14820
  return false;
14541
14821
  }
14542
14822
  const task = cron.schedule(cronExpr, () => {
@@ -14551,7 +14831,7 @@ var Scheduler = class {
14551
14831
  });
14552
14832
  }
14553
14833
  const label = schedule.preset ?? cronExpr;
14554
- log15.info("cron.registered", { projectId, kind, schedule: label, timezone });
14834
+ log16.info("cron.registered", { projectId, kind, schedule: label, timezone });
14555
14835
  return true;
14556
14836
  }
14557
14837
  /** Calendar schedules are checked at most once a minute so long intervals
@@ -14609,7 +14889,7 @@ var Scheduler = class {
14609
14889
  }
14610
14890
  if (options.preserveNextRunAt) arm(registeredNextRunAt);
14611
14891
  else timer = setTimeout(tick, 0);
14612
- log15.info("calendar.registered", { projectId, kind, recurrence, timezone });
14892
+ log16.info("calendar.registered", { projectId, kind, recurrence, timezone });
14613
14893
  }
14614
14894
  /** Atomically advance a due callback-only calendar occurrence before dispatch. */
14615
14895
  claimCalendarOccurrence(schedule, dueAt, now) {
@@ -14637,13 +14917,13 @@ var Scheduler = class {
14637
14917
  const now = (/* @__PURE__ */ new Date()).toISOString();
14638
14918
  const currentSchedule = this.db.select().from(schedules).where(eq17(schedules.id, scheduleId)).get();
14639
14919
  if (!currentSchedule || !currentSchedule.enabled) {
14640
- log15.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
14920
+ log16.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
14641
14921
  this.remove(projectId, kind);
14642
14922
  return;
14643
14923
  }
14644
14924
  const recurrence = scheduleRecurrence(currentSchedule);
14645
14925
  if (claimedOccurrence && (!recurrence || currentSchedule.nextRunAt !== claimedOccurrence)) {
14646
- log15.info("calendar.stale-claim", { projectId, scheduleId, kind });
14926
+ log16.info("calendar.stale-claim", { projectId, scheduleId, kind });
14647
14927
  return;
14648
14928
  }
14649
14929
  const nextRunAt = nextRunFromSchedule({
@@ -14653,31 +14933,31 @@ var Scheduler = class {
14653
14933
  });
14654
14934
  const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
14655
14935
  if (!project) {
14656
- log15.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
14936
+ log16.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
14657
14937
  this.remove(projectId, kind);
14658
14938
  return;
14659
14939
  }
14660
14940
  if (kind === SchedulableRunKinds["traffic-sync"]) {
14661
14941
  const sourceId = currentSchedule.sourceId;
14662
14942
  if (!sourceId) {
14663
- log15.warn("traffic-sync.missing-source", { scheduleId, projectId });
14943
+ log16.warn("traffic-sync.missing-source", { scheduleId, projectId });
14664
14944
  return;
14665
14945
  }
14666
14946
  if (!this.callbacks.onTrafficSyncRequested) {
14667
- log15.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
14947
+ log16.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
14668
14948
  return;
14669
14949
  }
14670
14950
  this.updateScheduleTiming(currentSchedule.id, {
14671
14951
  lastRunAt: now,
14672
14952
  nextRunAt
14673
14953
  });
14674
- log15.info("traffic-sync.triggered", { projectName: project.name, sourceId });
14954
+ log16.info("traffic-sync.triggered", { projectName: project.name, sourceId });
14675
14955
  this.callbacks.onTrafficSyncRequested(project.name, sourceId);
14676
14956
  return;
14677
14957
  }
14678
14958
  if (kind === SchedulableRunKinds["gbp-sync"]) {
14679
14959
  if (!this.callbacks.onGbpSyncRequested) {
14680
- log15.warn("gbp-sync.no-callback", { scheduleId, projectId, msg: "host did not register onGbpSyncRequested" });
14960
+ log16.warn("gbp-sync.no-callback", { scheduleId, projectId, msg: "host did not register onGbpSyncRequested" });
14681
14961
  return;
14682
14962
  }
14683
14963
  const runId2 = crypto21.randomUUID();
@@ -14693,13 +14973,13 @@ var Scheduler = class {
14693
14973
  lastRunAt: now,
14694
14974
  nextRunAt
14695
14975
  });
14696
- log15.info("gbp-sync.triggered", { runId: runId2, projectName: project.name });
14976
+ log16.info("gbp-sync.triggered", { runId: runId2, projectName: project.name });
14697
14977
  this.callbacks.onGbpSyncRequested(runId2, projectId);
14698
14978
  return;
14699
14979
  }
14700
14980
  if (kind === SchedulableRunKinds["ads-sync"]) {
14701
14981
  if (!this.callbacks.onAdsSyncRequested) {
14702
- log15.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
14982
+ log16.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
14703
14983
  return;
14704
14984
  }
14705
14985
  const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and15(
@@ -14708,7 +14988,7 @@ var Scheduler = class {
14708
14988
  inArray7(runs.status, [RunStatuses.queued, RunStatuses.running])
14709
14989
  )).get();
14710
14990
  if (activeAdsRun) {
14711
- log15.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
14991
+ log16.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
14712
14992
  this.updateScheduleTiming(currentSchedule.id, { nextRunAt });
14713
14993
  return;
14714
14994
  }
@@ -14725,52 +15005,52 @@ var Scheduler = class {
14725
15005
  lastRunAt: now,
14726
15006
  nextRunAt
14727
15007
  });
14728
- log15.info("ads-sync.triggered", { runId: runId2, projectName: project.name });
15008
+ log16.info("ads-sync.triggered", { runId: runId2, projectName: project.name });
14729
15009
  this.callbacks.onAdsSyncRequested(runId2, projectId);
14730
15010
  return;
14731
15011
  }
14732
15012
  if (kind === SchedulableRunKinds["data-refresh"]) {
14733
15013
  if (!this.callbacks.onDataRefreshRequested) {
14734
- log15.warn("data-refresh.no-callback", { scheduleId, projectId, msg: "host did not register onDataRefreshRequested" });
15014
+ log16.warn("data-refresh.no-callback", { scheduleId, projectId, msg: "host did not register onDataRefreshRequested" });
14735
15015
  return;
14736
15016
  }
14737
15017
  this.updateScheduleTiming(currentSchedule.id, {
14738
15018
  lastRunAt: now,
14739
15019
  nextRunAt
14740
15020
  });
14741
- log15.info("data-refresh.triggered", { projectName: project.name });
15021
+ log16.info("data-refresh.triggered", { projectName: project.name });
14742
15022
  this.callbacks.onDataRefreshRequested(project.name);
14743
15023
  return;
14744
15024
  }
14745
15025
  if (kind === SchedulableRunKinds.doctor) {
14746
15026
  if (!this.callbacks.onDoctorRequested) {
14747
- log15.warn("doctor.no-callback", { scheduleId, projectId, msg: "host did not register onDoctorRequested" });
15027
+ log16.warn("doctor.no-callback", { scheduleId, projectId, msg: "host did not register onDoctorRequested" });
14748
15028
  return;
14749
15029
  }
14750
15030
  this.updateScheduleTiming(currentSchedule.id, {
14751
15031
  lastRunAt: now,
14752
15032
  nextRunAt
14753
15033
  });
14754
- log15.info("doctor.triggered", { projectName: project.name });
15034
+ log16.info("doctor.triggered", { projectName: project.name });
14755
15035
  this.callbacks.onDoctorRequested(project.name);
14756
15036
  return;
14757
15037
  }
14758
15038
  if (kind === SchedulableRunKinds["backlinks-sync"]) {
14759
15039
  if (!this.callbacks.onBacklinksSyncRequested) {
14760
- log15.warn("backlinks-sync.no-callback", { scheduleId, projectId, msg: "host did not register onBacklinksSyncRequested" });
15040
+ log16.warn("backlinks-sync.no-callback", { scheduleId, projectId, msg: "host did not register onBacklinksSyncRequested" });
14761
15041
  return;
14762
15042
  }
14763
15043
  this.updateScheduleTiming(currentSchedule.id, {
14764
15044
  lastRunAt: now,
14765
15045
  nextRunAt
14766
15046
  });
14767
- log15.info("backlinks-sync.triggered", { projectName: project.name });
15047
+ log16.info("backlinks-sync.triggered", { projectName: project.name });
14768
15048
  this.callbacks.onBacklinksSyncRequested(project.name);
14769
15049
  return;
14770
15050
  }
14771
15051
  if (kind === SchedulableRunKinds["site-audit"]) {
14772
15052
  if (!this.callbacks.onSiteAuditRequested) {
14773
- log15.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
15053
+ log16.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
14774
15054
  return;
14775
15055
  }
14776
15056
  const active = this.db.select({ id: runs.id }).from(runs).where(and15(
@@ -14779,7 +15059,7 @@ var Scheduler = class {
14779
15059
  inArray7(runs.status, [RunStatuses.queued, RunStatuses.running])
14780
15060
  )).get();
14781
15061
  if (active) {
14782
- log15.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
15062
+ log16.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
14783
15063
  this.updateScheduleTiming(currentSchedule.id, { nextRunAt });
14784
15064
  return;
14785
15065
  }
@@ -14806,7 +15086,7 @@ var Scheduler = class {
14806
15086
  lastRunAt: now,
14807
15087
  nextRunAt
14808
15088
  });
14809
- log15.info("site-audit.triggered", { runId: runId2, projectName: project.name });
15089
+ log16.info("site-audit.triggered", { runId: runId2, projectName: project.name });
14810
15090
  this.callbacks.onSiteAuditRequested(runId2, projectId, { maxPages: effectiveRequest.maxPages });
14811
15091
  return;
14812
15092
  }
@@ -14815,7 +15095,7 @@ var Scheduler = class {
14815
15095
  if (project.defaultLocation) {
14816
15096
  const loc = projectLocations.find((l) => l.label === project.defaultLocation);
14817
15097
  if (!loc) {
14818
- log15.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
15098
+ log16.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
14819
15099
  return;
14820
15100
  }
14821
15101
  resolvedLocation = loc;
@@ -14824,7 +15104,7 @@ var Scheduler = class {
14824
15104
  const scheduleProviders = currentSchedule.providers;
14825
15105
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
14826
15106
  if (claimedOccurrence && recurrence && !nextRunAt) {
14827
- log15.error("calendar.invalid", { projectId, kind, scheduleId });
15107
+ log16.error("calendar.invalid", { projectId, kind, scheduleId });
14828
15108
  return;
14829
15109
  }
14830
15110
  const queueResult = queueRunIfProjectIdle(this.db, {
@@ -14848,11 +15128,11 @@ var Scheduler = class {
14848
15128
  });
14849
15129
  if (queueResult.conflict) {
14850
15130
  if (queueResult.scheduleClaimed === false) {
14851
- log15.info("calendar.skipped-claimed", { projectName: project.name, scheduleId: currentSchedule.id });
15131
+ log16.info("calendar.skipped-claimed", { projectName: project.name, scheduleId: currentSchedule.id });
14852
15132
  return;
14853
15133
  }
14854
15134
  const batchPending = hasOutstandingProviderBatch(this.db, queueResult.activeRunId);
14855
- log15.info("run.skipped-active", {
15135
+ log16.info("run.skipped-active", {
14856
15136
  projectName: project.name,
14857
15137
  activeRunId: queueResult.activeRunId,
14858
15138
  ...batchPending ? { reason: "batch-pending" } : {}
@@ -14861,7 +15141,7 @@ var Scheduler = class {
14861
15141
  return;
14862
15142
  }
14863
15143
  if (queueResult.refused) {
14864
- log15.warn("run.skipped-providers-failing", {
15144
+ log16.warn("run.skipped-providers-failing", {
14865
15145
  projectName: project.name,
14866
15146
  scheduleId: currentSchedule.id,
14867
15147
  ...queueResult.refused
@@ -14872,7 +15152,7 @@ var Scheduler = class {
14872
15152
  const runId = queueResult.runId;
14873
15153
  for (const [provider, reason] of Object.entries(queueResult.dispatch.ineligible)) {
14874
15154
  if (!reason) continue;
14875
- log15.warn("run.dispatch-sync-fallback", {
15155
+ log16.warn("run.dispatch-sync-fallback", {
14876
15156
  runId,
14877
15157
  projectName: project.name,
14878
15158
  provider,
@@ -14886,16 +15166,16 @@ var Scheduler = class {
14886
15166
  lastRunAt: now,
14887
15167
  nextRunAt
14888
15168
  });
14889
- log15.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
15169
+ log16.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
14890
15170
  this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
14891
15171
  } catch (err) {
14892
- log15.error("trigger.error", { scheduleId, projectId, kind, error: describeError(err) });
15172
+ log16.error("trigger.error", { scheduleId, projectId, kind, error: describeError(err) });
14893
15173
  }
14894
15174
  }
14895
15175
  };
14896
15176
 
14897
15177
  // src/site-liveness-loop.ts
14898
- var log16 = createLogger("SiteLiveness");
15178
+ var log17 = createLogger("SiteLiveness");
14899
15179
  var SITE_LIVENESS_INTERVAL_MS = 10 * 6e4;
14900
15180
  async function runSiteLivenessPass(deps) {
14901
15181
  const now = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
@@ -14910,10 +15190,10 @@ async function runSiteLivenessPass(deps) {
14910
15190
  const event = await deps.notify(project.id, { check, checkedAt: now() });
14911
15191
  if (event) {
14912
15192
  events += 1;
14913
- log16.info("site-liveness.notified", { projectName: project.name, event, code: check.code });
15193
+ log17.info("site-liveness.notified", { projectName: project.name, event, code: check.code });
14914
15194
  }
14915
15195
  } catch (err) {
14916
- log16.warn("site-liveness.project-failed", { projectName: project.name, err: String(err) });
15196
+ log17.warn("site-liveness.project-failed", { projectName: project.name, err: String(err) });
14917
15197
  }
14918
15198
  }
14919
15199
  return { checked, events };
@@ -14922,11 +15202,11 @@ function startSiteLivenessLoop(deps, intervalMs = SITE_LIVENESS_INTERVAL_MS) {
14922
15202
  let running = false;
14923
15203
  const timer = setInterval(() => {
14924
15204
  if (running) {
14925
- log16.warn("site-liveness.pass-still-running", { intervalMs });
15205
+ log17.warn("site-liveness.pass-still-running", { intervalMs });
14926
15206
  return;
14927
15207
  }
14928
15208
  running = true;
14929
- void runSiteLivenessPass(deps).catch((err) => log16.warn("site-liveness.pass-failed", { err: String(err) })).finally(() => {
15209
+ void runSiteLivenessPass(deps).catch((err) => log17.warn("site-liveness.pass-failed", { err: String(err) })).finally(() => {
14930
15210
  running = false;
14931
15211
  });
14932
15212
  }, intervalMs);
@@ -14936,7 +15216,7 @@ function startSiteLivenessLoop(deps, intervalMs = SITE_LIVENESS_INTERVAL_MS) {
14936
15216
 
14937
15217
  // src/provider-batch-poller.ts
14938
15218
  import { and as and16, eq as eq18, inArray as inArray8, isNotNull as isNotNull2, isNull as isNull2, notExists as notExists2, sql as sql8 } from "drizzle-orm";
14939
- var log17 = createLogger("ProviderBatchPoller");
15219
+ var log18 = createLogger("ProviderBatchPoller");
14940
15220
  var PROVIDER_BATCH_POLL_INITIAL_MS = 3e4;
14941
15221
  var PROVIDER_BATCH_POLL_MAX_MS = 10 * 6e4;
14942
15222
  var PROVIDER_BATCH_CANCEL_GRACE_MS = 60 * 6e4;
@@ -14960,7 +15240,7 @@ var ProviderBatchPoller = class {
14960
15240
  try {
14961
15241
  if (await this.advance(row)) settledRuns.set(row.runId, row.projectId);
14962
15242
  } catch (err) {
14963
- log17.warn("batch.advance-failed", { batchId: row.id, runId: row.runId, provider: row.provider, error: describeError(err) });
15243
+ log18.warn("batch.advance-failed", { batchId: row.id, runId: row.runId, provider: row.provider, error: describeError(err) });
14964
15244
  this.reschedule(row.id, null);
14965
15245
  }
14966
15246
  }
@@ -14969,7 +15249,7 @@ var ProviderBatchPoller = class {
14969
15249
  try {
14970
15250
  this.deps.runner.finalizeBatchRun(runId, projectId);
14971
15251
  } catch (err) {
14972
- log17.warn("batch.finalize-failed", { runId, error: describeError(err) });
15252
+ log18.warn("batch.finalize-failed", { runId, error: describeError(err) });
14973
15253
  }
14974
15254
  }
14975
15255
  }
@@ -15000,10 +15280,10 @@ var ProviderBatchPoller = class {
15000
15280
  return ended ? this.ingest({ ...row, status: ProviderBatchStatuses.ended, resultsExpireAt: poll.resultsExpireAt ?? null }) : false;
15001
15281
  }
15002
15282
  } catch (err) {
15003
- log17.warn("batch.poll-failed", { batchId: row.id, runId: row.runId, provider: row.provider, error: describeError(err) });
15283
+ log18.warn("batch.poll-failed", { batchId: row.id, runId: row.runId, provider: row.provider, error: describeError(err) });
15004
15284
  }
15005
15285
  } else {
15006
- log17.warn("batch.poll-unavailable", { batchId: row.id, runId: row.runId, provider: row.provider });
15286
+ log18.warn("batch.poll-unavailable", { batchId: row.id, runId: row.runId, provider: row.provider });
15007
15287
  }
15008
15288
  this.reschedule(row.id, status);
15009
15289
  return this.enforceDeadline(row, registered);
@@ -15025,12 +15305,12 @@ var ProviderBatchPoller = class {
15025
15305
  isNull2(providerBatches.cancelRequestedAt)
15026
15306
  )).run().changes === 1;
15027
15307
  if (!claimed) return false;
15028
- log17.warn("batch.deadline-passed", { batchId: row.id, runId: row.runId, provider: row.provider, deadlineAt: row.deadlineAt });
15308
+ log18.warn("batch.deadline-passed", { batchId: row.id, runId: row.runId, provider: row.provider, deadlineAt: row.deadlineAt });
15029
15309
  if (registered?.adapter.batch && row.providerBatchId) {
15030
15310
  try {
15031
15311
  await registered.adapter.batch.cancel(row.providerBatchId, registered.config);
15032
15312
  } catch (err) {
15033
- log17.warn("batch.cancel-failed", { batchId: row.id, provider: row.provider, error: describeError(err) });
15313
+ log18.warn("batch.cancel-failed", { batchId: row.id, provider: row.provider, error: describeError(err) });
15034
15314
  }
15035
15315
  }
15036
15316
  return false;
@@ -15042,7 +15322,7 @@ var ProviderBatchPoller = class {
15042
15322
  updatedAt: at
15043
15323
  }).where(and16(eq18(providerBatches.id, row.id), eq18(providerBatches.status, ProviderBatchStatuses.submitted))).run().changes === 1;
15044
15324
  this.schedule.delete(row.id);
15045
- if (gaveUp) log17.warn("batch.abandoned-after-deadline", { batchId: row.id, runId: row.runId, provider: row.provider });
15325
+ if (gaveUp) log18.warn("batch.abandoned-after-deadline", { batchId: row.id, runId: row.runId, provider: row.provider });
15046
15326
  return gaveUp;
15047
15327
  }
15048
15328
  async ingest(row) {
@@ -15051,7 +15331,7 @@ var ProviderBatchPoller = class {
15051
15331
  return result2.kind !== "skipped";
15052
15332
  } catch (err) {
15053
15333
  const error = describeError(err);
15054
- log17.warn("batch.ingest-failed", { batchId: row.id, runId: row.runId, provider: row.provider, error });
15334
+ log18.warn("batch.ingest-failed", { batchId: row.id, runId: row.runId, provider: row.provider, error });
15055
15335
  const now = this.now();
15056
15336
  const unreadable = row.resultsExpireAt !== null && now >= Date.parse(row.resultsExpireAt);
15057
15337
  if (unreadable || now >= Date.parse(row.deadlineAt) + PROVIDER_BATCH_CANCEL_GRACE_MS) {
@@ -15093,7 +15373,7 @@ function startProviderBatchPoller(deps, intervalMs = PROVIDER_BATCH_TICK_MS) {
15093
15373
  const timer = setInterval(() => {
15094
15374
  if (running) return;
15095
15375
  running = true;
15096
- void poller.tick().catch((err) => log17.warn("batch.pass-failed", { error: describeError(err) })).finally(() => {
15376
+ void poller.tick().catch((err) => log18.warn("batch.pass-failed", { error: describeError(err) })).finally(() => {
15097
15377
  running = false;
15098
15378
  });
15099
15379
  }, intervalMs);
@@ -15102,7 +15382,7 @@ function startProviderBatchPoller(deps, intervalMs = PROVIDER_BATCH_TICK_MS) {
15102
15382
  }
15103
15383
 
15104
15384
  // src/data-refresh.ts
15105
- var log18 = createLogger("DataRefresh");
15385
+ var log19 = createLogger("DataRefresh");
15106
15386
  function gaRefreshDays(now = /* @__PURE__ */ new Date()) {
15107
15387
  const [reportMonth] = reportMonthsForDoctor(void 0, now);
15108
15388
  const reach = inclusiveDayCount(`${reportMonth}-01`, now.toISOString().slice(0, 10)) ?? 0;
@@ -15124,11 +15404,11 @@ async function refreshAllIntegrations(client, projectName, now = /* @__PURE__ */
15124
15404
  results.forEach((result2, idx) => {
15125
15405
  const integration = integrations[idx].name;
15126
15406
  if (result2.status === "fulfilled") {
15127
- log18.info("integration.refreshed", { projectName, integration });
15407
+ log19.info("integration.refreshed", { projectName, integration });
15128
15408
  } else {
15129
15409
  const reason = result2.reason;
15130
15410
  const message = describeError(reason);
15131
- log18.warn("integration.refresh-failed", { projectName, integration, error: message });
15411
+ log19.warn("integration.refresh-failed", { projectName, integration, error: message });
15132
15412
  }
15133
15413
  });
15134
15414
  }
@@ -15136,7 +15416,7 @@ async function refreshAllIntegrations(client, projectName, now = /* @__PURE__ */
15136
15416
  // src/notifier.ts
15137
15417
  import { eq as eq19, desc as desc8, and as and17, inArray as inArray9, or } from "drizzle-orm";
15138
15418
  import crypto22 from "crypto";
15139
- var log19 = createLogger("Notifier");
15419
+ var log20 = createLogger("Notifier");
15140
15420
  var MATERIAL_MAGNITUDE_DELTA = 20;
15141
15421
  var SEVERITY_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
15142
15422
  function severityRank(severity) {
@@ -15166,26 +15446,26 @@ var Notifier = class {
15166
15446
  }
15167
15447
  /** Called after a run completes (success, partial, or failed). */
15168
15448
  async onRunCompleted(runId, projectId, opts) {
15169
- log19.info("run.completed", { runId, projectId, origin: opts?.origin ?? "sweep" });
15449
+ log20.info("run.completed", { runId, projectId, origin: opts?.origin ?? "sweep" });
15170
15450
  const notifs = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled);
15171
15451
  if (notifs.length === 0) {
15172
- log19.info("notifications.none-enabled", { projectId });
15452
+ log20.info("notifications.none-enabled", { projectId });
15173
15453
  return;
15174
15454
  }
15175
- log19.info("notifications.found", { projectId, count: notifs.length });
15455
+ log20.info("notifications.found", { projectId, count: notifs.length });
15176
15456
  const run = this.db.select().from(runs).where(eq19(runs.id, runId)).get();
15177
15457
  if (!run) {
15178
- log19.error("run.not-found", { runId, msg: "skipping notification dispatch" });
15458
+ log20.error("run.not-found", { runId, msg: "skipping notification dispatch" });
15179
15459
  return;
15180
15460
  }
15181
15461
  const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
15182
15462
  if (!project) {
15183
- log19.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
15463
+ log20.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
15184
15464
  return;
15185
15465
  }
15186
15466
  const transitions = this.computeTransitions(runId, projectId);
15187
15467
  const events = [];
15188
- log19.info("run.status", { runId: run.id, status: run.status, projectId });
15468
+ log20.info("run.status", { runId: run.id, status: run.status, projectId });
15189
15469
  if ((run.status === "completed" || run.status === "partial") && opts?.origin !== "fill") {
15190
15470
  events.push("run.completed");
15191
15471
  }
@@ -15201,7 +15481,7 @@ var Notifier = class {
15201
15481
  if (!config.url) continue;
15202
15482
  const subscribedEvents = config.events;
15203
15483
  const matchingEvents = events.filter((e) => subscribedEvents.includes(e));
15204
- log19.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
15484
+ log20.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
15205
15485
  if (matchingEvents.length === 0) continue;
15206
15486
  for (const event of matchingEvents) {
15207
15487
  const relevantTransitions = event === "citation.lost" ? lostTransitions : event === "citation.gained" ? gainedTransitions : transitions;
@@ -15233,7 +15513,7 @@ var Notifier = class {
15233
15513
  async onHealthChecked(projectId, report) {
15234
15514
  const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
15235
15515
  if (!project) {
15236
- log19.error("project.not-found", { projectId, msg: "skipping health notification" });
15516
+ log20.error("project.not-found", { projectId, msg: "skipping health notification" });
15237
15517
  return null;
15238
15518
  }
15239
15519
  const rank = (status2) => status2 === "fail" ? 2 : status2 === "warn" ? 1 : 0;
@@ -15285,7 +15565,7 @@ var Notifier = class {
15285
15565
  this.db.update(doctorHealthState).set(observation).where(eq19(doctorHealthState.projectId, projectId)).run();
15286
15566
  }
15287
15567
  if (!event) {
15288
- log19.info("health.unchanged", { projectId, status, code });
15568
+ log20.info("health.unchanged", { projectId, status, code });
15289
15569
  return null;
15290
15570
  }
15291
15571
  const notifs = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled);
@@ -15313,7 +15593,7 @@ var Notifier = class {
15313
15593
  if (delivered > 0) {
15314
15594
  this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq19(doctorHealthState.projectId, projectId)).run();
15315
15595
  }
15316
- log19.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
15596
+ log20.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
15317
15597
  return event;
15318
15598
  }
15319
15599
  /**
@@ -15334,7 +15614,7 @@ var Notifier = class {
15334
15614
  if (check.status !== "ok" && check.status !== "fail") return null;
15335
15615
  const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
15336
15616
  if (!project) {
15337
- log19.error("project.not-found", { projectId, msg: "skipping site liveness notification" });
15617
+ log20.error("project.not-found", { projectId, msg: "skipping site liveness notification" });
15338
15618
  return null;
15339
15619
  }
15340
15620
  const previous = this.db.select().from(siteLivenessState).where(eq19(siteLivenessState.projectId, projectId)).get();
@@ -15388,7 +15668,7 @@ var Notifier = class {
15388
15668
  if (event === "health.degraded" && delivered > 0) {
15389
15669
  this.db.update(siteLivenessState).set({ notifiedAt: checkedAt }).where(eq19(siteLivenessState.projectId, projectId)).run();
15390
15670
  }
15391
- log19.info("site-liveness.notified", { projectId, event, code: check.code, subscribers: notifs.length, delivered });
15671
+ log20.info("site-liveness.notified", { projectId, event, code: check.code, subscribers: notifs.length, delivered });
15392
15672
  return event;
15393
15673
  }
15394
15674
  /**
@@ -15625,7 +15905,7 @@ var Notifier = class {
15625
15905
  delivered += accepted;
15626
15906
  }
15627
15907
  }
15628
- log19.info("reviews.dispatched", {
15908
+ log20.info("reviews.dispatched", {
15629
15909
  projectId,
15630
15910
  reviews: reviewsToSend.length,
15631
15911
  ratingDrops: ratingsToSend.length,
@@ -15638,7 +15918,7 @@ var Notifier = class {
15638
15918
  if (!thisRun) return [];
15639
15919
  const completeness = measurementRunCompleteness(this.db, runId);
15640
15920
  if (completeness.planned && !completeness.complete) {
15641
- log19.info("transitions.skipped-incomplete", {
15921
+ log20.info("transitions.skipped-incomplete", {
15642
15922
  runId,
15643
15923
  executed: completeness.executed,
15644
15924
  expected: completeness.expected
@@ -15730,23 +16010,23 @@ var Notifier = class {
15730
16010
  const targetLabel = redactNotificationUrl(url).urlDisplay;
15731
16011
  const targetCheck = await resolveWebhookTarget(url);
15732
16012
  if (!targetCheck.ok) {
15733
- log19.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
16013
+ log20.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
15734
16014
  this.logDelivery(projectId, notificationId, payload.event, "failed", `SSRF: ${targetCheck.message}`);
15735
16015
  return false;
15736
16016
  }
15737
- log19.info("webhook.send", { event: payload.event, url: targetLabel });
16017
+ log20.info("webhook.send", { event: payload.event, url: targetLabel });
15738
16018
  const maxRetries = 3;
15739
16019
  const delays = [1e3, 4e3, 16e3];
15740
16020
  for (let attempt = 0; attempt < maxRetries; attempt++) {
15741
16021
  try {
15742
16022
  const response = await deliverWebhook(targetCheck.target, body, signingSecret);
15743
16023
  if (response.status >= 200 && response.status < 300) {
15744
- log19.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
16024
+ log20.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
15745
16025
  this.logDelivery(projectId, notificationId, payload.event, "sent", null);
15746
16026
  return true;
15747
16027
  }
15748
16028
  const errorDetail = response.error ?? `HTTP ${response.status}`;
15749
- log19.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
16029
+ log20.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
15750
16030
  if (attempt === maxRetries - 1) {
15751
16031
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
15752
16032
  }
@@ -15754,7 +16034,7 @@ var Notifier = class {
15754
16034
  const errorDetail = describeError(err);
15755
16035
  if (attempt === maxRetries - 1) {
15756
16036
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
15757
- log19.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
16037
+ log20.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
15758
16038
  }
15759
16039
  }
15760
16040
  if (attempt < maxRetries - 1) {
@@ -15779,7 +16059,7 @@ var Notifier = class {
15779
16059
 
15780
16060
  // src/run-coordinator.ts
15781
16061
  import { eq as eq20 } from "drizzle-orm";
15782
- var log20 = createLogger("RunCoordinator");
16062
+ var log21 = createLogger("RunCoordinator");
15783
16063
  var RunCoordinator = class {
15784
16064
  constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
15785
16065
  this.db = db;
@@ -15795,16 +16075,31 @@ var RunCoordinator = class {
15795
16075
  onAeroEvent;
15796
16076
  /** Host wakeup only; durable completion receipts remain the delivery authority. */
15797
16077
  onSentimentCompleted;
16078
+ /**
16079
+ * Host hook after a non-probe answer-visibility run finishes `completed` or
16080
+ * `partial`: the run stored new answers, so answer-derived competitor
16081
+ * aliases may have new evidence. The local server schedules a detection pass
16082
+ * off the completion path (`createCompetitorAutoAliasRunner`). Failures are
16083
+ * logged and never reach the other subscribers.
16084
+ */
16085
+ onAnswersRecorded;
15798
16086
  async onRunCompleted(runId, projectId, opts) {
15799
16087
  const runRow = this.db.select().from(runs).where(eq20(runs.id, runId)).get();
15800
16088
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
15801
16089
  if (runRow?.trigger === RunTriggers.probe) {
15802
- log20.info("probe.skip-side-effects", { runId, projectId, kind });
16090
+ log21.info("probe.skip-side-effects", { runId, projectId, kind });
15803
16091
  return;
15804
16092
  }
16093
+ if (kind === RunKinds["answer-visibility"] && (runRow?.status === RunStatuses.completed || runRow?.status === RunStatuses.partial) && this.onAnswersRecorded) {
16094
+ try {
16095
+ await this.onAnswersRecorded(runId, projectId);
16096
+ } catch (err) {
16097
+ log21.error("answers-recorded.failed", { runId, projectId, error: describeError(err) });
16098
+ }
16099
+ }
15805
16100
  const completeness = measurementRunCompleteness(this.db, runId);
15806
16101
  if (completeness.planned && !completeness.complete) {
15807
- log20.info("measurement.incomplete-skip-derived", {
16102
+ log21.info("measurement.incomplete-skip-derived", {
15808
16103
  runId,
15809
16104
  projectId,
15810
16105
  executed: completeness.executed,
@@ -15813,7 +16108,7 @@ var RunCoordinator = class {
15813
16108
  try {
15814
16109
  await this.notifier.onRunCompleted(runId, projectId);
15815
16110
  } catch (err) {
15816
- log20.error("notifier.failed", { runId, error: describeError(err) });
16111
+ log21.error("notifier.failed", { runId, error: describeError(err) });
15817
16112
  }
15818
16113
  return;
15819
16114
  }
@@ -15821,7 +16116,7 @@ var RunCoordinator = class {
15821
16116
  try {
15822
16117
  await this.onSentimentCompleted(runId, projectId);
15823
16118
  } catch {
15824
- log20.error("sentiment.wakeup-failed", { runId, projectId });
16119
+ log21.error("sentiment.wakeup-failed", { runId, projectId });
15825
16120
  }
15826
16121
  }
15827
16122
  let insightCount = 0;
@@ -15838,12 +16133,12 @@ var RunCoordinator = class {
15838
16133
  try {
15839
16134
  await this.onInsightsGenerated(runId, projectId, result2);
15840
16135
  } catch (err) {
15841
- log20.error("insight-webhook.failed", { runId, error: describeError(err) });
16136
+ log21.error("insight-webhook.failed", { runId, error: describeError(err) });
15842
16137
  }
15843
16138
  }
15844
16139
  }
15845
16140
  } catch (err) {
15846
- log20.error("intelligence.failed", { runId, error: describeError(err) });
16141
+ log21.error("intelligence.failed", { runId, error: describeError(err) });
15847
16142
  }
15848
16143
  } else if (kind === RunKinds["gbp-sync"]) {
15849
16144
  try {
@@ -15856,22 +16151,22 @@ var RunCoordinator = class {
15856
16151
  try {
15857
16152
  await this.onInsightsGenerated(runId, projectId, analysisResultFromInsights(gbpInsights));
15858
16153
  } catch (err) {
15859
- log20.error("gbp-insight-webhook.failed", { runId, error: describeError(err) });
16154
+ log21.error("gbp-insight-webhook.failed", { runId, error: describeError(err) });
15860
16155
  }
15861
16156
  }
15862
16157
  } catch (err) {
15863
- log20.error("gbp-intelligence.failed", { runId, error: describeError(err) });
16158
+ log21.error("gbp-intelligence.failed", { runId, error: describeError(err) });
15864
16159
  }
15865
16160
  try {
15866
16161
  await this.notifier.dispatchReviewAlerts(projectId);
15867
16162
  } catch (err) {
15868
- log20.error("review-webhook.failed", { runId, error: describeError(err) });
16163
+ log21.error("review-webhook.failed", { runId, error: describeError(err) });
15869
16164
  }
15870
16165
  }
15871
16166
  try {
15872
16167
  await (opts ? this.notifier.onRunCompleted(runId, projectId, opts) : this.notifier.onRunCompleted(runId, projectId));
15873
16168
  } catch (err) {
15874
- log20.error("notifier.failed", { runId, error: describeError(err) });
16169
+ log21.error("notifier.failed", { runId, error: describeError(err) });
15875
16170
  }
15876
16171
  if (this.onAeroEvent) {
15877
16172
  try {
@@ -15884,7 +16179,7 @@ var RunCoordinator = class {
15884
16179
  };
15885
16180
  await this.onAeroEvent(ctx);
15886
16181
  } catch (err) {
15887
- log20.error("aero.failed", { runId, error: describeError(err) });
16182
+ log21.error("aero.failed", { runId, error: describeError(err) });
15888
16183
  }
15889
16184
  }
15890
16185
  }
@@ -16289,7 +16584,7 @@ function responseSchemasFor(operations) {
16289
16584
  return schema ? [schema] : [];
16290
16585
  });
16291
16586
  }
16292
- function isRecord4(value) {
16587
+ function isRecord7(value) {
16293
16588
  return value !== null && typeof value === "object" && !Array.isArray(value);
16294
16589
  }
16295
16590
  function resolve(schema, components) {
@@ -16322,7 +16617,7 @@ function render(schema, value, components, depth) {
16322
16617
  }
16323
16618
  let current = value;
16324
16619
  for (const member of members(node)) current = render(member, current, components, depth + 1);
16325
- if (Array.isArray(current) && isRecord4(node.items)) {
16620
+ if (Array.isArray(current) && isRecord7(node.items)) {
16326
16621
  const items = node.items;
16327
16622
  let changed = false;
16328
16623
  const out = current.map((item) => {
@@ -16332,9 +16627,9 @@ function render(schema, value, components, depth) {
16332
16627
  });
16333
16628
  return changed ? out : current;
16334
16629
  }
16335
- if (isRecord4(current)) {
16336
- const properties = isRecord4(node.properties) ? node.properties : {};
16337
- const extra = isRecord4(node.additionalProperties) ? node.additionalProperties : void 0;
16630
+ if (isRecord7(current)) {
16631
+ const properties = isRecord7(node.properties) ? node.properties : {};
16632
+ const extra = isRecord7(node.additionalProperties) ? node.additionalProperties : void 0;
16338
16633
  let changed = false;
16339
16634
  const out = {};
16340
16635
  for (const [key, child] of Object.entries(current)) {
@@ -16380,7 +16675,7 @@ var MAX_PARTIAL_NOTES = 12;
16380
16675
  function serializeResult(value) {
16381
16676
  return JSON.stringify(value);
16382
16677
  }
16383
- function isRecord5(value) {
16678
+ function isRecord8(value) {
16384
16679
  return !!value && typeof value === "object" && !Array.isArray(value);
16385
16680
  }
16386
16681
  function keptSummary(counts, cursors = {}) {
@@ -16400,7 +16695,7 @@ function noteCursor(owner, ownerPath, listPath, kept, total, notes, root = owner
16400
16695
  }
16401
16696
  function cursorForList(owner, ownerPath, listKey, root = owner) {
16402
16697
  const prefix = ownerPath ? `${ownerPath}.` : "";
16403
- if (listKey === "queries" && isRecord5(owner.queryPage) && owner.queryPage.nextCursor !== void 0 && owner.queryPage.nextCursor !== null) {
16698
+ if (listKey === "queries" && isRecord8(owner.queryPage) && owner.queryPage.nextCursor !== void 0 && owner.queryPage.nextCursor !== null) {
16404
16699
  const page = owner.queryPage;
16405
16700
  return {
16406
16701
  path: `${prefix}queryPage.nextCursor`,
@@ -16466,7 +16761,7 @@ function partialLists(root) {
16466
16761
  if (uncounted.length === 1) {
16467
16762
  const index = uncounted[0];
16468
16763
  const key = lists[index][0];
16469
- const paged = cursorForList(object, path9, key, root) || typeof object.truncated === "boolean" || typeof object[`${key}Truncated`] === "boolean" || ["items", "rows", "results"].includes(key);
16764
+ const paged = object === root || cursorForList(object, path9, key, root) || typeof object.truncated === "boolean" || typeof object[`${key}Truncated`] === "boolean" || ["items", "rows", "results"].includes(key);
16470
16765
  if (paged) counts[index] = bareTotal(numbers);
16471
16766
  }
16472
16767
  for (const [index, [key, rows]] of lists.entries()) {
@@ -16477,7 +16772,7 @@ function partialLists(root) {
16477
16772
  if (count3.count <= rows.length) continue;
16478
16773
  notes.push([label, `${rows.length} of ${count3.text}`]);
16479
16774
  noted = true;
16480
- } else if (ownFlag === true || object.truncated === true && typeof ownFlag !== "boolean" && (lists.length === 1 || rows.some(isRecord5))) {
16775
+ } else if (ownFlag === true || object.truncated === true && typeof ownFlag !== "boolean" && (lists.length === 1 || rows.some(isRecord8))) {
16481
16776
  notes.push([label, `${rows.length} shown; the tool cut this list`]);
16482
16777
  noted = true;
16483
16778
  }
@@ -16486,17 +16781,17 @@ function partialLists(root) {
16486
16781
  if (object.truncated === true && !noted) notes.push([path9 || "(root)", "the tool cut the lists here"]);
16487
16782
  if (depth >= MAX_PARTIAL_DEPTH || notes.length >= MAX_PARTIAL_NOTES) return;
16488
16783
  for (const [key, value] of Object.entries(object)) {
16489
- if (!key.startsWith("__") && isRecord5(value)) visit(value, path9 ? `${path9}.${key}` : key, depth + 1);
16784
+ if (!key.startsWith("__") && isRecord8(value)) visit(value, path9 ? `${path9}.${key}` : key, depth + 1);
16490
16785
  }
16491
16786
  };
16492
16787
  visit(root, "", 1);
16493
16788
  return notes.length > 0 ? Object.fromEntries(notes.slice(0, MAX_PARTIAL_NOTES)) : void 0;
16494
16789
  }
16495
16790
  function withPartialLists(value) {
16496
- if (!isRecord5(value) || typeof value.toJSON === "function") return value;
16791
+ if (!isRecord8(value) || typeof value.toJSON === "function") return value;
16497
16792
  const hadNote = Object.hasOwn(value, PARTIAL_LISTS_KEY);
16498
16793
  const notes = partialLists(value);
16499
- if (!notes && !hadNote && value.nextAttemptCursor == null && !CURSOR_KEYS.some((key) => value[key] !== void 0 && value[key] !== null) && !Object.values(value).some((child) => isRecord5(child) && CURSOR_KEYS.some((key) => child[key] !== void 0 && child[key] !== null))) return value;
16794
+ if (!notes && !hadNote && value.nextAttemptCursor == null && !CURSOR_KEYS.some((key) => value[key] !== void 0 && value[key] !== null) && !Object.values(value).some((child) => isRecord8(child) && CURSOR_KEYS.some((key) => child[key] !== void 0 && child[key] !== null))) return value;
16500
16795
  const pagination = {};
16501
16796
  const visit = (owner, path9, depth) => {
16502
16797
  const lists = Object.entries(owner).filter((entry) => Array.isArray(entry[1]));
@@ -16506,11 +16801,11 @@ function withPartialLists(value) {
16506
16801
  if (!cursor || key.startsWith("__") || Object.keys(pagination).length >= MAX_PARTIAL_NOTES) continue;
16507
16802
  const label = path9 ? `${path9}.${key}` : key;
16508
16803
  const total = cursor.total ?? ownListTotal(numbers, key) ?? (lists.length === 1 ? bareTotal(numbers) : void 0);
16509
- const incomplete = owner.__truncated === true || isRecord5(owner.__omittedRowsByField) && typeof owner.__omittedRowsByField[key] === "number";
16804
+ const incomplete = owner.__truncated === true || isRecord8(owner.__omittedRowsByField) && typeof owner.__omittedRowsByField[key] === "number";
16510
16805
  pagination[label] = `showing ${rows.length}${total ? ` of ${total.text}` : " rows"}; ${incomplete ? `incomplete page: re-request the original cursor with a smaller ${cursor.limit}; the next cursor skips omitted rows` : `pass ${cursor.parameter} ${JSON.stringify(cursor.value)}`}`;
16511
16806
  }
16512
16807
  if (depth >= MAX_PARTIAL_DEPTH) return;
16513
- for (const [key, child] of Object.entries(owner)) if (!key.startsWith("__") && isRecord5(child)) visit(child, path9 ? `${path9}.${key}` : key, depth + 1);
16808
+ for (const [key, child] of Object.entries(owner)) if (!key.startsWith("__") && isRecord8(child)) visit(child, path9 ? `${path9}.${key}` : key, depth + 1);
16514
16809
  };
16515
16810
  visit(value, "", 1);
16516
16811
  const { [PARTIAL_LISTS_KEY]: _previous, __pagination: _pagination, ...rest } = value;
@@ -16563,7 +16858,7 @@ function listTrimmer(lists) {
16563
16858
  hadTruncated: Object.hasOwn(owner, "__truncated"),
16564
16859
  priorTruncated: owner.__truncated,
16565
16860
  previous,
16566
- priorOmitted: isRecord5(previous) ? previous : void 0
16861
+ priorOmitted: isRecord8(previous) ? previous : void 0
16567
16862
  };
16568
16863
  });
16569
16864
  return (counts) => {
@@ -16599,12 +16894,12 @@ function collectCollections(root) {
16599
16894
  if (Array.isArray(value)) {
16600
16895
  let records = false;
16601
16896
  for (const row of value) {
16602
- if (!isRecord5(row)) continue;
16897
+ if (!isRecord8(row)) continue;
16603
16898
  records = true;
16604
16899
  visit(row, `${valuePath}[]`, true);
16605
16900
  }
16606
16901
  if (value.length > 0) found.push({ path: valuePath, nested, records, owner: object, key, rows: value });
16607
- } else if (isRecord5(value)) {
16902
+ } else if (isRecord8(value)) {
16608
16903
  visit(value, valuePath, nested);
16609
16904
  }
16610
16905
  }
@@ -16641,8 +16936,8 @@ function currentSummary(root, owned) {
16641
16936
  counts.set(list.path, count3);
16642
16937
  noteCursor(object, path9, list.path, value.length, list.rows.length, cursors, root);
16643
16938
  }
16644
- for (const row of value) if (isRecord5(row)) visit(row, `${valuePath}[]`);
16645
- } else if (isRecord5(value)) {
16939
+ for (const row of value) if (isRecord8(row)) visit(row, `${valuePath}[]`);
16940
+ } else if (isRecord8(value)) {
16646
16941
  visit(value, valuePath);
16647
16942
  }
16648
16943
  }
@@ -16655,7 +16950,7 @@ function emptiedFloorChars(root) {
16655
16950
  }
16656
16951
  function trimNestedArrays(full) {
16657
16952
  const copy = JSON.parse(full);
16658
- if (!isRecord5(copy)) return void 0;
16953
+ if (!isRecord8(copy)) return void 0;
16659
16954
  copy.__truncated = true;
16660
16955
  copy[TRUNCATION_SUMMARY_KEY] = keptSummary(/* @__PURE__ */ new Map());
16661
16956
  if (emptiedFloorChars(copy) > MAX_TOOL_RESULT_CHARS) return void 0;
@@ -16726,7 +17021,7 @@ function trimNestedArrays(full) {
16726
17021
  function keptRowTotal(serialized) {
16727
17022
  const count3 = (value) => {
16728
17023
  if (Array.isArray(value)) return value.length;
16729
- if (!isRecord5(value)) return 0;
17024
+ if (!isRecord8(value)) return 0;
16730
17025
  return Object.entries(value).reduce((sum, [key, child]) => key.startsWith("__") ? sum : sum + count3(child), 0);
16731
17026
  };
16732
17027
  try {
@@ -16738,21 +17033,21 @@ function keptRowTotal(serialized) {
16738
17033
  function hasOtherLists(value, primary) {
16739
17034
  if (Array.isArray(value)) {
16740
17035
  if (value !== primary && value.length > 0) return true;
16741
- return value.some((row) => (isRecord5(row) || Array.isArray(row)) && hasOtherLists(row, primary));
17036
+ return value.some((row) => (isRecord8(row) || Array.isArray(row)) && hasOtherLists(row, primary));
16742
17037
  }
16743
- return isRecord5(value) && Object.values(value).some((child) => hasOtherLists(child, primary));
17038
+ return isRecord8(value) && Object.values(value).some((child) => hasOtherLists(child, primary));
16744
17039
  }
16745
17040
  function projectionPath(path9) {
16746
17041
  if (path9.length <= MAX_SUMMARY_PATH_CHARS) return path9;
16747
17042
  const digest = createHash5("sha256").update(path9).digest("hex").slice(0, 12);
16748
17043
  return `${truncateUtf16(path9, 48)}...#${digest}${path9.slice(-24)}`;
16749
17044
  }
16750
- function boundedProjection(full) {
17045
+ function boundedProjection(full, partialRows, reserve = 0) {
16751
17046
  const value = JSON.parse(full);
16752
- const summary = {
17047
+ let summary = {
16753
17048
  droppedKeys: [],
16754
17049
  keptItems: {},
16755
- projection: "Only complete array rows and marked partial string prefixes are shown; omitted fields are not evidence. Never follow a cursor from a cut page; re-request the original cursor with a smaller limit."
17050
+ projection: partialRows ? "Array rows are complete unless slicedKeys or droppedKeys name a field inside one; sliced strings are partial prefixes and omitted fields are not evidence. Never follow a cursor from a cut page; re-request the original cursor with a smaller limit." : "Only complete array rows and marked partial string prefixes are shown; omitted fields are not evidence. Never follow a cursor from a cut page; re-request the original cursor with a smaller limit."
16756
17051
  };
16757
17052
  const skippedMetadata = () => {
16758
17053
  summary.moreMetadataEntries = (summary.moreMetadataEntries ?? 0) + 1;
@@ -16778,6 +17073,7 @@ function boundedProjection(full) {
16778
17073
  }
16779
17074
  if (dropped > summary.droppedKeys.length) summary.moreDroppedKeys = dropped - summary.droppedKeys.length;
16780
17075
  };
17076
+ const cutOwners = /* @__PURE__ */ new Set();
16781
17077
  const project = (input, budget, path9, owner, ownerPath = "") => {
16782
17078
  if (Array.isArray(input)) {
16783
17079
  const rows = [];
@@ -16785,16 +17081,31 @@ function boundedProjection(full) {
16785
17081
  for (const row of input) {
16786
17082
  const text = serializeResult(row);
16787
17083
  const cost = text.length + (rows.length > 0 ? 1 : 0);
16788
- if (used2 + cost > budget) break;
16789
- rows.push(text);
16790
- used2 += cost;
17084
+ if (used2 + cost <= budget) {
17085
+ rows.push(text);
17086
+ used2 += cost;
17087
+ continue;
17088
+ }
17089
+ if (partialRows && (rows.length === 0 || text.length > MAX_TOOL_RESULT_CHARS)) {
17090
+ const before = { summary: serializeResult(summary), dropped };
17091
+ const partial = project(row, budget - used2 - (rows.length > 0 ? 1 : 0), `${path9 || "items"}[${rows.length}]`);
17092
+ if (partial !== void 0 && partial !== "{}" && partial !== "[]") {
17093
+ partialRows.set(path9 || "items", rows.length);
17094
+ rows.push(partial);
17095
+ } else {
17096
+ summary = JSON.parse(before.summary);
17097
+ dropped = before.dropped;
17098
+ }
17099
+ }
17100
+ break;
16791
17101
  }
16792
17102
  if (rows.length < input.length) {
17103
+ if (partialRows && owner) cutOwners.add(owner);
16793
17104
  record2(summary.keptItems, path9 || "items", `${rows.length} of ${input.length}`);
16794
17105
  if (rows.length === 0) omit(path9 || "items");
16795
17106
  if (owner) {
16796
17107
  const cursors = {};
16797
- noteCursor(owner, ownerPath, path9, rows.length, input.length, cursors, isRecord5(value) ? value : owner);
17108
+ noteCursor(owner, ownerPath, path9, rows.length, input.length, cursors, isRecord8(value) ? value : owner);
16798
17109
  for (const [cursor, note] of Object.entries(cursors)) {
16799
17110
  record2(summary.cursors ??= {}, cursor, note.replace(path9, projectionPath(path9)));
16800
17111
  }
@@ -16802,7 +17113,7 @@ function boundedProjection(full) {
16802
17113
  }
16803
17114
  return `[${rows.join(",")}]`;
16804
17115
  }
16805
- if (!isRecord5(input)) {
17116
+ if (!isRecord8(input)) {
16806
17117
  const text = serializeResult(input);
16807
17118
  if (text.length <= budget) return text;
16808
17119
  if (typeof input === "string" && budget > 2) {
@@ -16813,9 +17124,8 @@ function boundedProjection(full) {
16813
17124
  omit(path9 || "(root)");
16814
17125
  return void 0;
16815
17126
  }
16816
- const fields = Object.entries(input).filter(([key]) => !key.startsWith("__"));
16817
- const priority = ([key, child]) => key === "queryPage" && isRecord5(child) ? 0 : typeof child === "string" && child.length > budget ? 2 : Number(typeof child === "object" && child !== null);
16818
- fields.sort((a, b) => priority(a) - priority(b));
17127
+ const priority = ([key, child]) => key === "queryPage" && isRecord8(child) ? 0 : typeof child === "string" && child.length > budget ? 2 : typeof child !== "object" || child === null ? 0 : partialRows && serializeResult(child).length > budget ? 2 : 1;
17128
+ const fields = Object.entries(input).filter(([key]) => !key.startsWith("__")).map((field) => ({ field, rank: priority(field) })).sort((a, b) => a.rank - b.rank).map(({ field }) => field);
16819
17129
  const shown2 = [];
16820
17130
  let used = 2;
16821
17131
  for (const [key, child] of fields) {
@@ -16831,17 +17141,40 @@ function boundedProjection(full) {
16831
17141
  shown2.push(prefix + text);
16832
17142
  used += prefix.length + text.length + (shown2.length > 1 ? 1 : 0);
16833
17143
  }
17144
+ if (path9 && cutOwners.has(input)) shown2.push('"__truncated":true');
16834
17145
  return `{${shown2.join(",")}}`;
16835
17146
  };
16836
- const projected = project(value, MAX_TOOL_RESULT_CHARS - PROJECTION_SUMMARY_CHARS, "");
17147
+ const projected = project(value, MAX_TOOL_RESULT_CHARS - PROJECTION_SUMMARY_CHARS - reserve, "");
16837
17148
  const shown = projected === void 0 ? {} : JSON.parse(projected);
16838
- const output = isRecord5(shown) ? shown : { items: shown };
16839
- return serializeResult({ ...output, __truncated: true, [TRUNCATION_SUMMARY_KEY]: summary });
16840
- }
16841
- function truncateToolResult(details) {
17149
+ const output = isRecord8(shown) ? shown : { items: shown };
17150
+ const result2 = { ...output, __truncated: true, [TRUNCATION_SUMMARY_KEY]: summary };
17151
+ return serializeResult(partialRows ? withPartialLists(result2) : result2);
17152
+ }
17153
+ function rowsShown(cut, path9) {
17154
+ if (path9.includes("[")) return Infinity;
17155
+ const list = path9.split(".").reduce((owner, key) => isRecord8(owner) ? owner[key] : void 0, cut);
17156
+ return Array.isArray(list) ? list.length : 0;
17157
+ }
17158
+ function withPartialRows(full, whole) {
17159
+ const rescued = /* @__PURE__ */ new Map();
17160
+ let partial = boundedProjection(full, rescued);
17161
+ if (!fitsCap(partial)) {
17162
+ const { [PARTIAL_LISTS_KEY]: lists, __pagination: pagination } = JSON.parse(partial);
17163
+ rescued.clear();
17164
+ partial = boundedProjection(full, rescued, serializeResult({ lists, pagination }).length);
17165
+ }
17166
+ if (!fitsCap(partial)) return whole;
17167
+ const cut = JSON.parse(whole);
17168
+ return [...rescued].some(([path9, index]) => rowsShown(cut, path9) <= index) ? partial : whole;
17169
+ }
17170
+ function truncateToolResult(details, options = {}) {
16842
17171
  const annotated = withPartialLists(details);
16843
17172
  const full = serializeResult(annotated);
16844
17173
  if (full.length <= MAX_TOOL_RESULT_CHARS) return full;
17174
+ const whole = truncateWholeRows(details, full);
17175
+ return options.partialRows ? withPartialRows(full, whole) : whole;
17176
+ }
17177
+ function truncateWholeRows(details, full) {
16845
17178
  if (full.length > MAX_STRUCTURED_TRUNCATION_CHARS) return boundedProjection(full);
16846
17179
  if (Array.isArray(details)) {
16847
17180
  const render2 = (rows) => serializeResult({
@@ -16961,6 +17294,12 @@ var AERO_EXCLUDED_MCP_TOOLS = /* @__PURE__ */ new Set([
16961
17294
  // start paid classifier work and remain operator actions.
16962
17295
  "canonry_sentiment_configure",
16963
17296
  "canonry_sentiment_backfill",
17297
+ // Applying answer-derived competitor names changes the measured competitor
17298
+ // identity and restates stored history. Aero may preview detection
17299
+ // (`canonry_competitors_auto_aliases_detect`); applying stays an operator action.
17300
+ // The project writes that set the mode refuse a change instead; see
17301
+ // `refuseCompetitorAutoAliasModeChange`.
17302
+ CanonryMcpToolNames.canonry_competitors_auto_aliases_apply,
16964
17303
  CanonryMcpToolNames.canonry_agent_clear,
16965
17304
  CanonryMcpToolNames.canonry_agent_conversations_new,
16966
17305
  CanonryMcpToolNames.canonry_agent_conversations_resume,
@@ -16989,9 +17328,48 @@ function refuseManagedSweepCancel(tool, ctx) {
16989
17328
  }
16990
17329
  };
16991
17330
  }
17331
+ var COMPETITOR_AUTO_ALIAS_MODE_REFUSAL = "Aero cannot change a project's competitorAutoAliases mode. Omit competitorAutoAliases to keep the stored mode, or ask the operator to change it.";
17332
+ var COMPETITOR_AUTO_ALIAS_MODE_WRITES = /* @__PURE__ */ new Set([
17333
+ CanonryMcpToolNames.canonry_project_upsert,
17334
+ CanonryMcpToolNames.canonry_apply_config
17335
+ ]);
17336
+ function requestedCompetitorAutoAliasMode(toolName, params, ctx) {
17337
+ if (!isRecord8(params)) return { project: void 0, mode: void 0 };
17338
+ if (toolName === CanonryMcpToolNames.canonry_project_upsert) {
17339
+ const request = isRecord8(params.request) ? params.request : {};
17340
+ return { project: ctx.projectName, mode: request.competitorAutoAliases };
17341
+ }
17342
+ const config = isRecord8(params.config) ? params.config : {};
17343
+ const metadata2 = isRecord8(config.metadata) ? config.metadata : {};
17344
+ const spec = isRecord8(config.spec) ? config.spec : {};
17345
+ return { project: metadata2.name, mode: spec.competitorAutoAliases };
17346
+ }
17347
+ async function storedCompetitorAutoAliasMode(client, project) {
17348
+ if (typeof project !== "string") return DEFAULT_COMPETITOR_AUTO_ALIAS_MODE;
17349
+ try {
17350
+ return (await client.getProject(project)).competitorAutoAliases ?? DEFAULT_COMPETITOR_AUTO_ALIAS_MODE;
17351
+ } catch (err) {
17352
+ if (err instanceof CliError && err.code === "NOT_FOUND") return DEFAULT_COMPETITOR_AUTO_ALIAS_MODE;
17353
+ throw err;
17354
+ }
17355
+ }
17356
+ function refuseCompetitorAutoAliasModeChange(tool, ctx) {
17357
+ const write = tool.execute;
17358
+ return {
17359
+ ...tool,
17360
+ execute: async (toolCallId, params, ...rest) => {
17361
+ const { project, mode } = requestedCompetitorAutoAliasMode(tool.name, params, ctx);
17362
+ if (mode !== void 0 && mode !== await storedCompetitorAutoAliasMode(ctx.client, project)) {
17363
+ throw new Error(COMPETITOR_AUTO_ALIAS_MODE_REFUSAL);
17364
+ }
17365
+ return write(toolCallId, params, ...rest);
17366
+ }
17367
+ };
17368
+ }
16992
17369
  function buildMcpAgentTools(registry, ctx, opts = {}) {
16993
17370
  return registry.filter((tool) => !AERO_EXCLUDED_MCP_TOOLS.has(tool.name)).filter((tool) => opts.includeNames ? opts.includeNames.has(tool.name) : true).filter((tool) => opts.readOnly ? tool.access === "read" : true).filter((tool) => opts.managedSweeps ? !AERO_MANAGED_SWEEP_MCP_TOOLS.has(tool.name) : true).map((tool) => {
16994
17371
  const agentTool = mcpToAgentTool(tool, ctx);
17372
+ if (COMPETITOR_AUTO_ALIAS_MODE_WRITES.has(tool.name)) return refuseCompetitorAutoAliasModeChange(agentTool, ctx);
16995
17373
  return opts.managedSweeps && tool.name === CanonryMcpToolNames.canonry_run_cancel ? refuseManagedSweepCancel(agentTool, ctx) : agentTool;
16996
17374
  });
16997
17375
  }
@@ -17322,11 +17700,11 @@ function recordLlmUsageEvent(args) {
17322
17700
  var AERO_MEMORY_SEPARATOR = "\n\n---\n\n";
17323
17701
  var AERO_MEMORY_BLOCK_PREFIX = `${AERO_MEMORY_SEPARATOR}<memory>
17324
17702
  `;
17325
- function isRecord6(value) {
17703
+ function isRecord9(value) {
17326
17704
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
17327
17705
  }
17328
17706
  function isTextSystemBlock(value) {
17329
- return isRecord6(value) && value.type === "text" && typeof value.text === "string";
17707
+ return isRecord9(value) && value.type === "text" && typeof value.text === "string";
17330
17708
  }
17331
17709
  function splitAeroHydratedSystemPrompt(systemPrompt) {
17332
17710
  const idx = systemPrompt.lastIndexOf(AERO_MEMORY_BLOCK_PREFIX);
@@ -17338,7 +17716,7 @@ function splitAeroHydratedSystemPrompt(systemPrompt) {
17338
17716
  }
17339
17717
  function splitAeroAnthropicSystemCachePayload(payload, model) {
17340
17718
  if (model.api !== "anthropic-messages") return void 0;
17341
- if (!isRecord6(payload) || !Array.isArray(payload.system)) return void 0;
17719
+ if (!isRecord9(payload) || !Array.isArray(payload.system)) return void 0;
17342
17720
  let changed = false;
17343
17721
  const nextSystem = [];
17344
17722
  for (const block of payload.system) {
@@ -18098,7 +18476,7 @@ function resolveSessionProviderAndModel(config, opts) {
18098
18476
  import { Type as Type6 } from "@earendil-works/pi-ai";
18099
18477
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
18100
18478
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
18101
- var log21 = createLogger("RemoteMcp");
18479
+ var log22 = createLogger("RemoteMcp");
18102
18480
  var AERO_EXCLUDED_MCP_TOOLS2 = /* @__PURE__ */ new Set([]);
18103
18481
  async function connectStreamableHttp(server) {
18104
18482
  const transport = new StreamableHTTPClientTransport(new URL(server.url), {
@@ -18141,7 +18519,8 @@ function adaptRemoteTool(client, tool) {
18141
18519
  }
18142
18520
  }
18143
18521
  return {
18144
- content: [{ type: "text", text: truncateToolResult(shown) }],
18522
+ // A fetched document can be one row larger than the cap; show it in part rather than not at all.
18523
+ content: [{ type: "text", text: truncateToolResult(shown, { partialRows: true }) }],
18145
18524
  details: result2,
18146
18525
  isError: isRemoteToolError(result2)
18147
18526
  };
@@ -18165,7 +18544,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
18165
18544
  try {
18166
18545
  client = await connect(server);
18167
18546
  } catch (err) {
18168
- log21.error("external-mcp.connect-failed", {
18547
+ log22.error("external-mcp.connect-failed", {
18169
18548
  label,
18170
18549
  error: describeError(err)
18171
18550
  });
@@ -18175,7 +18554,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
18175
18554
  try {
18176
18555
  listed = await client.listTools();
18177
18556
  } catch (err) {
18178
- log21.error("external-mcp.list-failed", {
18557
+ log22.error("external-mcp.list-failed", {
18179
18558
  label,
18180
18559
  error: describeError(err)
18181
18560
  });
@@ -18186,7 +18565,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
18186
18565
  if (!isReadOnly(tool)) continue;
18187
18566
  tools.push(adaptRemoteTool(client, tool));
18188
18567
  }
18189
- log21.info("external-mcp.loaded", {
18568
+ log22.info("external-mcp.loaded", {
18190
18569
  label,
18191
18570
  discovered: listed.tools.length,
18192
18571
  adopted: tools.length
@@ -18494,7 +18873,7 @@ async function compactMessages(args) {
18494
18873
  }
18495
18874
 
18496
18875
  // src/agent/session-registry.ts
18497
- var log22 = createLogger("SessionRegistry");
18876
+ var log23 = createLogger("SessionRegistry");
18498
18877
  var MAX_HYDRATE_NOTES = 20;
18499
18878
  var MAX_HYDRATE_BYTES = 32 * 1024;
18500
18879
  function escapeMemoryFragment(value) {
@@ -18553,7 +18932,7 @@ var SessionRegistry = class {
18553
18932
  if (!this.externalToolsPromise) {
18554
18933
  this.externalToolsPromise = loadExternalMcpTools(this.opts.config.externalMcpServers).catch(
18555
18934
  (err) => {
18556
- log22.error("external-mcp.load-failed", {
18935
+ log23.error("external-mcp.load-failed", {
18557
18936
  error: describeError(err)
18558
18937
  });
18559
18938
  return [];
@@ -18827,13 +19206,13 @@ ${lines.join("\n")}
18827
19206
  agent.state.messages = result2.messages;
18828
19207
  setAeroSystemPrompt(agent, this.buildHydratedSystemPrompt(projectId, row.systemPrompt));
18829
19208
  this.save(projectName);
18830
- log22.info("compaction.completed", {
19209
+ log23.info("compaction.completed", {
18831
19210
  projectName,
18832
19211
  removedCount: result2.removedCount,
18833
19212
  summaryBytes: Buffer.byteLength(result2.summary, "utf8")
18834
19213
  });
18835
19214
  } catch (err) {
18836
- log22.error("compaction.failed", {
19215
+ log23.error("compaction.failed", {
18837
19216
  projectName,
18838
19217
  error: describeError(err)
18839
19218
  });
@@ -18984,7 +19363,7 @@ ${lines.join("\n")}
18984
19363
  await agent.prompt(msgs);
18985
19364
  this.save(projectName);
18986
19365
  } catch (err) {
18987
- log22.error("drain.failed", {
19366
+ log23.error("drain.failed", {
18988
19367
  projectName,
18989
19368
  error: describeError(err)
18990
19369
  });
@@ -20150,7 +20529,7 @@ function formatAuditFactorScore(factor) {
20150
20529
  }
20151
20530
 
20152
20531
  // src/snapshot-service.ts
20153
- var log23 = createLogger("Snapshot");
20532
+ var log24 = createLogger("Snapshot");
20154
20533
  var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "muse", "local"];
20155
20534
  var SNAPSHOT_QUERY_COUNT = 6;
20156
20535
  var SnapshotService = class {
@@ -20247,7 +20626,7 @@ var SnapshotService = class {
20247
20626
  return mapAuditReport(report);
20248
20627
  } catch (err) {
20249
20628
  const message = describeError(err);
20250
- log23.warn("audit.failed", { homepageUrl, error: message });
20629
+ log24.warn("audit.failed", { homepageUrl, error: message });
20251
20630
  return {
20252
20631
  url: homepageUrl,
20253
20632
  finalUrl: homepageUrl,
@@ -20276,7 +20655,7 @@ var SnapshotService = class {
20276
20655
  queries: parsedQueries
20277
20656
  };
20278
20657
  } catch (err) {
20279
- log23.warn("profile.generation-failed", {
20658
+ log24.warn("profile.generation-failed", {
20280
20659
  domain: ctx.domain,
20281
20660
  provider: ctx.analysisProvider.adapter.name,
20282
20661
  error: describeError(err)
@@ -20419,7 +20798,7 @@ var SnapshotService = class {
20419
20798
  recommendedActions: uniqueStrings2(parsed.recommendedActions ?? []).slice(0, 4)
20420
20799
  };
20421
20800
  } catch (err) {
20422
- log23.warn("response.analysis-failed", {
20801
+ log24.warn("response.analysis-failed", {
20423
20802
  provider: ctx.analysisProvider.adapter.name,
20424
20803
  error: describeError(err)
20425
20804
  });
@@ -20712,9 +21091,9 @@ async function executeResearchRun(db, registry, runId, projectId) {
20712
21091
  reserved = run.totalQueries;
20713
21092
  reservation = { scope, period };
20714
21093
  const rows = db.select().from(researchRunQueries).where(eq27(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
20715
- const competitorRows = db.select({ domain: competitors.domain, aliases: competitors.aliases }).from(competitors).where(eq27(competitors.projectId, projectId)).all();
21094
+ const competitorRows = db.select(competitorIdentityColumns).from(competitors).where(eq27(competitors.projectId, projectId)).orderBy(competitors.domain).all();
20716
21095
  const competitorDomains = competitorRows.map((row) => row.domain);
20717
- const competitorAliases = new Map(competitorRows.filter((row) => row.aliases.length > 0).map((row) => [row.domain, row.aliases]));
21096
+ const competitorAliases = new Map(competitorRows.map((row) => [row.domain, competitorIdentityAliases(row)]).filter(([, aliases]) => aliases.length > 0));
20718
21097
  const domains = effectiveDomains(project);
20719
21098
  const brands = effectiveBrandNames(project);
20720
21099
  const config = { ...provider.config, model: run.resolvedModel };
@@ -21047,7 +21426,7 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
21047
21426
  // src/server.ts
21048
21427
  var _require3 = createRequire3(import.meta.url);
21049
21428
  var { version: PKG_VERSION2 } = _require3("../package.json");
21050
- var log24 = createLogger("Server");
21429
+ var log25 = createLogger("Server");
21051
21430
  var runtimeStartupByServer = /* @__PURE__ */ new WeakMap();
21052
21431
  function waitForServerRuntimeStartup(app) {
21053
21432
  const startup = runtimeStartupByServer.get(app);
@@ -21253,7 +21632,7 @@ function applyLegacyCredentials(rows, config) {
21253
21632
  }
21254
21633
  if (migratedGoogle > 0) {
21255
21634
  saveConfigPatch({ google: config.google });
21256
- log24.info("credentials.migrated", { type: "google", count: migratedGoogle });
21635
+ log25.info("credentials.migrated", { type: "google", count: migratedGoogle });
21257
21636
  }
21258
21637
  let migratedGa4 = 0;
21259
21638
  for (const row of rows.ga4) {
@@ -21271,7 +21650,7 @@ function applyLegacyCredentials(rows, config) {
21271
21650
  }
21272
21651
  if (migratedGa4 > 0) {
21273
21652
  saveConfigPatch({ ga4: config.ga4 });
21274
- log24.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
21653
+ log25.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
21275
21654
  }
21276
21655
  }
21277
21656
  function isLoopbackBindHost(host) {
@@ -21508,11 +21887,11 @@ async function createServer(opts) {
21508
21887
  applyLegacyCredentials(legacyRows, opts.config);
21509
21888
  dropLegacyCredentialColumns(opts.db);
21510
21889
  } catch (err) {
21511
- log24.warn("credentials.migration.failed", {
21890
+ log25.warn("credentials.migration.failed", {
21512
21891
  error: describeError(err)
21513
21892
  });
21514
21893
  }
21515
- log24.info("providers.configured", {
21894
+ log25.info("providers.configured", {
21516
21895
  providers: Object.keys(providers).filter((k) => {
21517
21896
  const p = providers[k];
21518
21897
  return p?.apiKey || p?.baseUrl || p?.vertexProject;
@@ -21527,7 +21906,7 @@ async function createServer(opts) {
21527
21906
  }
21528
21907
  }
21529
21908
  for (const name of providersWithUnsupportedBatch(providers, (provider) => adapterMap[provider])) {
21530
- log24.warn("provider.batch.unsupported", {
21909
+ log25.warn("provider.batch.unsupported", {
21531
21910
  provider: name,
21532
21911
  message: `providers.${name}.batch.enabled is true, but the ${name} adapter has no batch API; its sweeps run sync.`
21533
21912
  });
@@ -22599,7 +22978,7 @@ async function createServer(opts) {
22599
22978
  const sentimentPoller = createSentimentPoller(
22600
22979
  () => sentimentWorker.tick(),
22601
22980
  () => {
22602
- log24.error("sentiment.worker-failed", { reason: "Stored sentiment worker could not finish its tick." });
22981
+ log25.error("sentiment.worker-failed", { reason: "Stored sentiment worker could not finish its tick." });
22603
22982
  }
22604
22983
  );
22605
22984
  const pollSentiment = sentimentPoller.poll;
@@ -22607,6 +22986,110 @@ async function createServer(opts) {
22607
22986
  runCoordinator.onSentimentCompleted = async () => {
22608
22987
  pollSentiment();
22609
22988
  };
22989
+ const answerFieldRecomputes = createAnswerFieldRecomputeQueue({ db: opts.db });
22990
+ const recomputeDebts = /* @__PURE__ */ new Map();
22991
+ let recomputeSequence = 0;
22992
+ const markAnswerFieldRecompute = (projectId, full) => {
22993
+ opts.db.update(projects).set({ answerFieldsRecompute: full ? "full" : sql11`CASE WHEN ${projects.answerFieldsRecompute} = 'full' THEN 'full' ELSE 'competitors' END` }).where(eq29(projects.id, projectId)).run();
22994
+ };
22995
+ const requestAnswerFieldRecompute = (projectId, projectName, scope, reason) => {
22996
+ const sequence = ++recomputeSequence;
22997
+ const debt = recomputeDebts.get(projectId) ?? { latest: 0, latestFull: 0 };
22998
+ debt.latest = sequence;
22999
+ if (scope.full) debt.latestFull = sequence;
23000
+ recomputeDebts.set(projectId, debt);
23001
+ try {
23002
+ markAnswerFieldRecompute(projectId, scope.full === true);
23003
+ } catch (err) {
23004
+ app.log.error({ err, projectId }, "could not mark the owed answer field recompute");
23005
+ }
23006
+ answerFieldRecomputes.request(projectId, scope).then(
23007
+ (pass) => {
23008
+ if (pass.full && debt.latestFull <= sequence) debt.latestFull = 0;
23009
+ try {
23010
+ if (debt.latest === sequence && debt.latestFull === 0) {
23011
+ opts.db.update(projects).set({ answerFieldsRecompute: null }).where(eq29(projects.id, projectId)).run();
23012
+ if (recomputeDebts.get(projectId) === debt) recomputeDebts.delete(projectId);
23013
+ } else if (debt.latestFull === 0) {
23014
+ opts.db.update(projects).set({ answerFieldsRecompute: "competitors" }).where(and23(eq29(projects.id, projectId), eq29(projects.answerFieldsRecompute, "full"))).run();
23015
+ }
23016
+ } catch (err) {
23017
+ app.log.error({ err, projectId }, "could not clear the owed answer field recompute");
23018
+ }
23019
+ app.log.info(
23020
+ { projectId, projectName, reason, full: pass.full, ...pass.result },
23021
+ "identity changed: recomputed stored answer fields on historical snapshots"
23022
+ );
23023
+ },
23024
+ (err) => {
23025
+ app.log.error(
23026
+ { err, projectId, projectName, reason },
23027
+ "identity-triggered answer field recompute failed"
23028
+ );
23029
+ }
23030
+ );
23031
+ };
23032
+ const resumeAnswerFieldRecomputes = () => {
23033
+ const owed = opts.db.select({ id: projects.id, name: projects.name, scope: projects.answerFieldsRecompute }).from(projects).where(isNotNull4(projects.answerFieldsRecompute)).all();
23034
+ for (const project of owed) {
23035
+ requestAnswerFieldRecompute(project.id, project.name, { full: project.scope === "full" }, "resume-after-restart");
23036
+ }
23037
+ };
23038
+ const recomputeCompetitorFields = (projectId, projectName) => {
23039
+ requestAnswerFieldRecompute(projectId, projectName, {}, "competitor-names");
23040
+ };
23041
+ const competitorAutoAliases = createCompetitorAutoAliasRunner({
23042
+ db: opts.db,
23043
+ readAnchors: extractStoredAnswerAnchors,
23044
+ onNamesChanged: recomputeCompetitorFields
23045
+ });
23046
+ const competitorAutoAliasReasons = /* @__PURE__ */ new Map();
23047
+ const competitorAutoAliasPasses = createProjectPassQueue(async (projectId) => {
23048
+ await yieldToEventLoop4();
23049
+ const reason = (competitorAutoAliasReasons.get(projectId) ?? []).join(",");
23050
+ competitorAutoAliasReasons.delete(projectId);
23051
+ const project = opts.db.select().from(projects).where(eq29(projects.id, projectId)).get();
23052
+ if (!project || project.competitorAutoAliases === "off") return;
23053
+ if (project.competitorAutoAliases === "apply") {
23054
+ await competitorAutoAliases.request(projectId, reason).catch(() => void 0);
23055
+ return;
23056
+ }
23057
+ const detection = await previewCompetitorAutoAliases(opts.db, project, { readAnchors: extractStoredAnswerAnchors });
23058
+ const wouldAdd = detection.competitors.flatMap((competitor) => competitor.added.map((name) => `${competitor.domain}: ${name}`));
23059
+ const wouldRemove = detection.competitors.flatMap((competitor) => competitor.removed.map((entry) => `${competitor.domain}: ${entry.name}`));
23060
+ app.log.info(
23061
+ {
23062
+ projectId,
23063
+ projectName: project.name,
23064
+ reason,
23065
+ mode: "preview",
23066
+ runs: detection.scan.runs,
23067
+ snapshots: detection.scan.snapshots,
23068
+ wouldAdd: wouldAdd.slice(0, 20),
23069
+ wouldAddCount: wouldAdd.length,
23070
+ wouldRemove: wouldRemove.slice(0, 20),
23071
+ wouldRemoveCount: wouldRemove.length
23072
+ },
23073
+ "competitor auto aliases (preview): stored nothing; set the project to apply, or run detect --apply, to store them"
23074
+ );
23075
+ });
23076
+ const scheduleCompetitorAutoAliases = (projectId, reason) => {
23077
+ competitorAutoAliasReasons.set(projectId, [...competitorAutoAliasReasons.get(projectId) ?? [], reason]);
23078
+ competitorAutoAliasPasses.request(projectId).catch((err) => {
23079
+ app.log.error({ err, projectId, reason }, "competitor auto alias pass failed");
23080
+ });
23081
+ };
23082
+ runCoordinator.onAnswersRecorded = (runId, projectId) => {
23083
+ scheduleCompetitorAutoAliases(projectId, `run:${runId}`);
23084
+ };
23085
+ app.addHook("onClose", async () => {
23086
+ await competitorAutoAliasPasses.settled();
23087
+ await competitorAutoAliases.settled();
23088
+ await answerFieldRecomputes.settled();
23089
+ });
23090
+ app.addHook("onReady", async () => {
23091
+ resumeAnswerFieldRecomputes();
23092
+ });
22610
23093
  app.addHook("onReady", async () => {
22611
23094
  sentimentTimer = setInterval(pollSentiment, 5e3);
22612
23095
  sentimentTimer.unref();
@@ -23168,37 +23651,14 @@ async function createServer(opts) {
23168
23651
  }
23169
23652
  },
23170
23653
  onAliasesChanged: (projectId, projectName) => {
23171
- setImmediate(() => {
23172
- try {
23173
- const result2 = backfillProjectAnswerMentions(opts.db, projectId);
23174
- app.log.info(
23175
- { projectId, projectName, ...result2 },
23176
- "aliases changed \u2014 recomputed mention fields on historical snapshots"
23177
- );
23178
- } catch (err) {
23179
- app.log.error(
23180
- { err, projectId, projectName },
23181
- "alias-triggered backfill failed"
23182
- );
23183
- }
23184
- });
23654
+ requestAnswerFieldRecompute(projectId, projectName, { full: true }, "project-aliases");
23185
23655
  },
23186
- onCompetitorAliasesChanged: (projectId, projectName) => {
23187
- setImmediate(() => {
23188
- try {
23189
- const result2 = backfillProjectAnswerMentions(opts.db, projectId, { competitorFieldsOnly: true });
23190
- app.log.info(
23191
- { projectId, projectName, ...result2 },
23192
- "competitor aliases changed: recomputed competitor fields on historical snapshots"
23193
- );
23194
- } catch (err) {
23195
- app.log.error(
23196
- { err, projectId, projectName },
23197
- "competitor-alias-triggered backfill failed"
23198
- );
23199
- }
23200
- });
23656
+ onCompetitorAliasesChanged: recomputeCompetitorFields,
23657
+ onCompetitorAutoAliasRescan: (projectId) => {
23658
+ scheduleCompetitorAutoAliases(projectId, "rescan");
23201
23659
  },
23660
+ competitorAnswerAnchors: extractStoredAnswerAnchors,
23661
+ runCompetitorAutoAliasPass: (projectId) => competitorAutoAliases.request(projectId, "api"),
23202
23662
  operatorApiKeyIds,
23203
23663
  listOperationalLogs: (query) => operationalLogs.list(query),
23204
23664
  getTelemetryStatus,
@@ -23575,6 +24035,7 @@ export {
23575
24035
  reparseStoredResult as reparseStoredResult3,
23576
24036
  reparseStoredResult4,
23577
24037
  reparseStoredResult5,
24038
+ resolveStoredApiResponse,
23578
24039
  getCloudflareTrafficConnectionBySourceId,
23579
24040
  setGoogleAuthConfig,
23580
24041
  formatAuditFactorScore,