@canonry/canonry 4.154.1 → 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 (26) hide show
  1. package/assets/agent-workspace/AGENTS.md +2 -2
  2. package/assets/agent-workspace/skills/aero/references/orchestration.md +3 -3
  3. package/assets/agent-workspace/skills/canonry/SKILL.md +2 -2
  4. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +14 -6
  5. package/assets/assets/{AuditHistoryPanel-WKf-vvmg.js → AuditHistoryPanel-BlbMBxOY.js} +1 -1
  6. package/assets/assets/{BacklinksPage-By06DbHN.js → BacklinksPage-DA9EaV9q.js} +1 -1
  7. package/assets/assets/{HistoryPage-C_PcPiyX.js → HistoryPage-BTrIHjMQ.js} +1 -1
  8. package/assets/assets/{MeasurementPropertyPage-BHnNegEa.js → MeasurementPropertyPage-CN6o5xFg.js} +1 -1
  9. package/assets/assets/{ProjectPage-VTbW21dz.js → ProjectPage-CsDbgp_0.js} +1 -1
  10. package/assets/assets/{RunRow-9qn5Eer_.js → RunRow-ch_OlCPs.js} +1 -1
  11. package/assets/assets/{RunsPage-FxMTprK1.js → RunsPage-T89Y_yS8.js} +1 -1
  12. package/assets/assets/{SettingsPage-Cr7_PrNW.js → SettingsPage-DnmEq3F9.js} +1 -1
  13. package/assets/assets/{TrafficPage-D0qk9uMN.js → TrafficPage-CW1UhaJt.js} +1 -1
  14. package/assets/assets/{TrafficSourceDetailPage-CXa_3_Gy.js → TrafficSourceDetailPage-r4Ji3wJm.js} +1 -1
  15. package/assets/assets/{extract-error-message-RkTEH1Vb.js → extract-error-message-a8EErBoZ.js} +1 -1
  16. package/assets/assets/{index-nSWJ9yka.js → index-BKq6k5Of.js} +26 -26
  17. package/assets/index.html +1 -1
  18. package/dist/{chunk-ANSQ6JNO.js → chunk-OSHRDV36.js} +317 -4
  19. package/dist/{chunk-NNW7TJUZ.js → chunk-VIKPFI6F.js} +200 -5
  20. package/dist/{chunk-KRMLGFY3.js → chunk-VOL2BSM5.js} +1113 -92
  21. package/dist/{chunk-JOP2DGXL.js → chunk-YSESZNVT.js} +609 -165
  22. package/dist/cli.js +319 -7
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-ZY5KOSZV.js → intelligence-service-G4NV3X36.js} +2 -2
  25. package/dist/mcp.js +2 -2
  26. package/package.json +8 -8
@@ -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,
@@ -23110,7 +23546,7 @@ function buildInsightList(db, projectId, locationFilter) {
23110
23546
  )
23111
23547
  ).orderBy(desc12(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
23112
23548
  if (recentRunIds.length === 0) return [];
23113
- 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();
23114
23550
  const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
23115
23551
  const flat = rows.filter((r) => !r.dismissed).map((r) => {
23116
23552
  const recommendation = r.recommendation;
@@ -24859,7 +25295,7 @@ async function organicEvidenceRoutes(app) {
24859
25295
  }
24860
25296
 
24861
25297
  // ../api-routes/src/citations.ts
24862
- 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";
24863
25299
  async function citationRoutes(app) {
24864
25300
  app.get("/projects/:name/citations/visibility", async (request, reply) => {
24865
25301
  const project = resolveProject(app.db, request.params.name);
@@ -24883,7 +25319,7 @@ async function citationRoutes(app) {
24883
25319
  competitorOverlap: querySnapshots.competitorOverlap,
24884
25320
  answerMentioned: querySnapshots.answerMentioned,
24885
25321
  createdAt: querySnapshots.createdAt
24886
- }).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();
24887
25323
  if (rawSnapshots.length === 0) {
24888
25324
  return reply.send(emptyCitationVisibility("no-runs-yet"));
24889
25325
  }
@@ -25022,7 +25458,7 @@ function computeCitationVisibility(input) {
25022
25458
  }
25023
25459
 
25024
25460
  // ../api-routes/src/visibility-stats.ts
25025
- 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";
25026
25462
 
25027
25463
  // ../api-routes/src/visibility-compare.ts
25028
25464
  var VISIBILITY_COMPARE_MIN_RUNS = 5;
@@ -25433,7 +25869,7 @@ async function visibilityStatsRoutes(app) {
25433
25869
  citationState: querySnapshots.citationState,
25434
25870
  answerMentioned: querySnapshots.answerMentioned,
25435
25871
  createdAt: querySnapshots.createdAt
25436
- }).from(querySnapshots).where(inArray13(querySnapshots.runId, runIds)).all() : [];
25872
+ }).from(querySnapshots).where(inArray14(querySnapshots.runId, runIds)).all() : [];
25437
25873
  const stats = computeVisibilityStats({ queries: projectQueries, snapshots, groupBy });
25438
25874
  const queryAttribution = buildQueryAttribution(projectQueries);
25439
25875
  let shareOfVoice;
@@ -25448,7 +25884,7 @@ async function visibilityStatsRoutes(app) {
25448
25884
  queryText: querySnapshots.queryText,
25449
25885
  answerMentioned: querySnapshots.answerMentioned,
25450
25886
  answerText: querySnapshots.answerText
25451
- }).from(querySnapshots).where(inArray13(querySnapshots.runId, runIds)).all() : [];
25887
+ }).from(querySnapshots).where(inArray14(querySnapshots.runId, runIds)).all() : [];
25452
25888
  const attributedSovSnapshots = sovSnapshots.filter((s) => resolveCurrentQuery(queryAttribution, s) !== void 0);
25453
25889
  const result = buildMentionShare(
25454
25890
  attributedSovSnapshots.map((s) => ({ projectMentioned: s.answerMentioned === true, answerText: s.answerText })),
@@ -25529,7 +25965,7 @@ async function visibilityStatsRoutes(app) {
25529
25965
  answerMentioned: querySnapshots.answerMentioned,
25530
25966
  answerText: querySnapshots.answerText,
25531
25967
  citedDomains: querySnapshots.citedDomains
25532
- }).from(querySnapshots).where(inArray13(querySnapshots.runId, runIds)).all() : [];
25968
+ }).from(querySnapshots).where(inArray14(querySnapshots.runId, runIds)).all() : [];
25533
25969
  return { runCount: monthRuns.length, snapshots };
25534
25970
  };
25535
25971
  const fromMonth = loadMonth(fromBounds);
@@ -25784,7 +26220,7 @@ async function resultsExportRoutes(app) {
25784
26220
  }
25785
26221
 
25786
26222
  // ../api-routes/src/composites.ts
25787
- 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";
25788
26224
  var TOP_INSIGHT_LIMIT = 5;
25789
26225
  var SEARCH_HIT_HARD_LIMIT = 50;
25790
26226
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -26051,7 +26487,7 @@ function loadSnapshotsByRunIds(app, runIds, queryIdByText) {
26051
26487
  answerText: querySnapshots.answerText,
26052
26488
  competitorOverlap: querySnapshots.competitorOverlap,
26053
26489
  citedDomains: querySnapshots.citedDomains
26054
- }).from(querySnapshots).where(inArray14(querySnapshots.runId, [...runIds])).all();
26490
+ }).from(querySnapshots).where(inArray15(querySnapshots.runId, [...runIds])).all();
26055
26491
  for (const row of rows) {
26056
26492
  const queryText = row.queryText?.trim() || null;
26057
26493
  let queryId;
@@ -26686,6 +27122,12 @@ var SCHEMA_TABLE = {
26686
27122
  SiteAuditRunResponseDto: siteAuditRunResponseSchema,
26687
27123
  SiteAuditScoreDto: siteAuditScoreSchema,
26688
27124
  SiteAuditTrendResponseDto: siteAuditTrendResponseSchema,
27125
+ SiteCrawlDeadLinksResponseDto: siteCrawlDeadLinksResponseSchema,
27126
+ SiteCrawlInternalLinksResponseDto: siteCrawlInternalLinksResponseSchema,
27127
+ SiteCrawlNeighborsResponseDto: siteCrawlNeighborsResponseSchema,
27128
+ SiteCrawlPagesResponseDto: siteCrawlPagesResponseSchema,
27129
+ SiteCrawlStructureResponseDto: siteCrawlStructureResponseSchema,
27130
+ SiteCrawlSummaryDto: siteCrawlSummarySchema,
26689
27131
  SnapshotDiffResponse: snapshotDiffResponseSchema,
26690
27132
  SnapshotListResponse: snapshotListResponseSchema,
26691
27133
  SnapshotReportDto: snapshotReportSchema,
@@ -27142,6 +27584,18 @@ var measurementCursorParameter = {
27142
27584
  description: "Opaque cursor from the previous page.",
27143
27585
  schema: stringSchema
27144
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
+ };
27145
27599
  var measurementOverviewSortParameter = {
27146
27600
  name: "sort",
27147
27601
  in: "query",
@@ -32593,11 +33047,122 @@ var routeCatalog = [
32593
33047
  404: errorResponse("Project not found.")
32594
33048
  }
32595
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
+ },
32596
33161
  {
32597
33162
  method: "post",
32598
33163
  path: "/api/v1/projects/{name}/technical-aeo/runs",
32599
33164
  summary: "Trigger a Technical AEO site-audit run",
32600
- 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.",
32601
33166
  tags: ["technical-aeo"],
32602
33167
  parameters: [nameParameter],
32603
33168
  requestBody: {
@@ -32607,8 +33172,12 @@ var routeCatalog = [
32607
33172
  schema: {
32608
33173
  type: "object",
32609
33174
  properties: {
32610
- sitemapUrl: { ...stringSchema, description: "Override the sitemap URL. Defaults to https://<canonicalDomain>/sitemap.xml." },
32611
- 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." }
32612
33181
  }
32613
33182
  }
32614
33183
  }
@@ -32617,6 +33186,7 @@ var routeCatalog = [
32617
33186
  responses: {
32618
33187
  200: jsonResponse("Site-audit run queued (or the in-flight run returned).", "SiteAuditRunResponseDto"),
32619
33188
  400: errorResponse("Invalid site-audit request."),
33189
+ 409: errorResponse("A crawl with different effective options is already queued or running."),
32620
33190
  404: errorResponse("Project not found.")
32621
33191
  }
32622
33192
  }
@@ -33654,7 +34224,7 @@ function formatNotification(row) {
33654
34224
 
33655
34225
  // ../api-routes/src/google.ts
33656
34226
  import crypto29 from "crypto";
33657
- 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";
33658
34228
 
33659
34229
  // ../api-routes/src/gbp-summary.ts
33660
34230
  function computeMetricTotals(rows) {
@@ -36914,10 +37484,10 @@ async function googleRoutes(app, opts) {
36914
37484
  locationProfiles: []
36915
37485
  });
36916
37486
  }
36917
- const metricRows = app.db.select().from(gbpDailyMetrics).where(and30(eq39(gbpDailyMetrics.projectId, project.id), inArray15(gbpDailyMetrics.locationName, locationNames))).all();
36918
- const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and30(eq39(gbpKeywordImpressions.projectId, project.id), inArray15(gbpKeywordImpressions.locationName, locationNames))).all();
36919
- const placeActionRows = app.db.select().from(gbpPlaceActions).where(and30(eq39(gbpPlaceActions.projectId, project.id), inArray15(gbpPlaceActions.locationName, locationNames))).all();
36920
- 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();
36921
37491
  const latestLodgingByLocation = /* @__PURE__ */ new Map();
36922
37492
  for (const row of lodgingRows) {
36923
37493
  if (!latestLodgingByLocation.has(row.locationName)) {
@@ -36931,7 +37501,7 @@ async function googleRoutes(app, opts) {
36931
37501
  regularHours: gbpLocations.regularHours,
36932
37502
  primaryPhone: gbpLocations.primaryPhone,
36933
37503
  openStatus: gbpLocations.openStatus
36934
- }).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();
36935
37505
  return buildGbpSummary({
36936
37506
  locationName,
36937
37507
  locationCount: locationNames.length,
@@ -36947,7 +37517,7 @@ async function googleRoutes(app, opts) {
36947
37517
 
36948
37518
  // ../api-routes/src/ads.ts
36949
37519
  import crypto32 from "crypto";
36950
- 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";
36951
37521
 
36952
37522
  // ../api-routes/src/ads-activation-routes.ts
36953
37523
  import crypto31 from "crypto";
@@ -39894,7 +40464,7 @@ function semanticallyMatchesMaterializedEntity(app, input) {
39894
40464
  eq41(adsOperations.projectId, input.projectId),
39895
40465
  eq41(adsOperations.entityType, input.entityType),
39896
40466
  eq41(adsOperations.entityId, input.entityId),
39897
- inArray16(adsOperations.kind, [createKind, updateKind])
40467
+ inArray17(adsOperations.kind, [createKind, updateKind])
39898
40468
  )).orderBy(asc8(adsOperations.createdAt), asc8(adsOperations.id)).all();
39899
40469
  if (rows.some((row) => row.state === AdsOperationStates.pending || row.state === AdsOperationStates.reconciling || row.state === AdsOperationStates.unknown || row.kind === updateKind && row.state === AdsOperationStates.succeeded)) {
39900
40470
  return false;
@@ -40331,7 +40901,7 @@ function quarantineExhaustedReconciliation(app, row, now, policy, context) {
40331
40901
  eq41(adsOperations.id, row.id),
40332
40902
  eq41(adsOperations.projectId, row.projectId),
40333
40903
  gte9(adsOperations.reconcileAttempts, policy.maxAttempts),
40334
- inArray16(adsOperations.state, [
40904
+ inArray17(adsOperations.state, [
40335
40905
  AdsOperationStates.pending,
40336
40906
  AdsOperationStates.unknown,
40337
40907
  AdsOperationStates.reconciling
@@ -40555,7 +41125,7 @@ async function sweepAdsOperationsOnce(app, opts, config) {
40555
41125
  eq41(adsOperations.reconcileStrategy, AdsReconcileStrategies.create_fingerprint),
40556
41126
  or9(
40557
41127
  isNotNull2(adsOperations.entityId),
40558
- inArray16(adsOperations.state, [
41128
+ inArray17(adsOperations.state, [
40559
41129
  AdsOperationStates.pending,
40560
41130
  AdsOperationStates.reconciling
40561
41131
  ])
@@ -40603,7 +41173,7 @@ async function sweepAdsOperationsOnce(app, opts, config) {
40603
41173
  const eligibleProjectIds = [...credentialsByProjectId.keys()];
40604
41174
  if (eligibleProjectIds.length === 0) return;
40605
41175
  const rows = app.db.select().from(adsOperations).where(and32(
40606
- inArray16(adsOperations.projectId, eligibleProjectIds),
41176
+ inArray17(adsOperations.projectId, eligibleProjectIds),
40607
41177
  staleReconcileWhere
40608
41178
  )).orderBy(asc8(adsOperations.updatedAt)).limit(config.batchSize).all();
40609
41179
  for (const row of rows) {
@@ -40931,7 +41501,7 @@ async function adsRoutes(app, opts) {
40931
41501
  const cursor = parsed.data.cursor ? decodeAdsOperationCursor(parsed.data.cursor, project.id, parsed.data.state) : void 0;
40932
41502
  const rows = app.db.select().from(adsOperations).where(and32(
40933
41503
  eq41(adsOperations.projectId, project.id),
40934
- inArray16(adsOperations.state, parsed.data.state),
41504
+ inArray17(adsOperations.state, parsed.data.state),
40935
41505
  cursor ? or9(
40936
41506
  gt2(adsOperations.createdAt, cursor.createdAt),
40937
41507
  and32(
@@ -41278,7 +41848,7 @@ async function adsRoutes(app, opts) {
41278
41848
  const inFlight = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and32(
41279
41849
  eq41(runs.projectId, project.id),
41280
41850
  eq41(runs.kind, RunKinds["ads-sync"]),
41281
- inArray16(runs.status, [RunStatuses.queued, RunStatuses.running])
41851
+ inArray17(runs.status, [RunStatuses.queued, RunStatuses.running])
41282
41852
  )).get();
41283
41853
  if (inFlight) {
41284
41854
  const existing = { runId: inFlight.id, status: inFlight.status };
@@ -54135,7 +54705,7 @@ async function doctorRoutes(app, opts) {
54135
54705
 
54136
54706
  // ../api-routes/src/discovery/routes.ts
54137
54707
  import crypto41 from "crypto";
54138
- 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";
54139
54709
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
54140
54710
  async function discoveryRoutes(app, opts) {
54141
54711
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -54190,7 +54760,7 @@ async function discoveryRoutes(app, opts) {
54190
54760
  // must never reuse another set's session. Null (= Gemini-only
54191
54761
  // default, including explicit ['gemini']) matches legacy rows.
54192
54762
  seedProviders == null ? isNull4(discoverySessions.seedProviders) : eq53(discoverySessions.seedProviders, seedProviders),
54193
- inArray17(discoverySessions.status, [
54763
+ inArray18(discoverySessions.status, [
54194
54764
  DiscoverySessionStatuses.queued,
54195
54765
  DiscoverySessionStatuses.seeding,
54196
54766
  DiscoverySessionStatuses.probing
@@ -54815,7 +55385,7 @@ function dedupeStrings(input) {
54815
55385
 
54816
55386
  // ../api-routes/src/technical-aeo.ts
54817
55387
  import crypto43 from "crypto";
54818
- 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";
54819
55389
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
54820
55390
  function emptyScore(projectName) {
54821
55391
  return {
@@ -54844,13 +55414,133 @@ function parsePositiveInt(value, fallback, max) {
54844
55414
  if (!Number.isFinite(n) || n < 0) return fallback;
54845
55415
  return Math.min(max, Math.floor(n));
54846
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
+ }
54847
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
+ });
54848
55537
  app.get("/projects/:name/technical-aeo", async (request) => {
54849
55538
  const project = resolveProject(app.db, request.params.name);
54850
55539
  const baseFilters = [
54851
55540
  eq55(siteAuditSnapshots.projectId, project.id),
55541
+ eq55(runs.projectId, project.id),
54852
55542
  eq55(runs.kind, RunKinds["site-audit"]),
54853
- inArray18(runs.status, SURFACEABLE_STATUSES),
55543
+ inArray19(runs.status, SURFACEABLE_STATUSES),
54854
55544
  notProbeRun()
54855
55545
  ];
54856
55546
  const targetFilters = request.query.runId ? [...baseFilters, eq55(siteAuditSnapshots.runId, request.query.runId)] : baseFilters;
@@ -54888,8 +55578,9 @@ async function technicalAeoRoutes(app, opts) {
54888
55578
  const project = resolveProject(app.db, request.params.name);
54889
55579
  const targetFilters = [
54890
55580
  eq55(siteAuditSnapshots.projectId, project.id),
55581
+ eq55(runs.projectId, project.id),
54891
55582
  eq55(runs.kind, RunKinds["site-audit"]),
54892
- inArray18(runs.status, SURFACEABLE_STATUSES),
55583
+ inArray19(runs.status, SURFACEABLE_STATUSES),
54893
55584
  notProbeRun()
54894
55585
  ];
54895
55586
  if (request.query.runId) targetFilters.push(eq55(siteAuditSnapshots.runId, request.query.runId));
@@ -54901,7 +55592,7 @@ async function technicalAeoRoutes(app, opts) {
54901
55592
  return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
54902
55593
  }
54903
55594
  const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
54904
- const conds = [eq55(siteAuditPages.runId, latest.runId)];
55595
+ const conds = [eq55(siteAuditPages.projectId, project.id), eq55(siteAuditPages.runId, latest.runId)];
54905
55596
  if (statusFilter) conds.push(eq55(siteAuditPages.status, statusFilter));
54906
55597
  const where = and43(...conds);
54907
55598
  const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
@@ -54929,41 +55620,363 @@ async function technicalAeoRoutes(app, opts) {
54929
55620
  pagesAudited: siteAuditSnapshots.pagesAudited
54930
55621
  }).from(siteAuditSnapshots).innerJoin(runs, eq55(siteAuditSnapshots.runId, runs.id)).where(and43(
54931
55622
  eq55(siteAuditSnapshots.projectId, project.id),
55623
+ eq55(runs.projectId, project.id),
54932
55624
  eq55(runs.kind, RunKinds["site-audit"]),
54933
- inArray18(runs.status, SURFACEABLE_STATUSES),
55625
+ inArray19(runs.status, SURFACEABLE_STATUSES),
54934
55626
  notProbeRun()
54935
55627
  )).orderBy(desc24(siteAuditSnapshots.createdAt)).limit(limit).all();
54936
55628
  return { project: project.name, points: rows.reverse() };
54937
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
+ });
54938
55914
  app.post("/projects/:name/technical-aeo/runs", async (request) => {
54939
55915
  const project = resolveProject(app.db, request.params.name);
54940
55916
  const parsed = siteAuditRunRequestSchema.safeParse(request.body ?? {});
54941
55917
  if (!parsed.success) {
54942
55918
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
54943
55919
  }
54944
- const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and43(
54945
- eq55(runs.projectId, project.id),
54946
- eq55(runs.kind, RunKinds["site-audit"]),
54947
- inArray18(runs.status, [RunStatuses.queued, RunStatuses.running])
54948
- )).get();
54949
- if (existing) {
54950
- 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
+ });
54951
55978
  }
54952
- const now = (/* @__PURE__ */ new Date()).toISOString();
54953
- const runId = crypto43.randomUUID();
54954
- app.db.insert(runs).values({
54955
- id: runId,
54956
- projectId: project.id,
54957
- kind: RunKinds["site-audit"],
54958
- status: RunStatuses.queued,
54959
- trigger: RunTriggers.manual,
54960
- createdAt: now
54961
- }).run();
54962
- opts.onSiteAuditRequested?.(runId, project.id, {
54963
- sitemapUrl: parsed.data.sitemapUrl,
54964
- limit: parsed.data.limit
54965
- });
54966
- return { runId, status: RunStatuses.queued };
55979
+ return { runId: result.runId, status: result.status };
54967
55980
  });
54968
55981
  }
54969
55982
 
@@ -55113,6 +56126,7 @@ async function apiRoutes(app, opts) {
55113
56126
  await api.register(competitorRoutes);
55114
56127
  await api.register(runRoutes, {
55115
56128
  onRunCreated: opts.onRunCreated,
56129
+ onRunCancelled: opts.onRunCancelled,
55116
56130
  validProviderNames: opts.providerAdapters?.map((a) => a.name),
55117
56131
  getRunnableProviderNames: opts.getRunnableProviderNames,
55118
56132
  getEffectiveProviderModels: opts.getEffectiveProviderModels
@@ -55634,7 +56648,7 @@ var IntelligenceService = class {
55634
56648
  const recentRuns = this.db.select().from(runs).where(
55635
56649
  and45(
55636
56650
  eq57(runs.projectId, projectId),
55637
- or11(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
56651
+ or12(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
55638
56652
  // Defensive: RunCoordinator already skips probes before this is
55639
56653
  // called, but if a future call site invokes analyzeAndPersist
55640
56654
  // directly for a probe, probes still must not pollute the
@@ -55937,7 +56951,7 @@ var IntelligenceService = class {
55937
56951
  const allRuns = this.db.select().from(runs).where(
55938
56952
  and45(
55939
56953
  eq57(runs.projectId, project.id),
55940
- or11(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
56954
+ or12(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
55941
56955
  // Backfill must not replay probe runs as if they were real sweeps.
55942
56956
  ne7(runs.trigger, RunTriggers.probe)
55943
56957
  )
@@ -55970,7 +56984,7 @@ var IntelligenceService = class {
55970
56984
  let wouldDeleteTotal = 0;
55971
56985
  const existingByRunId = /* @__PURE__ */ new Map();
55972
56986
  if (isDryRun && targetRuns.length > 0) {
55973
- 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();
55974
56988
  for (const r of rows) {
55975
56989
  if (r.runId == null) continue;
55976
56990
  existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
@@ -56115,7 +57129,7 @@ var IntelligenceService = class {
56115
57129
  and45(
56116
57130
  eq57(runs.projectId, projectId),
56117
57131
  eq57(runs.kind, RunKinds["answer-visibility"]),
56118
- or11(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
57132
+ or12(eq57(runs.status, "completed"), eq57(runs.status, "partial")),
56119
57133
  // Defensive — see top of file.
56120
57134
  ne7(runs.trigger, RunTriggers.probe)
56121
57135
  )
@@ -56135,7 +57149,7 @@ var IntelligenceService = class {
56135
57149
  const haveHistory = recentRunIds.length > 0;
56136
57150
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
56137
57151
  if (haveHistory) {
56138
- 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();
56139
57153
  const regressionGroups = /* @__PURE__ */ new Map();
56140
57154
  for (const row of priorRows) {
56141
57155
  if (!row.runId) continue;
@@ -56241,6 +57255,13 @@ export {
56241
57255
  gscCoverageSnapshots,
56242
57256
  siteAuditSnapshots,
56243
57257
  siteAuditPages,
57258
+ siteCrawlRunRequests,
57259
+ siteCrawlAttempts,
57260
+ siteCrawlSnapshots,
57261
+ siteCrawlPages,
57262
+ siteCrawlEdges,
57263
+ siteCrawlFindings,
57264
+ siteCrawlEventReceipts,
56244
57265
  bingCoverageSnapshots,
56245
57266
  bingUrlInspections,
56246
57267
  gaTrafficSnapshots,