@canonry/canonry 5.19.0 → 5.20.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/agent-workspace/skills/aero/SKILL.md +13 -8
- package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +27 -10
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +7 -2
- package/assets/assets/{AuditHistoryPanel-C_p8jX7Q.js → AuditHistoryPanel-BSh3Oezj.js} +1 -1
- package/assets/assets/{BacklinksPage-jD4nOGw1.js → BacklinksPage-DQ59dhCx.js} +1 -1
- package/assets/assets/{HistoryPage-DJGBbtBY.js → HistoryPage-BBvgid3o.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-Dmr3TVlo.js → MeasurementPropertyPage-Dmb18JbT.js} +1 -1
- package/assets/assets/{ProjectPage-B_haCKiA.js → ProjectPage-CPg0q2Dy.js} +1 -1
- package/assets/assets/{RunRow-BMbwR2yk.js → RunRow-BK8sZCID.js} +1 -1
- package/assets/assets/{RunsPage-D8hHakq2.js → RunsPage-CIcVBTdP.js} +1 -1
- package/assets/assets/{SettingsPage-QKnRcFA9.js → SettingsPage-Bc3aR-gz.js} +1 -1
- package/assets/assets/{SiteHealthSection-CDkUXqmb.js → SiteHealthSection-FF14aIXT.js} +3 -3
- package/assets/assets/{TrafficPage-CMif8RlM.js → TrafficPage-C_qoeGDl.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-DOCTP_4-.js → TrafficSourceDetailPage-B3i_B2MI.js} +1 -1
- package/assets/assets/{extract-error-message-Dm8RvYE5.js → extract-error-message-BisUn6nN.js} +1 -1
- package/assets/assets/{index-lj0fuFl4.js → index-Dht7JNX5.js} +44 -44
- package/assets/assets/{react-sigma_core.esm.min-DOPuwLtN.js → react-sigma_core.esm.min-tWhS09SS.js} +1 -1
- package/assets/assets/{v2-overview-adapter-C19tUcDS.js → v2-overview-adapter-CAoeWKQJ.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-QJWIOCEO.js → chunk-4PENJKRN.js} +2 -2
- package/dist/{chunk-SPCEXW3B.js → chunk-5FKR2K76.js} +11 -4
- package/dist/{chunk-GUJY3RIM.js → chunk-DRZWMFRV.js} +627 -263
- package/dist/{chunk-WLL7Q7JD.js → chunk-QF7SY7AF.js} +222 -29
- package/dist/{chunk-ABUO27BV.js → chunk-TSFKNQ5X.js} +3 -3
- package/dist/{chunk-P2EFPLIO.js → chunk-WL27HO5C.js} +565 -139
- package/dist/{chunk-YHKKCXGY.js → chunk-XBGPHZKA.js} +24 -8
- package/dist/cli.js +73 -17
- package/dist/{demo-server-BOIBS74F.js → demo-server-OCBV5OMS.js} +4 -4
- package/dist/index.js +6 -6
- package/dist/{intelligence-service-IWR2H6OW.js → intelligence-service-MKHINDHN.js} +2 -2
- package/dist/mcp.js +4 -4
- package/package.json +14 -14
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
AppError,
|
|
33
33
|
BacklinkSources,
|
|
34
34
|
COMPETITOR_LANDSCAPE_MODEL_GROUP_LIMIT,
|
|
35
|
+
COMPETITOR_LANDSCAPE_OBSERVED_NAME_LIMIT,
|
|
35
36
|
CcReleaseSyncStatuses,
|
|
36
37
|
CheckCategories,
|
|
37
38
|
CheckScopes,
|
|
@@ -79,6 +80,10 @@ import {
|
|
|
79
80
|
MEASUREMENT_PAGE_DEFAULT_LIMIT,
|
|
80
81
|
MEASUREMENT_PAGE_MAX_LIMIT,
|
|
81
82
|
MEASUREMENT_PLAN_V2_SCHEMA_VERSION,
|
|
83
|
+
MEASUREMENT_PORTFOLIO_ANSWER_SOURCES_LIMIT,
|
|
84
|
+
MEASUREMENT_PORTFOLIO_DEFAULT_LIMIT,
|
|
85
|
+
MEASUREMENT_PORTFOLIO_ROW_EVIDENCE_LIMIT,
|
|
86
|
+
MEASUREMENT_PORTFOLIO_TIE_NOTE,
|
|
82
87
|
MIN_DOMAIN_BRAND_KEY_LENGTH,
|
|
83
88
|
MIN_TREND_POINTS,
|
|
84
89
|
MODEL_ATTRIBUTION_EVENT_LIMIT,
|
|
@@ -666,6 +671,7 @@ import {
|
|
|
666
671
|
resolveLocations,
|
|
667
672
|
resolveMeasurementRunQueryScope,
|
|
668
673
|
resolveMeasurementRunScope,
|
|
674
|
+
resolveProviderModel,
|
|
669
675
|
resolveSnapshotRequestQueries,
|
|
670
676
|
restrictedWriteScopes,
|
|
671
677
|
resultsClearRequestSchema,
|
|
@@ -733,6 +739,7 @@ import {
|
|
|
733
739
|
snapshotRequestInputSchema,
|
|
734
740
|
snapshotRequestSchema,
|
|
735
741
|
sourceBreakdownDtoSchema,
|
|
742
|
+
sourceBreakdownQuerySchema,
|
|
736
743
|
splitList,
|
|
737
744
|
sumInfraHits,
|
|
738
745
|
summarizeCheckResults,
|
|
@@ -785,7 +792,7 @@ import {
|
|
|
785
792
|
wordpressSchemaDeployResultDtoSchema,
|
|
786
793
|
wordpressSchemaStatusResultDtoSchema,
|
|
787
794
|
wordpressStatusDtoSchema
|
|
788
|
-
} from "./chunk-
|
|
795
|
+
} from "./chunk-QF7SY7AF.js";
|
|
789
796
|
|
|
790
797
|
// src/intelligence-service.ts
|
|
791
798
|
import { eq as eq71, desc as desc32, asc as asc12, and as and60, ne as ne11, or as or18, inArray as inArray27, gte as gte18, lte as lte15, isNull as isNull13, sql as sql28, exists } from "drizzle-orm";
|
|
@@ -9155,7 +9162,7 @@ function compileCompetitiveSignalResolver(competitorDomains) {
|
|
|
9155
9162
|
...evidence.citedDomains ?? [],
|
|
9156
9163
|
...(evidence.groundingSources ?? []).map((source) => source.uri)
|
|
9157
9164
|
];
|
|
9158
|
-
const
|
|
9165
|
+
const answerDomains2 = evidence.answerDomains ?? extractDomainsFromText(evidence.answerText);
|
|
9159
9166
|
const mentionedBrandKeys = matchedAliasKeys(domainBrandMatcher, evidence.answerText);
|
|
9160
9167
|
const citedCompetitorDomains = [];
|
|
9161
9168
|
const mentionedCompetitorDomains = [];
|
|
@@ -9163,7 +9170,7 @@ function compileCompetitiveSignalResolver(competitorDomains) {
|
|
|
9163
9170
|
if (citationCandidates.some((candidate) => hostMatchesDomain(candidate, identity.domain))) {
|
|
9164
9171
|
citedCompetitorDomains.push(identity.domain);
|
|
9165
9172
|
}
|
|
9166
|
-
if (
|
|
9173
|
+
if (answerDomains2.some((candidate) => hostMatchesDomain(candidate, identity.domain)) || identity.domainBrandKey !== null && mentionedBrandKeys.has(identity.domainBrandKey)) {
|
|
9167
9174
|
mentionedCompetitorDomains.push(identity.domain);
|
|
9168
9175
|
}
|
|
9169
9176
|
}
|
|
@@ -12733,7 +12740,7 @@ function validateProviderModels(models, adapters) {
|
|
|
12733
12740
|
{ provider, model, hint: adapter.modelValidationHint }
|
|
12734
12741
|
);
|
|
12735
12742
|
}
|
|
12736
|
-
normalized[provider] = model;
|
|
12743
|
+
normalized[provider] = resolveProviderModel(provider, model);
|
|
12737
12744
|
}
|
|
12738
12745
|
return normalized;
|
|
12739
12746
|
}
|
|
@@ -16052,7 +16059,8 @@ function readCompetitorLandscape(app, projectName, query, selection) {
|
|
|
16052
16059
|
throw validationError("Invalid competitor landscape query", { issues: parsed.error.issues });
|
|
16053
16060
|
}
|
|
16054
16061
|
const filters = parsed.data;
|
|
16055
|
-
|
|
16062
|
+
const classSplitByDefault = filters.scope === void 0 && filters.groupKey === void 0 && filters.queryClass !== void 0 && filters.queryClass !== "all";
|
|
16063
|
+
if ((selection?.autoAdvanced || classSplitByDefault) && activeMeasurementPlan(app.db, project.id)?.plan.schemaVersion === 2) filters.scope = "all-markets";
|
|
16056
16064
|
const window = parseWindow(filters.window);
|
|
16057
16065
|
const cutoff = windowCutoff(window);
|
|
16058
16066
|
const candidateRuns = app.db.select().from(runs).where(and10(
|
|
@@ -16125,8 +16133,10 @@ function readCompetitorLandscape(app, projectName, query, selection) {
|
|
|
16125
16133
|
const pinned = mergePins(advanced?.pendingPins ?? [], advanced?.activePinned ?? [], projectPins);
|
|
16126
16134
|
const buildHistory = (selectedSnapshots) => {
|
|
16127
16135
|
const inputs = buildMentionShareInputs({ project, competitorDomains: [], snapshots: selectedSnapshots, queryTextById });
|
|
16136
|
+
const allObservedNames = observedCompetitorNames(selectedSnapshots);
|
|
16128
16137
|
return {
|
|
16129
|
-
observedNames:
|
|
16138
|
+
observedNames: allObservedNames.slice(0, COMPETITOR_LANDSCAPE_OBSERVED_NAME_LIMIT),
|
|
16139
|
+
observedNamesTotal: allObservedNames.length,
|
|
16130
16140
|
...buildCompetitorLandscapeHistory({
|
|
16131
16141
|
project: {
|
|
16132
16142
|
domain: project.canonicalDomain,
|
|
@@ -16242,21 +16252,7 @@ function resolveAdvancedScope(app, projectId, kind, groupKey, candidateRuns) {
|
|
|
16242
16252
|
if (kind === "group" && (!groupKey || !active.plan.groups.some((group) => group.stableKey === groupKey))) {
|
|
16243
16253
|
throw validationError(`Unknown Advanced Measurement group "${groupKey ?? ""}".`);
|
|
16244
16254
|
}
|
|
16245
|
-
const
|
|
16246
|
-
const historicalVersions = planVersionIds.length === 0 ? [] : app.db.select().from(measurementPlanVersions).where(and10(
|
|
16247
|
-
eq15(measurementPlanVersions.projectId, projectId),
|
|
16248
|
-
inArray4(measurementPlanVersions.id, planVersionIds)
|
|
16249
|
-
)).all();
|
|
16250
|
-
const frozenByVersionId = new Map(historicalVersions.map((version) => [
|
|
16251
|
-
version.id,
|
|
16252
|
-
scopeForFrozenPlan(parseStoredMeasurementPlanAnyVersion(version.canonicalJson), kind, groupKey)
|
|
16253
|
-
]));
|
|
16254
|
-
const runScopes = /* @__PURE__ */ new Map();
|
|
16255
|
-
for (const run of candidateRuns) {
|
|
16256
|
-
if (!run.measurementPlanVersionId) continue;
|
|
16257
|
-
const frozen = frozenByVersionId.get(run.measurementPlanVersionId);
|
|
16258
|
-
if (frozen) runScopes.set(run.id, frozen);
|
|
16259
|
-
}
|
|
16255
|
+
const runScopes = frozenRunScopes(app.db, projectId, kind, groupKey, candidateRuns);
|
|
16260
16256
|
const activeScope = scopeForFrozenPlan(active.plan, kind, groupKey);
|
|
16261
16257
|
if (!activeScope) throw validationError("The active Advanced Measurement plan cannot resolve this market scope.");
|
|
16262
16258
|
const draft = draftRow(app.db, projectId);
|
|
@@ -16274,25 +16270,58 @@ function resolveAdvancedScope(app, projectId, kind, groupKey, candidateRuns) {
|
|
|
16274
16270
|
runScopes
|
|
16275
16271
|
};
|
|
16276
16272
|
}
|
|
16273
|
+
function frozenRunScopes(db, projectId, kind, groupKey, candidateRuns) {
|
|
16274
|
+
const planVersionIds = [...new Set(candidateRuns.map((run) => run.measurementPlanVersionId).filter((id) => typeof id === "string" && id.length > 0))];
|
|
16275
|
+
const historicalVersions = planVersionIds.length === 0 ? [] : db.select().from(measurementPlanVersions).where(and10(
|
|
16276
|
+
eq15(measurementPlanVersions.projectId, projectId),
|
|
16277
|
+
inArray4(measurementPlanVersions.id, planVersionIds)
|
|
16278
|
+
)).all();
|
|
16279
|
+
const frozenByVersionId = new Map(historicalVersions.map((version) => [
|
|
16280
|
+
version.id,
|
|
16281
|
+
scopeForFrozenPlan(parseStoredMeasurementPlanAnyVersion(version.canonicalJson), kind, groupKey)
|
|
16282
|
+
]));
|
|
16283
|
+
const runScopes = /* @__PURE__ */ new Map();
|
|
16284
|
+
for (const run of candidateRuns) {
|
|
16285
|
+
if (!run.measurementPlanVersionId) continue;
|
|
16286
|
+
const frozen = frozenByVersionId.get(run.measurementPlanVersionId);
|
|
16287
|
+
if (frozen) runScopes.set(run.id, frozen);
|
|
16288
|
+
}
|
|
16289
|
+
return runScopes;
|
|
16290
|
+
}
|
|
16291
|
+
function planQueryClassesByRun(db, projectId, candidateRuns) {
|
|
16292
|
+
const byRun = /* @__PURE__ */ new Map();
|
|
16293
|
+
for (const [runId, scope] of frozenRunScopes(db, projectId, "all-markets", void 0, candidateRuns)) {
|
|
16294
|
+
byRun.set(runId, scope.queryClassesByExecution);
|
|
16295
|
+
}
|
|
16296
|
+
return byRun;
|
|
16297
|
+
}
|
|
16277
16298
|
function scopeForFrozenPlan(plan, kind, groupKey) {
|
|
16278
16299
|
if (plan.schemaVersion !== 2) return null;
|
|
16279
|
-
|
|
16280
|
-
|
|
16281
|
-
|
|
16282
|
-
|
|
16283
|
-
|
|
16284
|
-
|
|
16285
|
-
|
|
16286
|
-
edge.targetKey
|
|
16287
|
-
edge.
|
|
16288
|
-
|
|
16289
|
-
|
|
16290
|
-
|
|
16291
|
-
|
|
16300
|
+
let groups = plan.groups;
|
|
16301
|
+
let scopedUsageEdges = plan.usageEdges;
|
|
16302
|
+
let scopedAssignments = plan.assignments;
|
|
16303
|
+
if (kind === "group") {
|
|
16304
|
+
groups = plan.groups.filter((group) => group.stableKey === groupKey);
|
|
16305
|
+
if (groups.length === 0) return null;
|
|
16306
|
+
const targetKeys = new Set(groups.flatMap((group) => group.targetKeys));
|
|
16307
|
+
scopedUsageEdges = plan.usageEdges.filter((edge) => targetKeys.has(edge.targetKey));
|
|
16308
|
+
const scopedUsages = new Set(scopedUsageEdges.map((edge) => JSON.stringify([
|
|
16309
|
+
edge.executionNodeKey,
|
|
16310
|
+
edge.targetKey,
|
|
16311
|
+
edge.queryId
|
|
16312
|
+
])));
|
|
16313
|
+
scopedAssignments = plan.assignments.filter((assignment) => scopedUsages.has(JSON.stringify([
|
|
16292
16314
|
assignment.executionNodeKey,
|
|
16293
16315
|
assignment.targetKey,
|
|
16294
16316
|
assignment.queryId
|
|
16295
|
-
])))
|
|
16317
|
+
])));
|
|
16318
|
+
}
|
|
16319
|
+
const executionNodeKeys = /* @__PURE__ */ new Set([
|
|
16320
|
+
...scopedUsageEdges.map((edge) => edge.executionNodeKey),
|
|
16321
|
+
...scopedAssignments.map((assignment) => assignment.executionNodeKey)
|
|
16322
|
+
]);
|
|
16323
|
+
const queryClassesByExecution = /* @__PURE__ */ new Map();
|
|
16324
|
+
for (const assignment of scopedAssignments) {
|
|
16296
16325
|
const classes = queryClassesByExecution.get(assignment.executionNodeKey) ?? /* @__PURE__ */ new Set();
|
|
16297
16326
|
classes.add(assignment.queryClass);
|
|
16298
16327
|
queryClassesByExecution.set(assignment.executionNodeKey, classes);
|
|
@@ -16466,7 +16495,7 @@ function effectiveModels(tx, params, providers) {
|
|
|
16466
16495
|
const resolved = {};
|
|
16467
16496
|
for (const provider of providers) {
|
|
16468
16497
|
const model = overrides[provider] ?? instance[provider];
|
|
16469
|
-
if (model) resolved[provider] = model;
|
|
16498
|
+
if (model) resolved[provider] = resolveProviderModel(provider, model);
|
|
16470
16499
|
}
|
|
16471
16500
|
return resolved;
|
|
16472
16501
|
}
|
|
@@ -16508,6 +16537,8 @@ function materializeV2ExecutionNodes(nodes, instanceModels) {
|
|
|
16508
16537
|
const expectedSlots = [];
|
|
16509
16538
|
const providers = /* @__PURE__ */ new Set();
|
|
16510
16539
|
const models = /* @__PURE__ */ new Map();
|
|
16540
|
+
const answering = /* @__PURE__ */ new Map();
|
|
16541
|
+
const retired = /* @__PURE__ */ new Set();
|
|
16511
16542
|
const claimed = /* @__PURE__ */ new Set();
|
|
16512
16543
|
for (const node of [...nodes].sort((left, right) => compareText4(left.stableKey, right.stableKey))) {
|
|
16513
16544
|
const nodeProviders = normalizeProviders(node.context.providers);
|
|
@@ -16525,6 +16556,11 @@ function materializeV2ExecutionNodes(nodes, instanceModels) {
|
|
|
16525
16556
|
const model = resolved.get(provider) ?? null;
|
|
16526
16557
|
if (!models.has(provider)) models.set(provider, model);
|
|
16527
16558
|
else if (models.get(provider) !== model) models.set(provider, null);
|
|
16559
|
+
if (model) {
|
|
16560
|
+
const current = resolveProviderModel(provider, model);
|
|
16561
|
+
if (current !== model) retired.add(provider);
|
|
16562
|
+
answering.set(provider, (answering.get(provider) ?? /* @__PURE__ */ new Set()).add(current));
|
|
16563
|
+
}
|
|
16528
16564
|
expectedSlots.push({
|
|
16529
16565
|
executionId: node.stableKey,
|
|
16530
16566
|
queryText: node.queryText,
|
|
@@ -16537,7 +16573,11 @@ function materializeV2ExecutionNodes(nodes, instanceModels) {
|
|
|
16537
16573
|
return {
|
|
16538
16574
|
expectedSlots,
|
|
16539
16575
|
providers: [...providers].sort(compareText4),
|
|
16540
|
-
models: Object.fromEntries([...models].flatMap(([provider, model]) =>
|
|
16576
|
+
models: Object.fromEntries([...models].flatMap(([provider, model]) => {
|
|
16577
|
+
if (model) return [[provider, model]];
|
|
16578
|
+
const current = answering.get(provider);
|
|
16579
|
+
return retired.has(provider) && current ? [[provider, [...current].sort(compareText4).join(" + ")]] : [];
|
|
16580
|
+
}))
|
|
16541
16581
|
};
|
|
16542
16582
|
}
|
|
16543
16583
|
function sliceForV2(plan, params) {
|
|
@@ -16821,6 +16861,11 @@ function newerFullSweep(db, run) {
|
|
|
16821
16861
|
)
|
|
16822
16862
|
)).orderBy(desc4(runs.createdAt)).get();
|
|
16823
16863
|
}
|
|
16864
|
+
function measuredOnRetiredModel(run, provider, missing) {
|
|
16865
|
+
const recorded = run.measurementExecutionIdentity?.models[provider];
|
|
16866
|
+
if (recorded !== void 0) return resolveProviderModel(provider, recorded) !== recorded;
|
|
16867
|
+
return missing.some((slot) => typeof slot.requestedModel === "string" && resolveProviderModel(provider, slot.requestedModel) !== slot.requestedModel);
|
|
16868
|
+
}
|
|
16824
16869
|
function evaluateRunFill(db, run, input = {}) {
|
|
16825
16870
|
const slots = measurementRunSlotState(db, run.id);
|
|
16826
16871
|
const refuse = (code, message) => ({ kind: "refused", code, message, slots });
|
|
@@ -16910,6 +16955,13 @@ function evaluateRunFill(db, run, input = {}) {
|
|
|
16910
16955
|
`${unfrozen.length} missing answer(s) have no model frozen in the run's manifest, so filling them would use today's model instead of the one the run measured with.`
|
|
16911
16956
|
);
|
|
16912
16957
|
}
|
|
16958
|
+
const retired = providers.filter((provider) => measuredOnRetiredModel(run, provider, missingByProvider.get(provider) ?? []));
|
|
16959
|
+
if (retired.length) {
|
|
16960
|
+
return refuse(
|
|
16961
|
+
RunFillRefusalCodes.model_retired,
|
|
16962
|
+
`This run measured ${retired.join(", ")} on a model the provider has since retired, and the replacement is a different engine. Filling it now would mix two engines in one run; run a new sweep instead.`
|
|
16963
|
+
);
|
|
16964
|
+
}
|
|
16913
16965
|
if (input.runnableProviders) {
|
|
16914
16966
|
const runnable = new Set(input.runnableProviders.map(normalizeProvider));
|
|
16915
16967
|
const absent = providers.filter((provider) => !runnable.has(provider));
|
|
@@ -22807,34 +22859,155 @@ function filteredOverviewWithDb(db, active, plan, run, filters, targetKeys) {
|
|
|
22807
22859
|
overview: evaluator.evaluate(targetKeys)
|
|
22808
22860
|
};
|
|
22809
22861
|
}
|
|
22810
|
-
function
|
|
22811
|
-
const
|
|
22812
|
-
|
|
22813
|
-
|
|
22814
|
-
|
|
22815
|
-
|
|
22862
|
+
function pushTo(map, key, value) {
|
|
22863
|
+
const existing = map.get(key);
|
|
22864
|
+
if (existing) existing.push(value);
|
|
22865
|
+
else map.set(key, [value]);
|
|
22866
|
+
}
|
|
22867
|
+
function innerMap(outer, key) {
|
|
22868
|
+
let inner = outer.get(key);
|
|
22869
|
+
if (!inner) {
|
|
22870
|
+
inner = /* @__PURE__ */ new Map();
|
|
22871
|
+
outer.set(key, inner);
|
|
22872
|
+
}
|
|
22873
|
+
return inner;
|
|
22874
|
+
}
|
|
22875
|
+
function createTargetAnswerReader(plan, materialized, filters) {
|
|
22876
|
+
const assignmentsByTarget = /* @__PURE__ */ new Map();
|
|
22877
|
+
for (const assignment of plan.assignments) {
|
|
22878
|
+
if (filters.queryClass !== "all" && assignment.queryClass !== filters.queryClass) continue;
|
|
22879
|
+
innerMap(assignmentsByTarget, assignment.targetKey).set(assignment.executionNodeKey, assignment);
|
|
22880
|
+
}
|
|
22881
|
+
const edgesByTarget = /* @__PURE__ */ new Map();
|
|
22882
|
+
for (const edge of materialized.input.usageEdges) {
|
|
22883
|
+
if (edge.type !== "target") continue;
|
|
22884
|
+
innerMap(edgesByTarget, edge.targetId).set(edge.executionId, edge);
|
|
22885
|
+
}
|
|
22886
|
+
const answersByEdge = /* @__PURE__ */ new Map();
|
|
22887
|
+
for (const answer of materialized.evidence.answers) {
|
|
22888
|
+
innerMap(answersByEdge, answer.usageEdgeId).set(answer.expectedSlotId, answer);
|
|
22889
|
+
}
|
|
22890
|
+
const citedSlotsByEdge = /* @__PURE__ */ new Map();
|
|
22891
|
+
for (const row of materialized.evidence.evidence) {
|
|
22892
|
+
if (row.classification !== "assigned") continue;
|
|
22893
|
+
const slots = citedSlotsByEdge.get(row.usageEdgeId);
|
|
22894
|
+
if (slots) slots.add(row.expectedSlotId);
|
|
22895
|
+
else citedSlotsByEdge.set(row.usageEdgeId, /* @__PURE__ */ new Set([row.expectedSlotId]));
|
|
22896
|
+
}
|
|
22816
22897
|
const incompleteObservationIds = new Set(materialized.evidence.diagnostics.evidenceIncompleteObservationIds);
|
|
22817
22898
|
const questionsById = new Map(plan.querySnapshots.map((question) => [question.queryId, question.queryText]));
|
|
22818
22899
|
const provider = filters.provider === void 0 ? void 0 : normalizeText2(filters.provider);
|
|
22819
22900
|
const location = filters.location === void 0 ? void 0 : normalizeMeasurementLocation(filters.location);
|
|
22820
|
-
const
|
|
22821
|
-
const
|
|
22822
|
-
|
|
22823
|
-
|
|
22824
|
-
|
|
22825
|
-
|
|
22826
|
-
|
|
22827
|
-
|
|
22828
|
-
|
|
22829
|
-
|
|
22830
|
-
|
|
22831
|
-
|
|
22832
|
-
|
|
22833
|
-
|
|
22834
|
-
|
|
22835
|
-
|
|
22901
|
+
const slotsByExecution = /* @__PURE__ */ new Map();
|
|
22902
|
+
const slotOrder = /* @__PURE__ */ new Map();
|
|
22903
|
+
materialized.input.expectedSlots.forEach((slot, index2) => {
|
|
22904
|
+
if (provider !== void 0 && normalizeText2(slot.provider) !== provider) return;
|
|
22905
|
+
if (location !== void 0 && normalizeMeasurementLocation(slot.location) !== location) return;
|
|
22906
|
+
pushTo(slotsByExecution, slot.executionId, slot);
|
|
22907
|
+
slotOrder.set(slot.id, index2);
|
|
22908
|
+
});
|
|
22909
|
+
return (target) => {
|
|
22910
|
+
const assignments = assignmentsByTarget.get(target.stableKey);
|
|
22911
|
+
const edges = edgesByTarget.get(target.stableKey);
|
|
22912
|
+
const answers = [];
|
|
22913
|
+
const sourceAnswers = [];
|
|
22914
|
+
const queries2 = /* @__PURE__ */ new Set();
|
|
22915
|
+
const providers = /* @__PURE__ */ new Set();
|
|
22916
|
+
let expected = 0;
|
|
22917
|
+
if (!assignments || !edges) return { expected, queries: 0, providers, answers, sourceAnswers };
|
|
22918
|
+
for (const [executionId, assignment] of assignments) {
|
|
22919
|
+
const edge = edges.get(executionId);
|
|
22920
|
+
const slots = slotsByExecution.get(executionId);
|
|
22921
|
+
if (!edge || !slots) continue;
|
|
22922
|
+
queries2.add(assignment.queryId);
|
|
22923
|
+
const answerBySlot = answersByEdge.get(edge.id);
|
|
22924
|
+
const citedSlotIds = citedSlotsByEdge.get(edge.id);
|
|
22925
|
+
for (const slot of slots) {
|
|
22926
|
+
expected++;
|
|
22927
|
+
providers.add(slot.provider);
|
|
22928
|
+
const observation = materialized.observationsBySlot.get(slot.id);
|
|
22929
|
+
const snapshot = observation === void 0 ? void 0 : materialized.snapshotsById.get(observation.id);
|
|
22930
|
+
if (!observation || !snapshot) continue;
|
|
22931
|
+
const sourced = {
|
|
22932
|
+
slotId: slot.id,
|
|
22933
|
+
snapshot,
|
|
22934
|
+
citedUrls: observation.citedUrls ?? observation.historicalCitedUrls ?? []
|
|
22935
|
+
};
|
|
22936
|
+
sourceAnswers.push(sourced);
|
|
22937
|
+
if (snapshot.answerText === null) continue;
|
|
22938
|
+
answers.push({
|
|
22939
|
+
...sourced,
|
|
22940
|
+
provider: slot.provider,
|
|
22941
|
+
question: questionsById.get(assignment.queryId) ?? slot.queryText,
|
|
22942
|
+
mentioned: target.mentionNotApplicable ? null : answerBySlot?.get(slot.id)?.mentioned ?? null,
|
|
22943
|
+
cited: citedSlotIds?.has(slot.id) ? true : incompleteObservationIds.has(snapshot.id) ? null : false
|
|
22944
|
+
});
|
|
22945
|
+
}
|
|
22946
|
+
}
|
|
22947
|
+
const inRunOrder = (left, right) => slotOrder.get(left.slotId) - slotOrder.get(right.slotId);
|
|
22948
|
+
answers.sort(inRunOrder);
|
|
22949
|
+
sourceAnswers.sort(inRunOrder);
|
|
22950
|
+
return { expected, queries: queries2.size, providers, answers, sourceAnswers };
|
|
22951
|
+
};
|
|
22952
|
+
}
|
|
22953
|
+
function targetAnswers(plan, target, materialized, filters) {
|
|
22954
|
+
return createTargetAnswerReader(plan, materialized, filters)(target);
|
|
22955
|
+
}
|
|
22956
|
+
function plannedQueryCount(plan, targetKey, queryClass) {
|
|
22957
|
+
return new Set(plan.assignments.filter((assignment) => assignment.targetKey === targetKey).filter((assignment) => queryClass === "all" || assignment.queryClass === queryClass).map((assignment) => assignment.queryId)).size;
|
|
22958
|
+
}
|
|
22959
|
+
function propertyLocations(plan) {
|
|
22960
|
+
const groupsByKey = new Map(plan.groups.map((group) => [group.stableKey, group]));
|
|
22961
|
+
const depth = (group) => {
|
|
22962
|
+
let levels = 0;
|
|
22963
|
+
const seen2 = /* @__PURE__ */ new Set([group.stableKey]);
|
|
22964
|
+
let parentKey = group.parentGroupKey;
|
|
22965
|
+
while (parentKey !== void 0 && !seen2.has(parentKey)) {
|
|
22966
|
+
const parent = groupsByKey.get(parentKey);
|
|
22967
|
+
if (!parent) break;
|
|
22968
|
+
seen2.add(parentKey);
|
|
22969
|
+
levels++;
|
|
22970
|
+
parentKey = parent.parentGroupKey;
|
|
22971
|
+
}
|
|
22972
|
+
return levels;
|
|
22973
|
+
};
|
|
22974
|
+
const byLabel = (left, right) => compareText13(left.label, right.label) || compareText13(left.stableKey, right.stableKey);
|
|
22975
|
+
const metros = /* @__PURE__ */ new Map();
|
|
22976
|
+
const submarkets = /* @__PURE__ */ new Map();
|
|
22977
|
+
for (const group of plan.groups) {
|
|
22978
|
+
const level = depth(group);
|
|
22979
|
+
for (const targetKey of new Set(group.targetKeys)) {
|
|
22980
|
+
if (level === 0) pushTo(metros, targetKey, group);
|
|
22981
|
+
else pushTo(submarkets, targetKey, { group, depth: level });
|
|
22982
|
+
}
|
|
22836
22983
|
}
|
|
22837
|
-
|
|
22984
|
+
const metroDto = (group) => ({ groupKey: group.stableKey, label: group.label });
|
|
22985
|
+
return (targetKey) => {
|
|
22986
|
+
const own = [...metros.get(targetKey) ?? []].sort(byLabel);
|
|
22987
|
+
const nested = [...submarkets.get(targetKey) ?? []].sort((left, right) => left.depth - right.depth || byLabel(left.group, right.group));
|
|
22988
|
+
return {
|
|
22989
|
+
metro: own.length === 0 ? null : metroDto(own[0]),
|
|
22990
|
+
...own.length > 1 ? { otherMetros: own.slice(1).map(metroDto) } : {},
|
|
22991
|
+
submarkets: nested.map((row) => row.group.label)
|
|
22992
|
+
};
|
|
22993
|
+
};
|
|
22994
|
+
}
|
|
22995
|
+
function answerDomains(answer) {
|
|
22996
|
+
const domains = /* @__PURE__ */ new Set();
|
|
22997
|
+
for (const raw of [...answer.snapshot.citedDomains, ...answer.citedUrls]) {
|
|
22998
|
+
const host = hostOf(raw);
|
|
22999
|
+
if (!host || hostMatchesAnyDomain(host, AI_PROVIDER_INFRA_DOMAINS)) continue;
|
|
23000
|
+
domains.add(host);
|
|
23001
|
+
}
|
|
23002
|
+
return domains;
|
|
23003
|
+
}
|
|
23004
|
+
function domainRows(answers, limit) {
|
|
23005
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
23006
|
+
for (const answer of answers) {
|
|
23007
|
+
for (const domain of answerDomains(answer)) counts2.set(domain, (counts2.get(domain) ?? 0) + 1);
|
|
23008
|
+
}
|
|
23009
|
+
const rows = [...counts2].map(([domain, count4]) => ({ domain, answers: count4 })).sort((left, right) => right.answers - left.answers || compareText13(left.domain, right.domain));
|
|
23010
|
+
return { rows: rows.slice(0, limit), total: rows.length };
|
|
22838
23011
|
}
|
|
22839
23012
|
function targetAliasKeys(target) {
|
|
22840
23013
|
return new Set([target.label, ...target.aliases].map(brandKeyFromText).filter(Boolean));
|
|
@@ -22844,16 +23017,18 @@ function recommendationRows(target, population) {
|
|
|
22844
23017
|
const grouped = /* @__PURE__ */ new Map();
|
|
22845
23018
|
for (const answer of population.answers) {
|
|
22846
23019
|
if (answer.mentioned !== false || answer.cited !== false) continue;
|
|
23020
|
+
const seenInAnswer = /* @__PURE__ */ new Set();
|
|
22847
23021
|
for (const rawName of answer.snapshot.recommendedCompetitors) {
|
|
22848
23022
|
const name = rawName.normalize("NFKC").trim().replace(/\s+/g, " ");
|
|
22849
23023
|
const key = brandKeyFromText(name);
|
|
22850
23024
|
if (!key || aliases.has(key)) continue;
|
|
22851
23025
|
const existing = grouped.get(key);
|
|
22852
23026
|
if (existing) {
|
|
23027
|
+
if (compareText13(name, existing.name) < 0) existing.name = name;
|
|
23028
|
+
if (seenInAnswer.has(key)) continue;
|
|
22853
23029
|
existing.occurrences++;
|
|
22854
23030
|
existing.providers.add(answer.provider);
|
|
22855
23031
|
existing.questions.add(answer.question);
|
|
22856
|
-
if (compareText13(name, existing.name) < 0) existing.name = name;
|
|
22857
23032
|
} else {
|
|
22858
23033
|
grouped.set(key, {
|
|
22859
23034
|
name,
|
|
@@ -22862,6 +23037,7 @@ function recommendationRows(target, population) {
|
|
|
22862
23037
|
questions: /* @__PURE__ */ new Set([answer.question])
|
|
22863
23038
|
});
|
|
22864
23039
|
}
|
|
23040
|
+
seenInAnswer.add(key);
|
|
22865
23041
|
}
|
|
22866
23042
|
}
|
|
22867
23043
|
return [...grouped.values()].map((row) => {
|
|
@@ -22879,6 +23055,16 @@ function recommendationRows(target, population) {
|
|
|
22879
23055
|
};
|
|
22880
23056
|
}).sort((left, right) => right.occurrences - left.occurrences || compareText13(left.name, right.name));
|
|
22881
23057
|
}
|
|
23058
|
+
function namedInsteadFields(names) {
|
|
23059
|
+
const returned = names.slice(0, MEASUREMENT_PORTFOLIO_ROW_EVIDENCE_LIMIT);
|
|
23060
|
+
return {
|
|
23061
|
+
namedInsteadInAnswerText: returned.map(({ name, occurrences }) => ({ name, answers: occurrences })),
|
|
23062
|
+
namedInsteadInAnswerTextTotal: names.length,
|
|
23063
|
+
recommendedInstead: returned.map(({ name, occurrences }) => ({ name, occurrences })),
|
|
23064
|
+
recommendedInsteadTotal: names.length,
|
|
23065
|
+
recommendedInsteadTruncated: names.length > returned.length
|
|
23066
|
+
};
|
|
23067
|
+
}
|
|
22882
23068
|
function propertyDto2(target) {
|
|
22883
23069
|
return { targetKey: target.stableKey, label: target.label };
|
|
22884
23070
|
}
|
|
@@ -22925,20 +23111,60 @@ function targetKeysForRun(plan, run, requested, requireEntireScope) {
|
|
|
22925
23111
|
}
|
|
22926
23112
|
return selected;
|
|
22927
23113
|
}
|
|
22928
|
-
function marketRollup(plan,
|
|
22929
|
-
|
|
22930
|
-
|
|
22931
|
-
|
|
22932
|
-
|
|
22933
|
-
|
|
23114
|
+
function marketRollup(plan, measured, scope, includeNested, limit) {
|
|
23115
|
+
const childCounts = /* @__PURE__ */ new Map();
|
|
23116
|
+
for (const group of plan.groups) {
|
|
23117
|
+
if (group.parentGroupKey !== void 0) childCounts.set(group.parentGroupKey, (childCounts.get(group.parentGroupKey) ?? 0) + 1);
|
|
23118
|
+
}
|
|
23119
|
+
const groupKeys = new Set(plan.groups.map((group) => group.stableKey));
|
|
23120
|
+
const isTopLevel = (group) => group.parentGroupKey === void 0 || !groupKeys.has(group.parentGroupKey);
|
|
23121
|
+
let candidates;
|
|
23122
|
+
if (scope === void 0) {
|
|
23123
|
+
candidates = includeNested ? plan.groups : plan.groups.filter(isTopLevel);
|
|
23124
|
+
} else if (!includeNested) {
|
|
23125
|
+
candidates = plan.groups.filter((group) => group.parentGroupKey === scope.stableKey);
|
|
23126
|
+
} else {
|
|
23127
|
+
const descendants = /* @__PURE__ */ new Set([scope.stableKey]);
|
|
23128
|
+
let grew = true;
|
|
23129
|
+
while (grew) {
|
|
23130
|
+
grew = false;
|
|
23131
|
+
for (const group of plan.groups) {
|
|
23132
|
+
if (group.parentGroupKey !== void 0 && descendants.has(group.parentGroupKey) && !descendants.has(group.stableKey)) {
|
|
23133
|
+
descendants.add(group.stableKey);
|
|
23134
|
+
grew = true;
|
|
23135
|
+
}
|
|
23136
|
+
}
|
|
23137
|
+
}
|
|
23138
|
+
candidates = plan.groups.filter((group) => group.stableKey !== scope.stableKey && descendants.has(group.stableKey));
|
|
23139
|
+
}
|
|
23140
|
+
const markets = candidates.map((group) => {
|
|
23141
|
+
const identity = {
|
|
22934
23142
|
groupKey: group.stableKey,
|
|
22935
23143
|
label: group.label,
|
|
23144
|
+
parentGroupKey: isTopLevel(group) ? null : group.parentGroupKey,
|
|
23145
|
+
childMarketCount: childCounts.get(group.stableKey) ?? 0
|
|
23146
|
+
};
|
|
23147
|
+
if (measured === void 0) {
|
|
23148
|
+
return {
|
|
23149
|
+
...identity,
|
|
23150
|
+
propertyCount: group.targetKeys.length,
|
|
23151
|
+
propertiesMentioned: unavailable3("no_completed_run"),
|
|
23152
|
+
mentionCoverage: unavailable3("no_completed_run"),
|
|
23153
|
+
citationCoverage: unavailable3("no_completed_run")
|
|
23154
|
+
};
|
|
23155
|
+
}
|
|
23156
|
+
const targetKeys = targetKeysForRun(plan, measured.run, group.targetKeys, false);
|
|
23157
|
+
const overview = measured.evaluator.evaluate(targetKeys);
|
|
23158
|
+
return {
|
|
23159
|
+
...identity,
|
|
22936
23160
|
propertyCount: targetKeys.length,
|
|
22937
23161
|
propertiesMentioned: countMetric2(overview.propertiesMentioned),
|
|
22938
23162
|
mentionCoverage: coverageMetric2(overview.mentionCoverage),
|
|
22939
23163
|
citationCoverage: coverageMetric2(overview.citationCoverage)
|
|
22940
23164
|
};
|
|
22941
23165
|
}).sort(compareWeakestMarket);
|
|
23166
|
+
const returned = includeNested ? markets : markets.slice(0, limit);
|
|
23167
|
+
return { markets: returned, totalMarkets: markets.length, marketsTruncated: returned.length < markets.length };
|
|
22942
23168
|
}
|
|
22943
23169
|
function compareWeakestMarket(a, b) {
|
|
22944
23170
|
const aUnavailable = a.mentionCoverage.state === "unavailable" || a.citationCoverage.state === "unavailable";
|
|
@@ -22956,11 +23182,20 @@ function compareWeakestMarket(a, b) {
|
|
|
22956
23182
|
function mentionRanking(rows, limit) {
|
|
22957
23183
|
const eligible = [];
|
|
22958
23184
|
const excluded = [];
|
|
22959
|
-
for (const { targetKey, label, mentionCoverage, citationCoverage } of rows) {
|
|
23185
|
+
for (const { targetKey, label, mentionCoverage, citationCoverage, metro, otherMetros, submarkets, queries: queries2 } of rows) {
|
|
22960
23186
|
if (mentionCoverage.state === "unavailable") {
|
|
22961
23187
|
excluded.push({ targetKey, label, reason: mentionCoverage.reason });
|
|
22962
23188
|
} else {
|
|
22963
|
-
eligible.push({
|
|
23189
|
+
eligible.push({
|
|
23190
|
+
targetKey,
|
|
23191
|
+
label,
|
|
23192
|
+
metro,
|
|
23193
|
+
...otherMetros === void 0 ? {} : { otherMetros },
|
|
23194
|
+
submarkets,
|
|
23195
|
+
queries: queries2,
|
|
23196
|
+
mentionCoverage,
|
|
23197
|
+
citationCoverage
|
|
23198
|
+
});
|
|
22964
23199
|
}
|
|
22965
23200
|
}
|
|
22966
23201
|
const byLabel = (a, b) => compareText13(a.label, b.label) || compareText13(a.targetKey, b.targetKey);
|
|
@@ -22985,65 +23220,74 @@ function portfolioResponse(db, active, plan, query) {
|
|
|
22985
23220
|
provider: query.provider,
|
|
22986
23221
|
location: query.location
|
|
22987
23222
|
};
|
|
23223
|
+
const limit = query.limit ?? MEASUREMENT_PORTFOLIO_DEFAULT_LIMIT;
|
|
23224
|
+
const includeNestedMarkets = query.includeNestedMarkets ?? false;
|
|
23225
|
+
const locate = propertyLocations(plan);
|
|
22988
23226
|
if (!run) {
|
|
22989
23227
|
const rows2 = targets.map((target) => ({
|
|
22990
23228
|
...propertyDto2(target),
|
|
23229
|
+
...locate(target.stableKey),
|
|
23230
|
+
queries: plannedQueryCount(plan, target.stableKey, query.queryClass),
|
|
22991
23231
|
mentionCoverage: unavailable3("no_completed_run"),
|
|
22992
23232
|
citationCoverage: unavailable3("no_completed_run"),
|
|
22993
23233
|
flags: 0,
|
|
22994
|
-
|
|
22995
|
-
|
|
22996
|
-
|
|
23234
|
+
...namedInsteadFields([]),
|
|
23235
|
+
citedDomains: [],
|
|
23236
|
+
citedDomainsTotal: 0
|
|
22997
23237
|
})).sort(compareWeakest);
|
|
22998
|
-
const limit2 = query.limit ?? DEFAULT_LIMIT3;
|
|
22999
23238
|
return measurementPortfolioSummaryResponseSchema.parse({
|
|
23000
23239
|
portfolio: { groupKey: group?.stableKey ?? null, label: group?.label ?? null, measurementScope: null },
|
|
23001
23240
|
measurement,
|
|
23002
23241
|
queryClass: query.queryClass,
|
|
23242
|
+
engines: [],
|
|
23003
23243
|
metrics: {
|
|
23004
23244
|
propertiesMentioned: unavailable3("no_completed_run"),
|
|
23005
23245
|
mentionCoverage: unavailable3("no_completed_run"),
|
|
23006
23246
|
citationCoverage: unavailable3("no_completed_run")
|
|
23007
23247
|
},
|
|
23008
|
-
weakestProperties: rows2.slice(0,
|
|
23009
|
-
|
|
23248
|
+
weakestProperties: rows2.slice(0, limit),
|
|
23249
|
+
tiedAtWeakest: null,
|
|
23250
|
+
weakestAnswerSources: null,
|
|
23251
|
+
mentionRanking: mentionRanking(rows2, limit),
|
|
23010
23252
|
// Sorted through the same comparator as the measured branch. Plan order
|
|
23011
23253
|
// is `stableKey`, so emitting it raw put markets in an order the schema
|
|
23012
23254
|
// documents as worst-first and that changes the moment a run lands.
|
|
23013
|
-
|
|
23014
|
-
groupKey: g.stableKey,
|
|
23015
|
-
label: g.label,
|
|
23016
|
-
propertyCount: g.targetKeys.length,
|
|
23017
|
-
propertiesMentioned: unavailable3("no_completed_run"),
|
|
23018
|
-
mentionCoverage: unavailable3("no_completed_run"),
|
|
23019
|
-
citationCoverage: unavailable3("no_completed_run")
|
|
23020
|
-
})).sort(compareWeakestMarket),
|
|
23255
|
+
...marketRollup(plan, void 0, group, includeNestedMarkets, limit),
|
|
23021
23256
|
totalProperties: rows2.length,
|
|
23022
|
-
truncated: rows2.length >
|
|
23257
|
+
truncated: rows2.length > limit
|
|
23023
23258
|
});
|
|
23024
23259
|
}
|
|
23025
23260
|
const { materialized, overview, evaluator } = filteredOverviewWithDb(db, active, plan, run, filters, targetKeys);
|
|
23261
|
+
const readTarget2 = createTargetAnswerReader(plan, materialized, filters);
|
|
23262
|
+
const populations = new Map(targets.map((target) => [target.stableKey, readTarget2(target)]));
|
|
23026
23263
|
const measured = new Map(overview.properties.map((row) => [row.targetId, row]));
|
|
23027
23264
|
const ranked = targets.map((target) => {
|
|
23028
23265
|
const row = measured.get(target.stableKey);
|
|
23266
|
+
const population = populations.get(target.stableKey);
|
|
23029
23267
|
return {
|
|
23030
23268
|
target,
|
|
23269
|
+
population,
|
|
23031
23270
|
...propertyDto2(target),
|
|
23271
|
+
...locate(target.stableKey),
|
|
23272
|
+
queries: population.queries,
|
|
23032
23273
|
mentionCoverage: row ? coverageMetric2(row.mentionCoverage) : unavailable3("no_population"),
|
|
23033
23274
|
citationCoverage: row ? coverageMetric2(row.citationCoverage) : unavailable3("no_population"),
|
|
23034
23275
|
flags: row?.flags ?? 0
|
|
23035
23276
|
};
|
|
23036
23277
|
}).sort(compareWeakest);
|
|
23037
|
-
const
|
|
23038
|
-
const rows =
|
|
23039
|
-
const
|
|
23278
|
+
const displayed = ranked.slice(0, limit);
|
|
23279
|
+
const rows = displayed.map(({ target, population, ...row }) => {
|
|
23280
|
+
const domains = domainRows(population.sourceAnswers, MEASUREMENT_PORTFOLIO_ROW_EVIDENCE_LIMIT);
|
|
23040
23281
|
return {
|
|
23041
23282
|
...row,
|
|
23042
|
-
|
|
23043
|
-
|
|
23044
|
-
|
|
23283
|
+
...namedInsteadFields(recommendationRows(target, population)),
|
|
23284
|
+
citedDomains: domains.rows,
|
|
23285
|
+
citedDomainsTotal: domains.total
|
|
23045
23286
|
};
|
|
23046
23287
|
});
|
|
23288
|
+
const tie = weakestTie(ranked);
|
|
23289
|
+
const engines = /* @__PURE__ */ new Set();
|
|
23290
|
+
for (const population of populations.values()) for (const provider of population.providers) engines.add(provider);
|
|
23047
23291
|
return measurementPortfolioSummaryResponseSchema.parse({
|
|
23048
23292
|
portfolio: {
|
|
23049
23293
|
groupKey: group?.stableKey ?? null,
|
|
@@ -23052,18 +23296,46 @@ function portfolioResponse(db, active, plan, query) {
|
|
|
23052
23296
|
},
|
|
23053
23297
|
measurement,
|
|
23054
23298
|
queryClass: query.queryClass,
|
|
23299
|
+
engines: [...engines].sort(compareText13),
|
|
23055
23300
|
metrics: {
|
|
23056
23301
|
propertiesMentioned: countMetric2(overview.propertiesMentioned),
|
|
23057
23302
|
mentionCoverage: coverageMetric2(overview.mentionCoverage),
|
|
23058
23303
|
citationCoverage: coverageMetric2(overview.citationCoverage)
|
|
23059
23304
|
},
|
|
23060
|
-
weakestProperties: rows
|
|
23305
|
+
weakestProperties: rows,
|
|
23306
|
+
tiedAtWeakest: tie.summary,
|
|
23307
|
+
weakestAnswerSources: weakestAnswerSources([...displayed, ...tie.rows]),
|
|
23061
23308
|
mentionRanking: mentionRanking(ranked, limit),
|
|
23062
|
-
|
|
23309
|
+
...marketRollup(plan, { run, evaluator }, group, includeNestedMarkets, limit),
|
|
23063
23310
|
totalProperties: ranked.length,
|
|
23064
23311
|
truncated: ranked.length > limit
|
|
23065
23312
|
});
|
|
23066
23313
|
}
|
|
23314
|
+
function weakestTie(ranked) {
|
|
23315
|
+
if (ranked.length === 0) return { summary: null, rows: [] };
|
|
23316
|
+
const first = ranked[0];
|
|
23317
|
+
if (first.mentionCoverage.state !== "available" || first.citationCoverage.state !== "available") {
|
|
23318
|
+
return { summary: null, rows: [] };
|
|
23319
|
+
}
|
|
23320
|
+
const mentionRate3 = first.mentionCoverage.value;
|
|
23321
|
+
const citationRate = first.citationCoverage.value;
|
|
23322
|
+
const rows = ranked.filter((row) => row.mentionCoverage.state === "available" && row.mentionCoverage.value === mentionRate3 && row.citationCoverage.state === "available" && row.citationCoverage.value === citationRate);
|
|
23323
|
+
if (rows.length < 2) return { summary: null, rows: [] };
|
|
23324
|
+
return {
|
|
23325
|
+
summary: { count: rows.length, mentionRate: mentionRate3, citationRate, note: MEASUREMENT_PORTFOLIO_TIE_NOTE },
|
|
23326
|
+
rows
|
|
23327
|
+
};
|
|
23328
|
+
}
|
|
23329
|
+
function weakestAnswerSources(rows) {
|
|
23330
|
+
const properties = /* @__PURE__ */ new Set();
|
|
23331
|
+
const answers = /* @__PURE__ */ new Map();
|
|
23332
|
+
for (const row of rows) {
|
|
23333
|
+
properties.add(row.targetKey);
|
|
23334
|
+
for (const answer of row.population.sourceAnswers) answers.set(answer.snapshot.id, answer);
|
|
23335
|
+
}
|
|
23336
|
+
const domains = domainRows([...answers.values()], MEASUREMENT_PORTFOLIO_ANSWER_SOURCES_LIMIT);
|
|
23337
|
+
return { properties: properties.size, answers: answers.size, domains: domains.rows, domainTotal: domains.total };
|
|
23338
|
+
}
|
|
23067
23339
|
function propertyCompetitorsResponse(db, active, plan, query) {
|
|
23068
23340
|
const target = requireTarget3(plan, query.targetKey);
|
|
23069
23341
|
const run = selectMeasurementQuestionRun(db, active.version.projectId, active, query.runId);
|
|
@@ -23398,7 +23670,9 @@ async function measurementPortfolioReadRoutes(app) {
|
|
|
23398
23670
|
"/projects/:name/measurement-portfolio-summary",
|
|
23399
23671
|
async (request) => {
|
|
23400
23672
|
const project = resolveProject(app.db, request.params.name);
|
|
23401
|
-
const
|
|
23673
|
+
const nested = request.query.includeNestedMarkets;
|
|
23674
|
+
const raw = nested === "true" || nested === "false" ? { ...request.query, includeNestedMarkets: nested === "true" } : request.query;
|
|
23675
|
+
const query = parseLimitQuery(raw, measurementPortfolioSummaryQuerySchema, "Invalid measurement portfolio summary query");
|
|
23402
23676
|
const { active, plan } = activeV2Plan2(app.db, project.id, "Portfolio summary");
|
|
23403
23677
|
return portfolioResponse(app.db, active, plan, query);
|
|
23404
23678
|
}
|
|
@@ -24854,10 +25128,14 @@ function simpleDispatchEngines(project, opts) {
|
|
|
24854
25128
|
return selected.filter((provider) => available === null || available.has(provider)).map((provider) => ({
|
|
24855
25129
|
provider,
|
|
24856
25130
|
// `undefined` is materially different from an intentionally blank model
|
|
24857
|
-
// override/configuration, just as it is in JobRunner
|
|
24858
|
-
|
|
25131
|
+
// override/configuration, just as it is in JobRunner, which also resolves
|
|
25132
|
+
// a retired override to the id that answers now.
|
|
25133
|
+
requestedModel: resolvedOverride(provider, overrides[provider]) ?? summaries.get(provider)?.model ?? null
|
|
24859
25134
|
})).sort((left, right) => compareText15(left.provider, right.provider));
|
|
24860
25135
|
}
|
|
25136
|
+
function resolvedOverride(provider, model) {
|
|
25137
|
+
return model === void 0 ? void 0 : resolveProviderModel(provider, model);
|
|
25138
|
+
}
|
|
24861
25139
|
function simpleDefaultLocation(project) {
|
|
24862
25140
|
if (!project.defaultLocation) return null;
|
|
24863
25141
|
return project.locations.find((location) => location.label === project.defaultLocation) ?? null;
|
|
@@ -27866,6 +28144,31 @@ async function analyticsRoutes(app) {
|
|
|
27866
28144
|
if (!Number.isInteger(n) || n <= 0) throw validationError('"limit" must be a positive integer');
|
|
27867
28145
|
limit = n;
|
|
27868
28146
|
}
|
|
28147
|
+
const parsedFilters = sourceBreakdownQuerySchema.safeParse({
|
|
28148
|
+
runId: request.query.runId,
|
|
28149
|
+
queryClass: request.query.queryClass,
|
|
28150
|
+
includeByQuery: request.query.includeByQuery
|
|
28151
|
+
});
|
|
28152
|
+
if (!parsedFilters.success) {
|
|
28153
|
+
throw validationError("Invalid source breakdown query", { issues: parsedFilters.error.issues });
|
|
28154
|
+
}
|
|
28155
|
+
const requestedRunId = parsedFilters.data.runId ?? null;
|
|
28156
|
+
const queryClass = parsedFilters.data.queryClass ?? "all";
|
|
28157
|
+
const includeByQuery = parsedFilters.data.includeByQuery !== "false" && parsedFilters.data.includeByQuery !== "0";
|
|
28158
|
+
const splitByClass = queryClass !== "all";
|
|
28159
|
+
const planClassified = splitByClass && activeMeasurementPlan(app.db, project.id)?.plan.schemaVersion === 2;
|
|
28160
|
+
const classifyQueryText = splitByClass && !planClassified ? projectQueryClassifier(project) : null;
|
|
28161
|
+
if (splitByClass && !planClassified && !classifyQueryText) {
|
|
28162
|
+
throw validationError(
|
|
28163
|
+
`Query class "${queryClass}" needs a brand name or alias on this project. Add one, or omit queryClass for a pooled source list.`
|
|
28164
|
+
);
|
|
28165
|
+
}
|
|
28166
|
+
const filters = {
|
|
28167
|
+
runId: requestedRunId,
|
|
28168
|
+
queryClass,
|
|
28169
|
+
queryClassBasis: !splitByClass ? null : planClassified ? "measurement-plan" : "query-text",
|
|
28170
|
+
includeByQuery
|
|
28171
|
+
};
|
|
27869
28172
|
const classifyCtx = {
|
|
27870
28173
|
projectDomains: effectiveDomains(project),
|
|
27871
28174
|
competitorDomains: app.db.select({ domain: competitors.domain }).from(competitors).where(eq30(competitors.projectId, project.id)).all().map((r) => r.domain)
|
|
@@ -27875,48 +28178,114 @@ async function analyticsRoutes(app) {
|
|
|
27875
28178
|
const mapped = surfaceClassFromCompetitorType(row.competitorType);
|
|
27876
28179
|
if (mapped) storedSurfaceClasses.set(normalizeProjectDomain(row.domain), mapped);
|
|
27877
28180
|
}
|
|
27878
|
-
|
|
28181
|
+
if (requestedRunId) {
|
|
28182
|
+
const requested = app.db.select().from(runs).where(and25(
|
|
28183
|
+
eq30(runs.id, requestedRunId),
|
|
28184
|
+
eq30(runs.projectId, project.id),
|
|
28185
|
+
eq30(runs.kind, RunKinds["answer-visibility"])
|
|
28186
|
+
)).get();
|
|
28187
|
+
if (!requested) throw notFound("Answer-visibility run", requestedRunId);
|
|
28188
|
+
if (requested.trigger === RunTriggers.probe) {
|
|
28189
|
+
throw validationError(`Run "${requestedRunId}" is a probe run; probes never enter source analytics.`);
|
|
28190
|
+
}
|
|
28191
|
+
if (requested.status !== RunStatuses.completed && requested.status !== RunStatuses.partial) {
|
|
28192
|
+
throw validationError(`Run "${requestedRunId}" is ${requested.status}; only completed or partial runs have source analytics.`);
|
|
28193
|
+
}
|
|
28194
|
+
if (!measuredWhole(app.db, requested)) {
|
|
28195
|
+
throw validationError(`Run "${requestedRunId}" did not fill every slot its measurement plan promised, so it is excluded from source analytics.`);
|
|
28196
|
+
}
|
|
28197
|
+
if (cutoff && requested.createdAt < cutoff) {
|
|
28198
|
+
throw validationError(`Run "${requestedRunId}" is older than the ${window} window. Omit window, or widen it, to read this run.`);
|
|
28199
|
+
}
|
|
28200
|
+
}
|
|
28201
|
+
const windowRuns = app.db.select().from(runs).where(and25(
|
|
28202
|
+
eq30(runs.projectId, project.id),
|
|
28203
|
+
eq30(runs.kind, RunKinds["answer-visibility"]),
|
|
28204
|
+
notProbeRun(),
|
|
28205
|
+
requestedRunId ? eq30(runs.id, requestedRunId) : void 0
|
|
28206
|
+
)).orderBy(desc12(runs.createdAt), desc12(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => measuredWhole(app.db, r)).filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
27879
28207
|
if (windowRuns.length === 0) {
|
|
27880
28208
|
return reply.send({
|
|
27881
|
-
|
|
27882
|
-
byQuery: {},
|
|
27883
|
-
ranked: buildRankedList(/* @__PURE__ */ new Map(), limit),
|
|
28209
|
+
ranked: buildRankedList(/* @__PURE__ */ new Map(), limit, 0, 0),
|
|
27884
28210
|
byProvider: {},
|
|
28211
|
+
providersWithoutSources: [],
|
|
28212
|
+
answerTotal: 0,
|
|
28213
|
+
runCount: 0,
|
|
28214
|
+
unclassifiedAnswers: 0,
|
|
28215
|
+
filters,
|
|
27885
28216
|
runId: "",
|
|
27886
28217
|
window,
|
|
27887
|
-
limit
|
|
28218
|
+
limit,
|
|
28219
|
+
overall: [],
|
|
28220
|
+
...includeByQuery ? { byQuery: {} } : {}
|
|
27888
28221
|
});
|
|
27889
28222
|
}
|
|
27890
28223
|
const latestGroup = groupRunsByCreatedAt(windowRuns)[0] ?? [];
|
|
27891
28224
|
const latestRunId = pickGroupRepresentative(latestGroup)?.id ?? windowRuns[0].id;
|
|
27892
28225
|
const windowRunIds = windowRuns.map((r) => r.id);
|
|
27893
|
-
const
|
|
28226
|
+
const loadedSnapshots = app.db.select({
|
|
28227
|
+
runId: querySnapshots.runId,
|
|
27894
28228
|
queryId: querySnapshots.queryId,
|
|
28229
|
+
queryText: querySnapshots.queryText,
|
|
27895
28230
|
query: queries.query,
|
|
27896
28231
|
provider: querySnapshots.provider,
|
|
27897
|
-
|
|
28232
|
+
citedDomains: querySnapshots.citedDomains,
|
|
28233
|
+
citedUrls: querySnapshots.citedUrls,
|
|
28234
|
+
measurementExecutionId: querySnapshots.measurementExecutionId
|
|
27898
28235
|
}).from(querySnapshots).leftJoin(queries, eq30(querySnapshots.queryId, queries.id)).where(inArray13(querySnapshots.runId, windowRunIds)).all();
|
|
28236
|
+
let unclassifiedAnswers = 0;
|
|
28237
|
+
let snapshots = loadedSnapshots;
|
|
28238
|
+
if (splitByClass) {
|
|
28239
|
+
const planClasses = planClassified ? planQueryClassesByRun(app.db, project.id, windowRuns) : null;
|
|
28240
|
+
snapshots = loadedSnapshots.filter((snap) => {
|
|
28241
|
+
let classes;
|
|
28242
|
+
if (planClasses) {
|
|
28243
|
+
classes = snap.measurementExecutionId ? planClasses.get(snap.runId)?.get(snap.measurementExecutionId) : void 0;
|
|
28244
|
+
} else {
|
|
28245
|
+
const text2 = snap.query ?? snap.queryText;
|
|
28246
|
+
classes = text2?.trim() ? /* @__PURE__ */ new Set([classifyQueryText(text2)]) : void 0;
|
|
28247
|
+
}
|
|
28248
|
+
if (!classes || classes.size === 0) {
|
|
28249
|
+
unclassifiedAnswers += 1;
|
|
28250
|
+
return false;
|
|
28251
|
+
}
|
|
28252
|
+
return classes.has(queryClass);
|
|
28253
|
+
});
|
|
28254
|
+
}
|
|
27899
28255
|
const overallCounts = /* @__PURE__ */ new Map();
|
|
27900
|
-
const
|
|
28256
|
+
const queryCounts = /* @__PURE__ */ new Map();
|
|
27901
28257
|
const overallDomains = /* @__PURE__ */ new Map();
|
|
27902
28258
|
const providerDomains = /* @__PURE__ */ new Map();
|
|
28259
|
+
const answersByProvider = /* @__PURE__ */ new Map();
|
|
28260
|
+
let answersWithSources = 0;
|
|
27903
28261
|
for (const snap of snapshots) {
|
|
27904
|
-
const
|
|
27905
|
-
const
|
|
27906
|
-
|
|
27907
|
-
|
|
28262
|
+
const domains = sourceDomainsOf(snap);
|
|
28263
|
+
const providerAnswers = answersByProvider.get(snap.provider) ?? { total: 0, withSources: 0 };
|
|
28264
|
+
providerAnswers.total += 1;
|
|
28265
|
+
if (domains.length > 0) {
|
|
28266
|
+
providerAnswers.withSources += 1;
|
|
28267
|
+
answersWithSources += 1;
|
|
28268
|
+
}
|
|
28269
|
+
answersByProvider.set(snap.provider, providerAnswers);
|
|
28270
|
+
const queryKey = includeByQuery && domains.length > 0 ? snap.query : null;
|
|
28271
|
+
let qCounts;
|
|
28272
|
+
if (queryKey) {
|
|
28273
|
+
qCounts = queryCounts.get(queryKey);
|
|
28274
|
+
if (!qCounts) {
|
|
28275
|
+
qCounts = /* @__PURE__ */ new Map();
|
|
28276
|
+
queryCounts.set(queryKey, qCounts);
|
|
28277
|
+
}
|
|
28278
|
+
}
|
|
28279
|
+
for (const host of domains) {
|
|
28280
|
+
const { category, label, domain } = categorizeSource(host);
|
|
27908
28281
|
const surfaceClass = classifySurfaceFromCategory(
|
|
27909
28282
|
domain,
|
|
27910
28283
|
category,
|
|
27911
28284
|
classifyCtx,
|
|
27912
28285
|
storedSurfaceClasses.get(normalizeProjectDomain(domain))
|
|
27913
28286
|
);
|
|
27914
|
-
|
|
27915
|
-
|
|
27916
|
-
oDomains.set(domain, (oDomains.get(domain) ?? 0) + 1);
|
|
27917
|
-
if (!qCounts.has(category)) qCounts.set(category, /* @__PURE__ */ new Map());
|
|
27918
|
-
const qDomains = qCounts.get(category);
|
|
27919
|
-
qDomains.set(domain, (qDomains.get(domain) ?? 0) + 1);
|
|
28287
|
+
bumpCategory(overallCounts, category, domain);
|
|
28288
|
+
if (qCounts) bumpCategory(qCounts, category, domain);
|
|
27920
28289
|
bumpDomain(overallDomains, domain, category, label, surfaceClass);
|
|
27921
28290
|
let pm = providerDomains.get(snap.provider);
|
|
27922
28291
|
if (!pm) {
|
|
@@ -27925,30 +28294,56 @@ async function analyticsRoutes(app) {
|
|
|
27925
28294
|
}
|
|
27926
28295
|
bumpDomain(pm, domain, category, label, surfaceClass);
|
|
27927
28296
|
}
|
|
27928
|
-
if (sources.length > 0 && snap.query) {
|
|
27929
|
-
byQuery[snap.query] = buildCategoryCounts(qCounts);
|
|
27930
|
-
}
|
|
27931
28297
|
}
|
|
27932
|
-
const
|
|
27933
|
-
const ranked = buildRankedList(overallDomains, limit);
|
|
28298
|
+
const ranked = buildRankedList(overallDomains, limit, snapshots.length, answersWithSources);
|
|
27934
28299
|
const byProvider = {};
|
|
27935
28300
|
for (const [provider, domains] of providerDomains) {
|
|
27936
|
-
|
|
28301
|
+
const answers = answersByProvider.get(provider);
|
|
28302
|
+
byProvider[provider] = buildRankedList(domains, limit, answers.total, answers.withSources);
|
|
27937
28303
|
}
|
|
27938
|
-
|
|
28304
|
+
const providersWithoutSources = [...answersByProvider].filter(([, answers]) => answers.withSources === 0).map(([provider]) => provider).sort();
|
|
28305
|
+
let byQuery;
|
|
28306
|
+
if (includeByQuery) {
|
|
28307
|
+
byQuery = {};
|
|
28308
|
+
for (const [query, counts2] of queryCounts) byQuery[query] = buildCategoryCounts(counts2);
|
|
28309
|
+
}
|
|
28310
|
+
return reply.send({
|
|
28311
|
+
ranked,
|
|
28312
|
+
byProvider,
|
|
28313
|
+
providersWithoutSources,
|
|
28314
|
+
answerTotal: snapshots.length,
|
|
28315
|
+
runCount: windowRuns.length,
|
|
28316
|
+
unclassifiedAnswers,
|
|
28317
|
+
filters,
|
|
28318
|
+
runId: requestedRunId ?? latestRunId,
|
|
28319
|
+
window,
|
|
28320
|
+
limit,
|
|
28321
|
+
overall: buildCategoryCounts(overallCounts),
|
|
28322
|
+
...byQuery ? { byQuery } : {}
|
|
28323
|
+
});
|
|
27939
28324
|
});
|
|
27940
28325
|
}
|
|
27941
28326
|
var ANCHOR_SWEEP_SCAN_LIMIT = 60;
|
|
27942
28327
|
function isProviderInfraDomain(uri) {
|
|
27943
28328
|
return hostMatchesAnyDomain(uri, AI_PROVIDER_INFRA_DOMAINS);
|
|
27944
28329
|
}
|
|
27945
|
-
function
|
|
27946
|
-
const
|
|
27947
|
-
const
|
|
27948
|
-
|
|
27949
|
-
|
|
27950
|
-
(
|
|
27951
|
-
|
|
28330
|
+
function sourceDomainsOf(snapshot) {
|
|
28331
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
28332
|
+
for (const value of [...snapshot.citedDomains ?? [], ...snapshot.citedUrls ?? []]) {
|
|
28333
|
+
if (typeof value !== "string") continue;
|
|
28334
|
+
const host = hostOf(value);
|
|
28335
|
+
if (!host || isProviderInfraDomain(host)) continue;
|
|
28336
|
+
hosts.add(host);
|
|
28337
|
+
}
|
|
28338
|
+
return [...hosts];
|
|
28339
|
+
}
|
|
28340
|
+
function bumpCategory(counts2, category, domain) {
|
|
28341
|
+
let domains = counts2.get(category);
|
|
28342
|
+
if (!domains) {
|
|
28343
|
+
domains = /* @__PURE__ */ new Map();
|
|
28344
|
+
counts2.set(category, domains);
|
|
28345
|
+
}
|
|
28346
|
+
domains.set(domain, (domains.get(domain) ?? 0) + 1);
|
|
27952
28347
|
}
|
|
27953
28348
|
function bucketSizeForSpan(spanDays) {
|
|
27954
28349
|
if (spanDays <= 14) return 1;
|
|
@@ -28103,7 +28498,7 @@ function bumpDomain(map, domain, category, label, surfaceClass) {
|
|
|
28103
28498
|
if (existing) existing.count += 1;
|
|
28104
28499
|
else map.set(domain, { domain, count: 1, category, label, surfaceClass });
|
|
28105
28500
|
}
|
|
28106
|
-
function buildRankedList(domains, limit) {
|
|
28501
|
+
function buildRankedList(domains, limit, answerTotal, answersWithSources) {
|
|
28107
28502
|
const all = [...domains.values()];
|
|
28108
28503
|
const totalCitedSlots = all.reduce((sum, d) => sum + d.count, 0);
|
|
28109
28504
|
const domainTotal = all.length;
|
|
@@ -28113,6 +28508,7 @@ function buildRankedList(domains, limit) {
|
|
|
28113
28508
|
domain: d.domain,
|
|
28114
28509
|
count: d.count,
|
|
28115
28510
|
percentage: totalCitedSlots > 0 ? round4(d.count / totalCitedSlots) : 0,
|
|
28511
|
+
answerShare: answerTotal > 0 ? round4(d.count / answerTotal) : 0,
|
|
28116
28512
|
category: d.category,
|
|
28117
28513
|
label: d.label,
|
|
28118
28514
|
surfaceClass: d.surfaceClass
|
|
@@ -28134,6 +28530,8 @@ function buildRankedList(domains, limit) {
|
|
|
28134
28530
|
})).sort((a, b) => b.count - a.count || a.surfaceClass.localeCompare(b.surfaceClass));
|
|
28135
28531
|
return {
|
|
28136
28532
|
totalCitedSlots,
|
|
28533
|
+
answerTotal,
|
|
28534
|
+
answersWithSources,
|
|
28137
28535
|
domainTotal,
|
|
28138
28536
|
entries,
|
|
28139
28537
|
truncatedDomainCount: domainTotal - shownEntries.length,
|
|
@@ -36589,7 +36987,7 @@ var competitorLandscapeGroupKeyParameter = {
|
|
|
36589
36987
|
var competitorLandscapeScopeParameter = {
|
|
36590
36988
|
name: "scope",
|
|
36591
36989
|
in: "query",
|
|
36592
|
-
description: 'Set to "all-markets" to aggregate raw stored evidence across every Advanced Measurement market. It cannot be combined with groupKey.',
|
|
36990
|
+
description: 'Set to "all-markets" to aggregate raw stored evidence across every Property in the Advanced Measurement plan, including Properties in no market. It cannot be combined with groupKey.',
|
|
36593
36991
|
schema: { type: "string", enum: ["project", "all-markets"] }
|
|
36594
36992
|
};
|
|
36595
36993
|
var competitorLandscapeProviderParameter = {
|
|
@@ -36613,7 +37011,7 @@ var competitorLandscapeGroupByParameter = {
|
|
|
36613
37011
|
var competitorLandscapeQueryClassParameter = {
|
|
36614
37012
|
name: "queryClass",
|
|
36615
37013
|
in: "query",
|
|
36616
|
-
description: "Restrict evidence to a
|
|
37014
|
+
description: "Restrict evidence to a query class. Advanced groups use their frozen assignment classes; simple projects classify stored query text. On a project with an active v2 measurement plan, branded or non-brand without scope or groupKey defaults to scope=all-markets, so the class comes from the plan rather than the text classifier.",
|
|
36617
37015
|
schema: { type: "string", enum: ["all", "branded", "non-brand"] }
|
|
36618
37016
|
};
|
|
36619
37017
|
var competitorLandscapeLocationParameter = {
|
|
@@ -36628,6 +37026,24 @@ var competitorLandscapeRunIdParameter = {
|
|
|
36628
37026
|
description: "Restrict evidence to one stored answer-visibility run.",
|
|
36629
37027
|
schema: stringSchema
|
|
36630
37028
|
};
|
|
37029
|
+
var sourcesRunIdParameter = {
|
|
37030
|
+
name: "runId",
|
|
37031
|
+
in: "query",
|
|
37032
|
+
description: "Read one stored answer-visibility run instead of pooling every run in the window. An unknown id is 404; a probe, unfinished, partially measured, or out-of-window run is 400.",
|
|
37033
|
+
schema: stringSchema
|
|
37034
|
+
};
|
|
37035
|
+
var sourcesQueryClassParameter = {
|
|
37036
|
+
name: "queryClass",
|
|
37037
|
+
in: "query",
|
|
37038
|
+
description: "Restrict to branded or non-brand answers. With an active v2 measurement plan the class comes from each run's frozen plan assignments (the same answers as the competitor landscape at scope=all-markets); otherwise from the project's brand-name classifier over the query text. Answers neither basis can place are excluded and counted in unclassifiedAnswers. Defaults to all, which pools both classes.",
|
|
37039
|
+
schema: { type: "string", enum: ["all", "branded", "non-brand"] }
|
|
37040
|
+
};
|
|
37041
|
+
var sourcesIncludeByQueryParameter = {
|
|
37042
|
+
name: "includeByQuery",
|
|
37043
|
+
in: "query",
|
|
37044
|
+
description: "Set to false to omit the per-query breakdown (byQuery), which grows with every tracked query. Defaults to true.",
|
|
37045
|
+
schema: { type: "string", enum: ["true", "false", "1", "0"] }
|
|
37046
|
+
};
|
|
36631
37047
|
var analyticsStartDateParameter = {
|
|
36632
37048
|
name: "startDate",
|
|
36633
37049
|
in: "query",
|
|
@@ -37435,7 +37851,7 @@ var routeCatalog = [
|
|
|
37435
37851
|
method: "get",
|
|
37436
37852
|
path: "/api/v1/projects/{name}/measurement-portfolio-summary",
|
|
37437
37853
|
summary: "Get the weakest measured Properties",
|
|
37438
|
-
description: "Returns a compact, revision-pinned portfolio ranking from stored results, plus a worst-first roll-up
|
|
37854
|
+
description: "Returns a compact, revision-pinned portfolio ranking from stored results, plus a worst-first market roll-up. It defaults to non-brand queries, ranks measured mention coverage before citation coverage, and keeps unavailable rows separate from measured weakness. Every Property row carries its metro, submarkets and query count; weakest rows also carry namedInsteadInAnswerText (names written in the answer text of answers that neither named nor cited the Property, counted by answer) and citedDomains (domains cited by the Property's answers, counted by answer, every engine included). tiedAtWeakest reports Properties sharing the weakest rates, and weakestAnswerSources ranks the domains cited across the weakest and tied Properties, each answer once. markets holds one level by default (top-level markets, or the selected group's direct children), worst-first and capped at limit; includeNestedMarkets returns every level. Every market is scoped to the displayed run, so a market row matches that market read with groupKey; markets may share Properties and never sum to the portfolio totals. It never starts provider work.",
|
|
37439
37855
|
tags: ["measurement-plans"],
|
|
37440
37856
|
parameters: [
|
|
37441
37857
|
nameParameter,
|
|
@@ -37444,7 +37860,8 @@ var routeCatalog = [
|
|
|
37444
37860
|
{ name: "provider", in: "query", description: "Restrict to one answer provider.", schema: stringSchema },
|
|
37445
37861
|
{ name: "location", in: "query", description: "Restrict to one execution location label.", schema: stringSchema },
|
|
37446
37862
|
{ name: "runId", in: "query", description: "Read this completed or partial active-revision run, including a named spot check.", schema: stringSchema },
|
|
37447
|
-
{ name: "limit", in: "query", description: "
|
|
37863
|
+
{ name: "limit", in: "query", description: "Rows per list (weakest Properties, both mention rankings, markets). Defaults to 4, maximum 50.", schema: { type: "integer", minimum: 1, maximum: 50 } },
|
|
37864
|
+
{ name: "includeNestedMarkets", in: "query", description: "Return every market in scope at every level, uncapped. Defaults to false: one level (top-level markets, or the selected group's direct children), capped at limit.", schema: { type: "boolean" } }
|
|
37448
37865
|
],
|
|
37449
37866
|
responses: {
|
|
37450
37867
|
200: jsonResponse("Compact portfolio summary returned.", "MeasurementPortfolioSummaryResponse"),
|
|
@@ -38591,11 +39008,20 @@ var routeCatalog = [
|
|
|
38591
39008
|
method: "get",
|
|
38592
39009
|
path: "/api/v1/projects/{name}/analytics/sources",
|
|
38593
39010
|
summary: "Get source origin analytics",
|
|
39011
|
+
description: "Cited domains ranked by how many answers cite them, read from each answer's stored source list (citedDomains plus citedUrls), so every provider is counted, Gemini included. A domain counts at most once per answer. Without runId the response pools every run in the window (runCount says how many); without queryClass it pools branded and non-brand answers.",
|
|
38594
39012
|
tags: ["analytics"],
|
|
38595
|
-
parameters: [
|
|
39013
|
+
parameters: [
|
|
39014
|
+
nameParameter,
|
|
39015
|
+
analyticsWindowParameter,
|
|
39016
|
+
limitQueryParameter,
|
|
39017
|
+
sourcesRunIdParameter,
|
|
39018
|
+
sourcesQueryClassParameter,
|
|
39019
|
+
sourcesIncludeByQueryParameter
|
|
39020
|
+
],
|
|
38596
39021
|
responses: {
|
|
38597
39022
|
200: jsonResponse("Source breakdown returned.", "SourceBreakdownDto"),
|
|
38598
|
-
|
|
39023
|
+
400: errorResponse("Invalid query parameters."),
|
|
39024
|
+
404: errorResponse("Project or run not found.")
|
|
38599
39025
|
}
|
|
38600
39026
|
},
|
|
38601
39027
|
{
|
|
@@ -55513,7 +55939,7 @@ async function cdpRoutes(app, opts) {
|
|
|
55513
55939
|
browserOnlyCited++;
|
|
55514
55940
|
disagreed++;
|
|
55515
55941
|
}
|
|
55516
|
-
const
|
|
55942
|
+
const parseGroundingSources = (snap) => {
|
|
55517
55943
|
if (!snap?.rawResponse) return [];
|
|
55518
55944
|
try {
|
|
55519
55945
|
const raw = JSON.parse(snap.rawResponse);
|
|
@@ -55528,13 +55954,13 @@ async function cdpRoutes(app, opts) {
|
|
|
55528
55954
|
provider: api.provider,
|
|
55529
55955
|
citationState: api.citationState,
|
|
55530
55956
|
citedDomains: api.citedDomains,
|
|
55531
|
-
groundingSources:
|
|
55957
|
+
groundingSources: parseGroundingSources(api)
|
|
55532
55958
|
} : null,
|
|
55533
55959
|
browser: browser ? {
|
|
55534
55960
|
provider: browser.provider,
|
|
55535
55961
|
citationState: browser.citationState,
|
|
55536
55962
|
citedDomains: browser.citedDomains,
|
|
55537
|
-
groundingSources:
|
|
55963
|
+
groundingSources: parseGroundingSources(browser),
|
|
55538
55964
|
screenshotUrl: browser.screenshotPath ? `${opts.routePrefix ?? "/api/v1"}/screenshots/${browser.id}` : void 0
|
|
55539
55965
|
} : null,
|
|
55540
55966
|
agreement
|
|
@@ -85358,7 +85784,7 @@ async function researchRoutes(app, opts) {
|
|
|
85358
85784
|
if (location && !project.locations.some((item2) => sameLocation(item2, location))) throw validationError("Research location must exactly match a configured project location.", { location });
|
|
85359
85785
|
const template = input.template ? resolveResearchTemplate(app, project.id, input.template, scope, location) : null;
|
|
85360
85786
|
const requestedModel2 = input.model ?? null;
|
|
85361
|
-
const resolvedModel = requestedModel2 ?? (project.providerModels[providerName] || opts.getEffectiveProviderModels?.()[providerName] || adapter.defaultModel);
|
|
85787
|
+
const resolvedModel = resolveProviderModel(providerName, requestedModel2 ?? (project.providerModels[providerName] || opts.getEffectiveProviderModels?.()[providerName] || adapter.defaultModel));
|
|
85362
85788
|
adapter.modelValidationPattern.lastIndex = 0;
|
|
85363
85789
|
if (!adapter.modelValidationPattern.test(resolvedModel)) throw validationError('Invalid resolved model "' + resolvedModel + '" for provider "' + providerName + '".', { provider: providerName, model: resolvedModel, hint: adapter.modelValidationHint });
|
|
85364
85790
|
if (deduplicateResearchQueries(input.queries).length !== input.queries.length) throw validationError("Research queries must be unique within a batch.");
|
|
@@ -85495,7 +85921,7 @@ async function researchRoutes(app, opts) {
|
|
|
85495
85921
|
const configured = new Set(opts.configuredProviderNames ?? []);
|
|
85496
85922
|
const effectiveModels2 = opts.getEffectiveProviderModels?.() ?? {};
|
|
85497
85923
|
const providers = (opts.providerAdapters ?? []).filter((adapter) => adapter.mode === "api" && !isBrowserProvider(adapter.name) && configured.has(adapter.name)).map((adapter) => {
|
|
85498
|
-
const defaultModel = project.providerModels[adapter.name] || effectiveModels2[adapter.name] || adapter.defaultModel;
|
|
85924
|
+
const defaultModel = resolveProviderModel(adapter.name, project.providerModels[adapter.name] || effectiveModels2[adapter.name] || adapter.defaultModel);
|
|
85499
85925
|
const cached = opts.getCachedProviderModels?.(adapter.name) ?? [];
|
|
85500
85926
|
const models = cached.length ? cached : adapter.knownModels;
|
|
85501
85927
|
return {
|