@canonry/canonry 5.33.0 → 6.0.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 (40) hide show
  1. package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +15 -3
  2. package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +1 -1
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +16 -11
  4. package/assets/assets/{AuditHistoryPanel-CcCIqJzU.js → AuditHistoryPanel-Bz2NVB6-.js} +1 -1
  5. package/assets/assets/{BacklinksPage-B4r9SLIP.js → BacklinksPage-Dte5-usf.js} +1 -1
  6. package/assets/assets/{HistoryPage-DIpbpqWc.js → HistoryPage-C1S4nw3I.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-BF93li8O.js → MeasurementPropertyPage-DWjsHZdt.js} +1 -1
  8. package/assets/assets/ProjectPage-CAJ9XvNt.js +8 -0
  9. package/assets/assets/RunsPage-D9wEJFfV.js +1 -0
  10. package/assets/assets/SettingsPage-CN3Gew7Y.js +1 -0
  11. package/assets/assets/SiteHealthSection-Bft5TfoU.js +4 -0
  12. package/assets/assets/TrafficPage-tkNvySQH.js +40 -0
  13. package/assets/assets/{TrafficSourceDetailPage-psddSEHZ.js → TrafficSourceDetailPage-RhRfPizo.js} +1 -1
  14. package/assets/assets/{extract-error-message-BbHnZTOx.js → extract-error-message-tfl_SU7q.js} +1 -1
  15. package/assets/assets/index-B11OXdEC.css +1 -0
  16. package/assets/assets/index-Cu_xgdb1.js +86 -0
  17. package/assets/assets/{react-sigma_core.esm.min-6EgecOc3.js → react-sigma_core.esm.min-BUu3mcQJ.js} +1 -1
  18. package/assets/assets/{v2-overview-adapter-0pVxmjG8.js → v2-overview-adapter-CzeGhaXn.js} +1 -1
  19. package/assets/index.html +2 -2
  20. package/dist/{chunk-UDEZZMV5.js → chunk-4NC5ZAC2.js} +3 -3
  21. package/dist/{chunk-AQFRQTKY.js → chunk-6M7HSNXJ.js} +2 -2
  22. package/dist/{chunk-XICZEXE5.js → chunk-F4G7I7T7.js} +23 -135
  23. package/dist/{chunk-SJX7XJB6.js → chunk-HAALQW4R.js} +121 -18
  24. package/dist/{chunk-Y23FQK6O.js → chunk-K4356AGP.js} +10 -10
  25. package/dist/{chunk-G3CMWO26.js → chunk-XGERCNUR.js} +6 -3
  26. package/dist/{chunk-GIKWL2BQ.js → chunk-ZO62Z2WN.js} +137 -21
  27. package/dist/cli.js +52 -16
  28. package/dist/{demo-server-G4BFXA6G.js → demo-server-54OUTZMY.js} +4 -4
  29. package/dist/index.js +6 -6
  30. package/dist/{intelligence-service-FVOFIJP3.js → intelligence-service-MEIAMMLT.js} +2 -2
  31. package/dist/mcp.js +4 -4
  32. package/package.json +14 -14
  33. package/assets/assets/ProjectPage-CjadJKsG.js +0 -8
  34. package/assets/assets/RunRow-C-d8luIA.js +0 -1
  35. package/assets/assets/RunsPage-DEbjaNYY.js +0 -1
  36. package/assets/assets/SettingsPage-B5h3wM7i.js +0 -1
  37. package/assets/assets/SiteHealthSection-Cj2lo1GY.js +0 -4
  38. package/assets/assets/TrafficPage-CFDh9Rx4.js +0 -40
  39. package/assets/assets/index-CiWV331j.js +0 -86
  40. package/assets/assets/index-DX5M4MHr.css +0 -1
@@ -3455,6 +3455,9 @@ function brandLabelFromDomain(value) {
3455
3455
  if (value == null || value.trim() === "") return "";
3456
3456
  return getDomainWithoutSuffix2(value, DOMAIN_PARSE_OPTIONS)?.toLowerCase() ?? "";
3457
3457
  }
3458
+ function competitorLabelFromDomain(domain) {
3459
+ return brandLabelFromDomain(domain) || domain;
3460
+ }
3458
3461
  function hostMatchesDomain(candidate, domain) {
3459
3462
  const candidateHost = hostOf(candidate);
3460
3463
  const domainHost = hostOf(domain);
@@ -4126,6 +4129,8 @@ function resolveLocations(projectLocations, requestedLabels) {
4126
4129
  }
4127
4130
  return resolved;
4128
4131
  }
4132
+ var PROJECT_QUALIFIED_ALIAS_LIMIT = 20;
4133
+ var PROJECT_QUALIFIED_ALIAS_MAX_LENGTH = 200;
4129
4134
  var projectUpsertRequestSchema = z7.object({
4130
4135
  displayName: z7.string().min(1),
4131
4136
  canonicalDomain: z7.string().min(1),
@@ -4146,6 +4151,12 @@ var projectUpsertRequestSchema = z7.object({
4146
4151
  defaultLocation: z7.string().nullable().optional(),
4147
4152
  measurement: measurementConfigSchema.optional(),
4148
4153
  autoExtractBacklinks: z7.boolean().optional(),
4154
+ /**
4155
+ * Aliases the sentiment evaluator is told are this brand's own names. Each
4156
+ * entry must already be one of `aliases`. Omitted keeps the stored list,
4157
+ * minus names that no longer qualify; `[]` clears.
4158
+ */
4159
+ qualifiedAliases: z7.array(z7.string().trim().min(1).max(PROJECT_QUALIFIED_ALIAS_MAX_LENGTH)).max(PROJECT_QUALIFIED_ALIAS_LIMIT).optional(),
4149
4160
  /**
4150
4161
  * Highest star rating that counts as a negative Google review for this
4151
4162
  * project's `review.negative` webhook (1-4). Omit to keep the stored value;
@@ -4167,6 +4178,8 @@ var projectDtoSchema = z7.object({
4167
4178
  canonicalDomain: z7.string(),
4168
4179
  ownedDomains: z7.array(z7.string()).default([]),
4169
4180
  aliases: z7.array(z7.string()).default([]),
4181
+ /** Operator-chosen subset of `aliases` that Simple sentiment treats as this brand's own names. */
4182
+ qualifiedAliases: z7.array(z7.string()).default([]),
4170
4183
  country: z7.string().length(2),
4171
4184
  language: z7.string().min(2),
4172
4185
  tags: z7.array(z7.string()).default([]),
@@ -4259,6 +4272,40 @@ function normalizeProjectAliases(displayName, aliases) {
4259
4272
  }
4260
4273
  return result;
4261
4274
  }
4275
+ function resolveProjectQualifiedAliases(identity, requested, competitorNames = []) {
4276
+ const stored = /* @__PURE__ */ new Map();
4277
+ for (const alias of normalizeProjectAliases(identity.displayName, identity.aliases)) {
4278
+ stored.set(alias.toLowerCase(), alias);
4279
+ }
4280
+ const displayKey = brandKeyFromText(identity.displayName ?? "");
4281
+ const competitorKeys = new Set(competitorNames.map(brandKeyFromText).filter((key) => key.length > 0));
4282
+ const value = [];
4283
+ const rejected = [];
4284
+ const seen = /* @__PURE__ */ new Set();
4285
+ for (const raw of requested ?? []) {
4286
+ const name = raw.trim();
4287
+ if (!name) continue;
4288
+ const lower = name.toLowerCase();
4289
+ if (seen.has(lower)) continue;
4290
+ seen.add(lower);
4291
+ const alias = stored.get(lower);
4292
+ if (alias === void 0) {
4293
+ rejected.push({ name, reason: "not-an-alias" });
4294
+ continue;
4295
+ }
4296
+ const key = brandKeyFromText(alias);
4297
+ if (displayKey && key === displayKey) {
4298
+ rejected.push({ name, reason: "display-name" });
4299
+ } else if (key.length < MIN_DOMAIN_BRAND_KEY_LENGTH) {
4300
+ rejected.push({ name, reason: "too-short" });
4301
+ } else if (competitorKeys.has(key)) {
4302
+ rejected.push({ name, reason: "competitor-collision" });
4303
+ } else {
4304
+ value.push(alias);
4305
+ }
4306
+ }
4307
+ return { value: value.sort(), rejected };
4308
+ }
4262
4309
  function effectiveBrandNames(project) {
4263
4310
  const names = [];
4264
4311
  const seen = /* @__PURE__ */ new Set();
@@ -6558,6 +6605,10 @@ var configSpecSchema = z16.object({
6558
6605
  canonicalDomain: z16.string().min(1),
6559
6606
  ownedDomains: z16.array(z16.string().min(1)).optional().default([]),
6560
6607
  aliases: z16.array(z16.string().min(1)).optional().default([]),
6608
+ // No default on purpose: an apply that omits it keeps the project's stored
6609
+ // list, minus names that no longer qualify against this spec's `aliases`,
6610
+ // display name and competitors. Each entry must be an alias.
6611
+ qualifiedAliases: z16.array(z16.string().min(1).max(PROJECT_QUALIFIED_ALIAS_MAX_LENGTH)).max(PROJECT_QUALIFIED_ALIAS_LIMIT).optional(),
6561
6612
  country: z16.string().length(2),
6562
6613
  language: z16.string().min(2),
6563
6614
  queries: configQueryListSchema.optional(),
@@ -6623,7 +6674,12 @@ var simpleMeasurementIdentitySchema = z17.object({
6623
6674
  displayName: z17.string(),
6624
6675
  aliases: z17.array(z17.string()),
6625
6676
  canonicalDomain: z17.string(),
6626
- ownedDomains: z17.array(z17.string())
6677
+ ownedDomains: z17.array(z17.string()),
6678
+ // Sentiment-only: the aliases the evaluator is told are this brand's own
6679
+ // names. Omitted when empty, so a sidecar that never opted in keeps its
6680
+ // checksum. No bounds or matcher rules here: a later change to the write
6681
+ // rules must never make a stored sidecar unreadable.
6682
+ qualifiedAliases: z17.array(z17.string()).optional()
6627
6683
  }).strict();
6628
6684
  var simpleMeasurementEngineSchema = z17.object({
6629
6685
  provider: providerNameSchema.refine((value) => value.trim().length > 0, {
@@ -6711,9 +6767,17 @@ var simpleMeasurementDefinitionSchema = z17.object({
6711
6767
  competitors: z17.array(simpleMeasurementCompetitorSchema).optional(),
6712
6768
  queries: z17.array(simpleMeasurementQuerySchema)
6713
6769
  }).strict().superRefine(addCollectionIssues);
6770
+ function simpleMeasurementCompetitorNames(competitors) {
6771
+ return competitors?.flatMap((competitor) => [competitor.label, ...competitor.aliases]) ?? [];
6772
+ }
6714
6773
  function buildSimpleMeasurementDefinition(input) {
6715
6774
  const parsed = simpleMeasurementDefinitionInputSchema.parse(input);
6716
6775
  const classifier = compileQueryClassifier(effectiveBrandNames(parsed.identity));
6776
+ const qualifiedAliases = resolveProjectQualifiedAliases(
6777
+ { displayName: parsed.identity.displayName, aliases: parsed.identity.aliases },
6778
+ parsed.identity.qualifiedAliases,
6779
+ simpleMeasurementCompetitorNames(parsed.competitors)
6780
+ ).value;
6717
6781
  return simpleMeasurementDefinitionSchema.parse({
6718
6782
  schemaVersion: SIMPLE_MEASUREMENT_DEFINITION_SCHEMA_VERSION,
6719
6783
  capturedAt: parsed.capturedAt,
@@ -6721,7 +6785,8 @@ function buildSimpleMeasurementDefinition(input) {
6721
6785
  displayName: parsed.identity.displayName,
6722
6786
  aliases: [...parsed.identity.aliases],
6723
6787
  canonicalDomain: parsed.identity.canonicalDomain,
6724
- ownedDomains: [...parsed.identity.ownedDomains]
6788
+ ownedDomains: [...parsed.identity.ownedDomains],
6789
+ ...qualifiedAliases.length ? { qualifiedAliases } : {}
6725
6790
  },
6726
6791
  country: parsed.country,
6727
6792
  language: parsed.language,
@@ -6765,7 +6830,9 @@ function canonicalSimpleMeasurementDefinitionJson(definition) {
6765
6830
  displayName: parsed.identity.displayName,
6766
6831
  aliases: [...parsed.identity.aliases].sort(compareText3),
6767
6832
  canonicalDomain: parsed.identity.canonicalDomain,
6768
- ownedDomains: [...parsed.identity.ownedDomains].sort(compareText3)
6833
+ ownedDomains: [...parsed.identity.ownedDomains].sort(compareText3),
6834
+ // Absent and empty serialize alike, so pre-feature checksums never move.
6835
+ ...parsed.identity.qualifiedAliases?.length ? { qualifiedAliases: [...parsed.identity.qualifiedAliases].sort(compareText3) } : {}
6769
6836
  },
6770
6837
  country: parsed.country,
6771
6838
  language: parsed.language,
@@ -7995,8 +8062,16 @@ var measurementPortfolioAnswerSourcesSchema = z22.object({
7995
8062
  properties: measurementDemoCountSchema,
7996
8063
  answers: measurementDemoCountSchema,
7997
8064
  domains: z22.array(measurementPortfolioCountedDomainSchema).max(MEASUREMENT_PORTFOLIO_ANSWER_SOURCES_LIMIT),
7998
- domainTotal: measurementDemoCountSchema
8065
+ domainTotal: measurementDemoCountSchema,
8066
+ /**
8067
+ * Answers in the basis that cite any of the project's own domains, each
8068
+ * answer once. Optional only so an older server's response still parses.
8069
+ */
8070
+ ownDomainAnswers: measurementDemoCountSchema.optional()
7999
8071
  }).strict().superRefine((sources, ctx) => {
8072
+ if (sources.ownDomainAnswers !== void 0 && sources.ownDomainAnswers > sources.answers) {
8073
+ ctx.addIssue({ code: z22.ZodIssueCode.custom, path: ["ownDomainAnswers"], message: "Own-domain answers cannot exceed the basis" });
8074
+ }
8000
8075
  if (sources.domains.length > sources.domainTotal) {
8001
8076
  ctx.addIssue({ code: z22.ZodIssueCode.custom, path: ["domainTotal"], message: "Total cannot be smaller than the returned domains" });
8002
8077
  }
@@ -8347,7 +8422,14 @@ var measurementChangedPropertySchema = measurementDemoPropertySchema.extend({
8347
8422
  * whenever the denominator held. False when a metric was measured in one
8348
8423
  * run only.
8349
8424
  */
8350
- withinNoise: z22.boolean().optional()
8425
+ withinNoise: z22.boolean().optional(),
8426
+ /**
8427
+ * The same noise rule for each signal alone: a Property beyond noise overall
8428
+ * can still have one signal that moved two answers or fewer, which is not a
8429
+ * gain or loss on its own. Null when that signal cannot be sized.
8430
+ */
8431
+ mentionWithinNoise: z22.boolean().nullable().optional(),
8432
+ citationWithinNoise: z22.boolean().nullable().optional()
8351
8433
  }).strict();
8352
8434
  var measurementChangesResponseSchema = z22.object({
8353
8435
  current: measurementComparableRunSchema,
@@ -9195,6 +9277,14 @@ function formatDate(iso) {
9195
9277
  return iso;
9196
9278
  }
9197
9279
  }
9280
+ var MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
9281
+ function formatCalendarDay(day, now = /* @__PURE__ */ new Date()) {
9282
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(day);
9283
+ const month = match ? MONTH_NAMES[Number(match[2]) - 1] : void 0;
9284
+ if (!match || !month) return day;
9285
+ const label = `${month} ${Number(match[3])}`;
9286
+ return Number(match[1]) === now.getFullYear() ? label : `${label}, ${match[1]}`;
9287
+ }
9198
9288
  function formatIsoDate(iso) {
9199
9289
  if (!iso) return "\u2014";
9200
9290
  try {
@@ -9573,8 +9663,8 @@ function dayOf(value) {
9573
9663
  function sortedUnique(days) {
9574
9664
  return [...new Set(days.filter((d) => d !== null))].sort();
9575
9665
  }
9576
- function normalizeEngineChange(provider, entry) {
9577
- const engine = answerEngineName(provider);
9666
+ function normalizeEngineChange(provider, entry, engineName) {
9667
+ const engine = engineName(provider);
9578
9668
  const perChange = Array.isArray(entry.changes) ? entry.changes : [];
9579
9669
  const changeCount = countOf(entry.changeCount) || perChange.length;
9580
9670
  const unconfirmedCount = Math.min(
@@ -9610,8 +9700,8 @@ function joinNames(names) {
9610
9700
  }
9611
9701
  function whenPhrase(dates, count2) {
9612
9702
  if (dates.length === 0) return null;
9613
- if (dates.length === 1 && count2 <= 1) return `on ${dates[0]}`;
9614
- return `more than once between ${dates[0]} and ${dates[dates.length - 1]}`;
9703
+ if (dates.length === 1 && count2 <= 1) return `on ${formatCalendarDay(dates[0])}`;
9704
+ return `more than once between ${formatCalendarDay(dates[0])} and ${formatCalendarDay(dates[dates.length - 1])}`;
9615
9705
  }
9616
9706
  function engineClause(change) {
9617
9707
  const confirmedWhen = whenPhrase(change.confirmedDates, change.confirmedCount);
@@ -9633,10 +9723,11 @@ function uncheckedTailSentence(entries) {
9633
9723
  const unchecked = entries.filter((e) => !e.checkedThroughPeriodEnd && e.knownGoodAsOf !== null);
9634
9724
  if (unchecked.length === 0) return null;
9635
9725
  const oldest = unchecked.map((e) => e.knownGoodAsOf).sort()[0];
9636
- return `We last checked for model updates on ${oldest}, and this period runs past that date, so there may be later updates we do not know about.`;
9726
+ return `We last checked for model updates on ${formatCalendarDay(oldest)}, and this period runs past that date, so there may be later updates we do not know about.`;
9637
9727
  }
9638
- function buildModelChangeNotice(disclosures) {
9639
- const entries = Object.entries(disclosures ?? {}).map(([provider, entry]) => normalizeEngineChange(provider, entry ?? {})).sort((a, b) => a.engine.localeCompare(b.engine));
9728
+ function buildModelChangeNotice(disclosures, options = {}) {
9729
+ const engineName = options.engineName ?? answerEngineName;
9730
+ const entries = Object.entries(disclosures ?? {}).map(([provider, entry]) => normalizeEngineChange(provider, entry ?? {}, engineName)).sort((a, b) => a.engine.localeCompare(b.engine));
9640
9731
  if (entries.length === 0) return null;
9641
9732
  const changed = entries.filter((e) => e.confirmedCount + e.unconfirmedCount > 0);
9642
9733
  if (changed.length === 0) {
@@ -9644,7 +9735,7 @@ function buildModelChangeNotice(disclosures) {
9644
9735
  const plural = engines.length > 1;
9645
9736
  const tail2 = uncheckedTailSentence(entries);
9646
9737
  const checked = [...new Set(entries.map((e) => e.knownGoodAsOf).filter((d) => d !== null))].sort();
9647
- const freshness = tail2 ?? (checked.length > 0 ? `We last checked for model updates on ${checked[0]}.` : null);
9738
+ const freshness = tail2 ?? (checked.length > 0 ? `We last checked for model updates on ${formatCalendarDay(checked[0])}.` : null);
9648
9739
  return {
9649
9740
  kind: "no-known-change",
9650
9741
  text: `No model updates are on record for ${joinNames(engines)} in this period.`,
@@ -11653,8 +11744,7 @@ var ProviderIds = {
11653
11744
  muse: "muse",
11654
11745
  local: "local",
11655
11746
  cdpChatgpt: "cdp:chatgpt",
11656
- zai: "zai",
11657
- deepinfra: "deepinfra"
11747
+ zai: "zai"
11658
11748
  };
11659
11749
  var PROVIDER_IDS = Object.values(ProviderIds);
11660
11750
  var SweepProviderIds = {
@@ -11671,13 +11761,16 @@ var AgentProviderIds = {
11671
11761
  claude: ProviderIds.claude,
11672
11762
  openai: ProviderIds.openai,
11673
11763
  gemini: ProviderIds.gemini,
11674
- zai: ProviderIds.zai,
11675
- deepinfra: ProviderIds.deepinfra
11764
+ zai: ProviderIds.zai
11676
11765
  };
11677
11766
  var AGENT_PROVIDER_IDS = Object.values(AgentProviderIds);
11678
11767
  function isAgentProviderId(value) {
11679
11768
  return AGENT_PROVIDER_IDS.includes(value);
11680
11769
  }
11770
+ var RETIRED_AGENT_PROVIDER_IDS = ["deepinfra"];
11771
+ function isRetiredAgentProviderId(value) {
11772
+ return RETIRED_AGENT_PROVIDER_IDS.includes(value);
11773
+ }
11681
11774
  var LlmCapabilities = {
11682
11775
  agent: "agent",
11683
11776
  analyze: "analyze",
@@ -13981,6 +14074,12 @@ var sourceCategoryCountSchema = z43.object({
13981
14074
  });
13982
14075
  var sourceRankEntrySchema = z43.object({
13983
14076
  domain: z43.string(),
14077
+ /**
14078
+ * 1-based position by `count` over the whole scope, before any `limit`.
14079
+ * Domains with equal counts share a rank (competition ranking: 1, 2, 2, 4).
14080
+ * Optional only so an older server's response still parses.
14081
+ */
14082
+ rank: z43.number().int().positive().optional(),
13984
14083
  count: z43.number().int(),
13985
14084
  /** Share of the list's `totalCitedSlots`, 0..1 (at wire precision). */
13986
14085
  percentage: fraction(),
@@ -15251,7 +15350,7 @@ function agentVisibilityEvidence(report, project, retrievedAt) {
15251
15350
  ))
15252
15351
  };
15253
15352
  }
15254
- var agentProviderIdSchema = z49.enum(["claude", "openai", "gemini", "zai", "deepinfra"]);
15353
+ var agentProviderIdSchema = z49.enum(["claude", "openai", "gemini", "zai"]);
15255
15354
  var agentProviderOptionDtoSchema = z49.object({
15256
15355
  /** Stable identifier — what clients pass back as `provider` on the prompt endpoint. */
15257
15356
  id: agentProviderIdSchema,
@@ -21247,6 +21346,7 @@ export {
21247
21346
  hostOf,
21248
21347
  registrableDomain,
21249
21348
  brandLabelFromDomain,
21349
+ competitorLabelFromDomain,
21250
21350
  hostMatchesDomain,
21251
21351
  hostMatchesAnyDomain,
21252
21352
  extractDomainsFromText,
@@ -21305,6 +21405,7 @@ export {
21305
21405
  normalizeProjectDomain,
21306
21406
  effectiveDomains,
21307
21407
  normalizeProjectAliases,
21408
+ resolveProjectQualifiedAliases,
21308
21409
  effectiveBrandNames,
21309
21410
  projectConfigSchema,
21310
21411
  resolveConfigSpecQueries,
@@ -21356,6 +21457,7 @@ export {
21356
21457
  compileMeasurementPlanPreview,
21357
21458
  canonicalMeasurementPlanJson,
21358
21459
  simpleMeasurementDefinitionSchema,
21460
+ simpleMeasurementCompetitorNames,
21359
21461
  buildSimpleMeasurementDefinition,
21360
21462
  canonicalSimpleMeasurementDefinitionJson,
21361
21463
  visibilityReportRequestSchema,
@@ -21624,6 +21726,7 @@ export {
21624
21726
  AgentProviderIds,
21625
21727
  AGENT_PROVIDER_IDS,
21626
21728
  isAgentProviderId,
21729
+ isRetiredAgentProviderId,
21627
21730
  LlmCapabilities,
21628
21731
  LLM_CAPABILITIES,
21629
21732
  providerSummaryEntryDtoSchema,
@@ -128,7 +128,7 @@ import {
128
128
  trafficSeriesGranularitySchema,
129
129
  visibilityCompareSelectionSchema,
130
130
  visibilityReportRequestSchema
131
- } from "./chunk-SJX7XJB6.js";
131
+ } from "./chunk-HAALQW4R.js";
132
132
 
133
133
  // src/cli-error.ts
134
134
  function isMachineFormat(format) {
@@ -456,7 +456,7 @@ var MEASUREMENT_CHANGES_AGENT_ROWS = 20;
456
456
  var measurementChangesInputSchema = measurementChangesQuerySchema.safeExtend({
457
457
  project: projectNameSchema,
458
458
  queryClass: measurementQueryClassFilterSchema.default(AGENT_DEFAULT_QUERY_CLASS).describe("Defaults to non-brand. Call once per class; all pools both classes in rows and distribution."),
459
- limit: z3.number().int().positive().max(MEASUREMENT_CHANGES_AGENT_ROWS).optional().describe(`Changed rows, largest move first. Default 10, at most ${MEASUREMENT_CHANGES_AGENT_ROWS}; distribution counts every Property whatever the limit.`)
459
+ limit: z3.number().int().positive().max(MEASUREMENT_CHANGES_AGENT_ROWS).optional().describe(`Changed rows, largest move first, every move beyond noise before any within it. Default and maximum ${MEASUREMENT_CHANGES_AGENT_ROWS}; distribution counts every Property whatever the limit.`)
460
460
  }).strict();
461
461
  var measurementDataQualityInputSchema = measurementDataQualityQuerySchema.extend({
462
462
  project: projectNameSchema
@@ -948,7 +948,7 @@ var contentTargetsInputSchema = z3.object({
948
948
  var contentBriefInputSchema = z3.object({
949
949
  project: projectNameSchema,
950
950
  targetRef: z3.string().min(1).describe("Stable target ref from canonry_content_targets. The target must be ownable; ceded targets are rejected."),
951
- provider: z3.string().optional().describe("Optional provider override (claude|openai|gemini|zai|deepinfra)."),
951
+ provider: z3.string().optional().describe("Optional provider override (claude|openai|gemini|zai)."),
952
952
  model: z3.string().optional().describe("Optional model override within the chosen provider."),
953
953
  forceRefresh: z3.boolean().optional().describe("Force a fresh synthesis even if a cached brief exists.")
954
954
  });
@@ -1248,7 +1248,7 @@ var canonryMcpTools = [
1248
1248
  defineTool({
1249
1249
  name: "canonry_sentiment_configure",
1250
1250
  title: "Configure project sentiment",
1251
- description: "Install administrator only: explicitly enable or disable stance-and-evidence sentiment. Both install and project switches default off. Enabling processes future eligible complete runs; historical answers require explicit backfill. On dispatch TypeSafe receives the text of each answer with the frozen subject identity (name, aliases and URLs), the tracked query text and class, the answer engine with its requested and served models, the location, and internal query, subject, Property, group and market identifiers. API keys remain local install configuration and are never accepted here.",
1251
+ description: "Install administrator only: explicitly enable or disable stance-and-evidence sentiment. Both install and project switches default off. Enabling processes future eligible complete runs; historical answers require explicit backfill. On dispatch TypeSafe receives the text of each answer with the frozen subject identity (name, aliases, qualified aliases and URLs), the tracked query text and class, the answer engine with its requested and served models, the location, and internal query, subject, Property, group and market identifiers. API keys remain local install configuration and are never accepted here.",
1252
1252
  access: "write",
1253
1253
  tier: "monitoring",
1254
1254
  inputSchema: sentimentConfigureInputSchema,
@@ -1515,7 +1515,7 @@ var canonryMcpTools = [
1515
1515
  defineTool({
1516
1516
  name: "canonry_analytics_sources",
1517
1517
  title: "Get cited-source rankings",
1518
- description: "Where AI engines cite from for a project: cited domains ranked by how many answers cite them, each tagged with a category and a surface class (own / direct-competitor / ota-aggregator / editorial-media / other), plus a surface-class roll-up and a per-provider breakdown. Project-wide: without `queryClass` it pools branded and non-brand answers, where branded queries inflate your own domain. With `queryClass` set and neither `runId` nor `window`, this tool reads the latest sweep (`runId` latest); pass `window` to pool every sweep in it. `pooledAcrossRuns` true (`runCount` above 1) means the counts sum several runs and `runIds` is their scope; the legacy `runId` field is then only the newest run in the window. On a v2 measurement plan the class comes from the plan, the same answers as canonry_competitor_landscape with scope=all-markets. `countUnits` says what each count counts: `domainTotal` is distinct domains, never answers or citations. Compare engines by answer count or `answerShare`, never by rank position. Counts come from each answer's stored source list, so every engine is included, Gemini too. A domain counts at most once per answer; `answerShare` is the share of all answers in scope that cite it. `providersWithoutSources` names engines that answered but cited nothing. `limit` caps each ranked list (default 10 for this tool, which keeps each engine list whole); a long-tail rollup keeps the totals. The per-query breakdown is omitted unless `includeByQuery` is true. No LLM calls; probe runs excluded.",
1518
+ description: "Where AI engines cite from for a project: cited domains ranked by how many answers cite them, each tagged with a category and a surface class (own / direct-competitor / ota-aggregator / editorial-media / other), plus a surface-class roll-up and a per-provider breakdown. Every entry carries rank, its position by count in that list over the whole scope (equal counts share a rank); state where a domain places only from its rank, never by counting rows. Project-wide: without `queryClass` it pools branded and non-brand answers, where branded queries inflate your own domain. With `queryClass` set and neither `runId` nor `window`, this tool reads the latest sweep (`runId` latest); pass `window` to pool every sweep in it. `pooledAcrossRuns` true (`runCount` above 1) means the counts sum several runs and `runIds` is their scope; the legacy `runId` field is then only the newest run in the window. On a v2 measurement plan the class comes from the plan, the same answers as canonry_competitor_landscape with scope=all-markets. `countUnits` says what each count counts: `domainTotal` is distinct domains, never answers or citations. Compare engines by answer count or `answerShare`, never by rank position. Counts come from each answer's stored source list, so every engine is included, Gemini too. A domain counts at most once per answer; `answerShare` is the share of all answers in scope that cite it. `providersWithoutSources` names engines that answered but cited nothing. `limit` caps each ranked list (default 10 for this tool, which keeps each engine list whole); a long-tail rollup keeps the totals. The per-query breakdown is omitted unless `includeByQuery` is true. No LLM calls; probe runs excluded.",
1519
1519
  access: "read",
1520
1520
  tier: "monitoring",
1521
1521
  inputSchema: z3.object({
@@ -2626,7 +2626,7 @@ var canonryMcpTools = [
2626
2626
  defineTool({
2627
2627
  name: "canonry_project_upsert",
2628
2628
  title: "Create or replace project",
2629
- description: "Create or replace a Canonry project. PUT semantics \u2014 fields not in the request are reset to their defaults. Provide the full intended project shape. Exception: an omitted providerDispatchModes (provider \u2192 sync|batch for scheduled sweeps) keeps the stored preference; send {} to clear it.",
2629
+ description: "Create or replace a Canonry project. PUT semantics: fields not in the request are reset to their defaults. Provide the full intended project shape. Exceptions: an omitted providerDispatchModes (provider \u2192 sync|batch for scheduled sweeps) keeps the stored preference; send {} to clear it. An omitted qualifiedAliases (the aliases Simple sentiment treats as this brand's own names) keeps the stored list, minus names that no longer qualify (a removed alias, a spelling of the display name, or a competitor's name); send [] to clear it.",
2630
2630
  access: "write",
2631
2631
  tier: "setup",
2632
2632
  inputSchema: projectUpsertInputSchema,
@@ -2637,7 +2637,7 @@ var canonryMcpTools = [
2637
2637
  defineTool({
2638
2638
  name: "canonry_apply_config",
2639
2639
  title: "Apply project config",
2640
- description: "Apply one Canonry config-as-code project document. Replaces the project to match the config \u2014 fields omitted from the spec are reset to defaults, with one exception: a spec with neither queries nor keywords leaves the tracked-query basket unchanged. To clear the basket, pass an explicit empty queries list. For multi-document YAML, call this tool once per project document.",
2640
+ description: "Apply one Canonry config-as-code project document. Replaces the project to match the config; fields omitted from the spec are reset to defaults, with these exceptions: a spec with neither queries nor keywords leaves the tracked-query basket unchanged (to clear the basket, pass an explicit empty queries list); an omitted providerDispatchModes keeps the stored preference; and an omitted qualifiedAliases keeps the stored list, minus names that no longer qualify (a removed alias, a spelling of the display name, or a competitor's name); send [] to clear it. For multi-document YAML, call this tool once per project document.",
2641
2641
  access: "write",
2642
2642
  tier: "core",
2643
2643
  inputSchema: applyConfigInputSchema,
@@ -2842,7 +2842,7 @@ var canonryMcpTools = [
2842
2842
  defineTool({
2843
2843
  name: "canonry_measurement_portfolio_summary",
2844
2844
  title: "Summarize measured Properties",
2845
- description: "Start here for best and worst Properties. One call returns the weakest Properties, the names answers wrote instead of them, and the domains engines cited for them. Defaults to non-brand queries; state the returned queryClass and never pool branded with non-brand. Returns at most 4 Property rows per list, the most that fit one result; a larger limit is lowered and limitNote says so. For other rows pass groupKey (one metro's weakest), or page canonry_measurement_overview. Mention (answer text names the Property) and citation (a source URL on its page) are separate: report each as numerator/denominator, with any unattributed count beside mention. Denominators count answers, one per query per engine (queries x engines = answers). Group Properties only by each row's metro and submarkets, never by label. namedInsteadInAnswerText lists names written in the answer text of answers that neither named nor cited the Property, counted by answer: they are not citations, and namedInsteadInAnswerTextTotal counts distinct names, not answers. citedDomains and weakestAnswerSources count answers citing each domain, every engine included; weakestAnswerSources pools the weakest rows plus every tied Property, each answer once, and its domainTotal counts distinct domains, not citations. When tiedAtWeakest is set, tiedAtWeakest.count Properties share the weakest rates and the rows are the first of them by name, not a rank: report the count, tiedAtWeakest.byMetro (tied Properties per metro over the whole tie; a Property in two metros counts in both) and tiedAtWeakest.namedInstead (names written instead across the whole tie, counted by distinct answer; namedInsteadTotal is distinct names), and call the rows examples. mentionRanking.strongest/.weakest rank every Property with an available mention rate; .excluded lists the rest with reasons. markets lists every market at one level, worst-first (every metro, or a groupKey's submarkets), whatever the limit. Markets may share Properties and never sum to portfolio totals. Reads stored data only; never starts provider work.",
2845
+ description: "Start here for best and worst Properties. One call returns the weakest Properties, the names answers wrote instead of them, and the domains engines cited for them. Defaults to non-brand queries; state the returned queryClass and never pool branded with non-brand. Returns at most 4 Property rows per list, the most that fit one result; a larger limit is lowered and limitNote says so. For other rows pass groupKey (one metro's weakest), or page canonry_measurement_overview. Mention (answer text names the Property) and citation (a source URL on its page) are separate: report each as numerator/denominator, with any unattributed count beside mention. Denominators count answers, one per query per engine (queries x engines = answers). Group Properties only by each row's metro and submarkets, never by label. namedInsteadInAnswerText lists names written in the answer text of answers that neither named nor cited the Property, counted by answer: they are not citations, and namedInsteadInAnswerTextTotal counts distinct names, not answers. citedDomains and weakestAnswerSources count answers citing each domain, every engine included; weakestAnswerSources pools the weakest rows plus every tied Property, each answer once, and its domainTotal counts distinct domains, not citations. 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.",
2846
2846
  access: "read",
2847
2847
  tier: "monitoring",
2848
2848
  inputSchema: measurementPortfolioSummaryInputSchema,
@@ -2902,7 +2902,7 @@ var canonryMcpTools = [
2902
2902
  defineTool({
2903
2903
  name: "canonry_measurement_changes",
2904
2904
  title: "Compare stored measurement changes",
2905
- description: "What changed since the previous sweep: compares the displayed run with the previous run of the same plan revision and execution identity; an incompatible pair returns unavailable with a reason, never a trend. Defaults to non-brand queries: call once per class (queryClass branded for the branded move) and never report one class as the whole. queryClass all pools both classes in rows and distribution and adds metricsByClass, the headline metrics per class; report those per class, never the pooled metrics alone. changedProperties come largest move first (answers that named the Property, then answers that cited it; sort label for alphabetical), default 10 and at most 20 rows; totalProperties above the rows returned means rows you did not see. distribution counts every Property in scope: improved, declined, mixed, withinNoise, unchanged, notComparable. mentionAnswersDelta and citationAnswersDelta are signed answer counts; denominatorChanged true means a rate was taken over a different number of answers, so those counts are not like for like and the coverage rates give the direction. A move is the rate change times the larger answer count (the count change when the denominator held). withinNoise true means every move was 2 answers or fewer: within noise, never a gain, loss, trend or regression. Reads stored data only; it never starts provider work.",
2905
+ description: "What changed since the previous sweep: compares the displayed run with the previous run of the same plan revision and execution identity; an incompatible pair returns unavailable with a reason, never a trend. Defaults to non-brand queries: call once per class (queryClass branded for the branded move) and never report one class as the whole. queryClass all pools both classes in rows and distribution and adds metricsByClass, the headline metrics per class; report those per class, never the pooled metrics alone. changedProperties come largest move first (answers that named the Property, then answers that cited it; sort label for alphabetical), every move beyond noise before any within it, 20 rows by default and at most; totalProperties above the rows returned means rows you did not see. distribution counts every Property in scope: improved, declined, mixed, withinNoise, unchanged, notComparable. mentionAnswersDelta and citationAnswersDelta are signed answer counts; denominatorChanged true means a rate was taken over a different number of answers, so those counts are not like for like and the coverage rates give the direction. A move is the rate change times the larger answer count (the count change when the denominator held). withinNoise true means every move was 2 answers or fewer: within noise, never a gain, loss, trend or regression. mentionWithinNoise and citationWithinNoise apply the same rule to each signal alone: a Property beyond noise can still have one signal within noise, and that signal is not a gain or loss. Report every headline metric, branded included, as previous numerator/denominator to current numerator/denominator. Reads stored data only; it never starts provider work.",
2906
2906
  access: "read",
2907
2907
  tier: "monitoring",
2908
2908
  inputSchema: measurementChangesInputSchema,
@@ -2910,7 +2910,7 @@ var canonryMcpTools = [
2910
2910
  openApiOperations: ["GET /api/v1/projects/{name}/measurement-changes"],
2911
2911
  handler: (client, input) => {
2912
2912
  const { project, ...query } = input;
2913
- return client.getMeasurementChanges(project, { ...query, queryClass: agentQueryClass(query.queryClass) });
2913
+ return client.getMeasurementChanges(project, { ...query, limit: query.limit ?? MEASUREMENT_CHANGES_AGENT_ROWS, queryClass: agentQueryClass(query.queryClass) });
2914
2914
  }
2915
2915
  }),
2916
2916
  defineTool({
@@ -5,7 +5,7 @@ import {
5
5
  PACKAGE_VERSION,
6
6
  canonryMcpTools,
7
7
  isMachineFormat
8
- } from "./chunk-Y23FQK6O.js";
8
+ } from "./chunk-K4356AGP.js";
9
9
  import {
10
10
  AGENT_PROVIDER_IDS,
11
11
  CANONRY_NPM_PACKAGE_URL,
@@ -28,6 +28,7 @@ import {
28
28
  formatCanonryPluginVersionMismatch,
29
29
  isInstallMethod,
30
30
  isReadOnlyKey,
31
+ isRetiredAgentProviderId,
31
32
  isSensitiveDiagnosticQueryKey,
32
33
  isStrictSemver,
33
34
  normalizeAgentSlug,
@@ -49,7 +50,7 @@ import {
49
50
  skillsClientSchema,
50
51
  upgradeCaveatFor,
51
52
  upgradeCommandFor
52
- } from "./chunk-SJX7XJB6.js";
53
+ } from "./chunk-HAALQW4R.js";
53
54
 
54
55
  // src/config.ts
55
56
  import fs from "fs";
@@ -429,9 +430,11 @@ Do not use "canonry init --force" for recovery. It replaces credentials without
429
430
  });
430
431
  }
431
432
  if (!agentProviderSchema.safeParse(parsed.agent?.provider).success) {
433
+ const pinned = parsed.agent?.provider;
434
+ const retired = typeof pinned === "string" && isRetiredAgentProviderId(pinned);
432
435
  throw new CliError({
433
436
  code: "CONFIG_INVALID",
434
- message: `Invalid config at ${configPath}: agent.provider must be one of ${AGENT_PROVIDER_IDS.join(", ")}, or left blank.`
437
+ message: retired ? `Invalid config at ${configPath}: agent.provider ${pinned} was removed. Pin one of ${AGENT_PROVIDER_IDS.join(", ")} (with a key under providers.<name>.apiKey), or leave it blank.` : `Invalid config at ${configPath}: agent.provider must be one of ${AGENT_PROVIDER_IDS.join(", ")}, or left blank.`
435
438
  });
436
439
  }
437
440
  if (!agentModelSchema.safeParse(parsed.agent?.model).success) {