@canonry/canonry 5.32.0 → 5.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +1 -1
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +12 -3
  3. package/assets/assets/{AuditHistoryPanel-DW2aeqJP.js → AuditHistoryPanel-Ddg6JIi3.js} +1 -1
  4. package/assets/assets/{BacklinksPage-DroPpjd3.js → BacklinksPage-DI9BmaYt.js} +1 -1
  5. package/assets/assets/{HistoryPage-2o6p-ZDF.js → HistoryPage-CACIL005.js} +1 -1
  6. package/assets/assets/MeasurementPropertyPage-D_i3t0DT.js +1 -0
  7. package/assets/assets/{ProjectPage-CXV64SBB.js → ProjectPage-d1djqfh3.js} +7 -7
  8. package/assets/assets/{RunRow-CIhSIAoR.js → RunRow-CjhvvA6g.js} +1 -1
  9. package/assets/assets/{RunsPage-lO9xMTSl.js → RunsPage-DuBhw2uc.js} +1 -1
  10. package/assets/assets/{SettingsPage-CBwns7cb.js → SettingsPage-MUpfB0HH.js} +1 -1
  11. package/assets/assets/{SiteHealthSection-C372wrfT.js → SiteHealthSection-6qdXlFcO.js} +3 -3
  12. package/assets/assets/{TrafficPage-DGyS-IjJ.js → TrafficPage--nGSOgqn.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-BBaz_ivE.js → TrafficSourceDetailPage-BPqNmuws.js} +1 -1
  14. package/assets/assets/{extract-error-message-QcHrDOjc.js → extract-error-message-B7OeWj0x.js} +1 -1
  15. package/assets/assets/{index-CbnQjcJj.js → index-C4lreGwR.js} +24 -24
  16. package/assets/assets/index-DX5M4MHr.css +1 -0
  17. package/assets/assets/{react-sigma_core.esm.min-D9tjGma4.js → react-sigma_core.esm.min-jiMNkILi.js} +1 -1
  18. package/assets/assets/v2-overview-adapter-Caj3bHQE.js +1 -0
  19. package/assets/index.html +2 -2
  20. package/dist/{chunk-DFIK6IPP.js → chunk-25H4K7KL.js} +4 -4
  21. package/dist/{chunk-6UO23AKO.js → chunk-AIAVZY6L.js} +12 -10
  22. package/dist/{chunk-7YS62CJ4.js → chunk-G2K5KCUY.js} +172 -34
  23. package/dist/{chunk-RCX6I3DX.js → chunk-NOIK3FUO.js} +75 -4
  24. package/dist/{chunk-YVWYQBXW.js → chunk-NZCKJMLC.js} +2 -2
  25. package/dist/{chunk-WZL73LY3.js → chunk-QTETJEHI.js} +2 -2
  26. package/dist/{chunk-NEP2PJ2T.js → chunk-VHPGBX4P.js} +3 -3
  27. package/dist/cli.js +53 -13
  28. package/dist/{demo-server-ERFY4Q2P.js → demo-server-UI24OAHX.js} +4 -4
  29. package/dist/index.js +6 -6
  30. package/dist/{intelligence-service-PULUYOPR.js → intelligence-service-ZKFLGJ23.js} +2 -2
  31. package/dist/mcp.js +4 -4
  32. package/package.json +8 -8
  33. package/assets/assets/MeasurementPropertyPage-VTGLyVuD.js +0 -1
  34. package/assets/assets/index-D0bY1wpI.css +0 -1
  35. package/assets/assets/v2-overview-adapter-CLlJkExr.js +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-RCX6I3DX.js";
872
+ } from "./chunk-NOIK3FUO.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 = [
@@ -13504,7 +13523,10 @@ function selectSentimentSources(db, projectId, filters) {
13504
13523
  const selectedRuns = db.select(sourceRunColumns).from(runs).where(and3(...conditions)).orderBy(desc2(runs.createdAt)).limit(101).all();
13505
13524
  if (selectedRuns.length > 100) throw validationError("Selection exceeds 100 runs; narrow the date range.");
13506
13525
  if (requestedIds && new Set(selectedRuns.map((row) => row.id)).size !== new Set(requestedIds).size) throw validationError("One or more selected runs are unavailable in this project.");
13507
- const out = { assessments: [], runIds: selectedRuns.map((row) => row.id), sourceCoverage: { expected: 0, completed: 0 }, skipped: {}, skippedByRun: {} };
13526
+ const out = { assessments: [], runIds: selectedRuns.map((row) => row.id), sourceCoverage: { expected: 0, completed: 0 }, sourceSlotIds: { expected: [], completed: [] }, skipped: {}, skippedByRun: {} };
13527
+ const recordSlots = (runId, slots) => {
13528
+ for (const key of ["expected", "completed"]) out.sourceSlotIds[key].push(...slots[key].map((slot) => pairKey(runId, slot)));
13529
+ };
13508
13530
  let currentRunId = "";
13509
13531
  const skip = (reason, count4 = 1) => {
13510
13532
  out.skipped[reason] = (out.skipped[reason] ?? 0) + count4;
@@ -13531,6 +13553,7 @@ function selectSentimentSources(db, projectId, filters) {
13531
13553
  const completedSuperseded = run.status === "superseded" && Boolean(db.select({ sequence: sentimentCompletionReceipts.sequence }).from(sentimentCompletionReceipts).where(and3(eq8(sentimentCompletionReceipts.projectId, projectId), eq8(sentimentCompletionReceipts.runId, run.id))).get());
13532
13554
  if (run.status !== "completed" && !completedSuperseded) {
13533
13555
  const coverage = selectedSlotCoverage(db, projectId, run, filters, plans);
13556
+ recordSlots(run.id, coverage.slotIds);
13534
13557
  out.sourceCoverage.expected += coverage.expected;
13535
13558
  out.sourceCoverage.completed += coverage.completed;
13536
13559
  skip(run.status === "failed" ? "failed-run" : "incomplete-run");
@@ -13555,6 +13578,7 @@ function selectSentimentSources(db, projectId, filters) {
13555
13578
  out.sourceCoverage.completed += slots.executed;
13556
13579
  if (!slots.planned || !slots.readable || slots.missing.length > 0 || slots.hasUnboundSnapshot) {
13557
13580
  const coverage = selectedSlotCoverage(db, projectId, run, filters, plans);
13581
+ recordSlots(run.id, coverage.slotIds);
13558
13582
  out.sourceCoverage.expected = beforeCoverage.expected + coverage.expected;
13559
13583
  out.sourceCoverage.completed = beforeCoverage.completed + coverage.completed;
13560
13584
  skip("incomplete-run");
@@ -13626,12 +13650,14 @@ function selectSentimentSources(db, projectId, filters) {
13626
13650
  out.sourceCoverage.completed += expected.filter((key) => recorded.has(key)).length;
13627
13651
  if (!expected.length || expected.some((key) => !recorded.has(key)) || snapshots.length !== expected.length || snapshots.some(({ snapshot, query }) => !expectedSlots.has(simpleSlotKey(query?.queryId, snapshot.provider)))) {
13628
13652
  const coverage = selectedSlotCoverage(db, projectId, run, filters, plans);
13653
+ recordSlots(run.id, coverage.slotIds);
13629
13654
  out.sourceCoverage.expected = beforeCoverage.expected + coverage.expected;
13630
13655
  out.sourceCoverage.completed = beforeCoverage.completed + coverage.completed;
13631
13656
  skip("incomplete-run");
13632
13657
  continue;
13633
13658
  }
13634
13659
  const identity = definition.identity;
13660
+ const qualifiedAliases = [...identity.qualifiedAliases ?? []].sort();
13635
13661
  const names = effectiveBrandNames(identity);
13636
13662
  const urls = [identity.canonicalDomain, ...identity.ownedDomains];
13637
13663
  const mentionNotApplicable = !names.length && !urls.some((url) => url.trim());
@@ -13653,14 +13679,15 @@ function selectSentimentSources(db, projectId, filters) {
13653
13679
  language: definition.language,
13654
13680
  queryText: query.queryText,
13655
13681
  revision: null,
13656
- 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 },
13657
13683
  edges: [edge]
13658
13684
  });
13659
13685
  }
13660
13686
  }
13661
- const selectedSlots = new Set(out.assessments.slice(beforeAssessments).map((item2) => item2.snapshotId)).size;
13662
- out.sourceCoverage.expected = beforeCoverage.expected + selectedSlots;
13663
- out.sourceCoverage.completed = beforeCoverage.completed + selectedSlots;
13687
+ const selectedSlots = [...new Set(out.assessments.slice(beforeAssessments).map((item2) => item2.snapshotId))];
13688
+ recordSlots(run.id, { expected: selectedSlots, completed: selectedSlots });
13689
+ out.sourceCoverage.expected = beforeCoverage.expected + selectedSlots.length;
13690
+ out.sourceCoverage.completed = beforeCoverage.completed + selectedSlots.length;
13664
13691
  }
13665
13692
  return out;
13666
13693
  }
@@ -13707,7 +13734,8 @@ function indexedPlan(db, projectId, versionId, cache) {
13707
13734
  }
13708
13735
  function selectedSlotCoverage(db, projectId, run, filters, plans) {
13709
13736
  const expected = /* @__PURE__ */ new Set(), completed = /* @__PURE__ */ new Set();
13710
- if (run.status === "failed") return { expected: 0, completed: 0 };
13737
+ const coverage = () => ({ expected: expected.size, completed: completed.size, slotIds: { expected: [...expected], completed: [...completed] } });
13738
+ if (run.status === "failed") return coverage();
13711
13739
  const snapshots = db.select({ queryId: querySnapshots.queryId, queryText: querySnapshots.queryText, provider: querySnapshots.provider, servedModel: querySnapshots.servedModel, measurementExecutionId: querySnapshots.measurementExecutionId }).from(querySnapshots).where(eq8(querySnapshots.runId, run.id)).all();
13712
13740
  const count4 = (key, edge, recorded) => {
13713
13741
  if (!matchesSentimentEdge(edge, filters)) return;
@@ -13716,7 +13744,7 @@ function selectedSlotCoverage(db, projectId, run, filters, plans) {
13716
13744
  };
13717
13745
  if (run.measurementPlanVersionId) {
13718
13746
  const indexed = indexedPlan(db, projectId, run.measurementPlanVersionId, plans);
13719
- if (!indexed || filters.revision !== void 0 && indexed.revision !== filters.revision) return { expected: 0, completed: 0 };
13747
+ if (!indexed || filters.revision !== void 0 && indexed.revision !== filters.revision) return coverage();
13720
13748
  const bySlot = /* @__PURE__ */ new Map();
13721
13749
  for (const snapshot of snapshots) if (snapshot.measurementExecutionId !== null) keep(bySlot, pairKey(snapshot.measurementExecutionId, snapshot.provider), snapshot);
13722
13750
  const scopedTargets = run.measurementScope?.resolvedTargets.length ? new Set(run.measurementScope.resolvedTargets) : null;
@@ -13733,10 +13761,10 @@ function selectedSlotCoverage(db, projectId, run, filters, plans) {
13733
13761
  }
13734
13762
  }
13735
13763
  } else {
13736
- if (filters.revision !== void 0 || filters.scope && filters.scope !== "project" || filters.marketKey) return { expected: 0, completed: 0 };
13764
+ if (filters.revision !== void 0 || filters.scope && filters.scope !== "project" || filters.marketKey) return coverage();
13737
13765
  const row = db.select({ definition: simpleMeasurementDefinitions.definition }).from(simpleMeasurementDefinitions).where(eq8(simpleMeasurementDefinitions.runId, run.id)).get();
13738
13766
  const stored = simpleMeasurementDefinitionSchema.safeParse(row?.definition);
13739
- if (!stored.success) return { expected: 0, completed: 0 };
13767
+ if (!stored.success) return coverage();
13740
13768
  const sourceQuery = simpleSourceQueries(stored.data);
13741
13769
  const bySlot = /* @__PURE__ */ new Map();
13742
13770
  for (const snapshot of snapshots) {
@@ -13749,7 +13777,7 @@ function selectedSlotCoverage(db, projectId, run, filters, plans) {
13749
13777
  count4(`${query.queryId}:${engine.provider}`, { queryKey: query.queryId, queryText: query.queryText, executionNodeKey: null, queryClass: query.queryClass, propertyKey: run.projectId, groupKeys: [], marketKeys: [], provider: engine.provider, sourceModel: engine.requestedModel, servedModel: snapshot?.servedModel ?? null, context: stored.data.location }, Boolean(snapshot));
13750
13778
  }
13751
13779
  }
13752
- return { expected: expected.size, completed: completed.size };
13780
+ return coverage();
13753
13781
  }
13754
13782
  function matchesSentimentEdge(edge, filters) {
13755
13783
  if (edge.queryClass !== (filters.queryClass ?? "branded")) return false;
@@ -13836,7 +13864,7 @@ function sentimentClassifierInput(source, definition) {
13836
13864
  }
13837
13865
 
13838
13866
  // ../api-routes/src/sentiment-service.ts
13839
- 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.";
13840
13868
  var FULL_ROWS = { include: /* @__PURE__ */ new Set(["assessments", "locations"]), limit: null, after: null };
13841
13869
  var COMPACT_ROWS = { include: /* @__PURE__ */ new Set(), limit: null, after: null };
13842
13870
  var OPERATIONAL_OUTCOMES = /* @__PURE__ */ new Set([SentimentOutcomes.pending, SentimentOutcomes.running, SentimentOutcomes["waiting-to-retry"], SentimentOutcomes.failed, SentimentOutcomes.canceled]);
@@ -14154,26 +14182,12 @@ var SentimentService = class {
14154
14182
  const selections = resolved.map((entry) => entry.selection), sources = resolved.map((entry) => entry.source);
14155
14183
  const runIds = selections.flatMap((selection2) => selection2.runId ? [selection2.runId] : []).sort();
14156
14184
  const definitions = new Set(selections.flatMap((selection2) => selection2.evaluationDefinitionId ? [selection2.evaluationDefinitionId] : []));
14157
- const modes = new Set(selections.map((selection2) => selection2.mode));
14158
- const revisions = new Set(selections.map((selection2) => selection2.revision));
14159
- const source = { assessments: sources.flatMap((source2) => source2.assessments), runIds, sourceCoverage: { expected: sources.reduce((sum, source2) => sum + source2.sourceCoverage.expected, 0), completed: sources.reduce((sum, source2) => sum + source2.sourceCoverage.completed, 0) }, skipped: sources.reduce((counts2, source2) => {
14185
+ const source = { assessments: sources.flatMap((source2) => source2.assessments), runIds, sourceCoverage: { expected: sources.reduce((sum, source2) => sum + source2.sourceCoverage.expected, 0), completed: sources.reduce((sum, source2) => sum + source2.sourceCoverage.completed, 0) }, sourceSlotIds: { expected: sources.flatMap((source2) => source2.sourceSlotIds.expected), completed: sources.flatMap((source2) => source2.sourceSlotIds.completed) }, skipped: sources.reduce((counts2, source2) => {
14160
14186
  for (const [reason2, count4] of Object.entries(source2.skipped)) counts2[reason2] = (counts2[reason2] ?? 0) + count4;
14161
14187
  return counts2;
14162
14188
  }, {}) };
14163
14189
  const selection = { ...selections[0], runId: runIds.length === 1 ? runIds[0] : null, runIds: runIds.length > 1 ? runIds : void 0, evaluationDefinitionId: definitions.size === 1 ? [...definitions][0] : null };
14164
- let reason = definitions.size > 1 ? "evaluation-definition-changed" : modes.size > 1 ? "source-mode-changed" : revisions.size > 1 ? "measurement-revision-changed" : null;
14165
- const subjects = /* @__PURE__ */ new Map(), queries2 = /* @__PURE__ */ new Map();
14166
- const filter = sourceFilter(query);
14167
- for (const assessment of source.assessments) {
14168
- const identity = sentimentHash(assessment.subject), prior = subjects.get(assessment.subject.key);
14169
- if (prior && prior !== identity) reason ??= "subject-identity-changed";
14170
- subjects.set(assessment.subject.key, identity);
14171
- for (const edge of assessment.edges.filter((edge2) => matchesSentimentEdge(edge2, filter))) {
14172
- const previous = queries2.get(edge.queryKey);
14173
- if (previous && previous !== edge.queryText) reason ??= "query-identity-changed";
14174
- queries2.set(edge.queryKey, edge.queryText);
14175
- }
14176
- }
14190
+ const reason = incompatibleSelectionReason(selections, source.assessments, sourceFilter(query));
14177
14191
  return { selection, items, source, reason, fingerprint: sentimentHash(selections) };
14178
14192
  }
14179
14193
  /** Incompatible pooled identities replace every score in the response with an unavailable one. */
@@ -14205,6 +14219,7 @@ var SentimentService = class {
14205
14219
  overview(projectId, runIds, location) {
14206
14220
  const settings = this.settings(projectId);
14207
14221
  const configured = settings.enabled && settings.installEnabled;
14222
+ const reads = [];
14208
14223
  const headline = (queryClass) => {
14209
14224
  if (!configured || !runIds.length) {
14210
14225
  const empty = { mode: "simple", scope: "project", queryClass, runId: null, revision: null, evaluationDefinitionId: null };
@@ -14212,10 +14227,12 @@ var SentimentService = class {
14212
14227
  return { state: result2.state, reason: result2.reason, provisional: result2.provisional, coverage: result2.coverage, score: result2.score, selection: empty, runIds: [] };
14213
14228
  }
14214
14229
  const read = this.readSelection(projectId, { mode: "auto", scope: "project", queryClass, runIds, ...location ? { location } : {} });
14230
+ reads.push(read);
14215
14231
  const { summary: result } = this.present(read, settings, null);
14216
14232
  return { state: result.state, reason: result.reason, provisional: result.provisional, coverage: result.coverage, score: result.score, selection: result.selection, runIds: result.selection.runIds ?? (result.selection.runId ? [result.selection.runId] : []) };
14217
14233
  };
14218
- return { configured, branded: headline("branded"), nonBrand: headline("non-brand") };
14234
+ const branded = headline("branded"), nonBrand = headline("non-brand");
14235
+ return { configured, branded, nonBrand, overall: overallSentiment(reads, !configured) };
14219
14236
  }
14220
14237
  evidence(projectId, query, limit, cursor) {
14221
14238
  const { assessmentId, ...sourceSelection } = query;
@@ -14276,7 +14293,44 @@ var SentimentService = class {
14276
14293
  };
14277
14294
  var jobAttemptJoin = and4(eq9(sentimentJobItems.workItemId, sentimentAttempts.workItemId), eq9(sentimentJobItems.projectId, sentimentAttempts.projectId));
14278
14295
  function emptySource() {
14279
- return { assessments: [], runIds: [], sourceCoverage: { expected: 0, completed: 0 }, skipped: {} };
14296
+ return { assessments: [], runIds: [], sourceCoverage: { expected: 0, completed: 0 }, sourceSlotIds: { expected: [], completed: [] }, skipped: {} };
14297
+ }
14298
+ function incompatibleSelectionReason(selections, assessments, filter) {
14299
+ const definitions = new Set(selections.flatMap((selection) => selection.evaluationDefinitionId ? [selection.evaluationDefinitionId] : []));
14300
+ const modes = new Set(selections.map((selection) => selection.mode));
14301
+ const revisions = new Set(selections.map((selection) => selection.revision));
14302
+ let reason = definitions.size > 1 ? "evaluation-definition-changed" : modes.size > 1 ? "source-mode-changed" : revisions.size > 1 ? "measurement-revision-changed" : null;
14303
+ const subjects = /* @__PURE__ */ new Map(), queries2 = /* @__PURE__ */ new Map();
14304
+ for (const assessment of assessments) {
14305
+ const identity = sentimentHash(assessment.subject), prior = subjects.get(assessment.subject.key);
14306
+ if (prior && prior !== identity) reason ??= "subject-identity-changed";
14307
+ subjects.set(assessment.subject.key, identity);
14308
+ for (const edge of assessment.edges) {
14309
+ if (filter && !matchesSentimentEdge(edge, filter)) continue;
14310
+ const previous = queries2.get(edge.queryKey);
14311
+ if (previous && previous !== edge.queryText) reason ??= "query-identity-changed";
14312
+ queries2.set(edge.queryKey, edge.queryText);
14313
+ }
14314
+ }
14315
+ return reason;
14316
+ }
14317
+ function overallSentiment(reads, disabled) {
14318
+ const assessmentKey = (item2) => JSON.stringify([item2.snapshotId, item2.subject.key]);
14319
+ const assessments = reads.flatMap((read) => read.source.assessments);
14320
+ const eligible = [...new Map(assessments.map((item2) => [assessmentKey(item2), item2])).values()];
14321
+ const selected = [...new Map(reads.flatMap((read) => read.items).map((item2) => [assessmentKey(item2.member), item2])).values()];
14322
+ const slots = {
14323
+ expected: new Set(reads.flatMap((read) => read.source.sourceSlotIds.expected)).size,
14324
+ completed: new Set(reads.flatMap((read) => read.source.sourceSlotIds.completed)).size
14325
+ };
14326
+ const result = scoreAssessments(selected, eligible, { disabled, slots, incomplete: reads.some((read) => Boolean(read.source.skipped["incomplete-run"])) });
14327
+ const reason = reads.find((read) => read.reason)?.reason ?? incompatibleSelectionReason(reads.map((read) => read.selection), assessments);
14328
+ return {
14329
+ ...result,
14330
+ ...!disabled && reason ? { state: "unsupported", reason, provisional: true, score: aggregateSentiment([]).score } : {},
14331
+ queryClass: "all",
14332
+ runIds: [...new Set(reads.flatMap((read) => read.source.runIds))].sort()
14333
+ };
14280
14334
  }
14281
14335
  function append2(map, key, value) {
14282
14336
  const list = map.get(key);
@@ -16832,6 +16886,7 @@ async function projectRoutes(app, opts) {
16832
16886
  const id = crypto9.randomUUID();
16833
16887
  const now = (/* @__PURE__ */ new Date()).toISOString();
16834
16888
  const nextAliases = normalizeProjectAliases(body.displayName, body.aliases ?? []);
16889
+ const qualifiedAliases = body.qualifiedAliases === void 0 ? [] : requireQualifiedAliases({ displayName: body.displayName, aliases: nextAliases }, body.qualifiedAliases, []);
16835
16890
  const inserted = app.db.transaction((tx) => {
16836
16891
  const result = tx.insert(projects).values({
16837
16892
  id,
@@ -16840,6 +16895,7 @@ async function projectRoutes(app, opts) {
16840
16895
  canonicalDomain,
16841
16896
  ownedDomains: body.ownedDomains ?? [],
16842
16897
  aliases: nextAliases,
16898
+ qualifiedAliases,
16843
16899
  country: body.country,
16844
16900
  language: body.language,
16845
16901
  tags: body.tags ?? [],
@@ -16926,6 +16982,9 @@ async function projectRoutes(app, opts) {
16926
16982
  const nextNegativeReviewMaxStars = body.negativeReviewMaxStars !== void 0 ? body.negativeReviewMaxStars : existing ? readNegativeReviewMaxStars(app.db, existing.id) : null;
16927
16983
  const nextMeasurement = body.measurement ?? existing?.measurement ?? DEFAULT_MEASUREMENT_CONFIG;
16928
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;
16929
16988
  if (existing) {
16930
16989
  const prevAliases = existing.aliases;
16931
16990
  const aliasesChanged = !aliasArraysEqual(prevAliases, nextAliases);
@@ -16935,6 +16994,7 @@ async function projectRoutes(app, opts) {
16935
16994
  canonicalDomain: body.canonicalDomain,
16936
16995
  ownedDomains: body.ownedDomains ?? [],
16937
16996
  aliases: nextAliases,
16997
+ qualifiedAliases: nextQualifiedAliases,
16938
16998
  country: body.country,
16939
16999
  language: body.language,
16940
17000
  tags: body.tags ?? [],
@@ -16973,6 +17033,7 @@ async function projectRoutes(app, opts) {
16973
17033
  canonicalDomain: body.canonicalDomain,
16974
17034
  ownedDomains: body.ownedDomains ?? [],
16975
17035
  aliases: nextAliases,
17036
+ qualifiedAliases: nextQualifiedAliases,
16976
17037
  country: body.country,
16977
17038
  language: body.language,
16978
17039
  tags: body.tags ?? [],
@@ -17161,6 +17222,11 @@ async function projectRoutes(app, opts) {
17161
17222
  eq15(schedules.kind, SchedulableRunKinds["answer-visibility"])
17162
17223
  )).get();
17163
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;
17164
17230
  const config = {
17165
17231
  apiVersion: "canonry/v1",
17166
17232
  kind: "Project",
@@ -17173,6 +17239,7 @@ async function projectRoutes(app, opts) {
17173
17239
  canonicalDomain: project.canonicalDomain,
17174
17240
  ownedDomains: project.ownedDomains,
17175
17241
  aliases: project.aliases,
17242
+ ...exportedQualifiedAliases.length ? { qualifiedAliases: exportedQualifiedAliases } : {},
17176
17243
  country: project.country,
17177
17244
  language: project.language,
17178
17245
  queries: qs.map((q) => q.query),
@@ -17212,6 +17279,32 @@ function assertProviderModelScope(request, current, next, nextProviders) {
17212
17279
  if (providerModelsEqual(effectiveCurrent, effectiveNext)) return;
17213
17280
  requireScope(request, SETTINGS_WRITE_SCOPE);
17214
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
+ }
17215
17308
  function providerModelsEqual(a, b) {
17216
17309
  const aKeys = Object.keys(a);
17217
17310
  if (aKeys.length !== Object.keys(b).length) return false;
@@ -17225,6 +17318,7 @@ function formatProject(row, negativeReviewMaxStars) {
17225
17318
  canonicalDomain: row.canonicalDomain,
17226
17319
  ownedDomains: row.ownedDomains,
17227
17320
  aliases: row.aliases,
17321
+ qualifiedAliases: row.qualifiedAliases,
17228
17322
  country: row.country,
17229
17323
  language: row.language,
17230
17324
  tags: row.tags,
@@ -17943,12 +18037,16 @@ async function competitorRoutes(app) {
17943
18037
  createdAt: now
17944
18038
  }).run();
17945
18039
  }
18040
+ const droppedQualifiedAliases = pruneQualifiedAliasesForCompetitors(tx, project.id, now);
17946
18041
  writeAuditLog(tx, {
17947
18042
  projectId: project.id,
17948
18043
  actor: "api",
17949
18044
  action: "competitors.replaced",
17950
18045
  entityType: "competitor",
17951
- diff: { competitors: normalizedCompetitors }
18046
+ diff: {
18047
+ competitors: normalizedCompetitors,
18048
+ ...droppedQualifiedAliases.length ? { droppedQualifiedAliases } : {}
18049
+ }
17952
18050
  });
17953
18051
  });
17954
18052
  const rows = app.db.select().from(competitors).where(eq19(competitors.projectId, project.id)).all();
@@ -17975,12 +18073,13 @@ async function competitorRoutes(app) {
17975
18073
  target: [competitors.projectId, competitors.domain]
17976
18074
  }).run();
17977
18075
  }
18076
+ const droppedQualifiedAliases = pruneQualifiedAliasesForCompetitors(tx, project.id, now);
17978
18077
  writeAuditLog(tx, {
17979
18078
  projectId: project.id,
17980
18079
  actor: "api",
17981
18080
  action: "competitors.appended",
17982
18081
  entityType: "competitor",
17983
- diff: { added }
18082
+ diff: { added, ...droppedQualifiedAliases.length ? { droppedQualifiedAliases } : {} }
17984
18083
  });
17985
18084
  });
17986
18085
  const rows = app.db.select().from(competitors).where(eq19(competitors.projectId, project.id)).all();
@@ -29674,6 +29773,15 @@ async function applyRoutes(app, opts) {
29674
29773
  specProviders
29675
29774
  );
29676
29775
  const specDispatchModes = config.spec.providerDispatchModes === void 0 ? void 0 : validateProviderDispatchModes(config.spec.providerDispatchModes, opts?.providerAdapters);
29776
+ const specCompetitorNames = normalizeCompetitorList2(config.spec.competitors).map(competitorLabelFromDomain);
29777
+ const specQualifiedAliases = config.spec.qualifiedAliases === void 0 ? void 0 : requireQualifiedAliases(
29778
+ {
29779
+ displayName: config.spec.displayName,
29780
+ aliases: normalizeProjectAliases(config.spec.displayName, config.spec.aliases)
29781
+ },
29782
+ config.spec.qualifiedAliases,
29783
+ specCompetitorNames
29784
+ );
29677
29785
  let resolvedSchedule = null;
29678
29786
  let deleteSchedule = false;
29679
29787
  if (config.spec.schedule) {
@@ -29732,6 +29840,11 @@ async function applyRoutes(app, opts) {
29732
29840
  }
29733
29841
  if (existing) {
29734
29842
  projectId = existing.id;
29843
+ const qualifiedAliases = specQualifiedAliases ?? resolveProjectQualifiedAliases(
29844
+ { displayName: config.spec.displayName, aliases: nextAliases },
29845
+ existing.qualifiedAliases,
29846
+ specCompetitorNames
29847
+ ).value;
29735
29848
  const providerDispatchModes = pruneProviderDispatchModes(
29736
29849
  specDispatchModes ?? existing.providerDispatchModes,
29737
29850
  specProviders,
@@ -29742,6 +29855,7 @@ async function applyRoutes(app, opts) {
29742
29855
  canonicalDomain: config.spec.canonicalDomain,
29743
29856
  ownedDomains: config.spec.ownedDomains ?? [],
29744
29857
  aliases: nextAliases,
29858
+ qualifiedAliases,
29745
29859
  country: config.spec.country,
29746
29860
  language: config.spec.language,
29747
29861
  labels: config.metadata.labels,
@@ -29773,6 +29887,7 @@ async function applyRoutes(app, opts) {
29773
29887
  canonicalDomain: config.spec.canonicalDomain,
29774
29888
  ownedDomains: config.spec.ownedDomains ?? [],
29775
29889
  aliases: nextAliases,
29890
+ qualifiedAliases: specQualifiedAliases ?? [],
29776
29891
  country: config.spec.country,
29777
29892
  language: config.spec.language,
29778
29893
  tags: [],
@@ -29917,6 +30032,7 @@ async function applyRoutes(app, opts) {
29917
30032
  canonicalDomain: project.canonicalDomain,
29918
30033
  ownedDomains: project.ownedDomains,
29919
30034
  aliases: project.aliases,
30035
+ qualifiedAliases: project.qualifiedAliases,
29920
30036
  country: project.country,
29921
30037
  language: project.language,
29922
30038
  tags: project.tags,
@@ -39392,6 +39508,7 @@ function formatProject2(row, negativeReviewMaxStars) {
39392
39508
  canonicalDomain: row.canonicalDomain,
39393
39509
  ownedDomains: row.ownedDomains,
39394
39510
  aliases: row.aliases,
39511
+ qualifiedAliases: row.qualifiedAliases,
39395
39512
  country: row.country,
39396
39513
  language: row.language,
39397
39514
  tags: row.tags,
@@ -87632,13 +87749,18 @@ async function discoveryRoutes(app, opts) {
87632
87749
  createdAt: now
87633
87750
  }).run();
87634
87751
  }
87752
+ const droppedQualifiedAliases = promotedCompetitors.length ? pruneQualifiedAliasesForCompetitors(tx, project.id, now) : [];
87635
87753
  writeAuditLog(tx, {
87636
87754
  projectId: project.id,
87637
87755
  actor: "api",
87638
87756
  action: "discovery.promoted",
87639
87757
  entityType: "discovery_session",
87640
87758
  entityId: session.id,
87641
- diff: { queries: promotedQueries, competitors: promotedCompetitors }
87759
+ diff: {
87760
+ queries: promotedQueries,
87761
+ competitors: promotedCompetitors,
87762
+ ...droppedQualifiedAliases.length ? { droppedQualifiedAliases } : {}
87763
+ }
87642
87764
  });
87643
87765
  });
87644
87766
  }
@@ -90339,12 +90461,28 @@ function captureSimpleMeasurementDefinition(db, input) {
90339
90461
  if (definition.queries.some((query) => query.queryClass !== (classifier?.classify(query.queryText) ?? null))) {
90340
90462
  throw validationError("Query classes must match the captured identity and text.");
90341
90463
  }
90464
+ if (definition.identity.qualifiedAliases !== void 0) {
90465
+ const expected = resolveProjectQualifiedAliases(
90466
+ definition.identity,
90467
+ definition.identity.qualifiedAliases,
90468
+ simpleMeasurementCompetitorNames(definition.competitors)
90469
+ ).value;
90470
+ const captured = definition.identity.qualifiedAliases;
90471
+ if (captured.length !== expected.length || captured.some((name, index2) => name !== expected[index2])) {
90472
+ throw validationError("Qualified aliases must be a sorted subset of the captured aliases.");
90473
+ }
90474
+ }
90342
90475
  const existing = tx.select().from(simpleMeasurementDefinitions).where(and68(eq81(simpleMeasurementDefinitions.projectId, input.projectId), eq81(simpleMeasurementDefinitions.runId, input.runId))).get();
90343
90476
  if (existing) {
90344
90477
  const frozen = simpleMeasurementDefinitionSchema.parse(existing.definition);
90478
+ const { qualifiedAliases: _liveQualifiedAliases, ...liveIdentity } = definition.identity;
90345
90479
  const replayDefinition = {
90346
90480
  ...definition,
90347
- capturedAt: frozen.capturedAt
90481
+ capturedAt: frozen.capturedAt,
90482
+ identity: {
90483
+ ...liveIdentity,
90484
+ ...frozen.identity.qualifiedAliases?.length ? { qualifiedAliases: frozen.identity.qualifiedAliases } : {}
90485
+ }
90348
90486
  };
90349
90487
  const replayChecksum = crypto51.createHash("sha256").update(canonicalSimpleMeasurementDefinitionJson(replayDefinition)).digest("hex");
90350
90488
  if (existing.checksum !== replayChecksum) {