@ainyc/canonry 4.41.1 → 4.42.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/index.html CHANGED
@@ -12,8 +12,8 @@
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-JiyuncvN.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-Ca1Lty3H.css">
15
+ <script type="module" crossorigin src="./assets/index-NUBexmUO.js"></script>
16
+ <link rel="stylesheet" crossorigin href="./assets/index-DdOq6OIk.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -3,6 +3,7 @@ import {
3
3
  ContentActions,
4
4
  RunKinds,
5
5
  __export,
6
+ brandKeyFromText,
6
7
  brandLabelFromDomain,
7
8
  categorizeSourceWithCompetitors,
8
9
  categoryLabel,
@@ -3589,92 +3590,6 @@ function buildRunHistory(runs2, snapshotsByRunId, limit = DEFAULT_RUN_HISTORY_LI
3589
3590
  });
3590
3591
  }
3591
3592
 
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
3593
  // ../intelligence/src/provider-trends.ts
3679
3594
  function buildProviderTrends(runs2, snapshotsByRunId, limit = 12) {
3680
3595
  const recent = [...runs2].sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, limit).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
@@ -3712,6 +3627,119 @@ function collectProviderKeys(perRun) {
3712
3627
  return keys;
3713
3628
  }
3714
3629
 
3630
+ // ../intelligence/src/mention-share.ts
3631
+ function buildMentionShare(snapshots, options) {
3632
+ const tooltip = 'When AI answers your tracked queries and names a brand, the % of brand-name-drops that are you vs your tracked competitors. Cleaner than Citation Coverage for "am I winning the conversation".';
3633
+ const emptyBreakdown = {
3634
+ projectMentionSnapshots: 0,
3635
+ competitorMentionSnapshots: 0,
3636
+ perCompetitor: [],
3637
+ snapshotsWithAnswerText: 0,
3638
+ snapshotsTotal: snapshots.length
3639
+ };
3640
+ if (snapshots.length === 0) {
3641
+ return {
3642
+ label: "Mention Share",
3643
+ value: "No data",
3644
+ delta: "Run a sweep first",
3645
+ tone: "neutral",
3646
+ description: "No mention share data yet. Trigger a run to start tracking.",
3647
+ tooltip,
3648
+ trend: [],
3649
+ breakdown: emptyBreakdown
3650
+ };
3651
+ }
3652
+ if (options.competitors.length === 0) {
3653
+ return {
3654
+ label: "Mention Share",
3655
+ value: "Add competitors",
3656
+ delta: "No competitors configured",
3657
+ tone: "neutral",
3658
+ description: "Mention Share is a head-to-head competitive metric \u2014 add tracked competitors to compare brand mention rates.",
3659
+ tooltip,
3660
+ trend: [],
3661
+ breakdown: emptyBreakdown
3662
+ };
3663
+ }
3664
+ let projectMentionSnapshots = 0;
3665
+ let snapshotsWithAnswerText = 0;
3666
+ const competitorCounts = /* @__PURE__ */ new Map();
3667
+ for (const c of options.competitors) competitorCounts.set(c.domain, 0);
3668
+ for (const snap of snapshots) {
3669
+ const text2 = snap.answerText ?? "";
3670
+ if (text2.length === 0) continue;
3671
+ snapshotsWithAnswerText++;
3672
+ if (snap.projectMentioned) projectMentionSnapshots++;
3673
+ const answerBrandKey = brandKeyFromText(text2);
3674
+ for (const competitor of options.competitors) {
3675
+ if (competitorMentioned(text2, answerBrandKey, competitor.brandTokens)) {
3676
+ competitorCounts.set(competitor.domain, (competitorCounts.get(competitor.domain) ?? 0) + 1);
3677
+ }
3678
+ }
3679
+ }
3680
+ const competitorMentionSnapshots = [...competitorCounts.values()].reduce((a, b) => a + b, 0);
3681
+ const denom = projectMentionSnapshots + competitorMentionSnapshots;
3682
+ const score = denom > 0 ? Math.round(projectMentionSnapshots / denom * 100) : 0;
3683
+ const perCompetitor = options.competitors.map((c) => ({
3684
+ domain: c.domain,
3685
+ mentionSnapshots: competitorCounts.get(c.domain) ?? 0,
3686
+ shareOfCompetitiveTotal: competitorMentionSnapshots > 0 ? Math.round((competitorCounts.get(c.domain) ?? 0) / competitorMentionSnapshots * 1e3) / 10 : 0
3687
+ })).filter((row) => row.mentionSnapshots > 0).sort((a, b) => b.mentionSnapshots - a.mentionSnapshots);
3688
+ const breakdown = {
3689
+ projectMentionSnapshots,
3690
+ competitorMentionSnapshots,
3691
+ perCompetitor,
3692
+ snapshotsWithAnswerText,
3693
+ snapshotsTotal: snapshots.length
3694
+ };
3695
+ const description = describe({
3696
+ score,
3697
+ projectMentionSnapshots,
3698
+ competitorMentionSnapshots,
3699
+ perCompetitor
3700
+ });
3701
+ return {
3702
+ label: "Mention Share",
3703
+ value: denom > 0 ? `${score}` : "0",
3704
+ delta: denom > 0 ? `${projectMentionSnapshots} of ${denom} brand mentions` : "No brand mentions in this run",
3705
+ tone: denom > 0 ? mentionShareTone(score) : "neutral",
3706
+ description,
3707
+ tooltip,
3708
+ trend: [],
3709
+ progress: denom > 0 ? score : 0,
3710
+ breakdown
3711
+ };
3712
+ }
3713
+ function mentionShareTone(score) {
3714
+ if (score >= 50) return "positive";
3715
+ if (score >= 25) return "caution";
3716
+ return "negative";
3717
+ }
3718
+ function competitorMentioned(text2, answerBrandKey, brandTokens) {
3719
+ for (const token of brandTokens) {
3720
+ if (token.length < 3) continue;
3721
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3722
+ if (new RegExp(`\\b${escaped}\\b`, "i").test(text2)) return true;
3723
+ const tokenBrandKey = brandKeyFromText(token);
3724
+ if (tokenBrandKey.length >= 3 && answerBrandKey.includes(tokenBrandKey)) return true;
3725
+ }
3726
+ return false;
3727
+ }
3728
+ function describe(parts) {
3729
+ const { score, projectMentionSnapshots, competitorMentionSnapshots, perCompetitor } = parts;
3730
+ if (projectMentionSnapshots === 0 && competitorMentionSnapshots === 0) {
3731
+ return "No brand mentions detected for you or your tracked competitors in this run.";
3732
+ }
3733
+ if (competitorMentionSnapshots === 0) {
3734
+ return `${projectMentionSnapshots} brand mentions of you, zero competitor mentions \u2014 you own the conversation.`;
3735
+ }
3736
+ const top = perCompetitor[0];
3737
+ if (!top) {
3738
+ return `${score}% of brand mentions are you (${projectMentionSnapshots} of ${projectMentionSnapshots + competitorMentionSnapshots}).`;
3739
+ }
3740
+ return `${score}% of brand mentions are you. Top competitor: ${top.domain} (${top.mentionSnapshots} mentions).`;
3741
+ }
3742
+
3715
3743
  // src/intelligence-service.ts
3716
3744
  import crypto from "crypto";
3717
3745
 
@@ -4238,9 +4266,9 @@ export {
4238
4266
  buildProviderScores,
4239
4267
  DEFAULT_RUN_HISTORY_LIMIT,
4240
4268
  buildRunHistory,
4241
- buildShareOfVoice,
4242
4269
  buildProviderTrends,
4243
4270
  providerKey,
4271
+ buildMentionShare,
4244
4272
  createLogger,
4245
4273
  IntelligenceService
4246
4274
  };
@@ -32,12 +32,12 @@ import {
32
32
  buildMentionCoverage,
33
33
  buildMentionGapScore,
34
34
  buildMentionLandscape,
35
+ buildMentionShare,
35
36
  buildMovementSummary,
36
37
  buildOverviewCompetitors,
37
38
  buildProviderScores,
38
39
  buildProviderTrends,
39
40
  buildRunHistory,
40
- buildShareOfVoice,
41
41
  buildVisibilityScore,
42
42
  categorizeQueryByIntent,
43
43
  ccReleaseSyncs,
@@ -78,7 +78,7 @@ import {
78
78
  schedules,
79
79
  trafficSources,
80
80
  usageCounters
81
- } from "./chunk-3I6Y2PSQ.js";
81
+ } from "./chunk-BPJATXCC.js";
82
82
  import {
83
83
  AGENT_MEMORY_VALUE_MAX_BYTES,
84
84
  AGENT_PROVIDER_IDS,
@@ -6428,7 +6428,7 @@ function buildReportActionPlan(input) {
6428
6428
  action: "Review the recurring external source domains and add the true competitors before the next check.",
6429
6429
  why: [
6430
6430
  "The report can identify repeated external sources, but it cannot separate competitors from publishers until competitors are configured.",
6431
- "A clean competitor set makes future share-of-voice and content-gap reporting easier to explain to clients."
6431
+ "A clean competitor set makes future mention-share and content-gap reporting easier to explain to clients."
6432
6432
  ],
6433
6433
  evidence: topDomains.map((d) => `${d.domain} appeared in ${d.count} cited source${d.count === 1 ? "" : "s"}`),
6434
6434
  successMetric: "Next report separates tracked competitors from independent source domains in the competitor landscape.",
@@ -7239,17 +7239,23 @@ async function compositeRoutes(app) {
7239
7239
  const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq15(queries.projectId, project.id)).all();
7240
7240
  const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
7241
7241
  const configuredApiProviders = parseJsonColumn(project.providers, []).filter((p) => !p.startsWith("cdp:"));
7242
- const projectDomains = effectiveDomains({
7243
- canonicalDomain: project.canonicalDomain,
7244
- ownedDomains: parseJsonColumn(project.ownedDomains, [])
7245
- });
7242
+ const mentionShareCompetitors = competitorRows.map((c) => ({
7243
+ domain: c.domain,
7244
+ // Single brand token derived from the registrable domain (e.g.
7245
+ // "offers.roofle.com" → "roofle"). Future PR can layer operator-curated
7246
+ // aliases on top via a `competitor_aliases` column.
7247
+ brandTokens: [brandLabelFromDomain(c.domain)].filter((t) => t.length >= 3)
7248
+ }));
7246
7249
  const scores = {
7247
7250
  mention: buildMentionCoverage(latestSnapshots, { configuredApiProviders }),
7248
7251
  visibility: buildVisibilityScore(latestSnapshots, { configuredApiProviders }),
7249
- shareOfVoice: buildShareOfVoice(latestSnapshots, {
7250
- projectDomains,
7251
- competitorDomains: competitorRows.map((c) => c.domain)
7252
- }),
7252
+ mentionShare: buildMentionShare(
7253
+ latestSnapshots.map((s) => ({
7254
+ projectMentioned: s.answerMentioned === true,
7255
+ answerText: s.answerText
7256
+ })),
7257
+ { competitors: mentionShareCompetitors }
7258
+ ),
7253
7259
  gapQueries: buildGapQueryScore(latestSnapshots),
7254
7260
  mentionGaps: buildMentionGapScore(latestSnapshots),
7255
7261
  indexCoverage: buildIndexCoverageScore(app, project.id),
@@ -7412,6 +7418,7 @@ function loadSnapshotsByRunIds(app, runIds) {
7412
7418
  model: querySnapshots.model,
7413
7419
  citationState: querySnapshots.citationState,
7414
7420
  answerMentioned: querySnapshots.answerMentioned,
7421
+ answerText: querySnapshots.answerText,
7415
7422
  competitorOverlap: querySnapshots.competitorOverlap,
7416
7423
  citedDomains: querySnapshots.citedDomains
7417
7424
  }).from(querySnapshots).where(inArray8(querySnapshots.runId, [...runIds])).all());
@@ -7423,6 +7430,7 @@ function loadSnapshotsByRunIds(app, runIds) {
7423
7430
  model: row.model,
7424
7431
  citationState: row.citationState,
7425
7432
  answerMentioned: row.answerMentioned,
7433
+ answerText: row.answerText,
7426
7434
  competitorOverlap: parseJsonColumn(row.competitorOverlap, []),
7427
7435
  citedDomains: parseJsonColumn(row.citedDomains, [])
7428
7436
  });
@@ -26271,7 +26279,7 @@ function readStoredGroundingSources(rawResponse) {
26271
26279
  return result;
26272
26280
  }
26273
26281
  async function backfillInsightsCommand(project, opts) {
26274
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-FFCEIEOU.js");
26282
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-NPBBAN4Y.js");
26275
26283
  const config = loadConfig();
26276
26284
  const db = createClient(config.database);
26277
26285
  migrate(db);
package/dist/cli.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  setTelemetrySource,
22
22
  showFirstRunNotice,
23
23
  trackEvent
24
- } from "./chunk-PQ2IKBT5.js";
24
+ } from "./chunk-D37LKESQ.js";
25
25
  import {
26
26
  CliError,
27
27
  EXIT_SYSTEM_ERROR,
@@ -44,7 +44,7 @@ import {
44
44
  migrate,
45
45
  projects,
46
46
  queries
47
- } from "./chunk-3I6Y2PSQ.js";
47
+ } from "./chunk-BPJATXCC.js";
48
48
  import {
49
49
  CcReleaseSyncStatuses,
50
50
  CheckScopes,
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-PQ2IKBT5.js";
3
+ } from "./chunk-D37LKESQ.js";
4
4
  import {
5
5
  loadConfig
6
6
  } from "./chunk-JZ2VJW4U.js";
7
- import "./chunk-3I6Y2PSQ.js";
7
+ import "./chunk-BPJATXCC.js";
8
8
  import "./chunk-Q7XFJO2V.js";
9
9
  export {
10
10
  createServer,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-3I6Y2PSQ.js";
3
+ } from "./chunk-BPJATXCC.js";
4
4
  import "./chunk-Q7XFJO2V.js";
5
5
  export {
6
6
  IntelligenceService
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "4.41.1",
3
+ "version": "4.42.0",
4
4
  "type": "module",
5
5
  "description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -62,21 +62,21 @@
62
62
  "tsx": "^4.19.0",
63
63
  "@ainyc/canonry-api-routes": "0.0.0",
64
64
  "@ainyc/canonry-config": "0.0.0",
65
- "@ainyc/canonry-db": "0.0.0",
65
+ "@ainyc/canonry-contracts": "0.0.0",
66
66
  "@ainyc/canonry-intelligence": "0.0.0",
67
67
  "@ainyc/canonry-integration-bing": "0.0.0",
68
+ "@ainyc/canonry-db": "0.0.0",
68
69
  "@ainyc/canonry-integration-cloud-run": "0.0.0",
69
- "@ainyc/canonry-contracts": "0.0.0",
70
70
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
71
+ "@ainyc/canonry-integration-wordpress": "0.0.0",
71
72
  "@ainyc/canonry-integration-google": "0.0.0",
72
73
  "@ainyc/canonry-integration-traffic": "0.0.0",
73
- "@ainyc/canonry-integration-wordpress": "0.0.0",
74
- "@ainyc/canonry-provider-gemini": "0.0.0",
75
74
  "@ainyc/canonry-provider-cdp": "0.0.0",
76
75
  "@ainyc/canonry-provider-claude": "0.0.0",
77
- "@ainyc/canonry-provider-openai": "0.0.0",
76
+ "@ainyc/canonry-provider-gemini": "0.0.0",
78
77
  "@ainyc/canonry-provider-local": "0.0.0",
79
- "@ainyc/canonry-provider-perplexity": "0.0.0"
78
+ "@ainyc/canonry-provider-perplexity": "0.0.0",
79
+ "@ainyc/canonry-provider-openai": "0.0.0"
80
80
  },
81
81
  "scripts": {
82
82
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",