@ainyc/canonry 4.85.0 → 4.87.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 (27) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +3 -1
  2. package/assets/assets/{BacklinksPage-CDAv0ggn.js → BacklinksPage-BPvsw_Bi.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-CnAmsyt7.js → ChartPrimitives-BdlKCq7y.js} +1 -1
  4. package/assets/assets/ProjectPage-1Q8YC9Vd.js +6 -0
  5. package/assets/assets/{RunRow-CVZ5o8fg.js → RunRow-DcSsnE5c.js} +1 -1
  6. package/assets/assets/{RunsPage-Bzy5c0MZ.js → RunsPage-DKoIMkQL.js} +1 -1
  7. package/assets/assets/{SettingsPage-B1ocxPBe.js → SettingsPage-bH3PdNKb.js} +1 -1
  8. package/assets/assets/{TrafficPage-D2zepQOC.js → TrafficPage-IW_DX-0V.js} +1 -1
  9. package/assets/assets/{TrafficSourceDetailPage-C7JuAkaK.js → TrafficSourceDetailPage-DRHOGn9B.js} +1 -1
  10. package/assets/assets/{arrow-left-Bv3CWylm.js → arrow-left-B5Du72nk.js} +1 -1
  11. package/assets/assets/{extract-error-message-BtVid5TP.js → extract-error-message-C7Vhd5zH.js} +1 -1
  12. package/assets/assets/index-C_ZzKZfM.js +210 -0
  13. package/assets/assets/index-ClkRAeHL.css +1 -0
  14. package/assets/assets/{trash-2-BoimCsYz.js → trash-2-DWcofmpv.js} +1 -1
  15. package/assets/index.html +2 -2
  16. package/dist/{chunk-3K3QRSYE.js → chunk-5LW7CJAO.js} +276 -53
  17. package/dist/{chunk-62YB3ML7.js → chunk-6XMXBAEW.js} +47 -2
  18. package/dist/{chunk-7BMSWI2K.js → chunk-DUDFNP5Y.js} +19 -4
  19. package/dist/{chunk-I2BJC3DT.js → chunk-MDRDX5R2.js} +634 -205
  20. package/dist/cli.js +73 -9
  21. package/dist/index.js +4 -4
  22. package/dist/{intelligence-service-AHHBQKRD.js → intelligence-service-XUKYOHKL.js} +2 -2
  23. package/dist/mcp.js +2 -2
  24. package/package.json +7 -7
  25. package/assets/assets/ProjectPage-C9KEgRxD.js +0 -6
  26. package/assets/assets/index-BgWgJE7S.css +0 -1
  27. package/assets/assets/index-DmNti_xn.js +0 -210
@@ -46,8 +46,10 @@ import {
46
46
  adsSummaryDtoSchema,
47
47
  adsSyncResponseSchema,
48
48
  agentProvidersResponseDtoSchema,
49
+ aggregateHarvestedQueries,
49
50
  apiKeyDtoSchema,
50
51
  apiKeyListDtoSchema,
52
+ applyHarvestSemanticNovelty,
51
53
  auditLogEntrySchema,
52
54
  authInvalid,
53
55
  authRequired,
@@ -70,6 +72,7 @@ import {
70
72
  brandKeyFromText,
71
73
  brandLabelFromDomain,
72
74
  brandMetricsDtoSchema,
75
+ buildHarvestAnchorTerms,
73
76
  categorizeSource,
74
77
  categorizeSourceWithCompetitors,
75
78
  categoryLabel,
@@ -104,6 +107,7 @@ import {
104
107
  deriveWinnabilityClass,
105
108
  determineAnswerMentioned,
106
109
  discoveryBucketSchema,
110
+ discoveryHarvestDtoSchema,
107
111
  discoveryPromotePreviewSchema,
108
112
  discoveryPromoteRequestSchema,
109
113
  discoveryPromoteResultSchema,
@@ -130,6 +134,7 @@ import {
130
134
  ga4SocialReferralHistoryEntrySchema,
131
135
  ga4StatusDtoSchema,
132
136
  ga4SyncResponseDtoSchema,
137
+ gateHarvestedSearchQueries,
133
138
  gbpAccountListResponseSchema,
134
139
  gbpDailyMetricListResponseSchema,
135
140
  gbpDiscoverRequestSchema,
@@ -165,6 +170,7 @@ import {
165
170
  missingDependency,
166
171
  normalizeProjectAliases,
167
172
  normalizeProjectDomain,
173
+ normalizeQueryText,
168
174
  normalizeUrlPath,
169
175
  notFound,
170
176
  notImplemented,
@@ -175,6 +181,7 @@ import {
175
181
  pickClusterRepresentative,
176
182
  projectConfigSchema,
177
183
  projectDtoSchema,
184
+ projectOverviewDtoSchema,
178
185
  projectReportDtoSchema,
179
186
  projectUpsertRequestSchema,
180
187
  providerError,
@@ -248,7 +255,7 @@ import {
248
255
  wordpressSchemaDeployResultDtoSchema,
249
256
  wordpressSchemaStatusResultDtoSchema,
250
257
  wordpressStatusDtoSchema
251
- } from "./chunk-I2BJC3DT.js";
258
+ } from "./chunk-MDRDX5R2.js";
252
259
 
253
260
  // src/intelligence-service.ts
254
261
  import { eq as eq37, desc as desc18, asc as asc5, and as and27, ne as ne5, or as or5, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
@@ -4527,35 +4534,101 @@ function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains, topDo
4527
4534
 
4528
4535
  // ../intelligence/src/movement-summary.ts
4529
4536
  function buildMovementSummary(currentSnapshots, previousSnapshots, options = {}) {
4537
+ return buildSignalMovementSummary(
4538
+ currentSnapshots,
4539
+ previousSnapshots,
4540
+ (snapshot) => snapshot.citationState === CitationStates.cited,
4541
+ options
4542
+ );
4543
+ }
4544
+ function buildCitationMovementSummary(currentSnapshots, previousSnapshots, options = {}) {
4545
+ return buildMovementSummary(currentSnapshots, previousSnapshots, options);
4546
+ }
4547
+ function buildMentionMovementSummary(currentSnapshots, previousSnapshots, options = {}) {
4548
+ return buildSignalMovementSummary(
4549
+ currentSnapshots,
4550
+ previousSnapshots,
4551
+ (snapshot) => snapshot.answerMentioned === true,
4552
+ options
4553
+ );
4554
+ }
4555
+ function buildMovementComparison(currentSnapshots, previousSnapshots, options = {}) {
4556
+ const currentIds = collectQueryIds(currentSnapshots);
4557
+ const previousIds = collectQueryIds(previousSnapshots);
4558
+ const hasPreviousRun = previousSnapshots.length > 0;
4559
+ if (!hasPreviousRun) {
4560
+ return {
4561
+ hasPreviousRun: false,
4562
+ comparable: false,
4563
+ querySetChanged: false,
4564
+ previousRunAt: null,
4565
+ currentQueryCount: currentIds.size,
4566
+ previousQueryCount: 0,
4567
+ comparableQueryCount: 0,
4568
+ addedQueryCount: 0,
4569
+ removedQueryCount: 0,
4570
+ addedQueries: [],
4571
+ removedQueries: []
4572
+ };
4573
+ }
4574
+ const comparableIds = intersection(currentIds, previousIds);
4575
+ const addedIds = difference(currentIds, previousIds);
4576
+ const removedIds = difference(previousIds, currentIds);
4577
+ const querySetChanged = addedIds.size > 0 || removedIds.size > 0;
4578
+ return {
4579
+ hasPreviousRun: true,
4580
+ comparable: !querySetChanged && currentIds.size > 0,
4581
+ querySetChanged,
4582
+ previousRunAt: options.previousRunAt ?? null,
4583
+ currentQueryCount: currentIds.size,
4584
+ previousQueryCount: previousIds.size,
4585
+ comparableQueryCount: comparableIds.size,
4586
+ addedQueryCount: addedIds.size,
4587
+ removedQueryCount: removedIds.size,
4588
+ addedQueries: resolveQueryTexts(addedIds, options.queryLookup),
4589
+ removedQueries: resolveQueryTexts(removedIds, options.queryLookup)
4590
+ };
4591
+ }
4592
+ function buildSignalMovementSummary(currentSnapshots, previousSnapshots, isActive, options) {
4530
4593
  if (previousSnapshots.length === 0) {
4531
- const citedIds = collectCitedQueryIds(currentSnapshots);
4532
- const citedCount = citedIds.size;
4533
- const tone2 = citedCount > 0 ? "positive" : "neutral";
4594
+ const activeIds = collectActiveQueryIds(currentSnapshots, isActive);
4534
4595
  return withQueryLists(
4535
- { gained: citedCount, lost: 0, tone: tone2, hasPreviousRun: false },
4536
- citedIds,
4596
+ {
4597
+ gained: activeIds.size,
4598
+ lost: 0,
4599
+ tone: activeIds.size > 0 ? "positive" : "neutral",
4600
+ hasPreviousRun: false
4601
+ },
4602
+ activeIds,
4537
4603
  /* @__PURE__ */ new Set(),
4538
4604
  options.queryLookup
4539
4605
  );
4540
4606
  }
4541
- const latestCited = collectCitedQueryIds(currentSnapshots);
4542
- const previousCited = collectCitedQueryIds(previousSnapshots);
4543
- const gainedIds = /* @__PURE__ */ new Set();
4544
- const lostIds = /* @__PURE__ */ new Set();
4545
- for (const id of latestCited) {
4546
- if (!previousCited.has(id)) gainedIds.add(id);
4547
- }
4548
- for (const id of previousCited) {
4549
- if (!latestCited.has(id)) lostIds.add(id);
4550
- }
4551
- const tone = lostIds.size > gainedIds.size ? "negative" : gainedIds.size > lostIds.size ? "positive" : "neutral";
4607
+ const comparableIds = intersection(
4608
+ collectQueryIds(currentSnapshots),
4609
+ collectQueryIds(previousSnapshots)
4610
+ );
4611
+ const currentActive = intersection(collectActiveQueryIds(currentSnapshots, isActive), comparableIds);
4612
+ const previousActive = intersection(collectActiveQueryIds(previousSnapshots, isActive), comparableIds);
4613
+ const gainedIds = difference(currentActive, previousActive);
4614
+ const lostIds = difference(previousActive, currentActive);
4552
4615
  return withQueryLists(
4553
- { gained: gainedIds.size, lost: lostIds.size, tone, hasPreviousRun: true },
4616
+ {
4617
+ gained: gainedIds.size,
4618
+ lost: lostIds.size,
4619
+ tone: movementTone(gainedIds.size, lostIds.size),
4620
+ hasPreviousRun: true
4621
+ },
4554
4622
  gainedIds,
4555
4623
  lostIds,
4556
4624
  options.queryLookup
4557
4625
  );
4558
4626
  }
4627
+ function movementTone(gained, lost) {
4628
+ if (lost > gained) return "negative";
4629
+ if (gained > lost) return "positive";
4630
+ return "neutral";
4631
+ }
4559
4632
  function withQueryLists(base, gainedIds, lostIds, lookup) {
4560
4633
  if (!lookup) return base;
4561
4634
  return {
@@ -4565,6 +4638,7 @@ function withQueryLists(base, gainedIds, lostIds, lookup) {
4565
4638
  };
4566
4639
  }
4567
4640
  function resolveQueryTexts(ids, lookup) {
4641
+ if (!lookup) return [];
4568
4642
  const out = [];
4569
4643
  for (const id of ids) {
4570
4644
  const text2 = lookup.get(id);
@@ -4572,12 +4646,33 @@ function resolveQueryTexts(ids, lookup) {
4572
4646
  }
4573
4647
  return out.sort();
4574
4648
  }
4575
- function collectCitedQueryIds(snapshots) {
4576
- const cited = /* @__PURE__ */ new Set();
4577
- for (const s of snapshots) {
4578
- if (s.citationState === CitationStates.cited && s.queryId) cited.add(s.queryId);
4649
+ function collectQueryIds(snapshots) {
4650
+ const ids = /* @__PURE__ */ new Set();
4651
+ for (const snapshot of snapshots) {
4652
+ if (snapshot.queryId) ids.add(snapshot.queryId);
4579
4653
  }
4580
- return cited;
4654
+ return ids;
4655
+ }
4656
+ function collectActiveQueryIds(snapshots, isActive) {
4657
+ const active = /* @__PURE__ */ new Set();
4658
+ for (const snapshot of snapshots) {
4659
+ if (snapshot.queryId && isActive(snapshot)) active.add(snapshot.queryId);
4660
+ }
4661
+ return active;
4662
+ }
4663
+ function intersection(left, right) {
4664
+ const out = /* @__PURE__ */ new Set();
4665
+ for (const value of left) {
4666
+ if (right.has(value)) out.add(value);
4667
+ }
4668
+ return out;
4669
+ }
4670
+ function difference(left, right) {
4671
+ const out = /* @__PURE__ */ new Set();
4672
+ for (const value of left) {
4673
+ if (!right.has(value)) out.add(value);
4674
+ }
4675
+ return out;
4581
4676
  }
4582
4677
 
4583
4678
  // ../intelligence/src/score-tones.ts
@@ -5022,12 +5117,12 @@ var DEFAULT_LIMIT = 10;
5022
5117
  function buildSuggestedQueries(gscRows, options) {
5023
5118
  const minImpressions = options.minImpressions ?? DEFAULT_MIN_IMPRESSIONS;
5024
5119
  const limit = options.limit ?? DEFAULT_LIMIT;
5025
- const trackedSet = new Set(options.trackedQueries.map(normalizeQuery));
5120
+ const trackedSet = new Set(options.trackedQueries.map(normalizeQueryText));
5026
5121
  let skippedAlreadyTracked = 0;
5027
5122
  const candidates = [];
5028
5123
  for (const row of gscRows) {
5029
5124
  if (row.impressions < minImpressions) continue;
5030
- const normalized = normalizeQuery(row.query);
5125
+ const normalized = normalizeQueryText(row.query);
5031
5126
  if (normalized.length === 0) continue;
5032
5127
  if (trackedSet.has(normalized)) {
5033
5128
  skippedAlreadyTracked++;
@@ -5049,9 +5144,6 @@ function buildSuggestedQueries(gscRows, options) {
5049
5144
  skippedAlreadyTracked
5050
5145
  };
5051
5146
  }
5052
- function normalizeQuery(value) {
5053
- return value.trim().toLowerCase();
5054
- }
5055
5147
  function buildReason(row) {
5056
5148
  const impressionsLabel = formatImpressions2(row.impressions);
5057
5149
  if (row.avgPosition <= 10) {
@@ -12919,18 +13011,31 @@ async function compositeRoutes(app) {
12919
13011
  const snapshotRunIds = new Set(sparklineRunIds);
12920
13012
  for (const run of latestVisRunGroup) snapshotRunIds.add(run.id);
12921
13013
  for (const run of previousVisRunGroup) snapshotRunIds.add(run.id);
12922
- const snapshotsByRun = loadSnapshotsByRunIds(app, [...snapshotRunIds]);
13014
+ const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq17(queries.projectId, project.id)).all();
13015
+ const queryIdByText = new Map(projectQueries.map((q) => [normalizeQueryText(q.query), q.id]));
13016
+ const snapshotsByRun = loadSnapshotsByRunIds(app, [...snapshotRunIds], queryIdByText);
12923
13017
  const latestSnapshots = latestVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
12924
13018
  const previousSnapshots = previousVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
12925
- const { queryCounts, providers } = summarizeFromSnapshots(latestSnapshots);
13019
+ const trackedLatest = latestSnapshots.filter((s) => !s.archived);
13020
+ const trackedPrevious = previousSnapshots.filter((s) => !s.archived);
13021
+ const trackedSnapshotsByRun = new Map(
13022
+ [...snapshotsByRun].map(([runId, snaps]) => [runId, snaps.filter((s) => !s.archived)])
13023
+ );
13024
+ const { queryCounts, providers } = summarizeFromSnapshots(trackedLatest);
12926
13025
  const transitions = summarizeTransitionsFromSnapshots(
12927
- latestSnapshots,
12928
- previousSnapshots,
13026
+ trackedLatest,
13027
+ trackedPrevious,
12929
13028
  previousVisibilityRun?.createdAt ?? null
12930
13029
  );
12931
13030
  const competitorRows = app.db.select().from(competitors).where(eq17(competitors.projectId, project.id)).all();
12932
- const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq17(queries.projectId, project.id)).all();
12933
13031
  const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
13032
+ for (const snapshots of snapshotsByRun.values()) {
13033
+ for (const snapshot of snapshots) {
13034
+ if (snapshot.queryText && !queryLookup.byId.has(snapshot.queryId)) {
13035
+ queryLookup.byId.set(snapshot.queryId, snapshot.queryText);
13036
+ }
13037
+ }
13038
+ }
12934
13039
  const configuredApiProviders = project.providers.filter((p) => !p.startsWith("cdp:"));
12935
13040
  const mentionShareCompetitors = competitorRows.map((c) => ({
12936
13041
  domain: c.domain,
@@ -12940,32 +13045,39 @@ async function compositeRoutes(app) {
12940
13045
  brandTokens: [brandLabelFromDomain(c.domain)].filter((t) => t.length >= 3)
12941
13046
  }));
12942
13047
  const scores = {
12943
- mention: buildMentionCoverage(latestSnapshots, { configuredApiProviders }),
12944
- visibility: buildVisibilityScore(latestSnapshots, { configuredApiProviders }),
13048
+ mention: buildMentionCoverage(trackedLatest, { configuredApiProviders }),
13049
+ visibility: buildVisibilityScore(trackedLatest, { configuredApiProviders }),
12945
13050
  mentionShare: buildMentionShare(
12946
- latestSnapshots.map((s) => ({
13051
+ trackedLatest.map((s) => ({
12947
13052
  projectMentioned: s.answerMentioned === true,
12948
13053
  answerText: s.answerText
12949
13054
  })),
12950
13055
  { competitors: mentionShareCompetitors }
12951
13056
  ),
12952
- gapQueries: buildGapQueryScore(latestSnapshots),
12953
- mentionGaps: buildMentionGapScore(latestSnapshots),
13057
+ gapQueries: buildGapQueryScore(trackedLatest),
13058
+ mentionGaps: buildMentionGapScore(trackedLatest),
12954
13059
  indexCoverage: buildIndexCoverageScore(app, project.id),
12955
13060
  competitorPressure: buildCompetitorPressureScore(
12956
- latestSnapshots,
13061
+ trackedLatest,
12957
13062
  competitorRows.map((c) => c.domain),
12958
13063
  competitorRows.length
12959
13064
  ),
12960
13065
  runStatus: buildRunStatusScore(allRuns)
12961
13066
  };
12962
- const movementSummary = buildMovementSummary(latestSnapshots, previousSnapshots, {
13067
+ const citationMovement = buildCitationMovementSummary(latestSnapshots, previousSnapshots, {
12963
13068
  queryLookup: queryLookup.byId
12964
13069
  });
12965
- const providerScoresBase = buildProviderScores(latestSnapshots);
13070
+ const mentionMovement = buildMentionMovementSummary(latestSnapshots, previousSnapshots, {
13071
+ queryLookup: queryLookup.byId
13072
+ });
13073
+ const movementComparison = buildMovementComparison(latestSnapshots, previousSnapshots, {
13074
+ queryLookup: queryLookup.byId,
13075
+ previousRunAt: previousVisibilityRun?.createdAt ?? null
13076
+ });
13077
+ const providerScoresBase = buildProviderScores(trackedLatest);
12966
13078
  const providerTrends = buildProviderTrends(
12967
13079
  visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => ({ id: r.id, createdAt: r.createdAt })),
12968
- snapshotsByRun,
13080
+ trackedSnapshotsByRun,
12969
13081
  DEFAULT_RUN_HISTORY_LIMIT
12970
13082
  );
12971
13083
  const providerScores = providerScoresBase.map((score) => {
@@ -12973,13 +13085,13 @@ async function compositeRoutes(app) {
12973
13085
  return trend.length > 1 ? { ...score, trend: trend.map((p) => p.rate) } : score;
12974
13086
  });
12975
13087
  const overviewCompetitors = buildOverviewCompetitors(
12976
- latestSnapshots,
13088
+ trackedLatest,
12977
13089
  competitorRows.map((c) => ({ id: c.id, domain: c.domain })),
12978
13090
  queryLookup
12979
13091
  );
12980
13092
  const attentionItems = buildAttentionItems(insightRows, allRuns);
12981
13093
  const sparklineRuns = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => ({ id: r.id, createdAt: r.createdAt, status: r.status }));
12982
- const runHistory = buildRunHistory(sparklineRuns, snapshotsByRun);
13094
+ const runHistory = buildRunHistory(sparklineRuns, trackedSnapshotsByRun);
12983
13095
  scores.mention.trend = runHistory.map((p) => p.mentionRate);
12984
13096
  scores.visibility.trend = runHistory.map((p) => p.citationRate);
12985
13097
  const suggestedQueries = buildSuggestedQueriesFromGsc(
@@ -12996,7 +13108,12 @@ async function compositeRoutes(app) {
12996
13108
  providers,
12997
13109
  transitions,
12998
13110
  scores,
12999
- movementSummary,
13111
+ // Keep the legacy citation-only field for API compatibility. New
13112
+ // consumers read the explicitly named siblings below.
13113
+ movementSummary: citationMovement,
13114
+ citationMovement,
13115
+ mentionMovement,
13116
+ movementComparison,
13000
13117
  competitors: overviewCompetitors,
13001
13118
  providerScores,
13002
13119
  attentionItems,
@@ -13106,12 +13223,13 @@ function summarizeRun(run) {
13106
13223
  createdAt: run.createdAt
13107
13224
  };
13108
13225
  }
13109
- function loadSnapshotsByRunIds(app, runIds) {
13226
+ function loadSnapshotsByRunIds(app, runIds, queryIdByText) {
13110
13227
  const result = /* @__PURE__ */ new Map();
13111
13228
  if (runIds.length === 0) return result;
13112
- const rows = filterTrackedSnapshots(app.db.select({
13229
+ const rows = app.db.select({
13113
13230
  runId: querySnapshots.runId,
13114
13231
  queryId: querySnapshots.queryId,
13232
+ queryText: querySnapshots.queryText,
13115
13233
  provider: querySnapshots.provider,
13116
13234
  model: querySnapshots.model,
13117
13235
  citationState: querySnapshots.citationState,
@@ -13119,11 +13237,30 @@ function loadSnapshotsByRunIds(app, runIds) {
13119
13237
  answerText: querySnapshots.answerText,
13120
13238
  competitorOverlap: querySnapshots.competitorOverlap,
13121
13239
  citedDomains: querySnapshots.citedDomains
13122
- }).from(querySnapshots).where(inArray9(querySnapshots.runId, [...runIds])).all());
13240
+ }).from(querySnapshots).where(inArray9(querySnapshots.runId, [...runIds])).all();
13123
13241
  for (const row of rows) {
13242
+ const queryText = row.queryText?.trim() || null;
13243
+ let queryId;
13244
+ let archived = false;
13245
+ if (row.queryId) {
13246
+ queryId = row.queryId;
13247
+ } else if (queryText) {
13248
+ const tracked = queryIdByText.get(normalizeQueryText(queryText));
13249
+ if (tracked) {
13250
+ queryId = tracked;
13251
+ } else {
13252
+ queryId = `archived:${normalizeQueryText(queryText)}`;
13253
+ archived = true;
13254
+ }
13255
+ } else {
13256
+ queryId = null;
13257
+ }
13258
+ if (!queryId) continue;
13124
13259
  const list = result.get(row.runId) ?? [];
13125
13260
  list.push({
13126
- queryId: row.queryId,
13261
+ queryId,
13262
+ queryText,
13263
+ archived,
13127
13264
  provider: row.provider,
13128
13265
  model: row.model,
13129
13266
  citationState: row.citationState,
@@ -13577,6 +13714,7 @@ var SCHEMA_TABLE = {
13577
13714
  DomainClassificationsResponseDto: domainClassificationsResponseDtoSchema,
13578
13715
  RecommendationBriefDto: recommendationBriefDtoSchema,
13579
13716
  RecommendationExplanationDto: recommendationExplanationDtoSchema,
13717
+ DiscoveryHarvestDto: discoveryHarvestDtoSchema,
13580
13718
  DiscoveryPromotePreview: discoveryPromotePreviewSchema,
13581
13719
  DiscoveryPromoteResult: discoveryPromoteResultSchema,
13582
13720
  DiscoverySessionDetailDto: discoverySessionDetailDtoSchema,
@@ -13612,6 +13750,7 @@ var SCHEMA_TABLE = {
13612
13750
  LocationContext: locationContextSchema,
13613
13751
  NotificationDto: notificationDtoSchema,
13614
13752
  ProjectDto: projectDtoSchema,
13753
+ ProjectOverviewDto: projectOverviewDtoSchema,
13615
13754
  ProjectReportDto: projectReportDtoSchema,
13616
13755
  QueryDto: queryDtoSchema,
13617
13756
  RunDetailDto: runDetailDtoSchema,
@@ -16845,12 +16984,11 @@ var routeCatalog = [
16845
16984
  method: "get",
16846
16985
  path: "/api/v1/projects/{name}/overview",
16847
16986
  summary: "Get a composite overview of project health",
16848
- description: 'Bundles project info, latest run, top undismissed insights, the latest health snapshot, query cited rate, per-provider breakdown, and transitions vs. the previous run. Designed for the "how is project X doing?" question so agents can answer in one call.',
16987
+ description: 'Bundles project info, latest run, top undismissed insights, health, independent mention and citation coverage, query-basket comparability, and separate mention/citation movement over the shared query cohort. Designed for the "how is project X doing?" question so agents can answer in one call.',
16849
16988
  tags: ["intelligence"],
16850
16989
  parameters: [nameParameter],
16851
16990
  responses: {
16852
- // TODO: Add `ProjectOverviewDto` Zod schema in contracts.
16853
- 200: rawJsonResponse("Overview returned.", looseObjectSchema),
16991
+ 200: jsonResponse("Overview returned.", "ProjectOverviewDto"),
16854
16992
  404: errorResponse("Project not found.")
16855
16993
  }
16856
16994
  },
@@ -17432,6 +17570,23 @@ var routeCatalog = [
17432
17570
  404: errorResponse("Project or session not found.")
17433
17571
  }
17434
17572
  },
17573
+ {
17574
+ method: "get",
17575
+ path: "/api/v1/projects/{name}/discover/sessions/{id}/harvest",
17576
+ summary: "Harvest issued search queries (grounding fan-out) from a session",
17577
+ description: "Reads the search queries the answer engine actually issued to answer each probe (Gemini's `groundingMetadata.webSearchQueries` fan-out) back out of the session's stored probe payloads, then runs a mandatory quality gate and returns the survivors as candidate seeds, ranked by how many distinct probes issued each one. The gate drops navigational/phone lookups, over-specific outliers, off-subject acronym collisions, exact already-tracked matches, and \u2014 via an embedding cosine pass over the project's tracked queries \u2014 semantic duplicates (paraphrases/synonyms an exact match can't see). `semanticNoveltyApplied` reports whether that embedding pass ran (it falls back to exact-match when embeddings are unavailable). These are a THIRD signal \u2014 *issued retrieval queries* \u2014 distinct from `mention` (answer text) and `cited` (source list); they carry no demand of their own. Read-only and derived: nothing is probed, tracked, or promoted. `minProbeHits` raises the recurrence floor; `anchor=false` disables the subject anchor for new-subject discovery on a well-scoped project. `stats` carries the raw count and a per-reason rejection tally. Issue #713.",
17578
+ tags: ["discovery"],
17579
+ parameters: [
17580
+ nameParameter,
17581
+ { name: "id", in: "path", required: true, description: "Discovery session ID.", schema: stringSchema },
17582
+ { name: "minProbeHits", in: "query", required: false, description: "Minimum number of distinct probes a candidate must appear in to be admitted (recurrence floor). Default 1.", schema: stringSchema },
17583
+ { name: "anchor", in: "query", required: false, description: 'Set to "false" to disable the subject-anchor filter. Default applies it (when the subject corpus is rich enough).', schema: stringSchema }
17584
+ ],
17585
+ responses: {
17586
+ 200: jsonResponse("Harvested candidate seeds + gate stats returned.", "DiscoveryHarvestDto"),
17587
+ 404: errorResponse("Project or session not found.")
17588
+ }
17589
+ },
17435
17590
  {
17436
17591
  method: "get",
17437
17592
  path: "/api/v1/projects/{name}/discover/sessions/{id}/promote",
@@ -32896,6 +33051,72 @@ async function discoveryRoutes(app, opts) {
32896
33051
  return reply.send(detail);
32897
33052
  }
32898
33053
  );
33054
+ app.get(
33055
+ "/projects/:name/discover/sessions/:id/harvest",
33056
+ async (request, reply) => {
33057
+ const project = resolveProject(app.db, request.params.name);
33058
+ const session = app.db.select().from(discoverySessions).where(eq34(discoverySessions.id, request.params.id)).get();
33059
+ if (!session || session.projectId !== project.id) {
33060
+ throw notFound("Discovery session", request.params.id);
33061
+ }
33062
+ const parsedFloor = parseInt(request.query.minProbeHits ?? "", 10);
33063
+ const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
33064
+ const applyAnchor = request.query.anchor !== "false";
33065
+ const provider = session.seedProvider ?? "gemini";
33066
+ const probeRows = app.db.select().from(discoveryProbes).where(eq34(discoveryProbes.sessionId, session.id)).all();
33067
+ const extract = opts.harvestSearchQueries;
33068
+ const probesWithQueries = probeRows.map((row) => {
33069
+ if (!extract || !row.rawResponse) return { searchQueries: [] };
33070
+ try {
33071
+ const raw = JSON.parse(row.rawResponse);
33072
+ return { searchQueries: extract({ provider, rawResponse: raw }) };
33073
+ } catch {
33074
+ return { searchQueries: [] };
33075
+ }
33076
+ });
33077
+ const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq34(queries.projectId, project.id)).all().map((r) => r.query);
33078
+ const anchorTerms = buildHarvestAnchorTerms(
33079
+ [session.icpDescription ?? "", ...trackedQueries],
33080
+ effectiveDomains(project)
33081
+ );
33082
+ const aggregated = aggregateHarvestedQueries(probesWithQueries);
33083
+ let result = gateHarvestedSearchQueries({
33084
+ candidates: aggregated,
33085
+ trackedQueries,
33086
+ anchorTerms,
33087
+ minProbeHits,
33088
+ applyAnchor
33089
+ });
33090
+ let semanticNoveltyApplied = false;
33091
+ if (opts.embedQueries && result.admitted.length > 0 && trackedQueries.length > 0) {
33092
+ try {
33093
+ const candidateTexts = result.admitted.map((c) => c.query);
33094
+ const vectors = await opts.embedQueries([...candidateTexts, ...trackedQueries]);
33095
+ if (vectors.length === candidateTexts.length + trackedQueries.length) {
33096
+ result = applyHarvestSemanticNovelty({
33097
+ result,
33098
+ candidateVectors: vectors.slice(0, candidateTexts.length),
33099
+ trackedVectors: vectors.slice(candidateTexts.length)
33100
+ });
33101
+ semanticNoveltyApplied = true;
33102
+ }
33103
+ } catch {
33104
+ }
33105
+ }
33106
+ const harvest = {
33107
+ sessionId: session.id,
33108
+ projectId: project.id,
33109
+ provider,
33110
+ status: session.status,
33111
+ minProbeHits,
33112
+ anchorApplied: result.anchorApplied,
33113
+ semanticNoveltyApplied,
33114
+ candidates: result.admitted,
33115
+ stats: result.stats
33116
+ };
33117
+ return reply.send(harvest);
33118
+ }
33119
+ );
32899
33120
  app.get(
32900
33121
  "/projects/:name/discover/sessions/:id/promote",
32901
33122
  async (request, reply) => {
@@ -33581,7 +33802,9 @@ async function apiRoutes(app, opts) {
33581
33802
  discoverLatestRelease: opts.discoverLatestRelease
33582
33803
  });
33583
33804
  await api.register(discoveryRoutes, {
33584
- onDiscoveryRunRequested: opts.onDiscoveryRunRequested
33805
+ onDiscoveryRunRequested: opts.onDiscoveryRunRequested,
33806
+ harvestSearchQueries: opts.harvestSearchQueries,
33807
+ embedQueries: opts.embedQueries
33585
33808
  });
33586
33809
  await api.register(technicalAeoRoutes, {
33587
33810
  onSiteAuditRequested: opts.onSiteAuditRequested
@@ -23,7 +23,7 @@ import {
23
23
  trafficConnectVercelRequestSchema,
24
24
  trafficConnectWordpressRequestSchema,
25
25
  trafficEventKindSchema
26
- } from "./chunk-I2BJC3DT.js";
26
+ } from "./chunk-MDRDX5R2.js";
27
27
 
28
28
  // src/config.ts
29
29
  import fs from "fs";
@@ -3454,6 +3454,18 @@ var getApiV1ProjectsByNameDiscoverSessionsById = (options) => {
3454
3454
  ...options
3455
3455
  });
3456
3456
  };
3457
+ var getApiV1ProjectsByNameDiscoverSessionsByIdHarvest = (options) => {
3458
+ return (options.client ?? client).get({
3459
+ security: [
3460
+ {
3461
+ scheme: "bearer",
3462
+ type: "http"
3463
+ }
3464
+ ],
3465
+ url: "/api/v1/projects/{name}/discover/sessions/{id}/harvest",
3466
+ ...options
3467
+ });
3468
+ };
3457
3469
  var getApiV1ProjectsByNameDiscoverSessionsByIdPromote = (options) => {
3458
3470
  return (options.client ?? client).get({
3459
3471
  security: [
@@ -4750,6 +4762,19 @@ var ApiClient = class {
4750
4762
  })
4751
4763
  );
4752
4764
  }
4765
+ async getDiscoveryHarvest(project, sessionId, opts) {
4766
+ return this.invoke(
4767
+ () => getApiV1ProjectsByNameDiscoverSessionsByIdHarvest({
4768
+ client: this.heyClient,
4769
+ path: { name: project, id: sessionId },
4770
+ query: {
4771
+ minProbeHits: opts?.minProbeHits !== void 0 ? String(opts.minProbeHits) : void 0,
4772
+ // The server treats anchor=false as "disable"; omit otherwise.
4773
+ anchor: opts?.anchor === false ? "false" : void 0
4774
+ }
4775
+ })
4776
+ );
4777
+ }
4753
4778
  async previewDiscoveryPromote(project, sessionId) {
4754
4779
  return this.invoke(
4755
4780
  () => getApiV1ProjectsByNameDiscoverSessionsByIdPromote({
@@ -5494,6 +5519,12 @@ var discoverySessionIdInputSchema = z2.object({
5494
5519
  project: projectNameSchema,
5495
5520
  sessionId: z2.string().min(1).describe("Discovery session ID returned by canonry_discover_run_start.")
5496
5521
  });
5522
+ var discoveryHarvestInputSchema = z2.object({
5523
+ project: projectNameSchema,
5524
+ sessionId: z2.string().min(1).describe("Discovery session ID returned by canonry_discover_run_start."),
5525
+ minProbeHits: z2.number().int().positive().optional().describe("Recurrence floor \u2014 a candidate must have appeared in at least this many distinct probes to be admitted. Default 1."),
5526
+ anchor: z2.boolean().optional().describe("Apply the subject-anchor filter that drops off-topic acronym collisions. Default true; pass false for new-subject discovery on a well-scoped project.")
5527
+ });
5497
5528
  var discoveryPromoteInputSchema = z2.object({
5498
5529
  project: projectNameSchema,
5499
5530
  sessionId: z2.string().min(1).describe("Discovery session ID returned by canonry_discover_run_start."),
@@ -5566,7 +5597,7 @@ var canonryMcpTools = [
5566
5597
  defineTool({
5567
5598
  name: "canonry_project_overview",
5568
5599
  title: "Get project overview (composite)",
5569
- description: 'One-call summary for "how is project X doing?" \u2014 bundles project info, latest run, top undismissed insights, latest health snapshot, query cited rate, per-provider breakdown, gained/lost/emerging vs the previous run, the five score gauges (visibility, gap queries, index coverage, competitor pressure, run status), per-(provider, model) scores, configured competitors with pressure labels, an attention queue of critical/high insights, and a recent-runs sparkline. Filterable by location and time window. Prefer this over fanning out to separate tools.',
5600
+ description: 'One-call summary for "how is project X doing?". Returns independent mention and citation coverage, separate query-level movement for each signal, query-basket comparability with added/removed counts, latest run and health, insights, provider/model breakdowns, competitors, attention items, and recent history. Movement excludes queries not shared by both sweeps. Filterable by location and time window. Prefer this over fanning out to separate tools.',
5570
5601
  access: "read",
5571
5602
  tier: "core",
5572
5603
  inputSchema: z2.object({
@@ -6749,6 +6780,20 @@ var canonryMcpTools = [
6749
6780
  openApiOperations: ["GET /api/v1/projects/{name}/discover/sessions/{id}"],
6750
6781
  handler: (client2, input) => client2.getDiscoverySession(input.project, input.sessionId)
6751
6782
  }),
6783
+ defineTool({
6784
+ name: "canonry_discover_harvest",
6785
+ title: "Harvest discovery search queries",
6786
+ description: `Read the search queries the answer engine actually issued (Gemini's grounding fan-out) back out of a session's stored probes, gate them for buyer-intent + novelty, and return the survivors as candidate seeds ranked by how many distinct probes issued each one. These are a THIRD signal \u2014 issued retrieval queries \u2014 distinct from mention (answer text) and cited (source list); they carry no demand of their own. Read-only and derived: nothing is probed, tracked, or promoted. Use it to surface "queries the model searched for that you aren't tracking yet"; the operator/agent then decides what to add via canonry_query_add. minProbeHits raises the recurrence floor; anchor=false disables the subject filter. stats carries the raw count and per-reason rejection tally.`,
6787
+ access: "read",
6788
+ tier: "discovery",
6789
+ inputSchema: discoveryHarvestInputSchema,
6790
+ annotations: readAnnotations(),
6791
+ openApiOperations: ["GET /api/v1/projects/{name}/discover/sessions/{id}/harvest"],
6792
+ handler: (client2, input) => client2.getDiscoveryHarvest(input.project, input.sessionId, {
6793
+ minProbeHits: input.minProbeHits,
6794
+ anchor: input.anchor
6795
+ })
6796
+ }),
6752
6797
  defineTool({
6753
6798
  name: "canonry_discover_promote_preview",
6754
6799
  title: "Preview discovery promotion",