@ainyc/canonry 4.116.0 → 4.117.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/README.md +0 -1
- package/assets/agent-workspace/skills/aero/references/reporting.md +10 -0
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +6 -0
- package/assets/assets/{BacklinksPage-Dy6Xc-LT.js → BacklinksPage-Bkafm10m.js} +1 -1
- package/assets/assets/{ChartPrimitives-CHJh5M8x.js → ChartPrimitives-NV_GgqFn.js} +1 -1
- package/assets/assets/{ProjectPage-Bjfa4CI5.js → ProjectPage-0-NnRxys.js} +1 -1
- package/assets/assets/{RunRow-DmvQnWPf.js → RunRow-df0d24S6.js} +1 -1
- package/assets/assets/{RunsPage-BxkxQEKE.js → RunsPage-CU6-4YTM.js} +1 -1
- package/assets/assets/{SettingsPage-DCyqHqDh.js → SettingsPage-BKw6B6cd.js} +1 -1
- package/assets/assets/{TrafficPage-BT_ae5aO.js → TrafficPage-CzpiYHKj.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-Cps-tSHn.js → TrafficSourceDetailPage-CC5nuqM1.js} +1 -1
- package/assets/assets/{arrow-left-D8vdqhx4.js → arrow-left-CbQcW2pL.js} +1 -1
- package/assets/assets/{extract-error-message-BOY5txUA.js → extract-error-message-CvqIZ-le.js} +1 -1
- package/assets/assets/index-LjcWHaNZ.js +210 -0
- package/assets/assets/{trash-2-ap_Fa4zA.js → trash-2-3J2u0woR.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-WEAXJ25Y.js → chunk-FVVKNMZW.js} +353 -19
- package/dist/{chunk-IQPMK36Y.js → chunk-IAJ5TQ4S.js} +155 -3
- package/dist/{chunk-RVH6QPTA.js → chunk-PJATAPZW.js} +37 -1
- package/dist/{chunk-GV665WOU.js → chunk-ZZEA774R.js} +4 -4
- package/dist/cli.js +93 -4
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-HEN5HLUM.js → intelligence-service-J7G4CMI5.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +9 -9
- package/assets/assets/index-DxjY63VC.js +0 -210
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c}from"./index-
|
|
1
|
+
import{c}from"./index-LjcWHaNZ.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
|
package/assets/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
|
|
13
13
|
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
|
14
14
|
<title>Canonry</title>
|
|
15
|
-
<script type="module" crossorigin src="./assets/index-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index-LjcWHaNZ.js"></script>
|
|
16
16
|
<link rel="modulepreload" crossorigin href="./assets/vendor-tanstack-Be8JdV5M.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="./assets/vendor-radix-CQ77EO2y.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="./assets/vendor-recharts-C9EZkgbP.js">
|
|
@@ -173,6 +173,7 @@ import {
|
|
|
173
173
|
gscSitemapListResponseDtoSchema,
|
|
174
174
|
gscUrlInspectionDtoSchema,
|
|
175
175
|
hasLocationLabel,
|
|
176
|
+
hostOf,
|
|
176
177
|
indexingRequestResponseDtoSchema,
|
|
177
178
|
internalError,
|
|
178
179
|
isReadOnlyKey,
|
|
@@ -259,8 +260,10 @@ import {
|
|
|
259
260
|
trafficSyncResponseSchema,
|
|
260
261
|
unsupportedKind,
|
|
261
262
|
validationError,
|
|
263
|
+
visibilityCompareDtoSchema,
|
|
262
264
|
visibilityStateFromAnswerMentioned,
|
|
263
265
|
visibilityStatsDtoSchema,
|
|
266
|
+
wilsonInterval,
|
|
264
267
|
windowCutoff,
|
|
265
268
|
winnabilityClassLabel,
|
|
266
269
|
winnabilityClassSchema,
|
|
@@ -277,7 +280,7 @@ import {
|
|
|
277
280
|
wordpressSchemaDeployResultDtoSchema,
|
|
278
281
|
wordpressSchemaStatusResultDtoSchema,
|
|
279
282
|
wordpressStatusDtoSchema
|
|
280
|
-
} from "./chunk-
|
|
283
|
+
} from "./chunk-IAJ5TQ4S.js";
|
|
281
284
|
|
|
282
285
|
// src/intelligence-service.ts
|
|
283
286
|
import { eq as eq38, desc as desc18, asc as asc6, and as and30, ne as ne5, or as or6, inArray as inArray14, gte as gte7, lte as lte4 } from "drizzle-orm";
|
|
@@ -5647,8 +5650,8 @@ var METRIC_LABELS = {
|
|
|
5647
5650
|
WEBSITE_CLICKS: "Website clicks",
|
|
5648
5651
|
CALL_CLICKS: "Call clicks"
|
|
5649
5652
|
};
|
|
5650
|
-
function metricLabel(
|
|
5651
|
-
return METRIC_LABELS[
|
|
5653
|
+
function metricLabel(metric2) {
|
|
5654
|
+
return METRIC_LABELS[metric2] ?? metric2;
|
|
5652
5655
|
}
|
|
5653
5656
|
function formatAmenityList(amenities) {
|
|
5654
5657
|
if (amenities.length === 1) return amenities[0];
|
|
@@ -5742,12 +5745,12 @@ function analyzeGbp(signals) {
|
|
|
5742
5745
|
}
|
|
5743
5746
|
function pickWorstMetricDrop(loc) {
|
|
5744
5747
|
let worst = null;
|
|
5745
|
-
for (const
|
|
5746
|
-
const delta = loc.metricDeltaPct[
|
|
5747
|
-
const prior = loc.metricPrior7d[
|
|
5748
|
+
for (const metric2 of GBP_HEADLINE_METRICS) {
|
|
5749
|
+
const delta = loc.metricDeltaPct[metric2];
|
|
5750
|
+
const prior = loc.metricPrior7d[metric2] ?? 0;
|
|
5748
5751
|
if (delta == null || delta > -GBP_METRIC_DROP_PCT || prior < GBP_METRIC_MIN_BASELINE) continue;
|
|
5749
5752
|
if (!worst || delta < worst.deltaPct) {
|
|
5750
|
-
worst = { metric, deltaPct: delta, recent: loc.metricRecent7d[
|
|
5753
|
+
worst = { metric: metric2, deltaPct: delta, recent: loc.metricRecent7d[metric2] ?? 0, prior };
|
|
5751
5754
|
}
|
|
5752
5755
|
}
|
|
5753
5756
|
return worst;
|
|
@@ -8541,7 +8544,7 @@ function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt, ment
|
|
|
8541
8544
|
});
|
|
8542
8545
|
if (eligible.length > 0) usable = eligible;
|
|
8543
8546
|
}
|
|
8544
|
-
const
|
|
8547
|
+
const metric2 = computeProviderMetric(usable);
|
|
8545
8548
|
const queryCount = new Set(usable.map((s) => s.queryId)).size;
|
|
8546
8549
|
const byProvider = {};
|
|
8547
8550
|
for (const provider of new Set(usable.map((s) => s.provider))) {
|
|
@@ -8550,12 +8553,12 @@ function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt, ment
|
|
|
8550
8553
|
buckets.push({
|
|
8551
8554
|
startDate: startISO,
|
|
8552
8555
|
endDate: endISO,
|
|
8553
|
-
citationRate:
|
|
8554
|
-
cited:
|
|
8555
|
-
total:
|
|
8556
|
+
citationRate: metric2.citationRate,
|
|
8557
|
+
cited: metric2.cited,
|
|
8558
|
+
total: metric2.total,
|
|
8556
8559
|
queryCount,
|
|
8557
|
-
mentionRate:
|
|
8558
|
-
mentionedCount:
|
|
8560
|
+
mentionRate: metric2.mentionRate,
|
|
8561
|
+
mentionedCount: metric2.mentionedCount,
|
|
8559
8562
|
mentionShare: computeMentionShareBucketMetric(usable, mentionShareCompetitors),
|
|
8560
8563
|
byProvider
|
|
8561
8564
|
});
|
|
@@ -13631,6 +13634,253 @@ function normalizeDomain2(domain) {
|
|
|
13631
13634
|
|
|
13632
13635
|
// ../api-routes/src/visibility-stats.ts
|
|
13633
13636
|
import { and as and14, desc as desc8, eq as eq17, inArray as inArray8 } from "drizzle-orm";
|
|
13637
|
+
|
|
13638
|
+
// ../api-routes/src/visibility-compare.ts
|
|
13639
|
+
var VISIBILITY_COMPARE_MIN_RUNS = 5;
|
|
13640
|
+
function basketPairKey(queryId, provider) {
|
|
13641
|
+
return JSON.stringify([queryId, provider]);
|
|
13642
|
+
}
|
|
13643
|
+
function citedHostMatches(citedHost, competitorHost) {
|
|
13644
|
+
return citedHost === competitorHost || citedHost.endsWith(`.${competitorHost}`);
|
|
13645
|
+
}
|
|
13646
|
+
function restrict(snapshots, attribution, pairs) {
|
|
13647
|
+
const out = [];
|
|
13648
|
+
for (const snap of snapshots) {
|
|
13649
|
+
const resolved = resolveCurrentQuery(attribution, snap);
|
|
13650
|
+
if (!resolved) continue;
|
|
13651
|
+
if (!pairs.has(basketPairKey(resolved.id, snap.provider))) continue;
|
|
13652
|
+
out.push({ ...snap, queryId: resolved.id });
|
|
13653
|
+
}
|
|
13654
|
+
return out;
|
|
13655
|
+
}
|
|
13656
|
+
function observed(snapshots, attribution) {
|
|
13657
|
+
const queryIds = /* @__PURE__ */ new Set();
|
|
13658
|
+
const providers = /* @__PURE__ */ new Set();
|
|
13659
|
+
for (const snap of snapshots) {
|
|
13660
|
+
const resolved = resolveCurrentQuery(attribution, snap);
|
|
13661
|
+
if (!resolved) continue;
|
|
13662
|
+
queryIds.add(resolved.id);
|
|
13663
|
+
providers.add(snap.provider);
|
|
13664
|
+
}
|
|
13665
|
+
return { queryIds, providers };
|
|
13666
|
+
}
|
|
13667
|
+
function observedPairs(snapshots, attribution, queryIds) {
|
|
13668
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
13669
|
+
for (const snap of snapshots) {
|
|
13670
|
+
const resolved = resolveCurrentQuery(attribution, snap);
|
|
13671
|
+
if (!resolved || !queryIds.has(resolved.id)) continue;
|
|
13672
|
+
const pair = { queryId: resolved.id, provider: snap.provider };
|
|
13673
|
+
pairs.set(basketPairKey(pair.queryId, pair.provider), pair);
|
|
13674
|
+
}
|
|
13675
|
+
return pairs;
|
|
13676
|
+
}
|
|
13677
|
+
function period(numerator, denominator) {
|
|
13678
|
+
const ci = wilsonInterval(numerator, denominator);
|
|
13679
|
+
return {
|
|
13680
|
+
point: denominator > 0 ? Math.round(numerator / denominator * 1e4) / 1e4 : null,
|
|
13681
|
+
ciLow: ci ? ci.low : null,
|
|
13682
|
+
ciHigh: ci ? ci.high : null,
|
|
13683
|
+
numerator,
|
|
13684
|
+
denominator
|
|
13685
|
+
};
|
|
13686
|
+
}
|
|
13687
|
+
function ciOverlap(a, b) {
|
|
13688
|
+
if (a.ciLow === null || a.ciHigh === null || b.ciLow === null || b.ciHigh === null) return true;
|
|
13689
|
+
return a.ciLow <= b.ciHigh && b.ciLow <= a.ciHigh;
|
|
13690
|
+
}
|
|
13691
|
+
function metric(key, label, driftRobust, from, to, continuityBlock) {
|
|
13692
|
+
const verdict = continuityBlock ?? (from.denominator === 0 || to.denominator === 0 ? "insufficient-data" : ciOverlap(from, to) ? "within-noise" : "moved");
|
|
13693
|
+
const direction = from.point === null || to.point === null ? null : to.point > from.point ? "up" : to.point < from.point ? "down" : "flat";
|
|
13694
|
+
const rateRatio = from.point === null || from.point === 0 || to.point === null ? null : Math.round(to.point / from.point * 100) / 100;
|
|
13695
|
+
return { key, label, driftRobust, from, to, rateRatio, direction, verdict };
|
|
13696
|
+
}
|
|
13697
|
+
function countPeriod(snaps, competitors2) {
|
|
13698
|
+
let checked = 0;
|
|
13699
|
+
let mentioned = 0;
|
|
13700
|
+
let cited = 0;
|
|
13701
|
+
let competitorCited = 0;
|
|
13702
|
+
const perProvider = /* @__PURE__ */ new Map();
|
|
13703
|
+
const models = /* @__PURE__ */ new Map();
|
|
13704
|
+
const modelUnknownProviders = /* @__PURE__ */ new Set();
|
|
13705
|
+
const mentionedQueries = /* @__PURE__ */ new Set();
|
|
13706
|
+
const competitorHosts = competitors2.map((c) => hostOf(c.domain)).filter((h) => h !== null && h.length > 0);
|
|
13707
|
+
for (const snap of snaps) {
|
|
13708
|
+
const isMentioned = snap.answerMentioned === true;
|
|
13709
|
+
if (snap.answerMentioned === true || snap.answerMentioned === false) checked += 1;
|
|
13710
|
+
if (isMentioned) {
|
|
13711
|
+
mentioned += 1;
|
|
13712
|
+
mentionedQueries.add(snap.queryId);
|
|
13713
|
+
}
|
|
13714
|
+
const isCited = snap.citationState === CitationStates.cited;
|
|
13715
|
+
if (isCited) cited += 1;
|
|
13716
|
+
const pp = perProvider.get(snap.provider) ?? { checked: 0, mentioned: 0, cited: 0 };
|
|
13717
|
+
if (snap.answerMentioned === true || snap.answerMentioned === false) pp.checked += 1;
|
|
13718
|
+
if (isMentioned) pp.mentioned += 1;
|
|
13719
|
+
if (isCited) pp.cited += 1;
|
|
13720
|
+
perProvider.set(snap.provider, pp);
|
|
13721
|
+
if (snap.model) {
|
|
13722
|
+
const set = models.get(snap.provider) ?? /* @__PURE__ */ new Set();
|
|
13723
|
+
set.add(snap.model);
|
|
13724
|
+
models.set(snap.provider, set);
|
|
13725
|
+
} else {
|
|
13726
|
+
modelUnknownProviders.add(snap.provider);
|
|
13727
|
+
}
|
|
13728
|
+
if (competitorHosts.length > 0 && snap.citedDomains.length > 0) {
|
|
13729
|
+
const citedHosts = snap.citedDomains.map((d) => hostOf(d)).filter((h) => h !== null && h.length > 0);
|
|
13730
|
+
for (const compHost of competitorHosts) {
|
|
13731
|
+
if (citedHosts.some((ch) => citedHostMatches(ch, compHost))) competitorCited += 1;
|
|
13732
|
+
}
|
|
13733
|
+
}
|
|
13734
|
+
}
|
|
13735
|
+
const mentionShare = buildMentionShare(
|
|
13736
|
+
snaps.map((s) => ({ projectMentioned: s.answerMentioned === true, answerText: s.answerText })),
|
|
13737
|
+
{ competitors: competitors2 }
|
|
13738
|
+
);
|
|
13739
|
+
return {
|
|
13740
|
+
checked,
|
|
13741
|
+
mentioned,
|
|
13742
|
+
total: snaps.length,
|
|
13743
|
+
cited,
|
|
13744
|
+
projectCited: cited,
|
|
13745
|
+
competitorCited,
|
|
13746
|
+
queriesMentioned: mentionedQueries.size,
|
|
13747
|
+
perProvider,
|
|
13748
|
+
models,
|
|
13749
|
+
modelUnknownProviders,
|
|
13750
|
+
mentionShare,
|
|
13751
|
+
competitors: mentionShare.breakdown.perCompetitor.map((c) => ({ domain: c.domain, mentions: c.mentionSnapshots }))
|
|
13752
|
+
};
|
|
13753
|
+
}
|
|
13754
|
+
function computeVisibilityCompare(input) {
|
|
13755
|
+
const attribution = buildQueryAttribution(input.queries);
|
|
13756
|
+
const fromObs = observed(input.from.snapshots, attribution);
|
|
13757
|
+
const toObs = observed(input.to.snapshots, attribution);
|
|
13758
|
+
const queriesObservedBoth = new Set([...fromObs.queryIds].filter((q) => toObs.queryIds.has(q)));
|
|
13759
|
+
const fromPairs = observedPairs(input.from.snapshots, attribution, queriesObservedBoth);
|
|
13760
|
+
const toPairs = observedPairs(input.to.snapshots, attribution, queriesObservedBoth);
|
|
13761
|
+
const pairsBoth = new Map([...fromPairs].filter(([key]) => toPairs.has(key)));
|
|
13762
|
+
const candidateProviders = new Set([...pairsBoth.values()].map((pair) => pair.provider));
|
|
13763
|
+
const fromCandidateSnaps = restrict(input.from.snapshots, attribution, pairsBoth);
|
|
13764
|
+
const toCandidateSnaps = restrict(input.to.snapshots, attribution, pairsBoth);
|
|
13765
|
+
const fromCandidateCounts = countPeriod(fromCandidateSnaps, input.competitors);
|
|
13766
|
+
const toCandidateCounts = countPeriod(toCandidateSnaps, input.competitors);
|
|
13767
|
+
const continuityProviders = [...candidateProviders].sort((a, b) => a.localeCompare(b)).map((provider) => {
|
|
13768
|
+
const fromModels = [...fromCandidateCounts.models.get(provider) ?? /* @__PURE__ */ new Set()].sort((a, b) => a.localeCompare(b));
|
|
13769
|
+
const toModels = [...toCandidateCounts.models.get(provider) ?? /* @__PURE__ */ new Set()].sort((a, b) => a.localeCompare(b));
|
|
13770
|
+
const fromModelUnknown = fromCandidateCounts.modelUnknownProviders.has(provider) || fromModels.length === 0;
|
|
13771
|
+
const toModelUnknown = toCandidateCounts.modelUnknownProviders.has(provider) || toModels.length === 0;
|
|
13772
|
+
const status = fromModelUnknown || toModelUnknown ? "model-unknown" : fromModels.length === 1 && toModels.length === 1 && fromModels[0] === toModels[0] ? "included" : "model-discontinuous";
|
|
13773
|
+
return { provider, status, fromModels, toModels };
|
|
13774
|
+
});
|
|
13775
|
+
const providersBoth = new Set(
|
|
13776
|
+
continuityProviders.filter((provider) => provider.status === "included").map((provider) => provider.provider)
|
|
13777
|
+
);
|
|
13778
|
+
const continuityStatus = candidateProviders.size === 0 ? "insufficient-data" : providersBoth.size > 0 ? "comparable" : continuityProviders.every((provider) => provider.status === "model-unknown") ? "model-unknown" : "model-discontinuous";
|
|
13779
|
+
const continuityBlock = continuityStatus === "model-discontinuous" || continuityStatus === "model-unknown" ? continuityStatus : null;
|
|
13780
|
+
const comparedPairs = new Map([...pairsBoth].filter(([, pair]) => providersBoth.has(pair.provider)));
|
|
13781
|
+
const queriesBoth = new Set([...comparedPairs.values()].map((pair) => pair.queryId));
|
|
13782
|
+
const excludedProviders = [.../* @__PURE__ */ new Set([...fromObs.providers, ...toObs.providers])].filter((p) => !providersBoth.has(p)).sort((a, b) => a.localeCompare(b));
|
|
13783
|
+
const fromSnaps = restrict(input.from.snapshots, attribution, comparedPairs);
|
|
13784
|
+
const toSnaps = restrict(input.to.snapshots, attribution, comparedPairs);
|
|
13785
|
+
const fromCounts = countPeriod(fromSnaps, input.competitors);
|
|
13786
|
+
const toCounts = countPeriod(toSnaps, input.competitors);
|
|
13787
|
+
const shareCounts = (c) => ({
|
|
13788
|
+
proj: c.mentionShare.breakdown.projectMentionSnapshots,
|
|
13789
|
+
comp: c.mentionShare.breakdown.competitorMentionSnapshots
|
|
13790
|
+
});
|
|
13791
|
+
const fromShare = shareCounts(fromCounts);
|
|
13792
|
+
const toShare = shareCounts(toCounts);
|
|
13793
|
+
const metrics = [
|
|
13794
|
+
metric(
|
|
13795
|
+
"mention-share-of-voice",
|
|
13796
|
+
"Named share of voice",
|
|
13797
|
+
true,
|
|
13798
|
+
period(fromShare.proj, fromShare.proj + fromShare.comp),
|
|
13799
|
+
period(toShare.proj, toShare.proj + toShare.comp),
|
|
13800
|
+
continuityBlock
|
|
13801
|
+
),
|
|
13802
|
+
// Share of voice is undefined without a competitive frame: with zero
|
|
13803
|
+
// configured competitors the denominator degenerates to the project's own
|
|
13804
|
+
// count and the metric would fabricate a 100%. buildMentionShare already
|
|
13805
|
+
// refuses that on the mention side ("reporting 100% would mislead");
|
|
13806
|
+
// degrade the cited side identically to a 0/0 period -> insufficient-data.
|
|
13807
|
+
metric(
|
|
13808
|
+
"cited-share-of-voice",
|
|
13809
|
+
"Cited share of voice",
|
|
13810
|
+
true,
|
|
13811
|
+
input.competitors.length > 0 ? period(fromCounts.projectCited, fromCounts.projectCited + fromCounts.competitorCited) : period(0, 0),
|
|
13812
|
+
input.competitors.length > 0 ? period(toCounts.projectCited, toCounts.projectCited + toCounts.competitorCited) : period(0, 0),
|
|
13813
|
+
continuityBlock
|
|
13814
|
+
),
|
|
13815
|
+
metric(
|
|
13816
|
+
"mention-rate",
|
|
13817
|
+
"Named rate",
|
|
13818
|
+
false,
|
|
13819
|
+
period(fromCounts.mentioned, fromCounts.checked),
|
|
13820
|
+
period(toCounts.mentioned, toCounts.checked),
|
|
13821
|
+
continuityBlock
|
|
13822
|
+
),
|
|
13823
|
+
metric(
|
|
13824
|
+
"cited-rate",
|
|
13825
|
+
"Cited rate",
|
|
13826
|
+
false,
|
|
13827
|
+
period(fromCounts.cited, fromCounts.total),
|
|
13828
|
+
period(toCounts.cited, toCounts.total),
|
|
13829
|
+
continuityBlock
|
|
13830
|
+
)
|
|
13831
|
+
];
|
|
13832
|
+
const modelChanges = [...candidateProviders].sort((a, b) => a.localeCompare(b)).map((provider) => {
|
|
13833
|
+
const fromModels = [...fromCandidateCounts.models.get(provider) ?? /* @__PURE__ */ new Set()].sort((a, b) => a.localeCompare(b));
|
|
13834
|
+
const toModels = [...toCandidateCounts.models.get(provider) ?? /* @__PURE__ */ new Set()].sort((a, b) => a.localeCompare(b));
|
|
13835
|
+
return { provider, fromModels, toModels };
|
|
13836
|
+
}).filter(
|
|
13837
|
+
(c) => c.fromModels.length > 0 && c.toModels.length > 0 && JSON.stringify(c.fromModels) !== JSON.stringify(c.toModels)
|
|
13838
|
+
);
|
|
13839
|
+
const byProvider = [...providersBoth].sort((a, b) => a.localeCompare(b)).map((provider) => ({
|
|
13840
|
+
provider,
|
|
13841
|
+
from: fromCounts.perProvider.get(provider) ?? { checked: 0, mentioned: 0, cited: 0 },
|
|
13842
|
+
to: toCounts.perProvider.get(provider) ?? { checked: 0, mentioned: 0, cited: 0 }
|
|
13843
|
+
}));
|
|
13844
|
+
return {
|
|
13845
|
+
project: input.project,
|
|
13846
|
+
from: {
|
|
13847
|
+
month: input.from.month,
|
|
13848
|
+
since: input.from.since,
|
|
13849
|
+
until: input.from.until,
|
|
13850
|
+
runCount: input.from.runCount,
|
|
13851
|
+
lowRunCount: input.from.runCount < VISIBILITY_COMPARE_MIN_RUNS
|
|
13852
|
+
},
|
|
13853
|
+
to: {
|
|
13854
|
+
month: input.to.month,
|
|
13855
|
+
since: input.to.since,
|
|
13856
|
+
until: input.to.until,
|
|
13857
|
+
runCount: input.to.runCount,
|
|
13858
|
+
lowRunCount: input.to.runCount < VISIBILITY_COMPARE_MIN_RUNS
|
|
13859
|
+
},
|
|
13860
|
+
basket: {
|
|
13861
|
+
queryCount: queriesBoth.size,
|
|
13862
|
+
excludedFromOnly: [...fromObs.queryIds].filter((q) => !queriesBoth.has(q)).length,
|
|
13863
|
+
excludedToOnly: [...toObs.queryIds].filter((q) => !queriesBoth.has(q)).length,
|
|
13864
|
+
providers: [...providersBoth].sort((a, b) => a.localeCompare(b)),
|
|
13865
|
+
excludedProviders
|
|
13866
|
+
},
|
|
13867
|
+
metrics,
|
|
13868
|
+
queriesMentioned: {
|
|
13869
|
+
from: { count: fromCounts.queriesMentioned, of: queriesBoth.size },
|
|
13870
|
+
to: { count: toCounts.queriesMentioned, of: queriesBoth.size }
|
|
13871
|
+
},
|
|
13872
|
+
byProvider,
|
|
13873
|
+
modelChanges,
|
|
13874
|
+
continuity: {
|
|
13875
|
+
status: continuityStatus,
|
|
13876
|
+
comparedProviders: [...providersBoth].sort((a, b) => a.localeCompare(b)),
|
|
13877
|
+
providers: continuityProviders
|
|
13878
|
+
},
|
|
13879
|
+
competitors: { from: fromCounts.competitors, to: toCounts.competitors }
|
|
13880
|
+
};
|
|
13881
|
+
}
|
|
13882
|
+
|
|
13883
|
+
// ../api-routes/src/visibility-stats.ts
|
|
13634
13884
|
function round42(value) {
|
|
13635
13885
|
return Math.round(value * 1e4) / 1e4;
|
|
13636
13886
|
}
|
|
@@ -13849,6 +14099,62 @@ async function visibilityStatsRoutes(app) {
|
|
|
13849
14099
|
};
|
|
13850
14100
|
return reply.send(response);
|
|
13851
14101
|
});
|
|
14102
|
+
app.get("/projects/:name/visibility-compare", async (request, reply) => {
|
|
14103
|
+
const project = resolveProject(app.db, request.params.name);
|
|
14104
|
+
const { from: fromRaw, to: toRaw } = request.query;
|
|
14105
|
+
if (fromRaw === void 0 || fromRaw === "") throw validationError('"from" (YYYY-MM) is required');
|
|
14106
|
+
if (toRaw === void 0 || toRaw === "") throw validationError('"to" (YYYY-MM) is required');
|
|
14107
|
+
let fromBounds;
|
|
14108
|
+
let toBounds;
|
|
14109
|
+
try {
|
|
14110
|
+
fromBounds = calendarMonthBounds(fromRaw);
|
|
14111
|
+
} catch (err) {
|
|
14112
|
+
throw validationError(err instanceof RangeError ? `"from": ${err.message}` : '"from" must be in YYYY-MM format');
|
|
14113
|
+
}
|
|
14114
|
+
try {
|
|
14115
|
+
toBounds = calendarMonthBounds(toRaw);
|
|
14116
|
+
} catch (err) {
|
|
14117
|
+
throw validationError(err instanceof RangeError ? `"to": ${err.message}` : '"to" must be in YYYY-MM format');
|
|
14118
|
+
}
|
|
14119
|
+
if (Date.parse(fromBounds.since) >= Date.parse(toBounds.since)) {
|
|
14120
|
+
throw validationError('"from" must be a month strictly before "to"');
|
|
14121
|
+
}
|
|
14122
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq17(queries.projectId, project.id)).all();
|
|
14123
|
+
const competitorRows = app.db.select({ domain: competitors.domain }).from(competitors).where(eq17(competitors.projectId, project.id)).all();
|
|
14124
|
+
const competitorInputs = competitorRows.map((c) => ({
|
|
14125
|
+
domain: c.domain,
|
|
14126
|
+
brandTokens: [brandLabelFromDomain(c.domain)].filter((t) => t.length >= 3)
|
|
14127
|
+
}));
|
|
14128
|
+
const loadMonth = (bounds) => {
|
|
14129
|
+
const sinceMs = Date.parse(bounds.since);
|
|
14130
|
+
const untilMs = Date.parse(bounds.until);
|
|
14131
|
+
const monthRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and14(eq17(runs.projectId, project.id), eq17(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter(
|
|
14132
|
+
(r) => (r.status === RunStatuses.completed || r.status === RunStatuses.partial) && Date.parse(r.createdAt) >= sinceMs && Date.parse(r.createdAt) <= untilMs
|
|
14133
|
+
);
|
|
14134
|
+
const runIds = monthRuns.map((r) => r.id);
|
|
14135
|
+
const snapshots = runIds.length > 0 && projectQueries.length > 0 ? app.db.select({
|
|
14136
|
+
queryId: querySnapshots.queryId,
|
|
14137
|
+
queryText: querySnapshots.queryText,
|
|
14138
|
+
provider: querySnapshots.provider,
|
|
14139
|
+
model: querySnapshots.model,
|
|
14140
|
+
citationState: querySnapshots.citationState,
|
|
14141
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
14142
|
+
answerText: querySnapshots.answerText,
|
|
14143
|
+
citedDomains: querySnapshots.citedDomains
|
|
14144
|
+
}).from(querySnapshots).where(inArray8(querySnapshots.runId, runIds)).all() : [];
|
|
14145
|
+
return { runCount: monthRuns.length, snapshots };
|
|
14146
|
+
};
|
|
14147
|
+
const fromMonth = loadMonth(fromBounds);
|
|
14148
|
+
const toMonth = loadMonth(toBounds);
|
|
14149
|
+
const dto = computeVisibilityCompare({
|
|
14150
|
+
project: project.name,
|
|
14151
|
+
queries: projectQueries,
|
|
14152
|
+
competitors: competitorInputs,
|
|
14153
|
+
from: { month: fromRaw, since: fromBounds.since, until: fromBounds.until, runCount: fromMonth.runCount, snapshots: fromMonth.snapshots },
|
|
14154
|
+
to: { month: toRaw, since: toBounds.since, until: toBounds.until, runCount: toMonth.runCount, snapshots: toMonth.snapshots }
|
|
14155
|
+
});
|
|
14156
|
+
return reply.send(dto);
|
|
14157
|
+
});
|
|
13852
14158
|
}
|
|
13853
14159
|
|
|
13854
14160
|
// ../api-routes/src/composites.ts
|
|
@@ -14654,6 +14960,7 @@ var SCHEMA_TABLE = {
|
|
|
14654
14960
|
TrafficSourceListResponse: trafficSourceListResponseSchema,
|
|
14655
14961
|
TrafficStatusResponse: trafficStatusResponseSchema,
|
|
14656
14962
|
TrafficSyncResponse: trafficSyncResponseSchema,
|
|
14963
|
+
VisibilityCompareDto: visibilityCompareDtoSchema,
|
|
14657
14964
|
VisibilityStatsDto: visibilityStatsDtoSchema,
|
|
14658
14965
|
WordpressAuditPageDto: wordpressAuditPageDtoSchema,
|
|
14659
14966
|
WordpressBulkMetaResultDto: wordpressBulkMetaResultDtoSchema,
|
|
@@ -14930,6 +15237,20 @@ var monthQueryParameter = {
|
|
|
14930
15237
|
description: `Aggregate a single calendar month (YYYY-MM), expanded to that month's inclusive UTC bounds. Mutually exclusive with "since"/"until"/"lastRuns".`,
|
|
14931
15238
|
schema: stringSchema
|
|
14932
15239
|
};
|
|
15240
|
+
var compareFromQueryParameter = {
|
|
15241
|
+
name: "from",
|
|
15242
|
+
in: "query",
|
|
15243
|
+
required: true,
|
|
15244
|
+
description: 'Earlier calendar month (YYYY-MM) \u2014 the baseline period. Must be strictly before "to".',
|
|
15245
|
+
schema: stringSchema
|
|
15246
|
+
};
|
|
15247
|
+
var compareToQueryParameter = {
|
|
15248
|
+
name: "to",
|
|
15249
|
+
in: "query",
|
|
15250
|
+
required: true,
|
|
15251
|
+
description: 'Later calendar month (YYYY-MM) \u2014 the reporting period compared against "from".',
|
|
15252
|
+
schema: stringSchema
|
|
15253
|
+
};
|
|
14933
15254
|
var shareOfVoiceQueryParameter = {
|
|
14934
15255
|
name: "shareOfVoice",
|
|
14935
15256
|
in: "query",
|
|
@@ -15705,6 +16026,19 @@ var routeCatalog = [
|
|
|
15705
16026
|
404: errorResponse("Project not found.")
|
|
15706
16027
|
}
|
|
15707
16028
|
},
|
|
16029
|
+
{
|
|
16030
|
+
method: "get",
|
|
16031
|
+
path: "/api/v1/projects/{name}/visibility-compare",
|
|
16032
|
+
summary: "Compare AEO visibility month over month",
|
|
16033
|
+
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.",
|
|
16034
|
+
tags: ["analytics"],
|
|
16035
|
+
parameters: [nameParameter, compareFromQueryParameter, compareToQueryParameter],
|
|
16036
|
+
responses: {
|
|
16037
|
+
200: jsonResponse("Month-over-month visibility comparison returned.", "VisibilityCompareDto"),
|
|
16038
|
+
400: errorResponse("Invalid or missing from/to months."),
|
|
16039
|
+
404: errorResponse("Project not found.")
|
|
16040
|
+
}
|
|
16041
|
+
},
|
|
15708
16042
|
{
|
|
15709
16043
|
method: "get",
|
|
15710
16044
|
path: "/api/v1/projects/{name}/snapshots/diff",
|
|
@@ -19516,12 +19850,12 @@ function computeWindowDelta(rows, referenceDate) {
|
|
|
19516
19850
|
}
|
|
19517
19851
|
const deltaPct = {};
|
|
19518
19852
|
const metrics = /* @__PURE__ */ new Set([...Object.keys(recent7d), ...Object.keys(prior7d)]);
|
|
19519
|
-
for (const
|
|
19520
|
-
const recent = recent7d[
|
|
19521
|
-
const prior = prior7d[
|
|
19522
|
-
recent7d[
|
|
19523
|
-
prior7d[
|
|
19524
|
-
deltaPct[
|
|
19853
|
+
for (const metric2 of metrics) {
|
|
19854
|
+
const recent = recent7d[metric2] ?? 0;
|
|
19855
|
+
const prior = prior7d[metric2] ?? 0;
|
|
19856
|
+
recent7d[metric2] = recent;
|
|
19857
|
+
prior7d[metric2] = prior;
|
|
19858
|
+
deltaPct[metric2] = prior === 0 ? null : Math.round((recent - prior) / prior * 100);
|
|
19525
19859
|
}
|
|
19526
19860
|
return { recent7d, prior7d, deltaPct };
|
|
19527
19861
|
}
|