@ainyc/canonry 4.189.1 → 4.190.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +2 -2
  2. package/assets/assets/{AuditHistoryPanel-jkTli_49.js → AuditHistoryPanel-Dob_85ay.js} +1 -1
  3. package/assets/assets/{BacklinksPage-UvZsCycW.js → BacklinksPage-BDm4gQsa.js} +1 -1
  4. package/assets/assets/{HistoryPage-BNhAdEgU.js → HistoryPage-BUGJKNWJ.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-BlAQhKbf.js → MeasurementPropertyPage-H2g4VdcA.js} +1 -1
  6. package/assets/assets/ProjectPage-d7iOSkrd.js +8 -0
  7. package/assets/assets/{RunRow-DT_T7pqA.js → RunRow-Cja3vahD.js} +1 -1
  8. package/assets/assets/{RunsPage-Bl0L0rlS.js → RunsPage-BdVfLW9Z.js} +1 -1
  9. package/assets/assets/{SettingsPage-Bqn1dpbS.js → SettingsPage-Xw9rdDPd.js} +1 -1
  10. package/assets/assets/{SiteHealthSection-z2DhR3t6.js → SiteHealthSection-CjioXMWy.js} +3 -3
  11. package/assets/assets/{TrafficPage-C_5kd1cm.js → TrafficPage-D5ZqJrJm.js} +1 -1
  12. package/assets/assets/{TrafficSourceDetailPage-Rrwaz-mV.js → TrafficSourceDetailPage-DB5V8cdH.js} +1 -1
  13. package/assets/assets/{extract-error-message-CBGBgQDU.js → extract-error-message-lzuarNEg.js} +1 -1
  14. package/assets/assets/{index-KTeRanN1.css → index-BjjM151x.css} +1 -1
  15. package/assets/assets/index-YVHujfy1.js +86 -0
  16. package/assets/assets/{react-sigma_core.esm.min-5Q-_e_E0.js → react-sigma_core.esm.min-Ddu-c40B.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-J533DKYE.js → chunk-2II4ZMMH.js} +2 -2
  19. package/dist/{chunk-YDYB3H4O.js → chunk-32QBVRMN.js} +34 -3
  20. package/dist/{chunk-HG6NLN7H.js → chunk-BE366UCM.js} +4 -4
  21. package/dist/{chunk-QQLMRABM.js → chunk-ECPN4OUR.js} +184 -82
  22. package/dist/{chunk-CDI3YR57.js → chunk-ROPJUNQN.js} +55 -1
  23. package/dist/cli.js +47 -19
  24. package/dist/index.js +4 -4
  25. package/dist/{intelligence-service-M5TYY4TH.js → intelligence-service-QCKX2W4G.js} +2 -2
  26. package/dist/mcp.js +3 -3
  27. package/package.json +8 -8
  28. package/assets/assets/ProjectPage-C2oKQpyS.js +0 -9
  29. package/assets/assets/index-vRk2f6HY.js +0 -86
@@ -267,6 +267,7 @@ import {
267
267
  createdApiKeyDtoSchema,
268
268
  dedupeReportActions,
269
269
  dedupeReportOpportunities,
270
+ deduplicateResearchQueries,
270
271
  deliveryFailed,
271
272
  deltaPercent,
272
273
  deltaTone,
@@ -293,6 +294,8 @@ import {
293
294
  emptyCitationVisibility,
294
295
  escapeLikePattern,
295
296
  evaluateModelPointerExposure,
297
+ expandQueryTemplate,
298
+ expandResearchTemplate,
296
299
  extractAnswerMentions,
297
300
  extractDomainsFromText,
298
301
  factorStatusFromScore,
@@ -441,6 +444,7 @@ import {
441
444
  measurementDraftTargetPageSchema,
442
445
  measurementDraftUpsertCompetitorRequestSchema,
443
446
  measurementDraftUpsertGroupRequestSchema,
447
+ measurementDraftUpsertMarketRequestSchema,
444
448
  measurementDraftUpsertTargetRequestSchema,
445
449
  measurementIdempotencyKeyConflict,
446
450
  measurementIdempotencyKeyRequired,
@@ -672,7 +676,7 @@ import {
672
676
  wordpressSchemaDeployResultDtoSchema,
673
677
  wordpressSchemaStatusResultDtoSchema,
674
678
  wordpressStatusDtoSchema
675
- } from "./chunk-CDI3YR57.js";
679
+ } from "./chunk-ROPJUNQN.js";
676
680
 
677
681
  // src/intelligence-service.ts
678
682
  import { eq as eq67, desc as desc29, asc as asc12, and as and57, ne as ne10, or as or14, inArray as inArray25, gte as gte18, lte as lte15, isNull as isNull12, sql as sql26, exists } from "drizzle-orm";
@@ -2723,6 +2727,8 @@ var researchRuns = sqliteTable("research_runs", {
2723
2727
  requestedModel: text("requested_model"),
2724
2728
  resolvedModel: text("resolved_model").notNull(),
2725
2729
  location: text("location", { mode: "json" }).$type(),
2730
+ scope: text("scope", { mode: "json" }).$type(),
2731
+ template: text("template", { mode: "json" }).$type(),
2726
2732
  totalQueries: integer("total_queries").notNull(),
2727
2733
  completedQueries: integer("completed_queries").notNull().default(0),
2728
2734
  failedQueries: integer("failed_queries").notNull().default(0),
@@ -2739,6 +2745,7 @@ var researchRunQueries = sqliteTable("research_run_queries", {
2739
2745
  researchRunId: text("research_run_id").notNull().references(() => researchRuns.id, { onDelete: "cascade" }),
2740
2746
  position: integer("position").notNull(),
2741
2747
  queryText: text("query_text").notNull(),
2748
+ queryClass: text("query_class").$type(),
2742
2749
  status: text("status").notNull().default("queued"),
2743
2750
  requestedModel: text("requested_model"),
2744
2751
  resolvedModel: text("resolved_model").notNull(),
@@ -7161,6 +7168,15 @@ var MIGRATION_VERSIONS = [
7161
7168
  statements: [
7162
7169
  `ALTER TABLE schedules ADD COLUMN recurrence TEXT`
7163
7170
  ]
7171
+ },
7172
+ {
7173
+ version: 153,
7174
+ name: "research-run-scope",
7175
+ statements: [
7176
+ `ALTER TABLE research_runs ADD COLUMN scope TEXT`,
7177
+ `ALTER TABLE research_runs ADD COLUMN template TEXT`,
7178
+ `ALTER TABLE research_run_queries ADD COLUMN query_class TEXT`
7179
+ ]
7164
7180
  }
7165
7181
  ];
7166
7182
  function addRunsMeasurementPlanVersionForeignKey(tx) {
@@ -13789,54 +13805,61 @@ function scopeNamedShareOfVoice(identities, answered2, prepared) {
13789
13805
  entries: credited.map((row) => ({ ...row, share: row.credits / denominator }))
13790
13806
  };
13791
13807
  }
13792
- function buildMeasurementOverview(input, options = {}) {
13808
+ function createMeasurementOverviewEvaluator(input, options = {}) {
13793
13809
  const prepared = prepareReport(input, options);
13794
13810
  const indexes = buildMeasurementOverviewIndexes(input, prepared, options.onEvidenceIndexed);
13795
- const targetIds = new Set(input.scopeTargetIds);
13796
- const edges = indexedScopeEdges(indexes, targetIds);
13797
- const slots = indexedSlotsForEdges(edges, indexes);
13798
- const answered2 = indexedAnsweredSlots(slots, indexes);
13799
- const properties = sortedUnique([...targetIds]).map((targetId) => {
13800
- const ownEdges = indexes.targetEdgesByTargetId.get(targetId) ?? [];
13801
- const ownSlots = indexedSlotsForEdges(ownEdges, indexes);
13802
- const ownAnswered = indexedAnsweredSlots(ownSlots, indexes);
13803
- const ownTargets = indexes.targetsById.get(targetId);
13804
- return {
13805
- targetId,
13806
- mentionCoverage: targetMentionRate(ownTargets, ownSlots, ownAnswered, indexes),
13807
- citationCoverage: indexedScopeCitationRate(ownSlots, ownEdges, indexes),
13808
- // Each engine is measured over the slots it owns, using exactly the
13809
- // functions the Property total uses. A per-engine reading is therefore
13810
- // withheld for the same reasons the total is, never rounded down to zero.
13811
- providers: providersFor(ownSlots).map((provider) => {
13812
- const providerSlots = ownSlots.filter((slot) => slot.provider === provider);
13811
+ return {
13812
+ evaluate(scopeTargetIds, namedIdentities = input.namedIdentities) {
13813
+ const targetIds = new Set(scopeTargetIds);
13814
+ const edges = indexedScopeEdges(indexes, targetIds);
13815
+ const slots = indexedSlotsForEdges(edges, indexes);
13816
+ const answered2 = indexedAnsweredSlots(slots, indexes);
13817
+ const properties = sortedUnique([...targetIds]).map((targetId) => {
13818
+ const ownEdges = indexes.targetEdgesByTargetId.get(targetId) ?? [];
13819
+ const ownSlots = indexedSlotsForEdges(ownEdges, indexes);
13820
+ const ownAnswered = indexedAnsweredSlots(ownSlots, indexes);
13821
+ const ownTargets = indexes.targetsById.get(targetId);
13813
13822
  return {
13814
- provider,
13815
- mentionCoverage: targetMentionRate(
13816
- ownTargets,
13817
- providerSlots,
13818
- indexedAnsweredSlots(providerSlots, indexes),
13819
- indexes
13820
- ),
13821
- citationCoverage: indexedScopeCitationRate(providerSlots, ownEdges, indexes)
13823
+ targetId,
13824
+ mentionCoverage: targetMentionRate(ownTargets, ownSlots, ownAnswered, indexes),
13825
+ citationCoverage: indexedScopeCitationRate(ownSlots, ownEdges, indexes),
13826
+ // Each engine is measured over the slots it owns, using exactly the
13827
+ // functions the Property total uses. A per-engine reading is therefore
13828
+ // withheld for the same reasons the total is, never rounded down to zero.
13829
+ providers: providersFor(ownSlots).map((provider) => {
13830
+ const providerSlots = ownSlots.filter((slot) => slot.provider === provider);
13831
+ return {
13832
+ provider,
13833
+ mentionCoverage: targetMentionRate(
13834
+ ownTargets,
13835
+ providerSlots,
13836
+ indexedAnsweredSlots(providerSlots, indexes),
13837
+ indexes
13838
+ ),
13839
+ citationCoverage: indexedScopeCitationRate(providerSlots, ownEdges, indexes)
13840
+ };
13841
+ }),
13842
+ flags: indexes.ambiguousEvidenceKeysByTargetId.get(targetId)?.size ?? 0
13822
13843
  };
13823
- }),
13824
- flags: indexes.ambiguousEvidenceKeysByTargetId.get(targetId)?.size ?? 0
13825
- };
13826
- });
13827
- return {
13828
- eligibleSlots: slots.length,
13829
- answeredSlots: answered2.length,
13830
- includesHistoricalData: prepared.diagnostics.bridgedObservationIds.length > 0 || prepared.diagnostics.historicalObservationIds.length > 0,
13831
- propertiesMentioned: scopePropertiesMentioned(input, targetIds, slots, answered2, prepared),
13832
- mentionCoverage: scopeMentionRate(input, targetIds, slots, answered2, prepared),
13833
- citationCoverage: indexedScopeCitationRate(slots, edges, indexes),
13834
- brandPresence: scopeBrandPresence(input, slots, answered2, prepared),
13835
- namedShareOfVoice: scopeNamedShareOfVoice(input.namedIdentities ?? [], answered2, prepared),
13836
- properties,
13837
- flags: properties.reduce((total, row) => total + row.flags, 0)
13844
+ });
13845
+ return {
13846
+ eligibleSlots: slots.length,
13847
+ answeredSlots: answered2.length,
13848
+ includesHistoricalData: prepared.diagnostics.bridgedObservationIds.length > 0 || prepared.diagnostics.historicalObservationIds.length > 0,
13849
+ propertiesMentioned: scopePropertiesMentioned(input, targetIds, slots, answered2, prepared),
13850
+ mentionCoverage: scopeMentionRate(input, targetIds, slots, answered2, prepared),
13851
+ citationCoverage: indexedScopeCitationRate(slots, edges, indexes),
13852
+ brandPresence: scopeBrandPresence(input, slots, answered2, prepared),
13853
+ namedShareOfVoice: scopeNamedShareOfVoice(namedIdentities ?? [], answered2, prepared),
13854
+ properties,
13855
+ flags: properties.reduce((total, row) => total + row.flags, 0)
13856
+ };
13857
+ }
13838
13858
  };
13839
13859
  }
13860
+ function buildMeasurementOverview(input, options = {}) {
13861
+ return createMeasurementOverviewEvaluator(input, options).evaluate(input.scopeTargetIds, input.namedIdentities);
13862
+ }
13840
13863
  function buildMeasurementEvidence(input, options = {}) {
13841
13864
  const prepared = prepareReport(input, options);
13842
13865
  return { answers: prepared.answers, evidence: prepared.evidence, diagnostics: prepared.diagnostics };
@@ -17891,6 +17914,7 @@ var MEASUREMENT_DRAFT_ACTIONS = [
17891
17914
  "clear-assignments",
17892
17915
  "classify-assignments",
17893
17916
  "upsert-group",
17917
+ "upsert-market",
17894
17918
  "remove-group",
17895
17919
  "upsert-competitor",
17896
17920
  "remove-competitor"
@@ -18288,6 +18312,23 @@ function upsertGroup(authoring, body) {
18288
18312
  warnings: unknown.length ? [warn("group-unknown-target", `Group "${group.stableKey}" names ${unknown.length} Target(s) the draft does not hold yet.`, ["groups", groupIndex, "targetKeys"])] : []
18289
18313
  };
18290
18314
  }
18315
+ function upsertMarket(authoring, body) {
18316
+ const { market } = parseBody(measurementDraftUpsertMarketRequestSchema, body, "upsert-market");
18317
+ const included = new Set(authoring.targets.filter((target) => target.status === "included").map((target) => target.stableKey));
18318
+ const frozen = new Set(authoring.assignments.flatMap((assignment) => (assignment.executionContexts ?? []).filter((context) => context.executionNodeKey !== void 0).map((context) => measurementV2UsageEdgeKey({ targetKey: assignment.targetKey, queryId: assignment.queryId, executionNodeKey: context.executionNodeKey }))));
18319
+ const seen2 = /* @__PURE__ */ new Set();
18320
+ for (const edge of market.usageEdges) {
18321
+ const key = measurementV2UsageEdgeKey(edge);
18322
+ if (!included.has(edge.targetKey) || !frozen.has(key)) throw validationError("Market members must be existing frozen assignments on included Properties.");
18323
+ if (seen2.has(key)) throw validationError("Market membership contains a duplicate assignment.");
18324
+ seen2.add(key);
18325
+ }
18326
+ const reportingScopes = [...authoring.reportingScopes ?? []];
18327
+ const index2 = reportingScopes.findIndex((scope) => scope.stableKey === market.stableKey);
18328
+ if (index2 === -1) reportingScopes.push(market);
18329
+ else reportingScopes[index2] = market;
18330
+ return { authoring: { ...authoring, reportingScopes }, warnings: [] };
18331
+ }
18291
18332
  function removeGroup(authoring, body) {
18292
18333
  const { groupKey } = parseBody(measurementDraftRemoveGroupRequestSchema, body, "remove-group");
18293
18334
  const children = authoring.groups.filter((group) => group.parentGroupKey === groupKey);
@@ -18352,6 +18393,8 @@ function applyDraftAction(action, authoring, body, context) {
18352
18393
  return classifyAssignments(authoring, body);
18353
18394
  case "upsert-group":
18354
18395
  return upsertGroup(authoring, body);
18396
+ case "upsert-market":
18397
+ return upsertMarket(authoring, body);
18355
18398
  case "remove-group":
18356
18399
  return removeGroup(authoring, body);
18357
18400
  case "upsert-competitor":
@@ -20220,9 +20263,9 @@ async function measurementDraftRoutes(app, opts = {}) {
20220
20263
  if (missing.length) {
20221
20264
  throw validationError(`Binding ${index2} does not supply ${missing.map((name) => `"${name}"`).join(", ")}.`);
20222
20265
  }
20223
- let text2 = template.pattern;
20224
- for (const variable of template.variables) text2 = text2.split(`{${variable}}`).join(binding[variable]);
20225
- const trimmed = text2.trim();
20266
+ const trimmed = expandQueryTemplate(template.pattern, Object.fromEntries(
20267
+ template.variables.map((variable) => [variable, binding[variable]])
20268
+ ));
20226
20269
  if (!trimmed) throw validationError(`Binding ${index2} expands to an empty question.`);
20227
20270
  if (!expansions.includes(trimmed)) expansions.push(trimmed);
20228
20271
  });
@@ -21372,16 +21415,16 @@ function filteredOverviewWithDb(db, active, plan, run, filters, targetKeys) {
21372
21415
  const location = filters.location === void 0 ? void 0 : normalizeMeasurementLocation(filters.location);
21373
21416
  const expectedSlots = materialized.input.expectedSlots.filter((slot) => (provider === void 0 || normalizeText2(slot.provider) === provider) && (location === void 0 || normalizeMeasurementLocation(slot.location) === location));
21374
21417
  const usageEdges = materialized.input.usageEdges.filter((edge) => filters.queryClass === "all" || materialized.edgeQueryClass.get(edge.id) === filters.queryClass);
21375
- return {
21376
- materialized,
21418
+ const evaluator = createMeasurementOverviewEvaluator({
21419
+ ...materialized.input,
21377
21420
  expectedSlots,
21378
21421
  usageEdges,
21379
- overview: buildMeasurementOverview({
21380
- ...materialized.input,
21381
- expectedSlots,
21382
- usageEdges,
21383
- scopeTargetIds: [...targetKeys]
21384
- })
21422
+ scopeTargetIds: [...targetKeys]
21423
+ });
21424
+ return {
21425
+ materialized,
21426
+ evaluator,
21427
+ overview: evaluator.evaluate(targetKeys)
21385
21428
  };
21386
21429
  }
21387
21430
  function targetAnswers(plan, target, materialized, filters) {
@@ -21501,16 +21544,11 @@ function targetKeysForRun(plan, run, requested, requireEntireScope) {
21501
21544
  }
21502
21545
  return selected;
21503
21546
  }
21504
- function marketRollup(plan, run, materialized, expectedSlots, usageEdges, scopedToOneGroup) {
21547
+ function marketRollup(plan, run, evaluator, scopedToOneGroup) {
21505
21548
  if (scopedToOneGroup) return [];
21506
21549
  return plan.groups.map((group) => {
21507
21550
  const targetKeys = targetKeysForRun(plan, run, group.targetKeys, false);
21508
- const overview = buildMeasurementOverview({
21509
- ...materialized.input,
21510
- expectedSlots,
21511
- usageEdges,
21512
- scopeTargetIds: targetKeys
21513
- });
21551
+ const overview = evaluator.evaluate(targetKeys);
21514
21552
  return {
21515
21553
  groupKey: group.stableKey,
21516
21554
  label: group.label,
@@ -21582,22 +21620,28 @@ function portfolioResponse(db, active, plan, query) {
21582
21620
  truncated: rows2.length > limit2
21583
21621
  });
21584
21622
  }
21585
- const { materialized, overview, expectedSlots: filteredSlots, usageEdges: filteredEdges } = filteredOverviewWithDb(db, active, plan, run, filters, targetKeys);
21623
+ const { materialized, overview, evaluator } = filteredOverviewWithDb(db, active, plan, run, filters, targetKeys);
21586
21624
  const measured = new Map(overview.properties.map((row) => [row.targetId, row]));
21587
- const rows = targets.map((target) => {
21625
+ const ranked = targets.map((target) => {
21588
21626
  const row = measured.get(target.stableKey);
21589
- const recommendations = recommendationRows(target, targetAnswers(plan, target, materialized, filters));
21590
21627
  return {
21628
+ target,
21591
21629
  ...propertyDto2(target),
21592
21630
  mentionCoverage: row ? coverageMetric2(row.mentionCoverage) : unavailable3("no_population"),
21593
21631
  citationCoverage: row ? coverageMetric2(row.citationCoverage) : unavailable3("no_population"),
21594
- flags: row?.flags ?? 0,
21632
+ flags: row?.flags ?? 0
21633
+ };
21634
+ }).sort(compareWeakest);
21635
+ const limit = query.limit ?? DEFAULT_LIMIT3;
21636
+ const rows = ranked.slice(0, limit).map(({ target, ...row }) => {
21637
+ const recommendations = recommendationRows(target, targetAnswers(plan, target, materialized, filters));
21638
+ return {
21639
+ ...row,
21595
21640
  recommendedInstead: recommendations.slice(0, 5).map(({ name, occurrences }) => ({ name, occurrences })),
21596
21641
  recommendedInsteadTotal: recommendations.length,
21597
21642
  recommendedInsteadTruncated: recommendations.length > 5
21598
21643
  };
21599
- }).sort(compareWeakest);
21600
- const limit = query.limit ?? DEFAULT_LIMIT3;
21644
+ });
21601
21645
  return measurementPortfolioSummaryResponseSchema.parse({
21602
21646
  portfolio: {
21603
21647
  groupKey: group?.stableKey ?? null,
@@ -21612,9 +21656,9 @@ function portfolioResponse(db, active, plan, query) {
21612
21656
  citationCoverage: coverageMetric2(overview.citationCoverage)
21613
21657
  },
21614
21658
  weakestProperties: rows.slice(0, limit),
21615
- markets: marketRollup(plan, run, materialized, filteredSlots, filteredEdges, group !== void 0),
21616
- totalProperties: rows.length,
21617
- truncated: rows.length > limit
21659
+ markets: marketRollup(plan, run, evaluator, group !== void 0),
21660
+ totalProperties: ranked.length,
21661
+ truncated: ranked.length > limit
21618
21662
  });
21619
21663
  }
21620
21664
  function propertyCompetitorsResponse(db, active, plan, query) {
@@ -23842,9 +23886,9 @@ function templateRecords(db, state, addition, audience) {
23842
23886
  const needsMarket = template.variables.includes("market");
23843
23887
  const needsProperty = template.variables.includes("property");
23844
23888
  const expand = (bindings, scopedAudience) => {
23845
- let queryText = template.pattern;
23846
- for (const variable of template.variables) queryText = queryText.split(`{${variable}}`).join(bindings[variable] ?? "");
23847
- queryText = queryText.trim();
23889
+ const queryText = expandQueryTemplate(template.pattern, Object.fromEntries(
23890
+ template.variables.map((variable) => [variable, bindings[variable] ?? ""])
23891
+ ));
23848
23892
  if (!queryText) throw validationError("Template expansion produced an empty query.");
23849
23893
  return {
23850
23894
  queryText,
@@ -34582,6 +34626,7 @@ var SCHEMA_TABLE = {
34582
34626
  MeasurementDraftTargetPage: measurementDraftTargetPageSchema,
34583
34627
  MeasurementDraftUpsertCompetitorRequest: measurementDraftUpsertCompetitorRequestSchema,
34584
34628
  MeasurementDraftUpsertGroupRequest: measurementDraftUpsertGroupRequestSchema,
34629
+ MeasurementDraftUpsertMarketRequest: measurementDraftUpsertMarketRequestSchema,
34585
34630
  MeasurementDraftUpsertTargetRequest: measurementDraftUpsertTargetRequestSchema,
34586
34631
  MeasurementOverviewResponse: measurementOverviewResponseSchema,
34587
34632
  MeasurementPortfolioSummaryResponse: measurementPortfolioSummaryResponseSchema,
@@ -35707,6 +35752,12 @@ var routeCatalog = [
35707
35752
  description: "Reporting membership only. When `competitors` is present it replaces the complete competitor list atomically; omission preserves the existing list for backward compatibility. A payload carrying queries, providers, locations or models is rejected.",
35708
35753
  request: "MeasurementDraftUpsertGroupRequest"
35709
35754
  }),
35755
+ measurementDraftAction({
35756
+ action: "upsert-market",
35757
+ summary: "Configure an explicit market name and frozen membership",
35758
+ description: "Adds or updates a reporting market using existing frozen assignment edges. It never infers geography from groups or creates query executions. Requires draft review and publication before Research can select it.",
35759
+ request: "MeasurementDraftUpsertMarketRequest"
35760
+ }),
35710
35761
  measurementDraftAction({
35711
35762
  action: "remove-group",
35712
35763
  summary: "Remove a draft group",
@@ -36173,7 +36224,7 @@ var routeCatalog = [
36173
36224
  method: "post",
36174
36225
  path: "/api/v1/projects/{name}/research/runs",
36175
36226
  summary: "Start an isolated research query batch",
36176
- description: "Runs one to fifty ad-hoc queries through one API provider and saves the answer evidence. Research never creates tracked queries, shared runs, snapshots, insights, or notifications. Administrators are always allowed; signed-in viewers require the deployment opt-in and a per-project daily allowance. Read-only and narrowly scoped API keys are refused.",
36227
+ description: "Runs one to fifty ad-hoc queries through one API provider and saves the answer evidence. An optional published Advanced Measurement market or property scope and optional template provenance are frozen with the batch. The provider receives the exact submitted editor question; scope never infers geographic provider location. Research never creates tracked queries, shared runs, snapshots, insights, or notifications. Administrators are always allowed; signed-in viewers require the deployment opt-in and a per-project daily allowance. Read-only and narrowly scoped API keys are refused.",
36177
36228
  tags: ["research"],
36178
36229
  parameters: [nameParameter],
36179
36230
  requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ResearchRunCreate" } } } },
@@ -83142,14 +83193,19 @@ async function researchRoutes(app, opts) {
83142
83193
  adapter.modelValidationPattern.lastIndex = 0;
83143
83194
  if (!adapter.modelConfigurable || !adapter.modelValidationPattern.test(input.model)) throw validationError(`Invalid model "${input.model}" for provider "${providerName}".`, { provider: providerName, model: input.model, hint: adapter.modelValidationHint });
83144
83195
  }
83145
- const location = input.location === void 0 ? project.defaultLocation ? project.locations.find((item2) => item2.label === project.defaultLocation) ?? null : null : input.location;
83196
+ const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and55(eq65(researchRuns.projectId, project.id), eq65(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
83197
+ const active = activeMeasurementPlan(app.db, project.id);
83198
+ const scope = input.scope ? existingReceipt ? resolveStoredResearchScope(existingReceipt.scope, input.scope, input.idempotencyKey) : resolveResearchScope(active, input.scope) : null;
83199
+ const template = input.template ? existingReceipt ? resolveStoredResearchTemplate(existingReceipt.template, input.template, input.idempotencyKey) : resolveResearchTemplate(app, project.id, input.template, scope) : null;
83200
+ const location = input.location === void 0 ? scope ? null : project.defaultLocation ? project.locations.find((item2) => item2.label === project.defaultLocation) ?? null : null : input.location;
83146
83201
  if (location && !project.locations.some((item2) => sameLocation(item2, location))) throw validationError("Research location must exactly match a configured project location.", { location });
83147
83202
  const requestedModel2 = input.model ?? null;
83148
83203
  const resolvedModel = requestedModel2 ?? (project.providerModels[providerName] || opts.getEffectiveProviderModels?.()[providerName] || adapter.defaultModel);
83149
83204
  adapter.modelValidationPattern.lastIndex = 0;
83150
- if (!adapter.modelValidationPattern.test(resolvedModel)) throw validationError(`Invalid resolved model "${resolvedModel}" for provider "${providerName}".`, { provider: providerName, model: resolvedModel, hint: adapter.modelValidationHint });
83151
- if (new Set(input.queries.map((query) => query.toLocaleLowerCase())).size !== input.queries.length) throw validationError("Research queries must be unique within a batch.");
83152
- const normalized = { queries: input.queries, provider: providerName, model: requestedModel2, location: location ?? null };
83205
+ if (!adapter.modelValidationPattern.test(resolvedModel)) throw validationError('Invalid resolved model "' + resolvedModel + '" for provider "' + providerName + '".', { provider: providerName, model: resolvedModel, hint: adapter.modelValidationHint });
83206
+ if (deduplicateResearchQueries(input.queries).length !== input.queries.length) throw validationError("Research queries must be unique within a batch.");
83207
+ const queryClasses = classifyResearchQueries(project, active?.plan ?? null, input.queries);
83208
+ const normalized = { queries: input.queries, provider: providerName, model: requestedModel2, location: location ?? null, ...scope ? { scope } : {}, ...template ? { template } : {} };
83153
83209
  const requestHash2 = crypto49.createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
83154
83210
  const now = (/* @__PURE__ */ new Date()).toISOString();
83155
83211
  const initiatedBy = researchPrincipal(request);
@@ -83174,8 +83230,8 @@ async function researchRoutes(app, opts) {
83174
83230
  }
83175
83231
  }
83176
83232
  const id = crypto49.randomUUID();
83177
- tx.insert(researchRuns).values({ id, projectId: project.id, status: ResearchRunStatuses.queued, provider: providerName, requestedModel: requestedModel2, resolvedModel, location: location ?? null, totalQueries: input.queries.length, idempotencyKey: input.idempotencyKey ?? null, requestHash: input.idempotencyKey ? requestHash2 : null, initiatedBy, createdAt: now }).run();
83178
- for (const [position, query] of input.queries.entries()) tx.insert(researchRunQueries).values({ id: crypto49.randomUUID(), researchRunId: id, position, queryText: query, status: ResearchQueryStatuses.queued, requestedModel: requestedModel2, resolvedModel, groundingSources: [], citedDomains: [], searchQueries: [], createdAt: now }).run();
83233
+ tx.insert(researchRuns).values({ id, projectId: project.id, status: ResearchRunStatuses.queued, provider: providerName, requestedModel: requestedModel2, resolvedModel, location: location ?? null, totalQueries: input.queries.length, scope, template, idempotencyKey: input.idempotencyKey ?? null, requestHash: input.idempotencyKey ? requestHash2 : null, initiatedBy, createdAt: now }).run();
83234
+ for (const [position, query] of input.queries.entries()) tx.insert(researchRunQueries).values({ id: crypto49.randomUUID(), researchRunId: id, position, queryText: query, queryClass: queryClasses[position] ?? null, status: ResearchQueryStatuses.queued, requestedModel: requestedModel2, resolvedModel, groundingSources: [], citedDomains: [], searchQueries: [], createdAt: now }).run();
83179
83235
  writeAuditLog(tx, { projectId: project.id, actor: initiatedBy ? `${initiatedBy.kind}:${initiatedBy.id}` : "api", action: "research.created", entityType: "research_run", entityId: id });
83180
83236
  return { reused: false, id, shouldDispatch: true };
83181
83237
  });
@@ -83210,6 +83266,52 @@ async function researchRoutes(app, opts) {
83210
83266
  return getDetail(app, project.id, request.params.runId);
83211
83267
  });
83212
83268
  }
83269
+ function resolveStoredResearchScope(stored, selection, idempotencyKey) {
83270
+ if (!stored || stored.kind !== selection.kind || stored.key !== selection.key || selection.expectedPlanRevision !== void 0 && selection.expectedPlanRevision !== stored.planRevision) {
83271
+ throw alreadyExists("Research idempotency key", idempotencyKey);
83272
+ }
83273
+ return stored;
83274
+ }
83275
+ function resolveResearchScope(active, selection) {
83276
+ if (!active || active.plan.schemaVersion !== 2) {
83277
+ throw validationError("Research scope requires an active Advanced Measurement plan.");
83278
+ }
83279
+ if (selection.expectedPlanRevision !== void 0 && selection.expectedPlanRevision !== active.version.revision) {
83280
+ throw validationError("The selected research scope is stale. Reload the published measurement plan and try again.", {
83281
+ expectedPlanRevision: selection.expectedPlanRevision,
83282
+ activePlanRevision: active.version.revision
83283
+ });
83284
+ }
83285
+ const label = selection.kind === "market" ? active.plan.reportingScopes?.find((scope) => scope.stableKey === selection.key)?.label : active.plan.targets.find((target) => target.stableKey === selection.key)?.label;
83286
+ if (!label) throw validationError("Unknown published research " + selection.kind + ' scope "' + selection.key + '".');
83287
+ return { kind: selection.kind, key: selection.key, label, planRevision: active.version.revision };
83288
+ }
83289
+ function resolveStoredResearchTemplate(stored, selection, idempotencyKey) {
83290
+ if (!stored || stored.templateId !== selection.templateId || stored.templateVersion !== selection.templateVersion) {
83291
+ throw alreadyExists("Research idempotency key", idempotencyKey);
83292
+ }
83293
+ return stored;
83294
+ }
83295
+ function resolveResearchTemplate(app, projectId, selection, scope) {
83296
+ if (!scope) throw validationError("Research templates require a selected market or property scope.");
83297
+ const savedTemplate = app.db.select().from(measurementQueryTemplates).where(and55(
83298
+ eq65(measurementQueryTemplates.projectId, projectId),
83299
+ eq65(measurementQueryTemplates.id, selection.templateId)
83300
+ )).get();
83301
+ if (!savedTemplate) throw validationError("Unknown or stale research template. Reload the selected template and try again.");
83302
+ const templateVersion = savedTemplate.updatedAt;
83303
+ if (templateVersion !== selection.templateVersion) {
83304
+ throw validationError("The selected research template is stale. Reload it and try again.");
83305
+ }
83306
+ const { bindings, output } = expandResearchTemplate(savedTemplate, scope);
83307
+ if (!output || output.length > 4e3) throw validationError("Research template output must be between 1 and 4000 characters.");
83308
+ return { templateId: selection.templateId, templateVersion, template: savedTemplate.pattern, bindings, output };
83309
+ }
83310
+ function classifyResearchQueries(project, plan, queries2) {
83311
+ const propertyNames = plan ? plan.targets.flatMap((target) => [target.label, ...target.aliases]) : [];
83312
+ const classifier = compileQueryClassifier([...effectiveBrandNames(project), ...propertyNames]);
83313
+ return queries2.map((query) => classifier?.classify(query) ?? null);
83314
+ }
83213
83315
  function getDetail(app, projectId, id) {
83214
83316
  const row = app.db.select().from(researchRuns).where(and55(eq65(researchRuns.id, id), eq65(researchRuns.projectId, projectId))).get();
83215
83317
  if (!row) throw notFound("Research run", id);
@@ -83217,10 +83319,10 @@ function getDetail(app, projectId, id) {
83217
83319
  return { ...serializeRun(row), queries: queries2 };
83218
83320
  }
83219
83321
  function serializeRun(row) {
83220
- return { id: row.id, projectId: row.projectId, status: row.status, provider: row.provider, requestedModel: row.requestedModel, resolvedModel: row.resolvedModel, location: row.location ?? null, totalQueries: row.totalQueries, completedQueries: row.completedQueries, failedQueries: row.failedQueries, error: row.error, initiatedBy: row.initiatedBy ?? null, startedAt: row.startedAt, finishedAt: row.finishedAt, createdAt: row.createdAt };
83322
+ return { id: row.id, projectId: row.projectId, status: row.status, provider: row.provider, requestedModel: row.requestedModel, resolvedModel: row.resolvedModel, location: row.location ?? null, scope: row.scope ?? null, template: row.template ?? null, totalQueries: row.totalQueries, completedQueries: row.completedQueries, failedQueries: row.failedQueries, error: row.error, initiatedBy: row.initiatedBy ?? null, startedAt: row.startedAt, finishedAt: row.finishedAt, createdAt: row.createdAt };
83221
83323
  }
83222
83324
  function serializeQuery(row) {
83223
- return { id: row.id, position: row.position, query: row.queryText, status: row.status, requestedModel: row.requestedModel, resolvedModel: row.resolvedModel, servedModel: row.servedModel, answerText: row.answerText, groundingSources: row.groundingSources, citedDomains: row.citedDomains, searchQueries: row.searchQueries, namedCompetitors: row.namedCompetitors, citedCompetitorDomains: row.citedCompetitorDomains, answerMentioned: row.answerMentioned, citationState: row.citationState, error: row.error, startedAt: row.startedAt, finishedAt: row.finishedAt, createdAt: row.createdAt };
83325
+ return { id: row.id, position: row.position, query: row.queryText, queryClass: row.queryClass ?? null, status: row.status, requestedModel: row.requestedModel, resolvedModel: row.resolvedModel, servedModel: row.servedModel, answerText: row.answerText, groundingSources: row.groundingSources, citedDomains: row.citedDomains, searchQueries: row.searchQueries, namedCompetitors: row.namedCompetitors, citedCompetitorDomains: row.citedCompetitorDomains, answerMentioned: row.answerMentioned, citationState: row.citationState, error: row.error, startedAt: row.startedAt, finishedAt: row.finishedAt, createdAt: row.createdAt };
83224
83326
  }
83225
83327
  function researchPrincipal(request) {
83226
83328
  const principal = request.principal;
@@ -6379,6 +6379,11 @@ var visibilityReportResponseSchema = z15.object({
6379
6379
 
6380
6380
  // ../contracts/src/query-tracking.ts
6381
6381
  import { z as z16 } from "zod";
6382
+ function expandQueryTemplate(pattern, bindings) {
6383
+ let output = pattern;
6384
+ for (const [variable, value] of Object.entries(bindings)) output = output.split(`{${variable}}`).join(value);
6385
+ return output.trim();
6386
+ }
6382
6387
  var sha256HexSchema2 = z16.string().regex(/^[a-f0-9]{64}$/);
6383
6388
  var queryTrackingIdSchema = z16.string().trim().min(1).max(256);
6384
6389
  var queryTrackingTextSchema = z16.string().trim().min(1).max(4e3);
@@ -7611,6 +7616,9 @@ var measurementDraftUpsertGroupRequestSchema = z19.object({
7611
7616
  competitors: z19.array(measurementDraftCompetitorSchema).optional()
7612
7617
  }).strict()
7613
7618
  }).strict();
7619
+ var measurementDraftUpsertMarketRequestSchema = z19.object({
7620
+ market: measurementDraftReportingScopeSchema
7621
+ }).strict();
7614
7622
  var measurementDraftRemoveGroupRequestSchema = z19.object({
7615
7623
  groupKey: measurementV2StableKeySchema
7616
7624
  }).strict();
@@ -16217,11 +16225,50 @@ var ResearchRunStatuses = researchRunStatusSchema.enum;
16217
16225
  var researchQueryStatusSchema = z56.enum(["queued", "running", "completed", "failed"]);
16218
16226
  var ResearchQueryStatuses = researchQueryStatusSchema.enum;
16219
16227
  var DEFAULT_VIEWER_RESEARCH_DAILY_RUN_LIMIT = 20;
16228
+ var researchScopeSelectionSchema = z56.object({
16229
+ kind: z56.enum(["market", "property"]),
16230
+ key: measurementV2StableKeySchema,
16231
+ /** Guards a reviewed scope preview from silently using a newer plan revision. */
16232
+ expectedPlanRevision: z56.number().int().positive().optional()
16233
+ }).strict();
16234
+ var researchRunScopeSchema = z56.object({
16235
+ kind: z56.enum(["market", "property"]),
16236
+ key: measurementV2StableKeySchema,
16237
+ label: z56.string().trim().min(1),
16238
+ planRevision: z56.number().int().positive()
16239
+ }).strict();
16240
+ var researchQueryTextSchema = z56.string().min(1).max(4e3).refine((value) => value.trim().length > 0, "Research queries cannot be blank.");
16241
+ function deduplicateResearchQueries(queries) {
16242
+ const seen = /* @__PURE__ */ new Set();
16243
+ return queries.filter((query) => {
16244
+ const key = normalizeQueryText(query);
16245
+ if (!key || seen.has(key)) return false;
16246
+ seen.add(key);
16247
+ return true;
16248
+ });
16249
+ }
16250
+ function researchTemplateBindings(scope) {
16251
+ if (!scope) return {};
16252
+ return scope.kind === "market" ? { market: scope.label, submarket: scope.label } : { property: scope.label, propertyBrand: scope.label };
16253
+ }
16254
+ function expandResearchTemplate(template, scope) {
16255
+ const available = researchTemplateBindings(scope);
16256
+ const unavailable = template.variables.filter((variable) => available[variable] === void 0);
16257
+ if (unavailable.length) throw validationError("The selected research template requires unavailable bindings: " + unavailable.join(", "));
16258
+ const bindings = Object.fromEntries(template.variables.map((variable) => [variable, available[variable]]));
16259
+ return { bindings, output: expandQueryTemplate(template.pattern, bindings) };
16260
+ }
16261
+ var researchTemplateSelectionSchema = z56.object({
16262
+ templateId: z56.string().trim().min(1).max(256),
16263
+ templateVersion: z56.string().trim().min(1).max(256)
16264
+ }).strict();
16220
16265
  var researchRunCreateSchema = z56.object({
16221
- queries: z56.array(z56.string().trim().min(1).max(4e3)).min(1).max(50),
16266
+ queries: z56.array(researchQueryTextSchema).min(1).max(50),
16222
16267
  provider: z56.string().trim().min(1).optional(),
16223
16268
  model: z56.string().trim().min(1).max(200).optional(),
16224
16269
  location: locationContextSchema.nullable().optional(),
16270
+ scope: researchScopeSelectionSchema.optional(),
16271
+ template: researchTemplateSelectionSchema.optional(),
16225
16272
  idempotencyKey: z56.string().trim().min(1).max(128).optional()
16226
16273
  }).superRefine((value, ctx) => {
16227
16274
  if (value.model && !value.provider) ctx.addIssue({ code: z56.ZodIssueCode.custom, path: ["provider"], message: "provider is required when model is supplied" });
@@ -16240,6 +16287,8 @@ var researchRunSummarySchema = z56.object({
16240
16287
  requestedModel: z56.string().nullable(),
16241
16288
  resolvedModel: z56.string(),
16242
16289
  location: locationContextSchema.nullable(),
16290
+ scope: researchRunScopeSchema.nullable().optional(),
16291
+ template: queryTrackingTemplateProvenanceSchema.nullable().optional(),
16243
16292
  totalQueries: z56.number().int(),
16244
16293
  completedQueries: z56.number().int(),
16245
16294
  failedQueries: z56.number().int(),
@@ -16253,6 +16302,7 @@ var researchRunQuerySchema = z56.object({
16253
16302
  id: z56.string(),
16254
16303
  position: z56.number().int(),
16255
16304
  query: z56.string(),
16305
+ queryClass: queryClassSchema.nullable().optional(),
16256
16306
  status: researchQueryStatusSchema,
16257
16307
  requestedModel: z56.string().nullable(),
16258
16308
  resolvedModel: z56.string(),
@@ -17667,6 +17717,7 @@ export {
17667
17717
  visibilityReportRequestSchema,
17668
17718
  visibilityReportQuerySchema,
17669
17719
  visibilityReportResponseSchema,
17720
+ expandQueryTemplate,
17670
17721
  queryTrackingProvenanceSchema,
17671
17722
  queryTrackingPreviewRequestSchema,
17672
17723
  queryTrackingCommitRequestSchema,
@@ -17732,6 +17783,7 @@ export {
17732
17783
  measurementDraftClearAssignmentsRequestSchema,
17733
17784
  measurementDraftClassifyAssignmentsRequestSchema,
17734
17785
  measurementDraftUpsertGroupRequestSchema,
17786
+ measurementDraftUpsertMarketRequestSchema,
17735
17787
  measurementDraftRemoveGroupRequestSchema,
17736
17788
  measurementDraftUpsertCompetitorRequestSchema,
17737
17789
  measurementDraftPinCompetitorRequestSchema,
@@ -18169,6 +18221,8 @@ export {
18169
18221
  ResearchRunStatuses,
18170
18222
  ResearchQueryStatuses,
18171
18223
  DEFAULT_VIEWER_RESEARCH_DAILY_RUN_LIMIT,
18224
+ deduplicateResearchQueries,
18225
+ expandResearchTemplate,
18172
18226
  researchRunCreateSchema,
18173
18227
  researchRunDetailSchema,
18174
18228
  researchRunListSchema,