@ainyc/canonry 4.27.1 → 4.28.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/aero/SKILL.md +1 -0
- package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +49 -4
- package/assets/agent-workspace/skills/canonry-setup/references/canonry-cli.md +4 -2
- package/assets/assets/index--jYjUA0o.js +302 -0
- package/assets/assets/{index-rPok6yk8.css → index-BnALDZI7.css} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-2FAEQ56I.js → chunk-GB3QJURO.js} +32 -3
- package/dist/{chunk-QNXGCQEM.js → chunk-HONTKYY7.js} +226 -27
- package/dist/{chunk-HVW665A4.js → chunk-RLLFB3M3.js} +37 -0
- package/dist/{chunk-NXXD6TX7.js → chunk-UEV3HSRL.js} +1 -1
- package/dist/cli.js +81 -6
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-Z6QIELKP.js → intelligence-service-O6KB6YAM.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +7 -7
- package/assets/assets/index-BWjq1HP1.js +0 -302
package/assets/index.html
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
|
|
13
13
|
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
|
14
14
|
<title>Canonry</title>
|
|
15
|
-
<script type="module" crossorigin src="./assets/index
|
|
16
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index--jYjUA0o.js"></script>
|
|
16
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BnALDZI7.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
19
19
|
<div id="root"></div>
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
4
4
|
DISCOVERY_MAX_PROBES_CAP,
|
|
5
5
|
competitorBatchRequestSchema,
|
|
6
|
+
discoveryBucketSchema,
|
|
7
|
+
discoveryPromoteRequestSchema,
|
|
6
8
|
discoveryRunRequestSchema,
|
|
7
9
|
keywordBatchRequestSchema,
|
|
8
10
|
keywordGenerateRequestSchema,
|
|
@@ -18,7 +20,7 @@ import {
|
|
|
18
20
|
trafficConnectCloudRunRequestSchema,
|
|
19
21
|
trafficConnectWordpressRequestSchema,
|
|
20
22
|
trafficEventKindSchema
|
|
21
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-RLLFB3M3.js";
|
|
22
24
|
|
|
23
25
|
// src/config.ts
|
|
24
26
|
import fs from "fs";
|
|
@@ -847,6 +849,13 @@ var ApiClient = class {
|
|
|
847
849
|
`/projects/${encodeURIComponent(project)}/discover/sessions/${encodeURIComponent(sessionId)}/promote`
|
|
848
850
|
);
|
|
849
851
|
}
|
|
852
|
+
async promoteDiscovery(project, sessionId, body) {
|
|
853
|
+
return this.request(
|
|
854
|
+
"POST",
|
|
855
|
+
`/projects/${encodeURIComponent(project)}/discover/sessions/${encodeURIComponent(sessionId)}/promote`,
|
|
856
|
+
body ?? {}
|
|
857
|
+
);
|
|
858
|
+
}
|
|
850
859
|
async wordpressConnect(project, body) {
|
|
851
860
|
return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/connect`, body);
|
|
852
861
|
}
|
|
@@ -1272,6 +1281,15 @@ var discoverySessionIdInputSchema = z2.object({
|
|
|
1272
1281
|
project: projectNameSchema,
|
|
1273
1282
|
sessionId: z2.string().min(1).describe("Discovery session ID returned by canonry_discover_run_start.")
|
|
1274
1283
|
});
|
|
1284
|
+
var discoveryPromoteInputSchema = z2.object({
|
|
1285
|
+
project: projectNameSchema,
|
|
1286
|
+
sessionId: z2.string().min(1).describe("Discovery session ID returned by canonry_discover_run_start."),
|
|
1287
|
+
request: discoveryPromoteRequestSchema.extend({
|
|
1288
|
+
// Stronger descriptions for the LLM. The base Zod schema enforces the shape.
|
|
1289
|
+
buckets: z2.array(discoveryBucketSchema).min(1).optional().describe("Which probe buckets to adopt into the tracked basket. Omitted promotes cited + aspirational; include wasted-surface explicitly for off-ICP competitor gaps."),
|
|
1290
|
+
includeCompetitors: z2.boolean().optional().describe("Whether to also merge recurring discovered competitor domains into the project. Defaults to true.")
|
|
1291
|
+
}).optional()
|
|
1292
|
+
});
|
|
1275
1293
|
var AGENT_WEBHOOK_EVENTS = [
|
|
1276
1294
|
notificationEventSchema.enum["run.completed"],
|
|
1277
1295
|
notificationEventSchema.enum["insight.critical"],
|
|
@@ -2202,7 +2220,7 @@ var canonryMcpTools = [
|
|
|
2202
2220
|
defineTool({
|
|
2203
2221
|
name: "canonry_discover_session_get",
|
|
2204
2222
|
title: "Get discovery session",
|
|
2205
|
-
description: 'Get one discovery session with the full probe list (per-query bucket + cited domains). Use after canonry_discover_run_start to inspect what the discovery pipeline produced; this is the canonical read for "what did discovery find" before
|
|
2223
|
+
description: 'Get one discovery session with the full probe list (per-query bucket + cited domains). Use after canonry_discover_run_start to inspect what the discovery pipeline produced; this is the canonical read for "what did discovery find" before calling canonry_discover_promote.',
|
|
2206
2224
|
access: "read",
|
|
2207
2225
|
tier: "discovery",
|
|
2208
2226
|
inputSchema: discoverySessionIdInputSchema,
|
|
@@ -2213,13 +2231,24 @@ var canonryMcpTools = [
|
|
|
2213
2231
|
defineTool({
|
|
2214
2232
|
name: "canonry_discover_promote_preview",
|
|
2215
2233
|
title: "Preview discovery promotion",
|
|
2216
|
-
description: "Read-only preview of
|
|
2234
|
+
description: "Read-only preview of available promotion candidates for a session: bucketed query lists and recurring suggested competitor domains not already in the project's tracked competitor list. Use it to confirm a basket before calling canonry_discover_promote.",
|
|
2217
2235
|
access: "read",
|
|
2218
2236
|
tier: "discovery",
|
|
2219
2237
|
inputSchema: discoverySessionIdInputSchema,
|
|
2220
2238
|
annotations: readAnnotations(),
|
|
2221
2239
|
openApiOperations: ["GET /api/v1/projects/{name}/discover/sessions/{id}/promote"],
|
|
2222
2240
|
handler: (client, input) => client.previewDiscoveryPromote(input.project, input.sessionId)
|
|
2241
|
+
}),
|
|
2242
|
+
defineTool({
|
|
2243
|
+
name: "canonry_discover_promote",
|
|
2244
|
+
title: "Promote discovery session",
|
|
2245
|
+
description: 'Adopt a completed discovery session\'s bucketed queries into the project\'s tracked basket, tagged with provenance "discovery:<sessionId>". By default, only cited + aspirational queries are promoted; include wasted-surface explicitly when off-ICP competitor gaps should also be tracked. Recurring discovered competitor domains are also merged by default. Add-only and idempotent: queries/domains already tracked are returned under `skipped`, never inserted twice. Only sessions with status "completed" can be promoted. Call canonry_discover_promote_preview first to inspect candidates.',
|
|
2246
|
+
access: "write",
|
|
2247
|
+
tier: "discovery",
|
|
2248
|
+
inputSchema: discoveryPromoteInputSchema,
|
|
2249
|
+
annotations: writeAnnotations({ idempotentHint: true }),
|
|
2250
|
+
openApiOperations: ["POST /api/v1/projects/{name}/discover/sessions/{id}/promote"],
|
|
2251
|
+
handler: (client, input) => client.promoteDiscovery(input.project, input.sessionId, input.request)
|
|
2223
2252
|
})
|
|
2224
2253
|
];
|
|
2225
2254
|
var CANONRY_MCP_TOOL_COUNT = canonryMcpTools.length;
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
loadConfig,
|
|
6
6
|
loadConfigRaw,
|
|
7
7
|
saveConfigPatch
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-GB3QJURO.js";
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_RUN_HISTORY_LIMIT,
|
|
11
11
|
IntelligenceService,
|
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
schedules,
|
|
71
71
|
trafficSources,
|
|
72
72
|
usageCounters
|
|
73
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-UEV3HSRL.js";
|
|
74
74
|
import {
|
|
75
75
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
76
76
|
AGENT_PROVIDER_IDS,
|
|
@@ -81,6 +81,9 @@ import {
|
|
|
81
81
|
CheckScopes,
|
|
82
82
|
CheckStatuses,
|
|
83
83
|
CitationStates,
|
|
84
|
+
DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
|
|
85
|
+
DISCOVERY_PROMOTE_COMPETITOR_CAP,
|
|
86
|
+
DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS,
|
|
84
87
|
DiscoveryBuckets,
|
|
85
88
|
DiscoverySessionStatuses,
|
|
86
89
|
MemorySources,
|
|
@@ -119,6 +122,7 @@ import {
|
|
|
119
122
|
deltaTone,
|
|
120
123
|
determineAnswerMentioned,
|
|
121
124
|
discoveryBucketSchema,
|
|
125
|
+
discoveryPromoteRequestSchema,
|
|
122
126
|
discoveryRunRequestSchema,
|
|
123
127
|
effectiveDomains,
|
|
124
128
|
emptyCitationVisibility,
|
|
@@ -171,7 +175,7 @@ import {
|
|
|
171
175
|
visibilityStateFromAnswerMentioned,
|
|
172
176
|
windowCutoff,
|
|
173
177
|
wordpressEnvSchema
|
|
174
|
-
} from "./chunk-
|
|
178
|
+
} from "./chunk-RLLFB3M3.js";
|
|
175
179
|
|
|
176
180
|
// src/telemetry.ts
|
|
177
181
|
import crypto from "crypto";
|
|
@@ -2612,7 +2616,7 @@ function buildCategoryCounts(counts) {
|
|
|
2612
2616
|
}
|
|
2613
2617
|
|
|
2614
2618
|
// ../api-routes/src/intelligence.ts
|
|
2615
|
-
import { eq as eq11, desc as desc4, and as and3 } from "drizzle-orm";
|
|
2619
|
+
import { eq as eq11, desc as desc4, and as and3, inArray as inArray3 } from "drizzle-orm";
|
|
2616
2620
|
function emptyHealthSnapshot(projectId) {
|
|
2617
2621
|
return {
|
|
2618
2622
|
id: `no-data:${projectId}`,
|
|
@@ -2656,6 +2660,44 @@ function mapHealthRow(r) {
|
|
|
2656
2660
|
status: "ready"
|
|
2657
2661
|
};
|
|
2658
2662
|
}
|
|
2663
|
+
function aggregateHealthSnapshots(projectId, rows) {
|
|
2664
|
+
if (rows.length === 1) return mapHealthRow(rows[0]);
|
|
2665
|
+
let totalPairs = 0;
|
|
2666
|
+
let citedPairs = 0;
|
|
2667
|
+
const mergedProviders = {};
|
|
2668
|
+
let newestCreatedAt = "";
|
|
2669
|
+
const runIds = [];
|
|
2670
|
+
for (const row of rows) {
|
|
2671
|
+
totalPairs += row.totalPairs;
|
|
2672
|
+
citedPairs += row.citedPairs;
|
|
2673
|
+
if (row.createdAt > newestCreatedAt) newestCreatedAt = row.createdAt;
|
|
2674
|
+
if (row.runId) runIds.push(row.runId);
|
|
2675
|
+
const providerBreakdown = parseJsonColumn(row.providerBreakdown, {});
|
|
2676
|
+
for (const [provider, entry] of Object.entries(providerBreakdown)) {
|
|
2677
|
+
const existing = mergedProviders[provider] ?? { total: 0, cited: 0, citedRate: 0 };
|
|
2678
|
+
existing.total += entry.total;
|
|
2679
|
+
existing.cited += entry.cited;
|
|
2680
|
+
mergedProviders[provider] = existing;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
for (const entry of Object.values(mergedProviders)) {
|
|
2684
|
+
entry.citedRate = entry.total > 0 ? entry.cited / entry.total : 0;
|
|
2685
|
+
}
|
|
2686
|
+
const overallCitedRate = totalPairs > 0 ? citedPairs / totalPairs : 0;
|
|
2687
|
+
return {
|
|
2688
|
+
// Synthetic id so consumers can tell this is an aggregate; concatenate
|
|
2689
|
+
// source runIds for traceability without inventing a new schema column.
|
|
2690
|
+
id: `group:${runIds.join(",")}`,
|
|
2691
|
+
projectId,
|
|
2692
|
+
runId: runIds[0] ?? null,
|
|
2693
|
+
overallCitedRate,
|
|
2694
|
+
totalPairs,
|
|
2695
|
+
citedPairs,
|
|
2696
|
+
providerBreakdown: mergedProviders,
|
|
2697
|
+
createdAt: newestCreatedAt,
|
|
2698
|
+
status: "ready"
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2659
2701
|
async function intelligenceRoutes(app) {
|
|
2660
2702
|
app.get("/projects/:name/insights", async (request, reply) => {
|
|
2661
2703
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -2687,11 +2729,27 @@ async function intelligenceRoutes(app) {
|
|
|
2687
2729
|
});
|
|
2688
2730
|
app.get("/projects/:name/health/latest", async (request, reply) => {
|
|
2689
2731
|
const project = resolveProject(app.db, request.params.name);
|
|
2690
|
-
const
|
|
2691
|
-
|
|
2732
|
+
const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and3(
|
|
2733
|
+
eq11(runs.projectId, project.id),
|
|
2734
|
+
eq11(runs.kind, RunKinds["answer-visibility"]),
|
|
2735
|
+
inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
|
|
2736
|
+
)).orderBy(desc4(runs.createdAt), desc4(runs.id)).all();
|
|
2737
|
+
const latestGroup = groupRunsByCreatedAt(projectVisRuns)[0] ?? [];
|
|
2738
|
+
const latestGroupRunIds = latestGroup.map((r) => r.id);
|
|
2739
|
+
if (latestGroupRunIds.length > 0) {
|
|
2740
|
+
const groupRows = app.db.select().from(healthSnapshots).where(and3(
|
|
2741
|
+
eq11(healthSnapshots.projectId, project.id),
|
|
2742
|
+
inArray3(healthSnapshots.runId, latestGroupRunIds)
|
|
2743
|
+
)).all();
|
|
2744
|
+
if (groupRows.length > 0) {
|
|
2745
|
+
return reply.send(aggregateHealthSnapshots(project.id, groupRows));
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
const fallback = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
|
|
2749
|
+
if (!fallback) {
|
|
2692
2750
|
return reply.send(emptyHealthSnapshot(project.id));
|
|
2693
2751
|
}
|
|
2694
|
-
return reply.send(mapHealthRow(
|
|
2752
|
+
return reply.send(mapHealthRow(fallback));
|
|
2695
2753
|
});
|
|
2696
2754
|
app.get("/projects/:name/health/history", async (request, reply) => {
|
|
2697
2755
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -2703,7 +2761,7 @@ async function intelligenceRoutes(app) {
|
|
|
2703
2761
|
}
|
|
2704
2762
|
|
|
2705
2763
|
// ../api-routes/src/report.ts
|
|
2706
|
-
import { and as and5, desc as desc6, eq as eq13, gte, inArray as
|
|
2764
|
+
import { and as and5, desc as desc6, eq as eq13, gte, inArray as inArray5, lt, lte, ne, or as or2, sql as sql3 } from "drizzle-orm";
|
|
2707
2765
|
|
|
2708
2766
|
// ../api-routes/src/report-renderer.ts
|
|
2709
2767
|
var COLORS = {
|
|
@@ -4939,7 +4997,7 @@ function renderReportHtml(report, opts = {}) {
|
|
|
4939
4997
|
}
|
|
4940
4998
|
|
|
4941
4999
|
// ../api-routes/src/content-data.ts
|
|
4942
|
-
import { and as and4, eq as eq12, desc as desc5, inArray as
|
|
5000
|
+
import { and as and4, eq as eq12, desc as desc5, inArray as inArray4 } from "drizzle-orm";
|
|
4943
5001
|
var RECENT_RUNS_WINDOW = 5;
|
|
4944
5002
|
function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
4945
5003
|
const projectId = project.id;
|
|
@@ -5069,7 +5127,7 @@ function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter)
|
|
|
5069
5127
|
// Queued/running/failed/cancelled runs may have partial or no
|
|
5070
5128
|
// snapshots; including them risks pointing latestRunId at a run with
|
|
5071
5129
|
// no usable evidence.
|
|
5072
|
-
|
|
5130
|
+
inArray4(runs.status, [RunStatuses.completed, RunStatuses.partial])
|
|
5073
5131
|
)
|
|
5074
5132
|
).orderBy(desc5(runs.createdAt)).all();
|
|
5075
5133
|
const filtered = locationFilter === void 0 ? rows : rows.filter((r) => (r.location ?? null) === locationFilter);
|
|
@@ -5111,7 +5169,7 @@ function buildCandidateQueries(opts) {
|
|
|
5111
5169
|
const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq12(queries.projectId, opts.projectId)).all();
|
|
5112
5170
|
const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
|
|
5113
5171
|
const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
|
|
5114
|
-
const snapshotRows = opts.db.select().from(querySnapshots).where(
|
|
5172
|
+
const snapshotRows = opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateQueryIds.includes(r.queryId));
|
|
5115
5173
|
const snapshotsByQuery = /* @__PURE__ */ new Map();
|
|
5116
5174
|
for (const row of snapshotRows) {
|
|
5117
5175
|
const list = snapshotsByQuery.get(row.queryId) ?? [];
|
|
@@ -5329,7 +5387,7 @@ function loadSnapshotsForRun(db, runId) {
|
|
|
5329
5387
|
}
|
|
5330
5388
|
function loadSnapshotsForRunIds(db, runIds) {
|
|
5331
5389
|
if (runIds.length === 0) return [];
|
|
5332
|
-
const rows = db.select().from(querySnapshots).where(
|
|
5390
|
+
const rows = db.select().from(querySnapshots).where(inArray5(querySnapshots.runId, [...runIds])).all();
|
|
5333
5391
|
return rows.map((r) => ({
|
|
5334
5392
|
id: r.id,
|
|
5335
5393
|
runId: r.runId,
|
|
@@ -5909,7 +5967,7 @@ function buildInsightList(db, projectId, locationFilter) {
|
|
|
5909
5967
|
)
|
|
5910
5968
|
).orderBy(desc6(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
|
|
5911
5969
|
if (recentRunIds.length === 0) return [];
|
|
5912
|
-
const rows = db.select().from(insights).where(and5(eq13(insights.projectId, projectId),
|
|
5970
|
+
const rows = db.select().from(insights).where(and5(eq13(insights.projectId, projectId), inArray5(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
|
|
5913
5971
|
const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
5914
5972
|
const flat = rows.filter((r) => !r.dismissed).map((r) => {
|
|
5915
5973
|
const recommendation = parseJsonColumn(r.recommendation, null);
|
|
@@ -6671,7 +6729,7 @@ async function reportRoutes(app) {
|
|
|
6671
6729
|
}
|
|
6672
6730
|
|
|
6673
6731
|
// ../api-routes/src/citations.ts
|
|
6674
|
-
import { eq as eq14, inArray as
|
|
6732
|
+
import { eq as eq14, inArray as inArray6 } from "drizzle-orm";
|
|
6675
6733
|
async function citationRoutes(app) {
|
|
6676
6734
|
app.get("/projects/:name/citations/visibility", async (request, reply) => {
|
|
6677
6735
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -6695,7 +6753,7 @@ async function citationRoutes(app) {
|
|
|
6695
6753
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
6696
6754
|
answerMentioned: querySnapshots.answerMentioned,
|
|
6697
6755
|
createdAt: querySnapshots.createdAt
|
|
6698
|
-
}).from(querySnapshots).where(
|
|
6756
|
+
}).from(querySnapshots).where(inArray6(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
|
|
6699
6757
|
if (rawSnapshots.length === 0) {
|
|
6700
6758
|
return reply.send(emptyCitationVisibility("no-runs-yet"));
|
|
6701
6759
|
}
|
|
@@ -6835,7 +6893,7 @@ function normalizeDomain2(domain) {
|
|
|
6835
6893
|
}
|
|
6836
6894
|
|
|
6837
6895
|
// ../api-routes/src/composites.ts
|
|
6838
|
-
import { eq as eq15, and as and6, desc as desc7, sql as sql4, like, or as or3, inArray as
|
|
6896
|
+
import { eq as eq15, and as and6, desc as desc7, sql as sql4, like, or as or3, inArray as inArray7 } from "drizzle-orm";
|
|
6839
6897
|
var TOP_INSIGHT_LIMIT = 5;
|
|
6840
6898
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
6841
6899
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -7040,7 +7098,7 @@ function loadSnapshotsByRunIds(app, runIds) {
|
|
|
7040
7098
|
citationState: querySnapshots.citationState,
|
|
7041
7099
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
7042
7100
|
citedDomains: querySnapshots.citedDomains
|
|
7043
|
-
}).from(querySnapshots).where(
|
|
7101
|
+
}).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all();
|
|
7044
7102
|
for (const row of rows) {
|
|
7045
7103
|
const list = result.get(row.runId) ?? [];
|
|
7046
7104
|
list.push({
|
|
@@ -10352,7 +10410,7 @@ var routeCatalog = [
|
|
|
10352
10410
|
method: "get",
|
|
10353
10411
|
path: "/api/v1/projects/{name}/discover/sessions/{id}/promote",
|
|
10354
10412
|
summary: "Preview a discovery promotion plan (read-only)",
|
|
10355
|
-
description: "Returns
|
|
10413
|
+
description: "Returns available promotion candidates: queries grouped by bucket, plus recurring suggested competitor domains not already tracked. Read-only \u2014 use the POST to actually adopt the default subset or an explicit bucket subset.",
|
|
10356
10414
|
tags: ["discovery"],
|
|
10357
10415
|
parameters: [
|
|
10358
10416
|
nameParameter,
|
|
@@ -10362,6 +10420,43 @@ var routeCatalog = [
|
|
|
10362
10420
|
200: { description: "Promote preview returned." },
|
|
10363
10421
|
404: { description: "Project or session not found." }
|
|
10364
10422
|
}
|
|
10423
|
+
},
|
|
10424
|
+
{
|
|
10425
|
+
method: "post",
|
|
10426
|
+
path: "/api/v1/projects/{name}/discover/sessions/{id}/promote",
|
|
10427
|
+
summary: "Promote a discovery session into the tracked basket",
|
|
10428
|
+
description: 'Adopts a completed session\'s bucketed queries into the project\'s tracked basket, tagged with `provenance="discovery:<sessionId>"`. By default, only `cited` and `aspirational` queries are promoted; include `wasted-surface` explicitly when off-ICP competitor gaps should also be tracked. Recurring discovered competitor domains are also merged by default. Add-only and idempotent: queries/domains already tracked are returned under `skipped` rather than inserted twice. Only sessions with `status: "completed"` can be promoted.',
|
|
10429
|
+
tags: ["discovery"],
|
|
10430
|
+
parameters: [
|
|
10431
|
+
nameParameter,
|
|
10432
|
+
{ name: "id", in: "path", required: true, description: "Discovery session ID.", schema: stringSchema }
|
|
10433
|
+
],
|
|
10434
|
+
requestBody: {
|
|
10435
|
+
required: false,
|
|
10436
|
+
content: {
|
|
10437
|
+
"application/json": {
|
|
10438
|
+
schema: {
|
|
10439
|
+
type: "object",
|
|
10440
|
+
properties: {
|
|
10441
|
+
buckets: {
|
|
10442
|
+
type: "array",
|
|
10443
|
+
items: { type: "string", enum: ["cited", "aspirational", "wasted-surface"] },
|
|
10444
|
+
description: "Which probe buckets to promote. Omitted means cited + aspirational."
|
|
10445
|
+
},
|
|
10446
|
+
includeCompetitors: {
|
|
10447
|
+
type: "boolean",
|
|
10448
|
+
description: "Whether to also merge recurring discovered competitor domains. Defaults to true."
|
|
10449
|
+
}
|
|
10450
|
+
}
|
|
10451
|
+
}
|
|
10452
|
+
}
|
|
10453
|
+
}
|
|
10454
|
+
},
|
|
10455
|
+
responses: {
|
|
10456
|
+
200: { description: "Promotion applied; returns promoted + skipped query/competitor lists." },
|
|
10457
|
+
400: { description: "Session is not completed, or invalid request body." },
|
|
10458
|
+
404: { description: "Project or session not found." }
|
|
10459
|
+
}
|
|
10365
10460
|
}
|
|
10366
10461
|
];
|
|
10367
10462
|
var canonryLocalRouteCatalog = [
|
|
@@ -19631,7 +19726,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
19631
19726
|
else if (bucket === DiscoveryBuckets["wasted-surface"]) wasted.add(probe.query);
|
|
19632
19727
|
}
|
|
19633
19728
|
const competitorMap = parseJsonColumn(session.competitorMap, []);
|
|
19634
|
-
const newCompetitors = competitorMap.filter((entry) => !seenCompetitors.has(entry.domain.toLowerCase()))
|
|
19729
|
+
const newCompetitors = selectEligibleCompetitors(competitorMap).filter((entry) => !seenCompetitors.has(entry.domain.toLowerCase()));
|
|
19635
19730
|
return reply.send({
|
|
19636
19731
|
sessionId: session.id,
|
|
19637
19732
|
projectId: project.id,
|
|
@@ -19645,6 +19740,107 @@ async function discoveryRoutes(app, opts) {
|
|
|
19645
19740
|
});
|
|
19646
19741
|
}
|
|
19647
19742
|
);
|
|
19743
|
+
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
19744
|
+
const project = resolveProject(app.db, request.params.name);
|
|
19745
|
+
const session = app.db.select().from(discoverySessions).where(eq25(discoverySessions.id, request.params.id)).get();
|
|
19746
|
+
if (!session || session.projectId !== project.id) {
|
|
19747
|
+
throw notFound("Discovery session", request.params.id);
|
|
19748
|
+
}
|
|
19749
|
+
const parsed = discoveryPromoteRequestSchema.safeParse(request.body ?? {});
|
|
19750
|
+
if (!parsed.success) {
|
|
19751
|
+
throw validationError("Invalid discovery promote request", {
|
|
19752
|
+
issues: parsed.error.issues.map((issue) => ({
|
|
19753
|
+
path: issue.path.join("."),
|
|
19754
|
+
message: issue.message
|
|
19755
|
+
}))
|
|
19756
|
+
});
|
|
19757
|
+
}
|
|
19758
|
+
if (session.status !== DiscoverySessionStatuses.completed) {
|
|
19759
|
+
throw validationError(
|
|
19760
|
+
`Discovery session is "${session.status}" \u2014 only completed sessions can be promoted.`,
|
|
19761
|
+
{ status: session.status }
|
|
19762
|
+
);
|
|
19763
|
+
}
|
|
19764
|
+
const buckets = parsed.data.buckets ?? DEFAULT_DISCOVERY_PROMOTE_BUCKETS;
|
|
19765
|
+
const bucketSet = new Set(buckets);
|
|
19766
|
+
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
19767
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq25(discoveryProbes.sessionId, session.id)).all();
|
|
19768
|
+
const candidateQueries = /* @__PURE__ */ new Set();
|
|
19769
|
+
for (const probe of probeRows) {
|
|
19770
|
+
if (!probe.bucket) continue;
|
|
19771
|
+
const bucket = discoveryBucketSchema.safeParse(probe.bucket);
|
|
19772
|
+
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
19773
|
+
}
|
|
19774
|
+
const existingQueries = new Set(
|
|
19775
|
+
app.db.select({ query: queries.query }).from(queries).where(eq25(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
19776
|
+
);
|
|
19777
|
+
const promotedQueries = [];
|
|
19778
|
+
const skippedQueries = [];
|
|
19779
|
+
for (const query of Array.from(candidateQueries).sort()) {
|
|
19780
|
+
if (existingQueries.has(query.toLowerCase())) {
|
|
19781
|
+
skippedQueries.push(query);
|
|
19782
|
+
} else {
|
|
19783
|
+
promotedQueries.push(query);
|
|
19784
|
+
existingQueries.add(query.toLowerCase());
|
|
19785
|
+
}
|
|
19786
|
+
}
|
|
19787
|
+
const promotedCompetitors = [];
|
|
19788
|
+
const skippedCompetitors = [];
|
|
19789
|
+
if (includeCompetitors) {
|
|
19790
|
+
const existingCompetitors = new Set(
|
|
19791
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq25(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
19792
|
+
);
|
|
19793
|
+
const competitorMap = parseJsonColumn(session.competitorMap, []);
|
|
19794
|
+
for (const entry of selectEligibleCompetitors(competitorMap)) {
|
|
19795
|
+
const key = entry.domain.toLowerCase();
|
|
19796
|
+
if (existingCompetitors.has(key)) {
|
|
19797
|
+
skippedCompetitors.push(entry.domain);
|
|
19798
|
+
} else {
|
|
19799
|
+
promotedCompetitors.push(entry.domain);
|
|
19800
|
+
existingCompetitors.add(key);
|
|
19801
|
+
}
|
|
19802
|
+
}
|
|
19803
|
+
}
|
|
19804
|
+
const provenance = `discovery:${session.id}`;
|
|
19805
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19806
|
+
if (promotedQueries.length > 0 || promotedCompetitors.length > 0) {
|
|
19807
|
+
app.db.transaction((tx) => {
|
|
19808
|
+
for (const query of promotedQueries) {
|
|
19809
|
+
tx.insert(queries).values({
|
|
19810
|
+
id: crypto21.randomUUID(),
|
|
19811
|
+
projectId: project.id,
|
|
19812
|
+
query,
|
|
19813
|
+
provenance,
|
|
19814
|
+
createdAt: now
|
|
19815
|
+
}).run();
|
|
19816
|
+
}
|
|
19817
|
+
for (const domain of promotedCompetitors) {
|
|
19818
|
+
tx.insert(competitors).values({
|
|
19819
|
+
id: crypto21.randomUUID(),
|
|
19820
|
+
projectId: project.id,
|
|
19821
|
+
domain,
|
|
19822
|
+
provenance,
|
|
19823
|
+
createdAt: now
|
|
19824
|
+
}).run();
|
|
19825
|
+
}
|
|
19826
|
+
writeAuditLog(tx, {
|
|
19827
|
+
projectId: project.id,
|
|
19828
|
+
actor: "api",
|
|
19829
|
+
action: "discovery.promoted",
|
|
19830
|
+
entityType: "discovery_session",
|
|
19831
|
+
entityId: session.id,
|
|
19832
|
+
diff: { queries: promotedQueries, competitors: promotedCompetitors }
|
|
19833
|
+
});
|
|
19834
|
+
});
|
|
19835
|
+
}
|
|
19836
|
+
const result = {
|
|
19837
|
+
sessionId: session.id,
|
|
19838
|
+
projectId: project.id,
|
|
19839
|
+
promoted: { queries: promotedQueries, competitors: promotedCompetitors },
|
|
19840
|
+
skipped: { queries: skippedQueries, competitors: skippedCompetitors }
|
|
19841
|
+
};
|
|
19842
|
+
return reply.send(result);
|
|
19843
|
+
});
|
|
19648
19844
|
}
|
|
19649
19845
|
function serializeSession(row) {
|
|
19650
19846
|
return {
|
|
@@ -19681,6 +19877,9 @@ function serializeProbe(row) {
|
|
|
19681
19877
|
createdAt: row.createdAt
|
|
19682
19878
|
};
|
|
19683
19879
|
}
|
|
19880
|
+
function selectEligibleCompetitors(competitorMap) {
|
|
19881
|
+
return competitorMap.filter((entry) => entry.hits >= DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS).sort((a, b) => b.hits - a.hits || a.domain.localeCompare(b.domain)).slice(0, DISCOVERY_PROMOTE_COMPETITOR_CAP);
|
|
19882
|
+
}
|
|
19684
19883
|
|
|
19685
19884
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
19686
19885
|
import crypto22 from "crypto";
|
|
@@ -22614,7 +22813,7 @@ import crypto24 from "crypto";
|
|
|
22614
22813
|
import fs7 from "fs";
|
|
22615
22814
|
import path9 from "path";
|
|
22616
22815
|
import os5 from "os";
|
|
22617
|
-
import { and as and16, eq as eq27, inArray as
|
|
22816
|
+
import { and as and16, eq as eq27, inArray as inArray8, sql as sql10 } from "drizzle-orm";
|
|
22618
22817
|
|
|
22619
22818
|
// src/run-telemetry.ts
|
|
22620
22819
|
import crypto23 from "crypto";
|
|
@@ -22955,7 +23154,7 @@ var JobRunner = class {
|
|
|
22955
23154
|
this.registry = registry;
|
|
22956
23155
|
}
|
|
22957
23156
|
recoverStaleRuns() {
|
|
22958
|
-
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
23157
|
+
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray8(runs.status, ["running", "queued"])).all();
|
|
22959
23158
|
if (stale.length === 0) return;
|
|
22960
23159
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22961
23160
|
for (const run of stale) {
|
|
@@ -23018,7 +23217,7 @@ var JobRunner = class {
|
|
|
23018
23217
|
}
|
|
23019
23218
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
23020
23219
|
const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
|
|
23021
|
-
projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and16(eq27(queries.projectId, projectId),
|
|
23220
|
+
projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and16(eq27(queries.projectId, projectId), inArray8(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
|
|
23022
23221
|
const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
|
|
23023
23222
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
23024
23223
|
const allDomains = effectiveDomains({
|
|
@@ -24428,7 +24627,7 @@ function writeDiscoveryInsight(db, input) {
|
|
|
24428
24627
|
provider: input.seedProvider,
|
|
24429
24628
|
recommendation: JSON.stringify({
|
|
24430
24629
|
action: "review-discovered-basket",
|
|
24431
|
-
summary: `Run \`canonry discover show ${input.sessionId} --format json\` to inspect the per-query breakdown
|
|
24630
|
+
summary: `Run \`canonry discover show ${input.sessionId} --format json\` to inspect the per-query breakdown, then \`canonry discover promote <project> ${input.sessionId}\` to merge cited + aspirational findings into the project.`,
|
|
24432
24631
|
bucketCounts: buckets,
|
|
24433
24632
|
topCompetitors
|
|
24434
24633
|
}),
|
|
@@ -24672,7 +24871,7 @@ var Scheduler = class {
|
|
|
24672
24871
|
};
|
|
24673
24872
|
|
|
24674
24873
|
// src/notifier.ts
|
|
24675
|
-
import { eq as eq35, desc as desc16, and as and22, inArray as
|
|
24874
|
+
import { eq as eq35, desc as desc16, and as and22, inArray as inArray9, or as or4 } from "drizzle-orm";
|
|
24676
24875
|
import crypto31 from "crypto";
|
|
24677
24876
|
var log10 = createLogger("Notifier");
|
|
24678
24877
|
var Notifier = class {
|
|
@@ -24825,13 +25024,13 @@ var Notifier = class {
|
|
|
24825
25024
|
provider: querySnapshots.provider,
|
|
24826
25025
|
location: querySnapshots.location,
|
|
24827
25026
|
citationState: querySnapshots.citationState
|
|
24828
|
-
}).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(
|
|
25027
|
+
}).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(inArray9(querySnapshots.runId, currentRunIds)).all();
|
|
24829
25028
|
const previousSnapshots = this.db.select({
|
|
24830
25029
|
queryId: querySnapshots.queryId,
|
|
24831
25030
|
provider: querySnapshots.provider,
|
|
24832
25031
|
location: querySnapshots.location,
|
|
24833
25032
|
citationState: querySnapshots.citationState
|
|
24834
|
-
}).from(querySnapshots).where(
|
|
25033
|
+
}).from(querySnapshots).where(inArray9(querySnapshots.runId, previousRunIds)).all();
|
|
24835
25034
|
const prevMap = /* @__PURE__ */ new Map();
|
|
24836
25035
|
for (const s of previousSnapshots) {
|
|
24837
25036
|
prevMap.set(`${s.queryId}:${s.provider}:${s.location ?? ""}`, s.citationState);
|
|
@@ -27082,7 +27281,7 @@ async function createServer(opts) {
|
|
|
27082
27281
|
content = `[system] Discovery run ${ctx.runId} failed for project ${project.name}: ${ctx.error ?? "unknown error"}. Surface a one-line diagnosis and a suggested next step.`;
|
|
27083
27282
|
} else {
|
|
27084
27283
|
const top = ctx.topCompetitors.map((c) => `${c.domain}(${c.hits})`).join(", ") || "none";
|
|
27085
|
-
content = `[system] Discovery run ${ctx.runId} completed for project ${project.name} (session ${ctx.sessionId}). Buckets \u2014 cited:${ctx.buckets.cited}, wasted-surface:${ctx.buckets["wasted-surface"]}, aspirational:${ctx.buckets.aspirational} (${ctx.probeCount} probes; seed provider: ${ctx.seedProvider ?? "unknown"}). Top recurring competitor domains: ${top}. Use canonry_discover_session_get to pull per-query buckets and call out
|
|
27284
|
+
content = `[system] Discovery run ${ctx.runId} completed for project ${project.name} (session ${ctx.sessionId}). Buckets \u2014 cited:${ctx.buckets.cited}, wasted-surface:${ctx.buckets["wasted-surface"]}, aspirational:${ctx.buckets.aspirational} (${ctx.probeCount} probes; seed provider: ${ctx.seedProvider ?? "unknown"}). Top recurring competitor domains: ${top}. Use canonry_discover_session_get to pull per-query buckets and call out cited + aspirational findings worth promoting. Keep it tight.`;
|
|
27086
27285
|
}
|
|
27087
27286
|
} else {
|
|
27088
27287
|
content = `[system] Run ${ctx.runId} completed for project ${project.name}. ${ctx.insightCount} insights generated (${ctx.criticalOrHigh} critical/high). Use canonry_run_get to inspect the run and canonry_insights_list to review new findings. Surface anything notable briefly \u2014 skip chit-chat.`;
|
|
@@ -2356,6 +2356,12 @@ var trafficEventsResponseSchema = z20.object({
|
|
|
2356
2356
|
import { z as z21 } from "zod";
|
|
2357
2357
|
var discoveryBucketSchema = z21.enum(["cited", "aspirational", "wasted-surface"]);
|
|
2358
2358
|
var DiscoveryBuckets = discoveryBucketSchema.enum;
|
|
2359
|
+
var DEFAULT_DISCOVERY_PROMOTE_BUCKETS = [
|
|
2360
|
+
DiscoveryBuckets.cited,
|
|
2361
|
+
DiscoveryBuckets.aspirational
|
|
2362
|
+
];
|
|
2363
|
+
var DISCOVERY_PROMOTE_COMPETITOR_CAP = 20;
|
|
2364
|
+
var DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS = 2;
|
|
2359
2365
|
var discoverySessionStatusSchema = z21.enum(["queued", "seeding", "probing", "completed", "failed"]);
|
|
2360
2366
|
var DiscoverySessionStatuses = discoverySessionStatusSchema.enum;
|
|
2361
2367
|
var discoveryCompetitorMapEntrySchema = z21.object({
|
|
@@ -2400,6 +2406,33 @@ var discoveryRunRequestSchema = z21.object({
|
|
|
2400
2406
|
dedupThreshold: z21.number().min(0).max(1).optional(),
|
|
2401
2407
|
maxProbes: z21.number().int().positive().max(DISCOVERY_MAX_PROBES_CAP).optional()
|
|
2402
2408
|
});
|
|
2409
|
+
var discoveryPromoteRequestSchema = z21.object({
|
|
2410
|
+
buckets: z21.array(discoveryBucketSchema).min(1).optional(),
|
|
2411
|
+
includeCompetitors: z21.boolean().optional()
|
|
2412
|
+
});
|
|
2413
|
+
var discoveryPromotePreviewSchema = z21.object({
|
|
2414
|
+
sessionId: z21.string(),
|
|
2415
|
+
projectId: z21.string(),
|
|
2416
|
+
status: discoverySessionStatusSchema,
|
|
2417
|
+
queriesByBucket: z21.object({
|
|
2418
|
+
cited: z21.array(z21.string()),
|
|
2419
|
+
aspirational: z21.array(z21.string()),
|
|
2420
|
+
"wasted-surface": z21.array(z21.string())
|
|
2421
|
+
}),
|
|
2422
|
+
suggestedCompetitors: z21.array(discoveryCompetitorMapEntrySchema)
|
|
2423
|
+
});
|
|
2424
|
+
var discoveryPromoteResultSchema = z21.object({
|
|
2425
|
+
sessionId: z21.string(),
|
|
2426
|
+
projectId: z21.string(),
|
|
2427
|
+
promoted: z21.object({
|
|
2428
|
+
queries: z21.array(z21.string()),
|
|
2429
|
+
competitors: z21.array(z21.string())
|
|
2430
|
+
}),
|
|
2431
|
+
skipped: z21.object({
|
|
2432
|
+
queries: z21.array(z21.string()),
|
|
2433
|
+
competitors: z21.array(z21.string())
|
|
2434
|
+
})
|
|
2435
|
+
});
|
|
2403
2436
|
var queryProvenanceSchema = z21.union([
|
|
2404
2437
|
z21.literal("cli"),
|
|
2405
2438
|
z21.string().regex(/^discovery:.+$/)
|
|
@@ -2637,9 +2670,13 @@ export {
|
|
|
2637
2670
|
TrafficEventKinds,
|
|
2638
2671
|
discoveryBucketSchema,
|
|
2639
2672
|
DiscoveryBuckets,
|
|
2673
|
+
DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
|
|
2674
|
+
DISCOVERY_PROMOTE_COMPETITOR_CAP,
|
|
2675
|
+
DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS,
|
|
2640
2676
|
DiscoverySessionStatuses,
|
|
2641
2677
|
DISCOVERY_MAX_PROBES_CAP,
|
|
2642
2678
|
discoveryRunRequestSchema,
|
|
2679
|
+
discoveryPromoteRequestSchema,
|
|
2643
2680
|
clusterByCosine,
|
|
2644
2681
|
pickClusterRepresentative,
|
|
2645
2682
|
formatRatio,
|