@canonry/canonry 7.2.0 → 7.4.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/aeo-discovery.md +3 -2
  2. package/assets/agent-workspace/skills/aero/references/orchestration.md +1 -1
  3. package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +1 -1
  4. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +33 -4
  5. package/assets/assets/{AuditHistoryPanel-Duvyi8vx.js → AuditHistoryPanel-DIL5oTHF.js} +1 -1
  6. package/assets/assets/{BacklinksPage-CY5AllQu.js → BacklinksPage-BpCURZh8.js} +1 -1
  7. package/assets/assets/{HistoryPage-C0Gr0gcj.js → HistoryPage-TPbexxzA.js} +1 -1
  8. package/assets/assets/{MeasurementPropertyPage-DrAB6VKX.js → MeasurementPropertyPage-H991GW9h.js} +1 -1
  9. package/assets/assets/ProjectPage-l1NAl-9k.js +8 -0
  10. package/assets/assets/{RunsPage-DMxOrtfl.js → RunsPage-Dhu7Qe7B.js} +1 -1
  11. package/assets/assets/{SettingsPage-sxiUNt2A.js → SettingsPage-BCG1QJ07.js} +1 -1
  12. package/assets/assets/SiteHealthSection-BXueYSiu.js +4 -0
  13. package/assets/assets/{TrafficPage-CvSQDNTi.js → TrafficPage-ua9wsccA.js} +3 -3
  14. package/assets/assets/{TrafficSourceDetailPage-BaYeIBmc.js → TrafficSourceDetailPage-pwATnkp8.js} +1 -1
  15. package/assets/assets/{extract-error-message-76IWhqQF.js → extract-error-message-DJfNdKqh.js} +1 -1
  16. package/assets/assets/{index-BlrH2zRW.js → index-DvWWyhGp.js} +21 -21
  17. package/assets/assets/{react-sigma_core.esm.min-CkAcKW2k.js → react-sigma_core.esm.min-DnbWY5j_.js} +1 -1
  18. package/assets/assets/{v2-overview-adapter-DnEE4E9w.js → v2-overview-adapter-BIOvZAlo.js} +1 -1
  19. package/assets/index.html +1 -1
  20. package/dist/{chunk-UIDHHZH3.js → chunk-GEKMHZS2.js} +293 -1267
  21. package/dist/{chunk-BYSSJEL7.js → chunk-JU6TPMCJ.js} +33 -11
  22. package/dist/{chunk-TY3CJKKR.js → chunk-PXRUWG75.js} +11327 -10807
  23. package/dist/{chunk-RCQJI3UX.js → chunk-SWSWCUHS.js} +3215 -2723
  24. package/dist/{chunk-FDBQOQLP.js → chunk-TNLOOXUP.js} +29 -2
  25. package/dist/{chunk-SVTVBPIT.js → chunk-XFVJ6QVE.js} +2 -2
  26. package/dist/{chunk-EQBYK77W.js → chunk-ZP6GF32R.js} +3 -3
  27. package/dist/cli.js +1151 -68
  28. package/dist/{demo-server-GWRYDPJC.js → demo-server-INMOE4BA.js} +6 -6
  29. package/dist/index.js +6 -6
  30. package/dist/{intelligence-service-3L2665AK.js → intelligence-service-IBUS2IND.js} +2 -2
  31. package/dist/mcp.js +4 -4
  32. package/package.json +13 -13
  33. package/assets/assets/ProjectPage-DSxvvnAd.js +0 -8
  34. package/assets/assets/SiteHealthSection--6tZe3hM.js +0 -4
@@ -20,21 +20,20 @@ import {
20
20
  saveConfig,
21
21
  saveConfigPatch,
22
22
  sentimentInstallReadiness
23
- } from "./chunk-FDBQOQLP.js";
23
+ } from "./chunk-TNLOOXUP.js";
24
24
  import {
25
25
  computeFactorAverages,
26
26
  deleteSiteCrawlGraphLayout,
27
27
  persistSiteCrawlGraphLayout,
28
28
  prepareSiteCrawlGraphLayout,
29
29
  recordedSharePct
30
- } from "./chunk-SVTVBPIT.js";
30
+ } from "./chunk-XFVJ6QVE.js";
31
31
  import {
32
32
  CanonryMcpToolNames,
33
33
  CliError,
34
34
  PACKAGE_VERSION,
35
- canonryMcpTools,
36
- isMachineFormat
37
- } from "./chunk-BYSSJEL7.js";
35
+ canonryMcpTools
36
+ } from "./chunk-JU6TPMCJ.js";
38
37
  import {
39
38
  CC_CACHE_DIR,
40
39
  DUCKDB_SPEC,
@@ -59,12 +58,12 @@ import {
59
58
  agentMemory,
60
59
  agentSessions,
61
60
  agentToolEvents,
62
- aiUserFetchEventsHourly,
63
61
  anyUsersExist,
64
62
  apiKeys,
65
63
  apiRoutes,
66
64
  assessConversionTrackingIntegrity,
67
65
  auditLog,
66
+ backfillProjectAnswerMentions,
68
67
  backlinkDomains,
69
68
  backlinkSummaries,
70
69
  bingCoverageSnapshots,
@@ -74,16 +73,11 @@ import {
74
73
  captureSimpleMeasurementDefinition,
75
74
  ccReleasePaths,
76
75
  ccReleaseSyncs,
77
- classifyAiUserFetch,
78
- classifyCrawler,
79
76
  classifyReviewsError,
80
77
  competitors,
81
78
  computeCitedCompetitorDomains,
82
- computeCompetitorOverlap,
83
79
  countAttributes,
84
80
  countPopulatedGroups,
85
- crawlerEventsHourly,
86
- createClient,
87
81
  createCredentialChecker,
88
82
  createFastifyLogger,
89
83
  createLogger,
@@ -99,12 +93,9 @@ import {
99
93
  exchangeCode,
100
94
  executeDiscovery,
101
95
  extractLegacyCredentials,
102
- extractRecommendedCompetitors,
103
96
  fetchDailyMetrics,
104
97
  fetchSearchAnalytics,
105
98
  formatStorefrontAddress,
106
- gaAiReferrals,
107
- gaTrafficSnapshots,
108
99
  gbpAttributesSnapshots,
109
100
  gbpDailyMetrics,
110
101
  gbpKeywordImpressions,
@@ -158,7 +149,6 @@ import {
158
149
  measurementRunCompleteness,
159
150
  measurementRunSlotState,
160
151
  measurementSlotKey,
161
- migrate,
162
152
  newerFullSweep,
163
153
  nextRunFromCron,
164
154
  nextRunFromSchedule,
@@ -175,7 +165,6 @@ import {
175
165
  queryBacklinks,
176
166
  querySnapshots,
177
167
  queueRunIfProjectIdle,
178
- rawEventSamples,
179
168
  readInstalledVersion,
180
169
  readNegativeReviewMaxStars,
181
170
  recordSentimentCompletion,
@@ -216,7 +205,7 @@ import {
216
205
  siteLivenessState,
217
206
  toAlertView,
218
207
  usageCounters
219
- } from "./chunk-RCQJI3UX.js";
208
+ } from "./chunk-SWSWCUHS.js";
220
209
  import {
221
210
  AGENT_MEMORY_VALUE_MAX_BYTES,
222
211
  AGENT_NONE,
@@ -257,7 +246,6 @@ import {
257
246
  ProviderBatchStatuses,
258
247
  ProviderBatchSubmitError,
259
248
  ProviderDispatchModes,
260
- ProviderNames,
261
249
  RATIO_UNIT_META_KEY,
262
250
  ROUTE_CAPABLE_CITED_URL_PROVIDERS,
263
251
  RUN_FILL_PROVIDER_BREAKER,
@@ -275,10 +263,6 @@ import {
275
263
  SentimentOutcomes,
276
264
  SiteHealthLinkClassificationSources,
277
265
  SnapshotProviderModes,
278
- TrafficEventConfidences,
279
- TrafficEventKinds,
280
- TrafficEvidenceKinds,
281
- TrafficSourceTypes,
282
266
  UserRoles,
283
267
  VERTEX_AI_SEARCH_PROXY_DOMAIN,
284
268
  actionConfidenceLabel,
@@ -315,6 +299,7 @@ import {
315
299
  competitorLabelFromDomain,
316
300
  compileBrandAliases,
317
301
  computeBacklinkSummaryMetrics,
302
+ computeCompetitorOverlap,
318
303
  contentActionLabel,
319
304
  contentBriefDtoSchema,
320
305
  createTemplateLinkDetectionTally,
@@ -334,6 +319,7 @@ import {
334
319
  embedClientConfigForRequest,
335
320
  embedProjectTabsError,
336
321
  extractProviderHttpStatus,
322
+ extractRecommendedCompetitors,
337
323
  factorStatusFromScore,
338
324
  filterCapturedCitedUrls,
339
325
  forbidden,
@@ -376,6 +362,7 @@ import {
376
362
  missingDependency,
377
363
  nextScheduleUpdatedAt,
378
364
  normalizeAgentSlug,
365
+ normalizeCompetitorAliases,
379
366
  normalizeIdTokens,
380
367
  normalizeMeasurementExecutionQueryText,
381
368
  normalizeProjectDomain,
@@ -424,7 +411,7 @@ import {
424
411
  visibilityReportRequestSchema,
425
412
  winnabilityClassLabel,
426
413
  withRetry
427
- } from "./chunk-TY3CJKKR.js";
414
+ } from "./chunk-PXRUWG75.js";
428
415
 
429
416
  // src/runtime-context.ts
430
417
  function cliRuntimeContext(env = process.env, stdio = process) {
@@ -3554,7 +3541,7 @@ import crypto29 from "crypto";
3554
3541
  import fs9 from "fs";
3555
3542
  import path8 from "path";
3556
3543
  import { fileURLToPath as fileURLToPath3 } from "url";
3557
- import { and as and24, eq as eq30 } from "drizzle-orm";
3544
+ import { and as and23, eq as eq29 } from "drizzle-orm";
3558
3545
 
3559
3546
  // ../integration-typesafe/src/classifier.ts
3560
3547
  import { createHash } from "crypto";
@@ -4048,7 +4035,7 @@ function isVertexConfig(config) {
4048
4035
  function resolveModel(config) {
4049
4036
  return config.model || DEFAULT_MODEL;
4050
4037
  }
4051
- function createClient2(config) {
4038
+ function createClient(config) {
4052
4039
  const httpOptions = config.baseUrl ? { httpOptions: { baseUrl: config.baseUrl } } : {};
4053
4040
  if (isVertexConfig(config)) {
4054
4041
  return new GoogleGenAI({
@@ -4090,7 +4077,7 @@ async function healthcheck(config) {
4090
4077
  if (!validation.ok) return validation;
4091
4078
  try {
4092
4079
  const model = resolveModel(config);
4093
- const client = createClient2(config);
4080
+ const client = createClient(config);
4094
4081
  const result2 = await withRetry2(
4095
4082
  () => client.models.generateContent({
4096
4083
  model,
@@ -4127,7 +4114,7 @@ function buildTrackedQueryRequest(input) {
4127
4114
  async function executeTrackedQuery(input) {
4128
4115
  const model = resolveModel(input.config);
4129
4116
  const request = buildTrackedQueryRequest(input).body;
4130
- const client = createClient2(input.config);
4117
+ const client = createClient(input.config);
4131
4118
  let result2;
4132
4119
  try {
4133
4120
  result2 = await withRetry2(
@@ -4315,7 +4302,7 @@ function extractDomainFromUri(uri) {
4315
4302
  }
4316
4303
  async function generateText(prompt, config) {
4317
4304
  const model = resolveModel(config);
4318
- const client = createClient2(config);
4305
+ const client = createClient(config);
4319
4306
  const result2 = await withRetry2(
4320
4307
  () => client.models.generateContent({
4321
4308
  model,
@@ -4383,7 +4370,7 @@ function extractEmbeddingVectors(response, expectedLength) {
4383
4370
  function listModels(config, signal) {
4384
4371
  return withRetry(async () => {
4385
4372
  signal.throwIfAborted();
4386
- const client = createClient2(config);
4373
+ const client = createClient(config);
4387
4374
  const page = await client.models.list({ config: { pageSize: 100, queryBase: true, abortSignal: signal, httpOptions: { timeout: 2500 } } });
4388
4375
  const models = [];
4389
4376
  let seen = 0;
@@ -4553,7 +4540,7 @@ async function withRetry3(fn, options = {}) {
4553
4540
  // ../provider-openai/src/normalize.ts
4554
4541
  var DEFAULT_MODEL2 = "gpt-5.4";
4555
4542
  var OPENAI_RETRIEVAL_CONTRACT = RetrievalContracts["search-required-v1"];
4556
- function createClient3(config) {
4543
+ function createClient2(config) {
4557
4544
  return new OpenAI({
4558
4545
  apiKey: config.apiKey,
4559
4546
  ...config.baseUrl ? { baseURL: config.baseUrl } : {}
@@ -4574,7 +4561,7 @@ async function healthcheck2(config) {
4574
4561
  const validation = validateConfig2(config);
4575
4562
  if (!validation.ok) return validation;
4576
4563
  try {
4577
- const client = createClient3(config);
4564
+ const client = createClient2(config);
4578
4565
  const response = await withRetry3(
4579
4566
  () => client.responses.create({
4580
4567
  model: config.model ?? DEFAULT_MODEL2,
@@ -4622,7 +4609,7 @@ function buildTrackedQueryRequest2(input) {
4622
4609
  async function executeTrackedQuery2(input) {
4623
4610
  const model = input.config.model ?? DEFAULT_MODEL2;
4624
4611
  const params = buildTrackedQueryRequest2(input).body;
4625
- const client = createClient3(input.config);
4612
+ const client = createClient2(input.config);
4626
4613
  let response;
4627
4614
  try {
4628
4615
  response = await withRetry3(() => client.responses.create(params));
@@ -4812,7 +4799,7 @@ function extractDomainFromUri2(uri) {
4812
4799
  }
4813
4800
  async function generateText2(prompt, config) {
4814
4801
  const model = config.model ?? DEFAULT_MODEL2;
4815
- const client = createClient3(config);
4802
+ const client = createClient2(config);
4816
4803
  const response = await withRetry3(
4817
4804
  () => client.responses.create({
4818
4805
  model,
@@ -6556,7 +6543,7 @@ async function healthcheck5(config) {
6556
6543
  if (!validation.ok) return validation;
6557
6544
  const model = resolveModel3(config.model);
6558
6545
  try {
6559
- const client = createClient4(config.apiKey);
6546
+ const client = createClient3(config.apiKey);
6560
6547
  const response = await withRetry6(
6561
6548
  () => postAgent(client, { ...agentSelection(model), input: 'Say "ok"' })
6562
6549
  );
@@ -6585,7 +6572,7 @@ function buildTrackedQueryRequest5(input) {
6585
6572
  }
6586
6573
  async function executeTrackedQuery5(input) {
6587
6574
  const model = resolveModel3(input.config.model);
6588
- const client = createClient4(input.config.apiKey);
6575
+ const client = createClient3(input.config.apiKey);
6589
6576
  const { body } = buildTrackedQueryRequest5(input);
6590
6577
  try {
6591
6578
  const rawResponse = await withRetry6(() => postAgent(client, body));
@@ -6640,7 +6627,7 @@ function reparseStoredResult4(rawResponse) {
6640
6627
  if (agent) return parseAgentResponse(agent);
6641
6628
  return parseSonarResponse(rawResponse);
6642
6629
  }
6643
- function createClient4(apiKey) {
6630
+ function createClient3(apiKey) {
6644
6631
  return new OpenAI4({ apiKey, baseURL: BASE_URL });
6645
6632
  }
6646
6633
  function postAgent(client, body) {
@@ -6854,7 +6841,7 @@ function extractDomainFromUri4(uri) {
6854
6841
  }
6855
6842
  async function generateText5(prompt, config) {
6856
6843
  const model = resolveModel3(config.model);
6857
- const client = createClient4(config.apiKey);
6844
+ const client = createClient3(config.apiKey);
6858
6845
  const response = await withRetry6(() => postAgent(client, { ...agentSelection(model), input: prompt }));
6859
6846
  assertUsableResponse(response);
6860
6847
  return extractAgentAnswerText(response);
@@ -6974,7 +6961,7 @@ var MUSE_SPARK_MODEL = /^muse-spark-[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
6974
6961
  var MUSE_SELF_DOMAINS = [AI_ENGINE_DOMAINS.metaAi];
6975
6962
  var MAX_RETRY_AFTER_MS = 6e4;
6976
6963
  var RETRYABLE_RESPONSE_ERROR_CODES = /* @__PURE__ */ new Set(["rate_limit_exceeded", "server_error"]);
6977
- function createClient5(config) {
6964
+ function createClient4(config) {
6978
6965
  return new OpenAI5({
6979
6966
  apiKey: config.apiKey,
6980
6967
  baseURL: config.baseUrl || MUSE_BASE_URL,
@@ -7022,7 +7009,7 @@ function statusMessage(rawResponse) {
7022
7009
  return `Meta Model API response status: ${status}${detail ? ` (${detail})` : ""}`;
7023
7010
  }
7024
7011
  function createResponse(config, body) {
7025
- const client = createClient5(config);
7012
+ const client = createClient4(config);
7026
7013
  return retryRequest(async () => {
7027
7014
  const rawResponse = responseToRecord5(await client.responses.create(body));
7028
7015
  if (rawResponse.status !== "completed" && rawResponse.status !== "incomplete") {
@@ -7882,7 +7869,7 @@ function nonBlank(value) {
7882
7869
  return trimmed ? trimmed : void 0;
7883
7870
  }
7884
7871
  function resolveBuildCommit(env = process.env) {
7885
- const embedded = true ? "fd79321b46c7ef7c816bf08e424f67b869e07a0b" : void 0;
7872
+ const embedded = true ? "ecdc505a5f013390da765689de60fdc18065a2ea" : void 0;
7886
7873
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
7887
7874
  }
7888
7875
  function resolveInstanceIdentity(env = process.env) {
@@ -8304,13 +8291,17 @@ function runOutcome(inserted, providerErrors, planShortfall) {
8304
8291
  error: someFailed ? serializeRunError(buildRunErrorFromMessages(providerErrors)) : null
8305
8292
  };
8306
8293
  }
8294
+ function competitorIdentityKey(rows) {
8295
+ return JSON.stringify(rows.map((row) => ({ domain: row.domain, aliases: normalizeCompetitorAliases(row.aliases) })).sort((left, right) => left.domain.localeCompare(right.domain)));
8296
+ }
8307
8297
  function runRecordingContext(db, input) {
8308
- const resolveCompetitors = createRunCompetitorResolver(db, input.competitorDomains);
8298
+ const resolveCompetitors = createRunCompetitorResolver(db, input.competitors);
8309
8299
  return {
8310
8300
  runId: input.runId,
8311
8301
  allDomains: effectiveDomains({ canonicalDomain: input.project.canonicalDomain, ownedDomains: input.project.ownedDomains }),
8312
8302
  competitorsFor: (executionId) => resolveCompetitors(input.measurementPlanVersionId, executionId),
8313
- allBrandNames: effectiveBrandNames({ displayName: input.project.displayName, aliases: input.project.aliases })
8303
+ allBrandNames: effectiveBrandNames({ displayName: input.project.displayName, aliases: input.project.aliases }),
8304
+ competitorIdentity: competitorIdentityKey(input.competitors)
8314
8305
  };
8315
8306
  }
8316
8307
  function resolvePlanExecution(run, projectQueries) {
@@ -8510,6 +8501,7 @@ var JobRunner = class {
8510
8501
  let planExecution = null;
8511
8502
  let runTrigger;
8512
8503
  let canonicalDomain;
8504
+ let recording;
8513
8505
  const providerDispatchCounts = /* @__PURE__ */ new Map();
8514
8506
  const providerReservations = /* @__PURE__ */ new Map();
8515
8507
  const batchRowIds = [];
@@ -8580,13 +8572,14 @@ var JobRunner = class {
8580
8572
  log2.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
8581
8573
  const projectCompetitors = this.db.select().from(competitors).where(eq3(competitors.projectId, projectId)).all();
8582
8574
  const competitorDomains = projectCompetitors.map((c) => c.domain);
8583
- const recording = runRecordingContext(this.db, {
8575
+ recording = runRecordingContext(this.db, {
8584
8576
  runId,
8585
8577
  measurementPlanVersionId: existingRun.measurementPlanVersionId,
8586
8578
  project,
8587
- competitorDomains
8579
+ competitors: projectCompetitors
8588
8580
  });
8589
8581
  const { allDomains, allBrandNames } = recording;
8582
+ const { aliases: competitorAliases } = recording.competitorsFor(null);
8590
8583
  const executionContext = {
8591
8584
  providerCount: activeProviders.length,
8592
8585
  providers: activeProviders.map((provider) => provider.adapter.name),
@@ -8676,13 +8669,14 @@ var JobRunner = class {
8676
8669
  allBrandNames,
8677
8670
  allDomains
8678
8671
  );
8679
- const overlap = computeCompetitorOverlap(normalized, competitorDomains);
8672
+ const overlap = computeCompetitorOverlap(normalized, competitorDomains, competitorAliases);
8680
8673
  const extractedCompetitors = extractRecommendedCompetitors(
8681
8674
  normalized.answerText,
8682
8675
  allDomains,
8683
8676
  normalized.citedDomains,
8684
8677
  competitorDomains,
8685
- allBrandNames
8678
+ allBrandNames,
8679
+ competitorAliases
8686
8680
  );
8687
8681
  const answerLocation = runLocation && isSearchLocationIgnored(providerName, normalized.retrievalStatus) ? { location: null, requestedContext: runLocation, supportedContext: { status: "ignored" } } : { location: runLocation?.label ?? null };
8688
8682
  let screenshotRelPath = null;
@@ -8809,12 +8803,15 @@ var JobRunner = class {
8809
8803
  provider: adapter.name,
8810
8804
  requestedModel: config.model ?? null
8811
8805
  })),
8812
- // The legacy competitors table has domains only. Freeze the exact
8813
- // identity we actually dispatched with so later reporting never
8814
- // borrows renamed or newly added competitors from live project state.
8806
+ // Freeze the exact identity we actually dispatched with (domain label
8807
+ // plus curated aliases) so later reporting never borrows renamed or
8808
+ // newly added competitors, or later alias edits, from live project
8809
+ // state. A competitor with no curated alias freezes exactly as it did
8810
+ // before aliases existed, so its definition checksum is unchanged.
8815
8811
  competitors: projectCompetitors.map((competitor) => {
8816
8812
  const label = competitorLabelFromDomain(competitor.domain);
8817
- return { domain: competitor.domain, label, aliases: [label] };
8813
+ const curated = normalizeCompetitorAliases(competitor.aliases).filter((alias) => alias.toLowerCase() !== label.toLowerCase());
8814
+ return { domain: competitor.domain, label, aliases: [label, ...curated] };
8818
8815
  }),
8819
8816
  queries: projectQueries.map((query) => ({
8820
8817
  queryId: query.id,
@@ -8875,6 +8872,7 @@ var JobRunner = class {
8875
8872
  }
8876
8873
  }
8877
8874
  providerCallEnd = Date.now();
8875
+ this.reconcileRunCompetitorFields(recording, projectId);
8878
8876
  this.throwIfRunCancelled(runId);
8879
8877
  if (planExecution) {
8880
8878
  for (const [provider, units] of planExecution.unitsByProvider) {
@@ -8912,6 +8910,7 @@ var JobRunner = class {
8912
8910
  });
8913
8911
  if (!finalized && this.isRunCancelled(runId)) throw new RunCancelledError(runId);
8914
8912
  } catch (err) {
8913
+ if (recording) this.reconcileRunCompetitorFields(recording, projectId);
8915
8914
  const executionContext = {
8916
8915
  providerCount: activeProviders.length,
8917
8916
  providers: activeProviders.map((provider) => provider.adapter.name),
@@ -9552,6 +9551,8 @@ var JobRunner = class {
9552
9551
  if (!(failure2 instanceof RunCancelledError)) throw failure2;
9553
9552
  this.markBatchesCancelled([batch.id], "Cancelled because its run is no longer running.");
9554
9553
  return { kind: "cancelled" };
9554
+ } finally {
9555
+ this.reconcileRunCompetitorFields(ctx, batch.projectId);
9555
9556
  }
9556
9557
  return this.completeBatchIngest(batch);
9557
9558
  }
@@ -9723,12 +9724,13 @@ var JobRunner = class {
9723
9724
  let filled = 0;
9724
9725
  let fatal = null;
9725
9726
  let superseded = false;
9727
+ let recording;
9726
9728
  try {
9727
9729
  const run = this.getRunState(runId);
9728
9730
  if (!run) throw new Error(`Run ${runId} not found`);
9729
9731
  if (run.status !== "partial") throw new Error(`Run ${runId} is ${run.status}; only a partial run can be filled`);
9730
9732
  const runCreatedAt = this.db.select({ createdAt: runs.createdAt }).from(runs).where(eq3(runs.id, runId)).get().createdAt;
9731
- const recording = this.buildRunRecordingContext(runId, projectId);
9733
+ recording = this.buildRunRecordingContext(runId, projectId);
9732
9734
  const projectQueries = this.db.select().from(queries).where(eq3(queries.projectId, projectId)).all();
9733
9735
  const plan = resolvePlanExecution(run, projectQueries);
9734
9736
  if (!plan) throw new Error(`Run ${runId} did not measure a published plan`);
@@ -9841,6 +9843,7 @@ var JobRunner = class {
9841
9843
  fatal = describeError(err);
9842
9844
  log2.error("fill.failed", { fillId, runId, error: fatal });
9843
9845
  } finally {
9846
+ if (recording) this.reconcileRunCompetitorFields(recording, projectId);
9844
9847
  this.flushProviderUsage(providerDispatchCounts, providerReservations);
9845
9848
  }
9846
9849
  const outcome = this.finalizeRunFill({ fillId, runId, filled, fatal, providerErrors, superseded });
@@ -10095,9 +10098,31 @@ var JobRunner = class {
10095
10098
  runId,
10096
10099
  measurementPlanVersionId: run.measurementPlanVersionId,
10097
10100
  project,
10098
- competitorDomains: projectCompetitors.map((c) => c.domain)
10101
+ competitors: projectCompetitors
10099
10102
  });
10100
10103
  }
10104
+ /**
10105
+ * Rescore a run's stored competitor columns when the project's competitors
10106
+ * changed after `recording` read them.
10107
+ *
10108
+ * Saving a competitor alias rescores the stored answers once
10109
+ * (`onCompetitorAliasesChanged`). A writer that read the names before the
10110
+ * edit keeps scoring with them, so an answer it stores afterwards would
10111
+ * disagree with an identical one the edit rescored. Each writer calls this
10112
+ * after its last write: when its names are no longer current, the whole run
10113
+ * gets the same competitor-only pass the edit ran. A failure is logged and
10114
+ * never fails the writer.
10115
+ */
10116
+ reconcileRunCompetitorFields(recording, projectId) {
10117
+ try {
10118
+ const current = this.db.select({ domain: competitors.domain, aliases: competitors.aliases }).from(competitors).where(eq3(competitors.projectId, projectId)).all();
10119
+ if (competitorIdentityKey(current) === recording.competitorIdentity) return;
10120
+ const result2 = backfillProjectAnswerMentions(this.db, projectId, { competitorFieldsOnly: true, runId: recording.runId });
10121
+ log2.info("run.competitor-fields-rescored", { runId: recording.runId, projectId, ...result2 });
10122
+ } catch (err) {
10123
+ log2.error("run.competitor-fields-rescore-failed", { runId: recording.runId, projectId, error: describeError(err) });
10124
+ }
10125
+ }
10101
10126
  incrementUsage(scope, metric, count3) {
10102
10127
  const now = (/* @__PURE__ */ new Date()).toISOString();
10103
10128
  const period = now.slice(0, 10);
@@ -13001,8 +13026,7 @@ function buildDefaultDeps(registry) {
13001
13026
  cfg
13002
13027
  );
13003
13028
  const normalized = adapter.normalizeResult(raw);
13004
- const canonical = new Set(input.project.canonicalDomains.map((d) => d.toLowerCase()));
13005
- const isCited = normalized.citedDomains.some((d) => canonical.has(d.toLowerCase()));
13029
+ const isCited = normalized.citedDomains.some((d) => hostMatchesAnyDomain(d, input.project.canonicalDomains));
13006
13030
  const answerMentioned = determineAnswerMentioned(
13007
13031
  normalized.answerText,
13008
13032
  input.project.brandNames ?? [],
@@ -14214,1020 +14238,6 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
14214
14238
  }
14215
14239
  }
14216
14240
 
14217
- // src/commands/backfill.ts
14218
- import { setImmediate as yieldToEventLoop2 } from "timers/promises";
14219
- import { and as and15, asc as asc4, desc as desc8, eq as eq17, gt as gt3, inArray as inArray7, isNull as isNull2, lte as lte2, sql as sql7 } from "drizzle-orm";
14220
- var SNAPSHOT_BATCH_SIZE = 500;
14221
- var PATH_REPAIR_PAGE_SIZE = 128;
14222
- async function backfillAnswerVisibilityCommand(opts) {
14223
- const config = loadConfig();
14224
- const db = createClient(config.database);
14225
- migrate(db);
14226
- const projectFilter = opts?.project?.trim();
14227
- const isDryRun = opts?.dryRun === true;
14228
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq17(projects.name, projectFilter)).all() : db.select().from(projects).all();
14229
- let examined = 0;
14230
- let updated = 0;
14231
- let wouldUpdate = 0;
14232
- let mentioned = 0;
14233
- let reparsed = 0;
14234
- let providerErrors = 0;
14235
- let retrievalRelabeled = 0;
14236
- if (scopedProjects.length > 0) {
14237
- const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId, planVersionId: runs.measurementPlanVersionId }).from(runs).where(and15(
14238
- eq17(runs.kind, RunKinds["answer-visibility"]),
14239
- inArray7(runs.projectId, scopedProjects.map((project) => project.id))
14240
- )).all() : db.select({ id: runs.id, projectId: runs.projectId, planVersionId: runs.measurementPlanVersionId }).from(runs).where(eq17(runs.kind, RunKinds["answer-visibility"])).all();
14241
- const planVersionByRun = new Map(runRows.map((run) => [run.id, run.planVersionId]));
14242
- const runIdsByProject = /* @__PURE__ */ new Map();
14243
- for (const run of runRows) {
14244
- const existing = runIdsByProject.get(run.projectId);
14245
- if (existing) existing.push(run.id);
14246
- else runIdsByProject.set(run.projectId, [run.id]);
14247
- }
14248
- for (const project of scopedProjects) {
14249
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq17(competitors.projectId, project.id)).all().map((row) => row.domain);
14250
- const competitorsForRun = createRunCompetitorResolver(db, competitorDomains);
14251
- const runIds = runIdsByProject.get(project.id) ?? [];
14252
- if (runIds.length === 0) continue;
14253
- const projectDomains = effectiveDomains({
14254
- canonicalDomain: project.canonicalDomain,
14255
- ownedDomains: project.ownedDomains
14256
- });
14257
- const projectBrandNames = effectiveBrandNames({
14258
- displayName: project.displayName,
14259
- aliases: project.aliases
14260
- });
14261
- for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
14262
- const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
14263
- const snapshotRows = db.select({
14264
- id: querySnapshots.id,
14265
- runId: querySnapshots.runId,
14266
- executionId: querySnapshots.measurementExecutionId,
14267
- provider: querySnapshots.provider,
14268
- citationState: querySnapshots.citationState,
14269
- answerMentioned: querySnapshots.answerMentioned,
14270
- answerText: querySnapshots.answerText,
14271
- citedDomains: querySnapshots.citedDomains,
14272
- competitorOverlap: querySnapshots.competitorOverlap,
14273
- recommendedCompetitors: querySnapshots.recommendedCompetitors,
14274
- rawResponse: querySnapshots.rawResponse,
14275
- retrievalStatus: querySnapshots.retrievalStatus,
14276
- retrievalContract: querySnapshots.retrievalContract
14277
- }).from(querySnapshots).where(inArray7(querySnapshots.runId, batchRunIds)).all();
14278
- const pendingUpdates = [];
14279
- for (const snapshot of snapshotRows) {
14280
- examined++;
14281
- const reparsedResult = reparseProviderSnapshot(snapshot.provider, snapshot.rawResponse);
14282
- if (reparsedResult) reparsed++;
14283
- if (reparsedResult?.providerError) providerErrors++;
14284
- const answerText = reparsedResult?.answerText ?? snapshot.answerText ?? "";
14285
- const nextValue = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
14286
- if (nextValue) mentioned++;
14287
- const nextPatch = {};
14288
- if (snapshot.answerMentioned !== nextValue) {
14289
- nextPatch.answerMentioned = nextValue;
14290
- }
14291
- if ((snapshot.answerText ?? "") !== answerText) {
14292
- nextPatch.answerText = answerText;
14293
- }
14294
- if (reparsedResult) {
14295
- const normalized = {
14296
- provider: snapshot.provider,
14297
- answerText,
14298
- citedDomains: reparsedResult.citedDomains,
14299
- groundingSources: reparsedResult.groundingSources,
14300
- searchQueries: reparsedResult.searchQueries,
14301
- // Only feeds the citation/mention helpers below, which ignore
14302
- // it. Retrieval is written back solely by the narrow OpenAI
14303
- // contract correction after this block.
14304
- retrievalStatus: "unknown"
14305
- };
14306
- const nextCitationState = determineCitationState(normalized, projectDomains);
14307
- const nextCitedDomains = reparsedResult.citedDomains;
14308
- const runCompetitors = competitorsForRun(planVersionByRun.get(snapshot.runId), snapshot.executionId);
14309
- const nextCompetitorOverlap = computeCompetitorOverlap(normalized, runCompetitors.domains, runCompetitors.aliases);
14310
- const nextRecommendedCompetitors = extractRecommendedCompetitors(
14311
- normalized.answerText,
14312
- projectDomains,
14313
- normalized.citedDomains,
14314
- runCompetitors.domains,
14315
- projectBrandNames,
14316
- runCompetitors.aliases
14317
- );
14318
- const nextRawResponse = stringifyStoredSnapshotEnvelope(
14319
- snapshot.rawResponse,
14320
- reparsedResult
14321
- );
14322
- if (snapshot.citationState !== nextCitationState) {
14323
- nextPatch.citationState = nextCitationState;
14324
- }
14325
- if (JSON.stringify(snapshot.citedDomains) !== JSON.stringify(nextCitedDomains)) {
14326
- nextPatch.citedDomains = nextCitedDomains;
14327
- }
14328
- if (JSON.stringify(snapshot.competitorOverlap) !== JSON.stringify(nextCompetitorOverlap)) {
14329
- nextPatch.competitorOverlap = nextCompetitorOverlap;
14330
- }
14331
- if (JSON.stringify(snapshot.recommendedCompetitors) !== JSON.stringify(nextRecommendedCompetitors)) {
14332
- nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
14333
- }
14334
- if (snapshot.rawResponse !== nextRawResponse) {
14335
- nextPatch.rawResponse = nextRawResponse;
14336
- }
14337
- }
14338
- const retrievalCorrection = correctStoredOpenAIRetrieval(snapshot, reparsedResult);
14339
- if (retrievalCorrection) {
14340
- retrievalRelabeled++;
14341
- Object.assign(nextPatch, retrievalCorrection);
14342
- }
14343
- if (Object.keys(nextPatch).length > 0) {
14344
- pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
14345
- }
14346
- }
14347
- if (pendingUpdates.length > 0) {
14348
- if (isDryRun) {
14349
- wouldUpdate += pendingUpdates.length;
14350
- } else {
14351
- db.transaction((tx) => {
14352
- for (const update of pendingUpdates) {
14353
- tx.update(querySnapshots).set(update.patch).where(eq17(querySnapshots.id, update.id)).run();
14354
- }
14355
- });
14356
- updated += pendingUpdates.length;
14357
- }
14358
- }
14359
- }
14360
- }
14361
- }
14362
- const result2 = {
14363
- project: projectFilter ?? null,
14364
- projects: scopedProjects.length,
14365
- examined,
14366
- updated,
14367
- mentioned,
14368
- reparsed,
14369
- providerErrors,
14370
- retrievalRelabeled
14371
- };
14372
- if (isDryRun) {
14373
- result2.dryRun = true;
14374
- result2.wouldUpdate = wouldUpdate;
14375
- }
14376
- if (isMachineFormat(opts?.format)) {
14377
- console.log(JSON.stringify(result2, null, 2));
14378
- return;
14379
- }
14380
- console.log(`Answer visibility backfill ${isDryRun ? "preview" : "complete"}.
14381
- `);
14382
- if (projectFilter) {
14383
- console.log(` Project: ${projectFilter}`);
14384
- }
14385
- console.log(` Projects: ${scopedProjects.length}`);
14386
- console.log(` Examined: ${examined}`);
14387
- if (isDryRun) {
14388
- console.log(` Would update: ${wouldUpdate}`);
14389
- } else {
14390
- console.log(` Updated: ${updated}`);
14391
- }
14392
- console.log(` Mentioned: ${mentioned}`);
14393
- console.log(` Reparsed: ${reparsed}`);
14394
- console.log(` Errors: ${providerErrors}`);
14395
- console.log(` Relabeled: ${retrievalRelabeled} (OpenAI retrieval contract)`);
14396
- if (isDryRun) {
14397
- console.log(`
14398
- No DB writes performed. Re-run without --dry-run to apply.`);
14399
- }
14400
- }
14401
- function backfillNormalizedPaths(db, opts) {
14402
- return repairStoredPaths(db, gaTrafficSnapshots, opts);
14403
- }
14404
- async function repairStoredPaths(db, table, opts) {
14405
- const pageSize = opts?.pageSize ?? PATH_REPAIR_PAGE_SIZE;
14406
- const projectScope = opts?.projectId ? sql7`+${table.projectId} = ${opts.projectId}` : void 0;
14407
- const lastId = db.select({ id: table.id }).from(table).where(projectScope).orderBy(desc8(table.id)).limit(1).get()?.id;
14408
- if (lastId === void 0) return { examined: 0, updated: 0, unchanged: 0 };
14409
- let cursor;
14410
- let examined = 0;
14411
- let updated = 0;
14412
- for (; ; ) {
14413
- const rows = db.select({
14414
- id: table.id,
14415
- projectId: table.projectId,
14416
- landingPage: table.landingPage,
14417
- landingPageNormalized: table.landingPageNormalized
14418
- }).from(table).where(and15(
14419
- projectScope,
14420
- lte2(table.id, lastId),
14421
- cursor === void 0 ? void 0 : gt3(table.id, cursor)
14422
- )).orderBy(asc4(table.id)).limit(pageSize).all();
14423
- examined += rows.length;
14424
- const changes = rows.flatMap((row) => {
14425
- const next = normalizeUrlPath(row.landingPage);
14426
- return next === null || row.landingPageNormalized === next ? [] : [{ row, next }];
14427
- });
14428
- if (changes.length > 0) {
14429
- const written = db.transaction((tx) => {
14430
- let written2 = 0;
14431
- for (const { row, next } of changes) {
14432
- written2 += tx.update(table).set({ landingPageNormalized: next }).where(and15(
14433
- eq17(table.id, row.id),
14434
- eq17(table.projectId, row.projectId),
14435
- eq17(table.landingPage, row.landingPage),
14436
- row.landingPageNormalized === null ? isNull2(table.landingPageNormalized) : eq17(table.landingPageNormalized, row.landingPageNormalized)
14437
- )).run().changes;
14438
- }
14439
- return written2;
14440
- });
14441
- updated += written;
14442
- if (written < changes.length) opts?.onConflict?.();
14443
- }
14444
- if (rows.length < pageSize) break;
14445
- cursor = rows.at(-1).id;
14446
- if (cursor === lastId) break;
14447
- await yieldToEventLoop2();
14448
- }
14449
- return { examined, updated, unchanged: examined - updated };
14450
- }
14451
- async function backfillNormalizedPathsCommand(opts) {
14452
- const config = loadConfig();
14453
- const db = createClient(config.database);
14454
- migrate(db);
14455
- const projectFilter = opts?.project?.trim();
14456
- let projectId;
14457
- if (projectFilter) {
14458
- const project = db.select({ id: projects.id }).from(projects).where(eq17(projects.name, projectFilter)).get();
14459
- if (!project) {
14460
- const result3 = {
14461
- project: projectFilter,
14462
- examined: 0,
14463
- updated: 0,
14464
- unchanged: 0
14465
- };
14466
- if (isMachineFormat(opts?.format)) {
14467
- console.log(JSON.stringify(result3, null, 2));
14468
- return;
14469
- }
14470
- console.log(`Backfill normalized-paths: project "${projectFilter}" not found.`);
14471
- return;
14472
- }
14473
- projectId = project.id;
14474
- }
14475
- const { examined, updated, unchanged } = await backfillNormalizedPaths(db, { projectId });
14476
- const result2 = {
14477
- project: projectFilter ?? null,
14478
- examined,
14479
- updated,
14480
- unchanged
14481
- };
14482
- if (isMachineFormat(opts?.format)) {
14483
- console.log(JSON.stringify(result2, null, 2));
14484
- return;
14485
- }
14486
- console.log("Normalized-path backfill complete.\n");
14487
- if (projectFilter) console.log(` Project: ${projectFilter}`);
14488
- console.log(` Examined: ${examined}`);
14489
- console.log(` Updated: ${updated}`);
14490
- console.log(` Unchanged: ${unchanged}`);
14491
- }
14492
- function backfillAiReferralPaths(db, opts) {
14493
- return repairStoredPaths(db, gaAiReferrals, opts);
14494
- }
14495
- async function backfillAiReferralPathsCommand(opts) {
14496
- const config = loadConfig();
14497
- const db = createClient(config.database);
14498
- migrate(db);
14499
- const projectFilter = opts?.project?.trim();
14500
- let projectId;
14501
- if (projectFilter) {
14502
- const project = db.select({ id: projects.id }).from(projects).where(eq17(projects.name, projectFilter)).get();
14503
- if (!project) {
14504
- const result3 = {
14505
- project: projectFilter,
14506
- examined: 0,
14507
- updated: 0,
14508
- unchanged: 0
14509
- };
14510
- if (isMachineFormat(opts?.format)) {
14511
- console.log(JSON.stringify(result3, null, 2));
14512
- return;
14513
- }
14514
- console.log(`Backfill ai-referral-paths: project "${projectFilter}" not found.`);
14515
- return;
14516
- }
14517
- projectId = project.id;
14518
- }
14519
- const { examined, updated, unchanged } = await backfillAiReferralPaths(db, { projectId });
14520
- const result2 = {
14521
- project: projectFilter ?? null,
14522
- examined,
14523
- updated,
14524
- unchanged
14525
- };
14526
- if (isMachineFormat(opts?.format)) {
14527
- console.log(JSON.stringify(result2, null, 2));
14528
- return;
14529
- }
14530
- console.log("AI referral landing-page backfill complete.\n");
14531
- if (projectFilter) console.log(` Project: ${projectFilter}`);
14532
- console.log(` Examined: ${examined}`);
14533
- console.log(` Updated: ${updated}`);
14534
- console.log(` Unchanged: ${unchanged}`);
14535
- }
14536
- function backfillProjectAnswerMentions(db, projectId, opts) {
14537
- const isDryRun = opts?.dryRun === true;
14538
- const project = db.select().from(projects).where(eq17(projects.id, projectId)).get();
14539
- if (!project) return { examined: 0, updated: 0, mentioned: 0 };
14540
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq17(competitors.projectId, projectId)).all().map((row) => row.domain);
14541
- const runRows = db.select({ id: runs.id, planVersionId: runs.measurementPlanVersionId }).from(runs).where(and15(eq17(runs.kind, RunKinds["answer-visibility"]), eq17(runs.projectId, projectId))).all();
14542
- const runIds = runRows.map((r) => r.id);
14543
- const planVersionByRun = new Map(runRows.map((run) => [run.id, run.planVersionId]));
14544
- const competitorsForRun = createRunCompetitorResolver(db, competitorDomains);
14545
- let examined = 0;
14546
- let updated = 0;
14547
- let wouldUpdate = 0;
14548
- let mentioned = 0;
14549
- if (runIds.length === 0) {
14550
- return isDryRun ? { examined, updated, wouldUpdate, mentioned } : { examined, updated, mentioned };
14551
- }
14552
- const projectDomains = effectiveDomains({
14553
- canonicalDomain: project.canonicalDomain,
14554
- ownedDomains: project.ownedDomains
14555
- });
14556
- const projectBrandNames = effectiveBrandNames({
14557
- displayName: project.displayName,
14558
- aliases: project.aliases
14559
- });
14560
- for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
14561
- const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
14562
- const snapshotRows = db.select({
14563
- id: querySnapshots.id,
14564
- runId: querySnapshots.runId,
14565
- executionId: querySnapshots.measurementExecutionId,
14566
- provider: querySnapshots.provider,
14567
- answerMentioned: querySnapshots.answerMentioned,
14568
- answerText: querySnapshots.answerText,
14569
- citedDomains: querySnapshots.citedDomains,
14570
- competitorOverlap: querySnapshots.competitorOverlap,
14571
- recommendedCompetitors: querySnapshots.recommendedCompetitors,
14572
- rawResponse: querySnapshots.rawResponse
14573
- }).from(querySnapshots).where(inArray7(querySnapshots.runId, batchRunIds)).all();
14574
- const pendingUpdates = [];
14575
- for (const snapshot of snapshotRows) {
14576
- examined++;
14577
- const answerText = snapshot.answerText ?? "";
14578
- const nextAnswerMentioned = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
14579
- if (nextAnswerMentioned) mentioned++;
14580
- const citedDomains = snapshot.citedDomains;
14581
- const groundingSources = readStoredGroundingSources(snapshot.rawResponse);
14582
- const normalized = {
14583
- provider: snapshot.provider,
14584
- answerText,
14585
- citedDomains,
14586
- groundingSources,
14587
- // Only feeds the competitor helpers, which ignore it. This pass never
14588
- // observes or writes retrieval.
14589
- retrievalStatus: "unknown",
14590
- searchQueries: []
14591
- };
14592
- const runCompetitors = competitorsForRun(planVersionByRun.get(snapshot.runId), snapshot.executionId);
14593
- const nextCompetitorOverlap = computeCompetitorOverlap(normalized, runCompetitors.domains, runCompetitors.aliases);
14594
- const nextRecommendedCompetitors = extractRecommendedCompetitors(
14595
- answerText,
14596
- projectDomains,
14597
- citedDomains,
14598
- runCompetitors.domains,
14599
- projectBrandNames,
14600
- runCompetitors.aliases
14601
- );
14602
- const nextPatch = {};
14603
- if (snapshot.answerMentioned !== nextAnswerMentioned) {
14604
- nextPatch.answerMentioned = nextAnswerMentioned;
14605
- }
14606
- if (JSON.stringify(snapshot.competitorOverlap) !== JSON.stringify(nextCompetitorOverlap)) {
14607
- nextPatch.competitorOverlap = nextCompetitorOverlap;
14608
- }
14609
- if (JSON.stringify(snapshot.recommendedCompetitors) !== JSON.stringify(nextRecommendedCompetitors)) {
14610
- nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
14611
- }
14612
- if (Object.keys(nextPatch).length > 0) {
14613
- pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
14614
- }
14615
- }
14616
- if (pendingUpdates.length > 0) {
14617
- if (isDryRun) {
14618
- wouldUpdate += pendingUpdates.length;
14619
- } else {
14620
- db.transaction((tx) => {
14621
- for (const update of pendingUpdates) {
14622
- tx.update(querySnapshots).set(update.patch).where(eq17(querySnapshots.id, update.id)).run();
14623
- }
14624
- });
14625
- updated += pendingUpdates.length;
14626
- }
14627
- }
14628
- }
14629
- return isDryRun ? { examined, updated, wouldUpdate, mentioned } : { examined, updated, mentioned };
14630
- }
14631
- async function backfillAnswerMentionsCommand(opts) {
14632
- const config = loadConfig();
14633
- const db = createClient(config.database);
14634
- migrate(db);
14635
- const projectFilter = opts?.project?.trim();
14636
- const isDryRun = opts?.dryRun === true;
14637
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq17(projects.name, projectFilter)).all() : db.select().from(projects).all();
14638
- let examined = 0;
14639
- let updated = 0;
14640
- let wouldUpdate = 0;
14641
- let mentioned = 0;
14642
- for (const project of scopedProjects) {
14643
- const result3 = backfillProjectAnswerMentions(db, project.id, { dryRun: isDryRun });
14644
- examined += result3.examined;
14645
- updated += result3.updated;
14646
- wouldUpdate += result3.wouldUpdate ?? 0;
14647
- mentioned += result3.mentioned;
14648
- }
14649
- const result2 = {
14650
- project: projectFilter ?? null,
14651
- projects: scopedProjects.length,
14652
- examined,
14653
- updated,
14654
- mentioned
14655
- };
14656
- if (isDryRun) {
14657
- result2.dryRun = true;
14658
- result2.wouldUpdate = wouldUpdate;
14659
- }
14660
- if (isMachineFormat(opts?.format)) {
14661
- console.log(JSON.stringify(result2, null, 2));
14662
- return;
14663
- }
14664
- console.log(`Answer mentions backfill ${isDryRun ? "preview" : "complete"}.
14665
- `);
14666
- if (projectFilter) console.log(` Project: ${projectFilter}`);
14667
- console.log(` Projects: ${scopedProjects.length}`);
14668
- console.log(` Examined: ${examined}`);
14669
- if (isDryRun) {
14670
- console.log(` Would update: ${wouldUpdate}`);
14671
- } else {
14672
- console.log(` Updated: ${updated}`);
14673
- }
14674
- console.log(` Mentioned: ${mentioned}`);
14675
- if (isDryRun) {
14676
- console.log(`
14677
- No DB writes performed. Re-run without --dry-run to apply.`);
14678
- }
14679
- }
14680
- function readStoredGroundingSources(rawResponse) {
14681
- const envelope = parseJsonColumn(rawResponse, {});
14682
- const sources = envelope.groundingSources;
14683
- if (!Array.isArray(sources)) return [];
14684
- const result2 = [];
14685
- for (const source of sources) {
14686
- if (source && typeof source === "object") {
14687
- const uri = source.uri;
14688
- const title = source.title;
14689
- if (typeof uri === "string") {
14690
- result2.push({ uri, title: typeof title === "string" ? title : "" });
14691
- }
14692
- }
14693
- }
14694
- return result2;
14695
- }
14696
- async function backfillInsightsCommand(project, opts) {
14697
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-3L2665AK.js");
14698
- const config = loadConfig();
14699
- const db = createClient(config.database);
14700
- migrate(db);
14701
- const service = new IntelligenceService2(db);
14702
- const isJson = isMachineFormat(opts?.format);
14703
- const isDryRun = opts?.dryRun === true;
14704
- if (!isJson) {
14705
- const scope = opts?.since ? ` (since ${opts.since})` : "";
14706
- const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
14707
- process.stderr.write(`Backfilling insights for "${project}"${scope}${mode}...
14708
- `);
14709
- }
14710
- const result2 = service.backfill(project, {
14711
- fromRunId: opts?.fromRun,
14712
- toRunId: opts?.toRun,
14713
- since: opts?.since,
14714
- dryRun: isDryRun
14715
- }, (info) => {
14716
- if (!isJson) {
14717
- process.stderr.write(` [${info.index}/${info.total}] ${info.runId} \u2014 ${info.insights} insights
14718
- `);
14719
- }
14720
- });
14721
- const output = {
14722
- project,
14723
- processed: result2.processed,
14724
- skipped: result2.skipped,
14725
- totalInsights: result2.totalInsights
14726
- };
14727
- if (result2.dryRun) {
14728
- output.dryRun = true;
14729
- output.delta = result2.delta;
14730
- }
14731
- if (isJson) {
14732
- console.log(JSON.stringify(output, null, 2));
14733
- return;
14734
- }
14735
- console.log(`
14736
- Backfill ${isDryRun ? "preview" : "complete"}.`);
14737
- console.log(` Processed: ${result2.processed}`);
14738
- console.log(` Skipped: ${result2.skipped}`);
14739
- console.log(` Insights: ${result2.totalInsights}`);
14740
- if (result2.delta) {
14741
- console.log(` Delta: -${result2.delta.wouldDelete} existing +${result2.delta.wouldCreate} new (net ${result2.delta.netChange >= 0 ? "+" : ""}${result2.delta.netChange})`);
14742
- console.log(` No DB writes performed. Re-run without --dry-run to apply.`);
14743
- }
14744
- }
14745
- function replayQueryAuditLog(events) {
14746
- const active = [];
14747
- for (const ev of events) {
14748
- let diff;
14749
- try {
14750
- diff = ev.diff ? JSON.parse(ev.diff) : {};
14751
- } catch {
14752
- continue;
14753
- }
14754
- if (ev.action === "keywords.appended" || ev.action === "queries.appended") {
14755
- const added = Array.isArray(diff.added) ? diff.added : [];
14756
- for (const q of added) {
14757
- active.push({ text: q, addedAt: ev.createdAt, deletedAt: null });
14758
- }
14759
- } else if (ev.action === "keywords.deleted" || ev.action === "queries.deleted") {
14760
- const deleted = Array.isArray(diff.deleted) ? diff.deleted : [];
14761
- for (const q of deleted) {
14762
- for (let i = active.length - 1; i >= 0; i--) {
14763
- if (active[i].text === q && active[i].deletedAt === null) {
14764
- active[i].deletedAt = ev.createdAt;
14765
- break;
14766
- }
14767
- }
14768
- }
14769
- } else if (ev.action === "queries.replaced") {
14770
- const newSet = Array.isArray(diff.queries) ? diff.queries : [];
14771
- for (const e of active) {
14772
- if (e.deletedAt === null) e.deletedAt = ev.createdAt;
14773
- }
14774
- for (const q of newSet) {
14775
- active.push({ text: q, addedAt: ev.createdAt, deletedAt: null });
14776
- }
14777
- }
14778
- }
14779
- return active;
14780
- }
14781
- function activeQueriesAt(history, t) {
14782
- return history.filter((e) => e.addedAt <= t && (e.deletedAt === null || e.deletedAt > t)).map((e) => e.text);
14783
- }
14784
- var CONTENT_MATCH_STOPWORDS = /* @__PURE__ */ new Set([
14785
- "the",
14786
- "a",
14787
- "an",
14788
- "and",
14789
- "or",
14790
- "but",
14791
- "is",
14792
- "are",
14793
- "was",
14794
- "were",
14795
- "be",
14796
- "been",
14797
- "being",
14798
- "have",
14799
- "has",
14800
- "had",
14801
- "do",
14802
- "does",
14803
- "did",
14804
- "will",
14805
- "would",
14806
- "could",
14807
- "should",
14808
- "may",
14809
- "might",
14810
- "can",
14811
- "this",
14812
- "that",
14813
- "these",
14814
- "those",
14815
- "i",
14816
- "you",
14817
- "he",
14818
- "she",
14819
- "it",
14820
- "we",
14821
- "they",
14822
- "what",
14823
- "which",
14824
- "who",
14825
- "how",
14826
- "why",
14827
- "when",
14828
- "where",
14829
- "with",
14830
- "for",
14831
- "from",
14832
- "of",
14833
- "in",
14834
- "on",
14835
- "at",
14836
- "to",
14837
- "by",
14838
- "as",
14839
- "best",
14840
- "most"
14841
- ]);
14842
- function tokenizeForMatch(s) {
14843
- return s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3 && !CONTENT_MATCH_STOPWORDS.has(t));
14844
- }
14845
- function contentMatchScore(queryText, answerText) {
14846
- const queryTokens = [...new Set(tokenizeForMatch(queryText))];
14847
- if (queryTokens.length === 0) return 0;
14848
- const answerHead = answerText.slice(0, 300).toLowerCase();
14849
- const hit = queryTokens.filter((t) => answerHead.includes(t)).length;
14850
- return hit / queryTokens.length;
14851
- }
14852
- async function backfillSnapshotAttributionCommand(opts) {
14853
- const config = loadConfig();
14854
- const db = createClient(config.database);
14855
- migrate(db);
14856
- const project = db.select().from(projects).where(eq17(projects.name, opts.project)).get();
14857
- if (!project) {
14858
- throw new Error(`Project "${opts.project}" not found`);
14859
- }
14860
- const isJson = isMachineFormat(opts.format);
14861
- const isDryRun = opts.dryRun === true;
14862
- if (!isJson) {
14863
- const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
14864
- process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
14865
- `);
14866
- }
14867
- const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and15(
14868
- eq17(auditLog.projectId, project.id),
14869
- inArray7(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
14870
- )).orderBy(auditLog.createdAt).all();
14871
- const history = replayQueryAuditLog(events);
14872
- const orphanRuns = db.select({
14873
- runId: runs.id,
14874
- createdAt: runs.createdAt,
14875
- location: runs.location
14876
- }).from(runs).innerJoin(querySnapshots, eq17(querySnapshots.runId, runs.id)).where(and15(
14877
- eq17(runs.projectId, project.id),
14878
- isNull2(querySnapshots.queryId),
14879
- isNull2(querySnapshots.queryText)
14880
- )).groupBy(runs.id).orderBy(runs.createdAt).all();
14881
- const result2 = {
14882
- project: project.name,
14883
- examinedRuns: orphanRuns.length,
14884
- orphanSnapshots: 0,
14885
- recoveredByPosition: 0,
14886
- recoveredByContent: 0,
14887
- unrecovered: 0,
14888
- dryRun: isDryRun,
14889
- perRun: []
14890
- };
14891
- const updates = [];
14892
- for (const run of orphanRuns) {
14893
- const activeAt = activeQueriesAt(history, run.createdAt);
14894
- const orphanSnaps = db.select({
14895
- id: querySnapshots.id,
14896
- provider: querySnapshots.provider,
14897
- createdAt: querySnapshots.createdAt,
14898
- answerText: querySnapshots.answerText
14899
- }).from(querySnapshots).where(and15(
14900
- eq17(querySnapshots.runId, run.runId),
14901
- isNull2(querySnapshots.queryId),
14902
- isNull2(querySnapshots.queryText)
14903
- )).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
14904
- const byProvider = /* @__PURE__ */ new Map();
14905
- for (const s of orphanSnaps) {
14906
- const arr = byProvider.get(s.provider);
14907
- if (arr) arr.push(s);
14908
- else byProvider.set(s.provider, [s]);
14909
- }
14910
- let runPosition = 0;
14911
- let runContent = 0;
14912
- let runUnrecovered = 0;
14913
- for (const [, snaps] of byProvider) {
14914
- if (snaps.length === activeAt.length) {
14915
- for (let i = 0; i < snaps.length; i++) {
14916
- updates.push({ id: snaps[i].id, queryText: activeAt[i] });
14917
- runPosition++;
14918
- }
14919
- } else if (snaps.length < activeAt.length) {
14920
- const LOOKAHEAD_LIMIT = 5;
14921
- let cidx = 0;
14922
- for (const snap of snaps) {
14923
- const answerText = snap.answerText ?? "";
14924
- let matchedIdx = -1;
14925
- for (let i = cidx; i < Math.min(activeAt.length, cidx + LOOKAHEAD_LIMIT); i++) {
14926
- if (contentMatchScore(activeAt[i], answerText) >= 1) {
14927
- matchedIdx = i;
14928
- break;
14929
- }
14930
- }
14931
- if (matchedIdx >= 0) {
14932
- updates.push({ id: snap.id, queryText: activeAt[matchedIdx] });
14933
- runContent++;
14934
- cidx = matchedIdx + 1;
14935
- } else {
14936
- runUnrecovered++;
14937
- }
14938
- }
14939
- } else {
14940
- runUnrecovered += snaps.length;
14941
- }
14942
- }
14943
- result2.orphanSnapshots += orphanSnaps.length;
14944
- result2.recoveredByPosition += runPosition;
14945
- result2.recoveredByContent += runContent;
14946
- result2.unrecovered += runUnrecovered;
14947
- result2.perRun.push({
14948
- runId: run.runId,
14949
- runCreatedAt: run.createdAt,
14950
- activeQueryCount: activeAt.length,
14951
- orphanSnaps: orphanSnaps.length,
14952
- recoveredPosition: runPosition,
14953
- recoveredContent: runContent,
14954
- unrecovered: runUnrecovered
14955
- });
14956
- if (!isJson) {
14957
- process.stderr.write(
14958
- ` ${run.createdAt} loc=${run.location ?? "-"} \u2192 ${orphanSnaps.length} orphan; ${runPosition} position-matched, ${runContent} content-matched, ${runUnrecovered} unrecovered
14959
- `
14960
- );
14961
- }
14962
- }
14963
- if (!isDryRun && updates.length > 0) {
14964
- db.transaction((tx) => {
14965
- for (const u of updates) {
14966
- tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq17(querySnapshots.id, u.id)).run();
14967
- }
14968
- });
14969
- }
14970
- if (isJson) {
14971
- console.log(JSON.stringify(result2, null, 2));
14972
- return;
14973
- }
14974
- console.log(`
14975
- Snapshot attribution ${isDryRun ? "preview" : "recovery"} complete.`);
14976
- console.log(` Examined runs: ${result2.examinedRuns}`);
14977
- console.log(` Orphan snapshots: ${result2.orphanSnapshots}`);
14978
- console.log(` Position-matched: ${result2.recoveredByPosition}`);
14979
- console.log(` Content-matched: ${result2.recoveredByContent}`);
14980
- console.log(` Unrecovered: ${result2.unrecovered}`);
14981
- if (isDryRun) {
14982
- console.log(`
14983
- No DB writes performed. Re-run without --dry-run to apply.`);
14984
- }
14985
- }
14986
- function correctStoredOpenAIRetrieval(snapshot, reparsed) {
14987
- if (snapshot.provider !== ProviderNames.openai) return null;
14988
- if (snapshot.retrievalContract !== RetrievalContracts["native-auto-v1"]) return null;
14989
- return {
14990
- retrievalContract: OPENAI_RETRIEVAL_CONTRACT,
14991
- ...reparsed?.retrievalStatus === void 0 ? {} : { retrievalStatus: reparsed.retrievalStatus }
14992
- };
14993
- }
14994
- function reparseProviderSnapshot(provider, rawResponse) {
14995
- const envelope = parseJsonColumn(rawResponse, {});
14996
- const apiResponse = resolveStoredApiResponse(envelope);
14997
- if (!apiResponse) return null;
14998
- switch (provider) {
14999
- case ProviderNames.openai:
15000
- return reparseStoredResult2(apiResponse);
15001
- case ProviderNames.claude:
15002
- return reparseStoredResult3(apiResponse);
15003
- case ProviderNames.gemini:
15004
- return reparseStoredResult(apiResponse);
15005
- case ProviderNames.perplexity:
15006
- return reparseStoredResult4(apiResponse);
15007
- case ProviderNames.muse:
15008
- return reparseStoredResult5(apiResponse);
15009
- default:
15010
- return null;
15011
- }
15012
- }
15013
- function resolveStoredApiResponse(parsed) {
15014
- const nested = parsed.apiResponse;
15015
- if (nested !== null && typeof nested === "object" && !Array.isArray(nested)) {
15016
- return nested;
15017
- }
15018
- if (looksLikeProviderApiResponse(parsed)) {
15019
- return parsed;
15020
- }
15021
- return null;
15022
- }
15023
- function looksLikeProviderApiResponse(value) {
15024
- return Array.isArray(value.output) || Array.isArray(value.content) || Array.isArray(value.candidates) || Array.isArray(value.choices);
15025
- }
15026
- function stringifyStoredSnapshotEnvelope(rawResponse, reparsed) {
15027
- const parsed = parseJsonColumn(rawResponse, {});
15028
- const apiResponse = resolveStoredApiResponse(parsed);
15029
- const envelope = apiResponse === parsed ? {} : { ...parsed };
15030
- delete envelope.answerText;
15031
- delete envelope.citedDomains;
15032
- delete envelope.competitorOverlap;
15033
- delete envelope.recommendedCompetitors;
15034
- delete envelope.providerError;
15035
- return JSON.stringify({
15036
- ...envelope,
15037
- groundingSources: reparsed.groundingSources,
15038
- searchQueries: reparsed.searchQueries,
15039
- ...reparsed.providerError ? { providerError: reparsed.providerError } : {},
15040
- ...apiResponse ? { apiResponse } : {}
15041
- });
15042
- }
15043
- async function backfillTrafficClassificationCommand(opts) {
15044
- const config = loadConfig();
15045
- const db = createClient(config.database);
15046
- migrate(db);
15047
- const projectFilter = opts?.project?.trim();
15048
- const isDryRun = opts?.dryRun === true;
15049
- const isJson = isMachineFormat(opts?.format);
15050
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq17(projects.name, projectFilter)).all() : db.select().from(projects).all();
15051
- if (scopedProjects.length === 0) {
15052
- if (projectFilter && !isJson) {
15053
- process.stderr.write(`No project named "${projectFilter}".
15054
- `);
15055
- }
15056
- if (isJson) console.log(JSON.stringify({ project: projectFilter ?? null, examined: 0, reclassified: 0 }));
15057
- return;
15058
- }
15059
- if (!isJson) {
15060
- const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
15061
- const scope = projectFilter ? `"${projectFilter}"` : "all projects";
15062
- process.stderr.write(`Re-classifying unknown traffic samples for ${scope}${mode}...
15063
- `);
15064
- }
15065
- const projectIds = scopedProjects.map((p) => p.id);
15066
- const result2 = {
15067
- project: projectFilter ?? null,
15068
- examined: 0,
15069
- reclassified: 0,
15070
- unknownBefore: 0,
15071
- unknownAfter: 0,
15072
- dryRun: isDryRun,
15073
- byBot: {}
15074
- };
15075
- const unknownCountRow = db.select({ n: sql7`count(*)` }).from(rawEventSamples).where(and15(
15076
- eq17(rawEventSamples.eventType, "unknown"),
15077
- inArray7(rawEventSamples.projectId, projectIds)
15078
- )).get();
15079
- result2.unknownBefore = Number(unknownCountRow?.n ?? 0);
15080
- const unknownSamples = db.select({
15081
- id: rawEventSamples.id,
15082
- projectId: rawEventSamples.projectId,
15083
- sourceId: rawEventSamples.sourceId,
15084
- ts: rawEventSamples.ts,
15085
- userAgent: rawEventSamples.userAgent,
15086
- pathNormalized: rawEventSamples.pathNormalized,
15087
- status: rawEventSamples.status
15088
- }).from(rawEventSamples).where(and15(
15089
- eq17(rawEventSamples.eventType, "unknown"),
15090
- inArray7(rawEventSamples.projectId, projectIds)
15091
- )).all();
15092
- result2.examined = unknownSamples.length;
15093
- if (unknownSamples.length === 0) {
15094
- result2.unknownAfter = result2.unknownBefore;
15095
- emitTrafficClassificationReport(result2, isJson);
15096
- return;
15097
- }
15098
- const now = (/* @__PURE__ */ new Date()).toISOString();
15099
- for (const snap of unknownSamples) {
15100
- if (!snap.userAgent) continue;
15101
- const probe = {
15102
- sourceType: TrafficSourceTypes["cloud-run"],
15103
- evidenceKind: TrafficEvidenceKinds["raw-request"],
15104
- confidence: TrafficEventConfidences.observed,
15105
- eventId: snap.id,
15106
- observedAt: snap.ts,
15107
- method: "GET",
15108
- requestUrl: `https://placeholder${snap.pathNormalized}`,
15109
- host: "placeholder",
15110
- path: snap.pathNormalized,
15111
- queryString: null,
15112
- status: snap.status ?? 200,
15113
- userAgent: snap.userAgent,
15114
- remoteIp: null,
15115
- referer: null,
15116
- latencyMs: null,
15117
- requestSizeBytes: null,
15118
- responseSizeBytes: null,
15119
- providerResource: { type: "cloud_run_revision", labels: {} },
15120
- providerLabels: {}
15121
- };
15122
- const userFetch = classifyAiUserFetch(probe);
15123
- const classified = userFetch ?? classifyCrawler(probe);
15124
- if (!classified) continue;
15125
- result2.reclassified++;
15126
- result2.byBot[classified.botId] = (result2.byBot[classified.botId] ?? 0) + 1;
15127
- if (isDryRun) continue;
15128
- const tsHour = new Date(snap.ts);
15129
- tsHour.setUTCMinutes(0, 0, 0);
15130
- const tsHourIso = tsHour.toISOString();
15131
- const status = snap.status ?? 200;
15132
- db.transaction((tx) => {
15133
- tx.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq17(rawEventSamples.id, snap.id)).run();
15134
- if (userFetch) {
15135
- tx.insert(aiUserFetchEventsHourly).values({
15136
- projectId: snap.projectId,
15137
- sourceId: snap.sourceId,
15138
- tsHour: tsHourIso,
15139
- botId: userFetch.botId,
15140
- operator: userFetch.operator,
15141
- verificationStatus: userFetch.verificationStatus,
15142
- pathNormalized: snap.pathNormalized,
15143
- status,
15144
- hits: 1,
15145
- sampledUserAgent: snap.userAgent,
15146
- createdAt: now,
15147
- updatedAt: now
15148
- }).onConflictDoUpdate({
15149
- target: [
15150
- aiUserFetchEventsHourly.projectId,
15151
- aiUserFetchEventsHourly.sourceId,
15152
- aiUserFetchEventsHourly.tsHour,
15153
- aiUserFetchEventsHourly.botId,
15154
- aiUserFetchEventsHourly.verificationStatus,
15155
- aiUserFetchEventsHourly.pathNormalized,
15156
- aiUserFetchEventsHourly.status
15157
- ],
15158
- set: {
15159
- hits: sql7`${aiUserFetchEventsHourly.hits} + 1`,
15160
- updatedAt: now
15161
- }
15162
- }).run();
15163
- } else {
15164
- tx.insert(crawlerEventsHourly).values({
15165
- projectId: snap.projectId,
15166
- sourceId: snap.sourceId,
15167
- tsHour: tsHourIso,
15168
- botId: classified.botId,
15169
- operator: classified.operator,
15170
- verificationStatus: classified.verificationStatus,
15171
- pathNormalized: snap.pathNormalized,
15172
- status,
15173
- hits: 1,
15174
- sampledUserAgent: snap.userAgent,
15175
- createdAt: now,
15176
- updatedAt: now
15177
- }).onConflictDoUpdate({
15178
- target: [
15179
- crawlerEventsHourly.projectId,
15180
- crawlerEventsHourly.sourceId,
15181
- crawlerEventsHourly.tsHour,
15182
- crawlerEventsHourly.botId,
15183
- crawlerEventsHourly.verificationStatus,
15184
- crawlerEventsHourly.pathNormalized,
15185
- crawlerEventsHourly.status
15186
- ],
15187
- set: {
15188
- hits: sql7`${crawlerEventsHourly.hits} + 1`,
15189
- updatedAt: now
15190
- }
15191
- }).run();
15192
- }
15193
- });
15194
- }
15195
- if (!isDryRun) {
15196
- const afterRow = db.select({ n: sql7`count(*)` }).from(rawEventSamples).where(and15(
15197
- eq17(rawEventSamples.eventType, "unknown"),
15198
- inArray7(rawEventSamples.projectId, projectIds)
15199
- )).get();
15200
- result2.unknownAfter = Number(afterRow?.n ?? 0);
15201
- } else {
15202
- result2.unknownAfter = result2.unknownBefore - result2.reclassified;
15203
- }
15204
- emitTrafficClassificationReport(result2, isJson);
15205
- }
15206
- function emitTrafficClassificationReport(result2, isJson) {
15207
- if (isJson) {
15208
- console.log(JSON.stringify(result2, null, 2));
15209
- return;
15210
- }
15211
- const verb = result2.dryRun ? "Would re-classify" : "Re-classified";
15212
- console.log(`
15213
- Traffic classification ${result2.dryRun ? "preview" : "recovery"} complete.`);
15214
- console.log(` Examined unknowns: ${result2.examined}`);
15215
- console.log(` ${verb}: ${result2.reclassified}`);
15216
- console.log(` Unknown before: ${result2.unknownBefore}`);
15217
- console.log(` Unknown after: ${result2.unknownAfter}`);
15218
- if (result2.reclassified > 0) {
15219
- console.log(` By bot:`);
15220
- const sorted = Object.entries(result2.byBot).sort(([, a], [, b]) => b - a);
15221
- for (const [bot, count3] of sorted) {
15222
- console.log(` ${count3.toString().padStart(5)} ${bot}`);
15223
- }
15224
- }
15225
- if (result2.dryRun) {
15226
- console.log(`
15227
- No DB writes performed. Re-run without --dry-run to apply.`);
15228
- }
15229
- }
15230
-
15231
14241
  // src/provider-registry.ts
15232
14242
  var ProviderRegistry = class {
15233
14243
  providers = /* @__PURE__ */ new Map();
@@ -15343,7 +14353,7 @@ function createProviderModelCatalog(registry) {
15343
14353
  // src/scheduler.ts
15344
14354
  import crypto21 from "crypto";
15345
14355
  import cron from "node-cron";
15346
- import { and as and16, eq as eq18, inArray as inArray8, notExists, sql as sql8 } from "drizzle-orm";
14356
+ import { and as and15, eq as eq17, inArray as inArray7, notExists, sql as sql7 } from "drizzle-orm";
15347
14357
  var log15 = createLogger("Scheduler");
15348
14358
  var DEFAULT_HEALTH_CRON = "0 */6 * * *";
15349
14359
  function ensureDefaultHealthSchedule(db, projectId, now = (/* @__PURE__ */ new Date()).toISOString()) {
@@ -15394,9 +14404,9 @@ var Scheduler = class {
15394
14404
  */
15395
14405
  ensureHealthSchedules() {
15396
14406
  const projectsWithoutHealth = this.db.select({ id: projects.id }).from(projects).where(notExists(
15397
- this.db.select({ one: sql8`1` }).from(schedules).where(and16(
15398
- eq18(schedules.projectId, projects.id),
15399
- eq18(schedules.kind, SchedulableRunKinds.doctor)
14407
+ this.db.select({ one: sql7`1` }).from(schedules).where(and15(
14408
+ eq17(schedules.projectId, projects.id),
14409
+ eq17(schedules.kind, SchedulableRunKinds.doctor)
15400
14410
  ))
15401
14411
  )).all();
15402
14412
  if (projectsWithoutHealth.length === 0) return;
@@ -15439,7 +14449,7 @@ var Scheduler = class {
15439
14449
  start() {
15440
14450
  this.ensureHealthSchedules();
15441
14451
  this.ensureQueryBaskets();
15442
- const allSchedules = this.db.select().from(schedules).where(eq18(schedules.enabled, true)).all();
14452
+ const allSchedules = this.db.select().from(schedules).where(eq17(schedules.enabled, true)).all();
15443
14453
  for (const schedule of allSchedules) {
15444
14454
  const missedRunAt = schedule.nextRunAt;
15445
14455
  const recurrence = scheduleRecurrence(schedule);
@@ -15473,7 +14483,7 @@ var Scheduler = class {
15473
14483
  this.stopTask(key, existing, "Stopped");
15474
14484
  this.tasks.delete(key);
15475
14485
  }
15476
- const schedule = this.db.select().from(schedules).where(and16(eq18(schedules.projectId, projectId), eq18(schedules.kind, kind))).get();
14486
+ const schedule = this.db.select().from(schedules).where(and15(eq17(schedules.projectId, projectId), eq17(schedules.kind, kind))).get();
15477
14487
  if (schedule && schedule.enabled) {
15478
14488
  this.registerCronTask(schedule);
15479
14489
  }
@@ -15501,12 +14511,12 @@ var Scheduler = class {
15501
14511
  /** Runtime timing writes must not restore a version already issued to a settings caller. */
15502
14512
  updateScheduleTiming(scheduleId, timing) {
15503
14513
  this.db.transaction((tx) => {
15504
- const current = tx.select({ updatedAt: schedules.updatedAt }).from(schedules).where(eq18(schedules.id, scheduleId)).get();
14514
+ const current = tx.select({ updatedAt: schedules.updatedAt }).from(schedules).where(eq17(schedules.id, scheduleId)).get();
15505
14515
  if (!current) return;
15506
14516
  tx.update(schedules).set({
15507
14517
  ...timing,
15508
14518
  updatedAt: nextScheduleUpdatedAt(current.updatedAt)
15509
- }).where(eq18(schedules.id, scheduleId)).run();
14519
+ }).where(eq17(schedules.id, scheduleId)).run();
15510
14520
  });
15511
14521
  }
15512
14522
  registerCronTask(schedule, options = {}) {
@@ -15567,7 +14577,7 @@ var Scheduler = class {
15567
14577
  };
15568
14578
  const tick = () => {
15569
14579
  if (stopped) return;
15570
- const current = this.db.select().from(schedules).where(eq18(schedules.id, scheduleId)).get();
14580
+ const current = this.db.select().from(schedules).where(eq17(schedules.id, scheduleId)).get();
15571
14581
  if (!current || !current.enabled || !scheduleRecurrence(current)) {
15572
14582
  this.remove(projectId, kind);
15573
14583
  return;
@@ -15585,7 +14595,7 @@ var Scheduler = class {
15585
14595
  this.triggerRun(scheduleId, projectId, kind);
15586
14596
  }
15587
14597
  }
15588
- arm(this.db.select({ nextRunAt: schedules.nextRunAt }).from(schedules).where(eq18(schedules.id, scheduleId)).get()?.nextRunAt);
14598
+ arm(this.db.select({ nextRunAt: schedules.nextRunAt }).from(schedules).where(eq17(schedules.id, scheduleId)).get()?.nextRunAt);
15589
14599
  };
15590
14600
  this.tasks.set(key, task);
15591
14601
  let registeredNextRunAt = schedule.nextRunAt;
@@ -15600,7 +14610,7 @@ var Scheduler = class {
15600
14610
  /** Atomically advance a due callback-only calendar occurrence before dispatch. */
15601
14611
  claimCalendarOccurrence(schedule, dueAt, now) {
15602
14612
  return this.db.transaction((tx) => {
15603
- const current = tx.select().from(schedules).where(eq18(schedules.id, schedule.id)).get();
14613
+ const current = tx.select().from(schedules).where(eq17(schedules.id, schedule.id)).get();
15604
14614
  if (!current || !current.enabled || current.updatedAt !== schedule.updatedAt || current.nextRunAt !== dueAt) return false;
15605
14615
  const recurrence = scheduleRecurrence(current);
15606
14616
  if (!recurrence) return false;
@@ -15609,11 +14619,11 @@ var Scheduler = class {
15609
14619
  const result2 = tx.update(schedules).set({
15610
14620
  nextRunAt,
15611
14621
  updatedAt: nextScheduleUpdatedAt(current.updatedAt)
15612
- }).where(and16(
15613
- eq18(schedules.id, current.id),
15614
- eq18(schedules.enabled, true),
15615
- eq18(schedules.nextRunAt, dueAt),
15616
- eq18(schedules.updatedAt, schedule.updatedAt)
14622
+ }).where(and15(
14623
+ eq17(schedules.id, current.id),
14624
+ eq17(schedules.enabled, true),
14625
+ eq17(schedules.nextRunAt, dueAt),
14626
+ eq17(schedules.updatedAt, schedule.updatedAt)
15617
14627
  )).run();
15618
14628
  return result2.changes === 1;
15619
14629
  });
@@ -15621,7 +14631,7 @@ var Scheduler = class {
15621
14631
  triggerRun(scheduleId, projectId, kind, claimedOccurrence) {
15622
14632
  try {
15623
14633
  const now = (/* @__PURE__ */ new Date()).toISOString();
15624
- const currentSchedule = this.db.select().from(schedules).where(eq18(schedules.id, scheduleId)).get();
14634
+ const currentSchedule = this.db.select().from(schedules).where(eq17(schedules.id, scheduleId)).get();
15625
14635
  if (!currentSchedule || !currentSchedule.enabled) {
15626
14636
  log15.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
15627
14637
  this.remove(projectId, kind);
@@ -15637,7 +14647,7 @@ var Scheduler = class {
15637
14647
  timezone: currentSchedule.timezone,
15638
14648
  recurrence
15639
14649
  });
15640
- const project = this.db.select().from(projects).where(eq18(projects.id, projectId)).get();
14650
+ const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
15641
14651
  if (!project) {
15642
14652
  log15.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
15643
14653
  this.remove(projectId, kind);
@@ -15688,10 +14698,10 @@ var Scheduler = class {
15688
14698
  log15.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
15689
14699
  return;
15690
14700
  }
15691
- const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and16(
15692
- eq18(runs.projectId, projectId),
15693
- eq18(runs.kind, RunKinds["ads-sync"]),
15694
- inArray8(runs.status, [RunStatuses.queued, RunStatuses.running])
14701
+ const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and15(
14702
+ eq17(runs.projectId, projectId),
14703
+ eq17(runs.kind, RunKinds["ads-sync"]),
14704
+ inArray7(runs.status, [RunStatuses.queued, RunStatuses.running])
15695
14705
  )).get();
15696
14706
  if (activeAdsRun) {
15697
14707
  log15.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
@@ -15759,10 +14769,10 @@ var Scheduler = class {
15759
14769
  log15.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
15760
14770
  return;
15761
14771
  }
15762
- const active = this.db.select({ id: runs.id }).from(runs).where(and16(
15763
- eq18(runs.projectId, projectId),
15764
- eq18(runs.kind, RunKinds["site-audit"]),
15765
- inArray8(runs.status, [RunStatuses.queued, RunStatuses.running])
14772
+ const active = this.db.select({ id: runs.id }).from(runs).where(and15(
14773
+ eq17(runs.projectId, projectId),
14774
+ eq17(runs.kind, RunKinds["site-audit"]),
14775
+ inArray7(runs.status, [RunStatuses.queued, RunStatuses.running])
15766
14776
  )).get();
15767
14777
  if (active) {
15768
14778
  log15.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
@@ -15770,7 +14780,7 @@ var Scheduler = class {
15770
14780
  return;
15771
14781
  }
15772
14782
  const runId2 = crypto21.randomUUID();
15773
- const effectiveRequest = normalizeSiteAuditRunRequest({ maxPages: SITE_AUDIT_MAX_PAGE_LIMIT });
14783
+ const effectiveRequest = normalizeSiteAuditRunRequest({}, project.siteAuditMaxPages);
15774
14784
  this.db.transaction((tx) => {
15775
14785
  tx.insert(runs).values({
15776
14786
  id: runId2,
@@ -15912,7 +14922,7 @@ function startSiteLivenessLoop(deps, intervalMs = SITE_LIVENESS_INTERVAL_MS) {
15912
14922
  }
15913
14923
 
15914
14924
  // src/provider-batch-poller.ts
15915
- import { and as and17, eq as eq19, inArray as inArray9, isNotNull as isNotNull2, isNull as isNull3, notExists as notExists2, sql as sql9 } from "drizzle-orm";
14925
+ 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";
15916
14926
  var log17 = createLogger("ProviderBatchPoller");
15917
14927
  var PROVIDER_BATCH_POLL_INITIAL_MS = 3e4;
15918
14928
  var PROVIDER_BATCH_POLL_MAX_MS = 10 * 6e4;
@@ -15929,7 +14939,7 @@ var ProviderBatchPoller = class {
15929
14939
  /** One pass: advance every outstanding batch that is due, then finalize what that settled. */
15930
14940
  async tick() {
15931
14941
  const { db } = this.deps;
15932
- const rows = db.select().from(providerBatches).where(inArray9(providerBatches.status, [...OUTSTANDING_PROVIDER_BATCH_STATUSES])).all();
14942
+ const rows = db.select().from(providerBatches).where(inArray8(providerBatches.status, [...OUTSTANDING_PROVIDER_BATCH_STATUSES])).all();
15933
14943
  const live = new Set(rows.map((row) => row.id));
15934
14944
  for (const id of this.schedule.keys()) if (!live.has(id)) this.schedule.delete(id);
15935
14945
  const settledRuns = /* @__PURE__ */ new Map();
@@ -15955,7 +14965,7 @@ var ProviderBatchPoller = class {
15955
14965
  * outstanding (ingested or cancelled), so its run may now finalize.
15956
14966
  */
15957
14967
  async advance(row) {
15958
- const runStatus = this.deps.db.select({ status: runs.status }).from(runs).where(eq19(runs.id, row.runId)).get()?.status;
14968
+ const runStatus = this.deps.db.select({ status: runs.status }).from(runs).where(eq18(runs.id, row.runId)).get()?.status;
15959
14969
  if (runStatus !== RunStatuses.running) {
15960
14970
  await this.deps.runner.abandonProviderBatches([row.id], "Cancelled because its run is no longer running.");
15961
14971
  this.schedule.delete(row.id);
@@ -15972,7 +14982,7 @@ var ProviderBatchPoller = class {
15972
14982
  status = poll.status;
15973
14983
  if (poll.status === "ended") {
15974
14984
  const endedAt = poll.endedAt ?? new Date(this.now()).toISOString();
15975
- const ended = this.deps.db.update(providerBatches).set({ status: ProviderBatchStatuses.ended, endedAt, resultsExpireAt: poll.resultsExpireAt ?? null, updatedAt: endedAt }).where(and17(eq19(providerBatches.id, row.id), eq19(providerBatches.status, ProviderBatchStatuses.submitted))).run().changes === 1;
14985
+ const ended = this.deps.db.update(providerBatches).set({ status: ProviderBatchStatuses.ended, endedAt, resultsExpireAt: poll.resultsExpireAt ?? null, updatedAt: endedAt }).where(and16(eq18(providerBatches.id, row.id), eq18(providerBatches.status, ProviderBatchStatuses.submitted))).run().changes === 1;
15976
14986
  this.schedule.delete(row.id);
15977
14987
  return ended ? this.ingest({ ...row, status: ProviderBatchStatuses.ended, resultsExpireAt: poll.resultsExpireAt ?? null }) : false;
15978
14988
  }
@@ -15996,10 +15006,10 @@ var ProviderBatchPoller = class {
15996
15006
  const at = new Date(now).toISOString();
15997
15007
  if (row.cancelRequestedAt === null) {
15998
15008
  if (now < Date.parse(row.deadlineAt)) return false;
15999
- const claimed = this.deps.db.update(providerBatches).set({ cancelRequestedAt: at, updatedAt: at }).where(and17(
16000
- eq19(providerBatches.id, row.id),
16001
- eq19(providerBatches.status, ProviderBatchStatuses.submitted),
16002
- isNull3(providerBatches.cancelRequestedAt)
15009
+ const claimed = this.deps.db.update(providerBatches).set({ cancelRequestedAt: at, updatedAt: at }).where(and16(
15010
+ eq18(providerBatches.id, row.id),
15011
+ eq18(providerBatches.status, ProviderBatchStatuses.submitted),
15012
+ isNull2(providerBatches.cancelRequestedAt)
16003
15013
  )).run().changes === 1;
16004
15014
  if (!claimed) return false;
16005
15015
  log17.warn("batch.deadline-passed", { batchId: row.id, runId: row.runId, provider: row.provider, deadlineAt: row.deadlineAt });
@@ -16017,7 +15027,7 @@ var ProviderBatchPoller = class {
16017
15027
  status: ProviderBatchStatuses.cancelled,
16018
15028
  error: `The provider batch did not end within an hour of being cancelled at its deadline; its ${row.requestCount} answer(s) were not recorded.`,
16019
15029
  updatedAt: at
16020
- }).where(and17(eq19(providerBatches.id, row.id), eq19(providerBatches.status, ProviderBatchStatuses.submitted))).run().changes === 1;
15030
+ }).where(and16(eq18(providerBatches.id, row.id), eq18(providerBatches.status, ProviderBatchStatuses.submitted))).run().changes === 1;
16021
15031
  this.schedule.delete(row.id);
16022
15032
  if (gaveUp) log17.warn("batch.abandoned-after-deadline", { batchId: row.id, runId: row.runId, provider: row.provider });
16023
15033
  return gaveUp;
@@ -16057,10 +15067,10 @@ var ProviderBatchPoller = class {
16057
15067
  /** Runs the sweep handed off whose batches have all settled. */
16058
15068
  finalizableRuns() {
16059
15069
  const holding = [ProviderBatchStatuses.submitting, ...OUTSTANDING_PROVIDER_BATCH_STATUSES];
16060
- return this.deps.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and17(
16061
- eq19(runs.status, RunStatuses.running),
15070
+ return this.deps.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and16(
15071
+ eq18(runs.status, RunStatuses.running),
16062
15072
  isNotNull2(runs.pendingProviderErrors),
16063
- notExists2(this.deps.db.select({ one: sql9`1` }).from(providerBatches).where(and17(eq19(providerBatches.runId, runs.id), inArray9(providerBatches.status, holding))))
15073
+ notExists2(this.deps.db.select({ one: sql8`1` }).from(providerBatches).where(and16(eq18(providerBatches.runId, runs.id), inArray8(providerBatches.status, holding))))
16064
15074
  )).all();
16065
15075
  }
16066
15076
  };
@@ -16111,7 +15121,7 @@ async function refreshAllIntegrations(client, projectName, now = /* @__PURE__ */
16111
15121
  }
16112
15122
 
16113
15123
  // src/notifier.ts
16114
- import { eq as eq20, desc as desc9, and as and18, inArray as inArray10, or } from "drizzle-orm";
15124
+ import { eq as eq19, desc as desc8, and as and17, inArray as inArray9, or } from "drizzle-orm";
16115
15125
  import crypto22 from "crypto";
16116
15126
  var log19 = createLogger("Notifier");
16117
15127
  var MATERIAL_MAGNITUDE_DELTA = 20;
@@ -16144,18 +15154,18 @@ var Notifier = class {
16144
15154
  /** Called after a run completes (success, partial, or failed). */
16145
15155
  async onRunCompleted(runId, projectId, opts) {
16146
15156
  log19.info("run.completed", { runId, projectId, origin: opts?.origin ?? "sweep" });
16147
- const notifs = this.db.select().from(notifications).where(eq20(notifications.projectId, projectId)).all().filter((n) => n.enabled);
15157
+ const notifs = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled);
16148
15158
  if (notifs.length === 0) {
16149
15159
  log19.info("notifications.none-enabled", { projectId });
16150
15160
  return;
16151
15161
  }
16152
15162
  log19.info("notifications.found", { projectId, count: notifs.length });
16153
- const run = this.db.select().from(runs).where(eq20(runs.id, runId)).get();
15163
+ const run = this.db.select().from(runs).where(eq19(runs.id, runId)).get();
16154
15164
  if (!run) {
16155
15165
  log19.error("run.not-found", { runId, msg: "skipping notification dispatch" });
16156
15166
  return;
16157
15167
  }
16158
- const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
15168
+ const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
16159
15169
  if (!project) {
16160
15170
  log19.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
16161
15171
  return;
@@ -16208,7 +15218,7 @@ var Notifier = class {
16208
15218
  * inferred from the absence of a webhook.
16209
15219
  */
16210
15220
  async onHealthChecked(projectId, report) {
16211
- const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
15221
+ const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
16212
15222
  if (!project) {
16213
15223
  log19.error("project.not-found", { projectId, msg: "skipping health notification" });
16214
15224
  return null;
@@ -16241,7 +15251,7 @@ var Notifier = class {
16241
15251
  const status = noSignal ? "warn" : worst ? worst.status : "ok";
16242
15252
  const code = noSignal ? "health.no-signal" : worst?.code ?? "health.ok";
16243
15253
  const summary = noSignal ? `No health check produced a result (${healthChecks.length} skipped) \u2014 health is unknown, not confirmed.` : worst?.summary ?? `All ${graded.length} health check(s) passing.`;
16244
- const previous = this.db.select().from(doctorHealthState).where(eq20(doctorHealthState.projectId, projectId)).get();
15254
+ const previous = this.db.select().from(doctorHealthState).where(eq19(doctorHealthState.projectId, projectId)).get();
16245
15255
  const previousStatus = previous?.status ?? null;
16246
15256
  const failingSignature = failing.map((c) => `${c.status}:${c.code}`).join(",");
16247
15257
  let event = null;
@@ -16259,13 +15269,13 @@ var Notifier = class {
16259
15269
  if (previous === void 0) {
16260
15270
  this.db.insert(doctorHealthState).values({ ...observation, notifiedAt: null }).run();
16261
15271
  } else {
16262
- this.db.update(doctorHealthState).set(observation).where(eq20(doctorHealthState.projectId, projectId)).run();
15272
+ this.db.update(doctorHealthState).set(observation).where(eq19(doctorHealthState.projectId, projectId)).run();
16263
15273
  }
16264
15274
  if (!event) {
16265
15275
  log19.info("health.unchanged", { projectId, status, code });
16266
15276
  return null;
16267
15277
  }
16268
- const notifs = this.db.select().from(notifications).where(eq20(notifications.projectId, projectId)).all().filter((n) => n.enabled);
15278
+ const notifs = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled);
16269
15279
  const payload = {
16270
15280
  source: "canonry",
16271
15281
  event,
@@ -16288,7 +15298,7 @@ var Notifier = class {
16288
15298
  if (await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null)) delivered += 1;
16289
15299
  }
16290
15300
  if (delivered > 0) {
16291
- this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq20(doctorHealthState.projectId, projectId)).run();
15301
+ this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq19(doctorHealthState.projectId, projectId)).run();
16292
15302
  }
16293
15303
  log19.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
16294
15304
  return event;
@@ -16309,12 +15319,12 @@ var Notifier = class {
16309
15319
  async onSiteLivenessChecked(projectId, result2) {
16310
15320
  const { check, checkedAt } = result2;
16311
15321
  if (check.status !== "ok" && check.status !== "fail") return null;
16312
- const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
15322
+ const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
16313
15323
  if (!project) {
16314
15324
  log19.error("project.not-found", { projectId, msg: "skipping site liveness notification" });
16315
15325
  return null;
16316
15326
  }
16317
- const previous = this.db.select().from(siteLivenessState).where(eq20(siteLivenessState.projectId, projectId)).get();
15327
+ const previous = this.db.select().from(siteLivenessState).where(eq19(siteLivenessState.projectId, projectId)).get();
16318
15328
  const sincePrevious = previous ? Date.parse(checkedAt) - Date.parse(previous.checkedAt) : Number.NaN;
16319
15329
  const countsAsRepeat = Number.isFinite(sincePrevious) && sincePrevious >= SITE_LIVENESS_MIN_PASS_GAP_MS && sincePrevious <= SITE_LIVENESS_MAX_PASS_GAP_MS;
16320
15330
  const failures = check.status === "fail" ? countsAsRepeat ? (previous?.consecutiveFailures ?? 0) + 1 : 1 : 0;
@@ -16334,10 +15344,10 @@ var Notifier = class {
16334
15344
  if (previous === void 0) {
16335
15345
  this.db.insert(siteLivenessState).values(observation).run();
16336
15346
  } else {
16337
- this.db.update(siteLivenessState).set(observation).where(eq20(siteLivenessState.projectId, projectId)).run();
15347
+ this.db.update(siteLivenessState).set(observation).where(eq19(siteLivenessState.projectId, projectId)).run();
16338
15348
  }
16339
15349
  if (!event) return null;
16340
- const notifs = this.db.select().from(notifications).where(eq20(notifications.projectId, projectId)).all().filter((n) => n.enabled);
15350
+ const notifs = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled);
16341
15351
  const payload = {
16342
15352
  source: "canonry",
16343
15353
  event,
@@ -16363,7 +15373,7 @@ var Notifier = class {
16363
15373
  if (await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null)) delivered += 1;
16364
15374
  }
16365
15375
  if (event === "health.degraded" && delivered > 0) {
16366
- this.db.update(siteLivenessState).set({ notifiedAt: checkedAt }).where(eq20(siteLivenessState.projectId, projectId)).run();
15376
+ this.db.update(siteLivenessState).set({ notifiedAt: checkedAt }).where(eq19(siteLivenessState.projectId, projectId)).run();
16367
15377
  }
16368
15378
  log19.info("site-liveness.notified", { projectId, event, code: check.code, subscribers: notifs.length, delivered });
16369
15379
  return event;
@@ -16386,7 +15396,7 @@ var Notifier = class {
16386
15396
  */
16387
15397
  isNewInsightNews(projectId, insight) {
16388
15398
  const key = insightNotifyKey(projectId, insight);
16389
- const previous = this.db.select().from(insightNotifyState).where(eq20(insightNotifyState.key, key)).get();
15399
+ const previous = this.db.select().from(insightNotifyState).where(eq19(insightNotifyState.key, key)).get();
16390
15400
  if (!previous) return true;
16391
15401
  if (severityRank(insight.severity) > severityRank(previous.severity)) return true;
16392
15402
  const magnitude = insightMagnitude(insight);
@@ -16408,9 +15418,9 @@ var Notifier = class {
16408
15418
  magnitude: insightMagnitude(insight),
16409
15419
  notifiedAt: (/* @__PURE__ */ new Date()).toISOString()
16410
15420
  };
16411
- const existing = this.db.select().from(insightNotifyState).where(eq20(insightNotifyState.key, key)).get();
15421
+ const existing = this.db.select().from(insightNotifyState).where(eq19(insightNotifyState.key, key)).get();
16412
15422
  if (existing) {
16413
- this.db.update(insightNotifyState).set(row).where(eq20(insightNotifyState.key, key)).run();
15423
+ this.db.update(insightNotifyState).set(row).where(eq19(insightNotifyState.key, key)).run();
16414
15424
  } else {
16415
15425
  this.db.insert(insightNotifyState).values(row).run();
16416
15426
  }
@@ -16424,11 +15434,11 @@ var Notifier = class {
16424
15434
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
16425
15435
  if (highInsights.length > 0) insightEvents.push("insight.high");
16426
15436
  if (insightEvents.length === 0) return;
16427
- const notifs = this.db.select().from(notifications).where(eq20(notifications.projectId, projectId)).all().filter((n) => n.enabled);
15437
+ const notifs = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled);
16428
15438
  if (notifs.length === 0) return;
16429
- const run = this.db.select().from(runs).where(eq20(runs.id, runId)).get();
15439
+ const run = this.db.select().from(runs).where(eq19(runs.id, runId)).get();
16430
15440
  if (!run) return;
16431
- const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
15441
+ const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
16432
15442
  if (!project) return;
16433
15443
  for (const notif of notifs) {
16434
15444
  const config = notif.config;
@@ -16490,10 +15500,10 @@ var Notifier = class {
16490
15500
  }
16491
15501
  }
16492
15502
  async dispatchQueuedReviewAlerts(projectId) {
16493
- const pendingReviews = this.db.select().from(gbpReviews).where(and18(eq20(gbpReviews.projectId, projectId), eq20(gbpReviews.alertState, "pending"))).all();
16494
- const pendingRatings = this.db.select().from(gbpReviewRatings).where(and18(eq20(gbpReviewRatings.projectId, projectId), eq20(gbpReviewRatings.alertState, "pending"))).all();
15503
+ const pendingReviews = this.db.select().from(gbpReviews).where(and17(eq19(gbpReviews.projectId, projectId), eq19(gbpReviews.alertState, "pending"))).all();
15504
+ const pendingRatings = this.db.select().from(gbpReviewRatings).where(and17(eq19(gbpReviewRatings.projectId, projectId), eq19(gbpReviewRatings.alertState, "pending"))).all();
16495
15505
  if (pendingReviews.length === 0 && pendingRatings.length === 0) return { reviews: 0, ratingDrops: 0, delivered: 0 };
16496
- const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
15506
+ const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
16497
15507
  if (!project) return { reviews: 0, ratingDrops: 0, delivered: 0 };
16498
15508
  const now = /* @__PURE__ */ new Date();
16499
15509
  const nowIso = now.toISOString();
@@ -16504,10 +15514,10 @@ var Notifier = class {
16504
15514
  displayName: gbpLocations.displayName,
16505
15515
  mapsUri: gbpLocations.mapsUri,
16506
15516
  reviewsAccess: gbpLocations.reviewsAccess
16507
- }).from(gbpLocations).where(eq20(gbpLocations.projectId, projectId)).all().map((loc) => [loc.locationName, loc])
15517
+ }).from(gbpLocations).where(eq19(gbpLocations.projectId, projectId)).all().map((loc) => [loc.locationName, loc])
16508
15518
  );
16509
15519
  const v4HasWorked = new Set(
16510
- this.db.select({ locationName: gbpReviewRatings.locationName }).from(gbpReviewRatings).where(and18(eq20(gbpReviewRatings.projectId, projectId), eq20(gbpReviewRatings.origin, "gbp"))).all().map((row) => row.locationName)
15520
+ this.db.select({ locationName: gbpReviewRatings.locationName }).from(gbpReviewRatings).where(and17(eq19(gbpReviewRatings.projectId, projectId), eq19(gbpReviewRatings.origin, "gbp"))).all().map((row) => row.locationName)
16511
15521
  );
16512
15522
  const coveredByBusinessProfile = (locationName) => businessProfileReviewsCover(locations.get(locationName)?.reviewsAccess ?? null, v4HasWorked.has(locationName));
16513
15523
  const locationRef = (locationName) => {
@@ -16516,11 +15526,11 @@ var Notifier = class {
16516
15526
  };
16517
15527
  const setReviewState = (ids, alertState) => {
16518
15528
  if (ids.length === 0) return;
16519
- this.db.update(gbpReviews).set({ alertState, alertStateAt: nowIso }).where(inArray10(gbpReviews.id, ids)).run();
15529
+ this.db.update(gbpReviews).set({ alertState, alertStateAt: nowIso }).where(inArray9(gbpReviews.id, ids)).run();
16520
15530
  };
16521
15531
  const setRatingState = (ids, alertState) => {
16522
15532
  if (ids.length === 0) return;
16523
- this.db.update(gbpReviewRatings).set({ alertState, alertStateAt: nowIso }).where(inArray10(gbpReviewRatings.id, ids)).run();
15533
+ this.db.update(gbpReviewRatings).set({ alertState, alertStateAt: nowIso }).where(inArray9(gbpReviewRatings.id, ids)).run();
16524
15534
  };
16525
15535
  const reviewsToSend = [];
16526
15536
  for (const row of pendingReviews) {
@@ -16536,7 +15546,7 @@ var Notifier = class {
16536
15546
  else if (row.rating === null || row.previousRating === null) setRatingState([row.id], "none");
16537
15547
  else ratingsToSend.push(row);
16538
15548
  }
16539
- const enabled = this.db.select().from(notifications).where(eq20(notifications.projectId, projectId)).all().filter((n) => n.enabled && n.config.url);
15549
+ const enabled = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled && n.config.url);
16540
15550
  const subscribedTo = (event) => enabled.filter((n) => (n.config.events ?? []).includes(event));
16541
15551
  const projectRef = { name: project.name, canonicalDomain: project.canonicalDomain };
16542
15552
  const dashboardUrl = `${this.serverUrl}/projects/${project.name}`;
@@ -16611,7 +15621,7 @@ var Notifier = class {
16611
15621
  return { reviews: reviewsToSend.length, ratingDrops: ratingsToSend.length, delivered };
16612
15622
  }
16613
15623
  computeTransitions(runId, projectId) {
16614
- const thisRun = this.db.select().from(runs).where(eq20(runs.id, runId)).get();
15624
+ const thisRun = this.db.select().from(runs).where(eq19(runs.id, runId)).get();
16615
15625
  if (!thisRun) return [];
16616
15626
  const completeness = measurementRunCompleteness(this.db, runId);
16617
15627
  if (completeness.planned && !completeness.complete) {
@@ -16622,10 +15632,10 @@ var Notifier = class {
16622
15632
  });
16623
15633
  return [];
16624
15634
  }
16625
- const groupSiblings = this.db.select().from(runs).where(and18(
16626
- eq20(runs.projectId, projectId),
16627
- eq20(runs.kind, thisRun.kind),
16628
- eq20(runs.createdAt, thisRun.createdAt),
15635
+ const groupSiblings = this.db.select().from(runs).where(and17(
15636
+ eq19(runs.projectId, projectId),
15637
+ eq19(runs.kind, thisRun.kind),
15638
+ eq19(runs.createdAt, thisRun.createdAt),
16629
15639
  notProbeRun()
16630
15640
  )).all();
16631
15641
  const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
@@ -16642,20 +15652,20 @@ var Notifier = class {
16642
15652
  return candidate.id > best.id ? candidate : best;
16643
15653
  });
16644
15654
  if (winner.id !== runId) return [];
16645
- const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq20(projects.id, projectId)).get();
15655
+ const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq19(projects.id, projectId)).get();
16646
15656
  const locationCount = Math.max(
16647
15657
  1,
16648
15658
  (projectLocations?.locations ?? []).length
16649
15659
  );
16650
15660
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
16651
15661
  const recentRuns = this.db.select().from(runs).where(
16652
- and18(
16653
- eq20(runs.projectId, projectId),
16654
- eq20(runs.kind, thisRun.kind),
16655
- or(eq20(runs.status, "completed"), eq20(runs.status, "partial")),
15662
+ and17(
15663
+ eq19(runs.projectId, projectId),
15664
+ eq19(runs.kind, thisRun.kind),
15665
+ or(eq19(runs.status, "completed"), eq19(runs.status, "partial")),
16656
15666
  notProbeRun()
16657
15667
  )
16658
- ).orderBy(desc9(runs.createdAt), desc9(runs.id)).limit(RECENT_FETCH_LIMIT).all();
15668
+ ).orderBy(desc8(runs.createdAt), desc8(runs.id)).limit(RECENT_FETCH_LIMIT).all();
16659
15669
  const groups = groupRunsByCreatedAt(recentRuns);
16660
15670
  const currentGroupIdx = groups.findIndex((g) => g[0]?.createdAt === thisRun.createdAt);
16661
15671
  if (currentGroupIdx < 0) return [];
@@ -16670,13 +15680,13 @@ var Notifier = class {
16670
15680
  provider: querySnapshots.provider,
16671
15681
  location: querySnapshots.location,
16672
15682
  citationState: querySnapshots.citationState
16673
- }).from(querySnapshots).leftJoin(queries, eq20(querySnapshots.queryId, queries.id)).where(inArray10(querySnapshots.runId, currentRunIds)).all();
15683
+ }).from(querySnapshots).leftJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(inArray9(querySnapshots.runId, currentRunIds)).all();
16674
15684
  const previousSnapshots = this.db.select({
16675
15685
  queryId: querySnapshots.queryId,
16676
15686
  provider: querySnapshots.provider,
16677
15687
  location: querySnapshots.location,
16678
15688
  citationState: querySnapshots.citationState
16679
- }).from(querySnapshots).where(inArray10(querySnapshots.runId, previousRunIds)).all();
15689
+ }).from(querySnapshots).where(inArray9(querySnapshots.runId, previousRunIds)).all();
16680
15690
  const prevMap = /* @__PURE__ */ new Map();
16681
15691
  for (const s of previousSnapshots) {
16682
15692
  if (s.queryId == null) continue;
@@ -16755,7 +15765,7 @@ var Notifier = class {
16755
15765
  };
16756
15766
 
16757
15767
  // src/run-coordinator.ts
16758
- import { eq as eq21 } from "drizzle-orm";
15768
+ import { eq as eq20 } from "drizzle-orm";
16759
15769
  var log20 = createLogger("RunCoordinator");
16760
15770
  var RunCoordinator = class {
16761
15771
  constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
@@ -16773,7 +15783,7 @@ var RunCoordinator = class {
16773
15783
  /** Host wakeup only; durable completion receipts remain the delivery authority. */
16774
15784
  onSentimentCompleted;
16775
15785
  async onRunCompleted(runId, projectId, opts) {
16776
- const runRow = this.db.select().from(runs).where(eq21(runs.id, runId)).get();
15786
+ const runRow = this.db.select().from(runs).where(eq20(runs.id, runId)).get();
16777
15787
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
16778
15788
  if (runRow?.trigger === RunTriggers.probe) {
16779
15789
  log20.info("probe.skip-side-effects", { runId, projectId, kind });
@@ -16876,7 +15886,7 @@ var RunCoordinator = class {
16876
15886
  * so the Aero queue is never starved of a follow-up.
16877
15887
  */
16878
15888
  buildDiscoveryAeroContext(runId, projectId, status, error) {
16879
- const session = this.db.select().from(discoverySessions).where(eq21(discoverySessions.runId, runId)).get();
15889
+ const session = this.db.select().from(discoverySessions).where(eq20(discoverySessions.runId, runId)).get();
16880
15890
  const competitorMap = session ? session.competitorMap : [];
16881
15891
  return {
16882
15892
  kind: RunKinds["aeo-discover-probe"],
@@ -16919,7 +15929,7 @@ function analysisResultFromInsights(insights2) {
16919
15929
 
16920
15930
  // src/agent/session-registry.ts
16921
15931
  import crypto26 from "crypto";
16922
- import { eq as eq24 } from "drizzle-orm";
15932
+ import { eq as eq23 } from "drizzle-orm";
16923
15933
 
16924
15934
  // src/agent/session.ts
16925
15935
  import fs8 from "fs";
@@ -18879,7 +17889,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
18879
17889
 
18880
17890
  // src/agent/memory-store.ts
18881
17891
  import crypto25 from "crypto";
18882
- import { and as and19, desc as desc10, eq as eq22, like, sql as sql10, or as or2 } from "drizzle-orm";
17892
+ import { and as and18, desc as desc9, eq as eq21, like, sql as sql9, or as or2 } from "drizzle-orm";
18883
17893
  var COMPACTION_KEY_PREFIX = "compaction:";
18884
17894
  var COMPACTION_NOTES_PER_SESSION = 3;
18885
17895
  function rowToDto(row) {
@@ -18893,7 +17903,7 @@ function rowToDto(row) {
18893
17903
  };
18894
17904
  }
18895
17905
  function listMemoryEntries(db, projectId, opts = {}) {
18896
- const query = db.select().from(agentMemory).where(eq22(agentMemory.projectId, projectId)).orderBy(desc10(agentMemory.updatedAt));
17906
+ const query = db.select().from(agentMemory).where(eq21(agentMemory.projectId, projectId)).orderBy(desc9(agentMemory.updatedAt));
18897
17907
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
18898
17908
  return rows.map(rowToDto);
18899
17909
  }
@@ -18924,22 +17934,22 @@ function upsertMemoryEntry(db, args) {
18924
17934
  updatedAt: now
18925
17935
  }
18926
17936
  }).run();
18927
- const row = db.select().from(agentMemory).where(and19(eq22(agentMemory.projectId, args.projectId), eq22(agentMemory.key, args.key))).get();
17937
+ const row = db.select().from(agentMemory).where(and18(eq21(agentMemory.projectId, args.projectId), eq21(agentMemory.key, args.key))).get();
18928
17938
  if (!row) throw new Error("memory upsert produced no row");
18929
17939
  return rowToDto(row);
18930
17940
  }
18931
17941
  function deleteMemoryEntry(db, projectId, key) {
18932
- const result2 = db.delete(agentMemory).where(and19(eq22(agentMemory.projectId, projectId), eq22(agentMemory.key, key))).run();
17942
+ const result2 = db.delete(agentMemory).where(and18(eq21(agentMemory.projectId, projectId), eq21(agentMemory.key, key))).run();
18933
17943
  const changes = result2.changes ?? 0;
18934
17944
  return changes > 0;
18935
17945
  }
18936
17946
  function loadRecentForHydrate(db, projectId, limit, sessionId) {
18937
17947
  if (sessionId === void 0) return listMemoryEntries(db, projectId, { limit });
18938
17948
  const sessionPrefix = `compaction:${sessionId ?? ""}:`;
18939
- return db.select().from(agentMemory).where(and19(
18940
- eq22(agentMemory.projectId, projectId),
18941
- or2(sql10`substr(${agentMemory.key}, 1, ${COMPACTION_KEY_PREFIX.length}) <> ${COMPACTION_KEY_PREFIX}`, sessionId ? sql10`substr(${agentMemory.key}, 1, ${sessionPrefix.length}) = ${sessionPrefix}` : void 0)
18942
- )).orderBy(desc10(agentMemory.updatedAt)).limit(limit).all().map(rowToDto);
17949
+ return db.select().from(agentMemory).where(and18(
17950
+ eq21(agentMemory.projectId, projectId),
17951
+ or2(sql9`substr(${agentMemory.key}, 1, ${COMPACTION_KEY_PREFIX.length}) <> ${COMPACTION_KEY_PREFIX}`, sessionId ? sql9`substr(${agentMemory.key}, 1, ${sessionPrefix.length}) = ${sessionPrefix}` : void 0)
17952
+ )).orderBy(desc9(agentMemory.updatedAt)).limit(limit).all().map(rowToDto);
18943
17953
  }
18944
17954
  function writeCompactionNote(db, args) {
18945
17955
  if (Buffer.byteLength(args.summary, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
@@ -18963,16 +17973,16 @@ function writeCompactionNote(db, args) {
18963
17973
  }).run();
18964
17974
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
18965
17975
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
18966
- and19(
18967
- eq22(agentMemory.projectId, args.projectId),
17976
+ and18(
17977
+ eq21(agentMemory.projectId, args.projectId),
18968
17978
  like(agentMemory.key, `${sessionPrefix}%`)
18969
17979
  )
18970
- ).orderBy(desc10(agentMemory.updatedAt)).all();
17980
+ ).orderBy(desc9(agentMemory.updatedAt)).all();
18971
17981
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
18972
17982
  if (stale.length > 0) {
18973
- tx.delete(agentMemory).where(sql10`${agentMemory.id} IN (${sql10.join(stale.map((s) => sql10`${s}`), sql10`, `)})`).run();
17983
+ tx.delete(agentMemory).where(sql9`${agentMemory.id} IN (${sql9.join(stale.map((s) => sql9`${s}`), sql9`, `)})`).run();
18974
17984
  }
18975
- const row = tx.select().from(agentMemory).where(and19(eq22(agentMemory.projectId, args.projectId), eq22(agentMemory.key, key))).get();
17985
+ const row = tx.select().from(agentMemory).where(and18(eq21(agentMemory.projectId, args.projectId), eq21(agentMemory.key, key))).get();
18976
17986
  if (row) inserted = rowToDto(row);
18977
17987
  });
18978
17988
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -18980,7 +17990,7 @@ function writeCompactionNote(db, args) {
18980
17990
  }
18981
17991
 
18982
17992
  // src/agent/project-shape.ts
18983
- import { and as and20, count as count2, eq as eq23 } from "drizzle-orm";
17993
+ import { and as and19, count as count2, eq as eq22 } from "drizzle-orm";
18984
17994
  var ADVANCED_TOOLS = [
18985
17995
  "canonry_measurement_portfolio_summary",
18986
17996
  "canonry_measurement_overview",
@@ -19006,10 +18016,10 @@ var ADVANCED_ROUTES = [
19006
18016
  ];
19007
18017
  function aeroProjectShape(db, projectId) {
19008
18018
  try {
19009
- const pointer = db.select().from(measurementPlans).where(eq23(measurementPlans.projectId, projectId)).get();
19010
- const version = pointer ? db.select().from(measurementPlanVersions).where(and20(
19011
- eq23(measurementPlanVersions.projectId, projectId),
19012
- eq23(measurementPlanVersions.id, pointer.activeVersionId)
18019
+ const pointer = db.select().from(measurementPlans).where(eq22(measurementPlans.projectId, projectId)).get();
18020
+ const version = pointer ? db.select().from(measurementPlanVersions).where(and19(
18021
+ eq22(measurementPlanVersions.projectId, projectId),
18022
+ eq22(measurementPlanVersions.id, pointer.activeVersionId)
19013
18023
  )).get() : void 0;
19014
18024
  if (version) {
19015
18025
  const plan = JSON.parse(version.canonicalJson);
@@ -19036,7 +18046,7 @@ canonry_visibility_report describes the whole project, not one Property. canonry
19036
18046
  pinned: ADVANCED_TOOLS
19037
18047
  };
19038
18048
  }
19039
- const tracked = db.select({ total: count2() }).from(queries).where(eq23(queries.projectId, projectId)).get()?.total ?? 0;
18049
+ const tracked = db.select({ total: count2() }).from(queries).where(eq22(queries.projectId, projectId)).get()?.total ?? 0;
19040
18050
  return {
19041
18051
  prompt: `
19042
18052
 
@@ -19292,7 +18302,7 @@ var SessionRegistry = class {
19292
18302
  modelProvider: effectiveProvider,
19293
18303
  modelId: effectiveModelId,
19294
18304
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
19295
- }).where(eq24(agentSessions.projectId, projectId)).run();
18305
+ }).where(eq23(agentSessions.projectId, projectId)).run();
19296
18306
  }
19297
18307
  const agent2 = createAeroSession({
19298
18308
  projectName,
@@ -19588,7 +18598,7 @@ ${lines.join("\n")}
19588
18598
  modelProvider: nextProvider,
19589
18599
  modelId: nextModelId,
19590
18600
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
19591
- }).where(eq24(agentSessions.projectId, projectId)).run();
18601
+ }).where(eq23(agentSessions.projectId, projectId)).run();
19592
18602
  }
19593
18603
  /** Snapshot pending work without consuming it before an atomic conversation switch. */
19594
18604
  prepareConversationChange(projectName) {
@@ -19763,11 +18773,11 @@ ${lines.join("\n")}
19763
18773
  return id;
19764
18774
  }
19765
18775
  tryResolveProjectId(projectName) {
19766
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq24(projects.name, projectName)).get();
18776
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq23(projects.name, projectName)).get();
19767
18777
  return row?.id;
19768
18778
  }
19769
18779
  loadRow(projectId) {
19770
- const row = this.opts.db.select().from(agentSessions).where(eq24(agentSessions.projectId, projectId)).get();
18780
+ const row = this.opts.db.select().from(agentSessions).where(eq23(agentSessions.projectId, projectId)).get();
19771
18781
  return row ?? null;
19772
18782
  }
19773
18783
  insertRow(params) {
@@ -19786,7 +18796,7 @@ ${lines.join("\n")}
19786
18796
  }
19787
18797
  updateRow(projectId, patch) {
19788
18798
  const now = (/* @__PURE__ */ new Date()).toISOString();
19789
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq24(agentSessions.projectId, projectId)).run();
18799
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq23(agentSessions.projectId, projectId)).run();
19790
18800
  }
19791
18801
  /**
19792
18802
  * Store a refreshed prompt snapshot without touching `updatedAt`, which the
@@ -19795,13 +18805,13 @@ ${lines.join("\n")}
19795
18805
  * recently used right after an upgrade.
19796
18806
  */
19797
18807
  persistPromptSnapshot(projectId, systemPrompt) {
19798
- this.opts.db.update(agentSessions).set({ systemPrompt }).where(eq24(agentSessions.projectId, projectId)).run();
18808
+ this.opts.db.update(agentSessions).set({ systemPrompt }).where(eq23(agentSessions.projectId, projectId)).run();
19799
18809
  }
19800
18810
  };
19801
18811
 
19802
18812
  // src/agent/viewer-sessions.ts
19803
18813
  import crypto27 from "crypto";
19804
- import { and as and21, eq as eq25, isNotNull as isNotNull3, like as like2 } from "drizzle-orm";
18814
+ import { and as and20, eq as eq24, isNotNull as isNotNull3, like as like2 } from "drizzle-orm";
19805
18815
  var AERO_VIEWER_EXCLUDED_MCP_TOOLS = /* @__PURE__ */ new Set([
19806
18816
  CanonryMcpToolNames.canonry_ads_account,
19807
18817
  CanonryMcpToolNames.canonry_ads_geo_search,
@@ -19977,10 +18987,10 @@ var ViewerAeroSessions = class {
19977
18987
  */
19978
18988
  removeKey(id) {
19979
18989
  try {
19980
- this.opts.db.delete(apiKeys).where(eq25(apiKeys.id, id)).run();
18990
+ this.opts.db.delete(apiKeys).where(eq24(apiKeys.id, id)).run();
19981
18991
  } catch {
19982
18992
  try {
19983
- this.opts.db.update(apiKeys).set({ revokedAt: new Date(this.now()).toISOString() }).where(eq25(apiKeys.id, id)).run();
18993
+ this.opts.db.update(apiKeys).set({ revokedAt: new Date(this.now()).toISOString() }).where(eq24(apiKeys.id, id)).run();
19984
18994
  } catch {
19985
18995
  }
19986
18996
  }
@@ -19992,7 +19002,7 @@ var ViewerAeroSessions = class {
19992
19002
  */
19993
19003
  revokeOrphanedKeys() {
19994
19004
  try {
19995
- this.opts.db.delete(apiKeys).where(and21(like2(apiKeys.name, `${VIEWER_AERO_KEY_PREFIX}%`), isNotNull3(apiKeys.delegatedUserId))).run();
19005
+ this.opts.db.delete(apiKeys).where(and20(like2(apiKeys.name, `${VIEWER_AERO_KEY_PREFIX}%`), isNotNull3(apiKeys.delegatedUserId))).run();
19996
19006
  } catch {
19997
19007
  }
19998
19008
  }
@@ -20008,7 +19018,7 @@ var ViewerAeroSessions = class {
20008
19018
 
20009
19019
  // src/mcp-http.ts
20010
19020
  import crypto28 from "crypto";
20011
- import { eq as eq26 } from "drizzle-orm";
19021
+ import { eq as eq25 } from "drizzle-orm";
20012
19022
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
20013
19023
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
20014
19024
  var SESSION_IDLE_MS = 10 * 60 * 1e3;
@@ -20029,7 +19039,7 @@ function mintSessionKey(db, scopes, userId) {
20029
19039
  }
20030
19040
  function revokeSessionKey(db, id) {
20031
19041
  try {
20032
- db.update(apiKeys).set({ revokedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(apiKeys.id, id)).run();
19042
+ db.update(apiKeys).set({ revokedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(apiKeys.id, id)).run();
20033
19043
  } catch {
20034
19044
  }
20035
19045
  }
@@ -20212,7 +19222,7 @@ function registerMcpHttpRoutes(scope, opts) {
20212
19222
  }
20213
19223
 
20214
19224
  // src/agent/agent-routes.ts
20215
- import { eq as eq27 } from "drizzle-orm";
19225
+ import { eq as eq26 } from "drizzle-orm";
20216
19226
  function viewerAeroCaller(request, opts) {
20217
19227
  if (!opts.viewerSessions) return null;
20218
19228
  const principal = request.principal;
@@ -20242,7 +19252,7 @@ function viewerTranscript(opts, projectName, userId) {
20242
19252
  };
20243
19253
  }
20244
19254
  function resolveProject(db, name) {
20245
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq27(projects.name, name)).get();
19255
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq26(projects.name, name)).get();
20246
19256
  if (!row) throw notFound("project", name);
20247
19257
  return row;
20248
19258
  }
@@ -20355,7 +19365,7 @@ function registerAgentRoutes(app, opts) {
20355
19365
  }
20356
19366
  requireInstanceAdministrator(request);
20357
19367
  const project = resolveProject(opts.db, request.params.name);
20358
- const row = opts.db.select().from(agentSessions).where(eq27(agentSessions.projectId, project.id)).get();
19368
+ const row = opts.db.select().from(agentSessions).where(eq26(agentSessions.projectId, project.id)).get();
20359
19369
  if (!row) {
20360
19370
  return { conversationId: null, messages: [], modelProvider: null, modelId: null, updatedAt: null };
20361
19371
  }
@@ -20395,7 +19405,7 @@ function registerAgentRoutes(app, opts) {
20395
19405
  const project = resolveProject(opts.db, request.params.name);
20396
19406
  if (opts.sessionRegistry.isBusy(project.name)) throw agentBusy(project.name);
20397
19407
  opts.sessionRegistry.reset(project.name);
20398
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq27(agentSessions.projectId, project.id)).run();
19408
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(agentSessions.projectId, project.id)).run();
20399
19409
  return { status: "reset" };
20400
19410
  }
20401
19411
  );
@@ -20419,7 +19429,7 @@ function registerAgentRoutes(app, opts) {
20419
19429
  return streamViewerTurn(opts, project, viewerId, body, reply);
20420
19430
  }
20421
19431
  if (body.conversationId !== void 0) {
20422
- const current = opts.db.select({ id: agentSessions.id }).from(agentSessions).where(eq27(agentSessions.projectId, project.id)).get();
19432
+ const current = opts.db.select({ id: agentSessions.id }).from(agentSessions).where(eq26(agentSessions.projectId, project.id)).get();
20423
19433
  if ((current?.id ?? null) !== body.conversationId) throw validationError("The active conversation changed. Reload the conversation before sending.");
20424
19434
  }
20425
19435
  const promptText = body.prompt;
@@ -21356,20 +20366,20 @@ function clipText(value, length) {
21356
20366
  }
21357
20367
 
21358
20368
  // src/research-runner.ts
21359
- import { and as and22, eq as eq28, inArray as inArray11, sql as sql11 } from "drizzle-orm";
20369
+ import { and as and21, eq as eq27, inArray as inArray10, sql as sql10 } from "drizzle-orm";
21360
20370
  var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
21361
20371
  function finalResearchRunStatus(completed, failed) {
21362
20372
  if (failed === 0) return ResearchRunStatuses.completed;
21363
20373
  return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
21364
20374
  }
21365
20375
  async function executeResearchRun(db, registry, runId, projectId) {
21366
- const run = db.select().from(researchRuns).where(and22(eq28(researchRuns.id, runId), eq28(researchRuns.projectId, projectId))).get();
20376
+ const run = db.select().from(researchRuns).where(and21(eq27(researchRuns.id, runId), eq27(researchRuns.projectId, projectId))).get();
21367
20377
  if (!run || run.status !== ResearchRunStatuses.queued) return;
21368
- const project = db.select().from(projects).where(eq28(projects.id, projectId)).get();
20378
+ const project = db.select().from(projects).where(eq27(projects.id, projectId)).get();
21369
20379
  if (!project) return;
21370
20380
  const provider = registry.get(run.provider);
21371
20381
  const now = (/* @__PURE__ */ new Date()).toISOString();
21372
- const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and22(eq28(researchRuns.id, runId), eq28(researchRuns.status, ResearchRunStatuses.queued))).run();
20382
+ const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and21(eq27(researchRuns.id, runId), eq27(researchRuns.status, ResearchRunStatuses.queued))).run();
21373
20383
  if (claim.changes !== 1) return;
21374
20384
  let reserved = 0;
21375
20385
  let dispatched = 0;
@@ -21393,8 +20403,10 @@ async function executeResearchRun(db, registry, runId, projectId) {
21393
20403
  }
21394
20404
  reserved = run.totalQueries;
21395
20405
  reservation = { scope, period };
21396
- const rows = db.select().from(researchRunQueries).where(eq28(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
21397
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq28(competitors.projectId, projectId)).all().map((row) => row.domain);
20406
+ const rows = db.select().from(researchRunQueries).where(eq27(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
20407
+ const competitorRows = db.select({ domain: competitors.domain, aliases: competitors.aliases }).from(competitors).where(eq27(competitors.projectId, projectId)).all();
20408
+ const competitorDomains = competitorRows.map((row) => row.domain);
20409
+ const competitorAliases = new Map(competitorRows.filter((row) => row.aliases.length > 0).map((row) => [row.domain, row.aliases]));
21398
20410
  const domains = effectiveDomains(project);
21399
20411
  const brands = effectiveBrandNames(project);
21400
20412
  const config = { ...provider.config, model: run.resolvedModel };
@@ -21402,7 +20414,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
21402
20414
  await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
21403
20415
  try {
21404
20416
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
21405
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and22(eq28(researchRunQueries.id, row.id), eq28(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
20417
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and21(eq27(researchRunQueries.id, row.id), eq27(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
21406
20418
  const raw = await gate.run(async () => {
21407
20419
  dispatched++;
21408
20420
  return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
@@ -21413,7 +20425,8 @@ async function executeResearchRun(db, registry, runId, projectId) {
21413
20425
  domains,
21414
20426
  normalized.citedDomains,
21415
20427
  competitorDomains,
21416
- brands
20428
+ brands,
20429
+ competitorAliases
21417
20430
  );
21418
20431
  const citedCompetitorDomains = computeCitedCompetitorDomains(normalized.citedDomains, competitorDomains);
21419
20432
  const completed = db.update(researchRunQueries).set({
@@ -21429,7 +20442,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
21429
20442
  citationState: determineCitationState(normalized, domains),
21430
20443
  rawResponse: raw.rawResponse,
21431
20444
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
21432
- }).where(and22(eq28(researchRunQueries.id, row.id), eq28(researchRunQueries.status, ResearchQueryStatuses.running))).run();
20445
+ }).where(and21(eq27(researchRunQueries.id, row.id), eq27(researchRunQueries.status, ResearchQueryStatuses.running))).run();
21433
20446
  if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
21434
20447
  } catch (error) {
21435
20448
  try {
@@ -21453,18 +20466,18 @@ async function executeResearchRun(db, registry, runId, projectId) {
21453
20466
  }
21454
20467
  }
21455
20468
  function incrementResearchProgress(db, runId, column) {
21456
- db.update(researchRuns).set({ [column]: sql11`${researchRuns[column]} + 1` }).where(eq28(researchRuns.id, runId)).run();
20469
+ db.update(researchRuns).set({ [column]: sql10`${researchRuns[column]} + 1` }).where(eq27(researchRuns.id, runId)).run();
21457
20470
  }
21458
20471
  function markResearchQueryFailed(db, runId, queryId, error) {
21459
20472
  const message = describeError(error);
21460
- const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and22(eq28(researchRunQueries.id, queryId), inArray11(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
20473
+ const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and21(eq27(researchRunQueries.id, queryId), inArray10(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
21461
20474
  if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
21462
20475
  }
21463
20476
  function markUnfinishedResearchQueriesFailed(db, runId, error) {
21464
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and22(eq28(researchRunQueries.researchRunId, runId), inArray11(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
20477
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and21(eq27(researchRunQueries.researchRunId, runId), inArray10(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
21465
20478
  }
21466
20479
  function finalizeResearchRun(db, runId, fatalError) {
21467
- const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq28(researchRunQueries.researchRunId, runId)).all();
20480
+ const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq27(researchRunQueries.researchRunId, runId)).all();
21468
20481
  const completed = rows.filter((row) => row.status === ResearchQueryStatuses.completed).length;
21469
20482
  const failed = rows.filter((row) => row.status === ResearchQueryStatuses.failed).length;
21470
20483
  db.update(researchRuns).set({
@@ -21473,11 +20486,11 @@ function finalizeResearchRun(db, runId, fatalError) {
21473
20486
  failedQueries: failed,
21474
20487
  error: fatalError ?? (failed === rows.length ? "Every research query failed." : null),
21475
20488
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
21476
- }).where(eq28(researchRuns.id, runId)).run();
20489
+ }).where(eq27(researchRuns.id, runId)).run();
21477
20490
  }
21478
20491
 
21479
20492
  // src/google-marketing-sync.ts
21480
- import { and as and23, eq as eq29, isNull as isNull4 } from "drizzle-orm";
20493
+ import { and as and22, eq as eq28, isNull as isNull3 } from "drizzle-orm";
21481
20494
  function safeErrorMessage(error) {
21482
20495
  const raw = describeError(error);
21483
20496
  return raw.replace(/(["']?authorization["']?\s*[:=]\s*["']?bearer\s+)[^\s,"'}]+/gi, "$1[redacted]").replace(/(["']?(?:developer|access|refresh|client)[_-]?(?:token|secret)["']?\s*[:=]\s*["']?)[^\s,"'}]+/gi, "$1[redacted]").replace(/[\r\n\t]+/g, " ").slice(0, 1e3);
@@ -21500,10 +20513,10 @@ function assertSnapshotContext(metadata2, expected) {
21500
20513
  }
21501
20514
  }
21502
20515
  function requireSyncRun(db, runId, projectId, kind) {
21503
- const run = db.select().from(runs).where(and23(
21504
- eq29(runs.id, runId),
21505
- eq29(runs.projectId, projectId),
21506
- eq29(runs.kind, kind)
20516
+ const run = db.select().from(runs).where(and22(
20517
+ eq28(runs.id, runId),
20518
+ eq28(runs.projectId, projectId),
20519
+ eq28(runs.kind, kind)
21507
20520
  )).get();
21508
20521
  if (!run) throw new Error(`Run ${runId} is not a ${kind} run for this project.`);
21509
20522
  if (run.status !== RunStatuses.queued) {
@@ -21516,19 +20529,19 @@ function markFailed(db, runId, projectId, kind, expectedStatus, error) {
21516
20529
  status: RunStatuses.failed,
21517
20530
  error: serializeRunError({ message: safeErrorMessage(error) }),
21518
20531
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
21519
- }).where(and23(
21520
- eq29(runs.id, runId),
21521
- eq29(runs.projectId, projectId),
21522
- eq29(runs.kind, kind),
21523
- eq29(runs.status, expectedStatus)
20532
+ }).where(and22(
20533
+ eq28(runs.id, runId),
20534
+ eq28(runs.projectId, projectId),
20535
+ eq28(runs.kind, kind),
20536
+ eq28(runs.status, expectedStatus)
21524
20537
  )).run();
21525
20538
  }
21526
20539
  function claimSyncRun(db, runId, projectId, kind, startedAt) {
21527
- const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and23(
21528
- eq29(runs.id, runId),
21529
- eq29(runs.projectId, projectId),
21530
- eq29(runs.kind, kind),
21531
- eq29(runs.status, RunStatuses.queued)
20540
+ const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and22(
20541
+ eq28(runs.id, runId),
20542
+ eq28(runs.projectId, projectId),
20543
+ eq28(runs.kind, kind),
20544
+ eq28(runs.status, RunStatuses.queued)
21532
20545
  )).run();
21533
20546
  return claim.changes === 1;
21534
20547
  }
@@ -21536,9 +20549,9 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
21536
20549
  let claimed = false;
21537
20550
  try {
21538
20551
  requireSyncRun(db, runId, projectId, RunKinds["google-ads-sync"]);
21539
- const project = db.select().from(projects).where(eq29(projects.id, projectId)).get();
20552
+ const project = db.select().from(projects).where(eq28(projects.id, projectId)).get();
21540
20553
  if (!project) throw new Error(`Project ${projectId} was not found.`);
21541
- const connection = db.select().from(googleAdsConnections).where(eq29(googleAdsConnections.projectId, projectId)).get();
20554
+ const connection = db.select().from(googleAdsConnections).where(eq28(googleAdsConnections.projectId, projectId)).get();
21542
20555
  if (!connection?.selectedCustomerId) {
21543
20556
  throw new Error("Google Ads needs a selected customer before it can sync.");
21544
20557
  }
@@ -21589,11 +20602,11 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
21589
20602
  status: RunStatuses.completed,
21590
20603
  finishedAt,
21591
20604
  error: null
21592
- }).where(and23(
21593
- eq29(runs.id, runId),
21594
- eq29(runs.projectId, projectId),
21595
- eq29(runs.kind, RunKinds["google-ads-sync"]),
21596
- eq29(runs.status, RunStatuses.running)
20605
+ }).where(and22(
20606
+ eq28(runs.id, runId),
20607
+ eq28(runs.projectId, projectId),
20608
+ eq28(runs.kind, RunKinds["google-ads-sync"]),
20609
+ eq28(runs.status, RunStatuses.running)
21597
20610
  )).run();
21598
20611
  if (terminalClaim.changes !== 1) {
21599
20612
  throw new Error(`Run ${runId} was no longer running when Google Ads evidence was ready.`);
@@ -21610,12 +20623,12 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
21610
20623
  lastMetricsSnapshotAt: result2.metrics?.metadata.capturedAt ?? connection.lastMetricsSnapshotAt,
21611
20624
  lastMetricsSnapshotId: result2.metrics?.metadata.id ?? connection.lastMetricsSnapshotId,
21612
20625
  updatedAt: finishedAt
21613
- }).where(and23(
21614
- eq29(googleAdsConnections.id, connection.id),
21615
- eq29(googleAdsConnections.projectId, projectId),
21616
- eq29(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
21617
- connection.selectedLoginCustomerId === null ? isNull4(googleAdsConnections.selectedLoginCustomerId) : eq29(googleAdsConnections.selectedLoginCustomerId, connection.selectedLoginCustomerId),
21618
- eq29(googleAdsConnections.selectionGeneration, connection.selectionGeneration)
20626
+ }).where(and22(
20627
+ eq28(googleAdsConnections.id, connection.id),
20628
+ eq28(googleAdsConnections.projectId, projectId),
20629
+ eq28(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
20630
+ connection.selectedLoginCustomerId === null ? isNull3(googleAdsConnections.selectedLoginCustomerId) : eq28(googleAdsConnections.selectedLoginCustomerId, connection.selectedLoginCustomerId),
20631
+ eq28(googleAdsConnections.selectionGeneration, connection.selectionGeneration)
21619
20632
  )).run();
21620
20633
  if (connectionClaim.changes !== 1) {
21621
20634
  throw new Error("Google Ads selection changed while its evidence was being collected.");
@@ -21642,9 +20655,9 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
21642
20655
  let claimed = false;
21643
20656
  try {
21644
20657
  requireSyncRun(db, runId, projectId, RunKinds["gtm-sync"]);
21645
- const project = db.select().from(projects).where(eq29(projects.id, projectId)).get();
20658
+ const project = db.select().from(projects).where(eq28(projects.id, projectId)).get();
21646
20659
  if (!project) throw new Error(`Project ${projectId} was not found.`);
21647
- const connection = db.select().from(gtmConnections).where(eq29(gtmConnections.projectId, projectId)).get();
20660
+ const connection = db.select().from(gtmConnections).where(eq28(gtmConnections.projectId, projectId)).get();
21648
20661
  if (!connection?.selectedAccountId || !connection.selectedContainerId) {
21649
20662
  throw new Error("GTM needs a selected account and container before it can sync.");
21650
20663
  }
@@ -21679,11 +20692,11 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
21679
20692
  status: RunStatuses.completed,
21680
20693
  finishedAt,
21681
20694
  error: null
21682
- }).where(and23(
21683
- eq29(runs.id, runId),
21684
- eq29(runs.projectId, projectId),
21685
- eq29(runs.kind, RunKinds["gtm-sync"]),
21686
- eq29(runs.status, RunStatuses.running)
20695
+ }).where(and22(
20696
+ eq28(runs.id, runId),
20697
+ eq28(runs.projectId, projectId),
20698
+ eq28(runs.kind, RunKinds["gtm-sync"]),
20699
+ eq28(runs.status, RunStatuses.running)
21687
20700
  )).run();
21688
20701
  if (terminalClaim.changes !== 1) {
21689
20702
  throw new Error(`Run ${runId} was no longer running when GTM evidence was ready.`);
@@ -21697,13 +20710,13 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
21697
20710
  lastSnapshotAt: snapshot.metadata.capturedAt,
21698
20711
  lastSnapshotId: snapshot.metadata.id,
21699
20712
  updatedAt: finishedAt
21700
- }).where(and23(
21701
- eq29(gtmConnections.id, connection.id),
21702
- eq29(gtmConnections.projectId, projectId),
21703
- eq29(gtmConnections.selectedAccountId, connection.selectedAccountId),
21704
- eq29(gtmConnections.selectedContainerId, connection.selectedContainerId),
21705
- connection.selectedWorkspaceId === null ? isNull4(gtmConnections.selectedWorkspaceId) : eq29(gtmConnections.selectedWorkspaceId, connection.selectedWorkspaceId),
21706
- eq29(gtmConnections.selectionGeneration, connection.selectionGeneration)
20713
+ }).where(and22(
20714
+ eq28(gtmConnections.id, connection.id),
20715
+ eq28(gtmConnections.projectId, projectId),
20716
+ eq28(gtmConnections.selectedAccountId, connection.selectedAccountId),
20717
+ eq28(gtmConnections.selectedContainerId, connection.selectedContainerId),
20718
+ connection.selectedWorkspaceId === null ? isNull3(gtmConnections.selectedWorkspaceId) : eq28(gtmConnections.selectedWorkspaceId, connection.selectedWorkspaceId),
20719
+ eq28(gtmConnections.selectionGeneration, connection.selectionGeneration)
21707
20720
  )).run();
21708
20721
  if (connectionClaim.changes !== 1) {
21709
20722
  throw new Error("GTM selection changed while its evidence was being collected.");
@@ -22206,7 +21219,7 @@ async function createServer(opts) {
22206
21219
  async (ctx) => {
22207
21220
  if (!sessionRegistry) return;
22208
21221
  if (!agentProactive) return;
22209
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq30(projects.id, ctx.projectId)).get();
21222
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq29(projects.id, ctx.projectId)).get();
22210
21223
  if (!project) return;
22211
21224
  let content;
22212
21225
  if (ctx.kind === RunKinds["aeo-discover-probe"]) {
@@ -22665,7 +21678,7 @@ async function createServer(opts) {
22665
21678
  void (async () => {
22666
21679
  try {
22667
21680
  const report = await schedulerClient.runDoctor({ project: projectName, checkIds: scheduledHealthCheckIds() });
22668
- const project = opts.db.select().from(projects).where(eq30(projects.name, projectName)).get();
21681
+ const project = opts.db.select().from(projects).where(eq29(projects.name, projectName)).get();
22669
21682
  if (!project) {
22670
21683
  app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
22671
21684
  return;
@@ -22693,9 +21706,9 @@ async function createServer(opts) {
22693
21706
  });
22694
21707
  if (!probed) return;
22695
21708
  const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
22696
- and24(
22697
- eq30(ccReleaseSyncs.release, probed.release),
22698
- eq30(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
21709
+ and23(
21710
+ eq29(ccReleaseSyncs.release, probed.release),
21711
+ eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
22699
21712
  )
22700
21713
  ).limit(1).get();
22701
21714
  if (alreadySynced) {
@@ -22965,7 +21978,7 @@ async function createServer(opts) {
22965
21978
  })();
22966
21979
  if (opts.config.apiKey) {
22967
21980
  const keyHash = hashApiKey2(opts.config.apiKey);
22968
- const existing = opts.db.select().from(apiKeys).where(eq30(apiKeys.keyHash, keyHash)).get();
21981
+ const existing = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, keyHash)).get();
22969
21982
  if (!existing) {
22970
21983
  const prefix = opts.config.apiKey.slice(0, 12);
22971
21984
  opts.db.insert(apiKeys).values({
@@ -23017,7 +22030,7 @@ async function createServer(opts) {
23017
22030
  };
23018
22031
  const getDefaultApiKey = () => {
23019
22032
  if (!opts.config.apiKey) return void 0;
23020
- return opts.db.select().from(apiKeys).where(eq30(apiKeys.keyHash, hashApiKey2(opts.config.apiKey))).get();
22033
+ return opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey2(opts.config.apiKey))).get();
23021
22034
  };
23022
22035
  const createPasswordSession = (reply) => {
23023
22036
  const key = getDefaultApiKey();
@@ -23041,7 +22054,7 @@ async function createServer(opts) {
23041
22054
  if (!header) return false;
23042
22055
  const parts = header.split(" ");
23043
22056
  if (parts.length !== 2 || parts[0] !== "Bearer") return false;
23044
- const key = opts.db.select().from(apiKeys).where(eq30(apiKeys.keyHash, hashApiKey2(parts[1]))).get();
22057
+ const key = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey2(parts[1]))).get();
23045
22058
  return Boolean(key && !key.revokedAt);
23046
22059
  };
23047
22060
  const namedAccountsInUse = () => anyUsersExist(opts.db);
@@ -23136,12 +22149,12 @@ async function createServer(opts) {
23136
22149
  return reply.send({ authenticated: true });
23137
22150
  }
23138
22151
  if (apiKey) {
23139
- const key = opts.db.select().from(apiKeys).where(eq30(apiKeys.keyHash, hashApiKey2(apiKey))).get();
22152
+ const key = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey2(apiKey))).get();
23140
22153
  if (!key || key.revokedAt) {
23141
22154
  const err2 = authInvalid();
23142
22155
  return reply.status(err2.statusCode).send(err2.toJSON());
23143
22156
  }
23144
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq30(apiKeys.id, key.id)).run();
22157
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq29(apiKeys.id, key.id)).run();
23145
22158
  const sessionId = createSession(key.id);
23146
22159
  reply.header(
23147
22160
  "set-cookie",
@@ -23344,8 +22357,8 @@ async function createServer(opts) {
23344
22357
  getAgentPin: () => describeAgentPin(opts.config),
23345
22358
  getGoogleMarketingDoctorInput: (ctx) => {
23346
22359
  if (!ctx.project) return null;
23347
- const googleAds = opts.db.select().from(googleAdsConnections).where(eq30(googleAdsConnections.projectId, ctx.project.id)).get();
23348
- const gtm = opts.db.select().from(gtmConnections).where(eq30(gtmConnections.projectId, ctx.project.id)).get();
22360
+ const googleAds = opts.db.select().from(googleAdsConnections).where(eq29(googleAdsConnections.projectId, ctx.project.id)).get();
22361
+ const gtm = opts.db.select().from(gtmConnections).where(eq29(gtmConnections.projectId, ctx.project.id)).get();
23349
22362
  const googleAdsCredential = getGoogleAdsConnection(opts.config, ctx.project.id);
23350
22363
  const gtmCredential = getGtmConnection(opts.config, ctx.project.id);
23351
22364
  const googleAdsConnected = Boolean(googleAdsCredential?.accessToken);
@@ -23596,7 +22609,7 @@ async function createServer(opts) {
23596
22609
  ...inspectOpts,
23597
22610
  config: opts.config
23598
22611
  }).then(() => {
23599
- const finished = opts.db.select({ status: runs.status }).from(runs).where(eq30(runs.id, runId)).get();
22612
+ const finished = opts.db.select({ status: runs.status }).from(runs).where(eq29(runs.id, runId)).get();
23600
22613
  if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
23601
22614
  return maybeRefreshGscCoverage(
23602
22615
  opts.db,
@@ -23799,6 +22812,22 @@ async function createServer(opts) {
23799
22812
  }
23800
22813
  });
23801
22814
  },
22815
+ onCompetitorAliasesChanged: (projectId, projectName) => {
22816
+ setImmediate(() => {
22817
+ try {
22818
+ const result2 = backfillProjectAnswerMentions(opts.db, projectId, { competitorFieldsOnly: true });
22819
+ app.log.info(
22820
+ { projectId, projectName, ...result2 },
22821
+ "competitor aliases changed: recomputed competitor fields on historical snapshots"
22822
+ );
22823
+ } catch (err) {
22824
+ app.log.error(
22825
+ { err, projectId, projectName },
22826
+ "competitor-alias-triggered backfill failed"
22827
+ );
22828
+ }
22829
+ });
22830
+ },
23802
22831
  operatorApiKeyIds,
23803
22832
  listOperationalLogs: (query) => operationalLogs.list(query),
23804
22833
  getTelemetryStatus,
@@ -24079,7 +23108,7 @@ async function createServer(opts) {
24079
23108
  notify: (projectId, result2) => notifier.onSiteLivenessChecked(projectId, result2)
24080
23109
  });
24081
23110
  stopProviderBatchPoller = startProviderBatchPoller({ db: opts.db, registry, runner: jobRunner });
24082
- for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq30(researchRuns.status, ResearchRunStatuses.queued)).all()) {
23111
+ for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq29(researchRuns.status, ResearchRunStatuses.queued)).all()) {
24083
23112
  dispatchResearchRun(run.id, run.projectId);
24084
23113
  }
24085
23114
  runtimeStartupSettled = true;
@@ -24169,15 +23198,12 @@ export {
24169
23198
  detectAndTrackUpgrade,
24170
23199
  trackEvent,
24171
23200
  setTelemetryPreference,
24172
- backfillAnswerVisibilityCommand,
24173
- backfillNormalizedPaths,
24174
- backfillNormalizedPathsCommand,
24175
- backfillAiReferralPaths,
24176
- backfillAiReferralPathsCommand,
24177
- backfillAnswerMentionsCommand,
24178
- backfillInsightsCommand,
24179
- backfillSnapshotAttributionCommand,
24180
- backfillTrafficClassificationCommand,
23201
+ OPENAI_RETRIEVAL_CONTRACT,
23202
+ reparseStoredResult2 as reparseStoredResult,
23203
+ reparseStoredResult3 as reparseStoredResult2,
23204
+ reparseStoredResult as reparseStoredResult3,
23205
+ reparseStoredResult4,
23206
+ reparseStoredResult5,
24181
23207
  getCloudflareTrafficConnectionBySourceId,
24182
23208
  setGoogleAuthConfig,
24183
23209
  formatAuditFactorScore,