@ainyc/canonry 4.72.3 → 4.74.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 (27) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +17 -0
  2. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +56 -3
  3. package/assets/assets/{BacklinksPage-CjfpwZEH.js → BacklinksPage-ClgP7CUd.js} +1 -1
  4. package/assets/assets/{ChartPrimitives-Ckf2FrUy.js → ChartPrimitives-PGDrQBXP.js} +1 -1
  5. package/assets/assets/ProjectPage-xfLeh2vB.js +6 -0
  6. package/assets/assets/{RunRow-BuFyG0V_.js → RunRow-DL-lUm35.js} +1 -1
  7. package/assets/assets/{RunsPage-D-pr000K.js → RunsPage-BCL_lU-R.js} +1 -1
  8. package/assets/assets/{SettingsPage-CiaapCYn.js → SettingsPage-D67UQYJa.js} +1 -1
  9. package/assets/assets/{TrafficPage-B40xytJD.js → TrafficPage-DVRcPxCk.js} +1 -1
  10. package/assets/assets/{TrafficSourceDetailPage-7hHem-gM.js → TrafficSourceDetailPage-JzX1fhGQ.js} +1 -1
  11. package/assets/assets/{extract-error-message-3GkDsu1h.js → extract-error-message-Cia_CilL.js} +1 -1
  12. package/assets/assets/index-CFVX11lK.css +1 -0
  13. package/assets/assets/{index-BVdH2O9w.js → index-DHg9_-PB.js} +118 -118
  14. package/assets/assets/{server-traffic-CsgPsudZ.js → server-traffic-GBmLS3L7.js} +1 -1
  15. package/assets/assets/{trash-2-B8Ipf9rI.js → trash-2-Bk7PYGBN.js} +1 -1
  16. package/assets/index.html +2 -2
  17. package/dist/{chunk-JXFNERK4.js → chunk-A7JX3FZB.js} +1094 -995
  18. package/dist/{chunk-SIB4NMEH.js → chunk-MRC4JMIH.js} +369 -176
  19. package/dist/{chunk-ZUBBADMR.js → chunk-W6GBIRFA.js} +162 -1
  20. package/dist/{chunk-HSX32G47.js → chunk-ZRZHIS22.js} +414 -73
  21. package/dist/cli.js +236 -30
  22. package/dist/index.js +4 -4
  23. package/dist/{intelligence-service-ZW3ARLJT.js → intelligence-service-GPO2VMEC.js} +2 -2
  24. package/dist/mcp.js +2 -2
  25. package/package.json +8 -8
  26. package/assets/assets/ProjectPage-DZeplYeC.js +0 -6
  27. package/assets/assets/index-B3nENtU0.css +0 -1
@@ -21,6 +21,7 @@ import {
21
21
  RunTriggers,
22
22
  SKILL_MANIFEST_FILENAME,
23
23
  SchedulableRunKinds,
24
+ SiteAuditTrendDirections,
24
25
  TrafficEventConfidences,
25
26
  TrafficEventKinds,
26
27
  TrafficEvidenceKinds,
@@ -186,6 +187,11 @@ import {
186
187
  scheduleUpsertRequestSchema,
187
188
  serializeRunError,
188
189
  settingsDtoSchema,
190
+ siteAuditPagesResponseSchema,
191
+ siteAuditRunRequestSchema,
192
+ siteAuditRunResponseSchema,
193
+ siteAuditScoreSchema,
194
+ siteAuditTrendResponseSchema,
189
195
  snapshotDiffResponseSchema,
190
196
  snapshotListResponseSchema,
191
197
  snapshotReportSchema,
@@ -223,10 +229,10 @@ import {
223
229
  wordpressSchemaDeployResultDtoSchema,
224
230
  wordpressSchemaStatusResultDtoSchema,
225
231
  wordpressStatusDtoSchema
226
- } from "./chunk-JXFNERK4.js";
232
+ } from "./chunk-A7JX3FZB.js";
227
233
 
228
234
  // src/intelligence-service.ts
229
- import { eq as eq32, desc as desc16, asc as asc3, and as and23, ne as ne5, or as or5, inArray as inArray11, gte as gte6, lte as lte3 } from "drizzle-orm";
235
+ import { eq as eq33, desc as desc17, asc as asc4, and as and24, ne as ne5, or as or5, inArray as inArray12, gte as gte6, lte as lte3 } from "drizzle-orm";
230
236
 
231
237
  // ../db/src/client.ts
232
238
  import { mkdirSync } from "fs";
@@ -285,6 +291,8 @@ __export(schema_exports, {
285
291
  recommendationExplanations: () => recommendationExplanations,
286
292
  runs: () => runs,
287
293
  schedules: () => schedules,
294
+ siteAuditPages: () => siteAuditPages,
295
+ siteAuditSnapshots: () => siteAuditSnapshots,
288
296
  trafficSources: () => trafficSources,
289
297
  usageCounters: () => usageCounters
290
298
  });
@@ -533,6 +541,39 @@ var gscCoverageSnapshots = sqliteTable("gsc_coverage_snapshots", {
533
541
  index("idx_gsc_coverage_snap_project_date").on(table.projectId, table.date),
534
542
  index("idx_gsc_coverage_snap_run").on(table.syncRunId)
535
543
  ]);
544
+ var siteAuditSnapshots = sqliteTable("site_audit_snapshots", {
545
+ id: text("id").primaryKey(),
546
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
547
+ runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
548
+ sitemapUrl: text("sitemap_url").notNull(),
549
+ auditedAt: text("audited_at").notNull(),
550
+ aggregateScore: integer("aggregate_score").notNull().default(0),
551
+ pagesDiscovered: integer("pages_discovered").notNull().default(0),
552
+ pagesAudited: integer("pages_audited").notNull().default(0),
553
+ pagesSkipped: integer("pages_skipped").notNull().default(0),
554
+ pagesErrored: integer("pages_errored").notNull().default(0),
555
+ factorAverages: text("factor_averages", { mode: "json" }).$type().notNull().default([]),
556
+ crossCuttingIssues: text("cross_cutting_issues", { mode: "json" }).$type().notNull().default([]),
557
+ prioritizedFixes: text("prioritized_fixes", { mode: "json" }).$type().notNull().default([]),
558
+ createdAt: text("created_at").notNull()
559
+ }, (table) => [
560
+ index("idx_site_audit_snap_project_created").on(table.projectId, table.createdAt),
561
+ index("idx_site_audit_snap_run").on(table.runId)
562
+ ]);
563
+ var siteAuditPages = sqliteTable("site_audit_pages", {
564
+ id: text("id").primaryKey(),
565
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
566
+ runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
567
+ url: text("url").notNull(),
568
+ overallScore: integer("overall_score").notNull().default(0),
569
+ status: text("status").notNull(),
570
+ error: text("error"),
571
+ factors: text("factors", { mode: "json" }).$type().notNull().default([]),
572
+ createdAt: text("created_at").notNull()
573
+ }, (table) => [
574
+ index("idx_site_audit_pages_run").on(table.runId),
575
+ index("idx_site_audit_pages_project_score").on(table.projectId, table.overallScore)
576
+ ]);
536
577
  var bingCoverageSnapshots = sqliteTable("bing_coverage_snapshots", {
537
578
  id: text("id").primaryKey(),
538
579
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
@@ -2780,6 +2821,47 @@ var MIGRATION_VERSIONS = [
2780
2821
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_recommendation_briefs_unique ON recommendation_briefs(project_id, target_ref, prompt_version)`,
2781
2822
  `CREATE INDEX IF NOT EXISTS idx_recommendation_briefs_project ON recommendation_briefs(project_id)`
2782
2823
  ]
2824
+ },
2825
+ {
2826
+ // Technical AEO — site-wide audit persistence. `site_audit_snapshots` is the
2827
+ // per-run summary (drives the score + trend); `site_audit_pages` is the
2828
+ // per-page breakdown (drives the drill-down table). Both cascade off runs so
2829
+ // a run delete cleans up its audit data.
2830
+ version: 75,
2831
+ name: "site-audit-tables",
2832
+ statements: [
2833
+ `CREATE TABLE IF NOT EXISTS site_audit_snapshots (
2834
+ id TEXT PRIMARY KEY,
2835
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
2836
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2837
+ sitemap_url TEXT NOT NULL,
2838
+ audited_at TEXT NOT NULL,
2839
+ aggregate_score INTEGER NOT NULL DEFAULT 0,
2840
+ pages_discovered INTEGER NOT NULL DEFAULT 0,
2841
+ pages_audited INTEGER NOT NULL DEFAULT 0,
2842
+ pages_skipped INTEGER NOT NULL DEFAULT 0,
2843
+ pages_errored INTEGER NOT NULL DEFAULT 0,
2844
+ factor_averages TEXT NOT NULL DEFAULT '[]',
2845
+ cross_cutting_issues TEXT NOT NULL DEFAULT '[]',
2846
+ prioritized_fixes TEXT NOT NULL DEFAULT '[]',
2847
+ created_at TEXT NOT NULL
2848
+ )`,
2849
+ `CREATE INDEX IF NOT EXISTS idx_site_audit_snap_project_created ON site_audit_snapshots(project_id, created_at)`,
2850
+ `CREATE INDEX IF NOT EXISTS idx_site_audit_snap_run ON site_audit_snapshots(run_id)`,
2851
+ `CREATE TABLE IF NOT EXISTS site_audit_pages (
2852
+ id TEXT PRIMARY KEY,
2853
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
2854
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2855
+ url TEXT NOT NULL,
2856
+ overall_score INTEGER NOT NULL DEFAULT 0,
2857
+ status TEXT NOT NULL,
2858
+ error TEXT,
2859
+ factors TEXT NOT NULL DEFAULT '[]',
2860
+ created_at TEXT NOT NULL
2861
+ )`,
2862
+ `CREATE INDEX IF NOT EXISTS idx_site_audit_pages_run ON site_audit_pages(run_id)`,
2863
+ `CREATE INDEX IF NOT EXISTS idx_site_audit_pages_project_score ON site_audit_pages(project_id, overall_score)`
2864
+ ]
2783
2865
  }
2784
2866
  ];
2785
2867
  function isDuplicateColumnError(err) {
@@ -4098,15 +4180,15 @@ function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains, topDo
4098
4180
  totalCitations++;
4099
4181
  }
4100
4182
  }
4101
- const categories = [...categoryCounts.entries()].map(([category, { label, count }]) => ({
4183
+ const categories = [...categoryCounts.entries()].map(([category, { label, count: count2 }]) => ({
4102
4184
  category,
4103
4185
  label,
4104
- count,
4105
- sharePct: totalCitations > 0 ? Math.round(count / totalCitations * 100) : 0
4186
+ count: count2,
4187
+ sharePct: totalCitations > 0 ? Math.round(count2 / totalCitations * 100) : 0
4106
4188
  })).sort((a, b) => b.count - a.count);
4107
- const topDomains = [...domainCounts.entries()].map(([domain, count]) => ({
4189
+ const topDomains = [...domainCounts.entries()].map(([domain, count2]) => ({
4108
4190
  domain,
4109
- count,
4191
+ count: count2,
4110
4192
  isCompetitor: citedDomainBelongsToProject(domain, competitorDomains)
4111
4193
  })).sort((a, b) => b.count - a.count).slice(0, topDomainsLimit);
4112
4194
  return { categories, topDomains };
@@ -5203,7 +5285,7 @@ async function projectRoutes(app, opts) {
5203
5285
  app.get("/projects/:name/delete-preview", async (request, reply) => {
5204
5286
  const project = resolveProject(app.db, request.params.name);
5205
5287
  const pid = project.id;
5206
- const count = (n) => n ?? 0;
5288
+ const count2 = (n) => n ?? 0;
5207
5289
  const queryCount = app.db.select({ n: sql3`count(*)` }).from(queries).where(eq3(queries.projectId, pid)).get();
5208
5290
  const competitorCount = app.db.select({ n: sql3`count(*)` }).from(competitors).where(eq3(competitors.projectId, pid)).get();
5209
5291
  const runCount = app.db.select({ n: sql3`count(*)` }).from(runs).where(eq3(runs.projectId, pid)).get();
@@ -5213,14 +5295,14 @@ async function projectRoutes(app, opts) {
5213
5295
  return reply.send({
5214
5296
  project: { id: project.id, name: project.name },
5215
5297
  cascadeRows: {
5216
- queries: count(queryCount?.n),
5217
- competitors: count(competitorCount?.n),
5218
- runs: count(runCount?.n),
5219
- snapshots: count(snapshotCount?.n),
5220
- insights: count(insightCount?.n)
5298
+ queries: count2(queryCount?.n),
5299
+ competitors: count2(competitorCount?.n),
5300
+ runs: count2(runCount?.n),
5301
+ snapshots: count2(snapshotCount?.n),
5302
+ insights: count2(insightCount?.n)
5221
5303
  },
5222
5304
  detachedRows: {
5223
- auditLog: count(auditLogCount?.n)
5305
+ auditLog: count2(auditLogCount?.n)
5224
5306
  }
5225
5307
  });
5226
5308
  });
@@ -5552,14 +5634,14 @@ async function queryRoutes(app, opts) {
5552
5634
  validProviders: validNames
5553
5635
  });
5554
5636
  }
5555
- const count = body.count ?? 5;
5637
+ const count2 = body.count ?? 5;
5556
5638
  if (!opts.onGenerateQueries) {
5557
5639
  throw notImplemented("Query generation is not supported in this deployment");
5558
5640
  }
5559
5641
  const existingRows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
5560
5642
  const existingQueries = existingRows.map((r) => r.query);
5561
5643
  try {
5562
- const generated = await opts.onGenerateQueries(provider, count, {
5644
+ const generated = await opts.onGenerateQueries(provider, count2, {
5563
5645
  domain: project.canonicalDomain,
5564
5646
  displayName: project.displayName,
5565
5647
  country: project.country,
@@ -5689,14 +5771,14 @@ async function queryRoutes(app, opts) {
5689
5771
  validProviders: validNames
5690
5772
  });
5691
5773
  }
5692
- const count = body.count ?? 5;
5774
+ const count2 = body.count ?? 5;
5693
5775
  if (!opts.onGenerateQueries) {
5694
5776
  throw notImplemented("Keyword generation is not supported in this deployment");
5695
5777
  }
5696
5778
  const existingRows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
5697
5779
  const existingQueries = existingRows.map((r) => r.query);
5698
5780
  try {
5699
- const generated = await opts.onGenerateQueries(provider, count, {
5781
+ const generated = await opts.onGenerateQueries(provider, count2, {
5700
5782
  domain: project.canonicalDomain,
5701
5783
  displayName: project.displayName,
5702
5784
  country: project.country,
@@ -7438,10 +7520,10 @@ function computeQueryChanges(projectQueries, cutoff) {
7438
7520
  }
7439
7521
  const days = [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0]));
7440
7522
  if (days.length <= 1) return [];
7441
- return days.slice(1).map(([date, count]) => ({
7523
+ return days.slice(1).map(([date, count2]) => ({
7442
7524
  date: (/* @__PURE__ */ new Date(date + "T00:00:00.000Z")).toISOString(),
7443
- delta: count,
7444
- label: `+${count} kp`
7525
+ delta: count2,
7526
+ label: `+${count2} kp`
7445
7527
  }));
7446
7528
  }
7447
7529
  function computeTrend(buckets, rateKey) {
@@ -7506,15 +7588,15 @@ function buildRankedList(domains, limit) {
7506
7588
  function buildCategoryCounts(counts) {
7507
7589
  let grandTotal = 0;
7508
7590
  for (const domains of counts.values()) {
7509
- for (const count of domains.values()) grandTotal += count;
7591
+ for (const count2 of domains.values()) grandTotal += count2;
7510
7592
  }
7511
7593
  const result = [];
7512
7594
  for (const [category, domains] of counts) {
7513
7595
  let categoryTotal = 0;
7514
7596
  const domainEntries = [];
7515
- for (const [domain, count] of domains) {
7516
- categoryTotal += count;
7517
- domainEntries.push({ domain, count });
7597
+ for (const [domain, count2] of domains) {
7598
+ categoryTotal += count2;
7599
+ domainEntries.push({ domain, count: count2 });
7518
7600
  }
7519
7601
  domainEntries.sort((a, b) => b.count - a.count);
7520
7602
  result.push({
@@ -8488,8 +8570,8 @@ function gscDateRange(report) {
8488
8570
  const end = summary?.periodEnd || gsc?.periodEnd || gsc?.trend.at(-1)?.date || "";
8489
8571
  return formatDateRange(start, end);
8490
8572
  }
8491
- function pluralize(count, singular, plural = `${singular}s`) {
8492
- return count === 1 ? singular : plural;
8573
+ function pluralize(count2, singular, plural = `${singular}s`) {
8574
+ return count2 === 1 ? singular : plural;
8493
8575
  }
8494
8576
  var PROVIDER_DISPLAY_NAMES = {
8495
8577
  gemini: "Gemini",
@@ -10292,9 +10374,9 @@ function renderInsights(report) {
10292
10374
  );
10293
10375
  }
10294
10376
  const haveDeduped = list.every((i) => typeof i.instanceCount === "number");
10295
- const rows = (haveDeduped ? list.map((i) => ({ rep: i, count: i.instanceCount })) : groupInsights(list).map((g) => ({ rep: g.representative, count: g.count }))).map(({ rep: i, count }) => {
10377
+ const rows = (haveDeduped ? list.map((i) => ({ rep: i, count: i.instanceCount })) : groupInsights(list).map((g) => ({ rep: g.representative, count: g.count }))).map(({ rep: i, count: count2 }) => {
10296
10378
  const tone = severityTone(i.severity);
10297
- const countChip = count > 1 ? ` <span class="badge tone-neutral">\xD7 ${count}</span>` : "";
10379
+ const countChip = count2 > 1 ? ` <span class="badge tone-neutral">\xD7 ${count2}</span>` : "";
10298
10380
  return `<tr>
10299
10381
  <td class="col-severity"><span class="badge tone-${tone}">${escapeHtml(reportSeverityLabel(i.severity))}</span></td>
10300
10382
  <td class="col-title">${escapeHtml(i.title)}${countChip}</td>
@@ -11494,9 +11576,9 @@ function contentActionVerb(action) {
11494
11576
  return "Add schema to";
11495
11577
  }
11496
11578
  }
11497
- function confidenceFromEvidence(count) {
11498
- if (count >= 3) return "high";
11499
- if (count >= 1) return "medium";
11579
+ function confidenceFromEvidence(count2) {
11580
+ if (count2 >= 3) return "high";
11581
+ if (count2 >= 1) return "medium";
11500
11582
  return "low";
11501
11583
  }
11502
11584
  function actionAudienceMatches2(action, audience) {
@@ -13003,6 +13085,10 @@ var SCHEMA_TABLE = {
13003
13085
  SchedulableRunKind: schedulableRunKindSchema,
13004
13086
  ScheduleDto: scheduleDtoSchema,
13005
13087
  SettingsDto: settingsDtoSchema,
13088
+ SiteAuditPagesResponseDto: siteAuditPagesResponseSchema,
13089
+ SiteAuditRunResponseDto: siteAuditRunResponseSchema,
13090
+ SiteAuditScoreDto: siteAuditScoreSchema,
13091
+ SiteAuditTrendResponseDto: siteAuditTrendResponseSchema,
13006
13092
  SnapshotDiffResponse: snapshotDiffResponseSchema,
13007
13093
  SnapshotListResponse: snapshotListResponseSchema,
13008
13094
  SnapshotReportDto: snapshotReportSchema,
@@ -16688,6 +16774,78 @@ var routeCatalog = [
16688
16774
  400: errorResponse("Session is not completed, or invalid request body."),
16689
16775
  404: errorResponse("Project or session not found.")
16690
16776
  }
16777
+ },
16778
+ {
16779
+ method: "get",
16780
+ path: "/api/v1/projects/{name}/technical-aeo",
16781
+ summary: "Get the Technical AEO scorecard for a project",
16782
+ description: "Returns the latest completed/partial site-audit: aggregate 0\u2013100 score, page counts, the full per-factor scorecard (site-level averages with pass/partial/fail distribution), cross-cutting issues, prioritized fixes, and the delta vs the previous audit. When the project has never been audited, `hasData` is false and the numeric fields are zeroed \u2014 render an onboarding state.",
16783
+ tags: ["technical-aeo"],
16784
+ parameters: [nameParameter],
16785
+ responses: {
16786
+ 200: jsonResponse("Technical AEO scorecard returned.", "SiteAuditScoreDto"),
16787
+ 404: errorResponse("Project not found.")
16788
+ }
16789
+ },
16790
+ {
16791
+ method: "get",
16792
+ path: "/api/v1/projects/{name}/technical-aeo/pages",
16793
+ summary: "List audited pages from the latest site-audit run",
16794
+ description: "Returns the per-page breakdown of the latest completed/partial site-audit run (paginated). Filter to `status=error` to surface unreachable pages; sort `score-asc` (default) to surface the worst-scoring pages first.",
16795
+ tags: ["technical-aeo"],
16796
+ parameters: [
16797
+ nameParameter,
16798
+ { name: "status", in: "query", description: "Filter by page audit status: `success` or `error`.", schema: { type: "string", enum: ["success", "error"] } },
16799
+ { name: "sort", in: "query", description: "Sort order: `score-asc` (default), `score-desc`, or `url`.", schema: { type: "string", enum: ["score-asc", "score-desc", "url"] } },
16800
+ limitQueryParameter,
16801
+ offsetQueryParameter
16802
+ ],
16803
+ responses: {
16804
+ 200: jsonResponse("Audited pages returned.", "SiteAuditPagesResponseDto"),
16805
+ 404: errorResponse("Project not found.")
16806
+ }
16807
+ },
16808
+ {
16809
+ method: "get",
16810
+ path: "/api/v1/projects/{name}/technical-aeo/trend",
16811
+ summary: "Get the Technical AEO aggregate-score trend",
16812
+ description: "Returns historical aggregate scores across completed/partial site-audit runs, oldest-first, for the trend chart.",
16813
+ tags: ["technical-aeo"],
16814
+ parameters: [
16815
+ nameParameter,
16816
+ { name: "limit", in: "query", description: "Max data points returned (most recent runs). Default 30.", schema: integerSchema }
16817
+ ],
16818
+ responses: {
16819
+ 200: jsonResponse("Technical AEO trend returned.", "SiteAuditTrendResponseDto"),
16820
+ 404: errorResponse("Project not found.")
16821
+ }
16822
+ },
16823
+ {
16824
+ method: "post",
16825
+ path: "/api/v1/projects/{name}/technical-aeo/runs",
16826
+ summary: "Trigger a Technical AEO site-audit run",
16827
+ description: "Queues a `site-audit` run that crawls the project sitemap and audits every reachable page. Idempotent: if a site-audit run is already queued/running for the project, returns that run instead of starting a second.",
16828
+ tags: ["technical-aeo"],
16829
+ parameters: [nameParameter],
16830
+ requestBody: {
16831
+ required: false,
16832
+ content: {
16833
+ "application/json": {
16834
+ schema: {
16835
+ type: "object",
16836
+ properties: {
16837
+ sitemapUrl: { ...stringSchema, description: "Override the sitemap URL. Defaults to https://<canonicalDomain>/sitemap.xml." },
16838
+ limit: { ...integerSchema, description: "Cap pages audited (highest sitemap <priority> first). Max 2000." }
16839
+ }
16840
+ }
16841
+ }
16842
+ }
16843
+ },
16844
+ responses: {
16845
+ 200: jsonResponse("Site-audit run queued (or the in-flight run returned).", "SiteAuditRunResponseDto"),
16846
+ 400: errorResponse("Invalid site-audit request."),
16847
+ 404: errorResponse("Project not found.")
16848
+ }
16691
16849
  }
16692
16850
  ];
16693
16851
  var canonryLocalRouteCatalog = [
@@ -17201,6 +17359,9 @@ async function scheduleRoutes(app, opts) {
17201
17359
  if (kind === SchedulableRunKinds["backlinks-sync"] && providers && providers.length > 0) {
17202
17360
  throw validationError('"providers" is not valid for kind "backlinks-sync"');
17203
17361
  }
17362
+ if (kind === SchedulableRunKinds["site-audit"] && providers && providers.length > 0) {
17363
+ throw validationError('"providers" is not valid for kind "site-audit"');
17364
+ }
17204
17365
  const validNames = opts.validProviderNames ?? [];
17205
17366
  if (validNames.length && providers?.length) {
17206
17367
  const invalid = providers.filter((p) => !validNames.includes(p));
@@ -18898,12 +19059,12 @@ async function getLodging(accessToken, locationName, opts = {}) {
18898
19059
  }
18899
19060
  }
18900
19061
  function countPopulatedGroups(lodging) {
18901
- let count = 0;
19062
+ let count2 = 0;
18902
19063
  for (const [key, value] of Object.entries(lodging)) {
18903
19064
  if (key === "name" || key === "metadata") continue;
18904
- if (isPopulated(value)) count++;
19065
+ if (isPopulated(value)) count2++;
18905
19066
  }
18906
- return count;
19067
+ return count2;
18907
19068
  }
18908
19069
  function isPopulated(value) {
18909
19070
  if (value === null || value === void 0) return false;
@@ -30949,11 +31110,41 @@ var scopesCheck3 = {
30949
31110
  return summarizePerSourceResults("scopes", "scopes", results);
30950
31111
  }
30951
31112
  };
31113
+ var cacheBlindSpotCheck = {
31114
+ id: "traffic.source.cache-blindspot",
31115
+ category: CheckCategories.integrations,
31116
+ scope: CheckScopes.project,
31117
+ title: "WordPress traffic cache blind spot",
31118
+ run: (ctx) => {
31119
+ if (!ctx.project) return skippedNoProject4();
31120
+ const wpSources = loadProbes(ctx).filter(
31121
+ (s) => s.sourceType === TrafficSourceTypes.wordpress
31122
+ );
31123
+ if (wpSources.length === 0) {
31124
+ return {
31125
+ status: CheckStatuses.skipped,
31126
+ code: "traffic.cache-blindspot.no-wordpress-source",
31127
+ summary: "No WordPress traffic source connected, so the plugin cache blind spot does not apply (log and edge adapters see cache-served requests)."
31128
+ };
31129
+ }
31130
+ return {
31131
+ status: CheckStatuses.warn,
31132
+ code: "traffic.cache-blindspot.wordpress-plugin",
31133
+ summary: `${wpSources.length} WordPress traffic source(s) capture via the Canonry Traffic Logger plugin, which only logs requests that execute PHP. A full-page cache (LiteSpeed, WP Rocket, W3 Total Cache, WP Super Cache) or CDN serves cached pages before PHP runs, so cache-served page views, including live AI user-fetches such as Claude-User and ChatGPT-User, are not captured. Bot crawls of uncached endpoints (sitemap, feeds, assets, cache misses) still appear, which can make capture look healthy while real page views go uncounted.`,
31134
+ remediation: 'Exclude AI user-agents from the page cache so their requests reach PHP: LiteSpeed Cache has "Do Not Cache User Agents" under Cache > Excludes; WP Rocket uses the `rocket_cache_reject_ua` filter; W3 Total Cache and WP Super Cache have a "Rejected User Agents" box. Mirror the rule at any CDN in front. For cache-independent capture, ingest from server or edge access logs (a `cloud-run` or `vercel` source, or an edge worker) instead of the WordPress plugin.',
31135
+ details: {
31136
+ wordpressSourceCount: wpSources.length,
31137
+ wordpressSourceIds: wpSources.map((s) => s.id)
31138
+ }
31139
+ };
31140
+ }
31141
+ };
30952
31142
  var TRAFFIC_SOURCE_CHECKS = [
30953
31143
  sourceConnectedCheck,
30954
31144
  recentDataCheck,
30955
31145
  credentialsCheck,
30956
- scopesCheck3
31146
+ scopesCheck3,
31147
+ cacheBlindSpotCheck
30957
31148
  ];
30958
31149
 
30959
31150
  // ../api-routes/src/doctor/checks/wordpress-publish.ts
@@ -31645,6 +31836,151 @@ function dedupeStrings(input) {
31645
31836
  return out;
31646
31837
  }
31647
31838
 
31839
+ // ../api-routes/src/technical-aeo.ts
31840
+ import crypto27 from "crypto";
31841
+ import { and as and23, asc as asc3, count, desc as desc16, eq as eq32, inArray as inArray11 } from "drizzle-orm";
31842
+ var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
31843
+ function emptyScore(projectName) {
31844
+ return {
31845
+ project: projectName,
31846
+ hasData: false,
31847
+ runId: null,
31848
+ runStatus: null,
31849
+ sitemapUrl: null,
31850
+ auditedAt: null,
31851
+ aggregateScore: 0,
31852
+ pagesDiscovered: 0,
31853
+ pagesAudited: 0,
31854
+ pagesSkipped: 0,
31855
+ pagesErrored: 0,
31856
+ deltaScore: null,
31857
+ trend: null,
31858
+ previousScore: null,
31859
+ previousAuditedAt: null,
31860
+ factors: [],
31861
+ crossCuttingIssues: [],
31862
+ prioritizedFixes: []
31863
+ };
31864
+ }
31865
+ function parsePositiveInt(value, fallback, max) {
31866
+ const n = typeof value === "string" ? Number.parseInt(value, 10) : typeof value === "number" ? value : NaN;
31867
+ if (!Number.isFinite(n) || n < 0) return fallback;
31868
+ return Math.min(max, Math.floor(n));
31869
+ }
31870
+ async function technicalAeoRoutes(app, opts) {
31871
+ app.get("/projects/:name/technical-aeo", async (request) => {
31872
+ const project = resolveProject(app.db, request.params.name);
31873
+ const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
31874
+ eq32(siteAuditSnapshots.projectId, project.id),
31875
+ eq32(runs.kind, RunKinds["site-audit"]),
31876
+ inArray11(runs.status, SURFACEABLE_STATUSES),
31877
+ notProbeRun()
31878
+ )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
31879
+ const latest = rows[0];
31880
+ if (!latest) return emptyScore(project.name);
31881
+ const snap = latest.snap;
31882
+ const previous = rows[1]?.snap ?? null;
31883
+ const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
31884
+ const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
31885
+ return {
31886
+ project: project.name,
31887
+ hasData: true,
31888
+ runId: snap.runId,
31889
+ runStatus: latest.runStatus,
31890
+ sitemapUrl: snap.sitemapUrl,
31891
+ auditedAt: snap.auditedAt,
31892
+ aggregateScore: snap.aggregateScore,
31893
+ pagesDiscovered: snap.pagesDiscovered,
31894
+ pagesAudited: snap.pagesAudited,
31895
+ pagesSkipped: snap.pagesSkipped,
31896
+ pagesErrored: snap.pagesErrored,
31897
+ deltaScore,
31898
+ trend,
31899
+ previousScore: previous?.aggregateScore ?? null,
31900
+ previousAuditedAt: previous?.auditedAt ?? null,
31901
+ factors: snap.factorAverages,
31902
+ crossCuttingIssues: snap.crossCuttingIssues,
31903
+ prioritizedFixes: snap.prioritizedFixes
31904
+ };
31905
+ });
31906
+ app.get("/projects/:name/technical-aeo/pages", async (request) => {
31907
+ const project = resolveProject(app.db, request.params.name);
31908
+ const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
31909
+ eq32(siteAuditSnapshots.projectId, project.id),
31910
+ eq32(runs.kind, RunKinds["site-audit"]),
31911
+ inArray11(runs.status, SURFACEABLE_STATUSES),
31912
+ notProbeRun()
31913
+ )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
31914
+ if (!latest) {
31915
+ return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
31916
+ }
31917
+ const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
31918
+ const conds = [eq32(siteAuditPages.runId, latest.runId)];
31919
+ if (statusFilter) conds.push(eq32(siteAuditPages.status, statusFilter));
31920
+ const where = and23(...conds);
31921
+ const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
31922
+ const total = totalRow?.value ?? 0;
31923
+ const limit = parsePositiveInt(request.query.limit, 100, 500);
31924
+ const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
31925
+ const orderBy = request.query.sort === "score-desc" ? desc16(siteAuditPages.overallScore) : request.query.sort === "url" ? asc3(siteAuditPages.url) : asc3(siteAuditPages.overallScore);
31926
+ const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
31927
+ const pages = rows.map((row) => ({
31928
+ url: row.url,
31929
+ overallScore: row.overallScore,
31930
+ status: row.status === "error" ? "error" : "success",
31931
+ error: row.error,
31932
+ factors: row.factors
31933
+ }));
31934
+ return { project: project.name, runId: latest.runId, auditedAt: latest.auditedAt, total, pages };
31935
+ });
31936
+ app.get("/projects/:name/technical-aeo/trend", async (request) => {
31937
+ const project = resolveProject(app.db, request.params.name);
31938
+ const limit = parsePositiveInt(request.query.limit, 30, 365);
31939
+ const rows = app.db.select({
31940
+ runId: siteAuditSnapshots.runId,
31941
+ auditedAt: siteAuditSnapshots.auditedAt,
31942
+ aggregateScore: siteAuditSnapshots.aggregateScore,
31943
+ pagesAudited: siteAuditSnapshots.pagesAudited
31944
+ }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
31945
+ eq32(siteAuditSnapshots.projectId, project.id),
31946
+ eq32(runs.kind, RunKinds["site-audit"]),
31947
+ inArray11(runs.status, SURFACEABLE_STATUSES),
31948
+ notProbeRun()
31949
+ )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
31950
+ return { project: project.name, points: rows.reverse() };
31951
+ });
31952
+ app.post("/projects/:name/technical-aeo/runs", async (request) => {
31953
+ const project = resolveProject(app.db, request.params.name);
31954
+ const parsed = siteAuditRunRequestSchema.safeParse(request.body ?? {});
31955
+ if (!parsed.success) {
31956
+ throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
31957
+ }
31958
+ const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and23(
31959
+ eq32(runs.projectId, project.id),
31960
+ eq32(runs.kind, RunKinds["site-audit"]),
31961
+ inArray11(runs.status, [RunStatuses.queued, RunStatuses.running])
31962
+ )).get();
31963
+ if (existing) {
31964
+ return { runId: existing.id, status: existing.status };
31965
+ }
31966
+ const now = (/* @__PURE__ */ new Date()).toISOString();
31967
+ const runId = crypto27.randomUUID();
31968
+ app.db.insert(runs).values({
31969
+ id: runId,
31970
+ projectId: project.id,
31971
+ kind: RunKinds["site-audit"],
31972
+ status: RunStatuses.queued,
31973
+ trigger: RunTriggers.manual,
31974
+ createdAt: now
31975
+ }).run();
31976
+ opts.onSiteAuditRequested?.(runId, project.id, {
31977
+ sitemapUrl: parsed.data.sitemapUrl,
31978
+ limit: parsed.data.limit
31979
+ });
31980
+ return { runId, status: RunStatuses.queued };
31981
+ });
31982
+ }
31983
+
31648
31984
  // ../api-routes/src/index.ts
31649
31985
  async function apiRoutes(app, opts) {
31650
31986
  app.decorate("db", opts.db);
@@ -31809,6 +32145,9 @@ async function apiRoutes(app, opts) {
31809
32145
  await api.register(discoveryRoutes, {
31810
32146
  onDiscoveryRunRequested: opts.onDiscoveryRunRequested
31811
32147
  });
32148
+ await api.register(technicalAeoRoutes, {
32149
+ onSiteAuditRequested: opts.onSiteAuditRequested
32150
+ });
31812
32151
  await api.register(doctorRoutes, {
31813
32152
  googleConnectionStore: opts.googleConnectionStore,
31814
32153
  bingConnectionStore: opts.bingConnectionStore,
@@ -31964,7 +32303,7 @@ function buildTrafficSourceValidators(opts) {
31964
32303
  }
31965
32304
 
31966
32305
  // src/intelligence-service.ts
31967
- import crypto27 from "crypto";
32306
+ import crypto28 from "crypto";
31968
32307
 
31969
32308
  // src/logger.ts
31970
32309
  var IS_TTY = process.stdout.isTTY === true;
@@ -32195,16 +32534,16 @@ var IntelligenceService = class {
32195
32534
  */
32196
32535
  analyzeAndPersist(runId, projectId) {
32197
32536
  const recentRuns = this.db.select().from(runs).where(
32198
- and23(
32199
- eq32(runs.projectId, projectId),
32200
- or5(eq32(runs.status, "completed"), eq32(runs.status, "partial")),
32537
+ and24(
32538
+ eq33(runs.projectId, projectId),
32539
+ or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
32201
32540
  // Defensive: RunCoordinator already skips probes before this is
32202
32541
  // called, but if a future call site invokes analyzeAndPersist
32203
32542
  // directly for a probe, probes still must not pollute the
32204
32543
  // intelligence window.
32205
32544
  ne5(runs.trigger, RunTriggers.probe)
32206
32545
  )
32207
- ).orderBy(desc16(runs.finishedAt), desc16(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
32546
+ ).orderBy(desc17(runs.finishedAt), desc17(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
32208
32547
  if (recentRuns.length === 0) {
32209
32548
  log.info("intelligence.skip", { runId, reason: "no completed runs" });
32210
32549
  return null;
@@ -32279,7 +32618,7 @@ var IntelligenceService = class {
32279
32618
  * Returns the persisted insights so the coordinator can count critical/high.
32280
32619
  */
32281
32620
  analyzeAndPersistGbp(runId, projectId) {
32282
- const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq32(runs.id, runId)).get();
32621
+ const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq33(runs.id, runId)).get();
32283
32622
  if (!runRow) {
32284
32623
  log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
32285
32624
  this.persistGbpInsights(runId, projectId, [], []);
@@ -32287,9 +32626,9 @@ var IntelligenceService = class {
32287
32626
  }
32288
32627
  const windowStart = runRow.startedAt ?? runRow.createdAt;
32289
32628
  const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
32290
- const selected = this.db.select().from(gbpLocations).where(and23(
32291
- eq32(gbpLocations.projectId, projectId),
32292
- eq32(gbpLocations.selected, true),
32629
+ const selected = this.db.select().from(gbpLocations).where(and24(
32630
+ eq33(gbpLocations.projectId, projectId),
32631
+ eq33(gbpLocations.selected, true),
32293
32632
  gte6(gbpLocations.syncedAt, windowStart),
32294
32633
  lte3(gbpLocations.syncedAt, windowEnd)
32295
32634
  )).all();
@@ -32324,10 +32663,10 @@ var IntelligenceService = class {
32324
32663
  }
32325
32664
  /** Build the per-location signal bundle the GBP analyzer consumes. */
32326
32665
  buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
32327
- const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and23(eq32(gbpDailyMetrics.projectId, projectId), eq32(gbpDailyMetrics.locationName, locationName))).all();
32328
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and23(eq32(gbpPlaceActions.projectId, projectId), eq32(gbpPlaceActions.locationName, locationName))).all();
32329
- const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and23(eq32(gbpLodgingSnapshots.projectId, projectId), eq32(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc16(gbpLodgingSnapshots.syncedAt)).limit(1).get();
32330
- const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and23(eq32(gbpPlaceDetails.projectId, projectId), eq32(gbpPlaceDetails.locationName, locationName))).orderBy(desc16(gbpPlaceDetails.syncedAt)).limit(1).get();
32666
+ const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and24(eq33(gbpDailyMetrics.projectId, projectId), eq33(gbpDailyMetrics.locationName, locationName))).all();
32667
+ const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and24(eq33(gbpPlaceActions.projectId, projectId), eq33(gbpPlaceActions.locationName, locationName))).all();
32668
+ const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and24(eq33(gbpLodgingSnapshots.projectId, projectId), eq33(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).limit(1).get();
32669
+ const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and24(eq33(gbpPlaceDetails.projectId, projectId), eq33(gbpPlaceDetails.locationName, locationName))).orderBy(desc17(gbpPlaceDetails.syncedAt)).limit(1).get();
32331
32670
  const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
32332
32671
  const summary = buildGbpSummary({
32333
32672
  locationName,
@@ -32359,7 +32698,7 @@ var IntelligenceService = class {
32359
32698
  /** Build the month-over-month keyword series for a location from the
32360
32699
  * accumulating gbp_keyword_monthly table (latest complete month vs prior). */
32361
32700
  buildGbpKeywordTrend(projectId, locationName) {
32362
- const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and23(eq32(gbpKeywordMonthly.projectId, projectId), eq32(gbpKeywordMonthly.locationName, locationName))).all();
32701
+ const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and24(eq33(gbpKeywordMonthly.projectId, projectId), eq33(gbpKeywordMonthly.locationName, locationName))).all();
32363
32702
  if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
32364
32703
  const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
32365
32704
  const recentMonth = months[0] ?? null;
@@ -32390,7 +32729,7 @@ var IntelligenceService = class {
32390
32729
  */
32391
32730
  persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
32392
32731
  const covered = new Set(coveredLocationNames);
32393
- const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and23(eq32(insights.projectId, projectId), eq32(insights.provider, GBP_INSIGHT_PROVIDER))).all();
32732
+ const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and24(eq33(insights.projectId, projectId), eq33(insights.provider, GBP_INSIGHT_PROVIDER))).all();
32394
32733
  const staleIds = [];
32395
32734
  const dismissedSlots = /* @__PURE__ */ new Set();
32396
32735
  for (const row of existing) {
@@ -32401,7 +32740,7 @@ var IntelligenceService = class {
32401
32740
  }
32402
32741
  this.db.transaction((tx) => {
32403
32742
  for (const id of staleIds) {
32404
- tx.delete(insights).where(eq32(insights.id, id)).run();
32743
+ tx.delete(insights).where(eq33(insights.id, id)).run();
32405
32744
  }
32406
32745
  for (const insight of gbpInsights) {
32407
32746
  const parsed = parseGbpInsightId(insight.id);
@@ -32479,7 +32818,7 @@ var IntelligenceService = class {
32479
32818
  * create per run + aggregate). DB is left untouched.
32480
32819
  */
32481
32820
  backfill(projectName, opts, onProgress) {
32482
- const project = this.db.select().from(projects).where(eq32(projects.name, projectName)).get();
32821
+ const project = this.db.select().from(projects).where(eq33(projects.name, projectName)).get();
32483
32822
  if (!project) {
32484
32823
  throw new Error(`Project "${projectName}" not found`);
32485
32824
  }
@@ -32492,13 +32831,13 @@ var IntelligenceService = class {
32492
32831
  sinceTimestamp = parsed;
32493
32832
  }
32494
32833
  const allRuns = this.db.select().from(runs).where(
32495
- and23(
32496
- eq32(runs.projectId, project.id),
32497
- or5(eq32(runs.status, "completed"), eq32(runs.status, "partial")),
32834
+ and24(
32835
+ eq33(runs.projectId, project.id),
32836
+ or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
32498
32837
  // Backfill must not replay probe runs as if they were real sweeps.
32499
32838
  ne5(runs.trigger, RunTriggers.probe)
32500
32839
  )
32501
- ).orderBy(asc3(runs.finishedAt)).all();
32840
+ ).orderBy(asc4(runs.finishedAt)).all();
32502
32841
  let startIdx = 0;
32503
32842
  let endIdx = allRuns.length;
32504
32843
  if (opts?.fromRunId) {
@@ -32527,7 +32866,7 @@ var IntelligenceService = class {
32527
32866
  let wouldDeleteTotal = 0;
32528
32867
  const existingByRunId = /* @__PURE__ */ new Map();
32529
32868
  if (isDryRun && targetRuns.length > 0) {
32530
- const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray11(insights.runId, targetRuns.map((r) => r.id))).all();
32869
+ const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray12(insights.runId, targetRuns.map((r) => r.id))).all();
32531
32870
  for (const r of rows) {
32532
32871
  if (r.runId == null) continue;
32533
32872
  existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
@@ -32573,7 +32912,7 @@ var IntelligenceService = class {
32573
32912
  return { processed, skipped, totalInsights };
32574
32913
  }
32575
32914
  loadTrackedCompetitors(projectId) {
32576
- return this.db.select({ domain: competitors.domain }).from(competitors).where(eq32(competitors.projectId, projectId)).all().map((r) => r.domain);
32915
+ return this.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, projectId)).all().map((r) => r.domain);
32577
32916
  }
32578
32917
  /**
32579
32918
  * Wipe transition signals from an analysis result while keeping health.
@@ -32594,15 +32933,15 @@ var IntelligenceService = class {
32594
32933
  }
32595
32934
  persistResult(result, runId, projectId) {
32596
32935
  const previouslyDismissed = /* @__PURE__ */ new Set();
32597
- const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq32(insights.runId, runId)).all();
32936
+ const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq33(insights.runId, runId)).all();
32598
32937
  for (const row of existingInsights) {
32599
32938
  if (row.dismissed) {
32600
32939
  previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
32601
32940
  }
32602
32941
  }
32603
32942
  this.db.transaction((tx) => {
32604
- tx.delete(insights).where(eq32(insights.runId, runId)).run();
32605
- tx.delete(healthSnapshots).where(eq32(healthSnapshots.runId, runId)).run();
32943
+ tx.delete(insights).where(eq33(insights.runId, runId)).run();
32944
+ tx.delete(healthSnapshots).where(eq33(healthSnapshots.runId, runId)).run();
32606
32945
  const now = (/* @__PURE__ */ new Date()).toISOString();
32607
32946
  for (const insight of result.insights) {
32608
32947
  const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
@@ -32622,7 +32961,7 @@ var IntelligenceService = class {
32622
32961
  }).run();
32623
32962
  }
32624
32963
  tx.insert(healthSnapshots).values({
32625
- id: crypto27.randomUUID(),
32964
+ id: crypto28.randomUUID(),
32626
32965
  projectId,
32627
32966
  runId,
32628
32967
  overallCitedRate: String(result.health.overallCitedRate),
@@ -32653,28 +32992,28 @@ var IntelligenceService = class {
32653
32992
  applySeverityTiering(rawInsights, excludeRunId, projectId) {
32654
32993
  const regressions = rawInsights.filter((i) => i.type === "regression");
32655
32994
  if (regressions.length === 0) return rawInsights;
32656
- const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq32(gscSearchData.projectId, projectId)).all();
32995
+ const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq33(gscSearchData.projectId, projectId)).all();
32657
32996
  const gscConnected = gscRows.length > 0;
32658
32997
  const gscImpressionsByQuery = /* @__PURE__ */ new Map();
32659
32998
  for (const row of gscRows) {
32660
32999
  const key = row.query.toLowerCase();
32661
33000
  gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
32662
33001
  }
32663
- const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq32(projects.id, projectId)).get();
33002
+ const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq33(projects.id, projectId)).get();
32664
33003
  const locationCount = Math.max(
32665
33004
  1,
32666
33005
  (projectRow?.locations ?? []).length
32667
33006
  );
32668
33007
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
32669
33008
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
32670
- and23(
32671
- eq32(runs.projectId, projectId),
32672
- eq32(runs.kind, RunKinds["answer-visibility"]),
32673
- or5(eq32(runs.status, "completed"), eq32(runs.status, "partial")),
33009
+ and24(
33010
+ eq33(runs.projectId, projectId),
33011
+ eq33(runs.kind, RunKinds["answer-visibility"]),
33012
+ or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
32674
33013
  // Defensive — see top of file.
32675
33014
  ne5(runs.trigger, RunTriggers.probe)
32676
33015
  )
32677
- ).orderBy(desc16(runs.createdAt), desc16(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
33016
+ ).orderBy(desc17(runs.createdAt), desc17(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
32678
33017
  const recentGroups = groupRunsByCreatedAt(recentRunRows);
32679
33018
  const recentRunIds = [];
32680
33019
  const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
@@ -32690,7 +33029,7 @@ var IntelligenceService = class {
32690
33029
  const haveHistory = recentRunIds.length > 0;
32691
33030
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
32692
33031
  if (haveHistory) {
32693
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and23(eq32(insights.type, "regression"), inArray11(insights.runId, recentRunIds))).all();
33032
+ const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and24(eq33(insights.type, "regression"), inArray12(insights.runId, recentRunIds))).all();
32694
33033
  const regressionGroups = /* @__PURE__ */ new Map();
32695
33034
  for (const row of priorRows) {
32696
33035
  if (!row.runId) continue;
@@ -32719,7 +33058,7 @@ var IntelligenceService = class {
32719
33058
  });
32720
33059
  }
32721
33060
  buildRunData(runId, projectId, completedAt, location = null) {
32722
- const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq32(projects.id, projectId)).get();
33061
+ const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq33(projects.id, projectId)).get();
32723
33062
  const projectDomains = projectDomainRow ? effectiveDomains({
32724
33063
  canonicalDomain: projectDomainRow.canonicalDomain,
32725
33064
  ownedDomains: projectDomainRow.ownedDomains
@@ -32735,7 +33074,7 @@ var IntelligenceService = class {
32735
33074
  citedDomains: querySnapshots.citedDomains,
32736
33075
  competitorOverlap: querySnapshots.competitorOverlap,
32737
33076
  snapshotLocation: querySnapshots.location
32738
- }).from(querySnapshots).leftJoin(queries, eq32(querySnapshots.queryId, queries.id)).where(eq32(querySnapshots.runId, runId)).all();
33077
+ }).from(querySnapshots).leftJoin(queries, eq33(querySnapshots.queryId, queries.id)).where(eq33(querySnapshots.runId, runId)).all();
32739
33078
  const snapshots = [];
32740
33079
  let orphanCount = 0;
32741
33080
  for (const r of rows) {
@@ -32786,6 +33125,8 @@ export {
32786
33125
  gscSearchData,
32787
33126
  gscUrlInspections,
32788
33127
  gscCoverageSnapshots,
33128
+ siteAuditSnapshots,
33129
+ siteAuditPages,
32789
33130
  bingCoverageSnapshots,
32790
33131
  bingUrlInspections,
32791
33132
  gaTrafficSnapshots,