@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.
- package/CHANGELOG.md +520 -0
- package/README.md +22 -6
- package/dist/db.d.ts +38 -0
- package/dist/db.js +78 -23
- package/dist/doctor.js +6 -3
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.js +16 -14
- package/dist/index.js +73 -103
- package/dist/live/advisory-groups.d.ts +68 -0
- package/dist/live/advisory-groups.js +131 -0
- package/dist/live/cache.js +11 -3
- package/dist/live/cargo-platform.d.ts +6 -0
- package/dist/live/cargo-platform.js +8 -2
- package/dist/live/changelog.js +38 -6
- package/dist/live/dependency-set.d.ts +9 -1
- package/dist/live/dependency-set.js +10 -2
- package/dist/live/fix-paths.d.ts +289 -16
- package/dist/live/fix-paths.js +876 -93
- package/dist/live/github-releases.js +3 -0
- package/dist/live/held-dependencies.d.ts +93 -0
- package/dist/live/held-dependencies.js +212 -0
- package/dist/live/held-targets.d.ts +30 -0
- package/dist/live/held-targets.js +107 -0
- package/dist/live/http-utils.js +13 -3
- package/dist/live/index.d.ts +7 -4
- package/dist/live/index.js +24 -19
- package/dist/live/install-state.js +6 -2
- package/dist/live/js-lockfile-readers.d.ts +8 -1
- package/dist/live/js-lockfile-readers.js +43 -6
- package/dist/live/lockfile-parents.d.ts +15 -0
- package/dist/live/lockfile-parents.js +85 -10
- package/dist/live/lockfile-parsers-pygo.d.ts +47 -2
- package/dist/live/lockfile-parsers-pygo.js +230 -12
- package/dist/live/lockfile-parsers-rbphp.d.ts +38 -0
- package/dist/live/lockfile-parsers-rbphp.js +123 -0
- package/dist/live/lockfile-parsers.d.ts +6 -0
- package/dist/live/lockfile-parsers.js +116 -4
- package/dist/live/lockfile-types.d.ts +28 -1
- package/dist/live/maintenance.d.ts +20 -4
- package/dist/live/maintenance.js +31 -2
- package/dist/live/malware.d.ts +20 -0
- package/dist/live/malware.js +31 -0
- package/dist/live/npm-packument.d.ts +4 -1
- package/dist/live/npm-packument.js +5 -2
- package/dist/live/offered-release.d.ts +53 -0
- package/dist/live/offered-release.js +117 -0
- package/dist/live/osv-ranges.d.ts +111 -0
- package/dist/live/osv-ranges.js +173 -14
- package/dist/live/osv-scanner.d.ts +87 -5
- package/dist/live/osv-scanner.js +310 -59
- package/dist/live/pinned-advisories.d.ts +2 -2
- package/dist/live/pinned-advisories.js +3 -3
- package/dist/live/pypi-advice.d.ts +36 -0
- package/dist/live/pypi-advice.js +158 -0
- package/dist/live/range-satisfies.d.ts +4 -2
- package/dist/live/range-satisfies.js +7 -5
- package/dist/live/rate-limiter.d.ts +7 -0
- package/dist/live/rate-limiter.js +22 -1
- package/dist/live/refresh-rule.d.ts +36 -0
- package/dist/live/refresh-rule.js +49 -0
- package/dist/live/release-metadata.d.ts +5 -0
- package/dist/live/resolution.d.ts +3 -0
- package/dist/live/resolution.js +6 -3
- package/dist/live/scan-coverage.d.ts +26 -0
- package/dist/live/scan-coverage.js +70 -0
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +26 -3
- package/dist/live/severity-scope.d.ts +26 -1
- package/dist/live/severity-scope.js +32 -0
- package/dist/live/shell-safe.d.ts +19 -0
- package/dist/live/shell-safe.js +36 -0
- package/dist/live/types.d.ts +134 -8
- package/dist/live/version-compare.d.ts +51 -0
- package/dist/live/version-compare.js +152 -1
- package/dist/live/withdrawn-releases.d.ts +50 -0
- package/dist/live/withdrawn-releases.js +125 -0
- package/dist/project-manifests.d.ts +7 -0
- package/dist/project-manifests.js +45 -5
- package/dist/project-scanner.js +6 -4
- package/dist/project-tree.d.ts +56 -0
- package/dist/project-tree.js +174 -5
- package/dist/schema-registry.d.ts +47 -4
- package/dist/schema-registry.js +76 -9
- package/dist/server-instructions.js +4 -2
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +21 -2
- package/dist/tool-dispatch.js +3 -3
- package/dist/tools/agent-memory.d.ts +5 -0
- package/dist/tools/agent-memory.js +9 -0
- package/dist/tools/briefing-task-scope.js +5 -2
- package/dist/tools/decision-memory.js +2 -0
- package/dist/tools/dependency-check-held.d.ts +44 -0
- package/dist/tools/dependency-check-held.js +98 -0
- package/dist/tools/dependency-check-signals.d.ts +19 -2
- package/dist/tools/dependency-check-signals.js +84 -13
- package/dist/tools/dependency-check-versions.d.ts +37 -4
- package/dist/tools/dependency-check-versions.js +48 -13
- package/dist/tools/dependency-check.d.ts +11 -1
- package/dist/tools/dependency-check.js +81 -29
- package/dist/tools/dependency-health.js +209 -45
- package/dist/tools/get-context.d.ts +3 -0
- package/dist/tools/get-context.js +4 -0
- package/dist/tools/project-scope.d.ts +8 -0
- package/dist/tools/project-scope.js +21 -0
- package/dist/tools/upgrade-impact-features.d.ts +37 -0
- package/dist/tools/upgrade-impact-features.js +176 -0
- package/dist/tools/upgrade-impact-literals.js +26 -4
- package/dist/tools/upgrade-impact-report.d.ts +5 -1
- package/dist/tools/upgrade-impact-report.js +26 -5
- package/dist/tools/upgrade-impact.js +10 -3
- package/dist/tools/upgrade-planner-fixpaths.d.ts +164 -5
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +70 -7
- package/dist/tools/upgrade-planner.js +237 -61
- package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
- package/dist/tools/vulnerability-scan-concise.js +37 -3
- package/dist/tools/vulnerability-scan-format.d.ts +126 -24
- package/dist/tools/vulnerability-scan-format.js +193 -26
- package/dist/tools/vulnerability-scan.d.ts +224 -63
- package/dist/tools/vulnerability-scan.js +50 -15
- package/dist/tools/what-should-i-know.d.ts +6 -1
- package/dist/tools/what-should-i-know.js +72 -15
- 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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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("*");
|
package/dist/project-scanner.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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);
|
package/dist/project-tree.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/project-tree.js
CHANGED
|
@@ -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",
|
|
45
|
-
"
|
|
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
|
-
|
|
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: {
|
|
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).
|