@canonry/canonry 5.20.3 → 5.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +1 -1
- package/assets/agent-workspace/skills/aero/references/reporting.md +9 -4
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +3 -0
- package/assets/assets/{AuditHistoryPanel-BSh3Oezj.js → AuditHistoryPanel-CjxWA3lx.js} +1 -1
- package/assets/assets/{BacklinksPage-DQ59dhCx.js → BacklinksPage-Bdd164Up.js} +1 -1
- package/assets/assets/{HistoryPage-BBvgid3o.js → HistoryPage-BvJmE-kP.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-Dmb18JbT.js → MeasurementPropertyPage-HuP6RMZ-.js} +1 -1
- package/assets/assets/{ProjectPage-CPg0q2Dy.js → ProjectPage-DOvXVbyy.js} +1 -1
- package/assets/assets/{RunRow-BK8sZCID.js → RunRow-Cnb3JOBw.js} +1 -1
- package/assets/assets/{RunsPage-CIcVBTdP.js → RunsPage-Dvz1JYIm.js} +1 -1
- package/assets/assets/{SettingsPage-Bc3aR-gz.js → SettingsPage-DYGvphkg.js} +1 -1
- package/assets/assets/{SiteHealthSection-FF14aIXT.js → SiteHealthSection-CUNiKaod.js} +3 -3
- package/assets/assets/{TrafficPage-C_qoeGDl.js → TrafficPage-Bf4Tq95q.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-B3i_B2MI.js → TrafficSourceDetailPage-CIuLWZgT.js} +1 -1
- package/assets/assets/{extract-error-message-BisUn6nN.js → extract-error-message-DZex1e4H.js} +1 -1
- package/assets/assets/{index-Dht7JNX5.js → index-CeXW-lzj.js} +21 -21
- package/assets/assets/{react-sigma_core.esm.min-tWhS09SS.js → react-sigma_core.esm.min-CxvtwEkv.js} +1 -1
- package/assets/assets/{v2-overview-adapter-CAoeWKQJ.js → v2-overview-adapter-Bh4qIzLp.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-TOSPZ7I5.js → chunk-2LO7FLVD.js} +48 -5
- package/dist/{chunk-7YGOE4R2.js → chunk-FAVYLZQV.js} +341 -96
- package/dist/{chunk-PXVJ3P5G.js → chunk-FRWZWJB3.js} +4 -4
- package/dist/{chunk-KHNPBUUN.js → chunk-HDJHQPW3.js} +2 -2
- package/dist/{chunk-CVUR3O5G.js → chunk-MEAKE5W7.js} +3 -3
- package/dist/{chunk-H6XSHOS5.js → chunk-NFN5GSEJ.js} +7 -7
- package/dist/{chunk-JJPZ4OQ2.js → chunk-WUT3G2GM.js} +5 -3
- package/dist/cli.js +69 -12
- package/dist/{demo-server-JYDAUEP2.js → demo-server-7BVVVXPM.js} +4 -4
- package/dist/index.js +6 -6
- package/dist/{intelligence-service-H4LGPJLZ.js → intelligence-service-3UUKRAZ5.js} +2 -2
- package/dist/mcp.js +4 -4
- package/package.json +12 -12
|
@@ -417,6 +417,7 @@ import {
|
|
|
417
417
|
isTemplateLinkRatio,
|
|
418
418
|
isTrendBaseline,
|
|
419
419
|
isVertexGroundingRedirect,
|
|
420
|
+
isVisibilityCompareClassMetric,
|
|
420
421
|
isoDateDaysBeforeInTimeZone,
|
|
421
422
|
keywordDtoSchema,
|
|
422
423
|
keywordGenerateRequestSchema,
|
|
@@ -771,6 +772,7 @@ import {
|
|
|
771
772
|
userListDtoSchema,
|
|
772
773
|
validationError,
|
|
773
774
|
visibilityCompareDtoSchema,
|
|
775
|
+
visibilityCompareSelectionSchema,
|
|
774
776
|
visibilityReportQuerySchema,
|
|
775
777
|
visibilityReportResponseSchema,
|
|
776
778
|
visibilityStateFromAnswerMentioned,
|
|
@@ -792,7 +794,7 @@ import {
|
|
|
792
794
|
wordpressSchemaDeployResultDtoSchema,
|
|
793
795
|
wordpressSchemaStatusResultDtoSchema,
|
|
794
796
|
wordpressStatusDtoSchema
|
|
795
|
-
} from "./chunk-
|
|
797
|
+
} from "./chunk-2LO7FLVD.js";
|
|
796
798
|
|
|
797
799
|
// src/intelligence-service.ts
|
|
798
800
|
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";
|
|
@@ -9286,9 +9288,11 @@ function buildMentionShare(snapshots, options) {
|
|
|
9286
9288
|
if (text2.length === 0) continue;
|
|
9287
9289
|
tally.snapshotsWithAnswerText++;
|
|
9288
9290
|
if (snap.projectMentioned) tally.projectMentionSnapshots++;
|
|
9291
|
+
if (options.competitors.length === 0) continue;
|
|
9292
|
+
const prepared = prepareBrandMatchText(text2);
|
|
9289
9293
|
for (const competitor of options.competitors) {
|
|
9290
9294
|
const matcher = competitorMatchers.get(competitor.domain);
|
|
9291
|
-
if (matcher && matcherMatchesText(matcher,
|
|
9295
|
+
if (matcher && matcherMatchesText(matcher, prepared)) {
|
|
9292
9296
|
tally.competitorCounts.set(competitor.domain, (tally.competitorCounts.get(competitor.domain) ?? 0) + 1);
|
|
9293
9297
|
}
|
|
9294
9298
|
}
|
|
@@ -16044,6 +16048,17 @@ function modelEvidenceStatesEqual(a, b) {
|
|
|
16044
16048
|
if (a.status !== "mixed" || b.status !== "mixed") return false;
|
|
16045
16049
|
return a.includesUnknown === b.includesUnknown && a.models.length === b.models.length && a.models.every((model, index2) => model === b.models[index2]);
|
|
16046
16050
|
}
|
|
16051
|
+
function compareModelContinuity(from, to) {
|
|
16052
|
+
const fromEvidence = classifyModelEvidence(from);
|
|
16053
|
+
const toEvidence = classifyModelEvidence(to);
|
|
16054
|
+
const ids = (evidence) => evidence.status === "known" ? [evidence.model] : evidence.status === "mixed" ? evidence.models : [];
|
|
16055
|
+
const unknown = (evidence) => evidence.status === "unknown" || evidence.status === "mixed" && evidence.includesUnknown;
|
|
16056
|
+
return {
|
|
16057
|
+
status: unknown(fromEvidence) || unknown(toEvidence) ? "model-unknown" : fromEvidence.status === "known" && toEvidence.status === "known" && fromEvidence.model === toEvidence.model ? "included" : "model-discontinuous",
|
|
16058
|
+
fromModels: ids(fromEvidence),
|
|
16059
|
+
toModels: ids(toEvidence)
|
|
16060
|
+
};
|
|
16061
|
+
}
|
|
16047
16062
|
|
|
16048
16063
|
// ../api-routes/src/competitor-landscape.ts
|
|
16049
16064
|
var COMPETITOR_LANDSCAPE_RANKED_ROW_LIMIT = 100;
|
|
@@ -24410,6 +24425,50 @@ function buildVisibilityReport(input) {
|
|
|
24410
24425
|
populations
|
|
24411
24426
|
});
|
|
24412
24427
|
}
|
|
24428
|
+
function visibilityComparisonPopulation(run, selection) {
|
|
24429
|
+
const targets = targetMap(run.definition);
|
|
24430
|
+
const byClass = ALL_CLASSES.map((queryClass) => ({ queryClass, candidates: candidatesFor(run, selection, queryClass).candidates }));
|
|
24431
|
+
const pooled = /* @__PURE__ */ new Map();
|
|
24432
|
+
for (const { candidates } of byClass) {
|
|
24433
|
+
for (const candidate of candidates) {
|
|
24434
|
+
const existing = pooled.get(candidate.slot.id);
|
|
24435
|
+
if (existing) existing.edges.push(...candidate.edges);
|
|
24436
|
+
else pooled.set(candidate.slot.id, { ...candidate, edges: [...candidate.edges] });
|
|
24437
|
+
}
|
|
24438
|
+
}
|
|
24439
|
+
const cohortKeys = new Map([...pooled.values()].map((candidate) => [candidate.slot.id, JSON.stringify({
|
|
24440
|
+
definition: run.comparableDefinitionIds.at(-1) ?? run.definitionId,
|
|
24441
|
+
query: candidate.slot.query,
|
|
24442
|
+
execution: candidate.slot.executionId,
|
|
24443
|
+
location: candidate.slot.location,
|
|
24444
|
+
edges: candidate.edges.map((edge) => [edge.targetKey, edge.queryClass, [...edge.marketKeys].sort()]).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))
|
|
24445
|
+
})]));
|
|
24446
|
+
const row = (candidate, queryClass) => {
|
|
24447
|
+
const signals = targetValues(candidate, targets);
|
|
24448
|
+
return {
|
|
24449
|
+
queryId: candidate.slot.queryId ?? candidate.slot.queryKey,
|
|
24450
|
+
queryText: candidate.slot.query,
|
|
24451
|
+
provider: candidate.slot.provider,
|
|
24452
|
+
model: candidate.observation.model,
|
|
24453
|
+
answerMentioned: signals.mention,
|
|
24454
|
+
mentionApplicable: candidate.edges.some((edge) => targets.get(edge.targetKey)?.mentionEligible === true),
|
|
24455
|
+
citation: citationForCoverage(candidate, targets),
|
|
24456
|
+
answerText: candidate.observation.answerText,
|
|
24457
|
+
competitorDomains: [...new Set(candidate.edges.flatMap((edge) => edge.competitorDomains))],
|
|
24458
|
+
competitorMentions: candidate.observation.competitorMentionDomains.filter((domain) => candidate.edges.some((edge) => edge.competitorDomains.includes(domain))),
|
|
24459
|
+
competitorCitations: candidate.observation.competitorCitationDomains.filter((domain) => candidate.edges.some((edge) => edge.competitorDomains.includes(domain))),
|
|
24460
|
+
cohortKey: cohortKeys.get(candidate.slot.id),
|
|
24461
|
+
queryClass: queryClass === "unknown" ? null : queryClass
|
|
24462
|
+
};
|
|
24463
|
+
};
|
|
24464
|
+
return {
|
|
24465
|
+
snapshots: [...pooled.values()].filter((candidate) => candidate.observation !== null).map((candidate) => {
|
|
24466
|
+
const classes = new Set(candidate.edges.map((edge) => edge.queryClass));
|
|
24467
|
+
return row(candidate, classes.size === 1 ? candidate.edges[0].queryClass : null);
|
|
24468
|
+
}),
|
|
24469
|
+
classSnapshots: byClass.flatMap(({ queryClass, candidates }) => candidates.filter((candidate) => candidate.observation !== null).map((candidate) => row(candidate, queryClass)))
|
|
24470
|
+
};
|
|
24471
|
+
}
|
|
24413
24472
|
|
|
24414
24473
|
// ../api-routes/src/visibility-report.ts
|
|
24415
24474
|
function normalizedText3(value) {
|
|
@@ -24926,6 +24985,13 @@ function comparableVersionIds(rows, versionId) {
|
|
|
24926
24985
|
}
|
|
24927
24986
|
return ids;
|
|
24928
24987
|
}
|
|
24988
|
+
function comparableChainHeads(rows, rowsById) {
|
|
24989
|
+
const heads = /* @__PURE__ */ new Map();
|
|
24990
|
+
for (const row of [...rows].sort((left, right) => left.revision - right.revision)) {
|
|
24991
|
+
for (const id of comparableVersionIds(rowsById, row.id)) heads.set(id, row.id);
|
|
24992
|
+
}
|
|
24993
|
+
return heads;
|
|
24994
|
+
}
|
|
24929
24995
|
function assignmentSignature(plan, assignment) {
|
|
24930
24996
|
const node = plan.executionNodes.find((candidate) => candidate.stableKey === assignment.executionNodeKey);
|
|
24931
24997
|
if (!node) throw new Error(`Frozen assignment references missing execution node ${assignment.executionNodeKey}`);
|
|
@@ -25080,6 +25146,37 @@ async function visibilityReportRoutes(app) {
|
|
|
25080
25146
|
async (request) => readVisibilityReport(app.db, resolveProject(app.db, request.params.name), request.query)
|
|
25081
25147
|
);
|
|
25082
25148
|
}
|
|
25149
|
+
function readVisibilityComparisonRuns(db, projectId, since, until) {
|
|
25150
|
+
const sourceRuns = db.select().from(runs).where(and21(
|
|
25151
|
+
eq26(runs.projectId, projectId),
|
|
25152
|
+
eq26(runs.kind, RunKinds["answer-visibility"]),
|
|
25153
|
+
inArray10(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
25154
|
+
notProbeRun(),
|
|
25155
|
+
isNotNull(runs.measurementPlanVersionId),
|
|
25156
|
+
isNull5(runs.measurementScope),
|
|
25157
|
+
gte4(runs.createdAt, since),
|
|
25158
|
+
lte4(runs.createdAt, until)
|
|
25159
|
+
)).all();
|
|
25160
|
+
const versionRows = db.select().from(measurementPlanVersions).where(eq26(measurementPlanVersions.projectId, projectId)).all();
|
|
25161
|
+
const rowsById = new Map(versionRows.map((version) => [version.id, version]));
|
|
25162
|
+
const versions = parseV2Versions(versionRows);
|
|
25163
|
+
const heads = comparableChainHeads(versionRows, rowsById);
|
|
25164
|
+
const byRun = loadVisibilitySnapshots(db, sourceRuns.map((run) => run.id));
|
|
25165
|
+
let unavailable5 = false;
|
|
25166
|
+
const selected = sourceRuns.flatMap((run) => {
|
|
25167
|
+
const source = versions.get(run.measurementPlanVersionId);
|
|
25168
|
+
if (!source) {
|
|
25169
|
+
unavailable5 = true;
|
|
25170
|
+
return [];
|
|
25171
|
+
}
|
|
25172
|
+
const presentation = versions.get(heads.get(source.row.id) ?? source.row.id) ?? source;
|
|
25173
|
+
const snapshots = byRun.get(run.id) ?? [];
|
|
25174
|
+
const models = new Map(snapshots.map((snapshot) => [snapshot.id, snapshot.model]));
|
|
25175
|
+
const materialized = advancedRun(run, presentation.row, presentation.plan, snapshots, comparableVersionIds(rowsById, presentation.row.id));
|
|
25176
|
+
return [{ ...materialized, observations: materialized.observations.map((observation) => ({ ...observation, model: models.get(observation.answerId) ?? null })) }];
|
|
25177
|
+
});
|
|
25178
|
+
return { runs: selected, unavailable: unavailable5 };
|
|
25179
|
+
}
|
|
25083
25180
|
|
|
25084
25181
|
// ../api-routes/src/query-tracking.ts
|
|
25085
25182
|
import crypto21 from "crypto";
|
|
@@ -34820,15 +34917,15 @@ import { and as and35, desc as desc18, eq as eq39, inArray as inArray18 } from "
|
|
|
34820
34917
|
|
|
34821
34918
|
// ../api-routes/src/visibility-compare.ts
|
|
34822
34919
|
var VISIBILITY_COMPARE_MIN_RUNS = 5;
|
|
34823
|
-
function basketPairKey(queryId, provider) {
|
|
34824
|
-
return JSON.stringify([queryId, provider]);
|
|
34920
|
+
function basketPairKey(queryId, provider, cohortKey) {
|
|
34921
|
+
return JSON.stringify([queryId, provider, cohortKey ?? null]);
|
|
34825
34922
|
}
|
|
34826
34923
|
function restrict(snapshots, attribution, pairs) {
|
|
34827
34924
|
const out = [];
|
|
34828
34925
|
for (const snap of snapshots) {
|
|
34829
34926
|
const resolved = resolveCurrentQuery(attribution, snap);
|
|
34830
34927
|
if (!resolved) continue;
|
|
34831
|
-
if (!pairs.has(basketPairKey(resolved.id, snap.provider))) continue;
|
|
34928
|
+
if (!pairs.has(basketPairKey(resolved.id, snap.provider, snap.cohortKey))) continue;
|
|
34832
34929
|
out.push({ ...snap, queryId: resolved.id });
|
|
34833
34930
|
}
|
|
34834
34931
|
return out;
|
|
@@ -34849,8 +34946,8 @@ function observedPairs(snapshots, attribution, queryIds) {
|
|
|
34849
34946
|
for (const snap of snapshots) {
|
|
34850
34947
|
const resolved = resolveCurrentQuery(attribution, snap);
|
|
34851
34948
|
if (!resolved || !queryIds.has(resolved.id)) continue;
|
|
34852
|
-
const pair = { queryId: resolved.id, provider: snap.provider };
|
|
34853
|
-
pairs.set(basketPairKey(pair.queryId, pair.provider), pair);
|
|
34949
|
+
const pair = { queryId: resolved.id, provider: snap.provider, cohortKey: snap.cohortKey };
|
|
34950
|
+
pairs.set(basketPairKey(pair.queryId, pair.provider, pair.cohortKey), pair);
|
|
34854
34951
|
}
|
|
34855
34952
|
return pairs;
|
|
34856
34953
|
}
|
|
@@ -34885,13 +34982,23 @@ function metric(key, label, queryClass, driftRobust, from, to, continuityBlock)
|
|
|
34885
34982
|
const rateRatio = from.point === null || from.point === 0 || to.point === null ? null : Math.round(to.point / from.point * 100) / 100;
|
|
34886
34983
|
return { key, label, queryClass, driftRobust, from, to, rateRatio, direction, verdict };
|
|
34887
34984
|
}
|
|
34888
|
-
function
|
|
34985
|
+
function modelEvidenceOf(snaps) {
|
|
34986
|
+
const evidence = /* @__PURE__ */ new Map();
|
|
34987
|
+
for (const snap of snaps) {
|
|
34988
|
+
const models = evidence.get(snap.provider) ?? [];
|
|
34989
|
+
models.push(snap.model);
|
|
34990
|
+
evidence.set(snap.provider, models);
|
|
34991
|
+
}
|
|
34992
|
+
return evidence;
|
|
34993
|
+
}
|
|
34994
|
+
function countPeriod(snaps, competitors2, queryClassOf, classificationAvailable, projectMatcher) {
|
|
34889
34995
|
let checked = 0;
|
|
34890
34996
|
let mentioned = 0;
|
|
34997
|
+
let total = 0;
|
|
34891
34998
|
let cited = 0;
|
|
34999
|
+
let citationUnknown = 0;
|
|
34892
35000
|
let competitorCited = 0;
|
|
34893
35001
|
const perProvider = /* @__PURE__ */ new Map();
|
|
34894
|
-
const modelEvidence = /* @__PURE__ */ new Map();
|
|
34895
35002
|
const mentionedQueries = /* @__PURE__ */ new Set();
|
|
34896
35003
|
const competitorHosts = competitors2.map((c) => hostOf(c.domain)).filter((h) => h !== null && h.length > 0);
|
|
34897
35004
|
for (const snap of snaps) {
|
|
@@ -34901,43 +35008,59 @@ function countPeriod(snaps, competitors2, queryClassOf, classificationAvailable,
|
|
|
34901
35008
|
mentioned += 1;
|
|
34902
35009
|
mentionedQueries.add(snap.queryId);
|
|
34903
35010
|
}
|
|
34904
|
-
const
|
|
35011
|
+
const citationKnown = snap.citationChecked !== false;
|
|
35012
|
+
if (citationKnown) total += 1;
|
|
35013
|
+
else citationUnknown += 1;
|
|
35014
|
+
const isCited = citationKnown && snap.citationState === CitationStates.cited;
|
|
34905
35015
|
if (isCited) cited += 1;
|
|
34906
35016
|
const pp = perProvider.get(snap.provider) ?? { checked: 0, mentioned: 0, cited: 0 };
|
|
34907
35017
|
if (snap.answerMentioned === true || snap.answerMentioned === false) pp.checked += 1;
|
|
34908
35018
|
if (isMentioned) pp.mentioned += 1;
|
|
34909
35019
|
if (isCited) pp.cited += 1;
|
|
34910
35020
|
perProvider.set(snap.provider, pp);
|
|
34911
|
-
|
|
34912
|
-
|
|
34913
|
-
|
|
34914
|
-
if (competitorHosts.length > 0 && snap.citedDomains.length > 0) {
|
|
35021
|
+
if (!citationKnown) continue;
|
|
35022
|
+
if (snap.competitorCitations !== void 0) competitorCited += snap.competitorCitations.length;
|
|
35023
|
+
else if (competitorHosts.length > 0 && snap.citedDomains.length > 0) {
|
|
34915
35024
|
const citedHosts = snap.citedDomains.map((d) => hostOf(d)).filter((h) => h !== null && h.length > 0);
|
|
34916
35025
|
for (const compHost of competitorHosts) {
|
|
34917
35026
|
if (citedHosts.some((ch) => hostMatchesDomain(ch, compHost))) competitorCited += 1;
|
|
34918
35027
|
}
|
|
34919
35028
|
}
|
|
34920
35029
|
}
|
|
34921
|
-
|
|
35030
|
+
let mentionShare = buildMentionShare(
|
|
34922
35031
|
snaps.map((s) => ({
|
|
34923
35032
|
// Share uses current identity; named-rate counts above deliberately keep
|
|
34924
35033
|
// their historical persisted-boolean semantics.
|
|
34925
|
-
projectMentioned: s.answerText ?
|
|
35034
|
+
projectMentioned: s.answerText ? matcherMatchesText(projectMatcher, s.answerText) : s.answerMentioned === true,
|
|
34926
35035
|
answerText: s.answerText,
|
|
34927
35036
|
queryClass: queryClassOf(s)
|
|
34928
35037
|
})),
|
|
34929
35038
|
{ competitors: competitors2, classificationAvailable }
|
|
34930
35039
|
);
|
|
35040
|
+
if (snaps.some((snapshot) => snapshot.competitorMentions !== void 0)) {
|
|
35041
|
+
const selected = snaps.filter((snapshot) => (!classificationAvailable || queryClassOf(snapshot) === "non-brand") && snapshot.answerMentioned !== null);
|
|
35042
|
+
const perCompetitor = competitors2.map((competitor) => ({
|
|
35043
|
+
domain: competitor.domain,
|
|
35044
|
+
mentionSnapshots: selected.filter((snapshot) => snapshot.competitorMentions?.includes(competitor.domain)).length
|
|
35045
|
+
})).filter((row) => row.mentionSnapshots > 0).sort((a, b) => b.mentionSnapshots - a.mentionSnapshots || (a.domain < b.domain ? -1 : 1));
|
|
35046
|
+
mentionShare = { ...mentionShare, breakdown: {
|
|
35047
|
+
...mentionShare.breakdown,
|
|
35048
|
+
projectMentionSnapshots: selected.filter((snapshot) => snapshot.answerMentioned === true).length,
|
|
35049
|
+
competitorMentionSnapshots: perCompetitor.reduce((total2, competitor) => total2 + competitor.mentionSnapshots, 0),
|
|
35050
|
+
perCompetitor
|
|
35051
|
+
} };
|
|
35052
|
+
}
|
|
34931
35053
|
return {
|
|
34932
35054
|
checked,
|
|
34933
35055
|
mentioned,
|
|
34934
|
-
total
|
|
35056
|
+
total,
|
|
34935
35057
|
cited,
|
|
35058
|
+
citationUnknown,
|
|
35059
|
+
citationEvidence: snaps.some((snapshot) => snapshot.citationChecked !== void 0),
|
|
34936
35060
|
projectCited: cited,
|
|
34937
35061
|
competitorCited,
|
|
34938
35062
|
queriesMentioned: mentionedQueries.size,
|
|
34939
35063
|
perProvider,
|
|
34940
|
-
modelEvidence,
|
|
34941
35064
|
mentionShare,
|
|
34942
35065
|
competitors: mentionShare.breakdown.perCompetitor.map((c) => ({ domain: c.domain, mentions: c.mentionSnapshots }))
|
|
34943
35066
|
};
|
|
@@ -34947,15 +35070,13 @@ function modelIds(evidence) {
|
|
|
34947
35070
|
if (evidence.status === "mixed") return evidence.models;
|
|
34948
35071
|
return [];
|
|
34949
35072
|
}
|
|
34950
|
-
function isUnknownModelEvidence(evidence) {
|
|
34951
|
-
return evidence.status === "unknown" || evidence.status === "mixed" && evidence.includesUnknown;
|
|
34952
|
-
}
|
|
34953
35073
|
function computeVisibilityCompare(input) {
|
|
34954
35074
|
const attribution = buildQueryAttribution(input.queries);
|
|
34955
35075
|
const classifier = compileQueryClassifier(input.brandNames ?? []);
|
|
34956
|
-
const classificationAvailable = classifier !== null;
|
|
35076
|
+
const classificationAvailable = input.frozenClassification === true || classifier !== null;
|
|
34957
35077
|
const queryTextById = new Map(input.queries.map((q) => [q.id, q.query]));
|
|
34958
|
-
const queryClassOf = (snap) => classifier ? classifier.classify(queryTextById.get(snap.queryId) ?? snap.queryText) : null;
|
|
35078
|
+
const queryClassOf = (snap) => input.frozenClassification ? snap.queryClass ?? null : classifier ? classifier.classify(queryTextById.get(snap.queryId) ?? snap.queryText) : null;
|
|
35079
|
+
const projectMatcher = compileBrandAliases(input.brandNames ?? []);
|
|
34959
35080
|
const fromObs = observed(input.from.snapshots, attribution);
|
|
34960
35081
|
const toObs = observed(input.to.snapshots, attribution);
|
|
34961
35082
|
const queriesObservedBoth = new Set([...fromObs.queryIds].filter((q) => toObs.queryIds.has(q)));
|
|
@@ -34963,17 +35084,13 @@ function computeVisibilityCompare(input) {
|
|
|
34963
35084
|
const toPairs = observedPairs(input.to.snapshots, attribution, queriesObservedBoth);
|
|
34964
35085
|
const pairsBoth = new Map([...fromPairs].filter(([key]) => toPairs.has(key)));
|
|
34965
35086
|
const candidateProviders = new Set([...pairsBoth.values()].map((pair) => pair.provider));
|
|
34966
|
-
const
|
|
34967
|
-
const
|
|
34968
|
-
const fromCandidateCounts = countPeriod(fromCandidateSnaps, input.competitors, queryClassOf, classificationAvailable, input.brandNames ?? []);
|
|
34969
|
-
const toCandidateCounts = countPeriod(toCandidateSnaps, input.competitors, queryClassOf, classificationAvailable, input.brandNames ?? []);
|
|
35087
|
+
const fromCandidateModels = modelEvidenceOf(restrict(input.from.snapshots, attribution, pairsBoth));
|
|
35088
|
+
const toCandidateModels = modelEvidenceOf(restrict(input.to.snapshots, attribution, pairsBoth));
|
|
34970
35089
|
const continuityProviders = [...candidateProviders].sort((a, b) => a.localeCompare(b)).map((provider) => {
|
|
34971
|
-
|
|
34972
|
-
|
|
34973
|
-
|
|
34974
|
-
|
|
34975
|
-
const status = isUnknownModelEvidence(fromEvidence) || isUnknownModelEvidence(toEvidence) ? "model-unknown" : fromEvidence.status === "known" && toEvidence.status === "known" && fromEvidence.model === toEvidence.model ? "included" : "model-discontinuous";
|
|
34976
|
-
return { provider, status, fromModels, toModels };
|
|
35090
|
+
return { provider, ...compareModelContinuity(
|
|
35091
|
+
fromCandidateModels.get(provider) ?? [],
|
|
35092
|
+
toCandidateModels.get(provider) ?? []
|
|
35093
|
+
) };
|
|
34977
35094
|
});
|
|
34978
35095
|
const providersBoth = new Set(
|
|
34979
35096
|
continuityProviders.filter((provider) => provider.status === "included").map((provider) => provider.provider)
|
|
@@ -34985,14 +35102,20 @@ function computeVisibilityCompare(input) {
|
|
|
34985
35102
|
const excludedProviders = [.../* @__PURE__ */ new Set([...fromObs.providers, ...toObs.providers])].filter((p) => !providersBoth.has(p)).sort((a, b) => a.localeCompare(b));
|
|
34986
35103
|
const fromSnaps = restrict(input.from.snapshots, attribution, comparedPairs);
|
|
34987
35104
|
const toSnaps = restrict(input.to.snapshots, attribution, comparedPairs);
|
|
34988
|
-
const fromCounts = countPeriod(fromSnaps, input.competitors, queryClassOf, classificationAvailable,
|
|
34989
|
-
const toCounts = countPeriod(toSnaps, input.competitors, queryClassOf, classificationAvailable,
|
|
35105
|
+
const fromCounts = countPeriod(fromSnaps, input.competitors, queryClassOf, classificationAvailable, projectMatcher);
|
|
35106
|
+
const toCounts = countPeriod(toSnaps, input.competitors, queryClassOf, classificationAvailable, projectMatcher);
|
|
35107
|
+
const fromClassSnaps = input.from.classSnapshots === void 0 ? fromSnaps : restrict(input.from.classSnapshots, attribution, comparedPairs);
|
|
35108
|
+
const toClassSnaps = input.to.classSnapshots === void 0 ? toSnaps : restrict(input.to.classSnapshots, attribution, comparedPairs);
|
|
35109
|
+
const noProjectMatcher = compileBrandAliases([]);
|
|
35110
|
+
const fromShareCounts = input.frozenClassification ? countPeriod(fromClassSnaps, input.competitors, queryClassOf, true, noProjectMatcher) : fromCounts;
|
|
35111
|
+
const toShareCounts = input.frozenClassification ? countPeriod(toClassSnaps, input.competitors, queryClassOf, true, noProjectMatcher) : toCounts;
|
|
34990
35112
|
const shareCounts = (c) => ({
|
|
34991
35113
|
proj: c.mentionShare.breakdown.projectMentionSnapshots,
|
|
34992
35114
|
comp: c.mentionShare.breakdown.competitorMentionSnapshots
|
|
34993
35115
|
});
|
|
34994
|
-
const fromShare = shareCounts(
|
|
34995
|
-
const toShare = shareCounts(
|
|
35116
|
+
const fromShare = shareCounts(fromShareCounts);
|
|
35117
|
+
const toShare = shareCounts(toShareCounts);
|
|
35118
|
+
const citationPeriod = (c, value) => c.citationEvidence ? { ...value, excludedUnknown: c.citationUnknown } : value;
|
|
34996
35119
|
const metrics = [
|
|
34997
35120
|
metric(
|
|
34998
35121
|
"mention-share-of-voice",
|
|
@@ -35013,8 +35136,8 @@ function computeVisibilityCompare(input) {
|
|
|
35013
35136
|
"Cited share of voice",
|
|
35014
35137
|
"all",
|
|
35015
35138
|
true,
|
|
35016
|
-
input.competitors.length > 0 ? period(fromCounts.projectCited, fromCounts.projectCited + fromCounts.competitorCited) : unavailableCompetitivePeriod(fromCounts.projectCited),
|
|
35017
|
-
input.competitors.length > 0 ? period(toCounts.projectCited, toCounts.projectCited + toCounts.competitorCited) : unavailableCompetitivePeriod(toCounts.projectCited),
|
|
35139
|
+
citationPeriod(fromCounts, input.competitors.length > 0 ? period(fromCounts.projectCited, fromCounts.projectCited + fromCounts.competitorCited) : unavailableCompetitivePeriod(fromCounts.projectCited)),
|
|
35140
|
+
citationPeriod(toCounts, input.competitors.length > 0 ? period(toCounts.projectCited, toCounts.projectCited + toCounts.competitorCited) : unavailableCompetitivePeriod(toCounts.projectCited)),
|
|
35018
35141
|
continuityBlock
|
|
35019
35142
|
),
|
|
35020
35143
|
metric(
|
|
@@ -35031,14 +35154,34 @@ function computeVisibilityCompare(input) {
|
|
|
35031
35154
|
"Cited rate",
|
|
35032
35155
|
"all",
|
|
35033
35156
|
false,
|
|
35034
|
-
period(fromCounts.cited, fromCounts.total),
|
|
35035
|
-
period(toCounts.cited, toCounts.total),
|
|
35157
|
+
citationPeriod(fromCounts, period(fromCounts.cited, fromCounts.total)),
|
|
35158
|
+
citationPeriod(toCounts, period(toCounts.cited, toCounts.total)),
|
|
35036
35159
|
continuityBlock
|
|
35037
35160
|
)
|
|
35038
35161
|
];
|
|
35162
|
+
const classPeriod = (snapshots, queryClass, signal) => {
|
|
35163
|
+
if (!classificationAvailable) return { ...period(0, 0), availability: "classification-unavailable" };
|
|
35164
|
+
const selected = snapshots.filter((snapshot) => queryClassOf(snapshot) === queryClass && (signal === "cited" || snapshot.mentionApplicable !== false));
|
|
35165
|
+
const checked = selected.filter((snapshot) => signal === "mention" ? typeof snapshot.answerMentioned === "boolean" : snapshot.citationChecked !== false);
|
|
35166
|
+
const numerator = checked.filter((snapshot) => signal === "mention" ? snapshot.answerMentioned === true : snapshot.citationState === CitationStates.cited).length;
|
|
35167
|
+
return { ...period(numerator, checked.length), excludedUnknown: selected.length - checked.length };
|
|
35168
|
+
};
|
|
35169
|
+
for (const queryClass of ["branded", "non-brand"]) {
|
|
35170
|
+
for (const signal of ["mention", "cited"]) {
|
|
35171
|
+
metrics.push(metric(
|
|
35172
|
+
`${signal}-rate-${queryClass}`,
|
|
35173
|
+
signal === "mention" ? "Mention rate" : "Cited rate",
|
|
35174
|
+
queryClass,
|
|
35175
|
+
false,
|
|
35176
|
+
classPeriod(fromClassSnaps, queryClass, signal),
|
|
35177
|
+
classPeriod(toClassSnaps, queryClass, signal),
|
|
35178
|
+
continuityBlock
|
|
35179
|
+
));
|
|
35180
|
+
}
|
|
35181
|
+
}
|
|
35039
35182
|
const modelChanges = [...candidateProviders].sort((a, b) => a.localeCompare(b)).map((provider) => {
|
|
35040
|
-
const fromModels = modelIds(classifyModelEvidence(
|
|
35041
|
-
const toModels = modelIds(classifyModelEvidence(
|
|
35183
|
+
const fromModels = modelIds(classifyModelEvidence(fromCandidateModels.get(provider) ?? []));
|
|
35184
|
+
const toModels = modelIds(classifyModelEvidence(toCandidateModels.get(provider) ?? []));
|
|
35042
35185
|
return { provider, fromModels, toModels };
|
|
35043
35186
|
}).filter(
|
|
35044
35187
|
(c) => c.fromModels.length > 0 && c.toModels.length > 0 && JSON.stringify(c.fromModels) !== JSON.stringify(c.toModels)
|
|
@@ -35083,7 +35226,7 @@ function computeVisibilityCompare(input) {
|
|
|
35083
35226
|
comparedProviders: [...providersBoth].sort((a, b) => a.localeCompare(b)),
|
|
35084
35227
|
providers: continuityProviders
|
|
35085
35228
|
},
|
|
35086
|
-
competitors: { from:
|
|
35229
|
+
competitors: { from: fromShareCounts.competitors, to: toShareCounts.competitors }
|
|
35087
35230
|
};
|
|
35088
35231
|
}
|
|
35089
35232
|
|
|
@@ -35301,59 +35444,150 @@ async function visibilityStatsRoutes(app) {
|
|
|
35301
35444
|
return reply.send(response);
|
|
35302
35445
|
});
|
|
35303
35446
|
app.get("/projects/:name/visibility-compare", async (request, reply) => {
|
|
35304
|
-
const
|
|
35305
|
-
|
|
35306
|
-
|
|
35307
|
-
|
|
35308
|
-
|
|
35309
|
-
|
|
35310
|
-
|
|
35311
|
-
|
|
35312
|
-
|
|
35313
|
-
|
|
35314
|
-
|
|
35315
|
-
|
|
35316
|
-
|
|
35317
|
-
|
|
35318
|
-
|
|
35319
|
-
|
|
35320
|
-
|
|
35321
|
-
|
|
35322
|
-
|
|
35323
|
-
|
|
35324
|
-
|
|
35325
|
-
|
|
35326
|
-
|
|
35327
|
-
|
|
35328
|
-
|
|
35329
|
-
|
|
35330
|
-
|
|
35331
|
-
|
|
35332
|
-
|
|
35333
|
-
|
|
35334
|
-
|
|
35335
|
-
|
|
35336
|
-
|
|
35337
|
-
|
|
35338
|
-
|
|
35339
|
-
|
|
35340
|
-
|
|
35341
|
-
|
|
35342
|
-
|
|
35343
|
-
|
|
35447
|
+
const dto = readVisibilityCompare(app.db, request.params.name, request.query);
|
|
35448
|
+
return reply.send(dto);
|
|
35449
|
+
});
|
|
35450
|
+
}
|
|
35451
|
+
function readVisibilityCompare(db, projectName, query) {
|
|
35452
|
+
const project = resolveProject(db, projectName);
|
|
35453
|
+
const selection = visibilityCompareSelectionSchema.safeParse(query);
|
|
35454
|
+
if (!selection.success) throw validationError("Invalid comparison selection", { issues: selection.error.issues });
|
|
35455
|
+
const filters = selection.data;
|
|
35456
|
+
const scope = filters.scope ?? "project";
|
|
35457
|
+
if (scope !== "project" && filters.scopeKey === void 0) throw validationError("A non-project comparison scope requires scopeKey.");
|
|
35458
|
+
if (scope === "project" && filters.scopeKey !== void 0) throw validationError("scopeKey is not valid for project scope.");
|
|
35459
|
+
if (scope === "market" && filters.marketKey !== void 0) throw validationError("marketKey is not valid for market scope.");
|
|
35460
|
+
const { from: fromRaw, to: toRaw } = query;
|
|
35461
|
+
if (fromRaw === void 0 || fromRaw === "") throw validationError('"from" (YYYY-MM) is required');
|
|
35462
|
+
if (toRaw === void 0 || toRaw === "") throw validationError('"to" (YYYY-MM) is required');
|
|
35463
|
+
let fromBounds;
|
|
35464
|
+
let toBounds;
|
|
35465
|
+
try {
|
|
35466
|
+
fromBounds = calendarMonthBounds(fromRaw);
|
|
35467
|
+
} catch (err) {
|
|
35468
|
+
throw validationError(err instanceof RangeError ? `"from": ${err.message}` : '"from" must be in YYYY-MM format');
|
|
35469
|
+
}
|
|
35470
|
+
try {
|
|
35471
|
+
toBounds = calendarMonthBounds(toRaw);
|
|
35472
|
+
} catch (err) {
|
|
35473
|
+
throw validationError(err instanceof RangeError ? `"to": ${err.message}` : '"to" must be in YYYY-MM format');
|
|
35474
|
+
}
|
|
35475
|
+
if (Date.parse(fromBounds.since) >= Date.parse(toBounds.since)) {
|
|
35476
|
+
throw validationError('"from" must be a month strictly before "to"');
|
|
35477
|
+
}
|
|
35478
|
+
const providerKey2 = filters.provider === void 0 ? void 0 : normalizeText3(filters.provider);
|
|
35479
|
+
const locationKey2 = filters.location === void 0 ? void 0 : normalizeText3(filters.location);
|
|
35480
|
+
const scoped = scope !== "project" || filters.marketKey !== void 0;
|
|
35481
|
+
let advancedComparison;
|
|
35482
|
+
if (activeMeasurementPlan(db, project.id) !== null) {
|
|
35483
|
+
let missingScope;
|
|
35484
|
+
let measuredScope = 0;
|
|
35485
|
+
const loadAdvancedMonth = (bounds) => {
|
|
35486
|
+
const selected = readVisibilityComparisonRuns(db, project.id, bounds.since, bounds.until);
|
|
35487
|
+
const snapshots = [];
|
|
35488
|
+
const classSnapshots = [];
|
|
35489
|
+
let runCount = 0;
|
|
35490
|
+
for (const run of selected.runs) {
|
|
35491
|
+
let population;
|
|
35492
|
+
try {
|
|
35493
|
+
population = visibilityComparisonPopulation(run, {
|
|
35494
|
+
...filters,
|
|
35495
|
+
scope,
|
|
35496
|
+
queryClass: "all",
|
|
35497
|
+
location: locationKey2 === void 0 ? { kind: "all" } : locationKey2 === "none" ? { kind: "none" } : { kind: "exact", value: filters.location },
|
|
35498
|
+
limit: 100
|
|
35499
|
+
});
|
|
35500
|
+
} catch (error) {
|
|
35501
|
+
if (error instanceof VisibilityReportScopeError && error.details !== void 0) {
|
|
35502
|
+
missingScope ??= error;
|
|
35503
|
+
continue;
|
|
35504
|
+
}
|
|
35505
|
+
if (error instanceof VisibilityReportScopeError) throw validationError(error.message, error.details);
|
|
35506
|
+
throw error;
|
|
35507
|
+
}
|
|
35508
|
+
runCount += 1;
|
|
35509
|
+
const adapt = (snapshot) => ({
|
|
35510
|
+
...snapshot,
|
|
35511
|
+
citationState: snapshot.citation === true ? CitationStates.cited : CitationStates["not-cited"],
|
|
35512
|
+
citationChecked: snapshot.citation !== null,
|
|
35513
|
+
citedDomains: []
|
|
35514
|
+
});
|
|
35515
|
+
snapshots.push(...population.snapshots.map(adapt));
|
|
35516
|
+
classSnapshots.push(...population.classSnapshots.map(adapt));
|
|
35517
|
+
}
|
|
35518
|
+
measuredScope += runCount;
|
|
35519
|
+
return { runCount, snapshots, classSnapshots, classificationAvailable: !selected.unavailable };
|
|
35344
35520
|
};
|
|
35345
|
-
const
|
|
35346
|
-
const
|
|
35347
|
-
|
|
35521
|
+
const fromMonth2 = loadAdvancedMonth(fromBounds);
|
|
35522
|
+
const toMonth2 = loadAdvancedMonth(toBounds);
|
|
35523
|
+
if (missingScope !== void 0 && measuredScope === 0) throw validationError(missingScope.message, missingScope.details);
|
|
35524
|
+
const queries2 = [...new Map([...fromMonth2.snapshots, ...toMonth2.snapshots].map((snapshot) => [snapshot.queryId, { id: snapshot.queryId, query: snapshot.queryText }])).values()];
|
|
35525
|
+
advancedComparison = { ...computeVisibilityCompare({
|
|
35348
35526
|
project: project.name,
|
|
35349
|
-
queries:
|
|
35350
|
-
competitors:
|
|
35351
|
-
|
|
35352
|
-
from: { month: fromRaw,
|
|
35353
|
-
to: { month: toRaw,
|
|
35354
|
-
});
|
|
35355
|
-
|
|
35527
|
+
queries: queries2,
|
|
35528
|
+
competitors: [...new Set([...fromMonth2.snapshots, ...toMonth2.snapshots].flatMap((snapshot) => snapshot.competitorDomains ?? []))].map((domain) => ({ domain, brandTokens: [] })),
|
|
35529
|
+
frozenClassification: fromMonth2.classificationAvailable && toMonth2.classificationAvailable,
|
|
35530
|
+
from: { ...fromMonth2, month: fromRaw, ...fromBounds },
|
|
35531
|
+
to: { ...toMonth2, month: toRaw, ...toBounds }
|
|
35532
|
+
}), selection: filters };
|
|
35533
|
+
if (scoped) {
|
|
35534
|
+
const unreadable = [fromMonth2.classificationAvailable ? null : fromRaw, toMonth2.classificationAvailable ? null : toRaw].filter((month) => month !== null);
|
|
35535
|
+
if (unreadable.length > 0) {
|
|
35536
|
+
throw validationError(`Property, group and market comparisons need frozen schema-v2 plan history; ${unreadable.join(" and ")} include runs whose plan cannot be reconstructed.`, { months: unreadable });
|
|
35537
|
+
}
|
|
35538
|
+
return advancedComparison;
|
|
35539
|
+
}
|
|
35540
|
+
}
|
|
35541
|
+
if (advancedComparison === void 0 && scoped) throw validationError("Property, group and market comparison scopes require an Advanced portfolio.");
|
|
35542
|
+
const projectQueries = db.select({ id: queries.id, query: queries.query }).from(queries).where(eq39(queries.projectId, project.id)).all();
|
|
35543
|
+
const competitorRows2 = db.select({ domain: competitors.domain }).from(competitors).where(eq39(competitors.projectId, project.id)).all();
|
|
35544
|
+
const competitorInputs = mentionShareCompetitorsFromDomains(competitorRows2.map((c) => c.domain));
|
|
35545
|
+
const loadMonth = (bounds) => {
|
|
35546
|
+
const sinceMs = Date.parse(bounds.since);
|
|
35547
|
+
const untilMs = Date.parse(bounds.until);
|
|
35548
|
+
const monthRuns = db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and35(eq39(runs.projectId, project.id), eq39(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter(
|
|
35549
|
+
(r) => (r.status === RunStatuses.completed || r.status === RunStatuses.partial) && Date.parse(r.createdAt) >= sinceMs && Date.parse(r.createdAt) <= untilMs
|
|
35550
|
+
);
|
|
35551
|
+
const runIds = monthRuns.map((r) => r.id);
|
|
35552
|
+
const snapshots = runIds.length > 0 && projectQueries.length > 0 ? db.select({
|
|
35553
|
+
queryId: querySnapshots.queryId,
|
|
35554
|
+
queryText: querySnapshots.queryText,
|
|
35555
|
+
provider: querySnapshots.provider,
|
|
35556
|
+
model: querySnapshots.model,
|
|
35557
|
+
citationState: querySnapshots.citationState,
|
|
35558
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
35559
|
+
answerText: querySnapshots.answerText,
|
|
35560
|
+
citedDomains: querySnapshots.citedDomains,
|
|
35561
|
+
location: querySnapshots.location
|
|
35562
|
+
}).from(querySnapshots).where(inArray18(querySnapshots.runId, runIds)).all() : [];
|
|
35563
|
+
return {
|
|
35564
|
+
runCount: monthRuns.length,
|
|
35565
|
+
snapshots: snapshots.filter((snapshot) => (providerKey2 === void 0 || normalizeText3(snapshot.provider) === providerKey2) && (locationKey2 === void 0 || (locationKey2 === "none" ? snapshot.location === null : snapshot.location !== null && normalizeText3(snapshot.location) === locationKey2)))
|
|
35566
|
+
};
|
|
35567
|
+
};
|
|
35568
|
+
const fromMonth = loadMonth(fromBounds);
|
|
35569
|
+
const toMonth = loadMonth(toBounds);
|
|
35570
|
+
const dto = computeVisibilityCompare({
|
|
35571
|
+
project: project.name,
|
|
35572
|
+
queries: projectQueries,
|
|
35573
|
+
competitors: competitorInputs,
|
|
35574
|
+
brandNames: effectiveBrandNames(project),
|
|
35575
|
+
from: { month: fromRaw, since: fromBounds.since, until: fromBounds.until, runCount: fromMonth.runCount, snapshots: fromMonth.snapshots },
|
|
35576
|
+
to: { month: toRaw, since: toBounds.since, until: toBounds.until, runCount: toMonth.runCount, snapshots: toMonth.snapshots }
|
|
35356
35577
|
});
|
|
35578
|
+
if (advancedComparison !== void 0) {
|
|
35579
|
+
const { from, to, basket, continuity: continuity2, modelChanges } = advancedComparison;
|
|
35580
|
+
return {
|
|
35581
|
+
...dto,
|
|
35582
|
+
selection: filters,
|
|
35583
|
+
metrics: [
|
|
35584
|
+
...dto.metrics.filter((metric2) => !isVisibilityCompareClassMetric(metric2.key)),
|
|
35585
|
+
...advancedComparison.metrics.filter((metric2) => isVisibilityCompareClassMetric(metric2.key))
|
|
35586
|
+
],
|
|
35587
|
+
classComparison: { from, to, basket, continuity: continuity2, modelChanges }
|
|
35588
|
+
};
|
|
35589
|
+
}
|
|
35590
|
+
return { ...dto, selection: filters };
|
|
35357
35591
|
}
|
|
35358
35592
|
|
|
35359
35593
|
// ../api-routes/src/results-export.ts
|
|
@@ -39050,9 +39284,18 @@ var routeCatalog = [
|
|
|
39050
39284
|
method: "get",
|
|
39051
39285
|
path: "/api/v1/projects/{name}/visibility-compare",
|
|
39052
39286
|
summary: "Compare AEO visibility month over month",
|
|
39053
|
-
description: "Statistically honest month-over-month AEO comparison in one call. PRIMARY metric is share of voice (brand vs competitor mentions in the same answers), which is less exposed to broad model-wide naming propensity than an absolute rate but never bypasses model continuity. Rates are pooled per-snapshot over each month (invariant to sweep count), restricted to query/provider pairs present in BOTH months, and then restricted again to providers with exactly one known, identical configured model id in both months. `continuity` reports every provider and its model evidence; changed, mixed mid-month, or legacy-unknown models are excluded. When no provider remains, metrics return a continuity-blocked verdict rather than a directional call. `from` must be a month strictly before `to`. A silent upstream version bump under an unchanged configured id remains undetectable.",
|
|
39287
|
+
description: "Statistically honest month-over-month AEO comparison in one call. PRIMARY metric is share of voice (brand vs competitor mentions in the same answers), which is less exposed to broad model-wide naming propensity than an absolute rate but never bypasses model continuity. Rates are pooled per-snapshot over each month (invariant to sweep count), restricted to query/provider pairs present in BOTH months, and then restricted again to providers with exactly one known, identical configured model id in both months. `continuity` reports every provider and its model evidence; changed, mixed mid-month, or legacy-unknown models are excluded. When no provider remains, metrics return a continuity-blocked verdict rather than a directional call. `from` must be a month strictly before `to`. Adds separate branded/non-brand mention and cited rates with classification-unavailable rather than pooled fallback. Advanced class metrics read frozen Property/market/execution assignments and reuse each answer once per class. Without a scope or marketKey the four original metrics keep the project frame (provider/location narrow it the same way for Simple and Advanced) and Advanced class metrics carry their own `classComparison` cohort. A scope or marketKey answers entirely from the frozen frame and is refused when either month holds runs whose plan cannot be reconstructed (schema v1). On frozen frames, answers with incomplete source capture leave every citation figure and are counted in `excludedUnknown`. A silent upstream version bump under an unchanged configured id remains undetectable.",
|
|
39054
39288
|
tags: ["analytics"],
|
|
39055
|
-
parameters: [
|
|
39289
|
+
parameters: [
|
|
39290
|
+
nameParameter,
|
|
39291
|
+
compareFromQueryParameter,
|
|
39292
|
+
compareToQueryParameter,
|
|
39293
|
+
{ name: "scope", in: "query", description: "Advanced frozen population scope; default project.", schema: { type: "string", enum: ["project", "group", "market", "property"] } },
|
|
39294
|
+
{ name: "scopeKey", in: "query", description: "Stable Property Target, group, or market key; required for non-project scope.", schema: stringSchema },
|
|
39295
|
+
{ name: "marketKey", in: "query", description: "Intersect project/group/property selection with exact frozen market edges.", schema: stringSchema },
|
|
39296
|
+
{ name: "provider", in: "query", description: "Restrict both frames to one provider (case-insensitive).", schema: stringSchema },
|
|
39297
|
+
{ name: "location", in: "query", description: "Execution location label (case-insensitive), or none for no location.", schema: stringSchema }
|
|
39298
|
+
],
|
|
39056
39299
|
responses: {
|
|
39057
39300
|
200: jsonResponse("Month-over-month visibility comparison returned.", "VisibilityCompareDto"),
|
|
39058
39301
|
400: errorResponse("Invalid or missing from/to months."),
|
|
@@ -48037,6 +48280,8 @@ async function googleRoutes(app, opts) {
|
|
|
48037
48280
|
tx.delete(gbpKeywordMonthly).where(eq46(gbpKeywordMonthly.projectId, projectId)).run();
|
|
48038
48281
|
tx.delete(gbpPlaceActions).where(eq46(gbpPlaceActions.projectId, projectId)).run();
|
|
48039
48282
|
tx.delete(gbpLodgingSnapshots).where(eq46(gbpLodgingSnapshots.projectId, projectId)).run();
|
|
48283
|
+
tx.delete(gbpAttributesSnapshots).where(eq46(gbpAttributesSnapshots.projectId, projectId)).run();
|
|
48284
|
+
tx.delete(gbpPlaceDetails).where(eq46(gbpPlaceDetails.projectId, projectId)).run();
|
|
48040
48285
|
tx.delete(gbpLocations).where(eq46(gbpLocations.projectId, projectId)).run();
|
|
48041
48286
|
}
|
|
48042
48287
|
function currentProjectAccount(projectId) {
|