@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.
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +3 -1
- package/assets/assets/{BacklinksPage-CDAv0ggn.js → BacklinksPage-BPvsw_Bi.js} +1 -1
- package/assets/assets/{ChartPrimitives-CnAmsyt7.js → ChartPrimitives-BdlKCq7y.js} +1 -1
- package/assets/assets/ProjectPage-1Q8YC9Vd.js +6 -0
- package/assets/assets/{RunRow-CVZ5o8fg.js → RunRow-DcSsnE5c.js} +1 -1
- package/assets/assets/{RunsPage-Bzy5c0MZ.js → RunsPage-DKoIMkQL.js} +1 -1
- package/assets/assets/{SettingsPage-B1ocxPBe.js → SettingsPage-bH3PdNKb.js} +1 -1
- package/assets/assets/{TrafficPage-D2zepQOC.js → TrafficPage-IW_DX-0V.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-C7JuAkaK.js → TrafficSourceDetailPage-DRHOGn9B.js} +1 -1
- package/assets/assets/{arrow-left-Bv3CWylm.js → arrow-left-B5Du72nk.js} +1 -1
- package/assets/assets/{extract-error-message-BtVid5TP.js → extract-error-message-C7Vhd5zH.js} +1 -1
- package/assets/assets/index-C_ZzKZfM.js +210 -0
- package/assets/assets/index-ClkRAeHL.css +1 -0
- package/assets/assets/{trash-2-BoimCsYz.js → trash-2-DWcofmpv.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-3K3QRSYE.js → chunk-5LW7CJAO.js} +276 -53
- package/dist/{chunk-62YB3ML7.js → chunk-6XMXBAEW.js} +47 -2
- package/dist/{chunk-7BMSWI2K.js → chunk-DUDFNP5Y.js} +19 -4
- package/dist/{chunk-I2BJC3DT.js → chunk-MDRDX5R2.js} +634 -205
- package/dist/cli.js +73 -9
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-AHHBQKRD.js → intelligence-service-XUKYOHKL.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +7 -7
- package/assets/assets/ProjectPage-C9KEgRxD.js +0 -6
- package/assets/assets/index-BgWgJE7S.css +0 -1
- 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-
|
|
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
|
|
4532
|
-
const citedCount = citedIds.size;
|
|
4533
|
-
const tone2 = citedCount > 0 ? "positive" : "neutral";
|
|
4594
|
+
const activeIds = collectActiveQueryIds(currentSnapshots, isActive);
|
|
4534
4595
|
return withQueryLists(
|
|
4535
|
-
{
|
|
4536
|
-
|
|
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
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
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
|
-
{
|
|
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
|
|
4576
|
-
const
|
|
4577
|
-
for (const
|
|
4578
|
-
if (
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
12928
|
-
|
|
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(
|
|
12944
|
-
visibility: buildVisibilityScore(
|
|
13048
|
+
mention: buildMentionCoverage(trackedLatest, { configuredApiProviders }),
|
|
13049
|
+
visibility: buildVisibilityScore(trackedLatest, { configuredApiProviders }),
|
|
12945
13050
|
mentionShare: buildMentionShare(
|
|
12946
|
-
|
|
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(
|
|
12953
|
-
mentionGaps: buildMentionGapScore(
|
|
13057
|
+
gapQueries: buildGapQueryScore(trackedLatest),
|
|
13058
|
+
mentionGaps: buildMentionGapScore(trackedLatest),
|
|
12954
13059
|
indexCoverage: buildIndexCoverageScore(app, project.id),
|
|
12955
13060
|
competitorPressure: buildCompetitorPressureScore(
|
|
12956
|
-
|
|
13061
|
+
trackedLatest,
|
|
12957
13062
|
competitorRows.map((c) => c.domain),
|
|
12958
13063
|
competitorRows.length
|
|
12959
13064
|
),
|
|
12960
13065
|
runStatus: buildRunStatusScore(allRuns)
|
|
12961
13066
|
};
|
|
12962
|
-
const
|
|
13067
|
+
const citationMovement = buildCitationMovementSummary(latestSnapshots, previousSnapshots, {
|
|
12963
13068
|
queryLookup: queryLookup.byId
|
|
12964
13069
|
});
|
|
12965
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
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
|
-
|
|
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-
|
|
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?"
|
|
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",
|