@ainyc/canonry 2.16.0 → 2.16.3

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.
@@ -23,11 +23,13 @@ import {
23
23
  canonryMcpTools,
24
24
  categorizeSource,
25
25
  categoryLabel,
26
+ citationStateToCited,
26
27
  competitorBatchRequestSchema,
27
28
  configExists,
28
29
  deliveryFailed,
29
30
  determineAnswerMentioned,
30
31
  effectiveDomains,
32
+ emptyCitationVisibility,
31
33
  extractAnswerMentions,
32
34
  findDuplicateLocationLabels,
33
35
  hasLocationLabel,
@@ -60,7 +62,7 @@ import {
60
62
  visibilityStateFromAnswerMentioned,
61
63
  windowCutoff,
62
64
  wordpressEnvSchema
63
- } from "./chunk-HNVRN5QL.js";
65
+ } from "./chunk-7DVIJC6L.js";
64
66
  import {
65
67
  IntelligenceService,
66
68
  agentMemory,
@@ -178,7 +180,7 @@ import crypto28 from "crypto";
178
180
  import fs12 from "fs";
179
181
  import path14 from "path";
180
182
  import { fileURLToPath as fileURLToPath2 } from "url";
181
- import { eq as eq32 } from "drizzle-orm";
183
+ import { eq as eq33 } from "drizzle-orm";
182
184
  import Fastify from "fastify";
183
185
 
184
186
  // ../api-routes/src/auth.ts
@@ -2358,22 +2360,168 @@ async function intelligenceRoutes(app) {
2358
2360
  });
2359
2361
  }
2360
2362
 
2363
+ // ../api-routes/src/citations.ts
2364
+ import { eq as eq12, inArray as inArray3 } from "drizzle-orm";
2365
+ async function citationRoutes(app) {
2366
+ app.get("/projects/:name/citations/visibility", async (request, reply) => {
2367
+ const project = resolveProject(app.db, request.params.name);
2368
+ const configuredProviders = parseJsonColumn(project.providers, []);
2369
+ const projectKeywords = app.db.select().from(keywords).where(eq12(keywords.projectId, project.id)).all();
2370
+ if (projectKeywords.length === 0) {
2371
+ return reply.send(emptyCitationVisibility("no-keywords"));
2372
+ }
2373
+ const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(eq12(runs.projectId, project.id)).all();
2374
+ if (projectRuns.length === 0) {
2375
+ return reply.send(emptyCitationVisibility("no-runs-yet"));
2376
+ }
2377
+ const runCreatedAt = new Map(projectRuns.map((r) => [r.id, r.createdAt]));
2378
+ const rawSnapshots = app.db.select({
2379
+ id: querySnapshots.id,
2380
+ runId: querySnapshots.runId,
2381
+ keywordId: querySnapshots.keywordId,
2382
+ provider: querySnapshots.provider,
2383
+ citationState: querySnapshots.citationState,
2384
+ citedDomains: querySnapshots.citedDomains,
2385
+ competitorOverlap: querySnapshots.competitorOverlap,
2386
+ createdAt: querySnapshots.createdAt
2387
+ }).from(querySnapshots).where(inArray3(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
2388
+ if (rawSnapshots.length === 0) {
2389
+ return reply.send(emptyCitationVisibility("no-runs-yet"));
2390
+ }
2391
+ const snapshots = rawSnapshots.map((s) => ({
2392
+ ...s,
2393
+ runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
2394
+ }));
2395
+ const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq12(competitors.projectId, project.id)).all().map((c) => normalizeDomain(c.domain)).filter((d) => d.length > 0);
2396
+ const response = computeCitationVisibility({
2397
+ keywords: projectKeywords.map((k) => ({ id: k.id, keyword: k.keyword })),
2398
+ snapshots,
2399
+ configuredProviders,
2400
+ competitorDomains: projectCompetitors
2401
+ });
2402
+ return reply.send(response);
2403
+ });
2404
+ }
2405
+ function computeCitationVisibility(input) {
2406
+ const { keywords: kws, snapshots, configuredProviders, competitorDomains } = input;
2407
+ const latestByPair = /* @__PURE__ */ new Map();
2408
+ for (const snap of snapshots) {
2409
+ const key = `${snap.keywordId}::${snap.provider}`;
2410
+ const existing = latestByPair.get(key);
2411
+ if (!existing || snap.createdAt > existing.createdAt) {
2412
+ latestByPair.set(key, snap);
2413
+ }
2414
+ }
2415
+ const observedProviders = /* @__PURE__ */ new Set();
2416
+ for (const pair of latestByPair.values()) observedProviders.add(pair.provider);
2417
+ const providerUniverse = configuredProviders.length > 0 ? Array.from(new Set(configuredProviders)) : Array.from(observedProviders).sort();
2418
+ const byKeyword = [];
2419
+ const providersCitingTracker = /* @__PURE__ */ new Set();
2420
+ let keywordsCited = 0;
2421
+ let keywordsFullyCovered = 0;
2422
+ let keywordsUncovered = 0;
2423
+ for (const kw of kws) {
2424
+ const providers = [];
2425
+ let citedCount = 0;
2426
+ for (const provider of providerUniverse) {
2427
+ const snap = latestByPair.get(`${kw.id}::${provider}`);
2428
+ if (!snap) continue;
2429
+ const state = snap.citationState;
2430
+ const cited = citationStateToCited(state);
2431
+ if (cited) {
2432
+ citedCount++;
2433
+ providersCitingTracker.add(provider);
2434
+ }
2435
+ providers.push({
2436
+ provider,
2437
+ citationState: state,
2438
+ cited,
2439
+ runId: snap.runId,
2440
+ runCreatedAt: snap.runCreatedAt
2441
+ });
2442
+ }
2443
+ if (citedCount > 0) keywordsCited++;
2444
+ if (providerUniverse.length > 0 && citedCount === providerUniverse.length) keywordsFullyCovered++;
2445
+ if (providers.length > 0 && citedCount === 0) keywordsUncovered++;
2446
+ byKeyword.push({
2447
+ keywordId: kw.id,
2448
+ keyword: kw.keyword,
2449
+ providers,
2450
+ citedCount,
2451
+ totalProviders: providers.length
2452
+ });
2453
+ }
2454
+ const competitorSet = new Set(competitorDomains);
2455
+ const competitorGaps = [];
2456
+ const keywordById = new Map(kws.map((k) => [k.id, k.keyword]));
2457
+ for (const snap of latestByPair.values()) {
2458
+ if (citationStateToCited(snap.citationState)) continue;
2459
+ if (competitorSet.size === 0) continue;
2460
+ const cited = parseJsonColumn(snap.citedDomains, []);
2461
+ const overlap = parseJsonColumn(snap.competitorOverlap, []);
2462
+ const candidates = new Set(
2463
+ [...cited, ...overlap].map((d) => normalizeDomain(d)).filter((d) => d.length > 0)
2464
+ );
2465
+ const citingCompetitors = Array.from(candidates).filter((d) => competitorSet.has(d));
2466
+ if (citingCompetitors.length === 0) continue;
2467
+ competitorGaps.push({
2468
+ keywordId: snap.keywordId,
2469
+ keyword: keywordById.get(snap.keywordId) ?? "",
2470
+ provider: snap.provider,
2471
+ citingCompetitors: citingCompetitors.sort(),
2472
+ runId: snap.runId,
2473
+ runCreatedAt: snap.runCreatedAt
2474
+ });
2475
+ }
2476
+ competitorGaps.sort((a, b) => {
2477
+ if (a.keyword !== b.keyword) return a.keyword.localeCompare(b.keyword);
2478
+ return a.provider.localeCompare(b.provider);
2479
+ });
2480
+ let latestRunId = null;
2481
+ let latestRunAt = null;
2482
+ for (const snap of latestByPair.values()) {
2483
+ if (latestRunAt === null || snap.runCreatedAt > latestRunAt) {
2484
+ latestRunAt = snap.runCreatedAt;
2485
+ latestRunId = snap.runId;
2486
+ }
2487
+ }
2488
+ const summary = {
2489
+ providersConfigured: providerUniverse.length,
2490
+ providersCiting: providersCitingTracker.size,
2491
+ totalKeywords: kws.length,
2492
+ keywordsCited,
2493
+ keywordsFullyCovered,
2494
+ keywordsUncovered,
2495
+ latestRunId,
2496
+ latestRunAt
2497
+ };
2498
+ return {
2499
+ summary,
2500
+ byKeyword,
2501
+ competitorGaps,
2502
+ status: "ready"
2503
+ };
2504
+ }
2505
+ function normalizeDomain(domain) {
2506
+ return domain.toLowerCase().trim().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
2507
+ }
2508
+
2361
2509
  // ../api-routes/src/composites.ts
2362
- import { eq as eq12, and as and3, desc as desc5, sql as sql3, like, or as or2 } from "drizzle-orm";
2510
+ import { eq as eq13, and as and3, desc as desc5, sql as sql3, like, or as or2 } from "drizzle-orm";
2363
2511
  var TOP_INSIGHT_LIMIT = 5;
2364
2512
  var SEARCH_HIT_HARD_LIMIT = 50;
2365
2513
  var SEARCH_SNIPPET_RADIUS = 80;
2366
2514
  async function compositeRoutes(app) {
2367
2515
  app.get("/projects/:name/overview", async (request, reply) => {
2368
2516
  const project = resolveProject(app.db, request.params.name);
2369
- const totalRunsRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(eq12(runs.projectId, project.id)).get();
2517
+ const totalRunsRow = app.db.select({ count: sql3`count(*)` }).from(runs).where(eq13(runs.projectId, project.id)).get();
2370
2518
  const totalRuns = totalRunsRow?.count ?? 0;
2371
- const recentRuns = app.db.select().from(runs).where(eq12(runs.projectId, project.id)).orderBy(desc5(runs.createdAt)).limit(2).all();
2519
+ const recentRuns = app.db.select().from(runs).where(eq13(runs.projectId, project.id)).orderBy(desc5(runs.createdAt)).limit(2).all();
2372
2520
  const [latestRunRow, previousRunRow] = recentRuns;
2373
2521
  const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
2374
- const healthRow = app.db.select().from(healthSnapshots).where(eq12(healthSnapshots.projectId, project.id)).orderBy(desc5(healthSnapshots.createdAt)).limit(1).get();
2522
+ const healthRow = app.db.select().from(healthSnapshots).where(eq13(healthSnapshots.projectId, project.id)).orderBy(desc5(healthSnapshots.createdAt)).limit(1).get();
2375
2523
  const health = healthRow ? mapHealthRow2(healthRow) : null;
2376
- const insightRows = app.db.select().from(insights).where(eq12(insights.projectId, project.id)).orderBy(desc5(insights.createdAt)).all();
2524
+ const insightRows = app.db.select().from(insights).where(eq13(insights.projectId, project.id)).orderBy(desc5(insights.createdAt)).all();
2377
2525
  const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
2378
2526
  const { keywordCounts, providers } = summarizeLatestRun(app, latestRunRow ?? null);
2379
2527
  const transitions = summarizeTransitions(app, latestRunRow ?? null, previousRunRow ?? null);
@@ -2409,9 +2557,9 @@ async function compositeRoutes(app) {
2409
2557
  citedDomains: querySnapshots.citedDomains,
2410
2558
  rawResponse: querySnapshots.rawResponse,
2411
2559
  createdAt: querySnapshots.createdAt
2412
- }).from(querySnapshots).innerJoin(keywords, eq12(querySnapshots.keywordId, keywords.id)).where(
2560
+ }).from(querySnapshots).innerJoin(keywords, eq13(querySnapshots.keywordId, keywords.id)).where(
2413
2561
  and3(
2414
- eq12(keywords.projectId, project.id),
2562
+ eq13(keywords.projectId, project.id),
2415
2563
  or2(
2416
2564
  sql3`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
2417
2565
  sql3`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
@@ -2422,7 +2570,7 @@ async function compositeRoutes(app) {
2422
2570
  ).orderBy(desc5(querySnapshots.createdAt)).limit(limit + 1).all();
2423
2571
  const insightMatches = app.db.select().from(insights).where(
2424
2572
  and3(
2425
- eq12(insights.projectId, project.id),
2573
+ eq13(insights.projectId, project.id),
2426
2574
  or2(
2427
2575
  like(insights.title, pattern),
2428
2576
  like(insights.keyword, pattern),
@@ -2485,7 +2633,7 @@ function summarizeLatestRun(app, run) {
2485
2633
  keywordId: querySnapshots.keywordId,
2486
2634
  provider: querySnapshots.provider,
2487
2635
  citationState: querySnapshots.citationState
2488
- }).from(querySnapshots).where(eq12(querySnapshots.runId, run.id)).all();
2636
+ }).from(querySnapshots).where(eq13(querySnapshots.runId, run.id)).all();
2489
2637
  if (rows.length === 0) return empty;
2490
2638
  const perKeyword = /* @__PURE__ */ new Map();
2491
2639
  const perProvider = /* @__PURE__ */ new Map();
@@ -2524,7 +2672,7 @@ function summarizeTransitions(app, latest, previous) {
2524
2672
  const rows = app.db.select({
2525
2673
  keywordId: querySnapshots.keywordId,
2526
2674
  citationState: querySnapshots.citationState
2527
- }).from(querySnapshots).where(eq12(querySnapshots.runId, runId)).all();
2675
+ }).from(querySnapshots).where(eq13(querySnapshots.runId, runId)).all();
2528
2676
  const map = /* @__PURE__ */ new Map();
2529
2677
  for (const row of rows) {
2530
2678
  const cited = row.citationState === "cited";
@@ -2681,16 +2829,16 @@ function makeSnippet(text, query) {
2681
2829
  }
2682
2830
 
2683
2831
  // ../api-routes/src/content-data.ts
2684
- import { and as and4, eq as eq13, desc as desc6, inArray as inArray3 } from "drizzle-orm";
2832
+ import { and as and4, eq as eq14, desc as desc6, inArray as inArray4 } from "drizzle-orm";
2685
2833
  var RECENT_RUNS_WINDOW = 5;
2686
2834
  function loadOrchestratorInput(db, project) {
2687
2835
  const projectId = project.id;
2688
- const ownDomain = normalizeDomain(project.canonicalDomain);
2836
+ const ownDomain = normalizeDomain2(project.canonicalDomain);
2689
2837
  const ownedDomains = parseJsonColumn(project.ownedDomains, []);
2690
- const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain)]);
2838
+ const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain2)]);
2691
2839
  const trackedKeywords = listKeywords(db, projectId);
2692
2840
  const candidateQueryStrings = trackedKeywords.filter(isBlogShapedQuery);
2693
- const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain);
2841
+ const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain2);
2694
2842
  const competitorSet = new Set(trackedCompetitors);
2695
2843
  const recentRunIds = listRecentAnswerVisibilityRunIds(db, projectId, RECENT_RUNS_WINDOW);
2696
2844
  const latestRunId = recentRunIds[0] ?? "";
@@ -2727,43 +2875,43 @@ function loadOrchestratorInput(db, project) {
2727
2875
  };
2728
2876
  }
2729
2877
  function listKeywords(db, projectId) {
2730
- const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq13(keywords.projectId, projectId)).all();
2878
+ const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq14(keywords.projectId, projectId)).all();
2731
2879
  return rows.map((r) => r.text);
2732
2880
  }
2733
2881
  function listCompetitorDomains(db, projectId) {
2734
- const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq13(competitors.projectId, projectId)).all();
2882
+ const rows = db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, projectId)).all();
2735
2883
  return rows.map((r) => r.domain);
2736
2884
  }
2737
2885
  function listRecentAnswerVisibilityRunIds(db, projectId, limit) {
2738
2886
  const rows = db.select({ id: runs.id }).from(runs).where(
2739
2887
  and4(
2740
- eq13(runs.projectId, projectId),
2741
- eq13(runs.kind, RunKinds["answer-visibility"]),
2888
+ eq14(runs.projectId, projectId),
2889
+ eq14(runs.kind, RunKinds["answer-visibility"]),
2742
2890
  // Queued/running/failed/cancelled runs may have partial or no
2743
2891
  // snapshots; including them risks pointing latestRunId at a run with
2744
2892
  // no usable evidence.
2745
- inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
2893
+ inArray4(runs.status, [RunStatuses.completed, RunStatuses.partial])
2746
2894
  )
2747
2895
  ).orderBy(desc6(runs.createdAt)).limit(limit).all();
2748
2896
  return rows.map((r) => r.id);
2749
2897
  }
2750
2898
  function lookupRunTimestamp(db, runId) {
2751
- const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq13(runs.id, runId)).get();
2899
+ const row = db.select({ createdAt: runs.createdAt }).from(runs).where(eq14(runs.id, runId)).get();
2752
2900
  return row?.createdAt ?? "";
2753
2901
  }
2754
2902
  function listGscPagesForProject(db, projectId) {
2755
- const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
2903
+ const rows = db.selectDistinct({ page: gscSearchData.page }).from(gscSearchData).where(eq14(gscSearchData.projectId, projectId)).all();
2756
2904
  return rows.map((r) => r.page);
2757
2905
  }
2758
2906
  function listGa4LandingPagesForProject(db, projectId) {
2759
- const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
2907
+ const rows = db.selectDistinct({ landingPage: gaTrafficSnapshots.landingPage }).from(gaTrafficSnapshots).where(eq14(gaTrafficSnapshots.projectId, projectId)).all();
2760
2908
  return rows.map((r) => r.landingPage);
2761
2909
  }
2762
2910
  function buildGaTrafficByPage(db, projectId) {
2763
2911
  const rows = db.select({
2764
2912
  landingPage: gaTrafficSnapshots.landingPage,
2765
2913
  sessions: gaTrafficSnapshots.sessions
2766
- }).from(gaTrafficSnapshots).where(eq13(gaTrafficSnapshots.projectId, projectId)).all();
2914
+ }).from(gaTrafficSnapshots).where(eq14(gaTrafficSnapshots.projectId, projectId)).all();
2767
2915
  const map = /* @__PURE__ */ new Map();
2768
2916
  for (const row of rows) {
2769
2917
  const path15 = extractPath(row.landingPage);
@@ -2773,24 +2921,24 @@ function buildGaTrafficByPage(db, projectId) {
2773
2921
  return map;
2774
2922
  }
2775
2923
  function sumAiReferralSessions(db, projectId) {
2776
- const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq13(gaAiReferrals.projectId, projectId)).all();
2924
+ const rows = db.select({ sessions: gaAiReferrals.sessions }).from(gaAiReferrals).where(eq14(gaAiReferrals.projectId, projectId)).all();
2777
2925
  return rows.reduce((acc, r) => acc + (r.sessions ?? 0), 0);
2778
2926
  }
2779
2927
  function buildCandidateQueries(opts) {
2780
2928
  if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
2781
2929
  return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
2782
2930
  }
2783
- const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq13(keywords.projectId, opts.projectId)).all();
2931
+ const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq14(keywords.projectId, opts.projectId)).all();
2784
2932
  const keywordIdByText = new Map(keywordRows.map((r) => [r.text, r.id]));
2785
2933
  const candidateKeywordIds = opts.candidateQueryStrings.map((q) => keywordIdByText.get(q)).filter((id) => Boolean(id));
2786
- const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
2934
+ const snapshotRows = opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
2787
2935
  const snapshotsByKeyword = /* @__PURE__ */ new Map();
2788
2936
  for (const row of snapshotRows) {
2789
2937
  const list = snapshotsByKeyword.get(row.keywordId) ?? [];
2790
2938
  list.push(row);
2791
2939
  snapshotsByKeyword.set(row.keywordId, list);
2792
2940
  }
2793
- const gscRows = opts.db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, opts.projectId)).all();
2941
+ const gscRows = opts.db.select().from(gscSearchData).where(eq14(gscSearchData.projectId, opts.projectId)).all();
2794
2942
  const gscByQuery = aggregateGscByQuery(gscRows);
2795
2943
  return opts.candidateQueryStrings.map((query) => {
2796
2944
  const keywordId = keywordIdByText.get(query);
@@ -2854,13 +3002,13 @@ function aggregateCandidate(opts) {
2854
3002
  const isLatestRun = snap.runId === opts.latestRunId;
2855
3003
  const competitorOverlap = parseJsonColumn(snap.competitorOverlap, []);
2856
3004
  for (const domain of competitorOverlap) {
2857
- const normalized = normalizeDomain(domain);
3005
+ const normalized = normalizeDomain2(domain);
2858
3006
  if (!opts.competitorSet.has(normalized)) continue;
2859
3007
  competitorTally.set(normalized, (competitorTally.get(normalized) ?? 0) + 1);
2860
3008
  }
2861
3009
  const grounding = extractGroundingSources(snap.rawResponse);
2862
3010
  for (const g of grounding) {
2863
- const domain = normalizeDomain(extractHostFromUri(g.uri));
3011
+ const domain = normalizeDomain2(extractHostFromUri(g.uri));
2864
3012
  if (!domain) continue;
2865
3013
  if (opts.ourDomains.has(domain)) {
2866
3014
  if (isLatestRun) ourCitedInLatestRun = true;
@@ -2946,7 +3094,7 @@ function extractHostFromUri(uri) {
2946
3094
  return "";
2947
3095
  }
2948
3096
  }
2949
- function normalizeDomain(domain) {
3097
+ function normalizeDomain2(domain) {
2950
3098
  return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
2951
3099
  }
2952
3100
  function extractPath(url) {
@@ -5237,6 +5385,18 @@ var routeCatalog = [
5237
5385
  404: { description: "Project not found." }
5238
5386
  }
5239
5387
  },
5388
+ {
5389
+ method: "get",
5390
+ path: "/api/v1/projects/{name}/citations/visibility",
5391
+ summary: "Citation visibility headline (cited by N of M engines)",
5392
+ description: 'Single-call read for the AI citation surface. Returns project headline (`providersConfigured`/`providersCiting`/keyword coverage counts), per-keyword engine coverage rows from the latest snapshot per (keyword \xD7 provider), and a competitor-gap list (keywords where the project is not cited but a configured competitor is). Status `no-data` with `reason: "no-runs-yet"` or `"no-keywords"` when the project lacks the inputs.',
5393
+ tags: ["intelligence"],
5394
+ parameters: [nameParameter],
5395
+ responses: {
5396
+ 200: { description: "Citation visibility report or no-data sentinel returned." },
5397
+ 404: { description: "Project not found." }
5398
+ }
5399
+ },
5240
5400
  // Content opportunity engine
5241
5401
  {
5242
5402
  method: "get",
@@ -5881,7 +6041,7 @@ async function telemetryRoutes(app, opts) {
5881
6041
 
5882
6042
  // ../api-routes/src/schedules.ts
5883
6043
  import crypto11 from "crypto";
5884
- import { eq as eq14 } from "drizzle-orm";
6044
+ import { eq as eq15 } from "drizzle-orm";
5885
6045
  async function scheduleRoutes(app, opts) {
5886
6046
  app.put("/projects/:name/schedule", async (request, reply) => {
5887
6047
  const project = resolveProject(app.db, request.params.name);
@@ -5924,7 +6084,7 @@ async function scheduleRoutes(app, opts) {
5924
6084
  }
5925
6085
  const now = (/* @__PURE__ */ new Date()).toISOString();
5926
6086
  const enabledInt = enabled === false ? 0 : 1;
5927
- const existing = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
6087
+ const existing = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
5928
6088
  if (existing) {
5929
6089
  app.db.update(schedules).set({
5930
6090
  cronExpr,
@@ -5933,7 +6093,7 @@ async function scheduleRoutes(app, opts) {
5933
6093
  providers: JSON.stringify(providers),
5934
6094
  enabled: enabledInt,
5935
6095
  updatedAt: now
5936
- }).where(eq14(schedules.id, existing.id)).run();
6096
+ }).where(eq15(schedules.id, existing.id)).run();
5937
6097
  } else {
5938
6098
  app.db.insert(schedules).values({
5939
6099
  id: crypto11.randomUUID(),
@@ -5955,12 +6115,12 @@ async function scheduleRoutes(app, opts) {
5955
6115
  diff: { cronExpr, preset, timezone, providers }
5956
6116
  });
5957
6117
  opts.onScheduleUpdated?.("upsert", project.id);
5958
- const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
6118
+ const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
5959
6119
  return reply.status(existing ? 200 : 201).send(formatSchedule(schedule));
5960
6120
  });
5961
6121
  app.get("/projects/:name/schedule", async (request, reply) => {
5962
6122
  const project = resolveProject(app.db, request.params.name);
5963
- const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
6123
+ const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
5964
6124
  if (!schedule) {
5965
6125
  throw notFound("Schedule", request.params.name);
5966
6126
  }
@@ -5968,11 +6128,11 @@ async function scheduleRoutes(app, opts) {
5968
6128
  });
5969
6129
  app.delete("/projects/:name/schedule", async (request, reply) => {
5970
6130
  const project = resolveProject(app.db, request.params.name);
5971
- const schedule = app.db.select().from(schedules).where(eq14(schedules.projectId, project.id)).get();
6131
+ const schedule = app.db.select().from(schedules).where(eq15(schedules.projectId, project.id)).get();
5972
6132
  if (!schedule) {
5973
6133
  throw notFound("Schedule", request.params.name);
5974
6134
  }
5975
- app.db.delete(schedules).where(eq14(schedules.id, schedule.id)).run();
6135
+ app.db.delete(schedules).where(eq15(schedules.id, schedule.id)).run();
5976
6136
  writeAuditLog(app.db, {
5977
6137
  projectId: project.id,
5978
6138
  actor: "api",
@@ -6002,7 +6162,7 @@ function formatSchedule(row) {
6002
6162
 
6003
6163
  // ../api-routes/src/notifications.ts
6004
6164
  import crypto12 from "crypto";
6005
- import { eq as eq15 } from "drizzle-orm";
6165
+ import { eq as eq16 } from "drizzle-orm";
6006
6166
  var VALID_EVENTS = ["citation.lost", "citation.gained", "run.completed", "run.failed", "insight.critical", "insight.high"];
6007
6167
  async function notificationRoutes(app) {
6008
6168
  app.get("/notifications/events", async (_request, reply) => {
@@ -6041,22 +6201,22 @@ async function notificationRoutes(app) {
6041
6201
  diff: { channel, ...redactNotificationUrl(url), events }
6042
6202
  });
6043
6203
  return reply.status(201).send({
6044
- ...formatNotification(app.db.select().from(notifications).where(eq15(notifications.id, id)).get()),
6204
+ ...formatNotification(app.db.select().from(notifications).where(eq16(notifications.id, id)).get()),
6045
6205
  webhookSecret
6046
6206
  });
6047
6207
  });
6048
6208
  app.get("/projects/:name/notifications", async (request, reply) => {
6049
6209
  const project = resolveProject(app.db, request.params.name);
6050
- const rows = app.db.select().from(notifications).where(eq15(notifications.projectId, project.id)).all();
6210
+ const rows = app.db.select().from(notifications).where(eq16(notifications.projectId, project.id)).all();
6051
6211
  return reply.send(rows.map(formatNotification));
6052
6212
  });
6053
6213
  app.delete("/projects/:name/notifications/:id", async (request, reply) => {
6054
6214
  const project = resolveProject(app.db, request.params.name);
6055
- const notification = app.db.select().from(notifications).where(eq15(notifications.id, request.params.id)).get();
6215
+ const notification = app.db.select().from(notifications).where(eq16(notifications.id, request.params.id)).get();
6056
6216
  if (!notification || notification.projectId !== project.id) {
6057
6217
  throw notFound("Notification", request.params.id);
6058
6218
  }
6059
- app.db.delete(notifications).where(eq15(notifications.id, notification.id)).run();
6219
+ app.db.delete(notifications).where(eq16(notifications.id, notification.id)).run();
6060
6220
  writeAuditLog(app.db, {
6061
6221
  projectId: project.id,
6062
6222
  actor: "api",
@@ -6068,7 +6228,7 @@ async function notificationRoutes(app) {
6068
6228
  });
6069
6229
  app.post("/projects/:name/notifications/:id/test", async (request, reply) => {
6070
6230
  const project = resolveProject(app.db, request.params.name);
6071
- const notification = app.db.select().from(notifications).where(eq15(notifications.id, request.params.id)).get();
6231
+ const notification = app.db.select().from(notifications).where(eq16(notifications.id, request.params.id)).get();
6072
6232
  if (!notification || notification.projectId !== project.id) {
6073
6233
  throw notFound("Notification", request.params.id);
6074
6234
  }
@@ -6121,7 +6281,7 @@ function formatNotification(row) {
6121
6281
 
6122
6282
  // ../api-routes/src/google.ts
6123
6283
  import crypto14 from "crypto";
6124
- import { eq as eq16, and as and5, desc as desc7, sql as sql4 } from "drizzle-orm";
6284
+ import { eq as eq17, and as and5, desc as desc7, sql as sql4 } from "drizzle-orm";
6125
6285
 
6126
6286
  // ../integration-google/src/constants.ts
6127
6287
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -7256,14 +7416,14 @@ async function googleRoutes(app, opts) {
7256
7416
  if (opts.onGscSyncRequested) {
7257
7417
  opts.onGscSyncRequested(runId, project.id, { days, full });
7258
7418
  }
7259
- const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
7419
+ const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
7260
7420
  return run;
7261
7421
  });
7262
7422
  app.get("/projects/:name/google/gsc/performance", async (request) => {
7263
7423
  const project = resolveProject(app.db, request.params.name);
7264
7424
  const { startDate, endDate, query, page, limit } = request.query;
7265
7425
  const cutoffDate = !startDate ? windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null : null;
7266
- const conditions = [eq16(gscSearchData.projectId, project.id)];
7426
+ const conditions = [eq17(gscSearchData.projectId, project.id)];
7267
7427
  if (startDate) conditions.push(sql4`${gscSearchData.date} >= ${startDate}`);
7268
7428
  else if (cutoffDate) conditions.push(sql4`${gscSearchData.date} >= ${cutoffDate}`);
7269
7429
  if (endDate) conditions.push(sql4`${gscSearchData.date} <= ${endDate}`);
@@ -7341,8 +7501,8 @@ async function googleRoutes(app, opts) {
7341
7501
  app.get("/projects/:name/google/gsc/inspections", async (request) => {
7342
7502
  const project = resolveProject(app.db, request.params.name);
7343
7503
  const { url, limit } = request.query;
7344
- const conditions = [eq16(gscUrlInspections.projectId, project.id)];
7345
- if (url) conditions.push(eq16(gscUrlInspections.url, url));
7504
+ const conditions = [eq17(gscUrlInspections.projectId, project.id)];
7505
+ if (url) conditions.push(eq17(gscUrlInspections.url, url));
7346
7506
  const rows = app.db.select().from(gscUrlInspections).where(and5(...conditions)).orderBy(desc7(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
7347
7507
  return rows.map((r) => ({
7348
7508
  id: r.id,
@@ -7362,7 +7522,7 @@ async function googleRoutes(app, opts) {
7362
7522
  });
7363
7523
  app.get("/projects/:name/google/gsc/deindexed", async (request) => {
7364
7524
  const project = resolveProject(app.db, request.params.name);
7365
- const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7525
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7366
7526
  const byUrl = /* @__PURE__ */ new Map();
7367
7527
  for (const row of allInspections) {
7368
7528
  const existing = byUrl.get(row.url);
@@ -7390,7 +7550,7 @@ async function googleRoutes(app, opts) {
7390
7550
  });
7391
7551
  app.get("/projects/:name/google/gsc/coverage", async (request) => {
7392
7552
  const project = resolveProject(app.db, request.params.name);
7393
- const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7553
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7394
7554
  const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
7395
7555
  const latestByUrl = /* @__PURE__ */ new Map();
7396
7556
  const historyByUrl = /* @__PURE__ */ new Map();
@@ -7487,7 +7647,7 @@ async function googleRoutes(app, opts) {
7487
7647
  const project = resolveProject(app.db, request.params.name);
7488
7648
  const parsed = parseInt(request.query.limit ?? "90", 10);
7489
7649
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
7490
- const rows = app.db.select().from(gscCoverageSnapshots).where(eq16(gscCoverageSnapshots.projectId, project.id)).orderBy(desc7(gscCoverageSnapshots.date)).limit(limit).all();
7650
+ const rows = app.db.select().from(gscCoverageSnapshots).where(eq17(gscCoverageSnapshots.projectId, project.id)).orderBy(desc7(gscCoverageSnapshots.date)).limit(limit).all();
7491
7651
  return rows.map((r) => ({
7492
7652
  date: r.date,
7493
7653
  indexed: r.indexed,
@@ -7547,7 +7707,7 @@ async function googleRoutes(app, opts) {
7547
7707
  if (opts.onInspectSitemapRequested) {
7548
7708
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl });
7549
7709
  }
7550
- const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
7710
+ const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
7551
7711
  return { sitemaps, primarySitemapUrl: sitemapUrl, run };
7552
7712
  });
7553
7713
  app.post("/projects/:name/google/gsc/inspect-sitemap", async (request) => {
@@ -7574,7 +7734,7 @@ async function googleRoutes(app, opts) {
7574
7734
  if (opts.onInspectSitemapRequested) {
7575
7735
  opts.onInspectSitemapRequested(runId, project.id, { sitemapUrl: sitemapUrl ?? void 0 });
7576
7736
  }
7577
- const run = app.db.select().from(runs).where(eq16(runs.id, runId)).get();
7737
+ const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
7578
7738
  return run;
7579
7739
  });
7580
7740
  app.put("/projects/:name/google/connections/:type/sitemap", async (request) => {
@@ -7621,7 +7781,7 @@ async function googleRoutes(app, opts) {
7621
7781
  const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
7622
7782
  let urlsToNotify = request.body?.urls ?? [];
7623
7783
  if (request.body?.allUnindexed) {
7624
- const allInspections = app.db.select().from(gscUrlInspections).where(eq16(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7784
+ const allInspections = app.db.select().from(gscUrlInspections).where(eq17(gscUrlInspections.projectId, project.id)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
7625
7785
  const latestByUrl = /* @__PURE__ */ new Map();
7626
7786
  for (const row of allInspections) {
7627
7787
  if (!latestByUrl.has(row.url)) {
@@ -7692,7 +7852,7 @@ async function googleRoutes(app, opts) {
7692
7852
 
7693
7853
  // ../api-routes/src/bing.ts
7694
7854
  import crypto15 from "crypto";
7695
- import { eq as eq17, and as and6, desc as desc8 } from "drizzle-orm";
7855
+ import { eq as eq18, and as and6, desc as desc8 } from "drizzle-orm";
7696
7856
 
7697
7857
  // ../integration-bing/src/constants.ts
7698
7858
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
@@ -8005,7 +8165,7 @@ async function bingRoutes(app, opts) {
8005
8165
  const store = requireConnectionStore();
8006
8166
  const project = resolveProject(app.db, request.params.name);
8007
8167
  requireConnection(store, project.canonicalDomain);
8008
- const allInspections = app.db.select().from(bingUrlInspections).where(eq17(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
8168
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq18(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
8009
8169
  const latestByUrl = /* @__PURE__ */ new Map();
8010
8170
  const definitiveByUrl = /* @__PURE__ */ new Map();
8011
8171
  for (const row of allInspections) {
@@ -8094,7 +8254,7 @@ async function bingRoutes(app, opts) {
8094
8254
  const project = resolveProject(app.db, request.params.name);
8095
8255
  const parsed = parseInt(request.query.limit ?? "90", 10);
8096
8256
  const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
8097
- const rows = app.db.select().from(bingCoverageSnapshots).where(eq17(bingCoverageSnapshots.projectId, project.id)).orderBy(desc8(bingCoverageSnapshots.date)).limit(limit).all();
8257
+ const rows = app.db.select().from(bingCoverageSnapshots).where(eq18(bingCoverageSnapshots.projectId, project.id)).orderBy(desc8(bingCoverageSnapshots.date)).limit(limit).all();
8098
8258
  return rows.map((r) => ({
8099
8259
  date: r.date,
8100
8260
  indexed: r.indexed,
@@ -8106,7 +8266,7 @@ async function bingRoutes(app, opts) {
8106
8266
  requireConnectionStore();
8107
8267
  const project = resolveProject(app.db, request.params.name);
8108
8268
  const { url, limit } = request.query;
8109
- const whereClause = url ? and6(eq17(bingUrlInspections.projectId, project.id), eq17(bingUrlInspections.url, url)) : eq17(bingUrlInspections.projectId, project.id);
8269
+ const whereClause = url ? and6(eq18(bingUrlInspections.projectId, project.id), eq18(bingUrlInspections.url, url)) : eq18(bingUrlInspections.projectId, project.id);
8110
8270
  const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc8(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
8111
8271
  return filtered.map((r) => ({
8112
8272
  id: r.id,
@@ -8196,7 +8356,7 @@ async function bingRoutes(app, opts) {
8196
8356
  anchorCount: result.AnchorCount ?? null,
8197
8357
  discoveryDate
8198
8358
  }).run();
8199
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq17(runs.id, runId)).run();
8359
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq18(runs.id, runId)).run();
8200
8360
  return {
8201
8361
  id,
8202
8362
  url,
@@ -8212,7 +8372,7 @@ async function bingRoutes(app, opts) {
8212
8372
  } catch (e) {
8213
8373
  const msg = e instanceof Error ? e.message : String(e);
8214
8374
  bingLog("error", "inspect-url.failed", { domain: project.canonicalDomain, url, error: msg });
8215
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq17(runs.id, runId)).run();
8375
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq18(runs.id, runId)).run();
8216
8376
  throw e;
8217
8377
  }
8218
8378
  });
@@ -8239,7 +8399,7 @@ async function bingRoutes(app, opts) {
8239
8399
  } else {
8240
8400
  bingLog("warn", "inspect-sitemap.no-callback", { domain: project.canonicalDomain, runId });
8241
8401
  }
8242
- const run = app.db.select().from(runs).where(eq17(runs.id, runId)).get();
8402
+ const run = app.db.select().from(runs).where(eq18(runs.id, runId)).get();
8243
8403
  return run;
8244
8404
  });
8245
8405
  app.post("/projects/:name/bing/request-indexing", async (request) => {
@@ -8251,7 +8411,7 @@ async function bingRoutes(app, opts) {
8251
8411
  }
8252
8412
  let urlsToSubmit = request.body?.urls ?? [];
8253
8413
  if (request.body?.allUnindexed) {
8254
- const allInspections = app.db.select().from(bingUrlInspections).where(eq17(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
8414
+ const allInspections = app.db.select().from(bingUrlInspections).where(eq18(bingUrlInspections.projectId, project.id)).orderBy(desc8(bingUrlInspections.inspectedAt)).all();
8255
8415
  const latestByUrl = /* @__PURE__ */ new Map();
8256
8416
  for (const row of allInspections) {
8257
8417
  if (!latestByUrl.has(row.url)) {
@@ -8338,14 +8498,14 @@ async function bingRoutes(app, opts) {
8338
8498
  import fs from "fs";
8339
8499
  import path from "path";
8340
8500
  import os from "os";
8341
- import { eq as eq18, and as and7 } from "drizzle-orm";
8501
+ import { eq as eq19, and as and7 } from "drizzle-orm";
8342
8502
  function getScreenshotDir() {
8343
8503
  return path.join(os.homedir(), ".canonry", "screenshots");
8344
8504
  }
8345
8505
  async function cdpRoutes(app, opts) {
8346
8506
  app.get("/screenshots/:snapshotId", async (request, reply) => {
8347
8507
  const { snapshotId } = request.params;
8348
- const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq18(querySnapshots.id, snapshotId)).get();
8508
+ const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq19(querySnapshots.id, snapshotId)).get();
8349
8509
  if (!snapshot?.screenshotPath) {
8350
8510
  const err = notFound("Screenshot", snapshotId);
8351
8511
  return reply.code(err.statusCode).send(err.toJSON());
@@ -8411,7 +8571,7 @@ async function cdpRoutes(app, opts) {
8411
8571
  async (request, reply) => {
8412
8572
  const project = resolveProject(app.db, request.params.name);
8413
8573
  const { runId } = request.params;
8414
- const run = app.db.select().from(runs).where(and7(eq18(runs.id, runId), eq18(runs.projectId, project.id))).get();
8574
+ const run = app.db.select().from(runs).where(and7(eq19(runs.id, runId), eq19(runs.projectId, project.id))).get();
8415
8575
  if (!run) {
8416
8576
  const err = notFound("Run", runId);
8417
8577
  return reply.code(err.statusCode).send(err.toJSON());
@@ -8424,8 +8584,8 @@ async function cdpRoutes(app, opts) {
8424
8584
  citedDomains: querySnapshots.citedDomains,
8425
8585
  screenshotPath: querySnapshots.screenshotPath,
8426
8586
  rawResponse: querySnapshots.rawResponse
8427
- }).from(querySnapshots).where(eq18(querySnapshots.runId, runId)).all();
8428
- const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq18(keywords.projectId, project.id)).all();
8587
+ }).from(querySnapshots).where(eq19(querySnapshots.runId, runId)).all();
8588
+ const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq19(keywords.projectId, project.id)).all();
8429
8589
  const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
8430
8590
  const byKeyword = /* @__PURE__ */ new Map();
8431
8591
  for (const snap of snapshots) {
@@ -8508,7 +8668,7 @@ async function cdpRoutes(app, opts) {
8508
8668
 
8509
8669
  // ../api-routes/src/ga.ts
8510
8670
  import crypto16 from "crypto";
8511
- import { eq as eq19, desc as desc9, and as and8, sql as sql5 } from "drizzle-orm";
8671
+ import { eq as eq20, desc as desc9, and as and8, sql as sql5 } from "drizzle-orm";
8512
8672
  function gaLog(level, action, ctx) {
8513
8673
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
8514
8674
  const stream = level === "error" ? process.stderr : process.stdout;
@@ -8665,10 +8825,10 @@ async function ga4Routes(app, opts) {
8665
8825
  if (!saConn && !oauthConn) {
8666
8826
  throw notFound("GA4 connection", project.name);
8667
8827
  }
8668
- app.db.delete(gaTrafficSnapshots).where(eq19(gaTrafficSnapshots.projectId, project.id)).run();
8669
- app.db.delete(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).run();
8670
- app.db.delete(gaAiReferrals).where(eq19(gaAiReferrals.projectId, project.id)).run();
8671
- app.db.delete(gaSocialReferrals).where(eq19(gaSocialReferrals.projectId, project.id)).run();
8828
+ app.db.delete(gaTrafficSnapshots).where(eq20(gaTrafficSnapshots.projectId, project.id)).run();
8829
+ app.db.delete(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).run();
8830
+ app.db.delete(gaAiReferrals).where(eq20(gaAiReferrals.projectId, project.id)).run();
8831
+ app.db.delete(gaSocialReferrals).where(eq20(gaSocialReferrals.projectId, project.id)).run();
8672
8832
  const propertyId = saConn?.propertyId ?? oauthConn?.propertyId ?? null;
8673
8833
  opts.ga4CredentialStore?.deleteConnection(project.name);
8674
8834
  opts.googleConnectionStore?.deleteConnection(project.canonicalDomain, "ga4");
@@ -8689,7 +8849,7 @@ async function ga4Routes(app, opts) {
8689
8849
  if (!connected) {
8690
8850
  return { connected: false, propertyId: null, clientEmail: null, authMethod: null, lastSyncedAt: null };
8691
8851
  }
8692
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
8852
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
8693
8853
  return {
8694
8854
  connected: true,
8695
8855
  propertyId: saConn?.propertyId ?? oauthConn?.propertyId ?? null,
@@ -8749,7 +8909,7 @@ async function ga4Routes(app, opts) {
8749
8909
  if (syncTraffic) {
8750
8910
  tx.delete(gaTrafficSnapshots).where(
8751
8911
  and8(
8752
- eq19(gaTrafficSnapshots.projectId, project.id),
8912
+ eq20(gaTrafficSnapshots.projectId, project.id),
8753
8913
  sql5`${gaTrafficSnapshots.date} >= ${summary.periodStart}`,
8754
8914
  sql5`${gaTrafficSnapshots.date} <= ${summary.periodEnd}`
8755
8915
  )
@@ -8773,7 +8933,7 @@ async function ga4Routes(app, opts) {
8773
8933
  if (syncAi) {
8774
8934
  tx.delete(gaAiReferrals).where(
8775
8935
  and8(
8776
- eq19(gaAiReferrals.projectId, project.id),
8936
+ eq20(gaAiReferrals.projectId, project.id),
8777
8937
  sql5`${gaAiReferrals.date} >= ${summary.periodStart}`,
8778
8938
  sql5`${gaAiReferrals.date} <= ${summary.periodEnd}`
8779
8939
  )
@@ -8796,7 +8956,7 @@ async function ga4Routes(app, opts) {
8796
8956
  if (syncSocial) {
8797
8957
  tx.delete(gaSocialReferrals).where(
8798
8958
  and8(
8799
- eq19(gaSocialReferrals.projectId, project.id),
8959
+ eq20(gaSocialReferrals.projectId, project.id),
8800
8960
  sql5`${gaSocialReferrals.date} >= ${summary.periodStart}`,
8801
8961
  sql5`${gaSocialReferrals.date} <= ${summary.periodEnd}`
8802
8962
  )
@@ -8817,7 +8977,7 @@ async function ga4Routes(app, opts) {
8817
8977
  }
8818
8978
  }
8819
8979
  if (syncSummary) {
8820
- tx.delete(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).run();
8980
+ tx.delete(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).run();
8821
8981
  tx.insert(gaTrafficSummaries).values({
8822
8982
  id: crypto16.randomUUID(),
8823
8983
  projectId: project.id,
@@ -8831,7 +8991,7 @@ async function ga4Routes(app, opts) {
8831
8991
  }).run();
8832
8992
  }
8833
8993
  });
8834
- app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq19(runs.id, runId)).run();
8994
+ app.db.update(runs).set({ status: RunStatuses.completed, finishedAt: now }).where(eq20(runs.id, runId)).run();
8835
8995
  const syncedComponents = only ? [only, ...only !== "social" && only !== "ai" && only !== "traffic" ? [] : []] : void 0;
8836
8996
  gaLog("info", "sync.complete", {
8837
8997
  projectId: project.id,
@@ -8855,7 +9015,7 @@ async function ga4Routes(app, opts) {
8855
9015
  } catch (e) {
8856
9016
  const msg = e instanceof Error ? e.message : String(e);
8857
9017
  gaLog("error", "sync.fetch-failed", { projectId: project.id, runId, error: msg });
8858
- app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq19(runs.id, runId)).run();
9018
+ app.db.update(runs).set({ status: RunStatuses.failed, error: msg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq20(runs.id, runId)).run();
8859
9019
  throw e;
8860
9020
  }
8861
9021
  });
@@ -8866,11 +9026,11 @@ async function ga4Routes(app, opts) {
8866
9026
  const window = parseWindow(request.query.window);
8867
9027
  const cutoff = windowCutoff(window);
8868
9028
  const cutoffDate = cutoff?.slice(0, 10) ?? null;
8869
- const snapshotConditions = [eq19(gaTrafficSnapshots.projectId, project.id)];
9029
+ const snapshotConditions = [eq20(gaTrafficSnapshots.projectId, project.id)];
8870
9030
  if (cutoffDate) snapshotConditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
8871
- const aiConditions = [eq19(gaAiReferrals.projectId, project.id)];
9031
+ const aiConditions = [eq20(gaAiReferrals.projectId, project.id)];
8872
9032
  if (cutoffDate) aiConditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
8873
- const socialConditions = [eq19(gaSocialReferrals.projectId, project.id)];
9033
+ const socialConditions = [eq20(gaSocialReferrals.projectId, project.id)];
8874
9034
  if (cutoffDate) socialConditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
8875
9035
  const summaryRow = cutoffDate ? app.db.select({
8876
9036
  totalSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)`,
@@ -8880,14 +9040,14 @@ async function ga4Routes(app, opts) {
8880
9040
  totalSessions: gaTrafficSummaries.totalSessions,
8881
9041
  totalOrganicSessions: gaTrafficSummaries.totalOrganicSessions,
8882
9042
  totalUsers: gaTrafficSummaries.totalUsers
8883
- }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
9043
+ }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).get();
8884
9044
  const directTotalRow = app.db.select({
8885
9045
  totalDirectSessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)`
8886
9046
  }).from(gaTrafficSnapshots).where(and8(...snapshotConditions)).get();
8887
9047
  const summaryMeta = app.db.select({
8888
9048
  periodStart: gaTrafficSummaries.periodStart,
8889
9049
  periodEnd: gaTrafficSummaries.periodEnd
8890
- }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).get();
9050
+ }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).get();
8891
9051
  const rows = app.db.select({
8892
9052
  landingPage: sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`,
8893
9053
  sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
@@ -8918,7 +9078,7 @@ async function ga4Routes(app, opts) {
8918
9078
  const aiBySession = app.db.select({
8919
9079
  sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
8920
9080
  users: sql5`COALESCE(SUM(${gaAiReferrals.users}), 0)`
8921
- }).from(gaAiReferrals).where(and8(...aiConditions, eq19(gaAiReferrals.sourceDimension, "session"))).get();
9081
+ }).from(gaAiReferrals).where(and8(...aiConditions, eq20(gaAiReferrals.sourceDimension, "session"))).get();
8922
9082
  const socialReferrals = app.db.select({
8923
9083
  source: gaSocialReferrals.source,
8924
9084
  medium: gaSocialReferrals.medium,
@@ -8930,7 +9090,7 @@ async function ga4Routes(app, opts) {
8930
9090
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`,
8931
9091
  users: sql5`SUM(${gaSocialReferrals.users})`
8932
9092
  }).from(gaSocialReferrals).where(and8(...socialConditions)).get();
8933
- const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq19(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
9093
+ const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq20(gaTrafficSummaries.projectId, project.id)).orderBy(desc9(gaTrafficSummaries.syncedAt)).limit(1).get();
8934
9094
  const total = summaryRow?.totalSessions ?? 0;
8935
9095
  const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
8936
9096
  return {
@@ -8984,7 +9144,7 @@ async function ga4Routes(app, opts) {
8984
9144
  const project = resolveProject(app.db, request.params.name);
8985
9145
  requireGa4Connection(opts, project.name, project.canonicalDomain);
8986
9146
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
8987
- const conditions = [eq19(gaAiReferrals.projectId, project.id)];
9147
+ const conditions = [eq20(gaAiReferrals.projectId, project.id)];
8988
9148
  if (cutoffDate) conditions.push(sql5`${gaAiReferrals.date} >= ${cutoffDate}`);
8989
9149
  const rows = app.db.select({
8990
9150
  date: gaAiReferrals.date,
@@ -9000,7 +9160,7 @@ async function ga4Routes(app, opts) {
9000
9160
  const project = resolveProject(app.db, request.params.name);
9001
9161
  requireGa4Connection(opts, project.name, project.canonicalDomain);
9002
9162
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
9003
- const conditions = [eq19(gaSocialReferrals.projectId, project.id)];
9163
+ const conditions = [eq20(gaSocialReferrals.projectId, project.id)];
9004
9164
  if (cutoffDate) conditions.push(sql5`${gaSocialReferrals.date} >= ${cutoffDate}`);
9005
9165
  const rows = app.db.select({
9006
9166
  date: gaSocialReferrals.date,
@@ -9023,7 +9183,7 @@ async function ga4Routes(app, opts) {
9023
9183
  return fmt(d);
9024
9184
  };
9025
9185
  const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(
9026
- eq19(gaSocialReferrals.projectId, project.id),
9186
+ eq20(gaSocialReferrals.projectId, project.id),
9027
9187
  sql5`${gaSocialReferrals.date} >= ${from}`,
9028
9188
  sql5`${gaSocialReferrals.date} < ${to}`
9029
9189
  )).get();
@@ -9036,7 +9196,7 @@ async function ga4Routes(app, opts) {
9036
9196
  source: gaSocialReferrals.source,
9037
9197
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`
9038
9198
  }).from(gaSocialReferrals).where(and8(
9039
- eq19(gaSocialReferrals.projectId, project.id),
9199
+ eq20(gaSocialReferrals.projectId, project.id),
9040
9200
  sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
9041
9201
  sql5`${gaSocialReferrals.date} < ${fmt(today)}`
9042
9202
  )).groupBy(gaSocialReferrals.source).all();
@@ -9044,7 +9204,7 @@ async function ga4Routes(app, opts) {
9044
9204
  source: gaSocialReferrals.source,
9045
9205
  sessions: sql5`SUM(${gaSocialReferrals.sessions})`
9046
9206
  }).from(gaSocialReferrals).where(and8(
9047
- eq19(gaSocialReferrals.projectId, project.id),
9207
+ eq20(gaSocialReferrals.projectId, project.id),
9048
9208
  sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
9049
9209
  sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`
9050
9210
  )).groupBy(gaSocialReferrals.source).all();
@@ -9085,16 +9245,16 @@ async function ga4Routes(app, opts) {
9085
9245
  return fmt(d);
9086
9246
  };
9087
9247
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
9088
- const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq19(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9089
- const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq19(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9090
- const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq19(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9248
+ const sumTotal = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.sessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9249
+ const sumOrganic = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.organicSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9250
+ const sumDirect = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaTrafficSnapshots.directSessions}), 0)` }).from(gaTrafficSnapshots).where(and8(eq20(gaTrafficSnapshots.projectId, project.id), sql5`${gaTrafficSnapshots.date} >= ${from}`, sql5`${gaTrafficSnapshots.date} < ${to}`)).get();
9091
9251
  const sumAi = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9092
- eq19(gaAiReferrals.projectId, project.id),
9252
+ eq20(gaAiReferrals.projectId, project.id),
9093
9253
  sql5`${gaAiReferrals.date} >= ${from}`,
9094
9254
  sql5`${gaAiReferrals.date} < ${to}`,
9095
- eq19(gaAiReferrals.sourceDimension, "session")
9255
+ eq20(gaAiReferrals.sourceDimension, "session")
9096
9256
  )).get();
9097
- const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(eq19(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
9257
+ const sumSocial = (from, to) => app.db.select({ sessions: sql5`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${from}`, sql5`${gaSocialReferrals.date} < ${to}`)).get();
9098
9258
  const todayStr = fmt(today);
9099
9259
  const buildTrend = (sum) => {
9100
9260
  const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
@@ -9104,16 +9264,16 @@ async function ga4Routes(app, opts) {
9104
9264
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
9105
9265
  };
9106
9266
  const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9107
- eq19(gaAiReferrals.projectId, project.id),
9267
+ eq20(gaAiReferrals.projectId, project.id),
9108
9268
  sql5`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
9109
9269
  sql5`${gaAiReferrals.date} < ${todayStr}`,
9110
- eq19(gaAiReferrals.sourceDimension, "session")
9270
+ eq20(gaAiReferrals.sourceDimension, "session")
9111
9271
  )).groupBy(gaAiReferrals.source).all();
9112
9272
  const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and8(
9113
- eq19(gaAiReferrals.projectId, project.id),
9273
+ eq20(gaAiReferrals.projectId, project.id),
9114
9274
  sql5`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
9115
9275
  sql5`${gaAiReferrals.date} < ${daysAgo2(7)}`,
9116
- eq19(gaAiReferrals.sourceDimension, "session")
9276
+ eq20(gaAiReferrals.sourceDimension, "session")
9117
9277
  )).groupBy(gaAiReferrals.source).all();
9118
9278
  const findBiggestMover = (current, prev) => {
9119
9279
  const prevMap = new Map(prev.map((r) => [r.source, r.sessions]));
@@ -9129,8 +9289,8 @@ async function ga4Routes(app, opts) {
9129
9289
  }
9130
9290
  return mover;
9131
9291
  };
9132
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq19(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql5`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
9133
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq19(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
9292
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql5`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
9293
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql5`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and8(eq20(gaSocialReferrals.projectId, project.id), sql5`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql5`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
9134
9294
  return {
9135
9295
  total: buildTrend(sumTotal),
9136
9296
  organic: buildTrend(sumOrganic),
@@ -9145,7 +9305,7 @@ async function ga4Routes(app, opts) {
9145
9305
  const project = resolveProject(app.db, request.params.name);
9146
9306
  requireGa4Connection(opts, project.name, project.canonicalDomain);
9147
9307
  const cutoffDate = windowCutoff(parseWindow(request.query.window))?.slice(0, 10) ?? null;
9148
- const conditions = [eq19(gaTrafficSnapshots.projectId, project.id)];
9308
+ const conditions = [eq20(gaTrafficSnapshots.projectId, project.id)];
9149
9309
  if (cutoffDate) conditions.push(sql5`${gaTrafficSnapshots.date} >= ${cutoffDate}`);
9150
9310
  const rows = app.db.select({
9151
9311
  date: gaTrafficSnapshots.date,
@@ -9168,7 +9328,7 @@ async function ga4Routes(app, opts) {
9168
9328
  sessions: sql5`SUM(${gaTrafficSnapshots.sessions})`,
9169
9329
  organicSessions: sql5`SUM(${gaTrafficSnapshots.organicSessions})`,
9170
9330
  users: sql5`SUM(${gaTrafficSnapshots.users})`
9171
- }).from(gaTrafficSnapshots).where(eq19(gaTrafficSnapshots.projectId, project.id)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
9331
+ }).from(gaTrafficSnapshots).where(eq20(gaTrafficSnapshots.projectId, project.id)).groupBy(sql5`COALESCE(${gaTrafficSnapshots.landingPageNormalized}, ${gaTrafficSnapshots.landingPage})`).orderBy(sql5`SUM(${gaTrafficSnapshots.sessions}) DESC`).all();
9172
9332
  return {
9173
9333
  pages: trafficPages.map((r) => ({
9174
9334
  landingPage: r.landingPage,
@@ -10805,7 +10965,7 @@ async function wordpressRoutes(app, opts) {
10805
10965
 
10806
10966
  // ../api-routes/src/backlinks.ts
10807
10967
  import crypto18 from "crypto";
10808
- import { and as and9, asc as asc2, desc as desc10, eq as eq20, sql as sql6 } from "drizzle-orm";
10968
+ import { and as and9, asc as asc2, desc as desc10, eq as eq21, sql as sql6 } from "drizzle-orm";
10809
10969
 
10810
10970
  // ../integration-commoncrawl/src/constants.ts
10811
10971
  import os2 from "os";
@@ -11257,7 +11417,7 @@ function mapRunRow(row) {
11257
11417
  };
11258
11418
  }
11259
11419
  function latestSummaryForProject(db, projectId, release) {
11260
- const condition = release ? and9(eq20(backlinkSummaries.projectId, projectId), eq20(backlinkSummaries.release, release)) : eq20(backlinkSummaries.projectId, projectId);
11420
+ const condition = release ? and9(eq21(backlinkSummaries.projectId, projectId), eq21(backlinkSummaries.release, release)) : eq21(backlinkSummaries.projectId, projectId);
11261
11421
  return db.select().from(backlinkSummaries).where(condition).orderBy(desc10(backlinkSummaries.queriedAt)).limit(1).get();
11262
11422
  }
11263
11423
  async function backlinksRoutes(app, opts) {
@@ -11301,7 +11461,7 @@ async function backlinksRoutes(app, opts) {
11301
11461
  "@duckdb/node-api is not installed. Run `canonry backlinks install` to enable the backlinks feature."
11302
11462
  );
11303
11463
  }
11304
- const existing = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.release, release)).get();
11464
+ const existing = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.release, release)).get();
11305
11465
  const now = (/* @__PURE__ */ new Date()).toISOString();
11306
11466
  if (existing) {
11307
11467
  if (NON_TERMINAL_SYNC_STATUSES.has(existing.status)) {
@@ -11312,9 +11472,9 @@ async function backlinksRoutes(app, opts) {
11312
11472
  phaseDetail: null,
11313
11473
  error: null,
11314
11474
  updatedAt: now
11315
- }).where(eq20(ccReleaseSyncs.id, existing.id)).run();
11475
+ }).where(eq21(ccReleaseSyncs.id, existing.id)).run();
11316
11476
  opts.onReleaseSyncRequested(existing.id, release);
11317
- const refreshed = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.id, existing.id)).get();
11477
+ const refreshed = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.id, existing.id)).get();
11318
11478
  return reply.status(200).send(mapSyncRow(refreshed));
11319
11479
  }
11320
11480
  const id = crypto18.randomUUID();
@@ -11326,7 +11486,7 @@ async function backlinksRoutes(app, opts) {
11326
11486
  updatedAt: now
11327
11487
  }).run();
11328
11488
  opts.onReleaseSyncRequested(id, release);
11329
- const inserted = app.db.select().from(ccReleaseSyncs).where(eq20(ccReleaseSyncs.id, id)).get();
11489
+ const inserted = app.db.select().from(ccReleaseSyncs).where(eq21(ccReleaseSyncs.id, id)).get();
11330
11490
  return reply.status(201).send(mapSyncRow(inserted));
11331
11491
  });
11332
11492
  app.get("/backlinks/syncs/latest", async (_request, reply) => {
@@ -11384,7 +11544,7 @@ async function backlinksRoutes(app, opts) {
11384
11544
  createdAt: now
11385
11545
  }).run();
11386
11546
  opts.onBacklinkExtractRequested(runId, project.id, release);
11387
- const run = app.db.select().from(runs).where(eq20(runs.id, runId)).get();
11547
+ const run = app.db.select().from(runs).where(eq21(runs.id, runId)).get();
11388
11548
  return reply.status(201).send(mapRunRow(run));
11389
11549
  });
11390
11550
  app.get(
@@ -11406,8 +11566,8 @@ async function backlinksRoutes(app, opts) {
11406
11566
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
11407
11567
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
11408
11568
  const domainCondition = and9(
11409
- eq20(backlinkDomains.projectId, project.id),
11410
- eq20(backlinkDomains.release, targetRelease)
11569
+ eq21(backlinkDomains.projectId, project.id),
11570
+ eq21(backlinkDomains.release, targetRelease)
11411
11571
  );
11412
11572
  const totalRow = app.db.select({ count: sql6`count(*)` }).from(backlinkDomains).where(domainCondition).get();
11413
11573
  const rows = app.db.select({
@@ -11425,7 +11585,7 @@ async function backlinksRoutes(app, opts) {
11425
11585
  "/projects/:name/backlinks/history",
11426
11586
  async (request, reply) => {
11427
11587
  const project = resolveProject(app.db, request.params.name);
11428
- const rows = app.db.select().from(backlinkSummaries).where(eq20(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
11588
+ const rows = app.db.select().from(backlinkSummaries).where(eq21(backlinkSummaries.projectId, project.id)).orderBy(asc2(backlinkSummaries.queriedAt)).all();
11429
11589
  const response = rows.map((r) => ({
11430
11590
  release: r.release,
11431
11591
  totalLinkingDomains: r.totalLinkingDomains,
@@ -11438,6 +11598,152 @@ async function backlinksRoutes(app, opts) {
11438
11598
  );
11439
11599
  }
11440
11600
 
11601
+ // ../api-routes/src/doctor/checks/bing-auth.ts
11602
+ var BING_AUTH_CHECKS = [
11603
+ {
11604
+ id: "bing.auth.connection",
11605
+ category: CheckCategories.auth,
11606
+ scope: CheckScopes.project,
11607
+ title: "Bing WMT connection",
11608
+ run: async (ctx) => {
11609
+ if (!ctx.project) {
11610
+ return {
11611
+ status: CheckStatuses.skipped,
11612
+ code: "bing.auth.no-project",
11613
+ summary: "Project context required.",
11614
+ remediation: null
11615
+ };
11616
+ }
11617
+ const store = ctx.bingConnectionStore;
11618
+ if (!store) {
11619
+ return {
11620
+ status: CheckStatuses.skipped,
11621
+ code: "bing.auth.store-unavailable",
11622
+ summary: "Bing connection store is not configured for this deployment.",
11623
+ remediation: null
11624
+ };
11625
+ }
11626
+ const conn = store.getConnection(ctx.project.canonicalDomain);
11627
+ if (!conn) {
11628
+ return {
11629
+ status: CheckStatuses.fail,
11630
+ code: "bing.auth.no-connection",
11631
+ summary: `No Bing connection for ${ctx.project.canonicalDomain}.`,
11632
+ remediation: `Run \`canonry bing connect ${ctx.project.name} --api-key <key>\` to authorize.`
11633
+ };
11634
+ }
11635
+ if (!conn.apiKey) {
11636
+ return {
11637
+ status: CheckStatuses.fail,
11638
+ code: "bing.auth.no-api-key",
11639
+ summary: "Bing connection exists but has no API key stored.",
11640
+ remediation: `Run \`canonry bing connect ${ctx.project.name} --api-key <key>\` to re-authorize.`
11641
+ };
11642
+ }
11643
+ try {
11644
+ await getSites(conn.apiKey);
11645
+ return {
11646
+ status: CheckStatuses.ok,
11647
+ code: "bing.auth.connected",
11648
+ summary: "Bing API key is valid and can list sites.",
11649
+ remediation: null
11650
+ };
11651
+ } catch (err) {
11652
+ const message = err instanceof Error ? err.message : String(err);
11653
+ return {
11654
+ status: CheckStatuses.fail,
11655
+ code: "bing.auth.verification-failed",
11656
+ summary: "Bing API key verification failed.",
11657
+ remediation: "Verify your Bing API key is correct and active in Bing Webmaster Tools.",
11658
+ details: { error: message }
11659
+ };
11660
+ }
11661
+ }
11662
+ },
11663
+ {
11664
+ id: "bing.auth.site-access",
11665
+ category: CheckCategories.auth,
11666
+ scope: CheckScopes.project,
11667
+ title: "Bing site access",
11668
+ run: async (ctx) => {
11669
+ if (!ctx.project) {
11670
+ return {
11671
+ status: CheckStatuses.skipped,
11672
+ code: "bing.auth.no-project",
11673
+ summary: "Project context required.",
11674
+ remediation: null
11675
+ };
11676
+ }
11677
+ const store = ctx.bingConnectionStore;
11678
+ if (!store) {
11679
+ return {
11680
+ status: CheckStatuses.skipped,
11681
+ code: "bing.auth.store-unavailable",
11682
+ summary: "Bing connection store is not configured.",
11683
+ remediation: null
11684
+ };
11685
+ }
11686
+ const conn = store.getConnection(ctx.project.canonicalDomain);
11687
+ if (!conn || !conn.apiKey) {
11688
+ return {
11689
+ status: CheckStatuses.skipped,
11690
+ code: "bing.auth.no-connection",
11691
+ summary: "Skipped \u2014 no Bing connection (see bing.auth.connection).",
11692
+ remediation: null
11693
+ };
11694
+ }
11695
+ if (!conn.siteUrl) {
11696
+ return {
11697
+ status: CheckStatuses.fail,
11698
+ code: "bing.auth.no-site-selected",
11699
+ summary: "Bing connection has no site URL selected.",
11700
+ remediation: `Run \`canonry bing sites ${ctx.project.name}\` to see available sites, then \`canonry bing set-site ${ctx.project.name} <url>\`.`
11701
+ };
11702
+ }
11703
+ try {
11704
+ const sites = await getSites(conn.apiKey);
11705
+ const match = sites.find((s) => s.Url === conn.siteUrl);
11706
+ if (!match) {
11707
+ return {
11708
+ status: CheckStatuses.fail,
11709
+ code: "bing.auth.site-not-found",
11710
+ summary: `Configured site "${conn.siteUrl}" is not in the authorized account's site list.`,
11711
+ remediation: `Add and verify "${conn.siteUrl}" in Bing Webmaster Tools, or pick an existing site using \`canonry bing set-site ${ctx.project.name}\`.`,
11712
+ details: {
11713
+ configuredSite: conn.siteUrl,
11714
+ availableSites: sites.map((s) => s.Url)
11715
+ }
11716
+ };
11717
+ }
11718
+ if (!match.Verified) {
11719
+ return {
11720
+ status: CheckStatuses.fail,
11721
+ code: "bing.auth.site-not-verified",
11722
+ summary: `Site "${conn.siteUrl}" is registered but not verified in Bing.`,
11723
+ remediation: "Complete site verification in Bing Webmaster Tools (DNS, HTML file, or Meta tag).",
11724
+ details: { siteUrl: conn.siteUrl }
11725
+ };
11726
+ }
11727
+ return {
11728
+ status: CheckStatuses.ok,
11729
+ code: "bing.auth.site-verified",
11730
+ summary: `Site "${conn.siteUrl}" is verified and accessible.`,
11731
+ remediation: null
11732
+ };
11733
+ } catch (err) {
11734
+ const message = err instanceof Error ? err.message : String(err);
11735
+ return {
11736
+ status: CheckStatuses.fail,
11737
+ code: "bing.auth.site-check-failed",
11738
+ summary: "Failed to verify Bing site access.",
11739
+ remediation: "Check Bing Webmaster Tools availability.",
11740
+ details: { error: message }
11741
+ };
11742
+ }
11743
+ }
11744
+ }
11745
+ ];
11746
+
11441
11747
  // ../api-routes/src/doctor/checks/ga-auth.ts
11442
11748
  async function checkServiceAccount(conn) {
11443
11749
  if (!conn.propertyId) {
@@ -11915,6 +12221,7 @@ var PROVIDERS_CHECKS = [providersConfiguredCheck];
11915
12221
  // ../api-routes/src/doctor/registry.ts
11916
12222
  var ALL_CHECKS = [
11917
12223
  ...GOOGLE_AUTH_CHECKS,
12224
+ ...BING_AUTH_CHECKS,
11918
12225
  ...GA_AUTH_CHECKS,
11919
12226
  ...PROVIDERS_CHECKS
11920
12227
  ];
@@ -11999,6 +12306,7 @@ async function doctorRoutes(app, opts) {
11999
12306
  db: app.db,
12000
12307
  project: null,
12001
12308
  googleConnectionStore: opts.googleConnectionStore,
12309
+ bingConnectionStore: opts.bingConnectionStore,
12002
12310
  ga4CredentialStore: opts.ga4CredentialStore,
12003
12311
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
12004
12312
  redirectUri,
@@ -12018,6 +12326,7 @@ async function doctorRoutes(app, opts) {
12018
12326
  displayName: project.displayName
12019
12327
  },
12020
12328
  googleConnectionStore: opts.googleConnectionStore,
12329
+ bingConnectionStore: opts.bingConnectionStore,
12021
12330
  ga4CredentialStore: opts.ga4CredentialStore,
12022
12331
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
12023
12332
  redirectUri,
@@ -12087,6 +12396,7 @@ async function apiRoutes(app, opts) {
12087
12396
  await api.register(historyRoutes);
12088
12397
  await api.register(analyticsRoutes);
12089
12398
  await api.register(intelligenceRoutes);
12399
+ await api.register(citationRoutes);
12090
12400
  await api.register(compositeRoutes);
12091
12401
  await api.register(contentRoutes);
12092
12402
  await api.register(settingsRoutes, {
@@ -12149,6 +12459,7 @@ async function apiRoutes(app, opts) {
12149
12459
  });
12150
12460
  await api.register(doctorRoutes, {
12151
12461
  googleConnectionStore: opts.googleConnectionStore,
12462
+ bingConnectionStore: opts.bingConnectionStore,
12152
12463
  ga4CredentialStore: opts.ga4CredentialStore,
12153
12464
  getGoogleAuthConfig: opts.getGoogleAuthConfig,
12154
12465
  publicUrl: opts.publicUrl,
@@ -14618,7 +14929,7 @@ import crypto19 from "crypto";
14618
14929
  import fs7 from "fs";
14619
14930
  import path9 from "path";
14620
14931
  import os4 from "os";
14621
- import { and as and10, eq as eq21, inArray as inArray4, sql as sql7 } from "drizzle-orm";
14932
+ import { and as and10, eq as eq22, inArray as inArray5, sql as sql7 } from "drizzle-orm";
14622
14933
 
14623
14934
  // src/citation-utils.ts
14624
14935
  function domainMatches(domain, canonicalDomain) {
@@ -14870,11 +15181,11 @@ var JobRunner = class {
14870
15181
  this.registry = registry;
14871
15182
  }
14872
15183
  recoverStaleRuns() {
14873
- const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray4(runs.status, ["running", "queued"])).all();
15184
+ const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray5(runs.status, ["running", "queued"])).all();
14874
15185
  if (stale.length === 0) return;
14875
15186
  const now = (/* @__PURE__ */ new Date()).toISOString();
14876
15187
  for (const run of stale) {
14877
- this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq21(runs.id, run.id)).run();
15188
+ this.db.update(runs).set({ status: "failed", finishedAt: now, error: "Server restarted while run was in progress" }).where(eq22(runs.id, run.id)).run();
14878
15189
  log.warn("run.recovered-stale", { runId: run.id, previousStatus: run.status });
14879
15190
  }
14880
15191
  }
@@ -14902,10 +15213,10 @@ var JobRunner = class {
14902
15213
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
14903
15214
  }
14904
15215
  if (existingRun.status === "queued") {
14905
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and10(eq21(runs.id, runId), eq21(runs.status, "queued"))).run();
15216
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and10(eq22(runs.id, runId), eq22(runs.status, "queued"))).run();
14906
15217
  }
14907
15218
  this.throwIfRunCancelled(runId);
14908
- const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
15219
+ const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
14909
15220
  if (!project) {
14910
15221
  throw new Error(`Project ${projectId} not found`);
14911
15222
  }
@@ -14925,8 +15236,8 @@ var JobRunner = class {
14925
15236
  throw new Error("No providers configured. Add at least one provider API key.");
14926
15237
  }
14927
15238
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
14928
- projectKeywords = this.db.select().from(keywords).where(eq21(keywords.projectId, projectId)).all();
14929
- const projectCompetitors = this.db.select().from(competitors).where(eq21(competitors.projectId, projectId)).all();
15239
+ projectKeywords = this.db.select().from(keywords).where(eq22(keywords.projectId, projectId)).all();
15240
+ const projectCompetitors = this.db.select().from(competitors).where(eq22(competitors.projectId, projectId)).all();
14930
15241
  const competitorDomains = projectCompetitors.map((c) => c.domain);
14931
15242
  const allDomains = effectiveDomains({
14932
15243
  canonicalDomain: project.canonicalDomain,
@@ -14942,7 +15253,7 @@ var JobRunner = class {
14942
15253
  const todayPeriod = getCurrentUsageDay();
14943
15254
  for (const p of activeProviders) {
14944
15255
  const providerScope = `${projectId}:${p.adapter.name}`;
14945
- const providerUsage = this.db.select().from(usageCounters).where(eq21(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
15256
+ const providerUsage = this.db.select().from(usageCounters).where(eq22(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
14946
15257
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
14947
15258
  if (providerUsage + queriesPerProvider > limit) {
14948
15259
  throw new Error(
@@ -15083,12 +15394,12 @@ var JobRunner = class {
15083
15394
  const someFailed = providerErrors.size > 0;
15084
15395
  if (allFailed) {
15085
15396
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
15086
- this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq21(runs.id, runId)).run();
15397
+ this.db.update(runs).set({ status: "failed", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq22(runs.id, runId)).run();
15087
15398
  } else if (someFailed) {
15088
15399
  const errorDetail = serializeRunError(buildRunErrorFromMessages(providerErrors));
15089
- this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq21(runs.id, runId)).run();
15400
+ this.db.update(runs).set({ status: "partial", finishedAt: (/* @__PURE__ */ new Date()).toISOString(), error: errorDetail }).where(eq22(runs.id, runId)).run();
15090
15401
  } else {
15091
- this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq21(runs.id, runId)).run();
15402
+ this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
15092
15403
  }
15093
15404
  this.flushProviderUsage(projectId, providerDispatchCounts);
15094
15405
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
@@ -15123,7 +15434,7 @@ var JobRunner = class {
15123
15434
  status: "failed",
15124
15435
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
15125
15436
  error: errorMessage
15126
- }).where(eq21(runs.id, runId)).run();
15437
+ }).where(eq22(runs.id, runId)).run();
15127
15438
  this.flushProviderUsage(projectId, providerDispatchCounts);
15128
15439
  trackEvent("run.completed", {
15129
15440
  status: "failed",
@@ -15166,7 +15477,7 @@ var JobRunner = class {
15166
15477
  status: runs.status,
15167
15478
  finishedAt: runs.finishedAt,
15168
15479
  error: runs.error
15169
- }).from(runs).where(eq21(runs.id, runId)).get();
15480
+ }).from(runs).where(eq22(runs.id, runId)).get();
15170
15481
  }
15171
15482
  isRunCancelled(runId) {
15172
15483
  return this.getRunState(runId)?.status === "cancelled";
@@ -15182,7 +15493,7 @@ var JobRunner = class {
15182
15493
  this.db.update(runs).set({
15183
15494
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
15184
15495
  error: currentRun.error ?? "Cancelled by user"
15185
- }).where(eq21(runs.id, runId)).run();
15496
+ }).where(eq22(runs.id, runId)).run();
15186
15497
  }
15187
15498
  trackEvent("run.completed", {
15188
15499
  status: "cancelled",
@@ -15205,7 +15516,7 @@ function getCurrentUsageDay() {
15205
15516
 
15206
15517
  // src/gsc-sync.ts
15207
15518
  import crypto20 from "crypto";
15208
- import { eq as eq22, and as and11, sql as sql8 } from "drizzle-orm";
15519
+ import { eq as eq23, and as and11, sql as sql8 } from "drizzle-orm";
15209
15520
  var log2 = createLogger("GscSync");
15210
15521
  function formatDate2(d) {
15211
15522
  return d.toISOString().split("T")[0];
@@ -15217,13 +15528,13 @@ function daysAgo(n) {
15217
15528
  }
15218
15529
  async function executeGscSync(db, runId, projectId, opts) {
15219
15530
  const now = (/* @__PURE__ */ new Date()).toISOString();
15220
- db.update(runs).set({ status: "running", startedAt: now }).where(eq22(runs.id, runId)).run();
15531
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq23(runs.id, runId)).run();
15221
15532
  try {
15222
15533
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
15223
15534
  if (!googleClientId || !googleClientSecret) {
15224
15535
  throw new Error("Google OAuth is not configured in the local Canonry config");
15225
15536
  }
15226
- const project = db.select().from(projects).where(eq22(projects.id, projectId)).get();
15537
+ const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
15227
15538
  if (!project) {
15228
15539
  throw new Error(`Project not found: ${projectId}`);
15229
15540
  }
@@ -15258,7 +15569,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15258
15569
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
15259
15570
  db.delete(gscSearchData).where(
15260
15571
  and11(
15261
- eq22(gscSearchData.projectId, projectId),
15572
+ eq23(gscSearchData.projectId, projectId),
15262
15573
  sql8`${gscSearchData.date} >= ${startDate}`,
15263
15574
  sql8`${gscSearchData.date} <= ${endDate}`
15264
15575
  )
@@ -15325,7 +15636,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15325
15636
  log2.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
15326
15637
  }
15327
15638
  }
15328
- const allInspections = db.select().from(gscUrlInspections).where(eq22(gscUrlInspections.projectId, projectId)).all();
15639
+ const allInspections = db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, projectId)).all();
15329
15640
  const latestByUrl = /* @__PURE__ */ new Map();
15330
15641
  for (const row of allInspections) {
15331
15642
  const existing = latestByUrl.get(row.url);
@@ -15346,7 +15657,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15346
15657
  }
15347
15658
  }
15348
15659
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
15349
- db.delete(gscCoverageSnapshots).where(and11(eq22(gscCoverageSnapshots.projectId, projectId), eq22(gscCoverageSnapshots.date, snapshotDate))).run();
15660
+ db.delete(gscCoverageSnapshots).where(and11(eq23(gscCoverageSnapshots.projectId, projectId), eq23(gscCoverageSnapshots.date, snapshotDate))).run();
15350
15661
  db.insert(gscCoverageSnapshots).values({
15351
15662
  id: crypto20.randomUUID(),
15352
15663
  projectId,
@@ -15357,11 +15668,11 @@ async function executeGscSync(db, runId, projectId, opts) {
15357
15668
  reasonBreakdown: JSON.stringify(reasonCounts),
15358
15669
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
15359
15670
  }).run();
15360
- db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
15671
+ db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
15361
15672
  log2.info("sync.completed", { runId, projectId, searchDataRows: rows.length, urlInspections: topPages.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
15362
15673
  } catch (err) {
15363
15674
  const errorMsg = err instanceof Error ? err.message : String(err);
15364
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq22(runs.id, runId)).run();
15675
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
15365
15676
  log2.error("sync.failed", { runId, projectId, error: errorMsg });
15366
15677
  throw err;
15367
15678
  }
@@ -15369,7 +15680,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15369
15680
 
15370
15681
  // src/gsc-inspect-sitemap.ts
15371
15682
  import crypto21 from "crypto";
15372
- import { eq as eq23, and as and12 } from "drizzle-orm";
15683
+ import { eq as eq24, and as and12 } from "drizzle-orm";
15373
15684
 
15374
15685
  // src/sitemap-parser.ts
15375
15686
  var log3 = createLogger("SitemapParser");
@@ -15490,13 +15801,13 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
15490
15801
  var log4 = createLogger("InspectSitemap");
15491
15802
  async function executeInspectSitemap(db, runId, projectId, opts) {
15492
15803
  const now = (/* @__PURE__ */ new Date()).toISOString();
15493
- db.update(runs).set({ status: "running", startedAt: now }).where(eq23(runs.id, runId)).run();
15804
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq24(runs.id, runId)).run();
15494
15805
  try {
15495
15806
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
15496
15807
  if (!googleClientId || !googleClientSecret) {
15497
15808
  throw new Error("Google OAuth is not configured in the local Canonry config");
15498
15809
  }
15499
- const project = db.select().from(projects).where(eq23(projects.id, projectId)).get();
15810
+ const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
15500
15811
  if (!project) {
15501
15812
  throw new Error(`Project not found: ${projectId}`);
15502
15813
  }
@@ -15564,7 +15875,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
15564
15875
  await new Promise((r) => setTimeout(r, 1e3));
15565
15876
  }
15566
15877
  }
15567
- const allInspections = db.select().from(gscUrlInspections).where(eq23(gscUrlInspections.projectId, projectId)).all();
15878
+ const allInspections = db.select().from(gscUrlInspections).where(eq24(gscUrlInspections.projectId, projectId)).all();
15568
15879
  const latestByUrl = /* @__PURE__ */ new Map();
15569
15880
  for (const row of allInspections) {
15570
15881
  const existing = latestByUrl.get(row.url);
@@ -15585,7 +15896,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
15585
15896
  }
15586
15897
  }
15587
15898
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
15588
- db.delete(gscCoverageSnapshots).where(and12(eq23(gscCoverageSnapshots.projectId, projectId), eq23(gscCoverageSnapshots.date, snapshotDate))).run();
15899
+ db.delete(gscCoverageSnapshots).where(and12(eq24(gscCoverageSnapshots.projectId, projectId), eq24(gscCoverageSnapshots.date, snapshotDate))).run();
15589
15900
  db.insert(gscCoverageSnapshots).values({
15590
15901
  id: crypto21.randomUUID(),
15591
15902
  projectId,
@@ -15597,11 +15908,11 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
15597
15908
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
15598
15909
  }).run();
15599
15910
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
15600
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
15911
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
15601
15912
  log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
15602
15913
  } catch (err) {
15603
15914
  const errorMsg = err instanceof Error ? err.message : String(err);
15604
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq23(runs.id, runId)).run();
15915
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
15605
15916
  log4.error("inspect.failed", { runId, projectId, error: errorMsg });
15606
15917
  throw err;
15607
15918
  }
@@ -15609,7 +15920,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
15609
15920
 
15610
15921
  // src/bing-inspect-sitemap.ts
15611
15922
  import crypto22 from "crypto";
15612
- import { eq as eq24, desc as desc11 } from "drizzle-orm";
15923
+ import { eq as eq25, desc as desc11 } from "drizzle-orm";
15613
15924
  var log5 = createLogger("BingInspectSitemap");
15614
15925
  function parseBingDate2(value) {
15615
15926
  if (!value) return null;
@@ -15627,9 +15938,9 @@ function isBlockingIssueType2(issueType) {
15627
15938
  }
15628
15939
  async function executeBingInspectSitemap(db, runId, projectId, opts) {
15629
15940
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
15630
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq24(runs.id, runId)).run();
15941
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq25(runs.id, runId)).run();
15631
15942
  try {
15632
- const project = db.select().from(projects).where(eq24(projects.id, projectId)).get();
15943
+ const project = db.select().from(projects).where(eq25(projects.id, projectId)).get();
15633
15944
  if (!project) {
15634
15945
  throw new Error(`Project not found: ${projectId}`);
15635
15946
  }
@@ -15647,7 +15958,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
15647
15958
  if (sitemapUrls.length === 0) {
15648
15959
  throw new Error("No URLs found in sitemap");
15649
15960
  }
15650
- const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, projectId)).all();
15961
+ const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, projectId)).all();
15651
15962
  const trackedUrls = new Set(trackedRows.map((r) => r.url));
15652
15963
  const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
15653
15964
  log5.info("sitemap.diff", {
@@ -15730,7 +16041,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
15730
16041
  await new Promise((r) => setTimeout(r, 1e3));
15731
16042
  }
15732
16043
  }
15733
- const allInspections = db.select().from(bingUrlInspections).where(eq24(bingUrlInspections.projectId, projectId)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
16044
+ const allInspections = db.select().from(bingUrlInspections).where(eq25(bingUrlInspections.projectId, projectId)).orderBy(desc11(bingUrlInspections.inspectedAt)).all();
15734
16045
  const latestByUrl = /* @__PURE__ */ new Map();
15735
16046
  const definitiveByUrl = /* @__PURE__ */ new Map();
15736
16047
  for (const row of allInspections) {
@@ -15773,7 +16084,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
15773
16084
  }
15774
16085
  }).run();
15775
16086
  const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
15776
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
16087
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
15777
16088
  log5.info("inspect.completed", {
15778
16089
  runId,
15779
16090
  projectId,
@@ -15787,7 +16098,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
15787
16098
  });
15788
16099
  } catch (err) {
15789
16100
  const errorMsg = err instanceof Error ? err.message : String(err);
15790
- db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq24(runs.id, runId)).run();
16101
+ db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(runs.id, runId)).run();
15791
16102
  log5.error("inspect.failed", { runId, projectId, error: errorMsg });
15792
16103
  throw err;
15793
16104
  }
@@ -15796,7 +16107,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
15796
16107
  // src/commoncrawl-sync.ts
15797
16108
  import crypto23 from "crypto";
15798
16109
  import path10 from "path";
15799
- import { and as and13, eq as eq25, sql as sql9 } from "drizzle-orm";
16110
+ import { and as and13, eq as eq26, sql as sql9 } from "drizzle-orm";
15800
16111
  var log6 = createLogger("CommonCrawlSync");
15801
16112
  var INSERT_CHUNK_SIZE = 1e4;
15802
16113
  function defaultDeps() {
@@ -15822,7 +16133,7 @@ async function executeReleaseSync(db, syncId, opts) {
15822
16133
  phaseDetail: "downloading vertices + edges",
15823
16134
  updatedAt: downloadStartedAt,
15824
16135
  error: null
15825
- }).where(eq25(ccReleaseSyncs.id, syncId)).run();
16136
+ }).where(eq26(ccReleaseSyncs.id, syncId)).run();
15826
16137
  const paths = ccReleasePaths(release);
15827
16138
  const releaseCacheDir = path10.join(deps.cacheDir, release);
15828
16139
  const vertexPath = path10.join(releaseCacheDir, paths.vertexFilename);
@@ -15845,7 +16156,7 @@ async function executeReleaseSync(db, syncId, opts) {
15845
16156
  vertexSha256: vertex.sha256,
15846
16157
  edgesSha256: edges.sha256,
15847
16158
  updatedAt: downloadFinishedAt
15848
- }).where(eq25(ccReleaseSyncs.id, syncId)).run();
16159
+ }).where(eq26(ccReleaseSyncs.id, syncId)).run();
15849
16160
  const allProjects = db.select().from(projects).all();
15850
16161
  const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
15851
16162
  let rows = [];
@@ -15861,8 +16172,8 @@ async function executeReleaseSync(db, syncId, opts) {
15861
16172
  }
15862
16173
  const queriedAt = deps.now().toISOString();
15863
16174
  db.transaction((tx) => {
15864
- tx.delete(backlinkDomains).where(eq25(backlinkDomains.releaseSyncId, syncId)).run();
15865
- tx.delete(backlinkSummaries).where(eq25(backlinkSummaries.releaseSyncId, syncId)).run();
16175
+ tx.delete(backlinkDomains).where(eq26(backlinkDomains.releaseSyncId, syncId)).run();
16176
+ tx.delete(backlinkSummaries).where(eq26(backlinkSummaries.releaseSyncId, syncId)).run();
15866
16177
  const expanded = [];
15867
16178
  for (const r of rows) {
15868
16179
  const projectIds = projectsByDomain.get(r.targetDomain);
@@ -15921,7 +16232,7 @@ async function executeReleaseSync(db, syncId, opts) {
15921
16232
  domainsDiscovered: rows.length,
15922
16233
  updatedAt: finishedAt,
15923
16234
  error: null
15924
- }).where(eq25(ccReleaseSyncs.id, syncId)).run();
16235
+ }).where(eq26(ccReleaseSyncs.id, syncId)).run();
15925
16236
  log6.info("sync.completed", {
15926
16237
  syncId,
15927
16238
  release,
@@ -15951,7 +16262,7 @@ async function executeReleaseSync(db, syncId, opts) {
15951
16262
  error: errorMsg,
15952
16263
  phaseDetail: null,
15953
16264
  updatedAt: finishedAt
15954
- }).where(eq25(ccReleaseSyncs.id, syncId)).run();
16265
+ }).where(eq26(ccReleaseSyncs.id, syncId)).run();
15955
16266
  log6.error("sync.failed", { syncId, release, error: errorMsg });
15956
16267
  throw err;
15957
16268
  }
@@ -15987,7 +16298,7 @@ function computeSummary(rows) {
15987
16298
  // src/backlink-extract.ts
15988
16299
  import crypto24 from "crypto";
15989
16300
  import fs8 from "fs";
15990
- import { and as and14, desc as desc12, eq as eq26 } from "drizzle-orm";
16301
+ import { and as and14, desc as desc12, eq as eq27 } from "drizzle-orm";
15991
16302
  var log7 = createLogger("BacklinkExtract");
15992
16303
  function defaultDeps2() {
15993
16304
  return {
@@ -15999,13 +16310,13 @@ function defaultDeps2() {
15999
16310
  async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
16000
16311
  const deps = { ...defaultDeps2(), ...opts.deps };
16001
16312
  const startedAt = deps.now().toISOString();
16002
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq26(runs.id, runId)).run();
16313
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq27(runs.id, runId)).run();
16003
16314
  try {
16004
- const project = db.select().from(projects).where(eq26(projects.id, projectId)).get();
16315
+ const project = db.select().from(projects).where(eq27(projects.id, projectId)).get();
16005
16316
  if (!project) {
16006
16317
  throw new Error(`Project not found: ${projectId}`);
16007
16318
  }
16008
- const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq26(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc12(ccReleaseSyncs.createdAt)).limit(1).get();
16319
+ const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq27(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc12(ccReleaseSyncs.createdAt)).limit(1).get();
16009
16320
  if (!sync) {
16010
16321
  throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
16011
16322
  }
@@ -16033,7 +16344,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
16033
16344
  const targetDomain = project.canonicalDomain;
16034
16345
  db.transaction((tx) => {
16035
16346
  tx.delete(backlinkDomains).where(
16036
- and14(eq26(backlinkDomains.projectId, projectId), eq26(backlinkDomains.release, release))
16347
+ and14(eq27(backlinkDomains.projectId, projectId), eq27(backlinkDomains.release, release))
16037
16348
  ).run();
16038
16349
  if (rows.length > 0) {
16039
16350
  const values = rows.map((r) => ({
@@ -16073,7 +16384,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
16073
16384
  }).run();
16074
16385
  });
16075
16386
  const finishedAt = deps.now().toISOString();
16076
- db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq26(runs.id, runId)).run();
16387
+ db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq27(runs.id, runId)).run();
16077
16388
  log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
16078
16389
  } catch (err) {
16079
16390
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -16082,7 +16393,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
16082
16393
  status: RunStatuses.failed,
16083
16394
  error: errorMsg,
16084
16395
  finishedAt
16085
- }).where(eq26(runs.id, runId)).run();
16396
+ }).where(eq27(runs.id, runId)).run();
16086
16397
  log7.error("extract.failed", { runId, projectId, error: errorMsg });
16087
16398
  throw err;
16088
16399
  }
@@ -16155,7 +16466,7 @@ var ProviderRegistry = class {
16155
16466
 
16156
16467
  // src/scheduler.ts
16157
16468
  import cron from "node-cron";
16158
- import { eq as eq27 } from "drizzle-orm";
16469
+ import { eq as eq28 } from "drizzle-orm";
16159
16470
  var log8 = createLogger("Scheduler");
16160
16471
  var Scheduler = class {
16161
16472
  db;
@@ -16167,7 +16478,7 @@ var Scheduler = class {
16167
16478
  }
16168
16479
  /** Load all enabled schedules from DB and register cron jobs. */
16169
16480
  start() {
16170
- const allSchedules = this.db.select().from(schedules).where(eq27(schedules.enabled, 1)).all();
16481
+ const allSchedules = this.db.select().from(schedules).where(eq28(schedules.enabled, 1)).all();
16171
16482
  for (const schedule of allSchedules) {
16172
16483
  const missedRunAt = schedule.nextRunAt;
16173
16484
  this.registerCronTask(schedule);
@@ -16192,7 +16503,7 @@ var Scheduler = class {
16192
16503
  this.stopTask(projectId, existing, "Stopped");
16193
16504
  this.tasks.delete(projectId);
16194
16505
  }
16195
- const schedule = this.db.select().from(schedules).where(eq27(schedules.projectId, projectId)).get();
16506
+ const schedule = this.db.select().from(schedules).where(eq28(schedules.projectId, projectId)).get();
16196
16507
  if (schedule && schedule.enabled === 1) {
16197
16508
  this.registerCronTask(schedule);
16198
16509
  }
@@ -16225,14 +16536,14 @@ var Scheduler = class {
16225
16536
  this.db.update(schedules).set({
16226
16537
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
16227
16538
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16228
- }).where(eq27(schedules.id, scheduleId)).run();
16539
+ }).where(eq28(schedules.id, scheduleId)).run();
16229
16540
  const label = schedule.preset ?? cronExpr;
16230
16541
  log8.info("cron.registered", { projectId, schedule: label, timezone });
16231
16542
  }
16232
16543
  triggerRun(scheduleId, projectId) {
16233
16544
  try {
16234
16545
  const now = (/* @__PURE__ */ new Date()).toISOString();
16235
- const currentSchedule = this.db.select().from(schedules).where(eq27(schedules.id, scheduleId)).get();
16546
+ const currentSchedule = this.db.select().from(schedules).where(eq28(schedules.id, scheduleId)).get();
16236
16547
  if (!currentSchedule || currentSchedule.enabled !== 1) {
16237
16548
  log8.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
16238
16549
  this.remove(projectId);
@@ -16240,7 +16551,7 @@ var Scheduler = class {
16240
16551
  }
16241
16552
  const task = this.tasks.get(projectId);
16242
16553
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
16243
- const project = this.db.select().from(projects).where(eq27(projects.id, projectId)).get();
16554
+ const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
16244
16555
  if (!project) {
16245
16556
  log8.error("project.not-found", { projectId, msg: "skipping scheduled run" });
16246
16557
  this.remove(projectId);
@@ -16269,7 +16580,7 @@ var Scheduler = class {
16269
16580
  this.db.update(schedules).set({
16270
16581
  nextRunAt,
16271
16582
  updatedAt: now
16272
- }).where(eq27(schedules.id, currentSchedule.id)).run();
16583
+ }).where(eq28(schedules.id, currentSchedule.id)).run();
16273
16584
  return;
16274
16585
  }
16275
16586
  const runId = queueResult.runId;
@@ -16277,7 +16588,7 @@ var Scheduler = class {
16277
16588
  lastRunAt: now,
16278
16589
  nextRunAt,
16279
16590
  updatedAt: now
16280
- }).where(eq27(schedules.id, currentSchedule.id)).run();
16591
+ }).where(eq28(schedules.id, currentSchedule.id)).run();
16281
16592
  const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
16282
16593
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
16283
16594
  log8.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
@@ -16289,7 +16600,7 @@ var Scheduler = class {
16289
16600
  };
16290
16601
 
16291
16602
  // src/notifier.ts
16292
- import { eq as eq28, desc as desc13, and as and15, or as or3 } from "drizzle-orm";
16603
+ import { eq as eq29, desc as desc13, and as and15, or as or3 } from "drizzle-orm";
16293
16604
  import crypto25 from "crypto";
16294
16605
  var log9 = createLogger("Notifier");
16295
16606
  var Notifier = class {
@@ -16302,18 +16613,18 @@ var Notifier = class {
16302
16613
  /** Called after a run completes (success, partial, or failed). */
16303
16614
  async onRunCompleted(runId, projectId) {
16304
16615
  log9.info("run.completed", { runId, projectId });
16305
- const notifs = this.db.select().from(notifications).where(eq28(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
16616
+ const notifs = this.db.select().from(notifications).where(eq29(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
16306
16617
  if (notifs.length === 0) {
16307
16618
  log9.info("notifications.none-enabled", { projectId });
16308
16619
  return;
16309
16620
  }
16310
16621
  log9.info("notifications.found", { projectId, count: notifs.length });
16311
- const run = this.db.select().from(runs).where(eq28(runs.id, runId)).get();
16622
+ const run = this.db.select().from(runs).where(eq29(runs.id, runId)).get();
16312
16623
  if (!run) {
16313
16624
  log9.error("run.not-found", { runId, msg: "skipping notification dispatch" });
16314
16625
  return;
16315
16626
  }
16316
- const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
16627
+ const project = this.db.select().from(projects).where(eq29(projects.id, projectId)).get();
16317
16628
  if (!project) {
16318
16629
  log9.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
16319
16630
  return;
@@ -16360,11 +16671,11 @@ var Notifier = class {
16360
16671
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
16361
16672
  if (highInsights.length > 0) insightEvents.push("insight.high");
16362
16673
  if (insightEvents.length === 0) return;
16363
- const notifs = this.db.select().from(notifications).where(eq28(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
16674
+ const notifs = this.db.select().from(notifications).where(eq29(notifications.projectId, projectId)).all().filter((n) => n.enabled === 1);
16364
16675
  if (notifs.length === 0) return;
16365
- const run = this.db.select().from(runs).where(eq28(runs.id, runId)).get();
16676
+ const run = this.db.select().from(runs).where(eq29(runs.id, runId)).get();
16366
16677
  if (!run) return;
16367
- const project = this.db.select().from(projects).where(eq28(projects.id, projectId)).get();
16678
+ const project = this.db.select().from(projects).where(eq29(projects.id, projectId)).get();
16368
16679
  if (!project) return;
16369
16680
  for (const notif of notifs) {
16370
16681
  const config = parseJsonColumn(notif.config, { url: "", events: [] });
@@ -16396,8 +16707,8 @@ var Notifier = class {
16396
16707
  computeTransitions(runId, projectId) {
16397
16708
  const recentRuns = this.db.select().from(runs).where(
16398
16709
  and15(
16399
- eq28(runs.projectId, projectId),
16400
- or3(eq28(runs.status, "completed"), eq28(runs.status, "partial"))
16710
+ eq29(runs.projectId, projectId),
16711
+ or3(eq29(runs.status, "completed"), eq29(runs.status, "partial"))
16401
16712
  )
16402
16713
  ).orderBy(desc13(runs.createdAt)).limit(2).all();
16403
16714
  if (recentRuns.length < 2) return [];
@@ -16409,12 +16720,12 @@ var Notifier = class {
16409
16720
  keyword: keywords.keyword,
16410
16721
  provider: querySnapshots.provider,
16411
16722
  citationState: querySnapshots.citationState
16412
- }).from(querySnapshots).leftJoin(keywords, eq28(querySnapshots.keywordId, keywords.id)).where(eq28(querySnapshots.runId, currentRunId)).all();
16723
+ }).from(querySnapshots).leftJoin(keywords, eq29(querySnapshots.keywordId, keywords.id)).where(eq29(querySnapshots.runId, currentRunId)).all();
16413
16724
  const previousSnapshots = this.db.select({
16414
16725
  keywordId: querySnapshots.keywordId,
16415
16726
  provider: querySnapshots.provider,
16416
16727
  citationState: querySnapshots.citationState
16417
- }).from(querySnapshots).where(eq28(querySnapshots.runId, previousRunId)).all();
16728
+ }).from(querySnapshots).where(eq29(querySnapshots.runId, previousRunId)).all();
16418
16729
  const prevMap = /* @__PURE__ */ new Map();
16419
16730
  for (const s of previousSnapshots) {
16420
16731
  prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
@@ -16531,7 +16842,7 @@ var RunCoordinator = class {
16531
16842
 
16532
16843
  // src/agent/session-registry.ts
16533
16844
  import crypto27 from "crypto";
16534
- import { eq as eq30 } from "drizzle-orm";
16845
+ import { eq as eq31 } from "drizzle-orm";
16535
16846
 
16536
16847
  // src/agent/session.ts
16537
16848
  import fs11 from "fs";
@@ -16881,7 +17192,7 @@ function resolveSessionProviderAndModel(config, opts) {
16881
17192
 
16882
17193
  // src/agent/memory-store.ts
16883
17194
  import crypto26 from "crypto";
16884
- import { and as and16, desc as desc14, eq as eq29, like as like2, sql as sql10 } from "drizzle-orm";
17195
+ import { and as and16, desc as desc14, eq as eq30, like as like2, sql as sql10 } from "drizzle-orm";
16885
17196
  var COMPACTION_KEY_PREFIX = "compaction:";
16886
17197
  var COMPACTION_NOTES_PER_SESSION = 3;
16887
17198
  function rowToDto(row) {
@@ -16895,7 +17206,7 @@ function rowToDto(row) {
16895
17206
  };
16896
17207
  }
16897
17208
  function listMemoryEntries(db, projectId, opts = {}) {
16898
- const query = db.select().from(agentMemory).where(eq29(agentMemory.projectId, projectId)).orderBy(desc14(agentMemory.updatedAt));
17209
+ const query = db.select().from(agentMemory).where(eq30(agentMemory.projectId, projectId)).orderBy(desc14(agentMemory.updatedAt));
16899
17210
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
16900
17211
  return rows.map(rowToDto);
16901
17212
  }
@@ -16926,12 +17237,12 @@ function upsertMemoryEntry(db, args) {
16926
17237
  updatedAt: now
16927
17238
  }
16928
17239
  }).run();
16929
- const row = db.select().from(agentMemory).where(and16(eq29(agentMemory.projectId, args.projectId), eq29(agentMemory.key, args.key))).get();
17240
+ const row = db.select().from(agentMemory).where(and16(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, args.key))).get();
16930
17241
  if (!row) throw new Error("memory upsert produced no row");
16931
17242
  return rowToDto(row);
16932
17243
  }
16933
17244
  function deleteMemoryEntry(db, projectId, key) {
16934
- const result = db.delete(agentMemory).where(and16(eq29(agentMemory.projectId, projectId), eq29(agentMemory.key, key))).run();
17245
+ const result = db.delete(agentMemory).where(and16(eq30(agentMemory.projectId, projectId), eq30(agentMemory.key, key))).run();
16935
17246
  const changes = result.changes ?? 0;
16936
17247
  return changes > 0;
16937
17248
  }
@@ -16961,7 +17272,7 @@ function writeCompactionNote(db, args) {
16961
17272
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
16962
17273
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
16963
17274
  and16(
16964
- eq29(agentMemory.projectId, args.projectId),
17275
+ eq30(agentMemory.projectId, args.projectId),
16965
17276
  like2(agentMemory.key, `${sessionPrefix}%`)
16966
17277
  )
16967
17278
  ).orderBy(desc14(agentMemory.updatedAt)).all();
@@ -16969,7 +17280,7 @@ function writeCompactionNote(db, args) {
16969
17280
  if (stale.length > 0) {
16970
17281
  tx.delete(agentMemory).where(sql10`${agentMemory.id} IN (${sql10.join(stale.map((s) => sql10`${s}`), sql10`, `)})`).run();
16971
17282
  }
16972
- const row = tx.select().from(agentMemory).where(and16(eq29(agentMemory.projectId, args.projectId), eq29(agentMemory.key, key))).get();
17283
+ const row = tx.select().from(agentMemory).where(and16(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, key))).get();
16973
17284
  if (row) inserted = rowToDto(row);
16974
17285
  });
16975
17286
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -17151,7 +17462,7 @@ var SessionRegistry = class {
17151
17462
  modelProvider: effectiveProvider,
17152
17463
  modelId: effectiveModelId,
17153
17464
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
17154
- }).where(eq30(agentSessions.projectId, projectId)).run();
17465
+ }).where(eq31(agentSessions.projectId, projectId)).run();
17155
17466
  }
17156
17467
  const agent2 = createAeroSession({
17157
17468
  projectName,
@@ -17365,7 +17676,7 @@ ${lines.join("\n")}
17365
17676
  modelProvider: nextProvider,
17366
17677
  modelId: nextModelId,
17367
17678
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
17368
- }).where(eq30(agentSessions.projectId, projectId)).run();
17679
+ }).where(eq31(agentSessions.projectId, projectId)).run();
17369
17680
  }
17370
17681
  /** Persist a session's transcript back to the DB. Call after any run settles. */
17371
17682
  save(projectName) {
@@ -17527,11 +17838,11 @@ ${lines.join("\n")}
17527
17838
  return id;
17528
17839
  }
17529
17840
  tryResolveProjectId(projectName) {
17530
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq30(projects.name, projectName)).get();
17841
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq31(projects.name, projectName)).get();
17531
17842
  return row?.id;
17532
17843
  }
17533
17844
  loadRow(projectId) {
17534
- const row = this.opts.db.select().from(agentSessions).where(eq30(agentSessions.projectId, projectId)).get();
17845
+ const row = this.opts.db.select().from(agentSessions).where(eq31(agentSessions.projectId, projectId)).get();
17535
17846
  return row ?? null;
17536
17847
  }
17537
17848
  insertRow(params) {
@@ -17550,14 +17861,14 @@ ${lines.join("\n")}
17550
17861
  }
17551
17862
  updateRow(projectId, patch) {
17552
17863
  const now = (/* @__PURE__ */ new Date()).toISOString();
17553
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq30(agentSessions.projectId, projectId)).run();
17864
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq31(agentSessions.projectId, projectId)).run();
17554
17865
  }
17555
17866
  };
17556
17867
 
17557
17868
  // src/agent/agent-routes.ts
17558
- import { eq as eq31 } from "drizzle-orm";
17869
+ import { eq as eq32 } from "drizzle-orm";
17559
17870
  function resolveProject2(db, name) {
17560
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq31(projects.name, name)).get();
17871
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq32(projects.name, name)).get();
17561
17872
  if (!row) throw notFound("project", name);
17562
17873
  return row;
17563
17874
  }
@@ -17566,7 +17877,7 @@ function registerAgentRoutes(app, opts) {
17566
17877
  "/projects/:name/agent/transcript",
17567
17878
  async (request) => {
17568
17879
  const project = resolveProject2(opts.db, request.params.name);
17569
- const row = opts.db.select().from(agentSessions).where(eq31(agentSessions.projectId, project.id)).get();
17880
+ const row = opts.db.select().from(agentSessions).where(eq32(agentSessions.projectId, project.id)).get();
17570
17881
  if (!row) {
17571
17882
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
17572
17883
  }
@@ -17590,7 +17901,7 @@ function registerAgentRoutes(app, opts) {
17590
17901
  async (request) => {
17591
17902
  const project = resolveProject2(opts.db, request.params.name);
17592
17903
  opts.sessionRegistry.reset(project.name);
17593
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(agentSessions.projectId, project.id)).run();
17904
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(agentSessions.projectId, project.id)).run();
17594
17905
  return { status: "reset" };
17595
17906
  }
17596
17907
  );
@@ -17894,7 +18205,7 @@ var SnapshotService = class {
17894
18205
  }
17895
18206
  async createReport(input) {
17896
18207
  const companyName = input.companyName.trim();
17897
- const domain = normalizeDomain2(input.domain);
18208
+ const domain = normalizeDomain3(input.domain);
17898
18209
  const manualPhrases = normalizeStringList(input.phrases ?? []);
17899
18210
  const manualCompetitors = normalizeStringList(input.competitors ?? []);
17900
18211
  const providers = this.registry.getAll();
@@ -18334,7 +18645,7 @@ function extractCompetitorsFromResponse(ctx) {
18334
18645
  const targetDomain = extractHostname2(ctx.targetDomain);
18335
18646
  for (const hint of ctx.manualCompetitors) {
18336
18647
  if (isDomainLike(hint)) {
18337
- const normalizedHint = normalizeDomain2(hint);
18648
+ const normalizedHint = normalizeDomain3(hint);
18338
18649
  if (domainMatches2(normalizedHint, targetDomain)) continue;
18339
18650
  if (ctx.citedDomains.some((domain) => domainMatches2(domain, normalizedHint)) || lowerAnswer.includes(normalizedHint.toLowerCase())) {
18340
18651
  competitors2.add(normalizedHint);
@@ -18393,7 +18704,7 @@ function uniqueStrings2(values) {
18393
18704
  values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
18394
18705
  )];
18395
18706
  }
18396
- function normalizeDomain2(value) {
18707
+ function normalizeDomain3(value) {
18397
18708
  const trimmed = value.trim();
18398
18709
  if (!trimmed) return trimmed;
18399
18710
  try {
@@ -18404,15 +18715,15 @@ function normalizeDomain2(value) {
18404
18715
  }
18405
18716
  }
18406
18717
  function extractHostname2(value) {
18407
- return normalizeDomain2(value);
18718
+ return normalizeDomain3(value);
18408
18719
  }
18409
18720
  function domainMatches2(candidate, target) {
18410
- const normalizedCandidate = normalizeDomain2(candidate);
18411
- const normalizedTarget = normalizeDomain2(target);
18721
+ const normalizedCandidate = normalizeDomain3(candidate);
18722
+ const normalizedTarget = normalizeDomain3(target);
18412
18723
  return normalizedCandidate === normalizedTarget || normalizedCandidate.endsWith(`.${normalizedTarget}`);
18413
18724
  }
18414
18725
  function isDomainLike(value) {
18415
- const normalized = normalizeDomain2(value);
18726
+ const normalized = normalizeDomain3(value);
18416
18727
  return normalized.includes(".") && !normalized.includes(" ");
18417
18728
  }
18418
18729
  function clipText(value, length) {
@@ -18612,7 +18923,7 @@ async function createServer(opts) {
18612
18923
  intelligenceService,
18613
18924
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
18614
18925
  async ({ runId, projectId, insightCount, criticalOrHigh }) => {
18615
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq32(projects.id, projectId)).get();
18926
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq33(projects.id, projectId)).get();
18616
18927
  if (!project) return;
18617
18928
  sessionRegistry.queueFollowUp(project.name, {
18618
18929
  role: "user",
@@ -18752,7 +19063,7 @@ async function createServer(opts) {
18752
19063
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
18753
19064
  if (opts.config.apiKey) {
18754
19065
  const keyHash = hashApiKey(opts.config.apiKey);
18755
- const existing = opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, keyHash)).get();
19066
+ const existing = opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, keyHash)).get();
18756
19067
  if (!existing) {
18757
19068
  const prefix = opts.config.apiKey.slice(0, 12);
18758
19069
  opts.db.insert(apiKeys).values({
@@ -18804,7 +19115,7 @@ async function createServer(opts) {
18804
19115
  };
18805
19116
  const getDefaultApiKey = () => {
18806
19117
  if (!opts.config.apiKey) return void 0;
18807
- return opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
19118
+ return opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
18808
19119
  };
18809
19120
  const createPasswordSession = (reply) => {
18810
19121
  const key = getDefaultApiKey();
@@ -18861,12 +19172,12 @@ async function createServer(opts) {
18861
19172
  return reply.send({ authenticated: true });
18862
19173
  }
18863
19174
  if (apiKey) {
18864
- const key = opts.db.select().from(apiKeys).where(eq32(apiKeys.keyHash, hashApiKey(apiKey))).get();
19175
+ const key = opts.db.select().from(apiKeys).where(eq33(apiKeys.keyHash, hashApiKey(apiKey))).get();
18865
19176
  if (!key || key.revokedAt) {
18866
19177
  const err2 = authInvalid();
18867
19178
  return reply.status(err2.statusCode).send(err2.toJSON());
18868
19179
  }
18869
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(apiKeys.id, key.id)).run();
19180
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq33(apiKeys.id, key.id)).run();
18870
19181
  const sessionId = createSession(key.id);
18871
19182
  reply.header("set-cookie", serializeSessionCookie({
18872
19183
  name: SESSION_COOKIE_NAME,