@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,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
  /**
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import * as fs from "node:fs";
17
17
  import * as path from "node:path";
18
- import { normalizePythonName, parseGoMod } from "./live/lockfile-parsers-pygo.js";
18
+ import { compiledIndirect, compiledRequirements, confinedInclude, normalizePythonName, parseGoMod, requirementOptions } from "./live/lockfile-parsers-pygo.js";
19
19
  /** The body of a TOML table (`[name]`) up to the next table header, or null. */
20
20
  export function tomlTable(content, name) {
21
21
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -130,6 +130,37 @@ export function parseRequirementNames(content) {
130
130
  }
131
131
  return [...new Set(names)];
132
132
  }
133
+ /**
134
+ * Requirement names from a requirements file and every file it includes with `-r` (confined to
135
+ * `root`, as the version reader confines them): an included file's pins are declared too. A pin
136
+ * in `-r sub/inner.txt` read as "Transitive dependency, not declared in your manifest" (final
137
+ * release audit of 6.2.5, 7g: jinja2==2.10).
138
+ */
139
+ export function requirementNamesWithIncludes(file, root = path.dirname(path.resolve(file)), visited = new Set()) {
140
+ const key = path.resolve(file).toLowerCase();
141
+ if (visited.has(key))
142
+ return [];
143
+ visited.add(key);
144
+ let content;
145
+ try {
146
+ content = fs.readFileSync(file, "utf-8");
147
+ }
148
+ catch {
149
+ return [];
150
+ }
151
+ // A compiled file pins its whole tree: a pin there only because other packages require it
152
+ // (`# via requests`) is transitive, as go.mod's `// indirect` is (audit 6.2.5 A-4).
153
+ const compiled = compiledRequirements(content);
154
+ const names = parseRequirementNames(content).filter((n) => !compiled || !compiledIndirect(compiled, n));
155
+ for (const { option, target } of requirementOptions(content)) {
156
+ if (option !== "r")
157
+ continue;
158
+ const included = confinedInclude(path.dirname(file), target, root);
159
+ if (included)
160
+ names.push(...requirementNamesWithIncludes(included, root, visited));
161
+ }
162
+ return [...new Set(names)];
163
+ }
133
164
  /** Direct Go dependencies: go.mod requirements not marked `// indirect`. */
134
165
  export function parseGoModDirectDeps(content) {
135
166
  return parseGoMod(content)
@@ -162,12 +193,15 @@ export function cargoWorkspaceDeps(root, content) {
162
193
  const name = /^name\s*=\s*"([^"]+)"/m.exec(tomlTable(manifest, "package") ?? "")?.[1];
163
194
  if (name)
164
195
  memberNames.add(name);
165
- for (const dep of tomlKeys(tomlTable(manifest, "dependencies")))
166
- deps.add(dep);
167
- for (const section of ["dev-dependencies", "build-dependencies"]) {
196
+ // build-dependencies run at build time on every machine that builds the crate: they are not
197
+ // dev-only (the app reads them as normal), and discounting them hid memoffset with
198
+ // include_dev: false (audit 6.2.5 F-3).
199
+ for (const section of ["dependencies", "build-dependencies"]) {
168
200
  for (const dep of tomlKeys(tomlTable(manifest, section)))
169
- devDeps.add(dep);
201
+ deps.add(dep);
170
202
  }
203
+ for (const dep of tomlKeys(tomlTable(manifest, "dev-dependencies")))
204
+ devDeps.add(dep);
171
205
  }
172
206
  }
173
207
  const strip = (set) => [...set].filter((d) => !memberNames.has(d));
@@ -176,6 +210,12 @@ export function cargoWorkspaceDeps(root, content) {
176
210
  }
177
211
  /** Member directories for one `members` entry: a path, or a path whose last segment is `*`. */
178
212
  function expandMemberGlob(root, pattern) {
213
+ // `members = ["../outside/*"]` was followed out of the project: manifests outside it were read
214
+ // and their dependency names sent to the registries (audit 6.2.5 C-4). Members must resolve inside.
215
+ const inside = (dir) => confinedInclude(root, path.relative(root, dir), root) !== null;
216
+ return expandMemberGlobUnconfined(root, pattern).filter(inside);
217
+ }
218
+ function expandMemberGlobUnconfined(root, pattern) {
179
219
  if (!pattern.includes("*"))
180
220
  return [path.join(root, pattern)];
181
221
  const star = pattern.indexOf("*");
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
- import { cargoWorkspaceDeps, parseGoModDirectDeps, parsePyprojectDependencies, parseRequirementNames, } from "./project-manifests.js";
16
+ import { cargoWorkspaceDeps, parseGoModDirectDeps, parsePyprojectDependencies, requirementNamesWithIncludes, } from "./project-manifests.js";
17
17
  // =============================================================================
18
18
  // Framework Detection Maps
19
19
  // =============================================================================
@@ -190,7 +190,8 @@ export function scanCurrentProject(cwd) {
190
190
  const rustDevDeps = [];
191
191
  parseTOMLDependencies(content, "[dependencies]", rustDeps, fwSet, RUST_FRAMEWORK_MAP);
192
192
  parseTOMLDependencies(content, "[dev-dependencies]", rustDevDeps, fwSet, RUST_FRAMEWORK_MAP);
193
- parseTOMLDependencies(content, "[build-dependencies]", rustDevDeps, fwSet, RUST_FRAMEWORK_MAP);
193
+ // build-dependencies run at build time on every machine that builds the crate: normal, not dev (audit 6.2.5 F-3).
194
+ parseTOMLDependencies(content, "[build-dependencies]", rustDeps, fwSet, RUST_FRAMEWORK_MAP);
194
195
  // Platform-gated deps: [target.'cfg(...)'.dependencies] / [target.<triple>.dependencies].
195
196
  // Previously skipped entirely (e.g. windows-sys, winreg, libc were invisible).
196
197
  parseTargetDependencies(content, rustDeps, rustDevDeps, result.depTargets, fwSet, RUST_FRAMEWORK_MAP);
@@ -243,7 +244,8 @@ export function scanCurrentProject(cwd) {
243
244
  if (fs.existsSync(requirementsPath)) {
244
245
  langSet.add("python");
245
246
  try {
246
- parseRequirementNames(fs.readFileSync(requirementsPath, "utf-8")).forEach((d) => pyDeps.add(d));
247
+ // Included files (`-r sub/inner.txt`) declare their pins too (audit 7g).
248
+ requirementNamesWithIncludes(requirementsPath, cwd).forEach((d) => pyDeps.add(d));
247
249
  }
248
250
  catch {
249
251
  // Skip
@@ -364,7 +366,7 @@ function parseTargetDependencies(content, deps, devDeps, depTargets, frameworkSe
364
366
  let m;
365
367
  while ((m = headerRegex.exec(content)) !== null) {
366
368
  const spec = m[1].trim().replace(/^['"]|['"]$/g, ""); // strip surrounding quotes
367
- const isDev = m[2] !== "dependencies";
369
+ const isDev = m[2] === "dev-dependencies"; // build-dependencies are normal (audit 6.2.5 F-3)
368
370
  // Body runs from the end of this header line to the next section header.
369
371
  const rest = content.slice(m.index + m[0].length);
370
372
  const nextHeader = rest.search(/^\[/m);
@@ -22,6 +22,13 @@
22
22
  * lockfile that was left out and say why.
23
23
  */
24
24
  import { type ProjectScanResult } from "./project-scanner.js";
25
+ /**
26
+ * Manifests and lockfiles of ecosystems this server does not read, by file
27
+ * name: reported as `not_covered` so an empty or clean answer is never read
28
+ * as "no dependencies" (audit A2: a Maven or .NET project was told "No
29
+ * manifest files found... run from your project root").
30
+ */
31
+ export declare function unsupportedEcosystem(name: string, names: string[]): string | null;
25
32
  /**
26
33
  * Measured on langchain (108 lockfiles, up to 3 levels down; 2026-10-10):
27
34
  * the walk plus every project scan takes about 2 s. Depth 4 covers it with a
@@ -48,6 +55,18 @@ export interface TreeDiscovery {
48
55
  skippedCount: number;
49
56
  /** True when the walk itself stopped at MAX_WALK_DIRS: lockfiles beyond it were not even counted. */
50
57
  walkTruncated: boolean;
58
+ /** Manifests and lockfiles of ecosystems this server does not read (the first MAX_LISTED_NOT_COVERED), relative to the root. */
59
+ notCovered?: Array<{
60
+ path: string;
61
+ ecosystem: string;
62
+ }>;
63
+ /** Every such file found (`notCovered` may list fewer). */
64
+ notCoveredCount?: number;
65
+ /** Lockfiles under test-fixture directories, left out unless include_test_fixtures was set. */
66
+ fixtures?: {
67
+ count: number;
68
+ dirs: string[];
69
+ };
51
70
  }
52
71
  /** Python requirement files: requirements.txt, requirements-dev.txt, dev_requirements.txt, _test_minimum_requirements.txt. */
53
72
  export declare function isRequirementsFileName(name: string): boolean;
@@ -62,10 +81,47 @@ export declare function lockfilesIn(dir: string, listing?: string[]): string[];
62
81
  export declare function scanProjectTree(root: string, limits?: {
63
82
  maxDepth: number;
64
83
  maxProjects: number;
84
+ }, options?: {
85
+ includeFixtures?: boolean;
65
86
  }): {
66
87
  entries: ProjectTreeEntry[];
67
88
  discovery: TreeDiscovery;
68
89
  };
90
+ /**
91
+ * The walk's report for `root`, taken once per process. For answers that
92
+ * had no walk recorded and must not claim "nothing here" unchecked (H3):
93
+ * vulnerability_scan's "nothing found" message and dependency_health's
94
+ * nothing-checked answer. Re-walking on every call cost a full tree walk per
95
+ * call (review of H3, 2026-10-18).
96
+ */
97
+ export declare function discoveryFor(root: string): TreeDiscovery;
98
+ /** A lockfile problem as the notes read it (lockfile-types.ts LockfileProblem). */
99
+ export type LockfileProblemLike = {
100
+ file: string;
101
+ error: string;
102
+ skipped?: number;
103
+ refused?: true;
104
+ };
105
+ /**
106
+ * The three kinds, kept apart: a file that could not be read (UNREADABLE),
107
+ * one read with entries skipped (`skipped`, PARTIAL: a yarn.lock or
108
+ * requirements line past the length cap, audit L5), and a requirements
109
+ * include refused because it points outside the project (`refused`, B-11).
110
+ */
111
+ export declare function splitLockfileProblems<P extends LockfileProblemLike>(problems: P[]): {
112
+ unreadable: P[];
113
+ partial: P[];
114
+ refused: P[];
115
+ };
116
+ /**
117
+ * One wording for every tool (vulnerability_scan, dependency_health,
118
+ * upgrade_planner). `show` renders a path (the scan shows it relative).
119
+ */
120
+ export declare function lockfileProblemNotes(problems: LockfileProblemLike[], show?: (file: string) => string): string[];
121
+ /** Kept for its importers (upgrade_planner): lockfileProblemNotes. */
122
+ export declare function unreadableNotes(problems: LockfileProblemLike[]): string[];
123
+ /** What a scan left out on purpose or cannot read: ecosystems it does not cover, and test fixtures. */
124
+ export declare function exclusionNotes(discovery: TreeDiscovery | null): string[];
69
125
  /** One sentence for an answer, or null when nothing was left out. */
70
126
  export declare function discoveryNote(discovery: TreeDiscovery | null): string | null;
71
127
  /**
@@ -38,12 +38,57 @@ const LOCK_MARKERS = [
38
38
  "pdm.lock",
39
39
  "Pipfile.lock",
40
40
  "go.mod",
41
+ "Gemfile.lock",
42
+ "composer.lock",
41
43
  ];
42
44
  const SKIP_DIRS = new Set([
43
45
  "node_modules", "target", ".git", "dist", "build", "out", "vendor", ".venv", "venv",
44
- "__pycache__", ".next", ".nuxt", "coverage", ".claude", ".codex", ".cursor", "fixtures",
45
- "__fixtures__", "testdata", "test-fixtures", "__tests__", ".turbo", ".cache", ".pnpm-store",
46
+ "__pycache__", ".next", ".nuxt", "coverage", ".claude", ".codex", ".cursor",
47
+ ".turbo", ".cache", ".pnpm-store",
46
48
  ]);
49
+ /**
50
+ * Directories whose lockfiles are test data, not the project's dependencies:
51
+ * a scan of D:\4DA\src-tauri read the 4DA conformance corpus
52
+ * (tests/conformance/cases, 10,000+ vendored packages) as the app's own
53
+ * (audit A3, 2026-10-12). Left out by default, counted and named in the
54
+ * answer (`not_scanned`), and scanned on request (include_test_fixtures).
55
+ */
56
+ const FIXTURE_DIRS = new Set([
57
+ "fixtures", "__fixtures__", "testdata", "test-data", "test-fixtures", "__tests__", "tests", "test", "conformance",
58
+ ]);
59
+ /**
60
+ * Manifests and lockfiles of ecosystems this server does not read, by file
61
+ * name: reported as `not_covered` so an empty or clean answer is never read
62
+ * as "no dependencies" (audit A2: a Maven or .NET project was told "No
63
+ * manifest files found... run from your project root").
64
+ */
65
+ export function unsupportedEcosystem(name, names) {
66
+ if (name === "pom.xml")
67
+ return "Maven";
68
+ if (/^build\.gradle(\.kts)?$/.test(name) || name === "gradle.lockfile")
69
+ return "Maven (Gradle)";
70
+ if (/\.(cs|fs|vb)proj$/i.test(name) || name === "packages.lock.json" || name === "packages.config" || name === "Directory.Packages.props")
71
+ return "NuGet";
72
+ if (name === "pubspec.lock")
73
+ return "Pub";
74
+ if (name === "mix.lock")
75
+ return "Hex";
76
+ if (name === "Package.resolved")
77
+ return "SwiftPM";
78
+ if (name === "Podfile.lock")
79
+ return "CocoaPods";
80
+ if (name === "conan.lock")
81
+ return "ConanCenter";
82
+ if (name === "renv.lock")
83
+ return "CRAN";
84
+ if (name === "Gemfile" && !names.includes("Gemfile.lock"))
85
+ return "RubyGems (Gemfile with no Gemfile.lock)";
86
+ if (name === "composer.json" && !names.includes("composer.lock"))
87
+ return "Packagist (composer.json with no composer.lock)";
88
+ return null;
89
+ }
90
+ /** Entries listed in `notCovered`; the count covers the rest. */
91
+ const MAX_LISTED_NOT_COVERED = 25;
47
92
  /**
48
93
  * Measured on langchain (108 lockfiles, up to 3 levels down; 2026-10-10):
49
94
  * the walk plus every project scan takes about 2 s. Depth 4 covers it with a
@@ -88,13 +133,26 @@ export function lockfilesIn(dir, listing) {
88
133
  * repository's .gitignore excludes are not part of the project and are
89
134
  * skipped (gitignore.ts); they are not reported as left out either.
90
135
  */
91
- export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProjects: MAX_PROJECTS }) {
136
+ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProjects: MAX_PROJECTS }, options = {}) {
92
137
  const entries = [{ dir: root, scan: scanCurrentProject(root) }];
93
138
  const ignore = new IgnoreRules(root);
94
139
  const skipped = [];
95
140
  let skippedCount = 0;
96
141
  let visited = 0;
97
142
  let walkTruncated = false;
143
+ const notCovered = [];
144
+ let notCoveredCount = 0;
145
+ const fixtures = { count: 0, dirs: [] };
146
+ const noteUnsupported = (rel, names) => {
147
+ for (const name of names) {
148
+ const ecosystem = unsupportedEcosystem(name, names);
149
+ if (!ecosystem)
150
+ continue;
151
+ notCoveredCount++;
152
+ if (notCovered.length < MAX_LISTED_NOT_COVERED)
153
+ notCovered.push({ path: rel ? `${rel}/${name}` : name, ecosystem });
154
+ }
155
+ };
98
156
  // One directory listing per directory: it decides both whether the
99
157
  // directory is a project and which children to descend into.
100
158
  const listing = (dir) => {
@@ -105,6 +163,25 @@ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProject
105
163
  return null;
106
164
  }
107
165
  };
166
+ // Test-fixture lockfiles: counted, never scanned (unless asked for).
167
+ const countFixtures = (dir, children) => {
168
+ for (const child of children) {
169
+ if (!child.isDirectory() || SKIP_DIRS.has(child.name) || child.name.startsWith("."))
170
+ continue;
171
+ if (++visited > MAX_WALK_DIRS) {
172
+ walkTruncated = true;
173
+ return;
174
+ }
175
+ const sub = path.join(dir, child.name);
176
+ const grandchildren = listing(sub);
177
+ if (!grandchildren)
178
+ continue;
179
+ fixtures.count += lockfilesIn(sub, grandchildren.map((g) => g.name)).length;
180
+ countFixtures(sub, grandchildren);
181
+ if (walkTruncated)
182
+ return;
183
+ }
184
+ };
108
185
  const walk = (dir, rel, depth, children) => {
109
186
  for (const child of children) {
110
187
  if (!child.isDirectory() || SKIP_DIRS.has(child.name) || child.name.startsWith("."))
@@ -120,10 +197,21 @@ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProject
120
197
  const grandchildren = listing(sub);
121
198
  if (!grandchildren)
122
199
  continue;
200
+ if (!options.includeFixtures && FIXTURE_DIRS.has(child.name.toLowerCase())) {
201
+ const before = fixtures.count;
202
+ fixtures.count += lockfilesIn(sub, grandchildren.map((g) => g.name)).length;
203
+ countFixtures(sub, grandchildren);
204
+ if (fixtures.count > before && fixtures.dirs.length < 10)
205
+ fixtures.dirs.push(childRel);
206
+ if (walkTruncated)
207
+ return;
208
+ continue;
209
+ }
123
210
  // A .gitignore is only read where one exists.
124
211
  if (grandchildren.some((g) => g.name === ".gitignore"))
125
212
  ignore.load(childRel);
126
213
  const names = grandchildren.map((g) => g.name);
214
+ noteUnsupported(childRel, names.filter((n) => !ignore.ignores(`${childRel}/${n}`, false)));
127
215
  const locks = lockfilesIn(sub, names).filter((name) => !ignore.ignores(`${childRel}/${name}`, false));
128
216
  if (locks.length > 0) {
129
217
  const reason = depth > limits.maxDepth ? "depth" : entries.length >= limits.maxProjects ? "project_limit" : null;
@@ -142,12 +230,91 @@ export function scanProjectTree(root, limits = { maxDepth: MAX_DEPTH, maxProject
142
230
  return;
143
231
  }
144
232
  };
145
- walk(root, "", 1, listing(root) ?? []);
233
+ const rootListing = listing(root) ?? [];
234
+ noteUnsupported("", rootListing.filter((d) => d.isFile()).map((d) => d.name));
235
+ walk(root, "", 1, rootListing);
146
236
  return {
147
237
  entries,
148
- discovery: { projects: entries.length, maxDepth: limits.maxDepth, maxProjects: limits.maxProjects, skipped, skippedCount, walkTruncated },
238
+ discovery: {
239
+ projects: entries.length,
240
+ maxDepth: limits.maxDepth,
241
+ maxProjects: limits.maxProjects,
242
+ skipped,
243
+ skippedCount,
244
+ walkTruncated,
245
+ ...(notCoveredCount > 0 ? { notCovered, notCoveredCount } : {}),
246
+ ...(fixtures.count > 0 ? { fixtures } : {}),
247
+ },
149
248
  };
150
249
  }
250
+ const walkedDiscovery = new Map();
251
+ /**
252
+ * The walk's report for `root`, taken once per process. For answers that
253
+ * had no walk recorded and must not claim "nothing here" unchecked (H3):
254
+ * vulnerability_scan's "nothing found" message and dependency_health's
255
+ * nothing-checked answer. Re-walking on every call cost a full tree walk per
256
+ * call (review of H3, 2026-10-18).
257
+ */
258
+ export function discoveryFor(root) {
259
+ const key = path.resolve(root).toLowerCase();
260
+ let found = walkedDiscovery.get(key);
261
+ if (!found) {
262
+ found = scanProjectTree(root).discovery;
263
+ walkedDiscovery.set(key, found);
264
+ }
265
+ return found;
266
+ }
267
+ /**
268
+ * The three kinds, kept apart: a file that could not be read (UNREADABLE),
269
+ * one read with entries skipped (`skipped`, PARTIAL: a yarn.lock or
270
+ * requirements line past the length cap, audit L5), and a requirements
271
+ * include refused because it points outside the project (`refused`, B-11).
272
+ */
273
+ export function splitLockfileProblems(problems) {
274
+ return {
275
+ unreadable: problems.filter((p) => !p.skipped && !p.refused),
276
+ partial: problems.filter((p) => Boolean(p.skipped) && !p.refused),
277
+ refused: problems.filter((p) => p.refused === true),
278
+ };
279
+ }
280
+ /**
281
+ * One wording for every tool (vulnerability_scan, dependency_health,
282
+ * upgrade_planner). `show` renders a path (the scan shows it relative).
283
+ */
284
+ export function lockfileProblemNotes(problems, show = (f) => f) {
285
+ const listed = (ps) => `${ps.slice(0, 3).map((p) => `${show(p.file)} (${p.error})`).join("; ")}${ps.length > 3 ? "; ..." : ""}`;
286
+ const { unreadable, partial, refused } = splitLockfileProblems(problems);
287
+ const notes = [];
288
+ if (unreadable.length > 0) {
289
+ notes.push(`UNREADABLE: ${unreadable.length} lockfile${unreadable.length === 1 ? "" : "s"} could not be read, so ${unreadable.length === 1 ? "its" : "their"} dependencies were not checked: ${listed(unreadable)}.`);
290
+ }
291
+ if (partial.length > 0) {
292
+ notes.push(`PARTIAL: ${partial.length} lockfile${partial.length === 1 ? " was" : "s were"} read with entries skipped: ${listed(partial)}.`);
293
+ }
294
+ if (refused.length > 0) {
295
+ notes.push(`REFUSED: ${refused.length} requirements include${refused.length === 1 ? " was" : "s were"} not read: ${listed(refused)}.`);
296
+ }
297
+ return notes;
298
+ }
299
+ /** Kept for its importers (upgrade_planner): lockfileProblemNotes. */
300
+ export function unreadableNotes(problems) {
301
+ return lockfileProblemNotes(problems);
302
+ }
303
+ /** What a scan left out on purpose or cannot read: ecosystems it does not cover, and test fixtures. */
304
+ export function exclusionNotes(discovery) {
305
+ const notes = [];
306
+ const unsupported = discovery?.notCoveredCount ?? 0;
307
+ if (unsupported > 0) {
308
+ const ecosystems = [...new Set((discovery?.notCovered ?? []).map((n) => n.ecosystem.split(" ")[0]))].join(", ");
309
+ const listed = (discovery?.notCovered ?? []).slice(0, 3).map((n) => n.path).join(", ");
310
+ notes.push(`NOT COVERED: ${unsupported} manifest/lockfile${unsupported === 1 ? "" : "s"} of ecosystems this server does not read (${ecosystems}): ${listed}${unsupported > 3 ? ", ..." : ""}. Their dependencies were not checked at all; see not_covered.`);
311
+ }
312
+ const fixtures = discovery?.fixtures;
313
+ if (fixtures && fixtures.count > 0) {
314
+ notes.push(`Not scanned: ${fixtures.count} test-fixture lockfile${fixtures.count === 1 ? "" : "s"} under ${fixtures.dirs.slice(0, 3).join(", ")}${fixtures.dirs.length > 3 ? ", ..." : ""} (pass include_test_fixtures: true to scan them).`);
315
+ }
316
+ return notes;
317
+ }
151
318
  /** One sentence for an answer, or null when nothing was left out. */
152
319
  export function discoveryNote(discovery) {
153
320
  if (!discovery || (discovery.skippedCount === 0 && !discovery.walkTruncated))
@@ -173,6 +340,8 @@ const LOCKS_BY_LANGUAGE = {
173
340
  rust: ["Cargo.lock"],
174
341
  python: ["poetry.lock", "uv.lock", "pdm.lock", "Pipfile.lock"],
175
342
  go: ["go.mod"],
343
+ ruby: ["Gemfile.lock"],
344
+ php: ["composer.lock"],
176
345
  };
177
346
  /**
178
347
  * Resolution groups for the live layer: one per (project directory, ecosystem).