@canonry/canonry 5.21.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 +101 -6
  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-CjxWA3lx.js → AuditHistoryPanel-CWd9UN6L.js} +1 -1
  10. package/assets/assets/{BacklinksPage-Bdd164Up.js → BacklinksPage-B2gPTzyE.js} +1 -1
  11. package/assets/assets/ChartPrimitives-B5okB5g-.js +1 -0
  12. package/assets/assets/{HistoryPage-BvJmE-kP.js → HistoryPage-xtkG8SZN.js} +1 -1
  13. package/assets/assets/{MeasurementPropertyPage-HuP6RMZ-.js → MeasurementPropertyPage-DxIMU3_T.js} +1 -1
  14. package/assets/assets/{ProjectPage-DOvXVbyy.js → ProjectPage-CMAFZq_s.js} +5 -5
  15. package/assets/assets/{RunRow-Cnb3JOBw.js → RunRow-DnSo7Nle.js} +1 -1
  16. package/assets/assets/{RunsPage-Dvz1JYIm.js → RunsPage-FAEuo4Ra.js} +1 -1
  17. package/assets/assets/{SettingsPage-DYGvphkg.js → SettingsPage-C4OZ1HB9.js} +1 -1
  18. package/assets/assets/{SiteHealthSection-CUNiKaod.js → SiteHealthSection-BzMxlz8-.js} +3 -3
  19. package/assets/assets/{TrafficPage-Bf4Tq95q.js → TrafficPage-D03S6xF6.js} +1 -1
  20. package/assets/assets/{TrafficSourceDetailPage-CIuLWZgT.js → TrafficSourceDetailPage-DBu6oFFK.js} +1 -1
  21. package/assets/assets/{extract-error-message-DZex1e4H.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-CxvtwEkv.js → react-sigma_core.esm.min-CxwZsgxR.js} +1 -1
  25. package/assets/assets/{v2-overview-adapter-Bh4qIzLp.js → v2-overview-adapter-DTCKxzZT.js} +1 -1
  26. package/assets/index.html +2 -2
  27. package/dist/{chunk-FAVYLZQV.js → chunk-7RKCDOWO.js} +7005 -5086
  28. package/dist/{chunk-MEAKE5W7.js → chunk-DX2JUC5H.js} +3 -3
  29. package/dist/{chunk-NFN5GSEJ.js → chunk-EP4STEPI.js} +3558 -1170
  30. package/dist/{chunk-FRWZWJB3.js → chunk-FNGITGDF.js} +176 -8
  31. package/dist/{chunk-2LO7FLVD.js → chunk-KVQHG3WB.js} +6721 -5889
  32. package/dist/{chunk-WUT3G2GM.js → chunk-MPICT6V7.js} +131 -36
  33. package/dist/{chunk-HDJHQPW3.js → chunk-ONTEFAA7.js} +2 -2
  34. package/dist/cli.js +443 -48
  35. package/dist/{demo-server-7BVVVXPM.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-3UUKRAZ5.js → intelligence-service-6LDMVI66.js} +2 -2
  39. package/dist/mcp.js +4 -4
  40. package/package.json +12 -11
  41. package/assets/assets/ChartPrimitives-BQC2VH0O.js +0 -1
  42. package/assets/assets/index-CeXW-lzj.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,8 @@ import {
87
89
  queryGenerateRequestSchema,
88
90
  queryTrackingCommitRequestSchema,
89
91
  queryTrackingPreviewRequestSchema,
92
+ referralAssessmentQuerySchema,
93
+ reportMonthSchema,
90
94
  reportPeriodSchema,
91
95
  researchBatchCreateSchema,
92
96
  researchRunCreateSchema,
@@ -106,7 +110,7 @@ import {
106
110
  trafficSeriesGranularitySchema,
107
111
  visibilityCompareSelectionSchema,
108
112
  visibilityReportRequestSchema
109
- } from "./chunk-2LO7FLVD.js";
113
+ } from "./chunk-KVQHG3WB.js";
110
114
 
111
115
  // src/cli-error.ts
112
116
  function isMachineFormat(format) {
@@ -391,20 +395,34 @@ var measurementOverviewInputSchema = z3.object({
391
395
  context.addIssue({ code: "custom", path: ["targetKey"], message: `${input.scope} scope does not accept targetKey.` });
392
396
  }
393
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
+ }
394
403
  var measurementPropertyEvidenceInputSchema = measurementPropertyEvidenceQuerySchema.extend({
395
404
  project: projectNameSchema,
396
405
  targetKey: measurementPropertyEvidenceQuerySchema.shape.targetKey.describe("Property stable key. Required \u2014 this read is scoped to exactly one Property."),
406
+ queryClass: propertyQueryClassInputSchema,
397
407
  shape: measurementPropertyEvidenceQuerySchema.shape.shape.describe(
398
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."
399
409
  )
400
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
+ }
401
421
  var measurementPortfolioSummaryInputSchema = measurementPortfolioSummaryQuerySchema.extend({
402
422
  project: projectNameSchema,
403
- groupKey: measurementPortfolioSummaryQuerySchema.shape.groupKey.describe("A market stable key (a row's metro.groupKey). Scopes rows to that market and lists its direct submarkets."),
404
- // Capped below the route's 50: past about 10 rows the per-row evidence
405
- // (names given instead, cited domains) no longer fits the tool-result cap.
406
- 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."),
407
- 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.")
408
426
  }).strict();
409
427
  var measurementPropertyQuestionsInputSchema = measurementPropertyQuestionsQuerySchema.extend({
410
428
  project: projectNameSchema
@@ -413,10 +431,14 @@ var measurementQuestionResultInputSchema = measurementQuestionResultQuerySchema.
413
431
  project: projectNameSchema
414
432
  }).strict();
415
433
  var measurementPropertyCompetitorsInputSchema = measurementPropertyCompetitorsQuerySchema.extend({
416
- project: projectNameSchema
434
+ project: projectNameSchema,
435
+ queryClass: propertyQueryClassInputSchema
417
436
  }).strict();
418
- var measurementChangesInputSchema = measurementChangesQuerySchema.extend({
419
- 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.`)
420
442
  }).strict();
421
443
  var measurementDataQualityInputSchema = measurementDataQualityQuerySchema.extend({
422
444
  project: projectNameSchema
@@ -553,6 +575,14 @@ var gscTopPagesInputSchema = z3.object({
553
575
  limit: z3.number().int().positive().max(500).optional(),
554
576
  window: analyticsWindowSchema.optional()
555
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
+ });
556
586
  var gscInspectionsInputSchema = z3.object({
557
587
  project: projectNameSchema,
558
588
  url: z3.string().optional(),
@@ -703,6 +733,12 @@ var gbpLocationScopedInputSchema = z3.object({
703
733
  project: projectNameSchema,
704
734
  locationName: z3.string().optional()
705
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
+ });
706
742
  var gbpAccountsInputSchema = z3.object({
707
743
  project: projectNameSchema
708
744
  });
@@ -848,9 +884,16 @@ var competitorsInputSchema = z3.object({
848
884
  project: projectNameSchema,
849
885
  request: competitorBatchRequestSchema
850
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.";
851
888
  var competitorLandscapeInputSchema = competitorLandscapeQuerySchema.safeExtend({
852
- project: projectNameSchema
889
+ project: projectNameSchema,
890
+ runId: z3.string().trim().min(1).optional().describe(latestSweepRunIdDescription)
853
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
+ }
854
897
  var projectUpsertInputSchema = z3.object({
855
898
  project: projectNameSchema,
856
899
  request: projectUpsertRequestSchema
@@ -873,6 +916,7 @@ var agentWebhookAttachInputSchema = z3.object({
873
916
  url: z3.string().url()
874
917
  });
875
918
  var doctorInputSchema = z3.object({
919
+ reportMonth: reportMonthSchema.optional().describe("Report month (YYYY-MM). Defaults to this month, also retaining the prior month through UTC day 3."),
876
920
  project: projectNameSchema.optional().describe("Project name to scope project-level checks. Omit to run global checks (provider keys, config, etc.)."),
877
921
  checks: z3.array(z3.string().min(1)).optional().describe('Optional check IDs or wildcard prefixes (e.g. "google.auth.*", "config.providers"). Empty/omitted runs all matching checks for the chosen scope.')
878
922
  });
@@ -1145,14 +1189,24 @@ var canonryMcpTools = [
1145
1189
  defineTool({
1146
1190
  name: "canonry_visibility_report",
1147
1191
  title: "Read scoped AI visibility",
1148
- 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.",
1149
1193
  access: "read",
1150
1194
  tier: "monitoring",
1151
1195
  inputSchema: visibilityReportRequestSchema.safeExtend({ project: projectNameSchema }),
1152
1196
  annotations: readAnnotations(),
1153
1197
  openApiOperations: ["GET /api/v1/projects/{name}/visibility-report"],
1154
- handler: (client, input) => {
1198
+ handler: async (client, input) => {
1155
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
+ }
1156
1210
  return client.getVisibilityReport(project, selection);
1157
1211
  }
1158
1212
  }),
@@ -1225,7 +1279,7 @@ var canonryMcpTools = [
1225
1279
  defineTool({
1226
1280
  name: "canonry_project_overview",
1227
1281
  title: "Get project overview (composite)",
1228
- 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.`,
1229
1283
  access: "read",
1230
1284
  tier: "core",
1231
1285
  inputSchema: z3.object({
@@ -1285,14 +1339,14 @@ var canonryMcpTools = [
1285
1339
  defineTool({
1286
1340
  name: "canonry_analytics_sources",
1287
1341
  title: "Get cited-source rankings",
1288
- 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.",
1289
1343
  access: "read",
1290
1344
  tier: "monitoring",
1291
1345
  inputSchema: z3.object({
1292
1346
  project: projectNameSchema,
1293
1347
  window: analyticsWindowSchema.optional().describe("Time range: 7d, 30d, 90d, or all (default all)."),
1294
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."),
1295
- 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),
1296
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."),
1297
1351
  includeByQuery: z3.boolean().optional().describe("Include the per-query breakdown. It is large; default false for this tool.")
1298
1352
  }),
@@ -1303,7 +1357,7 @@ var canonryMcpTools = [
1303
1357
  // The full ranked lists run past the agent's tool-result cap on a large
1304
1358
  // project, and the per-engine lists are the first thing lost.
1305
1359
  limit: input.limit ?? 10,
1306
- runId: input.runId,
1360
+ runId: latestSweepUnlessPooled(input).runId,
1307
1361
  queryClass: input.queryClass,
1308
1362
  includeByQuery: input.includeByQuery ?? false
1309
1363
  })
@@ -1311,7 +1365,7 @@ var canonryMcpTools = [
1311
1365
  defineTool({
1312
1366
  name: "canonry_competitor_landscape",
1313
1367
  title: "Get historical competitor landscape",
1314
- 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.",
1315
1369
  access: "read",
1316
1370
  tier: "monitoring",
1317
1371
  inputSchema: competitorLandscapeInputSchema,
@@ -1319,7 +1373,7 @@ var canonryMcpTools = [
1319
1373
  openApiOperations: ["GET /api/v1/projects/{name}/analytics/competitors"],
1320
1374
  handler: (client, input) => {
1321
1375
  const { project, ...query } = input;
1322
- return client.getCompetitorLandscape(project, query);
1376
+ return client.getCompetitorLandscape(project, latestSweepUnlessPooled(query));
1323
1377
  }
1324
1378
  }),
1325
1379
  defineTool({
@@ -1340,13 +1394,13 @@ var canonryMcpTools = [
1340
1394
  defineTool({
1341
1395
  name: "canonry_doctor",
1342
1396
  title: "Run health checks",
1343
- 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.',
1344
1398
  access: "read",
1345
1399
  tier: "core",
1346
1400
  inputSchema: doctorInputSchema,
1347
1401
  annotations: readAnnotations(true),
1348
1402
  openApiOperations: ["GET /api/v1/doctor", "GET /api/v1/projects/{name}/doctor"],
1349
- handler: (client, input) => client.runDoctor({ project: input.project, checkIds: input.checks })
1403
+ handler: (client, input) => client.runDoctor({ project: input.project, checkIds: input.checks, ...input.reportMonth ? { reportMonth: input.reportMonth } : {} })
1350
1404
  }),
1351
1405
  defineTool({
1352
1406
  name: "canonry_project_export",
@@ -1435,7 +1489,7 @@ var canonryMcpTools = [
1435
1489
  defineTool({
1436
1490
  name: "canonry_run_completeness",
1437
1491
  title: "Get run completeness",
1438
- 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.",
1439
1493
  access: "read",
1440
1494
  tier: "monitoring",
1441
1495
  inputSchema: runGetInputSchema,
@@ -1794,7 +1848,8 @@ var canonryMcpTools = [
1794
1848
  access: "read",
1795
1849
  tier: "setup",
1796
1850
  inputSchema: logQuerySchema,
1797
- outputSchema: operationalLogListDtoSchema,
1851
+ // Tolerant on purpose: a field a newer server adds is dropped, not a rejected page.
1852
+ outputSchema: operationalLogListReadSchema,
1798
1853
  annotations: readAnnotations(false),
1799
1854
  openApiOperations: ["GET /api/v1/operations/logs"],
1800
1855
  handler: async (client, input) => client.listOperationalLogs(input)
@@ -1883,6 +1938,17 @@ var canonryMcpTools = [
1883
1938
  openApiOperations: ["GET /api/v1/projects/{name}/google/gsc/top-pages"],
1884
1939
  handler: (client, input) => client.gscTopPages(input.project, compactStringParams(input, ["startDate", "endDate", "limit", "window"]))
1885
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
+ }),
1886
1952
  defineTool({
1887
1953
  name: "canonry_gsc_inspections",
1888
1954
  title: "List GSC inspections",
@@ -2215,6 +2281,17 @@ var canonryMcpTools = [
2215
2281
  openApiOperations: ["GET /api/v1/projects/{name}/gbp/places"],
2216
2282
  handler: (client, input) => client.listGbpPlaces(input.project, compactStringParams(input, ["locationName"]))
2217
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
+ }),
2218
2295
  defineTool({
2219
2296
  name: "canonry_gbp_summary",
2220
2297
  title: "Get GBP local-AEO summary",
@@ -2259,6 +2336,20 @@ var canonryMcpTools = [
2259
2336
  openApiOperations: ["GET /api/v1/projects/{name}/traffic/status"],
2260
2337
  handler: (client, input) => client.trafficStatus(input.project)
2261
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
+ }),
2262
2353
  defineTool({
2263
2354
  name: "canonry_traffic_events",
2264
2355
  title: "List traffic events",
@@ -2359,7 +2450,7 @@ var canonryMcpTools = [
2359
2450
  defineTool({
2360
2451
  name: "canonry_project_upsert",
2361
2452
  title: "Create or replace project",
2362
- 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.",
2363
2454
  access: "write",
2364
2455
  tier: "setup",
2365
2456
  inputSchema: projectUpsertInputSchema,
@@ -2547,7 +2638,7 @@ var canonryMcpTools = [
2547
2638
  defineTool({
2548
2639
  name: "canonry_measurement_overview",
2549
2640
  title: "Get Advanced Measurement overview",
2550
- 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.",
2551
2642
  access: "read",
2552
2643
  tier: "setup",
2553
2644
  inputSchema: measurementOverviewInputSchema,
@@ -2561,7 +2652,7 @@ var canonryMcpTools = [
2561
2652
  defineTool({
2562
2653
  name: "canonry_measurement_property_evidence",
2563
2654
  title: "Page one Property's measurement evidence",
2564
- 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.",
2565
2656
  access: "read",
2566
2657
  tier: "setup",
2567
2658
  inputSchema: measurementPropertyEvidenceInputSchema,
@@ -2569,21 +2660,25 @@ var canonryMcpTools = [
2569
2660
  openApiOperations: ["GET /api/v1/projects/{name}/measurement-property-evidence"],
2570
2661
  handler: (client, input) => {
2571
2662
  const { project, ...query } = input;
2572
- return client.getMeasurementPropertyEvidence(project, query);
2663
+ return client.getMeasurementPropertyEvidence(project, { ...query, queryClass: agentQueryClass(query.queryClass) });
2573
2664
  }
2574
2665
  }),
2575
2666
  defineTool({
2576
2667
  name: "canonry_measurement_portfolio_summary",
2577
2668
  title: "Summarize measured Properties",
2578
- 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.",
2579
2670
  access: "read",
2580
2671
  tier: "monitoring",
2581
2672
  inputSchema: measurementPortfolioSummaryInputSchema,
2582
2673
  annotations: readAnnotations(),
2583
2674
  openApiOperations: ["GET /api/v1/projects/{name}/measurement-portfolio-summary"],
2584
- handler: (client, input) => {
2675
+ handler: async (client, input) => {
2585
2676
  const { project, ...query } = input;
2586
- 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);
2587
2682
  }
2588
2683
  }),
2589
2684
  defineTool({
@@ -2617,7 +2712,7 @@ var canonryMcpTools = [
2617
2712
  defineTool({
2618
2713
  name: "canonry_measurement_property_competitors",
2619
2714
  title: "List a Property\u2019s stored replacements",
2620
- 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.",
2621
2716
  access: "read",
2622
2717
  tier: "monitoring",
2623
2718
  inputSchema: measurementPropertyCompetitorsInputSchema,
@@ -2625,13 +2720,13 @@ var canonryMcpTools = [
2625
2720
  openApiOperations: ["GET /api/v1/projects/{name}/measurement-property-competitors"],
2626
2721
  handler: (client, input) => {
2627
2722
  const { project, ...query } = input;
2628
- return client.getMeasurementPropertyCompetitors(project, query);
2723
+ return client.getMeasurementPropertyCompetitors(project, { ...query, queryClass: agentQueryClass(query.queryClass) });
2629
2724
  }
2630
2725
  }),
2631
2726
  defineTool({
2632
2727
  name: "canonry_measurement_changes",
2633
2728
  title: "Compare stored measurement changes",
2634
- 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.",
2635
2730
  access: "read",
2636
2731
  tier: "monitoring",
2637
2732
  inputSchema: measurementChangesInputSchema,
@@ -2639,13 +2734,13 @@ var canonryMcpTools = [
2639
2734
  openApiOperations: ["GET /api/v1/projects/{name}/measurement-changes"],
2640
2735
  handler: (client, input) => {
2641
2736
  const { project, ...query } = input;
2642
- return client.getMeasurementChanges(project, query);
2737
+ return client.getMeasurementChanges(project, { ...query, queryClass: agentQueryClass(query.queryClass) });
2643
2738
  }
2644
2739
  }),
2645
2740
  defineTool({
2646
2741
  name: "canonry_measurement_data_quality",
2647
2742
  title: "Inspect stored measurement data quality",
2648
- 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.",
2649
2744
  access: "read",
2650
2745
  tier: "monitoring",
2651
2746
  inputSchema: measurementDataQualityInputSchema,
@@ -2840,7 +2935,7 @@ var canonryMcpTools = [
2840
2935
  defineTool({
2841
2936
  name: "canonry_run_trigger",
2842
2937
  title: "Trigger run",
2843
- 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.",
2844
2939
  access: "write",
2845
2940
  tier: "core",
2846
2941
  inputSchema: runTriggerInputSchema,
@@ -5,12 +5,12 @@ import {
5
5
  siteCrawlGraphNodes,
6
6
  siteCrawlPages,
7
7
  siteCrawlSnapshots
8
- } from "./chunk-FAVYLZQV.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-2LO7FLVD.js";
13
+ } from "./chunk-KVQHG3WB.js";
14
14
 
15
15
  // src/site-crawl-graph-layout.ts
16
16
  import crypto from "crypto";