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