@ainyc/canonry 4.33.1 → 4.34.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.
@@ -123,7 +123,14 @@ var runs = sqliteTable("runs", {
123
123
  var querySnapshots = sqliteTable("query_snapshots", {
124
124
  id: text("id").primaryKey(),
125
125
  runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
126
- queryId: text("query_id").notNull().references(() => queries.id, { onDelete: "cascade" }),
126
+ // `query_id` is nullable + `ON DELETE SET NULL` so historical snapshots
127
+ // outlive their queries row. Pre-v58 this FK cascaded — deleting a tracked
128
+ // query (PUT /queries replace, individual delete, `canonry apply` dropping
129
+ // one) silently wiped the entire citation history for that query. With SET
130
+ // NULL the snapshot survives; `queryText` keeps it self-describing when
131
+ // the queries row is gone.
132
+ queryId: text("query_id").references(() => queries.id, { onDelete: "set null" }),
133
+ queryText: text("query_text"),
127
134
  provider: text("provider").notNull().default("gemini"),
128
135
  model: text("model"),
129
136
  citationState: text("citation_state").notNull(),
@@ -1804,6 +1811,80 @@ var MIGRATION_VERSIONS = [
1804
1811
  statements: [
1805
1812
  `ALTER TABLE runs ADD COLUMN queries TEXT`
1806
1813
  ]
1814
+ },
1815
+ {
1816
+ version: 58,
1817
+ name: "snapshots-preserve-on-query-delete",
1818
+ // The legacy `query_snapshots.query_id` FK was `ON DELETE CASCADE`, so a
1819
+ // routine basket edit (PUT /queries replace, individual delete, `canonry
1820
+ // apply` dropping a query) silently destroyed every historical citation
1821
+ // snapshot for the removed queries — the regression history, transitions,
1822
+ // and competitor-overlap evidence that are canonry's whole value.
1823
+ //
1824
+ // Fix: rebuild `query_snapshots` with `query_id` nullable + `ON DELETE
1825
+ // SET NULL`, and add a denormalized `query_text` column populated from
1826
+ // `queries.query` via the join. SQLite can't change FK or NOT NULL in
1827
+ // place — same canonical table-rebuild pattern v53 used. All statements
1828
+ // run inside the migration runner's single transaction.
1829
+ //
1830
+ // `run_id` keeps `ON DELETE CASCADE` — deleting a run legitimately
1831
+ // removes its snapshots. Indexes are recreated on the renamed table.
1832
+ statements: [
1833
+ `CREATE TABLE IF NOT EXISTS query_snapshots_v58 (
1834
+ id TEXT PRIMARY KEY,
1835
+ run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
1836
+ query_id TEXT REFERENCES queries(id) ON DELETE SET NULL,
1837
+ query_text TEXT,
1838
+ provider TEXT NOT NULL DEFAULT 'gemini',
1839
+ model TEXT,
1840
+ citation_state TEXT NOT NULL,
1841
+ answer_mentioned INTEGER,
1842
+ answer_text TEXT,
1843
+ cited_domains TEXT NOT NULL DEFAULT '[]',
1844
+ competitor_overlap TEXT NOT NULL DEFAULT '[]',
1845
+ recommended_competitors TEXT NOT NULL DEFAULT '[]',
1846
+ location TEXT,
1847
+ screenshot_path TEXT,
1848
+ raw_response TEXT,
1849
+ created_at TEXT NOT NULL
1850
+ )`,
1851
+ // Backfill `query_text` from joined queries.query so existing snapshots
1852
+ // stay readable even if their query is later deleted.
1853
+ //
1854
+ // IMPORTANT: we use `q.id` (the JOINED queries.id), not `qs.query_id`.
1855
+ // Production DBs may already contain snapshots whose `qs.query_id`
1856
+ // dangles — a queries row was hard-deleted at some point without
1857
+ // cascading (PRAGMA foreign_keys was OFF, or pre-FK schema). Copying
1858
+ // `qs.query_id` directly would re-introduce those dangling refs into
1859
+ // the new table, which now validates them at INSERT (the new FK still
1860
+ // requires query_id values to match queries.id when non-null). Reading
1861
+ // through the LEFT JOIN forces every value to be either a valid `q.id`
1862
+ // or NULL — pre-existing orphans land with NULL `query_id` / NULL
1863
+ // `query_text`, preserving the snapshot row instead of failing the
1864
+ // migration. The May 2026 azcoatings DB had 459 such pre-existing
1865
+ // orphans; without this guard, migrate() throws SQLITE_CONSTRAINT_FOREIGNKEY.
1866
+ `INSERT INTO query_snapshots_v58 (
1867
+ id, run_id, query_id, query_text, provider, model, citation_state,
1868
+ answer_mentioned, answer_text, cited_domains, competitor_overlap,
1869
+ recommended_competitors, location, screenshot_path, raw_response,
1870
+ created_at
1871
+ )
1872
+ SELECT qs.id, qs.run_id, q.id, q.query, qs.provider, qs.model,
1873
+ qs.citation_state, qs.answer_mentioned, qs.answer_text,
1874
+ qs.cited_domains, qs.competitor_overlap, qs.recommended_competitors,
1875
+ qs.location, qs.screenshot_path, qs.raw_response, qs.created_at
1876
+ FROM query_snapshots qs
1877
+ LEFT JOIN queries q ON q.id = qs.query_id`,
1878
+ `DROP TABLE query_snapshots`,
1879
+ `ALTER TABLE query_snapshots_v58 RENAME TO query_snapshots`,
1880
+ // Recreate the indexes that didn't survive the rename.
1881
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_run ON query_snapshots(run_id)`,
1882
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_query ON query_snapshots(query_id)`,
1883
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_citation_state ON query_snapshots(citation_state)`,
1884
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_provider_model ON query_snapshots(provider, model)`,
1885
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_location ON query_snapshots(location)`,
1886
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON query_snapshots(created_at)`
1887
+ ]
1807
1888
  }
1808
1889
  ];
1809
1890
  function isDuplicateColumnError(err) {
@@ -1987,6 +2068,11 @@ function pickGroupRepresentative(group) {
1987
2068
  return best;
1988
2069
  }
1989
2070
 
2071
+ // ../db/src/snapshot-helpers.ts
2072
+ function filterTrackedSnapshots(rows) {
2073
+ return rows.filter((r) => r.queryId !== null);
2074
+ }
2075
+
1990
2076
  // ../intelligence/src/regressions.ts
1991
2077
  function detectRegressions(currentRun, previousRun) {
1992
2078
  const regressions = [];
@@ -3707,6 +3793,7 @@ export {
3707
3793
  migrate,
3708
3794
  groupRunsByCreatedAt,
3709
3795
  pickGroupRepresentative,
3796
+ filterTrackedSnapshots,
3710
3797
  isBlogShapedQuery,
3711
3798
  buildInventory,
3712
3799
  buildContentTargetRows,
@@ -44,6 +44,7 @@ import {
44
44
  discoverySessions,
45
45
  dropLegacyCredentialColumns,
46
46
  extractLegacyCredentials,
47
+ filterTrackedSnapshots,
47
48
  gaAiReferrals,
48
49
  gaSocialReferrals,
49
50
  gaTrafficSnapshots,
@@ -70,7 +71,7 @@ import {
70
71
  schedules,
71
72
  trafficSources,
72
73
  usageCounters
73
- } from "./chunk-BJXHETQW.js";
74
+ } from "./chunk-7256SFYT.js";
74
75
  import {
75
76
  AGENT_MEMORY_VALUE_MAX_BYTES,
76
77
  AGENT_PROVIDER_IDS,
@@ -2292,7 +2293,7 @@ async function analyticsRoutes(app) {
2292
2293
  });
2293
2294
  }
2294
2295
  const runIds = projectRuns.map((r) => r.id);
2295
- const rawSnapshots = app.db.select({
2296
+ const rawSnapshots = filterTrackedSnapshots(app.db.select({
2296
2297
  runId: querySnapshots.runId,
2297
2298
  queryId: querySnapshots.queryId,
2298
2299
  provider: querySnapshots.provider,
@@ -2300,7 +2301,7 @@ async function analyticsRoutes(app) {
2300
2301
  answerMentioned: querySnapshots.answerMentioned,
2301
2302
  answerText: querySnapshots.answerText,
2302
2303
  createdAt: querySnapshots.createdAt
2303
- }).from(querySnapshots).where(inArray2(querySnapshots.runId, runIds)).all();
2304
+ }).from(querySnapshots).where(inArray2(querySnapshots.runId, runIds)).all());
2304
2305
  const allSnapshots = rawSnapshots.map((s) => ({
2305
2306
  ...s,
2306
2307
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
@@ -2339,13 +2340,13 @@ async function analyticsRoutes(app) {
2339
2340
  const runIdToCreatedAt = new Map(windowRuns.map((r) => [r.id, r.createdAt]));
2340
2341
  const consistencyMap = /* @__PURE__ */ new Map();
2341
2342
  if (windowRunIds.length > 0) {
2342
- const allWindowSnaps = app.db.select({
2343
+ const allWindowSnaps = filterTrackedSnapshots(app.db.select({
2343
2344
  queryId: querySnapshots.queryId,
2344
2345
  runId: querySnapshots.runId,
2345
2346
  citationState: querySnapshots.citationState,
2346
2347
  answerMentioned: querySnapshots.answerMentioned,
2347
2348
  answerText: querySnapshots.answerText
2348
- }).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all();
2349
+ }).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all());
2349
2350
  for (const s of allWindowSnaps) {
2350
2351
  const timePoint = runIdToCreatedAt.get(s.runId) ?? s.runId;
2351
2352
  let entry = consistencyMap.get(s.queryId);
@@ -2358,7 +2359,7 @@ async function analyticsRoutes(app) {
2358
2359
  if (resolveSnapshotAnswerMentioned(s, project)) entry.mentionedRuns.add(timePoint);
2359
2360
  }
2360
2361
  }
2361
- const rawSnapshots = app.db.select({
2362
+ const rawSnapshots = filterTrackedSnapshots(app.db.select({
2362
2363
  queryId: querySnapshots.queryId,
2363
2364
  query: queries.query,
2364
2365
  provider: querySnapshots.provider,
@@ -2366,7 +2367,7 @@ async function analyticsRoutes(app) {
2366
2367
  answerMentioned: querySnapshots.answerMentioned,
2367
2368
  answerText: querySnapshots.answerText,
2368
2369
  competitorOverlap: querySnapshots.competitorOverlap
2369
- }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, latestGroupRunIds)).all();
2370
+ }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, latestGroupRunIds)).all());
2370
2371
  const snapshots = rawSnapshots.map((s) => ({
2371
2372
  ...s,
2372
2373
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
@@ -5173,7 +5174,7 @@ function buildCandidateQueries(opts) {
5173
5174
  const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq12(queries.projectId, opts.projectId)).all();
5174
5175
  const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
5175
5176
  const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
5176
- const snapshotRows = opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateQueryIds.includes(r.queryId));
5177
+ const snapshotRows = filterTrackedSnapshots(opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all()).filter((r) => candidateQueryIds.includes(r.queryId));
5177
5178
  const snapshotsByQuery = /* @__PURE__ */ new Map();
5178
5179
  for (const row of snapshotRows) {
5179
5180
  const list = snapshotsByQuery.get(row.queryId) ?? [];
@@ -5392,7 +5393,7 @@ function loadSnapshotsForRun(db, runId) {
5392
5393
  function loadSnapshotsForRunIds(db, runIds) {
5393
5394
  if (runIds.length === 0) return [];
5394
5395
  const rows = db.select().from(querySnapshots).where(inArray5(querySnapshots.runId, [...runIds])).all();
5395
- return rows.map((r) => ({
5396
+ return rows.filter((r) => r.queryId !== null).map((r) => ({
5396
5397
  id: r.id,
5397
5398
  runId: r.runId,
5398
5399
  queryId: r.queryId,
@@ -6761,8 +6762,9 @@ async function citationRoutes(app) {
6761
6762
  if (rawSnapshots.length === 0) {
6762
6763
  return reply.send(emptyCitationVisibility("no-runs-yet"));
6763
6764
  }
6764
- const snapshots = rawSnapshots.map((s) => ({
6765
+ const snapshots = rawSnapshots.filter((s) => s.queryId !== null).map((s) => ({
6765
6766
  ...s,
6767
+ queryId: s.queryId,
6766
6768
  runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
6767
6769
  }));
6768
6770
  const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, project.id)).all().map((c) => normalizeDomain2(c.domain)).filter((d) => d.length > 0);
@@ -6998,7 +7000,7 @@ async function compositeRoutes(app) {
6998
7000
  const limit = clampSearchLimit(request.query.limit);
6999
7001
  const escaped = escapeLikePattern(rawQuery);
7000
7002
  const pattern = `%${escaped}%`;
7001
- const snapshotMatches = app.db.select({
7003
+ const snapshotMatches = filterTrackedSnapshots(app.db.select({
7002
7004
  id: querySnapshots.id,
7003
7005
  runId: querySnapshots.runId,
7004
7006
  queryId: querySnapshots.queryId,
@@ -7020,7 +7022,7 @@ async function compositeRoutes(app) {
7020
7022
  like(queries.query, pattern)
7021
7023
  )
7022
7024
  )
7023
- ).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all();
7025
+ ).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all());
7024
7026
  const insightMatches = app.db.select().from(insights).where(
7025
7027
  and6(
7026
7028
  eq15(insights.projectId, project.id),
@@ -7094,7 +7096,7 @@ function summarizeRun(run) {
7094
7096
  function loadSnapshotsByRunIds(app, runIds) {
7095
7097
  const result = /* @__PURE__ */ new Map();
7096
7098
  if (runIds.length === 0) return result;
7097
- const rows = app.db.select({
7099
+ const rows = filterTrackedSnapshots(app.db.select({
7098
7100
  runId: querySnapshots.runId,
7099
7101
  queryId: querySnapshots.queryId,
7100
7102
  provider: querySnapshots.provider,
@@ -7102,7 +7104,7 @@ function loadSnapshotsByRunIds(app, runIds) {
7102
7104
  citationState: querySnapshots.citationState,
7103
7105
  competitorOverlap: querySnapshots.competitorOverlap,
7104
7106
  citedDomains: querySnapshots.citedDomains
7105
- }).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all();
7107
+ }).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all());
7106
7108
  for (const row of rows) {
7107
7109
  const list = result.get(row.runId) ?? [];
7108
7110
  list.push({
@@ -13511,7 +13513,7 @@ async function cdpRoutes(app, opts) {
13511
13513
  const err = notFound("Run", runId);
13512
13514
  return reply.code(err.statusCode).send(err.toJSON());
13513
13515
  }
13514
- const snapshots = app.db.select({
13516
+ const snapshots = filterTrackedSnapshots(app.db.select({
13515
13517
  id: querySnapshots.id,
13516
13518
  queryId: querySnapshots.queryId,
13517
13519
  provider: querySnapshots.provider,
@@ -13519,7 +13521,7 @@ async function cdpRoutes(app, opts) {
13519
13521
  citedDomains: querySnapshots.citedDomains,
13520
13522
  screenshotPath: querySnapshots.screenshotPath,
13521
13523
  rawResponse: querySnapshots.rawResponse
13522
- }).from(querySnapshots).where(eq20(querySnapshots.runId, runId)).all();
13524
+ }).from(querySnapshots).where(eq20(querySnapshots.runId, runId)).all());
13523
13525
  const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq20(queries.projectId, project.id)).all();
13524
13526
  const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
13525
13527
  const byQuery = /* @__PURE__ */ new Map();
@@ -23851,6 +23853,7 @@ var JobRunner = class {
23851
23853
  id: snapshotId,
23852
23854
  runId,
23853
23855
  queryId: q.id,
23856
+ queryText: q.query,
23854
23857
  provider: providerName,
23855
23858
  model: raw.model,
23856
23859
  citationState,
@@ -23874,6 +23877,7 @@ var JobRunner = class {
23874
23877
  id: crypto24.randomUUID(),
23875
23878
  runId,
23876
23879
  queryId: q.id,
23880
+ queryText: q.query,
23877
23881
  provider: providerName,
23878
23882
  model: raw.model,
23879
23883
  citationState,
@@ -25674,10 +25678,12 @@ var Notifier = class {
25674
25678
  }).from(querySnapshots).where(inArray10(querySnapshots.runId, previousRunIds)).all();
25675
25679
  const prevMap = /* @__PURE__ */ new Map();
25676
25680
  for (const s of previousSnapshots) {
25681
+ if (s.queryId == null) continue;
25677
25682
  prevMap.set(`${s.queryId}:${s.provider}:${s.location ?? ""}`, s.citationState);
25678
25683
  }
25679
25684
  const transitions = [];
25680
25685
  for (const s of currentSnapshots) {
25686
+ if (s.queryId == null) continue;
25681
25687
  const key = `${s.queryId}:${s.provider}:${s.location ?? ""}`;
25682
25688
  const prevState = prevMap.get(key);
25683
25689
  if (prevState && prevState !== s.citationState) {
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  setTelemetrySource,
21
21
  showFirstRunNotice,
22
22
  trackEvent
23
- } from "./chunk-DZENHID5.js";
23
+ } from "./chunk-7AF6B3L6.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-BJXHETQW.js";
52
+ } from "./chunk-7256SFYT.js";
53
53
  import {
54
54
  CcReleaseSyncStatuses,
55
55
  CheckScopes,
@@ -623,7 +623,7 @@ function readStoredGroundingSources(rawResponse) {
623
623
  return result;
624
624
  }
625
625
  async function backfillInsightsCommand(project, opts) {
626
- const { IntelligenceService } = await import("./intelligence-service-XKOUBRCE.js");
626
+ const { IntelligenceService } = await import("./intelligence-service-3P2DMYRR.js");
627
627
  const config = loadConfig();
628
628
  const db = createClient(config.database);
629
629
  migrate(db);
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-DZENHID5.js";
3
+ } from "./chunk-7AF6B3L6.js";
4
4
  import {
5
5
  loadConfig
6
6
  } from "./chunk-5EBN7736.js";
7
- import "./chunk-BJXHETQW.js";
7
+ import "./chunk-7256SFYT.js";
8
8
  import "./chunk-XW3F5EEW.js";
9
9
  export {
10
10
  createServer,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-BJXHETQW.js";
3
+ } from "./chunk-7256SFYT.js";
4
4
  import "./chunk-XW3F5EEW.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.33.1",
3
+ "version": "4.34.0",
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,23 +60,23 @@
60
60
  "@types/node-cron": "^3.0.11",
61
61
  "tsup": "^8.5.1",
62
62
  "tsx": "^4.19.0",
63
- "@ainyc/canonry-api-routes": "0.0.0",
64
63
  "@ainyc/canonry-config": "0.0.0",
64
+ "@ainyc/canonry-api-routes": "0.0.0",
65
65
  "@ainyc/canonry-contracts": "0.0.0",
66
- "@ainyc/canonry-integration-bing": "0.0.0",
67
66
  "@ainyc/canonry-db": "0.0.0",
68
- "@ainyc/canonry-intelligence": "0.0.0",
67
+ "@ainyc/canonry-integration-bing": "0.0.0",
69
68
  "@ainyc/canonry-integration-cloud-run": "0.0.0",
70
- "@ainyc/canonry-integration-traffic": "0.0.0",
71
- "@ainyc/canonry-provider-cdp": "0.0.0",
72
69
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
70
+ "@ainyc/canonry-intelligence": "0.0.0",
73
71
  "@ainyc/canonry-integration-google": "0.0.0",
72
+ "@ainyc/canonry-integration-traffic": "0.0.0",
73
+ "@ainyc/canonry-provider-cdp": "0.0.0",
74
74
  "@ainyc/canonry-integration-wordpress": "0.0.0",
75
+ "@ainyc/canonry-provider-claude": "0.0.0",
75
76
  "@ainyc/canonry-provider-gemini": "0.0.0",
77
+ "@ainyc/canonry-provider-local": "0.0.0",
76
78
  "@ainyc/canonry-provider-openai": "0.0.0",
77
- "@ainyc/canonry-provider-claude": "0.0.0",
78
- "@ainyc/canonry-provider-perplexity": "0.0.0",
79
- "@ainyc/canonry-provider-local": "0.0.0"
79
+ "@ainyc/canonry-provider-perplexity": "0.0.0"
80
80
  },
81
81
  "scripts": {
82
82
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",