@canonry/canonry 4.158.0 → 4.159.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 (50) hide show
  1. package/README.md +2 -2
  2. package/THIRD_PARTY_NOTICES.md +102 -0
  3. package/assets/THIRD_PARTY_NOTICES.md +102 -0
  4. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +1 -0
  5. package/assets/assets/{AuditHistoryPanel-McQfjoU9.js → AuditHistoryPanel-BiloGcQA.js} +1 -1
  6. package/assets/assets/{BacklinksPage-Da8y1Wv8.js → BacklinksPage-CPM1O2bC.js} +1 -1
  7. package/assets/assets/{HistoryPage-9MJfhRyT.js → HistoryPage-1YOjtdJy.js} +1 -1
  8. package/assets/assets/{MeasurementPropertyPage-BiNAHEJW.js → MeasurementPropertyPage-CVDTY_gC.js} +1 -1
  9. package/assets/assets/ProjectPage-DnGXMx1n.js +12 -0
  10. package/assets/assets/{RunRow-DCG2LBlS.js → RunRow-fIaadH4h.js} +1 -1
  11. package/assets/assets/{RunsPage-ByWA_FSk.js → RunsPage-DfifdM5z.js} +1 -1
  12. package/assets/assets/{SettingsPage-DdSeKAKF.js → SettingsPage-efA2EEmS.js} +1 -1
  13. package/assets/assets/SiteHealthSection-BROtQGfu.js +4 -0
  14. package/assets/assets/{TrafficPage-5u8rPcBt.js → TrafficPage-Wk43Bm_q.js} +1 -1
  15. package/assets/assets/{TrafficSourceDetailPage-Dm-fK87u.js → TrafficSourceDetailPage-BhBvoLWu.js} +1 -1
  16. package/assets/assets/extract-error-message-YGzZFvWs.js +1 -0
  17. package/assets/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  18. package/assets/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  19. package/assets/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  20. package/assets/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  21. package/assets/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  22. package/assets/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  23. package/assets/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  24. package/assets/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  25. package/assets/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  26. package/assets/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  27. package/assets/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  28. package/assets/assets/index-CACRaOmZ.css +1 -0
  29. package/assets/assets/index-DmsIZQAm.js +83 -0
  30. package/assets/assets/{react-sigma_core.esm.min-ComIyDKi.js → react-sigma_core.esm.min-DM9XptbM.js} +2 -2
  31. package/assets/assets/{vendor-lucide-BtYdosYk.js → vendor-lucide-nYKv-BB7.js} +1 -1
  32. package/assets/assets/{vendor-markdown-D274_wsI.js → vendor-markdown-Bn7fRwrx.js} +1 -1
  33. package/assets/assets/{vendor-radix-BxMXSwbC.js → vendor-radix-4UOaVWza.js} +1 -1
  34. package/assets/assets/{vendor-recharts-C4ktw_aE.js → vendor-recharts-D8rt5lKn.js} +1 -1
  35. package/assets/assets/{vendor-tanstack-pFxw0eGP.js → vendor-tanstack-Dock0eH1.js} +1 -1
  36. package/assets/index.html +7 -7
  37. package/dist/{chunk-YP4K2KXH.js → chunk-5AFWHKKI.js} +776 -32
  38. package/dist/{chunk-4VZP64GZ.js → chunk-BZPBVOAM.js} +625 -262
  39. package/dist/{chunk-LK6YFHG4.js → chunk-ZEG43TY2.js} +45 -15
  40. package/dist/{chunk-DEVAEC2S.js → chunk-ZL3VY435.js} +213 -8
  41. package/dist/cli.js +42 -4
  42. package/dist/index.d.ts +9 -0
  43. package/dist/index.js +4 -4
  44. package/dist/{intelligence-service-LQ6ZRWMF.js → intelligence-service-DHY5CYWX.js} +2 -2
  45. package/dist/mcp.js +2 -2
  46. package/package.json +11 -10
  47. package/assets/assets/ProjectPage-mWSMjXrR.js +0 -15
  48. package/assets/assets/extract-error-message-CtxkAd6E.js +0 -1
  49. package/assets/assets/index-DUfg50Pl.js +0 -83
  50. package/assets/assets/index-cXb3mfeQ.css +0 -1
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-LK6YFHG4.js";
13
+ } from "./chunk-ZEG43TY2.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -117,6 +117,7 @@ import {
117
117
  researchRunQueries,
118
118
  researchRuns,
119
119
  resolveDestination,
120
+ resolveMeasurementSitemapTarget,
120
121
  resolveTrustProxy,
121
122
  resolveVercelSyncDeadlineMs,
122
123
  resolveWebhookTarget,
@@ -136,7 +137,7 @@ import {
136
137
  siteCrawlSnapshots,
137
138
  toAlertView,
138
139
  usageCounters
139
- } from "./chunk-YP4K2KXH.js";
140
+ } from "./chunk-5AFWHKKI.js";
140
141
  import {
141
142
  AGENT_MEMORY_VALUE_MAX_BYTES,
142
143
  AGENT_PROVIDER_IDS,
@@ -169,7 +170,9 @@ import {
169
170
  SITE_CRAWL_GRAPH_MAX_NODES,
170
171
  SKILL_MANIFEST_FILENAME,
171
172
  SchedulableRunKinds,
173
+ SiteHealthTemplateDetections,
172
174
  SkillsClients,
175
+ TEMPLATE_LINK_RATIO_THRESHOLD,
173
176
  TrafficEventConfidences,
174
177
  TrafficEventKinds,
175
178
  TrafficEvidenceKinds,
@@ -192,8 +195,10 @@ import {
192
195
  computeBacklinkSummaryMetrics,
193
196
  contentActionLabel,
194
197
  contentBriefDtoSchema,
198
+ createTemplateLinkPairIndex,
195
199
  deriveCitedUrlCandidates,
196
200
  deriveIndexCoverage,
201
+ deriveSiteHealthState,
197
202
  determineAnswerMentioned,
198
203
  dollarsToMicros,
199
204
  effectiveBrandNames,
@@ -211,6 +216,8 @@ import {
211
216
  isBrowserProvider,
212
217
  isGhostTelemetryEvent,
213
218
  isRetryableHttpError,
219
+ isSelfLink,
220
+ isTemplateLinkRatio,
214
221
  isVertexGroundingRedirect,
215
222
  mapWithConcurrency,
216
223
  normalizeIdTokens,
@@ -219,6 +226,7 @@ import {
219
226
  normalizeSiteAuditRunRequest,
220
227
  normalizeUrlPath,
221
228
  notFound,
229
+ observeTemplateLinkEdges,
222
230
  parseMeasurementRunManifestV1,
223
231
  parseOriginList,
224
232
  providerError,
@@ -231,12 +239,14 @@ import {
231
239
  skillsClientSchema,
232
240
  splitList,
233
241
  startOfDayHourInTimeZone,
242
+ templateLinkDetection,
243
+ templateLinkRatio,
234
244
  textContainsBrandAlias,
235
245
  textContainsDomain,
236
246
  validationError,
237
247
  winnabilityClassLabel,
238
248
  withRetry
239
- } from "./chunk-DEVAEC2S.js";
249
+ } from "./chunk-ZL3VY435.js";
240
250
 
241
251
  // src/telemetry.ts
242
252
  import crypto from "crypto";
@@ -571,7 +581,7 @@ import crypto25 from "crypto";
571
581
  import fs8 from "fs";
572
582
  import path9 from "path";
573
583
  import { fileURLToPath as fileURLToPath3 } from "url";
574
- import { and as and17, eq as eq23 } from "drizzle-orm";
584
+ import { and as and18, eq as eq24 } from "drizzle-orm";
575
585
  import Fastify from "fastify";
576
586
  import os5 from "os";
577
587
 
@@ -4169,12 +4179,12 @@ var JobRunner = class {
4169
4179
  error: errorMessage
4170
4180
  }).where(eq2(runs.id, runId)).run();
4171
4181
  this.flushProviderUsage(providerDispatchCounts, providerReservations);
4172
- const abortReason = classifyRunAbortReason(errorMessage);
4182
+ const abortReason2 = classifyRunAbortReason(errorMessage);
4173
4183
  const phases = buildPhases({ startTime, providerCallStart, providerCallEnd });
4174
- if (abortReason) {
4184
+ if (abortReason2) {
4175
4185
  const domainHash = hashDomain(executionContext.canonicalDomain ?? null);
4176
4186
  trackEvent("run.aborted", {
4177
- reason: abortReason,
4187
+ reason: abortReason2,
4178
4188
  providerCount: executionContext.providerCount,
4179
4189
  providers: executionContext.providers,
4180
4190
  queryCount: executionContext.queryCount,
@@ -6995,9 +7005,193 @@ function buildDiscoveryInsightTitle(input) {
6995
7005
 
6996
7006
  // src/execute-site-audit.ts
6997
7007
  import crypto17 from "crypto";
6998
- import { and as and11, eq as eq14, sql as sql6 } from "drizzle-orm";
7008
+ import { and as and12, eq as eq15, sql as sql6 } from "drizzle-orm";
6999
7009
  import { runSiteCrawl } from "@canonry/aeo-audit";
7000
7010
 
7011
+ // src/site-audit-root.ts
7012
+ import http from "http";
7013
+ import https from "https";
7014
+ var DEFAULT_TIMEOUT_MS = 1e4;
7015
+ var MAX_REDIRECTS = 5;
7016
+ var USER_AGENT = "Canonry/1.0 (site-health)";
7017
+ async function resolveSiteAuditRootUrl(rawUrl, options) {
7018
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
7019
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Site audit root timeout must be positive");
7020
+ const requested = parseHttpUrl2(rawUrl);
7021
+ let current = requested;
7022
+ const redirects = [];
7023
+ const deadlineAt = Date.now() + timeoutMs;
7024
+ const transport = options.transport ?? requestPinnedSiteAuditRoot;
7025
+ for (; ; ) {
7026
+ throwIfAborted(options.signal);
7027
+ const checked = await beforeDeadline(
7028
+ () => options.resolveTarget(current.href),
7029
+ deadlineAt,
7030
+ timeoutMs,
7031
+ options.signal
7032
+ );
7033
+ if (!checked.ok) throw new Error(`Site audit root URL rejected: ${checked.message}`);
7034
+ const checkedUrl = parseHttpUrl2(checked.target.url.href);
7035
+ if (checkedUrl.href !== current.href) {
7036
+ throw new Error("Site audit root target resolver returned a different URL than it validated");
7037
+ }
7038
+ const response = await beforeDeadline(
7039
+ () => transport(checked.target, { timeoutMs: remainingTime(deadlineAt, timeoutMs), signal: options.signal }),
7040
+ deadlineAt,
7041
+ timeoutMs,
7042
+ options.signal
7043
+ );
7044
+ if (!isRedirect(response.status)) {
7045
+ return { requestedUrl: requested.href, effectiveUrl: current.href, redirects };
7046
+ }
7047
+ if (redirects.length >= MAX_REDIRECTS) {
7048
+ throw new Error(`Site audit root has more than ${MAX_REDIRECTS} redirects`);
7049
+ }
7050
+ const location = firstHeader(response.headers.location);
7051
+ if (!location) throw new Error("Site audit root redirect response has no Location header");
7052
+ let next;
7053
+ try {
7054
+ next = parseHttpUrl2(new URL(location, current).href);
7055
+ } catch {
7056
+ throw new Error("Site audit root redirect has an invalid Location header");
7057
+ }
7058
+ assertAllowedRedirect(current, next);
7059
+ redirects.push({ status: response.status, from: current.href, to: next.href });
7060
+ current = next;
7061
+ }
7062
+ }
7063
+ function parseHttpUrl2(rawUrl) {
7064
+ let url;
7065
+ try {
7066
+ url = new URL(rawUrl);
7067
+ } catch {
7068
+ throw new Error("Site audit root must be a valid URL");
7069
+ }
7070
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
7071
+ throw new Error("Site audit root must use HTTP or HTTPS");
7072
+ }
7073
+ if (url.username || url.password) throw new Error("Site audit root must not include credentials");
7074
+ url.hash = "";
7075
+ return url;
7076
+ }
7077
+ function canonicalHostAlias(hostname) {
7078
+ return hostname.toLowerCase().replace(/\.$/, "").replace(/^www\./, "");
7079
+ }
7080
+ function assertAllowedRedirect(current, next) {
7081
+ if (current.protocol === "https:" && next.protocol === "http:") {
7082
+ throw new Error("Site audit root redirect would downgrade HTTPS to HTTP");
7083
+ }
7084
+ if (current.port !== next.port) throw new Error("Site audit root redirect changes port");
7085
+ if (canonicalHostAlias(current.hostname) !== canonicalHostAlias(next.hostname)) {
7086
+ throw new Error("Site audit root redirect points outside the approved site");
7087
+ }
7088
+ }
7089
+ function isRedirect(status) {
7090
+ return status >= 300 && status < 400;
7091
+ }
7092
+ function firstHeader(value) {
7093
+ return Array.isArray(value) ? value[0] : value;
7094
+ }
7095
+ function remainingTime(deadlineAt, timeoutMs) {
7096
+ const remaining = deadlineAt - Date.now();
7097
+ if (remaining <= 0) throw new Error(`Site audit root resolution exceeded its ${timeoutMs}ms deadline`);
7098
+ return remaining;
7099
+ }
7100
+ async function beforeDeadline(operation, deadlineAt, timeoutMs, signal) {
7101
+ throwIfAborted(signal);
7102
+ const remaining = remainingTime(deadlineAt, timeoutMs);
7103
+ let timer;
7104
+ let abortListener;
7105
+ try {
7106
+ const value = await Promise.race([
7107
+ operation(),
7108
+ new Promise((_resolve, reject) => {
7109
+ timer = setTimeout(
7110
+ () => reject(new Error(`Site audit root resolution exceeded its ${timeoutMs}ms deadline`)),
7111
+ remaining
7112
+ );
7113
+ }),
7114
+ new Promise((_resolve, reject) => {
7115
+ if (!signal) return;
7116
+ abortListener = () => reject(abortReason(signal));
7117
+ signal.addEventListener("abort", abortListener, { once: true });
7118
+ if (signal.aborted) abortListener();
7119
+ })
7120
+ ]);
7121
+ remainingTime(deadlineAt, timeoutMs);
7122
+ return value;
7123
+ } finally {
7124
+ if (timer !== void 0) clearTimeout(timer);
7125
+ if (abortListener) signal?.removeEventListener("abort", abortListener);
7126
+ }
7127
+ }
7128
+ function abortReason(signal) {
7129
+ return signal.reason instanceof Error ? signal.reason : new DOMException("Operation aborted", "AbortError");
7130
+ }
7131
+ function throwIfAborted(signal) {
7132
+ if (signal?.aborted) throw abortReason(signal);
7133
+ }
7134
+ function stripIpv6Brackets(hostname) {
7135
+ return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
7136
+ }
7137
+ function buildSiteAuditRootRequestOptions(target) {
7138
+ const secure = target.url.protocol === "https:";
7139
+ const requestOptions = {
7140
+ hostname: target.address,
7141
+ family: target.family,
7142
+ port: target.url.port ? Number(target.url.port) : secure ? 443 : 80,
7143
+ method: "GET",
7144
+ path: `${target.url.pathname}${target.url.search}`,
7145
+ headers: {
7146
+ Host: target.url.host,
7147
+ Accept: "text/html,*/*;q=0.1",
7148
+ "User-Agent": USER_AGENT
7149
+ }
7150
+ };
7151
+ if (secure) requestOptions.servername = stripIpv6Brackets(target.url.hostname);
7152
+ return requestOptions;
7153
+ }
7154
+ async function requestPinnedSiteAuditRoot(target, options) {
7155
+ throwIfAborted(options.signal);
7156
+ const secure = target.url.protocol === "https:";
7157
+ const requestOptions = buildSiteAuditRootRequestOptions(target);
7158
+ return await new Promise((resolve, reject) => {
7159
+ let settled = false;
7160
+ let abortListener;
7161
+ const cleanup = () => {
7162
+ clearTimeout(timer);
7163
+ if (abortListener) options.signal?.removeEventListener("abort", abortListener);
7164
+ };
7165
+ const succeed = (response) => {
7166
+ if (settled) return;
7167
+ settled = true;
7168
+ cleanup();
7169
+ resolve(response);
7170
+ };
7171
+ const fail = (error) => {
7172
+ if (settled) return;
7173
+ settled = true;
7174
+ cleanup();
7175
+ reject(error);
7176
+ };
7177
+ const request = (secure ? https.request : http.request)(requestOptions, (response) => {
7178
+ response.destroy();
7179
+ succeed({ status: response.statusCode ?? 0, headers: response.headers });
7180
+ });
7181
+ const timer = setTimeout(
7182
+ () => request.destroy(new Error(`Site audit root request timed out after ${options.timeoutMs}ms`)),
7183
+ options.timeoutMs
7184
+ );
7185
+ if (options.signal) {
7186
+ abortListener = () => request.destroy(abortReason(options.signal));
7187
+ options.signal.addEventListener("abort", abortListener, { once: true });
7188
+ if (options.signal.aborted) abortListener();
7189
+ }
7190
+ request.on("error", fail);
7191
+ request.end();
7192
+ });
7193
+ }
7194
+
7001
7195
  // src/site-crawl-graph-layout.ts
7002
7196
  import crypto16 from "crypto";
7003
7197
  import { createRequire as createRequire3 } from "module";
@@ -7023,6 +7217,22 @@ var FORCE_ATLAS_SETTINGS = Object.freeze({
7023
7217
  var RENDER_MAX_NODE_SIZE = 14;
7024
7218
  var RENDER_HALF_EXTENT_PX = 420;
7025
7219
  var NODE_SIZE_EXTENT_FRACTION = RENDER_MAX_NODE_SIZE / RENDER_HALF_EXTENT_PX;
7220
+ function siteCrawlGraphNodeSize(extent, nodeCount) {
7221
+ if (!Number.isFinite(extent) || extent <= 0 || nodeCount <= 0) return 0;
7222
+ const byExtent = extent * NODE_SIZE_EXTENT_FRACTION;
7223
+ const byArea = Math.sqrt(NOVERLAP_SETTINGS.maxPackingFraction * (2 * extent) ** 2 / nodeCount) / 2;
7224
+ return Math.min(byExtent, byArea);
7225
+ }
7226
+ function siteCrawlGraphExtent(positions) {
7227
+ if (positions.length === 0) return 0;
7228
+ const centerX = positions.reduce((sum, p) => sum + p.x, 0) / positions.length;
7229
+ const centerY = positions.reduce((sum, p) => sum + p.y, 0) / positions.length;
7230
+ let extent = 0;
7231
+ for (const position of positions) {
7232
+ extent = Math.max(extent, Math.abs(position.x - centerX), Math.abs(position.y - centerY));
7233
+ }
7234
+ return extent;
7235
+ }
7026
7236
  var NOVERLAP_SETTINGS = Object.freeze({
7027
7237
  /** Half a node of clear space between two touching nodes. */
7028
7238
  marginFactor: 0.5,
@@ -7058,26 +7268,17 @@ function noverlapMaxIterations(nodeCount) {
7058
7268
  }
7059
7269
  return NOVERLAP_SETTINGS.iterationsFloor;
7060
7270
  }
7061
- var OUTLIER_RING_INNER_FACTOR = 1.25;
7062
- var OUTLIER_RING_OUTER_FACTOR = 1.75;
7063
- var OUTLIER_RING_MAX_RINGS = 4;
7064
- var OUTLIER_RING_TARGET_PER_RING = 16;
7065
- var OUTLIER_RING_MIN_RADIUS = 1;
7271
+ var COMPONENT_PACKING_SPACING_FACTOR = 3;
7066
7272
  function siteCrawlGraphLayoutSettingsFingerprint(settings) {
7067
7273
  return crypto16.createHash("sha256").update(JSON.stringify(settings)).digest("hex").slice(0, 8);
7068
7274
  }
7069
- var SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM = "site-health-fa2-v2";
7275
+ var SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM = "site-health-fa2-v4";
7070
7276
  var SITE_CRAWL_GRAPH_LAYOUT_VERSION = `${SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM}-${siteCrawlGraphLayoutSettingsFingerprint({
7071
7277
  forceAtlas: FORCE_ATLAS_SETTINGS,
7072
7278
  noverlap: NOVERLAP_SETTINGS,
7073
7279
  nodeSizeExtentFraction: NODE_SIZE_EXTENT_FRACTION,
7074
- outlierRing: {
7075
- inner: OUTLIER_RING_INNER_FACTOR,
7076
- outer: OUTLIER_RING_OUTER_FACTOR,
7077
- maxRings: OUTLIER_RING_MAX_RINGS,
7078
- targetPerRing: OUTLIER_RING_TARGET_PER_RING,
7079
- minRadius: OUTLIER_RING_MIN_RADIUS
7080
- }
7280
+ componentPackingSpacingFactor: COMPONENT_PACKING_SPACING_FACTOR,
7281
+ templateLinkRatioThreshold: TEMPLATE_LINK_RATIO_THRESHOLD
7081
7282
  })}`;
7082
7283
  var WORKER_SOURCE = String.raw`
7083
7284
  const {parentPort, workerData} = require('node:worker_threads');
@@ -7103,6 +7304,7 @@ try {
7103
7304
  settings: workerData.settings,
7104
7305
  });
7105
7306
  }
7307
+ let nodeSize = 0;
7106
7308
  // Anti-overlap runs on the spatialization ForceAtlas2 just produced. Node
7107
7309
  // size is uniform and derived from the graph's own extent, so the pass is
7108
7310
  // scale invariant and stays deterministic for a fixed input.
@@ -7122,11 +7324,12 @@ try {
7122
7324
  if (Number.isFinite(extent) && extent > 0) {
7123
7325
  // Whichever bound is tighter: one render-sized node, or the largest disc
7124
7326
  // that still leaves the graph mostly empty space at this node count.
7327
+ // Mirrors siteCrawlGraphNodeSize; a test asserts the two agree.
7125
7328
  const byExtent = extent * workerData.nodeSizeExtentFraction;
7126
7329
  const byArea = Math.sqrt(
7127
7330
  (workerData.noverlap.maxPackingFraction * Math.pow(2 * extent, 2)) / graph.order,
7128
7331
  ) / 2;
7129
- const nodeSize = Math.min(byExtent, byArea);
7332
+ nodeSize = Math.min(byExtent, byArea);
7130
7333
  graph.forEachNode(function (nodeKey) {
7131
7334
  graph.setNodeAttribute(nodeKey, 'size', nodeSize);
7132
7335
  });
@@ -7146,7 +7349,7 @@ try {
7146
7349
  graph.forEachNode((nodeKey, attributes) => {
7147
7350
  positions.push({nodeKey, x: attributes.x, y: attributes.y});
7148
7351
  });
7149
- parentPort.postMessage({ok: true, positions});
7352
+ parentPort.postMessage({ok: true, positions, nodeSize});
7150
7353
  } catch (error) {
7151
7354
  parentPort.postMessage({
7152
7355
  ok: false,
@@ -7204,7 +7407,8 @@ function adaptiveForceAtlasIterations(nodeCount, edgeCount) {
7204
7407
  }
7205
7408
  async function computeForceAtlasPositions(input, options = {}) {
7206
7409
  if (input.nodes.length <= 1 || input.edges.length === 0) {
7207
- return input.nodes.map(({ nodeKey, x, y }) => ({ nodeKey, x, y }));
7410
+ const positions = input.nodes.map(({ nodeKey, x, y }) => ({ nodeKey, x, y }));
7411
+ return { positions, nodeSize: siteCrawlGraphNodeSize(siteCrawlGraphExtent(positions), positions.length) };
7208
7412
  }
7209
7413
  const requireFromPackage = createRequire3(import.meta.url);
7210
7414
  const worker = new Worker(WORKER_SOURCE, {
@@ -7233,7 +7437,7 @@ async function computeForceAtlasPositions(input, options = {}) {
7233
7437
  options.signal?.removeEventListener("abort", onAbort);
7234
7438
  void worker.terminate();
7235
7439
  if (result.error) reject(result.error instanceof Error ? result.error : new Error("Graph layout failed with a non-Error reason", { cause: result.error }));
7236
- else resolve(result.positions ?? []);
7440
+ else resolve({ positions: result.positions ?? [], nodeSize: result.nodeSize ?? 0 });
7237
7441
  };
7238
7442
  const onAbort = () => finish({ error: options.signal?.reason ?? new DOMException("Layout aborted", "AbortError") });
7239
7443
  const timer = setTimeout(() => finish({ error: new GraphLayoutTimeoutError(`Graph layout exceeded ${timeoutMs}ms`) }), timeoutMs);
@@ -7241,7 +7445,7 @@ async function computeForceAtlasPositions(input, options = {}) {
7241
7445
  if (options.signal?.aborted) return onAbort();
7242
7446
  worker.once("message", (message) => {
7243
7447
  if (!message.ok) finish({ error: new Error(message.error ?? "Graph layout worker failed") });
7244
- else finish({ positions: message.positions });
7448
+ else finish({ positions: message.positions, nodeSize: message.nodeSize });
7245
7449
  });
7246
7450
  worker.once("error", (error) => finish({ error }));
7247
7451
  worker.once("exit", (code) => {
@@ -7277,48 +7481,60 @@ function siteCrawlGraphComponents(nodeKeys, edges) {
7277
7481
  if (group) group.push(key);
7278
7482
  else members.set(root, [key]);
7279
7483
  }
7280
- let mainRoot = null;
7281
- for (const [root, group] of members) {
7282
- if (mainRoot === null) {
7283
- mainRoot = root;
7284
- continue;
7285
- }
7286
- const best = members.get(mainRoot);
7287
- if (group.length > best.length || group.length === best.length && root < mainRoot) mainRoot = root;
7288
- }
7289
- const main = new Set(mainRoot === null ? [] : members.get(mainRoot));
7290
- const outliers = [...members.entries()].filter(([root]) => root !== mainRoot).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).flatMap(([, group]) => group);
7291
- return { main, outliers };
7292
- }
7293
- function frameDisconnectedNodes(positions, edges) {
7294
- const { main, outliers } = siteCrawlGraphComponents(positions.map((p) => p.nodeKey), edges);
7295
- if (outliers.length === 0 || main.size === 0) return [...positions];
7296
- const mainPositions = positions.filter((position) => main.has(position.nodeKey));
7297
- const centerX = mainPositions.reduce((sum, p) => sum + p.x, 0) / mainPositions.length;
7298
- const centerY = mainPositions.reduce((sum, p) => sum + p.y, 0) / mainPositions.length;
7299
- if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) return [...positions];
7300
- const mainRadius = Math.max(
7301
- OUTLIER_RING_MIN_RADIUS,
7302
- ...mainPositions.map((p) => Math.hypot(p.x - centerX, p.y - centerY)).filter(Number.isFinite)
7303
- );
7304
- const ringCount = Math.max(1, Math.min(
7305
- OUTLIER_RING_MAX_RINGS,
7306
- Math.ceil(outliers.length / OUTLIER_RING_TARGET_PER_RING)
7307
- ));
7308
- const placed = /* @__PURE__ */ new Map();
7309
- for (const [index, nodeKey] of outliers.entries()) {
7310
- const ring = index % ringCount;
7311
- const ringRadius = mainRadius * (ringCount === 1 ? OUTLIER_RING_INNER_FACTOR : OUTLIER_RING_INNER_FACTOR + (OUTLIER_RING_OUTER_FACTOR - OUTLIER_RING_INNER_FACTOR) * (ring / (ringCount - 1)));
7312
- const slot = Math.floor(index / ringCount);
7313
- const slotsOnRing = Math.ceil((outliers.length - ring) / ringCount);
7314
- const angle = (slot + ring % 2 * 0.5) / Math.max(1, slotsOnRing) * Math.PI * 2;
7315
- placed.set(nodeKey, {
7316
- nodeKey,
7317
- x: centerX + Math.cos(angle) * ringRadius,
7318
- y: centerY + Math.sin(angle) * ringRadius
7319
- });
7320
- }
7321
- return positions.map((position) => placed.get(position.nodeKey) ?? position);
7484
+ return [...members.values()].sort((left, right) => right.length - left.length || (left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0));
7485
+ }
7486
+ function describeComponent(nodeKeys, byKey) {
7487
+ let sumX = 0;
7488
+ let sumY = 0;
7489
+ for (const nodeKey of nodeKeys) {
7490
+ const position = byKey.get(nodeKey);
7491
+ sumX += position.x;
7492
+ sumY += position.y;
7493
+ }
7494
+ const centerX = sumX / nodeKeys.length;
7495
+ const centerY = sumY / nodeKeys.length;
7496
+ let radius = 0;
7497
+ for (const nodeKey of nodeKeys) {
7498
+ const position = byKey.get(nodeKey);
7499
+ radius = Math.max(radius, Math.hypot(position.x - centerX, position.y - centerY));
7500
+ }
7501
+ return { nodeKeys, centerX, centerY, radius };
7502
+ }
7503
+ function packSiteCrawlGraphComponents(positions, edges, nodeSpacing) {
7504
+ const components = siteCrawlGraphComponents(positions.map((position) => position.nodeKey), edges);
7505
+ if (components.length <= 1) return [...positions];
7506
+ const byKey = new Map(positions.map((position) => [position.nodeKey, position]));
7507
+ if (positions.some((position) => !Number.isFinite(position.x) || !Number.isFinite(position.y))) {
7508
+ return [...positions];
7509
+ }
7510
+ const described = components.map((nodeKeys) => describeComponent(nodeKeys, byKey));
7511
+ const [anchor, ...rest] = described;
7512
+ const spacing = Math.max(nodeSpacing, Number.EPSILON) * COMPONENT_PACKING_SPACING_FACTOR;
7513
+ const ordered = [...rest].sort((left, right) => right.radius - left.radius || (left.nodeKeys[0] < right.nodeKeys[0] ? -1 : left.nodeKeys[0] > right.nodeKeys[0] ? 1 : 0));
7514
+ const moved = /* @__PURE__ */ new Map();
7515
+ let ringRadius = anchor.radius + spacing;
7516
+ let ringPitch = 0;
7517
+ let placedOnRing = 0;
7518
+ let ringCapacity = 0;
7519
+ for (const component of ordered) {
7520
+ if (placedOnRing >= ringCapacity) {
7521
+ ringPitch = 2 * component.radius + spacing;
7522
+ ringRadius += ringPitch / 2 + (placedOnRing === 0 ? 0 : ringPitch / 2);
7523
+ ringCapacity = Math.max(1, Math.floor(2 * Math.PI * ringRadius / ringPitch));
7524
+ placedOnRing = 0;
7525
+ }
7526
+ const angle = placedOnRing / ringCapacity * Math.PI * 2;
7527
+ placedOnRing += 1;
7528
+ const targetX = anchor.centerX + Math.cos(angle) * ringRadius;
7529
+ const targetY = anchor.centerY + Math.sin(angle) * ringRadius;
7530
+ const shiftX = targetX - component.centerX;
7531
+ const shiftY = targetY - component.centerY;
7532
+ for (const nodeKey of component.nodeKeys) {
7533
+ const position = byKey.get(nodeKey);
7534
+ moved.set(nodeKey, { nodeKey, x: position.x + shiftX, y: position.y + shiftY });
7535
+ }
7536
+ }
7537
+ return positions.map((position) => moved.get(position.nodeKey) ?? position);
7322
7538
  }
7323
7539
  function normalizePositions(seededNodes, computed, rootNodeKey) {
7324
7540
  const byKey = new Map(computed.map((position) => [position.nodeKey, position]));
@@ -7352,29 +7568,40 @@ async function layoutSiteCrawlGraphInput(input, options = {}) {
7352
7568
  failureCode: "empty-crawl",
7353
7569
  totalNodes: input.totalNodes,
7354
7570
  totalEdges: input.totalEdges,
7571
+ totalTemplateEdges: input.totalTemplateEdges,
7355
7572
  nodeCount: 0,
7356
7573
  edgeCount: 0,
7357
7574
  nodes: [],
7358
7575
  edges: []
7359
7576
  };
7360
7577
  }
7578
+ const layoutEdges = input.edges.filter((edge) => !edge.isTemplate);
7361
7579
  const seeded = seedSiteCrawlGraphNodes(input.nodes, input.rootNodeKey, input.priorPositions);
7580
+ const timeoutMs = options.timeoutMs ?? SITE_CRAWL_GRAPH_LAYOUT_TIMEOUT_MS;
7581
+ const deadline = Date.now() + timeoutMs;
7582
+ const assertWithinBudget = () => {
7583
+ options.signal?.throwIfAborted();
7584
+ if (Date.now() > deadline) throw new GraphLayoutTimeoutError(`Graph layout exceeded ${timeoutMs}ms`);
7585
+ };
7362
7586
  try {
7363
7587
  const compute = options.computePositions ?? computeForceAtlasPositions;
7364
- const positions = await compute({
7588
+ const { positions, nodeSize } = await compute({
7365
7589
  nodes: seeded,
7366
- edges: input.edges,
7367
- iterations: adaptiveForceAtlasIterations(input.nodes.length, input.edges.length)
7368
- }, { signal: options.signal, timeoutMs: options.timeoutMs });
7369
- options.signal?.throwIfAborted();
7370
- const framed = frameDisconnectedNodes(positions, input.edges);
7590
+ edges: layoutEdges,
7591
+ iterations: adaptiveForceAtlasIterations(input.nodes.length, layoutEdges.length)
7592
+ }, { signal: options.signal, timeoutMs: Math.max(1, deadline - Date.now()) });
7593
+ assertWithinBudget();
7594
+ const framed = packSiteCrawlGraphComponents(positions, layoutEdges, nodeSize);
7595
+ assertWithinBudget();
7371
7596
  return {
7372
7597
  state: "ready",
7373
7598
  layoutVersion: SITE_CRAWL_GRAPH_LAYOUT_VERSION,
7374
7599
  totalNodes: input.totalNodes,
7375
7600
  totalEdges: input.totalEdges,
7601
+ totalTemplateEdges: input.totalTemplateEdges,
7376
7602
  nodeCount: input.nodes.length,
7377
7603
  edgeCount: input.edges.length,
7604
+ templateLinksExcluded: true,
7378
7605
  nodes: normalizePositions(seeded, framed, input.rootNodeKey),
7379
7606
  edges: input.edges.map((edge, sampleRank) => ({ ...edge, sampleRank }))
7380
7607
  };
@@ -7386,6 +7613,7 @@ async function layoutSiteCrawlGraphInput(input, options = {}) {
7386
7613
  failureCode: error instanceof GraphLayoutTimeoutError ? "layout-timeout" : "layout-error",
7387
7614
  totalNodes: input.totalNodes,
7388
7615
  totalEdges: input.totalEdges,
7616
+ totalTemplateEdges: input.totalTemplateEdges,
7389
7617
  nodeCount: 0,
7390
7618
  edgeCount: 0,
7391
7619
  nodes: [],
@@ -7422,6 +7650,7 @@ function loadPriorSiteCrawlGraphPositions(db, scope) {
7422
7650
  async function prepareSiteCrawlGraphLayout(db, scope, options = {}) {
7423
7651
  let totalNodes = 0;
7424
7652
  let totalEdges = 0;
7653
+ let totalTemplateEdges = 0;
7425
7654
  try {
7426
7655
  const assertNotAborted = () => options.signal?.throwIfAborted();
7427
7656
  assertNotAborted();
@@ -7439,7 +7668,7 @@ async function prepareSiteCrawlGraphLayout(db, scope, options = {}) {
7439
7668
  ];
7440
7669
  totalNodes = db.select({ value: count() }).from(siteCrawlPages).where(and10(...pageScope)).get()?.value ?? 0;
7441
7670
  assertNotAborted();
7442
- totalEdges = db.select({ value: count() }).from(siteCrawlEdges).innerJoin(graphSourcePage, and10(
7671
+ const countGraphCompatibleEdges = (...extra) => db.select({ value: count() }).from(siteCrawlEdges).innerJoin(graphSourcePage, and10(
7443
7672
  eq13(graphSourcePage.projectId, scope.projectId),
7444
7673
  eq13(graphSourcePage.runId, scope.runId),
7445
7674
  eq13(graphSourcePage.attemptId, scope.attemptId),
@@ -7449,7 +7678,10 @@ async function prepareSiteCrawlGraphLayout(db, scope, options = {}) {
7449
7678
  eq13(graphTargetPage.runId, scope.runId),
7450
7679
  eq13(graphTargetPage.attemptId, scope.attemptId),
7451
7680
  eq13(graphTargetPage.nodeKey, siteCrawlEdges.targetNodeKey)
7452
- )).where(and10(...edgeScope)).get()?.value ?? 0;
7681
+ )).where(and10(...edgeScope, ...extra)).get()?.value ?? 0;
7682
+ totalEdges = countGraphCompatibleEdges();
7683
+ assertNotAborted();
7684
+ totalTemplateEdges = countGraphCompatibleEdges(eq13(siteCrawlEdges.isTemplate, true));
7453
7685
  assertNotAborted();
7454
7686
  const graphSeedColumns = {
7455
7687
  nodeKey: siteCrawlPages.nodeKey,
@@ -7488,7 +7720,8 @@ async function prepareSiteCrawlGraphLayout(db, scope, options = {}) {
7488
7720
  edge.source_node_key AS sourceNodeKey,
7489
7721
  edge.target_node_key AS targetNodeKey,
7490
7722
  edge.followable AS followable,
7491
- edge.occurrences AS occurrences
7723
+ edge.occurrences AS occurrences,
7724
+ edge.is_template AS isTemplate
7492
7725
  FROM site_crawl_edges AS edge
7493
7726
  INNER JOIN selected AS source ON source.node_key = edge.source_node_key
7494
7727
  INNER JOIN selected AS target ON target.node_key = edge.target_node_key
@@ -7504,8 +7737,9 @@ async function prepareSiteCrawlGraphLayout(db, scope, options = {}) {
7504
7737
  return await layoutSiteCrawlGraphInput({
7505
7738
  totalNodes,
7506
7739
  totalEdges,
7740
+ totalTemplateEdges,
7507
7741
  nodes,
7508
- edges: edgeRows,
7742
+ edges: edgeRows.map((row) => ({ ...row, isTemplate: Boolean(row.isTemplate) })),
7509
7743
  rootNodeKey,
7510
7744
  priorPositions
7511
7745
  }, options);
@@ -7517,6 +7751,7 @@ async function prepareSiteCrawlGraphLayout(db, scope, options = {}) {
7517
7751
  failureCode: "layout-error",
7518
7752
  totalNodes,
7519
7753
  totalEdges,
7754
+ totalTemplateEdges,
7520
7755
  nodeCount: 0,
7521
7756
  edgeCount: 0,
7522
7757
  nodes: [],
@@ -7541,8 +7776,10 @@ function persistSiteCrawlGraphLayout(db, scope, layout, now = (/* @__PURE__ */ n
7541
7776
  failureCode: layout.state === "unavailable" ? layout.failureCode : null,
7542
7777
  totalNodes: layout.totalNodes,
7543
7778
  totalEdges: layout.totalEdges,
7779
+ totalTemplateEdges: layout.totalTemplateEdges,
7544
7780
  nodeCount: layout.nodeCount,
7545
7781
  edgeCount: layout.edgeCount,
7782
+ templateLinksExcluded: layout.state === "ready",
7546
7783
  createdAt: now,
7547
7784
  updatedAt: now
7548
7785
  }).run();
@@ -7573,6 +7810,7 @@ function persistSiteCrawlGraphLayout(db, scope, layout, now = (/* @__PURE__ */ n
7573
7810
  targetNodeKey: edge.targetNodeKey,
7574
7811
  followable: edge.followable,
7575
7812
  occurrences: edge.occurrences,
7813
+ isTemplate: edge.isTemplate,
7576
7814
  createdAt: now
7577
7815
  }))).run();
7578
7816
  }
@@ -7586,6 +7824,70 @@ function deleteSiteCrawlGraphLayout(db, scope) {
7586
7824
  )).run();
7587
7825
  }
7588
7826
 
7827
+ // src/site-crawl-template-links.ts
7828
+ import { and as and11, asc as asc2, eq as eq14, gt, inArray as inArray4 } from "drizzle-orm";
7829
+ var TEMPLATE_LINK_READ_BATCH = 2e3;
7830
+ var TEMPLATE_LINK_WRITE_BATCH = 500;
7831
+ function* streamAttemptEdges(db, scope) {
7832
+ let after = "";
7833
+ for (; ; ) {
7834
+ const rows = db.select({
7835
+ edgeKey: siteCrawlEdges.edgeKey,
7836
+ sourceNodeKey: siteCrawlEdges.sourceNodeKey,
7837
+ targetNodeKey: siteCrawlEdges.targetNodeKey,
7838
+ anchors: siteCrawlEdges.anchors
7839
+ }).from(siteCrawlEdges).where(and11(
7840
+ eq14(siteCrawlEdges.projectId, scope.projectId),
7841
+ eq14(siteCrawlEdges.runId, scope.runId),
7842
+ eq14(siteCrawlEdges.attemptId, scope.attemptId),
7843
+ gt(siteCrawlEdges.edgeKey, after)
7844
+ )).orderBy(asc2(siteCrawlEdges.edgeKey)).limit(TEMPLATE_LINK_READ_BATCH).all();
7845
+ if (rows.length === 0) return;
7846
+ yield rows;
7847
+ after = rows[rows.length - 1].edgeKey;
7848
+ if (rows.length < TEMPLATE_LINK_READ_BATCH) return;
7849
+ }
7850
+ }
7851
+ function* chunked(values, size = TEMPLATE_LINK_WRITE_BATCH) {
7852
+ for (let offset = 0; offset < values.length; offset += size) {
7853
+ yield values.slice(offset, offset + size);
7854
+ }
7855
+ }
7856
+ function classifySiteCrawlTemplateLinks(db, scope, pagesFetched) {
7857
+ const scopeFilter = and11(
7858
+ eq14(siteCrawlEdges.projectId, scope.projectId),
7859
+ eq14(siteCrawlEdges.runId, scope.runId),
7860
+ eq14(siteCrawlEdges.attemptId, scope.attemptId)
7861
+ );
7862
+ const detection = templateLinkDetection(pagesFetched);
7863
+ db.update(siteCrawlEdges).set({ isTemplate: false, templateRatio: null }).where(scopeFilter).run();
7864
+ if (detection !== SiteHealthTemplateDetections.applied) {
7865
+ return { detection, templateEdgeCount: 0 };
7866
+ }
7867
+ const index = createTemplateLinkPairIndex();
7868
+ for (const batch of streamAttemptEdges(db, scope)) observeTemplateLinkEdges(index, batch);
7869
+ const edgeKeysByRatio = /* @__PURE__ */ new Map();
7870
+ let templateEdgeCount = 0;
7871
+ for (const batch of streamAttemptEdges(db, scope)) {
7872
+ for (const edge of batch) {
7873
+ const ratio = templateLinkRatio(index, pagesFetched, edge);
7874
+ if (ratio == null) continue;
7875
+ const group = edgeKeysByRatio.get(ratio);
7876
+ if (group) group.push(edge.edgeKey);
7877
+ else edgeKeysByRatio.set(ratio, [edge.edgeKey]);
7878
+ if (isTemplateLinkRatio(ratio)) templateEdgeCount += 1;
7879
+ }
7880
+ }
7881
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7882
+ for (const [ratio, edgeKeys] of edgeKeysByRatio) {
7883
+ const isTemplate = isTemplateLinkRatio(ratio);
7884
+ for (const chunk of chunked(edgeKeys)) {
7885
+ db.update(siteCrawlEdges).set({ isTemplate, templateRatio: ratio, updatedAt: now }).where(and11(scopeFilter, inArray4(siteCrawlEdges.edgeKey, chunk))).run();
7886
+ }
7887
+ }
7888
+ return { detection, templateEdgeCount };
7889
+ }
7890
+
7589
7891
  // src/execute-site-audit.ts
7590
7892
  var log12 = createLogger("SiteAudit");
7591
7893
  var SiteAuditCancelledError = class extends Error {
@@ -7733,9 +8035,9 @@ function deadLinkCheckedCount(edges, pages) {
7733
8035
  }
7734
8036
  async function executeSiteAudit(db, runId, projectId, opts = {}) {
7735
8037
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
7736
- const claim = db.update(runs).set({ status: "running", startedAt }).where(and11(eq14(runs.id, runId), eq14(runs.projectId, projectId), eq14(runs.status, "queued"))).run();
8038
+ const claim = db.update(runs).set({ status: "running", startedAt }).where(and12(eq15(runs.id, runId), eq15(runs.projectId, projectId), eq15(runs.status, "queued"))).run();
7737
8039
  if (claim.changes === 0) return;
7738
- const attemptNumber = (db.select({ value: sql6`coalesce(max(${siteCrawlAttempts.attemptNumber}), 0)` }).from(siteCrawlAttempts).where(eq14(siteCrawlAttempts.runId, runId)).get()?.value ?? 0) + 1;
8040
+ const attemptNumber = (db.select({ value: sql6`coalesce(max(${siteCrawlAttempts.attemptNumber}), 0)` }).from(siteCrawlAttempts).where(eq15(siteCrawlAttempts.runId, runId)).get()?.value ?? 0) + 1;
7739
8041
  const attemptId = crypto17.randomUUID();
7740
8042
  db.insert(siteCrawlAttempts).values({
7741
8043
  id: attemptId,
@@ -7770,28 +8072,49 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
7770
8072
  const nodeKey = nodeKeyByUrl.get(url);
7771
8073
  if (!nodeKey) continue;
7772
8074
  const scope = [
7773
- eq14(siteCrawlEdges.projectId, projectId),
7774
- eq14(siteCrawlEdges.runId, runId),
7775
- eq14(siteCrawlEdges.attemptId, attemptId)
8075
+ eq15(siteCrawlEdges.projectId, projectId),
8076
+ eq15(siteCrawlEdges.runId, runId),
8077
+ eq15(siteCrawlEdges.attemptId, attemptId)
7776
8078
  ];
7777
- tx.update(siteCrawlEdges).set({ sourceNodeKey: nodeKey, updatedAt: now }).where(and11(
8079
+ tx.update(siteCrawlEdges).set({ sourceNodeKey: nodeKey, updatedAt: now }).where(and12(
7778
8080
  ...scope,
7779
- eq14(siteCrawlEdges.sourceUrl, url)
8081
+ eq15(siteCrawlEdges.sourceUrl, url)
7780
8082
  )).run();
7781
- tx.update(siteCrawlEdges).set({ targetNodeKey: nodeKey, updatedAt: now }).where(and11(
8083
+ tx.update(siteCrawlEdges).set({ targetNodeKey: nodeKey, updatedAt: now }).where(and12(
7782
8084
  ...scope,
7783
- eq14(siteCrawlEdges.targetUrl, url)
7784
- )).run();
7785
- tx.update(siteCrawlPages).set({ canonicalNodeKey: nodeKey, updatedAt: now }).where(and11(
7786
- eq14(siteCrawlPages.projectId, projectId),
7787
- eq14(siteCrawlPages.runId, runId),
7788
- eq14(siteCrawlPages.attemptId, attemptId),
7789
- eq14(siteCrawlPages.canonicalUrl, url)
8085
+ eq15(siteCrawlEdges.targetUrl, url)
7790
8086
  )).run();
8087
+ const pageScope = [
8088
+ eq15(siteCrawlPages.projectId, projectId),
8089
+ eq15(siteCrawlPages.runId, runId),
8090
+ eq15(siteCrawlPages.attemptId, attemptId)
8091
+ ];
8092
+ const canonicalTargets = tx.select({
8093
+ id: siteCrawlPages.id,
8094
+ nodeKey: siteCrawlPages.nodeKey,
8095
+ fetchState: siteCrawlPages.fetchState,
8096
+ indexabilityState: siteCrawlPages.indexabilityState,
8097
+ indexabilityReasons: siteCrawlPages.indexabilityReasons
8098
+ }).from(siteCrawlPages).where(and12(...pageScope, eq15(siteCrawlPages.canonicalUrl, url))).all();
8099
+ for (const target of canonicalTargets) {
8100
+ tx.update(siteCrawlPages).set({
8101
+ canonicalNodeKey: nodeKey,
8102
+ healthState: deriveSiteHealthState({ ...target, canonicalNodeKey: nodeKey }),
8103
+ updatedAt: now
8104
+ }).where(eq15(siteCrawlPages.id, target.id)).run();
8105
+ }
7791
8106
  }
7792
8107
  };
7793
8108
  const persistPage = (tx, page, now) => {
7794
8109
  const audit = page.audit;
8110
+ const canonicalNodeKey = page.canonicalUrl ? nodeKeyByUrl.get(page.canonicalUrl) ?? null : null;
8111
+ const healthState = deriveSiteHealthState({
8112
+ fetchState: page.state,
8113
+ indexabilityState: page.indexability.state,
8114
+ indexabilityReasons: page.indexability.reasons,
8115
+ canonicalNodeKey,
8116
+ nodeKey: page.key
8117
+ });
7795
8118
  tx.insert(siteCrawlPages).values({
7796
8119
  id: crypto17.randomUUID(),
7797
8120
  projectId,
@@ -7816,9 +8139,10 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
7816
8139
  redirectChain: page.redirectChain.map((hop) => hop.to),
7817
8140
  directives: { metaRobots: page.metaRobots, xRobots: page.xRobots },
7818
8141
  canonicalUrl: page.canonicalUrl,
7819
- canonicalNodeKey: page.canonicalUrl ? nodeKeyByUrl.get(page.canonicalUrl) ?? null : null,
8142
+ canonicalNodeKey,
7820
8143
  indexabilityState: page.indexability.state,
7821
8144
  indexabilityReasons: page.indexability.reasons,
8145
+ healthState,
7822
8146
  auditState: pageAuditState(page),
7823
8147
  auditScore: audit?.overallScore ?? null,
7824
8148
  auditFields: audit ? crawlAuditFields(audit) : {},
@@ -7849,9 +8173,10 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
7849
8173
  redirectChain: page.redirectChain.map((hop) => hop.to),
7850
8174
  directives: { metaRobots: page.metaRobots, xRobots: page.xRobots },
7851
8175
  canonicalUrl: page.canonicalUrl,
7852
- canonicalNodeKey: page.canonicalUrl ? nodeKeyByUrl.get(page.canonicalUrl) ?? null : null,
8176
+ canonicalNodeKey,
7853
8177
  indexabilityState: page.indexability.state,
7854
8178
  indexabilityReasons: page.indexability.reasons,
8179
+ healthState,
7855
8180
  auditState: pageAuditState(page),
7856
8181
  auditScore: audit?.overallScore ?? null,
7857
8182
  auditFields: audit ? crawlAuditFields(audit) : {},
@@ -7874,6 +8199,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
7874
8199
  bindReferences(tx, changedUrls, now);
7875
8200
  };
7876
8201
  const persistEdge = (tx, edge, now) => {
8202
+ if (isSelfLink(edge.from, edge.to)) return;
7877
8203
  observedEdges.set(edge.key, edge);
7878
8204
  tx.insert(siteCrawlEdges).values({
7879
8205
  id: crypto17.randomUUID(),
@@ -7922,20 +8248,20 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
7922
8248
  linkScoreRaw: metrics.linkScoreRaw,
7923
8249
  linkScoreNormalized: metrics.linkScore,
7924
8250
  updatedAt: now
7925
- }).where(and11(
7926
- eq14(siteCrawlPages.projectId, projectId),
7927
- eq14(siteCrawlPages.runId, runId),
7928
- eq14(siteCrawlPages.attemptId, attemptId),
7929
- eq14(siteCrawlPages.nodeKey, key)
8251
+ }).where(and12(
8252
+ eq15(siteCrawlPages.projectId, projectId),
8253
+ eq15(siteCrawlPages.runId, runId),
8254
+ eq15(siteCrawlPages.attemptId, attemptId),
8255
+ eq15(siteCrawlPages.nodeKey, key)
7930
8256
  )).run();
7931
8257
  };
7932
8258
  const persistEvent = async (event) => {
7933
8259
  const now = (/* @__PURE__ */ new Date()).toISOString();
7934
8260
  db.transaction((tx) => {
7935
- const receipt = tx.select({ checksum: siteCrawlEventReceipts.checksum }).from(siteCrawlEventReceipts).where(and11(
7936
- eq14(siteCrawlEventReceipts.attemptId, attemptId),
7937
- eq14(siteCrawlEventReceipts.sequence, event.sequence),
7938
- eq14(siteCrawlEventReceipts.batchId, event.batchId)
8261
+ const receipt = tx.select({ checksum: siteCrawlEventReceipts.checksum }).from(siteCrawlEventReceipts).where(and12(
8262
+ eq15(siteCrawlEventReceipts.attemptId, attemptId),
8263
+ eq15(siteCrawlEventReceipts.sequence, event.sequence),
8264
+ eq15(siteCrawlEventReceipts.batchId, event.batchId)
7939
8265
  )).get();
7940
8266
  if (receipt) {
7941
8267
  if (receipt.checksum !== event.checksum) {
@@ -7968,26 +8294,39 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
7968
8294
  pagesErrored: event.type === "summary" ? observedErrorCount(observedPages.values()) : sql6`${siteCrawlAttempts.pagesErrored}`,
7969
8295
  edgesDiscovered: progress.edgesObserved,
7970
8296
  updatedAt: now
7971
- }).where(eq14(siteCrawlAttempts.id, attemptId)).run();
8297
+ }).where(eq15(siteCrawlAttempts.id, attemptId)).run();
7972
8298
  } else {
7973
8299
  tx.update(siteCrawlAttempts).set({
7974
8300
  lastEventSequence: event.sequence,
7975
8301
  lastEventChecksum: event.checksum,
7976
8302
  updatedAt: now
7977
- }).where(eq14(siteCrawlAttempts.id, attemptId)).run();
8303
+ }).where(eq15(siteCrawlAttempts.id, attemptId)).run();
7978
8304
  }
7979
8305
  });
7980
8306
  };
7981
8307
  try {
7982
- const project = db.select().from(projects).where(eq14(projects.id, projectId)).get();
8308
+ const project = db.select().from(projects).where(eq15(projects.id, projectId)).get();
7983
8309
  if (!project) throw new Error(`Project not found: ${projectId}`);
7984
8310
  const homepageUrl = toHomepageUrl(project.canonicalDomain);
7985
8311
  const maxPages = clampSiteAuditLimit(opts.maxPages ?? opts.limit);
7986
8312
  const maxEdges = clampSiteAuditEdgeLimit(opts.maxEdges);
7987
- await assertSiteAuditUrlAllowed(homepageUrl, "canonicalDomain");
8313
+ const resolvedRoot = await resolveSiteAuditRootUrl(homepageUrl, {
8314
+ resolveTarget: resolveMeasurementSitemapTarget,
8315
+ signal: opts.signal
8316
+ });
8317
+ const crawlRootUrl = resolvedRoot.effectiveUrl;
7988
8318
  if (opts.sitemapUrl) await assertSiteAuditUrlAllowed(opts.sitemapUrl, "sitemapUrl");
7989
- log12.info("start", { runId, projectId, homepageUrl, maxPages, maxEdges, maxDepth: opts.maxDepth ?? null });
7990
- const report = await runSiteCrawl(homepageUrl, {
8319
+ log12.info("start", {
8320
+ runId,
8321
+ projectId,
8322
+ homepageUrl,
8323
+ crawlRootUrl,
8324
+ redirects: resolvedRoot.redirects.length,
8325
+ maxPages,
8326
+ maxEdges,
8327
+ maxDepth: opts.maxDepth ?? null
8328
+ });
8329
+ const report = await runSiteCrawl(crawlRootUrl, {
7991
8330
  mode: "summary",
7992
8331
  sitemapUrl: opts.sitemapUrl,
7993
8332
  maxPages,
@@ -8002,6 +8341,17 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8002
8341
  if (crawlSummary.complete && !hasAuditedPages) {
8003
8342
  throw emptyCompleteCrawlError(crawlSummary.rootUrl, crawlSummary.finalRootUrl, crawlSummary.pagesObserved);
8004
8343
  }
8344
+ const templateLinks = classifySiteCrawlTemplateLinks(
8345
+ db,
8346
+ { projectId, runId, attemptId },
8347
+ crawlSummary.pagesFetched
8348
+ );
8349
+ log12.info("template-links", {
8350
+ runId,
8351
+ projectId,
8352
+ detection: templateLinks.detection,
8353
+ templateEdges: templateLinks.templateEdgeCount
8354
+ });
8005
8355
  const graphLayout = await prepareSiteCrawlGraphLayout(db, {
8006
8356
  projectId,
8007
8357
  runId,
@@ -8029,6 +8379,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8029
8379
  failureCode: "layout-error",
8030
8380
  totalNodes: graphLayout.totalNodes,
8031
8381
  totalEdges: graphLayout.totalEdges,
8382
+ totalTemplateEdges: graphLayout.totalTemplateEdges,
8032
8383
  nodeCount: 0,
8033
8384
  edgeCount: 0,
8034
8385
  nodes: [],
@@ -8043,14 +8394,14 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8043
8394
  }
8044
8395
  }
8045
8396
  const published = db.transaction((tx) => {
8046
- const claim2 = tx.update(runs).set({ status: terminalStatus, finishedAt }).where(and11(eq14(runs.id, runId), eq14(runs.status, "running"))).run();
8397
+ const claim2 = tx.update(runs).set({ status: terminalStatus, finishedAt }).where(and12(eq15(runs.id, runId), eq15(runs.status, "running"))).run();
8047
8398
  if (claim2.changes === 0) return false;
8048
8399
  if (hasAuditedPages) {
8049
8400
  tx.insert(siteAuditSnapshots).values({
8050
8401
  id: crypto17.randomUUID(),
8051
8402
  projectId,
8052
8403
  runId,
8053
- sitemapUrl: opts.sitemapUrl ?? `${homepageUrl}/sitemap.xml`,
8404
+ sitemapUrl: opts.sitemapUrl ?? new URL("/sitemap.xml", crawlRootUrl).href,
8054
8405
  auditedAt: crawlSummary.completedAt,
8055
8406
  aggregateScore: crawlSummary.auditRollup.aggregateScore ?? 0,
8056
8407
  pagesDiscovered: crawlSummary.pagesDiscovered,
@@ -8082,7 +8433,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8082
8433
  projectId,
8083
8434
  runId,
8084
8435
  attemptId,
8085
- requestedRootUrl: crawlSummary.rootUrl,
8436
+ requestedRootUrl: resolvedRoot.requestedUrl,
8086
8437
  rootUrl: crawlSummary.finalRootUrl ?? crawlSummary.rootUrl,
8087
8438
  crawlSchemaVersion: crawlSummary.crawlSchemaVersion,
8088
8439
  engineVersion: crawlSummary.engineVersion,
@@ -8113,6 +8464,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8113
8464
  deadLinkState: report.deadLinks.state,
8114
8465
  deadLinksChecked,
8115
8466
  deadLinksFound,
8467
+ templateDetection: templateLinks.detection,
8116
8468
  createdAt: finishedAt,
8117
8469
  updatedAt: finishedAt
8118
8470
  }).run();
@@ -8145,7 +8497,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8145
8497
  edgesDiscovered: crawlSummary.edgesObserved,
8146
8498
  finishedAt,
8147
8499
  updatedAt: finishedAt
8148
- }).where(eq14(siteCrawlAttempts.id, attemptId)).run();
8500
+ }).where(eq15(siteCrawlAttempts.id, attemptId)).run();
8149
8501
  return true;
8150
8502
  });
8151
8503
  if (!published) {
@@ -8158,7 +8510,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8158
8510
  error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
8159
8511
  });
8160
8512
  }
8161
- const current = db.select({ status: runs.status }).from(runs).where(eq14(runs.id, runId)).get();
8513
+ const current = db.select({ status: runs.status }).from(runs).where(eq15(runs.id, runId)).get();
8162
8514
  if (current?.status === "cancelled") throw new SiteAuditCancelledError(`Site audit cancelled before publication: ${runId}`);
8163
8515
  throw new Error(`Site audit run was no longer running before publication: ${runId}`);
8164
8516
  }
@@ -8173,12 +8525,12 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8173
8525
  error: message,
8174
8526
  finishedAt,
8175
8527
  updatedAt: finishedAt
8176
- }).where(eq14(siteCrawlAttempts.id, attemptId)).run();
8528
+ }).where(eq15(siteCrawlAttempts.id, attemptId)).run();
8177
8529
  tx.update(runs).set({
8178
8530
  status: cancelled ? "cancelled" : "failed",
8179
8531
  error: message,
8180
8532
  finishedAt
8181
- }).where(and11(eq14(runs.id, runId), eq14(runs.status, "running"))).run();
8533
+ }).where(and12(eq15(runs.id, runId), eq15(runs.status, "running"))).run();
8182
8534
  });
8183
8535
  log12.error("failed", { runId, projectId, cancelled, error: message });
8184
8536
  throw error;
@@ -8186,7 +8538,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
8186
8538
  }
8187
8539
 
8188
8540
  // src/commands/backfill.ts
8189
- import { and as and12, eq as eq15, inArray as inArray4, isNull as isNull2, sql as sql7 } from "drizzle-orm";
8541
+ import { and as and13, eq as eq16, inArray as inArray5, isNull as isNull2, sql as sql7 } from "drizzle-orm";
8190
8542
  var SNAPSHOT_BATCH_SIZE = 500;
8191
8543
  async function backfillAnswerVisibilityCommand(opts) {
8192
8544
  const config = loadConfig();
@@ -8194,7 +8546,7 @@ async function backfillAnswerVisibilityCommand(opts) {
8194
8546
  migrate(db);
8195
8547
  const projectFilter = opts?.project?.trim();
8196
8548
  const isDryRun = opts?.dryRun === true;
8197
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq15(projects.name, projectFilter)).all() : db.select().from(projects).all();
8549
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq16(projects.name, projectFilter)).all() : db.select().from(projects).all();
8198
8550
  let examined = 0;
8199
8551
  let updated = 0;
8200
8552
  let wouldUpdate = 0;
@@ -8202,10 +8554,10 @@ async function backfillAnswerVisibilityCommand(opts) {
8202
8554
  let reparsed = 0;
8203
8555
  let providerErrors = 0;
8204
8556
  if (scopedProjects.length > 0) {
8205
- const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and12(
8206
- eq15(runs.kind, RunKinds["answer-visibility"]),
8207
- inArray4(runs.projectId, scopedProjects.map((project) => project.id))
8208
- )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq15(runs.kind, RunKinds["answer-visibility"])).all();
8557
+ const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and13(
8558
+ eq16(runs.kind, RunKinds["answer-visibility"]),
8559
+ inArray5(runs.projectId, scopedProjects.map((project) => project.id))
8560
+ )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq16(runs.kind, RunKinds["answer-visibility"])).all();
8209
8561
  const runIdsByProject = /* @__PURE__ */ new Map();
8210
8562
  for (const run of runRows) {
8211
8563
  const existing = runIdsByProject.get(run.projectId);
@@ -8213,7 +8565,7 @@ async function backfillAnswerVisibilityCommand(opts) {
8213
8565
  else runIdsByProject.set(run.projectId, [run.id]);
8214
8566
  }
8215
8567
  for (const project of scopedProjects) {
8216
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq15(competitors.projectId, project.id)).all().map((row) => row.domain);
8568
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq16(competitors.projectId, project.id)).all().map((row) => row.domain);
8217
8569
  const runIds = runIdsByProject.get(project.id) ?? [];
8218
8570
  if (runIds.length === 0) continue;
8219
8571
  const projectDomains = effectiveDomains({
@@ -8236,7 +8588,7 @@ async function backfillAnswerVisibilityCommand(opts) {
8236
8588
  competitorOverlap: querySnapshots.competitorOverlap,
8237
8589
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
8238
8590
  rawResponse: querySnapshots.rawResponse
8239
- }).from(querySnapshots).where(inArray4(querySnapshots.runId, batchRunIds)).all();
8591
+ }).from(querySnapshots).where(inArray5(querySnapshots.runId, batchRunIds)).all();
8240
8592
  const pendingUpdates = [];
8241
8593
  for (const snapshot of snapshotRows) {
8242
8594
  examined++;
@@ -8306,7 +8658,7 @@ async function backfillAnswerVisibilityCommand(opts) {
8306
8658
  } else {
8307
8659
  db.transaction((tx) => {
8308
8660
  for (const update of pendingUpdates) {
8309
- tx.update(querySnapshots).set(update.patch).where(eq15(querySnapshots.id, update.id)).run();
8661
+ tx.update(querySnapshots).set(update.patch).where(eq16(querySnapshots.id, update.id)).run();
8310
8662
  }
8311
8663
  });
8312
8664
  updated += pendingUpdates.length;
@@ -8355,13 +8707,13 @@ No DB writes performed. Re-run without --dry-run to apply.`);
8355
8707
  function backfillNormalizedPaths(db, opts) {
8356
8708
  const baseConditions = [];
8357
8709
  if (opts?.projectId) {
8358
- baseConditions.push(eq15(gaTrafficSnapshots.projectId, opts.projectId));
8710
+ baseConditions.push(eq16(gaTrafficSnapshots.projectId, opts.projectId));
8359
8711
  }
8360
8712
  const rows = db.select({
8361
8713
  id: gaTrafficSnapshots.id,
8362
8714
  landingPage: gaTrafficSnapshots.landingPage,
8363
8715
  landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
8364
- }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and12(...baseConditions) : void 0).all();
8716
+ }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and13(...baseConditions) : void 0).all();
8365
8717
  let updated = 0;
8366
8718
  let unchanged = 0;
8367
8719
  if (rows.length > 0) {
@@ -8376,7 +8728,7 @@ function backfillNormalizedPaths(db, opts) {
8376
8728
  unchanged++;
8377
8729
  continue;
8378
8730
  }
8379
- tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq15(gaTrafficSnapshots.id, row.id)).run();
8731
+ tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq16(gaTrafficSnapshots.id, row.id)).run();
8380
8732
  updated++;
8381
8733
  }
8382
8734
  });
@@ -8390,7 +8742,7 @@ async function backfillNormalizedPathsCommand(opts) {
8390
8742
  const projectFilter = opts?.project?.trim();
8391
8743
  let projectId;
8392
8744
  if (projectFilter) {
8393
- const project = db.select({ id: projects.id }).from(projects).where(eq15(projects.name, projectFilter)).get();
8745
+ const project = db.select({ id: projects.id }).from(projects).where(eq16(projects.name, projectFilter)).get();
8394
8746
  if (!project) {
8395
8747
  const result2 = {
8396
8748
  project: projectFilter,
@@ -8427,13 +8779,13 @@ async function backfillNormalizedPathsCommand(opts) {
8427
8779
  function backfillAiReferralPaths(db, opts) {
8428
8780
  const baseConditions = [];
8429
8781
  if (opts?.projectId) {
8430
- baseConditions.push(eq15(gaAiReferrals.projectId, opts.projectId));
8782
+ baseConditions.push(eq16(gaAiReferrals.projectId, opts.projectId));
8431
8783
  }
8432
8784
  const rows = db.select({
8433
8785
  id: gaAiReferrals.id,
8434
8786
  landingPage: gaAiReferrals.landingPage,
8435
8787
  landingPageNormalized: gaAiReferrals.landingPageNormalized
8436
- }).from(gaAiReferrals).where(baseConditions.length > 0 ? and12(...baseConditions) : void 0).all();
8788
+ }).from(gaAiReferrals).where(baseConditions.length > 0 ? and13(...baseConditions) : void 0).all();
8437
8789
  let updated = 0;
8438
8790
  let unchanged = 0;
8439
8791
  if (rows.length > 0) {
@@ -8448,7 +8800,7 @@ function backfillAiReferralPaths(db, opts) {
8448
8800
  unchanged++;
8449
8801
  continue;
8450
8802
  }
8451
- tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq15(gaAiReferrals.id, row.id)).run();
8803
+ tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq16(gaAiReferrals.id, row.id)).run();
8452
8804
  updated++;
8453
8805
  }
8454
8806
  });
@@ -8462,7 +8814,7 @@ async function backfillAiReferralPathsCommand(opts) {
8462
8814
  const projectFilter = opts?.project?.trim();
8463
8815
  let projectId;
8464
8816
  if (projectFilter) {
8465
- const project = db.select({ id: projects.id }).from(projects).where(eq15(projects.name, projectFilter)).get();
8817
+ const project = db.select({ id: projects.id }).from(projects).where(eq16(projects.name, projectFilter)).get();
8466
8818
  if (!project) {
8467
8819
  const result2 = {
8468
8820
  project: projectFilter,
@@ -8498,10 +8850,10 @@ async function backfillAiReferralPathsCommand(opts) {
8498
8850
  }
8499
8851
  function backfillProjectAnswerMentions(db, projectId, opts) {
8500
8852
  const isDryRun = opts?.dryRun === true;
8501
- const project = db.select().from(projects).where(eq15(projects.id, projectId)).get();
8853
+ const project = db.select().from(projects).where(eq16(projects.id, projectId)).get();
8502
8854
  if (!project) return { examined: 0, updated: 0, mentioned: 0 };
8503
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq15(competitors.projectId, projectId)).all().map((row) => row.domain);
8504
- const runRows = db.select({ id: runs.id }).from(runs).where(and12(eq15(runs.kind, RunKinds["answer-visibility"]), eq15(runs.projectId, projectId))).all();
8855
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq16(competitors.projectId, projectId)).all().map((row) => row.domain);
8856
+ const runRows = db.select({ id: runs.id }).from(runs).where(and13(eq16(runs.kind, RunKinds["answer-visibility"]), eq16(runs.projectId, projectId))).all();
8505
8857
  const runIds = runRows.map((r) => r.id);
8506
8858
  let examined = 0;
8507
8859
  let updated = 0;
@@ -8529,7 +8881,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
8529
8881
  competitorOverlap: querySnapshots.competitorOverlap,
8530
8882
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
8531
8883
  rawResponse: querySnapshots.rawResponse
8532
- }).from(querySnapshots).where(inArray4(querySnapshots.runId, batchRunIds)).all();
8884
+ }).from(querySnapshots).where(inArray5(querySnapshots.runId, batchRunIds)).all();
8533
8885
  const pendingUpdates = [];
8534
8886
  for (const snapshot of snapshotRows) {
8535
8887
  examined++;
@@ -8575,7 +8927,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
8575
8927
  } else {
8576
8928
  db.transaction((tx) => {
8577
8929
  for (const update of pendingUpdates) {
8578
- tx.update(querySnapshots).set(update.patch).where(eq15(querySnapshots.id, update.id)).run();
8930
+ tx.update(querySnapshots).set(update.patch).where(eq16(querySnapshots.id, update.id)).run();
8579
8931
  }
8580
8932
  });
8581
8933
  updated += pendingUpdates.length;
@@ -8590,7 +8942,7 @@ async function backfillAnswerMentionsCommand(opts) {
8590
8942
  migrate(db);
8591
8943
  const projectFilter = opts?.project?.trim();
8592
8944
  const isDryRun = opts?.dryRun === true;
8593
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq15(projects.name, projectFilter)).all() : db.select().from(projects).all();
8945
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq16(projects.name, projectFilter)).all() : db.select().from(projects).all();
8594
8946
  let examined = 0;
8595
8947
  let updated = 0;
8596
8948
  let wouldUpdate = 0;
@@ -8650,7 +9002,7 @@ function readStoredGroundingSources(rawResponse) {
8650
9002
  return result;
8651
9003
  }
8652
9004
  async function backfillInsightsCommand(project, opts) {
8653
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-LQ6ZRWMF.js");
9005
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-DHY5CYWX.js");
8654
9006
  const config = loadConfig();
8655
9007
  const db = createClient(config.database);
8656
9008
  migrate(db);
@@ -8809,7 +9161,7 @@ async function backfillSnapshotAttributionCommand(opts) {
8809
9161
  const config = loadConfig();
8810
9162
  const db = createClient(config.database);
8811
9163
  migrate(db);
8812
- const project = db.select().from(projects).where(eq15(projects.name, opts.project)).get();
9164
+ const project = db.select().from(projects).where(eq16(projects.name, opts.project)).get();
8813
9165
  if (!project) {
8814
9166
  throw new Error(`Project "${opts.project}" not found`);
8815
9167
  }
@@ -8820,17 +9172,17 @@ async function backfillSnapshotAttributionCommand(opts) {
8820
9172
  process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
8821
9173
  `);
8822
9174
  }
8823
- const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and12(
8824
- eq15(auditLog.projectId, project.id),
8825
- inArray4(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
9175
+ const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and13(
9176
+ eq16(auditLog.projectId, project.id),
9177
+ inArray5(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
8826
9178
  )).orderBy(auditLog.createdAt).all();
8827
9179
  const history = replayQueryAuditLog(events);
8828
9180
  const orphanRuns = db.select({
8829
9181
  runId: runs.id,
8830
9182
  createdAt: runs.createdAt,
8831
9183
  location: runs.location
8832
- }).from(runs).innerJoin(querySnapshots, eq15(querySnapshots.runId, runs.id)).where(and12(
8833
- eq15(runs.projectId, project.id),
9184
+ }).from(runs).innerJoin(querySnapshots, eq16(querySnapshots.runId, runs.id)).where(and13(
9185
+ eq16(runs.projectId, project.id),
8834
9186
  isNull2(querySnapshots.queryId),
8835
9187
  isNull2(querySnapshots.queryText)
8836
9188
  )).groupBy(runs.id).orderBy(runs.createdAt).all();
@@ -8852,8 +9204,8 @@ async function backfillSnapshotAttributionCommand(opts) {
8852
9204
  provider: querySnapshots.provider,
8853
9205
  createdAt: querySnapshots.createdAt,
8854
9206
  answerText: querySnapshots.answerText
8855
- }).from(querySnapshots).where(and12(
8856
- eq15(querySnapshots.runId, run.runId),
9207
+ }).from(querySnapshots).where(and13(
9208
+ eq16(querySnapshots.runId, run.runId),
8857
9209
  isNull2(querySnapshots.queryId),
8858
9210
  isNull2(querySnapshots.queryText)
8859
9211
  )).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
@@ -8919,7 +9271,7 @@ async function backfillSnapshotAttributionCommand(opts) {
8919
9271
  if (!isDryRun && updates.length > 0) {
8920
9272
  db.transaction((tx) => {
8921
9273
  for (const u of updates) {
8922
- tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq15(querySnapshots.id, u.id)).run();
9274
+ tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq16(querySnapshots.id, u.id)).run();
8923
9275
  }
8924
9276
  });
8925
9277
  }
@@ -8993,7 +9345,7 @@ async function backfillTrafficClassificationCommand(opts) {
8993
9345
  const projectFilter = opts?.project?.trim();
8994
9346
  const isDryRun = opts?.dryRun === true;
8995
9347
  const isJson = isMachineFormat(opts?.format);
8996
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq15(projects.name, projectFilter)).all() : db.select().from(projects).all();
9348
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq16(projects.name, projectFilter)).all() : db.select().from(projects).all();
8997
9349
  if (scopedProjects.length === 0) {
8998
9350
  if (projectFilter && !isJson) {
8999
9351
  process.stderr.write(`No project named "${projectFilter}".
@@ -9018,9 +9370,9 @@ async function backfillTrafficClassificationCommand(opts) {
9018
9370
  dryRun: isDryRun,
9019
9371
  byBot: {}
9020
9372
  };
9021
- const unknownCountRow = db.select({ n: sql7`count(*)` }).from(rawEventSamples).where(and12(
9022
- eq15(rawEventSamples.eventType, "unknown"),
9023
- inArray4(rawEventSamples.projectId, projectIds)
9373
+ const unknownCountRow = db.select({ n: sql7`count(*)` }).from(rawEventSamples).where(and13(
9374
+ eq16(rawEventSamples.eventType, "unknown"),
9375
+ inArray5(rawEventSamples.projectId, projectIds)
9024
9376
  )).get();
9025
9377
  result.unknownBefore = Number(unknownCountRow?.n ?? 0);
9026
9378
  const unknownSamples = db.select({
@@ -9031,9 +9383,9 @@ async function backfillTrafficClassificationCommand(opts) {
9031
9383
  userAgent: rawEventSamples.userAgent,
9032
9384
  pathNormalized: rawEventSamples.pathNormalized,
9033
9385
  status: rawEventSamples.status
9034
- }).from(rawEventSamples).where(and12(
9035
- eq15(rawEventSamples.eventType, "unknown"),
9036
- inArray4(rawEventSamples.projectId, projectIds)
9386
+ }).from(rawEventSamples).where(and13(
9387
+ eq16(rawEventSamples.eventType, "unknown"),
9388
+ inArray5(rawEventSamples.projectId, projectIds)
9037
9389
  )).all();
9038
9390
  result.examined = unknownSamples.length;
9039
9391
  if (unknownSamples.length === 0) {
@@ -9071,7 +9423,7 @@ async function backfillTrafficClassificationCommand(opts) {
9071
9423
  result.reclassified++;
9072
9424
  result.byBot[classified.botId] = (result.byBot[classified.botId] ?? 0) + 1;
9073
9425
  if (isDryRun) continue;
9074
- db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq15(rawEventSamples.id, snap.id)).run();
9426
+ db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq16(rawEventSamples.id, snap.id)).run();
9075
9427
  const tsHour = new Date(snap.ts);
9076
9428
  tsHour.setUTCMinutes(0, 0, 0);
9077
9429
  if (userFetch) {
@@ -9135,9 +9487,9 @@ async function backfillTrafficClassificationCommand(opts) {
9135
9487
  }
9136
9488
  }
9137
9489
  if (!isDryRun) {
9138
- const afterRow = db.select({ n: sql7`count(*)` }).from(rawEventSamples).where(and12(
9139
- eq15(rawEventSamples.eventType, "unknown"),
9140
- inArray4(rawEventSamples.projectId, projectIds)
9490
+ const afterRow = db.select({ n: sql7`count(*)` }).from(rawEventSamples).where(and13(
9491
+ eq16(rawEventSamples.eventType, "unknown"),
9492
+ inArray5(rawEventSamples.projectId, projectIds)
9141
9493
  )).get();
9142
9494
  result.unknownAfter = Number(afterRow?.n ?? 0);
9143
9495
  } else {
@@ -9570,7 +9922,7 @@ var ProviderRegistry = class {
9570
9922
  // src/scheduler.ts
9571
9923
  import crypto19 from "crypto";
9572
9924
  import cron from "node-cron";
9573
- import { and as and13, eq as eq16, inArray as inArray5, notExists, sql as sql8 } from "drizzle-orm";
9925
+ import { and as and14, eq as eq17, inArray as inArray6, notExists, sql as sql8 } from "drizzle-orm";
9574
9926
  var log13 = createLogger("Scheduler");
9575
9927
  var DEFAULT_HEALTH_CRON = "0 */6 * * *";
9576
9928
  function taskKey(projectId, kind) {
@@ -9602,9 +9954,9 @@ var Scheduler = class {
9602
9954
  */
9603
9955
  ensureHealthSchedules() {
9604
9956
  const projectsWithoutHealth = this.db.select({ id: projects.id }).from(projects).where(notExists(
9605
- this.db.select({ one: sql8`1` }).from(schedules).where(and13(
9606
- eq16(schedules.projectId, projects.id),
9607
- eq16(schedules.kind, SchedulableRunKinds.doctor)
9957
+ this.db.select({ one: sql8`1` }).from(schedules).where(and14(
9958
+ eq17(schedules.projectId, projects.id),
9959
+ eq17(schedules.kind, SchedulableRunKinds.doctor)
9608
9960
  ))
9609
9961
  )).all();
9610
9962
  if (projectsWithoutHealth.length === 0) return;
@@ -9655,7 +10007,7 @@ var Scheduler = class {
9655
10007
  start() {
9656
10008
  this.ensureHealthSchedules();
9657
10009
  this.ensureQueryBaskets();
9658
- const allSchedules = this.db.select().from(schedules).where(eq16(schedules.enabled, true)).all();
10010
+ const allSchedules = this.db.select().from(schedules).where(eq17(schedules.enabled, true)).all();
9659
10011
  for (const schedule of allSchedules) {
9660
10012
  const missedRunAt = schedule.nextRunAt;
9661
10013
  this.registerCronTask(schedule);
@@ -9685,7 +10037,7 @@ var Scheduler = class {
9685
10037
  this.stopTask(key, existing, "Stopped");
9686
10038
  this.tasks.delete(key);
9687
10039
  }
9688
- const schedule = this.db.select().from(schedules).where(and13(eq16(schedules.projectId, projectId), eq16(schedules.kind, kind))).get();
10040
+ const schedule = this.db.select().from(schedules).where(and14(eq17(schedules.projectId, projectId), eq17(schedules.kind, kind))).get();
9689
10041
  if (schedule && schedule.enabled) {
9690
10042
  this.registerCronTask(schedule);
9691
10043
  }
@@ -9726,21 +10078,21 @@ var Scheduler = class {
9726
10078
  this.db.update(schedules).set({
9727
10079
  nextRunAt: nextRunFromCron(cronExpr, timezone),
9728
10080
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
9729
- }).where(eq16(schedules.id, scheduleId)).run();
10081
+ }).where(eq17(schedules.id, scheduleId)).run();
9730
10082
  const label = schedule.preset ?? cronExpr;
9731
10083
  log13.info("cron.registered", { projectId, kind, schedule: label, timezone });
9732
10084
  }
9733
10085
  triggerRun(scheduleId, projectId, kind) {
9734
10086
  try {
9735
10087
  const now = (/* @__PURE__ */ new Date()).toISOString();
9736
- const currentSchedule = this.db.select().from(schedules).where(eq16(schedules.id, scheduleId)).get();
10088
+ const currentSchedule = this.db.select().from(schedules).where(eq17(schedules.id, scheduleId)).get();
9737
10089
  if (!currentSchedule || !currentSchedule.enabled) {
9738
10090
  log13.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
9739
10091
  this.remove(projectId, kind);
9740
10092
  return;
9741
10093
  }
9742
10094
  const nextRunAt = nextRunFromCron(currentSchedule.cronExpr, currentSchedule.timezone);
9743
- const project = this.db.select().from(projects).where(eq16(projects.id, projectId)).get();
10095
+ const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
9744
10096
  if (!project) {
9745
10097
  log13.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
9746
10098
  this.remove(projectId, kind);
@@ -9760,7 +10112,7 @@ var Scheduler = class {
9760
10112
  lastRunAt: now,
9761
10113
  nextRunAt,
9762
10114
  updatedAt: now
9763
- }).where(eq16(schedules.id, currentSchedule.id)).run();
10115
+ }).where(eq17(schedules.id, currentSchedule.id)).run();
9764
10116
  log13.info("traffic-sync.triggered", { projectName: project.name, sourceId });
9765
10117
  this.callbacks.onTrafficSyncRequested(project.name, sourceId);
9766
10118
  return;
@@ -9783,7 +10135,7 @@ var Scheduler = class {
9783
10135
  lastRunAt: now,
9784
10136
  nextRunAt,
9785
10137
  updatedAt: now
9786
- }).where(eq16(schedules.id, currentSchedule.id)).run();
10138
+ }).where(eq17(schedules.id, currentSchedule.id)).run();
9787
10139
  log13.info("gbp-sync.triggered", { runId: runId2, projectName: project.name });
9788
10140
  this.callbacks.onGbpSyncRequested(runId2, projectId);
9789
10141
  return;
@@ -9793,14 +10145,14 @@ var Scheduler = class {
9793
10145
  log13.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
9794
10146
  return;
9795
10147
  }
9796
- const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and13(
9797
- eq16(runs.projectId, projectId),
9798
- eq16(runs.kind, RunKinds["ads-sync"]),
9799
- inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
10148
+ const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and14(
10149
+ eq17(runs.projectId, projectId),
10150
+ eq17(runs.kind, RunKinds["ads-sync"]),
10151
+ inArray6(runs.status, [RunStatuses.queued, RunStatuses.running])
9800
10152
  )).get();
9801
10153
  if (activeAdsRun) {
9802
10154
  log13.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
9803
- this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq16(schedules.id, currentSchedule.id)).run();
10155
+ this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq17(schedules.id, currentSchedule.id)).run();
9804
10156
  return;
9805
10157
  }
9806
10158
  const runId2 = crypto19.randomUUID();
@@ -9816,7 +10168,7 @@ var Scheduler = class {
9816
10168
  lastRunAt: now,
9817
10169
  nextRunAt,
9818
10170
  updatedAt: now
9819
- }).where(eq16(schedules.id, currentSchedule.id)).run();
10171
+ }).where(eq17(schedules.id, currentSchedule.id)).run();
9820
10172
  log13.info("ads-sync.triggered", { runId: runId2, projectName: project.name });
9821
10173
  this.callbacks.onAdsSyncRequested(runId2, projectId);
9822
10174
  return;
@@ -9830,7 +10182,7 @@ var Scheduler = class {
9830
10182
  lastRunAt: now,
9831
10183
  nextRunAt,
9832
10184
  updatedAt: now
9833
- }).where(eq16(schedules.id, currentSchedule.id)).run();
10185
+ }).where(eq17(schedules.id, currentSchedule.id)).run();
9834
10186
  log13.info("data-refresh.triggered", { projectName: project.name });
9835
10187
  this.callbacks.onDataRefreshRequested(project.name);
9836
10188
  return;
@@ -9844,7 +10196,7 @@ var Scheduler = class {
9844
10196
  lastRunAt: now,
9845
10197
  nextRunAt,
9846
10198
  updatedAt: now
9847
- }).where(eq16(schedules.id, currentSchedule.id)).run();
10199
+ }).where(eq17(schedules.id, currentSchedule.id)).run();
9848
10200
  log13.info("doctor.triggered", { projectName: project.name });
9849
10201
  this.callbacks.onDoctorRequested(project.name);
9850
10202
  return;
@@ -9858,7 +10210,7 @@ var Scheduler = class {
9858
10210
  lastRunAt: now,
9859
10211
  nextRunAt,
9860
10212
  updatedAt: now
9861
- }).where(eq16(schedules.id, currentSchedule.id)).run();
10213
+ }).where(eq17(schedules.id, currentSchedule.id)).run();
9862
10214
  log13.info("backlinks-sync.triggered", { projectName: project.name });
9863
10215
  this.callbacks.onBacklinksSyncRequested(project.name);
9864
10216
  return;
@@ -9868,14 +10220,14 @@ var Scheduler = class {
9868
10220
  log13.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
9869
10221
  return;
9870
10222
  }
9871
- const active = this.db.select({ id: runs.id }).from(runs).where(and13(
9872
- eq16(runs.projectId, projectId),
9873
- eq16(runs.kind, RunKinds["site-audit"]),
9874
- inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
10223
+ const active = this.db.select({ id: runs.id }).from(runs).where(and14(
10224
+ eq17(runs.projectId, projectId),
10225
+ eq17(runs.kind, RunKinds["site-audit"]),
10226
+ inArray6(runs.status, [RunStatuses.queued, RunStatuses.running])
9875
10227
  )).get();
9876
10228
  if (active) {
9877
10229
  log13.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
9878
- this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq16(schedules.id, currentSchedule.id)).run();
10230
+ this.db.update(schedules).set({ nextRunAt, updatedAt: now }).where(eq17(schedules.id, currentSchedule.id)).run();
9879
10231
  return;
9880
10232
  }
9881
10233
  const runId2 = crypto19.randomUUID();
@@ -9901,7 +10253,7 @@ var Scheduler = class {
9901
10253
  lastRunAt: now,
9902
10254
  nextRunAt,
9903
10255
  updatedAt: now
9904
- }).where(eq16(schedules.id, currentSchedule.id)).run();
10256
+ }).where(eq17(schedules.id, currentSchedule.id)).run();
9905
10257
  log13.info("site-audit.triggered", { runId: runId2, projectName: project.name });
9906
10258
  this.callbacks.onSiteAuditRequested(runId2, projectId);
9907
10259
  return;
@@ -9934,7 +10286,7 @@ var Scheduler = class {
9934
10286
  this.db.update(schedules).set({
9935
10287
  nextRunAt,
9936
10288
  updatedAt: now
9937
- }).where(eq16(schedules.id, currentSchedule.id)).run();
10289
+ }).where(eq17(schedules.id, currentSchedule.id)).run();
9938
10290
  return;
9939
10291
  }
9940
10292
  const runId = queueResult.runId;
@@ -9942,7 +10294,7 @@ var Scheduler = class {
9942
10294
  lastRunAt: now,
9943
10295
  nextRunAt,
9944
10296
  updatedAt: now
9945
- }).where(eq16(schedules.id, currentSchedule.id)).run();
10297
+ }).where(eq17(schedules.id, currentSchedule.id)).run();
9946
10298
  log13.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
9947
10299
  this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
9948
10300
  } catch (err) {
@@ -9975,7 +10327,7 @@ async function refreshAllIntegrations(client, projectName) {
9975
10327
  }
9976
10328
 
9977
10329
  // src/notifier.ts
9978
- import { eq as eq17, desc as desc7, and as and14, inArray as inArray6, or as or2 } from "drizzle-orm";
10330
+ import { eq as eq18, desc as desc7, and as and15, inArray as inArray7, or as or2 } from "drizzle-orm";
9979
10331
  import crypto20 from "crypto";
9980
10332
  var log15 = createLogger("Notifier");
9981
10333
  var Notifier = class {
@@ -9988,18 +10340,18 @@ var Notifier = class {
9988
10340
  /** Called after a run completes (success, partial, or failed). */
9989
10341
  async onRunCompleted(runId, projectId) {
9990
10342
  log15.info("run.completed", { runId, projectId });
9991
- const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
10343
+ const notifs = this.db.select().from(notifications).where(eq18(notifications.projectId, projectId)).all().filter((n) => n.enabled);
9992
10344
  if (notifs.length === 0) {
9993
10345
  log15.info("notifications.none-enabled", { projectId });
9994
10346
  return;
9995
10347
  }
9996
10348
  log15.info("notifications.found", { projectId, count: notifs.length });
9997
- const run = this.db.select().from(runs).where(eq17(runs.id, runId)).get();
10349
+ const run = this.db.select().from(runs).where(eq18(runs.id, runId)).get();
9998
10350
  if (!run) {
9999
10351
  log15.error("run.not-found", { runId, msg: "skipping notification dispatch" });
10000
10352
  return;
10001
10353
  }
10002
- const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
10354
+ const project = this.db.select().from(projects).where(eq18(projects.id, projectId)).get();
10003
10355
  if (!project) {
10004
10356
  log15.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
10005
10357
  return;
@@ -10052,7 +10404,7 @@ var Notifier = class {
10052
10404
  * inferred from the absence of a webhook.
10053
10405
  */
10054
10406
  async onHealthChecked(projectId, report) {
10055
- const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
10407
+ const project = this.db.select().from(projects).where(eq18(projects.id, projectId)).get();
10056
10408
  if (!project) {
10057
10409
  log15.error("project.not-found", { projectId, msg: "skipping health notification" });
10058
10410
  return null;
@@ -10083,7 +10435,7 @@ var Notifier = class {
10083
10435
  const status = noSignal ? "warn" : worst ? worst.status : "ok";
10084
10436
  const code = noSignal ? "health.no-signal" : worst?.code ?? "health.ok";
10085
10437
  const summary = noSignal ? `No health check produced a result (${report.checks.length} skipped) \u2014 health is unknown, not confirmed.` : worst?.summary ?? `All ${graded.length} health check(s) passing.`;
10086
- const previous = this.db.select().from(doctorHealthState).where(eq17(doctorHealthState.projectId, projectId)).get();
10438
+ const previous = this.db.select().from(doctorHealthState).where(eq18(doctorHealthState.projectId, projectId)).get();
10087
10439
  const previousStatus = previous?.status ?? null;
10088
10440
  let event = null;
10089
10441
  if (previous === void 0) {
@@ -10098,13 +10450,13 @@ var Notifier = class {
10098
10450
  if (previous === void 0) {
10099
10451
  this.db.insert(doctorHealthState).values({ ...observation, notifiedAt: null }).run();
10100
10452
  } else {
10101
- this.db.update(doctorHealthState).set(observation).where(eq17(doctorHealthState.projectId, projectId)).run();
10453
+ this.db.update(doctorHealthState).set(observation).where(eq18(doctorHealthState.projectId, projectId)).run();
10102
10454
  }
10103
10455
  if (!event) {
10104
10456
  log15.info("health.unchanged", { projectId, status, code });
10105
10457
  return null;
10106
10458
  }
10107
- const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
10459
+ const notifs = this.db.select().from(notifications).where(eq18(notifications.projectId, projectId)).all().filter((n) => n.enabled);
10108
10460
  const payload = {
10109
10461
  source: "canonry",
10110
10462
  event,
@@ -10128,7 +10480,7 @@ var Notifier = class {
10128
10480
  delivered += 1;
10129
10481
  }
10130
10482
  if (delivered > 0) {
10131
- this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq17(doctorHealthState.projectId, projectId)).run();
10483
+ this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq18(doctorHealthState.projectId, projectId)).run();
10132
10484
  }
10133
10485
  log15.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
10134
10486
  return event;
@@ -10141,11 +10493,11 @@ var Notifier = class {
10141
10493
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
10142
10494
  if (highInsights.length > 0) insightEvents.push("insight.high");
10143
10495
  if (insightEvents.length === 0) return;
10144
- const notifs = this.db.select().from(notifications).where(eq17(notifications.projectId, projectId)).all().filter((n) => n.enabled);
10496
+ const notifs = this.db.select().from(notifications).where(eq18(notifications.projectId, projectId)).all().filter((n) => n.enabled);
10145
10497
  if (notifs.length === 0) return;
10146
- const run = this.db.select().from(runs).where(eq17(runs.id, runId)).get();
10498
+ const run = this.db.select().from(runs).where(eq18(runs.id, runId)).get();
10147
10499
  if (!run) return;
10148
- const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
10500
+ const project = this.db.select().from(projects).where(eq18(projects.id, projectId)).get();
10149
10501
  if (!project) return;
10150
10502
  for (const notif of notifs) {
10151
10503
  const config = notif.config;
@@ -10175,7 +10527,7 @@ var Notifier = class {
10175
10527
  }
10176
10528
  }
10177
10529
  computeTransitions(runId, projectId) {
10178
- const thisRun = this.db.select().from(runs).where(eq17(runs.id, runId)).get();
10530
+ const thisRun = this.db.select().from(runs).where(eq18(runs.id, runId)).get();
10179
10531
  if (!thisRun) return [];
10180
10532
  const completeness = measurementRunCompleteness(this.db, runId);
10181
10533
  if (completeness.planned && !completeness.complete) {
@@ -10186,10 +10538,10 @@ var Notifier = class {
10186
10538
  });
10187
10539
  return [];
10188
10540
  }
10189
- const groupSiblings = this.db.select().from(runs).where(and14(
10190
- eq17(runs.projectId, projectId),
10191
- eq17(runs.kind, thisRun.kind),
10192
- eq17(runs.createdAt, thisRun.createdAt)
10541
+ const groupSiblings = this.db.select().from(runs).where(and15(
10542
+ eq18(runs.projectId, projectId),
10543
+ eq18(runs.kind, thisRun.kind),
10544
+ eq18(runs.createdAt, thisRun.createdAt)
10193
10545
  )).all();
10194
10546
  const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
10195
10547
  if (stillPending) return [];
@@ -10205,17 +10557,17 @@ var Notifier = class {
10205
10557
  return candidate.id > best.id ? candidate : best;
10206
10558
  });
10207
10559
  if (winner.id !== runId) return [];
10208
- const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq17(projects.id, projectId)).get();
10560
+ const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq18(projects.id, projectId)).get();
10209
10561
  const locationCount = Math.max(
10210
10562
  1,
10211
10563
  (projectLocations?.locations ?? []).length
10212
10564
  );
10213
10565
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
10214
10566
  const recentRuns = this.db.select().from(runs).where(
10215
- and14(
10216
- eq17(runs.projectId, projectId),
10217
- eq17(runs.kind, thisRun.kind),
10218
- or2(eq17(runs.status, "completed"), eq17(runs.status, "partial"))
10567
+ and15(
10568
+ eq18(runs.projectId, projectId),
10569
+ eq18(runs.kind, thisRun.kind),
10570
+ or2(eq18(runs.status, "completed"), eq18(runs.status, "partial"))
10219
10571
  )
10220
10572
  ).orderBy(desc7(runs.createdAt), desc7(runs.id)).limit(RECENT_FETCH_LIMIT).all();
10221
10573
  const groups = groupRunsByCreatedAt(recentRuns);
@@ -10232,13 +10584,13 @@ var Notifier = class {
10232
10584
  provider: querySnapshots.provider,
10233
10585
  location: querySnapshots.location,
10234
10586
  citationState: querySnapshots.citationState
10235
- }).from(querySnapshots).leftJoin(queries, eq17(querySnapshots.queryId, queries.id)).where(inArray6(querySnapshots.runId, currentRunIds)).all();
10587
+ }).from(querySnapshots).leftJoin(queries, eq18(querySnapshots.queryId, queries.id)).where(inArray7(querySnapshots.runId, currentRunIds)).all();
10236
10588
  const previousSnapshots = this.db.select({
10237
10589
  queryId: querySnapshots.queryId,
10238
10590
  provider: querySnapshots.provider,
10239
10591
  location: querySnapshots.location,
10240
10592
  citationState: querySnapshots.citationState
10241
- }).from(querySnapshots).where(inArray6(querySnapshots.runId, previousRunIds)).all();
10593
+ }).from(querySnapshots).where(inArray7(querySnapshots.runId, previousRunIds)).all();
10242
10594
  const prevMap = /* @__PURE__ */ new Map();
10243
10595
  for (const s of previousSnapshots) {
10244
10596
  if (s.queryId == null) continue;
@@ -10315,7 +10667,7 @@ var Notifier = class {
10315
10667
  };
10316
10668
 
10317
10669
  // src/run-coordinator.ts
10318
- import { eq as eq18 } from "drizzle-orm";
10670
+ import { eq as eq19 } from "drizzle-orm";
10319
10671
  var log16 = createLogger("RunCoordinator");
10320
10672
  var RunCoordinator = class {
10321
10673
  constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
@@ -10331,7 +10683,7 @@ var RunCoordinator = class {
10331
10683
  onInsightsGenerated;
10332
10684
  onAeroEvent;
10333
10685
  async onRunCompleted(runId, projectId) {
10334
- const runRow = this.db.select().from(runs).where(eq18(runs.id, runId)).get();
10686
+ const runRow = this.db.select().from(runs).where(eq19(runs.id, runId)).get();
10335
10687
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
10336
10688
  if (runRow?.trigger === RunTriggers.probe) {
10337
10689
  log16.info("probe.skip-side-effects", { runId, projectId, kind });
@@ -10422,7 +10774,7 @@ var RunCoordinator = class {
10422
10774
  * so the Aero queue is never starved of a follow-up.
10423
10775
  */
10424
10776
  buildDiscoveryAeroContext(runId, projectId, status, error) {
10425
- const session = this.db.select().from(discoverySessions).where(eq18(discoverySessions.runId, runId)).get();
10777
+ const session = this.db.select().from(discoverySessions).where(eq19(discoverySessions.runId, runId)).get();
10426
10778
  const competitorMap = session ? session.competitorMap : [];
10427
10779
  return {
10428
10780
  kind: RunKinds["aeo-discover-probe"],
@@ -10465,7 +10817,7 @@ function analysisResultFromInsights(insights2) {
10465
10817
 
10466
10818
  // src/agent/session-registry.ts
10467
10819
  import crypto24 from "crypto";
10468
- import { eq as eq20 } from "drizzle-orm";
10820
+ import { eq as eq21 } from "drizzle-orm";
10469
10821
 
10470
10822
  // src/agent/session.ts
10471
10823
  import fs7 from "fs";
@@ -11594,7 +11946,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
11594
11946
 
11595
11947
  // src/agent/memory-store.ts
11596
11948
  import crypto23 from "crypto";
11597
- import { and as and15, desc as desc8, eq as eq19, like, sql as sql9 } from "drizzle-orm";
11949
+ import { and as and16, desc as desc8, eq as eq20, like, sql as sql9 } from "drizzle-orm";
11598
11950
  var COMPACTION_KEY_PREFIX = "compaction:";
11599
11951
  var COMPACTION_NOTES_PER_SESSION = 3;
11600
11952
  function rowToDto(row) {
@@ -11608,7 +11960,7 @@ function rowToDto(row) {
11608
11960
  };
11609
11961
  }
11610
11962
  function listMemoryEntries(db, projectId, opts = {}) {
11611
- const query = db.select().from(agentMemory).where(eq19(agentMemory.projectId, projectId)).orderBy(desc8(agentMemory.updatedAt));
11963
+ const query = db.select().from(agentMemory).where(eq20(agentMemory.projectId, projectId)).orderBy(desc8(agentMemory.updatedAt));
11612
11964
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
11613
11965
  return rows.map(rowToDto);
11614
11966
  }
@@ -11639,12 +11991,12 @@ function upsertMemoryEntry(db, args) {
11639
11991
  updatedAt: now
11640
11992
  }
11641
11993
  }).run();
11642
- const row = db.select().from(agentMemory).where(and15(eq19(agentMemory.projectId, args.projectId), eq19(agentMemory.key, args.key))).get();
11994
+ const row = db.select().from(agentMemory).where(and16(eq20(agentMemory.projectId, args.projectId), eq20(agentMemory.key, args.key))).get();
11643
11995
  if (!row) throw new Error("memory upsert produced no row");
11644
11996
  return rowToDto(row);
11645
11997
  }
11646
11998
  function deleteMemoryEntry(db, projectId, key) {
11647
- const result = db.delete(agentMemory).where(and15(eq19(agentMemory.projectId, projectId), eq19(agentMemory.key, key))).run();
11999
+ const result = db.delete(agentMemory).where(and16(eq20(agentMemory.projectId, projectId), eq20(agentMemory.key, key))).run();
11648
12000
  const changes = result.changes ?? 0;
11649
12001
  return changes > 0;
11650
12002
  }
@@ -11673,8 +12025,8 @@ function writeCompactionNote(db, args) {
11673
12025
  }).run();
11674
12026
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
11675
12027
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
11676
- and15(
11677
- eq19(agentMemory.projectId, args.projectId),
12028
+ and16(
12029
+ eq20(agentMemory.projectId, args.projectId),
11678
12030
  like(agentMemory.key, `${sessionPrefix}%`)
11679
12031
  )
11680
12032
  ).orderBy(desc8(agentMemory.updatedAt)).all();
@@ -11682,7 +12034,7 @@ function writeCompactionNote(db, args) {
11682
12034
  if (stale.length > 0) {
11683
12035
  tx.delete(agentMemory).where(sql9`${agentMemory.id} IN (${sql9.join(stale.map((s) => sql9`${s}`), sql9`, `)})`).run();
11684
12036
  }
11685
- const row = tx.select().from(agentMemory).where(and15(eq19(agentMemory.projectId, args.projectId), eq19(agentMemory.key, key))).get();
12037
+ const row = tx.select().from(agentMemory).where(and16(eq20(agentMemory.projectId, args.projectId), eq20(agentMemory.key, key))).get();
11686
12038
  if (row) inserted = rowToDto(row);
11687
12039
  });
11688
12040
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -11909,7 +12261,7 @@ var SessionRegistry = class {
11909
12261
  modelProvider: effectiveProvider,
11910
12262
  modelId: effectiveModelId,
11911
12263
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
11912
- }).where(eq20(agentSessions.projectId, projectId)).run();
12264
+ }).where(eq21(agentSessions.projectId, projectId)).run();
11913
12265
  }
11914
12266
  const agent2 = createAeroSession({
11915
12267
  projectName,
@@ -12143,7 +12495,7 @@ ${lines.join("\n")}
12143
12495
  modelProvider: nextProvider,
12144
12496
  modelId: nextModelId,
12145
12497
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
12146
- }).where(eq20(agentSessions.projectId, projectId)).run();
12498
+ }).where(eq21(agentSessions.projectId, projectId)).run();
12147
12499
  }
12148
12500
  /** Persist a session's transcript back to the DB. Call after any run settles. */
12149
12501
  save(projectName) {
@@ -12307,11 +12659,11 @@ ${lines.join("\n")}
12307
12659
  return id;
12308
12660
  }
12309
12661
  tryResolveProjectId(projectName) {
12310
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq20(projects.name, projectName)).get();
12662
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq21(projects.name, projectName)).get();
12311
12663
  return row?.id;
12312
12664
  }
12313
12665
  loadRow(projectId) {
12314
- const row = this.opts.db.select().from(agentSessions).where(eq20(agentSessions.projectId, projectId)).get();
12666
+ const row = this.opts.db.select().from(agentSessions).where(eq21(agentSessions.projectId, projectId)).get();
12315
12667
  return row ?? null;
12316
12668
  }
12317
12669
  insertRow(params) {
@@ -12330,14 +12682,14 @@ ${lines.join("\n")}
12330
12682
  }
12331
12683
  updateRow(projectId, patch) {
12332
12684
  const now = (/* @__PURE__ */ new Date()).toISOString();
12333
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq20(agentSessions.projectId, projectId)).run();
12685
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq21(agentSessions.projectId, projectId)).run();
12334
12686
  }
12335
12687
  };
12336
12688
 
12337
12689
  // src/agent/agent-routes.ts
12338
- import { eq as eq21 } from "drizzle-orm";
12690
+ import { eq as eq22 } from "drizzle-orm";
12339
12691
  function resolveProject(db, name) {
12340
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq21(projects.name, name)).get();
12692
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq22(projects.name, name)).get();
12341
12693
  if (!row) throw notFound("project", name);
12342
12694
  return row;
12343
12695
  }
@@ -12346,7 +12698,7 @@ function registerAgentRoutes(app, opts) {
12346
12698
  "/projects/:name/agent/transcript",
12347
12699
  async (request) => {
12348
12700
  const project = resolveProject(opts.db, request.params.name);
12349
- const row = opts.db.select().from(agentSessions).where(eq21(agentSessions.projectId, project.id)).get();
12701
+ const row = opts.db.select().from(agentSessions).where(eq22(agentSessions.projectId, project.id)).get();
12350
12702
  if (!row) {
12351
12703
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
12352
12704
  }
@@ -12370,7 +12722,7 @@ function registerAgentRoutes(app, opts) {
12370
12722
  async (request) => {
12371
12723
  const project = resolveProject(opts.db, request.params.name);
12372
12724
  opts.sessionRegistry.reset(project.name);
12373
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(agentSessions.projectId, project.id)).run();
12725
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(agentSessions.projectId, project.id)).run();
12374
12726
  return { status: "reset" };
12375
12727
  }
12376
12728
  );
@@ -12697,16 +13049,16 @@ Your previous reply was not valid JSON. Return ONLY a JSON object with exactly t
12697
13049
  import { runAeoAudit } from "@canonry/aeo-audit";
12698
13050
 
12699
13051
  // src/site-fetch.ts
12700
- import https from "https";
13052
+ import https2 from "https";
12701
13053
  var FETCH_TIMEOUT_MS2 = 1e4;
12702
13054
  var MAX_TEXT_LENGTH = 4e3;
12703
13055
  var MAX_BODY_BYTES = 512e3;
12704
- var USER_AGENT = "Canonry/1.0 (site-analysis)";
13056
+ var USER_AGENT2 = "Canonry/1.0 (site-analysis)";
12705
13057
  function fetchWithPinnedAddress(target) {
12706
13058
  return new Promise((resolve) => {
12707
13059
  const port = target.url.port ? Number(target.url.port) : 443;
12708
13060
  const path10 = target.url.pathname + target.url.search;
12709
- const req = https.request(
13061
+ const req = https2.request(
12710
13062
  {
12711
13063
  hostname: target.address,
12712
13064
  family: target.family,
@@ -12718,7 +13070,7 @@ function fetchWithPinnedAddress(target) {
12718
13070
  // SNI for TLS
12719
13071
  headers: {
12720
13072
  Host: target.url.host,
12721
- "User-Agent": USER_AGENT,
13073
+ "User-Agent": USER_AGENT2,
12722
13074
  Accept: "text/html"
12723
13075
  }
12724
13076
  },
@@ -13313,20 +13665,20 @@ function clipText(value, length) {
13313
13665
  }
13314
13666
 
13315
13667
  // src/research-runner.ts
13316
- import { and as and16, eq as eq22, inArray as inArray7, sql as sql10 } from "drizzle-orm";
13668
+ import { and as and17, eq as eq23, inArray as inArray8, sql as sql10 } from "drizzle-orm";
13317
13669
  var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
13318
13670
  function finalResearchRunStatus(completed, failed) {
13319
13671
  if (failed === 0) return ResearchRunStatuses.completed;
13320
13672
  return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
13321
13673
  }
13322
13674
  async function executeResearchRun(db, registry, runId, projectId) {
13323
- const run = db.select().from(researchRuns).where(and16(eq22(researchRuns.id, runId), eq22(researchRuns.projectId, projectId))).get();
13675
+ const run = db.select().from(researchRuns).where(and17(eq23(researchRuns.id, runId), eq23(researchRuns.projectId, projectId))).get();
13324
13676
  if (!run || run.status !== ResearchRunStatuses.queued) return;
13325
- const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
13677
+ const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
13326
13678
  if (!project) return;
13327
13679
  const provider = registry.get(run.provider);
13328
13680
  const now = (/* @__PURE__ */ new Date()).toISOString();
13329
- const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and16(eq22(researchRuns.id, runId), eq22(researchRuns.status, ResearchRunStatuses.queued))).run();
13681
+ const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and17(eq23(researchRuns.id, runId), eq23(researchRuns.status, ResearchRunStatuses.queued))).run();
13330
13682
  if (claim.changes !== 1) return;
13331
13683
  let reserved = 0;
13332
13684
  let dispatched = 0;
@@ -13350,8 +13702,8 @@ async function executeResearchRun(db, registry, runId, projectId) {
13350
13702
  }
13351
13703
  reserved = run.totalQueries;
13352
13704
  reservation = { scope, period };
13353
- const rows = db.select().from(researchRunQueries).where(eq22(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
13354
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq22(competitors.projectId, projectId)).all().map((row) => row.domain);
13705
+ const rows = db.select().from(researchRunQueries).where(eq23(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
13706
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq23(competitors.projectId, projectId)).all().map((row) => row.domain);
13355
13707
  const domains = effectiveDomains(project);
13356
13708
  const brands = effectiveBrandNames(project);
13357
13709
  const config = { ...provider.config, model: run.resolvedModel };
@@ -13359,7 +13711,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
13359
13711
  await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
13360
13712
  try {
13361
13713
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
13362
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and16(eq22(researchRunQueries.id, row.id), eq22(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
13714
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and17(eq23(researchRunQueries.id, row.id), eq23(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
13363
13715
  const raw = await gate.run(async () => {
13364
13716
  dispatched++;
13365
13717
  return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
@@ -13386,7 +13738,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
13386
13738
  citationState: determineCitationState(normalized, domains),
13387
13739
  rawResponse: raw.rawResponse,
13388
13740
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
13389
- }).where(and16(eq22(researchRunQueries.id, row.id), eq22(researchRunQueries.status, ResearchQueryStatuses.running))).run();
13741
+ }).where(and17(eq23(researchRunQueries.id, row.id), eq23(researchRunQueries.status, ResearchQueryStatuses.running))).run();
13390
13742
  if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
13391
13743
  } catch (error) {
13392
13744
  try {
@@ -13410,18 +13762,18 @@ async function executeResearchRun(db, registry, runId, projectId) {
13410
13762
  }
13411
13763
  }
13412
13764
  function incrementResearchProgress(db, runId, column) {
13413
- db.update(researchRuns).set({ [column]: sql10`${researchRuns[column]} + 1` }).where(eq22(researchRuns.id, runId)).run();
13765
+ db.update(researchRuns).set({ [column]: sql10`${researchRuns[column]} + 1` }).where(eq23(researchRuns.id, runId)).run();
13414
13766
  }
13415
13767
  function markResearchQueryFailed(db, runId, queryId, error) {
13416
13768
  const message = error instanceof Error ? error.message : String(error);
13417
- const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and16(eq22(researchRunQueries.id, queryId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
13769
+ const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and17(eq23(researchRunQueries.id, queryId), inArray8(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
13418
13770
  if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
13419
13771
  }
13420
13772
  function markUnfinishedResearchQueriesFailed(db, runId, error) {
13421
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and16(eq22(researchRunQueries.researchRunId, runId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
13773
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and17(eq23(researchRunQueries.researchRunId, runId), inArray8(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
13422
13774
  }
13423
13775
  function finalizeResearchRun(db, runId, fatalError) {
13424
- const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq22(researchRunQueries.researchRunId, runId)).all();
13776
+ const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq23(researchRunQueries.researchRunId, runId)).all();
13425
13777
  const completed = rows.filter((row) => row.status === ResearchQueryStatuses.completed).length;
13426
13778
  const failed = rows.filter((row) => row.status === ResearchQueryStatuses.failed).length;
13427
13779
  db.update(researchRuns).set({
@@ -13430,7 +13782,7 @@ function finalizeResearchRun(db, runId, fatalError) {
13430
13782
  failedQueries: failed,
13431
13783
  error: fatalError ?? (failed === rows.length ? "Every research query failed." : null),
13432
13784
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
13433
- }).where(eq22(researchRuns.id, runId)).run();
13785
+ }).where(eq23(researchRuns.id, runId)).run();
13434
13786
  }
13435
13787
 
13436
13788
  // src/server.ts
@@ -13468,6 +13820,13 @@ function resolveDashboardShowResourceLinks(env, config) {
13468
13820
  function resolveDashboardShowUpdateNotification(env, config) {
13469
13821
  return parseBooleanEnv(env.CANONRY_DASHBOARD_SHOW_UPDATE_NOTIFICATION) ?? config.dashboard?.showUpdateNotification ?? true;
13470
13822
  }
13823
+ function parseDashboardOnboardingMode(value) {
13824
+ const normalized = value?.trim().toLowerCase();
13825
+ return normalized === "legacy" || normalized === "platform" || normalized === "auto" ? normalized : void 0;
13826
+ }
13827
+ function resolveDashboardOnboardingMode(env, config) {
13828
+ return parseDashboardOnboardingMode(env.CANONRY_ONBOARDING_MODE) ?? config.dashboard?.onboardingMode;
13829
+ }
13471
13830
  var API_ADAPTERS = [
13472
13831
  geminiAdapter,
13473
13832
  openaiAdapter,
@@ -13745,7 +14104,7 @@ async function createServer(opts) {
13745
14104
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
13746
14105
  async (ctx) => {
13747
14106
  if (!sessionRegistry) return;
13748
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq23(projects.id, ctx.projectId)).get();
14107
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq24(projects.id, ctx.projectId)).get();
13749
14108
  if (!project) return;
13750
14109
  let content;
13751
14110
  if (ctx.kind === RunKinds["aeo-discover-probe"]) {
@@ -14095,7 +14454,7 @@ async function createServer(opts) {
14095
14454
  void (async () => {
14096
14455
  try {
14097
14456
  const report = await aeroClient.runDoctor({ project: projectName });
14098
- const project = opts.db.select().from(projects).where(eq23(projects.name, projectName)).get();
14457
+ const project = opts.db.select().from(projects).where(eq24(projects.name, projectName)).get();
14099
14458
  if (!project) {
14100
14459
  app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
14101
14460
  return;
@@ -14123,9 +14482,9 @@ async function createServer(opts) {
14123
14482
  });
14124
14483
  if (!probed) return;
14125
14484
  const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
14126
- and17(
14127
- eq23(ccReleaseSyncs.release, probed.release),
14128
- eq23(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
14485
+ and18(
14486
+ eq24(ccReleaseSyncs.release, probed.release),
14487
+ eq24(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
14129
14488
  )
14130
14489
  ).limit(1).get();
14131
14490
  if (alreadySynced) {
@@ -14365,6 +14724,7 @@ async function createServer(opts) {
14365
14724
  const dashboardRequirePassword = resolveDashboardRequirePassword(process.env, opts.config);
14366
14725
  const dashboardShowResourceLinks = resolveDashboardShowResourceLinks(process.env, opts.config);
14367
14726
  const dashboardShowUpdateNotification = resolveDashboardShowUpdateNotification(process.env, opts.config);
14727
+ const dashboardOnboardingMode = resolveDashboardOnboardingMode(process.env, opts.config);
14368
14728
  app.log.info(
14369
14729
  { dashboardRequirePassword },
14370
14730
  "Dashboard password gate resolved"
@@ -14373,7 +14733,7 @@ async function createServer(opts) {
14373
14733
  const googlePublicUrl = resolveGooglePublicUrl(opts.config, basePath);
14374
14734
  if (opts.config.apiKey) {
14375
14735
  const keyHash = hashApiKey(opts.config.apiKey);
14376
- const existing = opts.db.select().from(apiKeys).where(eq23(apiKeys.keyHash, keyHash)).get();
14736
+ const existing = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, keyHash)).get();
14377
14737
  if (!existing) {
14378
14738
  const prefix = opts.config.apiKey.slice(0, 12);
14379
14739
  opts.db.insert(apiKeys).values({
@@ -14425,7 +14785,7 @@ async function createServer(opts) {
14425
14785
  };
14426
14786
  const getDefaultApiKey = () => {
14427
14787
  if (!opts.config.apiKey) return void 0;
14428
- return opts.db.select().from(apiKeys).where(eq23(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
14788
+ return opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
14429
14789
  };
14430
14790
  const createPasswordSession = (reply) => {
14431
14791
  const key = getDefaultApiKey();
@@ -14449,7 +14809,7 @@ async function createServer(opts) {
14449
14809
  if (!header) return false;
14450
14810
  const parts = header.split(" ");
14451
14811
  if (parts.length !== 2 || parts[0] !== "Bearer") return false;
14452
- const key = opts.db.select().from(apiKeys).where(eq23(apiKeys.keyHash, hashApiKey(parts[1]))).get();
14812
+ const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(parts[1]))).get();
14453
14813
  return Boolean(key && !key.revokedAt);
14454
14814
  };
14455
14815
  const namedAccountsInUse = () => anyUsersExist(opts.db);
@@ -14539,12 +14899,12 @@ async function createServer(opts) {
14539
14899
  return reply.send({ authenticated: true });
14540
14900
  }
14541
14901
  if (apiKey) {
14542
- const key = opts.db.select().from(apiKeys).where(eq23(apiKeys.keyHash, hashApiKey(apiKey))).get();
14902
+ const key = opts.db.select().from(apiKeys).where(eq24(apiKeys.keyHash, hashApiKey(apiKey))).get();
14543
14903
  if (!key || key.revokedAt) {
14544
14904
  const err2 = authInvalid();
14545
14905
  return reply.status(err2.statusCode).send(err2.toJSON());
14546
14906
  }
14547
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(apiKeys.id, key.id)).run();
14907
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(apiKeys.id, key.id)).run();
14548
14908
  const sessionId = createSession(key.id);
14549
14909
  reply.header(
14550
14910
  "set-cookie",
@@ -14886,7 +15246,7 @@ async function createServer(opts) {
14886
15246
  ...inspectOpts,
14887
15247
  config: opts.config
14888
15248
  }).then(() => {
14889
- const finished = opts.db.select({ status: runs.status }).from(runs).where(eq23(runs.id, runId)).get();
15249
+ const finished = opts.db.select({ status: runs.status }).from(runs).where(eq24(runs.id, runId)).get();
14890
15250
  if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
14891
15251
  return maybeRefreshGscCoverage(
14892
15252
  opts.db,
@@ -15163,6 +15523,9 @@ async function createServer(opts) {
15163
15523
  const clientConfig = {};
15164
15524
  if (basePath) clientConfig.basePath = basePath;
15165
15525
  const dashboardConfig = {};
15526
+ if (dashboardOnboardingMode) {
15527
+ dashboardConfig.onboardingMode = dashboardOnboardingMode;
15528
+ }
15166
15529
  if (!dashboardShowResourceLinks) {
15167
15530
  dashboardConfig.showResourceLinks = false;
15168
15531
  }
@@ -15264,7 +15627,7 @@ async function createServer(opts) {
15264
15627
  }
15265
15628
  checkLatestVersionForServer();
15266
15629
  scheduler.start();
15267
- for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq23(researchRuns.status, ResearchRunStatuses.queued)).all()) {
15630
+ for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq24(researchRuns.status, ResearchRunStatuses.queued)).all()) {
15268
15631
  dispatchResearchRun(run.id, run.projectId);
15269
15632
  }
15270
15633
  app.addHook("onClose", async () => {