@ainyc/canonry 4.27.0 → 4.27.2

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.
@@ -50,6 +50,7 @@ import {
50
50
  gaTrafficSummaries,
51
51
  gaTrafficWindowSummaries,
52
52
  groupInsights,
53
+ groupRunsByCreatedAt,
53
54
  gscCoverageSnapshots,
54
55
  gscSearchData,
55
56
  gscUrlInspections,
@@ -60,6 +61,7 @@ import {
60
61
  mapOpportunitiesToNextSteps,
61
62
  notifications,
62
63
  parseJsonColumn,
64
+ pickGroupRepresentative,
63
65
  projects,
64
66
  queries,
65
67
  querySnapshots,
@@ -68,7 +70,7 @@ import {
68
70
  schedules,
69
71
  trafficSources,
70
72
  usageCounters
71
- } from "./chunk-PN24DAGC.js";
73
+ } from "./chunk-NXXD6TX7.js";
72
74
  import {
73
75
  AGENT_MEMORY_VALUE_MAX_BYTES,
74
76
  AGENT_PROVIDER_IDS,
@@ -2317,12 +2319,16 @@ async function analyticsRoutes(app) {
2317
2319
  const project = resolveProject(app.db, request.params.name);
2318
2320
  const window = parseWindow(request.query.window);
2319
2321
  const cutoff = windowCutoff(window);
2320
- const latestRun = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().find((r) => r.status === "completed" || r.status === "partial");
2322
+ const completedRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial");
2323
+ const latestGroup = groupRunsByCreatedAt(completedRuns)[0] ?? [];
2324
+ const latestGroupRunIds = latestGroup.map((r) => r.id);
2325
+ const latestRun = pickGroupRepresentative(latestGroup);
2321
2326
  if (!latestRun) {
2322
2327
  return reply.send({ cited: [], gap: [], uncited: [], mentionedQueries: [], mentionGap: [], notMentioned: [], runId: "", window });
2323
2328
  }
2324
2329
  const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
2325
2330
  const windowRunIds = windowRuns.map((r) => r.id);
2331
+ const runIdToCreatedAt = new Map(windowRuns.map((r) => [r.id, r.createdAt]));
2326
2332
  const consistencyMap = /* @__PURE__ */ new Map();
2327
2333
  if (windowRunIds.length > 0) {
2328
2334
  const allWindowSnaps = app.db.select({
@@ -2333,14 +2339,15 @@ async function analyticsRoutes(app) {
2333
2339
  answerText: querySnapshots.answerText
2334
2340
  }).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all();
2335
2341
  for (const s of allWindowSnaps) {
2342
+ const timePoint = runIdToCreatedAt.get(s.runId) ?? s.runId;
2336
2343
  let entry = consistencyMap.get(s.queryId);
2337
2344
  if (!entry) {
2338
2345
  entry = { citedRuns: /* @__PURE__ */ new Set(), totalRuns: /* @__PURE__ */ new Set(), mentionedRuns: /* @__PURE__ */ new Set() };
2339
2346
  consistencyMap.set(s.queryId, entry);
2340
2347
  }
2341
- entry.totalRuns.add(s.runId);
2342
- if (s.citationState === CitationStates.cited) entry.citedRuns.add(s.runId);
2343
- if (resolveSnapshotAnswerMentioned(s, project)) entry.mentionedRuns.add(s.runId);
2348
+ entry.totalRuns.add(timePoint);
2349
+ if (s.citationState === CitationStates.cited) entry.citedRuns.add(timePoint);
2350
+ if (resolveSnapshotAnswerMentioned(s, project)) entry.mentionedRuns.add(timePoint);
2344
2351
  }
2345
2352
  }
2346
2353
  const rawSnapshots = app.db.select({
@@ -2351,7 +2358,7 @@ async function analyticsRoutes(app) {
2351
2358
  answerMentioned: querySnapshots.answerMentioned,
2352
2359
  answerText: querySnapshots.answerText,
2353
2360
  competitorOverlap: querySnapshots.competitorOverlap
2354
- }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(eq10(querySnapshots.runId, latestRun.id)).all();
2361
+ }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, latestGroupRunIds)).all();
2355
2362
  const snapshots = rawSnapshots.map((s) => ({
2356
2363
  ...s,
2357
2364
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
@@ -2435,11 +2442,12 @@ async function analyticsRoutes(app) {
2435
2442
  const project = resolveProject(app.db, request.params.name);
2436
2443
  const window = parseWindow(request.query.window);
2437
2444
  const cutoff = windowCutoff(window);
2438
- const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
2445
+ const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
2439
2446
  if (windowRuns.length === 0) {
2440
2447
  return reply.send({ overall: [], byQuery: {}, runId: "", window });
2441
2448
  }
2442
- const latestRunId = windowRuns[0].id;
2449
+ const latestGroup = groupRunsByCreatedAt(windowRuns)[0] ?? [];
2450
+ const latestRunId = pickGroupRepresentative(latestGroup)?.id ?? windowRuns[0].id;
2443
2451
  const windowRunIds = windowRuns.map((r) => r.id);
2444
2452
  const snapshots = app.db.select({
2445
2453
  queryId: querySnapshots.queryId,
@@ -2604,7 +2612,7 @@ function buildCategoryCounts(counts) {
2604
2612
  }
2605
2613
 
2606
2614
  // ../api-routes/src/intelligence.ts
2607
- import { eq as eq11, desc as desc4, and as and3 } from "drizzle-orm";
2615
+ import { eq as eq11, desc as desc4, and as and3, inArray as inArray3 } from "drizzle-orm";
2608
2616
  function emptyHealthSnapshot(projectId) {
2609
2617
  return {
2610
2618
  id: `no-data:${projectId}`,
@@ -2648,6 +2656,44 @@ function mapHealthRow(r) {
2648
2656
  status: "ready"
2649
2657
  };
2650
2658
  }
2659
+ function aggregateHealthSnapshots(projectId, rows) {
2660
+ if (rows.length === 1) return mapHealthRow(rows[0]);
2661
+ let totalPairs = 0;
2662
+ let citedPairs = 0;
2663
+ const mergedProviders = {};
2664
+ let newestCreatedAt = "";
2665
+ const runIds = [];
2666
+ for (const row of rows) {
2667
+ totalPairs += row.totalPairs;
2668
+ citedPairs += row.citedPairs;
2669
+ if (row.createdAt > newestCreatedAt) newestCreatedAt = row.createdAt;
2670
+ if (row.runId) runIds.push(row.runId);
2671
+ const providerBreakdown = parseJsonColumn(row.providerBreakdown, {});
2672
+ for (const [provider, entry] of Object.entries(providerBreakdown)) {
2673
+ const existing = mergedProviders[provider] ?? { total: 0, cited: 0, citedRate: 0 };
2674
+ existing.total += entry.total;
2675
+ existing.cited += entry.cited;
2676
+ mergedProviders[provider] = existing;
2677
+ }
2678
+ }
2679
+ for (const entry of Object.values(mergedProviders)) {
2680
+ entry.citedRate = entry.total > 0 ? entry.cited / entry.total : 0;
2681
+ }
2682
+ const overallCitedRate = totalPairs > 0 ? citedPairs / totalPairs : 0;
2683
+ return {
2684
+ // Synthetic id so consumers can tell this is an aggregate; concatenate
2685
+ // source runIds for traceability without inventing a new schema column.
2686
+ id: `group:${runIds.join(",")}`,
2687
+ projectId,
2688
+ runId: runIds[0] ?? null,
2689
+ overallCitedRate,
2690
+ totalPairs,
2691
+ citedPairs,
2692
+ providerBreakdown: mergedProviders,
2693
+ createdAt: newestCreatedAt,
2694
+ status: "ready"
2695
+ };
2696
+ }
2651
2697
  async function intelligenceRoutes(app) {
2652
2698
  app.get("/projects/:name/insights", async (request, reply) => {
2653
2699
  const project = resolveProject(app.db, request.params.name);
@@ -2679,11 +2725,27 @@ async function intelligenceRoutes(app) {
2679
2725
  });
2680
2726
  app.get("/projects/:name/health/latest", async (request, reply) => {
2681
2727
  const project = resolveProject(app.db, request.params.name);
2682
- const row = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
2683
- if (!row) {
2728
+ const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and3(
2729
+ eq11(runs.projectId, project.id),
2730
+ eq11(runs.kind, RunKinds["answer-visibility"]),
2731
+ inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
2732
+ )).orderBy(desc4(runs.createdAt), desc4(runs.id)).all();
2733
+ const latestGroup = groupRunsByCreatedAt(projectVisRuns)[0] ?? [];
2734
+ const latestGroupRunIds = latestGroup.map((r) => r.id);
2735
+ if (latestGroupRunIds.length > 0) {
2736
+ const groupRows = app.db.select().from(healthSnapshots).where(and3(
2737
+ eq11(healthSnapshots.projectId, project.id),
2738
+ inArray3(healthSnapshots.runId, latestGroupRunIds)
2739
+ )).all();
2740
+ if (groupRows.length > 0) {
2741
+ return reply.send(aggregateHealthSnapshots(project.id, groupRows));
2742
+ }
2743
+ }
2744
+ const fallback = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
2745
+ if (!fallback) {
2684
2746
  return reply.send(emptyHealthSnapshot(project.id));
2685
2747
  }
2686
- return reply.send(mapHealthRow(row));
2748
+ return reply.send(mapHealthRow(fallback));
2687
2749
  });
2688
2750
  app.get("/projects/:name/health/history", async (request, reply) => {
2689
2751
  const project = resolveProject(app.db, request.params.name);
@@ -2695,7 +2757,7 @@ async function intelligenceRoutes(app) {
2695
2757
  }
2696
2758
 
2697
2759
  // ../api-routes/src/report.ts
2698
- import { and as and5, desc as desc6, eq as eq13, gte, inArray as inArray4, lt, lte, ne, or as or2, sql as sql3 } from "drizzle-orm";
2760
+ import { and as and5, desc as desc6, eq as eq13, gte, inArray as inArray5, lt, lte, ne, or as or2, sql as sql3 } from "drizzle-orm";
2699
2761
 
2700
2762
  // ../api-routes/src/report-renderer.ts
2701
2763
  var COLORS = {
@@ -4931,7 +4993,7 @@ function renderReportHtml(report, opts = {}) {
4931
4993
  }
4932
4994
 
4933
4995
  // ../api-routes/src/content-data.ts
4934
- import { and as and4, eq as eq12, desc as desc5, inArray as inArray3 } from "drizzle-orm";
4996
+ import { and as and4, eq as eq12, desc as desc5, inArray as inArray4 } from "drizzle-orm";
4935
4997
  var RECENT_RUNS_WINDOW = 5;
4936
4998
  function loadOrchestratorInput(db, project, locationFilter = void 0) {
4937
4999
  const projectId = project.id;
@@ -5061,7 +5123,7 @@ function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter)
5061
5123
  // Queued/running/failed/cancelled runs may have partial or no
5062
5124
  // snapshots; including them risks pointing latestRunId at a run with
5063
5125
  // no usable evidence.
5064
- inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
5126
+ inArray4(runs.status, [RunStatuses.completed, RunStatuses.partial])
5065
5127
  )
5066
5128
  ).orderBy(desc5(runs.createdAt)).all();
5067
5129
  const filtered = locationFilter === void 0 ? rows : rows.filter((r) => (r.location ?? null) === locationFilter);
@@ -5103,7 +5165,7 @@ function buildCandidateQueries(opts) {
5103
5165
  const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq12(queries.projectId, opts.projectId)).all();
5104
5166
  const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
5105
5167
  const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
5106
- const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateQueryIds.includes(r.queryId));
5168
+ const snapshotRows = opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateQueryIds.includes(r.queryId));
5107
5169
  const snapshotsByQuery = /* @__PURE__ */ new Map();
5108
5170
  for (const row of snapshotRows) {
5109
5171
  const list = snapshotsByQuery.get(row.queryId) ?? [];
@@ -5317,7 +5379,11 @@ function categorizeQuery(query, projectDisplayName, canonicalDomain) {
5317
5379
  return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectDisplayName));
5318
5380
  }
5319
5381
  function loadSnapshotsForRun(db, runId) {
5320
- const rows = db.select().from(querySnapshots).where(eq13(querySnapshots.runId, runId)).all();
5382
+ return loadSnapshotsForRunIds(db, [runId]);
5383
+ }
5384
+ function loadSnapshotsForRunIds(db, runIds) {
5385
+ if (runIds.length === 0) return [];
5386
+ const rows = db.select().from(querySnapshots).where(inArray5(querySnapshots.runId, [...runIds])).all();
5321
5387
  return rows.map((r) => ({
5322
5388
  id: r.id,
5323
5389
  runId: r.runId,
@@ -5897,7 +5963,7 @@ function buildInsightList(db, projectId, locationFilter) {
5897
5963
  )
5898
5964
  ).orderBy(desc6(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
5899
5965
  if (recentRunIds.length === 0) return [];
5900
- const rows = db.select().from(insights).where(and5(eq13(insights.projectId, projectId), inArray4(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
5966
+ const rows = db.select().from(insights).where(and5(eq13(insights.projectId, projectId), inArray5(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
5901
5967
  const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
5902
5968
  const flat = rows.filter((r) => !r.dismissed).map((r) => {
5903
5969
  const recommendation = parseJsonColumn(r.recommendation, null);
@@ -6436,13 +6502,15 @@ function buildWhatsChanged(input) {
6436
6502
  function buildProjectReport(db, projectName) {
6437
6503
  const project = resolveProject(db, projectName);
6438
6504
  const queryLookup = loadQueryLookup(db, project.id);
6439
- const allRuns = db.select().from(runs).where(eq13(runs.projectId, project.id)).orderBy(desc6(runs.createdAt)).all();
6505
+ const allRuns = db.select().from(runs).where(eq13(runs.projectId, project.id)).orderBy(desc6(runs.createdAt), desc6(runs.id)).all();
6440
6506
  const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
6441
- const latestRun = visibilityRuns.find(
6442
- (r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial
6443
- ) ?? visibilityRuns[0];
6444
- const latestSnapshots = latestRun ? loadSnapshotsForRun(db, latestRun.id) : [];
6445
- const latestRunLocation = latestRun?.location ?? null;
6507
+ const completedVisRunGroups = groupRunsByCreatedAt(
6508
+ visibilityRuns.filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial)
6509
+ );
6510
+ const latestVisRunGroup = completedVisRunGroups[0] ?? [];
6511
+ const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? visibilityRuns[0] ?? null;
6512
+ const latestSnapshots = loadSnapshotsForRunIds(db, latestVisRunGroup.map((r) => r.id));
6513
+ const latestRunLocation = representativeLatestRun?.location ?? null;
6446
6514
  const competitorRows = db.select().from(competitors).where(eq13(competitors.projectId, project.id)).all();
6447
6515
  const competitorDomains = competitorRows.map((c) => c.domain);
6448
6516
  const ownedDomains = parseJsonColumn(project.ownedDomains, []);
@@ -6509,7 +6577,7 @@ function buildProjectReport(db, projectName) {
6509
6577
  const previousPoint = citationsTrend.length >= 2 ? citationsTrend.at(-2) : null;
6510
6578
  let trend = "unknown";
6511
6579
  if (!trendBaseline && latestPoint) {
6512
- const latestRunOnTrend = latestRun?.id === latestPoint.runId;
6580
+ const latestRunOnTrend = representativeLatestRun?.id === latestPoint.runId;
6513
6581
  const currentRate = latestRunOnTrend ? latestPoint.citationRate : citationRate;
6514
6582
  const priorRate = latestRunOnTrend ? previousPoint?.citationRate : latestPoint.citationRate;
6515
6583
  if (priorRate !== void 0) {
@@ -6531,7 +6599,7 @@ function buildProjectReport(db, projectName) {
6531
6599
  const periodStart = citationsTrend[0]?.date ?? null;
6532
6600
  const periodEnd = citationsTrend.at(-1)?.date ?? null;
6533
6601
  const configuredLocations = parseJsonColumn(project.locations, []);
6534
- const reportLocation = buildLocationMeta(latestRun?.location ?? null, configuredLocations);
6602
+ const reportLocation = buildLocationMeta(representativeLatestRun?.location ?? null, configuredLocations);
6535
6603
  const providerLocationHandling = reportLocation ? buildProviderLocationHandling(citationScorecard.providers) : [];
6536
6604
  const executiveSummary = {
6537
6605
  citationRate,
@@ -6657,7 +6725,7 @@ async function reportRoutes(app) {
6657
6725
  }
6658
6726
 
6659
6727
  // ../api-routes/src/citations.ts
6660
- import { eq as eq14, inArray as inArray5 } from "drizzle-orm";
6728
+ import { eq as eq14, inArray as inArray6 } from "drizzle-orm";
6661
6729
  async function citationRoutes(app) {
6662
6730
  app.get("/projects/:name/citations/visibility", async (request, reply) => {
6663
6731
  const project = resolveProject(app.db, request.params.name);
@@ -6681,7 +6749,7 @@ async function citationRoutes(app) {
6681
6749
  competitorOverlap: querySnapshots.competitorOverlap,
6682
6750
  answerMentioned: querySnapshots.answerMentioned,
6683
6751
  createdAt: querySnapshots.createdAt
6684
- }).from(querySnapshots).where(inArray5(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
6752
+ }).from(querySnapshots).where(inArray6(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
6685
6753
  if (rawSnapshots.length === 0) {
6686
6754
  return reply.send(emptyCitationVisibility("no-runs-yet"));
6687
6755
  }
@@ -6821,7 +6889,7 @@ function normalizeDomain2(domain) {
6821
6889
  }
6822
6890
 
6823
6891
  // ../api-routes/src/composites.ts
6824
- import { eq as eq15, and as and6, desc as desc7, sql as sql4, like, or as or3, inArray as inArray6 } from "drizzle-orm";
6892
+ import { eq as eq15, and as and6, desc as desc7, sql as sql4, like, or as or3, inArray as inArray7 } from "drizzle-orm";
6825
6893
  var TOP_INSIGHT_LIMIT = 5;
6826
6894
  var SEARCH_HIT_HARD_LIMIT = 50;
6827
6895
  var SEARCH_SNIPPET_RADIUS = 80;
@@ -6839,15 +6907,17 @@ async function compositeRoutes(app) {
6839
6907
  const project = resolveProject(app.db, request.params.name);
6840
6908
  const filterLocation = (request.query.location ?? "").trim() || null;
6841
6909
  const sinceIso = parseSinceFilter(request.query.since);
6842
- const allRunsRaw = app.db.select().from(runs).where(eq15(runs.projectId, project.id)).orderBy(desc7(runs.createdAt)).all();
6910
+ const allRunsRaw = app.db.select().from(runs).where(eq15(runs.projectId, project.id)).orderBy(desc7(runs.createdAt), desc7(runs.id)).all();
6843
6911
  const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
6844
6912
  const totalRuns = allRuns.length;
6845
6913
  const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
6846
6914
  const completedVisRuns = visibilityRuns.filter(
6847
6915
  (r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial
6848
6916
  );
6849
- const latestVisibilityRun = completedVisRuns[0] ?? null;
6850
- const previousVisibilityRun = completedVisRuns[1] ?? null;
6917
+ const visRunGroups = groupRunsByCreatedAt(completedVisRuns);
6918
+ const latestVisRunGroup = visRunGroups[0] ?? [];
6919
+ const previousVisRunGroup = visRunGroups[1] ?? [];
6920
+ const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
6851
6921
  const latestRunRow = allRuns[0] ?? null;
6852
6922
  const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
6853
6923
  const healthRow = app.db.select().from(healthSnapshots).where(eq15(healthSnapshots.projectId, project.id)).orderBy(desc7(healthSnapshots.createdAt)).limit(1).get();
@@ -6856,11 +6926,11 @@ async function compositeRoutes(app) {
6856
6926
  const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
6857
6927
  const sparklineRunIds = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => r.id);
6858
6928
  const snapshotRunIds = new Set(sparklineRunIds);
6859
- if (latestVisibilityRun) snapshotRunIds.add(latestVisibilityRun.id);
6860
- if (previousVisibilityRun) snapshotRunIds.add(previousVisibilityRun.id);
6929
+ for (const run of latestVisRunGroup) snapshotRunIds.add(run.id);
6930
+ for (const run of previousVisRunGroup) snapshotRunIds.add(run.id);
6861
6931
  const snapshotsByRun = loadSnapshotsByRunIds(app, [...snapshotRunIds]);
6862
- const latestSnapshots = latestVisibilityRun ? snapshotsByRun.get(latestVisibilityRun.id) ?? [] : [];
6863
- const previousSnapshots = previousVisibilityRun ? snapshotsByRun.get(previousVisibilityRun.id) ?? [] : [];
6932
+ const latestSnapshots = latestVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
6933
+ const previousSnapshots = previousVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
6864
6934
  const { queryCounts, providers } = summarizeFromSnapshots(latestSnapshots);
6865
6935
  const transitions = summarizeTransitionsFromSnapshots(
6866
6936
  latestSnapshots,
@@ -7024,7 +7094,7 @@ function loadSnapshotsByRunIds(app, runIds) {
7024
7094
  citationState: querySnapshots.citationState,
7025
7095
  competitorOverlap: querySnapshots.competitorOverlap,
7026
7096
  citedDomains: querySnapshots.citedDomains
7027
- }).from(querySnapshots).where(inArray6(querySnapshots.runId, [...runIds])).all();
7097
+ }).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all();
7028
7098
  for (const row of rows) {
7029
7099
  const list = result.get(row.runId) ?? [];
7030
7100
  list.push({
@@ -22598,7 +22668,7 @@ import crypto24 from "crypto";
22598
22668
  import fs7 from "fs";
22599
22669
  import path9 from "path";
22600
22670
  import os5 from "os";
22601
- import { and as and16, eq as eq27, inArray as inArray7, sql as sql10 } from "drizzle-orm";
22671
+ import { and as and16, eq as eq27, inArray as inArray8, sql as sql10 } from "drizzle-orm";
22602
22672
 
22603
22673
  // src/run-telemetry.ts
22604
22674
  import crypto23 from "crypto";
@@ -22939,7 +23009,7 @@ var JobRunner = class {
22939
23009
  this.registry = registry;
22940
23010
  }
22941
23011
  recoverStaleRuns() {
22942
- const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray7(runs.status, ["running", "queued"])).all();
23012
+ const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray8(runs.status, ["running", "queued"])).all();
22943
23013
  if (stale.length === 0) return;
22944
23014
  const now = (/* @__PURE__ */ new Date()).toISOString();
22945
23015
  for (const run of stale) {
@@ -23002,7 +23072,7 @@ var JobRunner = class {
23002
23072
  }
23003
23073
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
23004
23074
  const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
23005
- projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and16(eq27(queries.projectId, projectId), inArray7(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
23075
+ projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and16(eq27(queries.projectId, projectId), inArray8(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
23006
23076
  const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
23007
23077
  const competitorDomains = projectCompetitors.map((c) => c.domain);
23008
23078
  const allDomains = effectiveDomains({
@@ -24656,7 +24726,7 @@ var Scheduler = class {
24656
24726
  };
24657
24727
 
24658
24728
  // src/notifier.ts
24659
- import { eq as eq35, desc as desc16, and as and22, or as or4 } from "drizzle-orm";
24729
+ import { eq as eq35, desc as desc16, and as and22, inArray as inArray9, or as or4 } from "drizzle-orm";
24660
24730
  import crypto31 from "crypto";
24661
24731
  var log10 = createLogger("Notifier");
24662
24732
  var Notifier = class {
@@ -24761,41 +24831,76 @@ var Notifier = class {
24761
24831
  }
24762
24832
  }
24763
24833
  computeTransitions(runId, projectId) {
24834
+ const thisRun = this.db.select().from(runs).where(eq35(runs.id, runId)).get();
24835
+ if (!thisRun) return [];
24836
+ const groupSiblings = this.db.select().from(runs).where(and22(
24837
+ eq35(runs.projectId, projectId),
24838
+ eq35(runs.kind, thisRun.kind),
24839
+ eq35(runs.createdAt, thisRun.createdAt)
24840
+ )).all();
24841
+ const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
24842
+ if (stillPending) return [];
24843
+ const completedPartialSiblings = groupSiblings.filter(
24844
+ (r) => r.status === "completed" || r.status === "partial"
24845
+ );
24846
+ if (completedPartialSiblings.length === 0) return [];
24847
+ const winner = completedPartialSiblings.reduce((best, candidate) => {
24848
+ const candFinish = candidate.finishedAt ?? "";
24849
+ const bestFinish = best.finishedAt ?? "";
24850
+ if (candFinish > bestFinish) return candidate;
24851
+ if (candFinish < bestFinish) return best;
24852
+ return candidate.id > best.id ? candidate : best;
24853
+ });
24854
+ if (winner.id !== runId) return [];
24855
+ const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq35(projects.id, projectId)).get();
24856
+ const locationCount = Math.max(
24857
+ 1,
24858
+ parseJsonColumn(projectLocations?.locations ?? null, []).length
24859
+ );
24860
+ const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
24764
24861
  const recentRuns = this.db.select().from(runs).where(
24765
24862
  and22(
24766
24863
  eq35(runs.projectId, projectId),
24864
+ eq35(runs.kind, thisRun.kind),
24767
24865
  or4(eq35(runs.status, "completed"), eq35(runs.status, "partial"))
24768
24866
  )
24769
- ).orderBy(desc16(runs.createdAt)).limit(2).all();
24770
- if (recentRuns.length < 2) return [];
24771
- const currentRunId = recentRuns[0].id;
24772
- const previousRunId = recentRuns[1].id;
24773
- if (currentRunId !== runId) return [];
24867
+ ).orderBy(desc16(runs.createdAt), desc16(runs.id)).limit(RECENT_FETCH_LIMIT).all();
24868
+ const groups = groupRunsByCreatedAt(recentRuns);
24869
+ const currentGroupIdx = groups.findIndex((g) => g[0]?.createdAt === thisRun.createdAt);
24870
+ if (currentGroupIdx < 0) return [];
24871
+ const currentGroup = groups[currentGroupIdx] ?? [];
24872
+ const previousGroup = groups[currentGroupIdx + 1] ?? [];
24873
+ if (currentGroup.length === 0 || previousGroup.length === 0) return [];
24874
+ const currentRunIds = currentGroup.map((r) => r.id);
24875
+ const previousRunIds = previousGroup.map((r) => r.id);
24774
24876
  const currentSnapshots = this.db.select({
24775
24877
  queryId: querySnapshots.queryId,
24776
24878
  query: queries.query,
24777
24879
  provider: querySnapshots.provider,
24880
+ location: querySnapshots.location,
24778
24881
  citationState: querySnapshots.citationState
24779
- }).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(eq35(querySnapshots.runId, currentRunId)).all();
24882
+ }).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(inArray9(querySnapshots.runId, currentRunIds)).all();
24780
24883
  const previousSnapshots = this.db.select({
24781
24884
  queryId: querySnapshots.queryId,
24782
24885
  provider: querySnapshots.provider,
24886
+ location: querySnapshots.location,
24783
24887
  citationState: querySnapshots.citationState
24784
- }).from(querySnapshots).where(eq35(querySnapshots.runId, previousRunId)).all();
24888
+ }).from(querySnapshots).where(inArray9(querySnapshots.runId, previousRunIds)).all();
24785
24889
  const prevMap = /* @__PURE__ */ new Map();
24786
24890
  for (const s of previousSnapshots) {
24787
- prevMap.set(`${s.queryId}:${s.provider}`, s.citationState);
24891
+ prevMap.set(`${s.queryId}:${s.provider}:${s.location ?? ""}`, s.citationState);
24788
24892
  }
24789
24893
  const transitions = [];
24790
24894
  for (const s of currentSnapshots) {
24791
- const key = `${s.queryId}:${s.provider}`;
24895
+ const key = `${s.queryId}:${s.provider}:${s.location ?? ""}`;
24792
24896
  const prevState = prevMap.get(key);
24793
24897
  if (prevState && prevState !== s.citationState) {
24794
24898
  transitions.push({
24795
24899
  query: s.query ?? s.queryId,
24796
24900
  from: prevState,
24797
24901
  to: s.citationState,
24798
- provider: s.provider
24902
+ provider: s.provider,
24903
+ location: s.location
24799
24904
  });
24800
24905
  }
24801
24906
  }
@@ -1960,6 +1960,33 @@ function migrate(db) {
1960
1960
  }
1961
1961
  }
1962
1962
 
1963
+ // ../db/src/run-helpers.ts
1964
+ function groupRunsByCreatedAt(rows) {
1965
+ const groups = [];
1966
+ let current = [];
1967
+ let currentCreatedAt = null;
1968
+ for (const row of rows) {
1969
+ if (row.createdAt === currentCreatedAt) {
1970
+ current.push(row);
1971
+ } else {
1972
+ if (current.length > 0) groups.push(current);
1973
+ current = [row];
1974
+ currentCreatedAt = row.createdAt;
1975
+ }
1976
+ }
1977
+ if (current.length > 0) groups.push(current);
1978
+ return groups;
1979
+ }
1980
+ function pickGroupRepresentative(group) {
1981
+ if (group.length === 0) return null;
1982
+ let best = group[0];
1983
+ for (let i = 1; i < group.length; i++) {
1984
+ const candidate = group[i];
1985
+ if (candidate.id > best.id) best = candidate;
1986
+ }
1987
+ return best;
1988
+ }
1989
+
1963
1990
  // ../intelligence/src/regressions.ts
1964
1991
  function detectRegressions(currentRun, previousRun) {
1965
1992
  const regressions = [];
@@ -3560,20 +3587,49 @@ var IntelligenceService = class {
3560
3587
  const key = row.query.toLowerCase();
3561
3588
  gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
3562
3589
  }
3563
- const recentRunIds = this.db.select({ id: runs.id }).from(runs).where(
3590
+ const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq(projects.id, projectId)).get();
3591
+ const locationCount = Math.max(
3592
+ 1,
3593
+ parseJsonColumn(projectRow?.locations ?? null, []).length
3594
+ );
3595
+ const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
3596
+ const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
3564
3597
  and(
3565
3598
  eq(runs.projectId, projectId),
3566
3599
  eq(runs.kind, RunKinds["answer-visibility"]),
3567
3600
  or(eq(runs.status, "completed"), eq(runs.status, "partial"))
3568
3601
  )
3569
- ).orderBy(desc(runs.createdAt)).limit(RECURRENCE_LOOKBACK_RUNS + 1).all().map((r) => r.id).filter((id) => id !== excludeRunId).slice(0, RECURRENCE_LOOKBACK_RUNS);
3602
+ ).orderBy(desc(runs.createdAt), desc(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
3603
+ const recentGroups = groupRunsByCreatedAt(recentRunRows);
3604
+ const recentRunIds = [];
3605
+ const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
3606
+ let consumedGroups = 0;
3607
+ for (const group of recentGroups) {
3608
+ const groupIds = group.map((r) => r.id);
3609
+ if (groupIds.includes(excludeRunId)) continue;
3610
+ for (const r of group) recentRunIdToCreatedAt.set(r.id, r.createdAt);
3611
+ recentRunIds.push(...groupIds);
3612
+ consumedGroups++;
3613
+ if (consumedGroups >= RECURRENCE_LOOKBACK_RUNS) break;
3614
+ }
3570
3615
  const haveHistory = recentRunIds.length > 0;
3571
3616
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
3572
3617
  if (haveHistory) {
3573
- const priorRows = this.db.select({ query: insights.query, provider: insights.provider }).from(insights).where(and(eq(insights.type, "regression"), inArray(insights.runId, recentRunIds))).all();
3618
+ const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and(eq(insights.type, "regression"), inArray(insights.runId, recentRunIds))).all();
3619
+ const regressionGroups = /* @__PURE__ */ new Map();
3574
3620
  for (const row of priorRows) {
3621
+ if (!row.runId) continue;
3575
3622
  const key = `${row.query}:${row.provider}`;
3576
- priorRegressionsByPair.set(key, (priorRegressionsByPair.get(key) ?? 0) + 1);
3623
+ const groupKey = recentRunIdToCreatedAt.get(row.runId) ?? row.runId;
3624
+ let groups = regressionGroups.get(key);
3625
+ if (!groups) {
3626
+ groups = /* @__PURE__ */ new Set();
3627
+ regressionGroups.set(key, groups);
3628
+ }
3629
+ groups.add(groupKey);
3630
+ }
3631
+ for (const [key, groups] of regressionGroups) {
3632
+ priorRegressionsByPair.set(key, groups.size);
3577
3633
  }
3578
3634
  }
3579
3635
  return rawInsights.map((insight) => {
@@ -3649,6 +3705,8 @@ export {
3649
3705
  extractLegacyCredentials,
3650
3706
  dropLegacyCredentialColumns,
3651
3707
  migrate,
3708
+ groupRunsByCreatedAt,
3709
+ pickGroupRepresentative,
3652
3710
  isBlogShapedQuery,
3653
3711
  buildInventory,
3654
3712
  buildContentTargetRows,
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  setTelemetrySource,
21
21
  showFirstRunNotice,
22
22
  trackEvent
23
- } from "./chunk-M3HZAUWZ.js";
23
+ } from "./chunk-ICWFH4JA.js";
24
24
  import {
25
25
  CliError,
26
26
  EXIT_SYSTEM_ERROR,
@@ -49,7 +49,7 @@ import {
49
49
  queries,
50
50
  querySnapshots,
51
51
  runs
52
- } from "./chunk-PN24DAGC.js";
52
+ } from "./chunk-NXXD6TX7.js";
53
53
  import {
54
54
  CcReleaseSyncStatuses,
55
55
  CheckScopes,
@@ -621,7 +621,7 @@ function readStoredGroundingSources(rawResponse) {
621
621
  return result;
622
622
  }
623
623
  async function backfillInsightsCommand(project, opts) {
624
- const { IntelligenceService } = await import("./intelligence-service-VUBODIGG.js");
624
+ const { IntelligenceService } = await import("./intelligence-service-Z6QIELKP.js");
625
625
  const config = loadConfig();
626
626
  const db = createClient(config.database);
627
627
  migrate(db);
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-M3HZAUWZ.js";
3
+ } from "./chunk-ICWFH4JA.js";
4
4
  import {
5
5
  loadConfig
6
6
  } from "./chunk-2FAEQ56I.js";
7
- import "./chunk-PN24DAGC.js";
7
+ import "./chunk-NXXD6TX7.js";
8
8
  import "./chunk-HVW665A4.js";
9
9
  export {
10
10
  createServer,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-PN24DAGC.js";
3
+ } from "./chunk-NXXD6TX7.js";
4
4
  import "./chunk-HVW665A4.js";
5
5
  export {
6
6
  IntelligenceService
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "4.27.0",
3
+ "version": "4.27.2",
4
4
  "type": "module",
5
5
  "description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -60,18 +60,18 @@
60
60
  "tsup": "^8.5.1",
61
61
  "tsx": "^4.19.0",
62
62
  "@ainyc/canonry-api-routes": "0.0.0",
63
- "@ainyc/canonry-config": "0.0.0",
64
63
  "@ainyc/canonry-contracts": "0.0.0",
65
- "@ainyc/canonry-db": "0.0.0",
66
- "@ainyc/canonry-integration-commoncrawl": "0.0.0",
67
- "@ainyc/canonry-integration-google": "0.0.0",
64
+ "@ainyc/canonry-config": "0.0.0",
68
65
  "@ainyc/canonry-intelligence": "0.0.0",
66
+ "@ainyc/canonry-integration-commoncrawl": "0.0.0",
69
67
  "@ainyc/canonry-integration-cloud-run": "0.0.0",
68
+ "@ainyc/canonry-db": "0.0.0",
69
+ "@ainyc/canonry-integration-bing": "0.0.0",
70
70
  "@ainyc/canonry-integration-traffic": "0.0.0",
71
+ "@ainyc/canonry-integration-google": "0.0.0",
71
72
  "@ainyc/canonry-integration-wordpress": "0.0.0",
72
73
  "@ainyc/canonry-provider-cdp": "0.0.0",
73
74
  "@ainyc/canonry-provider-claude": "0.0.0",
74
- "@ainyc/canonry-integration-bing": "0.0.0",
75
75
  "@ainyc/canonry-provider-gemini": "0.0.0",
76
76
  "@ainyc/canonry-provider-local": "0.0.0",
77
77
  "@ainyc/canonry-provider-perplexity": "0.0.0",