@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.
Files changed (32) hide show
  1. package/assets/agent-workspace/skills/aero/SKILL.md +13 -8
  2. package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +27 -10
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +7 -2
  4. package/assets/assets/{AuditHistoryPanel-C_p8jX7Q.js → AuditHistoryPanel-BSh3Oezj.js} +1 -1
  5. package/assets/assets/{BacklinksPage-jD4nOGw1.js → BacklinksPage-DQ59dhCx.js} +1 -1
  6. package/assets/assets/{HistoryPage-DJGBbtBY.js → HistoryPage-BBvgid3o.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-Dmr3TVlo.js → MeasurementPropertyPage-Dmb18JbT.js} +1 -1
  8. package/assets/assets/{ProjectPage-B_haCKiA.js → ProjectPage-CPg0q2Dy.js} +1 -1
  9. package/assets/assets/{RunRow-BMbwR2yk.js → RunRow-BK8sZCID.js} +1 -1
  10. package/assets/assets/{RunsPage-D8hHakq2.js → RunsPage-CIcVBTdP.js} +1 -1
  11. package/assets/assets/{SettingsPage-QKnRcFA9.js → SettingsPage-Bc3aR-gz.js} +1 -1
  12. package/assets/assets/{SiteHealthSection-CDkUXqmb.js → SiteHealthSection-FF14aIXT.js} +3 -3
  13. package/assets/assets/{TrafficPage-CMif8RlM.js → TrafficPage-C_qoeGDl.js} +1 -1
  14. package/assets/assets/{TrafficSourceDetailPage-DOCTP_4-.js → TrafficSourceDetailPage-B3i_B2MI.js} +1 -1
  15. package/assets/assets/{extract-error-message-Dm8RvYE5.js → extract-error-message-BisUn6nN.js} +1 -1
  16. package/assets/assets/{index-lj0fuFl4.js → index-Dht7JNX5.js} +44 -44
  17. package/assets/assets/{react-sigma_core.esm.min-DOPuwLtN.js → react-sigma_core.esm.min-tWhS09SS.js} +1 -1
  18. package/assets/assets/{v2-overview-adapter-C19tUcDS.js → v2-overview-adapter-CAoeWKQJ.js} +1 -1
  19. package/assets/index.html +1 -1
  20. package/dist/{chunk-QJWIOCEO.js → chunk-4PENJKRN.js} +2 -2
  21. package/dist/{chunk-SPCEXW3B.js → chunk-5FKR2K76.js} +11 -4
  22. package/dist/{chunk-GUJY3RIM.js → chunk-DRZWMFRV.js} +627 -263
  23. package/dist/{chunk-WLL7Q7JD.js → chunk-QF7SY7AF.js} +222 -29
  24. package/dist/{chunk-ABUO27BV.js → chunk-TSFKNQ5X.js} +3 -3
  25. package/dist/{chunk-P2EFPLIO.js → chunk-WL27HO5C.js} +565 -139
  26. package/dist/{chunk-YHKKCXGY.js → chunk-XBGPHZKA.js} +24 -8
  27. package/dist/cli.js +73 -17
  28. package/dist/{demo-server-BOIBS74F.js → demo-server-OCBV5OMS.js} +4 -4
  29. package/dist/index.js +6 -6
  30. package/dist/{intelligence-service-IWR2H6OW.js → intelligence-service-MKHINDHN.js} +2 -2
  31. package/dist/mcp.js +4 -4
  32. 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-WLL7Q7JD.js";
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 answerDomains = evidence.answerDomains ?? extractDomainsFromText(evidence.answerText);
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 (answerDomains.some((candidate) => hostMatchesDomain(candidate, identity.domain)) || identity.domainBrandKey !== null && mentionedBrandKeys.has(identity.domainBrandKey)) {
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
- if (selection?.autoAdvanced && activeMeasurementPlan(app.db, project.id)?.plan.schemaVersion === 2) filters.scope = "all-markets";
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: observedCompetitorNames(selectedSnapshots),
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 planVersionIds = [...new Set(candidateRuns.map((run) => run.measurementPlanVersionId).filter((id) => typeof id === "string" && id.length > 0))];
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
- const groups = kind === "group" ? plan.groups.filter((group) => group.stableKey === groupKey) : plan.groups;
16280
- if (groups.length === 0) return null;
16281
- const targetKeys = new Set(groups.flatMap((group) => group.targetKeys));
16282
- const scopedUsageEdges = plan.usageEdges.filter((edge) => targetKeys.has(edge.targetKey));
16283
- const executionNodeKeys = new Set(scopedUsageEdges.map((edge) => edge.executionNodeKey));
16284
- const scopedUsages = new Set(scopedUsageEdges.map((edge) => JSON.stringify([
16285
- edge.executionNodeKey,
16286
- edge.targetKey,
16287
- edge.queryId
16288
- ])));
16289
- const queryClassesByExecution = /* @__PURE__ */ new Map();
16290
- for (const assignment of plan.assignments) {
16291
- if (!scopedUsages.has(JSON.stringify([
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
- ]))) continue;
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]) => model ? [[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 targetAnswers(plan, target, materialized, filters) {
22811
- 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]));
22812
- 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]));
22813
- const ownEdgeIds = new Set([...edgesByExecution.values()].map((edge) => edge.id));
22814
- const answerBySlot = new Map(materialized.evidence.answers.filter((answer) => ownEdgeIds.has(answer.usageEdgeId)).map((answer) => [answer.expectedSlotId, answer]));
22815
- const citedSlotIds = new Set(materialized.evidence.evidence.filter((row) => ownEdgeIds.has(row.usageEdgeId) && row.classification === "assigned").map((row) => row.expectedSlotId));
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 slots = materialized.input.expectedSlots.filter((slot) => assignmentsByExecution.has(slot.executionId) && edgesByExecution.has(slot.executionId) && (provider === void 0 || normalizeText2(slot.provider) === provider) && (location === void 0 || normalizeMeasurementLocation(slot.location) === location));
22821
- const answers = [];
22822
- for (const slot of slots) {
22823
- const observation = materialized.observationsBySlot.get(slot.id);
22824
- const snapshot = observation === void 0 ? void 0 : materialized.snapshotsById.get(observation.id);
22825
- if (!snapshot || snapshot.answerText === null) continue;
22826
- const assignment = assignmentsByExecution.get(slot.executionId);
22827
- if (!assignment) throw new Error(`measurement assignment provenance is corrupt for execution ${slot.executionId}`);
22828
- answers.push({
22829
- slotId: slot.id,
22830
- provider: slot.provider,
22831
- question: questionsById.get(assignment.queryId) ?? slot.queryText,
22832
- snapshot,
22833
- mentioned: target.mentionNotApplicable ? null : answerBySlot.get(slot.id)?.mentioned ?? null,
22834
- cited: citedSlotIds.has(slot.id) ? true : incompleteObservationIds.has(snapshot.id) ? null : false
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
- return { expected: slots.length, answers };
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, run, evaluator, scopedToOneGroup) {
22929
- if (scopedToOneGroup) return [];
22930
- return plan.groups.map((group) => {
22931
- const targetKeys = targetKeysForRun(plan, run, group.targetKeys, false);
22932
- const overview = evaluator.evaluate(targetKeys);
22933
- return {
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({ targetKey, label, mentionCoverage, citationCoverage });
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
- recommendedInstead: [],
22995
- recommendedInsteadTotal: 0,
22996
- recommendedInsteadTruncated: false
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, limit2),
23009
- mentionRanking: mentionRanking(rows2, limit2),
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
- markets: group !== void 0 ? [] : plan.groups.map((g) => ({
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 > limit2
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 limit = query.limit ?? DEFAULT_LIMIT3;
23038
- const rows = ranked.slice(0, limit).map(({ target, ...row }) => {
23039
- const recommendations = recommendationRows(target, targetAnswers(plan, target, materialized, filters));
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
- recommendedInstead: recommendations.slice(0, 5).map(({ name, occurrences }) => ({ name, occurrences })),
23043
- recommendedInsteadTotal: recommendations.length,
23044
- recommendedInsteadTruncated: recommendations.length > 5
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.slice(0, limit),
23305
+ weakestProperties: rows,
23306
+ tiedAtWeakest: tie.summary,
23307
+ weakestAnswerSources: weakestAnswerSources([...displayed, ...tie.rows]),
23061
23308
  mentionRanking: mentionRanking(ranked, limit),
23062
- markets: marketRollup(plan, run, evaluator, group !== void 0),
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 query = parseLimitQuery(request.query, measurementPortfolioSummaryQuerySchema, "Invalid measurement portfolio summary query");
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
- requestedModel: overrides[provider] ?? summaries.get(provider)?.model ?? null
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
- const windowRuns = app.db.select().from(runs).where(and25(eq30(runs.projectId, project.id), eq30(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).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);
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
- overall: [],
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 snapshots = app.db.select({
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
- rawResponse: querySnapshots.rawResponse
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 byQuery = {};
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 sources = parseGroundingSources(snap.rawResponse);
27905
- const qCounts = /* @__PURE__ */ new Map();
27906
- for (const source of sources) {
27907
- const { category, label, domain } = categorizeSource(source.uri);
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
- if (!overallCounts.has(category)) overallCounts.set(category, /* @__PURE__ */ new Map());
27915
- const oDomains = overallCounts.get(category);
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 overall = buildCategoryCounts(overallCounts);
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
- byProvider[provider] = buildRankedList(domains, limit);
28301
+ const answers = answersByProvider.get(provider);
28302
+ byProvider[provider] = buildRankedList(domains, limit, answers.total, answers.withSources);
27937
28303
  }
27938
- return reply.send({ overall, byQuery, ranked, byProvider, runId: latestRunId, window, limit });
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 parseGroundingSources(rawResponse) {
27946
- const parsed = parseJsonColumn(rawResponse, {});
27947
- const sources = parsed.groundingSources;
27948
- if (!Array.isArray(sources)) return [];
27949
- return sources.filter(
27950
- (s) => typeof s.uri === "string" && !isProviderInfraDomain(s.uri)
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 question class. Advanced groups use their frozen assignment classes; simple projects classify stored query text.",
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 of every named market. It defaults to Non-brand questions, ranks measured mention coverage before citation coverage, and keeps unavailable rows separate from measured weakness. 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. The markets array is empty when the request already narrowed to one group. Replacement names come only from stored answer extraction. It never starts provider work.",
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: "Maximum Property rows. Defaults to 10, maximum 50.", schema: { type: "integer", minimum: 1, maximum: 50 } }
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: [nameParameter, analyticsWindowParameter, limitQueryParameter],
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
- 404: errorResponse("Project not found.")
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 parseGroundingSources2 = (snap) => {
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: parseGroundingSources2(api)
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: parseGroundingSources2(browser),
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 {