@ainyc/canonry 4.72.4 → 4.75.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 (30) hide show
  1. package/README.md +1 -1
  2. package/assets/agent-workspace/skills/aero/references/orchestration.md +1 -1
  3. package/assets/agent-workspace/skills/canonry/SKILL.md +1 -1
  4. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +17 -0
  5. package/assets/assets/{BacklinksPage-CjfpwZEH.js → BacklinksPage-CwAplOLo.js} +1 -1
  6. package/assets/assets/{ChartPrimitives-Ckf2FrUy.js → ChartPrimitives-EGp5HFxn.js} +1 -1
  7. package/assets/assets/ProjectPage-C-zhkBKK.js +6 -0
  8. package/assets/assets/{RunRow-BuFyG0V_.js → RunRow-YFN2PwH-.js} +1 -1
  9. package/assets/assets/{RunsPage-D-pr000K.js → RunsPage-DlKS8zaS.js} +1 -1
  10. package/assets/assets/{SettingsPage-CiaapCYn.js → SettingsPage-Q0OZKjMD.js} +1 -1
  11. package/assets/assets/{TrafficPage-B40xytJD.js → TrafficPage-BbySUnhy.js} +1 -1
  12. package/assets/assets/{TrafficSourceDetailPage-7hHem-gM.js → TrafficSourceDetailPage-BGzuvTYp.js} +1 -1
  13. package/assets/assets/{extract-error-message-3GkDsu1h.js → extract-error-message-Czt2jFxA.js} +1 -1
  14. package/assets/assets/index-CFVX11lK.css +1 -0
  15. package/assets/assets/{index-BVdH2O9w.js → index-DYsYdWV8.js} +118 -118
  16. package/assets/assets/{server-traffic-CsgPsudZ.js → server-traffic-BzIFKqGS.js} +1 -1
  17. package/assets/assets/{trash-2-B8Ipf9rI.js → trash-2-DKCkbZUb.js} +1 -1
  18. package/assets/index.html +2 -2
  19. package/dist/{chunk-JXFNERK4.js → chunk-JNAKRK77.js} +1103 -998
  20. package/dist/{chunk-HOKVBMOD.js → chunk-JUWU2DV6.js} +402 -81
  21. package/dist/{chunk-SRBO33HB.js → chunk-QY5WZWU4.js} +403 -202
  22. package/dist/{chunk-ZUBBADMR.js → chunk-WFMEK34V.js} +162 -1
  23. package/dist/cli.js +237 -31
  24. package/dist/index.d.ts +10 -0
  25. package/dist/index.js +4 -4
  26. package/dist/{intelligence-service-CSW4R4I7.js → intelligence-service-L2A5MFB4.js} +2 -2
  27. package/dist/mcp.js +2 -2
  28. package/package.json +10 -10
  29. package/assets/assets/ProjectPage-DZeplYeC.js +0 -6
  30. 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,
@@ -100,6 +101,7 @@ import {
100
101
  effectiveBrandNames,
101
102
  effectiveDomains,
102
103
  emptyCitationVisibility,
104
+ escapeLikePattern,
103
105
  extractAnswerMentions,
104
106
  findDuplicateLocationLabels,
105
107
  forbidden,
@@ -186,6 +188,11 @@ import {
186
188
  scheduleUpsertRequestSchema,
187
189
  serializeRunError,
188
190
  settingsDtoSchema,
191
+ siteAuditPagesResponseSchema,
192
+ siteAuditRunRequestSchema,
193
+ siteAuditRunResponseSchema,
194
+ siteAuditScoreSchema,
195
+ siteAuditTrendResponseSchema,
189
196
  snapshotDiffResponseSchema,
190
197
  snapshotListResponseSchema,
191
198
  snapshotReportSchema,
@@ -223,10 +230,10 @@ import {
223
230
  wordpressSchemaDeployResultDtoSchema,
224
231
  wordpressSchemaStatusResultDtoSchema,
225
232
  wordpressStatusDtoSchema
226
- } from "./chunk-JXFNERK4.js";
233
+ } from "./chunk-JNAKRK77.js";
227
234
 
228
235
  // 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";
236
+ 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
237
 
231
238
  // ../db/src/client.ts
232
239
  import { mkdirSync } from "fs";
@@ -285,6 +292,8 @@ __export(schema_exports, {
285
292
  recommendationExplanations: () => recommendationExplanations,
286
293
  runs: () => runs,
287
294
  schedules: () => schedules,
295
+ siteAuditPages: () => siteAuditPages,
296
+ siteAuditSnapshots: () => siteAuditSnapshots,
288
297
  trafficSources: () => trafficSources,
289
298
  usageCounters: () => usageCounters
290
299
  });
@@ -533,6 +542,39 @@ var gscCoverageSnapshots = sqliteTable("gsc_coverage_snapshots", {
533
542
  index("idx_gsc_coverage_snap_project_date").on(table.projectId, table.date),
534
543
  index("idx_gsc_coverage_snap_run").on(table.syncRunId)
535
544
  ]);
545
+ var siteAuditSnapshots = sqliteTable("site_audit_snapshots", {
546
+ id: text("id").primaryKey(),
547
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
548
+ runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
549
+ sitemapUrl: text("sitemap_url").notNull(),
550
+ auditedAt: text("audited_at").notNull(),
551
+ aggregateScore: integer("aggregate_score").notNull().default(0),
552
+ pagesDiscovered: integer("pages_discovered").notNull().default(0),
553
+ pagesAudited: integer("pages_audited").notNull().default(0),
554
+ pagesSkipped: integer("pages_skipped").notNull().default(0),
555
+ pagesErrored: integer("pages_errored").notNull().default(0),
556
+ factorAverages: text("factor_averages", { mode: "json" }).$type().notNull().default([]),
557
+ crossCuttingIssues: text("cross_cutting_issues", { mode: "json" }).$type().notNull().default([]),
558
+ prioritizedFixes: text("prioritized_fixes", { mode: "json" }).$type().notNull().default([]),
559
+ createdAt: text("created_at").notNull()
560
+ }, (table) => [
561
+ index("idx_site_audit_snap_project_created").on(table.projectId, table.createdAt),
562
+ index("idx_site_audit_snap_run").on(table.runId)
563
+ ]);
564
+ var siteAuditPages = sqliteTable("site_audit_pages", {
565
+ id: text("id").primaryKey(),
566
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
567
+ runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
568
+ url: text("url").notNull(),
569
+ overallScore: integer("overall_score").notNull().default(0),
570
+ status: text("status").notNull(),
571
+ error: text("error"),
572
+ factors: text("factors", { mode: "json" }).$type().notNull().default([]),
573
+ createdAt: text("created_at").notNull()
574
+ }, (table) => [
575
+ index("idx_site_audit_pages_run").on(table.runId),
576
+ index("idx_site_audit_pages_project_score").on(table.projectId, table.overallScore)
577
+ ]);
536
578
  var bingCoverageSnapshots = sqliteTable("bing_coverage_snapshots", {
537
579
  id: text("id").primaryKey(),
538
580
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
@@ -2780,6 +2822,47 @@ var MIGRATION_VERSIONS = [
2780
2822
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_recommendation_briefs_unique ON recommendation_briefs(project_id, target_ref, prompt_version)`,
2781
2823
  `CREATE INDEX IF NOT EXISTS idx_recommendation_briefs_project ON recommendation_briefs(project_id)`
2782
2824
  ]
2825
+ },
2826
+ {
2827
+ // Technical AEO — site-wide audit persistence. `site_audit_snapshots` is the
2828
+ // per-run summary (drives the score + trend); `site_audit_pages` is the
2829
+ // per-page breakdown (drives the drill-down table). Both cascade off runs so
2830
+ // a run delete cleans up its audit data.
2831
+ version: 75,
2832
+ name: "site-audit-tables",
2833
+ statements: [
2834
+ `CREATE TABLE IF NOT EXISTS site_audit_snapshots (
2835
+ id TEXT PRIMARY KEY,
2836
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
2837
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2838
+ sitemap_url TEXT NOT NULL,
2839
+ audited_at TEXT NOT NULL,
2840
+ aggregate_score INTEGER NOT NULL DEFAULT 0,
2841
+ pages_discovered INTEGER NOT NULL DEFAULT 0,
2842
+ pages_audited INTEGER NOT NULL DEFAULT 0,
2843
+ pages_skipped INTEGER NOT NULL DEFAULT 0,
2844
+ pages_errored INTEGER NOT NULL DEFAULT 0,
2845
+ factor_averages TEXT NOT NULL DEFAULT '[]',
2846
+ cross_cutting_issues TEXT NOT NULL DEFAULT '[]',
2847
+ prioritized_fixes TEXT NOT NULL DEFAULT '[]',
2848
+ created_at TEXT NOT NULL
2849
+ )`,
2850
+ `CREATE INDEX IF NOT EXISTS idx_site_audit_snap_project_created ON site_audit_snapshots(project_id, created_at)`,
2851
+ `CREATE INDEX IF NOT EXISTS idx_site_audit_snap_run ON site_audit_snapshots(run_id)`,
2852
+ `CREATE TABLE IF NOT EXISTS site_audit_pages (
2853
+ id TEXT PRIMARY KEY,
2854
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
2855
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
2856
+ url TEXT NOT NULL,
2857
+ overall_score INTEGER NOT NULL DEFAULT 0,
2858
+ status TEXT NOT NULL,
2859
+ error TEXT,
2860
+ factors TEXT NOT NULL DEFAULT '[]',
2861
+ created_at TEXT NOT NULL
2862
+ )`,
2863
+ `CREATE INDEX IF NOT EXISTS idx_site_audit_pages_run ON site_audit_pages(run_id)`,
2864
+ `CREATE INDEX IF NOT EXISTS idx_site_audit_pages_project_score ON site_audit_pages(project_id, overall_score)`
2865
+ ]
2783
2866
  }
2784
2867
  ];
2785
2868
  function isDuplicateColumnError(err) {
@@ -4098,15 +4181,15 @@ function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains, topDo
4098
4181
  totalCitations++;
4099
4182
  }
4100
4183
  }
4101
- const categories = [...categoryCounts.entries()].map(([category, { label, count }]) => ({
4184
+ const categories = [...categoryCounts.entries()].map(([category, { label, count: count2 }]) => ({
4102
4185
  category,
4103
4186
  label,
4104
- count,
4105
- sharePct: totalCitations > 0 ? Math.round(count / totalCitations * 100) : 0
4187
+ count: count2,
4188
+ sharePct: totalCitations > 0 ? Math.round(count2 / totalCitations * 100) : 0
4106
4189
  })).sort((a, b) => b.count - a.count);
4107
- const topDomains = [...domainCounts.entries()].map(([domain, count]) => ({
4190
+ const topDomains = [...domainCounts.entries()].map(([domain, count2]) => ({
4108
4191
  domain,
4109
- count,
4192
+ count: count2,
4110
4193
  isCompetitor: citedDomainBelongsToProject(domain, competitorDomains)
4111
4194
  })).sort((a, b) => b.count - a.count).slice(0, topDomainsLimit);
4112
4195
  return { categories, topDomains };
@@ -4947,7 +5030,7 @@ var SKIP_PATHS = ["/health"];
4947
5030
  function shouldSkipAuth(url) {
4948
5031
  if (SKIP_PATHS.includes(url)) return true;
4949
5032
  if (url.endsWith("/openapi.json")) return true;
4950
- if (url.includes("/google/callback")) return true;
5033
+ if (url.endsWith("/google/callback")) return true;
4951
5034
  if (url.endsWith("/session") || url.endsWith("/session/setup")) return true;
4952
5035
  return false;
4953
5036
  }
@@ -5203,7 +5286,7 @@ async function projectRoutes(app, opts) {
5203
5286
  app.get("/projects/:name/delete-preview", async (request, reply) => {
5204
5287
  const project = resolveProject(app.db, request.params.name);
5205
5288
  const pid = project.id;
5206
- const count = (n) => n ?? 0;
5289
+ const count2 = (n) => n ?? 0;
5207
5290
  const queryCount = app.db.select({ n: sql3`count(*)` }).from(queries).where(eq3(queries.projectId, pid)).get();
5208
5291
  const competitorCount = app.db.select({ n: sql3`count(*)` }).from(competitors).where(eq3(competitors.projectId, pid)).get();
5209
5292
  const runCount = app.db.select({ n: sql3`count(*)` }).from(runs).where(eq3(runs.projectId, pid)).get();
@@ -5213,14 +5296,14 @@ async function projectRoutes(app, opts) {
5213
5296
  return reply.send({
5214
5297
  project: { id: project.id, name: project.name },
5215
5298
  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)
5299
+ queries: count2(queryCount?.n),
5300
+ competitors: count2(competitorCount?.n),
5301
+ runs: count2(runCount?.n),
5302
+ snapshots: count2(snapshotCount?.n),
5303
+ insights: count2(insightCount?.n)
5221
5304
  },
5222
5305
  detachedRows: {
5223
- auditLog: count(auditLogCount?.n)
5306
+ auditLog: count2(auditLogCount?.n)
5224
5307
  }
5225
5308
  });
5226
5309
  });
@@ -5552,14 +5635,14 @@ async function queryRoutes(app, opts) {
5552
5635
  validProviders: validNames
5553
5636
  });
5554
5637
  }
5555
- const count = body.count ?? 5;
5638
+ const count2 = body.count ?? 5;
5556
5639
  if (!opts.onGenerateQueries) {
5557
5640
  throw notImplemented("Query generation is not supported in this deployment");
5558
5641
  }
5559
5642
  const existingRows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
5560
5643
  const existingQueries = existingRows.map((r) => r.query);
5561
5644
  try {
5562
- const generated = await opts.onGenerateQueries(provider, count, {
5645
+ const generated = await opts.onGenerateQueries(provider, count2, {
5563
5646
  domain: project.canonicalDomain,
5564
5647
  displayName: project.displayName,
5565
5648
  country: project.country,
@@ -5689,14 +5772,14 @@ async function queryRoutes(app, opts) {
5689
5772
  validProviders: validNames
5690
5773
  });
5691
5774
  }
5692
- const count = body.count ?? 5;
5775
+ const count2 = body.count ?? 5;
5693
5776
  if (!opts.onGenerateQueries) {
5694
5777
  throw notImplemented("Keyword generation is not supported in this deployment");
5695
5778
  }
5696
5779
  const existingRows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
5697
5780
  const existingQueries = existingRows.map((r) => r.query);
5698
5781
  try {
5699
- const generated = await opts.onGenerateQueries(provider, count, {
5782
+ const generated = await opts.onGenerateQueries(provider, count2, {
5700
5783
  domain: project.canonicalDomain,
5701
5784
  displayName: project.displayName,
5702
5785
  country: project.country,
@@ -7391,10 +7474,10 @@ function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt) {
7391
7474
  const latest = new Date(projectRuns[projectRuns.length - 1].createdAt);
7392
7475
  const buckets = [];
7393
7476
  let start = new Date(earliest);
7394
- start.setHours(0, 0, 0, 0);
7477
+ start.setUTCHours(0, 0, 0, 0);
7395
7478
  while (start <= latest) {
7396
7479
  const end = new Date(start);
7397
- end.setDate(end.getDate() + bucketDays);
7480
+ end.setUTCDate(end.getUTCDate() + bucketDays);
7398
7481
  const startISO = start.toISOString();
7399
7482
  const endISO = end.toISOString();
7400
7483
  const inBucket = snapshots.filter((s) => s.createdAt >= startISO && s.createdAt < endISO);
@@ -7438,10 +7521,10 @@ function computeQueryChanges(projectQueries, cutoff) {
7438
7521
  }
7439
7522
  const days = [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0]));
7440
7523
  if (days.length <= 1) return [];
7441
- return days.slice(1).map(([date, count]) => ({
7524
+ return days.slice(1).map(([date, count2]) => ({
7442
7525
  date: (/* @__PURE__ */ new Date(date + "T00:00:00.000Z")).toISOString(),
7443
- delta: count,
7444
- label: `+${count} kp`
7526
+ delta: count2,
7527
+ label: `+${count2} kp`
7445
7528
  }));
7446
7529
  }
7447
7530
  function computeTrend(buckets, rateKey) {
@@ -7506,15 +7589,15 @@ function buildRankedList(domains, limit) {
7506
7589
  function buildCategoryCounts(counts) {
7507
7590
  let grandTotal = 0;
7508
7591
  for (const domains of counts.values()) {
7509
- for (const count of domains.values()) grandTotal += count;
7592
+ for (const count2 of domains.values()) grandTotal += count2;
7510
7593
  }
7511
7594
  const result = [];
7512
7595
  for (const [category, domains] of counts) {
7513
7596
  let categoryTotal = 0;
7514
7597
  const domainEntries = [];
7515
- for (const [domain, count] of domains) {
7516
- categoryTotal += count;
7517
- domainEntries.push({ domain, count });
7598
+ for (const [domain, count2] of domains) {
7599
+ categoryTotal += count2;
7600
+ domainEntries.push({ domain, count: count2 });
7518
7601
  }
7519
7602
  domainEntries.sort((a, b) => b.count - a.count);
7520
7603
  result.push({
@@ -8488,8 +8571,8 @@ function gscDateRange(report) {
8488
8571
  const end = summary?.periodEnd || gsc?.periodEnd || gsc?.trend.at(-1)?.date || "";
8489
8572
  return formatDateRange(start, end);
8490
8573
  }
8491
- function pluralize(count, singular, plural = `${singular}s`) {
8492
- return count === 1 ? singular : plural;
8574
+ function pluralize(count2, singular, plural = `${singular}s`) {
8575
+ return count2 === 1 ? singular : plural;
8493
8576
  }
8494
8577
  var PROVIDER_DISPLAY_NAMES = {
8495
8578
  gemini: "Gemini",
@@ -10292,9 +10375,9 @@ function renderInsights(report) {
10292
10375
  );
10293
10376
  }
10294
10377
  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 }) => {
10378
+ 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
10379
  const tone = severityTone(i.severity);
10297
- const countChip = count > 1 ? ` <span class="badge tone-neutral">\xD7 ${count}</span>` : "";
10380
+ const countChip = count2 > 1 ? ` <span class="badge tone-neutral">\xD7 ${count2}</span>` : "";
10298
10381
  return `<tr>
10299
10382
  <td class="col-severity"><span class="badge tone-${tone}">${escapeHtml(reportSeverityLabel(i.severity))}</span></td>
10300
10383
  <td class="col-title">${escapeHtml(i.title)}${countChip}</td>
@@ -11494,9 +11577,9 @@ function contentActionVerb(action) {
11494
11577
  return "Add schema to";
11495
11578
  }
11496
11579
  }
11497
- function confidenceFromEvidence(count) {
11498
- if (count >= 3) return "high";
11499
- if (count >= 1) return "medium";
11580
+ function confidenceFromEvidence(count2) {
11581
+ if (count2 >= 3) return "high";
11582
+ if (count2 >= 1) return "medium";
11500
11583
  return "low";
11501
11584
  }
11502
11585
  function actionAudienceMatches2(action, audience) {
@@ -12493,9 +12576,6 @@ function clampSearchLimit(raw) {
12493
12576
  if (parsed > SEARCH_HIT_HARD_LIMIT) return SEARCH_HIT_HARD_LIMIT;
12494
12577
  return parsed;
12495
12578
  }
12496
- function escapeLikePattern(value) {
12497
- return value.replace(/[\\%_]/g, (match) => `\\${match}`);
12498
- }
12499
12579
  function summarizeRun(run) {
12500
12580
  return {
12501
12581
  id: run.id,
@@ -13003,6 +13083,10 @@ var SCHEMA_TABLE = {
13003
13083
  SchedulableRunKind: schedulableRunKindSchema,
13004
13084
  ScheduleDto: scheduleDtoSchema,
13005
13085
  SettingsDto: settingsDtoSchema,
13086
+ SiteAuditPagesResponseDto: siteAuditPagesResponseSchema,
13087
+ SiteAuditRunResponseDto: siteAuditRunResponseSchema,
13088
+ SiteAuditScoreDto: siteAuditScoreSchema,
13089
+ SiteAuditTrendResponseDto: siteAuditTrendResponseSchema,
13006
13090
  SnapshotDiffResponse: snapshotDiffResponseSchema,
13007
13091
  SnapshotListResponse: snapshotListResponseSchema,
13008
13092
  SnapshotReportDto: snapshotReportSchema,
@@ -16688,6 +16772,78 @@ var routeCatalog = [
16688
16772
  400: errorResponse("Session is not completed, or invalid request body."),
16689
16773
  404: errorResponse("Project or session not found.")
16690
16774
  }
16775
+ },
16776
+ {
16777
+ method: "get",
16778
+ path: "/api/v1/projects/{name}/technical-aeo",
16779
+ summary: "Get the Technical AEO scorecard for a project",
16780
+ 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.",
16781
+ tags: ["technical-aeo"],
16782
+ parameters: [nameParameter],
16783
+ responses: {
16784
+ 200: jsonResponse("Technical AEO scorecard returned.", "SiteAuditScoreDto"),
16785
+ 404: errorResponse("Project not found.")
16786
+ }
16787
+ },
16788
+ {
16789
+ method: "get",
16790
+ path: "/api/v1/projects/{name}/technical-aeo/pages",
16791
+ summary: "List audited pages from the latest site-audit run",
16792
+ 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.",
16793
+ tags: ["technical-aeo"],
16794
+ parameters: [
16795
+ nameParameter,
16796
+ { name: "status", in: "query", description: "Filter by page audit status: `success` or `error`.", schema: { type: "string", enum: ["success", "error"] } },
16797
+ { name: "sort", in: "query", description: "Sort order: `score-asc` (default), `score-desc`, or `url`.", schema: { type: "string", enum: ["score-asc", "score-desc", "url"] } },
16798
+ limitQueryParameter,
16799
+ offsetQueryParameter
16800
+ ],
16801
+ responses: {
16802
+ 200: jsonResponse("Audited pages returned.", "SiteAuditPagesResponseDto"),
16803
+ 404: errorResponse("Project not found.")
16804
+ }
16805
+ },
16806
+ {
16807
+ method: "get",
16808
+ path: "/api/v1/projects/{name}/technical-aeo/trend",
16809
+ summary: "Get the Technical AEO aggregate-score trend",
16810
+ description: "Returns historical aggregate scores across completed/partial site-audit runs, oldest-first, for the trend chart.",
16811
+ tags: ["technical-aeo"],
16812
+ parameters: [
16813
+ nameParameter,
16814
+ { name: "limit", in: "query", description: "Max data points returned (most recent runs). Default 30.", schema: integerSchema }
16815
+ ],
16816
+ responses: {
16817
+ 200: jsonResponse("Technical AEO trend returned.", "SiteAuditTrendResponseDto"),
16818
+ 404: errorResponse("Project not found.")
16819
+ }
16820
+ },
16821
+ {
16822
+ method: "post",
16823
+ path: "/api/v1/projects/{name}/technical-aeo/runs",
16824
+ summary: "Trigger a Technical AEO site-audit run",
16825
+ 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.",
16826
+ tags: ["technical-aeo"],
16827
+ parameters: [nameParameter],
16828
+ requestBody: {
16829
+ required: false,
16830
+ content: {
16831
+ "application/json": {
16832
+ schema: {
16833
+ type: "object",
16834
+ properties: {
16835
+ sitemapUrl: { ...stringSchema, description: "Override the sitemap URL. Defaults to https://<canonicalDomain>/sitemap.xml." },
16836
+ limit: { ...integerSchema, description: "Cap pages audited (highest sitemap <priority> first). Max 2000." }
16837
+ }
16838
+ }
16839
+ }
16840
+ }
16841
+ },
16842
+ responses: {
16843
+ 200: jsonResponse("Site-audit run queued (or the in-flight run returned).", "SiteAuditRunResponseDto"),
16844
+ 400: errorResponse("Invalid site-audit request."),
16845
+ 404: errorResponse("Project not found.")
16846
+ }
16691
16847
  }
16692
16848
  ];
16693
16849
  var canonryLocalRouteCatalog = [
@@ -17201,6 +17357,9 @@ async function scheduleRoutes(app, opts) {
17201
17357
  if (kind === SchedulableRunKinds["backlinks-sync"] && providers && providers.length > 0) {
17202
17358
  throw validationError('"providers" is not valid for kind "backlinks-sync"');
17203
17359
  }
17360
+ if (kind === SchedulableRunKinds["site-audit"] && providers && providers.length > 0) {
17361
+ throw validationError('"providers" is not valid for kind "site-audit"');
17362
+ }
17204
17363
  const validNames = opts.validProviderNames ?? [];
17205
17364
  if (validNames.length && providers?.length) {
17206
17365
  const invalid = providers.filter((p) => !validNames.includes(p));
@@ -18898,12 +19057,12 @@ async function getLodging(accessToken, locationName, opts = {}) {
18898
19057
  }
18899
19058
  }
18900
19059
  function countPopulatedGroups(lodging) {
18901
- let count = 0;
19060
+ let count2 = 0;
18902
19061
  for (const [key, value] of Object.entries(lodging)) {
18903
19062
  if (key === "name" || key === "metadata") continue;
18904
- if (isPopulated(value)) count++;
19063
+ if (isPopulated(value)) count2++;
18905
19064
  }
18906
- return count;
19065
+ return count2;
18907
19066
  }
18908
19067
  function isPopulated(value) {
18909
19068
  if (value === null || value === void 0) return false;
@@ -19239,8 +19398,8 @@ async function googleRoutes(app, opts) {
19239
19398
  if (startDate) conditions.push(sql8`${gscSearchData.date} >= ${startDate}`);
19240
19399
  else if (cutoffDate) conditions.push(sql8`${gscSearchData.date} >= ${cutoffDate}`);
19241
19400
  if (endDate) conditions.push(sql8`${gscSearchData.date} <= ${endDate}`);
19242
- if (query) conditions.push(sql8`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
19243
- if (page) conditions.push(sql8`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
19401
+ if (query) conditions.push(sql8`${gscSearchData.query} LIKE ${"%" + escapeLikePattern(query) + "%"} ESCAPE '\\'`);
19402
+ if (page) conditions.push(sql8`${gscSearchData.page} LIKE ${"%" + escapeLikePattern(page) + "%"} ESCAPE '\\'`);
19244
19403
  const limitVal = Math.max(parseInt(limit ?? "500", 10) || 0, 1);
19245
19404
  const offsetVal = Math.max(parseInt(offset ?? "0", 10) || 0, 0);
19246
19405
  const rows = app.db.select().from(gscSearchData).where(and13(...conditions)).orderBy(desc10(gscSearchData.date)).limit(limitVal).offset(offsetVal).all();
@@ -22854,10 +23013,17 @@ async function withWordpressErrorHandling(handler) {
22854
23013
  }
22855
23014
  }
22856
23015
  async function wordpressRoutes(app, opts) {
23016
+ const allowLoopback = opts.allowLoopbackWebhooks === true;
22857
23017
  function requireStore() {
22858
23018
  if (opts.wordpressConnectionStore) return opts.wordpressConnectionStore;
22859
23019
  throw validationError("WordPress connection storage is not configured for this deployment");
22860
23020
  }
23021
+ async function assertWordpressUrlAllowed(rawUrl, field) {
23022
+ const check = await resolveWebhookTarget(rawUrl, { allowLoopback });
23023
+ if (!check.ok) {
23024
+ throw validationError(`${field} ${check.message.replace(/^"url" /, "")}`);
23025
+ }
23026
+ }
22861
23027
  function requireConnection(store, projectName) {
22862
23028
  const connection = store.getConnection(projectName);
22863
23029
  if (!connection) {
@@ -22877,6 +23043,8 @@ async function wordpressRoutes(app, opts) {
22877
23043
  if (defaultEnv === "staging" && !stagingUrl) {
22878
23044
  throw validationError('defaultEnv "staging" requires stagingUrl');
22879
23045
  }
23046
+ await assertWordpressUrlAllowed(url, "url");
23047
+ if (stagingUrl) await assertWordpressUrlAllowed(stagingUrl, "stagingUrl");
22880
23048
  const now = (/* @__PURE__ */ new Date()).toISOString();
22881
23049
  const existing = store.getConnection(project.name);
22882
23050
  const nextConnection = {
@@ -23205,6 +23373,8 @@ async function wordpressRoutes(app, opts) {
23205
23373
  if (defaultEnv === "staging" && !stagingUrl) {
23206
23374
  throw validationError('defaultEnv "staging" requires stagingUrl');
23207
23375
  }
23376
+ await assertWordpressUrlAllowed(url, "url");
23377
+ if (stagingUrl) await assertWordpressUrlAllowed(stagingUrl, "stagingUrl");
23208
23378
  const steps = [];
23209
23379
  let connection = null;
23210
23380
  let pageUrls = [];
@@ -31675,6 +31845,151 @@ function dedupeStrings(input) {
31675
31845
  return out;
31676
31846
  }
31677
31847
 
31848
+ // ../api-routes/src/technical-aeo.ts
31849
+ import crypto27 from "crypto";
31850
+ import { and as and23, asc as asc3, count, desc as desc16, eq as eq32, inArray as inArray11 } from "drizzle-orm";
31851
+ var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
31852
+ function emptyScore(projectName) {
31853
+ return {
31854
+ project: projectName,
31855
+ hasData: false,
31856
+ runId: null,
31857
+ runStatus: null,
31858
+ sitemapUrl: null,
31859
+ auditedAt: null,
31860
+ aggregateScore: 0,
31861
+ pagesDiscovered: 0,
31862
+ pagesAudited: 0,
31863
+ pagesSkipped: 0,
31864
+ pagesErrored: 0,
31865
+ deltaScore: null,
31866
+ trend: null,
31867
+ previousScore: null,
31868
+ previousAuditedAt: null,
31869
+ factors: [],
31870
+ crossCuttingIssues: [],
31871
+ prioritizedFixes: []
31872
+ };
31873
+ }
31874
+ function parsePositiveInt(value, fallback, max) {
31875
+ const n = typeof value === "string" ? Number.parseInt(value, 10) : typeof value === "number" ? value : NaN;
31876
+ if (!Number.isFinite(n) || n < 0) return fallback;
31877
+ return Math.min(max, Math.floor(n));
31878
+ }
31879
+ async function technicalAeoRoutes(app, opts) {
31880
+ app.get("/projects/:name/technical-aeo", async (request) => {
31881
+ const project = resolveProject(app.db, request.params.name);
31882
+ const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
31883
+ eq32(siteAuditSnapshots.projectId, project.id),
31884
+ eq32(runs.kind, RunKinds["site-audit"]),
31885
+ inArray11(runs.status, SURFACEABLE_STATUSES),
31886
+ notProbeRun()
31887
+ )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
31888
+ const latest = rows[0];
31889
+ if (!latest) return emptyScore(project.name);
31890
+ const snap = latest.snap;
31891
+ const previous = rows[1]?.snap ?? null;
31892
+ const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
31893
+ const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
31894
+ return {
31895
+ project: project.name,
31896
+ hasData: true,
31897
+ runId: snap.runId,
31898
+ runStatus: latest.runStatus,
31899
+ sitemapUrl: snap.sitemapUrl,
31900
+ auditedAt: snap.auditedAt,
31901
+ aggregateScore: snap.aggregateScore,
31902
+ pagesDiscovered: snap.pagesDiscovered,
31903
+ pagesAudited: snap.pagesAudited,
31904
+ pagesSkipped: snap.pagesSkipped,
31905
+ pagesErrored: snap.pagesErrored,
31906
+ deltaScore,
31907
+ trend,
31908
+ previousScore: previous?.aggregateScore ?? null,
31909
+ previousAuditedAt: previous?.auditedAt ?? null,
31910
+ factors: snap.factorAverages,
31911
+ crossCuttingIssues: snap.crossCuttingIssues,
31912
+ prioritizedFixes: snap.prioritizedFixes
31913
+ };
31914
+ });
31915
+ app.get("/projects/:name/technical-aeo/pages", async (request) => {
31916
+ const project = resolveProject(app.db, request.params.name);
31917
+ const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
31918
+ eq32(siteAuditSnapshots.projectId, project.id),
31919
+ eq32(runs.kind, RunKinds["site-audit"]),
31920
+ inArray11(runs.status, SURFACEABLE_STATUSES),
31921
+ notProbeRun()
31922
+ )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
31923
+ if (!latest) {
31924
+ return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
31925
+ }
31926
+ const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
31927
+ const conds = [eq32(siteAuditPages.runId, latest.runId)];
31928
+ if (statusFilter) conds.push(eq32(siteAuditPages.status, statusFilter));
31929
+ const where = and23(...conds);
31930
+ const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
31931
+ const total = totalRow?.value ?? 0;
31932
+ const limit = parsePositiveInt(request.query.limit, 100, 500);
31933
+ const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
31934
+ const orderBy = request.query.sort === "score-desc" ? desc16(siteAuditPages.overallScore) : request.query.sort === "url" ? asc3(siteAuditPages.url) : asc3(siteAuditPages.overallScore);
31935
+ const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
31936
+ const pages = rows.map((row) => ({
31937
+ url: row.url,
31938
+ overallScore: row.overallScore,
31939
+ status: row.status === "error" ? "error" : "success",
31940
+ error: row.error,
31941
+ factors: row.factors
31942
+ }));
31943
+ return { project: project.name, runId: latest.runId, auditedAt: latest.auditedAt, total, pages };
31944
+ });
31945
+ app.get("/projects/:name/technical-aeo/trend", async (request) => {
31946
+ const project = resolveProject(app.db, request.params.name);
31947
+ const limit = parsePositiveInt(request.query.limit, 30, 365);
31948
+ const rows = app.db.select({
31949
+ runId: siteAuditSnapshots.runId,
31950
+ auditedAt: siteAuditSnapshots.auditedAt,
31951
+ aggregateScore: siteAuditSnapshots.aggregateScore,
31952
+ pagesAudited: siteAuditSnapshots.pagesAudited
31953
+ }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
31954
+ eq32(siteAuditSnapshots.projectId, project.id),
31955
+ eq32(runs.kind, RunKinds["site-audit"]),
31956
+ inArray11(runs.status, SURFACEABLE_STATUSES),
31957
+ notProbeRun()
31958
+ )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
31959
+ return { project: project.name, points: rows.reverse() };
31960
+ });
31961
+ app.post("/projects/:name/technical-aeo/runs", async (request) => {
31962
+ const project = resolveProject(app.db, request.params.name);
31963
+ const parsed = siteAuditRunRequestSchema.safeParse(request.body ?? {});
31964
+ if (!parsed.success) {
31965
+ throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
31966
+ }
31967
+ const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and23(
31968
+ eq32(runs.projectId, project.id),
31969
+ eq32(runs.kind, RunKinds["site-audit"]),
31970
+ inArray11(runs.status, [RunStatuses.queued, RunStatuses.running])
31971
+ )).get();
31972
+ if (existing) {
31973
+ return { runId: existing.id, status: existing.status };
31974
+ }
31975
+ const now = (/* @__PURE__ */ new Date()).toISOString();
31976
+ const runId = crypto27.randomUUID();
31977
+ app.db.insert(runs).values({
31978
+ id: runId,
31979
+ projectId: project.id,
31980
+ kind: RunKinds["site-audit"],
31981
+ status: RunStatuses.queued,
31982
+ trigger: RunTriggers.manual,
31983
+ createdAt: now
31984
+ }).run();
31985
+ opts.onSiteAuditRequested?.(runId, project.id, {
31986
+ sitemapUrl: parsed.data.sitemapUrl,
31987
+ limit: parsed.data.limit
31988
+ });
31989
+ return { runId, status: RunStatuses.queued };
31990
+ });
31991
+ }
31992
+
31678
31993
  // ../api-routes/src/index.ts
31679
31994
  async function apiRoutes(app, opts) {
31680
31995
  app.decorate("db", opts.db);
@@ -31801,7 +32116,8 @@ async function apiRoutes(app, opts) {
31801
32116
  });
31802
32117
  await api.register(wordpressRoutes, {
31803
32118
  wordpressConnectionStore: opts.wordpressConnectionStore,
31804
- routePrefix: opts.routePrefix ?? "/api/v1"
32119
+ routePrefix: opts.routePrefix ?? "/api/v1",
32120
+ allowLoopbackWebhooks: opts.allowLoopbackWebhooks
31805
32121
  });
31806
32122
  await api.register(cdpRoutes, {
31807
32123
  getCdpStatus: opts.getCdpStatus,
@@ -31839,6 +32155,9 @@ async function apiRoutes(app, opts) {
31839
32155
  await api.register(discoveryRoutes, {
31840
32156
  onDiscoveryRunRequested: opts.onDiscoveryRunRequested
31841
32157
  });
32158
+ await api.register(technicalAeoRoutes, {
32159
+ onSiteAuditRequested: opts.onSiteAuditRequested
32160
+ });
31842
32161
  await api.register(doctorRoutes, {
31843
32162
  googleConnectionStore: opts.googleConnectionStore,
31844
32163
  bingConnectionStore: opts.bingConnectionStore,
@@ -31994,7 +32313,7 @@ function buildTrafficSourceValidators(opts) {
31994
32313
  }
31995
32314
 
31996
32315
  // src/intelligence-service.ts
31997
- import crypto27 from "crypto";
32316
+ import crypto28 from "crypto";
31998
32317
 
31999
32318
  // src/logger.ts
32000
32319
  var IS_TTY = process.stdout.isTTY === true;
@@ -32225,16 +32544,16 @@ var IntelligenceService = class {
32225
32544
  */
32226
32545
  analyzeAndPersist(runId, projectId) {
32227
32546
  const recentRuns = this.db.select().from(runs).where(
32228
- and23(
32229
- eq32(runs.projectId, projectId),
32230
- or5(eq32(runs.status, "completed"), eq32(runs.status, "partial")),
32547
+ and24(
32548
+ eq33(runs.projectId, projectId),
32549
+ or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
32231
32550
  // Defensive: RunCoordinator already skips probes before this is
32232
32551
  // called, but if a future call site invokes analyzeAndPersist
32233
32552
  // directly for a probe, probes still must not pollute the
32234
32553
  // intelligence window.
32235
32554
  ne5(runs.trigger, RunTriggers.probe)
32236
32555
  )
32237
- ).orderBy(desc16(runs.finishedAt), desc16(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
32556
+ ).orderBy(desc17(runs.finishedAt), desc17(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
32238
32557
  if (recentRuns.length === 0) {
32239
32558
  log.info("intelligence.skip", { runId, reason: "no completed runs" });
32240
32559
  return null;
@@ -32309,7 +32628,7 @@ var IntelligenceService = class {
32309
32628
  * Returns the persisted insights so the coordinator can count critical/high.
32310
32629
  */
32311
32630
  analyzeAndPersistGbp(runId, projectId) {
32312
- const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq32(runs.id, runId)).get();
32631
+ const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq33(runs.id, runId)).get();
32313
32632
  if (!runRow) {
32314
32633
  log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
32315
32634
  this.persistGbpInsights(runId, projectId, [], []);
@@ -32317,9 +32636,9 @@ var IntelligenceService = class {
32317
32636
  }
32318
32637
  const windowStart = runRow.startedAt ?? runRow.createdAt;
32319
32638
  const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
32320
- const selected = this.db.select().from(gbpLocations).where(and23(
32321
- eq32(gbpLocations.projectId, projectId),
32322
- eq32(gbpLocations.selected, true),
32639
+ const selected = this.db.select().from(gbpLocations).where(and24(
32640
+ eq33(gbpLocations.projectId, projectId),
32641
+ eq33(gbpLocations.selected, true),
32323
32642
  gte6(gbpLocations.syncedAt, windowStart),
32324
32643
  lte3(gbpLocations.syncedAt, windowEnd)
32325
32644
  )).all();
@@ -32354,10 +32673,10 @@ var IntelligenceService = class {
32354
32673
  }
32355
32674
  /** Build the per-location signal bundle the GBP analyzer consumes. */
32356
32675
  buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
32357
- 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();
32358
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and23(eq32(gbpPlaceActions.projectId, projectId), eq32(gbpPlaceActions.locationName, locationName))).all();
32359
- 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();
32360
- 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();
32676
+ 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();
32677
+ const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and24(eq33(gbpPlaceActions.projectId, projectId), eq33(gbpPlaceActions.locationName, locationName))).all();
32678
+ 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();
32679
+ 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();
32361
32680
  const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
32362
32681
  const summary = buildGbpSummary({
32363
32682
  locationName,
@@ -32389,7 +32708,7 @@ var IntelligenceService = class {
32389
32708
  /** Build the month-over-month keyword series for a location from the
32390
32709
  * accumulating gbp_keyword_monthly table (latest complete month vs prior). */
32391
32710
  buildGbpKeywordTrend(projectId, locationName) {
32392
- 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();
32711
+ 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();
32393
32712
  if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
32394
32713
  const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
32395
32714
  const recentMonth = months[0] ?? null;
@@ -32420,7 +32739,7 @@ var IntelligenceService = class {
32420
32739
  */
32421
32740
  persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
32422
32741
  const covered = new Set(coveredLocationNames);
32423
- 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();
32742
+ 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();
32424
32743
  const staleIds = [];
32425
32744
  const dismissedSlots = /* @__PURE__ */ new Set();
32426
32745
  for (const row of existing) {
@@ -32431,7 +32750,7 @@ var IntelligenceService = class {
32431
32750
  }
32432
32751
  this.db.transaction((tx) => {
32433
32752
  for (const id of staleIds) {
32434
- tx.delete(insights).where(eq32(insights.id, id)).run();
32753
+ tx.delete(insights).where(eq33(insights.id, id)).run();
32435
32754
  }
32436
32755
  for (const insight of gbpInsights) {
32437
32756
  const parsed = parseGbpInsightId(insight.id);
@@ -32509,7 +32828,7 @@ var IntelligenceService = class {
32509
32828
  * create per run + aggregate). DB is left untouched.
32510
32829
  */
32511
32830
  backfill(projectName, opts, onProgress) {
32512
- const project = this.db.select().from(projects).where(eq32(projects.name, projectName)).get();
32831
+ const project = this.db.select().from(projects).where(eq33(projects.name, projectName)).get();
32513
32832
  if (!project) {
32514
32833
  throw new Error(`Project "${projectName}" not found`);
32515
32834
  }
@@ -32522,13 +32841,13 @@ var IntelligenceService = class {
32522
32841
  sinceTimestamp = parsed;
32523
32842
  }
32524
32843
  const allRuns = this.db.select().from(runs).where(
32525
- and23(
32526
- eq32(runs.projectId, project.id),
32527
- or5(eq32(runs.status, "completed"), eq32(runs.status, "partial")),
32844
+ and24(
32845
+ eq33(runs.projectId, project.id),
32846
+ or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
32528
32847
  // Backfill must not replay probe runs as if they were real sweeps.
32529
32848
  ne5(runs.trigger, RunTriggers.probe)
32530
32849
  )
32531
- ).orderBy(asc3(runs.finishedAt)).all();
32850
+ ).orderBy(asc4(runs.finishedAt)).all();
32532
32851
  let startIdx = 0;
32533
32852
  let endIdx = allRuns.length;
32534
32853
  if (opts?.fromRunId) {
@@ -32557,7 +32876,7 @@ var IntelligenceService = class {
32557
32876
  let wouldDeleteTotal = 0;
32558
32877
  const existingByRunId = /* @__PURE__ */ new Map();
32559
32878
  if (isDryRun && targetRuns.length > 0) {
32560
- const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray11(insights.runId, targetRuns.map((r) => r.id))).all();
32879
+ const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray12(insights.runId, targetRuns.map((r) => r.id))).all();
32561
32880
  for (const r of rows) {
32562
32881
  if (r.runId == null) continue;
32563
32882
  existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
@@ -32603,7 +32922,7 @@ var IntelligenceService = class {
32603
32922
  return { processed, skipped, totalInsights };
32604
32923
  }
32605
32924
  loadTrackedCompetitors(projectId) {
32606
- return this.db.select({ domain: competitors.domain }).from(competitors).where(eq32(competitors.projectId, projectId)).all().map((r) => r.domain);
32925
+ return this.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, projectId)).all().map((r) => r.domain);
32607
32926
  }
32608
32927
  /**
32609
32928
  * Wipe transition signals from an analysis result while keeping health.
@@ -32624,15 +32943,15 @@ var IntelligenceService = class {
32624
32943
  }
32625
32944
  persistResult(result, runId, projectId) {
32626
32945
  const previouslyDismissed = /* @__PURE__ */ new Set();
32627
- 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();
32946
+ 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();
32628
32947
  for (const row of existingInsights) {
32629
32948
  if (row.dismissed) {
32630
32949
  previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
32631
32950
  }
32632
32951
  }
32633
32952
  this.db.transaction((tx) => {
32634
- tx.delete(insights).where(eq32(insights.runId, runId)).run();
32635
- tx.delete(healthSnapshots).where(eq32(healthSnapshots.runId, runId)).run();
32953
+ tx.delete(insights).where(eq33(insights.runId, runId)).run();
32954
+ tx.delete(healthSnapshots).where(eq33(healthSnapshots.runId, runId)).run();
32636
32955
  const now = (/* @__PURE__ */ new Date()).toISOString();
32637
32956
  for (const insight of result.insights) {
32638
32957
  const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
@@ -32652,7 +32971,7 @@ var IntelligenceService = class {
32652
32971
  }).run();
32653
32972
  }
32654
32973
  tx.insert(healthSnapshots).values({
32655
- id: crypto27.randomUUID(),
32974
+ id: crypto28.randomUUID(),
32656
32975
  projectId,
32657
32976
  runId,
32658
32977
  overallCitedRate: String(result.health.overallCitedRate),
@@ -32683,28 +33002,28 @@ var IntelligenceService = class {
32683
33002
  applySeverityTiering(rawInsights, excludeRunId, projectId) {
32684
33003
  const regressions = rawInsights.filter((i) => i.type === "regression");
32685
33004
  if (regressions.length === 0) return rawInsights;
32686
- const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq32(gscSearchData.projectId, projectId)).all();
33005
+ const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq33(gscSearchData.projectId, projectId)).all();
32687
33006
  const gscConnected = gscRows.length > 0;
32688
33007
  const gscImpressionsByQuery = /* @__PURE__ */ new Map();
32689
33008
  for (const row of gscRows) {
32690
33009
  const key = row.query.toLowerCase();
32691
33010
  gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
32692
33011
  }
32693
- const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq32(projects.id, projectId)).get();
33012
+ const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq33(projects.id, projectId)).get();
32694
33013
  const locationCount = Math.max(
32695
33014
  1,
32696
33015
  (projectRow?.locations ?? []).length
32697
33016
  );
32698
33017
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
32699
33018
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
32700
- and23(
32701
- eq32(runs.projectId, projectId),
32702
- eq32(runs.kind, RunKinds["answer-visibility"]),
32703
- or5(eq32(runs.status, "completed"), eq32(runs.status, "partial")),
33019
+ and24(
33020
+ eq33(runs.projectId, projectId),
33021
+ eq33(runs.kind, RunKinds["answer-visibility"]),
33022
+ or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
32704
33023
  // Defensive — see top of file.
32705
33024
  ne5(runs.trigger, RunTriggers.probe)
32706
33025
  )
32707
- ).orderBy(desc16(runs.createdAt), desc16(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
33026
+ ).orderBy(desc17(runs.createdAt), desc17(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
32708
33027
  const recentGroups = groupRunsByCreatedAt(recentRunRows);
32709
33028
  const recentRunIds = [];
32710
33029
  const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
@@ -32720,7 +33039,7 @@ var IntelligenceService = class {
32720
33039
  const haveHistory = recentRunIds.length > 0;
32721
33040
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
32722
33041
  if (haveHistory) {
32723
- 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();
33042
+ 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();
32724
33043
  const regressionGroups = /* @__PURE__ */ new Map();
32725
33044
  for (const row of priorRows) {
32726
33045
  if (!row.runId) continue;
@@ -32749,7 +33068,7 @@ var IntelligenceService = class {
32749
33068
  });
32750
33069
  }
32751
33070
  buildRunData(runId, projectId, completedAt, location = null) {
32752
- const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq32(projects.id, projectId)).get();
33071
+ const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq33(projects.id, projectId)).get();
32753
33072
  const projectDomains = projectDomainRow ? effectiveDomains({
32754
33073
  canonicalDomain: projectDomainRow.canonicalDomain,
32755
33074
  ownedDomains: projectDomainRow.ownedDomains
@@ -32765,7 +33084,7 @@ var IntelligenceService = class {
32765
33084
  citedDomains: querySnapshots.citedDomains,
32766
33085
  competitorOverlap: querySnapshots.competitorOverlap,
32767
33086
  snapshotLocation: querySnapshots.location
32768
- }).from(querySnapshots).leftJoin(queries, eq32(querySnapshots.queryId, queries.id)).where(eq32(querySnapshots.runId, runId)).all();
33087
+ }).from(querySnapshots).leftJoin(queries, eq33(querySnapshots.queryId, queries.id)).where(eq33(querySnapshots.runId, runId)).all();
32769
33088
  const snapshots = [];
32770
33089
  let orphanCount = 0;
32771
33090
  for (const r of rows) {
@@ -32816,6 +33135,8 @@ export {
32816
33135
  gscSearchData,
32817
33136
  gscUrlInspections,
32818
33137
  gscCoverageSnapshots,
33138
+ siteAuditSnapshots,
33139
+ siteAuditPages,
32819
33140
  bingCoverageSnapshots,
32820
33141
  bingUrlInspections,
32821
33142
  gaTrafficSnapshots,