@mcptoolshop/registry-stats 3.1.0 → 3.2.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 CHANGED
@@ -50,10 +50,12 @@ A self-updating stats dashboard lives at [`/dashboard/`](https://mcp-tool-shop-o
50
50
  - **Tabbed interface** — Home, Analytics, Leaderboard, and Help tabs
51
51
  - **Pulse AI co-pilot** — Ollama-powered conversational assistant with streaming voice synthesis (speaks as the LLM streams, 4 voices via [mcp-voice-soundboard](https://github.com/mcp-tool-shop-org/mcp-voice-soundboard)), web search (Wikipedia + optional SearXNG), auto-speak, fullscreen mode, GitHub org data connector, model selector, and conversation memory
52
52
  - **Executive snapshot** — health score (0–100), diversity index, weekly change, total downloads across all registries
53
- - **Six interactive charts** — 30-day trend (aggregate / per-registry / top-5 toggles), registry share (polar area), portfolio risk (histogram + Gini & P90), top-10 momentum, velocity tracker with sparklines, and 30-day heatmap with spike detection (>2σ)
53
+ - **Seven interactive charts** — 30-day trend (aggregate / per-registry / top-5 toggles + click-to-drill-down + scroll zoom/pan), registry share (polar area), portfolio risk (histogram + Gini & P90), top-10 momentum, velocity tracker with sparklines, 30-day heatmap with spike detection (>2σ), and portfolio trend (stacked area, yearly)
54
54
  - **Smart growth engine** — handles small-denominator distortion with baseline threshold, percentage cap, and damped velocity formula
55
- - **AI Inference Panel** — portfolio momentum (-100 to +100), risk score, 7-day forecast with confidence intervals, and automated recommendations (growth, risk, opportunity, attention)
56
- - **Actionable insights** — auto-generated recommendations and attention alerts for declining packages
55
+ - **AI Inference Panel** — portfolio momentum (-100 to +100), risk score, 7-day forecast with confidence intervals, automated recommendations, actionable advice with severity/urgency levels, and package health scoreboard (A–F grades)
56
+ - **Actionable advice** — severity-tagged advice cards (critical/warning/info/success) with urgency levels, specific action steps, and affected package lists
57
+ - **Package health scores** — 0–100 composite score (activity + consistency + growth + stability) with letter grades per package
58
+ - **Yearly progress tracking** — persistent history layer accumulates monthly per-package and weekly portfolio aggregates; portfolio trend chart with per-registry stacking
57
59
  - **Pulse panel** — split view of Established Movers (≥ 50 downloads/wk) and Emerging & New packages, with inline 7-day sparklines, absolute + percentage deltas, baseline context, and a one-line executive summary
58
60
  - **Live refresh** — on-demand client-side fetch from npm and PyPI APIs with progress indicator; results cached in sessionStorage (5 min TTL) so tab switches are instant
59
61
  - **Export reports** — dropdown next to the Refresh button offering three formats: **Exec PDF** (via jsPDF), **LLM JSONL** (typed records for AI ingestion), and **Dev Markdown** (GFM tables)
@@ -75,7 +77,9 @@ Zero-dependency, pure-math inference that runs at build time — no ML runtime,
75
77
  import {
76
78
  forecast, detectAnomalies, segmentTrends,
77
79
  detectSeasonality, computeMomentum,
78
- generateRecommendations, inferPortfolio,
80
+ generateRecommendations, computeHealthScore,
81
+ generateActionableAdvice, computeYearlyProgress,
82
+ inferPortfolio,
79
83
  } from '@mcptoolshop/registry-stats';
80
84
 
81
85
  // 7-day forecast with 80% confidence intervals
@@ -89,9 +93,17 @@ const anomalies = detectAnomalies(dailySeries);
89
93
  // Composite momentum score (-100 to +100)
90
94
  const momentum = computeMomentum(dailySeries);
91
95
 
92
- // Full portfolio analysis
96
+ // Package health score (0-100 with A-F grade)
97
+ const health = computeHealthScore('my-pkg', 'npm', dailySeries, momentum);
98
+ // → { score: 72, grade: 'B', components: { activity: 20, consistency: 18, growth: 16, stability: 18 } }
99
+
100
+ // Yearly progress from monthly history
101
+ const progress = computeYearlyProgress('my-pkg', 'npm', monthlyHistory);
102
+ // → { currentYearTotal, yoyGrowthPct, projectedYearEnd, milestones, ... }
103
+
104
+ // Full portfolio analysis (now includes health scores + actionable advice)
93
105
  const result = inferPortfolio(leaderboard, { gini: 0.6, npmPct: 85 });
94
- // → { packages, forecastTotal7, riskScore, portfolioMomentum, recommendations }
106
+ // → { packages, forecastTotal7, riskScore, portfolioMomentum, recommendations, healthScores, actionableAdvice }
95
107
  ```
96
108
 
97
109
  | Capability | Method | What it does |
@@ -101,6 +113,9 @@ const result = inferPortfolio(leaderboard, { gini: 0.6, npmPct: 85 });
101
113
  | **Trend segmentation** | Piecewise linear | Identifies up/down/flat segments in time series |
102
114
  | **Seasonality** | Day-of-week decomposition | Detects weekly patterns, reports peak day |
103
115
  | **Momentum** | Composite score | Direction + acceleration + consistency + volume |
116
+ | **Health score** | Multi-factor composite | Activity + consistency + growth + stability (0–100, A–F grade) |
117
+ | **Yearly progress** | Monthly accumulation | YoY growth, projected year-end, milestone tracking |
118
+ | **Actionable advice** | Severity rule engine | Critical/warning/info/success with urgency and specific actions |
104
119
  | **Recommendations** | Rule engine | Growth, risk, opportunity, and attention categories |
105
120
 
106
121
  ## Desktop App
package/dist/index.cjs CHANGED
@@ -22,13 +22,16 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  RegistryError: () => RegistryError,
24
24
  calc: () => calc,
25
+ computeHealthScore: () => computeHealthScore,
25
26
  computeMomentum: () => computeMomentum,
27
+ computeYearlyProgress: () => computeYearlyProgress,
26
28
  createCache: () => createCache,
27
29
  createHandler: () => createHandler,
28
30
  defaultConfig: () => defaultConfig,
29
31
  detectAnomalies: () => detectAnomalies,
30
32
  detectSeasonality: () => detectSeasonality,
31
33
  forecast: () => forecast,
34
+ generateActionableAdvice: () => generateActionableAdvice,
32
35
  generateRecommendations: () => generateRecommendations,
33
36
  inferPortfolio: () => inferPortfolio,
34
37
  loadConfig: () => loadConfig,
@@ -774,6 +777,181 @@ function computeMomentum(series) {
774
777
  const volumeScore = last7Sum > 0 ? Math.min(20, Math.log10(last7Sum + 1) * 5) : 0;
775
778
  return Math.round(Math.max(-100, Math.min(100, dirScore + accelScore + consistencyScore + volumeScore)));
776
779
  }
780
+ var MILESTONE_THRESHOLDS = [100, 500, 1e3, 5e3, 1e4, 5e4, 1e5, 5e5, 1e6];
781
+ function computeYearlyProgress(name, registry, monthlyHistory) {
782
+ const now = /* @__PURE__ */ new Date();
783
+ const currentYear = now.getFullYear();
784
+ const currentMonth = now.getMonth();
785
+ const prevYear = currentYear - 1;
786
+ const currentMonths = [];
787
+ const prevMonths = [];
788
+ for (const [monthKey, agg] of Object.entries(monthlyHistory)) {
789
+ const [yearStr, monthStr] = monthKey.split("-");
790
+ const year = parseInt(yearStr, 10);
791
+ const dl = agg.month || agg.week * 4;
792
+ if (year === currentYear) {
793
+ currentMonths.push({ month: monthKey, downloads: dl });
794
+ } else if (year === prevYear) {
795
+ prevMonths.push({ month: monthKey, downloads: dl });
796
+ }
797
+ }
798
+ currentMonths.sort((a, b) => a.month.localeCompare(b.month));
799
+ prevMonths.sort((a, b) => a.month.localeCompare(b.month));
800
+ const currentYearTotal = currentMonths.reduce((s, m) => s + m.downloads, 0);
801
+ const previousYearTotal = prevMonths.length > 0 ? prevMonths.reduce((s, m) => s + m.downloads, 0) : null;
802
+ const yoyGrowthPct = previousYearTotal !== null && previousYearTotal > 0 ? (currentYearTotal - previousYearTotal) / previousYearTotal * 100 : null;
803
+ const monthsElapsed = currentMonth + 1;
804
+ const monthlyRate = monthsElapsed > 0 ? currentYearTotal / monthsElapsed : 0;
805
+ const projectedYearEnd = Math.round(monthlyRate * 12);
806
+ const bestMonth = currentMonths.length > 0 ? currentMonths.reduce((best, m) => m.downloads > best.downloads ? m : best) : null;
807
+ const milestones = MILESTONE_THRESHOLDS.filter((t) => t <= projectedYearEnd * 2).map((threshold) => {
808
+ const crossed = currentYearTotal >= threshold;
809
+ const crossedAt = crossed ? currentMonths.find((_, idx) => {
810
+ const running = currentMonths.slice(0, idx + 1).reduce((s, m) => s + m.downloads, 0);
811
+ return running >= threshold;
812
+ })?.month : void 0;
813
+ return { threshold, crossed, ...crossedAt ? { crossedAt } : {} };
814
+ });
815
+ return {
816
+ name,
817
+ registry,
818
+ currentYearTotal,
819
+ previousYearTotal,
820
+ yoyGrowthPct: yoyGrowthPct !== null ? Math.round(yoyGrowthPct * 10) / 10 : null,
821
+ projectedYearEnd,
822
+ monthlyTotals: currentMonths,
823
+ bestMonth,
824
+ milestones
825
+ };
826
+ }
827
+ function computeHealthScore(name, registry, series, momentum) {
828
+ const empty = {
829
+ name,
830
+ registry,
831
+ score: 0,
832
+ grade: "F",
833
+ components: { activity: 0, consistency: 0, growth: 0, stability: 0 }
834
+ };
835
+ if (!series || series.length < 7) return empty;
836
+ const last7Sum = series.slice(-7).reduce((a, b) => a + b, 0);
837
+ const activity = Math.min(25, Math.round(Math.log10(last7Sum + 1) * 7));
838
+ const last14 = series.slice(-14);
839
+ const m = mean(last14);
840
+ const cv = m > 0 ? stddev(last14) / m : 1;
841
+ const consistency = Math.min(25, Math.round(Math.max(0, 25 - cv * 25)));
842
+ const growth = Math.round(Math.max(0, Math.min(25, (momentum + 100) / 8)));
843
+ const anomalies = detectAnomalies(series, 2.5);
844
+ const anomalyPenalty = Math.min(25, anomalies.length * 8);
845
+ const stability = Math.max(0, 25 - anomalyPenalty);
846
+ const score = activity + consistency + growth + stability;
847
+ const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : score >= 20 ? "D" : "F";
848
+ return {
849
+ name,
850
+ registry,
851
+ score,
852
+ grade,
853
+ components: { activity, consistency, growth, stability }
854
+ };
855
+ }
856
+ function generateActionableAdvice(packages, healthScores, opts = {}) {
857
+ const advice = [];
858
+ const failing = healthScores.filter((h) => h.grade === "F");
859
+ if (failing.length > 0) {
860
+ advice.push({
861
+ type: "attention",
862
+ severity: "critical",
863
+ urgency: "immediate",
864
+ title: `${failing.length} package${failing.length > 1 ? "s" : ""} in critical health`,
865
+ detail: `${failing.map((h) => h.name).slice(0, 5).join(", ")} scored below 20/100.`,
866
+ action: "Review these packages for broken installs, outdated dependencies, or missing documentation. Consider archiving if no longer maintained.",
867
+ packages: failing.map((h) => h.name)
868
+ });
869
+ }
870
+ const steepDecline = packages.filter((p) => p.momentum < -50);
871
+ if (steepDecline.length > 0) {
872
+ advice.push({
873
+ type: "attention",
874
+ severity: "warning",
875
+ urgency: "this-week",
876
+ title: `${steepDecline.length} package${steepDecline.length > 1 ? "s" : ""} in rapid decline`,
877
+ detail: `${steepDecline.map((p) => p.name).slice(0, 3).join(", ")} lost significant download momentum.`,
878
+ action: "Check for competing packages, broken releases, or ecosystem changes. Push a patch release or announcement.",
879
+ packages: steepDecline.map((p) => p.name)
880
+ });
881
+ }
882
+ if (opts.gini !== void 0 && opts.gini > 0.7) {
883
+ advice.push({
884
+ type: "risk",
885
+ severity: opts.gini > 0.85 ? "warning" : "info",
886
+ urgency: "this-month",
887
+ title: "Portfolio concentration risk",
888
+ detail: `Gini coefficient ${opts.gini.toFixed(2)} \u2014 downloads are concentrated in a few packages.`,
889
+ action: "Promote underperforming packages in README badges, blog posts, and release notes of popular packages.",
890
+ metric: `Gini: ${opts.gini.toFixed(2)}`
891
+ });
892
+ }
893
+ if (opts.npmPct !== void 0 && opts.npmPct > 75) {
894
+ advice.push({
895
+ type: "risk",
896
+ severity: "info",
897
+ urgency: "this-month",
898
+ title: `${opts.npmPct}% of traffic from npm`,
899
+ detail: "Single-registry dependency increases blast radius if npm has outages or policy changes.",
900
+ action: "Cross-publish key packages to PyPI (via wrapper) or NuGet. Add install instructions for all registries in READMEs.",
901
+ metric: `npm share: ${opts.npmPct}%`
902
+ });
903
+ }
904
+ const surging = packages.filter((p) => p.momentum > 50);
905
+ if (surging.length > 0) {
906
+ advice.push({
907
+ type: "opportunity",
908
+ severity: "success",
909
+ urgency: "this-week",
910
+ title: `${surging.length} package${surging.length > 1 ? "s" : ""} surging`,
911
+ detail: `${surging.map((p) => p.name).slice(0, 3).join(", ")} have strong positive momentum.`,
912
+ action: 'Capitalize now: write a blog post, tweet, or submit to newsletters. Post a "Thank you" issue or discussion.',
913
+ packages: surging.map((p) => p.name)
914
+ });
915
+ }
916
+ const highVolume = packages.filter((p) => {
917
+ const pkg = packages.find((pp) => pp.name === p.name);
918
+ return pkg && pkg.forecast7.length > 0 && pkg.forecast7[6]?.predicted > 100;
919
+ });
920
+ for (const pkg of highVolume.slice(0, 3)) {
921
+ const weekSum = pkg.forecast7.reduce((s, f) => s + f.predicted, 0);
922
+ for (const threshold of [1e3, 5e3, 1e4]) {
923
+ if (weekSum >= threshold * 0.9 && weekSum <= threshold * 1.1) {
924
+ advice.push({
925
+ type: "milestone",
926
+ severity: "success",
927
+ urgency: "informational",
928
+ title: `${pkg.name} approaching ${threshold.toLocaleString()} weekly downloads`,
929
+ detail: `Forecasted at ~${weekSum.toLocaleString()} downloads next week.`,
930
+ action: "Prepare a milestone announcement and update the README with a downloads badge.",
931
+ packages: [pkg.name]
932
+ });
933
+ }
934
+ }
935
+ }
936
+ const dGrade = healthScores.filter((h) => h.grade === "D");
937
+ const easyWins = dGrade.filter((h) => {
938
+ return h.components.activity >= 10 && h.components.growth < 10;
939
+ });
940
+ if (easyWins.length > 0) {
941
+ advice.push({
942
+ type: "opportunity",
943
+ severity: "info",
944
+ urgency: "this-month",
945
+ title: `${easyWins.length} package${easyWins.length > 1 ? "s" : ""} with untapped potential`,
946
+ detail: `${easyWins.map((h) => h.name).slice(0, 3).join(", ")} have active users but stalled growth.`,
947
+ action: "Add new features, improve docs, or create tutorials. Small efforts can shift these from D to C+ quickly.",
948
+ packages: easyWins.map((h) => h.name)
949
+ });
950
+ }
951
+ const severityOrder = { critical: 0, warning: 1, success: 2, info: 3 };
952
+ advice.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
953
+ return advice;
954
+ }
777
955
  function generateRecommendations(packages, opts = {}) {
778
956
  const recs = [];
779
957
  const declining = packages.filter((p) => p.momentum < -30);
@@ -891,13 +1069,20 @@ function inferPortfolio(leaderboard, opts = {}) {
891
1069
  const riskScore = Math.round(Math.max(0, Math.min(100, giniRisk + declineRisk + anomalyRisk)));
892
1070
  const diversityTrend = "stable";
893
1071
  const recommendations = generateRecommendations(packages, opts);
1072
+ const healthScores = leaderboard.map((row) => {
1073
+ const pkg = packages.find((p) => p.name === row.name);
1074
+ return computeHealthScore(row.name, row.registry, row.range30 ?? null, pkg?.momentum ?? 0);
1075
+ });
1076
+ const actionableAdvice = generateActionableAdvice(packages, healthScores, opts);
894
1077
  return {
895
1078
  packages,
896
1079
  recommendations,
897
1080
  forecastTotal7,
898
1081
  riskScore,
899
1082
  diversityTrend,
900
- portfolioMomentum: Math.round(weightedMomentum)
1083
+ portfolioMomentum: Math.round(weightedMomentum),
1084
+ healthScores,
1085
+ actionableAdvice
901
1086
  };
902
1087
  }
903
1088
 
@@ -1098,13 +1283,16 @@ stats.mine = async function mine(maintainer, options) {
1098
1283
  0 && (module.exports = {
1099
1284
  RegistryError,
1100
1285
  calc,
1286
+ computeHealthScore,
1101
1287
  computeMomentum,
1288
+ computeYearlyProgress,
1102
1289
  createCache,
1103
1290
  createHandler,
1104
1291
  defaultConfig,
1105
1292
  detectAnomalies,
1106
1293
  detectSeasonality,
1107
1294
  forecast,
1295
+ generateActionableAdvice,
1108
1296
  generateRecommendations,
1109
1297
  inferPortfolio,
1110
1298
  loadConfig,
package/dist/index.d.cts CHANGED
@@ -149,6 +149,55 @@ interface Recommendation {
149
149
  detail: string;
150
150
  metric?: string;
151
151
  }
152
+ interface MonthlyAggregate {
153
+ week: number;
154
+ month: number;
155
+ total: number;
156
+ lastUpdated: string;
157
+ }
158
+ interface YearlyProgress {
159
+ name: string;
160
+ registry: string;
161
+ currentYearTotal: number;
162
+ previousYearTotal: number | null;
163
+ yoyGrowthPct: number | null;
164
+ projectedYearEnd: number;
165
+ monthlyTotals: Array<{
166
+ month: string;
167
+ downloads: number;
168
+ }>;
169
+ bestMonth: {
170
+ month: string;
171
+ downloads: number;
172
+ } | null;
173
+ milestones: Array<{
174
+ threshold: number;
175
+ crossed: boolean;
176
+ crossedAt?: string;
177
+ }>;
178
+ }
179
+ interface PackageHealthScore {
180
+ name: string;
181
+ registry: string;
182
+ score: number;
183
+ grade: 'A' | 'B' | 'C' | 'D' | 'F';
184
+ components: {
185
+ activity: number;
186
+ consistency: number;
187
+ growth: number;
188
+ stability: number;
189
+ };
190
+ }
191
+ interface ActionableAdvice {
192
+ type: 'growth' | 'risk' | 'opportunity' | 'attention' | 'milestone';
193
+ severity: 'critical' | 'warning' | 'info' | 'success';
194
+ urgency: 'immediate' | 'this-week' | 'this-month' | 'informational';
195
+ title: string;
196
+ detail: string;
197
+ action: string;
198
+ metric?: string;
199
+ packages?: string[];
200
+ }
152
201
  interface PackageInference {
153
202
  name: string;
154
203
  registry: string;
@@ -168,6 +217,8 @@ interface PortfolioInference {
168
217
  riskScore: number;
169
218
  diversityTrend: 'improving' | 'stable' | 'declining';
170
219
  portfolioMomentum: number;
220
+ healthScores: PackageHealthScore[];
221
+ actionableAdvice: ActionableAdvice[];
171
222
  }
172
223
  /**
173
224
  * Forecast next N days using weighted linear regression on recent data.
@@ -199,6 +250,25 @@ declare function detectSeasonality(series: number[], startDaysAgo: number): {
199
250
  * Combines: short-term trend, acceleration, volume, and consistency.
200
251
  */
201
252
  declare function computeMomentum(series: number[]): number;
253
+ /**
254
+ * Compute yearly progress for a package given its monthly history.
255
+ * `monthlyHistory` maps month keys ("2026-01") to MonthlyAggregate.
256
+ */
257
+ declare function computeYearlyProgress(name: string, registry: string, monthlyHistory: Record<string, MonthlyAggregate>): YearlyProgress;
258
+ /**
259
+ * Compute a health score (0-100) for a package based on its 30-day series.
260
+ * Grade scale: A (80-100), B (60-79), C (40-59), D (20-39), F (0-19).
261
+ */
262
+ declare function computeHealthScore(name: string, registry: string, series: number[] | null, momentum: number): PackageHealthScore;
263
+ /**
264
+ * Generate specific, actionable advice with severity and urgency levels.
265
+ * More detailed than `generateRecommendations` — includes concrete steps.
266
+ */
267
+ declare function generateActionableAdvice(packages: PackageInference[], healthScores: PackageHealthScore[], opts?: {
268
+ gini?: number;
269
+ npmPct?: number;
270
+ totalWeekly?: number;
271
+ }): ActionableAdvice[];
202
272
  /**
203
273
  * Generate automated recommendations based on portfolio analysis.
204
274
  */
@@ -237,4 +307,4 @@ declare namespace stats {
237
307
  }) => Promise<PackageStats[]>;
238
308
  }
239
309
 
240
- export { type Anomaly, type ChartData, type ComparisonResult, type Config, type DailyDownloads, type ForecastPoint, type PackageConfig, type PackageInference, type PackageStats, type PortfolioInference, type RateLimitConfig, type Recommendation, RegistryError, type RegistryName, type RegistryProvider, type ServerOptions, type StatsCache, type StatsOptions, type TrendSegment, calc, computeMomentum, createCache, createHandler, defaultConfig, detectAnomalies, detectSeasonality, forecast, generateRecommendations, inferPortfolio, loadConfig, registerProvider, segmentTrends, serve, starterConfig, stats };
310
+ export { type ActionableAdvice, type Anomaly, type ChartData, type ComparisonResult, type Config, type DailyDownloads, type ForecastPoint, type MonthlyAggregate, type PackageConfig, type PackageHealthScore, type PackageInference, type PackageStats, type PortfolioInference, type RateLimitConfig, type Recommendation, RegistryError, type RegistryName, type RegistryProvider, type ServerOptions, type StatsCache, type StatsOptions, type TrendSegment, type YearlyProgress, calc, computeHealthScore, computeMomentum, computeYearlyProgress, createCache, createHandler, defaultConfig, detectAnomalies, detectSeasonality, forecast, generateActionableAdvice, generateRecommendations, inferPortfolio, loadConfig, registerProvider, segmentTrends, serve, starterConfig, stats };
package/dist/index.d.ts CHANGED
@@ -149,6 +149,55 @@ interface Recommendation {
149
149
  detail: string;
150
150
  metric?: string;
151
151
  }
152
+ interface MonthlyAggregate {
153
+ week: number;
154
+ month: number;
155
+ total: number;
156
+ lastUpdated: string;
157
+ }
158
+ interface YearlyProgress {
159
+ name: string;
160
+ registry: string;
161
+ currentYearTotal: number;
162
+ previousYearTotal: number | null;
163
+ yoyGrowthPct: number | null;
164
+ projectedYearEnd: number;
165
+ monthlyTotals: Array<{
166
+ month: string;
167
+ downloads: number;
168
+ }>;
169
+ bestMonth: {
170
+ month: string;
171
+ downloads: number;
172
+ } | null;
173
+ milestones: Array<{
174
+ threshold: number;
175
+ crossed: boolean;
176
+ crossedAt?: string;
177
+ }>;
178
+ }
179
+ interface PackageHealthScore {
180
+ name: string;
181
+ registry: string;
182
+ score: number;
183
+ grade: 'A' | 'B' | 'C' | 'D' | 'F';
184
+ components: {
185
+ activity: number;
186
+ consistency: number;
187
+ growth: number;
188
+ stability: number;
189
+ };
190
+ }
191
+ interface ActionableAdvice {
192
+ type: 'growth' | 'risk' | 'opportunity' | 'attention' | 'milestone';
193
+ severity: 'critical' | 'warning' | 'info' | 'success';
194
+ urgency: 'immediate' | 'this-week' | 'this-month' | 'informational';
195
+ title: string;
196
+ detail: string;
197
+ action: string;
198
+ metric?: string;
199
+ packages?: string[];
200
+ }
152
201
  interface PackageInference {
153
202
  name: string;
154
203
  registry: string;
@@ -168,6 +217,8 @@ interface PortfolioInference {
168
217
  riskScore: number;
169
218
  diversityTrend: 'improving' | 'stable' | 'declining';
170
219
  portfolioMomentum: number;
220
+ healthScores: PackageHealthScore[];
221
+ actionableAdvice: ActionableAdvice[];
171
222
  }
172
223
  /**
173
224
  * Forecast next N days using weighted linear regression on recent data.
@@ -199,6 +250,25 @@ declare function detectSeasonality(series: number[], startDaysAgo: number): {
199
250
  * Combines: short-term trend, acceleration, volume, and consistency.
200
251
  */
201
252
  declare function computeMomentum(series: number[]): number;
253
+ /**
254
+ * Compute yearly progress for a package given its monthly history.
255
+ * `monthlyHistory` maps month keys ("2026-01") to MonthlyAggregate.
256
+ */
257
+ declare function computeYearlyProgress(name: string, registry: string, monthlyHistory: Record<string, MonthlyAggregate>): YearlyProgress;
258
+ /**
259
+ * Compute a health score (0-100) for a package based on its 30-day series.
260
+ * Grade scale: A (80-100), B (60-79), C (40-59), D (20-39), F (0-19).
261
+ */
262
+ declare function computeHealthScore(name: string, registry: string, series: number[] | null, momentum: number): PackageHealthScore;
263
+ /**
264
+ * Generate specific, actionable advice with severity and urgency levels.
265
+ * More detailed than `generateRecommendations` — includes concrete steps.
266
+ */
267
+ declare function generateActionableAdvice(packages: PackageInference[], healthScores: PackageHealthScore[], opts?: {
268
+ gini?: number;
269
+ npmPct?: number;
270
+ totalWeekly?: number;
271
+ }): ActionableAdvice[];
202
272
  /**
203
273
  * Generate automated recommendations based on portfolio analysis.
204
274
  */
@@ -237,4 +307,4 @@ declare namespace stats {
237
307
  }) => Promise<PackageStats[]>;
238
308
  }
239
309
 
240
- export { type Anomaly, type ChartData, type ComparisonResult, type Config, type DailyDownloads, type ForecastPoint, type PackageConfig, type PackageInference, type PackageStats, type PortfolioInference, type RateLimitConfig, type Recommendation, RegistryError, type RegistryName, type RegistryProvider, type ServerOptions, type StatsCache, type StatsOptions, type TrendSegment, calc, computeMomentum, createCache, createHandler, defaultConfig, detectAnomalies, detectSeasonality, forecast, generateRecommendations, inferPortfolio, loadConfig, registerProvider, segmentTrends, serve, starterConfig, stats };
310
+ export { type ActionableAdvice, type Anomaly, type ChartData, type ComparisonResult, type Config, type DailyDownloads, type ForecastPoint, type MonthlyAggregate, type PackageConfig, type PackageHealthScore, type PackageInference, type PackageStats, type PortfolioInference, type RateLimitConfig, type Recommendation, RegistryError, type RegistryName, type RegistryProvider, type ServerOptions, type StatsCache, type StatsOptions, type TrendSegment, type YearlyProgress, calc, computeHealthScore, computeMomentum, computeYearlyProgress, createCache, createHandler, defaultConfig, detectAnomalies, detectSeasonality, forecast, generateActionableAdvice, generateRecommendations, inferPortfolio, loadConfig, registerProvider, segmentTrends, serve, starterConfig, stats };
package/dist/index.js CHANGED
@@ -732,6 +732,181 @@ function computeMomentum(series) {
732
732
  const volumeScore = last7Sum > 0 ? Math.min(20, Math.log10(last7Sum + 1) * 5) : 0;
733
733
  return Math.round(Math.max(-100, Math.min(100, dirScore + accelScore + consistencyScore + volumeScore)));
734
734
  }
735
+ var MILESTONE_THRESHOLDS = [100, 500, 1e3, 5e3, 1e4, 5e4, 1e5, 5e5, 1e6];
736
+ function computeYearlyProgress(name, registry, monthlyHistory) {
737
+ const now = /* @__PURE__ */ new Date();
738
+ const currentYear = now.getFullYear();
739
+ const currentMonth = now.getMonth();
740
+ const prevYear = currentYear - 1;
741
+ const currentMonths = [];
742
+ const prevMonths = [];
743
+ for (const [monthKey, agg] of Object.entries(monthlyHistory)) {
744
+ const [yearStr, monthStr] = monthKey.split("-");
745
+ const year = parseInt(yearStr, 10);
746
+ const dl = agg.month || agg.week * 4;
747
+ if (year === currentYear) {
748
+ currentMonths.push({ month: monthKey, downloads: dl });
749
+ } else if (year === prevYear) {
750
+ prevMonths.push({ month: monthKey, downloads: dl });
751
+ }
752
+ }
753
+ currentMonths.sort((a, b) => a.month.localeCompare(b.month));
754
+ prevMonths.sort((a, b) => a.month.localeCompare(b.month));
755
+ const currentYearTotal = currentMonths.reduce((s, m) => s + m.downloads, 0);
756
+ const previousYearTotal = prevMonths.length > 0 ? prevMonths.reduce((s, m) => s + m.downloads, 0) : null;
757
+ const yoyGrowthPct = previousYearTotal !== null && previousYearTotal > 0 ? (currentYearTotal - previousYearTotal) / previousYearTotal * 100 : null;
758
+ const monthsElapsed = currentMonth + 1;
759
+ const monthlyRate = monthsElapsed > 0 ? currentYearTotal / monthsElapsed : 0;
760
+ const projectedYearEnd = Math.round(monthlyRate * 12);
761
+ const bestMonth = currentMonths.length > 0 ? currentMonths.reduce((best, m) => m.downloads > best.downloads ? m : best) : null;
762
+ const milestones = MILESTONE_THRESHOLDS.filter((t) => t <= projectedYearEnd * 2).map((threshold) => {
763
+ const crossed = currentYearTotal >= threshold;
764
+ const crossedAt = crossed ? currentMonths.find((_, idx) => {
765
+ const running = currentMonths.slice(0, idx + 1).reduce((s, m) => s + m.downloads, 0);
766
+ return running >= threshold;
767
+ })?.month : void 0;
768
+ return { threshold, crossed, ...crossedAt ? { crossedAt } : {} };
769
+ });
770
+ return {
771
+ name,
772
+ registry,
773
+ currentYearTotal,
774
+ previousYearTotal,
775
+ yoyGrowthPct: yoyGrowthPct !== null ? Math.round(yoyGrowthPct * 10) / 10 : null,
776
+ projectedYearEnd,
777
+ monthlyTotals: currentMonths,
778
+ bestMonth,
779
+ milestones
780
+ };
781
+ }
782
+ function computeHealthScore(name, registry, series, momentum) {
783
+ const empty = {
784
+ name,
785
+ registry,
786
+ score: 0,
787
+ grade: "F",
788
+ components: { activity: 0, consistency: 0, growth: 0, stability: 0 }
789
+ };
790
+ if (!series || series.length < 7) return empty;
791
+ const last7Sum = series.slice(-7).reduce((a, b) => a + b, 0);
792
+ const activity = Math.min(25, Math.round(Math.log10(last7Sum + 1) * 7));
793
+ const last14 = series.slice(-14);
794
+ const m = mean(last14);
795
+ const cv = m > 0 ? stddev(last14) / m : 1;
796
+ const consistency = Math.min(25, Math.round(Math.max(0, 25 - cv * 25)));
797
+ const growth = Math.round(Math.max(0, Math.min(25, (momentum + 100) / 8)));
798
+ const anomalies = detectAnomalies(series, 2.5);
799
+ const anomalyPenalty = Math.min(25, anomalies.length * 8);
800
+ const stability = Math.max(0, 25 - anomalyPenalty);
801
+ const score = activity + consistency + growth + stability;
802
+ const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : score >= 20 ? "D" : "F";
803
+ return {
804
+ name,
805
+ registry,
806
+ score,
807
+ grade,
808
+ components: { activity, consistency, growth, stability }
809
+ };
810
+ }
811
+ function generateActionableAdvice(packages, healthScores, opts = {}) {
812
+ const advice = [];
813
+ const failing = healthScores.filter((h) => h.grade === "F");
814
+ if (failing.length > 0) {
815
+ advice.push({
816
+ type: "attention",
817
+ severity: "critical",
818
+ urgency: "immediate",
819
+ title: `${failing.length} package${failing.length > 1 ? "s" : ""} in critical health`,
820
+ detail: `${failing.map((h) => h.name).slice(0, 5).join(", ")} scored below 20/100.`,
821
+ action: "Review these packages for broken installs, outdated dependencies, or missing documentation. Consider archiving if no longer maintained.",
822
+ packages: failing.map((h) => h.name)
823
+ });
824
+ }
825
+ const steepDecline = packages.filter((p) => p.momentum < -50);
826
+ if (steepDecline.length > 0) {
827
+ advice.push({
828
+ type: "attention",
829
+ severity: "warning",
830
+ urgency: "this-week",
831
+ title: `${steepDecline.length} package${steepDecline.length > 1 ? "s" : ""} in rapid decline`,
832
+ detail: `${steepDecline.map((p) => p.name).slice(0, 3).join(", ")} lost significant download momentum.`,
833
+ action: "Check for competing packages, broken releases, or ecosystem changes. Push a patch release or announcement.",
834
+ packages: steepDecline.map((p) => p.name)
835
+ });
836
+ }
837
+ if (opts.gini !== void 0 && opts.gini > 0.7) {
838
+ advice.push({
839
+ type: "risk",
840
+ severity: opts.gini > 0.85 ? "warning" : "info",
841
+ urgency: "this-month",
842
+ title: "Portfolio concentration risk",
843
+ detail: `Gini coefficient ${opts.gini.toFixed(2)} \u2014 downloads are concentrated in a few packages.`,
844
+ action: "Promote underperforming packages in README badges, blog posts, and release notes of popular packages.",
845
+ metric: `Gini: ${opts.gini.toFixed(2)}`
846
+ });
847
+ }
848
+ if (opts.npmPct !== void 0 && opts.npmPct > 75) {
849
+ advice.push({
850
+ type: "risk",
851
+ severity: "info",
852
+ urgency: "this-month",
853
+ title: `${opts.npmPct}% of traffic from npm`,
854
+ detail: "Single-registry dependency increases blast radius if npm has outages or policy changes.",
855
+ action: "Cross-publish key packages to PyPI (via wrapper) or NuGet. Add install instructions for all registries in READMEs.",
856
+ metric: `npm share: ${opts.npmPct}%`
857
+ });
858
+ }
859
+ const surging = packages.filter((p) => p.momentum > 50);
860
+ if (surging.length > 0) {
861
+ advice.push({
862
+ type: "opportunity",
863
+ severity: "success",
864
+ urgency: "this-week",
865
+ title: `${surging.length} package${surging.length > 1 ? "s" : ""} surging`,
866
+ detail: `${surging.map((p) => p.name).slice(0, 3).join(", ")} have strong positive momentum.`,
867
+ action: 'Capitalize now: write a blog post, tweet, or submit to newsletters. Post a "Thank you" issue or discussion.',
868
+ packages: surging.map((p) => p.name)
869
+ });
870
+ }
871
+ const highVolume = packages.filter((p) => {
872
+ const pkg = packages.find((pp) => pp.name === p.name);
873
+ return pkg && pkg.forecast7.length > 0 && pkg.forecast7[6]?.predicted > 100;
874
+ });
875
+ for (const pkg of highVolume.slice(0, 3)) {
876
+ const weekSum = pkg.forecast7.reduce((s, f) => s + f.predicted, 0);
877
+ for (const threshold of [1e3, 5e3, 1e4]) {
878
+ if (weekSum >= threshold * 0.9 && weekSum <= threshold * 1.1) {
879
+ advice.push({
880
+ type: "milestone",
881
+ severity: "success",
882
+ urgency: "informational",
883
+ title: `${pkg.name} approaching ${threshold.toLocaleString()} weekly downloads`,
884
+ detail: `Forecasted at ~${weekSum.toLocaleString()} downloads next week.`,
885
+ action: "Prepare a milestone announcement and update the README with a downloads badge.",
886
+ packages: [pkg.name]
887
+ });
888
+ }
889
+ }
890
+ }
891
+ const dGrade = healthScores.filter((h) => h.grade === "D");
892
+ const easyWins = dGrade.filter((h) => {
893
+ return h.components.activity >= 10 && h.components.growth < 10;
894
+ });
895
+ if (easyWins.length > 0) {
896
+ advice.push({
897
+ type: "opportunity",
898
+ severity: "info",
899
+ urgency: "this-month",
900
+ title: `${easyWins.length} package${easyWins.length > 1 ? "s" : ""} with untapped potential`,
901
+ detail: `${easyWins.map((h) => h.name).slice(0, 3).join(", ")} have active users but stalled growth.`,
902
+ action: "Add new features, improve docs, or create tutorials. Small efforts can shift these from D to C+ quickly.",
903
+ packages: easyWins.map((h) => h.name)
904
+ });
905
+ }
906
+ const severityOrder = { critical: 0, warning: 1, success: 2, info: 3 };
907
+ advice.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
908
+ return advice;
909
+ }
735
910
  function generateRecommendations(packages, opts = {}) {
736
911
  const recs = [];
737
912
  const declining = packages.filter((p) => p.momentum < -30);
@@ -849,13 +1024,20 @@ function inferPortfolio(leaderboard, opts = {}) {
849
1024
  const riskScore = Math.round(Math.max(0, Math.min(100, giniRisk + declineRisk + anomalyRisk)));
850
1025
  const diversityTrend = "stable";
851
1026
  const recommendations = generateRecommendations(packages, opts);
1027
+ const healthScores = leaderboard.map((row) => {
1028
+ const pkg = packages.find((p) => p.name === row.name);
1029
+ return computeHealthScore(row.name, row.registry, row.range30 ?? null, pkg?.momentum ?? 0);
1030
+ });
1031
+ const actionableAdvice = generateActionableAdvice(packages, healthScores, opts);
852
1032
  return {
853
1033
  packages,
854
1034
  recommendations,
855
1035
  forecastTotal7,
856
1036
  riskScore,
857
1037
  diversityTrend,
858
- portfolioMomentum: Math.round(weightedMomentum)
1038
+ portfolioMomentum: Math.round(weightedMomentum),
1039
+ healthScores,
1040
+ actionableAdvice
859
1041
  };
860
1042
  }
861
1043
 
@@ -1055,13 +1237,16 @@ stats.mine = async function mine(maintainer, options) {
1055
1237
  export {
1056
1238
  RegistryError,
1057
1239
  calc,
1240
+ computeHealthScore,
1058
1241
  computeMomentum,
1242
+ computeYearlyProgress,
1059
1243
  createCache,
1060
1244
  createHandler,
1061
1245
  defaultConfig,
1062
1246
  detectAnomalies,
1063
1247
  detectSeasonality,
1064
1248
  forecast,
1249
+ generateActionableAdvice,
1065
1250
  generateRecommendations,
1066
1251
  inferPortfolio,
1067
1252
  loadConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcptoolshop/registry-stats",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Multi-registry download stats — engine, AI-powered dashboard, and desktop app for npm, PyPI, NuGet, VS Code Marketplace, and Docker Hub",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",