@4da/mcp-server 6.2.3 → 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 (123) hide show
  1. package/CHANGELOG.md +520 -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 +289 -16
  18. package/dist/live/fix-paths.js +876 -93
  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/range-satisfies.d.ts +4 -2
  56. package/dist/live/range-satisfies.js +7 -5
  57. package/dist/live/rate-limiter.d.ts +7 -0
  58. package/dist/live/rate-limiter.js +22 -1
  59. package/dist/live/refresh-rule.d.ts +36 -0
  60. package/dist/live/refresh-rule.js +49 -0
  61. package/dist/live/release-metadata.d.ts +5 -0
  62. package/dist/live/resolution.d.ts +3 -0
  63. package/dist/live/resolution.js +6 -3
  64. package/dist/live/scan-coverage.d.ts +26 -0
  65. package/dist/live/scan-coverage.js +70 -0
  66. package/dist/live/semver-utils.d.ts +8 -0
  67. package/dist/live/semver-utils.js +26 -3
  68. package/dist/live/severity-scope.d.ts +26 -1
  69. package/dist/live/severity-scope.js +32 -0
  70. package/dist/live/shell-safe.d.ts +19 -0
  71. package/dist/live/shell-safe.js +36 -0
  72. package/dist/live/types.d.ts +134 -8
  73. package/dist/live/version-compare.d.ts +51 -0
  74. package/dist/live/version-compare.js +152 -1
  75. package/dist/live/withdrawn-releases.d.ts +50 -0
  76. package/dist/live/withdrawn-releases.js +125 -0
  77. package/dist/project-manifests.d.ts +7 -0
  78. package/dist/project-manifests.js +45 -5
  79. package/dist/project-scanner.js +6 -4
  80. package/dist/project-tree.d.ts +56 -0
  81. package/dist/project-tree.js +174 -5
  82. package/dist/schema-registry.d.ts +47 -4
  83. package/dist/schema-registry.js +76 -9
  84. package/dist/server-instructions.js +4 -2
  85. package/dist/setup.d.ts +8 -0
  86. package/dist/setup.js +21 -2
  87. package/dist/tool-dispatch.js +3 -3
  88. package/dist/tools/agent-memory.d.ts +5 -0
  89. package/dist/tools/agent-memory.js +9 -0
  90. package/dist/tools/briefing-task-scope.js +5 -2
  91. package/dist/tools/decision-memory.js +2 -0
  92. package/dist/tools/dependency-check-held.d.ts +44 -0
  93. package/dist/tools/dependency-check-held.js +98 -0
  94. package/dist/tools/dependency-check-signals.d.ts +19 -2
  95. package/dist/tools/dependency-check-signals.js +84 -13
  96. package/dist/tools/dependency-check-versions.d.ts +37 -4
  97. package/dist/tools/dependency-check-versions.js +48 -13
  98. package/dist/tools/dependency-check.d.ts +11 -1
  99. package/dist/tools/dependency-check.js +81 -29
  100. package/dist/tools/dependency-health.js +209 -45
  101. package/dist/tools/get-context.d.ts +3 -0
  102. package/dist/tools/get-context.js +4 -0
  103. package/dist/tools/project-scope.d.ts +8 -0
  104. package/dist/tools/project-scope.js +21 -0
  105. package/dist/tools/upgrade-impact-features.d.ts +37 -0
  106. package/dist/tools/upgrade-impact-features.js +176 -0
  107. package/dist/tools/upgrade-impact-literals.js +26 -4
  108. package/dist/tools/upgrade-impact-report.d.ts +5 -1
  109. package/dist/tools/upgrade-impact-report.js +26 -5
  110. package/dist/tools/upgrade-impact.js +10 -3
  111. package/dist/tools/upgrade-planner-fixpaths.d.ts +164 -5
  112. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  113. package/dist/tools/upgrade-planner.d.ts +70 -7
  114. package/dist/tools/upgrade-planner.js +237 -61
  115. package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
  116. package/dist/tools/vulnerability-scan-concise.js +37 -3
  117. package/dist/tools/vulnerability-scan-format.d.ts +126 -24
  118. package/dist/tools/vulnerability-scan-format.js +193 -26
  119. package/dist/tools/vulnerability-scan.d.ts +224 -63
  120. package/dist/tools/vulnerability-scan.js +50 -15
  121. package/dist/tools/what-should-i-know.d.ts +6 -1
  122. package/dist/tools/what-should-i-know.js +72 -15
  123. package/package.json +2 -2
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Ruby (Gemfile.lock -> RubyGems) and PHP (composer.lock -> Packagist).
3
+ *
4
+ * Until 6.2.5 neither was read: a Rails or Symfony project came back "No
5
+ * manifest files found... run from your project root", which reads as the
6
+ * user's mistake rather than an ecosystem this server did not cover (audit
7
+ * A2, 2026-10-12). Both readers mirror the desktop app's
8
+ * (src-tauri/src/ace/lockfile/ruby.rs; ProjectScanner::parse_composer_lock),
9
+ * and the conformance corpus scores them against the same truth
10
+ * (rb-js-rails-6.0, php-js-symfony-demo).
11
+ */
12
+ import { InstanceSet, type VersionSource } from "./lockfile-types.js";
13
+ /**
14
+ * Gemfile.lock: the `specs:` of each `GEM` section (registry gems). GIT and
15
+ * PATH sections are local or git sources, not RubyGems releases, and are
16
+ * skipped. A platform suffix (`nokogiri (1.10.4-x86_64-linux)`) is not part
17
+ * of the version: RubyGems prereleases use `.pre`, never `-`.
18
+ */
19
+ export declare function readGemfileLock(content: string): {
20
+ instances: InstanceSet;
21
+ versions: Map<string, string>;
22
+ direct: string[];
23
+ };
24
+ /**
25
+ * composer.lock: `packages` (runtime) and `packages-dev` (dev). A leading `v`
26
+ * is dropped (`v5.4.0` is release 5.4.0 on Packagist and in OSV). A branch
27
+ * checkout (`dev-main`) names no version and is skipped; a numbered branch
28
+ * alias (`3.0.x-dev`) is kept, as osv-scanner and the desktop app keep it.
29
+ */
30
+ export declare function readComposerLock(lock: {
31
+ packages?: unknown;
32
+ "packages-dev"?: unknown;
33
+ }): {
34
+ instances: InstanceSet;
35
+ versions: Map<string, string>;
36
+ };
37
+ export declare function resolveRuby(cwd: string): VersionSource;
38
+ export declare function resolvePhp(cwd: string): VersionSource;
@@ -0,0 +1,123 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Ruby (Gemfile.lock -> RubyGems) and PHP (composer.lock -> Packagist).
4
+ *
5
+ * Until 6.2.5 neither was read: a Rails or Symfony project came back "No
6
+ * manifest files found... run from your project root", which reads as the
7
+ * user's mistake rather than an ecosystem this server did not cover (audit
8
+ * A2, 2026-10-12). Both readers mirror the desktop app's
9
+ * (src-tauri/src/ace/lockfile/ruby.rs; ProjectScanner::parse_composer_lock),
10
+ * and the conformance corpus scores them against the same truth
11
+ * (rb-js-rails-6.0, php-js-symfony-demo).
12
+ */
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ import { emptySource, found, InstanceSet } from "./lockfile-types.js";
16
+ /** Lines longer than this are not lockfile syntax; skipping them keeps every pattern below linear. */
17
+ const MAX_LINE = 4096;
18
+ /**
19
+ * Gemfile.lock: the `specs:` of each `GEM` section (registry gems). GIT and
20
+ * PATH sections are local or git sources, not RubyGems releases, and are
21
+ * skipped. A platform suffix (`nokogiri (1.10.4-x86_64-linux)`) is not part
22
+ * of the version: RubyGems prereleases use `.pre`, never `-`.
23
+ */
24
+ export function readGemfileLock(content) {
25
+ const instances = new InstanceSet();
26
+ const versions = new Map();
27
+ const direct = [];
28
+ let section = "";
29
+ let inSpecs = false;
30
+ for (const raw of content.split(/\r?\n/)) {
31
+ if (raw.length > MAX_LINE || raw.trim() === "")
32
+ continue;
33
+ if (!raw.startsWith(" ")) {
34
+ section = raw.trimEnd();
35
+ inSpecs = false;
36
+ continue;
37
+ }
38
+ const indent = raw.length - raw.trimStart().length;
39
+ const line = raw.trim();
40
+ if (section === "DEPENDENCIES" && indent === 2) {
41
+ const name = /^([^\s!(]+)/.exec(line)?.[1];
42
+ if (name)
43
+ direct.push(name);
44
+ continue;
45
+ }
46
+ if (line === "specs:" && indent === 2) {
47
+ inSpecs = true;
48
+ continue;
49
+ }
50
+ if (indent === 2) {
51
+ inSpecs = false;
52
+ continue;
53
+ }
54
+ if (!inSpecs || indent !== 4 || section !== "GEM")
55
+ continue;
56
+ const open = line.indexOf(" (");
57
+ if (open <= 0 || !line.endsWith(")"))
58
+ continue;
59
+ const name = line.slice(0, open);
60
+ const version = line.slice(open + 2, -1).split("-")[0].trim();
61
+ if (!name || name.includes(" ") || !version)
62
+ continue;
63
+ instances.add(name, version);
64
+ if (!versions.has(name))
65
+ versions.set(name, version);
66
+ }
67
+ return { instances, versions, direct };
68
+ }
69
+ /**
70
+ * composer.lock: `packages` (runtime) and `packages-dev` (dev). A leading `v`
71
+ * is dropped (`v5.4.0` is release 5.4.0 on Packagist and in OSV). A branch
72
+ * checkout (`dev-main`) names no version and is skipped; a numbered branch
73
+ * alias (`3.0.x-dev`) is kept, as osv-scanner and the desktop app keep it.
74
+ */
75
+ export function readComposerLock(lock) {
76
+ const instances = new InstanceSet();
77
+ const versions = new Map();
78
+ for (const [key, dev] of [["packages", false], ["packages-dev", true]]) {
79
+ const list = lock[key];
80
+ if (!Array.isArray(list))
81
+ continue;
82
+ for (const entry of list) {
83
+ if (typeof entry?.name !== "string" || typeof entry.version !== "string")
84
+ continue;
85
+ const version = entry.version.replace(/^v(?=\d)/, "");
86
+ if (!entry.name || !/^\d/.test(version))
87
+ continue;
88
+ instances.add(entry.name, version, dev);
89
+ if (!versions.has(entry.name))
90
+ versions.set(entry.name, version);
91
+ }
92
+ }
93
+ return { instances, versions };
94
+ }
95
+ export function resolveRuby(cwd) {
96
+ const lockPath = path.join(cwd, "Gemfile.lock");
97
+ if (!fs.existsSync(lockPath))
98
+ return emptySource();
99
+ const read = readGemfileLock(fs.readFileSync(lockPath, "utf-8"));
100
+ if (read.instances.size === 0)
101
+ return emptySource();
102
+ return found(read.versions, read.instances, lockPath, "lockfile", { direct: { deps: read.direct, devDeps: [] } });
103
+ }
104
+ export function resolvePhp(cwd) {
105
+ const lockPath = path.join(cwd, "composer.lock");
106
+ if (!fs.existsSync(lockPath))
107
+ return emptySource();
108
+ const read = readComposerLock(JSON.parse(fs.readFileSync(lockPath, "utf-8")));
109
+ if (read.instances.size === 0)
110
+ return emptySource();
111
+ return found(read.versions, read.instances, lockPath, "lockfile", { direct: composerDirect(cwd) });
112
+ }
113
+ /** composer.json `require` / `require-dev` package names (platform requirements such as `php` and `ext-*` left out). */
114
+ function composerDirect(cwd) {
115
+ try {
116
+ const manifest = JSON.parse(fs.readFileSync(path.join(cwd, "composer.json"), "utf-8"));
117
+ const names = (field) => Object.keys(manifest[field] ?? {}).filter((n) => n.includes("/"));
118
+ return { deps: names("require"), devDeps: names("require-dev") };
119
+ }
120
+ catch {
121
+ return { deps: [], devDeps: [] };
122
+ }
123
+ }
@@ -32,6 +32,12 @@ export type { PackageInstance, VersionSource } from "./lockfile-types.js";
32
32
  export declare function lockfileCandidates(cwd: string, ecosystem: OsvEcosystem): string[];
33
33
  /** The manifest the resolver falls back to for this ecosystem, or null when it has none. */
34
34
  export declare function manifestCandidate(cwd: string, ecosystem: OsvEcosystem): string | null;
35
+ /**
36
+ * The versions one directory pins for one ecosystem, plus every lockfile that
37
+ * is present but could not be read (`problems`). A damaged or conflicted
38
+ * lockfile used to fall through silently to the next candidate or to "no
39
+ * lockfile" (audit B-9): now the answer names the file and the error.
40
+ */
35
41
  export declare function resolveVersionSource(cwd: string, ecosystem: OsvEcosystem): VersionSource;
36
42
  /** The package name a package-lock `packages` key installs: the segment after its LAST `node_modules/`. */
37
43
  export declare function packageNameFromNodeModulesPath(lockPath: string): string;
@@ -26,14 +26,17 @@
26
26
  import * as fs from "node:fs";
27
27
  import * as path from "node:path";
28
28
  import { readBunLock } from "./bun-lockfile.js";
29
- import { readPnpmLock, readYarnLock } from "./js-lockfile-readers.js";
29
+ import { MAX_LOCKFILE_LINE, readPnpmLock, readYarnLock } from "./js-lockfile-readers.js";
30
30
  import { requirementFiles, resolveGo, resolvePython } from "./lockfile-parsers-pygo.js";
31
31
  import { emptySource, found, InstanceSet } from "./lockfile-types.js";
32
+ import { resolvePhp, resolveRuby } from "./lockfile-parsers-rbphp.js";
32
33
  const LOCKFILES = {
33
34
  npm: ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"],
34
35
  "crates.io": ["Cargo.lock"],
35
36
  PyPI: ["poetry.lock", "uv.lock", "pdm.lock", "Pipfile.lock"],
36
37
  Go: ["go.mod", "go.sum"],
38
+ RubyGems: ["Gemfile.lock"],
39
+ Packagist: ["composer.lock"],
37
40
  };
38
41
  const MANIFESTS = {
39
42
  npm: "package.json",
@@ -52,7 +55,29 @@ export function manifestCandidate(cwd, ecosystem) {
52
55
  const name = MANIFESTS[ecosystem];
53
56
  return name ? path.join(cwd, name) : null;
54
57
  }
58
+ /**
59
+ * The versions one directory pins for one ecosystem, plus every lockfile that
60
+ * is present but could not be read (`problems`). A damaged or conflicted
61
+ * lockfile used to fall through silently to the next candidate or to "no
62
+ * lockfile" (audit B-9): now the answer names the file and the error.
63
+ */
55
64
  export function resolveVersionSource(cwd, ecosystem) {
65
+ let read;
66
+ let thrown = null;
67
+ try {
68
+ read = readVersionSource(cwd, ecosystem);
69
+ }
70
+ catch (error) {
71
+ read = emptySource();
72
+ const file = (LOCKFILES[ecosystem] ?? []).map((n) => path.join(cwd, n)).find((f) => fs.existsSync(f));
73
+ if (file)
74
+ thrown = { file, error: describeParseError(file, error) ?? "could not be parsed" };
75
+ }
76
+ // The reader's own problems (a refused requirements include, M4) and the lockfiles it could not read.
77
+ const problems = [...(read.problems ?? []), ...(thrown ? [thrown] : unreadLockfiles(cwd, ecosystem, read))];
78
+ return problems.length > 0 ? { ...read, problems: problems.map((p) => ({ ...p, ecosystem })) } : read;
79
+ }
80
+ function readVersionSource(cwd, ecosystem) {
56
81
  switch (ecosystem) {
57
82
  case "npm":
58
83
  return resolveNpm(cwd);
@@ -62,10 +87,95 @@ export function resolveVersionSource(cwd, ecosystem) {
62
87
  return resolvePython(cwd);
63
88
  case "Go":
64
89
  return resolveGo(cwd);
90
+ case "RubyGems":
91
+ return resolveRuby(cwd);
92
+ case "Packagist":
93
+ return resolvePhp(cwd);
65
94
  default:
66
95
  return emptySource();
67
96
  }
68
97
  }
98
+ /**
99
+ * yarn.lock entries skipped because a line of theirs is past the reader's
100
+ * length cap (B-10 keeps the patterns linear). Said, so the read is never
101
+ * taken for complete (audit L5).
102
+ */
103
+ function yarnSkippedProblem(file, skipped) {
104
+ const entries = skipped === 1 ? "1 entry" : `${skipped} entries`;
105
+ return {
106
+ file,
107
+ skipped,
108
+ error: `${entries} on a line longer than ${MAX_LOCKFILE_LINE} characters (not lockfile syntax) ${skipped === 1 ? "was" : "were"} skipped, so ${skipped === 1 ? "its version was" : "their versions were"} not checked`,
109
+ };
110
+ }
111
+ function yarnSkips(file) {
112
+ try {
113
+ return readYarnLock(fs.readFileSync(file, "utf-8"), new Map());
114
+ }
115
+ catch {
116
+ return 0;
117
+ }
118
+ }
119
+ /** Lines git writes into a file it could not merge. */
120
+ const CONFLICT_MARKER = /^(?:<{7}|>{7})(?: |$)|^={7}$/m;
121
+ /**
122
+ * Lockfiles that exist here, rank above the file the versions came from (or
123
+ * there is none), and were not read: each with the reason, re-derived from
124
+ * the file so the note can say what is wrong with it.
125
+ */
126
+ function unreadLockfiles(cwd, ecosystem, read) {
127
+ const readFiles = new Set([read.source, ...(read.extraSources ?? [])].filter((f) => Boolean(f)).map((f) => path.resolve(f)));
128
+ const problems = [];
129
+ for (const name of LOCKFILES[ecosystem] ?? []) {
130
+ if (name === "go.sum")
131
+ continue; // read with go.mod, never on its own
132
+ const file = path.join(cwd, name);
133
+ if (readFiles.has(path.resolve(file)))
134
+ break; // everything after it ranks lower
135
+ if (!fs.existsSync(file))
136
+ continue;
137
+ // A yarn.lock whose entries were all skipped for length: named with the count, even when the file is short (L5).
138
+ const skipped = name === "yarn.lock" ? yarnSkips(file) : 0;
139
+ if (skipped > 0) {
140
+ problems.push(yarnSkippedProblem(file, skipped));
141
+ continue;
142
+ }
143
+ const error = describeParseError(file, null);
144
+ if (error)
145
+ problems.push({ file, error });
146
+ }
147
+ return problems;
148
+ }
149
+ /**
150
+ * Why a present lockfile yielded nothing, or null when nothing is wrong with
151
+ * it (a valid lockfile of a project with no dependencies).
152
+ */
153
+ function describeParseError(file, error) {
154
+ let content;
155
+ try {
156
+ content = fs.readFileSync(file, "utf-8");
157
+ }
158
+ catch (readError) {
159
+ return `could not be read (${readError instanceof Error ? readError.message : String(readError)})`;
160
+ }
161
+ if (CONFLICT_MARKER.test(content))
162
+ return "it contains git merge-conflict markers (<<<<<<< / >>>>>>>): resolve the conflict and reinstall to regenerate it";
163
+ if (/\.json$/i.test(file)) {
164
+ try {
165
+ JSON.parse(content);
166
+ }
167
+ catch (parseError) {
168
+ return `invalid JSON (${parseError instanceof Error ? parseError.message.slice(0, 120) : String(parseError)})`;
169
+ }
170
+ }
171
+ if (error)
172
+ return `could not be parsed (${error instanceof Error ? error.message.slice(0, 120) : String(error)})`;
173
+ if (/\.json$/i.test(file))
174
+ return null;
175
+ // A text lockfile of some size that gave no package at all is damaged or in a format this reader does not know.
176
+ const lines = content.split(/\r?\n/).filter((l) => l.trim() !== "").length;
177
+ return lines > 20 ? "no package versions could be read from it (unsupported format or damaged)" : null;
178
+ }
69
179
  /** Instances for a manifest-only read: one per name, scope unknown. */
70
180
  function manifestInstances(versions) {
71
181
  return [...versions].map(([name, version]) => ({ name, version }));
@@ -128,15 +238,17 @@ function resolveNpm(cwd) {
128
238
  const versions = new Map();
129
239
  const instances = new InstanceSet();
130
240
  const specs = new Map();
131
- readYarnLock(fs.readFileSync(yarnLockPath, "utf-8"), versions, instances, specs);
241
+ const skipped = readYarnLock(fs.readFileSync(yarnLockPath, "utf-8"), versions, instances, specs);
132
242
  // The copy a direct dependency resolves to is the one its package.json range names.
133
243
  for (const [name, range] of manifestRanges(cwd)) {
134
244
  const version = specs.get(`${name}@${range}`);
135
245
  if (version)
136
246
  versions.set(name, version);
137
247
  }
138
- if (versions.size > 0)
139
- return found(versions, instances, yarnLockPath, "lockfile");
248
+ if (versions.size > 0) {
249
+ const read = found(versions, instances, yarnLockPath, "lockfile");
250
+ return skipped > 0 ? { ...read, problems: [yarnSkippedProblem(yarnLockPath, skipped)] } : read;
251
+ }
140
252
  }
141
253
  catch { /* fall through */ }
142
254
  }
@@ -26,6 +26,33 @@ export interface VersionSource {
26
26
  extraSources?: string[];
27
27
  /** declared_ranges: the declared requirement per name ("^1.2.0"), for names whose version is only a floor. */
28
28
  ranges?: Map<string, string>;
29
+ /**
30
+ * The direct dependencies the lockfile itself names (Gemfile.lock
31
+ * DEPENDENCIES, composer.json require / require-dev), for ecosystems whose
32
+ * manifest the project scanner does not read.
33
+ */
34
+ direct?: {
35
+ deps: string[];
36
+ devDeps: string[];
37
+ };
38
+ /** Lockfiles present here that could not be read, and why: never a silent "no lockfile". */
39
+ problems?: LockfileProblem[];
40
+ }
41
+ /** A lockfile that exists but could not be read. */
42
+ export interface LockfileProblem {
43
+ /** Absolute path. */
44
+ file: string;
45
+ /** Why, in words ("merge-conflict markers", "invalid JSON: ..."). */
46
+ error: string;
47
+ /**
48
+ * Set when the file WAS read except for this many entries (a yarn.lock line
49
+ * past the length cap): a partial read, not an unreadable file.
50
+ */
51
+ skipped?: number;
52
+ /** Set for a requirements include refused because it points outside the project (B-11): a policy, not damage. */
53
+ refused?: true;
54
+ /** The ecosystem whose resolution reported it (stamped by resolveVersionSource), for ecosystem_filter. */
55
+ ecosystem?: string;
29
56
  }
30
57
  export type SourceKind = "lockfile" | "manifest" | "declared_ranges";
31
58
  export declare function emptySource(): VersionSource;
@@ -40,4 +67,4 @@ export declare class InstanceSet {
40
67
  get size(): number;
41
68
  toArray(): PackageInstance[];
42
69
  }
43
- export declare function found(versions: Map<string, string>, instances: InstanceSet | PackageInstance[], source: string, kind: SourceKind, extra?: Pick<VersionSource, "extraSources" | "ranges">): VersionSource;
70
+ export declare function found(versions: Map<string, string>, instances: InstanceSet | PackageInstance[], source: string, kind: SourceKind, extra?: Pick<VersionSource, "extraSources" | "ranges" | "direct">): VersionSource;
@@ -3,7 +3,7 @@
3
3
  * scan (vulnerability_scan, get_actionable_signals, what_should_i_know,
4
4
  * dependency_health) so they all draw the same line.
5
5
  */
6
- import type { VulnerabilityEntry } from "./types.js";
6
+ import type { OsvVulnerability, VulnerabilityEntry } from "./types.js";
7
7
  /**
8
8
  * Is this advisory a maintenance notice rather than a vulnerability?
9
9
  *
@@ -20,9 +20,23 @@ import type { VulnerabilityEntry } from "./types.js";
20
20
  *
21
21
  * Matched on the advisory's own wording, which RustSec keeps consistent, and
22
22
  * deliberately NOT on "deprecated" — that word appears in plenty of real
23
- * vulnerability summaries.
23
+ * vulnerability summaries. An entry the scan marked from the record's
24
+ * `informational` field (`maintenance`, isMaintenanceRecord) is one whatever
25
+ * its wording.
24
26
  */
25
- export declare function isMaintenanceNotice(v: Pick<VulnerabilityEntry, "summary">): boolean;
27
+ export declare function isMaintenanceNotice(v: Pick<VulnerabilityEntry, "summary"> & {
28
+ maintenance?: boolean;
29
+ }): boolean;
30
+ /**
31
+ * The same predicate over an OSV record, for one package (the rule both engines
32
+ * share, vectors ft-074 / sv-024): the summary wording, OR `informational` of
33
+ * `unmaintained` / `notice` on the record, OR on an `affected[]` entry for this
34
+ * package. RustSec writes it per affected entry, never on the record:
35
+ * RUSTSEC-2018-0017 (`tempdir` crate has been deprecated; use `tempfile`)
36
+ * carries it only there, and was graded a vulnerability while only the record
37
+ * level was read.
38
+ */
39
+ export declare function isMaintenanceRecord(vuln: Pick<OsvVulnerability, "summary" | "database_specific" | "affected">, packageName: string, ecosystem: string): boolean;
26
40
  /**
27
41
  * The advisories a scan can actually ask the user to act on today: built on
28
42
  * this host AND exploitable (not a maintenance notice). This is the set
@@ -30,4 +44,6 @@ export declare function isMaintenanceNotice(v: Pick<VulnerabilityEntry, "summary
30
44
  * every other tool that counts "vulnerable" must count the same set or the
31
45
  * numbers disagree across tools over one scan.
32
46
  */
33
- export declare function isActionableVulnerability(v: Pick<VulnerabilityEntry, "summary" | "platformActive">): boolean;
47
+ export declare function isActionableVulnerability(v: Pick<VulnerabilityEntry, "summary" | "platformActive"> & {
48
+ maintenance?: boolean;
49
+ }): boolean;
@@ -4,6 +4,10 @@
4
4
  * scan (vulnerability_scan, get_actionable_signals, what_should_i_know,
5
5
  * dependency_health) so they all draw the same line.
6
6
  */
7
+ import { samePackageName } from "./version-compare.js";
8
+ /** The `informational` kinds that make a notice. `unsound` is a vulnerability: it has real fixes. */
9
+ const NOTICE_KINDS = new Set(["unmaintained", "notice"]);
10
+ const NOTICE_WORDING = /\b(unmaintained|no longer maintained)\b/i;
7
11
  /**
8
12
  * Is this advisory a maintenance notice rather than a vulnerability?
9
13
  *
@@ -20,10 +24,35 @@
20
24
  *
21
25
  * Matched on the advisory's own wording, which RustSec keeps consistent, and
22
26
  * deliberately NOT on "deprecated" — that word appears in plenty of real
23
- * vulnerability summaries.
27
+ * vulnerability summaries. An entry the scan marked from the record's
28
+ * `informational` field (`maintenance`, isMaintenanceRecord) is one whatever
29
+ * its wording.
24
30
  */
25
31
  export function isMaintenanceNotice(v) {
26
- return /\b(unmaintained|no longer maintained)\b/i.test(v.summary || "");
32
+ // The scan decided from the record (isMaintenanceRecord); only an entry without that answer
33
+ // falls back to the wording.
34
+ return typeof v.maintenance === "boolean" ? v.maintenance : NOTICE_WORDING.test(v.summary || "");
35
+ }
36
+ /**
37
+ * The same predicate over an OSV record, for one package (the rule both engines
38
+ * share, vectors ft-074 / sv-024): the summary wording, OR `informational` of
39
+ * `unmaintained` / `notice` on the record, OR on an `affected[]` entry for this
40
+ * package. RustSec writes it per affected entry, never on the record:
41
+ * RUSTSEC-2018-0017 (`tempdir` crate has been deprecated; use `tempfile`)
42
+ * carries it only there, and was graded a vulnerability while only the record
43
+ * level was read.
44
+ */
45
+ export function isMaintenanceRecord(vuln, packageName, ecosystem) {
46
+ // Structured data beats wording (the rule as amended 2026-10-26, 4DA fix/notice-class-beats-wording):
47
+ // a class the record states for this package decides alone, the affected[] entry's first, else
48
+ // the record's. `unsound` is never a notice, whatever its summary says: serde_yml's
49
+ // unsound advisory reads "unmaintained" and was hidden as a notice that never blocks
50
+ // (audit 6.2.5 F-7). The wording counts only when no class is stated.
51
+ const own = (vuln.affected ?? []).find((a) => a.package?.ecosystem === ecosystem && samePackageName(a.package.name, packageName, ecosystem) && a.database_specific?.informational)?.database_specific?.informational;
52
+ const stated = own ?? vuln.database_specific?.informational;
53
+ if (stated)
54
+ return NOTICE_KINDS.has(stated);
55
+ return NOTICE_WORDING.test(vuln.summary ?? "");
27
56
  }
28
57
  /**
29
58
  * The advisories a scan can actually ask the user to act on today: built on
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Whether an advisory is a malicious-package record: THE predicate, decided by the 4DA lead for
3
+ * both engines (2026-10-18). A MAL- id (OpenSSF's malicious-packages feed), a MAL- alias, or
4
+ * CWE-506 ("Embedded Malicious Code") in the record's `database_specific.cwe_ids`. GitHub's
5
+ * records of npm's removed malware carry no MAL- id: GHSA-c2m4-w5hm-vqjw ("crossenv is malware")
6
+ * has aliases [CVE-2017-16074] and cwe_ids ["CWE-506"]. GitHub's older malware records carry
7
+ * neither: GHSA-28f8-hqmc-7ph8 "Malicious Package in ember-power-timepicker" (reviewed CRITICAL)
8
+ * has aliases [] and cwe_ids [], one of 49 such npm records, so GitHub's fixed advisory title
9
+ * template counts too: a summary that STARTS "Malicious Package in " or "Malicious code in "
10
+ * (any case). Free prose that merely mentions "malicious" or "malware" does not.
11
+ *
12
+ * Every malware decision goes through it: the offered-release check, the clean-release predicate
13
+ * (a release it affects is never clean, never a target, never "newest"), parent standing, the
14
+ * security-holding note and the severity rank (an installed malicious copy ranks critical).
15
+ */
16
+ export declare function isMalware(id: string, aliases?: readonly string[], cweIds?: readonly unknown[], summary?: string): boolean;
17
+ /** The CWE ids an OSV record names (`database_specific.cwe_ids`), as strings. */
18
+ export declare function cweIdsOf(record: {
19
+ database_specific?: unknown;
20
+ }): string[];
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Whether an advisory is a malicious-package record: THE predicate, decided by the 4DA lead for
4
+ * both engines (2026-10-18). A MAL- id (OpenSSF's malicious-packages feed), a MAL- alias, or
5
+ * CWE-506 ("Embedded Malicious Code") in the record's `database_specific.cwe_ids`. GitHub's
6
+ * records of npm's removed malware carry no MAL- id: GHSA-c2m4-w5hm-vqjw ("crossenv is malware")
7
+ * has aliases [CVE-2017-16074] and cwe_ids ["CWE-506"]. GitHub's older malware records carry
8
+ * neither: GHSA-28f8-hqmc-7ph8 "Malicious Package in ember-power-timepicker" (reviewed CRITICAL)
9
+ * has aliases [] and cwe_ids [], one of 49 such npm records, so GitHub's fixed advisory title
10
+ * template counts too: a summary that STARTS "Malicious Package in " or "Malicious code in "
11
+ * (any case). Free prose that merely mentions "malicious" or "malware" does not.
12
+ *
13
+ * Every malware decision goes through it: the offered-release check, the clean-release predicate
14
+ * (a release it affects is never clean, never a target, never "newest"), parent standing, the
15
+ * security-holding note and the severity rank (an installed malicious copy ranks critical).
16
+ */
17
+ export function isMalware(id, aliases = [], cweIds = [], summary = "") {
18
+ // Exactly fourda_depengine::is_malware (4DA #992, b18f364f5): ids and CWEs trimmed and
19
+ // case-insensitive; the summary trimStart-ed, lowercased, then matched at the start.
20
+ if ([id, ...aliases].some((x) => typeof x === "string" && x.trim().toUpperCase().startsWith("MAL-")))
21
+ return true;
22
+ if (cweIds.some((c) => typeof c === "string" && c.trim().toUpperCase() === "CWE-506"))
23
+ return true;
24
+ const title = (summary ?? "").trimStart().toLowerCase();
25
+ return title.startsWith("malicious package in ") || title.startsWith("malicious code in ");
26
+ }
27
+ /** The CWE ids an OSV record names (`database_specific.cwe_ids`), as strings. */
28
+ export function cweIdsOf(record) {
29
+ const ids = record.database_specific?.cwe_ids;
30
+ return Array.isArray(ids) ? ids.filter((x) => typeof x === "string") : [];
31
+ }
@@ -29,11 +29,14 @@ interface NpmVersionDoc {
29
29
  hasInstallScript?: boolean;
30
30
  dependencies?: Record<string, string>;
31
31
  optionalDependencies?: Record<string, string>;
32
- deprecated?: string;
32
+ deprecated?: string | boolean;
33
33
  }
34
34
  export interface NpmPackument {
35
35
  versions?: Record<string, NpmVersionDoc>;
36
36
  time?: Record<string, unknown>;
37
+ "dist-tags"?: {
38
+ latest?: string;
39
+ };
37
40
  }
38
41
  export declare class NpmPackumentReader implements ReleaseMetadataSource {
39
42
  private cache;
@@ -29,7 +29,8 @@ export class NpmPackumentReader {
29
29
  this.rateLimiter = rateLimiter;
30
30
  }
31
31
  async getReleases(name) {
32
- const key = `npm-packument:${name}`;
32
+ // v2: the projection carries `latest` (FX-H5); a v1 row revalidated by a 304 would lack it.
33
+ const key = `npm-packument:v2:${name}`;
33
34
  const cached = this.cache.getStale(key)?.data ?? null;
34
35
  if (cached && Date.now() - cached.fetchedAtMs < FRESH_MS) {
35
36
  return { status: "ok", data: cached.index };
@@ -85,7 +86,8 @@ export function projectPackument(name, doc) {
85
86
  trust,
86
87
  trustEvidence,
87
88
  installScripts: installScriptsOf(v),
88
- deprecated: typeof v.deprecated === "string" && v.deprecated.length > 0 ? v.deprecated : null,
89
+ // `deprecated: true` (no message) counts too, as in the 4DA app.
90
+ deprecated: typeof v.deprecated === "string" && v.deprecated.length > 0 ? v.deprecated : v.deprecated === true ? "deprecated" : null,
89
91
  withdrawn: null,
90
92
  dependencies: [
91
93
  ...new Set([...Object.keys(v.dependencies ?? {}), ...Object.keys(v.optionalDependencies ?? {})]),
@@ -123,6 +125,7 @@ export function projectPackument(name, doc) {
123
125
  name,
124
126
  createdAt: typeof time.created === "string" ? time.created : null,
125
127
  releases,
128
+ latest: typeof doc["dist-tags"]?.latest === "string" ? doc["dist-tags"].latest : null,
126
129
  };
127
130
  }
128
131
  function npmTrust(v) {
@@ -0,0 +1,53 @@
1
+ /**
2
+ * A release a tool OFFERS as an upgrade (upgrade_planner's target for a row with no advisory,
3
+ * dependency_health's latestStableVersion, what_should_i_know's "latest") is checked the way
4
+ * dependency_check decides `proceed`: against every advisory of the package with the same
5
+ * predicate (osv-ranges.ts advisoriesAffecting) and against the registry's withdrawals. A
6
+ * known-affected or withdrawn release is never offered as a safe upgrade, and the answer says why.
7
+ *
8
+ * Final release audit of 6.2.5 (2026-10-18): upgrade_planner sent crossenv 0.0.2-security, npm's
9
+ * placeholder for a removed malware package, to 1.0.1, which GHSA-c2m4-w5hm-vqjw (last_affected
10
+ * 1.0.1) lists as the malware itself. The same held for cross-env.js, mongose, babelcli, d3.js,
11
+ * jquery.js, ffmepg, nodesass and nodefabric: rows with no advisory on the installed version took
12
+ * the registry's latest stable release with no check at all.
13
+ */
14
+ import type { FixPathSources } from "./fix-paths.js";
15
+ import type { OsvEcosystem } from "./types.js";
16
+ export interface OfferedRelease {
17
+ /** The release the tool was about to offer. */
18
+ offered: string;
19
+ /** False when the package's advisories could not be read: the offer is unchecked, and said to be. */
20
+ checked: boolean;
21
+ /** Advisories that affect `offered`. */
22
+ affectedBy: string[];
23
+ /** The registry's words when it withdrew `offered` (yanked, or deprecated with a later clean release on its line). */
24
+ withdrawn: string | null;
25
+ /** The release to offer instead: the newest release above the installed one that is neither affected nor withdrawn; null when none. */
26
+ safe: string | null;
27
+ /**
28
+ * The installed version is npm's security placeholder (`0.0.x-security`, published after the
29
+ * malware was taken down) and no release above it is safe: the placeholder is the safe state,
30
+ * and the step is removing the dependency.
31
+ */
32
+ holding: boolean;
33
+ /** Plain words for an offer that changed or could not be checked; null when `offered` stands as it is. */
34
+ note: string | null;
35
+ }
36
+ /**
37
+ * Check one offered release. `installed` is the version the project holds; the replacement is
38
+ * looked for above it (a stable one unless nothing stable is newer).
39
+ */
40
+ export declare function vetOfferedRelease(sources: FixPathSources, ecosystem: OsvEcosystem, name: string, installed: string | null, offered: string): Promise<OfferedRelease>;
41
+ /**
42
+ * vetOfferedRelease over many rows within a time budget; a row not checked in time is absent from
43
+ * the result, and the caller says so. Keyed by `key`.
44
+ */
45
+ export declare function vetOfferedReleases(sources: FixPathSources | null, items: Array<{
46
+ key: string;
47
+ ecosystem: OsvEcosystem;
48
+ name: string;
49
+ installed: string | null;
50
+ offered: string;
51
+ }>, budgetMs: number, concurrency?: number): Promise<Map<string, OfferedRelease>>;
52
+ /** Said for an offer that could not be checked in time (the budget ran out, or offline). */
53
+ export declare function uncheckedOfferNote(name: string, offered: string): string;