@4da/mcp-server 5.0.3 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +667 -0
  2. package/README.md +66 -35
  3. package/dist/build-staleness.d.ts +35 -0
  4. package/dist/build-staleness.js +95 -0
  5. package/dist/db.d.ts +53 -2
  6. package/dist/db.js +242 -36
  7. package/dist/doctor.js +27 -7
  8. package/dist/embeddings.js +7 -1
  9. package/dist/freshness-markers.d.ts +34 -0
  10. package/dist/freshness-markers.js +74 -0
  11. package/dist/gitignore.d.ts +37 -0
  12. package/dist/gitignore.js +130 -0
  13. package/dist/index.js +127 -44
  14. package/dist/live/bun-lockfile.d.ts +26 -0
  15. package/dist/live/bun-lockfile.js +123 -0
  16. package/dist/live/cargo-platform.d.ts +38 -0
  17. package/dist/live/cargo-platform.js +178 -0
  18. package/dist/live/changelog-classify.d.ts +41 -0
  19. package/dist/live/changelog-classify.js +207 -0
  20. package/dist/live/changelog.d.ts +64 -0
  21. package/dist/live/changelog.js +318 -0
  22. package/dist/live/crates-registry.js +3 -1
  23. package/dist/live/crates-versions.d.ts +47 -0
  24. package/dist/live/crates-versions.js +201 -0
  25. package/dist/live/dependency-set.d.ts +13 -0
  26. package/dist/live/dependency-set.js +95 -0
  27. package/dist/live/dev-scope.d.ts +32 -0
  28. package/dist/live/dev-scope.js +102 -0
  29. package/dist/live/file-signature.d.ts +18 -0
  30. package/dist/live/file-signature.js +43 -0
  31. package/dist/live/go-registry.js +3 -1
  32. package/dist/live/hn-fetcher.js +9 -3
  33. package/dist/live/index.d.ts +105 -2
  34. package/dist/live/index.js +266 -139
  35. package/dist/live/install-state.d.ts +43 -0
  36. package/dist/live/install-state.js +150 -0
  37. package/dist/live/js-lockfile-readers.d.ts +46 -0
  38. package/dist/live/js-lockfile-readers.js +181 -0
  39. package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
  40. package/dist/live/lockfile-parsers-pygo.js +274 -0
  41. package/dist/live/lockfile-parsers.d.ts +69 -0
  42. package/dist/live/lockfile-parsers.js +311 -0
  43. package/dist/live/lockfile-types.d.ts +34 -0
  44. package/dist/live/lockfile-types.js +42 -0
  45. package/dist/live/maintenance.d.ts +33 -0
  46. package/dist/live/maintenance.js +37 -0
  47. package/dist/live/npm-packument.d.ts +47 -0
  48. package/dist/live/npm-packument.js +150 -0
  49. package/dist/live/npm-registry.js +67 -53
  50. package/dist/live/osv-scanner.d.ts +31 -1
  51. package/dist/live/osv-scanner.js +119 -10
  52. package/dist/live/package-archive.d.ts +49 -0
  53. package/dist/live/package-archive.js +186 -0
  54. package/dist/live/pypi-registry.js +3 -1
  55. package/dist/live/registry-health.d.ts +16 -0
  56. package/dist/live/registry-health.js +74 -0
  57. package/dist/live/release-metadata.d.ts +67 -0
  58. package/dist/live/release-metadata.js +16 -0
  59. package/dist/live/resolution.d.ts +43 -0
  60. package/dist/live/resolution.js +62 -0
  61. package/dist/live/semver-precedence.d.ts +30 -0
  62. package/dist/live/semver-precedence.js +83 -0
  63. package/dist/live/severity-scope.d.ts +57 -0
  64. package/dist/live/severity-scope.js +89 -0
  65. package/dist/live/types.d.ts +83 -0
  66. package/dist/live/upgrade-sources.d.ts +71 -0
  67. package/dist/live/upgrade-sources.js +198 -0
  68. package/dist/live/version-compare.d.ts +39 -0
  69. package/dist/live/version-compare.js +130 -0
  70. package/dist/live/version-resolver.d.ts +14 -1
  71. package/dist/live/version-resolver.js +93 -303
  72. package/dist/native-bindings.d.ts +19 -0
  73. package/dist/native-bindings.js +39 -0
  74. package/dist/project-manifests.d.ts +40 -0
  75. package/dist/project-manifests.js +193 -0
  76. package/dist/project-scanner.js +38 -48
  77. package/dist/project-tree.d.ts +41 -0
  78. package/dist/project-tree.js +110 -0
  79. package/dist/prompts.d.ts +28 -0
  80. package/dist/prompts.js +54 -0
  81. package/dist/schema-registry.d.ts +41 -29
  82. package/dist/schema-registry.js +98 -105
  83. package/dist/server-instructions.d.ts +11 -0
  84. package/dist/server-instructions.js +36 -0
  85. package/dist/tool-args.d.ts +28 -0
  86. package/dist/tool-args.js +93 -0
  87. package/dist/tool-dispatch.d.ts +1 -1
  88. package/dist/tool-dispatch.js +42 -8
  89. package/dist/tools/app-plan.d.ts +92 -0
  90. package/dist/tools/app-plan.js +155 -0
  91. package/dist/tools/briefing-stored-advisories.d.ts +20 -0
  92. package/dist/tools/briefing-stored-advisories.js +58 -0
  93. package/dist/tools/briefing-task-scope.d.ts +73 -0
  94. package/dist/tools/briefing-task-scope.js +197 -0
  95. package/dist/tools/briefing-wisdom.d.ts +31 -0
  96. package/dist/tools/briefing-wisdom.js +148 -0
  97. package/dist/tools/decision-enforcement.d.ts +0 -5
  98. package/dist/tools/decision-enforcement.js +0 -4
  99. package/dist/tools/decision-memory.js +18 -66
  100. package/dist/tools/dependency-check-signals.d.ts +74 -0
  101. package/dist/tools/dependency-check-signals.js +264 -0
  102. package/dist/tools/dependency-check.d.ts +98 -0
  103. package/dist/tools/dependency-check.js +295 -0
  104. package/dist/tools/dependency-health.js +74 -21
  105. package/dist/tools/developer-dna.d.ts +3 -0
  106. package/dist/tools/developer-dna.js +16 -15
  107. package/dist/tools/ecosystem-pulse.d.ts +6 -2
  108. package/dist/tools/ecosystem-pulse.js +53 -14
  109. package/dist/tools/get-actionable-signals.d.ts +26 -4
  110. package/dist/tools/get-actionable-signals.js +214 -251
  111. package/dist/tools/get-context.d.ts +2 -0
  112. package/dist/tools/get-context.js +9 -1
  113. package/dist/tools/get-relevant-content.d.ts +3 -1
  114. package/dist/tools/get-relevant-content.js +34 -3
  115. package/dist/tools/index.d.ts +4 -2
  116. package/dist/tools/index.js +4 -2
  117. package/dist/tools/install-drift-notes.d.ts +51 -0
  118. package/dist/tools/install-drift-notes.js +172 -0
  119. package/dist/tools/knowledge-gap-citations.d.ts +55 -0
  120. package/dist/tools/knowledge-gap-citations.js +132 -0
  121. package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
  122. package/dist/tools/knowledge-gap-exposure.js +160 -0
  123. package/dist/tools/knowledge-gap-grading.d.ts +91 -0
  124. package/dist/tools/knowledge-gap-grading.js +157 -0
  125. package/dist/tools/knowledge-gap-installs.d.ts +65 -0
  126. package/dist/tools/knowledge-gap-installs.js +134 -0
  127. package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
  128. package/dist/tools/knowledge-gap-ranges.js +146 -0
  129. package/dist/tools/knowledge-gap-scope.d.ts +63 -0
  130. package/dist/tools/knowledge-gap-scope.js +118 -0
  131. package/dist/tools/knowledge-gaps.d.ts +12 -3
  132. package/dist/tools/knowledge-gaps.js +172 -121
  133. package/dist/tools/package-presence.d.ts +24 -0
  134. package/dist/tools/package-presence.js +35 -0
  135. package/dist/tools/record-feedback.d.ts +0 -1
  136. package/dist/tools/record-feedback.js +7 -8
  137. package/dist/tools/signal-classifier.d.ts +25 -0
  138. package/dist/tools/signal-classifier.js +40 -0
  139. package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
  140. package/dist/tools/upgrade-impact-callsites.js +226 -0
  141. package/dist/tools/upgrade-impact-literals.d.ts +45 -0
  142. package/dist/tools/upgrade-impact-literals.js +118 -0
  143. package/dist/tools/upgrade-impact-report.d.ts +80 -0
  144. package/dist/tools/upgrade-impact-report.js +178 -0
  145. package/dist/tools/upgrade-impact.d.ts +84 -0
  146. package/dist/tools/upgrade-impact.js +305 -0
  147. package/dist/tools/upgrade-planner.d.ts +41 -8
  148. package/dist/tools/upgrade-planner.js +201 -53
  149. package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
  150. package/dist/tools/vulnerability-scan-concise.js +97 -0
  151. package/dist/tools/vulnerability-scan-format.d.ts +137 -0
  152. package/dist/tools/vulnerability-scan-format.js +276 -0
  153. package/dist/tools/vulnerability-scan.d.ts +338 -18
  154. package/dist/tools/vulnerability-scan.js +84 -125
  155. package/dist/tools/what-should-i-know.d.ts +86 -19
  156. package/dist/tools/what-should-i-know.js +277 -283
  157. package/dist/types.d.ts +4 -1
  158. package/package.json +27 -30
  159. package/dist/schemas/agent-memory.json +0 -49
  160. package/dist/schemas/decision-enforcement.json +0 -22
  161. package/dist/schemas/decision-memory.json +0 -54
  162. package/dist/schemas/dependency-health.json +0 -27
  163. package/dist/schemas/developer-dna.json +0 -219
  164. package/dist/schemas/ecosystem-pulse.json +0 -17
  165. package/dist/schemas/get-actionable-signals.json +0 -29
  166. package/dist/schemas/get-context.json +0 -17
  167. package/dist/schemas/get-relevant-content.json +0 -26
  168. package/dist/schemas/knowledge-gaps.json +0 -14
  169. package/dist/schemas/record-feedback.json +0 -24
  170. package/dist/schemas/upgrade-planner.json +0 -22
  171. package/dist/schemas/vulnerability-scan.json +0 -26
  172. package/dist/schemas/what-should-i-know.json +0 -19
@@ -0,0 +1,137 @@
1
+ /**
2
+ * vulnerability_scan output: alias collapsing, relevance splits,
3
+ * recommendations, install drift, the shared severity grade, and resolution
4
+ * provenance. Split out of vulnerability-scan.ts, which keeps execution.
5
+ */
6
+ import type { InstallDriftRecord, ResolutionProvenance, VulnerabilityScanResult } from "../live/types.js";
7
+ /** What the formatter needs beyond the scan itself. */
8
+ export interface ScanContext {
9
+ resolution: ResolutionProvenance | null;
10
+ reResolvedThisCall: boolean;
11
+ drift: InstallDriftRecord[];
12
+ includeDev: boolean;
13
+ }
14
+ /**
15
+ * Collapse advisories that are the same finding under different identifiers.
16
+ *
17
+ * OSV returns GHSA and RUSTSEC records for one vulnerability, each naming the
18
+ * other in `aliases`. Reported separately they double the count and read as two
19
+ * problems: `quinn-proto` showed as "2 high" for a single memory-exhaustion
20
+ * bug. Keeps the entry with the most usable detail (a CVSS score, then a fix
21
+ * version) and folds the rest into its alias list.
22
+ */
23
+ export declare function collapseAliases(vulns: VulnerabilityScanResult["vulnerabilities"]): import("../live/types.js").VulnerabilityEntry[];
24
+ /**
25
+ * Describe the platform filter truthfully, based on whether it actually ran.
26
+ *
27
+ * A crate is only knowable as platform-inactive when cargo could resolve the
28
+ * graph for this host. When it could not (cargo missing, offline resolution
29
+ * impossible, a non-Rust tree), every crate stays active — and saying
30
+ * "advisories are filtered to the host platform" would then be a claim about
31
+ * work that never happened.
32
+ */
33
+ export declare function platformFilterNote(vulns: VulnerabilityScanResult["vulnerabilities"]): string;
34
+ /** Shortest readable label for a manifest directory, relative to the scan root. */
35
+ export declare function relativeDir(dir: string, root: string): string;
36
+ export declare function formatScanResult(result: VulnerabilityScanResult, severityFilter: string | undefined, ctx: ScanContext): {
37
+ scan_time: string;
38
+ project: string;
39
+ project_root: string;
40
+ workspaces_with_findings: string[];
41
+ host_platform: string;
42
+ ecosystems_scanned: string[];
43
+ total_dependencies_scanned: number;
44
+ total_vulnerable_packages: number;
45
+ platform_inactive_packages: number;
46
+ by_severity: import("../live/severity-scope.js").SeverityCounts;
47
+ advisory_by_severity: import("../live/severity-scope.js").SeverityCounts;
48
+ vulnerabilities: {
49
+ package: string;
50
+ current_version: string;
51
+ /** Manifest directories pinning this version — where to apply the fix. */
52
+ source_dirs: string[];
53
+ ecosystem: import("../live/types.js").OsvEcosystem;
54
+ is_dev: boolean;
55
+ is_direct: boolean;
56
+ dev_scope_known: boolean;
57
+ target: string | null;
58
+ platform_active: boolean;
59
+ vuln_id: string;
60
+ aliases: string[];
61
+ severity: "critical" | "high" | "low" | "medium" | "unknown";
62
+ advisory_severity: "critical" | "high" | "low" | "medium" | "unknown";
63
+ severity_note?: string | undefined;
64
+ cvss_score: number | null;
65
+ summary: string;
66
+ fixed_version: string | null;
67
+ published: string;
68
+ references: string[];
69
+ }[];
70
+ platform_inactive_vulnerabilities: {
71
+ package: string;
72
+ current_version: string;
73
+ /** Manifest directories pinning this version — where to apply the fix. */
74
+ source_dirs: string[];
75
+ ecosystem: import("../live/types.js").OsvEcosystem;
76
+ is_dev: boolean;
77
+ is_direct: boolean;
78
+ dev_scope_known: boolean;
79
+ target: string | null;
80
+ platform_active: boolean;
81
+ vuln_id: string;
82
+ aliases: string[];
83
+ severity: "critical" | "high" | "low" | "medium" | "unknown";
84
+ advisory_severity: "critical" | "high" | "low" | "medium" | "unknown";
85
+ severity_note?: string | undefined;
86
+ cvss_score: number | null;
87
+ summary: string;
88
+ fixed_version: string | null;
89
+ published: string;
90
+ references: string[];
91
+ }[];
92
+ maintenance_notices: {
93
+ package: string;
94
+ current_version: string;
95
+ /** Manifest directories pinning this version — where to apply the fix. */
96
+ source_dirs: string[];
97
+ ecosystem: import("../live/types.js").OsvEcosystem;
98
+ is_dev: boolean;
99
+ is_direct: boolean;
100
+ dev_scope_known: boolean;
101
+ target: string | null;
102
+ platform_active: boolean;
103
+ vuln_id: string;
104
+ aliases: string[];
105
+ severity: "critical" | "high" | "low" | "medium" | "unknown";
106
+ advisory_severity: "critical" | "high" | "low" | "medium" | "unknown";
107
+ severity_note?: string | undefined;
108
+ cvss_score: number | null;
109
+ summary: string;
110
+ fixed_version: string | null;
111
+ published: string;
112
+ references: string[];
113
+ }[];
114
+ install_drift: Record<string, unknown>[];
115
+ recommendations: string[];
116
+ clean_dependencies: number;
117
+ scan_duration_ms: number;
118
+ _meta: {
119
+ source: string;
120
+ cached: boolean;
121
+ osv_cached: boolean;
122
+ offline: boolean;
123
+ privacy: string;
124
+ relevance: string;
125
+ severity_rule: string;
126
+ resolution: {
127
+ resolved_at: string | null;
128
+ lockfiles: {
129
+ path: string;
130
+ kind: "lockfile" | "manifest";
131
+ mtime: string | null;
132
+ }[];
133
+ re_resolved_this_call: boolean;
134
+ note: string;
135
+ };
136
+ };
137
+ };
@@ -0,0 +1,276 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * vulnerability_scan output: alias collapsing, relevance splits,
4
+ * recommendations, install drift, the shared severity grade, and resolution
5
+ * provenance. Split out of vulnerability-scan.ts, which keeps execution.
6
+ */
7
+ import { hostTarget } from "../live/platform.js";
8
+ import { maxSemver } from "../live/semver-utils.js";
9
+ import { isMaintenanceNotice } from "../live/maintenance.js";
10
+ import { countBySeverity, presentedSeverity, scopeAdjustmentReason, SEVERITY_RANK, } from "../live/severity-scope.js";
11
+ import { buildDriftView, groupDriftRows, lockfileAlsoAffected } from "./install-drift-notes.js";
12
+ const SEVERITY_RULE = "`severity` is the grade every 4DA surface presents: a transitive-only dependency caps critical at high, and a dev-only dependency drops one level (critical to high, high to medium, medium to low). Unknown dev scope gets no discount. `advisory_severity` is the advisory's own tier (counted in `advisory_by_severity`); `by_severity` counts the presented grade.";
13
+ const RESOLUTION_NOTE = "Dependency versions were read from these files at resolved_at. Every call re-checks them (stat only) and re-resolves when one changes. `cached` and `osv_cached` describe the OSV advisory lookup, not dependency resolution.";
14
+ /**
15
+ * Collapse advisories that are the same finding under different identifiers.
16
+ *
17
+ * OSV returns GHSA and RUSTSEC records for one vulnerability, each naming the
18
+ * other in `aliases`. Reported separately they double the count and read as two
19
+ * problems: `quinn-proto` showed as "2 high" for a single memory-exhaustion
20
+ * bug. Keeps the entry with the most usable detail (a CVSS score, then a fix
21
+ * version) and folds the rest into its alias list.
22
+ */
23
+ export function collapseAliases(vulns) {
24
+ const kept = [];
25
+ const claimed = new Map(); // advisory id -> index in `kept`
26
+ const richness = (v) => (v.cvssScore !== null ? 2 : 0) + (v.fixedVersion ? 1 : 0);
27
+ for (const v of vulns) {
28
+ const scope = `${v.ecosystem}\0${v.package}\0${v.currentVersion}`;
29
+ const ids = [v.vulnId, ...v.aliases];
30
+ const hitIndex = ids
31
+ .map((id) => claimed.get(`${scope}\0${id}`))
32
+ .find((i) => i !== undefined);
33
+ if (hitIndex === undefined) {
34
+ const index = kept.length;
35
+ kept.push({ ...v, aliases: [...v.aliases] });
36
+ for (const id of ids)
37
+ claimed.set(`${scope}\0${id}`, index);
38
+ continue;
39
+ }
40
+ const incumbent = kept[hitIndex];
41
+ const winner = richness(v) > richness(incumbent) ? v : incumbent;
42
+ const merged = {
43
+ ...winner,
44
+ aliases: [
45
+ ...new Set([...incumbent.aliases, ...ids, incumbent.vulnId].filter((id) => id !== winner.vulnId)),
46
+ ],
47
+ };
48
+ kept[hitIndex] = merged;
49
+ for (const id of [merged.vulnId, ...merged.aliases])
50
+ claimed.set(`${scope}\0${id}`, hitIndex);
51
+ }
52
+ return kept;
53
+ }
54
+ /**
55
+ * Describe the platform filter truthfully, based on whether it actually ran.
56
+ *
57
+ * A crate is only knowable as platform-inactive when cargo could resolve the
58
+ * graph for this host. When it could not (cargo missing, offline resolution
59
+ * impossible, a non-Rust tree), every crate stays active — and saying
60
+ * "advisories are filtered to the host platform" would then be a claim about
61
+ * work that never happened.
62
+ */
63
+ export function platformFilterNote(vulns) {
64
+ const host = hostTarget();
65
+ const platform = `${host.os}/${host.arch}`;
66
+ const inactive = vulns.filter((v) => v.platformActive === false).length;
67
+ const anyTargetKnown = vulns.some((v) => v.target);
68
+ if (inactive > 0) {
69
+ return `Filtered to ${platform}: ${inactive} advisor${inactive === 1 ? "y is" : "ies are"} against packages not built on this host, listed separately under platform_inactive_vulnerabilities.`;
70
+ }
71
+ if (anyTargetKnown) {
72
+ return `Filtered to ${platform}: every advisory below is against a package that IS built on this host.`;
73
+ }
74
+ return `Not platform-filtered: no per-package target information was resolvable (cargo metadata unavailable, or a non-Rust tree). Some advisories below may be against packages that never build on ${platform}.`;
75
+ }
76
+ /** Shortest readable label for a manifest directory, relative to the scan root. */
77
+ export function relativeDir(dir, root) {
78
+ const n = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
79
+ const d = n(dir);
80
+ const r = n(root);
81
+ if (r && d.toLowerCase().startsWith(`${r.toLowerCase()}/`))
82
+ return d.slice(r.length + 1);
83
+ if (r && d.toLowerCase() === r.toLowerCase())
84
+ return ".";
85
+ return d;
86
+ }
87
+ export function formatScanResult(result, severityFilter, ctx) {
88
+ const root = result.projectPath;
89
+ const relative = (dir) => relativeDir(dir, root);
90
+ let vulns = collapseAliases(result.vulnerabilities);
91
+ // Filter and sort on the PRESENTED grade, the one the `severity` field shows.
92
+ if (severityFilter) {
93
+ const minRank = SEVERITY_RANK[severityFilter] ?? 0;
94
+ vulns = vulns.filter((v) => SEVERITY_RANK[presentedSeverity(v)] >= minRank);
95
+ }
96
+ vulns.sort((a, b) => SEVERITY_RANK[presentedSeverity(b)] - SEVERITY_RANK[presentedSeverity(a)]);
97
+ // Relevance split: advisories on deps NOT active on the host platform are not
98
+ // actionable for this user (e.g. a Linux-only crate's CVE shown to a Windows
99
+ // dev). Keep them, but separated and labeled so they don't pollute the signal.
100
+ const onHost = vulns.filter((v) => v.platformActive !== false);
101
+ const inactive = vulns.filter((v) => v.platformActive === false);
102
+ // Second relevance split: "X is unmaintained" is a maintenance notice, not a
103
+ // vulnerability. Nothing is exploitable and there is nothing to upgrade to.
104
+ // Live scan: 23 of 41 findings were these, generating 27 "Review X" lines
105
+ // that buried the six real upgrades. Kept, labelled, and out of the counts.
106
+ const active = onHost.filter((v) => !isMaintenanceNotice(v));
107
+ const maintenance = onHost.filter((v) => isMaintenanceNotice(v));
108
+ const drift = buildDriftView(result, ctx.drift, ctx.includeDev, relative);
109
+ const host = hostTarget();
110
+ const activePackages = new Set(active.map((v) => v.package)).size;
111
+ // Every distinct manifest directory the scan actually covered. A single
112
+ // "project" label hides the fact that a repo root can span several
113
+ // independently-versioned workspaces.
114
+ const scannedProjects = [...new Set(vulns.flatMap((v) => v.sourceDirs ?? []).map(relative))].sort();
115
+ return {
116
+ scan_time: result.scannedAt,
117
+ project: result.projectPath.split(/[/\\]/).pop() || result.projectPath,
118
+ project_root: result.projectPath,
119
+ // Workspaces under `project_root` that contributed a finding. Advisories
120
+ // below are attributed per entry via `source_dirs`.
121
+ workspaces_with_findings: scannedProjects,
122
+ host_platform: `${host.os}/${host.arch}`,
123
+ ecosystems_scanned: result.ecosystemsScanned,
124
+ total_dependencies_scanned: result.totalScanned,
125
+ total_vulnerable_packages: activePackages,
126
+ platform_inactive_packages: result.platformInactiveVulnerable,
127
+ // Counted over the listed findings: the same rows `vulnerabilities` shows,
128
+ // one per vulnerability (aliases collapsed). Counting every raw OSV row
129
+ // (alias duplicates, maintenance notices, other-platform advisories) gave
130
+ // "medium 3, low 1, unknown 9" beside three listed rows (2026-10-02 eval).
131
+ by_severity: countBySeverity(active, presentedSeverity),
132
+ advisory_by_severity: countBySeverity(active, (v) => v.severity),
133
+ vulnerabilities: active.map((v) => formatVuln(v, root, drift)),
134
+ // Advisories suppressed from the main list because the affected crate is not
135
+ // built on this host (e.g. cfg(not(windows)) deps on Windows). Surfaced for
136
+ // cross-platform builds, clearly labeled with the gating target.
137
+ platform_inactive_vulnerabilities: inactive.map((v) => formatVuln(v, root, drift)),
138
+ // Unmaintained-dependency notices: real information to plan around, but
139
+ // nothing exploitable and nothing to upgrade to. Separated so they cannot
140
+ // outnumber the advisories that need action today.
141
+ maintenance_notices: maintenance.map((v) => formatVuln(v, root, drift)),
142
+ // npm direct dependencies whose node_modules copy is not what the lockfile
143
+ // pins. The lockfile is what every other tool reads; this is what runs.
144
+ install_drift: drift.rows,
145
+ recommendations: buildRecommendations(active, result.vulnerabilities, relative),
146
+ clean_dependencies: result.cleanCount,
147
+ scan_duration_ms: result.scanDurationMs,
148
+ _meta: {
149
+ source: "osv.dev",
150
+ // Kept for compatibility; it has only ever described the OSV lookup.
151
+ cached: result.cached,
152
+ osv_cached: result.cached,
153
+ offline: result.offline,
154
+ privacy: "Only package names and versions were sent (public manifest data)",
155
+ // State what actually happened. This previously asserted that advisories
156
+ // WERE platform-filtered while reporting zero inactive packages and
157
+ // listing nine Linux-only crates on a Windows host — a claim of filtering
158
+ // is worse than no filtering, because it invites the reader to trust it.
159
+ relevance: platformFilterNote(vulns),
160
+ severity_rule: SEVERITY_RULE,
161
+ resolution: {
162
+ resolved_at: ctx.resolution?.resolvedAt ?? null,
163
+ lockfiles: (ctx.resolution?.sources ?? []).map((s) => ({
164
+ path: relative(s.path),
165
+ kind: s.kind,
166
+ mtime: s.mtimeMs === null ? null : new Date(s.mtimeMs).toISOString(),
167
+ })),
168
+ re_resolved_this_call: ctx.reResolvedThisCall,
169
+ note: RESOLUTION_NOTE,
170
+ },
171
+ },
172
+ };
173
+ }
174
+ function buildRecommendations(active, all, relative) {
175
+ const lines = [];
176
+ const rankOf = (vulns) => Math.max(...vulns.map((v) => SEVERITY_RANK[presentedSeverity(v)]));
177
+ // Name the manifest directories that actually pin the vulnerable version.
178
+ // Without this a reader cannot tell a finding in the primary crate from one
179
+ // in a sibling workspace, and a patched primary reads as vulnerable.
180
+ const whereOf = (vulns) => {
181
+ const dirs = [...new Set(vulns.flatMap((v) => v.sourceDirs ?? []).map(relative))].sort();
182
+ return dirs.length > 0 ? ` in ${dirs.join(", ")}` : "";
183
+ };
184
+ // Recommendations are derived from platform-active, exploitable vulns only.
185
+ const byPackage = new Map();
186
+ for (const v of active) {
187
+ if (v.installDriftOf)
188
+ continue; // reinstall advice below, not an upgrade
189
+ const list = byPackage.get(v.package);
190
+ if (list)
191
+ list.push(v);
192
+ else
193
+ byPackage.set(v.package, [v]);
194
+ }
195
+ for (const [pkg, pkgVulns] of byPackage) {
196
+ const highest = pkgVulns[0];
197
+ const breakdown = severityBreakdown(pkgVulns);
198
+ // Target the HIGHEST published fix so the upgrade resolves every fixable CVE,
199
+ // not just the first one. (The old code picked the first fix version, which
200
+ // could leave higher-version-gated CVEs unfixed while claiming to fix them.)
201
+ const target = maxSemver(pkgVulns.map((v) => v.fixedVersion).filter((v) => Boolean(v)));
202
+ const total = pkgVulns.length;
203
+ const fixable = pkgVulns.filter((v) => v.fixedVersion).length;
204
+ const where = whereOf(pkgVulns);
205
+ let text;
206
+ if (target) {
207
+ const scope = fixable === total
208
+ ? `${total} CVE${total > 1 ? "s" : ""}`
209
+ : `${fixable} of ${total} CVEs (${total - fixable} with no published fix)`;
210
+ text = `Upgrade ${pkg} ${highest.currentVersion} → ${target}${where} — resolves ${scope} [${breakdown}]`;
211
+ }
212
+ else {
213
+ text = `Review ${pkg} ${highest.currentVersion}${where} — ${total} known vulnerabilit${total > 1 ? "ies" : "y"} [${breakdown}], no fix version published`;
214
+ }
215
+ lines.push({ rank: rankOf(pkgVulns), text });
216
+ }
217
+ // Installed copies the lockfile does not pin. The lockfile already says what
218
+ // should be installed, so the advice is the reinstall, not a version bump.
219
+ for (const entries of groupDriftRows(active)) {
220
+ const first = entries[0];
221
+ const count = entries.length;
222
+ const dirs = [...new Set(entries.flatMap((v) => v.sourceDirs ?? []).map(relative))]
223
+ .map((d) => (d === "." ? "the project root" : d))
224
+ .sort();
225
+ const where = dirs.length > 0 ? ` in ${dirs.join(", ")}` : "";
226
+ const command = first.installFix ?? "a reinstall";
227
+ const tail = entries.some((e) => lockfileAlsoAffected(e, all))
228
+ ? `the lockfile's ${first.installDriftOf} is also affected — upgrade it, then run \`${command}\`${where}`
229
+ : `the lockfile already pins ${first.installDriftOf}, which is not affected — run \`${command}\`${where}`;
230
+ lines.push({
231
+ rank: rankOf(entries),
232
+ text: `Reinstall ${first.package}${where}: node_modules has ${first.currentVersion} with ${count} known vulnerabilit${count > 1 ? "ies" : "y"} [${severityBreakdown(entries)}]; ${tail}`,
233
+ });
234
+ }
235
+ return lines
236
+ .map((line, index) => ({ ...line, index }))
237
+ .sort((a, b) => b.rank - a.rank || a.index - b.index)
238
+ .map((line) => line.text);
239
+ }
240
+ /** "1 critical, 3 high, 2 medium" (presented grades), omitting empty buckets. */
241
+ function severityBreakdown(vulns) {
242
+ const counts = countBySeverity(vulns, presentedSeverity);
243
+ return ["critical", "high", "medium", "low", "unknown"]
244
+ .filter((s) => counts[s])
245
+ .map((s) => `${counts[s]} ${s}`)
246
+ .join(", ");
247
+ }
248
+ function formatVuln(v, root, drift) {
249
+ const presented = presentedSeverity(v);
250
+ const reason = scopeAdjustmentReason(v);
251
+ return {
252
+ package: v.package,
253
+ current_version: v.currentVersion,
254
+ /** Manifest directories pinning this version — where to apply the fix. */
255
+ source_dirs: [...new Set((v.sourceDirs ?? []).map((d) => relativeDir(d, root)))].sort(),
256
+ ecosystem: v.ecosystem,
257
+ is_dev: v.isDev,
258
+ is_direct: v.isDirect ?? null,
259
+ dev_scope_known: v.devScopeKnown ?? false,
260
+ target: v.target,
261
+ platform_active: v.platformActive,
262
+ vuln_id: v.vulnId,
263
+ aliases: v.aliases,
264
+ severity: presented,
265
+ advisory_severity: v.severity,
266
+ ...(reason
267
+ ? { severity_note: `Graded ${presented}: the advisory rates it ${v.severity}, and this is a ${reason}.` }
268
+ : {}),
269
+ cvss_score: v.cvssScore,
270
+ summary: v.summary,
271
+ fixed_version: v.fixedVersion,
272
+ published: v.published,
273
+ references: v.references,
274
+ ...drift.annotate(v),
275
+ };
276
+ }