@4da/mcp-server 6.2.4 → 6.2.5

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 (119) hide show
  1. package/CHANGELOG.md +484 -0
  2. package/README.md +22 -6
  3. package/dist/db.d.ts +38 -0
  4. package/dist/db.js +78 -23
  5. package/dist/doctor.js +6 -3
  6. package/dist/embeddings.d.ts +1 -1
  7. package/dist/embeddings.js +16 -14
  8. package/dist/index.js +73 -103
  9. package/dist/live/advisory-groups.d.ts +68 -0
  10. package/dist/live/advisory-groups.js +131 -0
  11. package/dist/live/cache.js +11 -3
  12. package/dist/live/cargo-platform.d.ts +6 -0
  13. package/dist/live/cargo-platform.js +8 -2
  14. package/dist/live/changelog.js +38 -6
  15. package/dist/live/dependency-set.d.ts +9 -1
  16. package/dist/live/dependency-set.js +10 -2
  17. package/dist/live/fix-paths.d.ts +258 -9
  18. package/dist/live/fix-paths.js +790 -45
  19. package/dist/live/github-releases.js +3 -0
  20. package/dist/live/held-dependencies.d.ts +93 -0
  21. package/dist/live/held-dependencies.js +212 -0
  22. package/dist/live/held-targets.d.ts +30 -0
  23. package/dist/live/held-targets.js +107 -0
  24. package/dist/live/http-utils.js +13 -3
  25. package/dist/live/index.d.ts +7 -4
  26. package/dist/live/index.js +24 -19
  27. package/dist/live/install-state.js +6 -2
  28. package/dist/live/js-lockfile-readers.d.ts +8 -1
  29. package/dist/live/js-lockfile-readers.js +43 -6
  30. package/dist/live/lockfile-parents.d.ts +15 -0
  31. package/dist/live/lockfile-parents.js +85 -10
  32. package/dist/live/lockfile-parsers-pygo.d.ts +47 -2
  33. package/dist/live/lockfile-parsers-pygo.js +230 -12
  34. package/dist/live/lockfile-parsers-rbphp.d.ts +38 -0
  35. package/dist/live/lockfile-parsers-rbphp.js +123 -0
  36. package/dist/live/lockfile-parsers.d.ts +6 -0
  37. package/dist/live/lockfile-parsers.js +116 -4
  38. package/dist/live/lockfile-types.d.ts +28 -1
  39. package/dist/live/maintenance.d.ts +20 -4
  40. package/dist/live/maintenance.js +31 -2
  41. package/dist/live/malware.d.ts +20 -0
  42. package/dist/live/malware.js +31 -0
  43. package/dist/live/npm-packument.d.ts +4 -1
  44. package/dist/live/npm-packument.js +5 -2
  45. package/dist/live/offered-release.d.ts +53 -0
  46. package/dist/live/offered-release.js +117 -0
  47. package/dist/live/osv-ranges.d.ts +111 -0
  48. package/dist/live/osv-ranges.js +173 -14
  49. package/dist/live/osv-scanner.d.ts +87 -5
  50. package/dist/live/osv-scanner.js +310 -59
  51. package/dist/live/pinned-advisories.d.ts +2 -2
  52. package/dist/live/pinned-advisories.js +3 -3
  53. package/dist/live/pypi-advice.d.ts +36 -0
  54. package/dist/live/pypi-advice.js +158 -0
  55. package/dist/live/rate-limiter.d.ts +7 -0
  56. package/dist/live/rate-limiter.js +22 -1
  57. package/dist/live/release-metadata.d.ts +5 -0
  58. package/dist/live/resolution.d.ts +3 -0
  59. package/dist/live/resolution.js +6 -3
  60. package/dist/live/scan-coverage.d.ts +26 -0
  61. package/dist/live/scan-coverage.js +70 -0
  62. package/dist/live/semver-utils.d.ts +8 -0
  63. package/dist/live/semver-utils.js +26 -3
  64. package/dist/live/severity-scope.d.ts +26 -1
  65. package/dist/live/severity-scope.js +32 -0
  66. package/dist/live/shell-safe.d.ts +19 -0
  67. package/dist/live/shell-safe.js +36 -0
  68. package/dist/live/types.d.ts +134 -8
  69. package/dist/live/version-compare.d.ts +49 -0
  70. package/dist/live/version-compare.js +131 -0
  71. package/dist/live/withdrawn-releases.d.ts +50 -0
  72. package/dist/live/withdrawn-releases.js +125 -0
  73. package/dist/project-manifests.d.ts +7 -0
  74. package/dist/project-manifests.js +45 -5
  75. package/dist/project-scanner.js +6 -4
  76. package/dist/project-tree.d.ts +56 -0
  77. package/dist/project-tree.js +174 -5
  78. package/dist/schema-registry.d.ts +47 -4
  79. package/dist/schema-registry.js +76 -9
  80. package/dist/server-instructions.js +4 -2
  81. package/dist/setup.d.ts +8 -0
  82. package/dist/setup.js +21 -2
  83. package/dist/tool-dispatch.js +3 -3
  84. package/dist/tools/agent-memory.d.ts +5 -0
  85. package/dist/tools/agent-memory.js +9 -0
  86. package/dist/tools/briefing-task-scope.js +5 -2
  87. package/dist/tools/decision-memory.js +2 -0
  88. package/dist/tools/dependency-check-held.d.ts +44 -0
  89. package/dist/tools/dependency-check-held.js +98 -0
  90. package/dist/tools/dependency-check-signals.d.ts +19 -2
  91. package/dist/tools/dependency-check-signals.js +84 -13
  92. package/dist/tools/dependency-check-versions.d.ts +37 -4
  93. package/dist/tools/dependency-check-versions.js +48 -13
  94. package/dist/tools/dependency-check.d.ts +11 -1
  95. package/dist/tools/dependency-check.js +81 -29
  96. package/dist/tools/dependency-health.js +209 -45
  97. package/dist/tools/get-context.d.ts +3 -0
  98. package/dist/tools/get-context.js +4 -0
  99. package/dist/tools/project-scope.d.ts +8 -0
  100. package/dist/tools/project-scope.js +21 -0
  101. package/dist/tools/upgrade-impact-features.d.ts +37 -0
  102. package/dist/tools/upgrade-impact-features.js +176 -0
  103. package/dist/tools/upgrade-impact-literals.js +26 -4
  104. package/dist/tools/upgrade-impact-report.d.ts +5 -1
  105. package/dist/tools/upgrade-impact-report.js +26 -5
  106. package/dist/tools/upgrade-impact.js +10 -3
  107. package/dist/tools/upgrade-planner-fixpaths.d.ts +162 -5
  108. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  109. package/dist/tools/upgrade-planner.d.ts +59 -4
  110. package/dist/tools/upgrade-planner.js +237 -61
  111. package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
  112. package/dist/tools/vulnerability-scan-concise.js +37 -3
  113. package/dist/tools/vulnerability-scan-format.d.ts +126 -24
  114. package/dist/tools/vulnerability-scan-format.js +193 -26
  115. package/dist/tools/vulnerability-scan.d.ts +224 -63
  116. package/dist/tools/vulnerability-scan.js +50 -15
  117. package/dist/tools/what-should-i-know.d.ts +6 -1
  118. package/dist/tools/what-should-i-know.js +72 -15
  119. package/package.json +2 -2
@@ -0,0 +1,36 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Which values may be spliced into a command an agent is told to run.
4
+ *
5
+ * Package names, installed versions, Cargo dependency keys and `[target.'cfg(...)']`
6
+ * headers come from the scanned project's own files: a cloned repository controls
7
+ * them. upgrade_planner put a Cargo.toml target header inside `--target "..."`,
8
+ * where bash and PowerShell still expand `$(...)` and backticks, and a dependency
9
+ * key unquoted after `--rename` (audit 6.2.5 C-1). A command is named only when
10
+ * every spliced value is a plain token no shell (sh, bash, zsh, cmd.exe,
11
+ * PowerShell) reads specially; otherwise the caller falls back to words.
12
+ */
13
+ /** npm (scoped or not), crates.io and PyPI names: letters, digits, `.`, `_`, `-`; npm's `@scope/`. */
14
+ const PACKAGE_NAME = /^(?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/;
15
+ /** Go module paths: host/path segments of letters, digits, `.`, `_`, `~`, `-`. */
16
+ const GO_MODULE = /^[A-Za-z0-9][A-Za-z0-9._~-]*(?:\/[A-Za-z0-9._~-]+)*$/;
17
+ /** Versions as every supported ecosystem writes them: `1.2.3`, `v1.2.3`, `1.0.0-rc.1+build`, `2.0.0b1`. */
18
+ const VERSION = /^v?[0-9A-Za-z][0-9A-Za-z.+_-]*$/;
19
+ /** A Cargo target given as a plain triple (`x86_64-pc-windows-msvc`). */
20
+ const TARGET_TRIPLE = /^[A-Za-z0-9_.-]+$/;
21
+ /** A Cargo `cfg(...)` target with no string literals: `cfg(windows)`, `cfg(not(unix))`, `cfg(all(unix, target_pointer_width = 64))`. */
22
+ const CFG_PLAIN = /^cfg\([A-Za-z0-9_(), =]*\)$/;
23
+ export function isShellSafeName(name, ecosystem) {
24
+ return (ecosystem === "Go" ? GO_MODULE : PACKAGE_NAME).test(name) && name.length <= 214;
25
+ }
26
+ export function isShellSafeVersion(version) {
27
+ return VERSION.test(version) && version.length <= 128;
28
+ }
29
+ /**
30
+ * A Cargo target header that can go inside double quotes on every shell. A `cfg` with a string
31
+ * literal (`cfg(target_os = "linux")`) cannot: its quotes end the argument early. It is left to
32
+ * the words, which was always the only correct answer for it.
33
+ */
34
+ export function isShellSafeCargoTarget(target) {
35
+ return (TARGET_TRIPLE.test(target) || CFG_PLAIN.test(target)) && target.length <= 256;
36
+ }
@@ -86,7 +86,8 @@ export interface OsvVulnerability {
86
86
  score: string;
87
87
  }>;
88
88
  affected?: Array<{
89
- package: {
89
+ /** Optional in the OSV schema: an entry with only GIT ranges has no package (EEF-CVE-2026-56812). */
90
+ package?: {
90
91
  name: string;
91
92
  ecosystem: string;
92
93
  };
@@ -101,14 +102,20 @@ export interface OsvVulnerability {
101
102
  }>;
102
103
  /** Versions listed explicitly, in addition to (or instead of) the ranges. */
103
104
  versions?: string[];
105
+ /** RustSec puts `informational` (unmaintained | unsound | notice) here, per package. */
106
+ database_specific?: {
107
+ informational?: string;
108
+ };
104
109
  }>;
105
110
  references?: Array<{
106
111
  type: string;
107
112
  url: string;
108
113
  }>;
109
- /** GitHub-advisory severity label: LOW | MODERATE | HIGH | CRITICAL. */
114
+ /** GitHub-advisory severity label: LOW | MODERATE | HIGH | CRITICAL; `cwe_ids` (CWE-506 = malicious code). */
110
115
  database_specific?: {
111
116
  severity?: string;
117
+ cwe_ids?: string[];
118
+ informational?: string;
112
119
  };
113
120
  /** Set (to an RFC3339 timestamp) when the advisory has been withdrawn/retracted. */
114
121
  withdrawn?: string;
@@ -126,6 +133,22 @@ export interface VulnerabilityEntry {
126
133
  aliases: string[];
127
134
  severity: "critical" | "high" | "medium" | "low" | "unknown";
128
135
  cvssScore: number | null;
136
+ /**
137
+ * Set when `severity` is not this record's own: it rates nothing (Go's
138
+ * records) and names a GHSA alias, whose rating it took (osv-scanner.ts
139
+ * rateFromAliases). The alias's id.
140
+ */
141
+ severityFrom?: string;
142
+ /** A malicious-package record (malware.ts isMalware: a MAL- id or alias, or CWE-506). */
143
+ malware?: boolean;
144
+ /**
145
+ * Whether the record makes this a maintenance notice for this package
146
+ * (maintenance.ts isMaintenanceRecord: a stated `informational` class decides, the
147
+ * wording only when none is stated). Absent only on foreign or stub entries, which
148
+ * fall back to the summary wording; the scan cache key moved to osv:v3 so entries
149
+ * cached by 6.2.4 without it are never read.
150
+ */
151
+ maintenance?: boolean;
129
152
  summary: string;
130
153
  fixedVersion: string | null;
131
154
  published: string;
@@ -164,10 +187,55 @@ export interface VulnerabilityScanResult {
164
187
  unknown: number;
165
188
  };
166
189
  vulnerabilities: VulnerabilityEntry[];
190
+ /** Dependencies OSV answered for and found no advisory on (unchecked ones are never counted clean). */
167
191
  cleanCount: number;
168
192
  scanDurationMs: number;
169
193
  cached: boolean;
194
+ /** True when any dependency has no advisory answer (`unchecked` > 0), or lookups are disabled. */
170
195
  offline: boolean;
196
+ /**
197
+ * Dependencies with NO advisory answer: OSV.dev could not be reached, failed,
198
+ * timed out, or the query budget ran out, and no earlier answer was cached.
199
+ * Their absence from `vulnerabilities` means nothing. Absent (= 0) on older
200
+ * stubs.
201
+ */
202
+ unchecked?: number;
203
+ /** Why `unchecked` dependencies have no answer, in words; null when every one was answered. */
204
+ uncheckedReason?: string | null;
205
+ /** Dependencies answered from an OSV answer older than the 1 h cache life, because a fresh query failed. */
206
+ staleAnswers?: number;
207
+ /** Why fresh answers were not had for the `staleAnswers` dependencies. */
208
+ staleReason?: string;
209
+ /** fetched_at (SQLite UTC, "YYYY-MM-DD HH:MM:SS") of the oldest stale answer used. */
210
+ staleOldestFetchedAt?: string | null;
211
+ /** Records rated from a GHSA alias, and those that name one but stay unknown (cap, time budget, no answer), with why. */
212
+ aliasSeverity?: AliasSeverityReport;
213
+ }
214
+ export interface AliasSeverityReport {
215
+ /** Records whose severity came from their GHSA alias. */
216
+ rated: number;
217
+ /** GHSA alias ids that could not be read (cap, time budget, no answer): they never lower a grade. */
218
+ unresolved: string[];
219
+ /** Record ids that name a GHSA alias and are still unknown: no alias of theirs could be read or graded. */
220
+ ungraded: string[];
221
+ /** Why `unresolved` or `ungraded` is not empty; null when both are. */
222
+ reason: string | null;
223
+ }
224
+ /** How much of a dependency set OSV actually answered for. */
225
+ export interface VulnerabilityCoverage {
226
+ complete: false;
227
+ /** Dependencies sent for checking. */
228
+ total_dependencies: number;
229
+ /** Dependencies with an advisory answer (fresh or stale). */
230
+ checked_dependencies: number;
231
+ /** Dependencies with NO answer: nothing is known about their advisories. */
232
+ unchecked_dependencies: number;
233
+ /** Dependencies answered only from an expired cached answer (a fresh query failed); present when any were. */
234
+ stale_dependencies?: number;
235
+ /** Why, in words ("OSV.dev unreachable (fetch failed)", "live lookups disabled (FOURDA_OFFLINE=true)"). */
236
+ reason: string;
237
+ /** One line to read first; never a clean result. */
238
+ note: string;
171
239
  }
172
240
  export interface LiveHeadline {
173
241
  id: string;
@@ -213,6 +281,14 @@ export interface RegistryPackageInfo {
213
281
  * workspaces are listed comma-separated.
214
282
  */
215
283
  installedVersion?: string;
284
+ /**
285
+ * dependency_health: present when `latestStableVersion` is NOT offered as an upgrade: an advisory
286
+ * affects it or the registry withdrew it. The newest release above `currentVersion` that is
287
+ * neither (null when none), which `versionsBehind` is then measured to.
288
+ */
289
+ latestSafeVersion?: string | null;
290
+ /** dependency_health: why `latestStableVersion` is not offered, or that it could not be checked. */
291
+ latestStableNote?: string;
216
292
  }
217
293
  export interface SemverDistance {
218
294
  major: number;
@@ -224,7 +300,23 @@ export interface DependencyHealthResult {
224
300
  scannedAt: string;
225
301
  projectPath: string;
226
302
  ecosystemsScanned: string[];
303
+ /**
304
+ * Direct dependencies checked against their registries (the rows in
305
+ * `dependencies`; devDependencies only with include_dev). Freshness,
306
+ * deprecation and `outdatedCount` / `deprecatedCount` are over these.
307
+ */
227
308
  totalDeps: number;
309
+ /** Installed packages (direct and transitive) the vulnerability scan covered: what `vulnerableCount` ranges over. */
310
+ treeDeps?: number;
311
+ /** Of `vulnerableCount`, the packages a manifest declares directly. Null when the check is incomplete. */
312
+ vulnerableDirectCount?: number | null;
313
+ /** The penalties the score subtracts from 100, and how each is computed. Absent when the score is withheld. */
314
+ healthScoreBasis?: {
315
+ vulnerabilities: number;
316
+ outdated: number;
317
+ deprecated: number;
318
+ note: string;
319
+ };
228
320
  outdatedCount: number;
229
321
  deprecatedCount: number;
230
322
  /**
@@ -232,21 +324,55 @@ export interface DependencyHealthResult {
232
324
  * maintenance notice — the same set `vulnerability_scan` reports as
233
325
  * `total_vulnerable_packages`, so the two tools agree over one scan.
234
326
  */
235
- vulnerableCount: number;
236
327
  /**
237
- * Every advisory row the scan returned, before the platform and maintenance
238
- * splits and before alias collapsing. Present so nothing is hidden by the
239
- * `vulnerableCount` filter: the difference is platform-inactive packages and
240
- * unmaintained-dependency notices.
328
+ * Distinct vulnerable packages across the full installed tree (`treeDeps`),
329
+ * the same set vulnerability_scan reports as `total_vulnerable_packages`
330
+ * for the same scope. It can exceed `totalDeps`, which counts direct
331
+ * dependencies only. Null when any dependency could not be checked.
332
+ */
333
+ vulnerableCount: number | null;
334
+ /**
335
+ * Distinct advisories the scan returned (records naming each other, such as
336
+ * a GHSA and its PYSEC twin, are one advisory per installed copy, as in
337
+ * vulnerability_scan), before the platform and maintenance splits. Present
338
+ * so nothing is hidden by the `vulnerableCount` filter: the difference is
339
+ * platform-inactive packages and unmaintained-dependency notices.
241
340
  */
242
341
  advisoryCount: number;
243
- healthScore: number;
342
+ /** Null when vulnerability data is missing or incomplete: a score computed without it would read as healthier than it is. */
343
+ healthScore: number | null;
344
+ /**
345
+ * Why `healthScore` is null: OSV gave no answer for some dependencies, a
346
+ * lockfile could not be read, or nothing was resolved at all. Absent when
347
+ * there is a score.
348
+ */
349
+ withheldReason?: "vulnerability_data_incomplete" | "lockfile_unreadable" | "nothing_checked";
350
+ /**
351
+ * Present when the score is over the checked part only: a lockfile read with
352
+ * entries skipped (PARTIAL) or a requirements include refused (REFUSED).
353
+ * The counts are lower bounds; `notes` say what is missing.
354
+ */
355
+ scoreCoverage?: {
356
+ complete: false;
357
+ basis: "checked_part";
358
+ notes: string[];
359
+ };
360
+ /** Records graded from a GHSA alias, and those still unknown, as in vulnerability_scan's `_meta.alias_severity`. */
361
+ aliasSeverity?: AliasSeverityReport;
362
+ /** Present when the vulnerability check is incomplete: what was not checked and why. */
363
+ vulnerabilityCoverage?: VulnerabilityCoverage;
244
364
  dependencies: RegistryPackageInfo[];
365
+ /**
366
+ * Advisories (not packages) on the full tree by their own grade, over the same actionable set: the
367
+ * same counts as vulnerability_scan's `advisory_by_severity` for the same scope. `unknown` counts the
368
+ * ungraded ones, which were left out of every bucket before (pygoat: 186 counted of 187).
369
+ */
245
370
  vulnerabilitySummary: {
246
371
  critical: number;
247
372
  high: number;
248
373
  medium: number;
249
374
  low: number;
375
+ unknown: number;
250
376
  } | null;
251
377
  summary: string;
252
378
  scanDurationMs: number;
@@ -25,13 +25,62 @@ interface Pep440 {
25
25
  }
26
26
  export declare function parsePep440(version: string): Pep440 | null;
27
27
  export declare function comparePep440(a: Pep440, b: Pep440): number;
28
+ /**
29
+ * A Composer version as `VersionParser::normalize` reads it: an optional `v`,
30
+ * up to four (here: any number of) numeric parts padded with zeros, then a
31
+ * stability suffix. OSV's Packagist ranges use two-part bounds
32
+ * (`introduced: "6.4"`, symfony/process GHSA-r39x-jcww-82v6), which
33
+ * MAJOR.MINOR.PATCH could not read: compared as "equal" to everything, the
34
+ * 5.4 line's fix 5.4.51 counted as inside [6.4, 6.4.33) and the target jumped
35
+ * to 8.0.5 (readiness audit H2, 2026-10-16). `1.2` is `1.2.0.0`.
36
+ */
37
+ interface ComposerVersion {
38
+ release: number[];
39
+ /** dev=0 < alpha=1 < beta=2 < RC=3 < stable=4 < patch=5. */
40
+ stability: number;
41
+ stabilityNum: number[];
42
+ /** A trailing `-dev` (`1.0.0-beta2-dev`): below the same version without it. */
43
+ dev: boolean;
44
+ }
45
+ export declare function parseComposerVersion(version: string): ComposerVersion | null;
46
+ export declare function compareComposer(a: ComposerVersion, b: ComposerVersion): number;
47
+ /**
48
+ * Gem::Version's canonical segments: `-` reads as `.pre.`, the text splits
49
+ * into runs of digits and letters, and trailing zeros of the release part
50
+ * and of the prerelease part are dropped, so `3.0` equals `3.0.0` (OSV's
51
+ * RubyGems bounds are two-part: rack GHSA-6xw4-3v39-52mm `introduced: "3.0"`).
52
+ */
53
+ export declare function parseGemVersion(version: string): Array<number | string> | null;
54
+ /** Gem::Version#<=>: a missing segment is 0, a letter run sorts below a number. */
55
+ export declare function compareGem(a: Array<number | string>, b: Array<number | string>): number;
56
+ /**
57
+ * A version as the ecosystem itself writes it, for output. Go module versions always carry a `v`
58
+ * (go.mod, `go get`), while OSV's Go ranges and the proxy list read here do not: targetVersion
59
+ * 0.60.0 sat beside currentVersion v0.60.0's line (final release audit of 6.2.5, 7e). Every other
60
+ * ecosystem is returned as it is.
61
+ */
62
+ export declare function inEcosystemForm<T extends string | null | undefined>(ecosystem: string, version: T): T;
28
63
  /** Whether `version` is readable for this ecosystem's ordering. */
29
64
  export declare function isComparable(version: string, ecosystem: OsvEcosystem | string): boolean;
65
+ /** Whether a readable version is a prerelease in Composer's or Gem::Version's own terms; null for other ecosystems or unreadable text. */
66
+ export declare function isComposerOrGemPrerelease(version: string, ecosystem: OsvEcosystem | string): boolean | null;
30
67
  /**
31
68
  * Order two versions of one ecosystem: 1, -1, or 0 for equal OR unreadable
32
69
  * (the same contract as compareSemver, so an unreadable bound never decides).
33
70
  */
34
71
  export declare function compareVersions(a: string, b: string, ecosystem: OsvEcosystem | string): number;
72
+ /**
73
+ * `version` against an advisory window's LOWER bound (`introduced`). As
74
+ * Composer reads a constraint, a Packagist bound with no stability suffix
75
+ * opens at that release's dev stability (`>=8.0` is `>= 8.0.0.0-dev`), so the
76
+ * line's own prereleases are inside it: GHSA-r39x-jcww-82v6 opens at `8.0`
77
+ * and lists v8.0.0-BETA1 and v8.0.0-RC1 as affected. Compared as a plain
78
+ * version they fell below it (review of 6.2.5 H2, 2026-10-18). The upper
79
+ * bound keeps the plain order on purpose: a prerelease of the fix release
80
+ * (8.0.5-RC1) stays affected, the safe reading (Composer would exclude it).
81
+ * Every other ecosystem: compareVersions.
82
+ */
83
+ export declare function compareToLowerBound(version: string, bound: string, ecosystem: OsvEcosystem | string): number;
35
84
  /** Highest of `versions` in this ecosystem's order, or null when empty. */
36
85
  export declare function maxVersion(versions: string[], ecosystem: OsvEcosystem | string): string | null;
37
86
  /** Package-name equality as the ecosystem's registry defines it. */
@@ -77,10 +77,95 @@ function compareLocal(a, b) {
77
77
  }
78
78
  return Math.sign(a.length - b.length);
79
79
  }
80
+ const COMPOSER = /^v?(\d+(?:\.\d+){0,3})(?:[._-]?(stable|beta|b|alpha|a|rc|patch|pl|p)((?:[.-]?\d+)*))?(?:[.-]?(dev))?(?:\+[0-9a-z.-]+)?$/i;
81
+ const COMPOSER_RANK = { alpha: 1, a: 1, beta: 2, b: 2, rc: 3, stable: 4, patch: 5, pl: 5, p: 5 };
82
+ export function parseComposerVersion(version) {
83
+ const m = COMPOSER.exec(version.trim());
84
+ if (!m)
85
+ return null;
86
+ const token = m[2]?.toLowerCase();
87
+ const devOnly = !token && m[4] !== undefined;
88
+ return {
89
+ release: m[1].split(".").map(Number),
90
+ stability: devOnly ? 0 : token ? COMPOSER_RANK[token] : 4,
91
+ stabilityNum: m[3] ? m[3].split(/[.-]/).filter(Boolean).map(Number) : [],
92
+ dev: !devOnly && m[4] !== undefined,
93
+ };
94
+ }
95
+ function comparePadded(a, b) {
96
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
97
+ const x = a[i] ?? 0;
98
+ const y = b[i] ?? 0;
99
+ if (x !== y)
100
+ return x > y ? 1 : -1;
101
+ }
102
+ return 0;
103
+ }
104
+ export function compareComposer(a, b) {
105
+ const byRelease = comparePadded(a.release, b.release) || Math.sign(a.stability - b.stability);
106
+ if (byRelease !== 0)
107
+ return byRelease;
108
+ // A bare tag sorts below a numbered one: `beta` < `beta0` (composer/semver, by version_compare).
109
+ const bareA = a.stabilityNum.length === 0;
110
+ const bareB = b.stabilityNum.length === 0;
111
+ if (bareA !== bareB)
112
+ return bareA ? -1 : 1;
113
+ return comparePadded(a.stabilityNum, b.stabilityNum) || (a.dev === b.dev ? 0 : a.dev ? -1 : 1);
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // Gem::Version (RubyGems)
117
+ // ---------------------------------------------------------------------------
118
+ /** Gem::Version::ANCHORED_VERSION_PATTERN. */
119
+ const GEM = /^[0-9]+(?:\.[0-9a-zA-Z]+)*(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
120
+ /**
121
+ * Gem::Version's canonical segments: `-` reads as `.pre.`, the text splits
122
+ * into runs of digits and letters, and trailing zeros of the release part
123
+ * and of the prerelease part are dropped, so `3.0` equals `3.0.0` (OSV's
124
+ * RubyGems bounds are two-part: rack GHSA-6xw4-3v39-52mm `introduced: "3.0"`).
125
+ */
126
+ export function parseGemVersion(version) {
127
+ const v = version.trim();
128
+ if (!GEM.test(v))
129
+ return null;
130
+ const segments = (v.replace(/-/g, ".pre.").match(/[0-9]+|[a-z]+/gi) ?? []).map((s) => (/^\d/.test(s) ? Number(s) : s));
131
+ const firstString = segments.findIndex((s) => typeof s === "string");
132
+ const cut = firstString < 0 ? segments.length : firstString;
133
+ const trim = (part) => {
134
+ let end = part.length;
135
+ while (end > 0 && part[end - 1] === 0)
136
+ end--;
137
+ return part.slice(0, end);
138
+ };
139
+ return [...trim(segments.slice(0, cut)), ...trim(segments.slice(cut))];
140
+ }
141
+ /** Gem::Version#<=>: a missing segment is 0, a letter run sorts below a number. */
142
+ export function compareGem(a, b) {
143
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
144
+ const x = a[i] ?? 0;
145
+ const y = b[i] ?? 0;
146
+ if (x === y)
147
+ continue;
148
+ if (typeof x !== typeof y)
149
+ return typeof x === "string" ? -1 : 1;
150
+ return x > y ? 1 : -1;
151
+ }
152
+ return 0;
153
+ }
80
154
  // ---------------------------------------------------------------------------
81
155
  // Dispatch
82
156
  // ---------------------------------------------------------------------------
83
157
  const stripGoV = (v) => v.replace(/^v(?=\d)/, "");
158
+ /**
159
+ * A version as the ecosystem itself writes it, for output. Go module versions always carry a `v`
160
+ * (go.mod, `go get`), while OSV's Go ranges and the proxy list read here do not: targetVersion
161
+ * 0.60.0 sat beside currentVersion v0.60.0's line (final release audit of 6.2.5, 7e). Every other
162
+ * ecosystem is returned as it is.
163
+ */
164
+ export function inEcosystemForm(ecosystem, version) {
165
+ if (ecosystem !== "Go" || typeof version !== "string" || !/^\d/.test(version))
166
+ return version;
167
+ return `v${version}`;
168
+ }
84
169
  /** Whether `version` is readable for this ecosystem's ordering. */
85
170
  export function isComparable(version, ecosystem) {
86
171
  if (version === "0")
@@ -93,10 +178,26 @@ export function isComparable(version, ecosystem) {
93
178
  case "npm":
94
179
  case "crates.io":
95
180
  return parseSemverPrecedence(version) !== null;
181
+ case "Packagist":
182
+ return parseComposerVersion(version) !== null;
183
+ case "RubyGems":
184
+ return parseGemVersion(version) !== null;
96
185
  default:
97
186
  return parseSemver(version) !== null;
98
187
  }
99
188
  }
189
+ /** Whether a readable version is a prerelease in Composer's or Gem::Version's own terms; null for other ecosystems or unreadable text. */
190
+ export function isComposerOrGemPrerelease(version, ecosystem) {
191
+ if (ecosystem === "Packagist") {
192
+ const c = parseComposerVersion(version);
193
+ return c ? c.stability < 4 || c.dev : null;
194
+ }
195
+ if (ecosystem === "RubyGems") {
196
+ const g = parseGemVersion(version);
197
+ return g ? g.some((s) => typeof s === "string") : null;
198
+ }
199
+ return null;
200
+ }
100
201
  /**
101
202
  * Order two versions of one ecosystem: 1, -1, or 0 for equal OR unreadable
102
203
  * (the same contract as compareSemver, so an unreadable bound never decides).
@@ -119,10 +220,40 @@ export function compareVersions(a, b, ecosystem) {
119
220
  case "npm":
120
221
  case "crates.io":
121
222
  return compareVersionPrecedence(a, b) ?? compareSemver(a, b);
223
+ case "Packagist": {
224
+ const ca = parseComposerVersion(a);
225
+ const cb = parseComposerVersion(b);
226
+ return ca && cb ? compareComposer(ca, cb) : 0;
227
+ }
228
+ case "RubyGems": {
229
+ const ga = parseGemVersion(a);
230
+ const gb = parseGemVersion(b);
231
+ return ga && gb ? compareGem(ga, gb) : 0;
232
+ }
122
233
  default:
123
234
  return compareSemver(a, b);
124
235
  }
125
236
  }
237
+ /**
238
+ * `version` against an advisory window's LOWER bound (`introduced`). As
239
+ * Composer reads a constraint, a Packagist bound with no stability suffix
240
+ * opens at that release's dev stability (`>=8.0` is `>= 8.0.0.0-dev`), so the
241
+ * line's own prereleases are inside it: GHSA-r39x-jcww-82v6 opens at `8.0`
242
+ * and lists v8.0.0-BETA1 and v8.0.0-RC1 as affected. Compared as a plain
243
+ * version they fell below it (review of 6.2.5 H2, 2026-10-18). The upper
244
+ * bound keeps the plain order on purpose: a prerelease of the fix release
245
+ * (8.0.5-RC1) stays affected, the safe reading (Composer would exclude it).
246
+ * Every other ecosystem: compareVersions.
247
+ */
248
+ export function compareToLowerBound(version, bound, ecosystem) {
249
+ if (ecosystem === "Packagist" && bound !== "0") {
250
+ const v = parseComposerVersion(version);
251
+ const b = parseComposerVersion(bound);
252
+ if (v && b && b.stability === 4 && !/stable/i.test(bound))
253
+ return compareComposer(v, { ...b, stability: 0, stabilityNum: [], dev: false });
254
+ }
255
+ return compareVersions(version, bound, ecosystem);
256
+ }
126
257
  /** Highest of `versions` in this ecosystem's order, or null when empty. */
127
258
  export function maxVersion(versions, ecosystem) {
128
259
  if (versions.length === 0)
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Which releases a registry has withdrawn, so that no fix target is ever one
3
+ * of them (4DA FX-H5, the app's `osv/release_facts.rs`; the rule the shared
4
+ * fix_target vectors pin, README "Optional fix_target registry fields").
5
+ *
6
+ * - crates.io: `yanked`.
7
+ * - PyPI: a release whose every file is `yanked` (PEP 592).
8
+ * - npm: `deprecated`, but ONLY when a later, non-deprecated, stable release
9
+ * exists on the same caret line (same major; same minor for 0.x). npm's
10
+ * `deprecated` is often a whole-major "please upgrade" notice rather than a
11
+ * withdrawn release: glob deprecates 160 of its 168 versions ("Old versions
12
+ * of glob are not supported"), 10.5.0 included, and excluding them all sent
13
+ * glob 10.4.5 to 12.0.0, a major jump. lodash 4.18.0 ("Bad release") is
14
+ * excluded because 4.18.1 is clean on 4.x. A package whose `latest` is
15
+ * itself deprecated (retired whole) excludes nothing.
16
+ *
17
+ * Pure functions; the registry readers (fix-paths.ts, npm-packument.ts) call
18
+ * them on what they already fetched.
19
+ */
20
+ import type { PackageReleaseIndex } from "./release-metadata.js";
21
+ /**
22
+ * dependency_check's view: every release of `index` that is never a candidate, with the
23
+ * registry's words. Withdrawn records (crates.io/PyPI yanked, npm unpublished, Go retracted:
24
+ * none of them installable as asked) plus the npm deprecations the rule above withdraws.
25
+ */
26
+ export declare function excludedReleases(index: PackageReleaseIndex): Map<string, string>;
27
+ /** npm's per-version `deprecated`: a non-empty message, or `true` (the app's `npm_deprecated`). */
28
+ export declare function isNpmDeprecated(value: unknown): boolean;
29
+ /**
30
+ * The npm releases to exclude as fix targets, from a packument's `versions`
31
+ * (version -> its `deprecated`) and `dist-tags.latest` (see the module notes).
32
+ */
33
+ export declare function npmExcludedVersions(deprecation: Record<string, unknown>, latest: string | null | undefined): string[];
34
+ /** How a registry names a withdrawal, for the release `version`. */
35
+ export declare function withdrawalText(ecosystem: string, version: string, detail?: string | null): string;
36
+ /**
37
+ * The withdrawn releases in plain words, releases with the same reason said once:
38
+ * "7.5.3, 7.5.4 and 7.5.5 are deprecated on npm ("Old versions of tar ...")".
39
+ */
40
+ export declare function sayWithdrawn(versions: string[], why: Record<string, string>, ecosystem: string): string;
41
+ /**
42
+ * The plain-words note for a target moved past withdrawn releases, or for no
43
+ * target because every release that would fix it was withdrawn (the app's
44
+ * Upgrade Plan wording: "No installable fix").
45
+ * "4.18.0 is deprecated on npm ("Bad release..."); 4.18.1 is the smallest clean release"
46
+ * "No installable fix: 0.3.17 is yanked on crates.io, and no later release is known to clear it"
47
+ * `why` holds the registry's own text per withdrawn release (withdrawalText).
48
+ * `clean: false`: the target clears only the advisories a release fixes.
49
+ */
50
+ export declare function withheldNote(withheld: string[], target: string | null, why: Record<string, string>, ecosystem: string, clean?: boolean): string | null;
@@ -0,0 +1,125 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Which releases a registry has withdrawn, so that no fix target is ever one
4
+ * of them (4DA FX-H5, the app's `osv/release_facts.rs`; the rule the shared
5
+ * fix_target vectors pin, README "Optional fix_target registry fields").
6
+ *
7
+ * - crates.io: `yanked`.
8
+ * - PyPI: a release whose every file is `yanked` (PEP 592).
9
+ * - npm: `deprecated`, but ONLY when a later, non-deprecated, stable release
10
+ * exists on the same caret line (same major; same minor for 0.x). npm's
11
+ * `deprecated` is often a whole-major "please upgrade" notice rather than a
12
+ * withdrawn release: glob deprecates 160 of its 168 versions ("Old versions
13
+ * of glob are not supported"), 10.5.0 included, and excluding them all sent
14
+ * glob 10.4.5 to 12.0.0, a major jump. lodash 4.18.0 ("Bad release") is
15
+ * excluded because 4.18.1 is clean on 4.x. A package whose `latest` is
16
+ * itself deprecated (retired whole) excludes nothing.
17
+ *
18
+ * Pure functions; the registry readers (fix-paths.ts, npm-packument.ts) call
19
+ * them on what they already fetched.
20
+ */
21
+ import { comparePrecedence, parseSemverPrecedence } from "./semver-precedence.js";
22
+ /**
23
+ * dependency_check's view: every release of `index` that is never a candidate, with the
24
+ * registry's words. Withdrawn records (crates.io/PyPI yanked, npm unpublished, Go retracted:
25
+ * none of them installable as asked) plus the npm deprecations the rule above withdraws.
26
+ */
27
+ export function excludedReleases(index) {
28
+ const out = new Map();
29
+ const releases = Object.values(index.releases);
30
+ for (const r of releases) {
31
+ if (r.withdrawn)
32
+ out.set(r.version, r.withdrawn === "yanked" ? withdrawalText(index.ecosystem, r.version) : `${r.version} was ${r.withdrawn} from the registry`);
33
+ }
34
+ if (index.ecosystem === "npm") {
35
+ const live = Object.fromEntries(releases.filter((r) => !r.withdrawn).map((r) => [r.version, r.deprecated]));
36
+ for (const v of npmExcludedVersions(live, index.latest))
37
+ out.set(v, withdrawalText("npm", v, index.releases[v]?.deprecated));
38
+ }
39
+ return out;
40
+ }
41
+ /** npm's per-version `deprecated`: a non-empty message, or `true` (the app's `npm_deprecated`). */
42
+ export function isNpmDeprecated(value) {
43
+ return (typeof value === "string" && value !== "") || value === true;
44
+ }
45
+ /**
46
+ * The npm releases to exclude as fix targets, from a packument's `versions`
47
+ * (version -> its `deprecated`) and `dist-tags.latest` (see the module notes).
48
+ */
49
+ export function npmExcludedVersions(deprecation, latest) {
50
+ if (latest && latest in deprecation && isNpmDeprecated(deprecation[latest]))
51
+ return [];
52
+ const clean = Object.entries(deprecation)
53
+ .filter(([, d]) => !isNpmDeprecated(d))
54
+ .map(([v]) => parseSemverPrecedence(v))
55
+ .filter((p) => p !== null && p.prerelease.length === 0);
56
+ return Object.entries(deprecation)
57
+ .filter(([, d]) => isNpmDeprecated(d))
58
+ .filter(([v]) => {
59
+ const p = parseSemverPrecedence(v);
60
+ return p !== null && clean.some((c) => c.major === p.major && (p.major > 0 || c.minor === p.minor) && comparePrecedence(c, p) > 0);
61
+ })
62
+ .map(([v]) => v);
63
+ }
64
+ /** How a registry names a withdrawal, for the release `version`. */
65
+ export function withdrawalText(ecosystem, version, detail) {
66
+ const quoted = detail?.trim() ? ` ("${clip(detail.trim(), 120)}")` : "";
67
+ switch (ecosystem) {
68
+ case "npm":
69
+ return `${version} is deprecated on npm${quoted}`;
70
+ case "crates.io":
71
+ return `${version} is yanked on crates.io`;
72
+ case "PyPI":
73
+ return `${version} is yanked on PyPI${quoted}`;
74
+ default:
75
+ return `${version} is withdrawn on the registry${quoted}`;
76
+ }
77
+ }
78
+ /** At most `max` characters, cut at a word, "..." when cut. */
79
+ function clip(text, max) {
80
+ if (text.length <= max)
81
+ return text;
82
+ const cut = text.slice(0, max);
83
+ return `${cut.slice(0, Math.max(cut.lastIndexOf(" "), max / 2)).replace(/[\s,;:.]+$/, "")}...`;
84
+ }
85
+ /**
86
+ * The withdrawn releases in plain words, releases with the same reason said once:
87
+ * "7.5.3, 7.5.4 and 7.5.5 are deprecated on npm ("Old versions of tar ...")".
88
+ */
89
+ export function sayWithdrawn(versions, why, ecosystem) {
90
+ const groups = [];
91
+ for (const v of versions) {
92
+ const text = why[v] ?? withdrawalText(ecosystem, v);
93
+ const rest = text.startsWith(`${v} is `) ? text.slice(v.length + " is ".length) : null;
94
+ const last = groups[groups.length - 1];
95
+ if (rest !== null && last && last.rest === rest)
96
+ last.versions.push(v);
97
+ else
98
+ groups.push({ rest: rest ?? text, versions: rest === null ? [] : [v] });
99
+ }
100
+ return groups
101
+ .map((g) => {
102
+ if (g.versions.length === 0)
103
+ return g.rest;
104
+ const names = g.versions.length === 1 ? g.versions[0] : `${g.versions.slice(0, -1).join(", ")} and ${g.versions[g.versions.length - 1]}`;
105
+ return `${names} ${g.versions.length === 1 ? "is" : "are"} ${g.rest}`;
106
+ })
107
+ .join("; ");
108
+ }
109
+ /**
110
+ * The plain-words note for a target moved past withdrawn releases, or for no
111
+ * target because every release that would fix it was withdrawn (the app's
112
+ * Upgrade Plan wording: "No installable fix").
113
+ * "4.18.0 is deprecated on npm ("Bad release..."); 4.18.1 is the smallest clean release"
114
+ * "No installable fix: 0.3.17 is yanked on crates.io, and no later release is known to clear it"
115
+ * `why` holds the registry's own text per withdrawn release (withdrawalText).
116
+ * `clean: false`: the target clears only the advisories a release fixes.
117
+ */
118
+ export function withheldNote(withheld, target, why, ecosystem, clean = true) {
119
+ if (withheld.length === 0)
120
+ return null;
121
+ const said = sayWithdrawn(withheld, why, ecosystem);
122
+ if (!target)
123
+ return `No installable fix: ${said}, and no later release is known to clear it — pin, patch, or replace`;
124
+ return `${said}; ${target} is the smallest ${clean ? "clean release" : "installable release that clears the advisories a release fixes"}`;
125
+ }
@@ -26,6 +26,13 @@ export declare function parsePyprojectDependencies(content: string): {
26
26
  };
27
27
  /** Requirement names (pins and ranges alike) from a requirements file, PEP 503 normalised. */
28
28
  export declare function parseRequirementNames(content: string): string[];
29
+ /**
30
+ * Requirement names from a requirements file and every file it includes with `-r` (confined to
31
+ * `root`, as the version reader confines them): an included file's pins are declared too. A pin
32
+ * in `-r sub/inner.txt` read as "Transitive dependency, not declared in your manifest" (final
33
+ * release audit of 6.2.5, 7g: jinja2==2.10).
34
+ */
35
+ export declare function requirementNamesWithIncludes(file: string, root?: string, visited?: Set<string>): string[];
29
36
  /** Direct Go dependencies: go.mod requirements not marked `// indirect`. */
30
37
  export declare function parseGoModDirectDeps(content: string): string[];
31
38
  /**