dep-oracle 1.2.1 → 1.4.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.
Files changed (43) hide show
  1. package/README.md +33 -8
  2. package/dist/action/index.js +630 -16
  3. package/dist/badge-5Z3WAD2B.js +89 -0
  4. package/dist/badge-5Z3WAD2B.js.map +1 -0
  5. package/dist/chunk-32B3QIPY.js +1505 -0
  6. package/dist/chunk-32B3QIPY.js.map +1 -0
  7. package/dist/{chunk-TXSNFX3N.js → chunk-3SHHSWZU.js} +635 -17
  8. package/dist/chunk-3SHHSWZU.js.map +1 -0
  9. package/dist/chunk-7DST6SNA.js +258 -0
  10. package/dist/chunk-7DST6SNA.js.map +1 -0
  11. package/dist/{chunk-VHQCTVCZ.js → chunk-DBORVN6C.js} +361 -2338
  12. package/dist/chunk-DBORVN6C.js.map +1 -0
  13. package/dist/chunk-HX6MGNBD.js +271 -0
  14. package/dist/chunk-HX6MGNBD.js.map +1 -0
  15. package/dist/chunk-IVXGOPRU.js +145 -0
  16. package/dist/chunk-IVXGOPRU.js.map +1 -0
  17. package/dist/chunk-SP3VYPXX.js +218 -0
  18. package/dist/chunk-SP3VYPXX.js.map +1 -0
  19. package/dist/chunk-UMB5MJHL.js +239 -0
  20. package/dist/chunk-UMB5MJHL.js.map +1 -0
  21. package/dist/cli/index.js +414 -19
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/index.js +9 -84
  24. package/dist/index.js.map +1 -1
  25. package/dist/mcp/server.js +33 -12
  26. package/dist/mcp/server.js.map +1 -1
  27. package/dist/npm-UB54H37N.js +9 -0
  28. package/dist/npm-UB54H37N.js.map +1 -0
  29. package/dist/orchestrator-VOOYKDPT.js +8 -0
  30. package/dist/orchestrator-VOOYKDPT.js.map +1 -0
  31. package/dist/python-U4G2GK4J.js +9 -0
  32. package/dist/python-U4G2GK4J.js.map +1 -0
  33. package/dist/{server-TKLM7YIF.js → server-MLFC2O2B.js} +50 -18
  34. package/dist/server-MLFC2O2B.js.map +1 -0
  35. package/dist/store-Z5UANEBB.js +8 -0
  36. package/dist/store-Z5UANEBB.js.map +1 -0
  37. package/dist/trust-score-YXYDFVPZ.js +8 -0
  38. package/dist/trust-score-YXYDFVPZ.js.map +1 -0
  39. package/package.json +1 -1
  40. package/server.json +2 -2
  41. package/dist/chunk-TXSNFX3N.js.map +0 -1
  42. package/dist/chunk-VHQCTVCZ.js.map +0 -1
  43. package/dist/server-TKLM7YIF.js.map +0 -1
@@ -5608,6 +5608,87 @@ function createLogger(label) {
5608
5608
  };
5609
5609
  }
5610
5610
 
5611
+ // src/utils/rate-limiter.ts
5612
+ var RateLimiter = class {
5613
+ tokens;
5614
+ maxTokens;
5615
+ refillIntervalMs;
5616
+ lastRefill;
5617
+ waitQueue = [];
5618
+ /**
5619
+ * @param maxRequests Maximum number of requests allowed in the window
5620
+ * @param windowMs Window duration in milliseconds
5621
+ */
5622
+ constructor(maxRequests, windowMs) {
5623
+ this.maxTokens = maxRequests;
5624
+ this.tokens = maxRequests;
5625
+ this.refillIntervalMs = windowMs;
5626
+ this.lastRefill = Date.now();
5627
+ }
5628
+ /**
5629
+ * Acquire a token. Resolves immediately when tokens are available,
5630
+ * otherwise waits until the bucket is refilled.
5631
+ */
5632
+ async acquire() {
5633
+ this.refill();
5634
+ if (this.tokens > 0) {
5635
+ this.tokens--;
5636
+ return;
5637
+ }
5638
+ return new Promise((resolve2) => {
5639
+ this.waitQueue.push(resolve2);
5640
+ this.scheduleRefill();
5641
+ });
5642
+ }
5643
+ /**
5644
+ * Return the number of tokens currently available (without waiting).
5645
+ */
5646
+ get remaining() {
5647
+ this.refill();
5648
+ return this.tokens;
5649
+ }
5650
+ /**
5651
+ * Return the number of milliseconds until the next refill.
5652
+ */
5653
+ get msUntilRefill() {
5654
+ const elapsed = Date.now() - this.lastRefill;
5655
+ return Math.max(0, this.refillIntervalMs - elapsed);
5656
+ }
5657
+ // -----------------------------------------------------------------------
5658
+ // Internal
5659
+ // -----------------------------------------------------------------------
5660
+ refill() {
5661
+ const now = Date.now();
5662
+ const elapsed = now - this.lastRefill;
5663
+ if (elapsed >= this.refillIntervalMs) {
5664
+ const periods = Math.floor(elapsed / this.refillIntervalMs);
5665
+ this.tokens = Math.min(this.maxTokens, this.tokens + periods * this.maxTokens);
5666
+ this.lastRefill = now - elapsed % this.refillIntervalMs;
5667
+ this.drainWaitQueue();
5668
+ }
5669
+ }
5670
+ scheduleRefill() {
5671
+ const delay = this.msUntilRefill;
5672
+ if (delay <= 0) {
5673
+ this.refill();
5674
+ return;
5675
+ }
5676
+ setTimeout(() => {
5677
+ this.refill();
5678
+ }, delay);
5679
+ }
5680
+ drainWaitQueue() {
5681
+ while (this.waitQueue.length > 0 && this.tokens > 0) {
5682
+ this.tokens--;
5683
+ const resolve2 = this.waitQueue.shift();
5684
+ resolve2?.();
5685
+ }
5686
+ }
5687
+ };
5688
+ var githubRateLimiter = new RateLimiter(5e3, 36e5);
5689
+ var npmRateLimiter = new RateLimiter(300, 6e4);
5690
+ var pypiRateLimiter = new RateLimiter(100, 6e4);
5691
+
5611
5692
  // src/collectors/base.ts
5612
5693
  var BaseCollector = class {
5613
5694
  cache;
@@ -5720,6 +5801,7 @@ var RegistryCollector = class extends BaseCollector {
5720
5801
  async fetchPackument(packageName) {
5721
5802
  const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
5722
5803
  logger.debug(`Fetching packument: ${url}`);
5804
+ await npmRateLimiter.acquire();
5723
5805
  const res = await fetch(url, {
5724
5806
  headers: { Accept: "application/json" }
5725
5807
  });
@@ -5732,6 +5814,7 @@ var RegistryCollector = class extends BaseCollector {
5732
5814
  const url = `https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`;
5733
5815
  logger.debug(`Fetching weekly downloads: ${url}`);
5734
5816
  try {
5817
+ await npmRateLimiter.acquire();
5735
5818
  const res = await fetch(url, {
5736
5819
  headers: { Accept: "application/json" }
5737
5820
  });
@@ -5759,6 +5842,166 @@ var RegistryCollector = class extends BaseCollector {
5759
5842
  }
5760
5843
  };
5761
5844
 
5845
+ // src/collectors/pypi-registry.ts
5846
+ var PyPIRegistryCollector = class extends BaseCollector {
5847
+ name = "pypi-registry";
5848
+ constructor(cache2) {
5849
+ super(cache2);
5850
+ }
5851
+ async collect(packageName, version) {
5852
+ const cached = await this.getCached(packageName, version);
5853
+ if (cached) return cached;
5854
+ try {
5855
+ const [metadataResult, downloadsResult] = await Promise.allSettled([
5856
+ this.fetchMetadata(packageName),
5857
+ this.fetchWeeklyDownloads(packageName)
5858
+ ]);
5859
+ const metadata = metadataResult.status === "fulfilled" ? metadataResult.value : null;
5860
+ const downloads = downloadsResult.status === "fulfilled" ? downloadsResult.value : 0;
5861
+ if (!metadata) {
5862
+ throw new Error(`PyPI registry returned no data for ${packageName}`);
5863
+ }
5864
+ const versionCount = metadata.releases ? Object.keys(metadata.releases).length : 0;
5865
+ const lastPublishDate = this.findLastPublishDate(metadata.releases);
5866
+ const deprecated = this.checkYanked(metadata.releases, version);
5867
+ const data = {
5868
+ packageName,
5869
+ version: version === "latest" ? metadata.info.version : version,
5870
+ description: metadata.info.summary ?? null,
5871
+ lastPublishDate,
5872
+ versionCount,
5873
+ deprecated,
5874
+ weeklyDownloads: downloads,
5875
+ license: metadata.info.license ?? null,
5876
+ repositoryUrl: this.extractRepoUrl(metadata.info)
5877
+ };
5878
+ await this.setCache(packageName, version, data);
5879
+ return {
5880
+ status: "success",
5881
+ data,
5882
+ collectedAt: (/* @__PURE__ */ new Date()).toISOString()
5883
+ };
5884
+ } catch (err) {
5885
+ const message = err instanceof Error ? err.message : String(err);
5886
+ logger.error(
5887
+ `PyPIRegistryCollector failed for ${packageName}@${version}: ${message}`
5888
+ );
5889
+ return {
5890
+ status: "error",
5891
+ data: null,
5892
+ error: message,
5893
+ collectedAt: (/* @__PURE__ */ new Date()).toISOString()
5894
+ };
5895
+ }
5896
+ }
5897
+ // ---------------------------------------------------------------------------
5898
+ // Private helpers
5899
+ // ---------------------------------------------------------------------------
5900
+ async fetchMetadata(packageName) {
5901
+ await pypiRateLimiter.acquire();
5902
+ const url = `https://pypi.org/pypi/${encodeURIComponent(packageName)}/json`;
5903
+ logger.debug(`Fetching PyPI metadata: ${url}`);
5904
+ const res = await fetch(url, {
5905
+ headers: { Accept: "application/json" }
5906
+ });
5907
+ if (!res.ok) {
5908
+ if (res.status === 404) return null;
5909
+ throw new Error(`PyPI registry returned ${res.status} for ${packageName}`);
5910
+ }
5911
+ return await res.json();
5912
+ }
5913
+ async fetchWeeklyDownloads(packageName) {
5914
+ await pypiRateLimiter.acquire();
5915
+ const url = `https://pypistats.org/api/packages/${encodeURIComponent(packageName)}/recent`;
5916
+ logger.debug(`Fetching PyPI weekly downloads: ${url}`);
5917
+ try {
5918
+ const res = await fetch(url, {
5919
+ headers: { Accept: "application/json" }
5920
+ });
5921
+ if (!res.ok) {
5922
+ logger.warn(
5923
+ `PyPI stats API returned ${res.status} for ${packageName}`
5924
+ );
5925
+ return 0;
5926
+ }
5927
+ const body = await res.json();
5928
+ return body.data?.last_week ?? 0;
5929
+ } catch {
5930
+ logger.warn(`Could not fetch PyPI download stats for ${packageName}`);
5931
+ return 0;
5932
+ }
5933
+ }
5934
+ /**
5935
+ * Find the most recent upload date across all releases.
5936
+ */
5937
+ findLastPublishDate(releases) {
5938
+ if (!releases) return null;
5939
+ const dates = [];
5940
+ for (const files of Object.values(releases)) {
5941
+ for (const file of files) {
5942
+ const dateStr = file.upload_time_iso_8601 ?? file.upload_time;
5943
+ if (dateStr) {
5944
+ const ts = new Date(dateStr).getTime();
5945
+ if (!isNaN(ts)) dates.push(ts);
5946
+ }
5947
+ }
5948
+ }
5949
+ if (dates.length === 0) return null;
5950
+ dates.sort((a, b) => b - a);
5951
+ return new Date(dates[0]).toISOString();
5952
+ }
5953
+ /**
5954
+ * Check if the requested version is yanked (PyPI's deprecation mechanism).
5955
+ * Returns the yank reason string, or null if not yanked.
5956
+ */
5957
+ checkYanked(releases, version) {
5958
+ if (!releases || version === "latest") return null;
5959
+ const files = releases[version];
5960
+ if (!files || files.length === 0) return null;
5961
+ const yankedFile = files.find((f) => f.yanked);
5962
+ if (yankedFile) {
5963
+ return yankedFile.yanked_reason || "This version has been yanked";
5964
+ }
5965
+ return null;
5966
+ }
5967
+ /**
5968
+ * Extract a normalised repository URL from PyPI project_urls or home_page.
5969
+ */
5970
+ extractRepoUrl(info) {
5971
+ const projectUrls = info.project_urls ?? {};
5972
+ const repoKeys = [
5973
+ "Source",
5974
+ "Source Code",
5975
+ "Repository",
5976
+ "GitHub",
5977
+ "Code",
5978
+ "Homepage",
5979
+ "source",
5980
+ "source_code",
5981
+ "repository",
5982
+ "github",
5983
+ "code",
5984
+ "homepage"
5985
+ ];
5986
+ for (const key of repoKeys) {
5987
+ const url = projectUrls[key];
5988
+ if (url && (url.includes("github.com") || url.includes("gitlab.com") || url.includes("bitbucket.org"))) {
5989
+ return url.replace(/\.git$/, "");
5990
+ }
5991
+ }
5992
+ for (const url of Object.values(projectUrls)) {
5993
+ if (url && (url.includes("github.com") || url.includes("gitlab.com") || url.includes("bitbucket.org"))) {
5994
+ return url.replace(/\.git$/, "");
5995
+ }
5996
+ }
5997
+ const homePage = info.home_page;
5998
+ if (homePage && (homePage.includes("github.com") || homePage.includes("gitlab.com"))) {
5999
+ return homePage.replace(/\.git$/, "");
6000
+ }
6001
+ return null;
6002
+ }
6003
+ };
6004
+
5762
6005
  // src/collectors/github.ts
5763
6006
  var GitHubCollector = class extends BaseCollector {
5764
6007
  name = "github";
@@ -5830,6 +6073,7 @@ var GitHubCollector = class extends BaseCollector {
5830
6073
  async resolveRepoSlug(packageName) {
5831
6074
  try {
5832
6075
  const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
6076
+ await npmRateLimiter.acquire();
5833
6077
  const res = await fetch(url, {
5834
6078
  headers: { Accept: "application/json" }
5835
6079
  });
@@ -5872,6 +6116,7 @@ var GitHubCollector = class extends BaseCollector {
5872
6116
  async fetchRepoInfo(owner, repo) {
5873
6117
  const url = `https://api.github.com/repos/${owner}/${repo}`;
5874
6118
  logger.debug(`GitHub: fetching repo info ${url}`);
6119
+ await githubRateLimiter.acquire();
5875
6120
  const res = await fetch(url, { headers: this.headers() });
5876
6121
  if (!res.ok) {
5877
6122
  throw new Error(`GitHub API ${res.status} for ${url}`);
@@ -5886,6 +6131,7 @@ var GitHubCollector = class extends BaseCollector {
5886
6131
  const url = `https://api.github.com/repos/${owner}/${repo}/contributors?per_page=1&anon=true`;
5887
6132
  logger.debug(`GitHub: fetching contributor count ${url}`);
5888
6133
  try {
6134
+ await githubRateLimiter.acquire();
5889
6135
  const res = await fetch(url, { headers: this.headers() });
5890
6136
  if (!res.ok) return 0;
5891
6137
  const count = this.extractLastPage(res.headers.get("link"));
@@ -5906,6 +6152,7 @@ var GitHubCollector = class extends BaseCollector {
5906
6152
  const url = `https://api.github.com/repos/${owner}/${repo}/commits?since=${since}&per_page=1`;
5907
6153
  logger.debug(`GitHub: fetching recent commit count ${url}`);
5908
6154
  try {
6155
+ await githubRateLimiter.acquire();
5909
6156
  const res = await fetch(url, { headers: this.headers() });
5910
6157
  if (!res.ok) return 0;
5911
6158
  const count = this.extractLastPage(res.headers.get("link"));
@@ -5921,6 +6168,7 @@ var GitHubCollector = class extends BaseCollector {
5921
6168
  const url = `https://api.github.com/repos/${owner}/${repo}/commits?per_page=1`;
5922
6169
  logger.debug(`GitHub: fetching latest commit ${url}`);
5923
6170
  try {
6171
+ await githubRateLimiter.acquire();
5924
6172
  const res = await fetch(url, { headers: this.headers() });
5925
6173
  if (!res.ok) return null;
5926
6174
  const body = await res.json();
@@ -5937,6 +6185,7 @@ var GitHubCollector = class extends BaseCollector {
5937
6185
  const url = `https://api.github.com/repos/${owner}/${repo}/contents/.github/FUNDING.yml`;
5938
6186
  logger.debug(`GitHub: checking FUNDING.yml ${url}`);
5939
6187
  try {
6188
+ await githubRateLimiter.acquire();
5940
6189
  const res = await fetch(url, { headers: this.headers() });
5941
6190
  return res.ok;
5942
6191
  } catch {
@@ -5968,11 +6217,11 @@ var SecurityCollector = class extends BaseCollector {
5968
6217
  constructor(cache2) {
5969
6218
  super(cache2);
5970
6219
  }
5971
- async collect(packageName, version) {
6220
+ async collect(packageName, version, ecosystem) {
5972
6221
  const cached = await this.getCached(packageName, version);
5973
6222
  if (cached) return cached;
5974
6223
  try {
5975
- const vulns = await this.queryOsv(packageName);
6224
+ const vulns = await this.queryOsv(packageName, ecosystem);
5976
6225
  const totalVulnerabilities = vulns.length;
5977
6226
  const severityCounts = {
5978
6227
  critical: 0,
@@ -6022,16 +6271,16 @@ var SecurityCollector = class extends BaseCollector {
6022
6271
  // ---------------------------------------------------------------------------
6023
6272
  // OSV API
6024
6273
  // ---------------------------------------------------------------------------
6025
- async queryOsv(packageName) {
6274
+ async queryOsv(packageName, ecosystem = "npm") {
6026
6275
  const url = "https://api.osv.dev/v1/query";
6027
- logger.debug(`OSV: querying vulnerabilities for ${packageName}`);
6276
+ logger.debug(`OSV: querying vulnerabilities for ${packageName} (ecosystem=${ecosystem})`);
6028
6277
  const res = await fetch(url, {
6029
6278
  method: "POST",
6030
6279
  headers: { "Content-Type": "application/json" },
6031
6280
  body: JSON.stringify({
6032
6281
  package: {
6033
6282
  name: packageName,
6034
- ecosystem: "npm"
6283
+ ecosystem
6035
6284
  }
6036
6285
  })
6037
6286
  });
@@ -6195,6 +6444,7 @@ var FundingCollector = class extends BaseCollector {
6195
6444
  async fetchNpmFunding(packageName) {
6196
6445
  try {
6197
6446
  const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
6447
+ await npmRateLimiter.acquire();
6198
6448
  const res = await fetch(url, {
6199
6449
  headers: { Accept: "application/json" }
6200
6450
  });
@@ -6229,6 +6479,7 @@ var FundingCollector = class extends BaseCollector {
6229
6479
  if (this.githubToken) {
6230
6480
  headers.Authorization = `Bearer ${this.githubToken}`;
6231
6481
  }
6482
+ await githubRateLimiter.acquire();
6232
6483
  const res = await fetch(url, { headers });
6233
6484
  if (!res.ok) return null;
6234
6485
  return await res.text();
@@ -6259,6 +6510,7 @@ var FundingCollector = class extends BaseCollector {
6259
6510
  async resolveRepoSlug(packageName) {
6260
6511
  try {
6261
6512
  const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
6513
+ await npmRateLimiter.acquire();
6262
6514
  const res = await fetch(url, {
6263
6515
  headers: { Accept: "application/json" }
6264
6516
  });
@@ -6377,6 +6629,7 @@ var PopularityCollector = class extends BaseCollector {
6377
6629
  const url = `https://api.npmjs.org/downloads/point/${period}/${encodeURIComponent(packageName)}`;
6378
6630
  logger.debug(`Popularity: fetching ${period} downloads: ${url}`);
6379
6631
  try {
6632
+ await npmRateLimiter.acquire();
6380
6633
  const res = await fetch(url, {
6381
6634
  headers: { Accept: "application/json" }
6382
6635
  });
@@ -6404,6 +6657,7 @@ var PopularityCollector = class extends BaseCollector {
6404
6657
  const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(packageName)}&size=1`;
6405
6658
  logger.debug(`Popularity: fetching dependent count: ${url}`);
6406
6659
  try {
6660
+ await npmRateLimiter.acquire();
6407
6661
  const res = await fetch(url, {
6408
6662
  headers: { Accept: "application/json" }
6409
6663
  });
@@ -6429,6 +6683,7 @@ var PopularityCollector = class extends BaseCollector {
6429
6683
  async fetchDependentCountFromRegistry(packageName) {
6430
6684
  try {
6431
6685
  const url = `https://www.npmjs.com/package/${encodeURIComponent(packageName)}`;
6686
+ await npmRateLimiter.acquire();
6432
6687
  const res = await fetch(url, {
6433
6688
  headers: {
6434
6689
  Accept: "text/html",
@@ -6596,6 +6851,7 @@ var LicenseCollector = class extends BaseCollector {
6596
6851
  async fetchLicense(packageName, version) {
6597
6852
  const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
6598
6853
  logger.debug(`License: fetching packument ${url}`);
6854
+ await npmRateLimiter.acquire();
6599
6855
  const res = await fetch(url, {
6600
6856
  headers: { Accept: "application/json" }
6601
6857
  });
@@ -6655,6 +6911,7 @@ var CollectorOrchestrator = class {
6655
6911
  cache;
6656
6912
  options;
6657
6913
  registryCollector;
6914
+ pypiRegistryCollector;
6658
6915
  githubCollector;
6659
6916
  securityCollector;
6660
6917
  fundingCollector;
@@ -6668,6 +6925,7 @@ var CollectorOrchestrator = class {
6668
6925
  concurrency: options.concurrency ?? 10
6669
6926
  };
6670
6927
  this.registryCollector = new RegistryCollector(this.cache);
6928
+ this.pypiRegistryCollector = new PyPIRegistryCollector(this.cache);
6671
6929
  this.githubCollector = new GitHubCollector(this.cache, this.options.githubToken || void 0);
6672
6930
  this.securityCollector = new SecurityCollector(this.cache);
6673
6931
  this.fundingCollector = new FundingCollector(this.cache, this.options.githubToken || void 0);
@@ -6681,13 +6939,15 @@ var CollectorOrchestrator = class {
6681
6939
  * only the cache is consulted; if there is no cached entry the result gets
6682
6940
  * `status: 'offline'` with `data: null`.
6683
6941
  */
6684
- async collectAll(packageName, version) {
6942
+ async collectAll(packageName, version, ecosystem = "npm") {
6685
6943
  logger.info(
6686
- `Collecting data for ${packageName}@${version} (offline=${String(this.options.offline)})`
6944
+ `Collecting data for ${packageName}@${version} (ecosystem=${ecosystem}, offline=${String(this.options.offline)})`
6687
6945
  );
6688
6946
  const limit = pLimit(this.options.concurrency);
6947
+ const activeRegistryCollector = ecosystem === "pypi" ? this.pypiRegistryCollector : this.registryCollector;
6948
+ const osvEcosystem = ecosystem === "pypi" ? "PyPI" : "npm";
6689
6949
  const entries = [
6690
- { key: "registry", collector: this.registryCollector },
6950
+ { key: "registry", collector: activeRegistryCollector },
6691
6951
  { key: "github", collector: this.githubCollector },
6692
6952
  { key: "security", collector: this.securityCollector },
6693
6953
  { key: "funding", collector: this.fundingCollector },
@@ -6703,12 +6963,21 @@ var CollectorOrchestrator = class {
6703
6963
  result = await this.offlineCollect(collector, packageName, version);
6704
6964
  } else {
6705
6965
  try {
6706
- result = await Promise.race([
6707
- this.onlineCollect(collector, packageName, version),
6708
- new Promise(
6709
- (_, reject) => setTimeout(() => reject(new Error("Collector timeout")), COLLECTOR_TIMEOUT)
6710
- )
6711
- ]);
6966
+ if (key === "security") {
6967
+ result = await Promise.race([
6968
+ this.securityCollector.collect(packageName, version, osvEcosystem),
6969
+ new Promise(
6970
+ (_, reject) => setTimeout(() => reject(new Error("Collector timeout")), COLLECTOR_TIMEOUT)
6971
+ )
6972
+ ]);
6973
+ } else {
6974
+ result = await Promise.race([
6975
+ this.onlineCollect(collector, packageName, version),
6976
+ new Promise(
6977
+ (_, reject) => setTimeout(() => reject(new Error("Collector timeout")), COLLECTOR_TIMEOUT)
6978
+ )
6979
+ ]);
6980
+ }
6712
6981
  } catch {
6713
6982
  logger.warn(`[${collector.name}] ${packageName}@${version} => timeout (${COLLECTOR_TIMEOUT}ms)`);
6714
6983
  result = {
@@ -6833,7 +7102,7 @@ var TrustScoreEngine = class {
6833
7102
  return {
6834
7103
  trustScore: Math.round(trustScore),
6835
7104
  metrics,
6836
- insufficientData: unavailableMetrics.length >= 3,
7105
+ insufficientData: unavailableMetrics.length >= 2,
6837
7106
  unavailableMetrics
6838
7107
  };
6839
7108
  }
@@ -6975,6 +7244,11 @@ var TrustScoreEngine = class {
6975
7244
  const effectiveWeight = m.weight / totalAvailableWeight;
6976
7245
  score += m.score * effectiveWeight;
6977
7246
  }
7247
+ const missingWeightFraction = 1 - totalAvailableWeight;
7248
+ if (missingWeightFraction > 0) {
7249
+ const penalty = (score - 50) * missingWeightFraction;
7250
+ score = score - penalty;
7251
+ }
6978
7252
  return clamp(Math.round(score));
6979
7253
  }
6980
7254
  };
@@ -8496,6 +8770,238 @@ var MIGRATION_MAP = {
8496
8770
  description: "Native trimEnd() is available since ES2019. Remove the polyfill.",
8497
8771
  difficulty: "easy"
8498
8772
  }
8773
+ ],
8774
+ // ─── v1.4.0 additions ───────────────────────────────────────────────
8775
+ // Modern frameworks
8776
+ "express": [
8777
+ {
8778
+ alternative: "hono",
8779
+ description: "Ultra-fast web framework. Works on Node, Deno, Bun, Cloudflare Workers. Similar routing API.",
8780
+ difficulty: "moderate"
8781
+ },
8782
+ {
8783
+ alternative: "fastify",
8784
+ description: "Fast, low-overhead web framework with plugin architecture. Schema-based validation built-in.",
8785
+ difficulty: "moderate"
8786
+ },
8787
+ {
8788
+ alternative: "elysia",
8789
+ description: "Bun-native web framework with end-to-end type safety. Extremely fast.",
8790
+ difficulty: "hard"
8791
+ }
8792
+ ],
8793
+ "create-react-app": [
8794
+ {
8795
+ alternative: "vite",
8796
+ description: 'Lightning-fast build tool with HMR. Use "npm create vite@latest" with React template.',
8797
+ difficulty: "moderate"
8798
+ },
8799
+ {
8800
+ alternative: "next",
8801
+ description: "Full-stack React framework with SSR, file-based routing, and API routes.",
8802
+ difficulty: "hard"
8803
+ }
8804
+ ],
8805
+ "react-scripts": [
8806
+ {
8807
+ alternative: "vite",
8808
+ description: "Replace CRA build tooling with Vite. Much faster dev server and builds.",
8809
+ difficulty: "moderate"
8810
+ }
8811
+ ],
8812
+ // ORM / Database
8813
+ "prisma": [
8814
+ {
8815
+ alternative: "drizzle-orm",
8816
+ description: "Lightweight TypeScript ORM with SQL-like syntax. Better performance, smaller bundle.",
8817
+ difficulty: "hard"
8818
+ }
8819
+ ],
8820
+ // Testing
8821
+ "jest": [
8822
+ {
8823
+ alternative: "vitest",
8824
+ description: "Vite-native test runner. Jest-compatible API, much faster. Near drop-in replacement.",
8825
+ difficulty: "easy"
8826
+ }
8827
+ ],
8828
+ // CSS-in-JS
8829
+ "styled-components": [
8830
+ {
8831
+ alternative: "tailwindcss",
8832
+ description: "Utility-first CSS framework. Zero runtime CSS. Requires rewriting component styles.",
8833
+ difficulty: "hard"
8834
+ },
8835
+ {
8836
+ alternative: "vanilla-extract",
8837
+ description: "Zero-runtime CSS-in-TypeScript. Type-safe styles with static extraction.",
8838
+ difficulty: "moderate"
8839
+ }
8840
+ ],
8841
+ "@emotion/react": [
8842
+ {
8843
+ alternative: "tailwindcss",
8844
+ description: "Utility-first CSS framework. No runtime CSS overhead.",
8845
+ difficulty: "hard"
8846
+ },
8847
+ {
8848
+ alternative: "vanilla-extract",
8849
+ description: "Zero-runtime CSS-in-TypeScript. Static extraction at build time.",
8850
+ difficulty: "moderate"
8851
+ }
8852
+ ],
8853
+ // State management
8854
+ "redux": [
8855
+ {
8856
+ alternative: "zustand",
8857
+ description: "Minimal state management with hooks. No boilerplate, no providers needed.",
8858
+ difficulty: "moderate"
8859
+ },
8860
+ {
8861
+ alternative: "jotai",
8862
+ description: "Primitive and flexible state management. Atomic approach, minimal API.",
8863
+ difficulty: "moderate"
8864
+ }
8865
+ ],
8866
+ "mobx": [
8867
+ {
8868
+ alternative: "zustand",
8869
+ description: "Simpler state management with hooks API. Less magic, more predictable.",
8870
+ difficulty: "moderate"
8871
+ }
8872
+ ],
8873
+ // Process managers
8874
+ "pm2": [
8875
+ {
8876
+ alternative: "node --watch",
8877
+ description: "Node.js 18+ native watch mode. For development, no extra dependency needed.",
8878
+ difficulty: "easy"
8879
+ }
8880
+ ],
8881
+ "forever": [
8882
+ {
8883
+ alternative: "pm2",
8884
+ description: "Modern process manager with clustering, monitoring, and log management.",
8885
+ difficulty: "easy"
8886
+ }
8887
+ ],
8888
+ // Validation
8889
+ "joi": [
8890
+ {
8891
+ alternative: "zod",
8892
+ description: "TypeScript-first schema validation. Better type inference, smaller bundle.",
8893
+ difficulty: "moderate"
8894
+ },
8895
+ {
8896
+ alternative: "valibot",
8897
+ description: "Ultra-small schema validation library. Modular, tree-shakeable.",
8898
+ difficulty: "moderate"
8899
+ }
8900
+ ],
8901
+ "yup": [
8902
+ {
8903
+ alternative: "zod",
8904
+ description: "TypeScript-first schema validation. Similar API but better type inference.",
8905
+ difficulty: "easy"
8906
+ },
8907
+ {
8908
+ alternative: "valibot",
8909
+ description: "Modular validation library. Much smaller bundle size than yup.",
8910
+ difficulty: "moderate"
8911
+ }
8912
+ ],
8913
+ "class-validator": [
8914
+ {
8915
+ alternative: "zod",
8916
+ description: "Functional schema validation without decorators. Works with any framework.",
8917
+ difficulty: "moderate"
8918
+ }
8919
+ ],
8920
+ // HTTP / Fetch
8921
+ "supertest": [
8922
+ {
8923
+ alternative: "undici",
8924
+ description: "Node.js native HTTP/1.1 client. Fast, spec-compliant, built into Node 18+.",
8925
+ difficulty: "moderate"
8926
+ }
8927
+ ],
8928
+ "needle": [
8929
+ {
8930
+ alternative: "undici",
8931
+ description: "Node.js native HTTP client. No external dependency needed in Node 18+.",
8932
+ difficulty: "moderate"
8933
+ }
8934
+ ],
8935
+ // Logging (new additions)
8936
+ "console-log-level": [
8937
+ {
8938
+ alternative: "pino",
8939
+ description: "Ultra-fast JSON logger. Structured logging with minimal overhead.",
8940
+ difficulty: "easy"
8941
+ }
8942
+ ],
8943
+ // File system
8944
+ "fs-extra": [
8945
+ {
8946
+ alternative: "node:fs/promises",
8947
+ description: "Native Node.js fs promises API. Most fs-extra methods are now in core (cp, mkdir recursive).",
8948
+ difficulty: "moderate"
8949
+ }
8950
+ ],
8951
+ "graceful-fs": [
8952
+ {
8953
+ alternative: "node:fs",
8954
+ description: "Modern Node.js handles EMFILE gracefully. The polyfill is rarely needed now.",
8955
+ difficulty: "easy"
8956
+ }
8957
+ ],
8958
+ // Environment
8959
+ "cross-env": [
8960
+ {
8961
+ alternative: "node --env-file",
8962
+ description: "Node.js 20+ supports --env-file flag natively. No extra package needed.",
8963
+ difficulty: "easy"
8964
+ }
8965
+ ],
8966
+ "env-cmd": [
8967
+ {
8968
+ alternative: "node --env-file",
8969
+ description: "Node.js 20+ supports --env-file flag. Replace env-cmd with native feature.",
8970
+ difficulty: "easy"
8971
+ }
8972
+ ],
8973
+ // Linting
8974
+ "eslint": [
8975
+ {
8976
+ alternative: "biome",
8977
+ description: "Rust-based linter and formatter. 10-100x faster than ESLint. Minimal config.",
8978
+ difficulty: "moderate"
8979
+ },
8980
+ {
8981
+ alternative: "oxlint",
8982
+ description: "Oxidation compiler linter. Extremely fast, compatible with many ESLint rules.",
8983
+ difficulty: "moderate"
8984
+ }
8985
+ ],
8986
+ "prettier": [
8987
+ {
8988
+ alternative: "biome",
8989
+ description: "Biome includes a Prettier-compatible formatter. One tool for lint + format.",
8990
+ difficulty: "easy"
8991
+ }
8992
+ ],
8993
+ // Monorepo
8994
+ "lerna": [
8995
+ {
8996
+ alternative: "turborepo",
8997
+ description: "High-performance monorepo build system. Caching, parallel execution.",
8998
+ difficulty: "moderate"
8999
+ },
9000
+ {
9001
+ alternative: "nx",
9002
+ description: "Smart monorepo tool with computation caching and affected commands.",
9003
+ difficulty: "moderate"
9004
+ }
8499
9005
  ]
8500
9006
  };
8501
9007
  var MigrationAdvisor = class {
@@ -10735,7 +11241,115 @@ var POPULAR_PACKAGES = [
10735
11241
  "request",
10736
11242
  "tslint",
10737
11243
  "node-pre-gyp",
10738
- "npm-lifecycle"
11244
+ "npm-lifecycle",
11245
+ // ---------------------------------------------------------------------------
11246
+ // Popular Python packages (PyPI)
11247
+ // ---------------------------------------------------------------------------
11248
+ "requests",
11249
+ "flask",
11250
+ "django",
11251
+ "fastapi",
11252
+ "numpy",
11253
+ "pandas",
11254
+ "scipy",
11255
+ "matplotlib",
11256
+ "tensorflow",
11257
+ "torch",
11258
+ "scikit-learn",
11259
+ "keras",
11260
+ "pytorch-lightning",
11261
+ "xgboost",
11262
+ "lightgbm",
11263
+ "pytest",
11264
+ "black",
11265
+ "mypy",
11266
+ "ruff",
11267
+ "pylint",
11268
+ "flake8",
11269
+ "isort",
11270
+ "celery",
11271
+ "redis",
11272
+ "sqlalchemy",
11273
+ "alembic",
11274
+ "pydantic",
11275
+ "httpx",
11276
+ "aiohttp",
11277
+ "uvicorn",
11278
+ "gunicorn",
11279
+ "starlette",
11280
+ "boto3",
11281
+ "botocore",
11282
+ "awscli",
11283
+ "google-cloud-storage",
11284
+ "azure-storage-blob",
11285
+ "pillow",
11286
+ "opencv-python",
11287
+ "beautifulsoup4",
11288
+ "lxml",
11289
+ "scrapy",
11290
+ "cryptography",
11291
+ "paramiko",
11292
+ "fabric",
11293
+ "ansible",
11294
+ "click",
11295
+ "typer",
11296
+ "rich",
11297
+ "tqdm",
11298
+ "colorama",
11299
+ "tabulate",
11300
+ "setuptools",
11301
+ "wheel",
11302
+ "pip",
11303
+ "twine",
11304
+ "poetry",
11305
+ "pdm",
11306
+ "hatch",
11307
+ "jinja2",
11308
+ "mako",
11309
+ "markupsafe",
11310
+ "werkzeug",
11311
+ "psycopg2",
11312
+ "pymongo",
11313
+ "motor",
11314
+ "peewee",
11315
+ "tortoise-orm",
11316
+ "marshmallow",
11317
+ "attrs",
11318
+ "dataclasses-json",
11319
+ "sentry-sdk",
11320
+ "prometheus-client",
11321
+ "opentelemetry-api",
11322
+ "transformers",
11323
+ "huggingface-hub",
11324
+ "tokenizers",
11325
+ "datasets",
11326
+ "langchain",
11327
+ "openai",
11328
+ "anthropic",
11329
+ "tiktoken",
11330
+ "pytest-cov",
11331
+ "pytest-asyncio",
11332
+ "pytest-mock",
11333
+ "coverage",
11334
+ "pyyaml",
11335
+ "toml",
11336
+ "python-dotenv",
11337
+ "decouple",
11338
+ "arrow",
11339
+ "pendulum",
11340
+ "python-dateutil",
11341
+ "stripe",
11342
+ "twilio",
11343
+ "sendgrid",
11344
+ "selenium",
11345
+ "playwright",
11346
+ "httptools",
11347
+ "orjson",
11348
+ "ujson",
11349
+ "msgpack",
11350
+ "networkx",
11351
+ "sympy",
11352
+ "statsmodels"
10739
11353
  ];
10740
11354
  var TyposquatDetector = class _TyposquatDetector {
10741
11355
  popularPackages;