@4da/mcp-server 5.0.3 → 6.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.
Files changed (172) hide show
  1. package/CHANGELOG.md +667 -0
  2. package/README.md +66 -35
  3. package/dist/build-staleness.d.ts +35 -0
  4. package/dist/build-staleness.js +95 -0
  5. package/dist/db.d.ts +53 -2
  6. package/dist/db.js +242 -36
  7. package/dist/doctor.js +27 -7
  8. package/dist/embeddings.js +7 -1
  9. package/dist/freshness-markers.d.ts +34 -0
  10. package/dist/freshness-markers.js +74 -0
  11. package/dist/gitignore.d.ts +37 -0
  12. package/dist/gitignore.js +130 -0
  13. package/dist/index.js +127 -44
  14. package/dist/live/bun-lockfile.d.ts +26 -0
  15. package/dist/live/bun-lockfile.js +123 -0
  16. package/dist/live/cargo-platform.d.ts +38 -0
  17. package/dist/live/cargo-platform.js +178 -0
  18. package/dist/live/changelog-classify.d.ts +41 -0
  19. package/dist/live/changelog-classify.js +207 -0
  20. package/dist/live/changelog.d.ts +64 -0
  21. package/dist/live/changelog.js +318 -0
  22. package/dist/live/crates-registry.js +3 -1
  23. package/dist/live/crates-versions.d.ts +47 -0
  24. package/dist/live/crates-versions.js +201 -0
  25. package/dist/live/dependency-set.d.ts +13 -0
  26. package/dist/live/dependency-set.js +95 -0
  27. package/dist/live/dev-scope.d.ts +32 -0
  28. package/dist/live/dev-scope.js +102 -0
  29. package/dist/live/file-signature.d.ts +18 -0
  30. package/dist/live/file-signature.js +43 -0
  31. package/dist/live/go-registry.js +3 -1
  32. package/dist/live/hn-fetcher.js +9 -3
  33. package/dist/live/index.d.ts +105 -2
  34. package/dist/live/index.js +266 -139
  35. package/dist/live/install-state.d.ts +43 -0
  36. package/dist/live/install-state.js +150 -0
  37. package/dist/live/js-lockfile-readers.d.ts +46 -0
  38. package/dist/live/js-lockfile-readers.js +181 -0
  39. package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
  40. package/dist/live/lockfile-parsers-pygo.js +274 -0
  41. package/dist/live/lockfile-parsers.d.ts +69 -0
  42. package/dist/live/lockfile-parsers.js +311 -0
  43. package/dist/live/lockfile-types.d.ts +34 -0
  44. package/dist/live/lockfile-types.js +42 -0
  45. package/dist/live/maintenance.d.ts +33 -0
  46. package/dist/live/maintenance.js +37 -0
  47. package/dist/live/npm-packument.d.ts +47 -0
  48. package/dist/live/npm-packument.js +150 -0
  49. package/dist/live/npm-registry.js +67 -53
  50. package/dist/live/osv-scanner.d.ts +31 -1
  51. package/dist/live/osv-scanner.js +119 -10
  52. package/dist/live/package-archive.d.ts +49 -0
  53. package/dist/live/package-archive.js +186 -0
  54. package/dist/live/pypi-registry.js +3 -1
  55. package/dist/live/registry-health.d.ts +16 -0
  56. package/dist/live/registry-health.js +74 -0
  57. package/dist/live/release-metadata.d.ts +67 -0
  58. package/dist/live/release-metadata.js +16 -0
  59. package/dist/live/resolution.d.ts +43 -0
  60. package/dist/live/resolution.js +62 -0
  61. package/dist/live/semver-precedence.d.ts +30 -0
  62. package/dist/live/semver-precedence.js +83 -0
  63. package/dist/live/severity-scope.d.ts +57 -0
  64. package/dist/live/severity-scope.js +89 -0
  65. package/dist/live/types.d.ts +83 -0
  66. package/dist/live/upgrade-sources.d.ts +71 -0
  67. package/dist/live/upgrade-sources.js +198 -0
  68. package/dist/live/version-compare.d.ts +39 -0
  69. package/dist/live/version-compare.js +130 -0
  70. package/dist/live/version-resolver.d.ts +14 -1
  71. package/dist/live/version-resolver.js +93 -303
  72. package/dist/native-bindings.d.ts +19 -0
  73. package/dist/native-bindings.js +39 -0
  74. package/dist/project-manifests.d.ts +40 -0
  75. package/dist/project-manifests.js +193 -0
  76. package/dist/project-scanner.js +38 -48
  77. package/dist/project-tree.d.ts +41 -0
  78. package/dist/project-tree.js +110 -0
  79. package/dist/prompts.d.ts +28 -0
  80. package/dist/prompts.js +54 -0
  81. package/dist/schema-registry.d.ts +41 -29
  82. package/dist/schema-registry.js +98 -105
  83. package/dist/server-instructions.d.ts +11 -0
  84. package/dist/server-instructions.js +36 -0
  85. package/dist/tool-args.d.ts +28 -0
  86. package/dist/tool-args.js +93 -0
  87. package/dist/tool-dispatch.d.ts +1 -1
  88. package/dist/tool-dispatch.js +42 -8
  89. package/dist/tools/app-plan.d.ts +92 -0
  90. package/dist/tools/app-plan.js +155 -0
  91. package/dist/tools/briefing-stored-advisories.d.ts +20 -0
  92. package/dist/tools/briefing-stored-advisories.js +58 -0
  93. package/dist/tools/briefing-task-scope.d.ts +73 -0
  94. package/dist/tools/briefing-task-scope.js +197 -0
  95. package/dist/tools/briefing-wisdom.d.ts +31 -0
  96. package/dist/tools/briefing-wisdom.js +148 -0
  97. package/dist/tools/decision-enforcement.d.ts +0 -5
  98. package/dist/tools/decision-enforcement.js +0 -4
  99. package/dist/tools/decision-memory.js +18 -66
  100. package/dist/tools/dependency-check-signals.d.ts +74 -0
  101. package/dist/tools/dependency-check-signals.js +264 -0
  102. package/dist/tools/dependency-check.d.ts +98 -0
  103. package/dist/tools/dependency-check.js +295 -0
  104. package/dist/tools/dependency-health.js +74 -21
  105. package/dist/tools/developer-dna.d.ts +3 -0
  106. package/dist/tools/developer-dna.js +16 -15
  107. package/dist/tools/ecosystem-pulse.d.ts +6 -2
  108. package/dist/tools/ecosystem-pulse.js +53 -14
  109. package/dist/tools/get-actionable-signals.d.ts +26 -4
  110. package/dist/tools/get-actionable-signals.js +214 -251
  111. package/dist/tools/get-context.d.ts +2 -0
  112. package/dist/tools/get-context.js +9 -1
  113. package/dist/tools/get-relevant-content.d.ts +3 -1
  114. package/dist/tools/get-relevant-content.js +34 -3
  115. package/dist/tools/index.d.ts +4 -2
  116. package/dist/tools/index.js +4 -2
  117. package/dist/tools/install-drift-notes.d.ts +51 -0
  118. package/dist/tools/install-drift-notes.js +172 -0
  119. package/dist/tools/knowledge-gap-citations.d.ts +55 -0
  120. package/dist/tools/knowledge-gap-citations.js +132 -0
  121. package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
  122. package/dist/tools/knowledge-gap-exposure.js +160 -0
  123. package/dist/tools/knowledge-gap-grading.d.ts +91 -0
  124. package/dist/tools/knowledge-gap-grading.js +157 -0
  125. package/dist/tools/knowledge-gap-installs.d.ts +65 -0
  126. package/dist/tools/knowledge-gap-installs.js +134 -0
  127. package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
  128. package/dist/tools/knowledge-gap-ranges.js +146 -0
  129. package/dist/tools/knowledge-gap-scope.d.ts +63 -0
  130. package/dist/tools/knowledge-gap-scope.js +118 -0
  131. package/dist/tools/knowledge-gaps.d.ts +12 -3
  132. package/dist/tools/knowledge-gaps.js +172 -121
  133. package/dist/tools/package-presence.d.ts +24 -0
  134. package/dist/tools/package-presence.js +35 -0
  135. package/dist/tools/record-feedback.d.ts +0 -1
  136. package/dist/tools/record-feedback.js +7 -8
  137. package/dist/tools/signal-classifier.d.ts +25 -0
  138. package/dist/tools/signal-classifier.js +40 -0
  139. package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
  140. package/dist/tools/upgrade-impact-callsites.js +226 -0
  141. package/dist/tools/upgrade-impact-literals.d.ts +45 -0
  142. package/dist/tools/upgrade-impact-literals.js +118 -0
  143. package/dist/tools/upgrade-impact-report.d.ts +80 -0
  144. package/dist/tools/upgrade-impact-report.js +178 -0
  145. package/dist/tools/upgrade-impact.d.ts +84 -0
  146. package/dist/tools/upgrade-impact.js +305 -0
  147. package/dist/tools/upgrade-planner.d.ts +41 -8
  148. package/dist/tools/upgrade-planner.js +201 -53
  149. package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
  150. package/dist/tools/vulnerability-scan-concise.js +97 -0
  151. package/dist/tools/vulnerability-scan-format.d.ts +137 -0
  152. package/dist/tools/vulnerability-scan-format.js +276 -0
  153. package/dist/tools/vulnerability-scan.d.ts +338 -18
  154. package/dist/tools/vulnerability-scan.js +84 -125
  155. package/dist/tools/what-should-i-know.d.ts +86 -19
  156. package/dist/tools/what-should-i-know.js +277 -283
  157. package/dist/types.d.ts +4 -1
  158. package/package.json +27 -30
  159. package/dist/schemas/agent-memory.json +0 -49
  160. package/dist/schemas/decision-enforcement.json +0 -22
  161. package/dist/schemas/decision-memory.json +0 -54
  162. package/dist/schemas/dependency-health.json +0 -27
  163. package/dist/schemas/developer-dna.json +0 -219
  164. package/dist/schemas/ecosystem-pulse.json +0 -17
  165. package/dist/schemas/get-actionable-signals.json +0 -29
  166. package/dist/schemas/get-context.json +0 -17
  167. package/dist/schemas/get-relevant-content.json +0 -26
  168. package/dist/schemas/knowledge-gaps.json +0 -14
  169. package/dist/schemas/record-feedback.json +0 -24
  170. package/dist/schemas/upgrade-planner.json +0 -22
  171. package/dist/schemas/vulnerability-scan.json +0 -26
  172. package/dist/schemas/what-should-i-know.json +0 -19
@@ -0,0 +1,198 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Network sources for `upgrade_impact`: registry version index, the target
4
+ * version's changelog (read from its registry archive) and OSV advisories.
5
+ *
6
+ * Privacy rule (Decision 1, option A): the only hosts contacted are the
7
+ * package's own registry (registry.npmjs.org; crates.io + static.crates.io)
8
+ * and api.osv.dev. A changelog is never fetched from GitHub or a docs site,
9
+ * even when the archive ships none — the report then hands the agent a
10
+ * `release_notes_url` built from registry metadata, and the agent decides
11
+ * whether to fetch it. Every request goes through `UpgradeNet`, which records
12
+ * the host so `_meta.sources` is a fact, not a claim, and which tests replace
13
+ * with a fake fetch.
14
+ *
15
+ * Request budget per call: index (1) + archive (1) + OSV for from/to (2) = 4,
16
+ * each cached (index 6 h, parsed changelog 7 d, OSV 1 h). The npm packument
17
+ * is large (vite's is ~39 MB, measured 2026-10-02), so only a reduced index
18
+ * is cached — never the raw document.
19
+ */
20
+ import { compareVersionPrecedence, parseSemverPrecedence } from "./semver-precedence.js";
21
+ import { ArchiveError, defaultFetch, fetchArchiveRootFiles } from "./package-archive.js";
22
+ import { findChangelogFile, isChangelogName, parseChangelog } from "./changelog.js";
23
+ /** crates.io refuses API requests without a descriptive User-Agent. */
24
+ export const CRATES_USER_AGENT = "4da-mcp-server (https://4da.ai; upgrade_impact)";
25
+ const INDEX_TTL = 6 * 3600;
26
+ const CHANGELOG_TTL = 7 * 86_400;
27
+ const OSV_TTL = 3600;
28
+ const CACHE_SOURCE = "upgrade-impact";
29
+ const INDEX_TIMEOUT_MS = 30_000;
30
+ const OSV_TIMEOUT_MS = 10_000;
31
+ export function createUpgradeNet(cache, fetchFn = defaultFetch) {
32
+ const contacted = new Set();
33
+ return {
34
+ contacted,
35
+ cache,
36
+ fetch: (url, init, timeoutMs) => {
37
+ contacted.add(new URL(url).host);
38
+ return fetchFn(url, init, timeoutMs);
39
+ },
40
+ };
41
+ }
42
+ const NPM_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i;
43
+ const CRATE_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
44
+ export function isValidPackageName(name, ecosystem) {
45
+ return ecosystem === "npm" ? NPM_NAME.test(name) && name.length <= 214 : CRATE_NAME.test(name);
46
+ }
47
+ async function cached(net, key, ttl, load) {
48
+ const hit = net.cache?.get(key);
49
+ if (hit !== null && hit !== undefined)
50
+ return hit;
51
+ const value = await load();
52
+ net.cache?.set(key, value, CACHE_SOURCE, ttl);
53
+ return value;
54
+ }
55
+ function repositoryUrl(repo) {
56
+ if (typeof repo === "string")
57
+ return repo;
58
+ if (repo && typeof repo === "object" && typeof repo.url === "string") {
59
+ return repo.url;
60
+ }
61
+ return null;
62
+ }
63
+ async function loadNpmIndex(net, name) {
64
+ const url = `https://registry.npmjs.org/${name.replaceAll("/", "%2F")}`;
65
+ const res = await net.fetch(url, { headers: { Accept: "application/json" } }, INDEX_TIMEOUT_MS);
66
+ if (res.status === 404)
67
+ return null;
68
+ if (!res.ok)
69
+ throw new Error(`npm registry HTTP ${res.status} for ${name}`);
70
+ const doc = (await res.json());
71
+ const versions = Object.entries(doc.versions ?? {}).map(([version, meta]) => {
72
+ const entry = { version, published: doc.time?.[version] ?? null };
73
+ if (typeof meta.deprecated === "string" && meta.deprecated)
74
+ entry.deprecated = meta.deprecated;
75
+ if (meta.dist?.tarball)
76
+ entry.tarball = meta.dist.tarball;
77
+ return entry;
78
+ });
79
+ return { name: doc.name ?? name, ecosystem: "npm", versions, repository: repositoryUrl(doc.repository) };
80
+ }
81
+ async function loadCratesIndex(net, name) {
82
+ const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}`;
83
+ const headers = { Accept: "application/json", "User-Agent": CRATES_USER_AGENT };
84
+ const res = await net.fetch(url, { headers }, INDEX_TIMEOUT_MS);
85
+ if (res.status === 404)
86
+ return null;
87
+ if (!res.ok)
88
+ throw new Error(`crates.io HTTP ${res.status} for ${name}`);
89
+ const doc = (await res.json());
90
+ const versions = (doc.versions ?? [])
91
+ .filter((v) => typeof v.num === "string")
92
+ .map((v) => {
93
+ const entry = { version: v.num, published: v.created_at ?? null };
94
+ if (v.yanked)
95
+ entry.yanked = true;
96
+ return entry;
97
+ });
98
+ return { name: doc.crate?.name ?? name, ecosystem: "crates.io", versions, repository: doc.crate?.repository ?? null };
99
+ }
100
+ /** Registry index with versions in ascending precedence order (unreadable versions dropped). Null = unknown package. */
101
+ export async function getRegistryIndex(net, ecosystem, name) {
102
+ const key = `upgrade-impact:index:${ecosystem}:${ecosystem === "crates.io" ? name.replace(/-/g, "_") : name}`;
103
+ const index = await cached(net, key, INDEX_TTL, () => ecosystem === "npm" ? loadNpmIndex(net, name) : loadCratesIndex(net, name));
104
+ if (!index)
105
+ return null;
106
+ const versions = index.versions
107
+ .filter((v) => parseSemverPrecedence(v.version) !== null)
108
+ .sort((a, b) => compareVersionPrecedence(a.version, b.version) ?? 0);
109
+ return { ...index, versions };
110
+ }
111
+ const ARCHIVE_HOSTS = new Set(["registry.npmjs.org", "static.crates.io"]);
112
+ /** The registry archive URL for one version, or null when it is not on the package's own registry. */
113
+ export function archiveUrl(index, version) {
114
+ const url = index.ecosystem === "crates.io"
115
+ ? `https://static.crates.io/crates/${index.name}/${index.name}-${version.version}.crate`
116
+ : version.tarball;
117
+ if (!url)
118
+ return null;
119
+ try {
120
+ const parsed = new URL(url);
121
+ return parsed.protocol === "https:" && ARCHIVE_HOSTS.has(parsed.host) ? url : null;
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
127
+ /** Parsed changelog shipped inside one version's archive (cached 7 days per version). */
128
+ export async function getChangelog(net, index, version) {
129
+ const url = archiveUrl(index, version);
130
+ if (!url) {
131
+ return { found: false, reason: "the version's archive is not hosted on the package's own registry" };
132
+ }
133
+ const key = `upgrade-impact:changelog:${index.ecosystem}:${index.name}:${version.version}`;
134
+ const hit = net.cache?.get(key);
135
+ if (hit)
136
+ return hit;
137
+ const headers = index.ecosystem === "crates.io" ? { "User-Agent": CRATES_USER_AGENT } : {};
138
+ let files;
139
+ try {
140
+ files = await fetchArchiveRootFiles(url, isChangelogName, net.fetch, headers);
141
+ }
142
+ catch (err) {
143
+ const result = { found: false, reason: `could not read the ${version.version} archive: ${err.message}` };
144
+ // A published archive never changes, so a cap or format refusal is permanent;
145
+ // a network failure is not, and must not be remembered for a week.
146
+ if (err instanceof ArchiveError)
147
+ net.cache?.set(key, result, CACHE_SOURCE, CHANGELOG_TTL);
148
+ return result;
149
+ }
150
+ const file = findChangelogFile(files.keys());
151
+ const sections = file ? parseChangelog(files.get(file) ?? "") : [];
152
+ const result = !file
153
+ ? { found: false, reason: `the ${version.version} archive ships no changelog file` }
154
+ : sections.length === 0
155
+ ? { found: false, file, reason: `${file} has no version headings this parser recognises` }
156
+ : { found: true, file, sections };
157
+ net.cache?.set(key, result, CACHE_SOURCE, CHANGELOG_TTL);
158
+ return result;
159
+ }
160
+ /** OSV advisory ids affecting one version, or null when OSV could not be reached. */
161
+ export async function getOsvAdvisories(net, ecosystem, name, version) {
162
+ const key = `upgrade-impact:osv:${ecosystem}:${name}:${version}`;
163
+ const hit = net.cache?.get(key);
164
+ if (hit)
165
+ return hit;
166
+ try {
167
+ const res = await net.fetch("https://api.osv.dev/v1/query", {
168
+ method: "POST",
169
+ headers: { "Content-Type": "application/json" },
170
+ body: JSON.stringify({ package: { name, ecosystem }, version }),
171
+ }, OSV_TIMEOUT_MS);
172
+ if (!res.ok)
173
+ return null;
174
+ const body = (await res.json());
175
+ const ids = (body.vulns ?? []).map((v) => v.id).filter((id) => typeof id === "string");
176
+ net.cache?.set(key, ids, CACHE_SOURCE, OSV_TTL);
177
+ return ids;
178
+ }
179
+ catch {
180
+ return null;
181
+ }
182
+ }
183
+ /**
184
+ * Release-notes page derived from the registry's own `repository` field: a
185
+ * GitHub repo becomes `https://github.com/<owner>/<repo>/releases`; any other
186
+ * https repository URL is returned as-is; anything else is null.
187
+ */
188
+ export function releaseNotesUrl(repository) {
189
+ if (!repository)
190
+ return null;
191
+ const raw = repository.trim();
192
+ const shorthand = /^(?:github:)?([\w.-]+)\/([\w.-]+)$/.exec(raw);
193
+ const gh = shorthand ?? /github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:[/#?].*)?$/i.exec(raw);
194
+ if (gh)
195
+ return `https://github.com/${gh[1]}/${gh[2].replace(/\.git$/, "")}/releases`;
196
+ const https = raw.replace(/^git\+/, "");
197
+ return /^https:\/\//i.test(https) ? https.replace(/\.git$/, "") : null;
198
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Version ordering per ecosystem, for advisory ranges and fix selection.
3
+ *
4
+ * `compareSemver` reads MAJOR.MINOR.PATCH and calls anything else "equal".
5
+ * That is wrong outside npm and crates.io, and it was wrong in practice
6
+ * (measured 2026-10-02 against pip-audit and osv-scanner):
7
+ * - PyPI versions are PEP 440, not semver. `5.1` and `4.2` are ordinary
8
+ * releases; read as "equal" to everything, django 3.2.0 was sent to the 5.1
9
+ * line's fix (5.1.1) instead of its own line's (3.2.25 / 4.2.16).
10
+ * - Go versions carry a `v`, OSV's Go ranges do not, and pseudo-versions
11
+ * (`v0.0.0-20200622213623-75b288015ac9`) order as semver prereleases.
12
+ *
13
+ * `0` is OSV's "since the beginning" bound and orders below every version.
14
+ */
15
+ import type { OsvEcosystem } from "./types.js";
16
+ interface Pep440 {
17
+ epoch: number;
18
+ release: number[];
19
+ /** Pre-release: [rank, n] with a=0, b=1, rc=2; null for none. */
20
+ pre: [number, number] | null;
21
+ post: number | null;
22
+ dev: number | null;
23
+ }
24
+ export declare function parsePep440(version: string): Pep440 | null;
25
+ export declare function comparePep440(a: Pep440, b: Pep440): number;
26
+ /** Whether `version` is readable for this ecosystem's ordering. */
27
+ export declare function isComparable(version: string, ecosystem: OsvEcosystem | string): boolean;
28
+ /**
29
+ * Order two versions of one ecosystem: 1, -1, or 0 for equal OR unreadable
30
+ * (the same contract as compareSemver, so an unreadable bound never decides).
31
+ */
32
+ export declare function compareVersions(a: string, b: string, ecosystem: OsvEcosystem | string): number;
33
+ /** Highest of `versions` in this ecosystem's order, or null when empty. */
34
+ export declare function maxVersion(versions: string[], ecosystem: OsvEcosystem | string): string | null;
35
+ /** Package-name equality as the ecosystem's registry defines it. */
36
+ export declare function samePackageName(a: string, b: string, ecosystem: OsvEcosystem | string): boolean;
37
+ /** Whether a user-typed package name (`package` parameters) names this dependency: registry rules, then case-insensitive. */
38
+ export declare function namesPackage(query: string, name: string, ecosystem: OsvEcosystem | string): boolean;
39
+ export {};
@@ -0,0 +1,130 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Version ordering per ecosystem, for advisory ranges and fix selection.
4
+ *
5
+ * `compareSemver` reads MAJOR.MINOR.PATCH and calls anything else "equal".
6
+ * That is wrong outside npm and crates.io, and it was wrong in practice
7
+ * (measured 2026-10-02 against pip-audit and osv-scanner):
8
+ * - PyPI versions are PEP 440, not semver. `5.1` and `4.2` are ordinary
9
+ * releases; read as "equal" to everything, django 3.2.0 was sent to the 5.1
10
+ * line's fix (5.1.1) instead of its own line's (3.2.25 / 4.2.16).
11
+ * - Go versions carry a `v`, OSV's Go ranges do not, and pseudo-versions
12
+ * (`v0.0.0-20200622213623-75b288015ac9`) order as semver prereleases.
13
+ *
14
+ * `0` is OSV's "since the beginning" bound and orders below every version.
15
+ */
16
+ import { compareSemver, parseSemver } from "./semver-utils.js";
17
+ import { compareVersionPrecedence, parseSemverPrecedence } from "./semver-precedence.js";
18
+ const PEP440 = /^v?(?:(\d+)!)?(\d+(?:\.\d+)*)(?:[-_.]?(a|alpha|b|beta|c|rc|pre|preview)[-_.]?(\d*))?(?:(?:[-_.]?(?:post|rev|r)[-_.]?(\d*))|-(\d+))?(?:[-_.]?dev[-_.]?(\d*))?(?:\+[a-z0-9]+(?:[-_.][a-z0-9]+)*)?$/i;
19
+ export function parsePep440(version) {
20
+ const m = PEP440.exec(version.trim());
21
+ if (!m)
22
+ return null;
23
+ const preRank = { a: 0, alpha: 0, b: 1, beta: 1, c: 2, rc: 2, pre: 2, preview: 2 };
24
+ return {
25
+ epoch: m[1] ? Number(m[1]) : 0,
26
+ release: m[2].split(".").map(Number),
27
+ pre: m[3] ? [preRank[m[3].toLowerCase()], m[4] ? Number(m[4]) : 0] : null,
28
+ post: m[5] !== undefined ? (m[5] ? Number(m[5]) : 0) : m[6] !== undefined ? Number(m[6]) : null,
29
+ dev: m[7] !== undefined ? (m[7] ? Number(m[7]) : 0) : null,
30
+ };
31
+ }
32
+ export function comparePep440(a, b) {
33
+ if (a.epoch !== b.epoch)
34
+ return a.epoch > b.epoch ? 1 : -1;
35
+ const len = Math.max(a.release.length, b.release.length);
36
+ for (let i = 0; i < len; i++) {
37
+ const x = a.release[i] ?? 0;
38
+ const y = b.release[i] ?? 0;
39
+ if (x !== y)
40
+ return x > y ? 1 : -1;
41
+ }
42
+ // PEP 440's sort key within one release:
43
+ // X.devN < X.aN.devM < X.aN < X.aN.postM < X.bN < X.rcN < X < X.postN.devM < X.postN
44
+ // pre: a/b/rc rank, a dev-only release below every pre-release, a final above;
45
+ // then post (none lowest); then dev (none highest).
46
+ const key = (v) => [
47
+ v.pre ? v.pre[0] : v.post === null && v.dev !== null ? -1 : 3,
48
+ v.pre ? v.pre[1] : 0,
49
+ v.post === null ? -1 : v.post,
50
+ v.dev === null ? Number.POSITIVE_INFINITY : v.dev,
51
+ ];
52
+ const ka = key(a);
53
+ const kb = key(b);
54
+ for (let i = 0; i < ka.length; i++) {
55
+ if (ka[i] !== kb[i])
56
+ return ka[i] > kb[i] ? 1 : -1;
57
+ }
58
+ return 0;
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Dispatch
62
+ // ---------------------------------------------------------------------------
63
+ const stripGoV = (v) => v.replace(/^v(?=\d)/, "");
64
+ /** Whether `version` is readable for this ecosystem's ordering. */
65
+ export function isComparable(version, ecosystem) {
66
+ if (version === "0")
67
+ return true;
68
+ switch (ecosystem) {
69
+ case "PyPI":
70
+ return parsePep440(version) !== null;
71
+ case "Go":
72
+ return parseSemverPrecedence(stripGoV(version)) !== null;
73
+ case "npm":
74
+ case "crates.io":
75
+ return parseSemverPrecedence(version) !== null;
76
+ default:
77
+ return parseSemver(version) !== null;
78
+ }
79
+ }
80
+ /**
81
+ * Order two versions of one ecosystem: 1, -1, or 0 for equal OR unreadable
82
+ * (the same contract as compareSemver, so an unreadable bound never decides).
83
+ */
84
+ export function compareVersions(a, b, ecosystem) {
85
+ if (a === b)
86
+ return 0;
87
+ if (a === "0")
88
+ return isComparable(b, ecosystem) ? -1 : 0;
89
+ if (b === "0")
90
+ return isComparable(a, ecosystem) ? 1 : 0;
91
+ switch (ecosystem) {
92
+ case "PyPI": {
93
+ const pa = parsePep440(a);
94
+ const pb = parsePep440(b);
95
+ return pa && pb ? comparePep440(pa, pb) : 0;
96
+ }
97
+ case "Go":
98
+ return compareVersionPrecedence(stripGoV(a), stripGoV(b)) ?? 0;
99
+ case "npm":
100
+ case "crates.io":
101
+ return compareVersionPrecedence(a, b) ?? compareSemver(a, b);
102
+ default:
103
+ return compareSemver(a, b);
104
+ }
105
+ }
106
+ /** Highest of `versions` in this ecosystem's order, or null when empty. */
107
+ export function maxVersion(versions, ecosystem) {
108
+ if (versions.length === 0)
109
+ return null;
110
+ return versions.reduce((max, v) => (compareVersions(v, max, ecosystem) > 0 ? v : max), versions[0]);
111
+ }
112
+ /** Package-name equality as the ecosystem's registry defines it. */
113
+ export function samePackageName(a, b, ecosystem) {
114
+ if (a === b)
115
+ return true;
116
+ if (ecosystem === "PyPI") {
117
+ const n = (s) => s.toLowerCase().replace(/[-_.]+/g, "-");
118
+ return n(a) === n(b);
119
+ }
120
+ if (ecosystem === "crates.io") {
121
+ const n = (s) => s.toLowerCase().replace(/_/g, "-");
122
+ return n(a) === n(b);
123
+ }
124
+ return false;
125
+ }
126
+ /** Whether a user-typed package name (`package` parameters) names this dependency: registry rules, then case-insensitive. */
127
+ export function namesPackage(query, name, ecosystem) {
128
+ const q = query.trim();
129
+ return samePackageName(q, name, ecosystem) || q.toLowerCase() === name.toLowerCase();
130
+ }
@@ -3,9 +3,12 @@
3
3
  *
4
4
  * Extracts exact dependency versions from lock files.
5
5
  * Priority: lock file (exact) > manifest (range/specifier).
6
- * Uses only Node.js built-ins — no external parsers.
6
+ * Uses only Node.js built-ins — no external parsers. The per-ecosystem
7
+ * parsers live in lockfile-parsers.ts, which also reports which file each
8
+ * resolution read from.
7
9
  */
8
10
  import type { OsvEcosystem, ResolvedDependency } from "./types.js";
11
+ import { type PackageInstance } from "./lockfile-parsers.js";
9
12
  /**
10
13
  * Map a language/ecosystem alias to its OSV ecosystem id (case-insensitive).
11
14
  *
@@ -18,8 +21,18 @@ import type { OsvEcosystem, ResolvedDependency } from "./types.js";
18
21
  */
19
22
  export declare function mapEcosystem(language: string): OsvEcosystem;
20
23
  export declare function resolveVersions(cwd: string, deps: string[], devDeps: string[], language: string, targets?: Record<string, string>): ResolvedDependency[];
24
+ /** `resolveVersions` over a version map the caller already read from `cwd`. */
25
+ export declare function resolveVersionsFrom(cwd: string, deps: string[], devDeps: string[], ecosystem: OsvEcosystem, versionMap: Map<string, string>, targets?: Record<string, string>): ResolvedDependency[];
21
26
  /**
22
27
  * Resolve the complete lockfile package set for vulnerability scanning.
23
28
  * Direct dependency health and upgrade planning continue to use resolveVersions.
24
29
  */
25
30
  export declare function resolveAuditVersions(cwd: string, deps: string[], devDeps: string[], language: string, targets?: Record<string, string>): ResolvedDependency[];
31
+ /**
32
+ * `resolveAuditVersions` over a version map the caller already read. `direct`
33
+ * lets the caller pass direct entries it has already enriched (install state)
34
+ * so the audit set carries the same objects' context. `instances` is every
35
+ * installed copy the lockfile holds; without it the version map (one copy per
36
+ * name) is all there is.
37
+ */
38
+ export declare function resolveAuditVersionsFrom(cwd: string, deps: string[], devDeps: string[], ecosystem: OsvEcosystem, versionMap: Map<string, string>, targets?: Record<string, string>, direct?: ResolvedDependency[], instances?: PackageInstance[]): ResolvedDependency[];