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