@canonry/canonry 4.177.2 → 4.177.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.
@@ -633,7 +633,7 @@ import {
633
633
  } from "./chunk-PQT3ECM3.js";
634
634
 
635
635
  // src/intelligence-service.ts
636
- import { eq as eq61, desc as desc27, asc as asc11, and as and50, ne as ne8, or as or14, inArray as inArray21, gte as gte14, lte as lte12 } from "drizzle-orm";
636
+ import { eq as eq61, desc as desc27, asc as asc11, and as and50, ne as ne8, or as or14, inArray as inArray21, gte as gte14, lte as lte12, isNull as isNull8, sql as sql25, exists } from "drizzle-orm";
637
637
 
638
638
  // ../db/src/client.ts
639
639
  import { mkdirSync } from "fs";
@@ -7208,6 +7208,13 @@ function detectRegressions(currentRun, previousRun) {
7208
7208
  return regressions;
7209
7209
  }
7210
7210
 
7211
+ // ../intelligence/src/observation-coverage.ts
7212
+ function observedKeys(run, keyOf) {
7213
+ const observed2 = /* @__PURE__ */ new Set();
7214
+ for (const snap of run.snapshots) observed2.add(keyOf(snap));
7215
+ return observed2;
7216
+ }
7217
+
7211
7218
  // ../intelligence/src/gains.ts
7212
7219
  function snapshotKey2(snap) {
7213
7220
  const loc = snap.location ?? "__none__";
@@ -7218,6 +7225,7 @@ function detectGains(currentRun, previousRun) {
7218
7225
  return [];
7219
7226
  }
7220
7227
  const gains = [];
7228
+ const previousObserved = observedKeys(previousRun, snapshotKey2);
7221
7229
  const previousCited = /* @__PURE__ */ new Set();
7222
7230
  for (const snap of previousRun.snapshots) {
7223
7231
  if (snap.cited) {
@@ -7226,7 +7234,7 @@ function detectGains(currentRun, previousRun) {
7226
7234
  }
7227
7235
  for (const snap of currentRun.snapshots) {
7228
7236
  const key = snapshotKey2(snap);
7229
- if (snap.cited && !previousCited.has(key)) {
7237
+ if (snap.cited && previousObserved.has(key) && !previousCited.has(key)) {
7230
7238
  gains.push({
7231
7239
  query: snap.query,
7232
7240
  provider: snap.provider,
@@ -7452,6 +7460,7 @@ function generateInsights(input) {
7452
7460
 
7453
7461
  // ../intelligence/src/first-citations.ts
7454
7462
  function detectFirstCitations(currentRun, previousRun) {
7463
+ const previousObservedQueries = observedKeys(previousRun, (snap) => snap.query);
7455
7464
  const previousCitedQueries = /* @__PURE__ */ new Set();
7456
7465
  for (const snap of previousRun.snapshots) {
7457
7466
  if (snap.cited) previousCitedQueries.add(snap.query);
@@ -7460,6 +7469,7 @@ function detectFirstCitations(currentRun, previousRun) {
7460
7469
  const seen = /* @__PURE__ */ new Set();
7461
7470
  for (const snap of currentRun.snapshots) {
7462
7471
  if (!snap.cited) continue;
7472
+ if (!previousObservedQueries.has(snap.query)) continue;
7463
7473
  if (previousCitedQueries.has(snap.query)) continue;
7464
7474
  const key = `${snap.query}:${snap.provider}`;
7465
7475
  if (seen.has(key)) continue;
@@ -7477,6 +7487,7 @@ function detectFirstCitations(currentRun, previousRun) {
7477
7487
 
7478
7488
  // ../intelligence/src/provider-pickups.ts
7479
7489
  function detectProviderPickups(currentRun, previousRun) {
7490
+ const previousObservedPairs = observedKeys(previousRun, (snap) => `${snap.query}:${snap.provider}`);
7480
7491
  const previousCitedQueries = /* @__PURE__ */ new Set();
7481
7492
  const previousCitedPairs = /* @__PURE__ */ new Set();
7482
7493
  for (const snap of previousRun.snapshots) {
@@ -7490,6 +7501,7 @@ function detectProviderPickups(currentRun, previousRun) {
7490
7501
  if (!snap.cited) continue;
7491
7502
  if (!previousCitedQueries.has(snap.query)) continue;
7492
7503
  const key = `${snap.query}:${snap.provider}`;
7504
+ if (!previousObservedPairs.has(key)) continue;
7493
7505
  if (previousCitedPairs.has(key)) continue;
7494
7506
  if (seen.has(key)) continue;
7495
7507
  seen.add(key);
@@ -7551,11 +7563,13 @@ function detectCompetitorGains(currentRun, previousRun, opts) {
7551
7563
  if (tracked.size === 0) return [];
7552
7564
  const currentMap = buildCompetitorQueryMap(currentRun, tracked);
7553
7565
  const previousMap = buildCompetitorQueryMap(previousRun, tracked);
7566
+ const previousObservedQueries = observedKeys(previousRun, (snap) => snap.query);
7554
7567
  const result = [];
7555
7568
  for (const competitorDomain of tracked) {
7556
7569
  const currentQs = currentMap.get(competitorDomain) ?? /* @__PURE__ */ new Set();
7557
7570
  const previousQs = previousMap.get(competitorDomain) ?? /* @__PURE__ */ new Set();
7558
7571
  for (const query of currentQs) {
7572
+ if (!previousObservedQueries.has(query)) continue;
7559
7573
  if (previousQs.has(query)) continue;
7560
7574
  result.push({ query, competitorDomain });
7561
7575
  }
@@ -7567,11 +7581,13 @@ function detectCompetitorLosses(currentRun, previousRun, opts) {
7567
7581
  if (tracked.size === 0) return [];
7568
7582
  const currentMap = buildCompetitorQueryMap(currentRun, tracked);
7569
7583
  const previousMap = buildCompetitorQueryMap(previousRun, tracked);
7584
+ const currentObservedQueries = observedKeys(currentRun, (snap) => snap.query);
7570
7585
  const result = [];
7571
7586
  for (const competitorDomain of tracked) {
7572
7587
  const currentQs = currentMap.get(competitorDomain) ?? /* @__PURE__ */ new Set();
7573
7588
  const previousQs = previousMap.get(competitorDomain) ?? /* @__PURE__ */ new Set();
7574
7589
  for (const query of previousQs) {
7590
+ if (!currentObservedQueries.has(query)) continue;
7575
7591
  if (currentQs.has(query)) continue;
7576
7592
  result.push({ query, competitorDomain });
7577
7593
  }
@@ -51119,7 +51135,7 @@ async function queryBacklinks(opts) {
51119
51135
  const reversed = opts.targets.map(reverseDomain);
51120
51136
  const targetList = reversed.map(quote).join(", ");
51121
51137
  const limitClause = opts.limitPerTarget ? `QUALIFY row_number() OVER (PARTITION BY t.target_rev_domain ORDER BY v.num_hosts DESC) <= ${Math.floor(opts.limitPerTarget)}` : "";
51122
- const sql25 = `
51138
+ const sql26 = `
51123
51139
  WITH vertices AS (
51124
51140
  SELECT * FROM read_csv(
51125
51141
  ${quote(opts.vertexPath)},
@@ -51155,7 +51171,7 @@ async function queryBacklinks(opts) {
51155
51171
  const conn = await instance.connect();
51156
51172
  let rows;
51157
51173
  try {
51158
- const reader = await conn.runAndReadAll(sql25);
51174
+ const reader = await conn.runAndReadAll(sql26);
51159
51175
  rows = reader.getRowObjects();
51160
51176
  } finally {
51161
51177
  conn.disconnectSync?.();
@@ -77833,6 +77849,19 @@ function matchesBrandAlias(candidate, aliases) {
77833
77849
  var RECURRENCE_LOOKBACK_RUNS = 5;
77834
77850
  var HISTORY_WINDOW_RUNS = Math.max(PERSISTENT_GAP_THRESHOLD, 5);
77835
77851
  var log = createLogger("IntelligenceService");
77852
+ var runChronologyKey = sql25`coalesce(${runs.finishedAt}, ${runs.createdAt})`;
77853
+ function analyzableRunPredicates(projectId) {
77854
+ return [
77855
+ eq61(runs.projectId, projectId),
77856
+ // Only this kind writes query_snapshots.
77857
+ eq61(runs.kind, RunKinds["answer-visibility"]),
77858
+ or14(eq61(runs.status, RunStatuses.completed), eq61(runs.status, RunStatuses.partial)),
77859
+ // Defensive: RunCoordinator already skips probes before analyzeAndPersist
77860
+ // is called, but a future call site invoking it directly for a probe must
77861
+ // still not pollute the intelligence window.
77862
+ ne8(runs.trigger, RunTriggers.probe)
77863
+ ];
77864
+ }
77836
77865
  function readStoredGroundingSources(rawResponse) {
77837
77866
  const parsed = parseJsonColumn(rawResponse, {});
77838
77867
  const sources = parsed.groundingSources;
@@ -77865,26 +77894,27 @@ var IntelligenceService = class {
77865
77894
  * Returns the analysis result for the coordinator to inspect (e.g. for webhook dispatch).
77866
77895
  */
77867
77896
  analyzeAndPersist(runId, projectId) {
77897
+ const currentRunRecord = this.db.select().from(runs).where(and50(eq61(runs.id, runId), ...analyzableRunPredicates(projectId))).get();
77898
+ if (!currentRunRecord) {
77899
+ log.info("intelligence.skip", { runId, reason: "run not eligible for analysis" });
77900
+ return null;
77901
+ }
77902
+ const currentLocation = currentRunRecord.location ?? null;
77868
77903
  const recentRuns = this.db.select().from(runs).where(
77869
77904
  and50(
77870
- eq61(runs.projectId, projectId),
77871
- or14(eq61(runs.status, "completed"), eq61(runs.status, "partial")),
77872
- // Defensive: RunCoordinator already skips probes before this is
77873
- // called, but if a future call site invokes analyzeAndPersist
77874
- // directly for a probe, probes still must not pollute the
77875
- // intelligence window.
77876
- ne8(runs.trigger, RunTriggers.probe)
77905
+ ...analyzableRunPredicates(projectId),
77906
+ currentLocation === null ? isNull8(runs.location) : eq61(runs.location, currentLocation),
77907
+ // Never look forward. The window is this run's own past, so
77908
+ // re-analyzing a historical run compares it against its true
77909
+ // predecessor rather than against sweeps that came after it.
77910
+ lte12(runChronologyKey, currentRunRecord.finishedAt ?? currentRunRecord.createdAt),
77911
+ // MEASUREMENT: only runs that actually wrote snapshots — see above.
77912
+ // Index seek on idx_snapshots_run, short-circuited on the first row.
77913
+ exists(
77914
+ this.db.select({ one: sql25`1` }).from(querySnapshots).where(eq61(querySnapshots.runId, runs.id))
77915
+ )
77877
77916
  )
77878
77917
  ).orderBy(desc27(runs.finishedAt), desc27(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
77879
- if (recentRuns.length === 0) {
77880
- log.info("intelligence.skip", { runId, reason: "no completed runs" });
77881
- return null;
77882
- }
77883
- const currentRunRecord = recentRuns.find((r) => r.id === runId);
77884
- if (!currentRunRecord) {
77885
- log.info("intelligence.skip", { runId, reason: "run not in recent completed list" });
77886
- return null;
77887
- }
77888
77918
  const trackedCompetitors = this.loadTrackedCompetitors(projectId);
77889
77919
  const currentRun = this.buildRunData(
77890
77920
  runId,
@@ -77898,10 +77928,8 @@ var IntelligenceService = class {
77898
77928
  return null;
77899
77929
  }
77900
77930
  const orderedRecent = [...recentRuns].reverse();
77901
- const currentLocation = currentRunRecord.location ?? null;
77902
- const sameLocationOrdered = orderedRecent.filter((r) => (r.location ?? null) === currentLocation);
77903
- const currentLocIdx = sameLocationOrdered.findIndex((r) => r.id === runId);
77904
- const previousRunRecord = currentLocIdx > 0 ? sameLocationOrdered[currentLocIdx - 1] : null;
77931
+ const currentIdx = orderedRecent.findIndex((r) => r.id === runId);
77932
+ const previousRunRecord = currentIdx > 0 ? orderedRecent[currentIdx - 1] : null;
77905
77933
  const previousRun = previousRunRecord ? this.buildRunData(
77906
77934
  previousRunRecord.id,
77907
77935
  projectId,
@@ -77909,7 +77937,7 @@ var IntelligenceService = class {
77909
77937
  previousRunRecord.location ?? null,
77910
77938
  trackedCompetitors
77911
77939
  ) : null;
77912
- const history = sameLocationOrdered.slice(0, currentLocIdx + 1).map((r) => r.id === runId ? currentRun : this.buildRunData(
77940
+ const history = orderedRecent.slice(0, currentIdx + 1).map((r) => r.id === runId ? currentRun : this.buildRunData(
77913
77941
  r.id,
77914
77942
  projectId,
77915
77943
  r.finishedAt ?? r.createdAt,
@@ -78184,14 +78212,9 @@ var IntelligenceService = class {
78184
78212
  }
78185
78213
  sinceTimestamp = parsed;
78186
78214
  }
78187
- const allRuns = this.db.select().from(runs).where(
78188
- and50(
78189
- eq61(runs.projectId, project.id),
78190
- or14(eq61(runs.status, "completed"), eq61(runs.status, "partial")),
78191
- // Backfill must not replay probe runs as if they were real sweeps.
78192
- ne8(runs.trigger, RunTriggers.probe)
78193
- )
78194
- ).orderBy(asc11(runs.finishedAt)).all();
78215
+ const allRuns = this.db.select().from(runs).where(and50(...analyzableRunPredicates(project.id))).orderBy(asc11(runs.finishedAt)).all();
78216
+ const measuredRunIds = this.runIdsWithSnapshots(allRuns.map((r) => r.id));
78217
+ const measuredRuns = allRuns.filter((r) => measuredRunIds.has(r.id));
78195
78218
  let startIdx = 0;
78196
78219
  let endIdx = allRuns.length;
78197
78220
  if (opts?.fromRunId) {
@@ -78229,7 +78252,7 @@ var IntelligenceService = class {
78229
78252
  for (let i = 0; i < targetRuns.length; i++) {
78230
78253
  const run = targetRuns[i];
78231
78254
  const runLocation = run.location ?? null;
78232
- const sameLocationRuns = allRuns.filter((r) => (r.location ?? null) === runLocation);
78255
+ const sameLocationRuns = measuredRuns.filter((r) => (r.location ?? null) === runLocation);
78233
78256
  const sameLocIdx = sameLocationRuns.indexOf(run);
78234
78257
  const previousRun = sameLocIdx > 0 ? sameLocationRuns[sameLocIdx - 1] : null;
78235
78258
  const historyStart = Math.max(0, sameLocIdx - (HISTORY_WINDOW_RUNS - 1));
@@ -78268,6 +78291,16 @@ var IntelligenceService = class {
78268
78291
  loadTrackedCompetitors(projectId) {
78269
78292
  return this.db.select({ domain: competitors.domain }).from(competitors).where(eq61(competitors.projectId, projectId)).all().map((r) => r.domain);
78270
78293
  }
78294
+ /**
78295
+ * Which of `runIds` actually wrote query snapshots. One grouped read, so
78296
+ * callers can drop the runs that measured nothing before paying to build
78297
+ * RunData for them. An empty input returns an empty set without querying.
78298
+ */
78299
+ runIdsWithSnapshots(runIds) {
78300
+ if (runIds.length === 0) return /* @__PURE__ */ new Set();
78301
+ const rows = this.db.select({ runId: querySnapshots.runId }).from(querySnapshots).where(inArray21(querySnapshots.runId, [...runIds])).groupBy(querySnapshots.runId).all();
78302
+ return new Set(rows.map((r) => r.runId));
78303
+ }
78271
78304
  /**
78272
78305
  * Wipe transition signals from an analysis result while keeping health.
78273
78306
  * Used when there's no baseline (first run) to avoid emitting false transitions.
@@ -145,7 +145,7 @@ import {
145
145
  siteCrawlSnapshots,
146
146
  toAlertView,
147
147
  usageCounters
148
- } from "./chunk-GJE334DA.js";
148
+ } from "./chunk-5EH666EO.js";
149
149
  import {
150
150
  AGENT_MEMORY_VALUE_MAX_BYTES,
151
151
  AGENT_PROVIDER_IDS,
@@ -11839,7 +11839,7 @@ function readStoredGroundingSources(rawResponse) {
11839
11839
  return result;
11840
11840
  }
11841
11841
  async function backfillInsightsCommand(project, opts) {
11842
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-C57RIB37.js");
11842
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-CHZGHFGS.js");
11843
11843
  const config = loadConfig();
11844
11844
  const db = createClient(config.database);
11845
11845
  migrate(db);
package/dist/cli.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  showFirstRunNotice,
27
27
  trackCliCommandFinished,
28
28
  trackEvent
29
- } from "./chunk-XFAPF2YX.js";
29
+ } from "./chunk-7ABFIVKO.js";
30
30
  import {
31
31
  autoSyncSkills,
32
32
  formatAutoSyncNotice
@@ -69,7 +69,7 @@ import {
69
69
  projects,
70
70
  queries,
71
71
  renderReportHtml
72
- } from "./chunk-GJE334DA.js";
72
+ } from "./chunk-5EH666EO.js";
73
73
  import {
74
74
  AdsDeliverySnapshotStatuses,
75
75
  AdsHistoricalCampaignRollupStatuses,
package/dist/index.js CHANGED
@@ -3,11 +3,11 @@ import {
3
3
  createGoogleMarketingCredentialStore,
4
4
  createGoogleMarketingRuntime,
5
5
  createServer
6
- } from "./chunk-XFAPF2YX.js";
6
+ } from "./chunk-7ABFIVKO.js";
7
7
  import {
8
8
  loadConfig
9
9
  } from "./chunk-6XUWDSLN.js";
10
- import "./chunk-GJE334DA.js";
10
+ import "./chunk-5EH666EO.js";
11
11
  import "./chunk-PQT3ECM3.js";
12
12
  export {
13
13
  GoogleMarketingRuntimeError,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-GJE334DA.js";
3
+ } from "./chunk-5EH666EO.js";
4
4
  import "./chunk-PQT3ECM3.js";
5
5
  export {
6
6
  IntelligenceService
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonry/canonry",
3
- "version": "4.177.2",
3
+ "version": "4.177.3",
4
4
  "type": "module",
5
5
  "description": "Self-hosted AI visibility (AEO) platform: track how ChatGPT, Claude, Gemini, and Perplexity cite your domain, join it with Search Console, GA4, server-side traffic, and paid media, and fix what you find through agent tools (CLI, REST, MCP). Local SQLite.",
6
6
  "license": "FSL-1.1-ALv2",
@@ -71,30 +71,30 @@
71
71
  "tsup": "^8.5.1",
72
72
  "tsx": "^4.19.0",
73
73
  "@ainyc/canonry-api-client": "0.0.0",
74
- "@ainyc/canonry-contracts": "0.0.0",
75
74
  "@ainyc/canonry-api-routes": "0.0.0",
76
75
  "@ainyc/canonry-config": "0.0.0",
77
76
  "@ainyc/canonry-db": "0.0.0",
77
+ "@ainyc/canonry-contracts": "0.0.0",
78
78
  "@ainyc/canonry-integration-bing": "0.0.0",
79
- "@ainyc/canonry-integration-cloudflare-worker": "0.0.0",
80
- "@ainyc/canonry-integration-cloudflare-queue": "0.0.0",
81
79
  "@ainyc/canonry-integration-cloud-run": "0.0.0",
82
- "@ainyc/canonry-integration-google": "0.0.0",
80
+ "@ainyc/canonry-integration-cloudflare-queue": "0.0.0",
81
+ "@ainyc/canonry-integration-cloudflare-worker": "0.0.0",
83
82
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
84
83
  "@ainyc/canonry-integration-google-ads": "0.0.0",
85
84
  "@ainyc/canonry-integration-google-business-profile": "0.0.0",
85
+ "@ainyc/canonry-integration-google": "0.0.0",
86
86
  "@ainyc/canonry-integration-google-places": "0.0.0",
87
87
  "@ainyc/canonry-integration-google-tag-manager": "0.0.0",
88
+ "@ainyc/canonry-integration-traffic": "0.0.0",
88
89
  "@ainyc/canonry-integration-openai-ads": "0.0.0",
89
90
  "@ainyc/canonry-integration-wordpress": "0.0.0",
90
- "@ainyc/canonry-intelligence": "0.0.0",
91
- "@ainyc/canonry-provider-claude": "0.0.0",
91
+ "@ainyc/canonry-provider-gemini": "0.0.0",
92
92
  "@ainyc/canonry-provider-cdp": "0.0.0",
93
+ "@ainyc/canonry-intelligence": "0.0.0",
93
94
  "@ainyc/canonry-provider-local": "0.0.0",
94
95
  "@ainyc/canonry-provider-openai": "0.0.0",
95
- "@ainyc/canonry-provider-gemini": "0.0.0",
96
- "@ainyc/canonry-provider-perplexity": "0.0.0",
97
- "@ainyc/canonry-integration-traffic": "0.0.0"
96
+ "@ainyc/canonry-provider-claude": "0.0.0",
97
+ "@ainyc/canonry-provider-perplexity": "0.0.0"
98
98
  },
99
99
  "scripts": {
100
100
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",