@ainyc/canonry 4.47.0 → 4.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/assets/agent-workspace/skills/aero/SKILL.md +11 -0
  2. package/assets/agent-workspace/skills/aero/references/orchestration.md +8 -0
  3. package/assets/agent-workspace/skills/aero/soul.md +1 -1
  4. package/assets/agent-workspace/skills/canonry/SKILL.md +2 -0
  5. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +14 -1
  6. package/assets/assets/BacklinksPage-DIZCcqsP.js +1 -0
  7. package/assets/assets/ChartPrimitives-9Kx3gzQL.js +1 -0
  8. package/assets/assets/ProjectPage-R2cxJb5Y.js +6 -0
  9. package/assets/assets/RunRow-DqezNIUy.js +1 -0
  10. package/assets/assets/RunsPage-CfvTJ9Ny.js +1 -0
  11. package/assets/assets/SettingsPage-HfMGIa5v.js +1 -0
  12. package/assets/assets/TrafficPage-DV_Dvpl3.js +1 -0
  13. package/assets/assets/TrafficSourceDetailPage-lefreKBO.js +1 -0
  14. package/assets/assets/arrow-left-DpxpMUNt.js +1 -0
  15. package/assets/assets/index-DKBPD33e.js +210 -0
  16. package/assets/assets/{index-BDMNXVHa.css → index-DeGyEwik.css} +1 -1
  17. package/assets/assets/server-traffic-Bm8iKtXK.js +1 -0
  18. package/assets/assets/trash-2-CnBiLbiZ.js +1 -0
  19. package/assets/assets/vendor-markdown-DK7fbRNb.js +14 -0
  20. package/assets/assets/vendor-radix-B57xfQbP.js +45 -0
  21. package/assets/assets/vendor-recharts-DWvKDyBF.js +36 -0
  22. package/assets/assets/vendor-tanstack-Dq7p98wZ.js +1 -0
  23. package/assets/index.html +6 -2
  24. package/dist/{chunk-4WXY57ET.js → chunk-2ARCCG5E.js} +1652 -753
  25. package/dist/{chunk-M7MSNUNQ.js → chunk-DLDLDWH4.js} +142 -44
  26. package/dist/{chunk-WYBKCDUH.js → chunk-FDR3G6SB.js} +2780 -2018
  27. package/dist/chunk-GGXU5VKI.js +5778 -0
  28. package/dist/cli.js +17 -8
  29. package/dist/index.js +4 -4
  30. package/dist/{intelligence-service-ADZRFCGO.js → intelligence-service-XMZEWLCW.js} +2 -2
  31. package/dist/mcp.js +2 -2
  32. package/package.json +7 -6
  33. package/assets/assets/index-CPUAzk7n.js +0 -302
  34. package/dist/chunk-ON545FBK.js +0 -2369
@@ -2,6 +2,7 @@ import {
2
2
  CitationStates,
3
3
  ContentActions,
4
4
  RunKinds,
5
+ RunTriggers,
5
6
  __export,
6
7
  brandKeyFromText,
7
8
  brandLabelFromDomain,
@@ -9,10 +10,10 @@ import {
9
10
  categoryLabel,
10
11
  determineAnswerMentioned,
11
12
  normalizeProjectDomain
12
- } from "./chunk-4WXY57ET.js";
13
+ } from "./chunk-2ARCCG5E.js";
13
14
 
14
15
  // src/intelligence-service.ts
15
- import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
16
+ import { eq, desc, asc, and, ne, or, inArray } from "drizzle-orm";
16
17
 
17
18
  // ../db/src/client.ts
18
19
  import { mkdirSync } from "fs";
@@ -36,6 +37,7 @@ __export(schema_exports, {
36
37
  bingUrlInspections: () => bingUrlInspections,
37
38
  ccReleaseSyncs: () => ccReleaseSyncs,
38
39
  competitors: () => competitors,
40
+ contentTargetDismissals: () => contentTargetDismissals,
39
41
  crawlerEventsHourly: () => crawlerEventsHourly,
40
42
  discoveryProbes: () => discoveryProbes,
41
43
  discoverySessions: () => discoverySessions,
@@ -57,6 +59,7 @@ __export(schema_exports, {
57
59
  queries: () => queries,
58
60
  querySnapshots: () => querySnapshots,
59
61
  rawEventSamples: () => rawEventSamples,
62
+ recommendationExplanations: () => recommendationExplanations,
60
63
  runs: () => runs,
61
64
  schedules: () => schedules,
62
65
  trafficSources: () => trafficSources,
@@ -68,16 +71,16 @@ var projects = sqliteTable("projects", {
68
71
  name: text("name").notNull().unique(),
69
72
  displayName: text("display_name").notNull(),
70
73
  canonicalDomain: text("canonical_domain").notNull(),
71
- ownedDomains: text("owned_domains").notNull().default("[]"),
72
- aliases: text("aliases").notNull().default("[]"),
74
+ ownedDomains: text("owned_domains", { mode: "json" }).$type().notNull().default([]),
75
+ aliases: text("aliases", { mode: "json" }).$type().notNull().default([]),
73
76
  country: text("country").notNull(),
74
77
  language: text("language").notNull(),
75
- tags: text("tags").notNull().default("[]"),
76
- labels: text("labels").notNull().default("{}"),
77
- providers: text("providers").notNull().default("[]"),
78
- locations: text("locations").notNull().default("[]"),
78
+ tags: text("tags", { mode: "json" }).$type().notNull().default([]),
79
+ labels: text("labels", { mode: "json" }).$type().notNull().default({}),
80
+ providers: text("providers", { mode: "json" }).$type().notNull().default([]),
81
+ locations: text("locations", { mode: "json" }).$type().notNull().default([]),
79
82
  defaultLocation: text("default_location"),
80
- autoExtractBacklinks: integer("auto_extract_backlinks").notNull().default(0),
83
+ autoExtractBacklinks: integer("auto_extract_backlinks", { mode: "boolean" }).notNull().default(false),
81
84
  configSource: text("config_source").notNull().default("cli"),
82
85
  configRevision: integer("config_revision").notNull().default(1),
83
86
  icpDescription: text("icp_description"),
@@ -111,7 +114,7 @@ var runs = sqliteTable("runs", {
111
114
  status: text("status").notNull().default("queued"),
112
115
  trigger: text("trigger").notNull().default("manual"),
113
116
  location: text("location"),
114
- queries: text("queries"),
117
+ queries: text("queries", { mode: "json" }).$type(),
115
118
  sourceId: text("source_id"),
116
119
  startedAt: text("started_at"),
117
120
  finishedAt: text("finished_at"),
@@ -138,9 +141,9 @@ var querySnapshots = sqliteTable("query_snapshots", {
138
141
  citationState: text("citation_state").notNull(),
139
142
  answerMentioned: integer("answer_mentioned", { mode: "boolean" }),
140
143
  answerText: text("answer_text"),
141
- citedDomains: text("cited_domains").notNull().default("[]"),
142
- competitorOverlap: text("competitor_overlap").notNull().default("[]"),
143
- recommendedCompetitors: text("recommended_competitors").notNull().default("[]"),
144
+ citedDomains: text("cited_domains", { mode: "json" }).$type().notNull().default([]),
145
+ competitorOverlap: text("competitor_overlap", { mode: "json" }).$type().notNull().default([]),
146
+ recommendedCompetitors: text("recommended_competitors", { mode: "json" }).$type().notNull().default([]),
144
147
  location: text("location"),
145
148
  screenshotPath: text("screenshot_path"),
146
149
  rawResponse: text("raw_response"),
@@ -176,7 +179,7 @@ var apiKeys = sqliteTable("api_keys", {
176
179
  name: text("name").notNull(),
177
180
  keyHash: text("key_hash").notNull().unique(),
178
181
  keyPrefix: text("key_prefix").notNull(),
179
- scopes: text("scopes").notNull().default('["*"]'),
182
+ scopes: text("scopes", { mode: "json" }).$type().notNull().default(["*"]),
180
183
  createdAt: text("created_at").notNull(),
181
184
  lastUsedAt: text("last_used_at"),
182
185
  revokedAt: text("revoked_at")
@@ -194,8 +197,8 @@ var schedules = sqliteTable("schedules", {
194
197
  cronExpr: text("cron_expr").notNull(),
195
198
  preset: text("preset"),
196
199
  timezone: text("timezone").notNull().default("UTC"),
197
- enabled: integer("enabled").notNull().default(1),
198
- providers: text("providers").notNull().default("[]"),
200
+ enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
201
+ providers: text("providers", { mode: "json" }).$type().notNull().default([]),
199
202
  /** Optional traffic-source UUID for traffic-sync schedules. Null for other kinds. */
200
203
  sourceId: text("source_id"),
201
204
  lastRunAt: text("last_run_at"),
@@ -209,9 +212,9 @@ var notifications = sqliteTable("notifications", {
209
212
  id: text("id").primaryKey(),
210
213
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
211
214
  channel: text("channel").notNull(),
212
- config: text("config").notNull(),
215
+ config: text("config", { mode: "json" }).$type().notNull(),
213
216
  webhookSecret: text("webhook_secret"),
214
- enabled: integer("enabled").notNull().default(1),
217
+ enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
215
218
  createdAt: text("created_at").notNull(),
216
219
  updatedAt: text("updated_at").notNull()
217
220
  }, (table) => [
@@ -223,7 +226,7 @@ var googleConnections = sqliteTable("google_connections", {
223
226
  connectionType: text("connection_type").notNull(),
224
227
  propertyId: text("property_id"),
225
228
  sitemapUrl: text("sitemap_url"),
226
- scopes: text("scopes").notNull().default("[]"),
229
+ scopes: text("scopes", { mode: "json" }).$type().notNull().default([]),
227
230
  createdAt: text("created_at").notNull(),
228
231
  updatedAt: text("updated_at").notNull()
229
232
  }, (table) => [
@@ -260,9 +263,9 @@ var gscUrlInspections = sqliteTable("gsc_url_inspections", {
260
263
  robotsTxtState: text("robots_txt_state"),
261
264
  crawlTime: text("crawl_time"),
262
265
  lastCrawlResult: text("last_crawl_result"),
263
- isMobileFriendly: integer("is_mobile_friendly"),
264
- richResults: text("rich_results").notNull().default("[]"),
265
- referringUrls: text("referring_urls").notNull().default("[]"),
266
+ isMobileFriendly: integer("is_mobile_friendly", { mode: "boolean" }),
267
+ richResults: text("rich_results", { mode: "json" }).$type().notNull().default([]),
268
+ referringUrls: text("referring_urls", { mode: "json" }).$type().notNull().default([]),
266
269
  inspectedAt: text("inspected_at").notNull(),
267
270
  createdAt: text("created_at").notNull()
268
271
  }, (table) => [
@@ -277,7 +280,7 @@ var gscCoverageSnapshots = sqliteTable("gsc_coverage_snapshots", {
277
280
  date: text("date").notNull(),
278
281
  indexed: integer("indexed").notNull().default(0),
279
282
  notIndexed: integer("not_indexed").notNull().default(0),
280
- reasonBreakdown: text("reason_breakdown").notNull().default("{}"),
283
+ reasonBreakdown: text("reason_breakdown", { mode: "json" }).$type().notNull().default({}),
281
284
  createdAt: text("created_at").notNull()
282
285
  }, (table) => [
283
286
  index("idx_gsc_coverage_snap_project_date").on(table.projectId, table.date),
@@ -310,7 +313,7 @@ var bingUrlInspections = sqliteTable("bing_url_inspections", {
310
313
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
311
314
  url: text("url").notNull(),
312
315
  httpCode: integer("http_code"),
313
- inIndex: integer("in_index"),
316
+ inIndex: integer("in_index", { mode: "boolean" }),
314
317
  lastCrawledDate: text("last_crawled_date"),
315
318
  inIndexDate: text("in_index_date"),
316
319
  inspectedAt: text("inspected_at").notNull(),
@@ -471,8 +474,8 @@ var insights = sqliteTable("insights", {
471
474
  title: text("title").notNull(),
472
475
  query: text("query").notNull(),
473
476
  provider: text("provider").notNull(),
474
- recommendation: text("recommendation"),
475
- cause: text("cause"),
477
+ recommendation: text("recommendation", { mode: "json" }).$type(),
478
+ cause: text("cause", { mode: "json" }).$type(),
476
479
  dismissed: integer("dismissed", { mode: "boolean" }).notNull().default(false),
477
480
  createdAt: text("created_at").notNull()
478
481
  }, (table) => [
@@ -488,7 +491,7 @@ var healthSnapshots = sqliteTable("health_snapshots", {
488
491
  overallCitedRate: text("overall_cited_rate").notNull(),
489
492
  totalPairs: integer("total_pairs").notNull(),
490
493
  citedPairs: integer("cited_pairs").notNull(),
491
- providerBreakdown: text("provider_breakdown").notNull().default("{}"),
494
+ providerBreakdown: text("provider_breakdown", { mode: "json" }).$type().notNull().default({}),
492
495
  createdAt: text("created_at").notNull()
493
496
  }, (table) => [
494
497
  index("idx_health_snapshots_project").on(table.projectId),
@@ -588,9 +591,9 @@ var trafficSources = sqliteTable("traffic_sources", {
588
591
  // observed in the most recent successful sync. Bounded ring buffer used to
589
592
  // dedupe across sync runs at the boundary timestamp where lastSyncedAt
590
593
  // clamping alone leaves a small overlap window.
591
- lastEventIds: text("last_event_ids"),
594
+ lastEventIds: text("last_event_ids", { mode: "json" }).$type(),
592
595
  archivedAt: text("archived_at"),
593
- configJson: text("config_json").notNull().default("{}"),
596
+ configJson: text("config_json", { mode: "json" }).$type().notNull().default({}),
594
597
  createdAt: text("created_at").notNull(),
595
598
  updatedAt: text("updated_at").notNull()
596
599
  }, (table) => [
@@ -666,7 +669,7 @@ var rawEventSamples = sqliteTable("raw_event_samples", {
666
669
  pathNormalized: text("path_normalized").notNull(),
667
670
  status: integer("status"),
668
671
  refererHost: text("referer_host"),
669
- classifierDetailsJson: text("classifier_details_json").notNull().default("{}"),
672
+ classifierDetailsJson: text("classifier_details_json", { mode: "json" }).$type().notNull().default({}),
670
673
  createdAt: text("created_at").notNull()
671
674
  }, (table) => [
672
675
  index("idx_raw_event_samples_project_ts").on(table.projectId, table.ts),
@@ -687,7 +690,7 @@ var discoverySessions = sqliteTable("discovery_sessions", {
687
690
  citedCount: integer("cited_count"),
688
691
  aspirationalCount: integer("aspirational_count"),
689
692
  wastedCount: integer("wasted_count"),
690
- competitorMap: text("competitor_map").notNull().default("[]"),
693
+ competitorMap: text("competitor_map", { mode: "json" }).$type().notNull().default([]),
691
694
  error: text("error"),
692
695
  startedAt: text("started_at"),
693
696
  finishedAt: text("finished_at"),
@@ -703,13 +706,43 @@ var discoveryProbes = sqliteTable("discovery_probes", {
703
706
  query: text("query").notNull(),
704
707
  bucket: text("bucket"),
705
708
  citationState: text("citation_state").notNull(),
706
- citedDomains: text("cited_domains").notNull().default("[]"),
709
+ citedDomains: text("cited_domains", { mode: "json" }).$type().notNull().default([]),
707
710
  rawResponse: text("raw_response"),
708
711
  createdAt: text("created_at").notNull()
709
712
  }, (table) => [
710
713
  index("idx_discovery_probes_session").on(table.sessionId),
711
714
  index("idx_discovery_probes_project").on(table.projectId)
712
715
  ]);
716
+ var contentTargetDismissals = sqliteTable("content_target_dismissals", {
717
+ id: text("id").primaryKey(),
718
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
719
+ targetRef: text("target_ref").notNull(),
720
+ addressedUrl: text("addressed_url"),
721
+ note: text("note"),
722
+ dismissedAt: text("dismissed_at").notNull()
723
+ }, (table) => [
724
+ uniqueIndex("idx_content_target_dismissals_project_ref").on(table.projectId, table.targetRef),
725
+ index("idx_content_target_dismissals_project").on(table.projectId)
726
+ ]);
727
+ var recommendationExplanations = sqliteTable("recommendation_explanations", {
728
+ id: text("id").primaryKey(),
729
+ projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
730
+ targetRef: text("target_ref").notNull(),
731
+ promptVersion: text("prompt_version").notNull(),
732
+ provider: text("provider").notNull(),
733
+ model: text("model").notNull(),
734
+ responseText: text("response_text").notNull(),
735
+ /** Estimated cost in millicents (1/100 of a cent) for audit; 0 if unknown. */
736
+ costMillicents: integer("cost_millicents").notNull().default(0),
737
+ generatedAt: text("generated_at").notNull()
738
+ }, (table) => [
739
+ uniqueIndex("idx_recommendation_explanations_unique").on(
740
+ table.projectId,
741
+ table.targetRef,
742
+ table.promptVersion
743
+ ),
744
+ index("idx_recommendation_explanations_project").on(table.projectId)
745
+ ]);
713
746
  var migrationsTable = sqliteTable("_migrations", {
714
747
  version: integer("version").primaryKey(),
715
748
  name: text("name").notNull(),
@@ -1941,6 +1974,55 @@ var MIGRATION_VERSIONS = [
1941
1974
  `CREATE INDEX IF NOT EXISTS idx_audit_log_project ON audit_log(project_id)`,
1942
1975
  `CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at)`
1943
1976
  ]
1977
+ },
1978
+ {
1979
+ version: 61,
1980
+ name: "content-target-dismissals",
1981
+ // Persistent per-recommendation dismissal so users can mark a content
1982
+ // opportunity "addressed" after they ship the page. The orchestrator
1983
+ // recomputes opportunities on every report load from live GSC / GA
1984
+ // inventory; without persistent dismissal, a recommendation lingers
1985
+ // until the next sync surfaces the new page (days–weeks of lag).
1986
+ //
1987
+ // Keyed by `(project_id, target_ref)` where `target_ref` is the stable
1988
+ // hash that `computeTargetRef()` already produces — same value the
1989
+ // ContentTargetRowDto exposes, so the client passes back the ref it
1990
+ // sees.
1991
+ statements: [
1992
+ `CREATE TABLE IF NOT EXISTS content_target_dismissals (
1993
+ id TEXT PRIMARY KEY,
1994
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
1995
+ target_ref TEXT NOT NULL,
1996
+ addressed_url TEXT,
1997
+ note TEXT,
1998
+ dismissed_at TEXT NOT NULL
1999
+ )`,
2000
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_content_target_dismissals_project_ref ON content_target_dismissals(project_id, target_ref)`,
2001
+ `CREATE INDEX IF NOT EXISTS idx_content_target_dismissals_project ON content_target_dismissals(project_id)`
2002
+ ]
2003
+ },
2004
+ {
2005
+ version: 62,
2006
+ name: "recommendation-explanations",
2007
+ // LLM-generated rationale for content recommendations. Cached per
2008
+ // (project, target_ref, prompt_version) so repeat clicks are free.
2009
+ // Bumping the prompt version invalidates the cache forward without
2010
+ // touching the table.
2011
+ statements: [
2012
+ `CREATE TABLE IF NOT EXISTS recommendation_explanations (
2013
+ id TEXT PRIMARY KEY,
2014
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
2015
+ target_ref TEXT NOT NULL,
2016
+ prompt_version TEXT NOT NULL,
2017
+ provider TEXT NOT NULL,
2018
+ model TEXT NOT NULL,
2019
+ response_text TEXT NOT NULL,
2020
+ cost_millicents INTEGER NOT NULL DEFAULT 0,
2021
+ generated_at TEXT NOT NULL
2022
+ )`,
2023
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_recommendation_explanations_unique ON recommendation_explanations(project_id, target_ref, prompt_version)`,
2024
+ `CREATE INDEX IF NOT EXISTS idx_recommendation_explanations_project ON recommendation_explanations(project_id)`
2025
+ ]
1944
2026
  }
1945
2027
  ];
1946
2028
  function isDuplicateColumnError(err) {
@@ -2635,9 +2717,15 @@ function isBlogShaped(path) {
2635
2717
  // ../intelligence/src/content-classifier.ts
2636
2718
  var SEO_STRONG_THRESHOLD = 10;
2637
2719
  var SEO_WEAK_THRESHOLD = 30;
2720
+ function isHomepageOnly(url) {
2721
+ if (url === "/" || url === "") return true;
2722
+ const stripped = url.split("?")[0].replace(/\/+$/, "");
2723
+ return stripped === "" || stripped === "/";
2724
+ }
2638
2725
  function classifyContentAction(input) {
2639
2726
  const { ourPage, ourPageInGroundingSources, ourPageHasSchema } = input;
2640
2727
  if (!ourPage) return "create";
2728
+ if (isHomepageOnly(ourPage.url)) return "create";
2641
2729
  if (ourPageInGroundingSources) {
2642
2730
  if (ourPageHasSchema === false) return "add-schema";
2643
2731
  return null;
@@ -2782,8 +2870,7 @@ function buildContentTargetRows(input) {
2782
2870
  const targetRef = computeTargetRef({
2783
2871
  projectId: input.projectId,
2784
2872
  query: cq.query,
2785
- action,
2786
- targetPage: ourPage?.url ?? null
2873
+ action
2787
2874
  });
2788
2875
  const winningCompetitor = pickTopCompetitor(cq.competitorGroundingUrls);
2789
2876
  const ourBestPage = ourPage ? {
@@ -2935,7 +3022,7 @@ function pickTopCompetitor(competitors2) {
2935
3022
  };
2936
3023
  }
2937
3024
  function computeTargetRef(input) {
2938
- const key = [input.projectId, input.query, input.action, input.targetPage ?? ""].join("|");
3025
+ const key = [input.projectId, input.query, input.action].join("|");
2939
3026
  let hash = 0;
2940
3027
  for (let i = 0; i < key.length; i++) {
2941
3028
  hash = (hash << 5) - hash + key.charCodeAt(i) | 0;
@@ -3875,7 +3962,12 @@ var IntelligenceService = class {
3875
3962
  const recentRuns = this.db.select().from(runs).where(
3876
3963
  and(
3877
3964
  eq(runs.projectId, projectId),
3878
- or(eq(runs.status, "completed"), eq(runs.status, "partial"))
3965
+ or(eq(runs.status, "completed"), eq(runs.status, "partial")),
3966
+ // Defensive: RunCoordinator already skips probes before this is
3967
+ // called, but if a future call site invokes analyzeAndPersist
3968
+ // directly for a probe, probes still must not pollute the
3969
+ // intelligence window.
3970
+ ne(runs.trigger, RunTriggers.probe)
3879
3971
  )
3880
3972
  ).orderBy(desc(runs.finishedAt), desc(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
3881
3973
  if (recentRuns.length === 0) {
@@ -4009,7 +4101,9 @@ var IntelligenceService = class {
4009
4101
  const allRuns = this.db.select().from(runs).where(
4010
4102
  and(
4011
4103
  eq(runs.projectId, project.id),
4012
- or(eq(runs.status, "completed"), eq(runs.status, "partial"))
4104
+ or(eq(runs.status, "completed"), eq(runs.status, "partial")),
4105
+ // Backfill must not replay probe runs as if they were real sweeps.
4106
+ ne(runs.trigger, RunTriggers.probe)
4013
4107
  )
4014
4108
  ).orderBy(asc(runs.finishedAt)).all();
4015
4109
  let startIdx = 0;
@@ -4128,8 +4222,8 @@ var IntelligenceService = class {
4128
4222
  title: insight.title,
4129
4223
  query: insight.query,
4130
4224
  provider: insight.provider,
4131
- recommendation: insight.recommendation ? JSON.stringify(insight.recommendation) : null,
4132
- cause: insight.cause ? JSON.stringify(insight.cause) : null,
4225
+ recommendation: insight.recommendation ?? null,
4226
+ cause: insight.cause ?? null,
4133
4227
  dismissed: wasDismissed,
4134
4228
  createdAt: insight.createdAt
4135
4229
  }).run();
@@ -4141,7 +4235,7 @@ var IntelligenceService = class {
4141
4235
  overallCitedRate: String(result.health.overallCitedRate),
4142
4236
  totalPairs: result.health.totalPairs,
4143
4237
  citedPairs: result.health.citedPairs,
4144
- providerBreakdown: JSON.stringify(result.health.providerBreakdown),
4238
+ providerBreakdown: result.health.providerBreakdown,
4145
4239
  createdAt: now
4146
4240
  }).run();
4147
4241
  });
@@ -4176,14 +4270,16 @@ var IntelligenceService = class {
4176
4270
  const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq(projects.id, projectId)).get();
4177
4271
  const locationCount = Math.max(
4178
4272
  1,
4179
- parseJsonColumn(projectRow?.locations ?? null, []).length
4273
+ (projectRow?.locations ?? []).length
4180
4274
  );
4181
4275
  const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
4182
4276
  const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
4183
4277
  and(
4184
4278
  eq(runs.projectId, projectId),
4185
4279
  eq(runs.kind, RunKinds["answer-visibility"]),
4186
- or(eq(runs.status, "completed"), eq(runs.status, "partial"))
4280
+ or(eq(runs.status, "completed"), eq(runs.status, "partial")),
4281
+ // Defensive — see top of file.
4282
+ ne(runs.trigger, RunTriggers.probe)
4187
4283
  )
4188
4284
  ).orderBy(desc(runs.createdAt), desc(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
4189
4285
  const recentGroups = groupRunsByCreatedAt(recentRunRows);
@@ -4250,8 +4346,8 @@ var IntelligenceService = class {
4250
4346
  orphanCount++;
4251
4347
  continue;
4252
4348
  }
4253
- const domains = parseJsonColumn(r.citedDomains, []);
4254
- const competitors2 = parseJsonColumn(r.competitorOverlap, []);
4349
+ const domains = r.citedDomains;
4350
+ const competitors2 = r.competitorOverlap;
4255
4351
  snapshots.push({
4256
4352
  query: resolvedQuery,
4257
4353
  provider: r.provider,
@@ -4311,6 +4407,8 @@ export {
4311
4407
  rawEventSamples,
4312
4408
  discoverySessions,
4313
4409
  discoveryProbes,
4410
+ contentTargetDismissals,
4411
+ recommendationExplanations,
4314
4412
  createClient,
4315
4413
  parseJsonColumn,
4316
4414
  extractLegacyCredentials,