@ainyc/canonry 4.180.8 → 4.183.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 (38) hide show
  1. package/README.md +166 -100
  2. package/assets/agent-workspace/skills/aero/references/orchestration.md +1 -1
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +1 -1
  4. package/assets/assets/{AuditHistoryPanel-qiuqNGvG.js → AuditHistoryPanel-ClwYugEs.js} +1 -1
  5. package/assets/assets/{BacklinksPage-D-g5f0Xv.js → BacklinksPage-DaEn1w9_.js} +1 -1
  6. package/assets/assets/{ChartPrimitives-DG35TyhH.js → ChartPrimitives-DLEICrrL.js} +1 -1
  7. package/assets/assets/{HistoryPage-q8_pCuOs.js → HistoryPage-B4WWIXmx.js} +1 -1
  8. package/assets/assets/{MeasurementPropertyPage-C_wMjUZe.js → MeasurementPropertyPage-DFvelKfS.js} +1 -1
  9. package/assets/assets/ProjectPage-D3AuGcau.js +9 -0
  10. package/assets/assets/{RunRow-D5hatdlY.js → RunRow-0HuBq5KN.js} +1 -1
  11. package/assets/assets/{RunsPage-Bj2snMM3.js → RunsPage-DhdjoIdK.js} +1 -1
  12. package/assets/assets/{SettingsPage-Cf5YgyJv.js → SettingsPage-XzHCmYz4.js} +1 -1
  13. package/assets/assets/{SiteHealthSection-D8RLvMAE.js → SiteHealthSection-BODUx0bX.js} +3 -3
  14. package/assets/assets/{TrafficPage-CotJ14le.js → TrafficPage-2AKICLH_.js} +1 -1
  15. package/assets/assets/{TrafficSourceDetailPage-BcxYX-uq.js → TrafficSourceDetailPage-CYVG7qZ0.js} +1 -1
  16. package/assets/assets/extract-error-message-CtGLEBMO.js +1 -0
  17. package/assets/assets/index-B64WXjg8.js +86 -0
  18. package/assets/assets/{index-DXCQf5s1.css → index-DwdK2e4e.css} +1 -1
  19. package/assets/assets/{react-sigma_core.esm.min-DxZINEmi.js → react-sigma_core.esm.min-D5Honb13.js} +1 -1
  20. package/assets/assets/{vendor-lucide-nYKv-BB7.js → vendor-lucide-XjCNFK0x.js} +1 -1
  21. package/assets/assets/{vendor-markdown-Bn7fRwrx.js → vendor-markdown-9tTeP8v7.js} +1 -1
  22. package/assets/assets/{vendor-radix-4UOaVWza.js → vendor-radix-BpmvLvpp.js} +1 -1
  23. package/assets/assets/{vendor-recharts-D8rt5lKn.js → vendor-recharts-FA0uySU7.js} +1 -1
  24. package/assets/assets/{vendor-tanstack-Dock0eH1.js → vendor-tanstack-CYBjbqVr.js} +1 -1
  25. package/assets/index.html +7 -7
  26. package/dist/{chunk-ZJ2VGTYU.js → chunk-ISONU3FA.js} +4276 -3389
  27. package/dist/{chunk-GFVZOVG3.js → chunk-OF3PKBW4.js} +72 -6
  28. package/dist/{chunk-G7M2KM7Q.js → chunk-RQSSKN5F.js} +2 -2
  29. package/dist/{chunk-IM4HFNGF.js → chunk-VFQPW2VX.js} +43 -3
  30. package/dist/{chunk-GN2G7VFK.js → chunk-Z2HTLTQV.js} +162 -4
  31. package/dist/cli.js +141 -8
  32. package/dist/index.js +4 -4
  33. package/dist/{intelligence-service-IURLYRB3.js → intelligence-service-4XQBUFBU.js} +2 -2
  34. package/dist/mcp.js +3 -3
  35. package/package.json +8 -8
  36. package/assets/assets/ProjectPage-33THfDtC.js +0 -9
  37. package/assets/assets/extract-error-message-Bxt42scd.js +0 -1
  38. package/assets/assets/index-BlILyZJ5.js +0 -86
@@ -11,7 +11,7 @@ import {
11
11
  loadConfig,
12
12
  loadConfigRaw,
13
13
  saveConfigPatch
14
- } from "./chunk-IM4HFNGF.js";
14
+ } from "./chunk-VFQPW2VX.js";
15
15
  import {
16
16
  CC_CACHE_DIR,
17
17
  DUCKDB_SPEC,
@@ -157,7 +157,7 @@ import {
157
157
  siteCrawlSnapshots,
158
158
  toAlertView,
159
159
  usageCounters
160
- } from "./chunk-ZJ2VGTYU.js";
160
+ } from "./chunk-ISONU3FA.js";
161
161
  import {
162
162
  AGENT_MEMORY_VALUE_MAX_BYTES,
163
163
  AGENT_PROVIDER_IDS,
@@ -296,7 +296,7 @@ import {
296
296
  validationError,
297
297
  winnabilityClassLabel,
298
298
  withRetry
299
- } from "./chunk-GN2G7VFK.js";
299
+ } from "./chunk-Z2HTLTQV.js";
300
300
 
301
301
  // src/telemetry.ts
302
302
  import crypto from "crypto";
@@ -11304,15 +11304,17 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
11304
11304
  indexabilityVersion: crawlSummary.indexabilityRulesetVersion,
11305
11305
  linkScoreVersion: crawlSummary.linkScoreAlgorithmVersion,
11306
11306
  effectiveOptions: {
11307
+ // Null, never absent: a reader must be able to tell "the operator set
11308
+ // no edge budget and the engine derived one" from a missing field.
11307
11309
  mode: "summary",
11308
11310
  sitemapUrl: opts.sitemapUrl ?? null,
11309
11311
  maxPages,
11310
- maxEdges,
11312
+ maxEdges: maxEdges ?? null,
11311
11313
  maxDepth: opts.maxDepth ?? null,
11312
11314
  checkDeadLinks: opts.checkDeadLinks ?? false
11313
11315
  },
11314
11316
  pageBudget: maxPages,
11315
- edgeBudget: maxEdges,
11317
+ edgeBudget: maxEdges ?? null,
11316
11318
  maxDepth: opts.maxDepth ?? null,
11317
11319
  checkDeadLinks: opts.checkDeadLinks ?? false,
11318
11320
  complete: crawlSummary.complete,
@@ -11867,7 +11869,7 @@ function readStoredGroundingSources(rawResponse) {
11867
11869
  return result;
11868
11870
  }
11869
11871
  async function backfillInsightsCommand(project, opts) {
11870
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-IURLYRB3.js");
11872
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-4XQBUFBU.js");
11871
11873
  const config = loadConfig();
11872
11874
  const db = createClient(config.database);
11873
11875
  migrate(db);
@@ -13803,6 +13805,68 @@ function trimRowsToFit(rows, render) {
13803
13805
  }
13804
13806
  return kept;
13805
13807
  }
13808
+ function nestedArrayCandidates(root) {
13809
+ const candidates = [];
13810
+ const visit = (object) => {
13811
+ let containsRecordCollection = false;
13812
+ for (const [key, value] of Object.entries(object)) {
13813
+ if (Array.isArray(value)) {
13814
+ let hasNestedRecords = false;
13815
+ let hasRecordRows = false;
13816
+ for (const row of value) {
13817
+ if (row && typeof row === "object" && !Array.isArray(row)) {
13818
+ hasRecordRows = true;
13819
+ hasNestedRecords = visit(row) || hasNestedRecords;
13820
+ }
13821
+ }
13822
+ if (value.length > 0 && !hasNestedRecords) {
13823
+ candidates.push({ owner: object, key, rows: value, size: serializeResult(value).length, hasRecordRows });
13824
+ }
13825
+ containsRecordCollection = hasRecordRows || hasNestedRecords || containsRecordCollection;
13826
+ } else if (value && typeof value === "object") {
13827
+ containsRecordCollection = visit(value) || containsRecordCollection;
13828
+ }
13829
+ }
13830
+ return containsRecordCollection;
13831
+ };
13832
+ visit(root);
13833
+ return candidates.sort(
13834
+ (left, right) => Number(right.hasRecordRows) - Number(left.hasRecordRows) || right.size - left.size
13835
+ );
13836
+ }
13837
+ function trimNestedArrays(full) {
13838
+ const parsed = JSON.parse(full);
13839
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
13840
+ const copy = parsed;
13841
+ copy.__truncated = true;
13842
+ let out = serializeResult(copy);
13843
+ while (out.length > MAX_TOOL_RESULT_CHARS) {
13844
+ const candidate = nestedArrayCandidates(copy).at(0);
13845
+ if (!candidate) return void 0;
13846
+ const { owner, key, rows } = candidate;
13847
+ const previous = owner.__omittedRowsByField;
13848
+ const omitted = previous && typeof previous === "object" && !Array.isArray(previous) ? { ...previous } : {};
13849
+ const priorCount = typeof omitted[key] === "number" ? omitted[key] : 0;
13850
+ owner.__truncated = true;
13851
+ owner.__omittedRowsByField = omitted;
13852
+ const keep = (count2) => {
13853
+ owner[key] = rows.slice(0, count2);
13854
+ omitted[key] = priorCount + rows.length - count2;
13855
+ return serializeResult(copy);
13856
+ };
13857
+ out = keep(0);
13858
+ if (out.length > MAX_TOOL_RESULT_CHARS) continue;
13859
+ let low = 0;
13860
+ let high = rows.length - 1;
13861
+ while (low < high) {
13862
+ const middle = Math.ceil((low + high) / 2);
13863
+ if (keep(middle).length <= MAX_TOOL_RESULT_CHARS) low = middle;
13864
+ else high = middle - 1;
13865
+ }
13866
+ out = keep(low);
13867
+ }
13868
+ return out;
13869
+ }
13806
13870
  function truncateToolResult(details) {
13807
13871
  const full = serializeResult(details);
13808
13872
  if (full.length <= MAX_TOOL_RESULT_CHARS) return full;
@@ -13830,6 +13894,8 @@ function truncateToolResult(details) {
13830
13894
  });
13831
13895
  if (out.length <= MAX_TOOL_RESULT_CHARS) return out;
13832
13896
  }
13897
+ const nested = trimNestedArrays(full);
13898
+ if (nested !== void 0) return nested;
13833
13899
  }
13834
13900
  return full.slice(0, MAX_TOOL_RESULT_CHARS) + "\n" + TRUNCATION_NOTE;
13835
13901
  }
@@ -5,11 +5,11 @@ import {
5
5
  installSkills,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-IM4HFNGF.js";
8
+ } from "./chunk-VFQPW2VX.js";
9
9
  import {
10
10
  SKILL_MANIFEST_FILENAME,
11
11
  SkillsClients
12
- } from "./chunk-GN2G7VFK.js";
12
+ } from "./chunk-Z2HTLTQV.js";
13
13
 
14
14
  // src/skills-autosync.ts
15
15
  import fs from "fs";
@@ -25,6 +25,7 @@ import {
25
25
  classifySkillFile,
26
26
  coerceSkillManifest,
27
27
  competitorBatchRequestSchema,
28
+ competitorLandscapeQuerySchema,
28
29
  describeError,
29
30
  discoveryBucketSchema,
30
31
  discoveryCompetitorTypeSchema,
@@ -92,7 +93,7 @@ import {
92
93
  trafficConnectWordpressRequestSchema,
93
94
  trafficEventKindSchema,
94
95
  trafficSeriesGranularitySchema
95
- } from "./chunk-GN2G7VFK.js";
96
+ } from "./chunk-Z2HTLTQV.js";
96
97
 
97
98
  // src/config.ts
98
99
  import fs from "fs";
@@ -2398,6 +2399,18 @@ var postApiV1ProjectsByNameCompetitors = (options) => {
2398
2399
  }
2399
2400
  });
2400
2401
  };
2402
+ var getApiV1ProjectsByNameAnalyticsCompetitors = (options) => {
2403
+ return (options.client ?? client).get({
2404
+ security: [
2405
+ {
2406
+ scheme: "bearer",
2407
+ type: "http"
2408
+ }
2409
+ ],
2410
+ url: "/api/v1/projects/{name}/analytics/competitors",
2411
+ ...options
2412
+ });
2413
+ };
2401
2414
  var getApiV1ProjectsByNameRuns = (options) => {
2402
2415
  return (options.client ?? client).get({
2403
2416
  security: [
@@ -6583,6 +6596,16 @@ var ApiClient = class {
6583
6596
  })
6584
6597
  );
6585
6598
  }
6599
+ /** Stored competitor SOV, optionally grouped or filtered by requested model; never starts provider work. */
6600
+ async getCompetitorLandscape(project, opts = {}) {
6601
+ return this.invoke(
6602
+ () => getApiV1ProjectsByNameAnalyticsCompetitors({
6603
+ client: this.heyClient,
6604
+ path: { name: project },
6605
+ query: opts
6606
+ })
6607
+ );
6608
+ }
6586
6609
  async getAnalyticsGaps(project, window) {
6587
6610
  return this.invoke(
6588
6611
  () => getApiV1ProjectsByNameAnalyticsGaps({
@@ -9088,6 +9111,9 @@ var competitorsInputSchema = z3.object({
9088
9111
  project: projectNameSchema,
9089
9112
  request: competitorBatchRequestSchema
9090
9113
  });
9114
+ var competitorLandscapeInputSchema = competitorLandscapeQuerySchema.safeExtend({
9115
+ project: projectNameSchema
9116
+ }).strict();
9091
9117
  var projectUpsertInputSchema = z3.object({
9092
9118
  project: projectNameSchema,
9093
9119
  request: projectUpsertRequestSchema
@@ -9361,7 +9387,7 @@ var technicalAeoRunInputSchema = z3.object({
9361
9387
  sitemapUrl: z3.string().url().optional().describe("Override the sitemap URL. Defaults to https://<canonicalDomain>/sitemap.xml."),
9362
9388
  limit: z3.number().int().positive().max(2e3).optional().describe("Deprecated compatibility alias for maxPages."),
9363
9389
  maxPages: z3.number().int().positive().max(5e4).optional().describe("Maximum pages crawled and audited. Defaults to 1,000; hard maximum 50,000."),
9364
- maxEdges: z3.number().int().positive().max(1e6).optional().describe("Maximum link observations retained for this crawl. Defaults to 100,000; hard maximum 1,000,000."),
9390
+ maxEdges: z3.number().int().positive().max(1e6).optional().describe("Maximum link observations retained for this crawl. When omitted the crawl engine derives the budget from the page count; hard maximum 1,000,000."),
9365
9391
  maxDepth: z3.number().int().min(0).max(100).optional().describe("Maximum internal-link depth from the root page."),
9366
9392
  checkDeadLinks: z3.boolean().optional().describe("Opt in to internal dead-link checks. Omitted and false both disable checks.")
9367
9393
  });
@@ -9480,6 +9506,20 @@ var canonryMcpTools = [
9480
9506
  openApiOperations: ["GET /api/v1/projects/{name}/analytics/sources"],
9481
9507
  handler: (client2, input) => client2.getAnalyticsSources(input.project, { window: input.window, limit: input.limit })
9482
9508
  }),
9509
+ defineTool({
9510
+ name: "canonry_competitor_landscape",
9511
+ title: "Get historical competitor landscape",
9512
+ 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. 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. No provider, discovery, or classifier work runs. Ranked lists are capped at 100 observed/other-source rows per group; pins remain complete. Model groups are capped at 50 and disclose truncation.",
9513
+ access: "read",
9514
+ tier: "monitoring",
9515
+ inputSchema: competitorLandscapeInputSchema,
9516
+ annotations: readAnnotations(),
9517
+ openApiOperations: ["GET /api/v1/projects/{name}/analytics/competitors"],
9518
+ handler: (client2, input) => {
9519
+ const { project, ...query } = input;
9520
+ return client2.getCompetitorLandscape(project, query);
9521
+ }
9522
+ }),
9483
9523
  defineTool({
9484
9524
  name: "canonry_search",
9485
9525
  title: "Search project (composite)",
@@ -11405,7 +11445,7 @@ var canonryMcpTools = [
11405
11445
  defineTool({
11406
11446
  name: "canonry_technical_aeo_run",
11407
11447
  title: "Run Technical AEO site audit",
11408
- description: "Start a site-audit run. The run discovers root, sitemap, and linked pages. Its unattended default is 1,000 pages and 100,000 link observations; callers may explicitly raise either to its hard maximum. It returns {runId, status} and continues in the background. If an active run has identical effective options, this tool returns it; different options are refused. Poll canonry_run_get, then read the crawl and score tools.",
11448
+ description: "Start a site-audit run. The run discovers root, sitemap, and linked pages. It defaults to 1,000 pages, and the engine derives the link-observation budget from that page count with a 100,000 floor, so raising maxPages raises it too. An explicit maxEdges is a ceiling that replaces the derivation and can sit below it. It returns {runId, status} and continues in the background. If an active run has identical effective options, this tool returns it; different options are refused. Poll canonry_run_get, then read the crawl and score tools.",
11409
11449
  access: "write",
11410
11450
  tier: "monitoring",
11411
11451
  inputSchema: technicalAeoRunInputSchema,
@@ -6723,6 +6723,22 @@ var measurementDraftUpsertCompetitorRequestSchema = z15.object({
6723
6723
  groupKey: measurementV2StableKeySchema,
6724
6724
  competitor: measurementDraftCompetitorSchema
6725
6725
  }).strict();
6726
+ var measurementDraftPinCompetitorRequestSchema = z15.object({
6727
+ expectedActiveRevision: measurementDraftRevisionSchema,
6728
+ groupKey: measurementV2StableKeySchema,
6729
+ domain: z15.string().trim().min(1).refine((value) => hostOf(value) !== null, "A competitor domain must be a valid hostname"),
6730
+ label: z15.string().trim().min(1).optional(),
6731
+ aliases: z15.array(z15.string().trim().min(1)).optional()
6732
+ }).strict();
6733
+ var measurementDraftPinCompetitorResponseSchema = measurementDraftMutationResponseSchema.extend({
6734
+ groupKey: measurementV2StableKeySchema,
6735
+ competitor: measurementDraftCompetitorSchema,
6736
+ draftCreated: z15.boolean(),
6737
+ published: z15.object({
6738
+ revision: measurementDraftRevisionSchema,
6739
+ competitorsChanged: z15.literal(false)
6740
+ }).strict()
6741
+ }).strict();
6726
6742
  var measurementDraftRemoveCompetitorRequestSchema = z15.object({
6727
6743
  groupKey: measurementV2StableKeySchema,
6728
6744
  competitorKey: measurementV2StableKeySchema
@@ -10627,7 +10643,6 @@ var siteCrawlDeadLinksResponseSchema = z30.discriminatedUnion("state", [
10627
10643
  ]);
10628
10644
  var SITE_AUDIT_DEFAULT_PAGE_LIMIT = 1e3;
10629
10645
  var SITE_AUDIT_MAX_PAGE_LIMIT = 5e4;
10630
- var SITE_AUDIT_DEFAULT_EDGE_LIMIT = 1e5;
10631
10646
  var SITE_AUDIT_MAX_EDGE_LIMIT = 1e6;
10632
10647
  var siteAuditRunRequestSchema = z30.object({
10633
10648
  /** @deprecated Prefer the configured project sitemap; retained for compatibility. */
@@ -10636,7 +10651,7 @@ var siteAuditRunRequestSchema = z30.object({
10636
10651
  limit: z30.number().int().positive().max(2e3).optional(),
10637
10652
  /** Crawl page budget. When omitted, Canonry crawls up to 1,000 pages. */
10638
10653
  maxPages: z30.number().int().positive().max(5e4).optional(),
10639
- /** Internal-link observation budget. When omitted, Canonry retains up to 100,000 edges. */
10654
+ /** Internal-link observation budget. When omitted, the crawl engine derives it from the page count. */
10640
10655
  maxEdges: z30.number().int().positive().max(1e6).optional(),
10641
10656
  /** Maximum crawl depth from the root. */
10642
10657
  maxDepth: z30.number().int().min(0).max(100).optional(),
@@ -10645,10 +10660,10 @@ var siteAuditRunRequestSchema = z30.object({
10645
10660
  });
10646
10661
  function normalizeSiteAuditRunRequest(request) {
10647
10662
  return {
10648
- schemaVersion: 1,
10663
+ schemaVersion: 2,
10649
10664
  sitemapUrl: request.sitemapUrl ? new URL(request.sitemapUrl).toString() : null,
10650
10665
  maxPages: request.maxPages ?? request.limit ?? SITE_AUDIT_DEFAULT_PAGE_LIMIT,
10651
- maxEdges: request.maxEdges ?? SITE_AUDIT_DEFAULT_EDGE_LIMIT,
10666
+ maxEdges: request.maxEdges ?? null,
10652
10667
  maxDepth: request.maxDepth ?? null,
10653
10668
  checkDeadLinks: request.checkDeadLinks === true
10654
10669
  };
@@ -11835,6 +11850,143 @@ var sourceBreakdownDtoSchema = z35.object({
11835
11850
  /** Applied ranked-list limit; null when the full list is returned. */
11836
11851
  limit: z35.number().int().nullable()
11837
11852
  });
11853
+ var competitorLandscapeSurfaceClassSchema = z35.enum([
11854
+ "own",
11855
+ "direct-competitor",
11856
+ "ota-aggregator",
11857
+ "editorial-media",
11858
+ "other",
11859
+ "unknown"
11860
+ ]);
11861
+ var competitorLandscapeScopeSchema = z35.discriminatedUnion("kind", [
11862
+ z35.object({ kind: z35.literal("project") }).strict(),
11863
+ z35.object({ kind: z35.literal("group"), groupKey: z35.string().trim().min(1) }).strict(),
11864
+ z35.object({ kind: z35.literal("all-markets") }).strict()
11865
+ ]);
11866
+ var competitorLandscapeQueryClassSchema = z35.enum(["all", "branded", "non-brand"]);
11867
+ var competitorLandscapeModeSchema = z35.enum(["project", "all-markets"]);
11868
+ var competitorLandscapeQuerySchema = z35.object({
11869
+ window: metricsWindowSchema.optional(),
11870
+ groupKey: z35.string().trim().min(1).optional(),
11871
+ scope: competitorLandscapeModeSchema.optional(),
11872
+ provider: z35.string().trim().min(1).optional(),
11873
+ /** Exact requested model ID; provider is required to disambiguate model names. */
11874
+ model: modelIdSchema.optional(),
11875
+ /** Opt-in agent comparison; the default landscape remains pooled. */
11876
+ groupBy: z35.literal("model").optional(),
11877
+ queryClass: competitorLandscapeQueryClassSchema.optional(),
11878
+ location: z35.string().trim().min(1).optional(),
11879
+ runId: z35.string().trim().min(1).optional()
11880
+ }).strict().superRefine((value, context) => {
11881
+ if (value.scope === "all-markets" && value.groupKey !== void 0) {
11882
+ context.addIssue({
11883
+ code: "custom",
11884
+ path: ["groupKey"],
11885
+ message: "groupKey cannot be combined with scope=all-markets"
11886
+ });
11887
+ }
11888
+ if (value.model !== void 0 && value.provider === void 0) {
11889
+ context.addIssue({
11890
+ code: "custom",
11891
+ path: ["provider"],
11892
+ message: "provider is required when filtering by model"
11893
+ });
11894
+ }
11895
+ });
11896
+ var competitorLandscapeRowSchema = z35.object({
11897
+ domain: z35.string().trim().min(1),
11898
+ label: z35.string().trim().min(1),
11899
+ surfaceClass: competitorLandscapeSurfaceClassSchema,
11900
+ pinned: z35.boolean(),
11901
+ /** One answer-text match at most per result. */
11902
+ mentionCount: z35.number().int().nonnegative(),
11903
+ /**
11904
+ * Percentage points (0..100). Null outside the competitive denominator, and
11905
+ * null for any selection that pools query classes: a brand wins its own
11906
+ * branded queries by definition, so a pooled ratio flatters the project and
11907
+ * buries every competitor. Counts stay populated either way.
11908
+ */
11909
+ shareOfVoice: z35.number().min(0).max(100).nullable(),
11910
+ /** One source-list credit at most per result. Independent of mentions. */
11911
+ citationCount: z35.number().int().nonnegative(),
11912
+ /** Answer-text result count behind the mention field. */
11913
+ answeredResults: z35.number().int().nonnegative(),
11914
+ firstSeenAt: z35.string().datetime().nullable(),
11915
+ lastSeenAt: z35.string().datetime().nullable(),
11916
+ sampleUrls: z35.array(z35.string().url()).max(3)
11917
+ }).strict();
11918
+ var competitorLandscapeEvidenceSchema = z35.object({
11919
+ answeredResults: z35.number().int().nonnegative(),
11920
+ sourceResults: z35.number().int().nonnegative(),
11921
+ missingAnswerTextResults: z35.number().int().nonnegative(),
11922
+ /** Project plus direct-competitor named credits behind SOV. */
11923
+ mentionCredits: z35.number().int().nonnegative(),
11924
+ /** Citation captures that cannot prove a complete source list; never inferred as misses. */
11925
+ incompleteSourceResults: z35.number().int().nonnegative(),
11926
+ /** Stored probe snapshots intentionally omitted from every landscape metric. */
11927
+ excludedProbeResults: z35.number().int().nonnegative(),
11928
+ /** Stored snapshots from queued/running/failed/cancelled runs omitted from every metric. */
11929
+ excludedNonCompletedResults: z35.number().int().nonnegative()
11930
+ }).strict();
11931
+ var COMPETITOR_LANDSCAPE_MODEL_GROUP_LIMIT = 50;
11932
+ var competitorLandscapeModelGroupSchema = z35.object({
11933
+ provider: z35.string().trim().min(1),
11934
+ /** Null preserves historical observations with unknown requested model identity. */
11935
+ model: modelIdSchema.nullable(),
11936
+ /** Raw upstream-reported identity; never filled from the requested model or current settings. */
11937
+ servedModels: modelEvidenceStateSchema,
11938
+ snapshotCount: z35.number().int().positive(),
11939
+ project: competitorLandscapeRowSchema,
11940
+ pinned: z35.array(competitorLandscapeRowSchema),
11941
+ observed: z35.array(competitorLandscapeRowSchema),
11942
+ otherSources: z35.array(competitorLandscapeRowSchema),
11943
+ evidence: competitorLandscapeEvidenceSchema,
11944
+ /** Per-group ranked-row truncation; all pins and full-population denominators remain included. */
11945
+ truncated: z35.boolean()
11946
+ }).strict();
11947
+ var competitorLandscapeModelComparisonSchema = z35.object({
11948
+ basis: z35.literal("requested-model"),
11949
+ groups: z35.array(competitorLandscapeModelGroupSchema).max(COMPETITOR_LANDSCAPE_MODEL_GROUP_LIMIT),
11950
+ totalGroups: z35.number().int().nonnegative(),
11951
+ /** Groups sort by provider and requested model. Use provider/model filters to narrow a truncated result. */
11952
+ truncated: z35.boolean()
11953
+ }).strict();
11954
+ var competitorLandscapeResponseSchema = z35.object({
11955
+ window: metricsWindowSchema,
11956
+ scope: competitorLandscapeScopeSchema,
11957
+ project: competitorLandscapeRowSchema,
11958
+ /** Explicit/custom competitors, preserved even with zero observations. */
11959
+ pinned: z35.array(competitorLandscapeRowSchema),
11960
+ /** Stored-discovery direct competitors, ordered by historical mention share. */
11961
+ observed: z35.array(competitorLandscapeRowSchema),
11962
+ /** Aggregators, editorial, unknown, and other cited sources; never share-of-voice competitors. */
11963
+ otherSources: z35.array(competitorLandscapeRowSchema),
11964
+ evidence: competitorLandscapeEvidenceSchema,
11965
+ /** Present only when groupBy=model; pooled fields remain available and unchanged. */
11966
+ modelComparison: competitorLandscapeModelComparisonSchema.optional(),
11967
+ /** Present only for Advanced market reads; active metrics stay frozen while draft pins remain pending publish. */
11968
+ marketState: z35.object({
11969
+ activeRevision: z35.number().int().positive(),
11970
+ draft: z35.object({
11971
+ etag: z35.string().trim().min(1),
11972
+ /** Draft-only market identities included in `pinned` for this response. */
11973
+ pendingCompetitorDomains: z35.array(z35.string().trim().min(1))
11974
+ }).strict().nullable()
11975
+ }).strict().nullable(),
11976
+ /** Echoes all applied filters so a client never mistakes a scoped reading for a project-wide one. */
11977
+ filters: z35.object({
11978
+ scope: competitorLandscapeModeSchema,
11979
+ groupKey: z35.string().nullable(),
11980
+ provider: z35.string().nullable(),
11981
+ model: modelIdSchema.optional(),
11982
+ groupBy: z35.literal("model").optional(),
11983
+ queryClass: competitorLandscapeQueryClassSchema,
11984
+ location: z35.string().nullable(),
11985
+ runId: z35.string().nullable()
11986
+ }).strict(),
11987
+ /** True when ranked observed/source lists exceed the server cap; pinned rows are never dropped. */
11988
+ truncated: z35.boolean()
11989
+ }).strict();
11838
11990
  function parseWindow(value) {
11839
11991
  if (value === void 0 || value === "") return "all";
11840
11992
  const parsed = metricsWindowSchema.safeParse(value);
@@ -16589,6 +16741,7 @@ export {
16589
16741
  measurementChangesResponseSchema,
16590
16742
  measurementDataQualityQuerySchema,
16591
16743
  measurementDataQualityResponseSchema,
16744
+ measurementDraftCompetitorSchema,
16592
16745
  measurementDraftAuthoringSchema,
16593
16746
  MEASUREMENT_DRAFT_MAX_ASSIGNMENTS_PER_ACTION,
16594
16747
  MEASUREMENT_DRAFT_MAX_GROUPS,
@@ -16621,6 +16774,8 @@ export {
16621
16774
  measurementDraftUpsertGroupRequestSchema,
16622
16775
  measurementDraftRemoveGroupRequestSchema,
16623
16776
  measurementDraftUpsertCompetitorRequestSchema,
16777
+ measurementDraftPinCompetitorRequestSchema,
16778
+ measurementDraftPinCompetitorResponseSchema,
16624
16779
  measurementDraftRemoveCompetitorRequestSchema,
16625
16780
  measurementDraftCompilePreviewResponseSchema,
16626
16781
  measurementDraftDiffPreviewResponseSchema,
@@ -16920,6 +17075,9 @@ export {
16920
17075
  modelIdsEquivalent,
16921
17076
  brandMetricsDtoSchema,
16922
17077
  sourceBreakdownDtoSchema,
17078
+ competitorLandscapeQuerySchema,
17079
+ COMPETITOR_LANDSCAPE_MODEL_GROUP_LIMIT,
17080
+ competitorLandscapeResponseSchema,
16923
17081
  parseWindow,
16924
17082
  windowCutoff,
16925
17083
  resolveDateRange,
package/dist/cli.js CHANGED
@@ -28,11 +28,11 @@ import {
28
28
  trackCliCommandFinished,
29
29
  trackEvent,
30
30
  waitForServerRuntimeStartup
31
- } from "./chunk-GFVZOVG3.js";
31
+ } from "./chunk-OF3PKBW4.js";
32
32
  import {
33
33
  autoSyncSkills,
34
34
  formatAutoSyncNotice
35
- } from "./chunk-G7M2KM7Q.js";
35
+ } from "./chunk-RQSSKN5F.js";
36
36
  import {
37
37
  CliError,
38
38
  EXIT_SYSTEM_ERROR,
@@ -57,7 +57,7 @@ import {
57
57
  saveConfigPatch,
58
58
  systemError,
59
59
  usageError
60
- } from "./chunk-IM4HFNGF.js";
60
+ } from "./chunk-VFQPW2VX.js";
61
61
  import {
62
62
  CLOUDFLARE_WORKER_BINDINGS,
63
63
  CLOUDFLARE_WORKER_GENERATED_MARKER,
@@ -71,7 +71,7 @@ import {
71
71
  projects,
72
72
  queries,
73
73
  renderReportHtml
74
- } from "./chunk-ZJ2VGTYU.js";
74
+ } from "./chunk-ISONU3FA.js";
75
75
  import {
76
76
  AdsDeliverySnapshotStatuses,
77
77
  AdsHistoricalCampaignRollupStatuses,
@@ -140,7 +140,7 @@ import {
140
140
  providerQuotaPolicySchema,
141
141
  resolveProviderInput,
142
142
  winnabilityClassSchema
143
- } from "./chunk-GN2G7VFK.js";
143
+ } from "./chunk-Z2HTLTQV.js";
144
144
 
145
145
  // src/cli.ts
146
146
  import { pathToFileURL } from "url";
@@ -7569,8 +7569,95 @@ async function listCompetitors(project, format) {
7569
7569
  console.log(` ${c.domain}`);
7570
7570
  }
7571
7571
  }
7572
+ async function showCompetitorLandscape(project, options) {
7573
+ const client = getClient10();
7574
+ const data = await client.getCompetitorLandscape(project, {
7575
+ window: options.window,
7576
+ scope: options.scope,
7577
+ groupKey: options.groupKey,
7578
+ provider: options.provider,
7579
+ ...options.groupBy ? { groupBy: options.groupBy } : {},
7580
+ ...options.model !== void 0 ? { model: options.model } : {},
7581
+ queryClass: options.queryClass,
7582
+ location: options.location,
7583
+ runId: options.runId
7584
+ });
7585
+ if (options.format === "json") {
7586
+ console.log(JSON.stringify(data, null, 2));
7587
+ return;
7588
+ }
7589
+ if (options.format === "jsonl") {
7590
+ console.log(JSON.stringify(data));
7591
+ return;
7592
+ }
7593
+ printCompetitorLandscape(data);
7594
+ }
7595
+ function printCompetitorLandscape(data) {
7596
+ const scope = data.scope.kind === "group" ? `market ${data.scope.groupKey}` : data.scope.kind === "all-markets" ? "all markets" : "project";
7597
+ console.log(`Competitor landscape \xB7 ${scope} \xB7 ${data.window}`);
7598
+ if (data.filters.model !== void 0) {
7599
+ console.log(`Requested model filter: ${data.filters.provider} \xB7 ${data.filters.model}`);
7600
+ }
7601
+ console.log("Mention share is answer-text evidence; citations are independent source-list evidence.");
7602
+ console.log(
7603
+ data.filters.queryClass === "all" ? "Share of voice is blank: branded and non-brand queries are pooled here. Add --query-class non-brand for a ratio." : `Counting ${data.filters.queryClass} queries only.`
7604
+ );
7605
+ if (data.truncated) console.log("Top 100 observed competitors and top 100 other cited sources shown; pinned competitors are complete.");
7606
+ console.log("");
7607
+ printLandscapeRows("Your brand", [data.project]);
7608
+ printLandscapeRows("Pinned competitors", data.pinned);
7609
+ printLandscapeRows("Observed competitors", data.observed);
7610
+ printLandscapeRows("Other cited sources", data.otherSources);
7611
+ console.log("");
7612
+ console.log(
7613
+ `Evidence: ${data.evidence.answeredResults} answer-text result(s), ${data.evidence.sourceResults} source result(s); excluded: ${data.evidence.excludedProbeResults} probe, ${data.evidence.excludedNonCompletedResults} non-completed.`
7614
+ );
7615
+ if (data.modelComparison) printModelComparison(data.modelComparison);
7616
+ }
7617
+ function describeServedModels(evidence) {
7618
+ if (evidence.status === "unknown") return "Unknown (not disclosed)";
7619
+ if (evidence.status === "known") return evidence.model;
7620
+ return [...evidence.models, ...evidence.includesUnknown ? ["Unknown (not disclosed)"] : []].join(", ");
7621
+ }
7622
+ function printModelComparison(comparison) {
7623
+ console.log("");
7624
+ console.log(`Model comparison \xB7 requested-model basis \xB7 ${comparison.groups.length} of ${comparison.totalGroups} groups`);
7625
+ console.log("Groups use stored observations. They do not form a matched-query or equal-weight comparison.");
7626
+ if (comparison.truncated) console.log("The first 50 provider/model groups are shown. Additional groups are omitted.");
7627
+ for (const group of comparison.groups) {
7628
+ console.log("");
7629
+ console.log(`${group.provider} \xB7 requested model: ${group.model ?? "Unknown (not recorded)"}`);
7630
+ console.log(`Served model evidence: ${describeServedModels(group.servedModels)}`);
7631
+ console.log(`Samples: ${group.snapshotCount} snapshot(s), ${group.evidence.answeredResults} answer-text result(s), ${group.evidence.sourceResults} source result(s).`);
7632
+ printLandscapeRows("Your brand", [group.project], true);
7633
+ printLandscapeRows("Pinned competitors", group.pinned, true);
7634
+ printLandscapeRows("Observed competitors", group.observed, true);
7635
+ printLandscapeRows("Other cited sources", group.otherSources, true);
7636
+ if (group.truncated) console.log("Top 100 observed competitors and top 100 other sources shown. Pinned competitors are complete.");
7637
+ }
7638
+ }
7639
+ function printLandscapeRows(heading, rows, showSampleCount = false) {
7640
+ console.log(`${heading}:`);
7641
+ if (rows.length === 0) {
7642
+ console.log(" \u2014");
7643
+ return;
7644
+ }
7645
+ for (const row of rows) {
7646
+ const sov = row.shareOfVoice === null ? "\u2014" : `${row.shareOfVoice.toFixed(1)}%`;
7647
+ console.log(` ${row.domain} mention ${row.mentionCount} \xB7 citation ${row.citationCount} \xB7 SOV ${sov}${showSampleCount ? ` \xB7 answers ${row.answeredResults}` : ""}`);
7648
+ }
7649
+ }
7572
7650
 
7573
7651
  // src/cli-commands/competitor.ts
7652
+ 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]";
7653
+ function parseLandscapeScope(value) {
7654
+ if (value === void 0 || value === "") return void 0;
7655
+ if (value !== "all-markets") throw usageError(`--scope must be "all-markets" (got "${value}")`, {
7656
+ message: '--scope must be "all-markets"',
7657
+ details: { command: "competitor.landscape", usage: LANDSCAPE_USAGE }
7658
+ });
7659
+ return value;
7660
+ }
7574
7661
  var COMPETITOR_CLI_COMMANDS = [
7575
7662
  {
7576
7663
  path: ["competitor", "add"],
@@ -7634,14 +7721,60 @@ var COMPETITOR_CLI_COMMANDS = [
7634
7721
  await listCompetitors(project, input.format);
7635
7722
  }
7636
7723
  },
7724
+ {
7725
+ path: ["competitor", "landscape"],
7726
+ usage: LANDSCAPE_USAGE,
7727
+ options: {
7728
+ window: stringOption(),
7729
+ "group-key": stringOption(),
7730
+ scope: stringOption(),
7731
+ provider: stringOption(),
7732
+ "by-model": { type: "boolean" },
7733
+ model: stringOption(),
7734
+ "query-class": stringOption(),
7735
+ location: stringOption(),
7736
+ "run-id": stringOption()
7737
+ },
7738
+ run: async (input) => {
7739
+ const project = requireProject(input, "competitor.landscape", LANDSCAPE_USAGE);
7740
+ const scope = parseLandscapeScope(getString(input.values, "scope"));
7741
+ const groupKey = getString(input.values, "group-key");
7742
+ const provider = getString(input.values, "provider");
7743
+ const model = getString(input.values, "model");
7744
+ if (model !== void 0 && !provider?.trim()) {
7745
+ throw usageError("--model requires --provider", {
7746
+ message: "--model requires --provider",
7747
+ details: { command: "competitor.landscape", usage: LANDSCAPE_USAGE }
7748
+ });
7749
+ }
7750
+ if (scope === "all-markets" && groupKey) {
7751
+ throw usageError("--group-key cannot be combined with --scope all-markets", {
7752
+ message: "--group-key cannot be combined with --scope all-markets",
7753
+ details: { command: "competitor.landscape", usage: LANDSCAPE_USAGE }
7754
+ });
7755
+ }
7756
+ await showCompetitorLandscape(project, {
7757
+ window: getString(input.values, "window"),
7758
+ groupKey,
7759
+ scope,
7760
+ provider,
7761
+ ...getBoolean(input.values, "by-model") ? { groupBy: "model" } : {},
7762
+ ...model !== void 0 ? { model } : {},
7763
+ queryClass: getString(input.values, "query-class"),
7764
+ location: getString(input.values, "location"),
7765
+ runId: getString(input.values, "run-id"),
7766
+ format: input.format
7767
+ });
7768
+ }
7769
+ },
7637
7770
  {
7638
7771
  path: ["competitor"],
7639
- usage: "canonry competitor <add|remove|delete|list> <project> [args]",
7772
+ usage: "canonry competitor <add|remove|delete|list|landscape> <project> [args]",
7640
7773
  run: async (input) => {
7641
7774
  unknownSubcommand(input.positionals[0], {
7642
7775
  command: "competitor",
7643
- usage: "canonry competitor <add|remove|delete|list> <project> [args]",
7644
- available: ["add", "remove", "delete", "list"]
7776
+ usage: "canonry competitor <add|remove|delete|list|landscape> <project> [args]",
7777
+ available: ["add", "remove", "delete", "list", "landscape"]
7645
7778
  });
7646
7779
  }
7647
7780
  }