@canonry/canonry 5.22.0 → 5.28.1

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 (43) hide show
  1. package/README.md +2 -1
  2. package/assets/agent-workspace/skills/aero/SKILL.md +37 -3
  3. package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +26 -12
  4. package/assets/agent-workspace/skills/aero/references/reporting.md +1 -0
  5. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +100 -5
  6. package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +30 -6
  7. package/assets/agent-workspace/skills/canonry/references/indexing.md +1 -0
  8. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +30 -0
  9. package/assets/assets/{AuditHistoryPanel-teTDC-4-.js → AuditHistoryPanel-CWd9UN6L.js} +1 -1
  10. package/assets/assets/{BacklinksPage-Bqm7bido.js → BacklinksPage-B2gPTzyE.js} +1 -1
  11. package/assets/assets/ChartPrimitives-B5okB5g-.js +1 -0
  12. package/assets/assets/{HistoryPage-IcqwPBmt.js → HistoryPage-xtkG8SZN.js} +1 -1
  13. package/assets/assets/{MeasurementPropertyPage-BLpptT3_.js → MeasurementPropertyPage-DxIMU3_T.js} +1 -1
  14. package/assets/assets/{ProjectPage-nn2Y9H-m.js → ProjectPage-CMAFZq_s.js} +5 -5
  15. package/assets/assets/{RunRow-S_SCHr0o.js → RunRow-DnSo7Nle.js} +1 -1
  16. package/assets/assets/{RunsPage-DFiif626.js → RunsPage-FAEuo4Ra.js} +1 -1
  17. package/assets/assets/{SettingsPage-CitZ6FAS.js → SettingsPage-C4OZ1HB9.js} +1 -1
  18. package/assets/assets/{SiteHealthSection-C2dsyNGY.js → SiteHealthSection-BzMxlz8-.js} +3 -3
  19. package/assets/assets/{TrafficPage-lY8na5Pg.js → TrafficPage-D03S6xF6.js} +1 -1
  20. package/assets/assets/{TrafficSourceDetailPage-BTykoVGT.js → TrafficSourceDetailPage-DBu6oFFK.js} +1 -1
  21. package/assets/assets/{extract-error-message-r0Gpc7mv.js → extract-error-message-BuH2CCaJ.js} +1 -1
  22. package/assets/assets/index-Ba-Zs45G.js +86 -0
  23. package/assets/assets/index-DfhmHULl.css +1 -0
  24. package/assets/assets/{react-sigma_core.esm.min-DDrDxbO1.js → react-sigma_core.esm.min-CxwZsgxR.js} +1 -1
  25. package/assets/assets/{v2-overview-adapter-OsQiglXU.js → v2-overview-adapter-DTCKxzZT.js} +1 -1
  26. package/assets/index.html +2 -2
  27. package/dist/{chunk-P5VXVFQW.js → chunk-7RKCDOWO.js} +6773 -5254
  28. package/dist/{chunk-YTSXQUJ3.js → chunk-DX2JUC5H.js} +3 -3
  29. package/dist/{chunk-XFHXKLNT.js → chunk-EP4STEPI.js} +3542 -1165
  30. package/dist/{chunk-GLLY2GEV.js → chunk-FNGITGDF.js} +172 -7
  31. package/dist/{chunk-S5OSY3PI.js → chunk-KVQHG3WB.js} +6693 -5891
  32. package/dist/{chunk-DDL2XSNM.js → chunk-MPICT6V7.js} +128 -35
  33. package/dist/{chunk-3LCR6WU6.js → chunk-ONTEFAA7.js} +2 -2
  34. package/dist/cli.js +436 -44
  35. package/dist/{demo-server-HULNEORN.js → demo-server-HWWIMGD6.js} +4 -4
  36. package/dist/index.d.ts +9 -1
  37. package/dist/index.js +6 -6
  38. package/dist/{intelligence-service-CXNBKRHV.js → intelligence-service-6LDMVI66.js} +2 -2
  39. package/dist/mcp.js +4 -4
  40. package/package.json +14 -13
  41. package/assets/assets/ChartPrimitives-BQC2VH0O.js +0 -1
  42. package/assets/assets/index-DwlO8KfE.js +0 -86
  43. package/assets/assets/index-EJC8PSbh.css +0 -1
@@ -4,6 +4,7 @@ import {
4
4
  DISCOVERY_MAX_PROBES_CAP,
5
5
  DISCOVERY_PROBE_CONCURRENCY_CAP,
6
6
  GOOGLE_MARKETING_STORED_SNAPSHOT_PAGE_MAX,
7
+ LATEST_RUN_ID,
7
8
  RESEARCH_RUN_SCOPE,
8
9
  adsActivateTreeRequestSchema,
9
10
  adsAdCreateRequestSchema,
@@ -71,13 +72,14 @@ import {
71
72
  measurementPropertyCompetitorsQuerySchema,
72
73
  measurementPropertyEvidenceQuerySchema,
73
74
  measurementPropertyQuestionsQuerySchema,
75
+ measurementQueryClassFilterSchema,
74
76
  measurementQuerySetUpsertRequestSchema,
75
77
  measurementQueryTemplateApplyRequestSchema,
76
78
  measurementQueryTemplateUpsertRequestSchema,
77
79
  measurementQuestionResultQuerySchema,
78
80
  notificationCreateRequestSchema,
79
81
  notificationEventSchema,
80
- operationalLogListDtoSchema,
82
+ operationalLogListReadSchema,
81
83
  organicEvidencePeriodSchema,
82
84
  projectConfigSchema,
83
85
  projectUpsertRequestSchema,
@@ -87,6 +89,7 @@ import {
87
89
  queryGenerateRequestSchema,
88
90
  queryTrackingCommitRequestSchema,
89
91
  queryTrackingPreviewRequestSchema,
92
+ referralAssessmentQuerySchema,
90
93
  reportMonthSchema,
91
94
  reportPeriodSchema,
92
95
  researchBatchCreateSchema,
@@ -107,7 +110,7 @@ import {
107
110
  trafficSeriesGranularitySchema,
108
111
  visibilityCompareSelectionSchema,
109
112
  visibilityReportRequestSchema
110
- } from "./chunk-S5OSY3PI.js";
113
+ } from "./chunk-KVQHG3WB.js";
111
114
 
112
115
  // src/cli-error.ts
113
116
  function isMachineFormat(format) {
@@ -392,20 +395,34 @@ var measurementOverviewInputSchema = z3.object({
392
395
  context.addIssue({ code: "custom", path: ["targetKey"], message: `${input.scope} scope does not accept targetKey.` });
393
396
  }
394
397
  });
398
+ var AGENT_DEFAULT_QUERY_CLASS = "non-brand";
399
+ var propertyQueryClassInputSchema = measurementQueryClassFilterSchema.optional().describe("Omit for non-brand. branded reads brand recall; all pools both classes, so name the class you report.");
400
+ function agentQueryClass(queryClass) {
401
+ return queryClass ?? AGENT_DEFAULT_QUERY_CLASS;
402
+ }
395
403
  var measurementPropertyEvidenceInputSchema = measurementPropertyEvidenceQuerySchema.extend({
396
404
  project: projectNameSchema,
397
405
  targetKey: measurementPropertyEvidenceQuerySchema.shape.targetKey.describe("Property stable key. Required \u2014 this read is scoped to exactly one Property."),
406
+ queryClass: propertyQueryClassInputSchema,
398
407
  shape: measurementPropertyEvidenceQuerySchema.shape.shape.describe(
399
408
  "What one row is. Omit for sources (one row per cited URL). answers gives one row per measured answer with its cited URLs nested, including the answers that cited nothing."
400
409
  )
401
410
  }).strict();
411
+ var PORTFOLIO_SUMMARY_AGENT_ROWS = 4;
412
+ var PORTFOLIO_SUMMARY_AGENT_LIMIT_MAX = 10;
413
+ function portfolioSummaryForAgents(summary, limitNote) {
414
+ const rows = summary.weakestProperties;
415
+ return {
416
+ ...limitNote === void 0 ? {} : { limitNote },
417
+ ...summary,
418
+ ...Array.isArray(rows) ? { weakestProperties: summary.weakestProperties.map(({ recommendedInstead: _names, recommendedInsteadTotal: _total, recommendedInsteadTruncated: _cut, ...row }) => row) } : {}
419
+ };
420
+ }
402
421
  var measurementPortfolioSummaryInputSchema = measurementPortfolioSummaryQuerySchema.extend({
403
422
  project: projectNameSchema,
404
- groupKey: measurementPortfolioSummaryQuerySchema.shape.groupKey.describe("A market stable key (a row's metro.groupKey). Scopes rows to that market and lists its direct submarkets."),
405
- // Capped below the route's 50: past about 10 rows the per-row evidence
406
- // (names given instead, cited domains) no longer fits the tool-result cap.
407
- limit: z3.number().int().positive().max(10).optional().describe("Rows per list, at most 10. Default 4 keeps the whole result under the tool-result cap; prefer groupKey over a larger limit."),
408
- includeNestedMarkets: measurementPortfolioSummaryQuerySchema.shape.includeNestedMarkets.describe("Every market at every level, uncapped. Off by default; large portfolios exceed the cap with it on.")
423
+ groupKey: measurementPortfolioSummaryQuerySchema.shape.groupKey.describe("A market stable key (a row's metro.groupKey or a markets row's groupKey). Scopes rows to that market and lists its direct submarkets."),
424
+ limit: z3.number().int().positive().max(PORTFOLIO_SUMMARY_AGENT_LIMIT_MAX).optional().describe(`Rows per Property list. Default ${PORTFOLIO_SUMMARY_AGENT_ROWS}, which is also the most returned (a larger value is lowered and limitNote says so). For other rows pass groupKey (one metro's weakest), or page canonry_measurement_overview.`),
425
+ includeNestedMarkets: measurementPortfolioSummaryQuerySchema.shape.includeNestedMarkets.describe("Every market at every level, uncapped. Off by default and rarely needed: markets already lists every metro, and groupKey lists one metro's submarkets. Large portfolios exceed the tool-result cap with it on.")
409
426
  }).strict();
410
427
  var measurementPropertyQuestionsInputSchema = measurementPropertyQuestionsQuerySchema.extend({
411
428
  project: projectNameSchema
@@ -414,10 +431,14 @@ var measurementQuestionResultInputSchema = measurementQuestionResultQuerySchema.
414
431
  project: projectNameSchema
415
432
  }).strict();
416
433
  var measurementPropertyCompetitorsInputSchema = measurementPropertyCompetitorsQuerySchema.extend({
417
- project: projectNameSchema
434
+ project: projectNameSchema,
435
+ queryClass: propertyQueryClassInputSchema
418
436
  }).strict();
419
- var measurementChangesInputSchema = measurementChangesQuerySchema.extend({
420
- project: projectNameSchema
437
+ var MEASUREMENT_CHANGES_AGENT_ROWS = 20;
438
+ var measurementChangesInputSchema = measurementChangesQuerySchema.safeExtend({
439
+ project: projectNameSchema,
440
+ queryClass: measurementQueryClassFilterSchema.default(AGENT_DEFAULT_QUERY_CLASS).describe("Defaults to non-brand. Call once per class; all pools both classes in rows and distribution."),
441
+ limit: z3.number().int().positive().max(MEASUREMENT_CHANGES_AGENT_ROWS).optional().describe(`Changed rows, largest move first. Default 10, at most ${MEASUREMENT_CHANGES_AGENT_ROWS}; distribution counts every Property whatever the limit.`)
421
442
  }).strict();
422
443
  var measurementDataQualityInputSchema = measurementDataQualityQuerySchema.extend({
423
444
  project: projectNameSchema
@@ -554,6 +575,14 @@ var gscTopPagesInputSchema = z3.object({
554
575
  limit: z3.number().int().positive().max(500).optional(),
555
576
  window: analyticsWindowSchema.optional()
556
577
  });
578
+ var gscQueryTotalsInputSchema = z3.object({
579
+ project: projectNameSchema,
580
+ startDate: z3.string().optional(),
581
+ endDate: z3.string().optional(),
582
+ limit: z3.number().int().positive().max(5e3).optional(),
583
+ offset: z3.number().int().nonnegative().optional(),
584
+ window: analyticsWindowSchema.optional()
585
+ });
557
586
  var gscInspectionsInputSchema = z3.object({
558
587
  project: projectNameSchema,
559
588
  url: z3.string().optional(),
@@ -704,6 +733,12 @@ var gbpLocationScopedInputSchema = z3.object({
704
733
  project: projectNameSchema,
705
734
  locationName: z3.string().optional()
706
735
  });
736
+ var gbpReviewsInputSchema = z3.object({
737
+ project: projectNameSchema,
738
+ locationName: z3.string().optional(),
739
+ negative: z3.boolean().optional().describe("Only reviews at or below the project's negative-review threshold (negativeReviewMaxStars, 3 stars by default), the one the review.negative webhook uses."),
740
+ limit: z3.number().int().positive().max(500).optional().describe("Max reviews, newest first. Default 50.")
741
+ });
707
742
  var gbpAccountsInputSchema = z3.object({
708
743
  project: projectNameSchema
709
744
  });
@@ -849,9 +884,16 @@ var competitorsInputSchema = z3.object({
849
884
  project: projectNameSchema,
850
885
  request: competitorBatchRequestSchema
851
886
  });
887
+ var latestSweepRunIdDescription = "A run id, or latest for the latest sweep (the run the measurement reads display). With queryClass branded or non-brand and neither runId nor window, this tool reads latest; pass window to pool every sweep in it.";
852
888
  var competitorLandscapeInputSchema = competitorLandscapeQuerySchema.safeExtend({
853
- project: projectNameSchema
889
+ project: projectNameSchema,
890
+ runId: z3.string().trim().min(1).optional().describe(latestSweepRunIdDescription)
854
891
  }).strict();
892
+ function latestSweepUnlessPooled(query) {
893
+ if (query.runId !== void 0 || query.window !== void 0) return query;
894
+ if (query.queryClass !== "branded" && query.queryClass !== "non-brand") return query;
895
+ return { ...query, runId: LATEST_RUN_ID };
896
+ }
855
897
  var projectUpsertInputSchema = z3.object({
856
898
  project: projectNameSchema,
857
899
  request: projectUpsertRequestSchema
@@ -1147,14 +1189,24 @@ var canonryMcpTools = [
1147
1189
  defineTool({
1148
1190
  name: "canonry_visibility_report",
1149
1191
  title: "Read scoped AI visibility",
1150
- description: "Read stored visibility for a site, group, market or property. Branded, non-brand and unclassified answers remain separate populations. The response owns rates, trends, query performance, answers and competitors under one frozen measured definition. Each population carries `comparison`, the change versus the previous eligible whole-project sweep when both are complete and comparable; it is absent when that sweep cannot be read. Material plan changes retain the prior measured revision; pending assignments are explicit. Search filters the query list only. Reuse cursors with identical selection. Never starts a sweep.",
1192
+ description: "Read stored visibility for a site, group, market or property. Branded, non-brand and unclassified answers remain separate populations. The response owns rates, trends, query performance, answers and competitors under one frozen measured definition. Each population carries `comparison`, the change versus the previous eligible whole-project sweep when both are complete and comparable; it is absent when that sweep cannot be read. Material plan changes retain the prior measured revision; pending assignments are explicit. Search filters the query list only. Reuse cursors with identical selection. On an Advanced project use mode advanced, or omit mode: mode simple reads only pre-plan sweeps, so it is refused there unless runId names one. Never starts a sweep.",
1151
1193
  access: "read",
1152
1194
  tier: "monitoring",
1153
1195
  inputSchema: visibilityReportRequestSchema.safeExtend({ project: projectNameSchema }),
1154
1196
  annotations: readAnnotations(),
1155
1197
  openApiOperations: ["GET /api/v1/projects/{name}/visibility-report"],
1156
- handler: (client, input) => {
1198
+ handler: async (client, input) => {
1157
1199
  const { project, ...selection } = input;
1200
+ if (selection.mode === "simple" && selection.runId === void 0) {
1201
+ const { versions } = await client.listMeasurementPlanVersions(project);
1202
+ if (versions.some((version) => version.active)) {
1203
+ throw new CliError({
1204
+ code: "VALIDATION_ERROR",
1205
+ message: 'This project measures through an Advanced Measurement plan, so mode "simple" reads only pre-plan sweeps. Use mode "advanced", or omit mode. To inspect one pre-plan sweep, pass its runId.',
1206
+ details: { project, mode: "simple" }
1207
+ });
1208
+ }
1209
+ }
1158
1210
  return client.getVisibilityReport(project, selection);
1159
1211
  }
1160
1212
  }),
@@ -1227,7 +1279,7 @@ var canonryMcpTools = [
1227
1279
  defineTool({
1228
1280
  name: "canonry_project_overview",
1229
1281
  title: "Get project overview (composite)",
1230
- 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 read tools for the same question. SCOPE LIMIT: this returns nothing about Advanced Measurement: no plan, Targets, Properties, groups, or segments. Its silence is not evidence that a project lacks them. Before stating that a project has no Properties, no Advanced plan, or cannot break out per-Property performance, call canonry_measurement_plan_get to establish the portfolio type, and canonry_measurement_portfolio_summary to rank Properties.',
1282
+ 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 read tools for the same question. queryClassScope says which figures pool branded and non-brand: the coverage, scores (except scores.mentionShare) and movement count queries across both classes, so never report them as one class or as answers. The latest run's status and health are not a completeness check: for expected, executed and missing answers read canonry_measurement_data_quality or, for a plan run, canonry_run_completeness. canonry_doctor checks configuration and integrations, not sweeps. SCOPE LIMIT: this returns nothing about Advanced Measurement: no plan, Targets, Properties, groups, or segments. Its silence is not evidence that a project lacks them. When the system prompt has no Project shape line, call canonry_measurement_plan_get before stating that a project has no Properties, no Advanced plan, or cannot break out per-Property performance; use canonry_measurement_portfolio_summary to rank Properties.`,
1231
1283
  access: "read",
1232
1284
  tier: "core",
1233
1285
  inputSchema: z3.object({
@@ -1287,14 +1339,14 @@ var canonryMcpTools = [
1287
1339
  defineTool({
1288
1340
  name: "canonry_analytics_sources",
1289
1341
  title: "Get cited-source rankings",
1290
- description: "Where AI engines cite from for a project: cited domains ranked by how many answers cite them, each tagged with a category and a surface class (own / direct-competitor / ota-aggregator / editorial-media / other), plus a surface-class roll-up and a per-provider breakdown. Project-wide: without `runId` it pools every sweep in the window (`runCount` says how many); without `queryClass` it pools branded and non-brand answers, where branded queries inflate your own domain. For the sources behind non-brand answers, set `queryClass=non-brand` and the latest `runId`; on a v2 measurement plan the class comes from the plan, the same answers as canonry_competitor_landscape with scope=all-markets. Counts come from each answer's stored source list, so every engine is included, Gemini too. A domain counts at most once per answer; `answerShare` is the share of all answers in scope that cite it. `providersWithoutSources` names engines that answered but cited nothing. `limit` caps each ranked list (default 10 for this tool, which keeps each engine list whole); a long-tail rollup keeps the totals. The per-query breakdown is omitted unless `includeByQuery` is true. No LLM calls; probe runs excluded.",
1342
+ description: "Where AI engines cite from for a project: cited domains ranked by how many answers cite them, each tagged with a category and a surface class (own / direct-competitor / ota-aggregator / editorial-media / other), plus a surface-class roll-up and a per-provider breakdown. Project-wide: without `queryClass` it pools branded and non-brand answers, where branded queries inflate your own domain. With `queryClass` set and neither `runId` nor `window`, this tool reads the latest sweep (`runId` latest); pass `window` to pool every sweep in it. `pooledAcrossRuns` true (`runCount` above 1) means the counts sum several runs and `runIds` is their scope; the legacy `runId` field is then only the newest run in the window. On a v2 measurement plan the class comes from the plan, the same answers as canonry_competitor_landscape with scope=all-markets. `countUnits` says what each count counts: `domainTotal` is distinct domains, never answers or citations. Compare engines by answer count or `answerShare`, never by rank position. Counts come from each answer's stored source list, so every engine is included, Gemini too. A domain counts at most once per answer; `answerShare` is the share of all answers in scope that cite it. `providersWithoutSources` names engines that answered but cited nothing. `limit` caps each ranked list (default 10 for this tool, which keeps each engine list whole); a long-tail rollup keeps the totals. The per-query breakdown is omitted unless `includeByQuery` is true. No LLM calls; probe runs excluded.",
1291
1343
  access: "read",
1292
1344
  tier: "monitoring",
1293
1345
  inputSchema: z3.object({
1294
1346
  project: projectNameSchema,
1295
1347
  window: analyticsWindowSchema.optional().describe("Time range: 7d, 30d, 90d, or all (default all)."),
1296
1348
  limit: z3.number().int().positive().max(50).optional().describe("Cap each ranked list to the top N domains. Default 10; larger lists can exceed the tool-result cap."),
1297
- runId: z3.string().trim().min(1).optional().describe("Read one answer-visibility run (for example the latest sweep) instead of pooling every run in the window."),
1349
+ runId: z3.string().trim().min(1).optional().describe(latestSweepRunIdDescription),
1298
1350
  queryClass: z3.enum(["all", "branded", "non-brand"]).optional().describe("Branded or non-brand answers only. With an active v2 measurement plan the class comes from the plan assignments. Default all pools both classes."),
1299
1351
  includeByQuery: z3.boolean().optional().describe("Include the per-query breakdown. It is large; default false for this tool.")
1300
1352
  }),
@@ -1305,7 +1357,7 @@ var canonryMcpTools = [
1305
1357
  // The full ranked lists run past the agent's tool-result cap on a large
1306
1358
  // project, and the per-engine lists are the first thing lost.
1307
1359
  limit: input.limit ?? 10,
1308
- runId: input.runId,
1360
+ runId: latestSweepUnlessPooled(input).runId,
1309
1361
  queryClass: input.queryClass,
1310
1362
  includeByQuery: input.includeByQuery ?? false
1311
1363
  })
@@ -1313,7 +1365,7 @@ var canonryMcpTools = [
1313
1365
  defineTool({
1314
1366
  name: "canonry_competitor_landscape",
1315
1367
  title: "Get historical competitor landscape",
1316
- description: "Returns pinned competitors first, then observed direct competitors and other cited sources from stored answer/source evidence only. Mention share is percentage points (0..100) from answer text; citations are independent. The response carries basis (tracked or observed), availability, and reason. Pins exclusively define the comparison set when present. Otherwise at least 3 stored direct competitors must each be mentioned in 3 answers in the selected scope; raw names and platforms never enter that denominator. Without a comparison set the ratio is null, never 100%. The full comparison array is uncapped. Share of voice needs ONE query class: `queryClass=all`, and omitting it, pool branded and non-brand, so every `shareOfVoice` comes back null and only the counts are published. Pass `queryClass=non-brand` (or `branded`) for a ratio, exactly as visibility-stats does. A project with no brand name or alias cannot be split by class, so a class-scoped read on one is refused rather than answered empty. Optional groupBy: model adds provider/requested-model groups with separate served-model evidence and sample counts. Optional model filters one exact requested model ID and requires provider. Unknown historical models remain explicit. Groups are not a matched-query or equal-weight comparison. Advanced reads support one market group or explicit scope: all-markets. On a project with an active v2 measurement plan, queryClass=branded or non-brand with no scope or groupKey defaults to scope=all-markets, so classes come from the plan; pass scope=project to force the text classifier. No provider, discovery, or classifier work runs. Ranked lists are capped at 100 observed/other-source rows per group; pins remain complete. `observedNames` are names written in answer text, not cited sources, capped at the top 50 by answer count; `observedNamesTotal` is the full count. Model groups are capped at 50 and disclose truncation.",
1368
+ description: "Returns pinned competitors first, then observed direct competitors and other cited sources from stored answer/source evidence only. Mention share is percentage points (0..100) from answer text; citations are independent. The response carries basis (tracked or observed), availability, and reason. Pins exclusively define the comparison set when present. Otherwise at least 3 stored direct competitors must each be mentioned in 3 answers in the selected scope; raw names and platforms never enter that denominator. Without a comparison set the ratio is null, never 100%. The full comparison array is uncapped. Share of voice needs ONE query class: `queryClass=all`, and omitting it, pool branded and non-brand, so every `shareOfVoice` comes back null and only the counts are published. Pass `queryClass=non-brand` (or `branded`) for a ratio, exactly as visibility-stats does. A project with no brand name or alias cannot be split by class, so a class-scoped read on one is refused rather than answered empty. Optional groupBy: model adds provider/requested-model groups with separate served-model evidence and sample counts. Optional model filters one exact requested model ID and requires provider. Unknown historical models remain explicit. Groups are not a matched-query or equal-weight comparison. Advanced reads support one market group or explicit scope: all-markets. On a project with an active v2 measurement plan, queryClass=branded or non-brand with no scope or groupKey defaults to scope=all-markets, so classes come from the plan; pass scope=project to force the text classifier. No provider, discovery, or classifier work runs. Ranked lists are capped at 100 observed/other-source rows per group; pins remain complete. `observedNames` are names written in answer text, not cited sources, capped at the top 50 by answer count; `observedNamesTotal` counts distinct names, never answers (`countUnits` says what each count counts). Model groups are capped at 50 and disclose truncation. For which names answers give instead across a portfolio, read this with queryClass and runId latest: per-Property named-instead lists are samples of weak Properties. With queryClass branded or non-brand and neither runId nor window, this tool reads the latest sweep; pass window to pool sweeps. `runCount` and `runIds` name the runs counted; above 1 the counts pool several runs.",
1317
1369
  access: "read",
1318
1370
  tier: "monitoring",
1319
1371
  inputSchema: competitorLandscapeInputSchema,
@@ -1321,7 +1373,7 @@ var canonryMcpTools = [
1321
1373
  openApiOperations: ["GET /api/v1/projects/{name}/analytics/competitors"],
1322
1374
  handler: (client, input) => {
1323
1375
  const { project, ...query } = input;
1324
- return client.getCompetitorLandscape(project, query);
1376
+ return client.getCompetitorLandscape(project, latestSweepUnlessPooled(query));
1325
1377
  }
1326
1378
  }),
1327
1379
  defineTool({
@@ -1342,7 +1394,7 @@ var canonryMcpTools = [
1342
1394
  defineTool({
1343
1395
  name: "canonry_doctor",
1344
1396
  title: "Run health checks",
1345
- description: 'Run canonry health checks. With `project`, runs project-scoped checks (Google/GA auth, redirect URI, scopes, property access). Without `project`, runs global checks (provider keys, etc.). Use `checks` to filter by exact ID or wildcard prefix (e.g. ["google.auth.*"]). Returns a structured DoctorReport with per-check status, code, summary, remediation, and details \u2014 use this to diagnose Google auth failures (401/403/redirect-mismatch/principal-mismatch) without parsing logs.',
1397
+ description: 'Run canonry health checks. With `project`, runs project-scoped checks (Google/GA auth, redirect URI, scopes, property access). Without `project`, runs global checks (provider keys, etc.). Use `checks` to filter by exact ID or wildcard prefix (e.g. ["google.auth.*"]). Returns a structured DoctorReport with per-check status, code, summary, remediation, and details. Use it to diagnose Google auth failures (401/403/redirect-mismatch/principal-mismatch) without parsing logs. It checks configuration and integrations, not whether a sweep completed or its answers are reliable: for that read canonry_measurement_data_quality or canonry_run_completeness.',
1346
1398
  access: "read",
1347
1399
  tier: "core",
1348
1400
  inputSchema: doctorInputSchema,
@@ -1437,7 +1489,7 @@ var canonryMcpTools = [
1437
1489
  defineTool({
1438
1490
  name: "canonry_run_completeness",
1439
1491
  title: "Get run completeness",
1440
- description: "Expected, answered and missing measurement slots per provider for a plan run, whether canonry_run_fill would be admitted now and why not, and the latest fill.",
1492
+ description: "Is a plan run complete? Expected, executed and missing answers (measurement slots, one per query per engine) for the run, missing answers per engine (missingByProvider), whether canonry_run_fill would be admitted now and why not, and the latest fill. Pass run.displayedRunId from canonry_measurement_data_quality, or measurement.displayedRunId from canonry_measurement_overview. Quote expected, executed and missing. A planless run (planned false) has no expected answers, so its zero counts are not a completeness check; readable false means the counts are unknown, not zero.",
1441
1493
  access: "read",
1442
1494
  tier: "monitoring",
1443
1495
  inputSchema: runGetInputSchema,
@@ -1796,7 +1848,8 @@ var canonryMcpTools = [
1796
1848
  access: "read",
1797
1849
  tier: "setup",
1798
1850
  inputSchema: logQuerySchema,
1799
- outputSchema: operationalLogListDtoSchema,
1851
+ // Tolerant on purpose: a field a newer server adds is dropped, not a rejected page.
1852
+ outputSchema: operationalLogListReadSchema,
1800
1853
  annotations: readAnnotations(false),
1801
1854
  openApiOperations: ["GET /api/v1/operations/logs"],
1802
1855
  handler: async (client, input) => client.listOperationalLogs(input)
@@ -1885,6 +1938,17 @@ var canonryMcpTools = [
1885
1938
  openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/top-pages"],
1886
1939
  handler: (client, input) => client.gscTopPages(input.project, compactStringParams(input, ["startDate", "endDate", "limit", "window"]))
1887
1940
  }),
1941
+ defineTool({
1942
+ name: "canonry_gsc_query_totals",
1943
+ title: "Get GSC query totals",
1944
+ description: "Get stored Google Search Console totals per search query for a date window (no call to Google): clicks, impressions, CTR, impression-weighted average position and the number of days the query appeared. Rows are ordered clicks desc, impressions desc, query asc; page with limit/offset until truncated is false. The rows are the queries Google names: Google leaves rare queries out, so never sum them for a property total, use canonry_gsc_performance_daily. Each row has `source`: google (Google's per-query figures), page-summed (legacy page table, impressions over-count) or mixed (both across the window).",
1945
+ access: "read",
1946
+ tier: "gsc",
1947
+ inputSchema: gscQueryTotalsInputSchema,
1948
+ annotations: readAnnotations(),
1949
+ openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/query-totals"],
1950
+ handler: (client, input) => client.gscQueryTotals(input.project, compactStringParams(input, ["startDate", "endDate", "limit", "offset", "window"]))
1951
+ }),
1888
1952
  defineTool({
1889
1953
  name: "canonry_gsc_inspections",
1890
1954
  title: "List GSC inspections",
@@ -2217,6 +2281,17 @@ var canonryMcpTools = [
2217
2281
  openApiOperations: ["GET /api/v1/projects/{name}/gbp/places"],
2218
2282
  handler: (client, input) => client.listGbpPlaces(input.project, compactStringParams(input, ["locationName"]))
2219
2283
  }),
2284
+ defineTool({
2285
+ name: "canonry_gbp_reviews",
2286
+ title: "Get GBP reviews",
2287
+ description: "List stored Google Business Profile reviews newest first, with each location's review access and latest rating. Reviews come from the Business Profile v4 API when Google has enabled it for the Cloud project (every review), otherwise from the public Places listing (at most five, chosen by relevance). `negative` marks reviews at or below the project's threshold (`negativeMaxStars` in the response; set per project with `negativeReviewMaxStars`, 3 stars by default), the one the `review.negative` webhook uses; `alertState` says whether each one alerted. Empty until a gbp sync runs.",
2288
+ access: "read",
2289
+ tier: "gbp",
2290
+ inputSchema: gbpReviewsInputSchema,
2291
+ annotations: readAnnotations(),
2292
+ openApiOperations: ["GET /api/v1/projects/{name}/gbp/reviews"],
2293
+ handler: (client, input) => client.listGbpReviews(input.project, compactStringParams(input, ["locationName", "negative", "limit"]))
2294
+ }),
2220
2295
  defineTool({
2221
2296
  name: "canonry_gbp_summary",
2222
2297
  title: "Get GBP local-AEO summary",
@@ -2261,6 +2336,20 @@ var canonryMcpTools = [
2261
2336
  openApiOperations: ["GET /api/v1/projects/{name}/traffic/status"],
2262
2337
  handler: (client, input) => client.trafficStatus(input.project)
2263
2338
  }),
2339
+ defineTool({
2340
+ name: "canonry_traffic_referral_assessment",
2341
+ title: "Assess AI referral bursts",
2342
+ description: "Read stored AI-referral burst evidence and a separate adjusted estimate without changing raw totals or report headlines. Candidate bursts are not confirmed automation. Project/source scope only, for Simple or Advanced portfolios; Property, Target and market attribution are unavailable. GA quotient is descriptive with unknown matching coverage. No provider calls.",
2343
+ access: "read",
2344
+ tier: "traffic",
2345
+ inputSchema: referralAssessmentQuerySchema.extend({ project: projectNameSchema }),
2346
+ annotations: readAnnotations(),
2347
+ openApiOperations: ["GET /api/v1/projects/{name}/traffic/referral-assessment"],
2348
+ handler: (client, input) => {
2349
+ const { project, ...query } = input;
2350
+ return client.trafficReferralAssessment(project, query);
2351
+ }
2352
+ }),
2264
2353
  defineTool({
2265
2354
  name: "canonry_traffic_events",
2266
2355
  title: "List traffic events",
@@ -2361,7 +2450,7 @@ var canonryMcpTools = [
2361
2450
  defineTool({
2362
2451
  name: "canonry_project_upsert",
2363
2452
  title: "Create or replace project",
2364
- description: "Create or replace a Canonry project. PUT semantics \u2014 fields not in the request are reset to their defaults. Provide the full intended project shape.",
2453
+ description: "Create or replace a Canonry project. PUT semantics \u2014 fields not in the request are reset to their defaults. Provide the full intended project shape. Exception: an omitted providerDispatchModes (provider \u2192 sync|batch for scheduled sweeps) keeps the stored preference; send {} to clear it.",
2365
2454
  access: "write",
2366
2455
  tier: "setup",
2367
2456
  inputSchema: projectUpsertInputSchema,
@@ -2549,7 +2638,7 @@ var canonryMcpTools = [
2549
2638
  defineTool({
2550
2639
  name: "canonry_measurement_overview",
2551
2640
  title: "Get Advanced Measurement overview",
2552
- description: "For best/worst Property mention rankings, prefer canonry_measurement_portfolio_summary and its mentionRanking. An unavailable aggregate does not invalidate available Property metrics. Return stored, revision-pinned Advanced Measurement metrics and a bounded page of Property rows for all Properties, one reporting group, or one Property. Filter by query class, provider, location, date window, run, or Property search; search filters rows without changing metric denominators. It ranks one run snapshot only and never infers a trend or compares across revisions. Choose label-asc (default), label-desc, citationCoverage-asc/desc, or mentionCoverage-asc/desc. For a coverage sort, unavailable rows form the first bucket in either direction; available rows then follow the requested numeric direction. The cursor is sort-aware, pins pagination to the active revision, displayed run, evidence snapshot, and filters even if a newer run completes, and must be reused unchanged with the same sort and filters. Legacy label cursors work only when sort is omitted, while any explicit sort needs a new sort-bound cursor. It never starts provider work or incurs provider cost; page size is at most 100, and it refuses invalid scope keys, cursor combinations, appended evidence, or a run pinned to another revision.",
2641
+ description: "For best/worst Property mention rankings, prefer canonry_measurement_portfolio_summary and its mentionRanking. An unavailable aggregate does not invalidate available Property metrics. Return stored, revision-pinned Advanced Measurement metrics and a bounded page of Property rows for all Properties, one reporting group, or one Property. Filter by query class, provider, location, date window, run, or Property search; search filters rows without changing metric denominators. On a schema-v2 plan each Property row carries its metro (and otherMetros when it is in several). It ranks one run snapshot only and never infers a trend or compares across revisions. Choose label-asc (default), label-desc, citationCoverage-asc/desc, or mentionCoverage-asc/desc. For a coverage sort, unavailable rows form the first bucket in either direction; available rows then follow the requested numeric direction. The cursor is sort-aware, pins pagination to the active revision, displayed run, evidence snapshot, and filters even if a newer run completes, and must be reused unchanged with the same sort and filters. Legacy label cursors work only when sort is omitted, while any explicit sort needs a new sort-bound cursor. It never starts provider work or incurs provider cost; page size is at most 100, and it refuses invalid scope keys, cursor combinations, appended evidence, or a run pinned to another revision.",
2553
2642
  access: "read",
2554
2643
  tier: "setup",
2555
2644
  inputSchema: measurementOverviewInputSchema,
@@ -2563,7 +2652,7 @@ var canonryMcpTools = [
2563
2652
  defineTool({
2564
2653
  name: "canonry_measurement_property_evidence",
2565
2654
  title: "Page one Property's measurement evidence",
2566
- description: "Return the stored evidence rows for exactly one Property out of one revision-pinned run, optionally narrowed to a question class, provider, or location. shape chooses what a row is: sources (the default) returns one row per cited URL under evidence; answers returns one row per measured answer under answers, with the cited URLs nested inside and both signals on the row (mentioned, cited). Use shape=answers to explain a GAP \u2014 an answer that mentioned the Property without linking it, or that named nobody at all, has no URL to hang a source row on and does not appear in the default shape, so counting source rows understates what was measured. mentioned is null, never false, when the answer text was never captured: that is a missing signal, not a measured no. Exactly one of evidence or answers is returned and the other is absent rather than empty. A cursor is bound to the shape that issued it and is refused on the other. Prefer this over canonry_measurement_report when you want one Property: the report reconstructs every group and every Target for a revision and does not paginate. Run selection matches canonry_measurement_overview \u2014 the most recent completed run pinned to the active revision unless runId names another, and a run pinned to a different revision is refused rather than joined. The cursor also pins the active revision, displayed run, evidence snapshot, and filters; reuse it unchanged. Not available for a schema v1 revision, which records no question class to scope by. It never starts provider work; page size is at most 100. An empty page under measurement.state = not_measured means the Property has not been measured at all, which is NOT a measured result of zero.",
2655
+ description: "Return the stored evidence rows for exactly one Property out of one revision-pinned run, optionally narrowed to a provider or location. Defaults to non-brand queries; pass queryClass branded or all to change it, and state the returned class. shape chooses what a row is: sources (the default) returns one row per cited URL under evidence; answers returns one row per measured answer under answers, with the cited URLs nested inside and both signals on the row (mentioned, cited). Use shape=answers to explain a GAP: an answer that mentioned the Property without linking it, or that named nobody at all, has no URL to hang a source row on and does not appear in the default shape, so counting source rows understates what was measured. mentioned is null, never false, when the answer text was never captured: that is a missing signal, not a measured no. Exactly one of evidence or answers is returned and the other is absent rather than empty. A cursor is bound to the shape that issued it and is refused on the other. Prefer this over canonry_measurement_report when you want one Property: the report reconstructs every group and every Target for a revision and does not paginate. Run selection matches canonry_measurement_overview: the most recent completed run pinned to the active revision unless runId names another, and a run pinned to a different revision is refused rather than joined. The cursor also pins the active revision, displayed run, evidence snapshot, and filters; reuse it unchanged. Not available for a schema v1 revision, which records no question class to scope by. It never starts provider work; page size is at most 100. An empty page under measurement.state = not_measured means the Property has not been measured at all, which is NOT a measured result of zero.",
2567
2656
  access: "read",
2568
2657
  tier: "setup",
2569
2658
  inputSchema: measurementPropertyEvidenceInputSchema,
@@ -2571,21 +2660,25 @@ var canonryMcpTools = [
2571
2660
  openApiOperations: ["GET /api/v1/projects/{name}/measurement-property-evidence"],
2572
2661
  handler: (client, input) => {
2573
2662
  const { project, ...query } = input;
2574
- return client.getMeasurementPropertyEvidence(project, query);
2663
+ return client.getMeasurementPropertyEvidence(project, { ...query, queryClass: agentQueryClass(query.queryClass) });
2575
2664
  }
2576
2665
  }),
2577
2666
  defineTool({
2578
2667
  name: "canonry_measurement_portfolio_summary",
2579
2668
  title: "Summarize measured Properties",
2580
- description: "Start here for best and worst Properties. One call returns the weakest Properties, the names answers wrote instead of them, and the domains engines cited for them. Defaults to non-brand queries; state the returned queryClass and never pool branded with non-brand. Keep the default limit; for more rows narrow with groupKey (a metro) rather than raising limit, which can overflow the tool-result cap. Mention (answer text names the Property) and citation (a source URL on its page) are separate: report each as numerator/denominator, with any unattributed count beside mention. Denominators count answers, one per query per engine (queries x engines = answers). Group Properties only by each row's metro and submarkets, never by label. namedInsteadInAnswerText lists names written in the answer text of answers that neither named nor cited the Property, counted by answer: they are not citations. citedDomains and weakestAnswerSources count answers citing each domain, every engine included; weakestAnswerSources covers the weakest rows plus every tied Property, each answer once. When tiedAtWeakest is set, that many Properties share the weakest rates and are ordered by name, not ranked. mentionRanking.strongest/.weakest rank every Property with an available mention rate; .excluded lists the rest with reasons. markets lists one level worst-first (metros, or a groupKey's submarkets), capped at limit, with totalMarkets; includeNestedMarkets returns every level. Markets may share Properties and never sum to portfolio totals. Reads stored data only; never starts provider work.",
2669
+ description: "Start here for best and worst Properties. One call returns the weakest Properties, the names answers wrote instead of them, and the domains engines cited for them. Defaults to non-brand queries; state the returned queryClass and never pool branded with non-brand. Returns at most 4 Property rows per list, the most that fit one result; a larger limit is lowered and limitNote says so. For other rows pass groupKey (one metro's weakest), or page canonry_measurement_overview. Mention (answer text names the Property) and citation (a source URL on its page) are separate: report each as numerator/denominator, with any unattributed count beside mention. Denominators count answers, one per query per engine (queries x engines = answers). Group Properties only by each row's metro and submarkets, never by label. namedInsteadInAnswerText lists names written in the answer text of answers that neither named nor cited the Property, counted by answer: they are not citations, and namedInsteadInAnswerTextTotal counts distinct names, not answers. citedDomains and weakestAnswerSources count answers citing each domain, every engine included; weakestAnswerSources pools the weakest rows plus every tied Property, each answer once, and its domainTotal counts distinct domains, not citations. When tiedAtWeakest is set, tiedAtWeakest.count Properties share the weakest rates and the rows are the first of them by name, not a rank: report the count, tiedAtWeakest.byMetro (tied Properties per metro over the whole tie; a Property in two metros counts in both) and tiedAtWeakest.namedInstead (names written instead across the whole tie, counted by distinct answer; namedInsteadTotal is distinct names), and call the rows examples. mentionRanking.strongest/.weakest rank every Property with an available mention rate; .excluded lists the rest with reasons. markets lists every market at one level, worst-first (every metro, or a groupKey's submarkets), whatever the limit. Markets may share Properties and never sum to portfolio totals. Reads stored data only; never starts provider work.",
2581
2670
  access: "read",
2582
2671
  tier: "monitoring",
2583
2672
  inputSchema: measurementPortfolioSummaryInputSchema,
2584
2673
  annotations: readAnnotations(),
2585
2674
  openApiOperations: ["GET /api/v1/projects/{name}/measurement-portfolio-summary"],
2586
- handler: (client, input) => {
2675
+ handler: async (client, input) => {
2587
2676
  const { project, ...query } = input;
2588
- return client.getMeasurementPortfolioSummary(project, query);
2677
+ const most = PORTFOLIO_SUMMARY_AGENT_ROWS;
2678
+ const lowered = query.limit !== void 0 && query.limit > most;
2679
+ const summary = await client.getMeasurementPortfolioSummary(project, lowered ? { ...query, limit: most } : query);
2680
+ const more = query.groupKey === void 0 ? "For other rows pass groupKey (one metro's weakest), or page canonry_measurement_overview." : "For more rows page canonry_measurement_overview with scope group.";
2681
+ return portfolioSummaryForAgents(summary, lowered ? `limit ${query.limit} was lowered to ${most}, the most rows that fit one result. ${more}` : void 0);
2589
2682
  }
2590
2683
  }),
2591
2684
  defineTool({
@@ -2619,7 +2712,7 @@ var canonryMcpTools = [
2619
2712
  defineTool({
2620
2713
  name: "canonry_measurement_property_competitors",
2621
2714
  title: "List a Property\u2019s stored replacements",
2622
- description: "After reviewing a Property\u2019s question outcomes, find repeated stored replacement names for its misses. Reads stored data only; it never starts provider work.",
2715
+ description: "For one Property: the names answers wrote in their text instead of it, and the domains its own answers cited. Defaults to non-brand queries; pass queryClass branded or all to change it, and state the returned queryClass. competitors[] are names written in the answer text of this Property's answers that did not name it, never citations: occurrences counts answers, and questions lists at most 5 of questionTotal, so never read those 5 as all. citedDomains (top 10), citedDomainsTotal (distinct domains) and citedDomainsAnswers (the answers counted) are the domains cited in this Property's own answers, counted by answer: sources, never names written instead. Use them, not the portfolio-wide weakestAnswerSources, for one Property's sources. Reads stored data only; it never starts provider work.",
2623
2716
  access: "read",
2624
2717
  tier: "monitoring",
2625
2718
  inputSchema: measurementPropertyCompetitorsInputSchema,
@@ -2627,13 +2720,13 @@ var canonryMcpTools = [
2627
2720
  openApiOperations: ["GET /api/v1/projects/{name}/measurement-property-competitors"],
2628
2721
  handler: (client, input) => {
2629
2722
  const { project, ...query } = input;
2630
- return client.getMeasurementPropertyCompetitors(project, query);
2723
+ return client.getMeasurementPropertyCompetitors(project, { ...query, queryClass: agentQueryClass(query.queryClass) });
2631
2724
  }
2632
2725
  }),
2633
2726
  defineTool({
2634
2727
  name: "canonry_measurement_changes",
2635
2728
  title: "Compare stored measurement changes",
2636
- description: "After reviewing the current summary, compare stored runs only when their execution identity is compatible. Reads stored data only; it never starts provider work.",
2729
+ description: "What changed since the previous sweep: compares the displayed run with the previous run of the same plan revision and execution identity; an incompatible pair returns unavailable with a reason, never a trend. Defaults to non-brand queries: call once per class (queryClass branded for the branded move) and never report one class as the whole. queryClass all pools both classes in rows and distribution and adds metricsByClass, the headline metrics per class; report those per class, never the pooled metrics alone. changedProperties come largest move first (answers that named the Property, then answers that cited it; sort label for alphabetical), default 10 and at most 20 rows; totalProperties above the rows returned means rows you did not see. distribution counts every Property in scope: improved, declined, mixed, withinNoise, unchanged, notComparable. mentionAnswersDelta and citationAnswersDelta are signed answer counts; denominatorChanged true means a rate was taken over a different number of answers, so those counts are not like for like and the coverage rates give the direction. A move is the rate change times the larger answer count (the count change when the denominator held). withinNoise true means every move was 2 answers or fewer: within noise, never a gain, loss, trend or regression. Reads stored data only; it never starts provider work.",
2637
2730
  access: "read",
2638
2731
  tier: "monitoring",
2639
2732
  inputSchema: measurementChangesInputSchema,
@@ -2641,13 +2734,13 @@ var canonryMcpTools = [
2641
2734
  openApiOperations: ["GET /api/v1/projects/{name}/measurement-changes"],
2642
2735
  handler: (client, input) => {
2643
2736
  const { project, ...query } = input;
2644
- return client.getMeasurementChanges(project, query);
2737
+ return client.getMeasurementChanges(project, { ...query, queryClass: agentQueryClass(query.queryClass) });
2645
2738
  }
2646
2739
  }),
2647
2740
  defineTool({
2648
2741
  name: "canonry_measurement_data_quality",
2649
2742
  title: "Inspect stored measurement data quality",
2650
- description: "Before acting on a measurement, inspect its stored completeness, capture, retrieval, and comparability context. Reads stored data only; it never starts provider work.",
2743
+ description: "Is the sweep complete, and is anything unreliable? For the displayed run (run.displayedRunId, or runId): completeness (expected, executed, answered and missing answers), capture, retrieval, population (expected, answered and missing questions), comparison (comparability with the previous run), unattributedByClass (per class, the answers every mention rate leaves out because they name the Property ambiguously; report per class, never pooled) and latestFill (the newest in-place fill: status, expected, filled; its answers already count in completeness). Quote expected, executed and missing. A Healthy run status is not a completeness check. For missing answers per engine and whether a fill would be admitted, pass run.displayedRunId to canonry_run_completeness. Reads stored data only; it never starts provider work.",
2651
2744
  access: "read",
2652
2745
  tier: "monitoring",
2653
2746
  inputSchema: measurementDataQualityInputSchema,
@@ -2842,7 +2935,7 @@ var canonryMcpTools = [
2842
2935
  defineTool({
2843
2936
  name: "canonry_run_trigger",
2844
2937
  title: "Trigger run",
2845
- description: "Trigger an answer-visibility run for a Canonry project. Omit both scoping fields for a full sweep. Pass request.queries[] to measure only some of the project's tracked questions. On a project with a published measurement plan, pass request.measurementScope with at least one group or target to measure one slice of that plan \u2014 a group stands for its member Targets, and only the questions those Targets selected are measured. A scope naming nothing is rejected rather than treated as a full sweep, so omit the field entirely when you want everything. Either kind of slice is recorded as a spot check: it never replaces a full sweep in the dashboard, analytics or report. For verification scenarios (testing whether a provider migration worked, reproducing a regression, sanity-checking after a code change), set request.trigger='probe' \u2014 probe runs write a snapshot you can inspect via canonry_runs_get but are EXCLUDED from dashboard, analytics, intelligence, report, and notifications. Use 'probe' whenever you are testing on your own initiative rather than producing data the user/dashboard will consume.",
2938
+ description: "Trigger an answer-visibility run for a Canonry project. Omit both scoping fields for a full sweep. Pass request.queries[] to measure only some of the project's tracked questions. On a project with a published measurement plan, pass request.measurementScope with at least one group or target to measure one slice of that plan \u2014 a group stands for its member Targets, and only the questions those Targets selected are measured. A scope naming nothing is rejected rather than treated as a full sweep, so omit the field entirely when you want everything. Either kind of slice is recorded as a spot check: it never replaces a full sweep in the dashboard, analytics or report. For verification scenarios (testing whether a provider migration worked, reproducing a regression, sanity-checking after a code change), set request.trigger='probe' \u2014 probe runs write a snapshot you can inspect via canonry_runs_get but are EXCLUDED from dashboard, analytics, intelligence, report, and notifications. Use 'probe' whenever you are testing on your own initiative rather than producing data the user/dashboard will consume. request.dispatchMode='batch' sends every provider that can (a full sweep of a published plan, a batch-capable provider enabled in config.yaml, every answer's model frozen) to its asynchronous batch API at the batch price; the rest run sync and the run stays running until the batch ends (up to its deadline, 24h by default). A batch request no provider can honour is refused with each provider's reason. Omit it for the ordinary sync sweep.",
2846
2939
  access: "write",
2847
2940
  tier: "core",
2848
2941
  inputSchema: runTriggerInputSchema,
@@ -5,12 +5,12 @@ import {
5
5
  siteCrawlGraphNodes,
6
6
  siteCrawlPages,
7
7
  siteCrawlSnapshots
8
- } from "./chunk-P5VXVFQW.js";
8
+ } from "./chunk-7RKCDOWO.js";
9
9
  import {
10
10
  SITE_CRAWL_GRAPH_MAX_EDGES,
11
11
  SITE_CRAWL_GRAPH_MAX_NODES,
12
12
  TEMPLATE_LINK_RATIO_THRESHOLD
13
- } from "./chunk-S5OSY3PI.js";
13
+ } from "./chunk-KVQHG3WB.js";
14
14
 
15
15
  // src/site-crawl-graph-layout.ts
16
16
  import crypto from "crypto";