@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 +21 -6
- package/dist/index.cjs +189 -1
- package/dist/index.d.cts +71 -1
- package/dist/index.d.ts +71 -1
- package/dist/index.js +186 -1
- package/package.json +1 -1
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
|
-
- **
|
|
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,
|
|
56
|
-
- **Actionable
|
|
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,
|
|
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
|
-
//
|
|
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.
|
|
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",
|