@ainyc/canonry 4.80.0 → 4.81.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 (25) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +20 -12
  2. package/assets/assets/{BacklinksPage-dRc62jAY.js → BacklinksPage-DHShKKpo.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-D2_IvTkk.js → ChartPrimitives-udHScxjY.js} +1 -1
  4. package/assets/assets/ProjectPage-BsS1anh7.js +6 -0
  5. package/assets/assets/{RunRow-C0MA3yuQ.js → RunRow-CXyPHMVQ.js} +1 -1
  6. package/assets/assets/{RunsPage-4uxTYgGy.js → RunsPage-BpQ_NpFt.js} +1 -1
  7. package/assets/assets/{SettingsPage-3-SLhcJ7.js → SettingsPage-1ep4ch7n.js} +1 -1
  8. package/assets/assets/{TrafficPage-DZ50qwme.js → TrafficPage-C3Hx-sE7.js} +1 -1
  9. package/assets/assets/TrafficSourceDetailPage-B26n2R6G.js +1 -0
  10. package/assets/assets/{arrow-left-BaZIkAXX.js → arrow-left-Dc_IPJxw.js} +1 -1
  11. package/assets/assets/{extract-error-message-cpvfuFqW.js → extract-error-message-B3PoKkHW.js} +1 -1
  12. package/assets/assets/{index-EnY_OBRd.js → index-DhdFTQkU.js} +86 -86
  13. package/assets/assets/{trash-2-JpcztiS5.js → trash-2-BQ69cGl0.js} +1 -1
  14. package/assets/index.html +1 -1
  15. package/dist/{chunk-2QBSRHSN.js → chunk-6XOZSS3Y.js} +72 -8
  16. package/dist/{chunk-AVN6Q6LM.js → chunk-GMT3YPLT.js} +76 -2
  17. package/dist/{chunk-CXIGHPBE.js → chunk-UAQ42NVJ.js} +440 -123
  18. package/dist/{chunk-LCABGFYN.js → chunk-VX5C7DK7.js} +404 -242
  19. package/dist/cli.js +103 -8
  20. package/dist/index.js +4 -4
  21. package/dist/{intelligence-service-ZWW3I3NL.js → intelligence-service-CAAQAKPN.js} +2 -2
  22. package/dist/mcp.js +2 -2
  23. package/package.json +8 -8
  24. package/assets/assets/ProjectPage-DSuvRUIf.js +0 -6
  25. package/assets/assets/TrafficSourceDetailPage-CzK5TMFp.js +0 -1
@@ -3,6 +3,7 @@ import {
3
3
  AI_ENGINE_DOMAINS,
4
4
  AI_PROVIDER_INFRA_DOMAINS,
5
5
  AppError,
6
+ BacklinkSources,
6
7
  CcReleaseSyncStatuses,
7
8
  CheckCategories,
8
9
  CheckScopes,
@@ -52,6 +53,8 @@ import {
52
53
  authRequired,
53
54
  backlinkHistoryEntrySchema,
54
55
  backlinkListResponseSchema,
56
+ backlinkSourceSchema,
57
+ backlinkSourcesResponseSchema,
55
58
  backlinkSummaryDtoSchema,
56
59
  backlinksInstallResultDtoSchema,
57
60
  backlinksInstallStatusDtoSchema,
@@ -242,10 +245,10 @@ import {
242
245
  wordpressSchemaDeployResultDtoSchema,
243
246
  wordpressSchemaStatusResultDtoSchema,
244
247
  wordpressStatusDtoSchema
245
- } from "./chunk-AVN6Q6LM.js";
248
+ } from "./chunk-GMT3YPLT.js";
246
249
 
247
250
  // src/intelligence-service.ts
248
- import { eq as eq35, desc as desc17, asc as asc5, and as and25, ne as ne5, or as or5, inArray as inArray13, gte as gte7, lte as lte4 } from "drizzle-orm";
251
+ import { eq as eq36, desc as desc17, asc as asc5, and as and26, ne as ne5, or as or5, inArray as inArray13, gte as gte7, lte as lte4 } from "drizzle-orm";
249
252
 
250
253
  // ../db/src/client.ts
251
254
  import { mkdirSync } from "fs";
@@ -850,7 +853,9 @@ var ccReleaseSyncs = sqliteTable("cc_release_syncs", {
850
853
  var backlinkDomains = sqliteTable("backlink_domains", {
851
854
  id: text("id").primaryKey(),
852
855
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
853
- releaseSyncId: text("release_sync_id").notNull().references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
856
+ // Nullable: Bing Webmaster backlink rows have no Common Crawl release sync.
857
+ releaseSyncId: text("release_sync_id").references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
858
+ source: text("source").$type().notNull().default("commoncrawl"),
854
859
  release: text("release").notNull(),
855
860
  targetDomain: text("target_domain").notNull(),
856
861
  linkingDomain: text("linking_domain").notNull(),
@@ -861,12 +866,14 @@ var backlinkDomains = sqliteTable("backlink_domains", {
861
866
  index("idx_backlink_domains_release_sync").on(table.releaseSyncId),
862
867
  index("idx_backlink_domains_project_release").on(table.projectId, table.release),
863
868
  index("idx_backlink_domains_hosts").on(table.numHosts),
864
- uniqueIndex("idx_backlink_domains_unique").on(table.projectId, table.release, table.linkingDomain)
869
+ uniqueIndex("idx_backlink_domains_unique").on(table.projectId, table.source, table.release, table.linkingDomain)
865
870
  ]);
866
871
  var backlinkSummaries = sqliteTable("backlink_summaries", {
867
872
  id: text("id").primaryKey(),
868
873
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
869
- releaseSyncId: text("release_sync_id").notNull().references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
874
+ // Nullable: Bing Webmaster summaries have no Common Crawl release sync.
875
+ releaseSyncId: text("release_sync_id").references(() => ccReleaseSyncs.id, { onDelete: "cascade" }),
876
+ source: text("source").$type().notNull().default("commoncrawl"),
870
877
  release: text("release").notNull(),
871
878
  targetDomain: text("target_domain").notNull(),
872
879
  totalLinkingDomains: integer("total_linking_domains").notNull(),
@@ -875,7 +882,7 @@ var backlinkSummaries = sqliteTable("backlink_summaries", {
875
882
  queriedAt: text("queried_at").notNull(),
876
883
  createdAt: text("created_at").notNull()
877
884
  }, (table) => [
878
- uniqueIndex("idx_backlink_summaries_project_release").on(table.projectId, table.release),
885
+ uniqueIndex("idx_backlink_summaries_project_release").on(table.projectId, table.source, table.release),
879
886
  index("idx_backlink_summaries_project").on(table.projectId)
880
887
  ]);
881
888
  var agentMemory = sqliteTable("agent_memory", {
@@ -3051,8 +3058,86 @@ var MIGRATION_VERSIONS = [
3051
3058
  `CREATE UNIQUE INDEX IF NOT EXISTS uniq_ads_insights_daily ON ads_insights_daily(project_id, level, entity_id, date)`,
3052
3059
  `CREATE INDEX IF NOT EXISTS idx_ads_insights_project_date ON ads_insights_daily(project_id, date)`
3053
3060
  ]
3061
+ },
3062
+ {
3063
+ // Bing Webmaster inbound links land in the SAME backlink store as Common
3064
+ // Crawl, tagged by a `source` discriminator (commoncrawl | bing-webmaster).
3065
+ // Bing rows have no `cc_release_syncs` row, so `release_sync_id` becomes
3066
+ // nullable and the per-window UNIQUE gains `source`. SQLite can't drop a
3067
+ // NOT NULL or rewrite a UNIQUE in place — canonical table rebuild (the
3068
+ // v58/v60 pattern). Guarded on the `source` column's absence so a replay
3069
+ // over the already-migrated schema is a no-op (the hardcoded
3070
+ // `source='commoncrawl'` backfill must never clobber real bing rows).
3071
+ version: 78,
3072
+ name: "backlinks-source-discriminator",
3073
+ statements: [],
3074
+ run: (tx) => {
3075
+ addBacklinkSourceDiscriminator(tx);
3076
+ }
3054
3077
  }
3055
3078
  ];
3079
+ function rebuildBacklinkTableWithSource(tx, table) {
3080
+ if (!tableExists(tx, table)) return;
3081
+ if (columnExists(tx, table, "source")) return;
3082
+ if (table === "backlink_domains") {
3083
+ tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_domains_v78`));
3084
+ tx.run(sql.raw(`CREATE TABLE backlink_domains_v78 (
3085
+ id TEXT PRIMARY KEY,
3086
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3087
+ release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
3088
+ source TEXT NOT NULL DEFAULT 'commoncrawl',
3089
+ release TEXT NOT NULL,
3090
+ target_domain TEXT NOT NULL,
3091
+ linking_domain TEXT NOT NULL,
3092
+ num_hosts INTEGER NOT NULL,
3093
+ created_at TEXT NOT NULL
3094
+ )`));
3095
+ tx.run(sql.raw(`INSERT INTO backlink_domains_v78
3096
+ (id, project_id, release_sync_id, source, release, target_domain, linking_domain, num_hosts, created_at)
3097
+ SELECT bd.id, bd.project_id,
3098
+ CASE WHEN bd.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bd.release_sync_id ELSE NULL END,
3099
+ 'commoncrawl', bd.release, bd.target_domain, bd.linking_domain, bd.num_hosts, bd.created_at
3100
+ FROM backlink_domains bd
3101
+ WHERE bd.project_id IN (SELECT id FROM projects)`));
3102
+ tx.run(sql.raw(`DROP TABLE backlink_domains`));
3103
+ tx.run(sql.raw(`ALTER TABLE backlink_domains_v78 RENAME TO backlink_domains`));
3104
+ tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project ON backlink_domains(project_id)`));
3105
+ tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_release_sync ON backlink_domains(release_sync_id)`));
3106
+ tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_project_release ON backlink_domains(project_id, release)`));
3107
+ tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_domains_hosts ON backlink_domains(num_hosts)`));
3108
+ tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_domains_unique ON backlink_domains(project_id, source, release, linking_domain)`));
3109
+ return;
3110
+ }
3111
+ tx.run(sql.raw(`DROP TABLE IF EXISTS backlink_summaries_v78`));
3112
+ tx.run(sql.raw(`CREATE TABLE backlink_summaries_v78 (
3113
+ id TEXT PRIMARY KEY,
3114
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
3115
+ release_sync_id TEXT REFERENCES cc_release_syncs(id) ON DELETE CASCADE,
3116
+ source TEXT NOT NULL DEFAULT 'commoncrawl',
3117
+ release TEXT NOT NULL,
3118
+ target_domain TEXT NOT NULL,
3119
+ total_linking_domains INTEGER NOT NULL,
3120
+ total_hosts INTEGER NOT NULL,
3121
+ top_10_hosts_share TEXT NOT NULL,
3122
+ queried_at TEXT NOT NULL,
3123
+ created_at TEXT NOT NULL
3124
+ )`));
3125
+ tx.run(sql.raw(`INSERT INTO backlink_summaries_v78
3126
+ (id, project_id, release_sync_id, source, release, target_domain, total_linking_domains, total_hosts, top_10_hosts_share, queried_at, created_at)
3127
+ SELECT bs.id, bs.project_id,
3128
+ CASE WHEN bs.release_sync_id IN (SELECT id FROM cc_release_syncs) THEN bs.release_sync_id ELSE NULL END,
3129
+ 'commoncrawl', bs.release, bs.target_domain, bs.total_linking_domains, bs.total_hosts, bs.top_10_hosts_share, bs.queried_at, bs.created_at
3130
+ FROM backlink_summaries bs
3131
+ WHERE bs.project_id IN (SELECT id FROM projects)`));
3132
+ tx.run(sql.raw(`DROP TABLE backlink_summaries`));
3133
+ tx.run(sql.raw(`ALTER TABLE backlink_summaries_v78 RENAME TO backlink_summaries`));
3134
+ tx.run(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS idx_backlink_summaries_project_release ON backlink_summaries(project_id, source, release)`));
3135
+ tx.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_backlink_summaries_project ON backlink_summaries(project_id)`));
3136
+ }
3137
+ function addBacklinkSourceDiscriminator(tx) {
3138
+ rebuildBacklinkTableWithSource(tx, "backlink_domains");
3139
+ rebuildBacklinkTableWithSource(tx, "backlink_summaries");
3140
+ }
3056
3141
  function isDuplicateColumnError(err) {
3057
3142
  if (!(err instanceof Error)) return false;
3058
3143
  if (err.message.includes("duplicate column name")) return true;
@@ -13206,6 +13291,7 @@ var SCHEMA_TABLE = {
13206
13291
  AuditLogEntry: auditLogEntrySchema,
13207
13292
  BacklinkHistoryEntry: backlinkHistoryEntrySchema,
13208
13293
  BacklinkListResponse: backlinkListResponseSchema,
13294
+ BacklinkSourcesResponse: backlinkSourcesResponseSchema,
13209
13295
  BacklinkSummaryDto: backlinkSummaryDtoSchema,
13210
13296
  BacklinksInstallResultDto: backlinksInstallResultDtoSchema,
13211
13297
  BacklinksInstallStatusDto: backlinksInstallStatusDtoSchema,
@@ -16666,7 +16752,8 @@ var routeCatalog = [
16666
16752
  tags: ["backlinks"],
16667
16753
  parameters: [
16668
16754
  nameParameter,
16669
- { name: "release", in: "query", description: "Release id filter.", schema: stringSchema }
16755
+ { name: "release", in: "query", description: "Release id filter.", schema: stringSchema },
16756
+ { name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
16670
16757
  ],
16671
16758
  responses: {
16672
16759
  200: rawJsonResponse("Summary returned, or null when no backlinks exist.", {
@@ -16684,7 +16771,8 @@ var routeCatalog = [
16684
16771
  nameParameter,
16685
16772
  { name: "release", in: "query", description: "Release id filter.", schema: stringSchema },
16686
16773
  { name: "limit", in: "query", description: "Max results (1-500).", schema: stringSchema },
16687
- { name: "offset", in: "query", description: "Pagination offset.", schema: stringSchema }
16774
+ { name: "offset", in: "query", description: "Pagination offset.", schema: stringSchema },
16775
+ { name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
16688
16776
  ],
16689
16777
  responses: {
16690
16778
  200: jsonResponse("Domain list returned.", "BacklinkListResponse"),
@@ -16696,12 +16784,44 @@ var routeCatalog = [
16696
16784
  path: "/api/v1/projects/{name}/backlinks/history",
16697
16785
  summary: "Get per-release backlink summaries for a project",
16698
16786
  tags: ["backlinks"],
16699
- parameters: [nameParameter],
16787
+ parameters: [
16788
+ nameParameter,
16789
+ { name: "source", in: "query", description: "Backlink source: commoncrawl (default) or bing-webmaster.", schema: stringSchema }
16790
+ ],
16700
16791
  responses: {
16701
16792
  200: jsonArrayResponse("History returned oldest-first by queriedAt.", "BacklinkHistoryEntry"),
16702
16793
  404: errorResponse("Project not found.")
16703
16794
  }
16704
16795
  },
16796
+ {
16797
+ method: "get",
16798
+ path: "/api/v1/projects/{name}/backlinks/sources",
16799
+ summary: "Report per-source backlink availability for a project",
16800
+ description: "Returns connection + data availability for every backlink source (commoncrawl, bing-webmaster) so callers can degrade gracefully across CC-only / Bing-only / both / neither.",
16801
+ tags: ["backlinks"],
16802
+ parameters: [
16803
+ nameParameter,
16804
+ { name: "excludeCrawlers", in: "query", description: 'When "1"/"true", count linking domains excluding crawler/proxy hosts (matches the dashboard). Default off.', schema: stringSchema }
16805
+ ],
16806
+ responses: {
16807
+ 200: jsonResponse("Per-source availability returned.", "BacklinkSourcesResponse"),
16808
+ 404: errorResponse("Project not found.")
16809
+ }
16810
+ },
16811
+ {
16812
+ method: "post",
16813
+ path: "/api/v1/projects/{name}/backlinks/bing-sync",
16814
+ summary: "Sync a project's inbound links from Bing Webmaster Tools",
16815
+ description: 'Creates a tracking run and pulls inbound links live from the connected Bing Webmaster account, writing source="bing-webmaster" backlink rows. Requires a Bing connection for the project domain.',
16816
+ tags: ["backlinks"],
16817
+ parameters: [nameParameter],
16818
+ responses: {
16819
+ 201: jsonResponse("Bing sync run queued.", "RunDto"),
16820
+ 400: errorResponse("No Bing Webmaster connection for this project."),
16821
+ 404: errorResponse("Project not found."),
16822
+ 422: errorResponse("Bing backlinks sync is not available on this deployment.")
16823
+ }
16824
+ },
16705
16825
  {
16706
16826
  method: "post",
16707
16827
  path: "/api/v1/projects/{name}/traffic/connect/cloud-run",
@@ -20901,6 +21021,7 @@ var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
20901
21021
  var BING_SUBMIT_URL_BATCH_LIMIT = 500;
20902
21022
  var BING_SUBMIT_URL_DAILY_LIMIT = 1e4;
20903
21023
  var BING_REQUEST_TIMEOUT_MS = 3e4;
21024
+ var BING_LINKS_MAX_PAGES = 20;
20904
21025
 
20905
21026
  // ../integration-bing/src/types.ts
20906
21027
  var BingApiError = class extends Error {
@@ -21064,6 +21185,51 @@ async function getCrawlIssues(apiKey, siteUrl) {
21064
21185
  const data = await bingFetch(apiKey, `GetCrawlIssues?siteUrl=${encodedSite}`);
21065
21186
  return data ?? [];
21066
21187
  }
21188
+ async function getLinkCounts(apiKey, siteUrl, opts = {}) {
21189
+ validateApiKey(apiKey);
21190
+ validateSiteUrl2(siteUrl);
21191
+ const encodedSite = encodeURIComponent(siteUrl);
21192
+ const maxPages = Math.max(1, opts.maxPages ?? BING_LINKS_MAX_PAGES);
21193
+ const out = [];
21194
+ let page = 0;
21195
+ let totalPages = 1;
21196
+ while (page < totalPages && page < maxPages) {
21197
+ const data = await bingFetch(apiKey, `GetLinkCounts?siteUrl=${encodedSite}&page=${page}`);
21198
+ for (const link of data?.Links ?? []) {
21199
+ if (link && typeof link.Url === "string") {
21200
+ out.push({ Url: link.Url, Count: Number(link.Count ?? 0) });
21201
+ }
21202
+ }
21203
+ totalPages = Number(data?.TotalPages ?? 1) || 1;
21204
+ page++;
21205
+ }
21206
+ return out;
21207
+ }
21208
+ async function getUrlLinks(apiKey, siteUrl, link, opts = {}) {
21209
+ validateApiKey(apiKey);
21210
+ validateSiteUrl2(siteUrl);
21211
+ validateUrl2(link);
21212
+ const encodedSite = encodeURIComponent(siteUrl);
21213
+ const encodedLink = encodeURIComponent(link);
21214
+ const maxPages = Math.max(1, opts.maxPages ?? BING_LINKS_MAX_PAGES);
21215
+ const out = [];
21216
+ let page = 0;
21217
+ let totalPages = 1;
21218
+ while (page < totalPages && page < maxPages) {
21219
+ const data = await bingFetch(
21220
+ apiKey,
21221
+ `GetUrlLinks?siteUrl=${encodedSite}&link=${encodedLink}&page=${page}`
21222
+ );
21223
+ for (const detail of data?.Details ?? []) {
21224
+ if (detail && typeof detail.Url === "string") {
21225
+ out.push({ Url: detail.Url, AnchorText: detail.AnchorText });
21226
+ }
21227
+ }
21228
+ totalPages = Number(data?.TotalPages ?? 1) || 1;
21229
+ page++;
21230
+ }
21231
+ return out;
21232
+ }
21067
21233
 
21068
21234
  // ../api-routes/src/bing.ts
21069
21235
  function parseBingDate(value) {
@@ -24662,9 +24828,18 @@ function mapSummaryRow(row) {
24662
24828
  totalLinkingDomains: row.totalLinkingDomains,
24663
24829
  totalHosts: row.totalHosts,
24664
24830
  top10HostsShare: row.top10HostsShare,
24665
- queriedAt: row.queriedAt
24831
+ queriedAt: row.queriedAt,
24832
+ source: row.source
24666
24833
  };
24667
24834
  }
24835
+ function parseSourceParam(value) {
24836
+ if (value === void 0 || value === "") return BacklinkSources.commoncrawl;
24837
+ const parsed = backlinkSourceSchema.safeParse(value);
24838
+ if (!parsed.success) {
24839
+ throw validationError(`Invalid source "${value}". Expected one of: ${Object.values(BacklinkSources).join(", ")}.`);
24840
+ }
24841
+ return parsed.data;
24842
+ }
24668
24843
  function mapRunRow(row) {
24669
24844
  return {
24670
24845
  id: row.id,
@@ -24679,9 +24854,13 @@ function mapRunRow(row) {
24679
24854
  createdAt: row.createdAt
24680
24855
  };
24681
24856
  }
24682
- function latestSummaryForProject(db, projectId, release) {
24683
- const condition = release ? and19(eq25(backlinkSummaries.projectId, projectId), eq25(backlinkSummaries.release, release)) : eq25(backlinkSummaries.projectId, projectId);
24684
- return db.select().from(backlinkSummaries).where(condition).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
24857
+ function latestSummaryForProject(db, projectId, source, release) {
24858
+ const conditions = [
24859
+ eq25(backlinkSummaries.projectId, projectId),
24860
+ eq25(backlinkSummaries.source, source)
24861
+ ];
24862
+ if (release) conditions.push(eq25(backlinkSummaries.release, release));
24863
+ return db.select().from(backlinkSummaries).where(and19(...conditions)).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
24685
24864
  }
24686
24865
  function parseExcludeCrawlers(value) {
24687
24866
  if (!value) return false;
@@ -24691,6 +24870,7 @@ function parseExcludeCrawlers(value) {
24691
24870
  function computeFilteredSummary(db, base) {
24692
24871
  const baseDomainCondition = and19(
24693
24872
  eq25(backlinkDomains.projectId, base.projectId),
24873
+ eq25(backlinkDomains.source, base.source),
24694
24874
  eq25(backlinkDomains.release, base.release)
24695
24875
  );
24696
24876
  const filteredCondition = and19(baseDomainCondition, backlinkCrawlerExclusionClause());
@@ -24717,10 +24897,48 @@ function computeFilteredSummary(db, base) {
24717
24897
  totalHosts,
24718
24898
  top10HostsShare: top10Share.toFixed(6),
24719
24899
  queriedAt: base.queriedAt,
24900
+ source: base.source,
24720
24901
  excludedLinkingDomains: Math.max(0, unfilteredLinkingDomains - totalLinkingDomains),
24721
24902
  excludedHosts: Math.max(0, unfilteredHosts - totalHosts)
24722
24903
  };
24723
24904
  }
24905
+ function buildSourceAvailability(db, projectId, source, connected, excludeCrawlers) {
24906
+ const summary = db.select().from(backlinkSummaries).where(and19(eq25(backlinkSummaries.projectId, projectId), eq25(backlinkSummaries.source, source))).orderBy(desc13(backlinkSummaries.queriedAt)).limit(1).get();
24907
+ let totalLinkingDomains = summary?.totalLinkingDomains ?? 0;
24908
+ if (summary && excludeCrawlers) {
24909
+ const filtered = db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(and19(
24910
+ eq25(backlinkDomains.projectId, projectId),
24911
+ eq25(backlinkDomains.source, source),
24912
+ eq25(backlinkDomains.release, summary.release),
24913
+ backlinkCrawlerExclusionClause()
24914
+ )).get();
24915
+ totalLinkingDomains = Number(filtered?.count ?? 0);
24916
+ }
24917
+ return {
24918
+ source,
24919
+ connected,
24920
+ hasData: !!summary,
24921
+ latestRelease: summary?.release ?? null,
24922
+ totalLinkingDomains,
24923
+ lastSyncedAt: summary?.queriedAt ?? null
24924
+ };
24925
+ }
24926
+ function computeSourceAvailability(db, project, bingStore, excludeCrawlers) {
24927
+ const ccReadySync = db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq25(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
24928
+ const ccConnected = project.autoExtractBacklinks === true && !!ccReadySync;
24929
+ const bingConnected = !!bingStore?.getConnection(project.canonicalDomain);
24930
+ const sources = [
24931
+ buildSourceAvailability(db, project.id, BacklinkSources.commoncrawl, ccConnected, excludeCrawlers),
24932
+ buildSourceAvailability(db, project.id, BacklinkSources["bing-webmaster"], bingConnected, excludeCrawlers)
24933
+ ];
24934
+ return {
24935
+ projectId: project.id,
24936
+ targetDomain: project.canonicalDomain,
24937
+ sources,
24938
+ anyConnected: sources.some((s) => s.connected),
24939
+ anyData: sources.some((s) => s.hasData)
24940
+ };
24941
+ }
24724
24942
  async function backlinksRoutes(app, opts) {
24725
24943
  app.get("/backlinks/status", async (_request, reply) => {
24726
24944
  if (!opts.getBacklinksStatus) {
@@ -24852,7 +25070,8 @@ async function backlinksRoutes(app, opts) {
24852
25070
  "/projects/:name/backlinks/summary",
24853
25071
  async (request, reply) => {
24854
25072
  const project = resolveProject(app.db, request.params.name);
24855
- const row = latestSummaryForProject(app.db, project.id, request.query.release);
25073
+ const source = parseSourceParam(request.query.source);
25074
+ const row = latestSummaryForProject(app.db, project.id, source, request.query.release);
24856
25075
  if (!row) return reply.send(null);
24857
25076
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
24858
25077
  return reply.send(excludeCrawlers ? computeFilteredSummary(app.db, row) : mapSummaryRow(row));
@@ -24860,10 +25079,11 @@ async function backlinksRoutes(app, opts) {
24860
25079
  );
24861
25080
  app.get("/projects/:name/backlinks/domains", async (request, reply) => {
24862
25081
  const project = resolveProject(app.db, request.params.name);
24863
- const summaryRow = latestSummaryForProject(app.db, project.id, request.query.release);
25082
+ const source = parseSourceParam(request.query.source);
25083
+ const summaryRow = latestSummaryForProject(app.db, project.id, source, request.query.release);
24864
25084
  const targetRelease = request.query.release ?? summaryRow?.release;
24865
25085
  if (!targetRelease) {
24866
- const response2 = { summary: null, total: 0, rows: [] };
25086
+ const response2 = { source, summary: null, total: 0, rows: [] };
24867
25087
  return reply.send(response2);
24868
25088
  }
24869
25089
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
@@ -24871,19 +25091,22 @@ async function backlinksRoutes(app, opts) {
24871
25091
  const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
24872
25092
  const baseDomainCondition = and19(
24873
25093
  eq25(backlinkDomains.projectId, project.id),
25094
+ eq25(backlinkDomains.source, source),
24874
25095
  eq25(backlinkDomains.release, targetRelease)
24875
25096
  );
24876
25097
  const domainCondition = excludeCrawlers ? and19(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
24877
25098
  const totalRow = app.db.select({ count: sql10`count(*)` }).from(backlinkDomains).where(domainCondition).get();
24878
25099
  const rows = app.db.select({
24879
25100
  linkingDomain: backlinkDomains.linkingDomain,
24880
- numHosts: backlinkDomains.numHosts
25101
+ numHosts: backlinkDomains.numHosts,
25102
+ source: backlinkDomains.source
24881
25103
  }).from(backlinkDomains).where(domainCondition).orderBy(desc13(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
24882
25104
  let summary = null;
24883
25105
  if (summaryRow) {
24884
25106
  summary = excludeCrawlers ? computeFilteredSummary(app.db, summaryRow) : mapSummaryRow(summaryRow);
24885
25107
  }
24886
25108
  const response = {
25109
+ source,
24887
25110
  summary,
24888
25111
  total: Number(totalRow?.count ?? 0),
24889
25112
  rows
@@ -24894,17 +25117,58 @@ async function backlinksRoutes(app, opts) {
24894
25117
  "/projects/:name/backlinks/history",
24895
25118
  async (request, reply) => {
24896
25119
  const project = resolveProject(app.db, request.params.name);
24897
- const rows = app.db.select().from(backlinkSummaries).where(eq25(backlinkSummaries.projectId, project.id)).orderBy(asc3(backlinkSummaries.queriedAt)).all();
25120
+ const source = parseSourceParam(request.query.source);
25121
+ const rows = app.db.select().from(backlinkSummaries).where(and19(eq25(backlinkSummaries.projectId, project.id), eq25(backlinkSummaries.source, source))).orderBy(asc3(backlinkSummaries.queriedAt)).all();
24898
25122
  const response = rows.map((r) => ({
24899
25123
  release: r.release,
24900
25124
  totalLinkingDomains: r.totalLinkingDomains,
24901
25125
  totalHosts: r.totalHosts,
24902
25126
  top10HostsShare: r.top10HostsShare,
24903
- queriedAt: r.queriedAt
25127
+ queriedAt: r.queriedAt,
25128
+ source: r.source
24904
25129
  }));
24905
25130
  return reply.send(response);
24906
25131
  }
24907
25132
  );
25133
+ app.get(
25134
+ "/projects/:name/backlinks/sources",
25135
+ async (request, reply) => {
25136
+ const project = resolveProject(app.db, request.params.name);
25137
+ const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
25138
+ const response = computeSourceAvailability(app.db, project, opts.bingConnectionStore, excludeCrawlers);
25139
+ return reply.send(response);
25140
+ }
25141
+ );
25142
+ app.post(
25143
+ "/projects/:name/backlinks/bing-sync",
25144
+ async (request, reply) => {
25145
+ const project = resolveProject(app.db, request.params.name);
25146
+ if (!opts.onBingBacklinkSyncRequested) {
25147
+ throw missingDependency(
25148
+ "Bing backlinks sync is only available from a local canonry install with Bing Webmaster connected."
25149
+ );
25150
+ }
25151
+ const conn = opts.bingConnectionStore?.getConnection(project.canonicalDomain);
25152
+ if (!conn) {
25153
+ throw validationError(
25154
+ `No Bing Webmaster connection for "${project.name}". Run \`canonry bing connect ${project.name} --api-key <key>\` first.`
25155
+ );
25156
+ }
25157
+ const now = (/* @__PURE__ */ new Date()).toISOString();
25158
+ const runId = crypto22.randomUUID();
25159
+ app.db.insert(runs).values({
25160
+ id: runId,
25161
+ projectId: project.id,
25162
+ kind: RunKinds["backlink-extract"],
25163
+ status: RunStatuses.queued,
25164
+ trigger: RunTriggers.manual,
25165
+ createdAt: now
25166
+ }).run();
25167
+ opts.onBingBacklinkSyncRequested(runId, project.id);
25168
+ const run = app.db.select().from(runs).where(eq25(runs.id, runId)).get();
25169
+ return reply.status(201).send(mapRunRow(run));
25170
+ }
25171
+ );
24908
25172
  }
24909
25173
 
24910
25174
  // ../api-routes/src/traffic.ts
@@ -30300,6 +30564,54 @@ function readInstalledManifest(skillDir) {
30300
30564
  }
30301
30565
  var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
30302
30566
 
30567
+ // ../api-routes/src/doctor/checks/backlinks.ts
30568
+ import { and as and21, eq as eq27 } from "drizzle-orm";
30569
+ function skippedNoProject() {
30570
+ return {
30571
+ status: CheckStatuses.skipped,
30572
+ code: "backlinks.source.no-project",
30573
+ summary: "Project context required."
30574
+ };
30575
+ }
30576
+ var BACKLINKS_CHECKS = [
30577
+ {
30578
+ id: "backlinks.source.connected",
30579
+ category: CheckCategories.integrations,
30580
+ scope: CheckScopes.project,
30581
+ title: "Backlinks source connected",
30582
+ run: (ctx) => {
30583
+ if (!ctx.project) return skippedNoProject();
30584
+ const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq27(projects.id, ctx.project.id)).get();
30585
+ const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
30586
+ const ccConnected = projectRow?.autoExtract === true && !!readySync;
30587
+ const bingConnected = !!ctx.bingConnectionStore?.getConnection(ctx.project.canonicalDomain);
30588
+ const connected = [];
30589
+ if (ccConnected) connected.push(BacklinkSources.commoncrawl);
30590
+ if (bingConnected) connected.push(BacklinkSources["bing-webmaster"]);
30591
+ if (connected.length === 0) {
30592
+ return {
30593
+ status: CheckStatuses.warn,
30594
+ code: "backlinks.source.none",
30595
+ summary: `No backlink source is set up for ${ctx.project.name}.`,
30596
+ remediation: `Enable Common Crawl (set autoExtractBacklinks on the project + run \`canonry backlinks sync\`) or connect Bing Webmaster (\`canonry bing connect ${ctx.project.name} --api-key <key>\` then \`canonry backlinks bing-sync ${ctx.project.name}\`).`,
30597
+ details: { commoncrawl: ccConnected, bingWebmaster: bingConnected }
30598
+ };
30599
+ }
30600
+ const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and21(
30601
+ eq27(backlinkSummaries.projectId, ctx.project.id),
30602
+ eq27(backlinkSummaries.source, BacklinkSources.commoncrawl)
30603
+ )).limit(1).get() : false;
30604
+ return {
30605
+ status: CheckStatuses.ok,
30606
+ code: "backlinks.source.connected",
30607
+ summary: `${connected.length} backlink source${connected.length === 1 ? "" : "s"} set up: ${connected.join(", ")}.`,
30608
+ remediation: ccConnected && !ccHasData ? `Common Crawl is ready but no backlinks have been extracted for ${ctx.project.name} yet \u2014 run \`canonry backlinks extract ${ctx.project.name}\`.` : null,
30609
+ details: { commoncrawl: ccConnected, bingWebmaster: bingConnected, connected, commoncrawlHasData: ccHasData }
30610
+ };
30611
+ }
30612
+ }
30613
+ ];
30614
+
30303
30615
  // ../api-routes/src/doctor/checks/bing-auth.ts
30304
30616
  var BING_AUTH_CHECKS = [
30305
30617
  {
@@ -30447,10 +30759,10 @@ var BING_AUTH_CHECKS = [
30447
30759
  ];
30448
30760
 
30449
30761
  // ../api-routes/src/doctor/checks/content.ts
30450
- import { eq as eq27 } from "drizzle-orm";
30762
+ import { eq as eq28 } from "drizzle-orm";
30451
30763
  var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
30452
30764
  var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
30453
- function skippedNoProject() {
30765
+ function skippedNoProject2() {
30454
30766
  return {
30455
30767
  status: CheckStatuses.skipped,
30456
30768
  code: "content.winnability.no-project",
@@ -30460,7 +30772,7 @@ function skippedNoProject() {
30460
30772
  }
30461
30773
  function loadProject(ctx) {
30462
30774
  if (!ctx.project) return null;
30463
- return ctx.db.select().from(projects).where(eq27(projects.id, ctx.project.id)).get() ?? null;
30775
+ return ctx.db.select().from(projects).where(eq28(projects.id, ctx.project.id)).get() ?? null;
30464
30776
  }
30465
30777
  function percent(value) {
30466
30778
  return Math.round(value * 100);
@@ -30471,7 +30783,7 @@ var winnabilityCoverageCheck = {
30471
30783
  scope: CheckScopes.project,
30472
30784
  title: "Content winnability classification coverage",
30473
30785
  run: (ctx) => {
30474
- if (!ctx.project) return skippedNoProject();
30786
+ if (!ctx.project) return skippedNoProject2();
30475
30787
  const project = loadProject(ctx);
30476
30788
  if (!project) {
30477
30789
  return {
@@ -30552,7 +30864,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
30552
30864
  );
30553
30865
 
30554
30866
  // ../api-routes/src/doctor/checks/ads.ts
30555
- import { eq as eq28 } from "drizzle-orm";
30867
+ import { eq as eq29 } from "drizzle-orm";
30556
30868
  var RECENT_SYNC_WARN_DAYS = 7;
30557
30869
  var RECENT_SYNC_FAIL_DAYS = 30;
30558
30870
  var adsConnectionCheck = {
@@ -30569,7 +30881,7 @@ var adsConnectionCheck = {
30569
30881
  remediation: null
30570
30882
  };
30571
30883
  }
30572
- const row = ctx.db.select().from(adsConnections).where(eq28(adsConnections.projectId, ctx.project.id)).get();
30884
+ const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
30573
30885
  if (!row) {
30574
30886
  return {
30575
30887
  status: CheckStatuses.skipped,
@@ -30619,7 +30931,7 @@ var adsRecentSyncCheck = {
30619
30931
  remediation: null
30620
30932
  };
30621
30933
  }
30622
- const row = ctx.db.select().from(adsConnections).where(eq28(adsConnections.projectId, ctx.project.id)).get();
30934
+ const row = ctx.db.select().from(adsConnections).where(eq29(adsConnections.projectId, ctx.project.id)).get();
30623
30935
  if (!row) {
30624
30936
  return {
30625
30937
  status: CheckStatuses.skipped,
@@ -30810,10 +31122,10 @@ var ga4ConnectionCheck = {
30810
31122
  var GA_AUTH_CHECKS = [ga4ConnectionCheck];
30811
31123
 
30812
31124
  // ../api-routes/src/doctor/checks/gbp-auth.ts
30813
- import { and as and21, eq as eq29 } from "drizzle-orm";
31125
+ import { and as and22, eq as eq30 } from "drizzle-orm";
30814
31126
  var RECENT_SYNC_WARN_DAYS2 = 7;
30815
31127
  var RECENT_SYNC_FAIL_DAYS2 = 30;
30816
- function skippedNoProject2() {
31128
+ function skippedNoProject3() {
30817
31129
  return {
30818
31130
  status: CheckStatuses.skipped,
30819
31131
  code: "gbp.auth.no-project",
@@ -30830,7 +31142,7 @@ function storeUnavailable() {
30830
31142
  };
30831
31143
  }
30832
31144
  async function resolveGbpToken(ctx) {
30833
- if (!ctx.project) return { ok: false, output: skippedNoProject2() };
31145
+ if (!ctx.project) return { ok: false, output: skippedNoProject3() };
30834
31146
  const store = ctx.googleConnectionStore;
30835
31147
  if (!store) return { ok: false, output: storeUnavailable() };
30836
31148
  const auth = ctx.getGoogleAuthConfig?.() ?? {};
@@ -30908,7 +31220,7 @@ var scopesCheck = {
30908
31220
  scope: CheckScopes.project,
30909
31221
  title: "GBP granted scopes",
30910
31222
  run: async (ctx) => {
30911
- if (!ctx.project) return skippedNoProject2();
31223
+ if (!ctx.project) return skippedNoProject3();
30912
31224
  const store = ctx.googleConnectionStore;
30913
31225
  if (!store) return storeUnavailable();
30914
31226
  const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
@@ -30945,7 +31257,7 @@ var accountAccessCheck = {
30945
31257
  scope: CheckScopes.project,
30946
31258
  title: "GBP account access",
30947
31259
  run: async (ctx) => {
30948
- if (!ctx.project) return skippedNoProject2();
31260
+ if (!ctx.project) return skippedNoProject3();
30949
31261
  const store = ctx.googleConnectionStore;
30950
31262
  if (!store) return storeUnavailable();
30951
31263
  const conn = store.getConnection(ctx.project.canonicalDomain, "gbp");
@@ -31042,8 +31354,8 @@ var recentSyncCheck = {
31042
31354
  scope: CheckScopes.project,
31043
31355
  title: "GBP recent sync",
31044
31356
  run: (ctx) => {
31045
- if (!ctx.project) return skippedNoProject2();
31046
- const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and21(eq29(gbpLocations.projectId, ctx.project.id), eq29(gbpLocations.selected, true))).all();
31357
+ if (!ctx.project) return skippedNoProject3();
31358
+ const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and22(eq30(gbpLocations.projectId, ctx.project.id), eq30(gbpLocations.selected, true))).all();
31047
31359
  if (selected.length === 0) {
31048
31360
  return {
31049
31361
  status: CheckStatuses.skipped,
@@ -31103,7 +31415,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
31103
31415
  );
31104
31416
 
31105
31417
  // ../api-routes/src/doctor/checks/places.ts
31106
- import { eq as eq30 } from "drizzle-orm";
31418
+ import { eq as eq31 } from "drizzle-orm";
31107
31419
  var apiKeyCheck = {
31108
31420
  id: "gbp.places.api-key",
31109
31421
  category: CheckCategories.auth,
@@ -31148,7 +31460,7 @@ var apiKeyCheck = {
31148
31460
  details: { tier: cfg.tier }
31149
31461
  };
31150
31462
  }
31151
- const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq30(gbpLocations.projectId, ctx.project.id)).all();
31463
+ const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq31(gbpLocations.projectId, ctx.project.id)).all();
31152
31464
  const selected = rows.filter((r) => r.selected);
31153
31465
  const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
31154
31466
  const details = {
@@ -31184,7 +31496,7 @@ var PLACES_CHECK_BY_ID = Object.fromEntries(
31184
31496
  var REQUIRED_GSC_SCOPES = [GSC_SCOPE, INDEXING_SCOPE];
31185
31497
  async function resolveAccessToken(ctx) {
31186
31498
  if (!ctx.project) {
31187
- return { ok: false, output: skippedNoProject3() };
31499
+ return { ok: false, output: skippedNoProject4() };
31188
31500
  }
31189
31501
  const store = ctx.googleConnectionStore;
31190
31502
  if (!store) {
@@ -31251,7 +31563,7 @@ async function resolveAccessToken(ctx) {
31251
31563
  };
31252
31564
  }
31253
31565
  }
31254
- function skippedNoProject3() {
31566
+ function skippedNoProject4() {
31255
31567
  return {
31256
31568
  status: CheckStatuses.skipped,
31257
31569
  code: "google.auth.no-project",
@@ -31281,7 +31593,7 @@ var propertyAccessCheck = {
31281
31593
  scope: CheckScopes.project,
31282
31594
  title: "GSC property access",
31283
31595
  run: async (ctx) => {
31284
- if (!ctx.project) return skippedNoProject3();
31596
+ if (!ctx.project) return skippedNoProject4();
31285
31597
  const store = ctx.googleConnectionStore;
31286
31598
  if (!store) {
31287
31599
  return {
@@ -31382,7 +31694,7 @@ var redirectUriCheck = {
31382
31694
  scope: CheckScopes.project,
31383
31695
  title: "OAuth redirect URI",
31384
31696
  run: async (ctx) => {
31385
- if (!ctx.project) return skippedNoProject3();
31697
+ if (!ctx.project) return skippedNoProject4();
31386
31698
  const auth = ctx.getGoogleAuthConfig?.() ?? {};
31387
31699
  if (!auth.clientId || !auth.clientSecret) {
31388
31700
  return {
@@ -31436,7 +31748,7 @@ var scopesCheck2 = {
31436
31748
  scope: CheckScopes.project,
31437
31749
  title: "GSC granted scopes",
31438
31750
  run: async (ctx) => {
31439
- if (!ctx.project) return skippedNoProject3();
31751
+ if (!ctx.project) return skippedNoProject4();
31440
31752
  const store = ctx.googleConnectionStore;
31441
31753
  if (!store) {
31442
31754
  return {
@@ -31599,10 +31911,10 @@ var RUNTIME_STATE_CHECKS = [
31599
31911
  ];
31600
31912
 
31601
31913
  // ../api-routes/src/doctor/checks/traffic-source.ts
31602
- import { and as and22, eq as eq31, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
31914
+ import { and as and23, eq as eq32, gte as gte5, ne as ne4, sql as sql12 } from "drizzle-orm";
31603
31915
  var RECENT_DATA_WARN_DAYS = 7;
31604
31916
  var RECENT_DATA_FAIL_DAYS = 30;
31605
- function skippedNoProject4() {
31917
+ function skippedNoProject5() {
31606
31918
  return {
31607
31919
  status: CheckStatuses.skipped,
31608
31920
  code: "traffic.no-project",
@@ -31613,8 +31925,8 @@ function skippedNoProject4() {
31613
31925
  function loadProbes(ctx) {
31614
31926
  if (!ctx.project) return [];
31615
31927
  const rows = ctx.db.select().from(trafficSources).where(
31616
- and22(
31617
- eq31(trafficSources.projectId, ctx.project.id),
31928
+ and23(
31929
+ eq32(trafficSources.projectId, ctx.project.id),
31618
31930
  ne4(trafficSources.status, TrafficSourceStatuses.archived)
31619
31931
  )
31620
31932
  ).all();
@@ -31636,7 +31948,7 @@ var sourceConnectedCheck = {
31636
31948
  scope: CheckScopes.project,
31637
31949
  title: "Traffic source connected",
31638
31950
  run: (ctx) => {
31639
- if (!ctx.project) return skippedNoProject4();
31951
+ if (!ctx.project) return skippedNoProject5();
31640
31952
  const sources = loadProbes(ctx);
31641
31953
  if (sources.length === 0) {
31642
31954
  return {
@@ -31680,7 +31992,7 @@ var recentDataCheck = {
31680
31992
  scope: CheckScopes.project,
31681
31993
  title: "Traffic source recent data",
31682
31994
  run: (ctx) => {
31683
- if (!ctx.project) return skippedNoProject4();
31995
+ if (!ctx.project) return skippedNoProject5();
31684
31996
  const sources = loadProbes(ctx);
31685
31997
  if (sources.length === 0) {
31686
31998
  return {
@@ -31694,16 +32006,16 @@ var recentDataCheck = {
31694
32006
  const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
31695
32007
  const recentCrawlers = Number(
31696
32008
  ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
31697
- and22(
31698
- eq31(crawlerEventsHourly.projectId, ctx.project.id),
32009
+ and23(
32010
+ eq32(crawlerEventsHourly.projectId, ctx.project.id),
31699
32011
  gte5(crawlerEventsHourly.tsHour, warnCutoff)
31700
32012
  )
31701
32013
  ).get()?.total ?? 0
31702
32014
  );
31703
32015
  const recentReferrals = Number(
31704
32016
  ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
31705
- and22(
31706
- eq31(aiReferralEventsHourly.projectId, ctx.project.id),
32017
+ and23(
32018
+ eq32(aiReferralEventsHourly.projectId, ctx.project.id),
31707
32019
  gte5(aiReferralEventsHourly.tsHour, warnCutoff)
31708
32020
  )
31709
32021
  ).get()?.total ?? 0
@@ -31718,16 +32030,16 @@ var recentDataCheck = {
31718
32030
  }
31719
32031
  const olderCrawlers = Number(
31720
32032
  ctx.db.select({ total: sql12`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
31721
- and22(
31722
- eq31(crawlerEventsHourly.projectId, ctx.project.id),
32033
+ and23(
32034
+ eq32(crawlerEventsHourly.projectId, ctx.project.id),
31723
32035
  gte5(crawlerEventsHourly.tsHour, failCutoff)
31724
32036
  )
31725
32037
  ).get()?.total ?? 0
31726
32038
  );
31727
32039
  const olderReferrals = Number(
31728
32040
  ctx.db.select({ total: sql12`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
31729
- and22(
31730
- eq31(aiReferralEventsHourly.projectId, ctx.project.id),
32041
+ and23(
32042
+ eq32(aiReferralEventsHourly.projectId, ctx.project.id),
31731
32043
  gte5(aiReferralEventsHourly.tsHour, failCutoff)
31732
32044
  )
31733
32045
  ).get()?.total ?? 0
@@ -31842,7 +32154,7 @@ var credentialsCheck = {
31842
32154
  scope: CheckScopes.project,
31843
32155
  title: "Traffic source credentials",
31844
32156
  run: async (ctx) => {
31845
- if (!ctx.project) return skippedNoProject4();
32157
+ if (!ctx.project) return skippedNoProject5();
31846
32158
  const sources = loadProbes(ctx);
31847
32159
  if (sources.length === 0) {
31848
32160
  return {
@@ -31871,7 +32183,7 @@ var scopesCheck3 = {
31871
32183
  scope: CheckScopes.project,
31872
32184
  title: "Traffic source scopes",
31873
32185
  run: async (ctx) => {
31874
- if (!ctx.project) return skippedNoProject4();
32186
+ if (!ctx.project) return skippedNoProject5();
31875
32187
  const sources = loadProbes(ctx);
31876
32188
  if (sources.length === 0) {
31877
32189
  return {
@@ -31900,7 +32212,7 @@ var cacheBlindSpotCheck = {
31900
32212
  scope: CheckScopes.project,
31901
32213
  title: "WordPress traffic cache blind spot",
31902
32214
  run: (ctx) => {
31903
- if (!ctx.project) return skippedNoProject4();
32215
+ if (!ctx.project) return skippedNoProject5();
31904
32216
  const wpSources = loadProbes(ctx).filter(
31905
32217
  (s) => s.sourceType === TrafficSourceTypes.wordpress
31906
32218
  );
@@ -32015,6 +32327,7 @@ var ALL_CHECKS = [
32015
32327
  ...ADS_CHECKS,
32016
32328
  ...PROVIDERS_CHECKS,
32017
32329
  ...TRAFFIC_SOURCE_CHECKS,
32330
+ ...BACKLINKS_CHECKS,
32018
32331
  ...CONTENT_CHECKS,
32019
32332
  ...AGENT_CHECKS
32020
32333
  ];
@@ -32140,7 +32453,7 @@ async function doctorRoutes(app, opts) {
32140
32453
 
32141
32454
  // ../api-routes/src/discovery/routes.ts
32142
32455
  import crypto26 from "crypto";
32143
- import { and as and23, desc as desc15, eq as eq32, gte as gte6, inArray as inArray11 } from "drizzle-orm";
32456
+ import { and as and24, desc as desc15, eq as eq33, gte as gte6, inArray as inArray11 } from "drizzle-orm";
32144
32457
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
32145
32458
  async function discoveryRoutes(app, opts) {
32146
32459
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -32172,9 +32485,9 @@ async function discoveryRoutes(app, opts) {
32172
32485
  const now = (/* @__PURE__ */ new Date()).toISOString();
32173
32486
  const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
32174
32487
  const decision = app.db.transaction((tx) => {
32175
- const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and23(
32176
- eq32(discoverySessions.projectId, project.id),
32177
- eq32(discoverySessions.icpDescription, icpDescription),
32488
+ const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and24(
32489
+ eq33(discoverySessions.projectId, project.id),
32490
+ eq33(discoverySessions.icpDescription, icpDescription),
32178
32491
  inArray11(discoverySessions.status, [
32179
32492
  DiscoverySessionStatuses.queued,
32180
32493
  DiscoverySessionStatuses.seeding,
@@ -32244,7 +32557,7 @@ async function discoveryRoutes(app, opts) {
32244
32557
  const project = resolveProject(app.db, request.params.name);
32245
32558
  const parsedLimit = parseInt(request.query.limit ?? "", 10);
32246
32559
  const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
32247
- const rows = app.db.select().from(discoverySessions).where(eq32(discoverySessions.projectId, project.id)).orderBy(desc15(discoverySessions.createdAt)).limit(limit).all();
32560
+ const rows = app.db.select().from(discoverySessions).where(eq33(discoverySessions.projectId, project.id)).orderBy(desc15(discoverySessions.createdAt)).limit(limit).all();
32248
32561
  return reply.send(rows.map(serializeSession));
32249
32562
  }
32250
32563
  );
@@ -32252,11 +32565,11 @@ async function discoveryRoutes(app, opts) {
32252
32565
  "/projects/:name/discover/sessions/:id",
32253
32566
  async (request, reply) => {
32254
32567
  const project = resolveProject(app.db, request.params.name);
32255
- const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
32568
+ const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
32256
32569
  if (!session || session.projectId !== project.id) {
32257
32570
  throw notFound("Discovery session", request.params.id);
32258
32571
  }
32259
- const probeRows = app.db.select().from(discoveryProbes).where(eq32(discoveryProbes.sessionId, session.id)).all();
32572
+ const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
32260
32573
  const detail = {
32261
32574
  ...serializeSession(session),
32262
32575
  probes: probeRows.map(serializeProbe)
@@ -32268,12 +32581,12 @@ async function discoveryRoutes(app, opts) {
32268
32581
  "/projects/:name/discover/sessions/:id/promote",
32269
32582
  async (request, reply) => {
32270
32583
  const project = resolveProject(app.db, request.params.name);
32271
- const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
32584
+ const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
32272
32585
  if (!session || session.projectId !== project.id) {
32273
32586
  throw notFound("Discovery session", request.params.id);
32274
32587
  }
32275
- const probeRows = app.db.select().from(discoveryProbes).where(eq32(discoveryProbes.sessionId, session.id)).all();
32276
- const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq32(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
32588
+ const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
32589
+ const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
32277
32590
  const seenCompetitors = new Set(existingCompetitors);
32278
32591
  const cited = /* @__PURE__ */ new Set();
32279
32592
  const aspirational = /* @__PURE__ */ new Set();
@@ -32302,7 +32615,7 @@ async function discoveryRoutes(app, opts) {
32302
32615
  );
32303
32616
  app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
32304
32617
  const project = resolveProject(app.db, request.params.name);
32305
- const session = app.db.select().from(discoverySessions).where(eq32(discoverySessions.id, request.params.id)).get();
32618
+ const session = app.db.select().from(discoverySessions).where(eq33(discoverySessions.id, request.params.id)).get();
32306
32619
  if (!session || session.projectId !== project.id) {
32307
32620
  throw notFound("Discovery session", request.params.id);
32308
32621
  }
@@ -32325,7 +32638,7 @@ async function discoveryRoutes(app, opts) {
32325
32638
  const bucketSet = new Set(buckets);
32326
32639
  const includeCompetitors = parsed.data.includeCompetitors ?? true;
32327
32640
  const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
32328
- const probeRows = app.db.select().from(discoveryProbes).where(eq32(discoveryProbes.sessionId, session.id)).all();
32641
+ const probeRows = app.db.select().from(discoveryProbes).where(eq33(discoveryProbes.sessionId, session.id)).all();
32329
32642
  const candidateQueries = /* @__PURE__ */ new Set();
32330
32643
  for (const probe of probeRows) {
32331
32644
  if (!probe.bucket) continue;
@@ -32333,7 +32646,7 @@ async function discoveryRoutes(app, opts) {
32333
32646
  if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
32334
32647
  }
32335
32648
  const existingQueries = new Set(
32336
- app.db.select({ query: queries.query }).from(queries).where(eq32(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
32649
+ app.db.select({ query: queries.query }).from(queries).where(eq33(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
32337
32650
  );
32338
32651
  const promotedQueries = [];
32339
32652
  const skippedQueries = [];
@@ -32349,7 +32662,7 @@ async function discoveryRoutes(app, opts) {
32349
32662
  const skippedCompetitors = [];
32350
32663
  if (includeCompetitors) {
32351
32664
  const existingCompetitors = new Set(
32352
- app.db.select({ domain: competitors.domain }).from(competitors).where(eq32(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
32665
+ app.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
32353
32666
  );
32354
32667
  const competitorMap = parseCompetitorMap(session.competitorMap);
32355
32668
  for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
@@ -32453,7 +32766,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
32453
32766
 
32454
32767
  // ../api-routes/src/discovery/orchestrate.ts
32455
32768
  import crypto27 from "crypto";
32456
- import { eq as eq33 } from "drizzle-orm";
32769
+ import { eq as eq34 } from "drizzle-orm";
32457
32770
  var DEFAULT_MAX_PROBES = 100;
32458
32771
  var ABSOLUTE_MAX_PROBES = 500;
32459
32772
  function classifyProbeBucket(input) {
@@ -32507,7 +32820,7 @@ async function executeDiscovery(opts) {
32507
32820
  status: DiscoverySessionStatuses.seeding,
32508
32821
  dedupThreshold,
32509
32822
  startedAt
32510
- }).where(eq33(discoverySessions.id, opts.sessionId)).run();
32823
+ }).where(eq34(discoverySessions.id, opts.sessionId)).run();
32511
32824
  const seedResult = await opts.deps.seed({
32512
32825
  project: opts.project,
32513
32826
  icpDescription: opts.icpDescription,
@@ -32533,7 +32846,7 @@ async function executeDiscovery(opts) {
32533
32846
  seedCountRaw,
32534
32847
  seedCount,
32535
32848
  warning
32536
- }).where(eq33(discoverySessions.id, opts.sessionId)).run();
32849
+ }).where(eq34(discoverySessions.id, opts.sessionId)).run();
32537
32850
  const probeRows = [];
32538
32851
  const buckets = { cited: 0, aspirational: 0, "wasted-surface": 0 };
32539
32852
  for (const query of probedCanonicals) {
@@ -32573,7 +32886,7 @@ async function executeDiscovery(opts) {
32573
32886
  wastedCount: buckets["wasted-surface"],
32574
32887
  competitorMap,
32575
32888
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
32576
- }).where(eq33(discoverySessions.id, opts.sessionId)).run();
32889
+ }).where(eq34(discoverySessions.id, opts.sessionId)).run();
32577
32890
  upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
32578
32891
  return {
32579
32892
  buckets,
@@ -32613,7 +32926,7 @@ function markSessionFailed(db, sessionId, error) {
32613
32926
  status: DiscoverySessionStatuses.failed,
32614
32927
  error,
32615
32928
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
32616
- }).where(eq33(discoverySessions.id, sessionId)).run();
32929
+ }).where(eq34(discoverySessions.id, sessionId)).run();
32617
32930
  }
32618
32931
  function dedupeStrings(input) {
32619
32932
  const seen = /* @__PURE__ */ new Set();
@@ -32631,7 +32944,7 @@ function dedupeStrings(input) {
32631
32944
 
32632
32945
  // ../api-routes/src/technical-aeo.ts
32633
32946
  import crypto28 from "crypto";
32634
- import { and as and24, asc as asc4, count, desc as desc16, eq as eq34, inArray as inArray12 } from "drizzle-orm";
32947
+ import { and as and25, asc as asc4, count, desc as desc16, eq as eq35, inArray as inArray12 } from "drizzle-orm";
32635
32948
  var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
32636
32949
  function emptyScore(projectName) {
32637
32950
  return {
@@ -32663,9 +32976,9 @@ function parsePositiveInt(value, fallback, max) {
32663
32976
  async function technicalAeoRoutes(app, opts) {
32664
32977
  app.get("/projects/:name/technical-aeo", async (request) => {
32665
32978
  const project = resolveProject(app.db, request.params.name);
32666
- const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq34(siteAuditSnapshots.runId, runs.id)).where(and24(
32667
- eq34(siteAuditSnapshots.projectId, project.id),
32668
- eq34(runs.kind, RunKinds["site-audit"]),
32979
+ const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
32980
+ eq35(siteAuditSnapshots.projectId, project.id),
32981
+ eq35(runs.kind, RunKinds["site-audit"]),
32669
32982
  inArray12(runs.status, SURFACEABLE_STATUSES),
32670
32983
  notProbeRun()
32671
32984
  )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
@@ -32698,9 +33011,9 @@ async function technicalAeoRoutes(app, opts) {
32698
33011
  });
32699
33012
  app.get("/projects/:name/technical-aeo/pages", async (request) => {
32700
33013
  const project = resolveProject(app.db, request.params.name);
32701
- const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq34(siteAuditSnapshots.runId, runs.id)).where(and24(
32702
- eq34(siteAuditSnapshots.projectId, project.id),
32703
- eq34(runs.kind, RunKinds["site-audit"]),
33014
+ const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
33015
+ eq35(siteAuditSnapshots.projectId, project.id),
33016
+ eq35(runs.kind, RunKinds["site-audit"]),
32704
33017
  inArray12(runs.status, SURFACEABLE_STATUSES),
32705
33018
  notProbeRun()
32706
33019
  )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
@@ -32708,9 +33021,9 @@ async function technicalAeoRoutes(app, opts) {
32708
33021
  return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
32709
33022
  }
32710
33023
  const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
32711
- const conds = [eq34(siteAuditPages.runId, latest.runId)];
32712
- if (statusFilter) conds.push(eq34(siteAuditPages.status, statusFilter));
32713
- const where = and24(...conds);
33024
+ const conds = [eq35(siteAuditPages.runId, latest.runId)];
33025
+ if (statusFilter) conds.push(eq35(siteAuditPages.status, statusFilter));
33026
+ const where = and25(...conds);
32714
33027
  const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
32715
33028
  const total = totalRow?.value ?? 0;
32716
33029
  const limit = parsePositiveInt(request.query.limit, 100, 500);
@@ -32734,9 +33047,9 @@ async function technicalAeoRoutes(app, opts) {
32734
33047
  auditedAt: siteAuditSnapshots.auditedAt,
32735
33048
  aggregateScore: siteAuditSnapshots.aggregateScore,
32736
33049
  pagesAudited: siteAuditSnapshots.pagesAudited
32737
- }).from(siteAuditSnapshots).innerJoin(runs, eq34(siteAuditSnapshots.runId, runs.id)).where(and24(
32738
- eq34(siteAuditSnapshots.projectId, project.id),
32739
- eq34(runs.kind, RunKinds["site-audit"]),
33050
+ }).from(siteAuditSnapshots).innerJoin(runs, eq35(siteAuditSnapshots.runId, runs.id)).where(and25(
33051
+ eq35(siteAuditSnapshots.projectId, project.id),
33052
+ eq35(runs.kind, RunKinds["site-audit"]),
32740
33053
  inArray12(runs.status, SURFACEABLE_STATUSES),
32741
33054
  notProbeRun()
32742
33055
  )).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
@@ -32748,9 +33061,9 @@ async function technicalAeoRoutes(app, opts) {
32748
33061
  if (!parsed.success) {
32749
33062
  throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
32750
33063
  }
32751
- const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and24(
32752
- eq34(runs.projectId, project.id),
32753
- eq34(runs.kind, RunKinds["site-audit"]),
33064
+ const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and25(
33065
+ eq35(runs.projectId, project.id),
33066
+ eq35(runs.kind, RunKinds["site-audit"]),
32754
33067
  inArray12(runs.status, [RunStatuses.queued, RunStatuses.running])
32755
33068
  )).get();
32756
33069
  if (existing) {
@@ -32937,6 +33250,8 @@ async function apiRoutes(app, opts) {
32937
33250
  onInstallBacklinks: opts.onInstallBacklinks,
32938
33251
  onReleaseSyncRequested: opts.onReleaseSyncRequested,
32939
33252
  onBacklinkExtractRequested: opts.onBacklinkExtractRequested,
33253
+ onBingBacklinkSyncRequested: opts.onBingBacklinkSyncRequested,
33254
+ bingConnectionStore: opts.bingConnectionStore,
32940
33255
  onBacklinksPruneCache: opts.onBacklinksPruneCache,
32941
33256
  listCachedReleases: opts.listCachedReleases,
32942
33257
  discoverLatestRelease: opts.discoverLatestRelease
@@ -33335,9 +33650,9 @@ var IntelligenceService = class {
33335
33650
  */
33336
33651
  analyzeAndPersist(runId, projectId) {
33337
33652
  const recentRuns = this.db.select().from(runs).where(
33338
- and25(
33339
- eq35(runs.projectId, projectId),
33340
- or5(eq35(runs.status, "completed"), eq35(runs.status, "partial")),
33653
+ and26(
33654
+ eq36(runs.projectId, projectId),
33655
+ or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
33341
33656
  // Defensive: RunCoordinator already skips probes before this is
33342
33657
  // called, but if a future call site invokes analyzeAndPersist
33343
33658
  // directly for a probe, probes still must not pollute the
@@ -33419,7 +33734,7 @@ var IntelligenceService = class {
33419
33734
  * Returns the persisted insights so the coordinator can count critical/high.
33420
33735
  */
33421
33736
  analyzeAndPersistGbp(runId, projectId) {
33422
- const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq35(runs.id, runId)).get();
33737
+ const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq36(runs.id, runId)).get();
33423
33738
  if (!runRow) {
33424
33739
  log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
33425
33740
  this.persistGbpInsights(runId, projectId, [], []);
@@ -33427,9 +33742,9 @@ var IntelligenceService = class {
33427
33742
  }
33428
33743
  const windowStart = runRow.startedAt ?? runRow.createdAt;
33429
33744
  const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
33430
- const selected = this.db.select().from(gbpLocations).where(and25(
33431
- eq35(gbpLocations.projectId, projectId),
33432
- eq35(gbpLocations.selected, true),
33745
+ const selected = this.db.select().from(gbpLocations).where(and26(
33746
+ eq36(gbpLocations.projectId, projectId),
33747
+ eq36(gbpLocations.selected, true),
33433
33748
  gte7(gbpLocations.syncedAt, windowStart),
33434
33749
  lte4(gbpLocations.syncedAt, windowEnd)
33435
33750
  )).all();
@@ -33464,10 +33779,10 @@ var IntelligenceService = class {
33464
33779
  }
33465
33780
  /** Build the per-location signal bundle the GBP analyzer consumes. */
33466
33781
  buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
33467
- const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and25(eq35(gbpDailyMetrics.projectId, projectId), eq35(gbpDailyMetrics.locationName, locationName))).all();
33468
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and25(eq35(gbpPlaceActions.projectId, projectId), eq35(gbpPlaceActions.locationName, locationName))).all();
33469
- const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and25(eq35(gbpLodgingSnapshots.projectId, projectId), eq35(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).limit(1).get();
33470
- const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and25(eq35(gbpPlaceDetails.projectId, projectId), eq35(gbpPlaceDetails.locationName, locationName))).orderBy(desc17(gbpPlaceDetails.syncedAt)).limit(1).get();
33782
+ const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and26(eq36(gbpDailyMetrics.projectId, projectId), eq36(gbpDailyMetrics.locationName, locationName))).all();
33783
+ const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and26(eq36(gbpPlaceActions.projectId, projectId), eq36(gbpPlaceActions.locationName, locationName))).all();
33784
+ const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and26(eq36(gbpLodgingSnapshots.projectId, projectId), eq36(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).limit(1).get();
33785
+ const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and26(eq36(gbpPlaceDetails.projectId, projectId), eq36(gbpPlaceDetails.locationName, locationName))).orderBy(desc17(gbpPlaceDetails.syncedAt)).limit(1).get();
33471
33786
  const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
33472
33787
  const summary = buildGbpSummary({
33473
33788
  locationName,
@@ -33499,7 +33814,7 @@ var IntelligenceService = class {
33499
33814
  /** Build the month-over-month keyword series for a location from the
33500
33815
  * accumulating gbp_keyword_monthly table (latest complete month vs prior). */
33501
33816
  buildGbpKeywordTrend(projectId, locationName) {
33502
- const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and25(eq35(gbpKeywordMonthly.projectId, projectId), eq35(gbpKeywordMonthly.locationName, locationName))).all();
33817
+ const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and26(eq36(gbpKeywordMonthly.projectId, projectId), eq36(gbpKeywordMonthly.locationName, locationName))).all();
33503
33818
  if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
33504
33819
  const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
33505
33820
  const recentMonth = months[0] ?? null;
@@ -33530,7 +33845,7 @@ var IntelligenceService = class {
33530
33845
  */
33531
33846
  persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
33532
33847
  const covered = new Set(coveredLocationNames);
33533
- const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and25(eq35(insights.projectId, projectId), eq35(insights.provider, GBP_INSIGHT_PROVIDER))).all();
33848
+ const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and26(eq36(insights.projectId, projectId), eq36(insights.provider, GBP_INSIGHT_PROVIDER))).all();
33534
33849
  const staleIds = [];
33535
33850
  const dismissedSlots = /* @__PURE__ */ new Set();
33536
33851
  for (const row of existing) {
@@ -33541,7 +33856,7 @@ var IntelligenceService = class {
33541
33856
  }
33542
33857
  this.db.transaction((tx) => {
33543
33858
  for (const id of staleIds) {
33544
- tx.delete(insights).where(eq35(insights.id, id)).run();
33859
+ tx.delete(insights).where(eq36(insights.id, id)).run();
33545
33860
  }
33546
33861
  for (const insight of gbpInsights) {
33547
33862
  const parsed = parseGbpInsightId(insight.id);
@@ -33619,7 +33934,7 @@ var IntelligenceService = class {
33619
33934
  * create per run + aggregate). DB is left untouched.
33620
33935
  */
33621
33936
  backfill(projectName, opts, onProgress) {
33622
- const project = this.db.select().from(projects).where(eq35(projects.name, projectName)).get();
33937
+ const project = this.db.select().from(projects).where(eq36(projects.name, projectName)).get();
33623
33938
  if (!project) {
33624
33939
  throw new Error(`Project "${projectName}" not found`);
33625
33940
  }
@@ -33632,9 +33947,9 @@ var IntelligenceService = class {
33632
33947
  sinceTimestamp = parsed;
33633
33948
  }
33634
33949
  const allRuns = this.db.select().from(runs).where(
33635
- and25(
33636
- eq35(runs.projectId, project.id),
33637
- or5(eq35(runs.status, "completed"), eq35(runs.status, "partial")),
33950
+ and26(
33951
+ eq36(runs.projectId, project.id),
33952
+ or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
33638
33953
  // Backfill must not replay probe runs as if they were real sweeps.
33639
33954
  ne5(runs.trigger, RunTriggers.probe)
33640
33955
  )
@@ -33713,7 +34028,7 @@ var IntelligenceService = class {
33713
34028
  return { processed, skipped, totalInsights };
33714
34029
  }
33715
34030
  loadTrackedCompetitors(projectId) {
33716
- return this.db.select({ domain: competitors.domain }).from(competitors).where(eq35(competitors.projectId, projectId)).all().map((r) => r.domain);
34031
+ return this.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, projectId)).all().map((r) => r.domain);
33717
34032
  }
33718
34033
  /**
33719
34034
  * Wipe transition signals from an analysis result while keeping health.
@@ -33734,15 +34049,15 @@ var IntelligenceService = class {
33734
34049
  }
33735
34050
  persistResult(result, runId, projectId) {
33736
34051
  const previouslyDismissed = /* @__PURE__ */ new Set();
33737
- const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq35(insights.runId, runId)).all();
34052
+ const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq36(insights.runId, runId)).all();
33738
34053
  for (const row of existingInsights) {
33739
34054
  if (row.dismissed) {
33740
34055
  previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
33741
34056
  }
33742
34057
  }
33743
34058
  this.db.transaction((tx) => {
33744
- tx.delete(insights).where(eq35(insights.runId, runId)).run();
33745
- tx.delete(healthSnapshots).where(eq35(healthSnapshots.runId, runId)).run();
34059
+ tx.delete(insights).where(eq36(insights.runId, runId)).run();
34060
+ tx.delete(healthSnapshots).where(eq36(healthSnapshots.runId, runId)).run();
33746
34061
  const now = (/* @__PURE__ */ new Date()).toISOString();
33747
34062
  for (const insight of result.insights) {
33748
34063
  const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
@@ -33793,24 +34108,24 @@ var IntelligenceService = class {
33793
34108
  applySeverityTiering(rawInsights, excludeRunId, projectId) {
33794
34109
  const regressions = rawInsights.filter((i) => i.type === "regression");
33795
34110
  if (regressions.length === 0) return rawInsights;
33796
- const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq35(gscSearchData.projectId, projectId)).all();
34111
+ const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq36(gscSearchData.projectId, projectId)).all();
33797
34112
  const gscConnected = gscRows.length > 0;
33798
34113
  const gscImpressionsByQuery = /* @__PURE__ */ new Map();
33799
34114
  for (const row of gscRows) {
33800
34115
  const key = row.query.toLowerCase();
33801
34116
  gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
33802
34117
  }
33803
- const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq35(projects.id, projectId)).get();
34118
+ const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq36(projects.id, projectId)).get();
33804
34119
  const locationCount = Math.max(
33805
34120
  1,
33806
34121
  (projectRow?.locations ?? []).length
33807
34122
  );
33808
34123
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
33809
34124
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
33810
- and25(
33811
- eq35(runs.projectId, projectId),
33812
- eq35(runs.kind, RunKinds["answer-visibility"]),
33813
- or5(eq35(runs.status, "completed"), eq35(runs.status, "partial")),
34125
+ and26(
34126
+ eq36(runs.projectId, projectId),
34127
+ eq36(runs.kind, RunKinds["answer-visibility"]),
34128
+ or5(eq36(runs.status, "completed"), eq36(runs.status, "partial")),
33814
34129
  // Defensive — see top of file.
33815
34130
  ne5(runs.trigger, RunTriggers.probe)
33816
34131
  )
@@ -33830,7 +34145,7 @@ var IntelligenceService = class {
33830
34145
  const haveHistory = recentRunIds.length > 0;
33831
34146
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
33832
34147
  if (haveHistory) {
33833
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and25(eq35(insights.type, "regression"), inArray13(insights.runId, recentRunIds))).all();
34148
+ const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and26(eq36(insights.type, "regression"), inArray13(insights.runId, recentRunIds))).all();
33834
34149
  const regressionGroups = /* @__PURE__ */ new Map();
33835
34150
  for (const row of priorRows) {
33836
34151
  if (!row.runId) continue;
@@ -33859,7 +34174,7 @@ var IntelligenceService = class {
33859
34174
  });
33860
34175
  }
33861
34176
  buildRunData(runId, projectId, completedAt, location = null) {
33862
- const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq35(projects.id, projectId)).get();
34177
+ const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq36(projects.id, projectId)).get();
33863
34178
  const projectDomains = projectDomainRow ? effectiveDomains({
33864
34179
  canonicalDomain: projectDomainRow.canonicalDomain,
33865
34180
  ownedDomains: projectDomainRow.ownedDomains
@@ -33875,7 +34190,7 @@ var IntelligenceService = class {
33875
34190
  citedDomains: querySnapshots.citedDomains,
33876
34191
  competitorOverlap: querySnapshots.competitorOverlap,
33877
34192
  snapshotLocation: querySnapshots.location
33878
- }).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(eq35(querySnapshots.runId, runId)).all();
34193
+ }).from(querySnapshots).leftJoin(queries, eq36(querySnapshots.queryId, queries.id)).where(eq36(querySnapshots.runId, runId)).all();
33879
34194
  const snapshots = [];
33880
34195
  let orphanCount = 0;
33881
34196
  for (const r of rows) {
@@ -33987,6 +34302,8 @@ export {
33987
34302
  hashLodging,
33988
34303
  getUrlInfo,
33989
34304
  getCrawlIssues,
34305
+ getLinkCounts,
34306
+ getUrlLinks,
33990
34307
  PLUGIN_DIR,
33991
34308
  DUCKDB_SPEC,
33992
34309
  CC_CACHE_DIR,