@canonry/canonry 4.154.0 → 4.155.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/assets/agent-workspace/AGENTS.md +2 -2
  3. package/assets/agent-workspace/skills/aero/references/orchestration.md +3 -3
  4. package/assets/agent-workspace/skills/canonry/SKILL.md +6 -21
  5. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +14 -6
  6. package/assets/assets/{AuditHistoryPanel-WWT1n8jp.js → AuditHistoryPanel-BlbMBxOY.js} +1 -1
  7. package/assets/assets/BacklinksPage-DA9EaV9q.js +1 -0
  8. package/assets/assets/HistoryPage-BTrIHjMQ.js +1 -0
  9. package/assets/assets/MeasurementPropertyPage-CN6o5xFg.js +1 -0
  10. package/assets/assets/{ProjectPage-BR5ECwUP.js → ProjectPage-CsDbgp_0.js} +9 -9
  11. package/assets/assets/{RunRow-OE5tiuih.js → RunRow-ch_OlCPs.js} +1 -1
  12. package/assets/assets/RunsPage-T89Y_yS8.js +1 -0
  13. package/assets/assets/SettingsPage-DnmEq3F9.js +1 -0
  14. package/assets/assets/TrafficPage-CW1UhaJt.js +1 -0
  15. package/assets/assets/TrafficSourceDetailPage-r4Ji3wJm.js +1 -0
  16. package/assets/assets/extract-error-message-a8EErBoZ.js +1 -0
  17. package/assets/assets/index-BKq6k5Of.js +83 -0
  18. package/assets/assets/vendor-lucide-ByU7vBHt.js +1 -0
  19. package/assets/assets/vendor-yaml-GWJz5H2G.js +131 -0
  20. package/assets/index.html +3 -1
  21. package/dist/{chunk-ANSQ6JNO.js → chunk-OSHRDV36.js} +317 -4
  22. package/dist/{chunk-NNW7TJUZ.js → chunk-VIKPFI6F.js} +200 -5
  23. package/dist/{chunk-IFZS2TXV.js → chunk-VOL2BSM5.js} +1133 -104
  24. package/dist/{chunk-X5I7CAXH.js → chunk-YSESZNVT.js} +609 -165
  25. package/dist/cli.js +342 -7
  26. package/dist/index.js +4 -4
  27. package/dist/{intelligence-service-B5AETQ5N.js → intelligence-service-G4NV3X36.js} +2 -2
  28. package/dist/mcp.js +2 -2
  29. package/package.json +9 -9
  30. package/assets/assets/BacklinksPage-DIjUnsVL.js +0 -1
  31. package/assets/assets/HistoryPage-CrIBhGuW.js +0 -1
  32. package/assets/assets/MeasurementPropertyPage-uN4BWigS.js +0 -1
  33. package/assets/assets/RunsPage-C6xBmjps.js +0 -1
  34. package/assets/assets/SettingsPage-CUjjr8U0.js +0 -1
  35. package/assets/assets/TrafficPage-DrEfsHiE.js +0 -1
  36. package/assets/assets/TrafficSourceDetailPage-B3DX5BEk.js +0 -1
  37. package/assets/assets/arrow-left-CLmqvCqU.js +0 -1
  38. package/assets/assets/extract-error-message-BEBd1a5v.js +0 -1
  39. package/assets/assets/index-CsM-Xxia.js +0 -213
  40. package/assets/assets/refresh-cw-DYSS2Wd-.js +0 -1
  41. package/assets/assets/trash-2-VoiF_ckU.js +0 -1
@@ -403,6 +403,7 @@ import {
403
403
  normalizeProjectAliases,
404
404
  normalizeProjectDomain,
405
405
  normalizeQueryText,
406
+ normalizeSiteAuditRunRequest,
406
407
  normalizeUrlPath,
407
408
  normalizeUserName,
408
409
  notFound,
@@ -469,10 +470,17 @@ import {
469
470
  serializeRunError,
470
471
  settingsDtoSchema,
471
472
  siteAuditPagesResponseSchema,
473
+ siteAuditRequestIdentity,
472
474
  siteAuditRunRequestSchema,
473
475
  siteAuditRunResponseSchema,
474
476
  siteAuditScoreSchema,
475
477
  siteAuditTrendResponseSchema,
478
+ siteCrawlDeadLinksResponseSchema,
479
+ siteCrawlInternalLinksResponseSchema,
480
+ siteCrawlNeighborsResponseSchema,
481
+ siteCrawlPagesResponseSchema,
482
+ siteCrawlStructureResponseSchema,
483
+ siteCrawlSummarySchema,
476
484
  snapshotDiffResponseSchema,
477
485
  snapshotListResponseSchema,
478
486
  snapshotReportSchema,
@@ -521,10 +529,10 @@ import {
521
529
  wordpressSchemaDeployResultDtoSchema,
522
530
  wordpressSchemaStatusResultDtoSchema,
523
531
  wordpressStatusDtoSchema
524
- } from "./chunk-NNW7TJUZ.js";
532
+ } from "./chunk-VIKPFI6F.js";
525
533
 
526
534
  // src/intelligence-service.ts
527
- import { eq as eq57, desc as desc26, asc as asc11, and as and45, ne as ne7, or as or11, inArray as inArray19, gte as gte13, lte as lte10 } from "drizzle-orm";
535
+ import { eq as eq57, desc as desc26, asc as asc11, and as and45, ne as ne7, or as or12, inArray as inArray20, gte as gte13, lte as lte10 } from "drizzle-orm";
528
536
 
529
537
  // ../db/src/client.ts
530
538
  import { mkdirSync } from "fs";
@@ -615,6 +623,13 @@ __export(schema_exports, {
615
623
  schedules: () => schedules,
616
624
  siteAuditPages: () => siteAuditPages,
617
625
  siteAuditSnapshots: () => siteAuditSnapshots,
626
+ siteCrawlAttempts: () => siteCrawlAttempts,
627
+ siteCrawlEdges: () => siteCrawlEdges,
628
+ siteCrawlEventReceipts: () => siteCrawlEventReceipts,
629
+ siteCrawlFindings: () => siteCrawlFindings,
630
+ siteCrawlPages: () => siteCrawlPages,
631
+ siteCrawlRunRequests: () => siteCrawlRunRequests,
632
+ siteCrawlSnapshots: () => siteCrawlSnapshots,
618
633
  trafficSources: () => trafficSources,
619
634
  usageCounters: () => usageCounters,
620
635
  userSessions: () => userSessions,
@@ -839,6 +854,9 @@ var runs = sqliteTable("runs", {
839
854
  createdAt: text("created_at").notNull()
840
855
  }, (table) => [
841
856
  index("idx_runs_project").on(table.projectId),
857
+ // Enables child tables that carry project + run to enforce that the two IDs
858
+ // belong together, rather than trusting a caller-provided project ID.
859
+ uniqueIndex("idx_runs_project_id").on(table.projectId, table.id),
842
860
  index("idx_runs_status").on(table.status),
843
861
  index("idx_runs_source").on(table.sourceId),
844
862
  index("idx_runs_measurement_plan").on(table.projectId, table.measurementPlanVersionId, table.createdAt),
@@ -1185,6 +1203,213 @@ var siteAuditPages = sqliteTable("site_audit_pages", {
1185
1203
  index("idx_site_audit_pages_run").on(table.runId),
1186
1204
  index("idx_site_audit_pages_project_score").on(table.projectId, table.overallScore)
1187
1205
  ]);
1206
+ var siteCrawlRunRequests = sqliteTable("site_crawl_run_requests", {
1207
+ runId: text("run_id").primaryKey(),
1208
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1209
+ identityKey: text("identity_key").notNull(),
1210
+ effectiveOptions: text("effective_options", { mode: "json" }).$type().notNull(),
1211
+ createdAt: text("created_at").notNull()
1212
+ }, (table) => [
1213
+ index("idx_site_crawl_run_requests_project").on(table.projectId, table.runId),
1214
+ foreignKey({
1215
+ name: "site_crawl_run_requests_project_run_fk",
1216
+ columns: [table.projectId, table.runId],
1217
+ foreignColumns: [runs.projectId, runs.id]
1218
+ }).onDelete("cascade")
1219
+ ]);
1220
+ var siteCrawlAttempts = sqliteTable("site_crawl_attempts", {
1221
+ id: text("id").primaryKey(),
1222
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1223
+ runId: text("run_id").notNull(),
1224
+ attemptNumber: integer("attempt_number").notNull(),
1225
+ state: text("state").notNull().default("queued"),
1226
+ lastEventSequence: integer("last_event_sequence").notNull().default(0),
1227
+ lastEventChecksum: text("last_event_checksum"),
1228
+ pagesDiscovered: integer("pages_discovered").notNull().default(0),
1229
+ pagesFetched: integer("pages_fetched").notNull().default(0),
1230
+ pagesEligible: integer("pages_eligible").notNull().default(0),
1231
+ pagesErrored: integer("pages_errored").notNull().default(0),
1232
+ edgesDiscovered: integer("edges_discovered").notNull().default(0),
1233
+ startedAt: text("started_at"),
1234
+ finishedAt: text("finished_at"),
1235
+ error: text("error"),
1236
+ createdAt: text("created_at").notNull(),
1237
+ updatedAt: text("updated_at").notNull()
1238
+ }, (table) => [
1239
+ uniqueIndex("idx_site_crawl_attempts_run_number").on(table.runId, table.attemptNumber),
1240
+ uniqueIndex("idx_site_crawl_attempts_project_run_id").on(table.projectId, table.runId, table.id),
1241
+ index("idx_site_crawl_attempts_project_run").on(table.projectId, table.runId),
1242
+ foreignKey({
1243
+ name: "site_crawl_attempts_project_run_fk",
1244
+ columns: [table.projectId, table.runId],
1245
+ foreignColumns: [runs.projectId, runs.id]
1246
+ }).onDelete("cascade")
1247
+ ]);
1248
+ var siteCrawlSnapshots = sqliteTable("site_crawl_snapshots", {
1249
+ id: text("id").primaryKey(),
1250
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1251
+ runId: text("run_id").notNull(),
1252
+ attemptId: text("attempt_id"),
1253
+ rootUrl: text("root_url").notNull(),
1254
+ crawlSchemaVersion: text("crawl_schema_version").notNull().default("1.0"),
1255
+ engineVersion: text("engine_version").notNull().default(""),
1256
+ normalizationVersion: text("normalization_version").notNull().default(""),
1257
+ indexabilityVersion: text("indexability_version").notNull().default(""),
1258
+ linkScoreVersion: text("link_score_version").notNull().default(""),
1259
+ effectiveOptions: text("effective_options", { mode: "json" }).$type().notNull().default({}),
1260
+ pageBudget: integer("page_budget"),
1261
+ edgeBudget: integer("edge_budget"),
1262
+ maxDepth: integer("max_depth"),
1263
+ checkDeadLinks: integer("check_dead_links", { mode: "boolean" }).notNull().default(false),
1264
+ complete: integer("complete", { mode: "boolean" }).notNull().default(false),
1265
+ termination: text("termination").notNull().default("unknown"),
1266
+ detailsAvailable: integer("details_available", { mode: "boolean" }).notNull().default(false),
1267
+ pagesDiscovered: integer("pages_discovered").notNull().default(0),
1268
+ pagesFetched: integer("pages_fetched").notNull().default(0),
1269
+ pagesEligible: integer("pages_eligible").notNull().default(0),
1270
+ pagesErrored: integer("pages_errored").notNull().default(0),
1271
+ edgesDiscovered: integer("edges_discovered").notNull().default(0),
1272
+ findingsCount: integer("findings_count").notNull().default(0),
1273
+ deadLinkState: text("dead_link_state").notNull().default("disabled"),
1274
+ deadLinksChecked: integer("dead_links_checked").notNull().default(0),
1275
+ deadLinksFound: integer("dead_links_found").notNull().default(0),
1276
+ createdAt: text("created_at").notNull(),
1277
+ updatedAt: text("updated_at").notNull()
1278
+ }, (table) => [
1279
+ uniqueIndex("idx_site_crawl_snapshots_run").on(table.runId),
1280
+ index("idx_site_crawl_snapshots_project_created").on(table.projectId, table.createdAt),
1281
+ foreignKey({
1282
+ name: "site_crawl_snapshots_project_run_fk",
1283
+ columns: [table.projectId, table.runId],
1284
+ foreignColumns: [runs.projectId, runs.id]
1285
+ }).onDelete("cascade"),
1286
+ foreignKey({
1287
+ name: "site_crawl_snapshots_attempt_fk",
1288
+ columns: [table.projectId, table.runId, table.attemptId],
1289
+ foreignColumns: [siteCrawlAttempts.projectId, siteCrawlAttempts.runId, siteCrawlAttempts.id]
1290
+ })
1291
+ ]);
1292
+ var siteCrawlPages = sqliteTable("site_crawl_pages", {
1293
+ id: text("id").primaryKey(),
1294
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1295
+ runId: text("run_id").notNull(),
1296
+ attemptId: text("attempt_id").notNull(),
1297
+ nodeKey: text("node_key").notNull(),
1298
+ url: text("url").notNull(),
1299
+ path: text("path").notNull(),
1300
+ parentPath: text("parent_path").notNull(),
1301
+ discoverySource: text("discovery_source").notNull().default("crawl"),
1302
+ discoveryProvenance: text("discovery_provenance", { mode: "json" }).$type().notNull().default([]),
1303
+ sitemapMetadata: text("sitemap_metadata", { mode: "json" }).$type().notNull().default({}),
1304
+ fetchState: text("fetch_state").notNull().default("queued"),
1305
+ fetchedAt: text("fetched_at"),
1306
+ httpStatus: integer("http_status"),
1307
+ contentType: text("content_type"),
1308
+ finalUrl: text("final_url"),
1309
+ redirectChain: text("redirect_chain", { mode: "json" }).$type().notNull().default([]),
1310
+ directives: text("directives", { mode: "json" }).$type().notNull().default({}),
1311
+ canonicalUrl: text("canonical_url"),
1312
+ canonicalNodeKey: text("canonical_node_key"),
1313
+ indexabilityState: text("indexability_state").notNull().default("unknown"),
1314
+ indexabilityReasons: text("indexability_reasons", { mode: "json" }).$type().notNull().default([]),
1315
+ auditState: text("audit_state").notNull().default("pending"),
1316
+ auditScore: real("audit_score"),
1317
+ auditFields: text("audit_fields", { mode: "json" }).$type().notNull().default({}),
1318
+ inventoryEligible: integer("inventory_eligible", { mode: "boolean" }).notNull().default(false),
1319
+ depth: integer("depth"),
1320
+ inboundUniqueEdges: integer("inbound_unique_edges").notNull().default(0),
1321
+ outboundUniqueEdges: integer("outbound_unique_edges").notNull().default(0),
1322
+ inboundOccurrences: integer("inbound_occurrences").notNull().default(0),
1323
+ outboundOccurrences: integer("outbound_occurrences").notNull().default(0),
1324
+ linkScoreRaw: real("link_score_raw"),
1325
+ linkScoreNormalized: real("link_score_normalized"),
1326
+ createdAt: text("created_at").notNull(),
1327
+ updatedAt: text("updated_at").notNull()
1328
+ }, (table) => [
1329
+ uniqueIndex("idx_site_crawl_pages_attempt_node").on(table.projectId, table.runId, table.attemptId, table.nodeKey),
1330
+ index("idx_site_crawl_pages_read").on(table.projectId, table.runId, table.attemptId, table.inventoryEligible, table.auditScore, table.url),
1331
+ index("idx_site_crawl_pages_parent").on(table.projectId, table.runId, table.attemptId, table.parentPath, table.path),
1332
+ index("idx_site_crawl_pages_url").on(table.projectId, table.runId, table.attemptId, table.url),
1333
+ foreignKey({
1334
+ name: "site_crawl_pages_attempt_fk",
1335
+ columns: [table.projectId, table.runId, table.attemptId],
1336
+ foreignColumns: [siteCrawlAttempts.projectId, siteCrawlAttempts.runId, siteCrawlAttempts.id]
1337
+ }).onDelete("cascade")
1338
+ ]);
1339
+ var siteCrawlEdges = sqliteTable("site_crawl_edges", {
1340
+ id: text("id").primaryKey(),
1341
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1342
+ runId: text("run_id").notNull(),
1343
+ attemptId: text("attempt_id").notNull(),
1344
+ edgeKey: text("edge_key").notNull(),
1345
+ sourceNodeKey: text("source_node_key").notNull(),
1346
+ sourceUrl: text("source_url").notNull(),
1347
+ targetNodeKey: text("target_node_key"),
1348
+ targetUrl: text("target_url").notNull(),
1349
+ relation: text("relation").notNull().default("link"),
1350
+ internal: integer("internal", { mode: "boolean" }).notNull().default(true),
1351
+ followable: integer("followable", { mode: "boolean" }).notNull().default(true),
1352
+ occurrences: integer("occurrences").notNull().default(1),
1353
+ followableOccurrences: integer("followable_occurrences").notNull().default(1),
1354
+ nofollowOccurrences: integer("nofollow_occurrences").notNull().default(0),
1355
+ anchors: text("anchors", { mode: "json" }).$type().notNull().default([]),
1356
+ createdAt: text("created_at").notNull(),
1357
+ updatedAt: text("updated_at").notNull()
1358
+ }, (table) => [
1359
+ uniqueIndex("idx_site_crawl_edges_attempt_key").on(table.projectId, table.runId, table.attemptId, table.edgeKey),
1360
+ index("idx_site_crawl_edges_outbound").on(table.projectId, table.runId, table.attemptId, table.sourceNodeKey, table.edgeKey),
1361
+ index("idx_site_crawl_edges_inbound").on(table.projectId, table.runId, table.attemptId, table.targetNodeKey, table.edgeKey),
1362
+ index("idx_site_crawl_edges_source_url").on(table.projectId, table.runId, table.attemptId, table.sourceUrl),
1363
+ index("idx_site_crawl_edges_target_url").on(table.projectId, table.runId, table.attemptId, table.targetUrl),
1364
+ foreignKey({
1365
+ name: "site_crawl_edges_attempt_fk",
1366
+ columns: [table.projectId, table.runId, table.attemptId],
1367
+ foreignColumns: [siteCrawlAttempts.projectId, siteCrawlAttempts.runId, siteCrawlAttempts.id]
1368
+ }).onDelete("cascade")
1369
+ ]);
1370
+ var siteCrawlFindings = sqliteTable("site_crawl_findings", {
1371
+ id: text("id").primaryKey(),
1372
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1373
+ runId: text("run_id").notNull(),
1374
+ attemptId: text("attempt_id").notNull(),
1375
+ findingKey: text("finding_key").notNull(),
1376
+ findingType: text("finding_type").notNull(),
1377
+ severity: text("severity").notNull().default("info"),
1378
+ sourceNodeKey: text("source_node_key"),
1379
+ sourceUrl: text("source_url"),
1380
+ targetNodeKey: text("target_node_key"),
1381
+ targetUrl: text("target_url"),
1382
+ evidence: text("evidence", { mode: "json" }).$type().notNull().default({}),
1383
+ createdAt: text("created_at").notNull(),
1384
+ updatedAt: text("updated_at").notNull()
1385
+ }, (table) => [
1386
+ uniqueIndex("idx_site_crawl_findings_attempt_key").on(table.projectId, table.runId, table.attemptId, table.findingKey),
1387
+ index("idx_site_crawl_findings_type").on(table.projectId, table.runId, table.attemptId, table.findingType, table.findingKey),
1388
+ foreignKey({
1389
+ name: "site_crawl_findings_attempt_fk",
1390
+ columns: [table.projectId, table.runId, table.attemptId],
1391
+ foreignColumns: [siteCrawlAttempts.projectId, siteCrawlAttempts.runId, siteCrawlAttempts.id]
1392
+ }).onDelete("cascade")
1393
+ ]);
1394
+ var siteCrawlEventReceipts = sqliteTable("site_crawl_event_receipts", {
1395
+ id: text("id").primaryKey(),
1396
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
1397
+ runId: text("run_id").notNull(),
1398
+ attemptId: text("attempt_id").notNull(),
1399
+ sequence: integer("sequence").notNull(),
1400
+ batchId: text("batch_id").notNull(),
1401
+ checksum: text("checksum").notNull(),
1402
+ receipt: text("receipt", { mode: "json" }).$type().notNull().default({}),
1403
+ createdAt: text("created_at").notNull()
1404
+ }, (table) => [
1405
+ uniqueIndex("idx_site_crawl_receipts_attempt_event").on(table.attemptId, table.sequence, table.batchId),
1406
+ index("idx_site_crawl_receipts_project_run").on(table.projectId, table.runId, table.attemptId),
1407
+ foreignKey({
1408
+ name: "site_crawl_receipts_attempt_fk",
1409
+ columns: [table.projectId, table.runId, table.attemptId],
1410
+ foreignColumns: [siteCrawlAttempts.projectId, siteCrawlAttempts.runId, siteCrawlAttempts.id]
1411
+ }).onDelete("cascade")
1412
+ ]);
1188
1413
  var bingCoverageSnapshots = sqliteTable("bing_coverage_snapshots", {
1189
1414
  id: text("id").primaryKey(),
1190
1415
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
@@ -5096,6 +5321,211 @@ var MIGRATION_VERSIONS = [
5096
5321
  `ALTER TABLE gsc_coverage_snapshots ADD COLUMN verified_by_inspection INTEGER NOT NULL DEFAULT 0`,
5097
5322
  `ALTER TABLE gsc_coverage_snapshots ADD COLUMN derived_from_impressions INTEGER NOT NULL DEFAULT 0`
5098
5323
  ]
5324
+ },
5325
+ {
5326
+ // Crawl persistence is intentionally separate from the legacy
5327
+ // site_audit_snapshots/site_audit_pages scorecard model. Older engines can
5328
+ // keep reading and writing that legacy surface after a rollback; new tables
5329
+ // only enrich an audit run when the crawler writes them.
5330
+ version: 126,
5331
+ name: "site-crawl-persistence",
5332
+ statements: [
5333
+ // Composite child FKs below enforce that project_id and run_id are a
5334
+ // matched pair. `runs.id` is globally unique already; this named unique
5335
+ // index provides SQLite's required parent key for the composite check.
5336
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_project_id ON runs(project_id, id)`,
5337
+ `CREATE TABLE IF NOT EXISTS site_crawl_run_requests (
5338
+ run_id TEXT PRIMARY KEY,
5339
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
5340
+ identity_key TEXT NOT NULL,
5341
+ effective_options TEXT NOT NULL,
5342
+ created_at TEXT NOT NULL,
5343
+ FOREIGN KEY (project_id, run_id) REFERENCES runs(project_id, id) ON DELETE CASCADE
5344
+ )`,
5345
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_run_requests_project
5346
+ ON site_crawl_run_requests(project_id, run_id)`,
5347
+ `CREATE TABLE IF NOT EXISTS site_crawl_attempts (
5348
+ id TEXT PRIMARY KEY,
5349
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
5350
+ run_id TEXT NOT NULL,
5351
+ attempt_number INTEGER NOT NULL,
5352
+ state TEXT NOT NULL DEFAULT 'queued',
5353
+ last_event_sequence INTEGER NOT NULL DEFAULT 0,
5354
+ last_event_checksum TEXT,
5355
+ pages_discovered INTEGER NOT NULL DEFAULT 0,
5356
+ pages_fetched INTEGER NOT NULL DEFAULT 0,
5357
+ pages_eligible INTEGER NOT NULL DEFAULT 0,
5358
+ pages_errored INTEGER NOT NULL DEFAULT 0,
5359
+ edges_discovered INTEGER NOT NULL DEFAULT 0,
5360
+ started_at TEXT,
5361
+ finished_at TEXT,
5362
+ error TEXT,
5363
+ created_at TEXT NOT NULL,
5364
+ updated_at TEXT NOT NULL,
5365
+ FOREIGN KEY (project_id, run_id) REFERENCES runs(project_id, id) ON DELETE CASCADE
5366
+ )`,
5367
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_site_crawl_attempts_run_number
5368
+ ON site_crawl_attempts(run_id, attempt_number)`,
5369
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_site_crawl_attempts_project_run_id
5370
+ ON site_crawl_attempts(project_id, run_id, id)`,
5371
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_attempts_project_run
5372
+ ON site_crawl_attempts(project_id, run_id)`,
5373
+ `CREATE TABLE IF NOT EXISTS site_crawl_snapshots (
5374
+ id TEXT PRIMARY KEY,
5375
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
5376
+ run_id TEXT NOT NULL,
5377
+ attempt_id TEXT,
5378
+ root_url TEXT NOT NULL,
5379
+ crawl_schema_version TEXT NOT NULL DEFAULT '1.0',
5380
+ engine_version TEXT NOT NULL DEFAULT '',
5381
+ normalization_version TEXT NOT NULL DEFAULT '',
5382
+ indexability_version TEXT NOT NULL DEFAULT '',
5383
+ link_score_version TEXT NOT NULL DEFAULT '',
5384
+ effective_options TEXT NOT NULL DEFAULT '{}',
5385
+ page_budget INTEGER,
5386
+ edge_budget INTEGER,
5387
+ max_depth INTEGER,
5388
+ check_dead_links INTEGER NOT NULL DEFAULT 0,
5389
+ complete INTEGER NOT NULL DEFAULT 0,
5390
+ termination TEXT NOT NULL DEFAULT 'unknown',
5391
+ details_available INTEGER NOT NULL DEFAULT 0,
5392
+ pages_discovered INTEGER NOT NULL DEFAULT 0,
5393
+ pages_fetched INTEGER NOT NULL DEFAULT 0,
5394
+ pages_eligible INTEGER NOT NULL DEFAULT 0,
5395
+ pages_errored INTEGER NOT NULL DEFAULT 0,
5396
+ edges_discovered INTEGER NOT NULL DEFAULT 0,
5397
+ findings_count INTEGER NOT NULL DEFAULT 0,
5398
+ dead_link_state TEXT NOT NULL DEFAULT 'disabled',
5399
+ dead_links_checked INTEGER NOT NULL DEFAULT 0,
5400
+ dead_links_found INTEGER NOT NULL DEFAULT 0,
5401
+ created_at TEXT NOT NULL,
5402
+ updated_at TEXT NOT NULL,
5403
+ FOREIGN KEY (project_id, run_id) REFERENCES runs(project_id, id) ON DELETE CASCADE,
5404
+ FOREIGN KEY (project_id, run_id, attempt_id)
5405
+ REFERENCES site_crawl_attempts(project_id, run_id, id)
5406
+ )`,
5407
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_site_crawl_snapshots_run ON site_crawl_snapshots(run_id)`,
5408
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_snapshots_project_created
5409
+ ON site_crawl_snapshots(project_id, created_at)`,
5410
+ `CREATE TABLE IF NOT EXISTS site_crawl_pages (
5411
+ id TEXT PRIMARY KEY,
5412
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
5413
+ run_id TEXT NOT NULL,
5414
+ attempt_id TEXT NOT NULL,
5415
+ node_key TEXT NOT NULL,
5416
+ url TEXT NOT NULL,
5417
+ path TEXT NOT NULL,
5418
+ parent_path TEXT NOT NULL,
5419
+ discovery_source TEXT NOT NULL DEFAULT 'crawl',
5420
+ discovery_provenance TEXT NOT NULL DEFAULT '[]',
5421
+ sitemap_metadata TEXT NOT NULL DEFAULT '{}',
5422
+ fetch_state TEXT NOT NULL DEFAULT 'queued',
5423
+ fetched_at TEXT,
5424
+ http_status INTEGER,
5425
+ content_type TEXT,
5426
+ final_url TEXT,
5427
+ redirect_chain TEXT NOT NULL DEFAULT '[]',
5428
+ directives TEXT NOT NULL DEFAULT '{}',
5429
+ canonical_url TEXT,
5430
+ canonical_node_key TEXT,
5431
+ indexability_state TEXT NOT NULL DEFAULT 'unknown',
5432
+ indexability_reasons TEXT NOT NULL DEFAULT '[]',
5433
+ audit_state TEXT NOT NULL DEFAULT 'pending',
5434
+ audit_score REAL,
5435
+ audit_fields TEXT NOT NULL DEFAULT '{}',
5436
+ inventory_eligible INTEGER NOT NULL DEFAULT 0,
5437
+ depth INTEGER,
5438
+ inbound_unique_edges INTEGER NOT NULL DEFAULT 0,
5439
+ outbound_unique_edges INTEGER NOT NULL DEFAULT 0,
5440
+ inbound_occurrences INTEGER NOT NULL DEFAULT 0,
5441
+ outbound_occurrences INTEGER NOT NULL DEFAULT 0,
5442
+ link_score_raw REAL,
5443
+ link_score_normalized REAL,
5444
+ created_at TEXT NOT NULL,
5445
+ updated_at TEXT NOT NULL,
5446
+ FOREIGN KEY (project_id, run_id, attempt_id)
5447
+ REFERENCES site_crawl_attempts(project_id, run_id, id) ON DELETE CASCADE
5448
+ )`,
5449
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_site_crawl_pages_attempt_node
5450
+ ON site_crawl_pages(project_id, run_id, attempt_id, node_key)`,
5451
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_pages_read
5452
+ ON site_crawl_pages(project_id, run_id, attempt_id, inventory_eligible, audit_score, url)`,
5453
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_pages_parent
5454
+ ON site_crawl_pages(project_id, run_id, attempt_id, parent_path, path)`,
5455
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_pages_url
5456
+ ON site_crawl_pages(project_id, run_id, attempt_id, url)`,
5457
+ `CREATE TABLE IF NOT EXISTS site_crawl_edges (
5458
+ id TEXT PRIMARY KEY,
5459
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
5460
+ run_id TEXT NOT NULL,
5461
+ attempt_id TEXT NOT NULL,
5462
+ edge_key TEXT NOT NULL,
5463
+ source_node_key TEXT NOT NULL,
5464
+ source_url TEXT NOT NULL,
5465
+ target_node_key TEXT,
5466
+ target_url TEXT NOT NULL,
5467
+ relation TEXT NOT NULL DEFAULT 'link',
5468
+ internal INTEGER NOT NULL DEFAULT 1,
5469
+ followable INTEGER NOT NULL DEFAULT 1,
5470
+ occurrences INTEGER NOT NULL DEFAULT 1,
5471
+ followable_occurrences INTEGER NOT NULL DEFAULT 1,
5472
+ nofollow_occurrences INTEGER NOT NULL DEFAULT 0,
5473
+ anchors TEXT NOT NULL DEFAULT '[]',
5474
+ created_at TEXT NOT NULL,
5475
+ updated_at TEXT NOT NULL,
5476
+ FOREIGN KEY (project_id, run_id, attempt_id)
5477
+ REFERENCES site_crawl_attempts(project_id, run_id, id) ON DELETE CASCADE
5478
+ )`,
5479
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_site_crawl_edges_attempt_key
5480
+ ON site_crawl_edges(project_id, run_id, attempt_id, edge_key)`,
5481
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_edges_outbound
5482
+ ON site_crawl_edges(project_id, run_id, attempt_id, source_node_key, edge_key)`,
5483
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_edges_inbound
5484
+ ON site_crawl_edges(project_id, run_id, attempt_id, target_node_key, edge_key)`,
5485
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_edges_source_url
5486
+ ON site_crawl_edges(project_id, run_id, attempt_id, source_url)`,
5487
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_edges_target_url
5488
+ ON site_crawl_edges(project_id, run_id, attempt_id, target_url)`,
5489
+ `CREATE TABLE IF NOT EXISTS site_crawl_findings (
5490
+ id TEXT PRIMARY KEY,
5491
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
5492
+ run_id TEXT NOT NULL,
5493
+ attempt_id TEXT NOT NULL,
5494
+ finding_key TEXT NOT NULL,
5495
+ finding_type TEXT NOT NULL,
5496
+ severity TEXT NOT NULL DEFAULT 'info',
5497
+ source_node_key TEXT,
5498
+ source_url TEXT,
5499
+ target_node_key TEXT,
5500
+ target_url TEXT,
5501
+ evidence TEXT NOT NULL DEFAULT '{}',
5502
+ created_at TEXT NOT NULL,
5503
+ updated_at TEXT NOT NULL,
5504
+ FOREIGN KEY (project_id, run_id, attempt_id)
5505
+ REFERENCES site_crawl_attempts(project_id, run_id, id) ON DELETE CASCADE
5506
+ )`,
5507
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_site_crawl_findings_attempt_key
5508
+ ON site_crawl_findings(project_id, run_id, attempt_id, finding_key)`,
5509
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_findings_type
5510
+ ON site_crawl_findings(project_id, run_id, attempt_id, finding_type, finding_key)`,
5511
+ `CREATE TABLE IF NOT EXISTS site_crawl_event_receipts (
5512
+ id TEXT PRIMARY KEY,
5513
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
5514
+ run_id TEXT NOT NULL,
5515
+ attempt_id TEXT NOT NULL,
5516
+ sequence INTEGER NOT NULL,
5517
+ batch_id TEXT NOT NULL,
5518
+ checksum TEXT NOT NULL,
5519
+ receipt TEXT NOT NULL DEFAULT '{}',
5520
+ created_at TEXT NOT NULL,
5521
+ FOREIGN KEY (project_id, run_id, attempt_id)
5522
+ REFERENCES site_crawl_attempts(project_id, run_id, id) ON DELETE CASCADE
5523
+ )`,
5524
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_site_crawl_receipts_attempt_event
5525
+ ON site_crawl_event_receipts(attempt_id, sequence, batch_id)`,
5526
+ `CREATE INDEX IF NOT EXISTS idx_site_crawl_receipts_project_run
5527
+ ON site_crawl_event_receipts(project_id, run_id, attempt_id)`
5528
+ ]
5099
5529
  }
5100
5530
  ];
5101
5531
  function addRunsMeasurementPlanVersionForeignKey(tx) {
@@ -7968,7 +8398,7 @@ function isOverviewRead(url) {
7968
8398
  }
7969
8399
  function isTechnicalAeoRead(url) {
7970
8400
  const rest = projectRouteRest(url);
7971
- return rest === "technical-aeo" || rest === "technical-aeo/pages" || rest === "technical-aeo/trend";
8401
+ return rest === "technical-aeo" || rest === "technical-aeo/pages" || rest === "technical-aeo/trend" || rest === "technical-aeo/crawl" || rest === "technical-aeo/crawl/pages" || rest === "technical-aeo/structure" || rest === "technical-aeo/internal-links" || rest === "technical-aeo/internal-links/neighbors" || rest === "technical-aeo/dead-links";
7972
8402
  }
7973
8403
  function isReportRead(url) {
7974
8404
  const rest = projectRouteRest(url);
@@ -9206,7 +9636,7 @@ function parseCompetitorBatch(value) {
9206
9636
 
9207
9637
  // ../api-routes/src/runs.ts
9208
9638
  import crypto12 from "crypto";
9209
- import { and as and6, eq as eq11, asc, desc as desc3, or as or2, sql as sql6 } from "drizzle-orm";
9639
+ import { and as and6, eq as eq11, asc, desc as desc3, inArray as inArray4, or as or2, sql as sql6 } from "drizzle-orm";
9210
9640
  import { gte as gte2 } from "drizzle-orm";
9211
9641
 
9212
9642
  // ../api-routes/src/run-queue.ts
@@ -10954,7 +11384,12 @@ async function runRoutes(app, opts) {
10954
11384
  const terminalStatuses = /* @__PURE__ */ new Set(["completed", "partial", "failed", "cancelled"]);
10955
11385
  if (terminalStatuses.has(run.status)) throw runNotCancellable(run.id, run.status);
10956
11386
  const now = (/* @__PURE__ */ new Date()).toISOString();
10957
- app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: serializeRunError({ message: "Cancelled by user" }) }).where(eq11(runs.id, run.id)).run();
11387
+ const cancelled = app.db.update(runs).set({ status: "cancelled", finishedAt: now, error: serializeRunError({ message: "Cancelled by user" }) }).where(and6(eq11(runs.id, run.id), inArray4(runs.status, ["queued", "running"]))).run();
11388
+ if (cancelled.changes === 0) {
11389
+ const current = app.db.select().from(runs).where(eq11(runs.id, run.id)).get();
11390
+ if (!current) throw notFound("Run", run.id);
11391
+ throw runNotCancellable(run.id, current.status);
11392
+ }
10958
11393
  writeAuditLog(app.db, {
10959
11394
  projectId: run.projectId,
10960
11395
  actor: "api",
@@ -10962,6 +11397,7 @@ async function runRoutes(app, opts) {
10962
11397
  entityType: "run",
10963
11398
  entityId: run.id
10964
11399
  });
11400
+ opts.onRunCancelled?.(run.id, run.projectId);
10965
11401
  const updated = app.db.select().from(runs).where(eq11(runs.id, run.id)).get();
10966
11402
  return reply.send(formatRun(updated));
10967
11403
  });
@@ -12231,7 +12667,7 @@ async function measurementServiceRoutes(app, opts) {
12231
12667
 
12232
12668
  // ../api-routes/src/measurement-draft.ts
12233
12669
  import crypto17 from "crypto";
12234
- import { and as and9, asc as asc2, desc as desc5, eq as eq14, inArray as inArray4 } from "drizzle-orm";
12670
+ import { and as and9, asc as asc2, desc as desc5, eq as eq14, inArray as inArray5 } from "drizzle-orm";
12235
12671
 
12236
12672
  // ../api-routes/src/measurement-draft-repo.ts
12237
12673
  import crypto15 from "crypto";
@@ -14753,7 +15189,7 @@ async function measurementDraftRoutes(app, opts = {}) {
14753
15189
  const sets = app.db.select().from(measurementQuerySets).where(eq14(measurementQuerySets.projectId, project.id)).orderBy(asc2(measurementQuerySets.name)).all();
14754
15190
  const counts2 = /* @__PURE__ */ new Map();
14755
15191
  if (sets.length) {
14756
- for (const item2 of app.db.select({ querySetId: measurementQuerySetItems.querySetId }).from(measurementQuerySetItems).where(inArray4(measurementQuerySetItems.querySetId, sets.map((set) => set.id))).all()) {
15192
+ for (const item2 of app.db.select({ querySetId: measurementQuerySetItems.querySetId }).from(measurementQuerySetItems).where(inArray5(measurementQuerySetItems.querySetId, sets.map((set) => set.id))).all()) {
14757
15193
  counts2.set(item2.querySetId, (counts2.get(item2.querySetId) ?? 0) + 1);
14758
15194
  }
14759
15195
  }
@@ -16276,10 +16712,10 @@ async function measurementPropertyEvidenceRoutes(app) {
16276
16712
  }
16277
16713
 
16278
16714
  // ../api-routes/src/measurement-portfolio-reads.ts
16279
- import { and as and14, desc as desc6, eq as eq20, inArray as inArray6, lt as lt3, or as or3 } from "drizzle-orm";
16715
+ import { and as and14, desc as desc6, eq as eq20, inArray as inArray7, lt as lt3, or as or3 } from "drizzle-orm";
16280
16716
 
16281
16717
  // ../api-routes/src/measurement-question-reads.ts
16282
- import { and as and13, eq as eq19, getTableColumns, inArray as inArray5, sql as sql7 } from "drizzle-orm";
16718
+ import { and as and13, eq as eq19, getTableColumns, inArray as inArray6, sql as sql7 } from "drizzle-orm";
16283
16719
  var DEFAULT_LIMIT2 = 50;
16284
16720
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
16285
16721
  RunStatuses.completed,
@@ -16395,7 +16831,7 @@ function selectedQuestionSnapshots(db, runId, manifest, scope) {
16395
16831
  if (scope !== void 0) {
16396
16832
  const executionIds = [...new Set(manifest.expectedSlots.map((slot) => slot.executionId))];
16397
16833
  if (executionIds.length === 0) return [];
16398
- conditions.push(inArray5(querySnapshots.measurementExecutionId, executionIds));
16834
+ conditions.push(inArray6(querySnapshots.measurementExecutionId, executionIds));
16399
16835
  }
16400
16836
  if (scope?.provider !== void 0) {
16401
16837
  conditions.push(sql7`lower(trim(${querySnapshots.provider})) = ${normalizeText(scope.provider)}`);
@@ -16410,7 +16846,7 @@ function rawResponsesForLegacyQuestionSnapshots(db, runId, snapshots) {
16410
16846
  rawResponse: querySnapshots.rawResponse
16411
16847
  }).from(querySnapshots).where(and13(
16412
16848
  eq19(querySnapshots.runId, runId),
16413
- inArray5(querySnapshots.id, legacyIds)
16849
+ inArray6(querySnapshots.id, legacyIds)
16414
16850
  )).all().map((snapshot) => [snapshot.id, snapshot.rawResponse]));
16415
16851
  }
16416
16852
  function withLegacyRawEvidence(db, runId, snapshots) {
@@ -17056,7 +17492,7 @@ function previousComparableRun(db, active, current) {
17056
17492
  eq20(runs.projectId, current.projectId),
17057
17493
  eq20(runs.measurementPlanVersionId, active.version.id),
17058
17494
  eq20(runs.kind, RunKinds["answer-visibility"]),
17059
- inArray6(runs.status, [RunStatuses.completed, RunStatuses.partial]),
17495
+ inArray7(runs.status, [RunStatuses.completed, RunStatuses.partial]),
17060
17496
  or3(
17061
17497
  lt3(runs.createdAt, current.createdAt),
17062
17498
  and14(eq20(runs.createdAt, current.createdAt), lt3(runs.id, current.id))
@@ -17882,7 +18318,7 @@ function normalizeCompetitorList2(domains) {
17882
18318
  }
17883
18319
 
17884
18320
  // ../api-routes/src/history.ts
17885
- import { and as and16, asc as asc3, desc as desc7, eq as eq22, gte as gte3, inArray as inArray7 } from "drizzle-orm";
18321
+ import { and as and16, asc as asc3, desc as desc7, eq as eq22, gte as gte3, inArray as inArray8 } from "drizzle-orm";
17886
18322
 
17887
18323
  // ../api-routes/src/notification-redaction.ts
17888
18324
  var REDACTED_URL = {
@@ -17968,7 +18404,7 @@ async function historyRoutes(app) {
17968
18404
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
17969
18405
  location: querySnapshots.location,
17970
18406
  createdAt: querySnapshots.createdAt
17971
- }).from(querySnapshots).leftJoin(queries, eq22(querySnapshots.queryId, queries.id)).where(inArray7(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc7(querySnapshots.createdAt)).all();
18407
+ }).from(querySnapshots).leftJoin(queries, eq22(querySnapshots.queryId, queries.id)).where(inArray8(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc7(querySnapshots.createdAt)).all();
17972
18408
  const locationFilter = request.query.location;
17973
18409
  const filtered = locationFilter !== void 0 ? allSnapshots.filter((s) => s.location === (locationFilter || null)) : allSnapshots;
17974
18410
  const total = filtered.length;
@@ -18014,7 +18450,7 @@ async function historyRoutes(app) {
18014
18450
  return reply.send([]);
18015
18451
  }
18016
18452
  const runIds = new Set(projectRuns.map((r) => r.id));
18017
- const rawSnapshots = app.db.select().from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all();
18453
+ const rawSnapshots = app.db.select().from(querySnapshots).where(inArray8(querySnapshots.runId, [...runIds])).all();
18018
18454
  const timelineLocationFilter = request.query.location;
18019
18455
  const filteredSnapshots = timelineLocationFilter !== void 0 ? rawSnapshots.filter((s) => s.location === (timelineLocationFilter || null)) : rawSnapshots;
18020
18456
  const allSnapshots = filteredSnapshots.map((snapshot) => ({
@@ -18135,7 +18571,7 @@ async function historyRoutes(app) {
18135
18571
  throw validationError("Both run1 and run2 query params are required");
18136
18572
  }
18137
18573
  const requestedRunIds = [.../* @__PURE__ */ new Set([run1, run2])];
18138
- const runRows = app.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(inArray7(runs.id, requestedRunIds)).all();
18574
+ const runRows = app.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(inArray8(runs.id, requestedRunIds)).all();
18139
18575
  const runsById = new Map(runRows.map((row) => [row.id, row]));
18140
18576
  for (const runId of requestedRunIds) {
18141
18577
  const run = runsById.get(runId);
@@ -18233,7 +18669,7 @@ function parseOptionalPositiveInt(value, max) {
18233
18669
  }
18234
18670
 
18235
18671
  // ../api-routes/src/analytics.ts
18236
- import { and as and17, desc as desc8, eq as eq23, gte as gte4, inArray as inArray8, lt as lt4 } from "drizzle-orm";
18672
+ import { and as and17, desc as desc8, eq as eq23, gte as gte4, inArray as inArray9, lt as lt4 } from "drizzle-orm";
18237
18673
 
18238
18674
  // ../api-routes/src/model-evidence.ts
18239
18675
  function classifyModelEvidence(values) {
@@ -18368,7 +18804,7 @@ async function analyticsRoutes(app) {
18368
18804
  const projectRuns = app.db.select().from(runs).where(and17(
18369
18805
  eq23(runs.projectId, project.id),
18370
18806
  eq23(runs.kind, RunKinds["answer-visibility"]),
18371
- inArray8(runs.status, [RunStatuses.completed, RunStatuses.partial]),
18807
+ inArray9(runs.status, [RunStatuses.completed, RunStatuses.partial]),
18372
18808
  notProbeRun(),
18373
18809
  cutoff ? gte4(runs.createdAt, cutoff) : void 0
18374
18810
  )).orderBy(desc8(runs.createdAt)).all().filter((r) => measuredWhole(app.db, r)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
@@ -18411,7 +18847,7 @@ async function analyticsRoutes(app) {
18411
18847
  citationState: querySnapshots.citationState,
18412
18848
  answerMentioned: querySnapshots.answerMentioned,
18413
18849
  answerText: querySnapshots.answerText
18414
- }).from(querySnapshots).where(inArray8(querySnapshots.runId, runIds)).all();
18850
+ }).from(querySnapshots).where(inArray9(querySnapshots.runId, runIds)).all();
18415
18851
  const rawSnapshots = referenceBasket ? loadedSnapshots.filter(
18416
18852
  (s) => s.queryId !== null || s.queryText !== null && referenceBasket.has(normalizeQueryText(s.queryText))
18417
18853
  ) : filterTrackedSnapshots(loadedSnapshots);
@@ -18453,7 +18889,7 @@ async function analyticsRoutes(app) {
18453
18889
  const anchorRunPredicate = and17(
18454
18890
  eq23(runs.projectId, project.id),
18455
18891
  eq23(runs.kind, RunKinds["answer-visibility"]),
18456
- inArray8(runs.status, [RunStatuses.completed, RunStatuses.partial]),
18892
+ inArray9(runs.status, [RunStatuses.completed, RunStatuses.partial]),
18457
18893
  notProbeRun(),
18458
18894
  lt4(runs.createdAt, cutoff)
18459
18895
  );
@@ -18483,7 +18919,7 @@ async function analyticsRoutes(app) {
18483
18919
  provider: querySnapshots.provider,
18484
18920
  model: querySnapshots.model,
18485
18921
  servedModel: querySnapshots.servedModel
18486
- }).from(querySnapshots).where(inArray8(querySnapshots.runId, runIds2)).all())) {
18922
+ }).from(querySnapshots).where(inArray9(querySnapshots.runId, runIds2)).all())) {
18487
18923
  if (pending.has(snapshot.provider)) {
18488
18924
  const models = modelsByProvider.get(snapshot.provider) ?? [];
18489
18925
  models.push(snapshot.model);
@@ -18629,7 +19065,7 @@ async function analyticsRoutes(app) {
18629
19065
  citationState: querySnapshots.citationState,
18630
19066
  answerMentioned: querySnapshots.answerMentioned,
18631
19067
  answerText: querySnapshots.answerText
18632
- }).from(querySnapshots).where(inArray8(querySnapshots.runId, windowRunIds)).all());
19068
+ }).from(querySnapshots).where(inArray9(querySnapshots.runId, windowRunIds)).all());
18633
19069
  for (const s of allWindowSnaps) {
18634
19070
  const timePoint = runIdToCreatedAt.get(s.runId) ?? s.runId;
18635
19071
  let entry = consistencyMap.get(s.queryId);
@@ -18650,7 +19086,7 @@ async function analyticsRoutes(app) {
18650
19086
  answerMentioned: querySnapshots.answerMentioned,
18651
19087
  answerText: querySnapshots.answerText,
18652
19088
  competitorOverlap: querySnapshots.competitorOverlap
18653
- }).from(querySnapshots).leftJoin(queries, eq23(querySnapshots.queryId, queries.id)).where(inArray8(querySnapshots.runId, latestGroupRunIds)).all());
19089
+ }).from(querySnapshots).leftJoin(queries, eq23(querySnapshots.queryId, queries.id)).where(inArray9(querySnapshots.runId, latestGroupRunIds)).all());
18654
19090
  const snapshots = rawSnapshots.map((s) => ({
18655
19091
  ...s,
18656
19092
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
@@ -18769,7 +19205,7 @@ async function analyticsRoutes(app) {
18769
19205
  query: queries.query,
18770
19206
  provider: querySnapshots.provider,
18771
19207
  rawResponse: querySnapshots.rawResponse
18772
- }).from(querySnapshots).leftJoin(queries, eq23(querySnapshots.queryId, queries.id)).where(inArray8(querySnapshots.runId, windowRunIds)).all();
19208
+ }).from(querySnapshots).leftJoin(queries, eq23(querySnapshots.queryId, queries.id)).where(inArray9(querySnapshots.runId, windowRunIds)).all();
18773
19209
  const overallCounts = /* @__PURE__ */ new Map();
18774
19210
  const byQuery = {};
18775
19211
  const overallDomains = /* @__PURE__ */ new Map();
@@ -19041,7 +19477,7 @@ function buildCategoryCounts(counts2) {
19041
19477
  }
19042
19478
 
19043
19479
  // ../api-routes/src/intelligence.ts
19044
- import { eq as eq24, desc as desc9, and as and18, inArray as inArray9, like } from "drizzle-orm";
19480
+ import { eq as eq24, desc as desc9, and as and18, inArray as inArray10, like } from "drizzle-orm";
19045
19481
  var SEVERITY_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
19046
19482
  function severitiesAtOrAbove(min) {
19047
19483
  const floor = SEVERITY_RANK[min];
@@ -19171,7 +19607,7 @@ async function intelligenceRoutes(app) {
19171
19607
  }
19172
19608
  const severityFilter = request.query.severity?.trim();
19173
19609
  if (severityFilter) {
19174
- conditions.push(inArray9(insights.severity, severitiesAtOrAbove(severityFilter)));
19610
+ conditions.push(inArray10(insights.severity, severitiesAtOrAbove(severityFilter)));
19175
19611
  }
19176
19612
  let limit;
19177
19613
  if (request.query.limit !== void 0) {
@@ -19209,7 +19645,7 @@ async function intelligenceRoutes(app) {
19209
19645
  const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and18(
19210
19646
  eq24(runs.projectId, project.id),
19211
19647
  eq24(runs.kind, RunKinds["answer-visibility"]),
19212
- inArray9(runs.status, [RunStatuses.completed, RunStatuses.partial]),
19648
+ inArray10(runs.status, [RunStatuses.completed, RunStatuses.partial]),
19213
19649
  // Health-latest is the dashboard headline; probe runs must not
19214
19650
  // displace the most recent real visibility sweep.
19215
19651
  notProbeRun()
@@ -19219,7 +19655,7 @@ async function intelligenceRoutes(app) {
19219
19655
  if (latestGroupRunIds.length > 0) {
19220
19656
  const groupRows = app.db.select().from(healthSnapshots).where(and18(
19221
19657
  eq24(healthSnapshots.projectId, project.id),
19222
- inArray9(healthSnapshots.runId, latestGroupRunIds)
19658
+ inArray10(healthSnapshots.runId, latestGroupRunIds)
19223
19659
  )).all();
19224
19660
  if (groupRows.length > 0) {
19225
19661
  return reply.send(aggregateHealthSnapshots(project.id, groupRows));
@@ -19241,14 +19677,14 @@ async function intelligenceRoutes(app) {
19241
19677
  }
19242
19678
 
19243
19679
  // ../api-routes/src/report.ts
19244
- import { and as and22, desc as desc12, eq as eq28, gte as gte5, inArray as inArray11, lt as lt5, lte as lte3, ne as ne3, or as or4, sql as sql9 } from "drizzle-orm";
19680
+ import { and as and22, desc as desc12, eq as eq28, gte as gte5, inArray as inArray12, lt as lt5, lte as lte3, ne as ne3, or as or4, sql as sql9 } from "drizzle-orm";
19245
19681
 
19246
19682
  // ../api-routes/src/content.ts
19247
19683
  import crypto21 from "crypto";
19248
19684
  import { and as and20, desc as desc11, eq as eq26 } from "drizzle-orm";
19249
19685
 
19250
19686
  // ../api-routes/src/content-data.ts
19251
- import { and as and19, eq as eq25, desc as desc10, inArray as inArray10 } from "drizzle-orm";
19687
+ import { and as and19, eq as eq25, desc as desc10, inArray as inArray11 } from "drizzle-orm";
19252
19688
  var RECENT_RUNS_WINDOW = 5;
19253
19689
  function loadOrchestratorInput(db, project, locationFilter = void 0) {
19254
19690
  const projectId = project.id;
@@ -19384,7 +19820,7 @@ function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter)
19384
19820
  // Queued/running/failed/cancelled runs may have partial or no
19385
19821
  // snapshots; including them risks pointing latestRunId at a run with
19386
19822
  // no usable evidence.
19387
- inArray10(runs.status, [RunStatuses.completed, RunStatuses.partial]),
19823
+ inArray11(runs.status, [RunStatuses.completed, RunStatuses.partial]),
19388
19824
  // Probe runs are operator/agent test runs; they must not poison the
19389
19825
  // recent-runs window the content engine uses to recommend actions.
19390
19826
  notProbeRun()
@@ -19434,7 +19870,7 @@ function buildCandidateQueries(opts) {
19434
19870
  const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq25(queries.projectId, opts.projectId)).all();
19435
19871
  const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
19436
19872
  const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
19437
- const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(inArray10(querySnapshots.runId, opts.recentRunIds)).all()).filter((r) => candidateQueryIds.includes(r.queryId));
19873
+ const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(inArray11(querySnapshots.runId, opts.recentRunIds)).all()).filter((r) => candidateQueryIds.includes(r.queryId));
19438
19874
  const snapshotsByQuery = /* @__PURE__ */ new Map();
19439
19875
  for (const row of snapshotRows) {
19440
19876
  const list = snapshotsByQuery.get(row.queryId) ?? [];
@@ -22361,7 +22797,7 @@ function loadSnapshotsForRun(db, runId) {
22361
22797
  }
22362
22798
  function loadSnapshotsForRunIds(db, runIds) {
22363
22799
  if (runIds.length === 0) return [];
22364
- const rows = db.select().from(querySnapshots).where(inArray11(querySnapshots.runId, [...runIds])).all();
22800
+ const rows = db.select().from(querySnapshots).where(inArray12(querySnapshots.runId, [...runIds])).all();
22365
22801
  return rows.filter((r) => r.queryId !== null).map((r) => ({
22366
22802
  id: r.id,
22367
22803
  runId: r.runId,
@@ -22384,15 +22820,19 @@ function loadQueryLookup(db, projectId) {
22384
22820
  return { byId };
22385
22821
  }
22386
22822
  function buildGscSection(db, projectId, projectBrandNames, canonicalDomain, trackedQueries, windowDays2) {
22387
- const allRows = db.select().from(gscSearchData).where(eq28(gscSearchData.projectId, projectId)).all();
22388
- const allDailyTotals = readGscDailyTotals(db, projectId, "", "9999-12-31");
22389
- let maxDate = "";
22390
- for (const r of allRows) if (r.date > maxDate) maxDate = r.date;
22391
- for (const r of allDailyTotals) if (r.date > maxDate) maxDate = r.date;
22823
+ const maxSearch = db.select({ m: sql9`MAX(${gscSearchData.date})` }).from(gscSearchData).where(eq28(gscSearchData.projectId, projectId)).get()?.m ?? "";
22824
+ const maxDaily = db.select({ m: sql9`MAX(${gscDailyTotals.date})` }).from(gscDailyTotals).where(eq28(gscDailyTotals.projectId, projectId)).get()?.m ?? "";
22825
+ const maxDate = maxSearch > maxDaily ? maxSearch : maxDaily;
22392
22826
  if (!maxDate) return null;
22393
22827
  const startDate = windowStartDate(maxDate, windowDays2);
22394
- const rows = allRows.filter((r) => r.date >= startDate && r.date <= maxDate);
22395
- const dailyTotals = allDailyTotals.filter((r) => r.date >= startDate && r.date <= maxDate);
22828
+ const rows = db.select().from(gscSearchData).where(
22829
+ and22(
22830
+ eq28(gscSearchData.projectId, projectId),
22831
+ gte5(gscSearchData.date, startDate),
22832
+ lte3(gscSearchData.date, maxDate)
22833
+ )
22834
+ ).all();
22835
+ const dailyTotals = readGscDailyTotals(db, projectId, startDate, maxDate);
22396
22836
  if (rows.length === 0 && dailyTotals.length === 0) return null;
22397
22837
  let dimensionedClicks = 0;
22398
22838
  const queryDayAgg = /* @__PURE__ */ new Map();
@@ -22492,12 +22932,16 @@ function buildGaSection(db, projectId, windowDays2) {
22492
22932
  )
22493
22933
  ).limit(1).get() : void 0;
22494
22934
  const fallbackSummary = windowSummary ? null : db.select().from(gaTrafficSummaries).where(eq28(gaTrafficSummaries.projectId, projectId)).orderBy(desc12(gaTrafficSummaries.syncedAt)).limit(1).get();
22495
- const allSnapshotRows = db.select().from(gaTrafficSnapshots).where(eq28(gaTrafficSnapshots.projectId, projectId)).all();
22496
- if (!windowSummary && !fallbackSummary && allSnapshotRows.length === 0) return null;
22497
- let snapshotMaxDate = "";
22498
- for (const r of allSnapshotRows) if (r.date > snapshotMaxDate) snapshotMaxDate = r.date;
22935
+ const snapshotMaxDate = db.select({ m: sql9`MAX(${gaTrafficSnapshots.date})` }).from(gaTrafficSnapshots).where(eq28(gaTrafficSnapshots.projectId, projectId)).get()?.m ?? "";
22936
+ if (!windowSummary && !fallbackSummary && !snapshotMaxDate) return null;
22499
22937
  const snapshotStartDate = snapshotMaxDate ? windowStartDate(snapshotMaxDate, windowDays2) : "";
22500
- const snapshotRows = snapshotStartDate ? allSnapshotRows.filter((r) => r.date >= snapshotStartDate && r.date <= snapshotMaxDate) : allSnapshotRows;
22938
+ const snapshotRows = snapshotStartDate ? db.select().from(gaTrafficSnapshots).where(
22939
+ and22(
22940
+ eq28(gaTrafficSnapshots.projectId, projectId),
22941
+ gte5(gaTrafficSnapshots.date, snapshotStartDate),
22942
+ lte3(gaTrafficSnapshots.date, snapshotMaxDate)
22943
+ )
22944
+ ).all() : db.select().from(gaTrafficSnapshots).where(eq28(gaTrafficSnapshots.projectId, projectId)).all();
22501
22945
  const totalSessions = windowSummary?.totalSessions ?? fallbackSummary?.totalSessions ?? snapshotRows.reduce((s, r) => s + r.sessions, 0);
22502
22946
  const totalUsers = windowSummary?.totalUsers ?? fallbackSummary?.totalUsers ?? snapshotRows.reduce((s, r) => s + r.users, 0);
22503
22947
  const totalOrganicSessions = windowSummary?.totalOrganicSessions ?? fallbackSummary?.totalOrganicSessions ?? snapshotRows.reduce((s, r) => s + r.organicSessions, 0);
@@ -23102,7 +23546,7 @@ function buildInsightList(db, projectId, locationFilter) {
23102
23546
  )
23103
23547
  ).orderBy(desc12(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
23104
23548
  if (recentRunIds.length === 0) return [];
23105
- const rows = db.select().from(insights).where(and22(eq28(insights.projectId, projectId), inArray11(insights.runId, recentRunIds))).orderBy(desc12(insights.createdAt)).all();
23549
+ const rows = db.select().from(insights).where(and22(eq28(insights.projectId, projectId), inArray12(insights.runId, recentRunIds))).orderBy(desc12(insights.createdAt)).all();
23106
23550
  const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
23107
23551
  const flat = rows.filter((r) => !r.dismissed).map((r) => {
23108
23552
  const recommendation = r.recommendation;
@@ -24851,7 +25295,7 @@ async function organicEvidenceRoutes(app) {
24851
25295
  }
24852
25296
 
24853
25297
  // ../api-routes/src/citations.ts
24854
- import { and as and25, eq as eq31, inArray as inArray12 } from "drizzle-orm";
25298
+ import { and as and25, eq as eq31, inArray as inArray13 } from "drizzle-orm";
24855
25299
  async function citationRoutes(app) {
24856
25300
  app.get("/projects/:name/citations/visibility", async (request, reply) => {
24857
25301
  const project = resolveProject(app.db, request.params.name);
@@ -24875,7 +25319,7 @@ async function citationRoutes(app) {
24875
25319
  competitorOverlap: querySnapshots.competitorOverlap,
24876
25320
  answerMentioned: querySnapshots.answerMentioned,
24877
25321
  createdAt: querySnapshots.createdAt
24878
- }).from(querySnapshots).where(inArray12(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
25322
+ }).from(querySnapshots).where(inArray13(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
24879
25323
  if (rawSnapshots.length === 0) {
24880
25324
  return reply.send(emptyCitationVisibility("no-runs-yet"));
24881
25325
  }
@@ -25014,7 +25458,7 @@ function computeCitationVisibility(input) {
25014
25458
  }
25015
25459
 
25016
25460
  // ../api-routes/src/visibility-stats.ts
25017
- import { and as and26, desc as desc14, eq as eq32, inArray as inArray13 } from "drizzle-orm";
25461
+ import { and as and26, desc as desc14, eq as eq32, inArray as inArray14 } from "drizzle-orm";
25018
25462
 
25019
25463
  // ../api-routes/src/visibility-compare.ts
25020
25464
  var VISIBILITY_COMPARE_MIN_RUNS = 5;
@@ -25425,7 +25869,7 @@ async function visibilityStatsRoutes(app) {
25425
25869
  citationState: querySnapshots.citationState,
25426
25870
  answerMentioned: querySnapshots.answerMentioned,
25427
25871
  createdAt: querySnapshots.createdAt
25428
- }).from(querySnapshots).where(inArray13(querySnapshots.runId, runIds)).all() : [];
25872
+ }).from(querySnapshots).where(inArray14(querySnapshots.runId, runIds)).all() : [];
25429
25873
  const stats = computeVisibilityStats({ queries: projectQueries, snapshots, groupBy });
25430
25874
  const queryAttribution = buildQueryAttribution(projectQueries);
25431
25875
  let shareOfVoice;
@@ -25440,7 +25884,7 @@ async function visibilityStatsRoutes(app) {
25440
25884
  queryText: querySnapshots.queryText,
25441
25885
  answerMentioned: querySnapshots.answerMentioned,
25442
25886
  answerText: querySnapshots.answerText
25443
- }).from(querySnapshots).where(inArray13(querySnapshots.runId, runIds)).all() : [];
25887
+ }).from(querySnapshots).where(inArray14(querySnapshots.runId, runIds)).all() : [];
25444
25888
  const attributedSovSnapshots = sovSnapshots.filter((s) => resolveCurrentQuery(queryAttribution, s) !== void 0);
25445
25889
  const result = buildMentionShare(
25446
25890
  attributedSovSnapshots.map((s) => ({ projectMentioned: s.answerMentioned === true, answerText: s.answerText })),
@@ -25521,7 +25965,7 @@ async function visibilityStatsRoutes(app) {
25521
25965
  answerMentioned: querySnapshots.answerMentioned,
25522
25966
  answerText: querySnapshots.answerText,
25523
25967
  citedDomains: querySnapshots.citedDomains
25524
- }).from(querySnapshots).where(inArray13(querySnapshots.runId, runIds)).all() : [];
25968
+ }).from(querySnapshots).where(inArray14(querySnapshots.runId, runIds)).all() : [];
25525
25969
  return { runCount: monthRuns.length, snapshots };
25526
25970
  };
25527
25971
  const fromMonth = loadMonth(fromBounds);
@@ -25776,7 +26220,7 @@ async function resultsExportRoutes(app) {
25776
26220
  }
25777
26221
 
25778
26222
  // ../api-routes/src/composites.ts
25779
- import { eq as eq34, and as and28, desc as desc15, sql as sql12, like as like2, or as or7, inArray as inArray14 } from "drizzle-orm";
26223
+ import { eq as eq34, and as and28, desc as desc15, sql as sql12, like as like2, or as or7, inArray as inArray15 } from "drizzle-orm";
25780
26224
  var TOP_INSIGHT_LIMIT = 5;
25781
26225
  var SEARCH_HIT_HARD_LIMIT = 50;
25782
26226
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -26043,7 +26487,7 @@ function loadSnapshotsByRunIds(app, runIds, queryIdByText) {
26043
26487
  answerText: querySnapshots.answerText,
26044
26488
  competitorOverlap: querySnapshots.competitorOverlap,
26045
26489
  citedDomains: querySnapshots.citedDomains
26046
- }).from(querySnapshots).where(inArray14(querySnapshots.runId, [...runIds])).all();
26490
+ }).from(querySnapshots).where(inArray15(querySnapshots.runId, [...runIds])).all();
26047
26491
  for (const row of rows) {
26048
26492
  const queryText = row.queryText?.trim() || null;
26049
26493
  let queryId;
@@ -26678,6 +27122,12 @@ var SCHEMA_TABLE = {
26678
27122
  SiteAuditRunResponseDto: siteAuditRunResponseSchema,
26679
27123
  SiteAuditScoreDto: siteAuditScoreSchema,
26680
27124
  SiteAuditTrendResponseDto: siteAuditTrendResponseSchema,
27125
+ SiteCrawlDeadLinksResponseDto: siteCrawlDeadLinksResponseSchema,
27126
+ SiteCrawlInternalLinksResponseDto: siteCrawlInternalLinksResponseSchema,
27127
+ SiteCrawlNeighborsResponseDto: siteCrawlNeighborsResponseSchema,
27128
+ SiteCrawlPagesResponseDto: siteCrawlPagesResponseSchema,
27129
+ SiteCrawlStructureResponseDto: siteCrawlStructureResponseSchema,
27130
+ SiteCrawlSummaryDto: siteCrawlSummarySchema,
26681
27131
  SnapshotDiffResponse: snapshotDiffResponseSchema,
26682
27132
  SnapshotListResponse: snapshotListResponseSchema,
26683
27133
  SnapshotReportDto: snapshotReportSchema,
@@ -27134,6 +27584,18 @@ var measurementCursorParameter = {
27134
27584
  description: "Opaque cursor from the previous page.",
27135
27585
  schema: stringSchema
27136
27586
  };
27587
+ var crawlCursorParameter = {
27588
+ name: "cursor",
27589
+ in: "query",
27590
+ description: "Opaque cursor from the previous bounded crawl response.",
27591
+ schema: stringSchema
27592
+ };
27593
+ var crawlLimitParameter = {
27594
+ name: "limit",
27595
+ in: "query",
27596
+ description: "Page size. Defaults to 100 (50 for structure/neighbors); maximum 200 (100 for structure/neighbors).",
27597
+ schema: { type: "integer", minimum: 1, maximum: 200 }
27598
+ };
27137
27599
  var measurementOverviewSortParameter = {
27138
27600
  name: "sort",
27139
27601
  in: "query",
@@ -32585,11 +33047,122 @@ var routeCatalog = [
32585
33047
  404: errorResponse("Project not found.")
32586
33048
  }
32587
33049
  },
33050
+ {
33051
+ method: "get",
33052
+ path: "/api/v1/projects/{name}/technical-aeo/crawl",
33053
+ summary: "Get persisted Technical AEO crawl metadata",
33054
+ description: "Returns the latest complete non-probe site-audit crawl, or the selected historical run (which may be partial). `hasCrawlData=false` never synthesizes a graph from legacy scorecard rows; `legacyAuditAvailable` says that the old score/pages/trend data can still be read separately.",
33055
+ tags: ["technical-aeo"],
33056
+ parameters: [
33057
+ nameParameter,
33058
+ { name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest crawl.", schema: stringSchema }
33059
+ ],
33060
+ responses: {
33061
+ 200: jsonResponse("Persisted crawl summary returned.", "SiteCrawlSummaryDto"),
33062
+ 404: errorResponse("Project or crawl-bearing site-audit run not found.")
33063
+ }
33064
+ },
33065
+ {
33066
+ method: "get",
33067
+ path: "/api/v1/projects/{name}/technical-aeo/crawl/pages",
33068
+ summary: "List persisted Technical AEO crawl pages",
33069
+ description: "Cursor-paged canonical crawl nodes for the latest or selected crawl. `inventoryEligible` is Canonry technical-inventory eligibility, not a statement about actual Google index state.",
33070
+ tags: ["technical-aeo"],
33071
+ parameters: [
33072
+ nameParameter,
33073
+ { name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest crawl.", schema: stringSchema },
33074
+ { name: "inventoryEligible", in: "query", description: "Filter Canonry technical inventory eligibility (`true` or `false`).", schema: booleanSchema },
33075
+ { name: "fetchState", in: "query", description: "Filter by persisted fetch state.", schema: stringSchema },
33076
+ { name: "indexabilityState", in: "query", description: "Filter by crawler-derived indexability state; this is not Google index coverage.", schema: stringSchema },
33077
+ { name: "auditState", in: "query", description: "Filter by audit state.", schema: stringSchema },
33078
+ { name: "sort", in: "query", description: "Sort order. Defaults to `url`.", schema: { type: "string", enum: ["url", "path", "score-asc", "score-desc"] } },
33079
+ crawlCursorParameter,
33080
+ crawlLimitParameter
33081
+ ],
33082
+ responses: {
33083
+ 200: jsonResponse("Crawl pages returned.", "SiteCrawlPagesResponseDto"),
33084
+ 404: errorResponse("Project or crawl-bearing site-audit run not found.")
33085
+ }
33086
+ },
33087
+ {
33088
+ method: "get",
33089
+ path: "/api/v1/projects/{name}/technical-aeo/structure",
33090
+ summary: "List one level of Technical AEO crawl structure",
33091
+ description: "Returns immediate children under `parentPath` only. It is deliberately bounded and never emits a full site tree.",
33092
+ tags: ["technical-aeo"],
33093
+ parameters: [
33094
+ nameParameter,
33095
+ { name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest crawl.", schema: stringSchema },
33096
+ { name: "parentPath", in: "query", description: "Parent URL path. Defaults to `/`.", schema: stringSchema },
33097
+ crawlCursorParameter,
33098
+ { name: "limit", in: "query", description: "Maximum immediate children. Defaults to 50; maximum 100.", schema: { type: "integer", minimum: 1, maximum: 100 } }
33099
+ ],
33100
+ responses: {
33101
+ 200: jsonResponse("Immediate crawl children returned.", "SiteCrawlStructureResponseDto"),
33102
+ 404: errorResponse("Project or crawl-bearing site-audit run not found.")
33103
+ }
33104
+ },
33105
+ {
33106
+ method: "get",
33107
+ path: "/api/v1/projects/{name}/technical-aeo/internal-links",
33108
+ summary: "List persisted internal crawl links",
33109
+ description: "Cursor-paged internal edges for the latest or selected crawl. Optional source/target/followability filters remain project-, run-, and attempt-scoped.",
33110
+ tags: ["technical-aeo"],
33111
+ parameters: [
33112
+ nameParameter,
33113
+ { name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest crawl.", schema: stringSchema },
33114
+ { name: "sourceUrl", in: "query", description: "Restrict to a source URL.", schema: stringSchema },
33115
+ { name: "targetUrl", in: "query", description: "Restrict to a target URL.", schema: stringSchema },
33116
+ { name: "followable", in: "query", description: "Restrict to followable or nofollow link observations.", schema: booleanSchema },
33117
+ crawlCursorParameter,
33118
+ crawlLimitParameter
33119
+ ],
33120
+ responses: {
33121
+ 200: jsonResponse("Internal crawl links returned.", "SiteCrawlInternalLinksResponseDto"),
33122
+ 404: errorResponse("Project or crawl-bearing site-audit run not found.")
33123
+ }
33124
+ },
33125
+ {
33126
+ method: "get",
33127
+ path: "/api/v1/projects/{name}/technical-aeo/internal-links/neighbors",
33128
+ summary: "Get a bounded internal-link neighborhood",
33129
+ description: "Returns bounded inbound and outbound internal edges for one `nodeKey` or URL; this is not a full graph traversal.",
33130
+ tags: ["technical-aeo"],
33131
+ parameters: [
33132
+ nameParameter,
33133
+ { name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest crawl.", schema: stringSchema },
33134
+ { name: "nodeKey", in: "query", description: "Canonical crawl node key.", schema: stringSchema },
33135
+ { name: "url", in: "query", description: "Canonical crawl URL.", schema: stringSchema },
33136
+ { name: "limit", in: "query", description: "Maximum inbound and outbound edges independently. Defaults to 50; maximum 100.", schema: { type: "integer", minimum: 1, maximum: 100 } }
33137
+ ],
33138
+ responses: {
33139
+ 200: jsonResponse("Bounded internal-link neighborhood returned.", "SiteCrawlNeighborsResponseDto"),
33140
+ 400: errorResponse("A nodeKey or URL is required."),
33141
+ 404: errorResponse("Project or crawl-bearing site-audit run not found.")
33142
+ }
33143
+ },
33144
+ {
33145
+ method: "get",
33146
+ path: "/api/v1/projects/{name}/technical-aeo/dead-links",
33147
+ summary: "List dead-link findings when checks were enabled",
33148
+ description: "Returns a discriminated `disabled`, `complete`, `partial`, or `unavailable` state. A disabled check is never returned as an empty zero-result list.",
33149
+ tags: ["technical-aeo"],
33150
+ parameters: [
33151
+ nameParameter,
33152
+ { name: "runId", in: "query", description: "Historical site-audit run ID. Omit for the latest crawl.", schema: stringSchema },
33153
+ crawlCursorParameter,
33154
+ crawlLimitParameter
33155
+ ],
33156
+ responses: {
33157
+ 200: jsonResponse("Dead-link status and findings returned.", "SiteCrawlDeadLinksResponseDto"),
33158
+ 404: errorResponse("Project or crawl-bearing site-audit run not found.")
33159
+ }
33160
+ },
32588
33161
  {
32589
33162
  method: "post",
32590
33163
  path: "/api/v1/projects/{name}/technical-aeo/runs",
32591
33164
  summary: "Trigger a Technical AEO site-audit run",
32592
- 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.",
33165
+ description: "Queues a `site-audit` run. The run discovers pages from the root, recursive sitemaps, and internal links. An active run is reused only when its persisted effective sitemap, budgets, depth, and dead-link option match exactly; a semantic mismatch returns 409.",
32593
33166
  tags: ["technical-aeo"],
32594
33167
  parameters: [nameParameter],
32595
33168
  requestBody: {
@@ -32599,8 +33172,12 @@ var routeCatalog = [
32599
33172
  schema: {
32600
33173
  type: "object",
32601
33174
  properties: {
32602
- sitemapUrl: { ...stringSchema, description: "Override the sitemap URL. Defaults to https://<canonicalDomain>/sitemap.xml." },
32603
- limit: { ...integerSchema, description: "Cap pages audited (highest sitemap <priority> first). Max 2000." }
33175
+ sitemapUrl: { ...stringSchema, description: "Deprecated compatibility alias for the sitemap override." },
33176
+ limit: { ...integerSchema, minimum: 1, maximum: 2e3, description: "Deprecated compatibility alias for maxPages. Max 2000." },
33177
+ maxPages: { ...integerSchema, minimum: 1, maximum: 5e4, description: "Crawl page budget. Defaults to 1000; max 50000." },
33178
+ maxEdges: { ...integerSchema, minimum: 1, maximum: 1e6, description: "Internal-link observation budget. Defaults to 100000; max 1000000." },
33179
+ maxDepth: { ...integerSchema, minimum: 0, maximum: 100, description: "Maximum crawl depth from the root. Max 100." },
33180
+ checkDeadLinks: { ...booleanSchema, default: false, description: "Enable dead-link checking. Defaults to false." }
32604
33181
  }
32605
33182
  }
32606
33183
  }
@@ -32609,6 +33186,7 @@ var routeCatalog = [
32609
33186
  responses: {
32610
33187
  200: jsonResponse("Site-audit run queued (or the in-flight run returned).", "SiteAuditRunResponseDto"),
32611
33188
  400: errorResponse("Invalid site-audit request."),
33189
+ 409: errorResponse("A crawl with different effective options is already queued or running."),
32612
33190
  404: errorResponse("Project not found.")
32613
33191
  }
32614
33192
  }
@@ -33646,7 +34224,7 @@ function formatNotification(row) {
33646
34224
 
33647
34225
  // ../api-routes/src/google.ts
33648
34226
  import crypto29 from "crypto";
33649
- import { eq as eq39, and as and30, desc as desc17, sql as sql13, inArray as inArray15 } from "drizzle-orm";
34227
+ import { eq as eq39, and as and30, desc as desc17, sql as sql13, inArray as inArray16 } from "drizzle-orm";
33650
34228
 
33651
34229
  // ../api-routes/src/gbp-summary.ts
33652
34230
  function computeMetricTotals(rows) {
@@ -36906,10 +37484,10 @@ async function googleRoutes(app, opts) {
36906
37484
  locationProfiles: []
36907
37485
  });
36908
37486
  }
36909
- const metricRows = app.db.select().from(gbpDailyMetrics).where(and30(eq39(gbpDailyMetrics.projectId, project.id), inArray15(gbpDailyMetrics.locationName, locationNames))).all();
36910
- const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and30(eq39(gbpKeywordImpressions.projectId, project.id), inArray15(gbpKeywordImpressions.locationName, locationNames))).all();
36911
- const placeActionRows = app.db.select().from(gbpPlaceActions).where(and30(eq39(gbpPlaceActions.projectId, project.id), inArray15(gbpPlaceActions.locationName, locationNames))).all();
36912
- const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and30(eq39(gbpLodgingSnapshots.projectId, project.id), inArray15(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).all();
37487
+ const metricRows = app.db.select().from(gbpDailyMetrics).where(and30(eq39(gbpDailyMetrics.projectId, project.id), inArray16(gbpDailyMetrics.locationName, locationNames))).all();
37488
+ const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and30(eq39(gbpKeywordImpressions.projectId, project.id), inArray16(gbpKeywordImpressions.locationName, locationNames))).all();
37489
+ const placeActionRows = app.db.select().from(gbpPlaceActions).where(and30(eq39(gbpPlaceActions.projectId, project.id), inArray16(gbpPlaceActions.locationName, locationNames))).all();
37490
+ const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and30(eq39(gbpLodgingSnapshots.projectId, project.id), inArray16(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).all();
36913
37491
  const latestLodgingByLocation = /* @__PURE__ */ new Map();
36914
37492
  for (const row of lodgingRows) {
36915
37493
  if (!latestLodgingByLocation.has(row.locationName)) {
@@ -36923,7 +37501,7 @@ async function googleRoutes(app, opts) {
36923
37501
  regularHours: gbpLocations.regularHours,
36924
37502
  primaryPhone: gbpLocations.primaryPhone,
36925
37503
  openStatus: gbpLocations.openStatus
36926
- }).from(gbpLocations).where(and30(eq39(gbpLocations.projectId, project.id), inArray15(gbpLocations.locationName, locationNames))).all();
37504
+ }).from(gbpLocations).where(and30(eq39(gbpLocations.projectId, project.id), inArray16(gbpLocations.locationName, locationNames))).all();
36927
37505
  return buildGbpSummary({
36928
37506
  locationName,
36929
37507
  locationCount: locationNames.length,
@@ -36939,7 +37517,7 @@ async function googleRoutes(app, opts) {
36939
37517
 
36940
37518
  // ../api-routes/src/ads.ts
36941
37519
  import crypto32 from "crypto";
36942
- import { eq as eq41, and as and32, asc as asc8, desc as desc18, gt as gt2, gte as gte9, lt as lt6, lte as lte8, ne as ne4, inArray as inArray16, or as or9, isNull as isNull3, isNotNull as isNotNull2, sql as sql15 } from "drizzle-orm";
37520
+ import { eq as eq41, and as and32, asc as asc8, desc as desc18, gt as gt2, gte as gte9, lt as lt6, lte as lte8, ne as ne4, inArray as inArray17, or as or9, isNull as isNull3, isNotNull as isNotNull2, sql as sql15 } from "drizzle-orm";
36943
37521
 
36944
37522
  // ../api-routes/src/ads-activation-routes.ts
36945
37523
  import crypto31 from "crypto";
@@ -39886,7 +40464,7 @@ function semanticallyMatchesMaterializedEntity(app, input) {
39886
40464
  eq41(adsOperations.projectId, input.projectId),
39887
40465
  eq41(adsOperations.entityType, input.entityType),
39888
40466
  eq41(adsOperations.entityId, input.entityId),
39889
- inArray16(adsOperations.kind, [createKind, updateKind])
40467
+ inArray17(adsOperations.kind, [createKind, updateKind])
39890
40468
  )).orderBy(asc8(adsOperations.createdAt), asc8(adsOperations.id)).all();
39891
40469
  if (rows.some((row) => row.state === AdsOperationStates.pending || row.state === AdsOperationStates.reconciling || row.state === AdsOperationStates.unknown || row.kind === updateKind && row.state === AdsOperationStates.succeeded)) {
39892
40470
  return false;
@@ -40323,7 +40901,7 @@ function quarantineExhaustedReconciliation(app, row, now, policy, context) {
40323
40901
  eq41(adsOperations.id, row.id),
40324
40902
  eq41(adsOperations.projectId, row.projectId),
40325
40903
  gte9(adsOperations.reconcileAttempts, policy.maxAttempts),
40326
- inArray16(adsOperations.state, [
40904
+ inArray17(adsOperations.state, [
40327
40905
  AdsOperationStates.pending,
40328
40906
  AdsOperationStates.unknown,
40329
40907
  AdsOperationStates.reconciling
@@ -40547,7 +41125,7 @@ async function sweepAdsOperationsOnce(app, opts, config) {
40547
41125
  eq41(adsOperations.reconcileStrategy, AdsReconcileStrategies.create_fingerprint),
40548
41126
  or9(
40549
41127
  isNotNull2(adsOperations.entityId),
40550
- inArray16(adsOperations.state, [
41128
+ inArray17(adsOperations.state, [
40551
41129
  AdsOperationStates.pending,
40552
41130
  AdsOperationStates.reconciling
40553
41131
  ])
@@ -40595,7 +41173,7 @@ async function sweepAdsOperationsOnce(app, opts, config) {
40595
41173
  const eligibleProjectIds = [...credentialsByProjectId.keys()];
40596
41174
  if (eligibleProjectIds.length === 0) return;
40597
41175
  const rows = app.db.select().from(adsOperations).where(and32(
40598
- inArray16(adsOperations.projectId, eligibleProjectIds),
41176
+ inArray17(adsOperations.projectId, eligibleProjectIds),
40599
41177
  staleReconcileWhere
40600
41178
  )).orderBy(asc8(adsOperations.updatedAt)).limit(config.batchSize).all();
40601
41179
  for (const row of rows) {
@@ -40923,7 +41501,7 @@ async function adsRoutes(app, opts) {
40923
41501
  const cursor = parsed.data.cursor ? decodeAdsOperationCursor(parsed.data.cursor, project.id, parsed.data.state) : void 0;
40924
41502
  const rows = app.db.select().from(adsOperations).where(and32(
40925
41503
  eq41(adsOperations.projectId, project.id),
40926
- inArray16(adsOperations.state, parsed.data.state),
41504
+ inArray17(adsOperations.state, parsed.data.state),
40927
41505
  cursor ? or9(
40928
41506
  gt2(adsOperations.createdAt, cursor.createdAt),
40929
41507
  and32(
@@ -41270,7 +41848,7 @@ async function adsRoutes(app, opts) {
41270
41848
  const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and32(
41271
41849
  eq41(runs.projectId, project.id),
41272
41850
  eq41(runs.kind, RunKinds["ads-sync"]),
41273
- inArray16(runs.status, [RunStatuses.queued, RunStatuses.running])
41851
+ inArray17(runs.status, [RunStatuses.queued, RunStatuses.running])
41274
41852
  )).get();
41275
41853
  if (inFlight) {
41276
41854
  const existing = { runId: inFlight.id, status: inFlight.status };
@@ -54127,7 +54705,7 @@ async function doctorRoutes(app, opts) {
54127
54705
 
54128
54706
  // ../api-routes/src/discovery/routes.ts
54129
54707
  import crypto41 from "crypto";
54130
- import { and as and42, desc as desc23, eq as eq53, gte as gte12, inArray as inArray17, isNull as isNull4, or as or10 } from "drizzle-orm";
54708
+ import { and as and42, desc as desc23, eq as eq53, gte as gte12, inArray as inArray18, isNull as isNull4, or as or10 } from "drizzle-orm";
54131
54709
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
54132
54710
  async function discoveryRoutes(app, opts) {
54133
54711
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -54182,7 +54760,7 @@ async function discoveryRoutes(app, opts) {
54182
54760
  // must never reuse another set's session. Null (= Gemini-only
54183
54761
  // default, including explicit ['gemini']) matches legacy rows.
54184
54762
  seedProviders == null ? isNull4(discoverySessions.seedProviders) : eq53(discoverySessions.seedProviders, seedProviders),
54185
- inArray17(discoverySessions.status, [
54763
+ inArray18(discoverySessions.status, [
54186
54764
  DiscoverySessionStatuses.queued,
54187
54765
  DiscoverySessionStatuses.seeding,
54188
54766
  DiscoverySessionStatuses.probing
@@ -54807,7 +55385,7 @@ function dedupeStrings(input) {
54807
55385
 
54808
55386
  // ../api-routes/src/technical-aeo.ts
54809
55387
  import crypto43 from "crypto";
54810
- import { and as and43, asc as asc10, count, desc as desc24, eq as eq55, inArray as inArray18, lt as lt7 } from "drizzle-orm";
55388
+ import { and as and43, asc as asc10, count, desc as desc24, eq as eq55, inArray as inArray19, lt as lt7, or as or11 } from "drizzle-orm";
54811
55389
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
54812
55390
  function emptyScore(projectName) {
54813
55391
  return {
@@ -54836,13 +55414,133 @@ function parsePositiveInt(value, fallback, max) {
54836
55414
  if (!Number.isFinite(n) || n < 0) return fallback;
54837
55415
  return Math.min(max, Math.floor(n));
54838
55416
  }
55417
+ function parseBoundedLimit(value, fallback, max) {
55418
+ const parsed = parsePositiveInt(value, fallback, max);
55419
+ return Math.max(1, parsed);
55420
+ }
55421
+ function parseBoolean2(value) {
55422
+ if (value === true || value === "true" || value === "1") return true;
55423
+ if (value === false || value === "false" || value === "0") return false;
55424
+ return null;
55425
+ }
55426
+ function encodeCursor2(offset) {
55427
+ return Buffer.from(JSON.stringify({ offset })).toString("base64url");
55428
+ }
55429
+ function decodeCursor(value) {
55430
+ if (typeof value !== "string" || value.length === 0 || value.length > 256) return 0;
55431
+ try {
55432
+ const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
55433
+ return typeof parsed.offset === "number" && Number.isSafeInteger(parsed.offset) && parsed.offset >= 0 ? Math.min(parsed.offset, 1e6) : 0;
55434
+ } catch {
55435
+ return 0;
55436
+ }
55437
+ }
55438
+ function normalizeParentPath(value) {
55439
+ if (typeof value !== "string" || value.trim() === "") return "/";
55440
+ const bare = value.trim().split(/[?#]/, 1)[0] ?? "/";
55441
+ const prefixed = bare.startsWith("/") ? bare : `/${bare}`;
55442
+ const collapsed = prefixed.replace(/\/{2,}/g, "/");
55443
+ return collapsed.length > 1 && collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
55444
+ }
55445
+ var MAX_STRUCTURE_SOURCE_ROWS = 5e4;
55446
+ function structureHierarchyPath(path8) {
55447
+ if (path8.length <= 1) return path8;
55448
+ return path8.replace(/\/+$/, "") || "/";
55449
+ }
55450
+ function structureChildPath(parentPath, path8) {
55451
+ const hierarchyPath = structureHierarchyPath(path8);
55452
+ if (hierarchyPath === parentPath) return null;
55453
+ const relative = parentPath === "/" ? hierarchyPath.startsWith("/") ? hierarchyPath.slice(1) : "" : hierarchyPath.startsWith(`${parentPath}/`) ? hierarchyPath.slice(parentPath.length + 1) : "";
55454
+ if (relative.length === 0) return null;
55455
+ const separator = relative.indexOf("/");
55456
+ const segment = separator === -1 ? relative : relative.slice(0, separator);
55457
+ return parentPath === "/" ? `/${segment}` : `${parentPath}/${segment}`;
55458
+ }
55459
+ function mapCrawlPage(row) {
55460
+ return {
55461
+ nodeKey: row.nodeKey,
55462
+ url: row.url,
55463
+ finalUrl: row.finalUrl,
55464
+ path: row.path,
55465
+ parentPath: row.parentPath,
55466
+ discoverySource: row.discoverySource,
55467
+ fetchState: row.fetchState,
55468
+ httpStatus: row.httpStatus,
55469
+ canonicalUrl: row.canonicalUrl,
55470
+ indexabilityState: row.indexabilityState,
55471
+ indexabilityReasons: row.indexabilityReasons,
55472
+ auditState: row.auditState,
55473
+ auditScore: row.auditScore,
55474
+ inventoryEligible: row.inventoryEligible,
55475
+ depth: row.depth,
55476
+ inboundUniqueEdges: row.inboundUniqueEdges,
55477
+ outboundUniqueEdges: row.outboundUniqueEdges,
55478
+ inboundOccurrences: row.inboundOccurrences,
55479
+ outboundOccurrences: row.outboundOccurrences,
55480
+ linkScoreRaw: row.linkScoreRaw,
55481
+ linkScoreNormalized: row.linkScoreNormalized
55482
+ };
55483
+ }
55484
+ function mapCrawlEdge(row) {
55485
+ return {
55486
+ edgeKey: row.edgeKey,
55487
+ sourceNodeKey: row.sourceNodeKey,
55488
+ sourceUrl: row.sourceUrl,
55489
+ targetNodeKey: row.targetNodeKey,
55490
+ targetUrl: row.targetUrl,
55491
+ relation: row.relation,
55492
+ internal: row.internal,
55493
+ followable: row.followable,
55494
+ occurrences: row.occurrences,
55495
+ followableOccurrences: row.followableOccurrences,
55496
+ nofollowOccurrences: row.nofollowOccurrences,
55497
+ anchors: row.anchors
55498
+ };
55499
+ }
54839
55500
  async function technicalAeoRoutes(app, opts) {
55501
+ const resolveCrawl = (projectId, runId) => {
55502
+ const filters = [
55503
+ eq55(siteCrawlSnapshots.projectId, projectId),
55504
+ eq55(runs.projectId, projectId),
55505
+ eq55(runs.kind, RunKinds["site-audit"]),
55506
+ inArray19(runs.status, SURFACEABLE_STATUSES),
55507
+ notProbeRun()
55508
+ ];
55509
+ if (runId) {
55510
+ filters.push(eq55(siteCrawlSnapshots.runId, runId));
55511
+ } else {
55512
+ filters.push(eq55(siteCrawlSnapshots.complete, true), eq55(runs.status, RunStatuses.completed));
55513
+ }
55514
+ return app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq55(siteCrawlSnapshots.runId, runs.id)).where(and43(...filters)).orderBy(desc24(siteCrawlSnapshots.createdAt)).limit(1).get();
55515
+ };
55516
+ const hasLegacyAudit = (projectId) => Boolean(app.db.select({ runId: siteAuditSnapshots.runId }).from(siteAuditSnapshots).innerJoin(runs, eq55(siteAuditSnapshots.runId, runs.id)).where(and43(
55517
+ eq55(siteAuditSnapshots.projectId, projectId),
55518
+ eq55(runs.projectId, projectId),
55519
+ eq55(runs.kind, RunKinds["site-audit"]),
55520
+ inArray19(runs.status, SURFACEABLE_STATUSES),
55521
+ notProbeRun()
55522
+ )).limit(1).get());
55523
+ const emptyCrawlSummary = (projectName, legacyAuditAvailable) => ({
55524
+ project: projectName,
55525
+ hasCrawlData: false,
55526
+ legacyAuditAvailable,
55527
+ runId: null,
55528
+ runStatus: null,
55529
+ rootUrl: null,
55530
+ effectiveOptions: {},
55531
+ complete: false,
55532
+ termination: null,
55533
+ detailsAvailable: false,
55534
+ counts: { pagesDiscovered: 0, pagesFetched: 0, pagesEligible: 0, edges: 0, findings: 0 },
55535
+ deadLinks: { state: "unavailable" }
55536
+ });
54840
55537
  app.get("/projects/:name/technical-aeo", async (request) => {
54841
55538
  const project = resolveProject(app.db, request.params.name);
54842
55539
  const baseFilters = [
54843
55540
  eq55(siteAuditSnapshots.projectId, project.id),
55541
+ eq55(runs.projectId, project.id),
54844
55542
  eq55(runs.kind, RunKinds["site-audit"]),
54845
- inArray18(runs.status, SURFACEABLE_STATUSES),
55543
+ inArray19(runs.status, SURFACEABLE_STATUSES),
54846
55544
  notProbeRun()
54847
55545
  ];
54848
55546
  const targetFilters = request.query.runId ? [...baseFilters, eq55(siteAuditSnapshots.runId, request.query.runId)] : baseFilters;
@@ -54880,8 +55578,9 @@ async function technicalAeoRoutes(app, opts) {
54880
55578
  const project = resolveProject(app.db, request.params.name);
54881
55579
  const targetFilters = [
54882
55580
  eq55(siteAuditSnapshots.projectId, project.id),
55581
+ eq55(runs.projectId, project.id),
54883
55582
  eq55(runs.kind, RunKinds["site-audit"]),
54884
- inArray18(runs.status, SURFACEABLE_STATUSES),
55583
+ inArray19(runs.status, SURFACEABLE_STATUSES),
54885
55584
  notProbeRun()
54886
55585
  ];
54887
55586
  if (request.query.runId) targetFilters.push(eq55(siteAuditSnapshots.runId, request.query.runId));
@@ -54893,7 +55592,7 @@ async function technicalAeoRoutes(app, opts) {
54893
55592
  return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
54894
55593
  }
54895
55594
  const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
54896
- const conds = [eq55(siteAuditPages.runId, latest.runId)];
55595
+ const conds = [eq55(siteAuditPages.projectId, project.id), eq55(siteAuditPages.runId, latest.runId)];
54897
55596
  if (statusFilter) conds.push(eq55(siteAuditPages.status, statusFilter));
54898
55597
  const where = and43(...conds);
54899
55598
  const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
@@ -54921,41 +55620,363 @@ async function technicalAeoRoutes(app, opts) {
54921
55620
  pagesAudited: siteAuditSnapshots.pagesAudited
54922
55621
  }).from(siteAuditSnapshots).innerJoin(runs, eq55(siteAuditSnapshots.runId, runs.id)).where(and43(
54923
55622
  eq55(siteAuditSnapshots.projectId, project.id),
55623
+ eq55(runs.projectId, project.id),
54924
55624
  eq55(runs.kind, RunKinds["site-audit"]),
54925
- inArray18(runs.status, SURFACEABLE_STATUSES),
55625
+ inArray19(runs.status, SURFACEABLE_STATUSES),
54926
55626
  notProbeRun()
54927
55627
  )).orderBy(desc24(siteAuditSnapshots.createdAt)).limit(limit).all();
54928
55628
  return { project: project.name, points: rows.reverse() };
54929
55629
  });
55630
+ app.get("/projects/:name/technical-aeo/crawl", async (request) => {
55631
+ const project = resolveProject(app.db, request.params.name);
55632
+ const target = resolveCrawl(project.id, request.query.runId);
55633
+ const legacyAuditAvailable = hasLegacyAudit(project.id);
55634
+ if (!target) {
55635
+ if (request.query.runId) throw notFound("Site crawl run", request.query.runId);
55636
+ return emptyCrawlSummary(project.name, legacyAuditAvailable);
55637
+ }
55638
+ const snapshot = target.snapshot;
55639
+ const deadLinks = !snapshot.checkDeadLinks || snapshot.deadLinkState === "disabled" ? { state: "disabled" } : snapshot.deadLinkState === "complete" ? { state: "complete", checked: snapshot.deadLinksChecked, found: snapshot.deadLinksFound } : snapshot.deadLinkState === "partial" ? { state: "partial", checked: snapshot.deadLinksChecked, found: snapshot.deadLinksFound } : { state: "unavailable" };
55640
+ return {
55641
+ project: project.name,
55642
+ hasCrawlData: true,
55643
+ legacyAuditAvailable,
55644
+ runId: snapshot.runId,
55645
+ runStatus: target.runStatus,
55646
+ rootUrl: snapshot.rootUrl,
55647
+ crawlSchemaVersion: snapshot.crawlSchemaVersion,
55648
+ engineVersion: snapshot.engineVersion,
55649
+ normalizationVersion: snapshot.normalizationVersion,
55650
+ indexabilityVersion: snapshot.indexabilityVersion,
55651
+ linkScoreVersion: snapshot.linkScoreVersion,
55652
+ effectiveOptions: snapshot.effectiveOptions,
55653
+ complete: snapshot.complete,
55654
+ termination: snapshot.termination,
55655
+ detailsAvailable: snapshot.detailsAvailable,
55656
+ counts: {
55657
+ pagesDiscovered: snapshot.pagesDiscovered,
55658
+ pagesFetched: snapshot.pagesFetched,
55659
+ pagesEligible: snapshot.pagesEligible,
55660
+ edges: snapshot.edgesDiscovered,
55661
+ findings: snapshot.findingsCount
55662
+ },
55663
+ deadLinks
55664
+ };
55665
+ });
55666
+ app.get("/projects/:name/technical-aeo/crawl/pages", async (request) => {
55667
+ const project = resolveProject(app.db, request.params.name);
55668
+ const target = resolveCrawl(project.id, request.query.runId);
55669
+ if (!target) {
55670
+ if (request.query.runId) throw notFound("Site crawl run", request.query.runId);
55671
+ return { project: project.name, hasCrawlData: false, runId: null, total: 0, nextCursor: null, pages: [] };
55672
+ }
55673
+ const snapshot = target.snapshot;
55674
+ if (!snapshot.detailsAvailable || !snapshot.attemptId) {
55675
+ return { project: project.name, hasCrawlData: true, runId: snapshot.runId, total: 0, nextCursor: null, pages: [] };
55676
+ }
55677
+ const filters = [
55678
+ eq55(siteCrawlPages.projectId, project.id),
55679
+ eq55(siteCrawlPages.runId, snapshot.runId),
55680
+ eq55(siteCrawlPages.attemptId, snapshot.attemptId)
55681
+ ];
55682
+ const inventoryEligible = parseBoolean2(request.query.inventoryEligible);
55683
+ if (inventoryEligible != null) filters.push(eq55(siteCrawlPages.inventoryEligible, inventoryEligible));
55684
+ if (request.query.fetchState) filters.push(eq55(siteCrawlPages.fetchState, request.query.fetchState));
55685
+ if (request.query.indexabilityState) filters.push(eq55(siteCrawlPages.indexabilityState, request.query.indexabilityState));
55686
+ if (request.query.auditState) filters.push(eq55(siteCrawlPages.auditState, request.query.auditState));
55687
+ const where = and43(...filters);
55688
+ const total = app.db.select({ value: count() }).from(siteCrawlPages).where(where).get()?.value ?? 0;
55689
+ const limit = parseBoundedLimit(request.query.limit, 100, 200);
55690
+ const offset = decodeCursor(request.query.cursor);
55691
+ const orderBy = request.query.sort === "score-desc" ? [desc24(siteCrawlPages.auditScore), asc10(siteCrawlPages.nodeKey)] : request.query.sort === "score-asc" ? [asc10(siteCrawlPages.auditScore), asc10(siteCrawlPages.nodeKey)] : request.query.sort === "path" ? [asc10(siteCrawlPages.path), asc10(siteCrawlPages.nodeKey)] : [asc10(siteCrawlPages.url), asc10(siteCrawlPages.nodeKey)];
55692
+ const rows = app.db.select().from(siteCrawlPages).where(where).orderBy(...orderBy).limit(limit).offset(offset).all();
55693
+ const nextOffset = offset + rows.length;
55694
+ return {
55695
+ project: project.name,
55696
+ hasCrawlData: true,
55697
+ runId: snapshot.runId,
55698
+ total,
55699
+ nextCursor: nextOffset < total ? encodeCursor2(nextOffset) : null,
55700
+ pages: rows.map(mapCrawlPage)
55701
+ };
55702
+ });
55703
+ app.get("/projects/:name/technical-aeo/structure", async (request) => {
55704
+ const project = resolveProject(app.db, request.params.name);
55705
+ const parentPath = normalizeParentPath(request.query.parentPath);
55706
+ const target = resolveCrawl(project.id, request.query.runId);
55707
+ if (!target) {
55708
+ if (request.query.runId) throw notFound("Site crawl run", request.query.runId);
55709
+ return { project: project.name, hasCrawlData: false, runId: null, parentPath, nextCursor: null, children: [] };
55710
+ }
55711
+ const snapshot = target.snapshot;
55712
+ if (!snapshot.detailsAvailable || !snapshot.attemptId) {
55713
+ return { project: project.name, hasCrawlData: true, runId: snapshot.runId, parentPath, nextCursor: null, children: [] };
55714
+ }
55715
+ const sourceRows = app.db.select({
55716
+ path: siteCrawlPages.path,
55717
+ url: siteCrawlPages.url,
55718
+ inventoryEligible: siteCrawlPages.inventoryEligible,
55719
+ fetchState: siteCrawlPages.fetchState
55720
+ }).from(siteCrawlPages).where(and43(
55721
+ eq55(siteCrawlPages.projectId, project.id),
55722
+ eq55(siteCrawlPages.runId, snapshot.runId),
55723
+ eq55(siteCrawlPages.attemptId, snapshot.attemptId)
55724
+ )).limit(MAX_STRUCTURE_SOURCE_ROWS + 1).all();
55725
+ if (sourceRows.length > MAX_STRUCTURE_SOURCE_ROWS) {
55726
+ throw validationError(`Persisted crawl exceeds the ${MAX_STRUCTURE_SOURCE_ROWS}-page structure limit`);
55727
+ }
55728
+ const children = /* @__PURE__ */ new Map();
55729
+ for (const row of sourceRows) {
55730
+ const childPath = structureChildPath(parentPath, row.path);
55731
+ if (!childPath) continue;
55732
+ const child = children.get(childPath) ?? {
55733
+ url: null,
55734
+ hasPage: false,
55735
+ pageCount: 0,
55736
+ inventoryEligibleCount: 0,
55737
+ fetchedCount: 0
55738
+ };
55739
+ child.pageCount++;
55740
+ if (row.inventoryEligible) child.inventoryEligibleCount++;
55741
+ if (["html", "redirect", "non-html", "fetch-error", "fetched"].includes(row.fetchState)) child.fetchedCount++;
55742
+ if (structureHierarchyPath(row.path) === childPath) {
55743
+ child.hasPage = true;
55744
+ if (child.url == null || row.url > child.url) child.url = row.url;
55745
+ }
55746
+ children.set(childPath, child);
55747
+ }
55748
+ const limit = parseBoundedLimit(request.query.limit, 50, 100);
55749
+ const offset = decodeCursor(request.query.cursor);
55750
+ const sortedChildren = [...children.entries()].sort(([left], [right]) => left.localeCompare(right));
55751
+ const page = sortedChildren.slice(offset, offset + limit + 1);
55752
+ const hasMore = page.length > limit;
55753
+ const visible = hasMore ? page.slice(0, limit) : page;
55754
+ return {
55755
+ project: project.name,
55756
+ hasCrawlData: true,
55757
+ runId: snapshot.runId,
55758
+ parentPath,
55759
+ nextCursor: hasMore ? encodeCursor2(offset + visible.length) : null,
55760
+ children: visible.map(([path8, child]) => ({
55761
+ path: path8,
55762
+ url: child.url,
55763
+ hasPage: child.hasPage,
55764
+ pageCount: child.pageCount,
55765
+ inventoryEligibleCount: child.inventoryEligibleCount,
55766
+ fetchedCount: child.fetchedCount
55767
+ }))
55768
+ };
55769
+ });
55770
+ app.get("/projects/:name/technical-aeo/internal-links", async (request) => {
55771
+ const project = resolveProject(app.db, request.params.name);
55772
+ const target = resolveCrawl(project.id, request.query.runId);
55773
+ if (!target) {
55774
+ if (request.query.runId) throw notFound("Site crawl run", request.query.runId);
55775
+ return { project: project.name, hasCrawlData: false, runId: null, total: 0, nextCursor: null, edges: [] };
55776
+ }
55777
+ const snapshot = target.snapshot;
55778
+ if (!snapshot.detailsAvailable || !snapshot.attemptId) {
55779
+ return { project: project.name, hasCrawlData: true, runId: snapshot.runId, total: 0, nextCursor: null, edges: [] };
55780
+ }
55781
+ const filters = [
55782
+ eq55(siteCrawlEdges.projectId, project.id),
55783
+ eq55(siteCrawlEdges.runId, snapshot.runId),
55784
+ eq55(siteCrawlEdges.attemptId, snapshot.attemptId),
55785
+ eq55(siteCrawlEdges.internal, true)
55786
+ ];
55787
+ if (request.query.sourceUrl) filters.push(eq55(siteCrawlEdges.sourceUrl, request.query.sourceUrl));
55788
+ if (request.query.targetUrl) filters.push(eq55(siteCrawlEdges.targetUrl, request.query.targetUrl));
55789
+ const followable = parseBoolean2(request.query.followable);
55790
+ if (followable != null) filters.push(eq55(siteCrawlEdges.followable, followable));
55791
+ const where = and43(...filters);
55792
+ const total = app.db.select({ value: count() }).from(siteCrawlEdges).where(where).get()?.value ?? 0;
55793
+ const limit = parseBoundedLimit(request.query.limit, 100, 200);
55794
+ const offset = decodeCursor(request.query.cursor);
55795
+ const rows = app.db.select().from(siteCrawlEdges).where(where).orderBy(asc10(siteCrawlEdges.edgeKey)).limit(limit).offset(offset).all();
55796
+ const nextOffset = offset + rows.length;
55797
+ return {
55798
+ project: project.name,
55799
+ hasCrawlData: true,
55800
+ runId: snapshot.runId,
55801
+ total,
55802
+ nextCursor: nextOffset < total ? encodeCursor2(nextOffset) : null,
55803
+ edges: rows.map(mapCrawlEdge)
55804
+ };
55805
+ });
55806
+ app.get("/projects/:name/technical-aeo/internal-links/neighbors", async (request) => {
55807
+ const project = resolveProject(app.db, request.params.name);
55808
+ if (!request.query.nodeKey && !request.query.url) {
55809
+ throw validationError("Provide nodeKey or url");
55810
+ }
55811
+ const target = resolveCrawl(project.id, request.query.runId);
55812
+ if (!target) {
55813
+ if (request.query.runId) throw notFound("Site crawl run", request.query.runId);
55814
+ return {
55815
+ project: project.name,
55816
+ hasCrawlData: false,
55817
+ runId: null,
55818
+ nodeKey: request.query.nodeKey ?? null,
55819
+ url: request.query.url ?? null,
55820
+ inbound: [],
55821
+ outbound: [],
55822
+ inboundTruncated: false,
55823
+ outboundTruncated: false
55824
+ };
55825
+ }
55826
+ const snapshot = target.snapshot;
55827
+ if (!snapshot.detailsAvailable || !snapshot.attemptId) {
55828
+ return {
55829
+ project: project.name,
55830
+ hasCrawlData: true,
55831
+ runId: snapshot.runId,
55832
+ nodeKey: request.query.nodeKey ?? null,
55833
+ url: request.query.url ?? null,
55834
+ inbound: [],
55835
+ outbound: [],
55836
+ inboundTruncated: false,
55837
+ outboundTruncated: false
55838
+ };
55839
+ }
55840
+ const scope = [
55841
+ eq55(siteCrawlEdges.projectId, project.id),
55842
+ eq55(siteCrawlEdges.runId, snapshot.runId),
55843
+ eq55(siteCrawlEdges.attemptId, snapshot.attemptId),
55844
+ eq55(siteCrawlEdges.internal, true)
55845
+ ];
55846
+ const inboundMatch = request.query.nodeKey && request.query.url ? or11(eq55(siteCrawlEdges.targetNodeKey, request.query.nodeKey), eq55(siteCrawlEdges.targetUrl, request.query.url)) : request.query.nodeKey ? eq55(siteCrawlEdges.targetNodeKey, request.query.nodeKey) : eq55(siteCrawlEdges.targetUrl, request.query.url);
55847
+ const outboundMatch = request.query.nodeKey && request.query.url ? or11(eq55(siteCrawlEdges.sourceNodeKey, request.query.nodeKey), eq55(siteCrawlEdges.sourceUrl, request.query.url)) : request.query.nodeKey ? eq55(siteCrawlEdges.sourceNodeKey, request.query.nodeKey) : eq55(siteCrawlEdges.sourceUrl, request.query.url);
55848
+ const limit = parseBoundedLimit(request.query.limit, 50, 100);
55849
+ const inboundRows = app.db.select().from(siteCrawlEdges).where(and43(...scope, inboundMatch)).orderBy(asc10(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
55850
+ const outboundRows = app.db.select().from(siteCrawlEdges).where(and43(...scope, outboundMatch)).orderBy(asc10(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
55851
+ return {
55852
+ project: project.name,
55853
+ hasCrawlData: true,
55854
+ runId: snapshot.runId,
55855
+ nodeKey: request.query.nodeKey ?? null,
55856
+ url: request.query.url ?? null,
55857
+ inbound: inboundRows.slice(0, limit).map(mapCrawlEdge),
55858
+ outbound: outboundRows.slice(0, limit).map(mapCrawlEdge),
55859
+ inboundTruncated: inboundRows.length > limit,
55860
+ outboundTruncated: outboundRows.length > limit
55861
+ };
55862
+ });
55863
+ app.get("/projects/:name/technical-aeo/dead-links", async (request) => {
55864
+ const project = resolveProject(app.db, request.params.name);
55865
+ const target = resolveCrawl(project.id, request.query.runId);
55866
+ const legacyAuditAvailable = hasLegacyAudit(project.id);
55867
+ if (!target) {
55868
+ if (request.query.runId) throw notFound("Site crawl run", request.query.runId);
55869
+ return { project: project.name, runId: null, state: "unavailable", legacyAuditAvailable };
55870
+ }
55871
+ const snapshot = target.snapshot;
55872
+ if (!snapshot.checkDeadLinks || snapshot.deadLinkState === "disabled") {
55873
+ return { project: project.name, runId: snapshot.runId, state: "disabled", checkDeadLinks: false };
55874
+ }
55875
+ if (snapshot.deadLinkState === "unavailable") {
55876
+ return { project: project.name, runId: snapshot.runId, state: "unavailable", legacyAuditAvailable };
55877
+ }
55878
+ if (!snapshot.attemptId) {
55879
+ return { project: project.name, runId: snapshot.runId, state: "partial", checkDeadLinks: true, checked: snapshot.deadLinksChecked, found: snapshot.deadLinksFound, total: 0, nextCursor: null, deadLinks: [] };
55880
+ }
55881
+ const where = and43(
55882
+ eq55(siteCrawlFindings.projectId, project.id),
55883
+ eq55(siteCrawlFindings.runId, snapshot.runId),
55884
+ eq55(siteCrawlFindings.attemptId, snapshot.attemptId),
55885
+ eq55(siteCrawlFindings.findingType, "dead-link")
55886
+ );
55887
+ const total = app.db.select({ value: count() }).from(siteCrawlFindings).where(where).get()?.value ?? 0;
55888
+ const limit = parseBoundedLimit(request.query.limit, 100, 200);
55889
+ const offset = decodeCursor(request.query.cursor);
55890
+ const rows = app.db.select().from(siteCrawlFindings).where(where).orderBy(asc10(siteCrawlFindings.findingKey)).limit(limit).offset(offset).all();
55891
+ const nextOffset = offset + rows.length;
55892
+ const deadLinks = rows.map((row) => ({
55893
+ findingKey: row.findingKey,
55894
+ severity: row.severity,
55895
+ sourceNodeKey: row.sourceNodeKey,
55896
+ sourceUrl: row.sourceUrl,
55897
+ targetNodeKey: row.targetNodeKey,
55898
+ targetUrl: row.targetUrl,
55899
+ evidence: row.evidence
55900
+ }));
55901
+ const state = snapshot.deadLinkState === "complete" ? "complete" : "partial";
55902
+ return {
55903
+ project: project.name,
55904
+ runId: snapshot.runId,
55905
+ state,
55906
+ checkDeadLinks: true,
55907
+ checked: snapshot.deadLinksChecked,
55908
+ found: snapshot.deadLinksFound,
55909
+ total,
55910
+ nextCursor: nextOffset < total ? encodeCursor2(nextOffset) : null,
55911
+ deadLinks
55912
+ };
55913
+ });
54930
55914
  app.post("/projects/:name/technical-aeo/runs", async (request) => {
54931
55915
  const project = resolveProject(app.db, request.params.name);
54932
55916
  const parsed = siteAuditRunRequestSchema.safeParse(request.body ?? {});
54933
55917
  if (!parsed.success) {
54934
55918
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
54935
55919
  }
54936
- const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and43(
54937
- eq55(runs.projectId, project.id),
54938
- eq55(runs.kind, RunKinds["site-audit"]),
54939
- inArray18(runs.status, [RunStatuses.queued, RunStatuses.running])
54940
- )).get();
54941
- if (existing) {
54942
- return { runId: existing.id, status: existing.status };
55920
+ const effectiveRequest = normalizeSiteAuditRunRequest(parsed.data);
55921
+ const identityKey = siteAuditRequestIdentity(effectiveRequest);
55922
+ const result = app.db.transaction((tx) => {
55923
+ const existing = tx.select({
55924
+ id: runs.id,
55925
+ status: runs.status,
55926
+ identityKey: siteCrawlRunRequests.identityKey,
55927
+ effectiveOptions: siteCrawlRunRequests.effectiveOptions
55928
+ }).from(runs).leftJoin(siteCrawlRunRequests, and43(
55929
+ eq55(siteCrawlRunRequests.projectId, runs.projectId),
55930
+ eq55(siteCrawlRunRequests.runId, runs.id)
55931
+ )).where(and43(
55932
+ eq55(runs.projectId, project.id),
55933
+ eq55(runs.kind, RunKinds["site-audit"]),
55934
+ inArray19(runs.status, [RunStatuses.queued, RunStatuses.running])
55935
+ )).get();
55936
+ if (existing) {
55937
+ if (existing.identityKey === identityKey) {
55938
+ return { runId: existing.id, status: existing.status, created: false };
55939
+ }
55940
+ throw operationInProgress(
55941
+ `A Technical AEO crawl is already ${existing.status} with different request options.`,
55942
+ {
55943
+ activeRunId: existing.id,
55944
+ activeStatus: existing.status,
55945
+ activeOptions: existing.effectiveOptions,
55946
+ requestedOptions: effectiveRequest
55947
+ }
55948
+ );
55949
+ }
55950
+ const now = (/* @__PURE__ */ new Date()).toISOString();
55951
+ const runId = crypto43.randomUUID();
55952
+ tx.insert(runs).values({
55953
+ id: runId,
55954
+ projectId: project.id,
55955
+ kind: RunKinds["site-audit"],
55956
+ status: RunStatuses.queued,
55957
+ trigger: RunTriggers.manual,
55958
+ createdAt: now
55959
+ }).run();
55960
+ tx.insert(siteCrawlRunRequests).values({
55961
+ runId,
55962
+ projectId: project.id,
55963
+ identityKey,
55964
+ effectiveOptions: effectiveRequest,
55965
+ createdAt: now
55966
+ }).run();
55967
+ return { runId, status: RunStatuses.queued, created: true };
55968
+ });
55969
+ if (result.created) {
55970
+ opts.onSiteAuditRequested?.(result.runId, project.id, {
55971
+ sitemapUrl: effectiveRequest.sitemapUrl ?? void 0,
55972
+ limit: parsed.data.limit,
55973
+ maxPages: effectiveRequest.maxPages,
55974
+ maxEdges: effectiveRequest.maxEdges,
55975
+ maxDepth: effectiveRequest.maxDepth ?? void 0,
55976
+ checkDeadLinks: effectiveRequest.checkDeadLinks
55977
+ });
54943
55978
  }
54944
- const now = (/* @__PURE__ */ new Date()).toISOString();
54945
- const runId = crypto43.randomUUID();
54946
- app.db.insert(runs).values({
54947
- id: runId,
54948
- projectId: project.id,
54949
- kind: RunKinds["site-audit"],
54950
- status: RunStatuses.queued,
54951
- trigger: RunTriggers.manual,
54952
- createdAt: now
54953
- }).run();
54954
- opts.onSiteAuditRequested?.(runId, project.id, {
54955
- sitemapUrl: parsed.data.sitemapUrl,
54956
- limit: parsed.data.limit
54957
- });
54958
- return { runId, status: RunStatuses.queued };
55979
+ return { runId: result.runId, status: result.status };
54959
55980
  });
54960
55981
  }
54961
55982
 
@@ -55105,6 +56126,7 @@ async function apiRoutes(app, opts) {
55105
56126
  await api.register(competitorRoutes);
55106
56127
  await api.register(runRoutes, {
55107
56128
  onRunCreated: opts.onRunCreated,
56129
+ onRunCancelled: opts.onRunCancelled,
55108
56130
  validProviderNames: opts.providerAdapters?.map((a) => a.name),
55109
56131
  getRunnableProviderNames: opts.getRunnableProviderNames,
55110
56132
  getEffectiveProviderModels: opts.getEffectiveProviderModels
@@ -55626,7 +56648,7 @@ var IntelligenceService = class {
55626
56648
  const recentRuns = this.db.select().from(runs).where(
55627
56649
  and45(
55628
56650
  eq57(runs.projectId, projectId),
55629
- or11(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
56651
+ or12(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
55630
56652
  // Defensive: RunCoordinator already skips probes before this is
55631
56653
  // called, but if a future call site invokes analyzeAndPersist
55632
56654
  // directly for a probe, probes still must not pollute the
@@ -55929,7 +56951,7 @@ var IntelligenceService = class {
55929
56951
  const allRuns = this.db.select().from(runs).where(
55930
56952
  and45(
55931
56953
  eq57(runs.projectId, project.id),
55932
- or11(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
56954
+ or12(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
55933
56955
  // Backfill must not replay probe runs as if they were real sweeps.
55934
56956
  ne7(runs.trigger, RunTriggers.probe)
55935
56957
  )
@@ -55962,7 +56984,7 @@ var IntelligenceService = class {
55962
56984
  let wouldDeleteTotal = 0;
55963
56985
  const existingByRunId = /* @__PURE__ */ new Map();
55964
56986
  if (isDryRun && targetRuns.length > 0) {
55965
- const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray19(insights.runId, targetRuns.map((r) => r.id))).all();
56987
+ const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray20(insights.runId, targetRuns.map((r) => r.id))).all();
55966
56988
  for (const r of rows) {
55967
56989
  if (r.runId == null) continue;
55968
56990
  existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
@@ -56107,7 +57129,7 @@ var IntelligenceService = class {
56107
57129
  and45(
56108
57130
  eq57(runs.projectId, projectId),
56109
57131
  eq57(runs.kind, RunKinds["answer-visibility"]),
56110
- or11(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
57132
+ or12(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
56111
57133
  // Defensive — see top of file.
56112
57134
  ne7(runs.trigger, RunTriggers.probe)
56113
57135
  )
@@ -56127,7 +57149,7 @@ var IntelligenceService = class {
56127
57149
  const haveHistory = recentRunIds.length > 0;
56128
57150
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
56129
57151
  if (haveHistory) {
56130
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and45(eq57(insights.type, "regression"), inArray19(insights.runId, recentRunIds))).all();
57152
+ const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and45(eq57(insights.type, "regression"), inArray20(insights.runId, recentRunIds))).all();
56131
57153
  const regressionGroups = /* @__PURE__ */ new Map();
56132
57154
  for (const row of priorRows) {
56133
57155
  if (!row.runId) continue;
@@ -56233,6 +57255,13 @@ export {
56233
57255
  gscCoverageSnapshots,
56234
57256
  siteAuditSnapshots,
56235
57257
  siteAuditPages,
57258
+ siteCrawlRunRequests,
57259
+ siteCrawlAttempts,
57260
+ siteCrawlSnapshots,
57261
+ siteCrawlPages,
57262
+ siteCrawlEdges,
57263
+ siteCrawlFindings,
57264
+ siteCrawlEventReceipts,
56236
57265
  bingCoverageSnapshots,
56237
57266
  bingUrlInspections,
56238
57267
  gaTrafficSnapshots,