@ainyc/canonry 4.36.0 → 4.40.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/assets/{index-CAmKaZIt.js → index-BXkHB0ox.js} +110 -110
- package/assets/assets/index-DkoFqd1J.css +1 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-JQQXMCQ7.js → chunk-EWYUJ2DG.js} +202 -11
- package/dist/{chunk-F2G67CIU.js → chunk-H6COOYA6.js} +329 -206
- package/dist/{chunk-O7S623DL.js → chunk-JS6KRBBZ.js} +18 -0
- package/dist/cli.js +68 -12
- package/dist/index.js +3 -3
- package/dist/{intelligence-service-7AWRUNI2.js → intelligence-service-ZZP3TVZZ.js} +1 -1
- package/dist/mcp.js +1 -1
- package/package.json +5 -5
- package/assets/assets/index-CTrHzgs-.css +0 -1
|
@@ -3256,24 +3256,51 @@ function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains, topDo
|
|
|
3256
3256
|
}
|
|
3257
3257
|
|
|
3258
3258
|
// ../intelligence/src/movement-summary.ts
|
|
3259
|
-
function buildMovementSummary(currentSnapshots, previousSnapshots) {
|
|
3259
|
+
function buildMovementSummary(currentSnapshots, previousSnapshots, options = {}) {
|
|
3260
3260
|
if (previousSnapshots.length === 0) {
|
|
3261
|
-
const
|
|
3261
|
+
const citedIds = collectCitedQueryIds(currentSnapshots);
|
|
3262
|
+
const citedCount = citedIds.size;
|
|
3262
3263
|
const tone2 = citedCount > 0 ? "positive" : "neutral";
|
|
3263
|
-
return
|
|
3264
|
+
return withQueryLists(
|
|
3265
|
+
{ gained: citedCount, lost: 0, tone: tone2, hasPreviousRun: false },
|
|
3266
|
+
citedIds,
|
|
3267
|
+
/* @__PURE__ */ new Set(),
|
|
3268
|
+
options.queryLookup
|
|
3269
|
+
);
|
|
3264
3270
|
}
|
|
3265
3271
|
const latestCited = collectCitedQueryIds(currentSnapshots);
|
|
3266
3272
|
const previousCited = collectCitedQueryIds(previousSnapshots);
|
|
3267
|
-
|
|
3268
|
-
|
|
3273
|
+
const gainedIds = /* @__PURE__ */ new Set();
|
|
3274
|
+
const lostIds = /* @__PURE__ */ new Set();
|
|
3269
3275
|
for (const id of latestCited) {
|
|
3270
|
-
if (!previousCited.has(id))
|
|
3276
|
+
if (!previousCited.has(id)) gainedIds.add(id);
|
|
3271
3277
|
}
|
|
3272
3278
|
for (const id of previousCited) {
|
|
3273
|
-
if (!latestCited.has(id))
|
|
3279
|
+
if (!latestCited.has(id)) lostIds.add(id);
|
|
3274
3280
|
}
|
|
3275
|
-
const tone =
|
|
3276
|
-
return
|
|
3281
|
+
const tone = lostIds.size > gainedIds.size ? "negative" : gainedIds.size > lostIds.size ? "positive" : "neutral";
|
|
3282
|
+
return withQueryLists(
|
|
3283
|
+
{ gained: gainedIds.size, lost: lostIds.size, tone, hasPreviousRun: true },
|
|
3284
|
+
gainedIds,
|
|
3285
|
+
lostIds,
|
|
3286
|
+
options.queryLookup
|
|
3287
|
+
);
|
|
3288
|
+
}
|
|
3289
|
+
function withQueryLists(base, gainedIds, lostIds, lookup) {
|
|
3290
|
+
if (!lookup) return base;
|
|
3291
|
+
return {
|
|
3292
|
+
...base,
|
|
3293
|
+
gainedQueries: resolveQueryTexts(gainedIds, lookup),
|
|
3294
|
+
lostQueries: resolveQueryTexts(lostIds, lookup)
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
function resolveQueryTexts(ids, lookup) {
|
|
3298
|
+
const out = [];
|
|
3299
|
+
for (const id of ids) {
|
|
3300
|
+
const text2 = lookup.get(id);
|
|
3301
|
+
if (text2) out.push(text2);
|
|
3302
|
+
}
|
|
3303
|
+
return out.sort();
|
|
3277
3304
|
}
|
|
3278
3305
|
function collectCitedQueryIds(snapshots) {
|
|
3279
3306
|
const cited = /* @__PURE__ */ new Set();
|
|
@@ -3405,11 +3432,48 @@ function buildGapQueryScore(snapshots) {
|
|
|
3405
3432
|
).length;
|
|
3406
3433
|
const gapQueryLabel = gapCount === 1 ? "query" : "queries";
|
|
3407
3434
|
return {
|
|
3408
|
-
label: "
|
|
3435
|
+
label: "Citation Gaps",
|
|
3409
3436
|
value: `${gapCount}`,
|
|
3410
3437
|
delta: `${gapCount} of ${totalCount} queries at risk`,
|
|
3411
3438
|
tone: gapTone(gapCount, totalCount),
|
|
3412
|
-
description: gapCount > 0 ? `${gapCount} tracked ${gapQueryLabel} currently cite competitors without citing your domain.` : "No competitive
|
|
3439
|
+
description: gapCount > 0 ? `${gapCount} tracked ${gapQueryLabel} currently cite competitors without citing your domain.` : "No competitive citation gaps detected in the latest visibility run.",
|
|
3440
|
+
tooltip,
|
|
3441
|
+
trend: [],
|
|
3442
|
+
progress: totalCount > 0 ? Math.round(gapCount / totalCount * 100) : 0
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
function buildMentionGapScore(snapshots) {
|
|
3446
|
+
const tooltip = "Tracked queries where a competitor surfaces in the latest run but your brand / domain is not mentioned in the answer text.";
|
|
3447
|
+
if (snapshots.length === 0) {
|
|
3448
|
+
return {
|
|
3449
|
+
label: "Mention Gaps",
|
|
3450
|
+
value: "No data",
|
|
3451
|
+
delta: "Run a sweep first",
|
|
3452
|
+
tone: "neutral",
|
|
3453
|
+
description: "Run a visibility sweep to identify queries where competitors are mentioned and your brand is not.",
|
|
3454
|
+
tooltip,
|
|
3455
|
+
trend: []
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
const byQuery = /* @__PURE__ */ new Map();
|
|
3459
|
+
for (const snap of snapshots) {
|
|
3460
|
+
const key = snap.queryId;
|
|
3461
|
+
const current = byQuery.get(key) ?? { mentioned: false, competitorOverlap: /* @__PURE__ */ new Set() };
|
|
3462
|
+
if (snap.answerMentioned === true) current.mentioned = true;
|
|
3463
|
+
for (const domain of snap.competitorOverlap) current.competitorOverlap.add(domain);
|
|
3464
|
+
byQuery.set(key, current);
|
|
3465
|
+
}
|
|
3466
|
+
const totalCount = byQuery.size;
|
|
3467
|
+
const gapCount = [...byQuery.values()].filter(
|
|
3468
|
+
(entry) => !entry.mentioned && entry.competitorOverlap.size > 0
|
|
3469
|
+
).length;
|
|
3470
|
+
const gapQueryLabel = gapCount === 1 ? "query" : "queries";
|
|
3471
|
+
return {
|
|
3472
|
+
label: "Mention Gaps",
|
|
3473
|
+
value: `${gapCount}`,
|
|
3474
|
+
delta: `${gapCount} of ${totalCount} queries at risk`,
|
|
3475
|
+
tone: gapTone(gapCount, totalCount),
|
|
3476
|
+
description: gapCount > 0 ? `${gapCount} tracked ${gapQueryLabel} mention competitors but never your brand.` : "No competitive mention gaps detected in the latest visibility run.",
|
|
3413
3477
|
tooltip,
|
|
3414
3478
|
trend: [],
|
|
3415
3479
|
progress: totalCount > 0 ? Math.round(gapCount / totalCount * 100) : 0
|
|
@@ -3525,6 +3589,129 @@ function buildRunHistory(runs2, snapshotsByRunId, limit = DEFAULT_RUN_HISTORY_LI
|
|
|
3525
3589
|
});
|
|
3526
3590
|
}
|
|
3527
3591
|
|
|
3592
|
+
// ../intelligence/src/share-of-voice.ts
|
|
3593
|
+
function sovTone(score) {
|
|
3594
|
+
if (score >= 30) return "positive";
|
|
3595
|
+
if (score >= 10) return "caution";
|
|
3596
|
+
return "negative";
|
|
3597
|
+
}
|
|
3598
|
+
function buildShareOfVoice(snapshots, options) {
|
|
3599
|
+
const tooltip = "Your domain's share of every distinct cited-source slot across the latest run. Subdomain-aware (cited docs.you.com counts for you.com). Distinct from Citation Coverage \u2014 SoV measures how much of the answer real-estate you own, not just whether you appear.";
|
|
3600
|
+
if (snapshots.length === 0) {
|
|
3601
|
+
return {
|
|
3602
|
+
label: "Share of Voice",
|
|
3603
|
+
value: "No data",
|
|
3604
|
+
delta: "Run a sweep first",
|
|
3605
|
+
tone: "neutral",
|
|
3606
|
+
description: "No SoV data yet. Trigger a run to start tracking.",
|
|
3607
|
+
tooltip,
|
|
3608
|
+
trend: []
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
3611
|
+
const breakdown = computeShareOfVoiceBreakdown(snapshots, options);
|
|
3612
|
+
if (breakdown.totalSlots === 0) {
|
|
3613
|
+
return {
|
|
3614
|
+
label: "Share of Voice",
|
|
3615
|
+
value: "0",
|
|
3616
|
+
delta: "No citations in this run",
|
|
3617
|
+
tone: "neutral",
|
|
3618
|
+
description: "The latest run produced no source-list citations across any provider, so SoV cannot be measured. (Mention Coverage may still be non-zero \u2014 answers can mention you without grounding to a URL.)",
|
|
3619
|
+
tooltip,
|
|
3620
|
+
trend: [],
|
|
3621
|
+
progress: 0
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
const { projectSlots, competitorSlots, otherSlots, totalSlots } = breakdown;
|
|
3625
|
+
const score = Math.round(projectSlots / totalSlots * 100);
|
|
3626
|
+
const competitorShare = Math.round(competitorSlots / totalSlots * 100);
|
|
3627
|
+
const otherShare = Math.max(0, 100 - score - competitorShare);
|
|
3628
|
+
const hasCompetitorsConfigured = options.competitorDomains.length > 0;
|
|
3629
|
+
const description = describeBreakdown({
|
|
3630
|
+
projectSlots,
|
|
3631
|
+
competitorSlots,
|
|
3632
|
+
otherSlots,
|
|
3633
|
+
totalSlots,
|
|
3634
|
+
score,
|
|
3635
|
+
competitorShare,
|
|
3636
|
+
otherShare,
|
|
3637
|
+
hasCompetitorsConfigured
|
|
3638
|
+
});
|
|
3639
|
+
return {
|
|
3640
|
+
label: "Share of Voice",
|
|
3641
|
+
value: `${score}`,
|
|
3642
|
+
delta: `${projectSlots} of ${totalSlots} cited slots`,
|
|
3643
|
+
tone: sovTone(score),
|
|
3644
|
+
description,
|
|
3645
|
+
tooltip,
|
|
3646
|
+
trend: [],
|
|
3647
|
+
progress: score
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
function computeShareOfVoiceBreakdown(snapshots, options) {
|
|
3651
|
+
let totalSlots = 0;
|
|
3652
|
+
let projectSlots = 0;
|
|
3653
|
+
let competitorSlots = 0;
|
|
3654
|
+
for (const snap of snapshots) {
|
|
3655
|
+
for (const domain of snap.citedDomains) {
|
|
3656
|
+
totalSlots++;
|
|
3657
|
+
if (citedDomainBelongsToProject(domain, options.projectDomains)) {
|
|
3658
|
+
projectSlots++;
|
|
3659
|
+
} else if (citedDomainBelongsToProject(domain, options.competitorDomains)) {
|
|
3660
|
+
competitorSlots++;
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
const otherSlots = totalSlots - projectSlots - competitorSlots;
|
|
3665
|
+
return { projectSlots, competitorSlots, otherSlots, totalSlots };
|
|
3666
|
+
}
|
|
3667
|
+
function describeBreakdown(parts) {
|
|
3668
|
+
const { projectSlots, competitorSlots, totalSlots, score, competitorShare, otherShare, hasCompetitorsConfigured } = parts;
|
|
3669
|
+
if (!hasCompetitorsConfigured) {
|
|
3670
|
+
return `${projectSlots} of ${totalSlots} cited slots were yours (${score}%). Add tracked competitors to break out the rest.`;
|
|
3671
|
+
}
|
|
3672
|
+
if (competitorSlots === 0) {
|
|
3673
|
+
return `${projectSlots} of ${totalSlots} cited slots were yours (${score}%); no tracked competitors surfaced in the run. The remaining ${otherShare}% goes to unrelated sources.`;
|
|
3674
|
+
}
|
|
3675
|
+
return `You own ${score}% of cited slots; tracked competitors hold ${competitorShare}%; the remaining ${otherShare}% goes to non-competitive sources.`;
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
// ../intelligence/src/provider-trends.ts
|
|
3679
|
+
function buildProviderTrends(runs2, snapshotsByRunId, limit = 12) {
|
|
3680
|
+
const recent = [...runs2].sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
3681
|
+
const keys = collectProviderKeys(snapshotsByRunId.values());
|
|
3682
|
+
const result = /* @__PURE__ */ new Map();
|
|
3683
|
+
for (const key of keys) result.set(key, []);
|
|
3684
|
+
for (const run of recent) {
|
|
3685
|
+
const snaps = snapshotsByRunId.get(run.id) ?? [];
|
|
3686
|
+
const perKey = /* @__PURE__ */ new Map();
|
|
3687
|
+
for (const snap of snaps) {
|
|
3688
|
+
const key = providerKey(snap.provider, snap.model);
|
|
3689
|
+
const queryMap = perKey.get(key) ?? /* @__PURE__ */ new Map();
|
|
3690
|
+
if (!queryMap.has(snap.queryId)) queryMap.set(snap.queryId, false);
|
|
3691
|
+
if (snap.citationState === CitationStates.cited) queryMap.set(snap.queryId, true);
|
|
3692
|
+
perKey.set(key, queryMap);
|
|
3693
|
+
}
|
|
3694
|
+
for (const key of keys) {
|
|
3695
|
+
const queryMap = perKey.get(key);
|
|
3696
|
+
const rate = queryMap && queryMap.size > 0 ? Math.round([...queryMap.values()].filter(Boolean).length / queryMap.size * 100) : 0;
|
|
3697
|
+
result.get(key).push({ rate, createdAt: run.createdAt });
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
return result;
|
|
3701
|
+
}
|
|
3702
|
+
function providerKey(provider, model) {
|
|
3703
|
+
return `${provider}::${model ?? "unknown"}`;
|
|
3704
|
+
}
|
|
3705
|
+
function collectProviderKeys(perRun) {
|
|
3706
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3707
|
+
for (const snaps of perRun) {
|
|
3708
|
+
for (const snap of snaps) {
|
|
3709
|
+
keys.add(providerKey(snap.provider, snap.model));
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
return keys;
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3528
3715
|
// src/intelligence-service.ts
|
|
3529
3716
|
import crypto from "crypto";
|
|
3530
3717
|
|
|
@@ -4045,11 +4232,15 @@ export {
|
|
|
4045
4232
|
buildVisibilityScore,
|
|
4046
4233
|
buildMentionCoverage,
|
|
4047
4234
|
buildGapQueryScore,
|
|
4235
|
+
buildMentionGapScore,
|
|
4048
4236
|
buildCompetitorPressureScore,
|
|
4049
4237
|
buildOverviewCompetitors,
|
|
4050
4238
|
buildProviderScores,
|
|
4051
4239
|
DEFAULT_RUN_HISTORY_LIMIT,
|
|
4052
4240
|
buildRunHistory,
|
|
4241
|
+
buildShareOfVoice,
|
|
4242
|
+
buildProviderTrends,
|
|
4243
|
+
providerKey,
|
|
4053
4244
|
createLogger,
|
|
4054
4245
|
IntelligenceService
|
|
4055
4246
|
};
|