@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.
- package/README.md +2 -2
- package/assets/assets/{AuditHistoryPanel-jkTli_49.js → AuditHistoryPanel-Dob_85ay.js} +1 -1
- package/assets/assets/{BacklinksPage-UvZsCycW.js → BacklinksPage-BDm4gQsa.js} +1 -1
- package/assets/assets/{HistoryPage-BNhAdEgU.js → HistoryPage-BUGJKNWJ.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-BlAQhKbf.js → MeasurementPropertyPage-H2g4VdcA.js} +1 -1
- package/assets/assets/ProjectPage-d7iOSkrd.js +8 -0
- package/assets/assets/{RunRow-DT_T7pqA.js → RunRow-Cja3vahD.js} +1 -1
- package/assets/assets/{RunsPage-Bl0L0rlS.js → RunsPage-BdVfLW9Z.js} +1 -1
- package/assets/assets/{SettingsPage-Bqn1dpbS.js → SettingsPage-Xw9rdDPd.js} +1 -1
- package/assets/assets/{SiteHealthSection-z2DhR3t6.js → SiteHealthSection-CjioXMWy.js} +3 -3
- package/assets/assets/{TrafficPage-C_5kd1cm.js → TrafficPage-D5ZqJrJm.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-Rrwaz-mV.js → TrafficSourceDetailPage-DB5V8cdH.js} +1 -1
- package/assets/assets/{extract-error-message-CBGBgQDU.js → extract-error-message-lzuarNEg.js} +1 -1
- package/assets/assets/{index-KTeRanN1.css → index-BjjM151x.css} +1 -1
- package/assets/assets/index-YVHujfy1.js +86 -0
- package/assets/assets/{react-sigma_core.esm.min-5Q-_e_E0.js → react-sigma_core.esm.min-Ddu-c40B.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-J533DKYE.js → chunk-2II4ZMMH.js} +2 -2
- package/dist/{chunk-YDYB3H4O.js → chunk-32QBVRMN.js} +34 -3
- package/dist/{chunk-HG6NLN7H.js → chunk-BE366UCM.js} +4 -4
- package/dist/{chunk-QQLMRABM.js → chunk-ECPN4OUR.js} +184 -82
- package/dist/{chunk-CDI3YR57.js → chunk-ROPJUNQN.js} +55 -1
- package/dist/cli.js +47 -19
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-M5TYY4TH.js → intelligence-service-QCKX2W4G.js} +2 -2
- package/dist/mcp.js +3 -3
- package/package.json +8 -8
- package/assets/assets/ProjectPage-C2oKQpyS.js +0 -9
- 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-
|
|
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
|
|
13808
|
+
function createMeasurementOverviewEvaluator(input, options = {}) {
|
|
13793
13809
|
const prepared = prepareReport(input, options);
|
|
13794
13810
|
const indexes = buildMeasurementOverviewIndexes(input, prepared, options.onEvidenceIndexed);
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
|
|
13805
|
-
|
|
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
|
-
|
|
13815
|
-
mentionCoverage: targetMentionRate(
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
)
|
|
13821
|
-
|
|
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
|
-
|
|
13825
|
-
|
|
13826
|
-
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
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
|
-
|
|
20224
|
-
|
|
20225
|
-
|
|
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
|
-
|
|
21376
|
-
materialized,
|
|
21418
|
+
const evaluator = createMeasurementOverviewEvaluator({
|
|
21419
|
+
...materialized.input,
|
|
21377
21420
|
expectedSlots,
|
|
21378
21421
|
usageEdges,
|
|
21379
|
-
|
|
21380
|
-
|
|
21381
|
-
|
|
21382
|
-
|
|
21383
|
-
|
|
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,
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
-
})
|
|
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,
|
|
21616
|
-
totalProperties:
|
|
21617
|
-
truncated:
|
|
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
|
-
|
|
23846
|
-
|
|
23847
|
-
|
|
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
|
|
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(
|
|
83151
|
-
if (
|
|
83152
|
-
const
|
|
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(
|
|
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,
|