@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
@@ -291,6 +291,7 @@ import {
291
291
  compareSemver,
292
292
  competitorBatchRequestSchema,
293
293
  competitorDtoSchema,
294
+ competitorLabelFromDomain,
294
295
  competitorLandscapeQuerySchema,
295
296
  competitorLandscapeResponseSchema,
296
297
  compileBrandAliases,
@@ -720,6 +721,7 @@ import {
720
721
  resolveMeasurementRunQueryScope,
721
722
  resolveMeasurementRunScope,
722
723
  resolveNegativeReviewMaxStars,
724
+ resolveProjectQualifiedAliases,
723
725
  resolveProviderModel,
724
726
  resolveRunDispatchModes,
725
727
  resolveSnapshotRequestQueries,
@@ -774,6 +776,7 @@ import {
774
776
  shareOfVoiceReason,
775
777
  shareOfVoiceSummary,
776
778
  shiftIsoCalendarDate,
779
+ simpleMeasurementCompetitorNames,
777
780
  simpleMeasurementDefinitionSchema,
778
781
  siteAuditLivePageHealthSchema,
779
782
  siteAuditPageFactorSchema,
@@ -866,7 +869,7 @@ import {
866
869
  wordpressSchemaDeployResultDtoSchema,
867
870
  wordpressSchemaStatusResultDtoSchema,
868
871
  wordpressStatusDtoSchema
869
- } from "./chunk-SJX7XJB6.js";
872
+ } from "./chunk-HAALQW4R.js";
870
873
 
871
874
  // src/intelligence-service.ts
872
875
  import { eq as eq82, desc as desc36, asc as asc15, and as and69, ne as ne14, or as or20, inArray as inArray32, gte as gte22, lte as lte20, isNull as isNull16, sql as sql31, exists } from "drizzle-orm";
@@ -1017,6 +1020,12 @@ var projects = sqliteTable("projects", {
1017
1020
  canonicalDomain: text("canonical_domain").notNull(),
1018
1021
  ownedDomains: text("owned_domains", { mode: "json" }).$type().notNull().default([]),
1019
1022
  aliases: text("aliases", { mode: "json" }).$type().notNull().default([]),
1023
+ /**
1024
+ * Operator-chosen subset of `aliases` that Simple sentiment tells the
1025
+ * evaluator are this brand's own names. Frozen into each Simple run's
1026
+ * sidecar at dispatch; never read by mention detection or query classes.
1027
+ */
1028
+ qualifiedAliases: text("qualified_aliases", { mode: "json" }).$type().notNull().default([]),
1020
1029
  country: text("country").notNull(),
1021
1030
  language: text("language").notNull(),
1022
1031
  tags: text("tags", { mode: "json" }).$type().notNull().default([]),
@@ -8172,6 +8181,16 @@ var MIGRATION_VERSIONS = [
8172
8181
  name: "aero-ledger-conversation-ids",
8173
8182
  statements: [],
8174
8183
  run: dropAeroLedgerConversationKeys
8184
+ },
8185
+ {
8186
+ // Sentiment-only project setting: the aliases the evaluator is told are
8187
+ // the brand's own names. Defaulted, so an older writer that omits it and
8188
+ // every existing project read as not opted in.
8189
+ version: 166,
8190
+ name: "projects-qualified-aliases",
8191
+ statements: [
8192
+ `ALTER TABLE projects ADD COLUMN qualified_aliases TEXT NOT NULL DEFAULT '[]'`
8193
+ ]
8175
8194
  }
8176
8195
  ];
8177
8196
  var AERO_LEDGER_REBUILDS = [
@@ -11275,7 +11294,7 @@ function gapTone(gapCount, totalCount) {
11275
11294
 
11276
11295
  // ../intelligence/src/visibility-score.ts
11277
11296
  function buildVisibilityScore(snapshots, options) {
11278
- const tooltip = "An LLM used a page on your domain as a source for its answer.";
11297
+ const tooltip = "An AI engine used a page on your domain as a source for its answer.";
11279
11298
  if (snapshots.length === 0) {
11280
11299
  return {
11281
11300
  label: "Citation Coverage",
@@ -11307,13 +11326,13 @@ function buildVisibilityScore(snapshots, options) {
11307
11326
  tooltip,
11308
11327
  trend: [],
11309
11328
  progress: score,
11310
- providerCoverage: isPartialProviderRun ? `${runApiProviderCount} of ${options.configuredApiProviders.length} providers` : void 0
11329
+ providerCoverage: isPartialProviderRun ? `${runApiProviderCount} of ${options.configuredApiProviders.length} engines` : void 0
11311
11330
  };
11312
11331
  }
11313
11332
 
11314
11333
  // ../intelligence/src/mention-coverage.ts
11315
11334
  function buildMentionCoverage(snapshots, options) {
11316
- const tooltip = "Your domain or company name was in the answer returned by the LLM.";
11335
+ const tooltip = "Your domain or company name was in the answer returned by the AI engine.";
11317
11336
  if (snapshots.length === 0) {
11318
11337
  return {
11319
11338
  label: "Mention Coverage",
@@ -11345,7 +11364,7 @@ function buildMentionCoverage(snapshots, options) {
11345
11364
  tooltip,
11346
11365
  trend: [],
11347
11366
  progress: score,
11348
- providerCoverage: isPartialProviderRun ? `${runApiProviderCount} of ${options.configuredApiProviders.length} providers` : void 0
11367
+ providerCoverage: isPartialProviderRun ? `${runApiProviderCount} of ${options.configuredApiProviders.length} engines` : void 0
11349
11368
  };
11350
11369
  }
11351
11370
 
@@ -11379,7 +11398,7 @@ function buildGapQueryScore(snapshots) {
11379
11398
  return {
11380
11399
  label: "Citation Gaps",
11381
11400
  value: `${gapCount}`,
11382
- delta: `${gapCount} of ${totalCount} queries at risk`,
11401
+ delta: `${gapCount} of ${totalCount} ${totalCount === 1 ? "query" : "queries"}`,
11383
11402
  tone: gapTone(gapCount, totalCount),
11384
11403
  description: gapCount > 0 ? `${gapCount} tracked ${gapQueryLabel} currently cite competitors without citing your domain.` : "No competitive citation gaps detected in the latest visibility run.",
11385
11404
  tooltip,
@@ -11416,7 +11435,7 @@ function buildMentionGapScore(snapshots) {
11416
11435
  return {
11417
11436
  label: "Mention Gaps",
11418
11437
  value: `${gapCount}`,
11419
- delta: `${gapCount} of ${totalCount} queries at risk`,
11438
+ delta: `${gapCount} of ${totalCount} ${totalCount === 1 ? "query" : "queries"}`,
11420
11439
  tone: gapTone(gapCount, totalCount),
11421
11440
  description: gapCount > 0 ? `${gapCount} tracked ${gapQueryLabel} mention competitors but never your brand.` : "No competitive mention gaps detected in the latest visibility run.",
11422
11441
  tooltip,
@@ -13638,6 +13657,7 @@ function selectSentimentSources(db, projectId, filters) {
13638
13657
  continue;
13639
13658
  }
13640
13659
  const identity = definition.identity;
13660
+ const qualifiedAliases = [...identity.qualifiedAliases ?? []].sort();
13641
13661
  const names = effectiveBrandNames(identity);
13642
13662
  const urls = [identity.canonicalDomain, ...identity.ownedDomains];
13643
13663
  const mentionNotApplicable = !names.length && !urls.some((url) => url.trim());
@@ -13659,7 +13679,7 @@ function selectSentimentSources(db, projectId, filters) {
13659
13679
  language: definition.language,
13660
13680
  queryText: query.queryText,
13661
13681
  revision: null,
13662
- subject: { key: projectId, name: identity.displayName, aliases: [...names], identityAliases: [], urls: [...urls], mentionNotApplicable },
13682
+ subject: { key: projectId, name: identity.displayName, aliases: [...names], identityAliases: [...qualifiedAliases], urls: [...urls], mentionNotApplicable },
13663
13683
  edges: [edge]
13664
13684
  });
13665
13685
  }
@@ -13844,7 +13864,7 @@ function sentimentClassifierInput(source, definition) {
13844
13864
  }
13845
13865
 
13846
13866
  // ../api-routes/src/sentiment-service.ts
13847
- var DISCLOSURE = "Experimental English sentiment classification. Enabling sends each stored answer's text to TypeSafe with the frozen subject identity (name, aliases and URLs), the tracked query text and query class, the answer engine with its requested and served models, the location, and internal query, subject, Property, group and market identifiers.";
13867
+ var DISCLOSURE = "Experimental English sentiment classification. Enabling sends each stored answer's text to TypeSafe with the frozen subject identity (name, aliases, qualified aliases and URLs), the tracked query text and query class, the answer engine with its requested and served models, the location, and internal query, subject, Property, group and market identifiers.";
13848
13868
  var FULL_ROWS = { include: /* @__PURE__ */ new Set(["assessments", "locations"]), limit: null, after: null };
13849
13869
  var COMPACT_ROWS = { include: /* @__PURE__ */ new Set(), limit: null, after: null };
13850
13870
  var OPERATIONAL_OUTCOMES = /* @__PURE__ */ new Set([SentimentOutcomes.pending, SentimentOutcomes.running, SentimentOutcomes["waiting-to-retry"], SentimentOutcomes.failed, SentimentOutcomes.canceled]);
@@ -16866,6 +16886,7 @@ async function projectRoutes(app, opts) {
16866
16886
  const id = crypto9.randomUUID();
16867
16887
  const now = (/* @__PURE__ */ new Date()).toISOString();
16868
16888
  const nextAliases = normalizeProjectAliases(body.displayName, body.aliases ?? []);
16889
+ const qualifiedAliases = body.qualifiedAliases === void 0 ? [] : requireQualifiedAliases({ displayName: body.displayName, aliases: nextAliases }, body.qualifiedAliases, []);
16869
16890
  const inserted = app.db.transaction((tx) => {
16870
16891
  const result = tx.insert(projects).values({
16871
16892
  id,
@@ -16874,6 +16895,7 @@ async function projectRoutes(app, opts) {
16874
16895
  canonicalDomain,
16875
16896
  ownedDomains: body.ownedDomains ?? [],
16876
16897
  aliases: nextAliases,
16898
+ qualifiedAliases,
16877
16899
  country: body.country,
16878
16900
  language: body.language,
16879
16901
  tags: body.tags ?? [],
@@ -16960,6 +16982,9 @@ async function projectRoutes(app, opts) {
16960
16982
  const nextNegativeReviewMaxStars = body.negativeReviewMaxStars !== void 0 ? body.negativeReviewMaxStars : existing ? readNegativeReviewMaxStars(app.db, existing.id) : null;
16961
16983
  const nextMeasurement = body.measurement ?? existing?.measurement ?? DEFAULT_MEASUREMENT_CONFIG;
16962
16984
  const nextAliases = normalizeProjectAliases(body.displayName, body.aliases ?? []);
16985
+ const nextIdentity = { displayName: body.displayName, aliases: nextAliases };
16986
+ const liveCompetitors = existing ? liveCompetitorNames(app.db, existing.id) : [];
16987
+ const nextQualifiedAliases = body.qualifiedAliases !== void 0 ? requireQualifiedAliases(nextIdentity, body.qualifiedAliases, liveCompetitors) : resolveProjectQualifiedAliases(nextIdentity, existing?.qualifiedAliases ?? [], liveCompetitors).value;
16963
16988
  if (existing) {
16964
16989
  const prevAliases = existing.aliases;
16965
16990
  const aliasesChanged = !aliasArraysEqual(prevAliases, nextAliases);
@@ -16969,6 +16994,7 @@ async function projectRoutes(app, opts) {
16969
16994
  canonicalDomain: body.canonicalDomain,
16970
16995
  ownedDomains: body.ownedDomains ?? [],
16971
16996
  aliases: nextAliases,
16997
+ qualifiedAliases: nextQualifiedAliases,
16972
16998
  country: body.country,
16973
16999
  language: body.language,
16974
17000
  tags: body.tags ?? [],
@@ -17007,6 +17033,7 @@ async function projectRoutes(app, opts) {
17007
17033
  canonicalDomain: body.canonicalDomain,
17008
17034
  ownedDomains: body.ownedDomains ?? [],
17009
17035
  aliases: nextAliases,
17036
+ qualifiedAliases: nextQualifiedAliases,
17010
17037
  country: body.country,
17011
17038
  language: body.language,
17012
17039
  tags: body.tags ?? [],
@@ -17195,6 +17222,11 @@ async function projectRoutes(app, opts) {
17195
17222
  eq15(schedules.kind, SchedulableRunKinds["answer-visibility"])
17196
17223
  )).get();
17197
17224
  const notificationRows = app.db.select().from(notifications).where(eq15(notifications.projectId, project.id)).all();
17225
+ const exportedQualifiedAliases = resolveProjectQualifiedAliases(
17226
+ project,
17227
+ project.qualifiedAliases,
17228
+ comps.map((c) => competitorLabelFromDomain(c.domain))
17229
+ ).value;
17198
17230
  const config = {
17199
17231
  apiVersion: "canonry/v1",
17200
17232
  kind: "Project",
@@ -17207,6 +17239,7 @@ async function projectRoutes(app, opts) {
17207
17239
  canonicalDomain: project.canonicalDomain,
17208
17240
  ownedDomains: project.ownedDomains,
17209
17241
  aliases: project.aliases,
17242
+ ...exportedQualifiedAliases.length ? { qualifiedAliases: exportedQualifiedAliases } : {},
17210
17243
  country: project.country,
17211
17244
  language: project.language,
17212
17245
  queries: qs.map((q) => q.query),
@@ -17246,6 +17279,32 @@ function assertProviderModelScope(request, current, next, nextProviders) {
17246
17279
  if (providerModelsEqual(effectiveCurrent, effectiveNext)) return;
17247
17280
  requireScope(request, SETTINGS_WRITE_SCOPE);
17248
17281
  }
17282
+ function requireQualifiedAliases(identity, requested, competitorNames) {
17283
+ const resolved = resolveProjectQualifiedAliases(identity, requested, competitorNames);
17284
+ if (resolved.rejected.length > 0) {
17285
+ const reasons = resolved.rejected.map((entry) => `${entry.name} (${entry.reason})`).join(", ");
17286
+ throw validationError(`Rejected qualifiedAliases: ${reasons}`, {
17287
+ rejectedQualifiedAliases: resolved.rejected
17288
+ });
17289
+ }
17290
+ return resolved.value;
17291
+ }
17292
+ function liveCompetitorNames(db, projectId) {
17293
+ return db.select({ domain: competitors.domain }).from(competitors).where(eq15(competitors.projectId, projectId)).all().map((row) => competitorLabelFromDomain(row.domain));
17294
+ }
17295
+ function pruneQualifiedAliasesForCompetitors(tx, projectId, now) {
17296
+ const project = tx.select({
17297
+ displayName: projects.displayName,
17298
+ aliases: projects.aliases,
17299
+ qualifiedAliases: projects.qualifiedAliases
17300
+ }).from(projects).where(eq15(projects.id, projectId)).get();
17301
+ if (!project?.qualifiedAliases.length) return [];
17302
+ const kept = resolveProjectQualifiedAliases(project, project.qualifiedAliases, liveCompetitorNames(tx, projectId)).value;
17303
+ const dropped = project.qualifiedAliases.filter((name) => !kept.includes(name));
17304
+ if (dropped.length === 0) return [];
17305
+ tx.update(projects).set({ qualifiedAliases: kept, updatedAt: now }).where(eq15(projects.id, projectId)).run();
17306
+ return dropped;
17307
+ }
17249
17308
  function providerModelsEqual(a, b) {
17250
17309
  const aKeys = Object.keys(a);
17251
17310
  if (aKeys.length !== Object.keys(b).length) return false;
@@ -17259,6 +17318,7 @@ function formatProject(row, negativeReviewMaxStars) {
17259
17318
  canonicalDomain: row.canonicalDomain,
17260
17319
  ownedDomains: row.ownedDomains,
17261
17320
  aliases: row.aliases,
17321
+ qualifiedAliases: row.qualifiedAliases,
17262
17322
  country: row.country,
17263
17323
  language: row.language,
17264
17324
  tags: row.tags,
@@ -17977,12 +18037,16 @@ async function competitorRoutes(app) {
17977
18037
  createdAt: now
17978
18038
  }).run();
17979
18039
  }
18040
+ const droppedQualifiedAliases = pruneQualifiedAliasesForCompetitors(tx, project.id, now);
17980
18041
  writeAuditLog(tx, {
17981
18042
  projectId: project.id,
17982
18043
  actor: "api",
17983
18044
  action: "competitors.replaced",
17984
18045
  entityType: "competitor",
17985
- diff: { competitors: normalizedCompetitors }
18046
+ diff: {
18047
+ competitors: normalizedCompetitors,
18048
+ ...droppedQualifiedAliases.length ? { droppedQualifiedAliases } : {}
18049
+ }
17986
18050
  });
17987
18051
  });
17988
18052
  const rows = app.db.select().from(competitors).where(eq19(competitors.projectId, project.id)).all();
@@ -18009,12 +18073,13 @@ async function competitorRoutes(app) {
18009
18073
  target: [competitors.projectId, competitors.domain]
18010
18074
  }).run();
18011
18075
  }
18076
+ const droppedQualifiedAliases = pruneQualifiedAliasesForCompetitors(tx, project.id, now);
18012
18077
  writeAuditLog(tx, {
18013
18078
  projectId: project.id,
18014
18079
  actor: "api",
18015
18080
  action: "competitors.appended",
18016
18081
  entityType: "competitor",
18017
- diff: { added }
18082
+ diff: { added, ...droppedQualifiedAliases.length ? { droppedQualifiedAliases } : {} }
18018
18083
  });
18019
18084
  });
18020
18085
  const rows = app.db.select().from(competitors).where(eq19(competitors.projectId, project.id)).all();
@@ -25670,7 +25735,7 @@ function mentionRanking(rows, limit) {
25670
25735
  truncated: eligible.length > limit
25671
25736
  };
25672
25737
  }
25673
- function portfolioResponse(db, active, plan, query) {
25738
+ function portfolioResponse(db, active, plan, query, ownDomains) {
25674
25739
  const group = query.groupKey === void 0 ? void 0 : requireGroup2(plan, query.groupKey);
25675
25740
  const run = selectMeasurementQuestionRun(db, active.version.projectId, active, query.runId);
25676
25741
  const requestedTargetKeys = group?.targetKeys ?? plan.targets.map((target) => target.stableKey);
@@ -25766,7 +25831,7 @@ function portfolioResponse(db, active, plan, query) {
25766
25831
  },
25767
25832
  weakestProperties: rows,
25768
25833
  tiedAtWeakest: tie.summary,
25769
- weakestAnswerSources: weakestAnswerSources([...displayed, ...tie.rows]),
25834
+ weakestAnswerSources: weakestAnswerSources([...displayed, ...tie.rows], ownDomains),
25770
25835
  mentionRanking: mentionRanking(ranked, limit),
25771
25836
  ...marketRollup(plan, { run, evaluator }, group, includeNestedMarkets),
25772
25837
  totalProperties: ranked.length,
@@ -25827,7 +25892,7 @@ function tieNamedInstead(rows) {
25827
25892
  const counted = [...grouped.values()].map((row) => ({ name: row.name, answers: row.answers.size })).sort((left, right) => right.answers - left.answers || compareText13(left.name, right.name));
25828
25893
  return { rows: counted.slice(0, MEASUREMENT_PORTFOLIO_TIE_NAMED_INSTEAD_LIMIT), total: counted.length };
25829
25894
  }
25830
- function weakestAnswerSources(rows) {
25895
+ function weakestAnswerSources(rows, ownDomains) {
25831
25896
  const properties = /* @__PURE__ */ new Set();
25832
25897
  const answers = /* @__PURE__ */ new Map();
25833
25898
  for (const row of rows) {
@@ -25835,7 +25900,11 @@ function weakestAnswerSources(rows) {
25835
25900
  for (const answer of row.population.sourceAnswers) answers.set(answer.snapshot.id, answer);
25836
25901
  }
25837
25902
  const domains = domainRows([...answers.values()], MEASUREMENT_PORTFOLIO_ANSWER_SOURCES_LIMIT);
25838
- return { properties: properties.size, answers: answers.size, domains: domains.rows, domainTotal: domains.total };
25903
+ let ownDomainAnswers = 0;
25904
+ for (const answer of answers.values()) {
25905
+ if ([...answerDomains(answer)].some((host) => hostMatchesAnyDomain(host, ownDomains))) ownDomainAnswers++;
25906
+ }
25907
+ return { properties: properties.size, answers: answers.size, domains: domains.rows, domainTotal: domains.total, ownDomainAnswers };
25839
25908
  }
25840
25909
  function propertyCompetitorsResponse(db, active, plan, query) {
25841
25910
  const target = requireTarget3(plan, query.targetKey);
@@ -26047,11 +26116,14 @@ function classifyPropertyMove(mention, citation) {
26047
26116
  const down = directions.some((move) => move < 0);
26048
26117
  bucket = up && down ? "mixed" : up ? "improved" : "declined";
26049
26118
  }
26119
+ const signalWithinNoise = (move) => move === null ? null : Math.abs(move) <= MEASUREMENT_CHANGES_NOISE_ANSWERS;
26050
26120
  return {
26051
26121
  mentionAnswersDelta: answersDelta(mention),
26052
26122
  citationAnswersDelta: answersDelta(citation),
26053
26123
  denominatorChanged: baseChanged(mention) || baseChanged(citation),
26054
26124
  withinNoise: changed && withinNoise,
26125
+ mentionWithinNoise: signalWithinNoise(mentionMove),
26126
+ citationWithinNoise: signalWithinNoise(citationMove),
26055
26127
  bucket,
26056
26128
  mentionSize: mentionMove === null ? -1 : Math.abs(mentionMove),
26057
26129
  citationSize: citationMove === null ? -1 : Math.abs(citationMove)
@@ -26140,7 +26212,9 @@ function changesResponse(db, active, plan, query) {
26140
26212
  mentionAnswersDelta: move.mentionAnswersDelta,
26141
26213
  citationAnswersDelta: move.citationAnswersDelta,
26142
26214
  denominatorChanged: move.denominatorChanged,
26143
- withinNoise: move.withinNoise
26215
+ withinNoise: move.withinNoise,
26216
+ mentionWithinNoise: move.mentionWithinNoise,
26217
+ citationWithinNoise: move.citationWithinNoise
26144
26218
  },
26145
26219
  move
26146
26220
  }];
@@ -26321,7 +26395,7 @@ async function measurementPortfolioReadRoutes(app) {
26321
26395
  const raw = nested === "true" || nested === "false" ? { ...request.query, includeNestedMarkets: nested === "true" } : request.query;
26322
26396
  const query = parseLimitQuery(raw, measurementPortfolioSummaryQuerySchema, "Invalid measurement portfolio summary query");
26323
26397
  const { active, plan } = activeV2Plan2(app.db, project.id, "Portfolio summary");
26324
- return portfolioResponse(app.db, active, plan, query);
26398
+ return portfolioResponse(app.db, active, plan, query, effectiveDomains(project));
26325
26399
  }
26326
26400
  );
26327
26401
  app.get(
@@ -29708,6 +29782,15 @@ async function applyRoutes(app, opts) {
29708
29782
  specProviders
29709
29783
  );
29710
29784
  const specDispatchModes = config.spec.providerDispatchModes === void 0 ? void 0 : validateProviderDispatchModes(config.spec.providerDispatchModes, opts?.providerAdapters);
29785
+ const specCompetitorNames = normalizeCompetitorList2(config.spec.competitors).map(competitorLabelFromDomain);
29786
+ const specQualifiedAliases = config.spec.qualifiedAliases === void 0 ? void 0 : requireQualifiedAliases(
29787
+ {
29788
+ displayName: config.spec.displayName,
29789
+ aliases: normalizeProjectAliases(config.spec.displayName, config.spec.aliases)
29790
+ },
29791
+ config.spec.qualifiedAliases,
29792
+ specCompetitorNames
29793
+ );
29711
29794
  let resolvedSchedule = null;
29712
29795
  let deleteSchedule = false;
29713
29796
  if (config.spec.schedule) {
@@ -29766,6 +29849,11 @@ async function applyRoutes(app, opts) {
29766
29849
  }
29767
29850
  if (existing) {
29768
29851
  projectId = existing.id;
29852
+ const qualifiedAliases = specQualifiedAliases ?? resolveProjectQualifiedAliases(
29853
+ { displayName: config.spec.displayName, aliases: nextAliases },
29854
+ existing.qualifiedAliases,
29855
+ specCompetitorNames
29856
+ ).value;
29769
29857
  const providerDispatchModes = pruneProviderDispatchModes(
29770
29858
  specDispatchModes ?? existing.providerDispatchModes,
29771
29859
  specProviders,
@@ -29776,6 +29864,7 @@ async function applyRoutes(app, opts) {
29776
29864
  canonicalDomain: config.spec.canonicalDomain,
29777
29865
  ownedDomains: config.spec.ownedDomains ?? [],
29778
29866
  aliases: nextAliases,
29867
+ qualifiedAliases,
29779
29868
  country: config.spec.country,
29780
29869
  language: config.spec.language,
29781
29870
  labels: config.metadata.labels,
@@ -29807,6 +29896,7 @@ async function applyRoutes(app, opts) {
29807
29896
  canonicalDomain: config.spec.canonicalDomain,
29808
29897
  ownedDomains: config.spec.ownedDomains ?? [],
29809
29898
  aliases: nextAliases,
29899
+ qualifiedAliases: specQualifiedAliases ?? [],
29810
29900
  country: config.spec.country,
29811
29901
  language: config.spec.language,
29812
29902
  tags: [],
@@ -29951,6 +30041,7 @@ async function applyRoutes(app, opts) {
29951
30041
  canonicalDomain: project.canonicalDomain,
29952
30042
  ownedDomains: project.ownedDomains,
29953
30043
  aliases: project.aliases,
30044
+ qualifiedAliases: project.qualifiedAliases,
29954
30045
  country: project.country,
29955
30046
  language: project.language,
29956
30047
  tags: project.tags,
@@ -31284,8 +31375,11 @@ function buildRankedList(domains, limit, answerTotal, answersWithSources) {
31284
31375
  const domainTotal = all.length;
31285
31376
  all.sort((a, b) => b.count - a.count || a.domain.localeCompare(b.domain));
31286
31377
  const shownEntries = limit != null && limit < all.length ? all.slice(0, limit) : all;
31287
- const entries = shownEntries.map((d) => ({
31378
+ let rank = 0;
31379
+ const ranks = all.map((d, i) => rank = i > 0 && d.count === all[i - 1].count ? rank : i + 1);
31380
+ const entries = shownEntries.map((d, i) => ({
31288
31381
  domain: d.domain,
31382
+ rank: ranks[i],
31289
31383
  count: d.count,
31290
31384
  percentage: totalCitedSlots > 0 ? roundRatio(d.count / totalCitedSlots, RatioUnits.fraction) : 0,
31291
31385
  answerShare: answerTotal > 0 ? roundRatio(d.count / answerTotal, RatioUnits.fraction) : 0,
@@ -39426,6 +39520,7 @@ function formatProject2(row, negativeReviewMaxStars) {
39426
39520
  canonicalDomain: row.canonicalDomain,
39427
39521
  ownedDomains: row.ownedDomains,
39428
39522
  aliases: row.aliases,
39523
+ qualifiedAliases: row.qualifiedAliases,
39429
39524
  country: row.country,
39430
39525
  language: row.language,
39431
39526
  tags: row.tags,
@@ -86021,7 +86116,7 @@ var agentProvidersConfiguredCheck = {
86021
86116
  status: CheckStatuses.warn,
86022
86117
  code: "agent-providers.none-configured",
86023
86118
  summary: "No agent LLM provider has credentials configured \u2014 the built-in Aero agent cannot run.",
86024
- remediation: "Add a key for one of the agent providers (claude, openai, gemini, zai, deepinfra) under `providers.<name>.apiKey` in ~/.canonry/config.yaml, or export its env var (e.g. ANTHROPIC_API_KEY, OPENAI_API_KEY, DEEPINFRA_TOKEN).",
86119
+ remediation: "Add a key for one of the agent providers (claude, openai, gemini, zai) under `providers.<name>.apiKey` in ~/.canonry/config.yaml, or export its env var (e.g. ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY).",
86025
86120
  details
86026
86121
  };
86027
86122
  }
@@ -87666,13 +87761,18 @@ async function discoveryRoutes(app, opts) {
87666
87761
  createdAt: now
87667
87762
  }).run();
87668
87763
  }
87764
+ const droppedQualifiedAliases = promotedCompetitors.length ? pruneQualifiedAliasesForCompetitors(tx, project.id, now) : [];
87669
87765
  writeAuditLog(tx, {
87670
87766
  projectId: project.id,
87671
87767
  actor: "api",
87672
87768
  action: "discovery.promoted",
87673
87769
  entityType: "discovery_session",
87674
87770
  entityId: session.id,
87675
- diff: { queries: promotedQueries, competitors: promotedCompetitors }
87771
+ diff: {
87772
+ queries: promotedQueries,
87773
+ competitors: promotedCompetitors,
87774
+ ...droppedQualifiedAliases.length ? { droppedQualifiedAliases } : {}
87775
+ }
87676
87776
  });
87677
87777
  });
87678
87778
  }
@@ -90373,12 +90473,28 @@ function captureSimpleMeasurementDefinition(db, input) {
90373
90473
  if (definition.queries.some((query) => query.queryClass !== (classifier?.classify(query.queryText) ?? null))) {
90374
90474
  throw validationError("Query classes must match the captured identity and text.");
90375
90475
  }
90476
+ if (definition.identity.qualifiedAliases !== void 0) {
90477
+ const expected = resolveProjectQualifiedAliases(
90478
+ definition.identity,
90479
+ definition.identity.qualifiedAliases,
90480
+ simpleMeasurementCompetitorNames(definition.competitors)
90481
+ ).value;
90482
+ const captured = definition.identity.qualifiedAliases;
90483
+ if (captured.length !== expected.length || captured.some((name, index2) => name !== expected[index2])) {
90484
+ throw validationError("Qualified aliases must be a sorted subset of the captured aliases.");
90485
+ }
90486
+ }
90376
90487
  const existing = tx.select().from(simpleMeasurementDefinitions).where(and68(eq81(simpleMeasurementDefinitions.projectId, input.projectId), eq81(simpleMeasurementDefinitions.runId, input.runId))).get();
90377
90488
  if (existing) {
90378
90489
  const frozen = simpleMeasurementDefinitionSchema.parse(existing.definition);
90490
+ const { qualifiedAliases: _liveQualifiedAliases, ...liveIdentity } = definition.identity;
90379
90491
  const replayDefinition = {
90380
90492
  ...definition,
90381
- capturedAt: frozen.capturedAt
90493
+ capturedAt: frozen.capturedAt,
90494
+ identity: {
90495
+ ...liveIdentity,
90496
+ ...frozen.identity.qualifiedAliases?.length ? { qualifiedAliases: frozen.identity.qualifiedAliases } : {}
90497
+ }
90382
90498
  };
90383
90499
  const replayChecksum = crypto51.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(replayDefinition)).digest("hex");
90384
90500
  if (existing.checksum !== replayChecksum) {