@ainyc/canonry 4.191.0 → 4.192.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/assets/assets/{AuditHistoryPanel-C2dvjYNh.js → AuditHistoryPanel-C7MwfCl9.js} +1 -1
  2. package/assets/assets/{BacklinksPage-D1yPQtfF.js → BacklinksPage-1Nq1zbRk.js} +1 -1
  3. package/assets/assets/{ChartPrimitives-DLEICrrL.js → ChartPrimitives-Bxa_vFmq.js} +1 -1
  4. package/assets/assets/{HistoryPage-DOEa7naA.js → HistoryPage-BNACyEiz.js} +1 -1
  5. package/assets/assets/MeasurementPropertyPage-BqjpIYAw.js +1 -0
  6. package/assets/assets/ProjectPage-BhBzznlv.js +8 -0
  7. package/assets/assets/{RunRow-BO7lyC1C.js → RunRow-DINPo5ES.js} +1 -1
  8. package/assets/assets/{RunsPage-DHSWfTav.js → RunsPage-BFuDLm7p.js} +1 -1
  9. package/assets/assets/{SettingsPage-oiJdhiq0.js → SettingsPage-CkqmyTSK.js} +1 -1
  10. package/assets/assets/{SiteHealthSection-gVdylFY-.js → SiteHealthSection-DzFKj9lX.js} +3 -3
  11. package/assets/assets/{TrafficPage-YIdwRpQh.js → TrafficPage-CkIGVLET.js} +1 -1
  12. package/assets/assets/{TrafficSourceDetailPage-B4Cop6ox.js → TrafficSourceDetailPage-CTmn_FmZ.js} +1 -1
  13. package/assets/assets/{extract-error-message-XavUZNf3.js → extract-error-message-CC2kxd3e.js} +1 -1
  14. package/assets/assets/{index-njfYnO22.js → index-CCPMPGar.js} +25 -25
  15. package/assets/assets/{react-sigma_core.esm.min-V7giUpEn.js → react-sigma_core.esm.min-DkpQa1sf.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-WSD5J6O4.js → chunk-32Q2NZ4S.js} +2 -2
  23. package/dist/{chunk-2A7E5TDU.js → chunk-36RKISAI.js} +5 -4
  24. package/dist/{chunk-KBUV3HGS.js → chunk-BULKBROE.js} +1326 -1227
  25. package/dist/{chunk-ZK4FJDCH.js → chunk-FTKZAOFD.js} +436 -205
  26. package/dist/{chunk-XI6FN52S.js → chunk-JOSSB4QC.js} +45 -14
  27. package/dist/cli.js +14 -10
  28. package/dist/index.js +4 -4
  29. package/dist/{intelligence-service-EVRWZHDG.js → intelligence-service-MEQ5VX5L.js} +2 -2
  30. package/dist/mcp.js +3 -3
  31. package/package.json +10 -10
  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-BULKBROE.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
@@ -18109,6 +18222,7 @@ function mergeTargets(authoring, body) {
18109
18222
  const merged = {
18110
18223
  ...survivor,
18111
18224
  aliases: unique([...survivor.aliases, ...authoring.targets.filter((target) => absorbedSet.has(target.stableKey)).flatMap((target) => target.aliases)]),
18225
+ ...[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
18226
  urlMatchers: unique([...survivor.urlMatchers, ...authoring.targets.filter((target) => absorbedSet.has(target.stableKey)).flatMap((target) => target.urlMatchers)])
18113
18227
  };
18114
18228
  const assignments = [];
@@ -19272,6 +19386,7 @@ function seedAuthoring(project, active, context, opts) {
19272
19386
  label: target.label,
19273
19387
  status: "included",
19274
19388
  aliases: [...target.aliases],
19389
+ ...target.identityAliases === void 0 ? {} : { identityAliases: [...target.identityAliases] },
19275
19390
  urlMatchers: target.urlMatchers.map(formatDraftMatcher),
19276
19391
  source: target.discoveryIdentity ? "sitemap" : "manual",
19277
19392
  ...target.discoveryIdentity ? { discoveryIdentity: target.discoveryIdentity } : {}
@@ -21198,6 +21313,7 @@ function questionRows(plan, target, materialized, filters = {}) {
21198
21313
  }
21199
21314
  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
21315
  const ownEdgeIds = new Set([...ownEdgesByExecution.values()].map((edge) => edge.id));
21316
+ const answerBySlot = new Map(materialized.evidence.answers.filter((answer) => ownEdgeIds.has(answer.usageEdgeId)).map((answer) => [answer.expectedSlotId, answer]));
21201
21317
  const citedSlotIds = new Set(materialized.evidence.evidence.filter((row) => ownEdgeIds.has(row.usageEdgeId) && row.classification === "assigned").map((row) => row.expectedSlotId));
21202
21318
  const incompleteObservationIds = new Set(materialized.evidence.diagnostics.evidenceIncompleteObservationIds);
21203
21319
  const rows = [];
@@ -21229,8 +21345,8 @@ function questionRows(plan, target, materialized, filters = {}) {
21229
21345
  servedModel: snapshot?.servedModel ?? null,
21230
21346
  location: slot.location,
21231
21347
  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,
21348
+ mentioned: answered2 && !target.mentionNotApplicable ? answerBySlot.get(slot.id)?.mentioned ?? null : null,
21349
+ cited: answered2 ? citedSlotIds.has(slot.id) ? true : incompleteObservationIds.has(snapshot.id) ? null : false : null,
21234
21350
  recommendedInstead: answered2 ? recommendedInstead(snapshot.recommendedCompetitors, target) : [],
21235
21351
  answerExcerpt: answered2 ? excerptOf(answer) : null,
21236
21352
  edgeId: edge.id
@@ -21411,6 +21527,8 @@ function runIdentity(run) {
21411
21527
  }
21412
21528
  function metricReason2(reason) {
21413
21529
  switch (reason) {
21530
+ case "identity-ambiguous":
21531
+ return "identity_ambiguous";
21414
21532
  case "no-population":
21415
21533
  return "no_population";
21416
21534
  case "incomplete":
@@ -21460,6 +21578,7 @@ function targetAnswers(plan, target, materialized, filters) {
21460
21578
  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
21579
  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
21580
  const ownEdgeIds = new Set([...edgesByExecution.values()].map((edge) => edge.id));
21581
+ const answerBySlot = new Map(materialized.evidence.answers.filter((answer) => ownEdgeIds.has(answer.usageEdgeId)).map((answer) => [answer.expectedSlotId, answer]));
21463
21582
  const citedSlotIds = new Set(materialized.evidence.evidence.filter((row) => ownEdgeIds.has(row.usageEdgeId) && row.classification === "assigned").map((row) => row.expectedSlotId));
21464
21583
  const incompleteObservationIds = new Set(materialized.evidence.diagnostics.evidenceIncompleteObservationIds);
21465
21584
  const questionsById = new Map(plan.querySnapshots.map((question) => [question.queryId, question.queryText]));
@@ -21478,8 +21597,8 @@ function targetAnswers(plan, target, materialized, filters) {
21478
21597
  provider: slot.provider,
21479
21598
  question: questionsById.get(assignment.queryId) ?? slot.queryText,
21480
21599
  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)
21600
+ mentioned: target.mentionNotApplicable ? null : answerBySlot.get(slot.id)?.mentioned ?? null,
21601
+ cited: citedSlotIds.has(slot.id) ? true : incompleteObservationIds.has(snapshot.id) ? null : false
21483
21602
  });
21484
21603
  }
21485
21604
  return { expected: slots.length, answers };
@@ -22094,10 +22213,10 @@ function selectedClasses(value) {
22094
22213
  function unavailable4(reason) {
22095
22214
  return { numerator: null, denominator: null, rate: null, reason };
22096
22215
  }
22097
- function rate(values, expected) {
22216
+ function rate(values, expected, unknownReason = "evidence-incomplete") {
22098
22217
  if (expected === 0) return unavailable4("no-population");
22099
22218
  if (values.length !== expected) return unavailable4("incomplete");
22100
- if (values.some((value) => value === null)) return unavailable4("evidence-incomplete");
22219
+ if (values.some((value) => value === null)) return unavailable4(unknownReason);
22101
22220
  const denominator = values.length;
22102
22221
  if (denominator === 0) return unavailable4("no-population");
22103
22222
  const numerator = values.filter(Boolean).length;
@@ -22199,17 +22318,30 @@ function targetValues(candidate, targets, targetKey) {
22199
22318
  const targetKeys = targetKey === void 0 ? [...new Set(candidate.edges.map((edge) => edge.targetKey))] : [targetKey];
22200
22319
  const eligible = targetKeys.filter((key) => targets.get(key)?.mentionEligible === true);
22201
22320
  const observation = candidate.observation;
22321
+ const citation = observation && targetKeys.some((key) => observation.citedTargetKeys.includes(key)) ? true : observation?.citationComplete && targetKeys.length > 0 ? false : null;
22202
22322
  const mentionComplete = observation !== null && (observation.mentionComplete ?? observation.answerText !== null);
22203
22323
  if (!observation || !mentionComplete || eligible.length === 0) {
22204
22324
  return {
22205
22325
  mention: null,
22206
- citation: observation?.citationComplete === true && targetKeys.length > 0 ? targetKeys.some((key) => observation.citedTargetKeys.includes(key)) : null
22326
+ citation
22207
22327
  };
22208
22328
  }
22209
- return {
22210
- mention: eligible.some((key) => observation.mentionedTargetKeys.includes(key)),
22211
- citation: observation.citationComplete ? targetKeys.some((key) => observation.citedTargetKeys.includes(key)) : null
22212
- };
22329
+ if (eligible.some((key) => observation.mentionedTargetKeys.includes(key))) return { mention: true, citation };
22330
+ if (eligible.some((key) => observation.unknownMentionTargetKeys?.includes(key))) {
22331
+ return { mention: null, citation, mentionUnavailableReason: "identity-ambiguous" };
22332
+ }
22333
+ return { mention: false, citation };
22334
+ }
22335
+ function citationForCoverage(candidate, targets, targetKey) {
22336
+ return candidate.observation?.citationComplete ? targetValues(candidate, targets, targetKey).citation : null;
22337
+ }
22338
+ function mentionRate2(candidates, targets, targetKey) {
22339
+ const values = candidates.map((candidate) => targetValues(candidate, targets, targetKey));
22340
+ return rate(
22341
+ values.map((value) => value.mention),
22342
+ values.length,
22343
+ values.some((value) => value.mentionUnavailableReason === "identity-ambiguous") ? "identity-ambiguous" : "evidence-incomplete"
22344
+ );
22213
22345
  }
22214
22346
  function answered(candidate) {
22215
22347
  return candidate.observation !== null;
@@ -22223,21 +22355,30 @@ function selectedTargetKeys(candidates) {
22223
22355
  function targetMetrics(candidates, targets, targetKey) {
22224
22356
  const own = candidates.filter((candidate) => candidate.edges.some((edge) => edge.targetKey === targetKey));
22225
22357
  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)
22358
+ mentionCoverage: mentionRate2(own, targets, targetKey),
22359
+ citationCoverage: rate(own.map((candidate) => citationForCoverage(candidate, targets, targetKey)), own.length)
22228
22360
  };
22229
22361
  }
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) {
22362
+ function targetPresence(candidates, targets, targetKey) {
22363
+ const values = candidates.filter((candidate) => candidate.edges.some((edge) => edge.targetKey === targetKey)).map((candidate) => targetValues(candidate, targets, targetKey));
22364
+ const anyKnown = (signals) => signals.includes(true) ? true : signals.length === 0 || signals.includes(null) ? null : false;
22365
+ return {
22366
+ targetKey,
22367
+ mention: anyKnown(values.map((value) => value.mention)),
22368
+ citation: anyKnown(values.map((value) => value.citation)),
22369
+ identityAmbiguous: values.some((value) => value.mentionUnavailableReason === "identity-ambiguous")
22370
+ };
22371
+ }
22372
+ function outcomeCounts(rows) {
22373
+ const counts2 = { bothSignals: 0, mentionedOnly: 0, citedOnly: 0, neither: 0, notMeasured: 0, total: rows.length };
22374
+ for (const row of rows) {
22375
+ if (row.mention === null || row.citation === null) {
22235
22376
  counts2.notMeasured++;
22236
- } else if (metrics.mentionCoverage.numerator > 0 && metrics.citationCoverage.numerator > 0) {
22377
+ } else if (row.mention && row.citation) {
22237
22378
  counts2.bothSignals++;
22238
- } else if (metrics.mentionCoverage.numerator > 0) {
22379
+ } else if (row.mention) {
22239
22380
  counts2.mentionedOnly++;
22240
- } else if (metrics.citationCoverage.numerator > 0) {
22381
+ } else if (row.citation) {
22241
22382
  counts2.citedOnly++;
22242
22383
  } else {
22243
22384
  counts2.neither++;
@@ -22252,19 +22393,20 @@ function coverageSummary(candidates, definition, targets = targetMap(definition)
22252
22393
  rows,
22253
22394
  queryCount: new Set(rows.map((candidate) => candidate.slot.queryKey)).size,
22254
22395
  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)
22396
+ mentionCoverage: mentionRate2(rows, targets),
22397
+ citationCoverage: rate(rows.map((candidate) => citationForCoverage(candidate, targets)), rows.length)
22257
22398
  };
22258
22399
  }
22259
22400
  function summary(candidates, definition, populationTargetKeys = selectedTargetKeys(candidates)) {
22260
22401
  const targets = targetMap(definition);
22261
22402
  const base = coverageSummary(candidates, definition, targets);
22262
22403
  const { rows } = base;
22263
- const targetRows = populationTargetKeys.map((targetKey) => ({ targetKey, ...targetMetrics(rows, targets, targetKey) }));
22404
+ const targetRows = populationTargetKeys.map((targetKey) => targetPresence(rows, targets, targetKey));
22264
22405
  const eligibleTargets = targetRows.filter((row) => targets.get(row.targetKey)?.mentionEligible === true);
22265
22406
  const propertyReach = rate(
22266
- eligibleTargets.map((row) => row.mentionCoverage.numerator === null ? null : row.mentionCoverage.numerator > 0),
22267
- eligibleTargets.length
22407
+ eligibleTargets.map((row) => row.mention),
22408
+ eligibleTargets.length,
22409
+ eligibleTargets.some((row) => row.mention === null && row.identityAmbiguous) ? "identity-ambiguous" : "evidence-incomplete"
22268
22410
  );
22269
22411
  return {
22270
22412
  queryCount: base.queryCount,
@@ -22272,7 +22414,7 @@ function summary(candidates, definition, populationTargetKeys = selectedTargetKe
22272
22414
  mentionCoverage: base.mentionCoverage,
22273
22415
  citationCoverage: base.citationCoverage,
22274
22416
  propertyReach,
22275
- outcomes: outcomeCounts(rows, targets, populationTargetKeys)
22417
+ outcomes: outcomeCounts(targetRows)
22276
22418
  };
22277
22419
  }
22278
22420
  function queryRows(candidates, definition, selection) {
@@ -22319,6 +22461,7 @@ function evidenceRows(candidates, definition, selection) {
22319
22461
  location: candidate.slot.location,
22320
22462
  targetKeys: selectedTargetKeys([candidate]),
22321
22463
  mentioned: signals.mention,
22464
+ ...signals.mentionUnavailableReason === void 0 ? {} : { mentionUnavailableReason: signals.mentionUnavailableReason },
22322
22465
  cited: signals.citation,
22323
22466
  // Answer bodies are intentionally limited to the stable query-key
22324
22467
  // drill-in. A query-id is enough to locate an evidence page, but it
@@ -22389,8 +22532,8 @@ function breakdown(candidates, definition, populationTargetKeys = selectedTarget
22389
22532
  const groups = definition.groups.map((group) => ({ group, targetKeys: group.targetKeys.filter((targetKey) => populationTargetSet.has(targetKey)) })).filter(({ targetKeys }) => targetKeys.length > 0).map(({ group, targetKeys }) => {
22390
22533
  const groupTargetKeys = new Set(targetKeys);
22391
22534
  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);
22535
+ const metrics = mentionRate2(own, targets);
22536
+ const citations = rate(own.map((candidate) => citationForCoverage(candidate, targets)), own.length);
22394
22537
  return {
22395
22538
  id: group.id,
22396
22539
  label: group.label,
@@ -22785,52 +22928,42 @@ function v2Definition(plan, revision, manifest, run) {
22785
22928
  edges
22786
22929
  };
22787
22930
  }
22788
- function v2Observations(plan, revision, run, snapshots) {
22931
+ function v2Observations(plan, revision, run, snapshots, includeEvidence = true) {
22789
22932
  const manifest = measurementRunExpectedSlots(run, plan);
22790
22933
  const materialized = buildMeasurementPlanV2ReportInput(revision, plan, manifest, adapterSnapshots(snapshots));
22791
- const evidence = buildMeasurementEvidence(materialized.input);
22934
+ const signalsByObservation = new Map(buildMeasurementObservationSignals(materialized.input).map((signal) => [signal.observationId, signal]));
22792
22935
  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
22936
  const competitors2 = exactCompetitorMatchers(plan);
22801
22937
  const output = [];
22802
22938
  for (const observation of materialized.input.observations) {
22803
22939
  const raw = rawById.get(observation.id);
22804
22940
  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);
22941
+ const attribution = signalsByObservation.get(observation.id);
22942
+ const signals = includeEvidence ? competitorSignals(raw, competitors2) : { mentioned: [], cited: [] };
22811
22943
  output.push({
22812
22944
  slotId: `slot:${observation.executionId}:${observation.provider}`,
22813
22945
  answerId: raw.id,
22814
22946
  model: raw.servedModel?.trim() || null,
22815
- answerText: raw.answerText,
22947
+ answerText: includeEvidence ? raw.answerText : null,
22816
22948
  // `answerMentioned` is the legacy project-level boolean. It cannot say
22817
22949
  // WHICH Property an answer named, so a portfolio may use it neither as
22818
22950
  // a positive nor as a measured negative when the answer body is absent.
22819
22951
  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),
22952
+ mentionedTargetKeys: attribution?.mentionedTargetIds ?? [],
22953
+ unknownMentionTargetKeys: attribution?.unknownMentionTargetIds ?? [],
22954
+ citedTargetKeys: attribution?.citedTargetIds ?? [],
22955
+ citationComplete: attribution?.sourceComplete ?? false,
22823
22956
  competitorMentionDomains: signals.mentioned,
22824
22957
  competitorCitationDomains: signals.cited,
22825
- observedCompetitorNames: observedCompetitorNames2(raw.recommendedCompetitors),
22826
- sources: [...new Set(answers.flatMap((answer) => answer.sources.map((source) => source.sourceUrl)))],
22958
+ observedCompetitorNames: includeEvidence ? observedCompetitorNames2(raw.recommendedCompetitors) : [],
22959
+ sources: includeEvidence ? attribution?.sourceUrls ?? [] : [],
22827
22960
  createdAt: raw.createdAt
22828
22961
  });
22829
22962
  }
22830
22963
  return { manifest, observations: output };
22831
22964
  }
22832
- function advancedRun(run, version, plan, snapshots, comparableDefinitionIds) {
22833
- const populated = v2Observations(plan, version.revision, run, snapshots);
22965
+ function advancedRun(run, version, plan, snapshots, comparableDefinitionIds, includeEvidence = true) {
22966
+ const populated = v2Observations(plan, version.revision, run, snapshots, includeEvidence);
22834
22967
  return {
22835
22968
  id: run.id,
22836
22969
  createdAt: run.createdAt,
@@ -22877,7 +23010,7 @@ function simpleModelFingerprint(definition) {
22877
23010
  const engines = [...definition.engines].map((engine) => ({ provider: normalizedText3(engine.provider), requestedModel: engine.requestedModel })).sort((left, right) => left.provider.localeCompare(right.provider, "en"));
22878
23011
  return `simple-model:${createHash4("sha256").update(JSON.stringify(engines)).digest("hex")}`;
22879
23012
  }
22880
- function frozenSimpleRun(run, definition, snapshots) {
23013
+ function frozenSimpleRun(run, definition, snapshots, includeEvidence = true) {
22881
23014
  const matcher = compileBrandAliases(effectiveBrandNames({
22882
23015
  displayName: definition.identity.displayName,
22883
23016
  aliases: definition.identity.aliases,
@@ -22925,20 +23058,20 @@ function frozenSimpleRun(run, definition, snapshots) {
22925
23058
  if (!slotKeys.has(slotId)) throw new Error(`Frozen simple definition has no slot for stored snapshot ${snapshot.id}`);
22926
23059
  const preparedAnswerText = prepareBrandMatchText(snapshot.answerText);
22927
23060
  const mentioned = snapshot.answerText !== null ? matcherMatchesText(matcher, preparedAnswerText) : snapshot.answerMentioned === true;
22928
- const competitorSignalsForSnapshot = competitors2 === null ? { mentioned: [], cited: [] } : competitorSignals(snapshot, competitors2, preparedAnswerText);
23061
+ const competitorSignalsForSnapshot = !includeEvidence || competitors2 === null ? { mentioned: [], cited: [] } : competitorSignals(snapshot, competitors2, preparedAnswerText);
22929
23062
  observations.push({
22930
23063
  slotId,
22931
23064
  answerId: snapshot.id,
22932
23065
  model: snapshot.servedModel?.trim() || null,
22933
- answerText: snapshot.answerText,
23066
+ answerText: includeEvidence ? snapshot.answerText : null,
22934
23067
  mentionComplete: snapshot.answerText !== null || snapshot.answerMentioned !== null,
22935
23068
  mentionedTargetKeys: mentioned ? ["project"] : [],
22936
23069
  citedTargetKeys: snapshot.citationState === "cited" ? ["project"] : [],
22937
23070
  citationComplete: true,
22938
23071
  competitorMentionDomains: competitorSignalsForSnapshot.mentioned,
22939
23072
  competitorCitationDomains: competitorSignalsForSnapshot.cited,
22940
- observedCompetitorNames: observedCompetitorNames2(snapshot.recommendedCompetitors),
22941
- sources: snapshot.citedUrls ?? [],
23073
+ observedCompetitorNames: includeEvidence ? observedCompetitorNames2(snapshot.recommendedCompetitors) : [],
23074
+ sources: includeEvidence ? snapshot.citedUrls ?? [] : [],
22942
23075
  createdAt: snapshot.createdAt
22943
23076
  });
22944
23077
  }
@@ -22970,7 +23103,7 @@ function frozenSimpleRun(run, definition, snapshots) {
22970
23103
  observations
22971
23104
  };
22972
23105
  }
22973
- function legacySimpleRun(project, run, snapshots) {
23106
+ function legacySimpleRun(project, run, snapshots, includeEvidence = true) {
22974
23107
  const slots = snapshots.map((snapshot) => ({
22975
23108
  id: `slot:legacy:${snapshot.id}`,
22976
23109
  executionId: `legacy:${snapshot.id}`,
@@ -23008,7 +23141,7 @@ function legacySimpleRun(project, run, snapshots) {
23008
23141
  slotId: `slot:legacy:${snapshot.id}`,
23009
23142
  answerId: snapshot.id,
23010
23143
  model: snapshot.servedModel?.trim() || null,
23011
- answerText: snapshot.answerText,
23144
+ answerText: includeEvidence ? snapshot.answerText : null,
23012
23145
  // A legacy answer body cannot be matched against today's identity. Only
23013
23146
  // the stored boolean is a frozen mention fact; null stays incomplete.
23014
23147
  mentionComplete: snapshot.answerMentioned !== null,
@@ -23017,8 +23150,8 @@ function legacySimpleRun(project, run, snapshots) {
23017
23150
  citationComplete: true,
23018
23151
  competitorMentionDomains: [],
23019
23152
  competitorCitationDomains: [],
23020
- observedCompetitorNames: observedCompetitorNames2(snapshot.recommendedCompetitors),
23021
- sources: snapshot.citedUrls ?? [],
23153
+ observedCompetitorNames: includeEvidence ? observedCompetitorNames2(snapshot.recommendedCompetitors) : [],
23154
+ sources: includeEvidence ? snapshot.citedUrls ?? [] : [],
23022
23155
  createdAt: snapshot.createdAt
23023
23156
  }))
23024
23157
  };
@@ -23156,41 +23289,37 @@ function advancedReaderInput(db, projectId, active, query) {
23156
23289
  const presentationComparableIds = new Set(comparableVersionIds(allVersionRowsById, presentationVersion.id));
23157
23290
  const activeComparableIds = new Set(comparableVersionIds(allVersionRowsById, active.version.id));
23158
23291
  const sourceRuns = completedVisibilityRuns(db, projectId, false, query);
23159
- const snapshotsByRun = loadVisibilitySnapshots(db, sourceRuns.map((run) => run.id));
23160
23292
  const sourceCandidates = sourceRuns.flatMap((run) => {
23161
23293
  if (run.measurementPlanVersionId === null) return [];
23162
23294
  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));
23295
+ if (!source || query.revision !== void 0 && !presentationComparableIds.has(source.row.id)) return [];
23296
+ return [{ run, source }];
23297
+ });
23173
23298
  if (query.runId !== void 0 && !sourceCandidates.some((candidate) => candidate.run.id === query.runId)) {
23174
23299
  throw validationError(`Measurement run "${query.runId}" is not an eligible advanced result.`);
23175
23300
  }
23301
+ const compatibleSourceCandidates = sourceCandidates.filter((candidate) => activeComparableIds.has(candidate.source.row.id));
23302
+ const preferredSource = query.runId === void 0 ? compatibleSourceCandidates.at(0) ?? sourceCandidates.at(0) : void 0;
23303
+ const selectedSource = query.runId === void 0 ? preferredSource : sourceCandidates.find((candidate) => candidate.run.id === query.runId);
23176
23304
  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;
23305
+ const usePresentation = presentationComparableIds.has(candidate.source.row.id);
23306
+ const version = usePresentation ? presentationVersion : candidate.source.row;
23307
+ const plan = usePresentation ? presentationPlan : candidate.source.plan;
23308
+ const snapshots = loadVisibilitySnapshots(db, [candidate.run.id]).get(candidate.run.id) ?? [];
23179
23309
  return advancedRun(
23180
23310
  candidate.run,
23181
- presentationVersion,
23182
- presentationPlan,
23183
- snapshotsByRun.get(candidate.run.id) ?? [],
23184
- [...presentationComparableIds]
23311
+ version,
23312
+ plan,
23313
+ snapshots,
23314
+ usePresentation ? [...presentationComparableIds] : comparableVersionIds(allVersionRowsById, candidate.source.row.id),
23315
+ candidate.run.id === selectedSource?.run.id
23185
23316
  );
23186
23317
  });
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);
23318
+ const selectedDefinition = candidates.find((candidate) => candidate.id === selectedSource?.run.id)?.definition;
23190
23319
  return {
23191
23320
  mode: "advanced",
23192
23321
  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),
23322
+ pendingAssignmentCount: selectedSource !== void 0 && activeComparableIds.has(selectedSource.source.row.id) ? 0 : pendingAssignments(active.plan, selectedSource?.source.plan, selectedDefinition),
23194
23323
  selection: query,
23195
23324
  activeDefinition: activeV2Definition(presentationPlan, presentationVersion.revision),
23196
23325
  ...preferredSource === void 0 ? {} : { preferredRunId: preferredSource.run.id },
@@ -23203,11 +23332,12 @@ function simpleReaderInput(db, project, query) {
23203
23332
  eq24(simpleMeasurementDefinitions.projectId, project.id),
23204
23333
  inArray9(simpleMeasurementDefinitions.runId, sourceRuns.map((run) => run.id))
23205
23334
  )).all().map((row) => [row.runId, row.definition]));
23206
- const snapshotsByRun = loadVisibilitySnapshots(db, sourceRuns.map((run) => run.id));
23335
+ const selectedRunId = sourceRuns[0]?.id;
23207
23336
  const candidates = sourceRuns.map((run) => {
23208
- const snapshots = snapshotsByRun.get(run.id) ?? [];
23337
+ const snapshots = loadVisibilitySnapshots(db, [run.id]).get(run.id) ?? [];
23209
23338
  const definition = frozen.get(run.id);
23210
- return definition ? frozenSimpleRun(run, definition, snapshots) : legacySimpleRun(project, run, snapshots);
23339
+ const includeEvidence = run.id === selectedRunId;
23340
+ return definition ? frozenSimpleRun(run, definition, snapshots, includeEvidence) : legacySimpleRun(project, run, snapshots, includeEvidence);
23211
23341
  });
23212
23342
  if (query.runId !== void 0 && !candidates.some((run) => run.id === query.runId)) {
23213
23343
  throw validationError(`Measurement run "${query.runId}" is not an eligible simple result.`);
@@ -23221,37 +23351,28 @@ function simpleReaderInput(db, project, query) {
23221
23351
  runs: candidates
23222
23352
  };
23223
23353
  }
23354
+ function readVisibilityReport(db, project, rawQuery) {
23355
+ const query = parseQuery(rawQuery);
23356
+ const active = activeMeasurementPlan(db, project.id);
23357
+ const mode = query.mode === "auto" ? active === null ? "simple" : "advanced" : query.mode;
23358
+ try {
23359
+ if (mode === "advanced") {
23360
+ if (active === null) throw validationError("This project has no advanced measurement plan.");
23361
+ if (active.plan.schemaVersion !== MEASUREMENT_PLAN_V2_SCHEMA_VERSION) return unsupportedAdvancedResponse(query, active);
23362
+ return buildVisibilityReport(advancedReaderInput(db, project.id, active, query));
23363
+ }
23364
+ return buildVisibilityReport(simpleReaderInput(db, project, query));
23365
+ } catch (error) {
23366
+ if (error instanceof VisibilityReportCursorError || error instanceof VisibilityReportScopeError) {
23367
+ throw validationError(error.message);
23368
+ }
23369
+ throw error;
23370
+ }
23371
+ }
23224
23372
  async function visibilityReportRoutes(app) {
23225
23373
  app.get(
23226
23374
  "/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
- }
23375
+ async (request) => readVisibilityReport(app.db, resolveProject(app.db, request.params.name), request.query)
23255
23376
  );
23256
23377
  }
23257
23378
 
@@ -23500,6 +23621,7 @@ function frozenAssignmentSignature(plan, assignment) {
23500
23621
  target: {
23501
23622
  stableKey: target.stableKey,
23502
23623
  aliases: target.aliases,
23624
+ ...target.identityAliases === void 0 ? {} : { identityAliases: target.identityAliases },
23503
23625
  urlMatchers: target.urlMatchers,
23504
23626
  mentionNotApplicable: target.mentionNotApplicable,
23505
23627
  discoveryIdentity: target.discoveryIdentity
@@ -24001,6 +24123,7 @@ function compileExecution(state, target, queryId, queryText, queryClass, classif
24001
24123
  label: target.label,
24002
24124
  status: "included",
24003
24125
  aliases: [...target.aliases],
24126
+ ...target.identityAliases === void 0 ? {} : { identityAliases: [...target.identityAliases] },
24004
24127
  urlMatchers: target.urlMatchers.map(draftMatcher),
24005
24128
  source: target.discoveryIdentity ? "sitemap" : "manual",
24006
24129
  ...target.discoveryIdentity ? { discoveryIdentity: target.discoveryIdentity } : {}
@@ -28764,10 +28887,10 @@ function renderExecutiveSummary(report) {
28764
28887
  const trendLabel = s.trend === "up" ? "\u2191 Up" : s.trend === "down" ? "\u2193 Down" : s.trend === "flat" ? "\u2192 Flat" : "\u2014";
28765
28888
  const trendTone = s.trend === "up" ? "positive" : s.trend === "down" ? "negative" : "neutral";
28766
28889
  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.";
28890
+ const citedFragment = (s.totalQueryCount ?? 0) > 0 ? `${s.citedQueryCount}/${s.totalQueryCount} ${queryNoun} cited` : "no queries";
28891
+ const mentionedFragment = (s.totalQueryCount ?? 0) > 0 ? `${s.mentionedQueryCount}/${s.totalQueryCount} ${queryNoun} mentioned` : "no queries";
28892
+ const headlineTitle = (s.totalQueryCount ?? 0) > 0 ? `${s.citedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} cite ${report.meta.project.displayName}` : "No AI citation data yet";
28893
+ 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
28894
  const priorityActions = report.agencyDiagnostics.priorities.length > 0 ? report.agencyDiagnostics.priorities : report.actionPlan;
28772
28895
  const actionCount = dedupeReportActions(report, priorityActions).length;
28773
28896
  const heroHtml = `<div class="executive-hero">
@@ -29864,9 +29987,9 @@ function renderAudienceActionPlan(report, audience) {
29864
29987
  function renderClientSummary(report) {
29865
29988
  const s = report.executiveSummary;
29866
29989
  const sc = report.citationScorecard;
29867
- const totalQ = s.totalQueryCount;
29990
+ const totalQ = s.totalQueryCount ?? 0;
29868
29991
  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.";
29992
+ 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
29993
  const trend = clientTrendCopy(report.whatsChanged.mentionRate);
29871
29994
  const heroTrend = trend ? `<p class="client-hero-trend tone-${trend.tone}"><span style="margin-right:6px;">${trend.arrow}</span>${escapeHtml2(trend.text)}</p>` : "";
29872
29995
  const hero = `<div class="client-hero">
@@ -29880,12 +30003,12 @@ function renderClientSummary(report) {
29880
30003
  <div class="client-metric-tile">
29881
30004
  <div class="label">AI mentions your name</div>
29882
30005
  <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>
30006
+ <div class="subtitle">${totalQ > 0 ? `Says your name in ${s.mentionedQueryCount} of ${totalQ} ${pluralize(totalQ, "query", "queries")}` : "No data yet"}</div>
29884
30007
  </div>
29885
30008
  <div class="client-metric-tile">
29886
30009
  <div class="label">AI links to your website</div>
29887
30010
  <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>
30011
+ <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
30012
  </div>
29890
30013
  <div class="client-metric-tile">
29891
30014
  <div class="label">AI tools tested</div>
@@ -30026,9 +30149,9 @@ function renderReportHtml(report, opts = {}) {
30026
30149
  const audience = opts.audience ?? "agency";
30027
30150
  const title = opts.title ?? `Canonry ${audience} report \u2014 ${report.meta.project.displayName}`;
30028
30151
  const sections = audience === "client" ? [
30029
- renderClientSummary(report),
30030
- renderReportShareOfVoice(report),
30031
- renderWhatsChanged(report, "client"),
30152
+ report.visibility ? renderReportVisibility(report.visibility) : renderClientSummary(report),
30153
+ report.visibility?.selection.mode === "advanced" ? "" : renderReportShareOfVoice(report),
30154
+ report.visibility ? "" : renderWhatsChanged(report, "client"),
30032
30155
  // Server-side AI visibility runs between WhatsChanged and the action
30033
30156
  // plan in BOTH the SPA and HTML so clients see the same ordered set
30034
30157
  // of sections in either surface (per the report-parity rule).
@@ -30036,13 +30159,13 @@ function renderReportHtml(report, opts = {}) {
30036
30159
  renderAudienceActionPlan(report, "client"),
30037
30160
  renderClientEvidenceSummary(report)
30038
30161
  ].join("\n") : [
30039
- renderExecutiveSummary(report),
30040
- renderReportShareOfVoice(report),
30041
- renderWhatsChanged(report, "agency"),
30162
+ report.visibility ? renderReportVisibility(report.visibility) : renderExecutiveSummary(report),
30163
+ report.visibility?.selection.mode === "advanced" ? "" : renderReportShareOfVoice(report),
30164
+ report.visibility ? "" : renderWhatsChanged(report, "agency"),
30042
30165
  renderAudienceActionPlan(report, "agency"),
30043
- renderAgencyDiagnostics(report),
30044
- renderCitationScorecard(report),
30045
- renderCompetitorLandscape(report),
30166
+ report.visibility?.selection.mode === "advanced" ? "" : renderAgencyDiagnostics(report),
30167
+ report.visibility?.selection.mode === "advanced" ? "" : renderCitationScorecard(report),
30168
+ report.visibility?.selection.mode === "advanced" ? "" : renderCompetitorLandscape(report),
30046
30169
  renderAiSourceOrigin(report),
30047
30170
  renderGsc(report),
30048
30171
  renderGa(report),
@@ -30050,7 +30173,7 @@ function renderReportHtml(report, opts = {}) {
30050
30173
  renderAiReferrals(report),
30051
30174
  renderServerActivity(report, "agency"),
30052
30175
  renderIndexingHealth(report),
30053
- renderCitationsTrend(report),
30176
+ report.visibility?.selection.mode === "advanced" ? "" : renderCitationsTrend(report),
30054
30177
  renderInsights(report),
30055
30178
  renderOpportunities(report),
30056
30179
  renderContentGaps(report),
@@ -30072,7 +30195,7 @@ function renderReportHtml(report, opts = {}) {
30072
30195
  <header class="header">
30073
30196
  <div class="eyebrow">AI Visibility Report</div>
30074
30197
  <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>
30198
+ <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
30199
  </header>
30077
30200
  ${sections}
30078
30201
  <footer class="footer">Generated by <a href="https://canonry.ai">canonry</a> \xB7 ${escapeHtml2(formatIsoDate(report.meta.generatedAt))}</footer>
@@ -30081,6 +30204,15 @@ function renderReportHtml(report, opts = {}) {
30081
30204
  </body>
30082
30205
  </html>`;
30083
30206
  }
30207
+ function renderReportVisibility(visibility) {
30208
+ const populations = visibility.populations.filter((population) => population.queryClass !== "unknown" || population.summary.answerCount > 0);
30209
+ const historyPopulations = visibility.populations.filter((population) => population.queryClass !== "unknown" || population.trend.some((point) => point.answerCount > 0));
30210
+ const rateCell = (rate2) => `<td><strong>${escapeHtml2(reportVisibilityRate(rate2))}</strong><p class="muted">${escapeHtml2(reportVisibilityEvidence(rate2))}</p></td>`;
30211
+ const headers = (labels) => `<thead><tr>${labels.map((label) => `<th>${escapeHtml2(label)}</th>`).join("")}</tr></thead>`;
30212
+ 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>`;
30213
+ 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>`;
30214
+ 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>`;
30215
+ }
30084
30216
 
30085
30217
  // ../api-routes/src/ga-ai-referral-aggregation.ts
30086
30218
  function normalizeAiTrafficClass(value) {
@@ -30942,7 +31074,6 @@ function buildIndexingHealth(db, projectId) {
30942
31074
  }
30943
31075
  function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
30944
31076
  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
31077
  const points = [];
30947
31078
  for (const run of visibilityRuns) {
30948
31079
  if (run.status !== RunStatuses.completed) continue;
@@ -30964,10 +31095,11 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
30964
31095
  providerCounts.set(snap.provider, counts2);
30965
31096
  }
30966
31097
  if (considered === 0) continue;
31098
+ const totalQueries = new Set(snaps.map((snapshot) => snapshot.queryId)).size;
30967
31099
  const citedQueryCount = citedQueryIds.size;
30968
31100
  const mentionedQueryCount = mentionedQueryIds.size;
30969
31101
  const citationRate = totalQueries > 0 ? Math.round(citedQueryCount / totalQueries * 100) : 0;
30970
- const mentionRate2 = totalQueries > 0 ? Math.round(mentionedQueryCount / totalQueries * 100) : 0;
31102
+ const mentionRate3 = totalQueries > 0 ? Math.round(mentionedQueryCount / totalQueries * 100) : 0;
30971
31103
  const providerRates = [...providerCounts.entries()].map(([provider, counts2]) => ({
30972
31104
  provider,
30973
31105
  citationRate: counts2.total > 0 ? Math.round(counts2.cited / counts2.total * 100) : 0,
@@ -30979,7 +31111,7 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
30979
31111
  citationRate,
30980
31112
  citedQueryCount,
30981
31113
  totalQueryCount: totalQueries,
30982
- mentionRate: mentionRate2,
31114
+ mentionRate: mentionRate3,
30983
31115
  mentionedQueryCount,
30984
31116
  providerRates
30985
31117
  });
@@ -31339,7 +31471,7 @@ function buildReportActionPlan(input) {
31339
31471
  });
31340
31472
  }
31341
31473
  if (actions.length === 0) {
31342
- const neverMeasured = input.citationScorecard.providers.length === 0;
31474
+ const neverMeasured = !(input.hasVisibilityMeasurement ?? input.citationScorecard.providers.length > 0);
31343
31475
  actions.push({
31344
31476
  audience: "both",
31345
31477
  priority: 90,
@@ -31372,12 +31504,12 @@ function mentionTrendSentence(delta) {
31372
31504
  function buildClientSummary(reportLike) {
31373
31505
  const s = reportLike.executiveSummary;
31374
31506
  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.";
31507
+ 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";
31508
+ 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
31509
  const confidenceNotes = [];
31378
31510
  if (s.totalQueryCount === 0) {
31379
31511
  confidenceNotes.push("Confidence is low until the first tracked query check completes.");
31380
- } else if (s.totalQueryCount < 5) {
31512
+ } else if ((s.totalQueryCount ?? 0) < 5) {
31381
31513
  confidenceNotes.push("Directional read: the tracked query set is still small, so each query has outsized impact on the percentage.");
31382
31514
  }
31383
31515
  if (isTrendBaseline(reportLike.citationsTrend)) {
@@ -31502,7 +31634,7 @@ function buildWhatsChanged(input) {
31502
31634
  direction: rateDirection(citationRateSmoothed.deltaAbs, RATE_REAL_MOVEMENT_THRESHOLD_PP)
31503
31635
  } : null;
31504
31636
  const mentionRateSmoothed = smoothedRunDelta(citationsTrend, (p) => p.mentionRate);
31505
- const mentionRate2 = enoughHistory && mentionRateSmoothed ? {
31637
+ const mentionRate3 = enoughHistory && mentionRateSmoothed ? {
31506
31638
  ...mentionRateSmoothed,
31507
31639
  direction: rateDirection(mentionRateSmoothed.deltaAbs, RATE_REAL_MOVEMENT_THRESHOLD_PP)
31508
31640
  } : null;
@@ -31549,7 +31681,7 @@ function buildWhatsChanged(input) {
31549
31681
  enoughHistory,
31550
31682
  headline,
31551
31683
  citationRate,
31552
- mentionRate: mentionRate2,
31684
+ mentionRate: mentionRate3,
31553
31685
  citedQueryCount,
31554
31686
  mentionedQueryCount,
31555
31687
  gscClicksDelta,
@@ -31563,14 +31695,30 @@ function buildWhatsChanged(input) {
31563
31695
  function buildProjectReport(db, projectName, periodDays) {
31564
31696
  const project = resolveProject(db, projectName);
31565
31697
  const queryLookup = loadQueryLookup(db, project.id);
31698
+ const canonicalVisibility = readVisibilityReport(db, project, { queryClass: "all", scope: "project" });
31699
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
31700
+ const historyWindow = { from: new Date(Date.parse(generatedAt) - periodDays * 864e5).toISOString(), to: generatedAt };
31701
+ const visibility = {
31702
+ selection: canonicalVisibility.selection,
31703
+ historyWindow,
31704
+ populations: canonicalVisibility.populations.map(({ queryClass, summary: summary2, trend: trend2 }) => ({
31705
+ queryClass,
31706
+ summary: summary2,
31707
+ // Keep each point's actual comparison. Renderers disclose when the prior
31708
+ // measurement is outside this window rather than inventing a baseline.
31709
+ trend: trend2.filter((point) => point.createdAt >= historyWindow.from && point.createdAt <= historyWindow.to)
31710
+ }))
31711
+ };
31712
+ const canonicalVisibilitySupported = visibility.selection.availability.state === "available";
31713
+ const advancedVisibility = canonicalVisibilitySupported && visibility.selection.mode === "advanced";
31566
31714
  const comparisonWindowDays = reportComparisonWindowDays(periodDays);
31567
31715
  const allRuns = db.select().from(runs).where(and29(eq33(runs.projectId, project.id), notProbeRun())).orderBy(desc14(runs.createdAt), desc14(runs.id)).all();
31568
31716
  const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
31569
31717
  const completedVisRunGroups = groupRunsByCreatedAt(
31570
31718
  visibilityRuns.filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial)
31571
31719
  );
31572
- const latestVisRunGroup = completedVisRunGroups[0] ?? [];
31573
- const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? visibilityRuns[0] ?? null;
31720
+ const latestVisRunGroup = advancedVisibility ? visibilityRuns.filter((run) => run.id === visibility.selection.run.id) : completedVisRunGroups[0] ?? [];
31721
+ const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? (advancedVisibility ? null : visibilityRuns[0] ?? null);
31574
31722
  const latestSnapshots = loadSnapshotsForRunIds(db, latestVisRunGroup.map((r) => r.id));
31575
31723
  const latestRunLocation = representativeLatestRun?.location ?? null;
31576
31724
  const competitorRows2 = db.select().from(competitors).where(eq33(competitors.projectId, project.id)).all();
@@ -31623,7 +31771,8 @@ function buildProjectReport(db, projectName, periodDays) {
31623
31771
  }));
31624
31772
  }
31625
31773
  Object.assign(mentionLandscape, mentionLandscape.nonBrand);
31626
- const aiSourceOrigin = buildAiSourceOrigin(latestSnapshots, projectDomains, competitorDomains);
31774
+ 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() : [];
31775
+ const aiSourceOrigin = buildAiSourceOrigin(advancedVisibility ? advancedSourceSnapshots : latestSnapshots, projectDomains, competitorDomains);
31627
31776
  const trackedQueries = [...queryLookup.byId.values()];
31628
31777
  const gscSection = buildGscSection(
31629
31778
  db,
@@ -31658,7 +31807,7 @@ function buildProjectReport(db, projectName, periodDays) {
31658
31807
  insights: insightList,
31659
31808
  comparisonWindowDays
31660
31809
  });
31661
- const totalQueryCount = queryLookup.byId.size;
31810
+ const totalQueryCount = new Set(latestSnapshots.map((snapshot) => snapshot.queryId)).size;
31662
31811
  const citedQueryIds = /* @__PURE__ */ new Set();
31663
31812
  const mentionedQueryIds = /* @__PURE__ */ new Set();
31664
31813
  for (const snap of latestSnapshots) {
@@ -31669,7 +31818,7 @@ function buildProjectReport(db, projectName, periodDays) {
31669
31818
  const citedQueryCount = citedQueryIds.size;
31670
31819
  const mentionedQueryCount = mentionedQueryIds.size;
31671
31820
  const citationRate = totalQueryCount > 0 ? Math.round(citedQueryCount / totalQueryCount * 100) : 0;
31672
- const mentionRate2 = totalQueryCount > 0 ? Math.round(mentionedQueryCount / totalQueryCount * 100) : 0;
31821
+ const mentionRate3 = totalQueryCount > 0 ? Math.round(mentionedQueryCount / totalQueryCount * 100) : 0;
31673
31822
  const trendBaseline = isTrendBaseline(citationsTrend);
31674
31823
  const latestPoint = citationsTrend.at(-1);
31675
31824
  const previousPoint = citationsTrend.length >= 2 ? citationsTrend.at(-2) : null;
@@ -31694,21 +31843,22 @@ function buildProjectReport(db, projectName, periodDays) {
31694
31843
  insightList,
31695
31844
  competitorLandscape.competitors
31696
31845
  );
31697
- const periodStart = citationsTrend[0]?.date ?? null;
31698
- const periodEnd = citationsTrend.at(-1)?.date ?? null;
31846
+ const visibilityDates = visibility.populations.flatMap((population) => population.trend.map((point) => point.createdAt)).sort();
31847
+ const periodStart = advancedVisibility ? visibilityDates[0] ?? visibility.selection.measurement.completedAt : citationsTrend[0]?.date ?? null;
31848
+ const periodEnd = advancedVisibility ? visibility.selection.measurement.completedAt : citationsTrend.at(-1)?.date ?? null;
31699
31849
  const configuredLocations = project.locations;
31700
31850
  const reportLocation = buildLocationMeta(representativeLatestRun?.location ?? null, configuredLocations);
31701
- const providerLocationHandling = reportLocation ? buildProviderLocationHandling(citationScorecard.providers) : [];
31851
+ const providerLocationHandling = advancedVisibility ? buildProviderLocationHandling([...new Set(advancedSourceSnapshots.filter((snapshot) => snapshot.location !== null).map((snapshot) => snapshot.provider))]) : reportLocation ? buildProviderLocationHandling(citationScorecard.providers) : [];
31702
31852
  const executiveSummary = {
31703
31853
  citationRate,
31704
31854
  citedQueryCount,
31705
31855
  totalQueryCount,
31706
- mentionRate: mentionRate2,
31856
+ mentionRate: mentionRate3,
31707
31857
  mentionedQueryCount,
31708
31858
  trend,
31709
31859
  queryCount: queryLookup.byId.size,
31710
31860
  competitorCount: competitorDomains.length,
31711
- providerCount: citationScorecard.providers.length,
31861
+ providerCount: advancedVisibility ? new Set(advancedSourceSnapshots.map((snapshot) => snapshot.provider)).size : citationScorecard.providers.length,
31712
31862
  gsc: gscSection ? {
31713
31863
  clicks: gscSection.totalClicks,
31714
31864
  impressions: gscSection.totalImpressions,
@@ -31729,6 +31879,7 @@ function buildProjectReport(db, projectName, periodDays) {
31729
31879
  canonicalDomain: project.canonicalDomain,
31730
31880
  competitorDomains: actionPlanCompetitorDomains,
31731
31881
  citationScorecard,
31882
+ hasVisibilityMeasurement: advancedVisibility ? visibility.selection.run.id !== null : void 0,
31732
31883
  aiSourceOrigin,
31733
31884
  gsc: gscSection,
31734
31885
  indexingHealth: indexingHealthSection,
@@ -31750,6 +31901,7 @@ function buildProjectReport(db, projectName, periodDays) {
31750
31901
  canonicalDomain: project.canonicalDomain,
31751
31902
  competitorDomains,
31752
31903
  citationScorecard,
31904
+ hasVisibilityMeasurement: advancedVisibility ? visibility.selection.run.id !== null : void 0,
31753
31905
  aiSourceOrigin,
31754
31906
  gsc: gscSection,
31755
31907
  indexingHealth: indexingHealthSection,
@@ -31761,7 +31913,7 @@ function buildProjectReport(db, projectName, periodDays) {
31761
31913
  });
31762
31914
  return {
31763
31915
  meta: {
31764
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
31916
+ generatedAt,
31765
31917
  project: {
31766
31918
  id: project.id,
31767
31919
  name: project.name,
@@ -31776,7 +31928,18 @@ function buildProjectReport(db, projectName, periodDays) {
31776
31928
  periodEnd,
31777
31929
  periodDays
31778
31930
  },
31779
- executiveSummary,
31931
+ ...canonicalVisibilitySupported ? { visibility } : {},
31932
+ executiveSummary: advancedVisibility ? {
31933
+ ...executiveSummary,
31934
+ visibilityBasis: "frozen-populations",
31935
+ citationRate: null,
31936
+ citedQueryCount: null,
31937
+ totalQueryCount: null,
31938
+ mentionRate: null,
31939
+ mentionedQueryCount: null,
31940
+ trend: "unknown",
31941
+ findings: []
31942
+ } : { ...executiveSummary, visibilityBasis: "legacy-project-queries" },
31780
31943
  citationScorecard,
31781
31944
  competitorLandscape,
31782
31945
  mentionLandscape,
@@ -31787,12 +31950,28 @@ function buildProjectReport(db, projectName, periodDays) {
31787
31950
  aiReferrals: aiReferralsSection,
31788
31951
  serverActivity: serverActivitySection,
31789
31952
  indexingHealth: indexingHealthSection,
31790
- citationsTrend,
31791
- whatsChanged,
31953
+ citationsTrend: advancedVisibility ? [] : citationsTrend,
31954
+ whatsChanged: advancedVisibility ? {
31955
+ ...whatsChanged,
31956
+ enoughHistory: false,
31957
+ headline: "See measurement history for each query type.",
31958
+ citationRate: null,
31959
+ mentionRate: null,
31960
+ citedQueryCount: null,
31961
+ mentionedQueryCount: null,
31962
+ providerMovements: [],
31963
+ wins: [],
31964
+ regressions: []
31965
+ } : whatsChanged,
31792
31966
  insights: insightList,
31793
31967
  recommendedNextSteps,
31794
31968
  actionPlan,
31795
- clientSummary,
31969
+ clientSummary: advancedVisibility ? {
31970
+ ...clientSummary,
31971
+ headline: "AI visibility by query type",
31972
+ overview: "Mention and citation coverage use the properties assigned to each measured query.",
31973
+ confidenceNotes: []
31974
+ } : clientSummary,
31796
31975
  agencyDiagnostics,
31797
31976
  contentOpportunities,
31798
31977
  contentGaps,
@@ -34107,7 +34286,7 @@ function summarizeFromSnapshots(snapshots) {
34107
34286
  if (wasMentioned) mentionedQueries += 1;
34108
34287
  }
34109
34288
  const notMentionedQueries = totalQueries - mentionedQueries;
34110
- const mentionRate2 = totalQueries === 0 ? 0 : Number((mentionedQueries / totalQueries).toFixed(4));
34289
+ const mentionRate3 = totalQueries === 0 ? 0 : Number((mentionedQueries / totalQueries).toFixed(4));
34111
34290
  const providers = [...perProvider.entries()].map(([provider, { cited, total }]) => ({
34112
34291
  provider,
34113
34292
  cited,
@@ -34122,7 +34301,7 @@ function summarizeFromSnapshots(snapshots) {
34122
34301
  citedRate,
34123
34302
  mentionedQueries,
34124
34303
  notMentionedQueries,
34125
- mentionRate: mentionRate2
34304
+ mentionRate: mentionRate3
34126
34305
  },
34127
34306
  providers
34128
34307
  };
@@ -36314,7 +36493,7 @@ var routeCatalog = [
36314
36493
  path: "/api/v1/projects/{name}/research/runs",
36315
36494
  summary: "List saved research query batches",
36316
36495
  tags: ["research"],
36317
- parameters: [nameParameter, { name: "limit", in: "query", description: "Max runs, default 20 and maximum 100.", schema: integerSchema }],
36496
+ 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
36497
  responses: { 200: jsonResponse("Research runs returned newest first.", "ResearchRunListDto"), 404: errorResponse("Project not found.") }
36319
36498
  },
36320
36499
  {
@@ -83209,7 +83388,8 @@ async function technicalAeoRoutes(app, opts) {
83209
83388
 
83210
83389
  // ../api-routes/src/research.ts
83211
83390
  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";
83391
+ import { z as z5 } from "zod";
83392
+ 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
83393
  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
83394
  var BATCH_RECEIPT_PREFIX = "__canonry_research_batch__:";
83215
83395
  async function researchRoutes(app, opts) {
@@ -83222,11 +83402,19 @@ async function researchRoutes(app, opts) {
83222
83402
  }, async (request, reply) => {
83223
83403
  requireResearchGrant(request, opts.allowViewers ?? false);
83224
83404
  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
83405
  const parsed = researchRunCreateSchema.safeParse(request.body ?? {});
83227
83406
  if (!parsed.success) throw validationError("Invalid research run request", { issues: parsed.error.issues });
83228
83407
  const input = parsed.data;
83229
83408
  if (input.idempotencyKey?.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
83409
+ const requestHash2 = directResearchRequestHash(input);
83410
+ const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and55(eq65(researchRuns.projectId, project.id), eq65(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
83411
+ if (existingReceipt) {
83412
+ assertSameDirectResearchRequest(existingReceipt, input, requestHash2);
83413
+ const result2 = getDetail(app, project.id, existingReceipt.id);
83414
+ if (existingReceipt.status === ResearchRunStatuses.queued) opts.onResearchRunRequested?.(existingReceipt.id, project.id);
83415
+ return reply.status(200).send(result2);
83416
+ }
83417
+ if (!opts.onResearchRunRequested) throw missingDependency("Research execution is not available on this deployment.", { reason: "no-research-handler" });
83230
83418
  const adapters = opts.providerAdapters ?? [];
83231
83419
  const configured = new Set(opts.configuredProviderNames ?? []);
83232
83420
  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 +83424,24 @@ async function researchRoutes(app, opts) {
83236
83424
  adapter.modelValidationPattern.lastIndex = 0;
83237
83425
  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
83426
  }
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
83427
  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;
83428
+ const scope = input.scope ? resolveResearchScope(active, input.scope) : null;
83242
83429
  const location = input.location === void 0 ? scope ? null : project.defaultLocation ? project.locations.find((item2) => item2.label === project.defaultLocation) ?? null : null : input.location;
83243
83430
  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;
83431
+ const template = input.template ? resolveResearchTemplate(app, project.id, input.template, scope, location) : null;
83245
83432
  const requestedModel2 = input.model ?? null;
83246
83433
  const resolvedModel = requestedModel2 ?? (project.providerModels[providerName] || opts.getEffectiveProviderModels?.()[providerName] || adapter.defaultModel);
83247
83434
  adapter.modelValidationPattern.lastIndex = 0;
83248
83435
  if (!adapter.modelValidationPattern.test(resolvedModel)) throw validationError('Invalid resolved model "' + resolvedModel + '" for provider "' + providerName + '".', { provider: providerName, model: resolvedModel, hint: adapter.modelValidationHint });
83249
83436
  if (deduplicateResearchQueries(input.queries).length !== input.queries.length) throw validationError("Research queries must be unique within a batch.");
83250
83437
  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
83438
  const now = (/* @__PURE__ */ new Date()).toISOString();
83254
83439
  const initiatedBy = researchPrincipal(request);
83255
83440
  const decision = app.db.transaction((tx) => {
83256
83441
  if (input.idempotencyKey) {
83257
83442
  const existing = tx.select().from(researchRuns).where(and55(eq65(researchRuns.projectId, project.id), eq65(researchRuns.idempotencyKey, input.idempotencyKey))).get();
83258
83443
  if (existing) {
83259
- if (existing.requestHash !== requestHash2) throw alreadyExists("Research idempotency key", input.idempotencyKey);
83444
+ assertSameDirectResearchRequest(existing, input, requestHash2);
83260
83445
  return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
83261
83446
  }
83262
83447
  }
@@ -83371,7 +83556,14 @@ async function researchRoutes(app, opts) {
83371
83556
  const project = resolveProject(app.db, request.params.name);
83372
83557
  const requested = Number.parseInt(request.query.limit ?? "", 10);
83373
83558
  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);
83559
+ const cursor = request.query.cursor ? parseResearchCursor(request.query.cursor, project.id) : null;
83560
+ const rows = app.db.select().from(researchRuns).where(and55(
83561
+ eq65(researchRuns.projectId, project.id),
83562
+ cursor ? or14(lt11(researchRuns.createdAt, cursor.createdAt), and55(eq65(researchRuns.createdAt, cursor.createdAt), lt11(researchRuns.id, cursor.id))) : void 0
83563
+ )).orderBy(desc28(researchRuns.createdAt), desc28(researchRuns.id)).limit(limit + 1).all();
83564
+ const runs2 = rows.slice(0, limit).map(serializeRun);
83565
+ const last = runs2.at(-1);
83566
+ const nextCursor = rows.length > limit && last ? Buffer.from(JSON.stringify({ projectId: project.id, createdAt: last.createdAt, id: last.id })).toString("base64url") : null;
83375
83567
  const configured = new Set(opts.configuredProviderNames ?? []);
83376
83568
  const effectiveModels2 = opts.getEffectiveProviderModels?.() ?? {};
83377
83569
  const providers = (opts.providerAdapters ?? []).filter((adapter) => adapter.mode === "api" && !isBrowserProvider(adapter.name) && configured.has(adapter.name)).map((adapter) => {
@@ -83388,13 +83580,47 @@ async function researchRoutes(app, opts) {
83388
83580
  });
83389
83581
  const canRun = canRunResearch(request, opts.allowViewers ?? false);
83390
83582
  const limited = request.principal !== void 0 && !request.principal.scopes.includes(WILDCARD_SCOPE);
83391
- return { runs: runs2, providers, access: { canRun, dailyRunLimit: canRun && limited ? viewerDailyRunLimit : null } };
83583
+ return { runs: runs2, nextCursor, providers, access: { canRun, dailyRunLimit: canRun && limited ? viewerDailyRunLimit : null } };
83392
83584
  });
83393
83585
  app.get("/projects/:name/research/runs/:runId", async (request) => {
83394
83586
  const project = resolveProject(app.db, request.params.name);
83395
83587
  return getDetail(app, project.id, request.params.runId);
83396
83588
  });
83397
83589
  }
83590
+ var DIRECT_REQUEST_HASH_VERSION = "direct-v2:";
83591
+ function directResearchRequestHash(input) {
83592
+ const { idempotencyKey: _key, ...request } = input;
83593
+ return DIRECT_REQUEST_HASH_VERSION + crypto49.createHash("sha256").update(JSON.stringify(request)).digest("hex");
83594
+ }
83595
+ function assertSameDirectResearchRequest(existing, input, hash) {
83596
+ if (existing.requestHash?.startsWith(DIRECT_REQUEST_HASH_VERSION)) {
83597
+ if (existing.requestHash !== hash) throw alreadyExists("Research idempotency key", input.idempotencyKey);
83598
+ return;
83599
+ }
83600
+ const scope = input.scope ? resolveStoredResearchScope(existing.scope, input.scope, input.idempotencyKey) : null;
83601
+ const template = input.template ? resolveStoredResearchTemplate(existing.template, input.template, input.idempotencyKey) : null;
83602
+ if (input.template?.bindingLocation !== void 0) throw alreadyExists("Research idempotency key", input.idempotencyKey);
83603
+ const normalized = {
83604
+ queries: input.queries,
83605
+ provider: input.provider ?? existing.provider,
83606
+ model: input.model ?? null,
83607
+ location: input.location === void 0 ? existing.location ?? null : input.location,
83608
+ ...scope ? { scope } : {},
83609
+ ...template ? { template } : {}
83610
+ };
83611
+ if (crypto49.createHash("sha256").update(JSON.stringify(normalized)).digest("hex") !== existing.requestHash) throw alreadyExists("Research idempotency key", input.idempotencyKey);
83612
+ }
83613
+ var researchCursorSchema = z5.object({ projectId: z5.string().min(1), createdAt: z5.string().datetime({ offset: true }), id: z5.string().min(1).max(256) }).strict();
83614
+ function parseResearchCursor(value, projectId) {
83615
+ try {
83616
+ if (value.length > 2048 || !/^[\w-]+$/.test(value)) throw new Error("Invalid encoding");
83617
+ const cursor = researchCursorSchema.parse(JSON.parse(Buffer.from(value, "base64url").toString("utf8")));
83618
+ if (cursor.projectId !== projectId) throw new Error("Different project");
83619
+ return cursor;
83620
+ } catch {
83621
+ throw validationError("Invalid research history cursor. Reload the first page and try again.");
83622
+ }
83623
+ }
83398
83624
  function resolveStoredResearchScope(stored, selection, idempotencyKey) {
83399
83625
  if (!stored || stored.kind !== selection.kind || stored.key !== selection.key || selection.expectedPlanRevision !== void 0 && selection.expectedPlanRevision !== stored.planRevision) {
83400
83626
  throw alreadyExists("Research idempotency key", idempotencyKey);
@@ -83469,7 +83695,12 @@ function resolveResearchTemplate(app, projectId, selection, scope, location) {
83469
83695
  if (templateVersion !== selection.templateVersion) {
83470
83696
  throw validationError("The selected research template is stale. Reload it and try again.");
83471
83697
  }
83472
- const { bindings, output } = expandResearchTemplate(savedTemplate, scope, location);
83698
+ const bindingLocation = selection.bindingLocation === void 0 ? location : selection.bindingLocation;
83699
+ if (bindingLocation) {
83700
+ const project = app.db.select({ locations: projects.locations }).from(projects).where(eq65(projects.id, projectId)).get();
83701
+ if (!project.locations.some((item2) => sameLocation(item2, bindingLocation))) throw validationError("Research template binding location must match a configured project location.");
83702
+ }
83703
+ const { bindings, output } = expandResearchTemplate(savedTemplate, scope, bindingLocation);
83473
83704
  if (!output || output.length > 4e3) throw validationError("Research template output must be between 1 and 4000 characters.");
83474
83705
  return { templateId: selection.templateId, templateVersion, template: savedTemplate.pattern, bindings, output };
83475
83706
  }
@@ -84283,7 +84514,7 @@ function analyzableRunPredicates(projectId) {
84283
84514
  eq67(runs.projectId, projectId),
84284
84515
  // Only this kind writes query_snapshots.
84285
84516
  eq67(runs.kind, RunKinds["answer-visibility"]),
84286
- or14(eq67(runs.status, RunStatuses.completed), eq67(runs.status, RunStatuses.partial)),
84517
+ or15(eq67(runs.status, RunStatuses.completed), eq67(runs.status, RunStatuses.partial)),
84287
84518
  // Defensive: RunCoordinator already skips probes before analyzeAndPersist
84288
84519
  // is called, but a future call site invoking it directly for a probe must
84289
84520
  // still not pollute the intelligence window.
@@ -84873,7 +85104,7 @@ var IntelligenceService = class {
84873
85104
  and57(
84874
85105
  eq67(runs.projectId, projectId),
84875
85106
  eq67(runs.kind, RunKinds["answer-visibility"]),
84876
- or14(eq67(runs.status, "completed"), eq67(runs.status, "partial")),
85107
+ or15(eq67(runs.status, "completed"), eq67(runs.status, "partial")),
84877
85108
  // Defensive — see top of file.
84878
85109
  ne10(runs.trigger, RunTriggers.probe)
84879
85110
  )