@4da/mcp-server 6.2.4 → 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 (119) hide show
  1. package/CHANGELOG.md +484 -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 +258 -9
  18. package/dist/live/fix-paths.js +790 -45
  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/rate-limiter.d.ts +7 -0
  56. package/dist/live/rate-limiter.js +22 -1
  57. package/dist/live/release-metadata.d.ts +5 -0
  58. package/dist/live/resolution.d.ts +3 -0
  59. package/dist/live/resolution.js +6 -3
  60. package/dist/live/scan-coverage.d.ts +26 -0
  61. package/dist/live/scan-coverage.js +70 -0
  62. package/dist/live/semver-utils.d.ts +8 -0
  63. package/dist/live/semver-utils.js +26 -3
  64. package/dist/live/severity-scope.d.ts +26 -1
  65. package/dist/live/severity-scope.js +32 -0
  66. package/dist/live/shell-safe.d.ts +19 -0
  67. package/dist/live/shell-safe.js +36 -0
  68. package/dist/live/types.d.ts +134 -8
  69. package/dist/live/version-compare.d.ts +49 -0
  70. package/dist/live/version-compare.js +131 -0
  71. package/dist/live/withdrawn-releases.d.ts +50 -0
  72. package/dist/live/withdrawn-releases.js +125 -0
  73. package/dist/project-manifests.d.ts +7 -0
  74. package/dist/project-manifests.js +45 -5
  75. package/dist/project-scanner.js +6 -4
  76. package/dist/project-tree.d.ts +56 -0
  77. package/dist/project-tree.js +174 -5
  78. package/dist/schema-registry.d.ts +47 -4
  79. package/dist/schema-registry.js +76 -9
  80. package/dist/server-instructions.js +4 -2
  81. package/dist/setup.d.ts +8 -0
  82. package/dist/setup.js +21 -2
  83. package/dist/tool-dispatch.js +3 -3
  84. package/dist/tools/agent-memory.d.ts +5 -0
  85. package/dist/tools/agent-memory.js +9 -0
  86. package/dist/tools/briefing-task-scope.js +5 -2
  87. package/dist/tools/decision-memory.js +2 -0
  88. package/dist/tools/dependency-check-held.d.ts +44 -0
  89. package/dist/tools/dependency-check-held.js +98 -0
  90. package/dist/tools/dependency-check-signals.d.ts +19 -2
  91. package/dist/tools/dependency-check-signals.js +84 -13
  92. package/dist/tools/dependency-check-versions.d.ts +37 -4
  93. package/dist/tools/dependency-check-versions.js +48 -13
  94. package/dist/tools/dependency-check.d.ts +11 -1
  95. package/dist/tools/dependency-check.js +81 -29
  96. package/dist/tools/dependency-health.js +209 -45
  97. package/dist/tools/get-context.d.ts +3 -0
  98. package/dist/tools/get-context.js +4 -0
  99. package/dist/tools/project-scope.d.ts +8 -0
  100. package/dist/tools/project-scope.js +21 -0
  101. package/dist/tools/upgrade-impact-features.d.ts +37 -0
  102. package/dist/tools/upgrade-impact-features.js +176 -0
  103. package/dist/tools/upgrade-impact-literals.js +26 -4
  104. package/dist/tools/upgrade-impact-report.d.ts +5 -1
  105. package/dist/tools/upgrade-impact-report.js +26 -5
  106. package/dist/tools/upgrade-impact.js +10 -3
  107. package/dist/tools/upgrade-planner-fixpaths.d.ts +162 -5
  108. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  109. package/dist/tools/upgrade-planner.d.ts +59 -4
  110. package/dist/tools/upgrade-planner.js +237 -61
  111. package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
  112. package/dist/tools/vulnerability-scan-concise.js +37 -3
  113. package/dist/tools/vulnerability-scan-format.d.ts +126 -24
  114. package/dist/tools/vulnerability-scan-format.js +193 -26
  115. package/dist/tools/vulnerability-scan.d.ts +224 -63
  116. package/dist/tools/vulnerability-scan.js +50 -15
  117. package/dist/tools/what-should-i-know.d.ts +6 -1
  118. package/dist/tools/what-should-i-know.js +72 -15
  119. package/package.json +2 -2
@@ -23,29 +23,42 @@
23
23
  * says so (`installedVersion`) and a reinstall is the step when nothing
24
24
  * else about the lockfile's version needs changing.
25
25
  */
26
- import { isMaintenanceNotice } from "../live/maintenance.js";
26
+ import { isActionableVulnerability, isMaintenanceNotice } from "../live/maintenance.js";
27
27
  import { computeSemverDistance } from "../live/semver-utils.js";
28
- import { compareVersions, maxVersion, namesPackage } from "../live/version-compare.js";
29
- import { formatAppPlan, readAppPlan } from "./app-plan.js";
28
+ import { compareVersions, inEcosystemForm, maxVersion, namesPackage } from "../live/version-compare.js";
29
+ import { formatAppPlan, readAppPlan, scopeAppPlanSteps } from "./app-plan.js";
30
30
  import { dirsLabel, driftFor } from "./install-drift-notes.js";
31
31
  import { emptyAnswerNote } from "./package-presence.js";
32
32
  import { relativeDir } from "./vulnerability-scan-format.js";
33
- import { liveIntelFor, resolveProjectScope } from "./project-scope.js";
34
- import { verifyFixPaths } from "./upgrade-planner-fixpaths.js";
35
- import { discoveryNote } from "../project-tree.js";
33
+ import { appTracksProject, liveIntelFor, resolveProjectScope } from "./project-scope.js";
34
+ import { reconcileDirectTargets, verifyFixPaths } from "./upgrade-planner-fixpaths.js";
35
+ import { groupAdvisories } from "../live/advisory-groups.js";
36
+ import { isDevOnlyCopy, rankedSeverity } from "../live/severity-scope.js";
37
+ import { discoveryNote, exclusionNotes, unreadableNotes } from "../project-tree.js";
38
+ import { vulnerabilityCoverage } from "../live/scan-coverage.js";
39
+ import { uncheckedOfferNote, vetOfferedReleases } from "../live/offered-release.js";
40
+ /** Time the plan spends checking the newer releases its rows offer against their advisories. */
41
+ const OFFER_CHECK_BUDGET_MS = 10_000;
42
+ /** The newer release a registry row would offer (its latest stable), or null when it offers none. */
43
+ function offeredUpgrade(dep) {
44
+ const latest = dep.latestStableVersion || dep.latestVersion;
45
+ if (!latest || !dep.currentVersion)
46
+ return null;
47
+ return compareVersions(latest, dep.currentVersion, dep.ecosystem) > 0 ? latest : null;
48
+ }
36
49
  /** How long a plan waits for its own OSV scan before answering CVE-blind (the scan keeps running). */
37
50
  const PLANNER_SCAN_TIMEOUT_MS = 60_000;
38
51
  const PROVENANCE_NOTE = "Point-in-time heuristic from local lockfiles plus live registry/OSV lookups. " +
39
52
  "Not the 4DA desktop app's cross-project, version-confirmed plan (none was available).";
40
53
  export const upgradePlannerTool = {
41
54
  name: "upgrade_planner",
42
- description: "Ranked dependency upgrade plan for one project — call before upgrading, adding, or auditing any dependency to pick the safest order. `project_path` picks the project (default: the one the server was started in; \"*\" for every project). When the 4DA desktop app has computed its plan, returns that plan's work order (provenance app_plan): per package the ecosystem, each installed version with its minimum clean target, upgrade type (patch/minor/major), the projects that hold it (direct/dev) and the mechanism (manifest_bump, lockfile_or_parent_update, mixed, no_fix); flagged stale past its freshness horizon. Otherwise falls back to a standalone plan from local lockfiles (provenance standalone_heuristic): the smallest version that fixes each vulnerability (direct and transitive), checked against every known advisory of the package, whether a lockfile refresh already reaches it (lockfile_refresh with the command) or a parent blocks it (waiting_on_upstream), deprecation and version distance; it runs the OSV scan itself when none has run. `package` narrows either plan to one package. Privacy: the fallback may query public registries and OSV with package names and versions; it never sends source code.",
55
+ description: "Ranked dependency upgrade plan for one project — call before upgrading, adding, or auditing any dependency to pick the safest order. `project_path` picks the project (default: the one the server was started in; \"*\" for every project). When the 4DA desktop app has computed its plan, returns that plan's work order (provenance app_plan): per package the ecosystem, each installed version with its minimum clean target, upgrade type (patch/minor/major), the projects that hold it (direct/dev) and the mechanism (manifest_bump, lockfile_or_parent_update, mixed, no_fix); flagged stale past its freshness horizon. Otherwise falls back to a standalone plan from local lockfiles (provenance standalone_heuristic): the smallest version that fixes each vulnerability (direct and transitive), checked against every known advisory of the package, whether a lockfile refresh already reaches it (lockfile_refresh with the command) or a parent blocks it: upgrade_parent with `parentUpgrades` naming the smallest parent release whose requirement admits a clean version (through the direct dependency it arrives by), override_or_replace with the override syntax when no parent release will (deprecated, already latest, or none admits one), waiting_on_upstream when nothing could be read; direct steps carry `upgradeCommands` and are raised to the release that also clears the transitives they pin; a copy bundled inside its owner waits on the owner; dev-only rows are included and labelled `isDev`, as in vulnerability_scan; deprecation and version distance; it runs the OSV scan itself when none has run. When OSV.dev could not answer for every dependency, `vulnerabilityDataAvailable` is false, `vulnerabilityCoverage` counts the unchecked ones and the summary starts with \"NOT CHECKED\": the plan then misses whatever those dependencies carry. `package` narrows either plan to one package. Privacy: the fallback may query public registries and OSV with package names and versions; it never sends source code.",
43
56
  inputSchema: {
44
57
  type: "object",
45
58
  properties: {
46
59
  include_dev: {
47
60
  type: "boolean",
48
- description: "Include devDependencies. Default: false",
61
+ description: "Dev-only packages (labelled isDev). Default (unset): vulnerable devDependencies and dev-only transitive packages are planned, as in vulnerability_scan; true also checks every devDependency for newer releases and deprecation; false leaves dev-only packages out and the summary counts them.",
49
62
  },
50
63
  max_recommendations: {
51
64
  type: "number",
@@ -68,23 +81,32 @@ export const upgradePlannerTool = {
68
81
  },
69
82
  };
70
83
  const RISK_LEVELS = { low: 0, medium: 1, high: 2, critical: 3 };
71
- /** critical > high > medium > low, then fixable now (direct) before transitive, then non-breaking first (quick wins). */
84
+ /** critical > high > medium > low, then runtime before dev-only, then fixable now (direct) before transitive, then non-breaking first (quick wins). */
72
85
  function planOrder(a, b) {
73
86
  const riskDiff = RISK_LEVELS[b.risk] - RISK_LEVELS[a.risk];
74
87
  if (riskDiff !== 0)
75
88
  return riskDiff;
89
+ if (a.isDev !== b.isDev)
90
+ return a.isDev ? 1 : -1;
76
91
  if (a.scope !== b.scope)
77
92
  return a.scope === "direct" ? -1 : 1;
78
93
  if (a.breaking !== b.breaking)
79
94
  return a.breaking ? 1 : -1;
80
95
  return 0;
81
96
  }
82
- function severityToRisk(severities) {
83
- if (severities.includes("critical"))
84
- return "critical";
85
- if (severities.includes("high"))
86
- return "high";
87
- return "medium";
97
+ /**
98
+ * The grade a row ranks by: the scope-adjusted severity vulnerability_scan presents (transitive
99
+ * caps critical at high; dev-only drops one level), over the 4DA app's base urgency (an ungraded
100
+ * advisory medium, a MAL- record high: severity-scope.ts rankedSeverity). A dev-only medium is low,
101
+ * as the scan presents it; it used to stay medium. Ranking on the raw grade let dev-only criticals
102
+ * push a runtime high out of max_recommendations (readiness review 2026-10-17, finding 6).
103
+ */
104
+ function riskOf(entries) {
105
+ const tiers = entries.map((v) => rankedSeverity(v));
106
+ for (const t of ["critical", "high", "medium"])
107
+ if (tiers.includes(t))
108
+ return t;
109
+ return "low";
88
110
  }
89
111
  /** Distance between the installed and target versions (patch/minor/major), or unknown. */
90
112
  function upgradeTypeBetween(current, target) {
@@ -104,8 +126,15 @@ export async function executeUpgradePlanner(db, params, liveIntel) {
104
126
  // One brain: the app's plan wins whenever there is one, stale or not (a
105
127
  // stale plan is returned flagged, never silently replaced by a heuristic).
106
128
  // It covers every project the app tracks, so it is narrowed to this one.
129
+ // A project the app does not track is not in its plan at all; narrowing the
130
+ // plan to it gave "0 steps" for a project with 83 vulnerable packages
131
+ // (audit 6.2.5 D-1). Plan it from its own lockfiles instead.
107
132
  const appPlan = readAppPlan(db);
108
- if (appPlan.kind === "plan")
133
+ const untracked = appPlan.kind === "plan" &&
134
+ scopePath !== null &&
135
+ scopeAppPlanSteps(appPlan.snapshot.steps, scopePath).length === 0 &&
136
+ !appTracksProject(db, scopePath);
137
+ if (appPlan.kind === "plan" && !untracked)
109
138
  return formatAppPlan(appPlan, params, scopePath);
110
139
  // The live layer already covers the server's own project; only an explicit
111
140
  // project_path elsewhere resolves another tree.
@@ -113,7 +142,10 @@ export async function executeUpgradePlanner(db, params, liveIntel) {
113
142
  ? liveIntelFor(liveIntel, db, scope)
114
143
  : liveIntel;
115
144
  const standalone = await executeStandalonePlanner(params, scoped);
116
- return { ...standalone, appPlanUnavailable: appPlan.reason, projectScope: scopePath ?? standalone.projectPath };
145
+ const appPlanUnavailable = appPlan.kind === "plan"
146
+ ? `the 4DA app does not track ${scopePath}, so its plan does not cover it; this plan is computed from the project's own lockfiles`
147
+ : appPlan.reason;
148
+ return { ...standalone, appPlanUnavailable, projectScope: scopePath ?? standalone.projectPath };
117
149
  }
118
150
  async function executeStandalonePlanner(params, liveIntel) {
119
151
  if (!liveIntel || !liveIntel.isInitialized()) {
@@ -127,6 +159,7 @@ async function executeStandalonePlanner(params, liveIntel) {
127
159
  breakingChanges: 0,
128
160
  waitingOnUpstream: 0,
129
161
  lockfileRefreshes: 0,
162
+ parentUpgradeSteps: 0,
130
163
  vulnerabilityDataAvailable: false,
131
164
  provenance: { mode: "standalone_heuristic", note: PROVENANCE_NOTE },
132
165
  };
@@ -134,16 +167,17 @@ async function executeStandalonePlanner(params, liveIntel) {
134
167
  // A long-lived server must not plan from the versions it read at startup:
135
168
  // re-resolve if a lockfile (or node_modules) changed. Stat calls only.
136
169
  liveIntel.refreshIfLockfilesChanged();
137
- const includeDev = params.include_dev ?? false;
170
+ // Dev dependencies by default, as vulnerability_scan (A8): leaving them out hid 30 of navcal's
171
+ // 34 vulnerable versions, vitest's critical among them (readiness audit H4).
172
+ const includeDev = params.include_dev ?? true;
138
173
  const only = params.package?.trim();
139
174
  const named = (name, ecosystem) => !only || namesPackage(only, name, ecosystem);
140
175
  let deps = liveIntel.getResolvedDeps();
141
- if (!includeDev)
142
- deps = deps.filter((d) => !d.isDev);
143
176
  if (only)
144
177
  deps = deps.filter((d) => named(d.name, d.ecosystem));
145
- // Fetch registry data (live lookups for direct deps; cached where fresh)
146
- const registryData = await liveIntel.fetchRegistryHealth(deps);
178
+ const devDirect = includeDev ? [] : deps.filter((d) => d.isDev);
179
+ if (!includeDev)
180
+ deps = deps.filter((d) => !d.isDev);
147
181
  const drift = liveIntel.getInstallDrift();
148
182
  const root = liveIntel.getProjectRoot() ?? process.cwd();
149
183
  // Vulnerability data at the plan's scope, loaded here (bounded) when no scan
@@ -152,7 +186,40 @@ async function executeStandalonePlanner(params, liveIntel) {
152
186
  // includes dev dependencies must not read a scan that left them out, so
153
187
  // ensureVulnerabilities rescans at the wider scope when needed.
154
188
  const vulnResult = (await liveIntel.ensureVulnerabilities(root, PLANNER_SCAN_TIMEOUT_MS, { includeDev })) ?? liveIntel.getVulnerabilities();
155
- const vulnerabilityDataAvailable = vulnResult !== null;
189
+ // include_dev: false: the summary counts the vulnerable devDependencies it left out, when the
190
+ // scan read them; otherwise how many it did not check (readiness review 2026-10-17, finding 10).
191
+ const devScanned = devDirect.length > 0 && liveIntel.lastScanIncludesDev();
192
+ const devVulnerable = new Set((vulnResult?.vulnerabilities ?? []).filter((v) => v.isDirect && v.isDev && !isMaintenanceNotice(v)).map((v) => `${v.ecosystem}\0${v.package}\0${v.currentVersion}`));
193
+ const devDirectLeftOut = devScanned ? devDirect.filter((d) => devVulnerable.has(`${d.ecosystem}\0${d.name}\0${d.version}`)).length : 0;
194
+ const devDirectUnchecked = devScanned ? 0 : devDirect.length;
195
+ // Registry rows (freshness, deprecation) for direct deps. By default a devDependency gets one
196
+ // only when it is vulnerable: fetching every dev tool's registry document (typescript,
197
+ // @types/node, vite...) more than doubled navcal's lookups for rows no advisory asks for.
198
+ // include_dev: true checks every devDependency's freshness too.
199
+ const vulnerableNames = new Set((vulnResult?.vulnerabilities ?? []).filter((v) => !isMaintenanceNotice(v)).map((v) => `${v.ecosystem}\0${v.package}`));
200
+ const registryDeps = params.include_dev === undefined ? deps.filter((d) => !d.isDev || vulnerableNames.has(`${d.ecosystem}\0${d.name}`)) : deps;
201
+ const devFreshnessSkipped = deps.length - registryDeps.length;
202
+ // Fetch registry data (live lookups for direct deps; cached where fresh)
203
+ const registryData = await liveIntel.fetchRegistryHealth(registryDeps);
204
+ // Every newer release a row offers is checked as dependency_check checks one (offered-release.ts):
205
+ // crossenv 0.0.2-security was sent to 1.0.1, the malware itself (final release audit of 6.2.5).
206
+ const offerSources = liveIntel.getFixPathSources();
207
+ const offers = await vetOfferedReleases(offerSources, registryData
208
+ .filter((d) => offeredUpgrade(d) !== null)
209
+ .map((d) => ({ key: `${d.ecosystem}\0${d.name}\0${d.currentVersion ?? "unknown"}`, ecosystem: d.ecosystem, name: d.name, installed: d.currentVersion, offered: offeredUpgrade(d) })), OFFER_CHECK_BUDGET_MS);
210
+ // Complete vulnerability data only: a partial scan (OSV.dev unreachable or
211
+ // rate limited for some dependencies) ranks what it found and says the rest
212
+ // is unchecked; it never claims the plan is CVE-aware (6.2.4 said
213
+ // vulnerabilityDataAvailable: true over 10,568 unchecked dependencies).
214
+ const scopeTotal = liveIntel.getAuditDeps().filter((d) => includeDev || !isDevOnlyCopy(d)).length;
215
+ const coverage = vulnerabilityCoverage(vulnResult, {
216
+ enabled: liveIntel.isEnabled(),
217
+ expectedTotal: scopeTotal,
218
+ notLoaded: `the OSV.dev scan did not finish within ${PLANNER_SCAN_TIMEOUT_MS / 1000} s or OSV.dev was unreachable`,
219
+ });
220
+ // A project whose only manifests are ones this server does not read (pom.xml) has no data at all (audit 6.2.5 A-6).
221
+ const nothingReadable = (vulnResult?.totalScanned ?? 0) === 0 && (liveIntel.getDiscovery()?.notCoveredCount ?? 0) > 0;
222
+ const vulnerabilityDataAvailable = vulnResult !== null && coverage === null && !nothingReadable;
156
223
  // Keyed by ecosystem + name + INSTANCE version. A name-only key attached every
157
224
  // instance's CVEs to whichever row asked first: the direct anyhow 1.0.104 was
158
225
  // charged with RUSTSEC-2026-0190 (fixed in 1.0.103 — it isn't affected) while
@@ -176,13 +243,7 @@ async function executeStandalonePlanner(params, liveIntel) {
176
243
  const key = instanceKey(v.ecosystem, v.package, v.currentVersion);
177
244
  if (!vulnsByPackage.has(key))
178
245
  vulnsByPackage.set(key, []);
179
- vulnsByPackage.get(key).push({
180
- severity: v.severity,
181
- vulnId: v.vulnId,
182
- summary: v.summary,
183
- fixedVersion: v.fixedVersion,
184
- platformActive: v.platformActive,
185
- });
246
+ vulnsByPackage.get(key).push(v);
186
247
  }
187
248
  }
188
249
  // Build recommendations — direct deps (registry-informed)
@@ -194,14 +255,24 @@ async function executeStandalonePlanner(params, liveIntel) {
194
255
  directPackages.add(instanceKey(dep.ecosystem, dep.name, dep.currentVersion));
195
256
  const reasons = [];
196
257
  let risk = "low";
197
- const latestStable = dep.latestStableVersion || dep.latestVersion;
258
+ const registryLatest = dep.latestStableVersion || dep.latestVersion;
259
+ // The newest release this row may offer: the registry's latest, unless an advisory affects it or
260
+ // the registry withdrew it; then the newest release above the installed one that is neither.
261
+ const offered = offeredUpgrade(dep);
262
+ const vet = offered ? offers.get(instanceKey(dep.ecosystem, dep.name, dep.currentVersion)) : undefined;
263
+ const offerNote = offered ? (vet ? vet.note : offerSources ? uncheckedOfferNote(dep.name, offered) : null) : null;
264
+ const latestStable = vet?.checked && vet.note ? vet.safe : registryLatest;
265
+ const noSafeUpgrade = Boolean(offered && vet?.checked && vet.note && vet.safe === null);
198
266
  let targetVersion = latestStable;
199
267
  let platformActive = true;
200
268
  let fixable = false;
201
269
  // Check vulnerabilities
202
270
  const vulns = vulnsByPackage.get(instanceKey(dep.ecosystem, dep.name, dep.currentVersion));
271
+ // One advisory per alias group: GHSA + PYSEC records of one issue counted twice (django 96 for 51, M1).
272
+ const groups = groupAdvisories(vulns);
273
+ const unfixedGroups = groups.filter((g) => !g.members.some((m) => m.fixedVersion));
203
274
  if (vulns && vulns.length > 0) {
204
- reasons.push(`${vulns.length} known vulnerabilit${vulns.length !== 1 ? "ies" : "y"} (${vulns.map((v) => v.vulnId).join(", ")})`);
275
+ reasons.push(vulnerabilitiesReason(groups));
205
276
  if (vulns.every((v) => !v.platformActive)) {
206
277
  // Code this host never compiles is not this host's exposure: listed,
207
278
  // never ranked as high (openssl topped the plan on Windows).
@@ -210,7 +281,7 @@ async function executeStandalonePlanner(params, liveIntel) {
210
281
  reasons.push("Affected code is gated behind a target not active on this platform");
211
282
  }
212
283
  else {
213
- risk = severityToRisk(vulns.filter((v) => v.platformActive).map((v) => v.severity));
284
+ risk = riskOf(vulns.filter((v) => v.platformActive));
214
285
  }
215
286
  // The smallest version that fixes every fixable advisory: each
216
287
  // fixedVersion is already the fix on the installed release line.
@@ -218,9 +289,8 @@ async function executeStandalonePlanner(params, liveIntel) {
218
289
  if (fixedVersions.length > 0) {
219
290
  targetVersion = maxVersion(fixedVersions, dep.ecosystem);
220
291
  fixable = true;
221
- if (fixedVersions.length < vulns.length) {
222
- reasons.push(`${vulns.length - fixedVersions.length} of them have no published fix`);
223
- }
292
+ if (unfixedGroups.length > 0)
293
+ reasons.push(noFixReason(unfixedGroups.length));
224
294
  }
225
295
  }
226
296
  // Check deprecation
@@ -229,9 +299,10 @@ async function executeStandalonePlanner(params, liveIntel) {
229
299
  risk = "high";
230
300
  reasons.push(dep.deprecationMessage ? `Deprecated: ${dep.deprecationMessage}` : "Package is deprecated");
231
301
  }
232
- // Check version distance
233
- if (dep.versionsBehind) {
234
- const d = dep.versionsBehind;
302
+ // Check version distance: to the release this row may offer, not to a known-affected latest.
303
+ const behind = latestStable === registryLatest ? dep.versionsBehind : latestStable && dep.currentVersion ? computeSemverDistance(dep.currentVersion, latestStable) : null;
304
+ if (behind && behind.label !== "up-to-date") {
305
+ const d = behind;
235
306
  if (d.label === "major" && d.major === 0) {
236
307
  // A 0.x bump: breaking under caret rules, though the major number is unchanged.
237
308
  const n = d.minor > 0 ? d.minor : d.patch;
@@ -270,13 +341,20 @@ async function executeStandalonePlanner(params, liveIntel) {
270
341
  const installedVulns = vulnsByPackage.get(instanceKey("npm", dep.name, installed));
271
342
  if (!installedVulns || installedVulns.length === 0)
272
343
  continue;
273
- reasons.push(`The installed ${installed} has ${installedVulns.length} known CVE${installedVulns.length !== 1 ? "s" : ""} (${installedVulns.map((v) => v.vulnId).join(", ")})`);
274
- risk = higherRisk(risk, severityToRisk(installedVulns.map((v) => v.severity)));
344
+ const installedGroups = groupAdvisories(installedVulns);
345
+ reasons.push(`The installed ${installed} has ${installedGroups.length} known CVE${installedGroups.length !== 1 ? "s" : ""} (${installedGroups.map((g) => g.best.vulnId).join(", ")})`);
346
+ risk = higherRisk(risk, riskOf(installedVulns));
275
347
  }
276
348
  }
349
+ // Every newer release is known-affected or withdrawn: the row says so (crossenv: remove it).
350
+ if (noSafeUpgrade && offerNote)
351
+ reasons.push(offerNote);
277
352
  // Skip if no upgrade needed (no vuln, not deprecated, not behind, no drift => no reasons)
278
353
  if (reasons.length === 0)
279
354
  continue;
355
+ // The newer release it would have offered was replaced or could not be checked: said, never silent.
356
+ if (offerNote && !noSafeUpgrade)
357
+ reasons.push(offerNote);
280
358
  if (dep.declaredRange) {
281
359
  reasons.push(`No lockfile: ${dep.currentVersion} is the floor of the declared range "${dep.declaredRange}", not an installed version`);
282
360
  }
@@ -299,19 +377,26 @@ async function executeStandalonePlanner(params, liveIntel) {
299
377
  ? "not_built_on_this_host"
300
378
  : vulnerable && !fixable
301
379
  ? "no_fix_available"
302
- : targetVersion
303
- ? "upgrade_direct"
304
- : "no_fix_available";
380
+ : !vulnerable && noSafeUpgrade
381
+ ? vet?.holding
382
+ ? "remove_dependency"
383
+ : "hold_current"
384
+ : targetVersion
385
+ ? "upgrade_direct"
386
+ : "no_fix_available";
305
387
  const rec = {
306
388
  package: dep.name,
307
389
  ecosystem: dep.ecosystem,
308
390
  currentVersion: dep.currentVersion,
309
- targetVersion: reinstallOnly ? dep.currentVersion : targetVersion,
391
+ // A vulnerable row with no fix names no release (its newest is affected too): the parent_release
392
+ // vectors' own_target is null there (pr-006, request 2.88.0), and verifyFixPaths names one only
393
+ // when a release past a last_affected bound is clean.
394
+ targetVersion: reinstallOnly ? dep.currentVersion : action === "no_fix_available" ? null : targetVersion,
310
395
  ...(latestStable && latestStable !== targetVersion ? { latestVersion: latestStable } : {}),
311
- upgradeType,
396
+ upgradeType: action === "no_fix_available" ? "unknown" : upgradeType,
312
397
  risk,
313
398
  reasons,
314
- breaking: upgradeType === "major",
399
+ breaking: action !== "no_fix_available" && upgradeType === "major",
315
400
  isDev: dep.isDev,
316
401
  scope: "direct",
317
402
  action,
@@ -321,7 +406,15 @@ async function executeStandalonePlanner(params, liveIntel) {
321
406
  };
322
407
  recommendations.push(rec);
323
408
  if (fixable && !reinstallOnly && action === "upgrade_direct" && vulns) {
324
- fixPathInputs.set(rec, { fixes: vulns.map((v) => v.fixedVersion).filter((f) => Boolean(f)), sourceDirs: [] });
409
+ // The manifest directories that declare it: where its upgrade command runs (audit E8).
410
+ fixPathInputs.set(rec, {
411
+ fixes: vulns.map((v) => v.fixedVersion).filter((f) => Boolean(f)),
412
+ sourceDirs: [...new Set(vulns.flatMap((v) => v.sourceDirs ?? []))],
413
+ unfixed: unfixedGroups.map((g) => ({ id: g.best.vulnId, ids: [...g.ids] })),
414
+ });
415
+ }
416
+ else if (vulnerable && action === "no_fix_available") {
417
+ fixPathInputs.set(rec, { fixes: [], sourceDirs: [] });
325
418
  }
326
419
  }
327
420
  // Transitive vulnerable packages — previously counted in scan totals but
@@ -329,8 +422,10 @@ async function executeStandalonePlanner(params, liveIntel) {
329
422
  // The skip is instance-aware: a transitive instance at a DIFFERENT version
330
423
  // than a same-named direct dep is its own finding (anyhow 1.0.102 transitive
331
424
  // beside anyhow 1.0.104 direct), not a duplicate of the direct row.
425
+ let devTransitiveLeftOut = 0;
332
426
  if (vulnResult) {
333
427
  const transitive = new Map();
428
+ const devSkipped = new Set();
334
429
  for (const v of vulnResult.vulnerabilities) {
335
430
  if (v.isDirect)
336
431
  continue;
@@ -340,19 +435,23 @@ async function executeStandalonePlanner(params, liveIntel) {
340
435
  continue;
341
436
  if (directPackages.has(instanceKey(v.ecosystem, v.package, v.currentVersion)))
342
437
  continue;
343
- if (!includeDev && v.isDev && v.devScopeKnown)
344
- continue;
345
438
  const key = instanceKey(v.ecosystem, v.package, v.currentVersion);
439
+ if (!includeDev && isDevOnlyCopy(v)) {
440
+ devSkipped.add(key);
441
+ continue;
442
+ }
346
443
  if (!transitive.has(key))
347
444
  transitive.set(key, []);
348
445
  transitive.get(key).push(v);
349
446
  }
447
+ devTransitiveLeftOut = [...devSkipped].filter((k) => !transitive.has(k)).length;
350
448
  for (const entries of transitive.values()) {
351
449
  const first = entries[0];
352
450
  const fixedVersions = entries.map((e) => e.fixedVersion).filter((f) => Boolean(f));
353
451
  const platformActive = !entries.every((e) => !e.platformActive);
452
+ const groups = groupAdvisories(entries);
354
453
  const reasons = [
355
- `${entries.length} known vulnerabilit${entries.length !== 1 ? "ies" : "y"} (${entries.map((e) => e.vulnId).join(", ")})`,
454
+ vulnerabilitiesReason(groups),
356
455
  "Transitive dependency — not declared in your manifest; the fix arrives via a parent package update or a lockfile refresh",
357
456
  ];
358
457
  if (!platformActive) {
@@ -365,7 +464,7 @@ async function executeStandalonePlanner(params, liveIntel) {
365
464
  currentVersion: first.currentVersion,
366
465
  targetVersion: target,
367
466
  upgradeType: upgradeTypeBetween(first.currentVersion, target),
368
- risk: platformActive ? severityToRisk(entries.filter((e) => e.platformActive).map((e) => e.severity)) : "low",
467
+ risk: platformActive ? riskOf(entries.filter((e) => e.platformActive)) : "low",
369
468
  reasons,
370
469
  breaking: false,
371
470
  isDev: entries.every((e) => e.isDev),
@@ -374,8 +473,13 @@ async function executeStandalonePlanner(params, liveIntel) {
374
473
  platformActive,
375
474
  };
376
475
  recommendations.push(rec);
377
- if (rec.action === "waiting_on_upstream") {
378
- fixPathInputs.set(rec, { fixes: fixedVersions, sourceDirs: [...new Set(entries.flatMap((e) => e.sourceDirs ?? []))] });
476
+ if (rec.action === "waiting_on_upstream" || rec.action === "no_fix_available") {
477
+ const unfixed = groups.filter((g) => !g.members.some((m) => m.fixedVersion));
478
+ fixPathInputs.set(rec, {
479
+ fixes: fixedVersions,
480
+ sourceDirs: [...new Set(entries.flatMap((e) => e.sourceDirs ?? []))],
481
+ unfixed: unfixed.map((g) => ({ id: g.best.vulnId, ids: [...g.ids] })),
482
+ });
379
483
  }
380
484
  }
381
485
  }
@@ -401,20 +505,39 @@ async function executeStandalonePlanner(params, liveIntel) {
401
505
  const limited = filtered.slice(0, maxRecs);
402
506
  // Verify the fix path of every vulnerable step the plan returns: a target
403
507
  // no advisory affects, and whether a lockfile refresh already reaches it.
404
- const unverified = await verifyFixPaths(limited.filter((r) => fixPathInputs.has(r)).map((r) => ({ step: r, ...fixPathInputs.get(r) })), liveIntel);
508
+ const verifiedInputs = limited.filter((r) => fixPathInputs.has(r)).map((r) => ({ step: r, ...fixPathInputs.get(r) }));
509
+ const unverified = await verifyFixPaths(verifiedInputs, liveIntel);
510
+ // A direct bump must not stop at a release that still pins a vulnerable transitive a slightly
511
+ // newer release clears (express 4.20.0 still pins path-to-regexp 0.1.10; audit H1).
512
+ await reconcileDirectTargets(limited, verifiedInputs, liveIntel);
405
513
  limited.sort(planOrder);
514
+ // Go module versions carry their `v` in every field, as currentVersion does (audit 7e).
515
+ for (const r of limited) {
516
+ r.targetVersion = inEcosystemForm(r.ecosystem, r.targetVersion);
517
+ if (r.latestVersion !== undefined)
518
+ r.latestVersion = inEcosystemForm(r.ecosystem, r.latestVersion);
519
+ }
406
520
  const upgrades = limited.filter((r) => r.scope === "direct" && r.action === "upgrade_direct");
407
521
  const quickWins = upgrades.filter((r) => !r.breaking).length;
408
522
  const breakingChanges = upgrades.filter((r) => r.breaking).length;
409
523
  const waitingOnUpstream = limited.filter((r) => r.action === "waiting_on_upstream").length;
410
524
  const lockfileRefreshes = limited.filter((r) => r.action === "lockfile_refresh").length;
525
+ const parentUpgradeSteps = limited.filter((r) => r.action === "upgrade_parent").length;
526
+ const devOnly = limited.filter((r) => r.isDev).length;
527
+ const overrides = limited.filter((r) => r.action === "override_or_replace").length;
411
528
  const reinstalls = limited.filter((r) => r.action === "reinstall").length;
412
529
  const noFix = limited.filter((r) => r.action === "no_fix_available").length;
413
530
  const inactive = limited.filter((r) => r.action === "not_built_on_this_host").length;
531
+ const unverifiedParents = limited.filter((r) => r.action === "upgrade_parent_unverified").length;
532
+ const removals = limited.filter((r) => r.action === "remove_dependency").length;
533
+ const holds = limited.filter((r) => r.action === "hold_current").length;
414
534
  // Summary
415
535
  const parts = [];
536
+ if (coverage)
537
+ parts.push(`${coverage.note} This plan ranks only what the checked part showed`);
416
538
  parts.push(filtered.length > limited.length
417
- ? `Showing ${limited.length} of ${filtered.length} recommendations (raise max_recommendations for the rest)`
539
+ ? // The counts that follow describe the rows shown, not the whole plan (audit 6.2.5 D-6: 6 waiting shown vs 43 real).
540
+ `Showing ${limited.length} of ${filtered.length} recommendations (raise max_recommendations for the rest); the counts below are for the ${limited.length} shown`
418
541
  : `${limited.length} recommendation${limited.length !== 1 ? "s" : ""}`);
419
542
  if (upgrades.length > 0)
420
543
  parts.push(`${upgrades.length} upgradable directly (${quickWins} non-breaking, ${breakingChanges} crossing a major version)`);
@@ -422,8 +545,19 @@ async function executeStandalonePlanner(params, liveIntel) {
422
545
  parts.push(`${reinstalls} need only a reinstall (node_modules behind the lockfile)`);
423
546
  if (lockfileRefreshes > 0)
424
547
  parts.push(`${lockfileRefreshes} transitive, fixed by a lockfile refresh (every parent already admits a clean version)`);
548
+ if (parentUpgradeSteps > 0)
549
+ parts.push(`${parentUpgradeSteps} transitive, fixed by upgrading the parent that pins it (parentUpgrades names the release)`);
550
+ if (unverifiedParents > 0) {
551
+ parts.push(`${unverifiedParents} transitive, a parent release that admits a clean version exists but no release of the declared dependency that brings it in was verified (parentUpgrades says what to check; not counted as fixed)`);
552
+ }
425
553
  if (waitingOnUpstream > 0)
426
554
  parts.push(`${waitingOnUpstream} transitive, waiting on upstream`);
555
+ if (removals > 0)
556
+ parts.push(`${removals} to remove or replace: no release is clean of the malware or advisories affecting the installed copy`);
557
+ if (holds > 0)
558
+ parts.push(`${holds} to keep where ${holds === 1 ? "it is" : "they are"}: every newer release is known-affected or withdrawn`);
559
+ if (overrides > 0)
560
+ parts.push(`${overrides} transitive, blocked by a parent that will not move (deprecated, already its latest release, or no release admits a clean version): override the version or replace the parent`);
427
561
  if (unverified > 0)
428
562
  parts.push(`${unverified} fix target${unverified !== 1 ? "s" : ""} checked only against the installed version's advisories (offline, or OSV or the registry did not answer in time; see fixPathChecked)`);
429
563
  if (noFix > 0)
@@ -433,20 +567,36 @@ async function executeStandalonePlanner(params, liveIntel) {
433
567
  const criticalCount = limited.filter((r) => r.risk === "critical").length;
434
568
  if (criticalCount > 0)
435
569
  parts.push(`${criticalCount} critical`);
570
+ if (devOnly > 0)
571
+ parts.push(`${devOnly} dev-only (isDev)`);
572
+ // The population, worded as dependency_health and vulnerability_scan word it (finding 6).
573
+ if (vulnResult)
574
+ parts.push(populationNote(vulnResult.vulnerabilities, liveIntel.getAuditDeps(), includeDev, named));
575
+ if (devDirectLeftOut + devTransitiveLeftOut + devDirectUnchecked > 0) {
576
+ const left = [];
577
+ if (devDirectLeftOut > 0)
578
+ left.push(`${devDirectLeftOut} vulnerable devDependenc${devDirectLeftOut !== 1 ? "ies" : "y"}`);
579
+ if (devDirectUnchecked > 0)
580
+ left.push(`${devDirectUnchecked} devDependenc${devDirectUnchecked !== 1 ? "ies" : "y"} not checked for vulnerabilities`);
581
+ if (devTransitiveLeftOut > 0)
582
+ left.push(`${devTransitiveLeftOut} dev-only vulnerable transitive cop${devTransitiveLeftOut !== 1 ? "ies" : "y"}`);
583
+ parts.push(`include_dev: false left out ${left.join(" and ")} (the default, true, plans them)`);
584
+ }
585
+ if (devFreshnessSkipped > 0) {
586
+ parts.push(`${devFreshnessSkipped} devDependenc${devFreshnessSkipped !== 1 ? "ies" : "y"} with no known vulnerability ${devFreshnessSkipped !== 1 ? "were" : "was"} not checked for newer releases or deprecation (include_dev: true checks them)`);
587
+ }
436
588
  if (maintenanceNotices.size > 0) {
437
589
  parts.push(`${maintenanceNotices.size} unmaintained-package notice${maintenanceNotices.size !== 1 ? "s" : ""} not ranked (no fix exists; see vulnerability_scan maintenance_notices)`);
438
590
  }
439
591
  const partial = discoveryNote(liveIntel.getDiscovery());
440
592
  if (partial)
441
593
  parts.push(partial.replace(/[.]$/, ""));
594
+ // Ecosystems not read, test fixtures left out, lockfiles that could not be read: never silent.
595
+ for (const note of [...exclusionNotes(liveIntel.getDiscovery()), ...unreadableNotes(liveIntel.getLockfileProblems())])
596
+ parts.push(note.replace(/[.]$/, ""));
442
597
  const nothing = vulnerabilityDataAvailable ? "nothing to upgrade (no known vulnerability, deprecation, or newer release of a direct dependency)" : "no vulnerability data to plan from";
443
598
  if (only && limited.length === 0)
444
599
  parts.push(emptyAnswerNote(only, liveIntel, includeDev, nothing));
445
- if (!vulnerabilityDataAvailable) {
446
- parts.push(liveIntel.isEnabled()
447
- ? `No vulnerability data: the OSV scan did not finish within ${PLANNER_SCAN_TIMEOUT_MS / 1000}s or OSV was unreachable, so this plan is CVE-blind. Call again to use the scan once it completes`
448
- : "No vulnerability data: offline mode (FOURDA_OFFLINE), so this plan is CVE-blind");
449
- }
450
600
  return {
451
601
  generatedAt: new Date().toISOString(),
452
602
  projectPath: liveIntel.getProjectRoot() ?? vulnResult?.projectPath ?? process.cwd(),
@@ -457,7 +607,33 @@ async function executeStandalonePlanner(params, liveIntel) {
457
607
  breakingChanges,
458
608
  waitingOnUpstream,
459
609
  lockfileRefreshes,
610
+ parentUpgradeSteps,
460
611
  vulnerabilityDataAvailable,
612
+ ...(coverage ? { vulnerabilityCoverage: coverage } : {}),
461
613
  provenance: { mode: "standalone_heuristic", note: PROVENANCE_NOTE },
462
614
  };
463
615
  }
616
+ /**
617
+ * "Planned over N vulnerable packages (M installed copies) in the full tree of T installed
618
+ * (K dev-only copies left out)": the same population, counted the same way, as
619
+ * dependency_health's vulnerableCount and vulnerability_scan's total_vulnerable_packages.
620
+ */
621
+ function populationNote(vulns, tree, includeDev, named) {
622
+ const active = vulns.filter((v) => isActionableVulnerability(v) && (includeDev || !isDevOnlyCopy(v)) && named(v.package, v.ecosystem));
623
+ const packages = new Set(active.map((v) => `${v.ecosystem}\0${v.package}`)).size;
624
+ const copies = new Set(active.map((v) => `${v.ecosystem}\0${v.package}\0${v.currentVersion}`)).size;
625
+ const versioned = tree.filter((d) => d.version !== null);
626
+ const devOnly = versioned.filter((d) => isDevOnlyCopy(d)).length;
627
+ const total = includeDev ? versioned.length : versioned.length - devOnly;
628
+ const dev = devOnly > 0 ? (includeDev ? "; dev-only copies included" : `; ${devOnly.toLocaleString("en-US")} dev-only cop${devOnly === 1 ? "y" : "ies"} left out`) : "";
629
+ return `Planned over ${packages} vulnerable package${packages !== 1 ? "s" : ""} (${copies} installed cop${copies !== 1 ? "ies" : "y"}) in the full tree of ${total.toLocaleString("en-US")} installed${dev ? ` (${dev.slice(2)})` : ""}`;
630
+ }
631
+ /** "N known vulnerabilities (ids)", one per alias group. */
632
+ function vulnerabilitiesReason(groups) {
633
+ const n = groups.length;
634
+ return `${n} known vulnerabilit${n !== 1 ? "ies" : "y"} (${groups.map((g) => g.best.vulnId).join(", ")})`;
635
+ }
636
+ /** Before the fix path is checked; verifyFixPaths rewrites it once the target is known (M2). */
637
+ function noFixReason(n) {
638
+ return `${n} of them ${n === 1 ? "has" : "have"} no published fix`;
639
+ }
@@ -20,11 +20,18 @@ export interface PackageRow {
20
20
  package: string;
21
21
  current_version: string;
22
22
  severity: SeverityTier;
23
+ /** Set when the row's grade is a GHSA alias's: the worst record rates nothing itself. */
24
+ severity_from?: string;
25
+ /** A malicious-package record (MAL- id or alias, CWE-506) affects this copy. */
26
+ malware?: true;
23
27
  advisories: number;
24
28
  ids: string[];
25
29
  /** The version to move to: checked against every advisory of the package when fix_checked says so. */
26
30
  fixed_version: string | null;
27
31
  fix_checked: "all_advisories" | "installed_advisories_only";
32
+ /** Releases the registry withdrew (yanked, deprecated) that `fixed_version` steps over, and why, in plain words (FX-H5). */
33
+ withheld?: string[];
34
+ withheld_note?: string;
28
35
  unfixed_advisories?: number;
29
36
  is_direct: boolean | null;
30
37
  is_dev?: true;
@@ -34,23 +41,55 @@ export interface PackageRow {
34
41
  /** One row per package version: worst graded severity, the fix that clears every fixable advisory. */
35
42
  export declare function groupByPackage(rows: FormattedVuln[], targets?: FullScan["fix_targets"]): PackageRow[];
36
43
  export declare function conciseScanResult(full: FullScan): {
44
+ summary?: string | undefined;
37
45
  scan_time: string;
38
46
  project_root: string;
39
47
  host_platform: string;
40
48
  ecosystems_scanned: string[];
41
49
  total_dependencies_scanned: number;
42
- total_vulnerable_packages: number;
50
+ dev_scope?: string | undefined;
51
+ total_vulnerable_packages: number | null;
43
52
  coverage?: {
44
53
  complete: false;
45
54
  note: string;
55
+ unchecked_dependencies?: number | undefined;
56
+ checked_dependencies?: number | undefined;
57
+ osv_status?: string | undefined;
46
58
  lockfiles_not_scanned: number;
59
+ unreadable_lockfiles?: {
60
+ path: string;
61
+ error: string;
62
+ }[] | undefined;
63
+ partial_lockfiles?: {
64
+ path: string;
65
+ skipped_entries: number;
66
+ reason: string;
67
+ }[] | undefined;
68
+ refused_includes?: {
69
+ path: string;
70
+ reason: string;
71
+ }[] | undefined;
72
+ not_covered?: {
73
+ path: string;
74
+ ecosystem: string;
75
+ }[] | undefined;
76
+ not_covered_count?: number | undefined;
77
+ not_scanned?: {
78
+ test_fixture_lockfiles: number;
79
+ dirs: string[];
80
+ } | undefined;
47
81
  unresolved_manifests?: {
48
82
  manifest: string;
49
83
  declared_ranges: number;
50
84
  fix: string;
51
85
  }[] | undefined;
52
86
  } | undefined;
53
- by_severity: import("../live/severity-scope.js").SeverityCounts;
87
+ by_severity: import("../live/severity-scope.js").SeverityCounts | null;
88
+ found_in_checked_dependencies?: {
89
+ note: string;
90
+ vulnerable_packages: number;
91
+ by_severity: import("../live/severity-scope.js").SeverityCounts;
92
+ } | undefined;
54
93
  vulnerable_packages: PackageRow[];
55
94
  vulnerable_packages_note?: string | undefined;
56
95
  recommendations: string[];
@@ -79,6 +118,12 @@ export declare function conciseScanResult(full: FullScan): {
79
118
  lockfiles_read: number;
80
119
  resolved_at: string | null;
81
120
  privacy: string;
121
+ alias_severity?: {
122
+ rated_from_alias: number;
123
+ unresolved: string[];
124
+ ungraded: string[];
125
+ note?: string | undefined;
126
+ } | undefined;
82
127
  };
83
128
  };
84
129
  export {};