dep-oracle 1.0.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/LICENSE +21 -0
- package/README.md +208 -0
- package/action/action.yml +21 -0
- package/action/index.ts +302 -0
- package/dist/chunk-RQV3VHZS.js +3512 -0
- package/dist/chunk-RQV3VHZS.js.map +1 -0
- package/dist/cli/index.js +3989 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.js +325 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.js +472 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +78 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseParser,
|
|
3
|
+
BlastRadiusCalculator,
|
|
4
|
+
CacheManager,
|
|
5
|
+
CollectorOrchestrator,
|
|
6
|
+
ConfigSchema,
|
|
7
|
+
DependencyNodeSchema,
|
|
8
|
+
DependencyTreeSchema,
|
|
9
|
+
MigrationAdvisor,
|
|
10
|
+
NpmParser,
|
|
11
|
+
PythonParser,
|
|
12
|
+
ScanResultSchema,
|
|
13
|
+
TrustMetricsSchema,
|
|
14
|
+
TrustReportSchema,
|
|
15
|
+
TrustScoreEngine,
|
|
16
|
+
TyposquatDetector,
|
|
17
|
+
ZombieDetector,
|
|
18
|
+
buildImportGraph,
|
|
19
|
+
collectorCached,
|
|
20
|
+
collectorError,
|
|
21
|
+
collectorOffline,
|
|
22
|
+
collectorSuccess,
|
|
23
|
+
createDependencyNode,
|
|
24
|
+
createDependencyTree,
|
|
25
|
+
createLogger,
|
|
26
|
+
getBlastRadius,
|
|
27
|
+
getImportingFiles,
|
|
28
|
+
isDebug,
|
|
29
|
+
isVerbose,
|
|
30
|
+
logger,
|
|
31
|
+
setVerbose
|
|
32
|
+
} from "./chunk-RQV3VHZS.js";
|
|
33
|
+
|
|
34
|
+
// src/analyzers/trend-predictor.ts
|
|
35
|
+
var HIGH_CONFIDENCE_THRESHOLD = 3;
|
|
36
|
+
var TrendPredictor = class {
|
|
37
|
+
/**
|
|
38
|
+
* Predict the trajectory of a package based on registry, popularity,
|
|
39
|
+
* and GitHub signals.
|
|
40
|
+
*/
|
|
41
|
+
predict(registry, popularity, github) {
|
|
42
|
+
const signals = [];
|
|
43
|
+
const reasons = [];
|
|
44
|
+
if (popularity !== null) {
|
|
45
|
+
switch (popularity.trend) {
|
|
46
|
+
case "rising":
|
|
47
|
+
signals.push({ direction: "rising", weight: 0.4 });
|
|
48
|
+
reasons.push("Downloads are trending upward");
|
|
49
|
+
break;
|
|
50
|
+
case "declining":
|
|
51
|
+
signals.push({ direction: "declining", weight: 0.4 });
|
|
52
|
+
reasons.push("Downloads are trending downward");
|
|
53
|
+
break;
|
|
54
|
+
case "stable":
|
|
55
|
+
signals.push({ direction: "stable", weight: 0.3 });
|
|
56
|
+
reasons.push("Downloads are stable");
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (github !== null) {
|
|
61
|
+
const commits30d = github.recentCommitCount;
|
|
62
|
+
if (commits30d >= 30) {
|
|
63
|
+
signals.push({ direction: "rising", weight: 0.3 });
|
|
64
|
+
reasons.push(`High commit activity (${commits30d} commits in 30 days)`);
|
|
65
|
+
} else if (commits30d >= 10) {
|
|
66
|
+
signals.push({ direction: "stable", weight: 0.2 });
|
|
67
|
+
reasons.push(`Moderate commit activity (${commits30d} commits in 30 days)`);
|
|
68
|
+
} else if (commits30d >= 1) {
|
|
69
|
+
signals.push({ direction: "stable", weight: 0.15 });
|
|
70
|
+
reasons.push(`Low commit activity (${commits30d} commits in 30 days)`);
|
|
71
|
+
} else {
|
|
72
|
+
signals.push({ direction: "declining", weight: 0.3 });
|
|
73
|
+
reasons.push("No commits in the last 30 days");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (registry !== null) {
|
|
77
|
+
if (registry.lastPublishDate !== null) {
|
|
78
|
+
const lastPublish = new Date(registry.lastPublishDate);
|
|
79
|
+
const monthsSincePublish = monthsBetween(lastPublish, /* @__PURE__ */ new Date());
|
|
80
|
+
if (monthsSincePublish < 2) {
|
|
81
|
+
signals.push({ direction: "rising", weight: 0.2 });
|
|
82
|
+
reasons.push("Recent version published within last 2 months");
|
|
83
|
+
} else if (monthsSincePublish < 6) {
|
|
84
|
+
signals.push({ direction: "stable", weight: 0.15 });
|
|
85
|
+
reasons.push(`Last publish ${Math.round(monthsSincePublish)} months ago`);
|
|
86
|
+
} else if (monthsSincePublish < 12) {
|
|
87
|
+
signals.push({ direction: "declining", weight: 0.2 });
|
|
88
|
+
reasons.push(`No new version in ${Math.round(monthsSincePublish)} months`);
|
|
89
|
+
} else {
|
|
90
|
+
signals.push({ direction: "declining", weight: 0.3 });
|
|
91
|
+
reasons.push(`No new version in ${Math.round(monthsSincePublish)} months \u2014 possibly abandoned`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (registry.versionCount > 50) {
|
|
95
|
+
signals.push({ direction: "stable", weight: 0.1 });
|
|
96
|
+
reasons.push(`Mature package with ${registry.versionCount} versions`);
|
|
97
|
+
}
|
|
98
|
+
if (registry.deprecated !== null) {
|
|
99
|
+
signals.push({ direction: "declining", weight: 0.5 });
|
|
100
|
+
reasons.push("Package is marked as deprecated");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (github !== null && github.stars > 0) {
|
|
104
|
+
const forkRatio = github.forks / github.stars;
|
|
105
|
+
if (forkRatio > 0.3) {
|
|
106
|
+
signals.push({ direction: "rising", weight: 0.1 });
|
|
107
|
+
reasons.push("High fork-to-star ratio indicates active community");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (signals.length === 0) {
|
|
111
|
+
return {
|
|
112
|
+
trend: "unknown",
|
|
113
|
+
confidence: 0,
|
|
114
|
+
riskProjection3m: 0,
|
|
115
|
+
reason: "Insufficient data to determine trend"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const { trend, confidence } = this.aggregateSignals(signals);
|
|
119
|
+
const riskProjection3m = this.calculateRiskProjection(trend, confidence, signals);
|
|
120
|
+
return {
|
|
121
|
+
trend,
|
|
122
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
123
|
+
riskProjection3m: Math.round(riskProjection3m),
|
|
124
|
+
reason: reasons.join(". ") + "."
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// -------------------------------------------------------------------------
|
|
128
|
+
// Internal helpers
|
|
129
|
+
// -------------------------------------------------------------------------
|
|
130
|
+
/**
|
|
131
|
+
* Aggregate multiple trend signals into a single direction and confidence.
|
|
132
|
+
* Uses weighted voting: each signal votes for its direction with its weight.
|
|
133
|
+
*/
|
|
134
|
+
aggregateSignals(signals) {
|
|
135
|
+
const votes = {
|
|
136
|
+
rising: 0,
|
|
137
|
+
stable: 0,
|
|
138
|
+
declining: 0
|
|
139
|
+
};
|
|
140
|
+
let totalWeight = 0;
|
|
141
|
+
for (const signal of signals) {
|
|
142
|
+
votes[signal.direction] += signal.weight;
|
|
143
|
+
totalWeight += signal.weight;
|
|
144
|
+
}
|
|
145
|
+
let maxVote = 0;
|
|
146
|
+
let trend = "stable";
|
|
147
|
+
for (const [direction, vote] of Object.entries(votes)) {
|
|
148
|
+
if (vote > maxVote) {
|
|
149
|
+
maxVote = vote;
|
|
150
|
+
trend = direction;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const dominance = totalWeight > 0 ? maxVote / totalWeight : 0;
|
|
154
|
+
const signalCountFactor = Math.min(signals.length / HIGH_CONFIDENCE_THRESHOLD, 1);
|
|
155
|
+
const confidence = Math.min(dominance * signalCountFactor, 1);
|
|
156
|
+
return { trend, confidence };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Estimate how much the trust score might change in 3 months based on
|
|
160
|
+
* the trend and confidence.
|
|
161
|
+
*/
|
|
162
|
+
calculateRiskProjection(trend, confidence, signals) {
|
|
163
|
+
const projectionMap = {
|
|
164
|
+
rising: 5,
|
|
165
|
+
stable: 0,
|
|
166
|
+
declining: -10,
|
|
167
|
+
unknown: 0
|
|
168
|
+
};
|
|
169
|
+
let base = projectionMap[trend];
|
|
170
|
+
base *= confidence;
|
|
171
|
+
const decliningWeight = signals.filter((s) => s.direction === "declining").reduce((sum, s) => sum + s.weight, 0);
|
|
172
|
+
if (decliningWeight > 0.5) {
|
|
173
|
+
base -= Math.round(decliningWeight * 5);
|
|
174
|
+
}
|
|
175
|
+
return Math.max(-20, Math.min(10, base));
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
function monthsBetween(from, to) {
|
|
179
|
+
const years = to.getFullYear() - from.getFullYear();
|
|
180
|
+
const months = to.getMonth() - from.getMonth();
|
|
181
|
+
const days = to.getDate() - from.getDate();
|
|
182
|
+
return years * 12 + months + (days < 0 ? -0.5 : 0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/utils/rate-limiter.ts
|
|
186
|
+
var RateLimiter = class {
|
|
187
|
+
tokens;
|
|
188
|
+
maxTokens;
|
|
189
|
+
refillIntervalMs;
|
|
190
|
+
lastRefill;
|
|
191
|
+
waitQueue = [];
|
|
192
|
+
/**
|
|
193
|
+
* @param maxRequests Maximum number of requests allowed in the window
|
|
194
|
+
* @param windowMs Window duration in milliseconds
|
|
195
|
+
*/
|
|
196
|
+
constructor(maxRequests, windowMs) {
|
|
197
|
+
this.maxTokens = maxRequests;
|
|
198
|
+
this.tokens = maxRequests;
|
|
199
|
+
this.refillIntervalMs = windowMs;
|
|
200
|
+
this.lastRefill = Date.now();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Acquire a token. Resolves immediately when tokens are available,
|
|
204
|
+
* otherwise waits until the bucket is refilled.
|
|
205
|
+
*/
|
|
206
|
+
async acquire() {
|
|
207
|
+
this.refill();
|
|
208
|
+
if (this.tokens > 0) {
|
|
209
|
+
this.tokens--;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
return new Promise((resolve) => {
|
|
213
|
+
this.waitQueue.push(resolve);
|
|
214
|
+
this.scheduleRefill();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Return the number of tokens currently available (without waiting).
|
|
219
|
+
*/
|
|
220
|
+
get remaining() {
|
|
221
|
+
this.refill();
|
|
222
|
+
return this.tokens;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Return the number of milliseconds until the next refill.
|
|
226
|
+
*/
|
|
227
|
+
get msUntilRefill() {
|
|
228
|
+
const elapsed = Date.now() - this.lastRefill;
|
|
229
|
+
return Math.max(0, this.refillIntervalMs - elapsed);
|
|
230
|
+
}
|
|
231
|
+
// -----------------------------------------------------------------------
|
|
232
|
+
// Internal
|
|
233
|
+
// -----------------------------------------------------------------------
|
|
234
|
+
refill() {
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
const elapsed = now - this.lastRefill;
|
|
237
|
+
if (elapsed >= this.refillIntervalMs) {
|
|
238
|
+
const periods = Math.floor(elapsed / this.refillIntervalMs);
|
|
239
|
+
this.tokens = Math.min(this.maxTokens, this.tokens + periods * this.maxTokens);
|
|
240
|
+
this.lastRefill = now - elapsed % this.refillIntervalMs;
|
|
241
|
+
this.drainWaitQueue();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
scheduleRefill() {
|
|
245
|
+
const delay = this.msUntilRefill;
|
|
246
|
+
if (delay <= 0) {
|
|
247
|
+
this.refill();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
setTimeout(() => {
|
|
251
|
+
this.refill();
|
|
252
|
+
}, delay);
|
|
253
|
+
}
|
|
254
|
+
drainWaitQueue() {
|
|
255
|
+
while (this.waitQueue.length > 0 && this.tokens > 0) {
|
|
256
|
+
this.tokens--;
|
|
257
|
+
const resolve = this.waitQueue.shift();
|
|
258
|
+
resolve?.();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
var githubRateLimiter = new RateLimiter(5e3, 36e5);
|
|
263
|
+
var npmRateLimiter = new RateLimiter(300, 6e4);
|
|
264
|
+
var pypiRateLimiter = new RateLimiter(100, 6e4);
|
|
265
|
+
|
|
266
|
+
// src/cli/config.ts
|
|
267
|
+
import { cosmiconfig } from "cosmiconfig";
|
|
268
|
+
var explorer = cosmiconfig("dep-oracle");
|
|
269
|
+
async function loadConfig(overrides) {
|
|
270
|
+
let fileConfig = {};
|
|
271
|
+
try {
|
|
272
|
+
const result = await explorer.search();
|
|
273
|
+
if (result?.config && typeof result.config === "object") {
|
|
274
|
+
fileConfig = result.config;
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
const merged = { ...fileConfig, ...overrides };
|
|
279
|
+
if (!merged.githubToken && process.env.GITHUB_TOKEN) {
|
|
280
|
+
merged.githubToken = process.env.GITHUB_TOKEN;
|
|
281
|
+
}
|
|
282
|
+
if (!merged.githubToken && process.env.DEP_ORACLE_GITHUB_TOKEN) {
|
|
283
|
+
merged.githubToken = process.env.DEP_ORACLE_GITHUB_TOKEN;
|
|
284
|
+
}
|
|
285
|
+
return ConfigSchema.parse(merged);
|
|
286
|
+
}
|
|
287
|
+
export {
|
|
288
|
+
BaseParser,
|
|
289
|
+
BlastRadiusCalculator,
|
|
290
|
+
CacheManager,
|
|
291
|
+
CollectorOrchestrator,
|
|
292
|
+
ConfigSchema,
|
|
293
|
+
DependencyNodeSchema,
|
|
294
|
+
DependencyTreeSchema,
|
|
295
|
+
MigrationAdvisor,
|
|
296
|
+
NpmParser,
|
|
297
|
+
PythonParser,
|
|
298
|
+
RateLimiter,
|
|
299
|
+
ScanResultSchema,
|
|
300
|
+
TrendPredictor,
|
|
301
|
+
TrustMetricsSchema,
|
|
302
|
+
TrustReportSchema,
|
|
303
|
+
TrustScoreEngine,
|
|
304
|
+
TyposquatDetector,
|
|
305
|
+
ZombieDetector,
|
|
306
|
+
buildImportGraph,
|
|
307
|
+
collectorCached,
|
|
308
|
+
collectorError,
|
|
309
|
+
collectorOffline,
|
|
310
|
+
collectorSuccess,
|
|
311
|
+
createDependencyNode,
|
|
312
|
+
createDependencyTree,
|
|
313
|
+
createLogger,
|
|
314
|
+
getBlastRadius,
|
|
315
|
+
getImportingFiles,
|
|
316
|
+
githubRateLimiter,
|
|
317
|
+
isDebug,
|
|
318
|
+
isVerbose,
|
|
319
|
+
loadConfig,
|
|
320
|
+
logger,
|
|
321
|
+
npmRateLimiter,
|
|
322
|
+
pypiRateLimiter,
|
|
323
|
+
setVerbose
|
|
324
|
+
};
|
|
325
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/analyzers/trend-predictor.ts","../src/utils/rate-limiter.ts","../src/cli/config.ts"],"sourcesContent":["import type {\n RegistryData,\n PopularityData,\n GitHubData,\n} from '../parsers/schema.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type TrendDirection = 'rising' | 'stable' | 'declining' | 'unknown';\n\nexport interface TrendResult {\n /** Overall trend direction. */\n trend: TrendDirection;\n /** Confidence in the prediction (0-1). */\n confidence: number;\n /**\n * Estimated trust score change over the next 3 months.\n * Negative values indicate a projected decline (e.g. -5 means the score\n * is expected to drop by roughly 5 points).\n */\n riskProjection3m: number;\n /** Human-readable explanation of the trend assessment. */\n reason: string;\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Minimum signals required for a high-confidence prediction. */\nconst HIGH_CONFIDENCE_THRESHOLD = 3;\n\n// ---------------------------------------------------------------------------\n// TrendPredictor\n// ---------------------------------------------------------------------------\n\nexport class TrendPredictor {\n /**\n * Predict the trajectory of a package based on registry, popularity,\n * and GitHub signals.\n */\n predict(\n registry: RegistryData | null,\n popularity: PopularityData | null,\n github: GitHubData | null,\n ): TrendResult {\n const signals: TrendSignal[] = [];\n const reasons: string[] = [];\n\n // -----------------------------------------------------------------------\n // Signal 1: Download trend from PopularityData\n // -----------------------------------------------------------------------\n if (popularity !== null) {\n switch (popularity.trend) {\n case 'rising':\n signals.push({ direction: 'rising', weight: 0.4 });\n reasons.push('Downloads are trending upward');\n break;\n case 'declining':\n signals.push({ direction: 'declining', weight: 0.4 });\n reasons.push('Downloads are trending downward');\n break;\n case 'stable':\n signals.push({ direction: 'stable', weight: 0.3 });\n reasons.push('Downloads are stable');\n break;\n }\n }\n\n // -----------------------------------------------------------------------\n // Signal 2: Commit frequency from GitHub\n // -----------------------------------------------------------------------\n if (github !== null) {\n const commits30d = github.recentCommitCount;\n\n if (commits30d >= 30) {\n signals.push({ direction: 'rising', weight: 0.3 });\n reasons.push(`High commit activity (${commits30d} commits in 30 days)`);\n } else if (commits30d >= 10) {\n signals.push({ direction: 'stable', weight: 0.2 });\n reasons.push(`Moderate commit activity (${commits30d} commits in 30 days)`);\n } else if (commits30d >= 1) {\n signals.push({ direction: 'stable', weight: 0.15 });\n reasons.push(`Low commit activity (${commits30d} commits in 30 days)`);\n } else {\n signals.push({ direction: 'declining', weight: 0.3 });\n reasons.push('No commits in the last 30 days');\n }\n }\n\n // -----------------------------------------------------------------------\n // Signal 3: Version release cadence from RegistryData\n // -----------------------------------------------------------------------\n if (registry !== null) {\n if (registry.lastPublishDate !== null) {\n const lastPublish = new Date(registry.lastPublishDate);\n const monthsSincePublish = monthsBetween(lastPublish, new Date());\n\n if (monthsSincePublish < 2) {\n signals.push({ direction: 'rising', weight: 0.2 });\n reasons.push('Recent version published within last 2 months');\n } else if (monthsSincePublish < 6) {\n signals.push({ direction: 'stable', weight: 0.15 });\n reasons.push(`Last publish ${Math.round(monthsSincePublish)} months ago`);\n } else if (monthsSincePublish < 12) {\n signals.push({ direction: 'declining', weight: 0.2 });\n reasons.push(`No new version in ${Math.round(monthsSincePublish)} months`);\n } else {\n signals.push({ direction: 'declining', weight: 0.3 });\n reasons.push(`No new version in ${Math.round(monthsSincePublish)} months — possibly abandoned`);\n }\n }\n\n // Version count as a maturity signal\n if (registry.versionCount > 50) {\n signals.push({ direction: 'stable', weight: 0.1 });\n reasons.push(`Mature package with ${registry.versionCount} versions`);\n }\n\n // Deprecated flag is a strong declining signal (string | null)\n if (registry.deprecated !== null) {\n signals.push({ direction: 'declining', weight: 0.5 });\n reasons.push('Package is marked as deprecated');\n }\n }\n\n // -----------------------------------------------------------------------\n // Signal 4: Star/fork ratio as community interest (GitHub)\n // -----------------------------------------------------------------------\n if (github !== null && github.stars > 0) {\n const forkRatio = github.forks / github.stars;\n if (forkRatio > 0.3) {\n // High fork ratio suggests active community interest\n signals.push({ direction: 'rising', weight: 0.1 });\n reasons.push('High fork-to-star ratio indicates active community');\n }\n }\n\n // -----------------------------------------------------------------------\n // Aggregate signals\n // -----------------------------------------------------------------------\n if (signals.length === 0) {\n return {\n trend: 'unknown',\n confidence: 0,\n riskProjection3m: 0,\n reason: 'Insufficient data to determine trend',\n };\n }\n\n const { trend, confidence } = this.aggregateSignals(signals);\n const riskProjection3m = this.calculateRiskProjection(trend, confidence, signals);\n\n return {\n trend,\n confidence: Math.round(confidence * 100) / 100,\n riskProjection3m: Math.round(riskProjection3m),\n reason: reasons.join('. ') + '.',\n };\n }\n\n // -------------------------------------------------------------------------\n // Internal helpers\n // -------------------------------------------------------------------------\n\n /**\n * Aggregate multiple trend signals into a single direction and confidence.\n * Uses weighted voting: each signal votes for its direction with its weight.\n */\n private aggregateSignals(signals: TrendSignal[]): {\n trend: TrendDirection;\n confidence: number;\n } {\n const votes: Record<Exclude<TrendDirection, 'unknown'>, number> = {\n rising: 0,\n stable: 0,\n declining: 0,\n };\n\n let totalWeight = 0;\n\n for (const signal of signals) {\n votes[signal.direction] += signal.weight;\n totalWeight += signal.weight;\n }\n\n // Find the winning direction\n let maxVote = 0;\n let trend: TrendDirection = 'stable';\n\n for (const [direction, vote] of Object.entries(votes)) {\n if (vote > maxVote) {\n maxVote = vote;\n trend = direction as TrendDirection;\n }\n }\n\n // Confidence is based on:\n // 1. How dominant the winning direction is (proportion of total weight)\n // 2. How many signals we have (more signals = higher confidence)\n const dominance = totalWeight > 0 ? maxVote / totalWeight : 0;\n const signalCountFactor = Math.min(signals.length / HIGH_CONFIDENCE_THRESHOLD, 1);\n const confidence = Math.min(dominance * signalCountFactor, 1);\n\n return { trend, confidence };\n }\n\n /**\n * Estimate how much the trust score might change in 3 months based on\n * the trend and confidence.\n */\n private calculateRiskProjection(\n trend: TrendDirection,\n confidence: number,\n signals: TrendSignal[],\n ): number {\n // Base projection ranges\n const projectionMap: Record<TrendDirection, number> = {\n rising: 5,\n stable: 0,\n declining: -10,\n unknown: 0,\n };\n\n let base = projectionMap[trend];\n\n // Scale by confidence\n base *= confidence;\n\n // Strong negative signals amplify the projection\n const decliningWeight = signals\n .filter((s) => s.direction === 'declining')\n .reduce((sum, s) => sum + s.weight, 0);\n\n if (decliningWeight > 0.5) {\n // Multiple strong declining signals — amplify the negative projection\n base -= Math.round(decliningWeight * 5);\n }\n\n // Clamp to reasonable range\n return Math.max(-20, Math.min(10, base));\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\ninterface TrendSignal {\n direction: 'rising' | 'stable' | 'declining';\n weight: number;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction monthsBetween(from: Date, to: Date): number {\n const years = to.getFullYear() - from.getFullYear();\n const months = to.getMonth() - from.getMonth();\n const days = to.getDate() - from.getDate();\n return years * 12 + months + (days < 0 ? -0.5 : 0);\n}\n","/**\n * Token-bucket rate limiter for controlling outbound HTTP request frequency.\n *\n * Usage:\n * ```ts\n * const limiter = new RateLimiter(10, 60_000); // 10 requests per minute\n * await limiter.acquire(); // blocks if bucket is empty\n * await fetch(url);\n * ```\n */\nexport class RateLimiter {\n private tokens: number;\n private readonly maxTokens: number;\n private readonly refillIntervalMs: number;\n private lastRefill: number;\n private waitQueue: Array<() => void> = [];\n\n /**\n * @param maxRequests Maximum number of requests allowed in the window\n * @param windowMs Window duration in milliseconds\n */\n constructor(maxRequests: number, windowMs: number) {\n this.maxTokens = maxRequests;\n this.tokens = maxRequests;\n this.refillIntervalMs = windowMs;\n this.lastRefill = Date.now();\n }\n\n /**\n * Acquire a token. Resolves immediately when tokens are available,\n * otherwise waits until the bucket is refilled.\n */\n async acquire(): Promise<void> {\n this.refill();\n\n if (this.tokens > 0) {\n this.tokens--;\n return;\n }\n\n // No tokens available — wait for the next refill cycle\n return new Promise<void>((resolve) => {\n this.waitQueue.push(resolve);\n this.scheduleRefill();\n });\n }\n\n /**\n * Return the number of tokens currently available (without waiting).\n */\n get remaining(): number {\n this.refill();\n return this.tokens;\n }\n\n /**\n * Return the number of milliseconds until the next refill.\n */\n get msUntilRefill(): number {\n const elapsed = Date.now() - this.lastRefill;\n return Math.max(0, this.refillIntervalMs - elapsed);\n }\n\n // -----------------------------------------------------------------------\n // Internal\n // -----------------------------------------------------------------------\n\n private refill(): void {\n const now = Date.now();\n const elapsed = now - this.lastRefill;\n\n if (elapsed >= this.refillIntervalMs) {\n // Full refill\n const periods = Math.floor(elapsed / this.refillIntervalMs);\n this.tokens = Math.min(this.maxTokens, this.tokens + periods * this.maxTokens);\n this.lastRefill = now - (elapsed % this.refillIntervalMs);\n this.drainWaitQueue();\n }\n }\n\n private scheduleRefill(): void {\n const delay = this.msUntilRefill;\n if (delay <= 0) {\n this.refill();\n return;\n }\n\n setTimeout(() => {\n this.refill();\n }, delay);\n }\n\n private drainWaitQueue(): void {\n while (this.waitQueue.length > 0 && this.tokens > 0) {\n this.tokens--;\n const resolve = this.waitQueue.shift();\n resolve?.();\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Pre-configured limiters\n// ---------------------------------------------------------------------------\n\n/**\n * GitHub API rate limiter: 5000 requests per hour (authenticated).\n *\n * GitHub's unauthenticated limit is 60/hr, but dep-oracle is designed to\n * work with a token so we use the authenticated limit.\n */\nexport const githubRateLimiter = new RateLimiter(5000, 3_600_000);\n\n/**\n * npm registry rate limiter: generous default of 300 requests per minute.\n * The npm registry does not publish official limits, but this is safe.\n */\nexport const npmRateLimiter = new RateLimiter(300, 60_000);\n\n/**\n * PyPI rate limiter: conservative 100 requests per minute.\n */\nexport const pypiRateLimiter = new RateLimiter(100, 60_000);\n","/**\n * Configuration loader for dep-oracle.\n *\n * Uses cosmiconfig to search for configuration in:\n * - package.json \"dep-oracle\" field\n * - .dep-oraclerc / .dep-oraclerc.json / .dep-oraclerc.yaml / .dep-oraclerc.yml\n * - dep-oracle.config.js / dep-oracle.config.cjs / dep-oracle.config.mjs\n *\n * Merge order: defaults < file config < environment variables < CLI overrides.\n */\n\nimport { cosmiconfig } from 'cosmiconfig';\nimport { ConfigSchema, type Config } from '../parsers/schema.js';\n\nconst explorer = cosmiconfig('dep-oracle');\n\n/**\n * Load and validate project configuration.\n *\n * @param overrides - Partial config values from CLI flags or programmatic use.\n * These take highest precedence.\n * @returns Fully validated Config object with defaults applied.\n */\nexport async function loadConfig(overrides?: Partial<Config>): Promise<Config> {\n let fileConfig: Record<string, unknown> = {};\n\n try {\n const result = await explorer.search();\n if (result?.config && typeof result.config === 'object') {\n fileConfig = result.config as Record<string, unknown>;\n }\n } catch {\n // Config file is optional -- continue with defaults if search fails\n }\n\n // Merge: file config < CLI overrides\n const merged: Record<string, unknown> = { ...fileConfig, ...overrides };\n\n // Read GitHub token from environment variables when not set by file or override\n if (!merged.githubToken && process.env.GITHUB_TOKEN) {\n merged.githubToken = process.env.GITHUB_TOKEN;\n }\n if (!merged.githubToken && process.env.DEP_ORACLE_GITHUB_TOKEN) {\n merged.githubToken = process.env.DEP_ORACLE_GITHUB_TOKEN;\n }\n\n return ConfigSchema.parse(merged);\n}\n"],"mappings":"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","names":[]}
|