@canonry/canonry 4.154.0 → 4.155.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 (41) hide show
  1. package/README.md +2 -2
  2. package/assets/agent-workspace/AGENTS.md +2 -2
  3. package/assets/agent-workspace/skills/aero/references/orchestration.md +3 -3
  4. package/assets/agent-workspace/skills/canonry/SKILL.md +6 -21
  5. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +14 -6
  6. package/assets/assets/{AuditHistoryPanel-WWT1n8jp.js → AuditHistoryPanel-BlbMBxOY.js} +1 -1
  7. package/assets/assets/BacklinksPage-DA9EaV9q.js +1 -0
  8. package/assets/assets/HistoryPage-BTrIHjMQ.js +1 -0
  9. package/assets/assets/MeasurementPropertyPage-CN6o5xFg.js +1 -0
  10. package/assets/assets/{ProjectPage-BR5ECwUP.js → ProjectPage-CsDbgp_0.js} +9 -9
  11. package/assets/assets/{RunRow-OE5tiuih.js → RunRow-ch_OlCPs.js} +1 -1
  12. package/assets/assets/RunsPage-T89Y_yS8.js +1 -0
  13. package/assets/assets/SettingsPage-DnmEq3F9.js +1 -0
  14. package/assets/assets/TrafficPage-CW1UhaJt.js +1 -0
  15. package/assets/assets/TrafficSourceDetailPage-r4Ji3wJm.js +1 -0
  16. package/assets/assets/extract-error-message-a8EErBoZ.js +1 -0
  17. package/assets/assets/index-BKq6k5Of.js +83 -0
  18. package/assets/assets/vendor-lucide-ByU7vBHt.js +1 -0
  19. package/assets/assets/vendor-yaml-GWJz5H2G.js +131 -0
  20. package/assets/index.html +3 -1
  21. package/dist/{chunk-ANSQ6JNO.js → chunk-OSHRDV36.js} +317 -4
  22. package/dist/{chunk-NNW7TJUZ.js → chunk-VIKPFI6F.js} +200 -5
  23. package/dist/{chunk-IFZS2TXV.js → chunk-VOL2BSM5.js} +1133 -104
  24. package/dist/{chunk-X5I7CAXH.js → chunk-YSESZNVT.js} +609 -165
  25. package/dist/cli.js +342 -7
  26. package/dist/index.js +4 -4
  27. package/dist/{intelligence-service-B5AETQ5N.js → intelligence-service-G4NV3X36.js} +2 -2
  28. package/dist/mcp.js +2 -2
  29. package/package.json +9 -9
  30. package/assets/assets/BacklinksPage-DIjUnsVL.js +0 -1
  31. package/assets/assets/HistoryPage-CrIBhGuW.js +0 -1
  32. package/assets/assets/MeasurementPropertyPage-uN4BWigS.js +0 -1
  33. package/assets/assets/RunsPage-C6xBmjps.js +0 -1
  34. package/assets/assets/SettingsPage-CUjjr8U0.js +0 -1
  35. package/assets/assets/TrafficPage-DrEfsHiE.js +0 -1
  36. package/assets/assets/TrafficSourceDetailPage-B3DX5BEk.js +0 -1
  37. package/assets/assets/arrow-left-CLmqvCqU.js +0 -1
  38. package/assets/assets/extract-error-message-BEBd1a5v.js +0 -1
  39. package/assets/assets/index-CsM-Xxia.js +0 -213
  40. package/assets/assets/refresh-cw-DYSS2Wd-.js +0 -1
  41. package/assets/assets/trash-2-VoiF_ckU.js +0 -1
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-ANSQ6JNO.js";
13
+ } from "./chunk-OSHRDV36.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -124,9 +124,16 @@ import {
124
124
  schedules,
125
125
  siteAuditPages,
126
126
  siteAuditSnapshots,
127
+ siteCrawlAttempts,
128
+ siteCrawlEdges,
129
+ siteCrawlEventReceipts,
130
+ siteCrawlFindings,
131
+ siteCrawlPages,
132
+ siteCrawlRunRequests,
133
+ siteCrawlSnapshots,
127
134
  toAlertView,
128
135
  usageCounters
129
- } from "./chunk-IFZS2TXV.js";
136
+ } from "./chunk-VOL2BSM5.js";
130
137
  import {
131
138
  AGENT_MEMORY_VALUE_MAX_BYTES,
132
139
  AGENT_PROVIDER_IDS,
@@ -151,6 +158,10 @@ import {
151
158
  RunKinds,
152
159
  RunStatuses,
153
160
  RunTriggers,
161
+ SITE_AUDIT_DEFAULT_EDGE_LIMIT,
162
+ SITE_AUDIT_DEFAULT_PAGE_LIMIT,
163
+ SITE_AUDIT_MAX_EDGE_LIMIT,
164
+ SITE_AUDIT_MAX_PAGE_LIMIT,
154
165
  SKILL_MANIFEST_FILENAME,
155
166
  SchedulableRunKinds,
156
167
  SkillsClients,
@@ -200,6 +211,7 @@ import {
200
211
  normalizeIdTokens,
201
212
  normalizeMeasurementExecutionQueryText,
202
213
  normalizeServedModel,
214
+ normalizeSiteAuditRunRequest,
203
215
  normalizeUrlPath,
204
216
  notFound,
205
217
  parseMeasurementRunManifestV1,
@@ -210,6 +222,7 @@ import {
210
222
  resolveSnapshotRequestQueries,
211
223
  serializeForInlineScript,
212
224
  serializeRunError,
225
+ siteAuditRequestIdentity,
213
226
  skillsClientSchema,
214
227
  splitList,
215
228
  startOfDayHourInTimeZone,
@@ -218,7 +231,7 @@ import {
218
231
  validationError,
219
232
  winnabilityClassLabel,
220
233
  withRetry
221
- } from "./chunk-NNW7TJUZ.js";
234
+ } from "./chunk-VIKPFI6F.js";
222
235
 
223
236
  // src/telemetry.ts
224
237
  import crypto from "crypto";
@@ -553,7 +566,7 @@ import crypto24 from "crypto";
553
566
  import fs8 from "fs";
554
567
  import path9 from "path";
555
568
  import { fileURLToPath as fileURLToPath3 } from "url";
556
- import { and as and15, eq as eq22 } from "drizzle-orm";
569
+ import { and as and16, eq as eq22 } from "drizzle-orm";
557
570
  import Fastify from "fastify";
558
571
  import os5 from "os";
559
572
 
@@ -3547,11 +3560,23 @@ var JobRunner = class {
3547
3560
  this.registry = registry;
3548
3561
  }
3549
3562
  recoverStaleRuns() {
3550
- const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray(runs.status, ["running", "queued"])).all();
3563
+ const stale = this.db.select({ id: runs.id, projectId: runs.projectId, kind: runs.kind, status: runs.status }).from(runs).where(inArray(runs.status, ["running", "queued"])).all();
3551
3564
  if (stale.length === 0) return;
3552
3565
  const now = (/* @__PURE__ */ new Date()).toISOString();
3553
3566
  for (const run of stale) {
3554
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq2(runs.id, run.id)).run();
3567
+ const recovered = this.db.transaction((tx) => {
3568
+ const claim = tx.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(and2(eq2(runs.id, run.id), eq2(runs.status, run.status))).run();
3569
+ if (claim.changes === 0) return false;
3570
+ if (run.kind === RunKinds["site-audit"]) {
3571
+ tx.update(siteCrawlAttempts).set({ state: "failed", finishedAt: now, updatedAt: now, error: "Server restarted while run was in progress" }).where(and2(
3572
+ eq2(siteCrawlAttempts.projectId, run.projectId),
3573
+ eq2(siteCrawlAttempts.runId, run.id),
3574
+ inArray(siteCrawlAttempts.state, ["queued", "running"])
3575
+ )).run();
3576
+ }
3577
+ return true;
3578
+ });
3579
+ if (!recovered) continue;
3555
3580
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
3556
3581
  }
3557
3582
  }
@@ -4250,8 +4275,19 @@ function writeCoverageSnapshot(db, projectId, runId, opts = {}) {
4250
4275
  const since = isoDaysAgo(windowDays);
4251
4276
  const pageRows = db.select({ page: gscSearchData.page, impressions: gscSearchData.impressions }).from(gscSearchData).where(and3(eq3(gscSearchData.projectId, projectId), gte(gscSearchData.date, since))).all();
4252
4277
  const allInspections = db.select().from(gscUrlInspections).where(eq3(gscUrlInspections.projectId, projectId)).all();
4278
+ const latestCrawl = db.select({ runId: siteCrawlSnapshots.runId, attemptId: siteCrawlSnapshots.attemptId }).from(siteCrawlSnapshots).innerJoin(runs, eq3(siteCrawlSnapshots.runId, runs.id)).where(and3(
4279
+ eq3(siteCrawlSnapshots.projectId, projectId),
4280
+ eq3(siteCrawlSnapshots.complete, true),
4281
+ eq3(runs.status, "completed")
4282
+ )).orderBy(desc(siteCrawlSnapshots.createdAt)).limit(1).get();
4283
+ const hasCrawlHistory = Boolean(db.select({ id: siteCrawlSnapshots.id }).from(siteCrawlSnapshots).where(eq3(siteCrawlSnapshots.projectId, projectId)).limit(1).get());
4253
4284
  const latestAudit = db.select({ runId: siteAuditSnapshots.runId }).from(siteAuditSnapshots).where(eq3(siteAuditSnapshots.projectId, projectId)).orderBy(desc(siteAuditSnapshots.createdAt)).limit(1).get();
4254
- const sitemapUrls = latestAudit ? db.select({ url: siteAuditPages.url }).from(siteAuditPages).where(eq3(siteAuditPages.runId, latestAudit.runId)).all() : [];
4285
+ const sitemapUrls = latestCrawl?.attemptId ? db.select({ url: siteCrawlPages.url }).from(siteCrawlPages).where(and3(
4286
+ eq3(siteCrawlPages.projectId, projectId),
4287
+ eq3(siteCrawlPages.runId, latestCrawl.runId),
4288
+ eq3(siteCrawlPages.attemptId, latestCrawl.attemptId),
4289
+ eq3(siteCrawlPages.inventoryEligible, true)
4290
+ )).all() : !hasCrawlHistory && latestAudit ? db.select({ url: siteAuditPages.url }).from(siteAuditPages).where(eq3(siteAuditPages.runId, latestAudit.runId)).all() : [];
4255
4291
  const impressionsByKey = /* @__PURE__ */ new Map();
4256
4292
  for (const row of pageRows) {
4257
4293
  const key = pageKey(row.page);
@@ -6895,164 +6931,549 @@ function buildDiscoveryInsightTitle(input) {
6895
6931
 
6896
6932
  // src/execute-site-audit.ts
6897
6933
  import crypto16 from "crypto";
6898
- import { eq as eq13 } from "drizzle-orm";
6899
- import { runSitemapAudit } from "@ainyc/aeo-audit";
6934
+ import { and as and10, eq as eq13, sql as sql5 } from "drizzle-orm";
6935
+ import { runSiteCrawl } from "@canonry/aeo-audit";
6900
6936
  var log12 = createLogger("SiteAudit");
6901
- var SITE_AUDIT_DEFAULT_PAGE_LIMIT = 500;
6902
- var SITE_AUDIT_MAX_PAGE_LIMIT = 2e3;
6937
+ var SiteAuditCancelledError = class extends Error {
6938
+ name = "SiteAuditCancelledError";
6939
+ };
6903
6940
  function toHomepageUrl(canonicalDomain) {
6904
6941
  const trimmed = canonicalDomain.trim().replace(/\/+$/, "");
6905
6942
  return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
6906
6943
  }
6907
6944
  async function assertSiteAuditUrlAllowed(rawUrl, field) {
6908
6945
  const check = await resolveWebhookTarget(rawUrl);
6909
- if (!check.ok) {
6910
- throw new Error(`${field} ${check.message.replace(/^"url" /, "")}`);
6911
- }
6946
+ if (!check.ok) throw new Error(`${field} ${check.message.replace(/^"url" /, "")}`);
6912
6947
  }
6913
6948
  function clampSiteAuditLimit(limit) {
6914
6949
  if (limit == null || !Number.isFinite(limit)) return SITE_AUDIT_DEFAULT_PAGE_LIMIT;
6915
6950
  return Math.max(1, Math.min(SITE_AUDIT_MAX_PAGE_LIMIT, Math.floor(limit)));
6916
6951
  }
6952
+ function clampSiteAuditEdgeLimit(limit) {
6953
+ if (limit == null || !Number.isFinite(limit)) return SITE_AUDIT_DEFAULT_EDGE_LIMIT;
6954
+ return Math.max(1, Math.min(SITE_AUDIT_MAX_EDGE_LIMIT, Math.floor(limit)));
6955
+ }
6917
6956
  function toPageFactor(factor) {
6918
- return {
6919
- id: factor.id,
6920
- name: factor.name,
6921
- weight: factor.weight,
6922
- score: factor.score
6923
- };
6957
+ return { id: factor.id, name: factor.name, weight: factor.weight, score: factor.score };
6924
6958
  }
6925
6959
  function computeFactorAverages(pages) {
6926
6960
  const byId = /* @__PURE__ */ new Map();
6927
6961
  for (const page of pages) {
6928
- if (page.status !== "success" || !page.factors) continue;
6929
- for (const factor of page.factors) {
6930
- let entry = byId.get(factor.id);
6931
- if (!entry) {
6932
- entry = { name: factor.name, weight: factor.weight, scores: [], pass: 0, partial: 0, fail: 0 };
6933
- byId.set(factor.id, entry);
6934
- }
6935
- entry.scores.push(factor.score);
6962
+ if (!page.audit) continue;
6963
+ for (const factor of page.audit.factors) {
6964
+ const current = byId.get(factor.id) ?? { name: factor.name, weight: factor.weight, scores: [], pass: 0, partial: 0, fail: 0 };
6965
+ byId.set(factor.id, current);
6966
+ current.scores.push(factor.score);
6936
6967
  const status = factorStatusFromScore(factor.score);
6937
- if (status === "pass") entry.pass++;
6938
- else if (status === "partial") entry.partial++;
6939
- else entry.fail++;
6968
+ if (status === "pass") current.pass++;
6969
+ else if (status === "partial") current.partial++;
6970
+ else current.fail++;
6940
6971
  }
6941
6972
  }
6942
- const summaries = [];
6943
- for (const [id, entry] of byId) {
6944
- const avgScore = entry.scores.length ? Math.round(entry.scores.reduce((sum, score) => sum + score, 0) / entry.scores.length) : 0;
6945
- summaries.push({
6973
+ return [...byId.entries()].map(([id, value]) => {
6974
+ const avgScore = Math.round(value.scores.reduce((sum, score) => sum + score, 0) / value.scores.length);
6975
+ return {
6946
6976
  id,
6947
- name: entry.name,
6948
- weight: entry.weight,
6977
+ name: value.name,
6978
+ weight: value.weight,
6949
6979
  avgScore,
6950
6980
  status: factorStatusFromScore(avgScore),
6951
- pagesPassing: entry.pass,
6952
- pagesPartial: entry.partial,
6953
- pagesFailing: entry.fail
6954
- });
6981
+ pagesPassing: value.pass,
6982
+ pagesPartial: value.partial,
6983
+ pagesFailing: value.fail
6984
+ };
6985
+ }).sort((a, b) => b.weight - a.weight || a.name.localeCompare(b.name));
6986
+ }
6987
+ function pagePath(page) {
6988
+ if (page.path) return page.path;
6989
+ try {
6990
+ return new URL(page.finalUrl ?? page.requestedUrl).pathname || "/";
6991
+ } catch {
6992
+ return "/";
6955
6993
  }
6956
- summaries.sort((a, b) => b.weight - a.weight || a.name.localeCompare(b.name));
6957
- return summaries;
6958
6994
  }
6959
- async function executeSiteAudit(db, runId, projectId, opts = {}) {
6960
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6961
- db.update(runs).set({ status: "running", startedAt }).where(eq13(runs.id, runId)).run();
6995
+ function parentPath(page) {
6996
+ const path10 = pagePath(page);
6997
+ if (path10 === "/") return "/";
6998
+ const trimmed = path10.endsWith("/") ? path10.slice(0, -1) : path10;
6999
+ const slash = trimmed.lastIndexOf("/");
7000
+ return slash <= 0 ? "/" : trimmed.slice(0, slash) || "/";
7001
+ }
7002
+ function pageDiscoverySource(page) {
7003
+ if (page.provenance.root) return "root";
7004
+ if (page.provenance.sitemapSources.length > 0) return "sitemap";
7005
+ return "link";
7006
+ }
7007
+ function pageAuditState(page) {
7008
+ if (page.audit) return "success";
7009
+ if (page.state === "fetch-error") return "error";
7010
+ return "not-applicable";
7011
+ }
7012
+ function isInventoryEligible(page) {
7013
+ return page.state === "html" && page.indexability.state === "indexable";
7014
+ }
7015
+ function isCancelled(error, signal) {
7016
+ if (error instanceof SiteAuditCancelledError) return true;
7017
+ if (signal?.aborted && signal.reason !== void 0 && error === signal.reason) return true;
7018
+ return error instanceof DOMException && error.name === "AbortError";
7019
+ }
7020
+ function isLegacyScorecardPage(page) {
7021
+ return page.audit != null || page.state === "fetch-error";
7022
+ }
7023
+ function emptyCompleteCrawlError(rootUrl, finalRootUrl, pagesObserved) {
7024
+ if (!finalRootUrl) return new Error(`Site audit could not successfully audit any of ${pagesObserved} observed page(s).`);
6962
7025
  try {
6963
- const project = db.select().from(projects).where(eq13(projects.id, projectId)).get();
6964
- if (!project) {
6965
- throw new Error(`Project not found: ${projectId}`);
7026
+ const rootHost = new URL(rootUrl).hostname;
7027
+ const finalRootHost = new URL(finalRootUrl).hostname;
7028
+ if (rootHost !== finalRootHost) {
7029
+ return new Error(
7030
+ `Site audit could not successfully audit any of ${pagesObserved} observed page(s): root URL redirected off-host from ${rootHost} to ${finalRootHost} (${finalRootUrl}).`
7031
+ );
6966
7032
  }
6967
- const homepageUrl = toHomepageUrl(project.canonicalDomain);
6968
- const limit = clampSiteAuditLimit(opts.limit);
6969
- log12.info("start", { runId, projectId, homepageUrl, sitemapUrl: opts.sitemapUrl ?? null, limit });
6970
- await assertSiteAuditUrlAllowed(homepageUrl, "canonicalDomain");
6971
- if (opts.sitemapUrl) await assertSiteAuditUrlAllowed(opts.sitemapUrl, "sitemapUrl");
6972
- const report = await runSitemapAudit(homepageUrl, { sitemapUrl: opts.sitemapUrl, limit });
6973
- const successCount = report.pages.filter((page) => page.status === "success").length;
6974
- const pagesErrored = report.pages.filter((page) => page.status === "error").length;
6975
- const auditable = report.pagesDiscovered - report.pagesSkipped;
6976
- if (auditable > report.pagesAudited) {
6977
- log12.info("truncated", {
6978
- runId,
6979
- projectId,
6980
- auditable,
6981
- audited: report.pagesAudited,
6982
- dropped: auditable - report.pagesAudited,
6983
- limit
6984
- });
7033
+ } catch {
7034
+ }
7035
+ return new Error(`Site audit could not successfully audit any of ${pagesObserved} observed page(s).`);
7036
+ }
7037
+ function toLegacyIssue(summary, totalPages) {
7038
+ const affected = summary.pagesPartial + summary.pagesFailing;
7039
+ if (affected === 0 || summary.avgScore >= 70) return null;
7040
+ return {
7041
+ factorId: summary.id,
7042
+ factorName: summary.name,
7043
+ avgScore: summary.avgScore,
7044
+ affectedPages: affected,
7045
+ totalPages,
7046
+ affectedPct: totalPages > 0 ? Math.round(affected / totalPages * 100) : 0,
7047
+ topRecommendations: []
7048
+ };
7049
+ }
7050
+ function observedErrorCount(pages) {
7051
+ let total = 0;
7052
+ for (const page of pages) if (page.state === "fetch-error") total++;
7053
+ return total;
7054
+ }
7055
+ function deadLinkCheckedCount(edges, pages) {
7056
+ const attemptedUrls = /* @__PURE__ */ new Set();
7057
+ for (const page of pages) {
7058
+ if (page.state === "discovered" || page.state === "robots-blocked") continue;
7059
+ attemptedUrls.add(page.requestedUrl);
7060
+ if (page.finalUrl) attemptedUrls.add(page.finalUrl);
7061
+ }
7062
+ return new Set([...edges].filter((edge) => edge.type === "anchor" && edge.classification === "internal" && attemptedUrls.has(edge.to)).map((edge) => edge.to)).size;
7063
+ }
7064
+ async function executeSiteAudit(db, runId, projectId, opts = {}) {
7065
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
7066
+ const claim = db.update(runs).set({ status: "running", startedAt }).where(and10(eq13(runs.id, runId), eq13(runs.projectId, projectId), eq13(runs.status, "queued"))).run();
7067
+ if (claim.changes === 0) return;
7068
+ const attemptNumber = (db.select({ value: sql5`coalesce(max(${siteCrawlAttempts.attemptNumber}), 0)` }).from(siteCrawlAttempts).where(eq13(siteCrawlAttempts.runId, runId)).get()?.value ?? 0) + 1;
7069
+ const attemptId = crypto16.randomUUID();
7070
+ db.insert(siteCrawlAttempts).values({
7071
+ id: attemptId,
7072
+ projectId,
7073
+ runId,
7074
+ attemptNumber,
7075
+ state: "running",
7076
+ startedAt,
7077
+ createdAt: startedAt,
7078
+ updatedAt: startedAt
7079
+ }).run();
7080
+ const observedPages = /* @__PURE__ */ new Map();
7081
+ const observedEdges = /* @__PURE__ */ new Map();
7082
+ const nodeKeyByUrl = /* @__PURE__ */ new Map();
7083
+ const nodePriorityByUrl = /* @__PURE__ */ new Map();
7084
+ const registerNode = (url, nodeKey, priority, changedUrls) => {
7085
+ const currentPriority = nodePriorityByUrl.get(url);
7086
+ const currentNodeKey = nodeKeyByUrl.get(url);
7087
+ if (currentPriority == null || priority > currentPriority || priority === currentPriority && currentNodeKey != null && nodeKey.localeCompare(currentNodeKey) < 0) {
7088
+ nodePriorityByUrl.set(url, priority);
7089
+ nodeKeyByUrl.set(url, nodeKey);
7090
+ changedUrls.add(url);
6985
7091
  }
6986
- if (successCount === 0) {
6987
- throw new Error(
6988
- `Site audit could not successfully audit any of ${report.pagesAudited} page(s) from ${report.sitemapUrl}.`
6989
- );
7092
+ };
7093
+ const registerPageNode = (page, changedUrls) => {
7094
+ observedPages.set(page.key, page);
7095
+ registerNode(page.requestedUrl, page.key, 2, changedUrls);
7096
+ if (page.finalUrl && page.finalUrl !== page.requestedUrl) registerNode(page.finalUrl, page.key, 1, changedUrls);
7097
+ };
7098
+ const bindReferences = (tx, changedUrls, now) => {
7099
+ for (const url of [...new Set(changedUrls)].sort()) {
7100
+ const nodeKey = nodeKeyByUrl.get(url);
7101
+ if (!nodeKey) continue;
7102
+ const scope = [
7103
+ eq13(siteCrawlEdges.projectId, projectId),
7104
+ eq13(siteCrawlEdges.runId, runId),
7105
+ eq13(siteCrawlEdges.attemptId, attemptId)
7106
+ ];
7107
+ tx.update(siteCrawlEdges).set({ sourceNodeKey: nodeKey, updatedAt: now }).where(and10(
7108
+ ...scope,
7109
+ eq13(siteCrawlEdges.sourceUrl, url)
7110
+ )).run();
7111
+ tx.update(siteCrawlEdges).set({ targetNodeKey: nodeKey, updatedAt: now }).where(and10(
7112
+ ...scope,
7113
+ eq13(siteCrawlEdges.targetUrl, url)
7114
+ )).run();
7115
+ tx.update(siteCrawlPages).set({ canonicalNodeKey: nodeKey, updatedAt: now }).where(and10(
7116
+ eq13(siteCrawlPages.projectId, projectId),
7117
+ eq13(siteCrawlPages.runId, runId),
7118
+ eq13(siteCrawlPages.attemptId, attemptId),
7119
+ eq13(siteCrawlPages.canonicalUrl, url)
7120
+ )).run();
6990
7121
  }
6991
- const factorAverages = computeFactorAverages(report.pages);
6992
- const status = pagesErrored === 0 ? "completed" : "partial";
6993
- const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
7122
+ };
7123
+ const persistPage = (tx, page, now) => {
7124
+ const audit = page.audit;
7125
+ tx.insert(siteCrawlPages).values({
7126
+ id: crypto16.randomUUID(),
7127
+ projectId,
7128
+ runId,
7129
+ attemptId,
7130
+ nodeKey: page.key,
7131
+ url: page.requestedUrl,
7132
+ path: pagePath(page),
7133
+ parentPath: parentPath(page),
7134
+ discoverySource: pageDiscoverySource(page),
7135
+ discoveryProvenance: [{
7136
+ discoveredFrom: page.provenance.discoveredFrom,
7137
+ sitemapSources: page.provenance.sitemapSources,
7138
+ root: page.provenance.root
7139
+ }],
7140
+ sitemapMetadata: { sources: page.provenance.sitemapSources },
7141
+ fetchState: page.state,
7142
+ fetchedAt: page.state === "discovered" || page.state === "robots-blocked" ? null : now,
7143
+ httpStatus: page.statusCode,
7144
+ contentType: page.contentType,
7145
+ finalUrl: page.finalUrl,
7146
+ redirectChain: page.redirectChain.map((hop) => hop.to),
7147
+ directives: { metaRobots: page.metaRobots, xRobots: page.xRobots },
7148
+ canonicalUrl: page.canonicalUrl,
7149
+ canonicalNodeKey: page.canonicalUrl ? nodeKeyByUrl.get(page.canonicalUrl) ?? null : null,
7150
+ indexabilityState: page.indexability.state,
7151
+ indexabilityReasons: page.indexability.reasons,
7152
+ auditState: pageAuditState(page),
7153
+ auditScore: audit?.overallScore ?? null,
7154
+ auditFields: audit ? { factors: audit.factors.map(toPageFactor) } : {},
7155
+ inventoryEligible: isInventoryEligible(page),
7156
+ depth: page.depth,
7157
+ inboundUniqueEdges: page.metrics.inbound.uniqueEdges,
7158
+ outboundUniqueEdges: page.metrics.outbound.uniqueEdges,
7159
+ inboundOccurrences: page.metrics.inbound.totalOccurrences,
7160
+ outboundOccurrences: page.metrics.outbound.totalOccurrences,
7161
+ linkScoreRaw: page.metrics.linkScoreRaw,
7162
+ linkScoreNormalized: page.metrics.linkScore,
7163
+ createdAt: now,
7164
+ updatedAt: now
7165
+ }).onConflictDoUpdate({
7166
+ target: [siteCrawlPages.projectId, siteCrawlPages.runId, siteCrawlPages.attemptId, siteCrawlPages.nodeKey],
7167
+ set: {
7168
+ url: page.requestedUrl,
7169
+ path: pagePath(page),
7170
+ parentPath: parentPath(page),
7171
+ discoverySource: pageDiscoverySource(page),
7172
+ discoveryProvenance: [{ discoveredFrom: page.provenance.discoveredFrom, sitemapSources: page.provenance.sitemapSources, root: page.provenance.root }],
7173
+ sitemapMetadata: { sources: page.provenance.sitemapSources },
7174
+ fetchState: page.state,
7175
+ fetchedAt: page.state === "discovered" || page.state === "robots-blocked" ? null : sql5`coalesce(${siteCrawlPages.fetchedAt}, ${now})`,
7176
+ httpStatus: page.statusCode,
7177
+ contentType: page.contentType,
7178
+ finalUrl: page.finalUrl,
7179
+ redirectChain: page.redirectChain.map((hop) => hop.to),
7180
+ directives: { metaRobots: page.metaRobots, xRobots: page.xRobots },
7181
+ canonicalUrl: page.canonicalUrl,
7182
+ canonicalNodeKey: page.canonicalUrl ? nodeKeyByUrl.get(page.canonicalUrl) ?? null : null,
7183
+ indexabilityState: page.indexability.state,
7184
+ indexabilityReasons: page.indexability.reasons,
7185
+ auditState: pageAuditState(page),
7186
+ auditScore: audit?.overallScore ?? null,
7187
+ auditFields: audit ? { factors: audit.factors.map(toPageFactor) } : {},
7188
+ inventoryEligible: isInventoryEligible(page),
7189
+ depth: page.depth,
7190
+ inboundUniqueEdges: page.metrics.inbound.uniqueEdges,
7191
+ outboundUniqueEdges: page.metrics.outbound.uniqueEdges,
7192
+ inboundOccurrences: page.metrics.inbound.totalOccurrences,
7193
+ outboundOccurrences: page.metrics.outbound.totalOccurrences,
7194
+ linkScoreRaw: page.metrics.linkScoreRaw,
7195
+ linkScoreNormalized: page.metrics.linkScore,
7196
+ updatedAt: now
7197
+ }
7198
+ }).run();
7199
+ };
7200
+ const persistPages = (tx, pages, now) => {
7201
+ const changedUrls = /* @__PURE__ */ new Set();
7202
+ for (const page of pages) registerPageNode(page, changedUrls);
7203
+ for (const page of pages) persistPage(tx, page, now);
7204
+ bindReferences(tx, changedUrls, now);
7205
+ };
7206
+ const persistEdge = (tx, edge, now) => {
7207
+ observedEdges.set(edge.key, edge);
7208
+ tx.insert(siteCrawlEdges).values({
7209
+ id: crypto16.randomUUID(),
7210
+ projectId,
7211
+ runId,
7212
+ attemptId,
7213
+ edgeKey: edge.key,
7214
+ sourceNodeKey: nodeKeyByUrl.get(edge.from) ?? edge.from,
7215
+ sourceUrl: edge.from,
7216
+ targetNodeKey: nodeKeyByUrl.get(edge.to) ?? null,
7217
+ targetUrl: edge.to,
7218
+ relation: edge.type,
7219
+ internal: edge.classification === "internal",
7220
+ followable: edge.followableOccurrences > 0,
7221
+ occurrences: edge.totalOccurrences,
7222
+ followableOccurrences: edge.followableOccurrences,
7223
+ nofollowOccurrences: edge.nofollowOccurrences,
7224
+ anchors: edge.anchorSummaries.map((anchor) => anchor.text),
7225
+ createdAt: now,
7226
+ updatedAt: now
7227
+ }).onConflictDoUpdate({
7228
+ target: [siteCrawlEdges.projectId, siteCrawlEdges.runId, siteCrawlEdges.attemptId, siteCrawlEdges.edgeKey],
7229
+ set: {
7230
+ sourceNodeKey: nodeKeyByUrl.get(edge.from) ?? edge.from,
7231
+ sourceUrl: edge.from,
7232
+ targetNodeKey: nodeKeyByUrl.get(edge.to) ?? null,
7233
+ targetUrl: edge.to,
7234
+ relation: edge.type,
7235
+ internal: edge.classification === "internal",
7236
+ followable: edge.followableOccurrences > 0,
7237
+ occurrences: edge.totalOccurrences,
7238
+ followableOccurrences: edge.followableOccurrences,
7239
+ nofollowOccurrences: edge.nofollowOccurrences,
7240
+ anchors: edge.anchorSummaries.map((anchor) => anchor.text),
7241
+ updatedAt: now
7242
+ }
7243
+ }).run();
7244
+ };
7245
+ const persistMetrics = (tx, key, metrics, now) => {
7246
+ tx.update(siteCrawlPages).set({
7247
+ inboundUniqueEdges: metrics.inbound.uniqueEdges,
7248
+ outboundUniqueEdges: metrics.outbound.uniqueEdges,
7249
+ inboundOccurrences: metrics.inbound.totalOccurrences,
7250
+ outboundOccurrences: metrics.outbound.totalOccurrences,
7251
+ depth: metrics.shortestFollowableAnchorDepth,
7252
+ linkScoreRaw: metrics.linkScoreRaw,
7253
+ linkScoreNormalized: metrics.linkScore,
7254
+ updatedAt: now
7255
+ }).where(and10(
7256
+ eq13(siteCrawlPages.projectId, projectId),
7257
+ eq13(siteCrawlPages.runId, runId),
7258
+ eq13(siteCrawlPages.attemptId, attemptId),
7259
+ eq13(siteCrawlPages.nodeKey, key)
7260
+ )).run();
7261
+ };
7262
+ const persistEvent = async (event) => {
7263
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6994
7264
  db.transaction((tx) => {
6995
- tx.insert(siteAuditSnapshots).values({
7265
+ const receipt = tx.select({ checksum: siteCrawlEventReceipts.checksum }).from(siteCrawlEventReceipts).where(and10(
7266
+ eq13(siteCrawlEventReceipts.attemptId, attemptId),
7267
+ eq13(siteCrawlEventReceipts.sequence, event.sequence),
7268
+ eq13(siteCrawlEventReceipts.batchId, event.batchId)
7269
+ )).get();
7270
+ if (receipt) {
7271
+ if (receipt.checksum !== event.checksum) {
7272
+ throw new Error(`Crawl checkpoint checksum mismatch for ${event.batchId} (${event.sequence})`);
7273
+ }
7274
+ return;
7275
+ }
7276
+ tx.insert(siteCrawlEventReceipts).values({
6996
7277
  id: crypto16.randomUUID(),
6997
7278
  projectId,
6998
7279
  runId,
6999
- sitemapUrl: report.sitemapUrl,
7000
- auditedAt: report.auditedAt,
7001
- aggregateScore: report.aggregateScore,
7002
- pagesDiscovered: report.pagesDiscovered,
7003
- pagesAudited: report.pagesAudited,
7004
- pagesSkipped: report.pagesSkipped,
7005
- pagesErrored,
7006
- factorAverages,
7007
- // aeo-audit v3 enriches these (topIssues, avgGrade-free); keep only the
7008
- // fields our DTO exposes so the stored JSON stays lean.
7009
- crossCuttingIssues: report.crossCuttingIssues.map((issue) => ({
7010
- factorId: issue.factorId,
7011
- factorName: issue.factorName,
7012
- avgScore: issue.avgScore,
7013
- affectedPages: issue.affectedPages,
7014
- totalPages: issue.totalPages,
7015
- affectedPct: issue.totalPages > 0 ? Math.round(issue.affectedPages / issue.totalPages * 100) : 0,
7016
- topRecommendations: issue.topRecommendations
7017
- })),
7018
- // v3 prioritizedFixes are structured PrioritizedFix objects; persist the
7019
- // ready-to-display one-line summary to keep the DTO a string list.
7020
- prioritizedFixes: report.prioritizedFixes.map((fix) => fix.summary),
7021
- createdAt: finishedAt
7280
+ attemptId,
7281
+ sequence: event.sequence,
7282
+ batchId: event.batchId,
7283
+ checksum: event.checksum,
7284
+ receipt: { type: event.type },
7285
+ createdAt: now
7022
7286
  }).run();
7023
- for (const page of report.pages) {
7024
- tx.insert(siteAuditPages).values({
7287
+ if (event.type === "pages") persistPages(tx, event.rows, now);
7288
+ if (event.type === "edges") for (const edge of event.rows) persistEdge(tx, edge, now);
7289
+ if (event.type === "metrics") for (const metric of event.rows) persistMetrics(tx, metric.key, metric.metrics, now);
7290
+ const progress = event.type === "progress" ? event.progress : event.type === "summary" ? event.summary : null;
7291
+ if (progress) {
7292
+ tx.update(siteCrawlAttempts).set({
7293
+ lastEventSequence: event.sequence,
7294
+ lastEventChecksum: event.checksum,
7295
+ pagesDiscovered: progress.pagesDiscovered,
7296
+ pagesFetched: progress.pagesFetched,
7297
+ pagesEligible: event.type === "summary" ? observedPages.size === 0 ? 0 : [...observedPages.values()].filter(isInventoryEligible).length : sql5`${siteCrawlAttempts.pagesEligible}`,
7298
+ pagesErrored: event.type === "summary" ? observedErrorCount(observedPages.values()) : sql5`${siteCrawlAttempts.pagesErrored}`,
7299
+ edgesDiscovered: progress.edgesObserved,
7300
+ updatedAt: now
7301
+ }).where(eq13(siteCrawlAttempts.id, attemptId)).run();
7302
+ } else {
7303
+ tx.update(siteCrawlAttempts).set({
7304
+ lastEventSequence: event.sequence,
7305
+ lastEventChecksum: event.checksum,
7306
+ updatedAt: now
7307
+ }).where(eq13(siteCrawlAttempts.id, attemptId)).run();
7308
+ }
7309
+ });
7310
+ };
7311
+ try {
7312
+ const project = db.select().from(projects).where(eq13(projects.id, projectId)).get();
7313
+ if (!project) throw new Error(`Project not found: ${projectId}`);
7314
+ const homepageUrl = toHomepageUrl(project.canonicalDomain);
7315
+ const maxPages = clampSiteAuditLimit(opts.maxPages ?? opts.limit);
7316
+ const maxEdges = clampSiteAuditEdgeLimit(opts.maxEdges);
7317
+ await assertSiteAuditUrlAllowed(homepageUrl, "canonicalDomain");
7318
+ if (opts.sitemapUrl) await assertSiteAuditUrlAllowed(opts.sitemapUrl, "sitemapUrl");
7319
+ log12.info("start", { runId, projectId, homepageUrl, maxPages, maxEdges, maxDepth: opts.maxDepth ?? null });
7320
+ const report = await runSiteCrawl(homepageUrl, {
7321
+ mode: "summary",
7322
+ sitemapUrl: opts.sitemapUrl,
7323
+ maxPages,
7324
+ maxEdges,
7325
+ maxDepth: opts.maxDepth,
7326
+ checkDeadLinks: opts.checkDeadLinks ?? false,
7327
+ signal: opts.signal,
7328
+ onEvent: persistEvent
7329
+ });
7330
+ const crawlSummary = report.summary;
7331
+ const hasAuditedPages = crawlSummary.auditRollup.auditedPages > 0;
7332
+ if (crawlSummary.complete && !hasAuditedPages) {
7333
+ throw emptyCompleteCrawlError(crawlSummary.rootUrl, crawlSummary.finalRootUrl, crawlSummary.pagesObserved);
7334
+ }
7335
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
7336
+ const factors = computeFactorAverages([...observedPages.values()]);
7337
+ const errorCount = observedErrorCount(observedPages.values());
7338
+ const terminalStatus = crawlSummary.complete ? "completed" : "partial";
7339
+ const deadLinksChecked = opts.checkDeadLinks ? deadLinkCheckedCount(observedEdges.values(), observedPages.values()) : 0;
7340
+ const deadLinksFound = report.deadLinks.findings.length;
7341
+ const legacyIssues = factors.map((factor) => toLegacyIssue(factor, crawlSummary.auditRollup.auditedPages)).filter((issue) => issue !== null);
7342
+ const published = db.transaction((tx) => {
7343
+ const claim2 = tx.update(runs).set({ status: terminalStatus, finishedAt }).where(and10(eq13(runs.id, runId), eq13(runs.status, "running"))).run();
7344
+ if (claim2.changes === 0) return false;
7345
+ if (hasAuditedPages) {
7346
+ tx.insert(siteAuditSnapshots).values({
7025
7347
  id: crypto16.randomUUID(),
7026
7348
  projectId,
7027
7349
  runId,
7028
- url: page.url,
7029
- overallScore: page.overallScore,
7030
- status: page.status,
7031
- error: page.error ?? null,
7032
- factors: (page.factors ?? []).map(toPageFactor),
7350
+ sitemapUrl: opts.sitemapUrl ?? `${homepageUrl}/sitemap.xml`,
7351
+ auditedAt: crawlSummary.completedAt,
7352
+ aggregateScore: crawlSummary.auditRollup.aggregateScore ?? 0,
7353
+ pagesDiscovered: crawlSummary.pagesDiscovered,
7354
+ pagesAudited: crawlSummary.auditRollup.auditedPages,
7355
+ pagesSkipped: Math.max(0, crawlSummary.pagesDiscovered - crawlSummary.pagesObserved),
7356
+ pagesErrored: errorCount,
7357
+ factorAverages: factors,
7358
+ crossCuttingIssues: legacyIssues,
7359
+ prioritizedFixes: legacyIssues.map((issue) => `Improve ${issue.factorName}`),
7033
7360
  createdAt: finishedAt
7034
7361
  }).run();
7362
+ for (const page of observedPages.values()) {
7363
+ if (!isLegacyScorecardPage(page)) continue;
7364
+ tx.insert(siteAuditPages).values({
7365
+ id: crypto16.randomUUID(),
7366
+ projectId,
7367
+ runId,
7368
+ url: page.finalUrl ?? page.requestedUrl,
7369
+ overallScore: page.audit?.overallScore ?? 0,
7370
+ status: page.audit ? "success" : "error",
7371
+ error: page.error,
7372
+ factors: page.audit?.factors.map(toPageFactor) ?? [],
7373
+ createdAt: finishedAt
7374
+ }).run();
7375
+ }
7035
7376
  }
7036
- tx.update(runs).set({ status, finishedAt }).where(eq13(runs.id, runId)).run();
7377
+ tx.insert(siteCrawlSnapshots).values({
7378
+ id: crypto16.randomUUID(),
7379
+ projectId,
7380
+ runId,
7381
+ attemptId,
7382
+ rootUrl: crawlSummary.rootUrl,
7383
+ crawlSchemaVersion: crawlSummary.crawlSchemaVersion,
7384
+ engineVersion: crawlSummary.engineVersion,
7385
+ normalizationVersion: crawlSummary.urlNormalizationVersion,
7386
+ indexabilityVersion: crawlSummary.indexabilityRulesetVersion,
7387
+ linkScoreVersion: crawlSummary.linkScoreAlgorithmVersion,
7388
+ effectiveOptions: {
7389
+ mode: "summary",
7390
+ sitemapUrl: opts.sitemapUrl ?? null,
7391
+ maxPages,
7392
+ maxEdges,
7393
+ maxDepth: opts.maxDepth ?? null,
7394
+ checkDeadLinks: opts.checkDeadLinks ?? false
7395
+ },
7396
+ pageBudget: maxPages,
7397
+ edgeBudget: maxEdges,
7398
+ maxDepth: opts.maxDepth ?? null,
7399
+ checkDeadLinks: opts.checkDeadLinks ?? false,
7400
+ complete: crawlSummary.complete,
7401
+ termination: crawlSummary.terminationReason ?? "complete",
7402
+ detailsAvailable: true,
7403
+ pagesDiscovered: crawlSummary.pagesDiscovered,
7404
+ pagesFetched: crawlSummary.pagesFetched,
7405
+ pagesEligible: [...observedPages.values()].filter(isInventoryEligible).length,
7406
+ pagesErrored: errorCount,
7407
+ edgesDiscovered: crawlSummary.edgesObserved,
7408
+ findingsCount: deadLinksFound,
7409
+ deadLinkState: report.deadLinks.state,
7410
+ deadLinksChecked,
7411
+ deadLinksFound,
7412
+ createdAt: finishedAt,
7413
+ updatedAt: finishedAt
7414
+ }).run();
7415
+ if (opts.checkDeadLinks) {
7416
+ for (const finding of report.deadLinks.findings) {
7417
+ tx.insert(siteCrawlFindings).values({
7418
+ id: crypto16.randomUUID(),
7419
+ projectId,
7420
+ runId,
7421
+ attemptId,
7422
+ findingKey: finding.key,
7423
+ findingType: "dead-link",
7424
+ severity: "error",
7425
+ sourceNodeKey: nodeKeyByUrl.get(finding.from) ?? null,
7426
+ sourceUrl: finding.from,
7427
+ targetNodeKey: nodeKeyByUrl.get(finding.to) ?? null,
7428
+ targetUrl: finding.to,
7429
+ evidence: { statusCode: finding.statusCode, reason: finding.reason },
7430
+ createdAt: finishedAt,
7431
+ updatedAt: finishedAt
7432
+ }).run();
7433
+ }
7434
+ }
7435
+ tx.update(siteCrawlAttempts).set({
7436
+ state: terminalStatus,
7437
+ pagesDiscovered: crawlSummary.pagesDiscovered,
7438
+ pagesFetched: crawlSummary.pagesFetched,
7439
+ pagesEligible: [...observedPages.values()].filter(isInventoryEligible).length,
7440
+ pagesErrored: errorCount,
7441
+ edgesDiscovered: crawlSummary.edgesObserved,
7442
+ finishedAt,
7443
+ updatedAt: finishedAt
7444
+ }).where(eq13(siteCrawlAttempts.id, attemptId)).run();
7445
+ return true;
7037
7446
  });
7038
- log12.info("completed", {
7039
- runId,
7040
- projectId,
7041
- status,
7042
- score: report.aggregateScore,
7043
- audited: report.pagesAudited,
7044
- errored: pagesErrored
7447
+ if (!published) {
7448
+ const current = db.select({ status: runs.status }).from(runs).where(eq13(runs.id, runId)).get();
7449
+ if (current?.status === "cancelled") throw new SiteAuditCancelledError(`Site audit cancelled before publication: ${runId}`);
7450
+ throw new Error(`Site audit run was no longer running before publication: ${runId}`);
7451
+ }
7452
+ log12.info("completed", { runId, projectId, status: terminalStatus, complete: crawlSummary.complete, pages: crawlSummary.pagesObserved });
7453
+ } catch (error) {
7454
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
7455
+ const cancelled = isCancelled(error, opts.signal);
7456
+ const message = error instanceof Error ? error.message : String(error);
7457
+ db.transaction((tx) => {
7458
+ tx.update(siteCrawlAttempts).set({
7459
+ state: cancelled ? "cancelled" : "failed",
7460
+ error: message,
7461
+ finishedAt,
7462
+ updatedAt: finishedAt
7463
+ }).where(eq13(siteCrawlAttempts.id, attemptId)).run();
7464
+ tx.update(runs).set({
7465
+ status: cancelled ? "cancelled" : "failed",
7466
+ error: message,
7467
+ finishedAt
7468
+ }).where(and10(eq13(runs.id, runId), eq13(runs.status, "running"))).run();
7045
7469
  });
7046
- } catch (err) {
7047
- const errorMsg = err instanceof Error ? err.message : String(err);
7048
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq13(runs.id, runId)).run();
7049
- log12.error("failed", { runId, projectId, error: errorMsg });
7050
- throw err;
7470
+ log12.error("failed", { runId, projectId, cancelled, error: message });
7471
+ throw error;
7051
7472
  }
7052
7473
  }
7053
7474
 
7054
7475
  // src/commands/backfill.ts
7055
- import { and as and10, eq as eq14, inArray as inArray4, isNull, sql as sql5 } from "drizzle-orm";
7476
+ import { and as and11, eq as eq14, inArray as inArray4, isNull, sql as sql6 } from "drizzle-orm";
7056
7477
  var SNAPSHOT_BATCH_SIZE = 500;
7057
7478
  async function backfillAnswerVisibilityCommand(opts) {
7058
7479
  const config = loadConfig();
@@ -7068,7 +7489,7 @@ async function backfillAnswerVisibilityCommand(opts) {
7068
7489
  let reparsed = 0;
7069
7490
  let providerErrors = 0;
7070
7491
  if (scopedProjects.length > 0) {
7071
- const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and10(
7492
+ const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and11(
7072
7493
  eq14(runs.kind, RunKinds["answer-visibility"]),
7073
7494
  inArray4(runs.projectId, scopedProjects.map((project) => project.id))
7074
7495
  )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq14(runs.kind, RunKinds["answer-visibility"])).all();
@@ -7227,7 +7648,7 @@ function backfillNormalizedPaths(db, opts) {
7227
7648
  id: gaTrafficSnapshots.id,
7228
7649
  landingPage: gaTrafficSnapshots.landingPage,
7229
7650
  landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
7230
- }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and10(...baseConditions) : void 0).all();
7651
+ }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and11(...baseConditions) : void 0).all();
7231
7652
  let updated = 0;
7232
7653
  let unchanged = 0;
7233
7654
  if (rows.length > 0) {
@@ -7299,7 +7720,7 @@ function backfillAiReferralPaths(db, opts) {
7299
7720
  id: gaAiReferrals.id,
7300
7721
  landingPage: gaAiReferrals.landingPage,
7301
7722
  landingPageNormalized: gaAiReferrals.landingPageNormalized
7302
- }).from(gaAiReferrals).where(baseConditions.length > 0 ? and10(...baseConditions) : void 0).all();
7723
+ }).from(gaAiReferrals).where(baseConditions.length > 0 ? and11(...baseConditions) : void 0).all();
7303
7724
  let updated = 0;
7304
7725
  let unchanged = 0;
7305
7726
  if (rows.length > 0) {
@@ -7367,7 +7788,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
7367
7788
  const project = db.select().from(projects).where(eq14(projects.id, projectId)).get();
7368
7789
  if (!project) return { examined: 0, updated: 0, mentioned: 0 };
7369
7790
  const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, projectId)).all().map((row) => row.domain);
7370
- const runRows = db.select({ id: runs.id }).from(runs).where(and10(eq14(runs.kind, RunKinds["answer-visibility"]), eq14(runs.projectId, projectId))).all();
7791
+ const runRows = db.select({ id: runs.id }).from(runs).where(and11(eq14(runs.kind, RunKinds["answer-visibility"]), eq14(runs.projectId, projectId))).all();
7371
7792
  const runIds = runRows.map((r) => r.id);
7372
7793
  let examined = 0;
7373
7794
  let updated = 0;
@@ -7516,7 +7937,7 @@ function readStoredGroundingSources(rawResponse) {
7516
7937
  return result;
7517
7938
  }
7518
7939
  async function backfillInsightsCommand(project, opts) {
7519
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-B5AETQ5N.js");
7940
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-G4NV3X36.js");
7520
7941
  const config = loadConfig();
7521
7942
  const db = createClient(config.database);
7522
7943
  migrate(db);
@@ -7686,7 +8107,7 @@ async function backfillSnapshotAttributionCommand(opts) {
7686
8107
  process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
7687
8108
  `);
7688
8109
  }
7689
- const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and10(
8110
+ const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and11(
7690
8111
  eq14(auditLog.projectId, project.id),
7691
8112
  inArray4(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
7692
8113
  )).orderBy(auditLog.createdAt).all();
@@ -7695,7 +8116,7 @@ async function backfillSnapshotAttributionCommand(opts) {
7695
8116
  runId: runs.id,
7696
8117
  createdAt: runs.createdAt,
7697
8118
  location: runs.location
7698
- }).from(runs).innerJoin(querySnapshots, eq14(querySnapshots.runId, runs.id)).where(and10(
8119
+ }).from(runs).innerJoin(querySnapshots, eq14(querySnapshots.runId, runs.id)).where(and11(
7699
8120
  eq14(runs.projectId, project.id),
7700
8121
  isNull(querySnapshots.queryId),
7701
8122
  isNull(querySnapshots.queryText)
@@ -7718,7 +8139,7 @@ async function backfillSnapshotAttributionCommand(opts) {
7718
8139
  provider: querySnapshots.provider,
7719
8140
  createdAt: querySnapshots.createdAt,
7720
8141
  answerText: querySnapshots.answerText
7721
- }).from(querySnapshots).where(and10(
8142
+ }).from(querySnapshots).where(and11(
7722
8143
  eq14(querySnapshots.runId, run.runId),
7723
8144
  isNull(querySnapshots.queryId),
7724
8145
  isNull(querySnapshots.queryText)
@@ -7884,7 +8305,7 @@ async function backfillTrafficClassificationCommand(opts) {
7884
8305
  dryRun: isDryRun,
7885
8306
  byBot: {}
7886
8307
  };
7887
- const unknownCountRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and10(
8308
+ const unknownCountRow = db.select({ n: sql6`count(*)` }).from(rawEventSamples).where(and11(
7888
8309
  eq14(rawEventSamples.eventType, "unknown"),
7889
8310
  inArray4(rawEventSamples.projectId, projectIds)
7890
8311
  )).get();
@@ -7897,7 +8318,7 @@ async function backfillTrafficClassificationCommand(opts) {
7897
8318
  userAgent: rawEventSamples.userAgent,
7898
8319
  pathNormalized: rawEventSamples.pathNormalized,
7899
8320
  status: rawEventSamples.status
7900
- }).from(rawEventSamples).where(and10(
8321
+ }).from(rawEventSamples).where(and11(
7901
8322
  eq14(rawEventSamples.eventType, "unknown"),
7902
8323
  inArray4(rawEventSamples.projectId, projectIds)
7903
8324
  )).all();
@@ -7965,7 +8386,7 @@ async function backfillTrafficClassificationCommand(opts) {
7965
8386
  aiUserFetchEventsHourly.status
7966
8387
  ],
7967
8388
  set: {
7968
- hits: sql5`${aiUserFetchEventsHourly.hits} + 1`,
8389
+ hits: sql6`${aiUserFetchEventsHourly.hits} + 1`,
7969
8390
  updatedAt: now
7970
8391
  }
7971
8392
  }).run();
@@ -7994,14 +8415,14 @@ async function backfillTrafficClassificationCommand(opts) {
7994
8415
  crawlerEventsHourly.status
7995
8416
  ],
7996
8417
  set: {
7997
- hits: sql5`${crawlerEventsHourly.hits} + 1`,
8418
+ hits: sql6`${crawlerEventsHourly.hits} + 1`,
7998
8419
  updatedAt: now
7999
8420
  }
8000
8421
  }).run();
8001
8422
  }
8002
8423
  }
8003
8424
  if (!isDryRun) {
8004
- const afterRow = db.select({ n: sql5`count(*)` }).from(rawEventSamples).where(and10(
8425
+ const afterRow = db.select({ n: sql6`count(*)` }).from(rawEventSamples).where(and11(
8005
8426
  eq14(rawEventSamples.eventType, "unknown"),
8006
8427
  inArray4(rawEventSamples.projectId, projectIds)
8007
8428
  )).get();
@@ -8436,7 +8857,7 @@ var ProviderRegistry = class {
8436
8857
  // src/scheduler.ts
8437
8858
  import crypto18 from "crypto";
8438
8859
  import cron from "node-cron";
8439
- import { and as and11, eq as eq15, inArray as inArray5, notExists, sql as sql6 } from "drizzle-orm";
8860
+ import { and as and12, eq as eq15, inArray as inArray5, notExists, sql as sql7 } from "drizzle-orm";
8440
8861
  var log13 = createLogger("Scheduler");
8441
8862
  var DEFAULT_HEALTH_CRON = "0 */6 * * *";
8442
8863
  function taskKey(projectId, kind) {
@@ -8468,7 +8889,7 @@ var Scheduler = class {
8468
8889
  */
8469
8890
  ensureHealthSchedules() {
8470
8891
  const projectsWithoutHealth = this.db.select({ id: projects.id }).from(projects).where(notExists(
8471
- this.db.select({ one: sql6`1` }).from(schedules).where(and11(
8892
+ this.db.select({ one: sql7`1` }).from(schedules).where(and12(
8472
8893
  eq15(schedules.projectId, projects.id),
8473
8894
  eq15(schedules.kind, SchedulableRunKinds.doctor)
8474
8895
  ))
@@ -8551,7 +8972,7 @@ var Scheduler = class {
8551
8972
  this.stopTask(key, existing, "Stopped");
8552
8973
  this.tasks.delete(key);
8553
8974
  }
8554
- const schedule = this.db.select().from(schedules).where(and11(eq15(schedules.projectId, projectId), eq15(schedules.kind, kind))).get();
8975
+ const schedule = this.db.select().from(schedules).where(and12(eq15(schedules.projectId, projectId), eq15(schedules.kind, kind))).get();
8555
8976
  if (schedule && schedule.enabled) {
8556
8977
  this.registerCronTask(schedule);
8557
8978
  }
@@ -8659,7 +9080,7 @@ var Scheduler = class {
8659
9080
  log13.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
8660
9081
  return;
8661
9082
  }
8662
- const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and11(
9083
+ const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and12(
8663
9084
  eq15(runs.projectId, projectId),
8664
9085
  eq15(runs.kind, RunKinds["ads-sync"]),
8665
9086
  inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
@@ -8734,7 +9155,7 @@ var Scheduler = class {
8734
9155
  log13.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
8735
9156
  return;
8736
9157
  }
8737
- const active = this.db.select({ id: runs.id }).from(runs).where(and11(
9158
+ const active = this.db.select({ id: runs.id }).from(runs).where(and12(
8738
9159
  eq15(runs.projectId, projectId),
8739
9160
  eq15(runs.kind, RunKinds["site-audit"]),
8740
9161
  inArray5(runs.status, [RunStatuses.queued, RunStatuses.running])
@@ -8745,14 +9166,24 @@ var Scheduler = class {
8745
9166
  return;
8746
9167
  }
8747
9168
  const runId2 = crypto18.randomUUID();
8748
- this.db.insert(runs).values({
8749
- id: runId2,
8750
- projectId,
8751
- kind: RunKinds["site-audit"],
8752
- status: RunStatuses.queued,
8753
- trigger: RunTriggers.scheduled,
8754
- createdAt: now
8755
- }).run();
9169
+ const effectiveRequest = normalizeSiteAuditRunRequest({});
9170
+ this.db.transaction((tx) => {
9171
+ tx.insert(runs).values({
9172
+ id: runId2,
9173
+ projectId,
9174
+ kind: RunKinds["site-audit"],
9175
+ status: RunStatuses.queued,
9176
+ trigger: RunTriggers.scheduled,
9177
+ createdAt: now
9178
+ }).run();
9179
+ tx.insert(siteCrawlRunRequests).values({
9180
+ runId: runId2,
9181
+ projectId,
9182
+ identityKey: siteAuditRequestIdentity(effectiveRequest),
9183
+ effectiveOptions: effectiveRequest,
9184
+ createdAt: now
9185
+ }).run();
9186
+ });
8756
9187
  this.db.update(schedules).set({
8757
9188
  lastRunAt: now,
8758
9189
  nextRunAt,
@@ -8831,7 +9262,7 @@ async function refreshAllIntegrations(client, projectName) {
8831
9262
  }
8832
9263
 
8833
9264
  // src/notifier.ts
8834
- import { eq as eq16, desc as desc6, and as and12, inArray as inArray6, or } from "drizzle-orm";
9265
+ import { eq as eq16, desc as desc6, and as and13, inArray as inArray6, or } from "drizzle-orm";
8835
9266
  import crypto19 from "crypto";
8836
9267
  var log15 = createLogger("Notifier");
8837
9268
  var Notifier = class {
@@ -9042,7 +9473,7 @@ var Notifier = class {
9042
9473
  });
9043
9474
  return [];
9044
9475
  }
9045
- const groupSiblings = this.db.select().from(runs).where(and12(
9476
+ const groupSiblings = this.db.select().from(runs).where(and13(
9046
9477
  eq16(runs.projectId, projectId),
9047
9478
  eq16(runs.kind, thisRun.kind),
9048
9479
  eq16(runs.createdAt, thisRun.createdAt)
@@ -9068,7 +9499,7 @@ var Notifier = class {
9068
9499
  );
9069
9500
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
9070
9501
  const recentRuns = this.db.select().from(runs).where(
9071
- and12(
9502
+ and13(
9072
9503
  eq16(runs.projectId, projectId),
9073
9504
  eq16(runs.kind, thisRun.kind),
9074
9505
  or(eq16(runs.status, "completed"), eq16(runs.status, "partial"))
@@ -10450,7 +10881,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
10450
10881
 
10451
10882
  // src/agent/memory-store.ts
10452
10883
  import crypto22 from "crypto";
10453
- import { and as and13, desc as desc7, eq as eq18, like, sql as sql7 } from "drizzle-orm";
10884
+ import { and as and14, desc as desc7, eq as eq18, like, sql as sql8 } from "drizzle-orm";
10454
10885
  var COMPACTION_KEY_PREFIX = "compaction:";
10455
10886
  var COMPACTION_NOTES_PER_SESSION = 3;
10456
10887
  function rowToDto(row) {
@@ -10495,12 +10926,12 @@ function upsertMemoryEntry(db, args) {
10495
10926
  updatedAt: now
10496
10927
  }
10497
10928
  }).run();
10498
- const row = db.select().from(agentMemory).where(and13(eq18(agentMemory.projectId, args.projectId), eq18(agentMemory.key, args.key))).get();
10929
+ const row = db.select().from(agentMemory).where(and14(eq18(agentMemory.projectId, args.projectId), eq18(agentMemory.key, args.key))).get();
10499
10930
  if (!row) throw new Error("memory upsert produced no row");
10500
10931
  return rowToDto(row);
10501
10932
  }
10502
10933
  function deleteMemoryEntry(db, projectId, key) {
10503
- const result = db.delete(agentMemory).where(and13(eq18(agentMemory.projectId, projectId), eq18(agentMemory.key, key))).run();
10934
+ const result = db.delete(agentMemory).where(and14(eq18(agentMemory.projectId, projectId), eq18(agentMemory.key, key))).run();
10504
10935
  const changes = result.changes ?? 0;
10505
10936
  return changes > 0;
10506
10937
  }
@@ -10529,16 +10960,16 @@ function writeCompactionNote(db, args) {
10529
10960
  }).run();
10530
10961
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
10531
10962
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
10532
- and13(
10963
+ and14(
10533
10964
  eq18(agentMemory.projectId, args.projectId),
10534
10965
  like(agentMemory.key, `${sessionPrefix}%`)
10535
10966
  )
10536
10967
  ).orderBy(desc7(agentMemory.updatedAt)).all();
10537
10968
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
10538
10969
  if (stale.length > 0) {
10539
- tx.delete(agentMemory).where(sql7`${agentMemory.id} IN (${sql7.join(stale.map((s) => sql7`${s}`), sql7`, `)})`).run();
10970
+ tx.delete(agentMemory).where(sql8`${agentMemory.id} IN (${sql8.join(stale.map((s) => sql8`${s}`), sql8`, `)})`).run();
10540
10971
  }
10541
- const row = tx.select().from(agentMemory).where(and13(eq18(agentMemory.projectId, args.projectId), eq18(agentMemory.key, key))).get();
10972
+ const row = tx.select().from(agentMemory).where(and14(eq18(agentMemory.projectId, args.projectId), eq18(agentMemory.key, key))).get();
10542
10973
  if (row) inserted = rowToDto(row);
10543
10974
  });
10544
10975
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -11550,7 +11981,7 @@ Your previous reply was not valid JSON. Return ONLY a JSON object with exactly t
11550
11981
  }
11551
11982
 
11552
11983
  // src/snapshot-service.ts
11553
- import { runAeoAudit } from "@ainyc/aeo-audit";
11984
+ import { runAeoAudit } from "@canonry/aeo-audit";
11554
11985
 
11555
11986
  // src/site-fetch.ts
11556
11987
  import https from "https";
@@ -12169,20 +12600,20 @@ function clipText(value, length) {
12169
12600
  }
12170
12601
 
12171
12602
  // src/research-runner.ts
12172
- import { and as and14, eq as eq21, inArray as inArray7, sql as sql8 } from "drizzle-orm";
12603
+ import { and as and15, eq as eq21, inArray as inArray7, sql as sql9 } from "drizzle-orm";
12173
12604
  var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
12174
12605
  function finalResearchRunStatus(completed, failed) {
12175
12606
  if (failed === 0) return ResearchRunStatuses.completed;
12176
12607
  return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
12177
12608
  }
12178
12609
  async function executeResearchRun(db, registry, runId, projectId) {
12179
- const run = db.select().from(researchRuns).where(and14(eq21(researchRuns.id, runId), eq21(researchRuns.projectId, projectId))).get();
12610
+ const run = db.select().from(researchRuns).where(and15(eq21(researchRuns.id, runId), eq21(researchRuns.projectId, projectId))).get();
12180
12611
  if (!run || run.status !== ResearchRunStatuses.queued) return;
12181
12612
  const project = db.select().from(projects).where(eq21(projects.id, projectId)).get();
12182
12613
  if (!project) return;
12183
12614
  const provider = registry.get(run.provider);
12184
12615
  const now = (/* @__PURE__ */ new Date()).toISOString();
12185
- const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and14(eq21(researchRuns.id, runId), eq21(researchRuns.status, ResearchRunStatuses.queued))).run();
12616
+ const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and15(eq21(researchRuns.id, runId), eq21(researchRuns.status, ResearchRunStatuses.queued))).run();
12186
12617
  if (claim.changes !== 1) return;
12187
12618
  let reserved = 0;
12188
12619
  let dispatched = 0;
@@ -12215,7 +12646,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
12215
12646
  await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
12216
12647
  try {
12217
12648
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
12218
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and14(eq21(researchRunQueries.id, row.id), eq21(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
12649
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and15(eq21(researchRunQueries.id, row.id), eq21(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
12219
12650
  const raw = await gate.run(async () => {
12220
12651
  dispatched++;
12221
12652
  return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
@@ -12242,7 +12673,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
12242
12673
  citationState: determineCitationState(normalized, domains),
12243
12674
  rawResponse: raw.rawResponse,
12244
12675
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
12245
- }).where(and14(eq21(researchRunQueries.id, row.id), eq21(researchRunQueries.status, ResearchQueryStatuses.running))).run();
12676
+ }).where(and15(eq21(researchRunQueries.id, row.id), eq21(researchRunQueries.status, ResearchQueryStatuses.running))).run();
12246
12677
  if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
12247
12678
  } catch (error) {
12248
12679
  try {
@@ -12266,15 +12697,15 @@ async function executeResearchRun(db, registry, runId, projectId) {
12266
12697
  }
12267
12698
  }
12268
12699
  function incrementResearchProgress(db, runId, column) {
12269
- db.update(researchRuns).set({ [column]: sql8`${researchRuns[column]} + 1` }).where(eq21(researchRuns.id, runId)).run();
12700
+ db.update(researchRuns).set({ [column]: sql9`${researchRuns[column]} + 1` }).where(eq21(researchRuns.id, runId)).run();
12270
12701
  }
12271
12702
  function markResearchQueryFailed(db, runId, queryId, error) {
12272
12703
  const message = error instanceof Error ? error.message : String(error);
12273
- const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and14(eq21(researchRunQueries.id, queryId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
12704
+ const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and15(eq21(researchRunQueries.id, queryId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
12274
12705
  if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
12275
12706
  }
12276
12707
  function markUnfinishedResearchQueriesFailed(db, runId, error) {
12277
- db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and14(eq21(researchRunQueries.researchRunId, runId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
12708
+ db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and15(eq21(researchRunQueries.researchRunId, runId), inArray7(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
12278
12709
  }
12279
12710
  function finalizeResearchRun(db, runId, fatalError) {
12280
12711
  const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq21(researchRunQueries.researchRunId, runId)).all();
@@ -12651,9 +13082,16 @@ async function createServer(opts) {
12651
13082
  app.log.error({ runId, err }, "Ads sync failed");
12652
13083
  });
12653
13084
  };
13085
+ const siteAuditAbortControllers = /* @__PURE__ */ new Map();
12654
13086
  const runSiteAudit = (runId, projectId, auditOpts) => {
12655
- executeSiteAudit(opts.db, runId, projectId, auditOpts ?? {}).then(() => runCoordinator.onRunCompleted(runId, projectId)).catch((err) => {
13087
+ const controller = new AbortController();
13088
+ siteAuditAbortControllers.set(runId, controller);
13089
+ executeSiteAudit(opts.db, runId, projectId, { ...auditOpts ?? {}, signal: controller.signal }).then(() => runCoordinator.onRunCompleted(runId, projectId)).catch((err) => {
12656
13090
  app.log.error({ runId, err }, "Site audit failed");
13091
+ }).finally(() => {
13092
+ if (siteAuditAbortControllers.get(runId) === controller) {
13093
+ siteAuditAbortControllers.delete(runId);
13094
+ }
12657
13095
  });
12658
13096
  };
12659
13097
  const adsCredentialStore = {
@@ -12972,7 +13410,7 @@ async function createServer(opts) {
12972
13410
  });
12973
13411
  if (!probed) return;
12974
13412
  const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
12975
- and15(
13413
+ and16(
12976
13414
  eq22(ccReleaseSyncs.release, probed.release),
12977
13415
  eq22(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
12978
13416
  )
@@ -13759,6 +14197,12 @@ async function createServer(opts) {
13759
14197
  app.log.error({ runId, err }, "Job runner failed");
13760
14198
  });
13761
14199
  },
14200
+ onRunCancelled: (runId) => {
14201
+ const controller = siteAuditAbortControllers.get(runId);
14202
+ if (controller && !controller.signal.aborted) {
14203
+ controller.abort(new Error("Cancelled by user"));
14204
+ }
14205
+ },
13762
14206
  getRunnableProviderNames: () => registry.getAll().map((provider) => provider.adapter.name),
13763
14207
  getEffectiveProviderModels: () => effectiveProviderModels(registry),
13764
14208
  onProviderUpdate: (providerName, apiKey, model, baseUrl, incomingQuota) => {