@ainyc/canonry 4.191.0 → 4.192.1

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 (33) hide show
  1. package/assets/assets/{AuditHistoryPanel-C2dvjYNh.js → AuditHistoryPanel-DvqnrQc_.js} +1 -1
  2. package/assets/assets/{BacklinksPage-D1yPQtfF.js → BacklinksPage-BCk2qBfC.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-DLEICrrL.js → ChartPrimitives-Bxa_vFmq.js} +1 -1
  4. package/assets/assets/{HistoryPage-DOEa7naA.js → HistoryPage-IMlkx--Q.js} +1 -1
  5. package/assets/assets/MeasurementPropertyPage-1o1e5vmH.js +1 -0
  6. package/assets/assets/ProjectPage-BusVoYaC.js +8 -0
  7. package/assets/assets/{RunRow-BO7lyC1C.js → RunRow-BherM7NU.js} +1 -1
  8. package/assets/assets/{RunsPage-DHSWfTav.js → RunsPage-B5G_OrmE.js} +1 -1
  9. package/assets/assets/{SettingsPage-oiJdhiq0.js → SettingsPage-CMeCn5uk.js} +1 -1
  10. package/assets/assets/{SiteHealthSection-gVdylFY-.js → SiteHealthSection-DZX1yv4k.js} +3 -3
  11. package/assets/assets/{TrafficPage-YIdwRpQh.js → TrafficPage-BV-HOztp.js} +1 -1
  12. package/assets/assets/{TrafficSourceDetailPage-B4Cop6ox.js → TrafficSourceDetailPage-9IN0q7bd.js} +1 -1
  13. package/assets/assets/{extract-error-message-XavUZNf3.js → extract-error-message-c7zJp5uX.js} +1 -1
  14. package/assets/assets/{index-njfYnO22.js → index-D3KGn0S0.js} +26 -26
  15. package/assets/assets/{react-sigma_core.esm.min-V7giUpEn.js → react-sigma_core.esm.min-DxHkjhKC.js} +1 -1
  16. package/assets/assets/{vendor-lucide-dNHw1I5G.js → vendor-lucide-CW2qe0uH.js} +1 -1
  17. package/assets/assets/{vendor-markdown-9tTeP8v7.js → vendor-markdown-B-Oq2NYq.js} +1 -1
  18. package/assets/assets/{vendor-radix-BpmvLvpp.js → vendor-radix-twS1M3FF.js} +1 -1
  19. package/assets/assets/{vendor-recharts-FA0uySU7.js → vendor-recharts-D-cUuLZo.js} +1 -1
  20. package/assets/assets/{vendor-tanstack-CYBjbqVr.js → vendor-tanstack-ZXXkndVj.js} +1 -1
  21. package/assets/index.html +6 -6
  22. package/dist/{chunk-KBUV3HGS.js → chunk-QRNKMGVD.js} +1351 -1228
  23. package/dist/{chunk-WSD5J6O4.js → chunk-S5QNBQ72.js} +2 -2
  24. package/dist/{chunk-XI6FN52S.js → chunk-T4OO575U.js} +45 -14
  25. package/dist/{chunk-ZK4FJDCH.js → chunk-T6GRXUOE.js} +519 -231
  26. package/dist/{chunk-2A7E5TDU.js → chunk-V4ZLHKOG.js} +5 -4
  27. package/dist/cli.js +14 -10
  28. package/dist/index.js +4 -4
  29. package/dist/{intelligence-service-EVRWZHDG.js → intelligence-service-QYRQHSJH.js} +2 -2
  30. package/dist/mcp.js +3 -3
  31. package/package.json +13 -13
  32. package/assets/assets/MeasurementPropertyPage-D-O8vnMS.js +0 -1
  33. package/assets/assets/ProjectPage-DqTCcimz.js +0 -8
@@ -88,6 +88,7 @@ import {
88
88
  MeasurementRunScopeError,
89
89
  PROJECTS_WRITE_SCOPE,
90
90
  READ_ONLY_SCOPE,
91
+ REPORT_VISIBILITY_COPY,
91
92
  RESEARCH_RUN_SCOPE,
92
93
  ResearchQueryStatuses,
93
94
  ResearchRunStatuses,
@@ -561,7 +562,14 @@ import {
561
562
  reportComparisonWindowDays,
562
563
  reportConfidenceLabel,
563
564
  reportHorizonLabel,
565
+ reportQueryClassLabel,
564
566
  reportSeverityLabel,
567
+ reportVisibilityComparison,
568
+ reportVisibilityEvidence,
569
+ reportVisibilityHistoryLabel,
570
+ reportVisibilityLocationLabel,
571
+ reportVisibilityMeasurementLabel,
572
+ reportVisibilityRate,
565
573
  researchBatchCreateSchema,
566
574
  researchBatchSchema,
567
575
  researchDailyLimitExceeded,
@@ -681,10 +689,10 @@ import {
681
689
  wordpressSchemaDeployResultDtoSchema,
682
690
  wordpressSchemaStatusResultDtoSchema,
683
691
  wordpressStatusDtoSchema
684
- } from "./chunk-KBUV3HGS.js";
692
+ } from "./chunk-QRNKMGVD.js";
685
693
 
686
694
  // src/intelligence-service.ts
687
- import { eq as eq67, desc as desc29, asc as asc12, and as and57, ne as ne10, or as or14, inArray as inArray25, gte as gte18, lte as lte15, isNull as isNull12, sql as sql26, exists } from "drizzle-orm";
695
+ import { eq as eq67, desc as desc29, asc as asc12, and as and57, ne as ne10, or as or15, inArray as inArray25, gte as gte18, lte as lte15, isNull as isNull12, sql as sql26, exists } from "drizzle-orm";
688
696
 
689
697
  // ../db/src/client.ts
690
698
  import { mkdirSync } from "fs";
@@ -8485,14 +8493,14 @@ function buildCitationScorecard(snapshots, queryLookup) {
8485
8493
  const providerRates = providerList.map((provider) => {
8486
8494
  const counts2 = providerCounts.get(provider) ?? { cited: 0, mentioned: 0, total: 0 };
8487
8495
  const citationRate = counts2.total > 0 ? Math.round(counts2.cited / counts2.total * 100) : 0;
8488
- const mentionRate2 = counts2.total > 0 ? Math.round(counts2.mentioned / counts2.total * 100) : 0;
8496
+ const mentionRate3 = counts2.total > 0 ? Math.round(counts2.mentioned / counts2.total * 100) : 0;
8489
8497
  return {
8490
8498
  provider,
8491
8499
  citedCount: counts2.cited,
8492
8500
  mentionedCount: counts2.mentioned,
8493
8501
  totalCount: counts2.total,
8494
8502
  citationRate,
8495
- mentionRate: mentionRate2
8503
+ mentionRate: mentionRate3
8496
8504
  };
8497
8505
  });
8498
8506
  return { queries: queryList, providers: providerList, matrix, providerRates };
@@ -9649,7 +9657,7 @@ function buildRunHistory(runs2, snapshotsByRunId, limit = DEFAULT_RUN_HISTORY_LI
9649
9657
  const citedCount = [...queryCited.values()].filter(Boolean).length;
9650
9658
  const citationRate = totalCount > 0 ? Math.round(citedCount / totalCount * 100) : 0;
9651
9659
  const mentionedCount = [...queryMentioned.values()].filter(Boolean).length;
9652
- const mentionRate2 = totalCount > 0 ? Math.round(mentionedCount / totalCount * 100) : 0;
9660
+ const mentionRate3 = totalCount > 0 ? Math.round(mentionedCount / totalCount * 100) : 0;
9653
9661
  return {
9654
9662
  runId: run.id,
9655
9663
  createdAt: run.createdAt,
@@ -9657,7 +9665,7 @@ function buildRunHistory(runs2, snapshotsByRunId, limit = DEFAULT_RUN_HISTORY_LI
9657
9665
  totalCount,
9658
9666
  citationRate,
9659
9667
  mentionedCount,
9660
- mentionRate: mentionRate2,
9668
+ mentionRate: mentionRate3,
9661
9669
  status: run.status
9662
9670
  };
9663
9671
  });
@@ -13322,7 +13330,7 @@ function normalizeMeasurementLocation(value) {
13322
13330
  return normalized || null;
13323
13331
  }
13324
13332
  function words(value) {
13325
- return value.normalize("NFKC").toLocaleLowerCase("en").match(/[\p{L}\p{N}]+/gu) ?? [];
13333
+ return value.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
13326
13334
  }
13327
13335
  function measurementMentionAliasKey(value) {
13328
13336
  return words(value).join("\0");
@@ -13355,10 +13363,11 @@ function indexMentionAliases(aliases) {
13355
13363
  }
13356
13364
  return byFirstWord;
13357
13365
  }
13358
- function mentionedTargetsForAliases(answerText, aliases) {
13366
+ function mentionedTargetsForAliases(answerText, aliases, preparedWords) {
13359
13367
  const result = /* @__PURE__ */ new Set();
13360
- if (answerText === null) return result;
13361
- const textWords = words(answerText);
13368
+ const unknown = /* @__PURE__ */ new Set();
13369
+ if (answerText === null) return { mentioned: result, unknown };
13370
+ const textWords = preparedWords ?? words(answerText);
13362
13371
  for (let start = 0; start < textWords.length; ) {
13363
13372
  const matches = (aliases.get(textWords[start]) ?? []).filter((alias2) => aliasMatchesAt(textWords, alias2.words, start));
13364
13373
  if (matches.length === 0) {
@@ -13368,16 +13377,46 @@ function mentionedTargetsForAliases(answerText, aliases) {
13368
13377
  const longest = Math.max(...matches.map((match) => match.words.length));
13369
13378
  const owners = sortedUnique(matches.filter((match) => match.words.length === longest).map((match) => match.targetId));
13370
13379
  if (owners.length === 1) result.add(owners[0]);
13380
+ else for (const owner of owners) unknown.add(owner);
13371
13381
  start += longest;
13372
13382
  }
13373
- return result;
13383
+ for (const owner of result) unknown.delete(owner);
13384
+ return { mentioned: result, unknown };
13374
13385
  }
13375
- function mentionedTargets(answerText, targets) {
13376
- return mentionedTargetsForAliases(answerText, indexMentionAliases(compiledMentionAliases(targets)));
13386
+ function identityAmbiguityPatterns(aliases) {
13387
+ const patterns = aliases.flatMap((alias2) => {
13388
+ const name = words(alias2).join(" ").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13389
+ if (!name) return [];
13390
+ return [{
13391
+ clarification: new RegExp(`(?:^| )which (?:specific )?${name} (?:(?:(?:do|did) )?you mean|(?:(?:are|were) you|you(?: re| are| were)?) (?:asking about|referring to|talking about))(?= |$)`, "u"),
13392
+ multipleEntities: [
13393
+ new RegExp(`(?:^| )${name} (?:turns out to |can |could |may )?refers? to (?:a )?(?:few |several |multiple |different )`, "u"),
13394
+ new RegExp(`(?:^| )${name} is (?:a |the )?name shared by(?= |$)`, "u"),
13395
+ new RegExp(`(?:^| )(?:several|multiple|different) (?:places|properties|businesses|communities|entities) (?:called|named) ${name}(?= |$)`, "u")
13396
+ ]
13397
+ }];
13398
+ });
13399
+ return {
13400
+ clarification: patterns.map((pattern) => pattern.clarification),
13401
+ multipleEntities: patterns.flatMap((pattern) => pattern.multipleEntities)
13402
+ };
13377
13403
  }
13378
- function targetMentionedInAnswer(answerText, targetId, targets) {
13379
- if (answerText === null) return null;
13380
- return mentionedTargets(answerText, targets).has(targetId);
13404
+ function resolveMentionIdentity(text2, targets, aliases, citedTargetIds, ambiguityPatterns = new Map(targets.map((target) => [target.id, identityAmbiguityPatterns(target.aliases)]))) {
13405
+ const textWords = text2 === null ? [] : words(text2);
13406
+ const state = mentionedTargetsForAliases(text2, aliases, textWords);
13407
+ if (text2 === null) return state;
13408
+ const normalized = textWords.join(" ");
13409
+ for (const target of targets) {
13410
+ if (!state.mentioned.has(target.id)) continue;
13411
+ const patterns = ambiguityPatterns.get(target.id);
13412
+ const qualified = citedTargetIds.has(target.id) || (target.identityAliases?.length ?? 0) > 0 && containsAnyAlias(text2, target.identityAliases);
13413
+ const unresolved = patterns?.clarification.some((pattern) => pattern.test(normalized)) || !qualified && (patterns?.multipleEntities.some((pattern) => pattern.test(normalized)) || (target.identityAliases?.length ?? 0) > 0);
13414
+ if (unresolved) {
13415
+ state.mentioned.delete(target.id);
13416
+ state.unknown.add(target.id);
13417
+ }
13418
+ }
13419
+ return state;
13381
13420
  }
13382
13421
  function observationSource(observation) {
13383
13422
  if (observation.citedUrls !== null) {
@@ -13393,15 +13432,28 @@ function observationSource(observation) {
13393
13432
  complete: observation.historicalCitedUrlsComplete === true
13394
13433
  };
13395
13434
  }
13396
- function prepareReport(input, options = {}) {
13435
+ function prepareReport(input, options = {}, detailedEvidence = true) {
13397
13436
  const ambiguous = /* @__PURE__ */ new Set();
13398
13437
  const unmatched = /* @__PURE__ */ new Set();
13399
13438
  const bridged = /* @__PURE__ */ new Set();
13400
13439
  const candidates = /* @__PURE__ */ new Map();
13401
13440
  const mentionAliases = compiledMentionAliases(input.targets);
13402
13441
  const aliasesByFirstWord = indexMentionAliases(mentionAliases);
13442
+ const ambiguityPatterns = new Map(input.targets.map((target) => [target.id, identityAmbiguityPatterns(target.aliases)]));
13443
+ const slotsByIdentity = /* @__PURE__ */ new Map();
13444
+ const slotsByLegacyIdentity = /* @__PURE__ */ new Map();
13445
+ for (const slot of input.expectedSlots) {
13446
+ for (const [index2, key] of [
13447
+ [slotsByIdentity, JSON.stringify([slot.executionId, slot.provider])],
13448
+ [slotsByLegacyIdentity, JSON.stringify([slot.queryText, slot.provider, normalizeMeasurementLocation(slot.location)])]
13449
+ ]) {
13450
+ const slots = index2.get(key) ?? [];
13451
+ slots.push(slot);
13452
+ index2.set(key, slots);
13453
+ }
13454
+ }
13403
13455
  for (const observation of input.observations) {
13404
- const slots = observation.executionId !== null ? input.expectedSlots.filter((slot2) => slot2.executionId === observation.executionId && slot2.provider === observation.provider) : input.expectedSlots.filter((slot2) => slot2.queryText === observation.queryText && slot2.provider === observation.provider && normalizeMeasurementLocation(slot2.location) === normalizeMeasurementLocation(observation.location));
13456
+ const slots = (observation.executionId !== null ? slotsByIdentity.get(JSON.stringify([observation.executionId, observation.provider])) : slotsByLegacyIdentity.get(JSON.stringify([observation.queryText, observation.provider, normalizeMeasurementLocation(observation.location)]))) ?? [];
13405
13457
  if (slots.length === 0) {
13406
13458
  unmatched.add(observation.id);
13407
13459
  continue;
@@ -13438,7 +13490,9 @@ function prepareReport(input, options = {}) {
13438
13490
  historical: source.historical,
13439
13491
  sourceComplete: source.complete,
13440
13492
  sourceUrls: source.urls,
13441
- mentionedTargetIds: mentionedTargetsForAliases(observation.answerText, aliasesByFirstWord)
13493
+ mentionedTargetIds: /* @__PURE__ */ new Set(),
13494
+ unknownMentionTargetIds: /* @__PURE__ */ new Set(),
13495
+ citedTargetIds: /* @__PURE__ */ new Set()
13442
13496
  });
13443
13497
  }
13444
13498
  const edgesByExecution = /* @__PURE__ */ new Map();
@@ -13462,6 +13516,15 @@ function prepareReport(input, options = {}) {
13462
13516
  mentionAliases.map((alias2) => alias2.targetId)
13463
13517
  );
13464
13518
  for (const observation of observationsBySlot.values()) {
13519
+ const citedTargetIds = new Set(observation.sourceUrls.flatMap((url) => {
13520
+ const source = sourceAttribution(url);
13521
+ return source.classification === "matched" ? source.matchedTargetIds : [];
13522
+ }));
13523
+ const mentions = resolveMentionIdentity(observation.input.answerText, input.targets, aliasesByFirstWord, citedTargetIds, ambiguityPatterns);
13524
+ observation.mentionedTargetIds = mentions.mentioned;
13525
+ observation.unknownMentionTargetIds = mentions.unknown;
13526
+ observation.citedTargetIds = citedTargetIds;
13527
+ if (!detailedEvidence) continue;
13465
13528
  const edges = [...edgesByExecution.get(observation.slot.executionId) ?? []].sort((left, right) => compareText2(left.id, right.id));
13466
13529
  for (const edge of edges) {
13467
13530
  const sources = observation.sourceUrls.map((sourceUrl) => {
@@ -13531,6 +13594,17 @@ function prepareReport(input, options = {}) {
13531
13594
  }
13532
13595
  };
13533
13596
  }
13597
+ function buildMeasurementObservationSignals(input) {
13598
+ const prepared = prepareReport(input, {}, false);
13599
+ return [...prepared.observationsBySlot.values()].map((observation) => ({
13600
+ observationId: observation.input.id,
13601
+ mentionedTargetIds: [...observation.mentionedTargetIds],
13602
+ unknownMentionTargetIds: [...observation.unknownMentionTargetIds],
13603
+ citedTargetIds: [...observation.citedTargetIds],
13604
+ sourceComplete: observation.sourceComplete,
13605
+ sourceUrls: observation.sourceUrls
13606
+ }));
13607
+ }
13534
13608
  function slotsForEdges(expectedSlots, edges) {
13535
13609
  const executionIds = new Set(edges.map((edge) => edge.executionId));
13536
13610
  const slots = /* @__PURE__ */ new Map();
@@ -13591,6 +13665,9 @@ function mentionRate(target, slots, prepared) {
13591
13665
  if (!status.complete || !status.answerComplete) {
13592
13666
  return { numerator: null, denominator: null, rate: null, reason: "incomplete" };
13593
13667
  }
13668
+ if (slots.some((slot) => prepared.observationsBySlot.get(slot.id)?.unknownMentionTargetIds.has(target.id))) {
13669
+ return unavailable("identity-ambiguous");
13670
+ }
13594
13671
  return { numerator, denominator: slots.length, rate: numerator / slots.length };
13595
13672
  }
13596
13673
  function providersFor(slots) {
@@ -13708,6 +13785,7 @@ function buildMeasurementOverviewIndexes(input, prepared, onEvidenceIndexed) {
13708
13785
  const answeredSlotIds = /* @__PURE__ */ new Set();
13709
13786
  const sourceCompleteSlotIds = /* @__PURE__ */ new Set();
13710
13787
  const mentionedSlotIdsByTargetId = /* @__PURE__ */ new Map();
13788
+ const unknownMentionSlotIdsByTargetId = /* @__PURE__ */ new Map();
13711
13789
  for (const [slotId, observation] of prepared.observationsBySlot) {
13712
13790
  if (observation.sourceComplete) sourceCompleteSlotIds.add(slotId);
13713
13791
  if (observation.input.answerText === null) continue;
@@ -13715,6 +13793,9 @@ function buildMeasurementOverviewIndexes(input, prepared, onEvidenceIndexed) {
13715
13793
  for (const targetId of observation.mentionedTargetIds) {
13716
13794
  addToSet(mentionedSlotIdsByTargetId, targetId, slotId);
13717
13795
  }
13796
+ for (const targetId of observation.unknownMentionTargetIds) {
13797
+ addToSet(unknownMentionSlotIdsByTargetId, targetId, slotId);
13798
+ }
13718
13799
  }
13719
13800
  const assignedSlotIdsByEdgeId = /* @__PURE__ */ new Map();
13720
13801
  const ambiguousEvidenceKeysByTargetId = /* @__PURE__ */ new Map();
@@ -13739,6 +13820,7 @@ function buildMeasurementOverviewIndexes(input, prepared, onEvidenceIndexed) {
13739
13820
  answeredSlotIds,
13740
13821
  sourceCompleteSlotIds,
13741
13822
  mentionedSlotIdsByTargetId,
13823
+ unknownMentionSlotIdsByTargetId,
13742
13824
  assignedSlotIdsByEdgeId,
13743
13825
  ambiguousEvidenceKeysByTargetId
13744
13826
  };
@@ -13762,27 +13844,34 @@ function mentionedForEdge(observation, edge, mentionableIds) {
13762
13844
  if (edge.type !== "target") return null;
13763
13845
  if (observation.input.answerText === null) return null;
13764
13846
  if (!mentionableIds.has(edge.targetId)) return null;
13847
+ if (observation.unknownMentionTargetIds.has(edge.targetId)) return null;
13765
13848
  return observation.mentionedTargetIds.has(edge.targetId);
13766
13849
  }
13767
13850
  function mentionableTargets(input, targetIds) {
13768
13851
  return input.targets.filter((target) => targetIds.has(target.id) && target.aliases.some((alias2) => words(alias2).length > 0));
13769
13852
  }
13770
- function scopeMentionRate(input, targetIds, slots, answered2, prepared) {
13853
+ function scopeMentionRate(input, targetIds, slots, answered2, prepared, edges) {
13771
13854
  const mentionable = mentionableTargets(input, targetIds);
13772
13855
  if (mentionable.length === 0) return unavailable("aliasless");
13773
13856
  if (slots.length === 0) return unavailable("no-population");
13774
- if (answered2.length === 0) return unavailable("evidence-incomplete");
13775
- const ids = mentionable.map((target) => target.id);
13776
- const numerator = answered2.filter((slot) => {
13777
- const mentioned = prepared.observationsBySlot.get(slot.id)?.mentionedTargetIds;
13778
- return mentioned !== void 0 && ids.some((id) => mentioned.has(id));
13779
- }).length;
13857
+ if (answered2.length !== slots.length) return unavailable("evidence-incomplete");
13858
+ const mentionableIds = new Set(mentionable.map((target) => target.id));
13859
+ const idsByExecution = /* @__PURE__ */ new Map();
13860
+ for (const edge of edges) if (mentionableIds.has(edge.targetId)) addToSet(idsByExecution, edge.executionId, edge.targetId);
13861
+ let numerator = 0;
13862
+ for (const slot of answered2) {
13863
+ const observation = prepared.observationsBySlot.get(slot.id);
13864
+ const ids = [...idsByExecution.get(slot.executionId) ?? []];
13865
+ if (ids.length === 0) return unavailable("aliasless");
13866
+ if (ids.some((id) => observation.mentionedTargetIds.has(id))) numerator++;
13867
+ else if (ids.some((id) => observation.unknownMentionTargetIds.has(id))) return unavailable("identity-ambiguous");
13868
+ }
13780
13869
  return { numerator, denominator: answered2.length, rate: numerator / answered2.length };
13781
13870
  }
13782
13871
  function indexedScopeCitationRate(slots, edges, indexes) {
13783
13872
  if (slots.length === 0) return unavailable("no-population");
13784
13873
  const basis = slots.filter((slot) => indexes.sourceCompleteSlotIds.has(slot.id));
13785
- if (basis.length === 0) return unavailable("evidence-incomplete");
13874
+ if (basis.length !== slots.length) return unavailable("evidence-incomplete");
13786
13875
  const assignedSlots = /* @__PURE__ */ new Set();
13787
13876
  for (const edge of edges) {
13788
13877
  for (const slotId of indexes.assignedSlotIdsByEdgeId.get(edge.id) ?? []) assignedSlots.add(slotId);
@@ -13794,8 +13883,10 @@ function targetMentionRate(targets, slots, answered2, indexes) {
13794
13883
  const mentionable = targets?.filter((target) => target.aliases.some((alias2) => words(alias2).length > 0)) ?? [];
13795
13884
  if (mentionable.length === 0) return unavailable("aliasless");
13796
13885
  if (slots.length === 0) return unavailable("no-population");
13797
- if (answered2.length === 0) return unavailable("evidence-incomplete");
13886
+ if (answered2.length !== slots.length) return unavailable("evidence-incomplete");
13798
13887
  const mentioned = indexes.mentionedSlotIdsByTargetId.get(mentionable[0].id) ?? /* @__PURE__ */ new Set();
13888
+ const unknown = indexes.unknownMentionSlotIdsByTargetId.get(mentionable[0].id);
13889
+ if (unknown && answered2.some((slot) => unknown.has(slot.id))) return unavailable("identity-ambiguous");
13799
13890
  const numerator = answered2.filter((slot) => mentioned.has(slot.id)).length;
13800
13891
  return { numerator, denominator: answered2.length, rate: numerator / answered2.length };
13801
13892
  }
@@ -13812,13 +13903,23 @@ function scopeBrandPresence(input, slots, answered2, prepared) {
13812
13903
  const numerator = presenceIn(answered2, input.projectBrandNames, prepared);
13813
13904
  return { numerator, denominator: answered2.length, rate: numerator / answered2.length };
13814
13905
  }
13815
- function scopePropertiesMentioned(input, targetIds, slots, answered2, prepared) {
13906
+ function scopePropertiesMentioned(input, targetIds, slots, answered2, indexes) {
13816
13907
  const mentionable = mentionableTargets(input, targetIds);
13817
13908
  if (mentionable.length === 0) return unavailable("aliasless");
13818
13909
  if (slots.length === 0) return unavailable("no-population");
13819
13910
  if (answered2.length === 0) return unavailable("evidence-incomplete");
13820
- const mentioned = new Set(answered2.flatMap((slot) => [...prepared.observationsBySlot.get(slot.id)?.mentionedTargetIds ?? []]));
13821
- const numerator = mentionable.filter((target) => mentioned.has(target.id)).length;
13911
+ let numerator = 0;
13912
+ for (const target of mentionable) {
13913
+ const own = indexedSlotsForEdges(indexes.targetEdgesByTargetId.get(target.id) ?? [], indexes);
13914
+ const mentioned = indexes.mentionedSlotIdsByTargetId.get(target.id);
13915
+ if (mentioned && own.some((slot) => mentioned.has(slot.id))) {
13916
+ numerator++;
13917
+ continue;
13918
+ }
13919
+ const unknown = indexes.unknownMentionSlotIdsByTargetId.get(target.id);
13920
+ if (unknown && own.some((slot) => unknown.has(slot.id))) return unavailable("identity-ambiguous");
13921
+ if (own.length === 0 || own.some((slot) => !indexes.answeredSlotIds.has(slot.id))) return unavailable("evidence-incomplete");
13922
+ }
13822
13923
  return { numerator, denominator: mentionable.length, rate: numerator / mentionable.length };
13823
13924
  }
13824
13925
  function scopeNamedShareOfVoice(identities, answered2, prepared) {
@@ -13875,8 +13976,8 @@ function createMeasurementOverviewEvaluator(input, options = {}) {
13875
13976
  eligibleSlots: slots.length,
13876
13977
  answeredSlots: answered2.length,
13877
13978
  includesHistoricalData: prepared.diagnostics.bridgedObservationIds.length > 0 || prepared.diagnostics.historicalObservationIds.length > 0,
13878
- propertiesMentioned: scopePropertiesMentioned(input, targetIds, slots, answered2, prepared),
13879
- mentionCoverage: scopeMentionRate(input, targetIds, slots, answered2, prepared),
13979
+ propertiesMentioned: scopePropertiesMentioned(input, targetIds, slots, answered2, indexes),
13980
+ mentionCoverage: scopeMentionRate(input, targetIds, slots, answered2, prepared, edges),
13880
13981
  citationCoverage: indexedScopeCitationRate(slots, edges, indexes),
13881
13982
  brandPresence: scopeBrandPresence(input, slots, answered2, prepared),
13882
13983
  namedShareOfVoice: scopeNamedShareOfVoice(namedIdentities ?? [], answered2, prepared),
@@ -14156,6 +14257,7 @@ function buildMeasurementPlanV2ReportInput(revision, plan, manifest, snapshots)
14156
14257
  // The revision already decided this Property cannot be mentioned. Feeding
14157
14258
  // its aliases in anyway would turn "not applicable" into a 0% reading.
14158
14259
  aliases: target.mentionNotApplicable ? [] : target.aliases,
14260
+ ...target.identityAliases === void 0 ? {} : { identityAliases: target.identityAliases },
14159
14261
  urls: target.urlMatchers.map((matcher, index2) => matcherInput(target.stableKey, matcher, index2))
14160
14262
  })),
14161
14263
  groups: plan.groups.map((group) => ({
@@ -14435,6 +14537,8 @@ function activeMeasurementPlan(db, projectId) {
14435
14537
  }
14436
14538
  function metricReason(reason) {
14437
14539
  switch (reason) {
14540
+ case "identity-ambiguous":
14541
+ return "identity_ambiguous";
14438
14542
  case "no-population":
14439
14543
  return "no_population";
14440
14544
  case "incomplete":
@@ -17566,6 +17670,14 @@ function compileMeasurementDraft(authoring, context) {
17566
17670
  aliasClaims.set(identity, target.stableKey);
17567
17671
  });
17568
17672
  const aliases = canonicalStrings(target.aliases);
17673
+ const aliasKeys = aliases.map(measurementMentionAliasKey).filter(Boolean);
17674
+ target.identityAliases?.forEach((identityAlias, index2) => {
17675
+ const identityKey = measurementMentionAliasKey(identityAlias);
17676
+ const qualifiesAlias = aliasKeys.some((aliasKey) => identityKey !== aliasKey && `\0${identityKey}\0`.includes(`\0${aliasKey}\0`));
17677
+ if (!qualifiesAlias) {
17678
+ sink.fail("target-identity-alias-unqualified", "An identity phrase must include a Property alias and additional identifying context.", ["targets", targetIndex, "identityAliases", index2]);
17679
+ }
17680
+ });
17569
17681
  if (aliases.length === 0) {
17570
17682
  sink.warn("target-without-aliases", `Target "${target.label}" has no aliases, so it can be cited but never mentioned.`, ["targets", targetIndex, "aliases"]);
17571
17683
  }
@@ -17574,6 +17686,7 @@ function compileMeasurementDraft(authoring, context) {
17574
17686
  stableKey: target.stableKey,
17575
17687
  label: target.label,
17576
17688
  aliases,
17689
+ ...target.identityAliases === void 0 ? {} : { identityAliases: canonicalStrings(target.identityAliases) },
17577
17690
  urlMatchers: matchers,
17578
17691
  mentionNotApplicable: aliases.length === 0,
17579
17692
  discoveryIdentity: target.discoveryIdentity ?? null
@@ -17908,6 +18021,12 @@ function diffCompiledPlans(active, candidate, activeRevision) {
17908
18021
  };
17909
18022
  }
17910
18023
  function plansAreLabelOnlyVariants(active, candidate) {
18024
+ const marketSurface = (scope) => {
18025
+ const { groupKey: _groupKey, ...population } = scope;
18026
+ return JSON.stringify(canonicalJsonValue({ ...population, label: "" }));
18027
+ };
18028
+ const nextMarkets = new Map((canonicalMeasurementPlanV2(candidate).reportingScopes ?? []).map((scope) => [scope.stableKey, marketSurface(scope)]));
18029
+ if ((canonicalMeasurementPlanV2(active).reportingScopes ?? []).some((scope) => nextMarkets.get(scope.stableKey) !== marketSurface(scope))) return false;
17911
18030
  const surface = (plan) => {
17912
18031
  const doc = canonicalMeasurementPlanV2(plan);
17913
18032
  const stripped = {
@@ -17915,6 +18034,7 @@ function plansAreLabelOnlyVariants(active, candidate) {
17915
18034
  // The checksum includes labels and navigation parents. Neutralize it
17916
18035
  // with those display fields; all measurement semantics stay in the comparison.
17917
18036
  compiledChecksum: "",
18037
+ reportingScopes: [],
17918
18038
  targets: doc.targets.map((target) => ({ ...target, label: "" })),
17919
18039
  groups: doc.groups.map(({ parentGroupKey: _parentGroupKey, ...group }) => ({
17920
18040
  // Parentage changes navigation only. It changes the frozen document and
@@ -18109,6 +18229,7 @@ function mergeTargets(authoring, body) {
18109
18229
  const merged = {
18110
18230
  ...survivor,
18111
18231
  aliases: unique([...survivor.aliases, ...authoring.targets.filter((target) => absorbedSet.has(target.stableKey)).flatMap((target) => target.aliases)]),
18232
+ ...[survivor, ...authoring.targets.filter((target) => absorbedSet.has(target.stableKey))].some((target) => target.identityAliases !== void 0) ? { identityAliases: unique([...survivor.identityAliases ?? [], ...authoring.targets.filter((target) => absorbedSet.has(target.stableKey)).flatMap((target) => target.identityAliases ?? [])]) } : {},
18112
18233
  urlMatchers: unique([...survivor.urlMatchers, ...authoring.targets.filter((target) => absorbedSet.has(target.stableKey)).flatMap((target) => target.urlMatchers)])
18113
18234
  };
18114
18235
  const assignments = [];
@@ -18343,6 +18464,10 @@ function upsertGroup(authoring, body) {
18343
18464
  }
18344
18465
  function upsertMarket(authoring, body) {
18345
18466
  const { market } = parseBody(measurementDraftUpsertMarketRequestSchema, body, "upsert-market");
18467
+ if (market.groupKey !== void 0) {
18468
+ const group = authoring.groups.find((group2) => group2.stableKey === market.groupKey);
18469
+ if (!group || market.usageEdges.some((edge) => !group.targetKeys.includes(edge.targetKey))) throw validationError("A market must link to an existing group containing all its assigned properties.");
18470
+ }
18346
18471
  const included = new Set(authoring.targets.filter((target) => target.status === "included").map((target) => target.stableKey));
18347
18472
  const frozen = new Set(authoring.assignments.flatMap((assignment) => (assignment.executionContexts ?? []).filter((context) => context.executionNodeKey !== void 0).map((context) => measurementV2UsageEdgeKey({ targetKey: assignment.targetKey, queryId: assignment.queryId, executionNodeKey: context.executionNodeKey }))));
18348
18473
  const seen2 = /* @__PURE__ */ new Set();
@@ -19272,6 +19397,7 @@ function seedAuthoring(project, active, context, opts) {
19272
19397
  label: target.label,
19273
19398
  status: "included",
19274
19399
  aliases: [...target.aliases],
19400
+ ...target.identityAliases === void 0 ? {} : { identityAliases: [...target.identityAliases] },
19275
19401
  urlMatchers: target.urlMatchers.map(formatDraftMatcher),
19276
19402
  source: target.discoveryIdentity ? "sitemap" : "manual",
19277
19403
  ...target.discoveryIdentity ? { discoveryIdentity: target.discoveryIdentity } : {}
@@ -21198,6 +21324,7 @@ function questionRows(plan, target, materialized, filters = {}) {
21198
21324
  }
21199
21325
  const ownEdgesByExecution = new Map(materialized.input.usageEdges.filter((edge) => edge.type === "target" && edge.targetId === targetKey).filter((edge) => assignmentsByExecution.has(edge.executionId)).map((edge) => [edge.executionId, edge]));
21200
21326
  const ownEdgeIds = new Set([...ownEdgesByExecution.values()].map((edge) => edge.id));
21327
+ const answerBySlot = new Map(materialized.evidence.answers.filter((answer) => ownEdgeIds.has(answer.usageEdgeId)).map((answer) => [answer.expectedSlotId, answer]));
21201
21328
  const citedSlotIds = new Set(materialized.evidence.evidence.filter((row) => ownEdgeIds.has(row.usageEdgeId) && row.classification === "assigned").map((row) => row.expectedSlotId));
21202
21329
  const incompleteObservationIds = new Set(materialized.evidence.diagnostics.evidenceIncompleteObservationIds);
21203
21330
  const rows = [];
@@ -21229,8 +21356,8 @@ function questionRows(plan, target, materialized, filters = {}) {
21229
21356
  servedModel: snapshot?.servedModel ?? null,
21230
21357
  location: slot.location,
21231
21358
  status: answered2 ? "answered" : "missing",
21232
- mentioned: answered2 && !target.mentionNotApplicable ? targetMentionedInAnswer(answer, targetKey, materialized.input.targets) : null,
21233
- cited: answered2 ? incompleteObservationIds.has(snapshot.id) ? null : citedSlotIds.has(slot.id) : null,
21359
+ mentioned: answered2 && !target.mentionNotApplicable ? answerBySlot.get(slot.id)?.mentioned ?? null : null,
21360
+ cited: answered2 ? citedSlotIds.has(slot.id) ? true : incompleteObservationIds.has(snapshot.id) ? null : false : null,
21234
21361
  recommendedInstead: answered2 ? recommendedInstead(snapshot.recommendedCompetitors, target) : [],
21235
21362
  answerExcerpt: answered2 ? excerptOf(answer) : null,
21236
21363
  edgeId: edge.id
@@ -21411,6 +21538,8 @@ function runIdentity(run) {
21411
21538
  }
21412
21539
  function metricReason2(reason) {
21413
21540
  switch (reason) {
21541
+ case "identity-ambiguous":
21542
+ return "identity_ambiguous";
21414
21543
  case "no-population":
21415
21544
  return "no_population";
21416
21545
  case "incomplete":
@@ -21460,6 +21589,7 @@ function targetAnswers(plan, target, materialized, filters) {
21460
21589
  const assignmentsByExecution = new Map(plan.assignments.filter((assignment) => assignment.targetKey === target.stableKey).filter((assignment) => filters.queryClass === "all" || assignment.queryClass === filters.queryClass).map((assignment) => [assignment.executionNodeKey, assignment]));
21461
21590
  const edgesByExecution = new Map(materialized.input.usageEdges.filter((edge) => edge.type === "target" && edge.targetId === target.stableKey).filter((edge) => assignmentsByExecution.has(edge.executionId)).map((edge) => [edge.executionId, edge]));
21462
21591
  const ownEdgeIds = new Set([...edgesByExecution.values()].map((edge) => edge.id));
21592
+ const answerBySlot = new Map(materialized.evidence.answers.filter((answer) => ownEdgeIds.has(answer.usageEdgeId)).map((answer) => [answer.expectedSlotId, answer]));
21463
21593
  const citedSlotIds = new Set(materialized.evidence.evidence.filter((row) => ownEdgeIds.has(row.usageEdgeId) && row.classification === "assigned").map((row) => row.expectedSlotId));
21464
21594
  const incompleteObservationIds = new Set(materialized.evidence.diagnostics.evidenceIncompleteObservationIds);
21465
21595
  const questionsById = new Map(plan.querySnapshots.map((question) => [question.queryId, question.queryText]));
@@ -21478,8 +21608,8 @@ function targetAnswers(plan, target, materialized, filters) {
21478
21608
  provider: slot.provider,
21479
21609
  question: questionsById.get(assignment.queryId) ?? slot.queryText,
21480
21610
  snapshot,
21481
- mentioned: target.mentionNotApplicable ? null : targetMentionedInAnswer(snapshot.answerText, target.stableKey, materialized.input.targets),
21482
- cited: incompleteObservationIds.has(snapshot.id) ? null : citedSlotIds.has(slot.id)
21611
+ mentioned: target.mentionNotApplicable ? null : answerBySlot.get(slot.id)?.mentioned ?? null,
21612
+ cited: citedSlotIds.has(slot.id) ? true : incompleteObservationIds.has(snapshot.id) ? null : false
21483
21613
  });
21484
21614
  }
21485
21615
  return { expected: slots.length, answers };
@@ -22094,10 +22224,10 @@ function selectedClasses(value) {
22094
22224
  function unavailable4(reason) {
22095
22225
  return { numerator: null, denominator: null, rate: null, reason };
22096
22226
  }
22097
- function rate(values, expected) {
22227
+ function rate(values, expected, unknownReason = "evidence-incomplete") {
22098
22228
  if (expected === 0) return unavailable4("no-population");
22099
22229
  if (values.length !== expected) return unavailable4("incomplete");
22100
- if (values.some((value) => value === null)) return unavailable4("evidence-incomplete");
22230
+ if (values.some((value) => value === null)) return unavailable4(unknownReason);
22101
22231
  const denominator = values.length;
22102
22232
  if (denominator === 0) return unavailable4("no-population");
22103
22233
  const numerator = values.filter(Boolean).length;
@@ -22106,35 +22236,43 @@ function rate(values, expected) {
22106
22236
  function scopeResolution(definition, selection) {
22107
22237
  const project = definition.scopeOptions.find((option2) => option2.kind === "project");
22108
22238
  if (!project) throw new VisibilityReportScopeError("Frozen definition has no project scope.");
22239
+ let option = project;
22240
+ let edgeIds;
22109
22241
  if (selection.scope === "project") {
22110
- return { option: project, edgeIds: new Set(definition.edges.map((edge) => edge.id)) };
22111
- }
22112
- const key = selection.scopeKey;
22113
- if (!key) throw new VisibilityReportScopeError(`${selection.scope} scope requires a scope key.`);
22114
- const option = definition.scopeOptions.find((candidate) => candidate.kind === selection.scope && candidate.id === key);
22115
- if (!option) throw new VisibilityReportScopeError(`${selection.scope} scope "${key}" is not in this frozen definition.`);
22116
- if (selection.scope === "property") {
22117
- return {
22118
- option,
22119
- edgeIds: new Set(definition.edges.filter((edge) => edge.targetKey === key).map((edge) => edge.id))
22120
- };
22242
+ edgeIds = new Set(definition.edges.map((edge) => edge.id));
22243
+ } else {
22244
+ const key = selection.scopeKey;
22245
+ if (!key) throw new VisibilityReportScopeError(`${selection.scope} scope requires a scope key.`);
22246
+ const scoped = definition.scopeOptions.find((candidate) => candidate.kind === selection.scope && candidate.id === key);
22247
+ if (!scoped) throw new VisibilityReportScopeError(`${selection.scope} scope "${key}" is not in this frozen definition.`);
22248
+ option = scoped;
22249
+ if (selection.scope === "property") {
22250
+ edgeIds = new Set(definition.edges.filter((edge) => edge.targetKey === key).map((edge) => edge.id));
22251
+ } else if (selection.scope === "group") {
22252
+ const group = definition.groups.find((candidate) => candidate.id === key);
22253
+ if (!group) throw new VisibilityReportScopeError(`Group "${key}" is not in this frozen definition.`);
22254
+ const targetKeys = new Set(group.targetKeys);
22255
+ edgeIds = new Set(definition.edges.filter((edge) => targetKeys.has(edge.targetKey)).map((edge) => edge.id));
22256
+ } else {
22257
+ edgeIds = new Set(definition.edges.filter((edge) => edge.marketKeys.includes(key)).map((edge) => edge.id));
22258
+ }
22121
22259
  }
22122
- if (selection.scope === "group") {
22123
- const group = definition.groups.find((candidate) => candidate.id === key);
22124
- if (!group) throw new VisibilityReportScopeError(`Group "${key}" is not in this frozen definition.`);
22125
- const targetKeys = new Set(group.targetKeys);
22126
- return {
22127
- option,
22128
- edgeIds: new Set(definition.edges.filter((edge) => targetKeys.has(edge.targetKey)).map((edge) => edge.id))
22129
- };
22260
+ if (selection.scope === "market" && selection.marketKey !== void 0) {
22261
+ throw new VisibilityReportScopeError("marketKey is not valid for market scope.");
22130
22262
  }
22263
+ const marketKey = selection.scope === "market" ? selection.scopeKey : selection.marketKey;
22264
+ if (marketKey === void 0) return { option, edgeIds };
22265
+ const market = definition.scopeOptions.find((candidate) => candidate.kind === "market" && candidate.id === marketKey);
22266
+ if (!market) throw new VisibilityReportScopeError(`Market "${marketKey}" is not in this frozen definition.`);
22131
22267
  return {
22132
22268
  option,
22133
- edgeIds: new Set(definition.edges.filter((edge) => edge.marketKeys.includes(key)).map((edge) => edge.id))
22269
+ market,
22270
+ edgeIds: new Set(definition.edges.filter((edge) => edgeIds.has(edge.id) && edge.marketKeys.includes(marketKey)).map((edge) => edge.id))
22134
22271
  };
22135
22272
  }
22136
22273
  function scopeTargetKeys(definition, selection) {
22137
22274
  const resolution = scopeResolution(definition, selection);
22275
+ if (resolution.market !== void 0) return [...new Set(definition.edges.filter((edge) => resolution.edgeIds.has(edge.id)).map((edge) => edge.targetKey))].sort(compareText14);
22138
22276
  if (selection.scope === "project") return definition.targets.map((target) => target.id).sort(compareText14);
22139
22277
  if (selection.scope === "property") return [resolution.option.id];
22140
22278
  if (selection.scope === "group") {
@@ -22199,17 +22337,30 @@ function targetValues(candidate, targets, targetKey) {
22199
22337
  const targetKeys = targetKey === void 0 ? [...new Set(candidate.edges.map((edge) => edge.targetKey))] : [targetKey];
22200
22338
  const eligible = targetKeys.filter((key) => targets.get(key)?.mentionEligible === true);
22201
22339
  const observation = candidate.observation;
22340
+ const citation = observation && targetKeys.some((key) => observation.citedTargetKeys.includes(key)) ? true : observation?.citationComplete && targetKeys.length > 0 ? false : null;
22202
22341
  const mentionComplete = observation !== null && (observation.mentionComplete ?? observation.answerText !== null);
22203
22342
  if (!observation || !mentionComplete || eligible.length === 0) {
22204
22343
  return {
22205
22344
  mention: null,
22206
- citation: observation?.citationComplete === true && targetKeys.length > 0 ? targetKeys.some((key) => observation.citedTargetKeys.includes(key)) : null
22345
+ citation
22207
22346
  };
22208
22347
  }
22209
- return {
22210
- mention: eligible.some((key) => observation.mentionedTargetKeys.includes(key)),
22211
- citation: observation.citationComplete ? targetKeys.some((key) => observation.citedTargetKeys.includes(key)) : null
22212
- };
22348
+ if (eligible.some((key) => observation.mentionedTargetKeys.includes(key))) return { mention: true, citation };
22349
+ if (eligible.some((key) => observation.unknownMentionTargetKeys?.includes(key))) {
22350
+ return { mention: null, citation, mentionUnavailableReason: "identity-ambiguous" };
22351
+ }
22352
+ return { mention: false, citation };
22353
+ }
22354
+ function citationForCoverage(candidate, targets, targetKey) {
22355
+ return candidate.observation?.citationComplete ? targetValues(candidate, targets, targetKey).citation : null;
22356
+ }
22357
+ function mentionRate2(candidates, targets, targetKey) {
22358
+ const values = candidates.map((candidate) => targetValues(candidate, targets, targetKey));
22359
+ return rate(
22360
+ values.map((value) => value.mention),
22361
+ values.length,
22362
+ values.some((value) => value.mentionUnavailableReason === "identity-ambiguous") ? "identity-ambiguous" : "evidence-incomplete"
22363
+ );
22213
22364
  }
22214
22365
  function answered(candidate) {
22215
22366
  return candidate.observation !== null;
@@ -22223,21 +22374,30 @@ function selectedTargetKeys(candidates) {
22223
22374
  function targetMetrics(candidates, targets, targetKey) {
22224
22375
  const own = candidates.filter((candidate) => candidate.edges.some((edge) => edge.targetKey === targetKey));
22225
22376
  return {
22226
- mentionCoverage: rate(own.map((candidate) => targetValues(candidate, targets, targetKey).mention), own.length),
22227
- citationCoverage: rate(own.map((candidate) => targetValues(candidate, targets, targetKey).citation), own.length)
22377
+ mentionCoverage: mentionRate2(own, targets, targetKey),
22378
+ citationCoverage: rate(own.map((candidate) => citationForCoverage(candidate, targets, targetKey)), own.length)
22379
+ };
22380
+ }
22381
+ function targetPresence(candidates, targets, targetKey) {
22382
+ const values = candidates.filter((candidate) => candidate.edges.some((edge) => edge.targetKey === targetKey)).map((candidate) => targetValues(candidate, targets, targetKey));
22383
+ const anyKnown = (signals) => signals.includes(true) ? true : signals.length === 0 || signals.includes(null) ? null : false;
22384
+ return {
22385
+ targetKey,
22386
+ mention: anyKnown(values.map((value) => value.mention)),
22387
+ citation: anyKnown(values.map((value) => value.citation)),
22388
+ identityAmbiguous: values.some((value) => value.mentionUnavailableReason === "identity-ambiguous")
22228
22389
  };
22229
22390
  }
22230
- function outcomeCounts(candidates, targets, targetKeys) {
22231
- const counts2 = { bothSignals: 0, mentionedOnly: 0, citedOnly: 0, neither: 0, notMeasured: 0, total: targetKeys.length };
22232
- for (const targetKey of targetKeys) {
22233
- const metrics = targetMetrics(candidates, targets, targetKey);
22234
- if (metrics.mentionCoverage.numerator === null || metrics.citationCoverage.numerator === null) {
22391
+ function outcomeCounts(rows) {
22392
+ const counts2 = { bothSignals: 0, mentionedOnly: 0, citedOnly: 0, neither: 0, notMeasured: 0, total: rows.length };
22393
+ for (const row of rows) {
22394
+ if (row.mention === null || row.citation === null) {
22235
22395
  counts2.notMeasured++;
22236
- } else if (metrics.mentionCoverage.numerator > 0 && metrics.citationCoverage.numerator > 0) {
22396
+ } else if (row.mention && row.citation) {
22237
22397
  counts2.bothSignals++;
22238
- } else if (metrics.mentionCoverage.numerator > 0) {
22398
+ } else if (row.mention) {
22239
22399
  counts2.mentionedOnly++;
22240
- } else if (metrics.citationCoverage.numerator > 0) {
22400
+ } else if (row.citation) {
22241
22401
  counts2.citedOnly++;
22242
22402
  } else {
22243
22403
  counts2.neither++;
@@ -22252,19 +22412,20 @@ function coverageSummary(candidates, definition, targets = targetMap(definition)
22252
22412
  rows,
22253
22413
  queryCount: new Set(rows.map((candidate) => candidate.slot.queryKey)).size,
22254
22414
  answerCount: rows.filter(answered).length,
22255
- mentionCoverage: rate(rows.map((candidate) => targetValues(candidate, targets).mention), rows.length),
22256
- citationCoverage: rate(rows.map((candidate) => targetValues(candidate, targets).citation), rows.length)
22415
+ mentionCoverage: mentionRate2(rows, targets),
22416
+ citationCoverage: rate(rows.map((candidate) => citationForCoverage(candidate, targets)), rows.length)
22257
22417
  };
22258
22418
  }
22259
22419
  function summary(candidates, definition, populationTargetKeys = selectedTargetKeys(candidates)) {
22260
22420
  const targets = targetMap(definition);
22261
22421
  const base = coverageSummary(candidates, definition, targets);
22262
22422
  const { rows } = base;
22263
- const targetRows = populationTargetKeys.map((targetKey) => ({ targetKey, ...targetMetrics(rows, targets, targetKey) }));
22423
+ const targetRows = populationTargetKeys.map((targetKey) => targetPresence(rows, targets, targetKey));
22264
22424
  const eligibleTargets = targetRows.filter((row) => targets.get(row.targetKey)?.mentionEligible === true);
22265
22425
  const propertyReach = rate(
22266
- eligibleTargets.map((row) => row.mentionCoverage.numerator === null ? null : row.mentionCoverage.numerator > 0),
22267
- eligibleTargets.length
22426
+ eligibleTargets.map((row) => row.mention),
22427
+ eligibleTargets.length,
22428
+ eligibleTargets.some((row) => row.mention === null && row.identityAmbiguous) ? "identity-ambiguous" : "evidence-incomplete"
22268
22429
  );
22269
22430
  return {
22270
22431
  queryCount: base.queryCount,
@@ -22272,7 +22433,7 @@ function summary(candidates, definition, populationTargetKeys = selectedTargetKe
22272
22433
  mentionCoverage: base.mentionCoverage,
22273
22434
  citationCoverage: base.citationCoverage,
22274
22435
  propertyReach,
22275
- outcomes: outcomeCounts(rows, targets, populationTargetKeys)
22436
+ outcomes: outcomeCounts(targetRows)
22276
22437
  };
22277
22438
  }
22278
22439
  function queryRows(candidates, definition, selection) {
@@ -22297,11 +22458,15 @@ function queryRows(candidates, definition, selection) {
22297
22458
  model: first.observation?.model ?? null,
22298
22459
  location: first.slot.location,
22299
22460
  targetKeys: selectedTargetKeys(rows),
22461
+ ...(() => {
22462
+ const marketKeys = [...new Set(rows.flatMap((row) => row.edges.flatMap((edge) => edge.marketKeys)))].sort(compareText14);
22463
+ return marketKeys.length === 0 ? {} : { marketKeys };
22464
+ })(),
22300
22465
  answerCount: value.answerCount,
22301
22466
  mentionCoverage: value.mentionCoverage,
22302
22467
  citationCoverage: value.citationCoverage
22303
22468
  };
22304
- }).sort((left, right) => compareText14(normalizeText3(left.query), normalizeText3(right.query)) || compareText14(left.provider, right.provider) || compareText14(left.model ?? "", right.model ?? "") || compareText14(left.location ?? "", right.location ?? "") || compareText14(left.queryKey, right.queryKey));
22469
+ }).sort((left, right) => (selection.scope === "property" && selection.marketKey === void 0 ? compareText14((left.marketKeys ?? []).join("\0"), (right.marketKeys ?? []).join("\0")) : 0) || compareText14(normalizeText3(left.query), normalizeText3(right.query)) || compareText14(left.provider, right.provider) || compareText14(left.model ?? "", right.model ?? "") || compareText14(left.location ?? "", right.location ?? "") || compareText14(left.queryKey, right.queryKey));
22305
22470
  }
22306
22471
  function evidenceRows(candidates, definition, selection) {
22307
22472
  if (selection.queryKey === void 0 && selection.queryId === void 0) return [];
@@ -22319,6 +22484,7 @@ function evidenceRows(candidates, definition, selection) {
22319
22484
  location: candidate.slot.location,
22320
22485
  targetKeys: selectedTargetKeys([candidate]),
22321
22486
  mentioned: signals.mention,
22487
+ ...signals.mentionUnavailableReason === void 0 ? {} : { mentionUnavailableReason: signals.mentionUnavailableReason },
22322
22488
  cited: signals.citation,
22323
22489
  // Answer bodies are intentionally limited to the stable query-key
22324
22490
  // drill-in. A query-id is enough to locate an evidence page, but it
@@ -22389,8 +22555,8 @@ function breakdown(candidates, definition, populationTargetKeys = selectedTarget
22389
22555
  const groups = definition.groups.map((group) => ({ group, targetKeys: group.targetKeys.filter((targetKey) => populationTargetSet.has(targetKey)) })).filter(({ targetKeys }) => targetKeys.length > 0).map(({ group, targetKeys }) => {
22390
22556
  const groupTargetKeys = new Set(targetKeys);
22391
22557
  const own = candidates.map((candidate) => narrowedToTargetKeys(candidate, groupTargetKeys)).filter((candidate) => candidate !== null);
22392
- const metrics = rate(own.map((candidate) => targetValues(candidate, targets).mention), own.length);
22393
- const citations = rate(own.map((candidate) => targetValues(candidate, targets).citation), own.length);
22558
+ const metrics = mentionRate2(own, targets);
22559
+ const citations = rate(own.map((candidate) => citationForCoverage(candidate, targets)), own.length);
22394
22560
  return {
22395
22561
  id: group.id,
22396
22562
  label: group.label,
@@ -22435,6 +22601,7 @@ function page2(rows, keyOf, kind, queryClass, input, run) {
22435
22601
  queryClass,
22436
22602
  scope: input.selection.scope,
22437
22603
  scopeKey: input.selection.scopeKey ?? null,
22604
+ marketKey: input.selection.marketKey ?? null,
22438
22605
  provider: input.selection.provider ?? null,
22439
22606
  model: input.selection.model ?? null,
22440
22607
  location: input.selection.location,
@@ -22525,7 +22692,8 @@ function buildVisibilityReport(input) {
22525
22692
  const runs2 = input.runs.filter((run) => !run.probe && inTime(run, input.selection)).sort((left, right) => compareText14(left.createdAt, right.createdAt) || compareText14(left.id, right.id));
22526
22693
  const selectedRun = input.selection.runId === void 0 ? input.preferredRunId === void 0 ? runs2.at(-1) : runs2.find((run) => run.id === input.preferredRunId) : runs2.find((run) => run.id === input.selection.runId);
22527
22694
  const definition = selectedRun?.definition ?? input.activeDefinition;
22528
- const selectedScope = scopeResolution(definition, input.selection).option;
22695
+ const selectedResolution = scopeResolution(definition, input.selection);
22696
+ const selectedScope = selectedResolution.option;
22529
22697
  const populationTargetKeys = scopeTargetKeys(definition, input.selection);
22530
22698
  const classes = selectedClasses(input.selection.queryClass);
22531
22699
  const runCandidates = /* @__PURE__ */ new Map();
@@ -22580,6 +22748,7 @@ function buildVisibilityReport(input) {
22580
22748
  mode: input.mode,
22581
22749
  queryClass: input.selection.queryClass,
22582
22750
  scope: selectedScope,
22751
+ ...selectedResolution.market === void 0 ? {} : { market: selectedResolution.market },
22583
22752
  provider: input.selection.provider ?? null,
22584
22753
  model: input.selection.model ?? null,
22585
22754
  location: input.selection.location,
@@ -22763,16 +22932,47 @@ function v2Definition(plan, revision, manifest, run) {
22763
22932
  for (const group of plan.groups) {
22764
22933
  for (const targetKey of group.targetKeys) groupKeysForTarget.get(targetKey)?.push(group.stableKey);
22765
22934
  }
22935
+ const marketKeysForGroup = /* @__PURE__ */ new Map();
22936
+ const marketKeysForTarget = new Map(plan.targets.map((target) => [target.stableKey, []]));
22937
+ for (const market of plan.reportingScopes ?? []) {
22938
+ if (market.groupKey !== void 0) {
22939
+ const keys = marketKeysForGroup.get(market.groupKey) ?? [];
22940
+ keys.push(market.stableKey);
22941
+ marketKeysForGroup.set(market.groupKey, keys);
22942
+ }
22943
+ for (const edge of market.usageEdges) marketKeysForTarget.get(edge.targetKey)?.push(market.stableKey);
22944
+ }
22766
22945
  const scopeOptions = [
22767
22946
  { id: "project", label: "Project", kind: "project", targetCount: plan.targets.length },
22768
- ...plan.groups.map((group) => ({ id: group.stableKey, label: group.label, kind: "group", targetCount: group.targetKeys.length, ...group.parentGroupKey === void 0 ? {} : { parentGroupIds: [group.parentGroupKey] } })),
22947
+ ...plan.groups.map((group) => {
22948
+ const marketKeys = [...new Set(marketKeysForGroup.get(group.stableKey) ?? [])].sort();
22949
+ return {
22950
+ id: group.stableKey,
22951
+ label: group.label,
22952
+ kind: "group",
22953
+ targetCount: group.targetKeys.length,
22954
+ ...group.parentGroupKey === void 0 ? {} : { parentGroupIds: [group.parentGroupKey] },
22955
+ ...marketKeys.length === 0 ? {} : { marketKeys }
22956
+ };
22957
+ }),
22769
22958
  ...plan.reportingScopes?.map((market) => ({
22770
22959
  id: market.stableKey,
22771
22960
  label: market.label,
22772
22961
  kind: "market",
22773
- targetCount: new Set(market.usageEdges.map((edge) => edge.targetKey)).size
22962
+ targetCount: new Set(market.usageEdges.map((edge) => edge.targetKey)).size,
22963
+ ...market.groupKey === void 0 ? {} : { parentGroupIds: [market.groupKey] }
22774
22964
  })) ?? [],
22775
- ...plan.targets.map((target) => ({ id: target.stableKey, label: target.label, kind: "property", targetCount: 1, parentGroupIds: [...new Set(groupKeysForTarget.get(target.stableKey) ?? [])].sort() }))
22965
+ ...plan.targets.map((target) => {
22966
+ const marketKeys = [...new Set(marketKeysForTarget.get(target.stableKey) ?? [])].sort();
22967
+ return {
22968
+ id: target.stableKey,
22969
+ label: target.label,
22970
+ kind: "property",
22971
+ targetCount: 1,
22972
+ parentGroupIds: [...new Set(groupKeysForTarget.get(target.stableKey) ?? [])].sort(),
22973
+ ...marketKeys.length === 0 ? {} : { marketKeys }
22974
+ };
22975
+ })
22776
22976
  ];
22777
22977
  return {
22778
22978
  revision,
@@ -22785,52 +22985,42 @@ function v2Definition(plan, revision, manifest, run) {
22785
22985
  edges
22786
22986
  };
22787
22987
  }
22788
- function v2Observations(plan, revision, run, snapshots) {
22988
+ function v2Observations(plan, revision, run, snapshots, includeEvidence = true) {
22789
22989
  const manifest = measurementRunExpectedSlots(run, plan);
22790
22990
  const materialized = buildMeasurementPlanV2ReportInput(revision, plan, manifest, adapterSnapshots(snapshots));
22791
- const evidence = buildMeasurementEvidence(materialized.input);
22991
+ const signalsByObservation = new Map(buildMeasurementObservationSignals(materialized.input).map((signal) => [signal.observationId, signal]));
22792
22992
  const rawById = new Map(snapshots.map((snapshot) => [snapshot.id, snapshot]));
22793
- const answersByObservation = /* @__PURE__ */ new Map();
22794
- for (const answer of evidence.answers) {
22795
- const rows = answersByObservation.get(answer.observationId) ?? [];
22796
- rows.push(answer);
22797
- answersByObservation.set(answer.observationId, rows);
22798
- }
22799
- const edgeById = new Map(materialized.input.usageEdges.map((edge) => [edge.id, edge]));
22800
22993
  const competitors2 = exactCompetitorMatchers(plan);
22801
22994
  const output = [];
22802
22995
  for (const observation of materialized.input.observations) {
22803
22996
  const raw = rawById.get(observation.id);
22804
22997
  if (!raw || observation.executionId === null) continue;
22805
- const answers = answersByObservation.get(observation.id) ?? [];
22806
- const targetFor = (answer) => {
22807
- const edge = edgeById.get(answer.usageEdgeId);
22808
- return edge?.type === "target" ? edge.targetId : null;
22809
- };
22810
- const signals = competitorSignals(raw, competitors2);
22998
+ const attribution = signalsByObservation.get(observation.id);
22999
+ const signals = includeEvidence ? competitorSignals(raw, competitors2) : { mentioned: [], cited: [] };
22811
23000
  output.push({
22812
23001
  slotId: `slot:${observation.executionId}:${observation.provider}`,
22813
23002
  answerId: raw.id,
22814
23003
  model: raw.servedModel?.trim() || null,
22815
- answerText: raw.answerText,
23004
+ answerText: includeEvidence ? raw.answerText : null,
22816
23005
  // `answerMentioned` is the legacy project-level boolean. It cannot say
22817
23006
  // WHICH Property an answer named, so a portfolio may use it neither as
22818
23007
  // a positive nor as a measured negative when the answer body is absent.
22819
23008
  mentionComplete: raw.answerText !== null,
22820
- mentionedTargetKeys: answers.filter((answer) => answer.mentioned === true).map(targetFor).filter((value) => value !== null),
22821
- citedTargetKeys: answers.filter((answer) => answer.cited === true).map(targetFor).filter((value) => value !== null),
22822
- citationComplete: answers.length > 0 && answers.every((answer) => answer.evidenceComplete),
23009
+ mentionedTargetKeys: attribution?.mentionedTargetIds ?? [],
23010
+ unknownMentionTargetKeys: attribution?.unknownMentionTargetIds ?? [],
23011
+ citedTargetKeys: attribution?.citedTargetIds ?? [],
23012
+ citationComplete: attribution?.sourceComplete ?? false,
22823
23013
  competitorMentionDomains: signals.mentioned,
22824
23014
  competitorCitationDomains: signals.cited,
22825
- observedCompetitorNames: observedCompetitorNames2(raw.recommendedCompetitors),
22826
- sources: [...new Set(answers.flatMap((answer) => answer.sources.map((source) => source.sourceUrl)))],
23015
+ observedCompetitorNames: includeEvidence ? observedCompetitorNames2(raw.recommendedCompetitors) : [],
23016
+ sources: includeEvidence ? attribution?.sourceUrls ?? [] : [],
22827
23017
  createdAt: raw.createdAt
22828
23018
  });
22829
23019
  }
22830
23020
  return { manifest, observations: output };
22831
23021
  }
22832
- function advancedRun(run, version, plan, snapshots, comparableDefinitionIds) {
22833
- const populated = v2Observations(plan, version.revision, run, snapshots);
23022
+ function advancedRun(run, version, plan, snapshots, comparableDefinitionIds, includeEvidence = true) {
23023
+ const populated = v2Observations(plan, version.revision, run, snapshots, includeEvidence);
22834
23024
  return {
22835
23025
  id: run.id,
22836
23026
  createdAt: run.createdAt,
@@ -22877,7 +23067,7 @@ function simpleModelFingerprint(definition) {
22877
23067
  const engines = [...definition.engines].map((engine) => ({ provider: normalizedText3(engine.provider), requestedModel: engine.requestedModel })).sort((left, right) => left.provider.localeCompare(right.provider, "en"));
22878
23068
  return `simple-model:${createHash4("sha256").update(JSON.stringify(engines)).digest("hex")}`;
22879
23069
  }
22880
- function frozenSimpleRun(run, definition, snapshots) {
23070
+ function frozenSimpleRun(run, definition, snapshots, includeEvidence = true) {
22881
23071
  const matcher = compileBrandAliases(effectiveBrandNames({
22882
23072
  displayName: definition.identity.displayName,
22883
23073
  aliases: definition.identity.aliases,
@@ -22925,20 +23115,20 @@ function frozenSimpleRun(run, definition, snapshots) {
22925
23115
  if (!slotKeys.has(slotId)) throw new Error(`Frozen simple definition has no slot for stored snapshot ${snapshot.id}`);
22926
23116
  const preparedAnswerText = prepareBrandMatchText(snapshot.answerText);
22927
23117
  const mentioned = snapshot.answerText !== null ? matcherMatchesText(matcher, preparedAnswerText) : snapshot.answerMentioned === true;
22928
- const competitorSignalsForSnapshot = competitors2 === null ? { mentioned: [], cited: [] } : competitorSignals(snapshot, competitors2, preparedAnswerText);
23118
+ const competitorSignalsForSnapshot = !includeEvidence || competitors2 === null ? { mentioned: [], cited: [] } : competitorSignals(snapshot, competitors2, preparedAnswerText);
22929
23119
  observations.push({
22930
23120
  slotId,
22931
23121
  answerId: snapshot.id,
22932
23122
  model: snapshot.servedModel?.trim() || null,
22933
- answerText: snapshot.answerText,
23123
+ answerText: includeEvidence ? snapshot.answerText : null,
22934
23124
  mentionComplete: snapshot.answerText !== null || snapshot.answerMentioned !== null,
22935
23125
  mentionedTargetKeys: mentioned ? ["project"] : [],
22936
23126
  citedTargetKeys: snapshot.citationState === "cited" ? ["project"] : [],
22937
23127
  citationComplete: true,
22938
23128
  competitorMentionDomains: competitorSignalsForSnapshot.mentioned,
22939
23129
  competitorCitationDomains: competitorSignalsForSnapshot.cited,
22940
- observedCompetitorNames: observedCompetitorNames2(snapshot.recommendedCompetitors),
22941
- sources: snapshot.citedUrls ?? [],
23130
+ observedCompetitorNames: includeEvidence ? observedCompetitorNames2(snapshot.recommendedCompetitors) : [],
23131
+ sources: includeEvidence ? snapshot.citedUrls ?? [] : [],
22942
23132
  createdAt: snapshot.createdAt
22943
23133
  });
22944
23134
  }
@@ -22970,7 +23160,7 @@ function frozenSimpleRun(run, definition, snapshots) {
22970
23160
  observations
22971
23161
  };
22972
23162
  }
22973
- function legacySimpleRun(project, run, snapshots) {
23163
+ function legacySimpleRun(project, run, snapshots, includeEvidence = true) {
22974
23164
  const slots = snapshots.map((snapshot) => ({
22975
23165
  id: `slot:legacy:${snapshot.id}`,
22976
23166
  executionId: `legacy:${snapshot.id}`,
@@ -23008,7 +23198,7 @@ function legacySimpleRun(project, run, snapshots) {
23008
23198
  slotId: `slot:legacy:${snapshot.id}`,
23009
23199
  answerId: snapshot.id,
23010
23200
  model: snapshot.servedModel?.trim() || null,
23011
- answerText: snapshot.answerText,
23201
+ answerText: includeEvidence ? snapshot.answerText : null,
23012
23202
  // A legacy answer body cannot be matched against today's identity. Only
23013
23203
  // the stored boolean is a frozen mention fact; null stays incomplete.
23014
23204
  mentionComplete: snapshot.answerMentioned !== null,
@@ -23017,8 +23207,8 @@ function legacySimpleRun(project, run, snapshots) {
23017
23207
  citationComplete: true,
23018
23208
  competitorMentionDomains: [],
23019
23209
  competitorCitationDomains: [],
23020
- observedCompetitorNames: observedCompetitorNames2(snapshot.recommendedCompetitors),
23021
- sources: snapshot.citedUrls ?? [],
23210
+ observedCompetitorNames: includeEvidence ? observedCompetitorNames2(snapshot.recommendedCompetitors) : [],
23211
+ sources: includeEvidence ? snapshot.citedUrls ?? [] : [],
23022
23212
  createdAt: snapshot.createdAt
23023
23213
  }))
23024
23214
  };
@@ -23156,41 +23346,37 @@ function advancedReaderInput(db, projectId, active, query) {
23156
23346
  const presentationComparableIds = new Set(comparableVersionIds(allVersionRowsById, presentationVersion.id));
23157
23347
  const activeComparableIds = new Set(comparableVersionIds(allVersionRowsById, active.version.id));
23158
23348
  const sourceRuns = completedVisibilityRuns(db, projectId, false, query);
23159
- const snapshotsByRun = loadVisibilitySnapshots(db, sourceRuns.map((run) => run.id));
23160
23349
  const sourceCandidates = sourceRuns.flatMap((run) => {
23161
23350
  if (run.measurementPlanVersionId === null) return [];
23162
23351
  const source = versions.get(run.measurementPlanVersionId);
23163
- if (!source) return [];
23164
- const own = advancedRun(
23165
- run,
23166
- source.row,
23167
- source.plan,
23168
- snapshotsByRun.get(run.id) ?? [],
23169
- comparableVersionIds(allVersionRowsById, source.row.id)
23170
- );
23171
- return [{ run, source, own }];
23172
- }).filter((candidate) => query.revision === void 0 || presentationComparableIds.has(candidate.source.row.id));
23352
+ if (!source || query.revision !== void 0 && !presentationComparableIds.has(source.row.id)) return [];
23353
+ return [{ run, source }];
23354
+ });
23173
23355
  if (query.runId !== void 0 && !sourceCandidates.some((candidate) => candidate.run.id === query.runId)) {
23174
23356
  throw validationError(`Measurement run "${query.runId}" is not an eligible advanced result.`);
23175
23357
  }
23358
+ const compatibleSourceCandidates = sourceCandidates.filter((candidate) => activeComparableIds.has(candidate.source.row.id));
23359
+ const preferredSource = query.runId === void 0 ? compatibleSourceCandidates.at(0) ?? sourceCandidates.at(0) : void 0;
23360
+ const selectedSource = query.runId === void 0 ? preferredSource : sourceCandidates.find((candidate) => candidate.run.id === query.runId);
23176
23361
  const candidates = sourceCandidates.map((candidate) => {
23177
- if (candidate.source.row.id === presentationVersion.id) return candidate.own;
23178
- if (!presentationComparableIds.has(candidate.source.row.id)) return candidate.own;
23362
+ const usePresentation = presentationComparableIds.has(candidate.source.row.id);
23363
+ const version = usePresentation ? presentationVersion : candidate.source.row;
23364
+ const plan = usePresentation ? presentationPlan : candidate.source.plan;
23365
+ const snapshots = loadVisibilitySnapshots(db, [candidate.run.id]).get(candidate.run.id) ?? [];
23179
23366
  return advancedRun(
23180
23367
  candidate.run,
23181
- presentationVersion,
23182
- presentationPlan,
23183
- snapshotsByRun.get(candidate.run.id) ?? [],
23184
- [...presentationComparableIds]
23368
+ version,
23369
+ plan,
23370
+ snapshots,
23371
+ usePresentation ? [...presentationComparableIds] : comparableVersionIds(allVersionRowsById, candidate.source.row.id),
23372
+ candidate.run.id === selectedSource?.run.id
23185
23373
  );
23186
23374
  });
23187
- const compatibleSourceCandidates = sourceCandidates.filter((candidate) => activeComparableIds.has(candidate.source.row.id));
23188
- const preferredSource = query.runId === void 0 ? compatibleSourceCandidates.at(0) ?? sourceCandidates.at(0) : void 0;
23189
- const selectedSource = query.runId === void 0 ? preferredSource : sourceCandidates.find((candidate) => candidate.run.id === query.runId);
23375
+ const selectedDefinition = candidates.find((candidate) => candidate.id === selectedSource?.run.id)?.definition;
23190
23376
  return {
23191
23377
  mode: "advanced",
23192
23378
  activeRevision: active.version.revision,
23193
- pendingAssignmentCount: selectedSource !== void 0 && activeComparableIds.has(selectedSource.source.row.id) ? 0 : pendingAssignments(active.plan, selectedSource?.source.plan, selectedSource?.own.definition),
23379
+ pendingAssignmentCount: selectedSource !== void 0 && activeComparableIds.has(selectedSource.source.row.id) ? 0 : pendingAssignments(active.plan, selectedSource?.source.plan, selectedDefinition),
23194
23380
  selection: query,
23195
23381
  activeDefinition: activeV2Definition(presentationPlan, presentationVersion.revision),
23196
23382
  ...preferredSource === void 0 ? {} : { preferredRunId: preferredSource.run.id },
@@ -23203,11 +23389,12 @@ function simpleReaderInput(db, project, query) {
23203
23389
  eq24(simpleMeasurementDefinitions.projectId, project.id),
23204
23390
  inArray9(simpleMeasurementDefinitions.runId, sourceRuns.map((run) => run.id))
23205
23391
  )).all().map((row) => [row.runId, row.definition]));
23206
- const snapshotsByRun = loadVisibilitySnapshots(db, sourceRuns.map((run) => run.id));
23392
+ const selectedRunId = sourceRuns[0]?.id;
23207
23393
  const candidates = sourceRuns.map((run) => {
23208
- const snapshots = snapshotsByRun.get(run.id) ?? [];
23394
+ const snapshots = loadVisibilitySnapshots(db, [run.id]).get(run.id) ?? [];
23209
23395
  const definition = frozen.get(run.id);
23210
- return definition ? frozenSimpleRun(run, definition, snapshots) : legacySimpleRun(project, run, snapshots);
23396
+ const includeEvidence = run.id === selectedRunId;
23397
+ return definition ? frozenSimpleRun(run, definition, snapshots, includeEvidence) : legacySimpleRun(project, run, snapshots, includeEvidence);
23211
23398
  });
23212
23399
  if (query.runId !== void 0 && !candidates.some((run) => run.id === query.runId)) {
23213
23400
  throw validationError(`Measurement run "${query.runId}" is not an eligible simple result.`);
@@ -23221,37 +23408,28 @@ function simpleReaderInput(db, project, query) {
23221
23408
  runs: candidates
23222
23409
  };
23223
23410
  }
23411
+ function readVisibilityReport(db, project, rawQuery) {
23412
+ const query = parseQuery(rawQuery);
23413
+ const active = activeMeasurementPlan(db, project.id);
23414
+ const mode = query.mode === "auto" ? active === null ? "simple" : "advanced" : query.mode;
23415
+ try {
23416
+ if (mode === "advanced") {
23417
+ if (active === null) throw validationError("This project has no advanced measurement plan.");
23418
+ if (active.plan.schemaVersion !== MEASUREMENT_PLAN_V2_SCHEMA_VERSION) return unsupportedAdvancedResponse(query, active);
23419
+ return buildVisibilityReport(advancedReaderInput(db, project.id, active, query));
23420
+ }
23421
+ return buildVisibilityReport(simpleReaderInput(db, project, query));
23422
+ } catch (error) {
23423
+ if (error instanceof VisibilityReportCursorError || error instanceof VisibilityReportScopeError) {
23424
+ throw validationError(error.message);
23425
+ }
23426
+ throw error;
23427
+ }
23428
+ }
23224
23429
  async function visibilityReportRoutes(app) {
23225
23430
  app.get(
23226
23431
  "/projects/:name/visibility-report",
23227
- async (request) => {
23228
- const project = resolveProject(app.db, request.params.name);
23229
- const query = parseQuery(request.query);
23230
- const active = activeMeasurementPlan(app.db, project.id);
23231
- const mode = query.mode === "auto" ? active === null ? "simple" : "advanced" : query.mode;
23232
- if (mode === "advanced") {
23233
- if (active === null) throw validationError("This project has no advanced measurement plan.");
23234
- if (active.plan.schemaVersion !== MEASUREMENT_PLAN_V2_SCHEMA_VERSION) {
23235
- return unsupportedAdvancedResponse(query, active);
23236
- }
23237
- try {
23238
- return buildVisibilityReport(advancedReaderInput(app.db, project.id, active, query));
23239
- } catch (error) {
23240
- if (error instanceof VisibilityReportCursorError || error instanceof VisibilityReportScopeError) {
23241
- throw validationError(error.message);
23242
- }
23243
- throw error;
23244
- }
23245
- }
23246
- try {
23247
- return buildVisibilityReport(simpleReaderInput(app.db, project, query));
23248
- } catch (error) {
23249
- if (error instanceof VisibilityReportCursorError || error instanceof VisibilityReportScopeError) {
23250
- throw validationError(error.message);
23251
- }
23252
- throw error;
23253
- }
23254
- }
23432
+ async (request) => readVisibilityReport(app.db, resolveProject(app.db, request.params.name), request.query)
23255
23433
  );
23256
23434
  }
23257
23435
 
@@ -23500,6 +23678,7 @@ function frozenAssignmentSignature(plan, assignment) {
23500
23678
  target: {
23501
23679
  stableKey: target.stableKey,
23502
23680
  aliases: target.aliases,
23681
+ ...target.identityAliases === void 0 ? {} : { identityAliases: target.identityAliases },
23503
23682
  urlMatchers: target.urlMatchers,
23504
23683
  mentionNotApplicable: target.mentionNotApplicable,
23505
23684
  discoveryIdentity: target.discoveryIdentity
@@ -23777,7 +23956,7 @@ function workspaceDto(db, state, opts) {
23777
23956
  targetKeys: group.targetKeys,
23778
23957
  ...group.parentGroupKey ? { parentGroupKey: group.parentGroupKey } : {}
23779
23958
  })) ?? [],
23780
- markets: plan?.reportingScopes?.map((scope) => ({ stableKey: scope.stableKey, label: scope.label, usageEdges: scope.usageEdges })) ?? [],
23959
+ markets: plan?.reportingScopes?.map((scope) => ({ stableKey: scope.stableKey, label: scope.label, usageEdges: scope.usageEdges, ...scope.groupKey ? { groupKey: scope.groupKey } : {} })) ?? [],
23781
23960
  tracked: trackedRows(db, state, rows, plan, opts),
23782
23961
  savedSources: savedSources(db, state.project.id)
23783
23962
  });
@@ -24001,6 +24180,7 @@ function compileExecution(state, target, queryId, queryText, queryClass, classif
24001
24180
  label: target.label,
24002
24181
  status: "included",
24003
24182
  aliases: [...target.aliases],
24183
+ ...target.identityAliases === void 0 ? {} : { identityAliases: [...target.identityAliases] },
24004
24184
  urlMatchers: target.urlMatchers.map(draftMatcher),
24005
24185
  source: target.discoveryIdentity ? "sitemap" : "manual",
24006
24186
  ...target.discoveryIdentity ? { discoveryIdentity: target.discoveryIdentity } : {}
@@ -28764,10 +28944,10 @@ function renderExecutiveSummary(report) {
28764
28944
  const trendLabel = s.trend === "up" ? "\u2191 Up" : s.trend === "down" ? "\u2193 Down" : s.trend === "flat" ? "\u2192 Flat" : "\u2014";
28765
28945
  const trendTone = s.trend === "up" ? "positive" : s.trend === "down" ? "negative" : "neutral";
28766
28946
  const queryNoun = s.totalQueryCount === 1 ? "query" : "queries";
28767
- const citedFragment = s.totalQueryCount > 0 ? `${s.citedQueryCount}/${s.totalQueryCount} ${queryNoun} cited` : "no queries";
28768
- const mentionedFragment = s.totalQueryCount > 0 ? `${s.mentionedQueryCount}/${s.totalQueryCount} ${queryNoun} mentioned` : "no queries";
28769
- const headlineTitle = s.totalQueryCount > 0 ? `${s.citedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} cite ${report.meta.project.displayName}` : "No AI citation data yet";
28770
- const headlineSubtitle = s.totalQueryCount > 0 ? `${s.citationRate}% citation coverage and ${s.mentionRate}% mention coverage across ${s.providerCount} ${pluralize(s.providerCount, "provider")}.` : "Run a check to populate the first citation and mention baseline.";
28947
+ const citedFragment = (s.totalQueryCount ?? 0) > 0 ? `${s.citedQueryCount}/${s.totalQueryCount} ${queryNoun} cited` : "no queries";
28948
+ const mentionedFragment = (s.totalQueryCount ?? 0) > 0 ? `${s.mentionedQueryCount}/${s.totalQueryCount} ${queryNoun} mentioned` : "no queries";
28949
+ const headlineTitle = (s.totalQueryCount ?? 0) > 0 ? `${s.citedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} cite ${report.meta.project.displayName}` : "No AI citation data yet";
28950
+ const headlineSubtitle = (s.totalQueryCount ?? 0) > 0 ? `${s.citationRate}% citation coverage and ${s.mentionRate}% mention coverage across ${s.providerCount} ${pluralize(s.providerCount, "provider")}.` : "Run a check to populate the first citation and mention baseline.";
28771
28951
  const priorityActions = report.agencyDiagnostics.priorities.length > 0 ? report.agencyDiagnostics.priorities : report.actionPlan;
28772
28952
  const actionCount = dedupeReportActions(report, priorityActions).length;
28773
28953
  const heroHtml = `<div class="executive-hero">
@@ -29864,9 +30044,9 @@ function renderAudienceActionPlan(report, audience) {
29864
30044
  function renderClientSummary(report) {
29865
30045
  const s = report.executiveSummary;
29866
30046
  const sc = report.citationScorecard;
29867
- const totalQ = s.totalQueryCount;
30047
+ const totalQ = s.totalQueryCount ?? 0;
29868
30048
  const heroNumber = totalQ > 0 ? `${s.mentionRate}%` : "\u2014";
29869
- const heroSentence = totalQ > 0 ? `When customers asked AI ${totalQ} ${pluralize(totalQ, "query", "queries")} about your industry, AI mentioned you in ${s.mentionedQueryCount} of ${totalQ === 1 ? "them" : "those answers"}.` : "No AI check has been run yet. Run a check to see how AI tools answer customer queries about your business.";
30049
+ const heroSentence = totalQ > 0 ? `When customers asked AI ${totalQ} ${pluralize(totalQ, "query", "queries")} about your industry, AI mentioned you in ${s.mentionedQueryCount} of ${totalQ === 1 ? "them" : "those queries"}.` : "No AI check has been run yet. Run a check to see how AI tools answer customer queries about your business.";
29870
30050
  const trend = clientTrendCopy(report.whatsChanged.mentionRate);
29871
30051
  const heroTrend = trend ? `<p class="client-hero-trend tone-${trend.tone}"><span style="margin-right:6px;">${trend.arrow}</span>${escapeHtml2(trend.text)}</p>` : "";
29872
30052
  const hero = `<div class="client-hero">
@@ -29880,12 +30060,12 @@ function renderClientSummary(report) {
29880
30060
  <div class="client-metric-tile">
29881
30061
  <div class="label">AI mentions your name</div>
29882
30062
  <div class="value">${s.mentionRate}%</div>
29883
- <div class="subtitle">${totalQ > 0 ? `Says your name in ${s.mentionedQueryCount} of ${totalQ} ${pluralize(totalQ, "answer")}` : "No data yet"}</div>
30063
+ <div class="subtitle">${totalQ > 0 ? `Says your name in ${s.mentionedQueryCount} of ${totalQ} ${pluralize(totalQ, "query", "queries")}` : "No data yet"}</div>
29884
30064
  </div>
29885
30065
  <div class="client-metric-tile">
29886
30066
  <div class="label">AI links to your website</div>
29887
30067
  <div class="value">${s.citationRate}%</div>
29888
- <div class="subtitle">${totalQ > 0 ? `Cites your site as a source in ${s.citedQueryCount} of ${totalQ} ${pluralize(totalQ, "answer")}` : "No data yet"}</div>
30068
+ <div class="subtitle">${totalQ > 0 ? `Cites your site as a source in ${s.citedQueryCount} of ${totalQ} ${pluralize(totalQ, "query", "queries")}` : "No data yet"}</div>
29889
30069
  </div>
29890
30070
  <div class="client-metric-tile">
29891
30071
  <div class="label">AI tools tested</div>
@@ -30026,9 +30206,9 @@ function renderReportHtml(report, opts = {}) {
30026
30206
  const audience = opts.audience ?? "agency";
30027
30207
  const title = opts.title ?? `Canonry ${audience} report \u2014 ${report.meta.project.displayName}`;
30028
30208
  const sections = audience === "client" ? [
30029
- renderClientSummary(report),
30030
- renderReportShareOfVoice(report),
30031
- renderWhatsChanged(report, "client"),
30209
+ report.visibility ? renderReportVisibility(report.visibility) : renderClientSummary(report),
30210
+ report.visibility?.selection.mode === "advanced" ? "" : renderReportShareOfVoice(report),
30211
+ report.visibility ? "" : renderWhatsChanged(report, "client"),
30032
30212
  // Server-side AI visibility runs between WhatsChanged and the action
30033
30213
  // plan in BOTH the SPA and HTML so clients see the same ordered set
30034
30214
  // of sections in either surface (per the report-parity rule).
@@ -30036,13 +30216,13 @@ function renderReportHtml(report, opts = {}) {
30036
30216
  renderAudienceActionPlan(report, "client"),
30037
30217
  renderClientEvidenceSummary(report)
30038
30218
  ].join("\n") : [
30039
- renderExecutiveSummary(report),
30040
- renderReportShareOfVoice(report),
30041
- renderWhatsChanged(report, "agency"),
30219
+ report.visibility ? renderReportVisibility(report.visibility) : renderExecutiveSummary(report),
30220
+ report.visibility?.selection.mode === "advanced" ? "" : renderReportShareOfVoice(report),
30221
+ report.visibility ? "" : renderWhatsChanged(report, "agency"),
30042
30222
  renderAudienceActionPlan(report, "agency"),
30043
- renderAgencyDiagnostics(report),
30044
- renderCitationScorecard(report),
30045
- renderCompetitorLandscape(report),
30223
+ report.visibility?.selection.mode === "advanced" ? "" : renderAgencyDiagnostics(report),
30224
+ report.visibility?.selection.mode === "advanced" ? "" : renderCitationScorecard(report),
30225
+ report.visibility?.selection.mode === "advanced" ? "" : renderCompetitorLandscape(report),
30046
30226
  renderAiSourceOrigin(report),
30047
30227
  renderGsc(report),
30048
30228
  renderGa(report),
@@ -30050,7 +30230,7 @@ function renderReportHtml(report, opts = {}) {
30050
30230
  renderAiReferrals(report),
30051
30231
  renderServerActivity(report, "agency"),
30052
30232
  renderIndexingHealth(report),
30053
- renderCitationsTrend(report),
30233
+ report.visibility?.selection.mode === "advanced" ? "" : renderCitationsTrend(report),
30054
30234
  renderInsights(report),
30055
30235
  renderOpportunities(report),
30056
30236
  renderContentGaps(report),
@@ -30072,7 +30252,7 @@ function renderReportHtml(report, opts = {}) {
30072
30252
  <header class="header">
30073
30253
  <div class="eyebrow">AI Visibility Report</div>
30074
30254
  <h1>${escapeHtml2(report.meta.project.displayName)}</h1>
30075
- <div class="subtitle">${escapeHtml2(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml2(report.meta.project.country)} / ${escapeHtml2(report.meta.project.language.toUpperCase())}${renderHeaderLocationFragment(report.meta.location)} \xB7 Last ${report.meta.periodDays} days \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
30255
+ <div class="subtitle">${escapeHtml2(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml2(report.meta.project.country)} / ${escapeHtml2(report.meta.project.language.toUpperCase())}${report.visibility?.selection.mode === "advanced" ? ` \xB7 ${escapeHtml2(reportVisibilityLocationLabel(report.visibility))}` : renderHeaderLocationFragment(report.meta.location)} \xB7 Last ${report.meta.periodDays} days \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
30076
30256
  </header>
30077
30257
  ${sections}
30078
30258
  <footer class="footer">Generated by <a href="https://canonry.ai">canonry</a> \xB7 ${escapeHtml2(formatIsoDate(report.meta.generatedAt))}</footer>
@@ -30081,6 +30261,15 @@ function renderReportHtml(report, opts = {}) {
30081
30261
  </body>
30082
30262
  </html>`;
30083
30263
  }
30264
+ function renderReportVisibility(visibility) {
30265
+ const populations = visibility.populations.filter((population) => population.queryClass !== "unknown" || population.summary.answerCount > 0);
30266
+ const historyPopulations = visibility.populations.filter((population) => population.queryClass !== "unknown" || population.trend.some((point) => point.answerCount > 0));
30267
+ const rateCell = (rate2) => `<td><strong>${escapeHtml2(reportVisibilityRate(rate2))}</strong><p class="muted">${escapeHtml2(reportVisibilityEvidence(rate2))}</p></td>`;
30268
+ const headers = (labels) => `<thead><tr>${labels.map((label) => `<th>${escapeHtml2(label)}</th>`).join("")}</tr></thead>`;
30269
+ const summary2 = `<table>${headers([REPORT_VISIBILITY_COPY.queryType, REPORT_VISIBILITY_COPY.queries, REPORT_VISIBILITY_COPY.answers, REPORT_VISIBILITY_COPY.mentioned, REPORT_VISIBILITY_COPY.cited])}<tbody>${populations.map((population) => `<tr><td>${escapeHtml2(reportQueryClassLabel(population.queryClass))}</td><td>${population.summary.queryCount}</td><td>${population.summary.answerCount}</td>${rateCell(population.summary.mentionCoverage)}${rateCell(population.summary.citationCoverage)}</tr>`).join("")}</tbody></table>`;
30270
+ const trend = `<details><summary>${escapeHtml2(reportVisibilityHistoryLabel(visibility))}</summary><div class="table-scroll"><table>${headers([REPORT_VISIBILITY_COPY.date, REPORT_VISIBILITY_COPY.queryType, REPORT_VISIBILITY_COPY.mentioned, REPORT_VISIBILITY_COPY.cited, REPORT_VISIBILITY_COPY.comparison])}<tbody>${historyPopulations.flatMap((population) => population.trend.map((point) => `<tr><td><time datetime="${escapeHtml2(point.createdAt)}">${escapeHtml2(point.createdAt.slice(0, 10))}</time></td><td>${escapeHtml2(reportQueryClassLabel(population.queryClass))}</td>${rateCell(point.mentionCoverage)}${rateCell(point.citationCoverage)}<td>${escapeHtml2(reportVisibilityComparison(point.continuity.state, point.continuity.comparedRunId !== null && !population.trend.some((previous) => previous.runId === point.continuity.comparedRunId)))}</td></tr>`)).join("")}</tbody></table></div></details>`;
30271
+ return `<section id="client-summary" class="report-section" aria-label="${escapeHtml2(REPORT_VISIBILITY_COPY.title)}"><h2>${escapeHtml2(REPORT_VISIBILITY_COPY.title)}</h2><p>${escapeHtml2(visibility.selection.mode === "advanced" ? REPORT_VISIBILITY_COPY.description : REPORT_VISIBILITY_COPY.simpleDescription)}</p><p>${escapeHtml2(reportVisibilityMeasurementLabel(visibility))}</p><div class="table-scroll">${summary2}</div>${trend}</section>`;
30272
+ }
30084
30273
 
30085
30274
  // ../api-routes/src/ga-ai-referral-aggregation.ts
30086
30275
  function normalizeAiTrafficClass(value) {
@@ -30942,7 +31131,6 @@ function buildIndexingHealth(db, projectId) {
30942
31131
  }
30943
31132
  function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
30944
31133
  const visibilityRuns = db.select().from(runs).where(and29(eq33(runs.projectId, projectId), eq33(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
30945
- const totalQueries = queryLookup.byId.size;
30946
31134
  const points = [];
30947
31135
  for (const run of visibilityRuns) {
30948
31136
  if (run.status !== RunStatuses.completed) continue;
@@ -30964,10 +31152,11 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
30964
31152
  providerCounts.set(snap.provider, counts2);
30965
31153
  }
30966
31154
  if (considered === 0) continue;
31155
+ const totalQueries = new Set(snaps.map((snapshot) => snapshot.queryId)).size;
30967
31156
  const citedQueryCount = citedQueryIds.size;
30968
31157
  const mentionedQueryCount = mentionedQueryIds.size;
30969
31158
  const citationRate = totalQueries > 0 ? Math.round(citedQueryCount / totalQueries * 100) : 0;
30970
- const mentionRate2 = totalQueries > 0 ? Math.round(mentionedQueryCount / totalQueries * 100) : 0;
31159
+ const mentionRate3 = totalQueries > 0 ? Math.round(mentionedQueryCount / totalQueries * 100) : 0;
30971
31160
  const providerRates = [...providerCounts.entries()].map(([provider, counts2]) => ({
30972
31161
  provider,
30973
31162
  citationRate: counts2.total > 0 ? Math.round(counts2.cited / counts2.total * 100) : 0,
@@ -30979,7 +31168,7 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
30979
31168
  citationRate,
30980
31169
  citedQueryCount,
30981
31170
  totalQueryCount: totalQueries,
30982
- mentionRate: mentionRate2,
31171
+ mentionRate: mentionRate3,
30983
31172
  mentionedQueryCount,
30984
31173
  providerRates
30985
31174
  });
@@ -31339,7 +31528,7 @@ function buildReportActionPlan(input) {
31339
31528
  });
31340
31529
  }
31341
31530
  if (actions.length === 0) {
31342
- const neverMeasured = input.citationScorecard.providers.length === 0;
31531
+ const neverMeasured = !(input.hasVisibilityMeasurement ?? input.citationScorecard.providers.length > 0);
31343
31532
  actions.push({
31344
31533
  audience: "both",
31345
31534
  priority: 90,
@@ -31372,12 +31561,12 @@ function mentionTrendSentence(delta) {
31372
31561
  function buildClientSummary(reportLike) {
31373
31562
  const s = reportLike.executiveSummary;
31374
31563
  const queryNoun = s.totalQueryCount === 1 ? "query" : "queries";
31375
- const headline = s.totalQueryCount > 0 ? `${s.mentionedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} mention the brand in AI answers` : "No tracked queries have completed a check yet";
31376
- const overview = s.totalQueryCount > 0 ? `${reportLike.canonicalDomain} is mentioned on ${s.mentionRate}% of tracked queries and cited on ${s.citationRate}% of tracked queries. ${mentionTrendSentence(reportLike.whatsChanged.mentionRate)}` : "At least one completed check is needed before this can summarize how the brand appears in AI answers.";
31564
+ const headline = (s.totalQueryCount ?? 0) > 0 ? `${s.mentionedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} mention the brand in AI answers` : "No tracked queries have completed a check yet";
31565
+ const overview = (s.totalQueryCount ?? 0) > 0 ? `${reportLike.canonicalDomain} is mentioned on ${s.mentionRate}% of tracked queries and cited on ${s.citationRate}% of tracked queries. ${mentionTrendSentence(reportLike.whatsChanged.mentionRate)}` : "At least one completed check is needed before this can summarize how the brand appears in AI answers.";
31377
31566
  const confidenceNotes = [];
31378
31567
  if (s.totalQueryCount === 0) {
31379
31568
  confidenceNotes.push("Confidence is low until the first tracked query check completes.");
31380
- } else if (s.totalQueryCount < 5) {
31569
+ } else if ((s.totalQueryCount ?? 0) < 5) {
31381
31570
  confidenceNotes.push("Directional read: the tracked query set is still small, so each query has outsized impact on the percentage.");
31382
31571
  }
31383
31572
  if (isTrendBaseline(reportLike.citationsTrend)) {
@@ -31502,7 +31691,7 @@ function buildWhatsChanged(input) {
31502
31691
  direction: rateDirection(citationRateSmoothed.deltaAbs, RATE_REAL_MOVEMENT_THRESHOLD_PP)
31503
31692
  } : null;
31504
31693
  const mentionRateSmoothed = smoothedRunDelta(citationsTrend, (p) => p.mentionRate);
31505
- const mentionRate2 = enoughHistory && mentionRateSmoothed ? {
31694
+ const mentionRate3 = enoughHistory && mentionRateSmoothed ? {
31506
31695
  ...mentionRateSmoothed,
31507
31696
  direction: rateDirection(mentionRateSmoothed.deltaAbs, RATE_REAL_MOVEMENT_THRESHOLD_PP)
31508
31697
  } : null;
@@ -31549,7 +31738,7 @@ function buildWhatsChanged(input) {
31549
31738
  enoughHistory,
31550
31739
  headline,
31551
31740
  citationRate,
31552
- mentionRate: mentionRate2,
31741
+ mentionRate: mentionRate3,
31553
31742
  citedQueryCount,
31554
31743
  mentionedQueryCount,
31555
31744
  gscClicksDelta,
@@ -31563,14 +31752,30 @@ function buildWhatsChanged(input) {
31563
31752
  function buildProjectReport(db, projectName, periodDays) {
31564
31753
  const project = resolveProject(db, projectName);
31565
31754
  const queryLookup = loadQueryLookup(db, project.id);
31755
+ const canonicalVisibility = readVisibilityReport(db, project, { queryClass: "all", scope: "project" });
31756
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
31757
+ const historyWindow = { from: new Date(Date.parse(generatedAt) - periodDays * 864e5).toISOString(), to: generatedAt };
31758
+ const visibility = {
31759
+ selection: canonicalVisibility.selection,
31760
+ historyWindow,
31761
+ populations: canonicalVisibility.populations.map(({ queryClass, summary: summary2, trend: trend2 }) => ({
31762
+ queryClass,
31763
+ summary: summary2,
31764
+ // Keep each point's actual comparison. Renderers disclose when the prior
31765
+ // measurement is outside this window rather than inventing a baseline.
31766
+ trend: trend2.filter((point) => point.createdAt >= historyWindow.from && point.createdAt <= historyWindow.to)
31767
+ }))
31768
+ };
31769
+ const canonicalVisibilitySupported = visibility.selection.availability.state === "available";
31770
+ const advancedVisibility = canonicalVisibilitySupported && visibility.selection.mode === "advanced";
31566
31771
  const comparisonWindowDays = reportComparisonWindowDays(periodDays);
31567
31772
  const allRuns = db.select().from(runs).where(and29(eq33(runs.projectId, project.id), notProbeRun())).orderBy(desc14(runs.createdAt), desc14(runs.id)).all();
31568
31773
  const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
31569
31774
  const completedVisRunGroups = groupRunsByCreatedAt(
31570
31775
  visibilityRuns.filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial)
31571
31776
  );
31572
- const latestVisRunGroup = completedVisRunGroups[0] ?? [];
31573
- const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? visibilityRuns[0] ?? null;
31777
+ const latestVisRunGroup = advancedVisibility ? visibilityRuns.filter((run) => run.id === visibility.selection.run.id) : completedVisRunGroups[0] ?? [];
31778
+ const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? (advancedVisibility ? null : visibilityRuns[0] ?? null);
31574
31779
  const latestSnapshots = loadSnapshotsForRunIds(db, latestVisRunGroup.map((r) => r.id));
31575
31780
  const latestRunLocation = representativeLatestRun?.location ?? null;
31576
31781
  const competitorRows2 = db.select().from(competitors).where(eq33(competitors.projectId, project.id)).all();
@@ -31623,7 +31828,8 @@ function buildProjectReport(db, projectName, periodDays) {
31623
31828
  }));
31624
31829
  }
31625
31830
  Object.assign(mentionLandscape, mentionLandscape.nonBrand);
31626
- const aiSourceOrigin = buildAiSourceOrigin(latestSnapshots, projectDomains, competitorDomains);
31831
+ const advancedSourceSnapshots = advancedVisibility && visibility.selection.run.id !== null ? db.select({ citedDomains: querySnapshots.citedDomains, provider: querySnapshots.provider, location: querySnapshots.location }).from(querySnapshots).where(eq33(querySnapshots.runId, visibility.selection.run.id)).all() : [];
31832
+ const aiSourceOrigin = buildAiSourceOrigin(advancedVisibility ? advancedSourceSnapshots : latestSnapshots, projectDomains, competitorDomains);
31627
31833
  const trackedQueries = [...queryLookup.byId.values()];
31628
31834
  const gscSection = buildGscSection(
31629
31835
  db,
@@ -31658,7 +31864,7 @@ function buildProjectReport(db, projectName, periodDays) {
31658
31864
  insights: insightList,
31659
31865
  comparisonWindowDays
31660
31866
  });
31661
- const totalQueryCount = queryLookup.byId.size;
31867
+ const totalQueryCount = new Set(latestSnapshots.map((snapshot) => snapshot.queryId)).size;
31662
31868
  const citedQueryIds = /* @__PURE__ */ new Set();
31663
31869
  const mentionedQueryIds = /* @__PURE__ */ new Set();
31664
31870
  for (const snap of latestSnapshots) {
@@ -31669,7 +31875,7 @@ function buildProjectReport(db, projectName, periodDays) {
31669
31875
  const citedQueryCount = citedQueryIds.size;
31670
31876
  const mentionedQueryCount = mentionedQueryIds.size;
31671
31877
  const citationRate = totalQueryCount > 0 ? Math.round(citedQueryCount / totalQueryCount * 100) : 0;
31672
- const mentionRate2 = totalQueryCount > 0 ? Math.round(mentionedQueryCount / totalQueryCount * 100) : 0;
31878
+ const mentionRate3 = totalQueryCount > 0 ? Math.round(mentionedQueryCount / totalQueryCount * 100) : 0;
31673
31879
  const trendBaseline = isTrendBaseline(citationsTrend);
31674
31880
  const latestPoint = citationsTrend.at(-1);
31675
31881
  const previousPoint = citationsTrend.length >= 2 ? citationsTrend.at(-2) : null;
@@ -31694,21 +31900,22 @@ function buildProjectReport(db, projectName, periodDays) {
31694
31900
  insightList,
31695
31901
  competitorLandscape.competitors
31696
31902
  );
31697
- const periodStart = citationsTrend[0]?.date ?? null;
31698
- const periodEnd = citationsTrend.at(-1)?.date ?? null;
31903
+ const visibilityDates = visibility.populations.flatMap((population) => population.trend.map((point) => point.createdAt)).sort();
31904
+ const periodStart = advancedVisibility ? visibilityDates[0] ?? visibility.selection.measurement.completedAt : citationsTrend[0]?.date ?? null;
31905
+ const periodEnd = advancedVisibility ? visibility.selection.measurement.completedAt : citationsTrend.at(-1)?.date ?? null;
31699
31906
  const configuredLocations = project.locations;
31700
31907
  const reportLocation = buildLocationMeta(representativeLatestRun?.location ?? null, configuredLocations);
31701
- const providerLocationHandling = reportLocation ? buildProviderLocationHandling(citationScorecard.providers) : [];
31908
+ const providerLocationHandling = advancedVisibility ? buildProviderLocationHandling([...new Set(advancedSourceSnapshots.filter((snapshot) => snapshot.location !== null).map((snapshot) => snapshot.provider))]) : reportLocation ? buildProviderLocationHandling(citationScorecard.providers) : [];
31702
31909
  const executiveSummary = {
31703
31910
  citationRate,
31704
31911
  citedQueryCount,
31705
31912
  totalQueryCount,
31706
- mentionRate: mentionRate2,
31913
+ mentionRate: mentionRate3,
31707
31914
  mentionedQueryCount,
31708
31915
  trend,
31709
31916
  queryCount: queryLookup.byId.size,
31710
31917
  competitorCount: competitorDomains.length,
31711
- providerCount: citationScorecard.providers.length,
31918
+ providerCount: advancedVisibility ? new Set(advancedSourceSnapshots.map((snapshot) => snapshot.provider)).size : citationScorecard.providers.length,
31712
31919
  gsc: gscSection ? {
31713
31920
  clicks: gscSection.totalClicks,
31714
31921
  impressions: gscSection.totalImpressions,
@@ -31729,6 +31936,7 @@ function buildProjectReport(db, projectName, periodDays) {
31729
31936
  canonicalDomain: project.canonicalDomain,
31730
31937
  competitorDomains: actionPlanCompetitorDomains,
31731
31938
  citationScorecard,
31939
+ hasVisibilityMeasurement: advancedVisibility ? visibility.selection.run.id !== null : void 0,
31732
31940
  aiSourceOrigin,
31733
31941
  gsc: gscSection,
31734
31942
  indexingHealth: indexingHealthSection,
@@ -31750,6 +31958,7 @@ function buildProjectReport(db, projectName, periodDays) {
31750
31958
  canonicalDomain: project.canonicalDomain,
31751
31959
  competitorDomains,
31752
31960
  citationScorecard,
31961
+ hasVisibilityMeasurement: advancedVisibility ? visibility.selection.run.id !== null : void 0,
31753
31962
  aiSourceOrigin,
31754
31963
  gsc: gscSection,
31755
31964
  indexingHealth: indexingHealthSection,
@@ -31761,7 +31970,7 @@ function buildProjectReport(db, projectName, periodDays) {
31761
31970
  });
31762
31971
  return {
31763
31972
  meta: {
31764
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
31973
+ generatedAt,
31765
31974
  project: {
31766
31975
  id: project.id,
31767
31976
  name: project.name,
@@ -31776,7 +31985,18 @@ function buildProjectReport(db, projectName, periodDays) {
31776
31985
  periodEnd,
31777
31986
  periodDays
31778
31987
  },
31779
- executiveSummary,
31988
+ ...canonicalVisibilitySupported ? { visibility } : {},
31989
+ executiveSummary: advancedVisibility ? {
31990
+ ...executiveSummary,
31991
+ visibilityBasis: "frozen-populations",
31992
+ citationRate: null,
31993
+ citedQueryCount: null,
31994
+ totalQueryCount: null,
31995
+ mentionRate: null,
31996
+ mentionedQueryCount: null,
31997
+ trend: "unknown",
31998
+ findings: []
31999
+ } : { ...executiveSummary, visibilityBasis: "legacy-project-queries" },
31780
32000
  citationScorecard,
31781
32001
  competitorLandscape,
31782
32002
  mentionLandscape,
@@ -31787,12 +32007,28 @@ function buildProjectReport(db, projectName, periodDays) {
31787
32007
  aiReferrals: aiReferralsSection,
31788
32008
  serverActivity: serverActivitySection,
31789
32009
  indexingHealth: indexingHealthSection,
31790
- citationsTrend,
31791
- whatsChanged,
32010
+ citationsTrend: advancedVisibility ? [] : citationsTrend,
32011
+ whatsChanged: advancedVisibility ? {
32012
+ ...whatsChanged,
32013
+ enoughHistory: false,
32014
+ headline: "See measurement history for each query type.",
32015
+ citationRate: null,
32016
+ mentionRate: null,
32017
+ citedQueryCount: null,
32018
+ mentionedQueryCount: null,
32019
+ providerMovements: [],
32020
+ wins: [],
32021
+ regressions: []
32022
+ } : whatsChanged,
31792
32023
  insights: insightList,
31793
32024
  recommendedNextSteps,
31794
32025
  actionPlan,
31795
- clientSummary,
32026
+ clientSummary: advancedVisibility ? {
32027
+ ...clientSummary,
32028
+ headline: "AI visibility by query type",
32029
+ overview: "Mention and citation coverage use the properties assigned to each measured query.",
32030
+ confidenceNotes: []
32031
+ } : clientSummary,
31796
32032
  agencyDiagnostics,
31797
32033
  contentOpportunities,
31798
32034
  contentGaps,
@@ -34107,7 +34343,7 @@ function summarizeFromSnapshots(snapshots) {
34107
34343
  if (wasMentioned) mentionedQueries += 1;
34108
34344
  }
34109
34345
  const notMentionedQueries = totalQueries - mentionedQueries;
34110
- const mentionRate2 = totalQueries === 0 ? 0 : Number((mentionedQueries / totalQueries).toFixed(4));
34346
+ const mentionRate3 = totalQueries === 0 ? 0 : Number((mentionedQueries / totalQueries).toFixed(4));
34111
34347
  const providers = [...perProvider.entries()].map(([provider, { cited, total }]) => ({
34112
34348
  provider,
34113
34349
  cited,
@@ -34122,7 +34358,7 @@ function summarizeFromSnapshots(snapshots) {
34122
34358
  citedRate,
34123
34359
  mentionedQueries,
34124
34360
  notMentionedQueries,
34125
- mentionRate: mentionRate2
34361
+ mentionRate: mentionRate3
34126
34362
  },
34127
34363
  providers
34128
34364
  };
@@ -36314,7 +36550,7 @@ var routeCatalog = [
36314
36550
  path: "/api/v1/projects/{name}/research/runs",
36315
36551
  summary: "List saved research query batches",
36316
36552
  tags: ["research"],
36317
- parameters: [nameParameter, { name: "limit", in: "query", description: "Max runs, default 20 and maximum 100.", schema: integerSchema }],
36553
+ parameters: [nameParameter, { name: "limit", in: "query", description: "Max runs, default 20 and maximum 100.", schema: integerSchema }, { name: "cursor", in: "query", description: "Opaque cursor from the previous page.", schema: stringSchema }],
36318
36554
  responses: { 200: jsonResponse("Research runs returned newest first.", "ResearchRunListDto"), 404: errorResponse("Project not found.") }
36319
36555
  },
36320
36556
  {
@@ -83209,7 +83445,8 @@ async function technicalAeoRoutes(app, opts) {
83209
83445
 
83210
83446
  // ../api-routes/src/research.ts
83211
83447
  import crypto49 from "crypto";
83212
- import { and as and55, count as count3, desc as desc28, eq as eq65, gte as gte17, lt as lt11, sql as sql25 } from "drizzle-orm";
83448
+ import { z as z5 } from "zod";
83449
+ import { and as and55, count as count3, desc as desc28, eq as eq65, gte as gte17, lt as lt11, or as or14, sql as sql25 } from "drizzle-orm";
83213
83450
  var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
83214
83451
  var BATCH_RECEIPT_PREFIX = "__canonry_research_batch__:";
83215
83452
  async function researchRoutes(app, opts) {
@@ -83222,11 +83459,19 @@ async function researchRoutes(app, opts) {
83222
83459
  }, async (request, reply) => {
83223
83460
  requireResearchGrant(request, opts.allowViewers ?? false);
83224
83461
  const project = resolveProject(app.db, request.params.name);
83225
- if (!opts.onResearchRunRequested) throw missingDependency("Research execution is not available on this deployment.", { reason: "no-research-handler" });
83226
83462
  const parsed = researchRunCreateSchema.safeParse(request.body ?? {});
83227
83463
  if (!parsed.success) throw validationError("Invalid research run request", { issues: parsed.error.issues });
83228
83464
  const input = parsed.data;
83229
83465
  if (input.idempotencyKey?.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
83466
+ const requestHash2 = directResearchRequestHash(input);
83467
+ const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and55(eq65(researchRuns.projectId, project.id), eq65(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
83468
+ if (existingReceipt) {
83469
+ assertSameDirectResearchRequest(existingReceipt, input, requestHash2);
83470
+ const result2 = getDetail(app, project.id, existingReceipt.id);
83471
+ if (existingReceipt.status === ResearchRunStatuses.queued) opts.onResearchRunRequested?.(existingReceipt.id, project.id);
83472
+ return reply.status(200).send(result2);
83473
+ }
83474
+ if (!opts.onResearchRunRequested) throw missingDependency("Research execution is not available on this deployment.", { reason: "no-research-handler" });
83230
83475
  const adapters = opts.providerAdapters ?? [];
83231
83476
  const configured = new Set(opts.configuredProviderNames ?? []);
83232
83477
  const providerName = input.provider ?? project.providers.find((name) => configured.has(name) && adapters.some((adapter2) => adapter2.name === name && adapter2.mode === "api")) ?? adapters.find((adapter2) => adapter2.mode === "api" && configured.has(adapter2.name))?.name;
@@ -83236,27 +83481,24 @@ async function researchRoutes(app, opts) {
83236
83481
  adapter.modelValidationPattern.lastIndex = 0;
83237
83482
  if (!adapter.modelConfigurable || !adapter.modelValidationPattern.test(input.model)) throw validationError(`Invalid model "${input.model}" for provider "${providerName}".`, { provider: providerName, model: input.model, hint: adapter.modelValidationHint });
83238
83483
  }
83239
- const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and55(eq65(researchRuns.projectId, project.id), eq65(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
83240
83484
  const active = activeMeasurementPlan(app.db, project.id);
83241
- const scope = input.scope ? existingReceipt ? resolveStoredResearchScope(existingReceipt.scope, input.scope, input.idempotencyKey) : resolveResearchScope(active, input.scope) : null;
83485
+ const scope = input.scope ? resolveResearchScope(active, input.scope) : null;
83242
83486
  const location = input.location === void 0 ? scope ? null : project.defaultLocation ? project.locations.find((item2) => item2.label === project.defaultLocation) ?? null : null : input.location;
83243
83487
  if (location && !project.locations.some((item2) => sameLocation(item2, location))) throw validationError("Research location must exactly match a configured project location.", { location });
83244
- const template = input.template ? existingReceipt ? resolveStoredResearchTemplate(existingReceipt.template, input.template, input.idempotencyKey) : resolveResearchTemplate(app, project.id, input.template, scope, location) : null;
83488
+ const template = input.template ? resolveResearchTemplate(app, project.id, input.template, scope, location) : null;
83245
83489
  const requestedModel2 = input.model ?? null;
83246
83490
  const resolvedModel = requestedModel2 ?? (project.providerModels[providerName] || opts.getEffectiveProviderModels?.()[providerName] || adapter.defaultModel);
83247
83491
  adapter.modelValidationPattern.lastIndex = 0;
83248
83492
  if (!adapter.modelValidationPattern.test(resolvedModel)) throw validationError('Invalid resolved model "' + resolvedModel + '" for provider "' + providerName + '".', { provider: providerName, model: resolvedModel, hint: adapter.modelValidationHint });
83249
83493
  if (deduplicateResearchQueries(input.queries).length !== input.queries.length) throw validationError("Research queries must be unique within a batch.");
83250
83494
  const queryClasses = classifyResearchQueries(project, active?.plan ?? null, input.queries);
83251
- const normalized = { queries: input.queries, provider: providerName, model: requestedModel2, location: location ?? null, ...scope ? { scope } : {}, ...template ? { template } : {} };
83252
- const requestHash2 = crypto49.createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
83253
83495
  const now = (/* @__PURE__ */ new Date()).toISOString();
83254
83496
  const initiatedBy = researchPrincipal(request);
83255
83497
  const decision = app.db.transaction((tx) => {
83256
83498
  if (input.idempotencyKey) {
83257
83499
  const existing = tx.select().from(researchRuns).where(and55(eq65(researchRuns.projectId, project.id), eq65(researchRuns.idempotencyKey, input.idempotencyKey))).get();
83258
83500
  if (existing) {
83259
- if (existing.requestHash !== requestHash2) throw alreadyExists("Research idempotency key", input.idempotencyKey);
83501
+ assertSameDirectResearchRequest(existing, input, requestHash2);
83260
83502
  return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
83261
83503
  }
83262
83504
  }
@@ -83371,7 +83613,14 @@ async function researchRoutes(app, opts) {
83371
83613
  const project = resolveProject(app.db, request.params.name);
83372
83614
  const requested = Number.parseInt(request.query.limit ?? "", 10);
83373
83615
  const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
83374
- const runs2 = app.db.select().from(researchRuns).where(eq65(researchRuns.projectId, project.id)).orderBy(desc28(researchRuns.createdAt)).limit(limit).all().map(serializeRun);
83616
+ const cursor = request.query.cursor ? parseResearchCursor(request.query.cursor, project.id) : null;
83617
+ const rows = app.db.select().from(researchRuns).where(and55(
83618
+ eq65(researchRuns.projectId, project.id),
83619
+ cursor ? or14(lt11(researchRuns.createdAt, cursor.createdAt), and55(eq65(researchRuns.createdAt, cursor.createdAt), lt11(researchRuns.id, cursor.id))) : void 0
83620
+ )).orderBy(desc28(researchRuns.createdAt), desc28(researchRuns.id)).limit(limit + 1).all();
83621
+ const runs2 = rows.slice(0, limit).map(serializeRun);
83622
+ const last = runs2.at(-1);
83623
+ const nextCursor = rows.length > limit && last ? Buffer.from(JSON.stringify({ projectId: project.id, createdAt: last.createdAt, id: last.id })).toString("base64url") : null;
83375
83624
  const configured = new Set(opts.configuredProviderNames ?? []);
83376
83625
  const effectiveModels2 = opts.getEffectiveProviderModels?.() ?? {};
83377
83626
  const providers = (opts.providerAdapters ?? []).filter((adapter) => adapter.mode === "api" && !isBrowserProvider(adapter.name) && configured.has(adapter.name)).map((adapter) => {
@@ -83388,13 +83637,47 @@ async function researchRoutes(app, opts) {
83388
83637
  });
83389
83638
  const canRun = canRunResearch(request, opts.allowViewers ?? false);
83390
83639
  const limited = request.principal !== void 0 && !request.principal.scopes.includes(WILDCARD_SCOPE);
83391
- return { runs: runs2, providers, access: { canRun, dailyRunLimit: canRun && limited ? viewerDailyRunLimit : null } };
83640
+ return { runs: runs2, nextCursor, providers, access: { canRun, dailyRunLimit: canRun && limited ? viewerDailyRunLimit : null } };
83392
83641
  });
83393
83642
  app.get("/projects/:name/research/runs/:runId", async (request) => {
83394
83643
  const project = resolveProject(app.db, request.params.name);
83395
83644
  return getDetail(app, project.id, request.params.runId);
83396
83645
  });
83397
83646
  }
83647
+ var DIRECT_REQUEST_HASH_VERSION = "direct-v2:";
83648
+ function directResearchRequestHash(input) {
83649
+ const { idempotencyKey: _key, ...request } = input;
83650
+ return DIRECT_REQUEST_HASH_VERSION + crypto49.createHash("sha256").update(JSON.stringify(request)).digest("hex");
83651
+ }
83652
+ function assertSameDirectResearchRequest(existing, input, hash) {
83653
+ if (existing.requestHash?.startsWith(DIRECT_REQUEST_HASH_VERSION)) {
83654
+ if (existing.requestHash !== hash) throw alreadyExists("Research idempotency key", input.idempotencyKey);
83655
+ return;
83656
+ }
83657
+ const scope = input.scope ? resolveStoredResearchScope(existing.scope, input.scope, input.idempotencyKey) : null;
83658
+ const template = input.template ? resolveStoredResearchTemplate(existing.template, input.template, input.idempotencyKey) : null;
83659
+ if (input.template?.bindingLocation !== void 0) throw alreadyExists("Research idempotency key", input.idempotencyKey);
83660
+ const normalized = {
83661
+ queries: input.queries,
83662
+ provider: input.provider ?? existing.provider,
83663
+ model: input.model ?? null,
83664
+ location: input.location === void 0 ? existing.location ?? null : input.location,
83665
+ ...scope ? { scope } : {},
83666
+ ...template ? { template } : {}
83667
+ };
83668
+ if (crypto49.createHash("sha256").update(JSON.stringify(normalized)).digest("hex") !== existing.requestHash) throw alreadyExists("Research idempotency key", input.idempotencyKey);
83669
+ }
83670
+ var researchCursorSchema = z5.object({ projectId: z5.string().min(1), createdAt: z5.string().datetime({ offset: true }), id: z5.string().min(1).max(256) }).strict();
83671
+ function parseResearchCursor(value, projectId) {
83672
+ try {
83673
+ if (value.length > 2048 || !/^[\w-]+$/.test(value)) throw new Error("Invalid encoding");
83674
+ const cursor = researchCursorSchema.parse(JSON.parse(Buffer.from(value, "base64url").toString("utf8")));
83675
+ if (cursor.projectId !== projectId) throw new Error("Different project");
83676
+ return cursor;
83677
+ } catch {
83678
+ throw validationError("Invalid research history cursor. Reload the first page and try again.");
83679
+ }
83680
+ }
83398
83681
  function resolveStoredResearchScope(stored, selection, idempotencyKey) {
83399
83682
  if (!stored || stored.kind !== selection.kind || stored.key !== selection.key || selection.expectedPlanRevision !== void 0 && selection.expectedPlanRevision !== stored.planRevision) {
83400
83683
  throw alreadyExists("Research idempotency key", idempotencyKey);
@@ -83469,7 +83752,12 @@ function resolveResearchTemplate(app, projectId, selection, scope, location) {
83469
83752
  if (templateVersion !== selection.templateVersion) {
83470
83753
  throw validationError("The selected research template is stale. Reload it and try again.");
83471
83754
  }
83472
- const { bindings, output } = expandResearchTemplate(savedTemplate, scope, location);
83755
+ const bindingLocation = selection.bindingLocation === void 0 ? location : selection.bindingLocation;
83756
+ if (bindingLocation) {
83757
+ const project = app.db.select({ locations: projects.locations }).from(projects).where(eq65(projects.id, projectId)).get();
83758
+ if (!project.locations.some((item2) => sameLocation(item2, bindingLocation))) throw validationError("Research template binding location must match a configured project location.");
83759
+ }
83760
+ const { bindings, output } = expandResearchTemplate(savedTemplate, scope, bindingLocation);
83473
83761
  if (!output || output.length > 4e3) throw validationError("Research template output must be between 1 and 4000 characters.");
83474
83762
  return { templateId: selection.templateId, templateVersion, template: savedTemplate.pattern, bindings, output };
83475
83763
  }
@@ -84283,7 +84571,7 @@ function analyzableRunPredicates(projectId) {
84283
84571
  eq67(runs.projectId, projectId),
84284
84572
  // Only this kind writes query_snapshots.
84285
84573
  eq67(runs.kind, RunKinds["answer-visibility"]),
84286
- or14(eq67(runs.status, RunStatuses.completed), eq67(runs.status, RunStatuses.partial)),
84574
+ or15(eq67(runs.status, RunStatuses.completed), eq67(runs.status, RunStatuses.partial)),
84287
84575
  // Defensive: RunCoordinator already skips probes before analyzeAndPersist
84288
84576
  // is called, but a future call site invoking it directly for a probe must
84289
84577
  // still not pollute the intelligence window.
@@ -84873,7 +85161,7 @@ var IntelligenceService = class {
84873
85161
  and57(
84874
85162
  eq67(runs.projectId, projectId),
84875
85163
  eq67(runs.kind, RunKinds["answer-visibility"]),
84876
- or14(eq67(runs.status, "completed"), eq67(runs.status, "partial")),
85164
+ or15(eq67(runs.status, "completed"), eq67(runs.status, "partial")),
84877
85165
  // Defensive — see top of file.
84878
85166
  ne10(runs.trigger, RunTriggers.probe)
84879
85167
  )