@canonry/canonry 7.7.0 → 7.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/assets/agent-workspace/skills/aero/SKILL.md +43 -17
  2. package/assets/agent-workspace/skills/aero/references/agent-operations.md +4 -1
  3. package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +15 -7
  4. package/assets/agent-workspace/skills/canonry/SKILL.md +4 -1
  5. package/assets/assets/{AuditHistoryPanel-qumC-I56.js → AuditHistoryPanel-D0nukTwD.js} +1 -1
  6. package/assets/assets/{BacklinksPage-BNyYcY5w.js → BacklinksPage-B525ij0g.js} +1 -1
  7. package/assets/assets/{HistoryPage-CQfOM6NA.js → HistoryPage-DvupqQDM.js} +1 -1
  8. package/assets/assets/{MeasurementPropertyPage-D0LTQHL7.js → MeasurementPropertyPage-CwDbi1XN.js} +1 -1
  9. package/assets/assets/{ProjectPage-QQErJAT8.js → ProjectPage-DmZZ2C7q.js} +1 -1
  10. package/assets/assets/{RunsPage-CniAGDVF.js → RunsPage-DHTvHcGm.js} +1 -1
  11. package/assets/assets/{SettingsPage-BPsrE4ao.js → SettingsPage-BwAFzB7A.js} +1 -1
  12. package/assets/assets/SiteHealthSection-hi54eSIt.js +4 -0
  13. package/assets/assets/{TrafficPage-DXKk92zy.js → TrafficPage-BOcXASAM.js} +1 -1
  14. package/assets/assets/{TrafficSourceDetailPage-4POLIA_c.js → TrafficSourceDetailPage-DTq0cN6g.js} +1 -1
  15. package/assets/assets/{extract-error-message-C6t0hB2w.js → extract-error-message-BOTDI6g5.js} +1 -1
  16. package/assets/assets/index-BLJfGYUw.js +86 -0
  17. package/assets/assets/{index-HWSQgVum.css → index-Rcbm8bK1.css} +1 -1
  18. package/assets/assets/{react-sigma_core.esm.min-D7GjzWYV.js → react-sigma_core.esm.min-BEBdJNtD.js} +1 -1
  19. package/assets/assets/{v2-overview-adapter-ChE5k_dV.js → v2-overview-adapter-BPZ-H3VY.js} +1 -1
  20. package/assets/index.html +2 -2
  21. package/dist/{chunk-ODEAE5T5.js → chunk-AAMKTL6B.js} +3 -3
  22. package/dist/{chunk-WJJTKZKK.js → chunk-DO7XPDG5.js} +5977 -5743
  23. package/dist/{chunk-75RSUOHZ.js → chunk-KRFTA4I7.js} +1126 -475
  24. package/dist/{chunk-WBTYXLXL.js → chunk-LO56QXPO.js} +5 -4
  25. package/dist/{chunk-LFUSODDL.js → chunk-NU62UT2S.js} +444 -158
  26. package/dist/{chunk-JJXTHJ4E.js → chunk-T2V2PESH.js} +4 -3
  27. package/dist/{chunk-3DB43KT2.js → chunk-ZLB7WX5D.js} +26 -33
  28. package/dist/cli.js +67 -28
  29. package/dist/{demo-server-UWBPY743.js → demo-server-KDMG3APY.js} +4 -4
  30. package/dist/index.js +6 -6
  31. package/dist/{intelligence-service-LOH37GPJ.js → intelligence-service-LAXJFE3U.js} +2 -2
  32. package/dist/mcp.js +4 -4
  33. package/package.json +9 -9
  34. package/assets/assets/SiteHealthSection-zW3ykBCu.js +0 -4
  35. package/assets/assets/index-DBQ-8jB1.js +0 -86
@@ -5,15 +5,16 @@ import {
5
5
  siteCrawlGraphNodes,
6
6
  siteCrawlPages,
7
7
  siteCrawlSnapshots
8
- } from "./chunk-75RSUOHZ.js";
8
+ } from "./chunk-KRFTA4I7.js";
9
9
  import {
10
10
  SITE_CRAWL_GRAPH_MAX_EDGES,
11
11
  SITE_CRAWL_GRAPH_MAX_NODES,
12
12
  SiteAuditFactorStatuses,
13
13
  TEMPLATE_LINK_RATIO_THRESHOLD,
14
+ factorStatusFromDistribution,
14
15
  factorStatusFromScore,
15
16
  roundPreservingTotal
16
- } from "./chunk-WJJTKZKK.js";
17
+ } from "./chunk-DO7XPDG5.js";
17
18
 
18
19
  // src/site-audit-factors.ts
19
20
  function recordedSharePct(factor) {
@@ -56,7 +57,7 @@ function computeFactorAverages(pages) {
56
57
  weight: rollup.weight,
57
58
  sharePct: shares?.[index] ?? null,
58
59
  avgScore,
59
- status: factorStatusFromScore(avgScore),
60
+ status: factorStatusFromDistribution({ pagesFailing: rollup.fail, pagesPartial: rollup.partial }),
60
61
  pagesPassing: rollup.pass,
61
62
  pagesPartial: rollup.partial,
62
63
  pagesFailing: rollup.fail
@@ -26,6 +26,7 @@ import {
26
26
  agentConversationCreateSchema,
27
27
  apiKeyDtoSchema,
28
28
  backlinkSourceSchema,
29
+ calendarDateSchema,
29
30
  canonicalizeGtmAccountId,
30
31
  canonicalizeGtmResourceSelection,
31
32
  competitorAliasesRequestSchema,
@@ -137,7 +138,7 @@ import {
137
138
  trafficSeriesGranularitySchema,
138
139
  visibilityCompareSelectionSchema,
139
140
  visibilityReportRequestSchema
140
- } from "./chunk-WJJTKZKK.js";
141
+ } from "./chunk-DO7XPDG5.js";
141
142
 
142
143
  // src/cli-error.ts
143
144
  function isMachineFormat(format) {
@@ -407,7 +408,8 @@ var measurementOverviewInputSchema = z3.object({
407
408
  search: measurementOverviewQuerySchema.shape.search,
408
409
  sort: measurementOverviewQuerySchema.shape.sort,
409
410
  cursor: measurementOverviewQuerySchema.shape.cursor,
410
- limit: measurementOverviewQuerySchema.shape.limit
411
+ limit: measurementOverviewQuerySchema.shape.limit,
412
+ compact: z3.boolean().default(true).describe("Defaults to true: bounded whole rows omit repeated per-Property engine detail with explicit detailsOmitted. Use compact=false for that detail; preserve all filters when following nextCursor.")
411
413
  }).strict().superRefine((input, context) => {
412
414
  if (input.scope === "group" && !input.groupKey) {
413
415
  context.addIssue({ code: "custom", path: ["groupKey"], message: "Group scope requires groupKey." });
@@ -435,21 +437,13 @@ var measurementPropertyEvidenceInputSchema = measurementPropertyEvidenceQuerySch
435
437
  "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."
436
438
  )
437
439
  }).strict();
438
- var PORTFOLIO_SUMMARY_AGENT_ROWS = 4;
439
- var PORTFOLIO_SUMMARY_AGENT_LIMIT_MAX = 10;
440
- function portfolioSummaryForAgents(summary, limitNote) {
441
- const rows = summary.weakestProperties;
442
- return {
443
- ...limitNote === void 0 ? {} : { limitNote },
444
- ...summary,
445
- ...Array.isArray(rows) ? { weakestProperties: summary.weakestProperties.map(({ recommendedInstead: _names, recommendedInsteadTotal: _total, recommendedInsteadTruncated: _cut, ...row }) => row) } : {}
446
- };
447
- }
448
440
  var measurementPortfolioSummaryInputSchema = measurementPortfolioSummaryQuerySchema.extend({
449
441
  project: projectNameSchema,
450
442
  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."),
451
- 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.`),
452
- 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.")
443
+ list: measurementPortfolioSummaryQuerySchema.shape.list.describe("Select one complete compact list. Default cursor walks weakest-properties; other first-page lists are bounded summaries. observed-names and cited-domains require answers=not-mentioned. Keep list unchanged across cursors."),
444
+ limit: measurementPortfolioSummaryQuerySchema.shape.limit.describe("Maximum selected-list rows; omitted auto-sizes up to 50 under the byte budget. nextCursor advances only pageList; use list to enumerate another ranking, market or evidence list."),
445
+ compact: z3.boolean().default(true).describe("Defaults to true. Omits detailed evidence with explicit detailsOmitted and returns bounded cursor pages."),
446
+ includeNestedMarkets: measurementPortfolioSummaryQuerySchema.shape.includeNestedMarkets.describe("Include every market level; compact pages preserve market totals and page the rows.")
453
447
  }).strict();
454
448
  var measurementPropertyQuestionsInputSchema = measurementPropertyQuestionsQuerySchema.extend({
455
449
  project: projectNameSchema
@@ -1105,7 +1099,8 @@ var crawlRunIdSchema = runIdSchema.optional().describe(
1105
1099
  );
1106
1100
  var technicalAeoCrawlInputSchema = z3.object({
1107
1101
  project: projectNameSchema,
1108
- runId: crawlRunIdSchema
1102
+ runId: crawlRunIdSchema,
1103
+ date: calendarDateSchema.optional().describe("Exact UTC scan date YYYY-MM-DD, mutually exclusive with runId. A dated no-data result may include availableScanDates. When the user omitted the year, resolve it from matchingMonthDayDates and request that exact returned date before concluding the scan is missing. Never silently select another date.")
1109
1104
  });
1110
1105
  var siteHealthPageAuditInputSchema = z3.object({
1111
1106
  project: projectNameSchema,
@@ -1163,6 +1158,7 @@ var siteHealthChangesInputSchema = z3.object({
1163
1158
  var technicalAeoCrawlPagesInputSchema = z3.object({
1164
1159
  project: projectNameSchema,
1165
1160
  runId: runIdSchema.optional(),
1161
+ date: calendarDateSchema.optional().describe("Exact UTC scan date YYYY-MM-DD, mutually exclusive with runId. A dated no-data result may include availableScanDates. When the user omitted the year, resolve it from matchingMonthDayDates and request that exact returned date before concluding the scan is missing. Missing data never falls back to another date."),
1166
1162
  inventoryEligible: z3.boolean().optional().describe("Filter Canonry technical-inventory eligibility. This is not actual Google index coverage."),
1167
1163
  fetchState: z3.string().min(1).optional().describe("Filter crawler fetch state, for example html, redirect, non-html, or fetch-error."),
1168
1164
  indexabilityState: z3.string().min(1).optional().describe("Filter crawler-derived indexability state. This is not Google index coverage."),
@@ -1286,7 +1282,7 @@ var canonryMcpTools = [
1286
1282
  defineTool({
1287
1283
  name: "canonry_sentiment",
1288
1284
  title: "Read sentiment and per-query scores",
1289
- description: 'Read stored model-classified language about frozen Simple identities or Advanced Properties. Returns headline and per-query favorable/mixed/unfavorable rates, judged and selected assessment counts, rated and eligible answers with their share (ratedAnswerRate: answers with a favorable, mixed or unfavorable rating over every eligible answer, admitted or not; each answer counts once), exclusions, intervals and method limitations. Favorable % is favorable / (favorable + mixed + unfavorable); factual and unjudged answers do not enter that denominator. Select branded or non-brand explicitly and keep their metrics separate; the default is branded. Optional queryId selects one frozen query. Query rows are compact and paged: queryLimit (default 25) rows per page, then pass queryPage.nextCursor as queryCursor. Per-engine assessment verdicts and per-location scores are opt-in with include=["assessments","locations"], or come by default when queryId names one query. Advanced rows are per executionNodeKey; pass it to narrow evidence to that node. Optional runIds selects the exact grouped location runs instead of runId. A known subject absent from a non-brand answer is not unfavorable. Branded reads carry criticizedProperties: total Properties with a mixed or unfavorable rating, and keys, the first five property breakdown keys, most criticized first ({total: 0, keys: []} with fewer than two Properties in view; absent for non-brand, disabled or unavailable reads); read their answers on canonry_sentiment_evidence with scope property, that scopeKey and outcome ["mixed","unfavorable"]. Partial values are provisional. Never starts a classifier. Preserve the resolved evaluationDefinitionId and complete selection for later evidence reads.',
1285
+ description: 'Read stored model-classified language about frozen Simple identities or Advanced Properties. Returns headline and per-query favorable/mixed/unfavorable rates, judged and selected assessment counts, rated and eligible answers with their share (ratedAnswerRate: answers with a favorable, mixed or unfavorable rating over every eligible answer, admitted or not; each answer counts once), exclusions, intervals and method limitations. Favorable % is favorable / (favorable + mixed + unfavorable); factual and unjudged answers do not enter that denominator. Select branded or non-brand explicitly and keep their metrics separate; the default is branded. Optional queryId selects one frozen query. Query rows are compact and paged: queryLimit (default 25) rows per page, then pass queryPage.nextCursor as queryCursor. Per-engine assessment verdicts and per-location scores are opt-in with include=["assessments","locations"], or come by default when queryId names one query. Advanced rows are per executionNodeKey; pass it to narrow evidence to that node. Optional runIds selects the exact grouped location runs instead of runId. coverage.subjectNotMentioned counts distinct answers where every assessed subject is absent; this is expected for non-brand queries and is not missing data. A known subject absent from a non-brand answer is not unfavorable. Branded reads carry criticizedProperties: total Properties with a mixed or unfavorable rating, and keys, the first five property breakdown keys, most criticized first ({total: 0, keys: []} with fewer than two Properties in view; absent for non-brand, disabled or unavailable reads); read their answers on canonry_sentiment_evidence with scope property, that scopeKey and outcome ["mixed","unfavorable"]. Partial values are provisional. Never starts a classifier. Preserve the resolved evaluationDefinitionId and complete selection for later evidence reads.',
1290
1286
  access: "read",
1291
1287
  tier: "monitoring",
1292
1288
  inputSchema: sentimentInputSchema,
@@ -1316,7 +1312,7 @@ var canonryMcpTools = [
1316
1312
  defineTool({
1317
1313
  name: "canonry_sentiment_compare",
1318
1314
  title: "Compare matched sentiment periods",
1319
- description: "Compare stored periods under compatible frozen scope, answer-engine identity and evaluator, with complete classification coverage. Returns matched/excluded units, each denominator, all refusal reasons, a nullable direction, and both period summaries with compact query rows (per-engine detail is on canonry_sentiment). Overlapping Wilson intervals mean no clear change detected; they do not establish equivalence. No provider calls.",
1315
+ description: "fromRunId accepts previous-rated to select the preceding rated run in the same scope within 50 rated candidates; identical run IDs are refused. Population changes, incomplete targets and search-limit refusals are distinct; search-limit asks for an explicit older run. Compare stored periods under compatible frozen scope, answer-engine identity and evaluator, with complete classification coverage. Returns matched/excluded units, each denominator, all refusal reasons, a nullable direction, and both period summaries with compact query rows (per-engine detail is on canonry_sentiment). Overlapping Wilson intervals mean no clear change detected; they do not establish equivalence. No provider calls.",
1320
1316
  access: "read",
1321
1317
  tier: "monitoring",
1322
1318
  inputSchema: sentimentCompareInputSchema,
@@ -1478,7 +1474,7 @@ var canonryMcpTools = [
1478
1474
  defineTool({
1479
1475
  name: "canonry_project_overview",
1480
1476
  title: "Get project overview (composite)",
1481
- 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.`,
1477
+ description: `Sentiment headline is sentiment.branded; nonBrand is a separate exception population. pooledOverall and deprecated overall are explicitly pooled compatibility figures and must never be reported as a class-specific headline. 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.`,
1482
1478
  access: "read",
1483
1479
  tier: "core",
1484
1480
  inputSchema: z3.object({
@@ -1550,7 +1546,7 @@ var canonryMcpTools = [
1550
1546
  defineTool({
1551
1547
  name: "canonry_competitor_landscape",
1552
1548
  title: "Get historical competitor landscape",
1553
- 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.",
1549
+ description: "answers=not-mentioned selects only answers where none of their targeted subjects was named, with answerSelection reporting population and selected counts. 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. answers=not-mentioned publishes names and mention/citation counts only; all competitive shares are null with reason answer-selection because the population is selected by the measured mention outcome. `runCount` and `runIds` name the runs counted; above 1 the counts pool several runs.",
1554
1550
  access: "read",
1555
1551
  tier: "monitoring",
1556
1552
  inputSchema: competitorLandscapeInputSchema,
@@ -2885,7 +2881,7 @@ var canonryMcpTools = [
2885
2881
  defineTool({
2886
2882
  name: "canonry_measurement_overview",
2887
2883
  title: "Get Advanced Measurement overview",
2888
- 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.",
2884
+ description: "Compact pages are byte-bounded whole rows by default; use nextCursor with unchanged filters. 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.",
2889
2885
  access: "read",
2890
2886
  tier: "setup",
2891
2887
  inputSchema: measurementOverviewInputSchema,
@@ -2893,7 +2889,7 @@ var canonryMcpTools = [
2893
2889
  openApiOperations: ["GET /api/v1/projects/{name}/measurement-overview"],
2894
2890
  handler: (client, input) => {
2895
2891
  const { project, ...query } = input;
2896
- return client.getMeasurementOverview(project, query);
2892
+ return client.getMeasurementOverview(project, { ...query, compact: query.compact ?? true });
2897
2893
  }
2898
2894
  }),
2899
2895
  defineTool({
@@ -2913,7 +2909,7 @@ var canonryMcpTools = [
2913
2909
  defineTool({
2914
2910
  name: "canonry_measurement_portfolio_summary",
2915
2911
  title: "Summarize measured Properties",
2916
- 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 and any unchecked count beside citation (saved answers whose source capture was incomplete; they are in neither side). Denominators count answers, one per query per engine (queries x engines = answers, less any unattributed or unchecked). 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. weakestAnswerSources.ownDomainAnswers counts those answers that cite any of your own domains. A Property's citationCoverage counts only answers citing that Property's own page, so 0% citation does not mean your domain went uncited; use ownDomainAnswers for that. 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.",
2912
+ description: "Read revision-pinned Property rankings and market rollups from stored answers. Start here for portfolio headlines: metrics.propertiesMentioned, propertiesNeverMentioned, mentionCoverage and citationCoverage. Defaults to non-brand; report branded separately. A never-mentioned Property may still be cited. Compact weakestMarkets ranks actual full top-level market rates; use it for weakest metros. tiedAtWeakest requires both zero mentions and zero citations; its byMetro rows describe only that Property cohort, not full metro rates, and its count is not the never-mentioned count. Compact pages are byte-bounded whole rows by default. nextCursor advances only pageList, default weakest-properties. Other lists are bounded first-page summaries, then omitted; totals remain complete. To enumerate another list, set list to strongest-mentions, weakest-mentions, excluded-mentions, markets, observed-names or cited-domains. Selected-list mode omits sibling row bodies; without limit, pages auto-size up to 50 rows. Follow nextCursor as cursor with unchanged list and filters; detailsOmitted names evidence available on drill-down tools. Mention and citation denominators are independent; preserve unattributed and unchecked counts. answers=not-mentioned returns answerEvidence names and registrable cited domains only from answers naming none of their targeted Properties, with populationSize, answerCount and unknownMentionAnswers. Counts for names and domains count distinct answers. Markets can overlap and must not be summed. Group rows by returned metro/submarkets rather than label. No provider calls.",
2917
2913
  access: "read",
2918
2914
  tier: "monitoring",
2919
2915
  inputSchema: measurementPortfolioSummaryInputSchema,
@@ -2921,11 +2917,7 @@ var canonryMcpTools = [
2921
2917
  openApiOperations: ["GET /api/v1/projects/{name}/measurement-portfolio-summary"],
2922
2918
  handler: async (client, input) => {
2923
2919
  const { project, ...query } = input;
2924
- const most = PORTFOLIO_SUMMARY_AGENT_ROWS;
2925
- const lowered = query.limit !== void 0 && query.limit > most;
2926
- const summary = await client.getMeasurementPortfolioSummary(project, lowered ? { ...query, limit: most } : query);
2927
- 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.";
2928
- return portfolioSummaryForAgents(summary, lowered ? `limit ${query.limit} was lowered to ${most}, the most rows that fit one result. ${more}` : void 0);
2920
+ return client.getMeasurementPortfolioSummary(project, { ...query, compact: query.compact ?? true });
2929
2921
  }
2930
2922
  }),
2931
2923
  defineTool({
@@ -3601,13 +3593,13 @@ var canonryMcpTools = [
3601
3593
  defineTool({
3602
3594
  name: "canonry_site_health_overview",
3603
3595
  title: "Get Site Health overview",
3604
- description: "Start a Site Health investigation with the latest or selected scan summary: root URL, completeness, crawl and link counts, budgets, versions, termination, and dead-link check state. Follow with a focused Site Health subgraph, shortest path, or scan changes; never request the interactive graph projection through MCP.",
3596
+ description: "For a requested scan date pass date=YYYY-MM-DD. If the year was omitted and that date has no data, use availableScanDates.matchingMonthDayDates to resolve the year and request the exact returned date before concluding the historical scan is missing. recentDates and matchingMonthDayDates are bounded; totalDates and matchingMonthDayTotal report their full counts. No automatic date fallback. inventorySummary provides complete eligibility counts and disjoint excluded reasons, including terminal redirects. Start a Site Health investigation with the latest or selected scan summary: root URL, completeness, crawl and link counts, budgets, versions, termination, and dead-link check state. Follow with a focused Site Health subgraph, shortest path, or scan changes; never request the interactive graph projection through MCP.",
3605
3597
  access: "read",
3606
3598
  tier: "monitoring",
3607
3599
  inputSchema: technicalAeoCrawlInputSchema,
3608
3600
  annotations: readAnnotations(),
3609
3601
  openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/crawl"],
3610
- handler: (client, input) => client.getTechnicalAeoCrawl(input.project, { runId: input.runId })
3602
+ handler: (client, input) => client.getTechnicalAeoCrawl(input.project, { runId: input.runId, date: input.date })
3611
3603
  }),
3612
3604
  defineTool({
3613
3605
  name: "canonry_site_health_page_audit",
@@ -3681,7 +3673,7 @@ var canonryMcpTools = [
3681
3673
  defineTool({
3682
3674
  name: "canonry_technical_aeo_score",
3683
3675
  title: "Get Technical AEO score",
3684
- description: "Get the Technical AEO scorecard for a project: the latest site-audit aggregate 0\u2013100 score, per-factor site-level averages (with pass/partial/fail distribution), cross-cutting issues, prioritized fixes, and the delta vs the previous audit. A factor's share of the site score is `sharePct` (0\u2013100, the shares add up to 100; null for a scan that did not record it); its `weight` is relative and never a percentage. When `hasData` is false the project has never been audited \u2014 call canonry_technical_aeo_run first.",
3676
+ description: "pagesFailing and pagesPartial are independent counts; their sum is the number of audited pages below pass for that factor. Any failing page makes a factor fail, independently of its average score or affected-page count. These counts use the audited-page population; crawl eligibility is a separate inventory population. affectedPageRanking orders recorded per-factor below-pass counts from largest to smallest; this is affected-page breadth, not a prediction of mention or citation lift. Factor cohorts overlap, so do not add their counts together. runSelection flags same-date ambiguity and prefers a complete run on the latest scan date, otherwise the largest page sample. Get the Technical AEO scorecard for a project: the latest site-audit aggregate 0\u2013100 score, per-factor site-level averages (with pass/partial/fail distribution), cross-cutting issues, prioritized fixes, and the delta vs the previous audit. A factor's share of the site score is `sharePct` (0\u2013100, the shares add up to 100; null for a scan that did not record it); its `weight` is relative and never a percentage. When `hasData` is false, no scored audit is available for the selected scan; a stored crawl may still exist. Inspect the stored crawl before proposing a new audit.",
3685
3677
  access: "read",
3686
3678
  tier: "monitoring",
3687
3679
  inputSchema: technicalAeoScoreInputSchema,
@@ -3720,18 +3712,18 @@ var canonryMcpTools = [
3720
3712
  defineTool({
3721
3713
  name: "canonry_technical_aeo_crawl",
3722
3714
  title: "Get persisted Technical AEO crawl",
3723
- description: "Read persisted full-crawl metadata for the latest or selected Technical AEO site-audit run: root URL, crawl/indexability/link-score versions, effective budgets, completeness, counts, and explicit dead-link check state. This does not fabricate a graph from older scorecard-only audits.",
3715
+ description: "For a historical question pass its exact UTC date (YYYY-MM-DD). If the year was omitted and that date has no data, use availableScanDates.matchingMonthDayDates to resolve the year and request the exact returned date before concluding the historical scan is missing. recentDates and matchingMonthDayDates are bounded; totalDates and matchingMonthDayTotal report their full counts. No automatic date fallback. inventorySummary gives whole-snapshot eligibility and disjoint excluded reasons; unknown is not canonical-away. Read persisted full-crawl metadata for the latest or selected Technical AEO site-audit run: root URL, crawl/indexability/link-score versions, effective budgets, completeness, counts, and explicit dead-link check state. This does not fabricate a graph from older scorecard-only audits.",
3724
3716
  access: "read",
3725
3717
  tier: "monitoring",
3726
3718
  inputSchema: technicalAeoCrawlInputSchema,
3727
3719
  annotations: readAnnotations(),
3728
3720
  openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/crawl"],
3729
- handler: (client, input) => client.getTechnicalAeoCrawl(input.project, { runId: input.runId })
3721
+ handler: (client, input) => client.getTechnicalAeoCrawl(input.project, { runId: input.runId, date: input.date })
3730
3722
  }),
3731
3723
  defineTool({
3732
3724
  name: "canonry_technical_aeo_crawl_pages",
3733
3725
  title: "List Technical AEO crawl pages",
3734
- description: "Read one bounded, cursor-paged list of canonical crawl nodes. Filter crawler-derived indexability and audit state, then follow nextCursor; this is technical inventory eligibility, not a claim about Google index coverage. `complete: false` means the crawl stopped at the budget named by `termination`, so a missing page or link is not proof it does not exist.",
3726
+ description: "For a requested scan date pass date=YYYY-MM-DD. If the year was omitted and that date has no data, use availableScanDates.matchingMonthDayDates to resolve the year and request the exact returned date before concluding the historical scan is missing. recentDates and matchingMonthDayDates are bounded; totalDates and matchingMonthDayTotal report their full counts. No automatic date fallback. inventorySummary gives whole-snapshot eligible/excluded counts and disjoint excluded reasons; healthReasonCounts covers the filtered list before paging. Use these exact counts, never infer canonical-away from unknown indexability. healthReason distinguishes terminal redirects from canonical-to-other pages. Default runSelection reports same-date ambiguity and selects complete or most-pages evidence on the latest scan date. Read one bounded, cursor-paged list of canonical crawl nodes. Filter crawler-derived indexability and audit state, then follow nextCursor; this is technical inventory eligibility, not a claim about Google index coverage. `complete: false` means the crawl stopped at the budget named by `termination`, so a missing page or link is not proof it does not exist.",
3735
3727
  access: "read",
3736
3728
  tier: "monitoring",
3737
3729
  inputSchema: technicalAeoCrawlPagesInputSchema,
@@ -3739,6 +3731,7 @@ var canonryMcpTools = [
3739
3731
  openApiOperations: ["GET /api/v1/projects/{name}/technical-aeo/crawl/pages"],
3740
3732
  handler: (client, input) => client.getTechnicalAeoCrawlPages(input.project, {
3741
3733
  runId: input.runId,
3734
+ date: input.date,
3742
3735
  inventoryEligible: input.inventoryEligible,
3743
3736
  fetchState: input.fetchState,
3744
3737
  indexabilityState: input.indexabilityState,
package/dist/cli.js CHANGED
@@ -32,11 +32,11 @@ import {
32
32
  trackCliCommandFinished,
33
33
  trackEvent,
34
34
  waitForServerRuntimeStartup
35
- } from "./chunk-LFUSODDL.js";
35
+ } from "./chunk-NU62UT2S.js";
36
36
  import {
37
37
  autoSyncSkills,
38
38
  formatAutoSyncNotice
39
- } from "./chunk-ODEAE5T5.js";
39
+ } from "./chunk-AAMKTL6B.js";
40
40
  import {
41
41
  configExists,
42
42
  createApiClient,
@@ -53,13 +53,13 @@ import {
53
53
  saveConfig,
54
54
  saveConfigPatch,
55
55
  shouldPrintServeSkillsNudge
56
- } from "./chunk-WBTYXLXL.js";
56
+ } from "./chunk-LO56QXPO.js";
57
57
  import {
58
58
  DEFAULT_DEMO_TRUSTED_PROXIES,
59
59
  trustedProxiesProblem,
60
60
  trustedProxyProblem
61
61
  } from "./chunk-RB2F4D3Z.js";
62
- import "./chunk-JJXTHJ4E.js";
62
+ import "./chunk-T2V2PESH.js";
63
63
  import {
64
64
  CliError,
65
65
  EXIT_SYSTEM_ERROR,
@@ -71,7 +71,7 @@ import {
71
71
  runMeasurementDraftAction,
72
72
  systemError,
73
73
  usageError
74
- } from "./chunk-3DB43KT2.js";
74
+ } from "./chunk-ZLB7WX5D.js";
75
75
  import {
76
76
  CLOUDFLARE_WORKER_BINDINGS,
77
77
  CLOUDFLARE_WORKER_GENERATED_MARKER,
@@ -100,7 +100,7 @@ import {
100
100
  querySnapshots,
101
101
  rawEventSamples,
102
102
  runs
103
- } from "./chunk-75RSUOHZ.js";
103
+ } from "./chunk-KRFTA4I7.js";
104
104
  import {
105
105
  AdsDeliverySnapshotStatuses,
106
106
  AdsHistoricalCampaignRollupStatuses,
@@ -155,6 +155,7 @@ import {
155
155
  buildModelChangeNotice,
156
156
  canonicalizeGtmAccountId,
157
157
  canonicalizeGtmResourceSelection,
158
+ competitorLandscapeQuerySchema,
158
159
  computeCompetitorOverlap,
159
160
  contentTargetDismissRequestSchema,
160
161
  conversionTrackingContractWriteRequestSchema,
@@ -228,7 +229,7 @@ import {
228
229
  visibilityCompareSelectionSchema,
229
230
  visibilityReportRequestSchema,
230
231
  winnabilityClassSchema
231
- } from "./chunk-WJJTKZKK.js";
232
+ } from "./chunk-DO7XPDG5.js";
232
233
 
233
234
  // src/cli.ts
234
235
  import { pathToFileURL } from "url";
@@ -685,8 +686,8 @@ var SENTIMENT_CLI_COMMANDS = [
685
686
  },
686
687
  {
687
688
  path: ["sentiment", "compare"],
688
- usage: "canonry sentiment compare <project> --from-run-id <id> --to-run-id <id> [selection options] [--format json]",
689
- help: `${selectionHelp} Complete matched coverage and compatible evaluators are required for a directional verdict.`,
689
+ usage: "canonry sentiment compare <project> --from-run-id <id|previous-rated> --to-run-id <id> [selection options] [--format json]",
690
+ help: `${selectionHelp} Use --from-run-id previous-rated to resolve the preceding rated run in the selected scope. Self-comparisons are refused. Complete matched coverage and compatible evaluators are required for a directional verdict.`,
690
691
  options: { ...selectionOptions, "from-run-id": stringOption(), "to-run-id": stringOption() },
691
692
  run: (input) => compareSentiment(project(input, "compare"), parse(sentimentCompareRequestSchema, {
692
693
  ...selection(input),
@@ -778,7 +779,7 @@ function parseDemoTrustedProxies(values) {
778
779
  async function demoCommand(options) {
779
780
  const { host, port } = parseDemoListenOptions(options);
780
781
  const trustProxy = parseDemoTrustedProxies(options.trustProxy);
781
- const { createDemoServer } = await import("./demo-server-UWBPY743.js");
782
+ const { createDemoServer } = await import("./demo-server-KDMG3APY.js");
782
783
  const app = await createDemoServer({ trustProxy });
783
784
  let stopping = false;
784
785
  const stop = () => {
@@ -1192,7 +1193,7 @@ No DB writes performed. Re-run without --dry-run to apply.`);
1192
1193
  }
1193
1194
  }
1194
1195
  async function backfillInsightsCommand(project2, opts) {
1195
- const { IntelligenceService } = await import("./intelligence-service-LOH37GPJ.js");
1196
+ const { IntelligenceService } = await import("./intelligence-service-LAXJFE3U.js");
1196
1197
  const config = loadConfig();
1197
1198
  const db = createClient(config.database);
1198
1199
  migrate(db);
@@ -9246,7 +9247,8 @@ async function showCompetitorLandscape(project2, options) {
9246
9247
  ...options.model !== void 0 ? { model: options.model } : {},
9247
9248
  queryClass: options.queryClass,
9248
9249
  location: options.location,
9249
- runId: options.runId
9250
+ runId: options.runId,
9251
+ answers: options.answers
9250
9252
  });
9251
9253
  if (options.format === "json") {
9252
9254
  console.log(JSON.stringify(data, null, 2));
@@ -9324,7 +9326,7 @@ function printLandscapeRows(heading, rows, showSampleCount = false, context, que
9324
9326
  // src/cli-commands/competitor.ts
9325
9327
  var ADD_USAGE = "canonry competitor add <project> <domain...> [--alias <name>]... [--format json]";
9326
9328
  var ALIASES_USAGE = "canonry competitor aliases <project> <domain> [--set <name>]... [--add <name>]... [--remove <name>]... [--clear] [--format json]";
9327
- var LANDSCAPE_USAGE = "canonry competitor landscape <project> [--window 7d|30d|90d|all] [--group-key <key>|--scope all-markets] [--by-model] [--provider <provider> [--model <id>]] [--query-class all|branded|non-brand] [--location <label>] [--run-id <id>] [--format json|jsonl]";
9329
+ var LANDSCAPE_USAGE = "canonry competitor landscape <project> [--window 7d|30d|90d|all] [--group-key <key>|--scope all-markets] [--by-model] [--provider <provider> [--model <id>]] [--query-class all|branded|non-brand] [--answers all|not-mentioned] [--location <label>] [--run-id <id>] [--format json|jsonl]";
9328
9330
  function parseLandscapeScope(value) {
9329
9331
  if (value === void 0 || value === "") return void 0;
9330
9332
  if (value !== "all-markets") throw usageError(`--scope must be "all-markets" (got "${value}")`, {
@@ -9451,7 +9453,8 @@ Usage: ${ALIASES_USAGE}`, {
9451
9453
  model: stringOption(),
9452
9454
  "query-class": stringOption(),
9453
9455
  location: stringOption(),
9454
- "run-id": stringOption()
9456
+ "run-id": stringOption(),
9457
+ answers: stringOption()
9455
9458
  },
9456
9459
  run: async (input) => {
9457
9460
  const project2 = requireProject(input, "competitor.landscape", LANDSCAPE_USAGE);
@@ -9471,7 +9474,10 @@ Usage: ${ALIASES_USAGE}`, {
9471
9474
  details: { command: "competitor.landscape", usage: LANDSCAPE_USAGE }
9472
9475
  });
9473
9476
  }
9477
+ const parsedAnswers = competitorLandscapeQuerySchema.safeParse({ answers: getString(input.values, "answers") });
9478
+ if (!parsedAnswers.success) throw usageError("--answers must be all or not-mentioned");
9474
9479
  await showCompetitorLandscape(project2, {
9480
+ answers: parsedAnswers.data.answers,
9475
9481
  window: getString(input.values, "window"),
9476
9482
  groupKey,
9477
9483
  scope,
@@ -11794,12 +11800,16 @@ async function runAdvancedMeasurementOperation(project2, operation, source, form
11794
11800
  advancedEmptyInputSchema.parse(input);
11795
11801
  result = await client.getMeasurementSetup(project2);
11796
11802
  break;
11797
- case "overview":
11798
- result = await client.getMeasurementOverview(project2, measurementOverviewQuerySchema.parse(input));
11803
+ case "overview": {
11804
+ const query = measurementOverviewQuerySchema.parse(input);
11805
+ result = await client.getMeasurementOverview(project2, { ...query, compact: typeof input === "object" && input !== null && "compact" in input ? query.compact : true });
11799
11806
  break;
11800
- case "portfolio-summary":
11801
- result = await client.getMeasurementPortfolioSummary(project2, measurementPortfolioSummaryQuerySchema.parse(input));
11807
+ }
11808
+ case "portfolio-summary": {
11809
+ const query = measurementPortfolioSummaryQuerySchema.parse(input);
11810
+ result = await client.getMeasurementPortfolioSummary(project2, { ...query, compact: typeof input === "object" && input !== null && "compact" in input ? query.compact : true });
11802
11811
  break;
11812
+ }
11803
11813
  case "property-questions":
11804
11814
  result = await client.getMeasurementPropertyQuestions(project2, measurementPropertyQuestionsQuerySchema.parse(input));
11805
11815
  break;
@@ -18491,12 +18501,16 @@ async function technicalAeoProgress(project2, opts) {
18491
18501
  console.log(lines.join("\n"));
18492
18502
  }
18493
18503
  async function technicalAeoCrawl(project2, opts) {
18494
- const crawl = await getClient27().getTechnicalAeoCrawl(project2, { runId: opts.runId });
18504
+ const crawl = await getClient27().getTechnicalAeoCrawl(project2, { runId: opts.runId, date: opts.date });
18495
18505
  if (isMachineFormat(opts.format)) {
18496
18506
  console.log(JSON.stringify(crawl, null, 2));
18497
18507
  return;
18498
18508
  }
18499
18509
  if (!crawl.hasCrawlData) {
18510
+ if (opts.date) {
18511
+ console.log([`No persisted site crawl for "${project2}" on ${opts.date}.`, ...availableScanDateLines(crawl.availableScanDates)].join("\n"));
18512
+ return;
18513
+ }
18500
18514
  console.log(`No persisted site crawl for "${project2}". Run \`canonry technical-aeo run ${project2}\` first.`);
18501
18515
  return;
18502
18516
  }
@@ -18507,6 +18521,7 @@ async function technicalAeoCrawl(project2, opts) {
18507
18521
  `Links: ${crawl.counts.edges} internal/external observations \xB7 ${crawl.counts.findings} findings`,
18508
18522
  `Dead links: ${formatDeadLinkState(crawl.deadLinks)}`
18509
18523
  ];
18524
+ lines.push(...inventorySummaryLines(crawl.inventorySummary));
18510
18525
  if (crawl.termination) lines.push(`Stopped: ${crawl.termination}`);
18511
18526
  console.log(lines.join("\n"));
18512
18527
  }
@@ -18618,6 +18633,21 @@ function partialCrawlNote(res) {
18618
18633
  if (!res.hasCrawlData || res.complete !== false) return null;
18619
18634
  return `Partial crawl (${res.termination ?? "incomplete"}): pages and links beyond the crawl budget were not observed, so a missing page or link is not proof it does not exist.`;
18620
18635
  }
18636
+ function availableScanDateLines(dates) {
18637
+ if (!dates) return [];
18638
+ return [
18639
+ `Known scan dates: ${dates.totalDates} distinct dates`,
18640
+ `Recent dates: ${dates.recentDates.join(", ") || "none"}`,
18641
+ `Same month and day: ${dates.matchingMonthDayDates.join(", ") || "none"} (${dates.matchingMonthDayTotal} distinct dates)`
18642
+ ];
18643
+ }
18644
+ function inventorySummaryLines(summary) {
18645
+ if (!summary) return [];
18646
+ return [
18647
+ `Stored canonical pages: ${summary.total} total \xB7 ${summary.eligible} eligible \xB7 ${summary.excluded} excluded`,
18648
+ ...summary.excludedReasons.map((reason) => `Excluded \xB7 ${reason.healthReason}: ${reason.pages}`)
18649
+ ];
18650
+ }
18621
18651
  async function technicalAeoCrawlPages(project2, opts) {
18622
18652
  const res = await getClient27().getTechnicalAeoCrawlPages(project2, opts);
18623
18653
  if (opts.format === "jsonl") {
@@ -18626,6 +18656,11 @@ async function technicalAeoCrawlPages(project2, opts) {
18626
18656
  project: project2,
18627
18657
  runId: res.runId,
18628
18658
  complete: res.complete,
18659
+ runSelection: res.runSelection,
18660
+ requestedDate: res.requestedDate,
18661
+ availableScanDates: res.availableScanDates,
18662
+ inventorySummary: res.inventorySummary,
18663
+ healthReasonCounts: res.healthReasonCounts,
18629
18664
  termination: res.termination,
18630
18665
  total: res.total,
18631
18666
  nextCursor: res.nextCursor
@@ -18638,14 +18673,16 @@ async function technicalAeoCrawlPages(project2, opts) {
18638
18673
  return;
18639
18674
  }
18640
18675
  const note = partialCrawlNote(res);
18676
+ const inventoryLines = inventorySummaryLines(res.inventorySummary);
18641
18677
  if (res.pages.length === 0) {
18642
- console.log([`No persisted crawl pages for "${project2}".`, ...note ? [note] : []].join("\n"));
18678
+ console.log([opts.date && !res.hasCrawlData ? `No persisted crawl pages for "${project2}" on ${opts.date}.` : `No persisted crawl pages for "${project2}".`, ...availableScanDateLines(res.availableScanDates), ...inventoryLines, ...note ? [note] : []].join("\n"));
18643
18679
  return;
18644
18680
  }
18645
18681
  const lines = [`${res.pages.length} of ${res.total} crawl page(s)${res.nextCursor ? " (more available)" : ""}:`, ""];
18646
- lines.push(`${"Depth".padStart(5)} ${"Score".padStart(5)} ${"Indexability".padEnd(15)} URL`);
18682
+ lines.push(...inventoryLines);
18683
+ lines.push(`${"Depth".padStart(5)} ${"Score".padStart(5)} ${"Page state".padEnd(21)} URL`);
18647
18684
  for (const page of res.pages) {
18648
- lines.push(`${String(page.depth ?? "-").padStart(5)} ${String(page.auditScore ?? "-").padStart(5)} ${page.indexabilityState.slice(0, 15).padEnd(15)} ${page.url}`);
18685
+ lines.push(`${String(page.depth ?? "-").padStart(5)} ${String(page.auditScore ?? "-").padStart(5)} ${(page.healthReason ?? page.healthState ?? page.indexabilityState).slice(0, 21).padEnd(21)} ${page.url}`);
18649
18686
  }
18650
18687
  if (res.nextCursor) lines.push(`
18651
18688
  Next cursor: ${res.nextCursor}`);
@@ -19003,12 +19040,12 @@ var TECHNICAL_AEO_CLI_COMMANDS_BASE = [
19003
19040
  },
19004
19041
  {
19005
19042
  path: ["technical-aeo", "crawl"],
19006
- usage: "canonry technical-aeo crawl <project> [--run-id <id>] [--format json]",
19007
- options: { "run-id": stringOption() },
19043
+ usage: "canonry technical-aeo crawl <project> [--run-id <id> | --date YYYY-MM-DD] [--format json]",
19044
+ options: { "run-id": stringOption(), date: stringOption() },
19008
19045
  run: async (input) => {
19009
- const usage = "canonry technical-aeo crawl <project> [--run-id <id>] [--format json]";
19046
+ const usage = "canonry technical-aeo crawl <project> [--run-id <id> | --date YYYY-MM-DD] [--format json]";
19010
19047
  const project2 = requireProject(input, "technical-aeo.crawl", usage);
19011
- await technicalAeoCrawl(project2, { runId: getString(input.values, "run-id"), format: input.format });
19048
+ await technicalAeoCrawl(project2, { runId: getString(input.values, "run-id"), date: getString(input.values, "date"), format: input.format });
19012
19049
  }
19013
19050
  },
19014
19051
  {
@@ -19148,9 +19185,10 @@ Usage: ${usage}`, {
19148
19185
  },
19149
19186
  {
19150
19187
  path: ["technical-aeo", "crawl-pages"],
19151
- usage: "canonry technical-aeo crawl-pages <project> [--run-id <id>] [--inventory-eligible true|false] [--fetch-state <state>] [--indexability-state <state>] [--audit-state <state>] [--sort url|path|score-asc|score-desc] [--cursor <cursor>] [--limit <n>] [--format json|jsonl]",
19188
+ usage: "canonry technical-aeo crawl-pages <project> [--run-id <id> | --date YYYY-MM-DD] [--inventory-eligible true|false] [--fetch-state <state>] [--indexability-state <state>] [--audit-state <state>] [--sort url|path|score-asc|score-desc] [--cursor <cursor>] [--limit <n>] [--format json|jsonl]",
19152
19189
  options: {
19153
19190
  "run-id": stringOption(),
19191
+ date: stringOption(),
19154
19192
  "inventory-eligible": stringOption(),
19155
19193
  "fetch-state": stringOption(),
19156
19194
  "indexability-state": stringOption(),
@@ -19160,7 +19198,7 @@ Usage: ${usage}`, {
19160
19198
  limit: stringOption()
19161
19199
  },
19162
19200
  run: async (input) => {
19163
- const usage = "canonry technical-aeo crawl-pages <project> [--run-id <id>] [--inventory-eligible true|false] [--fetch-state <state>] [--indexability-state <state>] [--audit-state <state>] [--sort url|path|score-asc|score-desc] [--cursor <cursor>] [--limit <n>] [--format json|jsonl]";
19201
+ const usage = "canonry technical-aeo crawl-pages <project> [--run-id <id> | --date YYYY-MM-DD] [--inventory-eligible true|false] [--fetch-state <state>] [--indexability-state <state>] [--audit-state <state>] [--sort url|path|score-asc|score-desc] [--cursor <cursor>] [--limit <n>] [--format json|jsonl]";
19164
19202
  const project2 = requireProject(input, "technical-aeo.crawl-pages", usage);
19165
19203
  const sort = getString(input.values, "sort");
19166
19204
  if (sort !== void 0 && !["url", "path", "score-asc", "score-desc"].includes(sort)) {
@@ -19172,6 +19210,7 @@ Usage: ${usage}`, {
19172
19210
  }
19173
19211
  await technicalAeoCrawlPages(project2, {
19174
19212
  runId: getString(input.values, "run-id"),
19213
+ date: getString(input.values, "date"),
19175
19214
  inventoryEligible: parseOptionalBoolean(input, "inventory-eligible", { command: "technical-aeo.crawl-pages", usage }),
19176
19215
  fetchState: getString(input.values, "fetch-state"),
19177
19216
  indexabilityState: getString(input.values, "indexability-state"),
@@ -5,11 +5,11 @@ import {
5
5
  computeFactorAverages,
6
6
  layoutSiteCrawlGraphInput,
7
7
  persistSiteCrawlGraphLayout
8
- } from "./chunk-JJXTHJ4E.js";
8
+ } from "./chunk-T2V2PESH.js";
9
9
  import {
10
10
  PACKAGE_VERSION,
11
11
  canonryMcpTools
12
- } from "./chunk-3DB43KT2.js";
12
+ } from "./chunk-ZLB7WX5D.js";
13
13
  import {
14
14
  HISTORY_WINDOW_RUNS,
15
15
  IntelligenceService,
@@ -83,7 +83,7 @@ import {
83
83
  siteCrawlPages,
84
84
  siteCrawlSnapshots,
85
85
  trafficSources
86
- } from "./chunk-75RSUOHZ.js";
86
+ } from "./chunk-KRFTA4I7.js";
87
87
  import {
88
88
  RunKinds,
89
89
  SiteCrawlFetchStates,
@@ -112,7 +112,7 @@ import {
112
112
  roundPreservingTotal,
113
113
  siteAuditPageFactorSchema,
114
114
  wilsonInterval
115
- } from "./chunk-WJJTKZKK.js";
115
+ } from "./chunk-DO7XPDG5.js";
116
116
 
117
117
  // src/demo-server.ts
118
118
  import { dirname, join as join2 } from "path";
package/dist/index.js CHANGED
@@ -3,14 +3,14 @@ import {
3
3
  createGoogleMarketingCredentialStore,
4
4
  createGoogleMarketingRuntime,
5
5
  createServer
6
- } from "./chunk-LFUSODDL.js";
6
+ } from "./chunk-NU62UT2S.js";
7
7
  import {
8
8
  loadConfig
9
- } from "./chunk-WBTYXLXL.js";
10
- import "./chunk-JJXTHJ4E.js";
11
- import "./chunk-3DB43KT2.js";
12
- import "./chunk-75RSUOHZ.js";
13
- import "./chunk-WJJTKZKK.js";
9
+ } from "./chunk-LO56QXPO.js";
10
+ import "./chunk-T2V2PESH.js";
11
+ import "./chunk-ZLB7WX5D.js";
12
+ import "./chunk-KRFTA4I7.js";
13
+ import "./chunk-DO7XPDG5.js";
14
14
  export {
15
15
  GoogleMarketingRuntimeError,
16
16
  createGoogleMarketingCredentialStore,
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  HISTORY_WINDOW_RUNS,
3
3
  IntelligenceService
4
- } from "./chunk-75RSUOHZ.js";
5
- import "./chunk-WJJTKZKK.js";
4
+ } from "./chunk-KRFTA4I7.js";
5
+ import "./chunk-DO7XPDG5.js";
6
6
  export {
7
7
  HISTORY_WINDOW_RUNS,
8
8
  IntelligenceService