@4da/mcp-server 6.2.3 → 6.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +520 -0
  2. package/README.md +22 -6
  3. package/dist/db.d.ts +38 -0
  4. package/dist/db.js +78 -23
  5. package/dist/doctor.js +6 -3
  6. package/dist/embeddings.d.ts +1 -1
  7. package/dist/embeddings.js +16 -14
  8. package/dist/index.js +73 -103
  9. package/dist/live/advisory-groups.d.ts +68 -0
  10. package/dist/live/advisory-groups.js +131 -0
  11. package/dist/live/cache.js +11 -3
  12. package/dist/live/cargo-platform.d.ts +6 -0
  13. package/dist/live/cargo-platform.js +8 -2
  14. package/dist/live/changelog.js +38 -6
  15. package/dist/live/dependency-set.d.ts +9 -1
  16. package/dist/live/dependency-set.js +10 -2
  17. package/dist/live/fix-paths.d.ts +289 -16
  18. package/dist/live/fix-paths.js +876 -93
  19. package/dist/live/github-releases.js +3 -0
  20. package/dist/live/held-dependencies.d.ts +93 -0
  21. package/dist/live/held-dependencies.js +212 -0
  22. package/dist/live/held-targets.d.ts +30 -0
  23. package/dist/live/held-targets.js +107 -0
  24. package/dist/live/http-utils.js +13 -3
  25. package/dist/live/index.d.ts +7 -4
  26. package/dist/live/index.js +24 -19
  27. package/dist/live/install-state.js +6 -2
  28. package/dist/live/js-lockfile-readers.d.ts +8 -1
  29. package/dist/live/js-lockfile-readers.js +43 -6
  30. package/dist/live/lockfile-parents.d.ts +15 -0
  31. package/dist/live/lockfile-parents.js +85 -10
  32. package/dist/live/lockfile-parsers-pygo.d.ts +47 -2
  33. package/dist/live/lockfile-parsers-pygo.js +230 -12
  34. package/dist/live/lockfile-parsers-rbphp.d.ts +38 -0
  35. package/dist/live/lockfile-parsers-rbphp.js +123 -0
  36. package/dist/live/lockfile-parsers.d.ts +6 -0
  37. package/dist/live/lockfile-parsers.js +116 -4
  38. package/dist/live/lockfile-types.d.ts +28 -1
  39. package/dist/live/maintenance.d.ts +20 -4
  40. package/dist/live/maintenance.js +31 -2
  41. package/dist/live/malware.d.ts +20 -0
  42. package/dist/live/malware.js +31 -0
  43. package/dist/live/npm-packument.d.ts +4 -1
  44. package/dist/live/npm-packument.js +5 -2
  45. package/dist/live/offered-release.d.ts +53 -0
  46. package/dist/live/offered-release.js +117 -0
  47. package/dist/live/osv-ranges.d.ts +111 -0
  48. package/dist/live/osv-ranges.js +173 -14
  49. package/dist/live/osv-scanner.d.ts +87 -5
  50. package/dist/live/osv-scanner.js +310 -59
  51. package/dist/live/pinned-advisories.d.ts +2 -2
  52. package/dist/live/pinned-advisories.js +3 -3
  53. package/dist/live/pypi-advice.d.ts +36 -0
  54. package/dist/live/pypi-advice.js +158 -0
  55. package/dist/live/range-satisfies.d.ts +4 -2
  56. package/dist/live/range-satisfies.js +7 -5
  57. package/dist/live/rate-limiter.d.ts +7 -0
  58. package/dist/live/rate-limiter.js +22 -1
  59. package/dist/live/refresh-rule.d.ts +36 -0
  60. package/dist/live/refresh-rule.js +49 -0
  61. package/dist/live/release-metadata.d.ts +5 -0
  62. package/dist/live/resolution.d.ts +3 -0
  63. package/dist/live/resolution.js +6 -3
  64. package/dist/live/scan-coverage.d.ts +26 -0
  65. package/dist/live/scan-coverage.js +70 -0
  66. package/dist/live/semver-utils.d.ts +8 -0
  67. package/dist/live/semver-utils.js +26 -3
  68. package/dist/live/severity-scope.d.ts +26 -1
  69. package/dist/live/severity-scope.js +32 -0
  70. package/dist/live/shell-safe.d.ts +19 -0
  71. package/dist/live/shell-safe.js +36 -0
  72. package/dist/live/types.d.ts +134 -8
  73. package/dist/live/version-compare.d.ts +51 -0
  74. package/dist/live/version-compare.js +152 -1
  75. package/dist/live/withdrawn-releases.d.ts +50 -0
  76. package/dist/live/withdrawn-releases.js +125 -0
  77. package/dist/project-manifests.d.ts +7 -0
  78. package/dist/project-manifests.js +45 -5
  79. package/dist/project-scanner.js +6 -4
  80. package/dist/project-tree.d.ts +56 -0
  81. package/dist/project-tree.js +174 -5
  82. package/dist/schema-registry.d.ts +47 -4
  83. package/dist/schema-registry.js +76 -9
  84. package/dist/server-instructions.js +4 -2
  85. package/dist/setup.d.ts +8 -0
  86. package/dist/setup.js +21 -2
  87. package/dist/tool-dispatch.js +3 -3
  88. package/dist/tools/agent-memory.d.ts +5 -0
  89. package/dist/tools/agent-memory.js +9 -0
  90. package/dist/tools/briefing-task-scope.js +5 -2
  91. package/dist/tools/decision-memory.js +2 -0
  92. package/dist/tools/dependency-check-held.d.ts +44 -0
  93. package/dist/tools/dependency-check-held.js +98 -0
  94. package/dist/tools/dependency-check-signals.d.ts +19 -2
  95. package/dist/tools/dependency-check-signals.js +84 -13
  96. package/dist/tools/dependency-check-versions.d.ts +37 -4
  97. package/dist/tools/dependency-check-versions.js +48 -13
  98. package/dist/tools/dependency-check.d.ts +11 -1
  99. package/dist/tools/dependency-check.js +81 -29
  100. package/dist/tools/dependency-health.js +209 -45
  101. package/dist/tools/get-context.d.ts +3 -0
  102. package/dist/tools/get-context.js +4 -0
  103. package/dist/tools/project-scope.d.ts +8 -0
  104. package/dist/tools/project-scope.js +21 -0
  105. package/dist/tools/upgrade-impact-features.d.ts +37 -0
  106. package/dist/tools/upgrade-impact-features.js +176 -0
  107. package/dist/tools/upgrade-impact-literals.js +26 -4
  108. package/dist/tools/upgrade-impact-report.d.ts +5 -1
  109. package/dist/tools/upgrade-impact-report.js +26 -5
  110. package/dist/tools/upgrade-impact.js +10 -3
  111. package/dist/tools/upgrade-planner-fixpaths.d.ts +164 -5
  112. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  113. package/dist/tools/upgrade-planner.d.ts +70 -7
  114. package/dist/tools/upgrade-planner.js +237 -61
  115. package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
  116. package/dist/tools/vulnerability-scan-concise.js +37 -3
  117. package/dist/tools/vulnerability-scan-format.d.ts +126 -24
  118. package/dist/tools/vulnerability-scan-format.js +193 -26
  119. package/dist/tools/vulnerability-scan.d.ts +224 -63
  120. package/dist/tools/vulnerability-scan.js +50 -15
  121. package/dist/tools/what-should-i-know.d.ts +6 -1
  122. package/dist/tools/what-should-i-know.js +72 -15
  123. package/package.json +2 -2
@@ -20,6 +20,27 @@ import path from "node:path";
20
20
  /** Punctuation that makes a fragment syntax rather than a name. */
21
21
  const SYNTAX_CHARS = /[/:*{}?$%@#<>]/g;
22
22
  const WORD = /[A-Za-z_][\w-]*/g;
23
+ /** WORD, with a Rust raw identifier (`r#type`) read as one token. */
24
+ const WORD_OR_RAW = /r#[A-Za-z_]\w*|[A-Za-z_][\w-]*/g;
25
+ /**
26
+ * The part of an entry that names the OLD syntax. "from X to Y" -> X;
27
+ * "... instead of X" -> X; otherwise the text before the replacement is named
28
+ * (";", "use", "instead", "in favour of", "replaced by/with", "->", "now").
29
+ * Before 6.2.5 the whole entry was read when it had no "from ... to", so
30
+ * thiserror 2's "`{r#type}` ... is no longer accepted; simply use ... `{type}`"
31
+ * took the new `{type}` for the old syntax and flagged every `{name}` format
32
+ * string in the project (audit 2026-10-12, B-7).
33
+ */
34
+ function oldSide(text) {
35
+ const fromTo = /\bfrom\b([\s\S]*?)\bto\b/i.exec(text);
36
+ if (fromTo)
37
+ return fromTo[1];
38
+ const insteadOf = /\binstead of\b([\s\S]*)$/i.exec(text);
39
+ if (insteadOf)
40
+ return insteadOf[1];
41
+ const cut = /;|\binstead\b|\buse\b|\bin favou?r of\b|\breplaced? (?:by|with)\b|->|→|\bnow\b/i.exec(text);
42
+ return cut ? text.slice(0, cut.index) : text;
43
+ }
23
44
  const MAX_FILES = 300;
24
45
  const MAX_FILE_BYTES = 512 * 1024;
25
46
  const MAX_HITS_PER_ENTRY = 5;
@@ -32,8 +53,7 @@ function escapeRegex(text) {
32
53
  * migrated project already uses) is never reported as needing a change.
33
54
  */
34
55
  export function oldSyntaxShapes(entryText) {
35
- const fromTo = /\bfrom\b([\s\S]*?)\bto\b/i.exec(entryText);
36
- const scope = fromTo ? fromTo[1] : entryText;
56
+ const scope = oldSide(entryText);
37
57
  const shapes = [];
38
58
  for (const [, fragment] of scope.matchAll(/`([^`\s]{2,40})`/g)) {
39
59
  // A path or call (`Router::route`, `res.json()`, `a.b`) is a symbol, not syntax.
@@ -45,8 +65,10 @@ export function oldSyntaxShapes(entryText) {
45
65
  continue;
46
66
  let pattern = "";
47
67
  let last = 0;
48
- for (const m of fragment.matchAll(WORD)) {
49
- pattern += escapeRegex(fragment.slice(last, m.index)) + "[A-Za-z_][\\w-]*";
68
+ for (const m of fragment.matchAll(WORD_OR_RAW)) {
69
+ // A Rust raw identifier keeps its `r#`: `{r#type}` is not any `{x#y}`.
70
+ const placeholder = m[0].startsWith("r#") ? "r#[A-Za-z_]\\w*" : "[A-Za-z_][\\w-]*";
71
+ pattern += escapeRegex(fragment.slice(last, m.index)) + placeholder;
50
72
  last = (m.index ?? 0) + m[0].length;
51
73
  }
52
74
  pattern += escapeRegex(fragment.slice(last));
@@ -28,6 +28,8 @@ export interface ReportEntry {
28
28
  details?: string[];
29
29
  touches_your_code?: boolean;
30
30
  matched_symbols?: string[];
31
+ /** Cargo features this project enables that the entry names (a renamed or removed feature). */
32
+ matched_features?: string[];
31
33
  /** String literals in your code that use the syntax this entry retires (route patterns and the like). */
32
34
  matched_literals?: Array<{
33
35
  file: string;
@@ -55,7 +57,9 @@ export interface ShapedChangelog {
55
57
  duplicates?: number;
56
58
  }
57
59
  /** Flag, order and trim changelog sections. Counts always cover every entry, trimmed or not. */
58
- export declare function shapeChangelog(sections: ChangelogSection[], symbols: string[], format: ResponseFormat): ShapedChangelog;
60
+ export declare function shapeChangelog(sections: ChangelogSection[], symbols: string[], format: ResponseFormat,
61
+ /** Cargo features the project enables on the crate (upgrade-impact-features.ts). */
62
+ features?: string[]): ShapedChangelog;
59
63
  export type UpgradeType = "patch" | "minor" | "major" | "prerelease" | "unknown";
60
64
  /**
61
65
  * Upgrade class by semver. A 0.x minor bump is reported as "major": under
@@ -20,12 +20,15 @@
20
20
  import { classifyHeading } from "../live/changelog-classify.js";
21
21
  import { compareVersionPrecedence, parseSemverPrecedence } from "../live/semver-precedence.js";
22
22
  import { matchSymbols } from "./upgrade-impact-callsites.js";
23
+ import { matchFeatures } from "./upgrade-impact-features.js";
23
24
  const CONCISE_NEUTRAL_PER_VERSION = 20;
24
25
  const CONCISE_ADDITIVE_PER_VERSION = 3;
25
26
  const DETAILED_ENTRY_CAP = 400;
26
27
  const KIND_ORDER = { breaking: 0, security: 1, deprecation: 2, change: 3 };
27
28
  /** Flag, order and trim changelog sections. Counts always cover every entry, trimmed or not. */
28
- export function shapeChangelog(sections, symbols, format) {
29
+ export function shapeChangelog(sections, symbols, format,
30
+ /** Cargo features the project enables on the crate (upgrade-impact-features.ts). */
31
+ features = []) {
29
32
  const out = {
30
33
  sections: [], breaking: 0, deprecations: 0, security: 0, touching: 0, touchingSymbols: [],
31
34
  };
@@ -37,7 +40,17 @@ export function shapeChangelog(sections, symbols, format) {
37
40
  // once. Measured 2026-10-02 in an agent eval: axum 0.7.9 -> 0.8.4 reported
38
41
  // "20 breaking changes" for about ten distinct ones.
39
42
  const seen = new Set();
40
- const key = (text) => text.toLowerCase().replace(/\(\[?#\d+\]?\)|\[#\d+\]|#\d+/g, "").replace(/[^a-z0-9]+/g, " ").trim();
43
+ // GitHub release notes list a change twice: the summary line, then the PR
44
+ // line with its conventional-commit prefix and "by @user in <url>" (reqwest
45
+ // 0.13: 12 such pairs). Neither makes it a different change.
46
+ const key = (text) => text
47
+ .toLowerCase()
48
+ .replace(/\s+by @[\w.-]+(?:\[bot\])?\s+in\s+https?:\/\/\S+/g, "")
49
+ .replace(/https?:\/\/\S+/g, "")
50
+ .replace(/^\W*(?:feat|fix|chore|docs|refactor|perf|test|build|ci|style|deps)(?:\([^)]*\))?!?:\s*/, "")
51
+ .replace(/\(\[?#\d+\]?\)|\[#\d+\]|#\d+/g, "")
52
+ .replace(/[^a-z0-9]+/g, " ")
53
+ .trim();
41
54
  for (const section of sections) {
42
55
  const unique = section.entries.filter((e) => {
43
56
  const k = key(e.text);
@@ -58,13 +71,19 @@ export function shapeChangelog(sections, symbols, format) {
58
71
  };
59
72
  if (e.kind === "breaking" || e.kind === "deprecation") {
60
73
  // The nested points name the affected functions (date-fns 3.0) as often as the entry does.
61
- const matched = matchSymbols([e.text, ...(e.details ?? [])].join(" "), symbols);
62
- if (matched.length > 0) {
63
- entry.touches_your_code = true;
74
+ const text = [e.text, ...(e.details ?? [])].join(" ");
75
+ const matched = matchSymbols(text, symbols);
76
+ const matchedFeatures = matchFeatures(text, features);
77
+ if (matched.length > 0)
64
78
  entry.matched_symbols = matched;
79
+ if (matchedFeatures.length > 0)
80
+ entry.matched_features = matchedFeatures;
81
+ if (matched.length > 0 || matchedFeatures.length > 0) {
82
+ entry.touches_your_code = true;
65
83
  if (e.kind === "breaking") {
66
84
  out.touching++;
67
85
  matched.forEach((s) => touched.add(s));
86
+ matchedFeatures.forEach((f) => touched.add(`feature "${f}"`));
68
87
  }
69
88
  }
70
89
  }
@@ -208,6 +227,8 @@ export function summarize(input) {
208
227
  breaking = flagged;
209
228
  if (input.changelogFrom)
210
229
  breaking += ` (from ${input.changelogFrom})`;
230
+ if (input.changelog === "partial" && input.githubHint)
231
+ breaking += ` (the changelog covers only part of the range${hint})`;
211
232
  }
212
233
  const advisories = input.advisoriesFixed === null ? "advisories unknown (OSV unreachable)" : `${input.advisoriesFixed} advisories fixed`;
213
234
  const path = input.pathChange
@@ -36,6 +36,7 @@ import { enrichGoVersions, goBasePath, goMajorPath, goPathMajor, goTagPrefix, go
36
36
  import { scanCallSites } from "./upgrade-impact-callsites.js";
37
37
  import { resolveChangelog } from "./upgrade-impact-changelog.js";
38
38
  import { pythonImportNames } from "./upgrade-impact-imports.js";
39
+ import { projectCargoFeatures } from "./upgrade-impact-features.js";
39
40
  import { flagOldSyntax } from "./upgrade-impact-literals.js";
40
41
  import { nearestVersions, shapeChangelog, summarize, upgradeType } from "./upgrade-impact-report.js";
41
42
  /**
@@ -319,7 +320,9 @@ export async function analyzeUpgradeImpact(params, ctx) {
319
320
  : Promise.resolve({ total_files: 0, files: [], symbols_used: [] }),
320
321
  ]);
321
322
  const { changelog, range } = resolved;
322
- const shaped = shapeChangelog(range.sections, yourCode.symbols_used, params.response_format === "detailed" ? "detailed" : "concise");
323
+ // Cargo features this project enables on the crate: a renamed or removed one is a breaking change symbols cannot see.
324
+ const features = eco === "crates.io" ? projectCargoFeatures(ctx.projectRoot, index.name, ctx.installed) : [];
325
+ const shaped = shapeChangelog(range.sections, yourCode.symbols_used, params.response_format === "detailed" ? "detailed" : "concise", features);
323
326
  // Route and pattern syntax the upgrade retires, found in this project's string literals.
324
327
  const literal = await flagOldSyntax(shaped.sections, yourCode.files, ctx.projectRoot);
325
328
  shaped.touching += literal.newlyTouching;
@@ -380,7 +383,9 @@ export async function analyzeUpgradeImpact(params, ctx) {
380
383
  : {}),
381
384
  your_code: py
382
385
  ? { ...yourCode, import_names: py.names, import_name_source: py.source, import_name_note: importNameNote(py, index.name) }
383
- : yourCode,
386
+ : features.length > 0
387
+ ? { ...yourCode, cargo_features: features }
388
+ : yourCode,
384
389
  advisories_fixed: advisoriesFixed,
385
390
  advisories_remaining: advTo,
386
391
  ...(advFrom === null || advTo === null ? { advisories_note: "OSV.dev could not be reached; advisory fields are null, not empty." } : {}),
@@ -403,7 +408,9 @@ export async function analyzeUpgradeImpact(params, ctx) {
403
408
  touchingSymbols: shaped.touchingSymbols,
404
409
  changelog: !changelog.found ? "missing" : !counted ? "no_entries" : range.coversRange ? "complete" : "partial",
405
410
  changelogFrom: fromGithub ? "GitHub release notes" : null,
406
- githubHint: resolved.github_fallback?.how_to_enable ? "FOURDA_GITHUB_CHANGELOG=true reads GitHub release notes" : null,
411
+ githubHint: resolved.github_fallback?.how_to_enable
412
+ ? "to read the GitHub release notes, set FOURDA_GITHUB_CHANGELOG=true in this MCP server's environment (opt-in; see github_fallback)"
413
+ : null,
407
414
  advisoriesFixed: advisoriesFixed ? advisoriesFixed.length : null,
408
415
  pathChange: pathChange ? { from: fromName, to: targetName, files: yourCode.total_files } : null,
409
416
  }),
@@ -2,13 +2,109 @@
2
2
  * Fix-path verification for upgrade_planner steps (see live/fix-paths.ts):
3
3
  * the target must clear EVERY known advisory of the package, and a
4
4
  * transitive step is "waiting on upstream" only when a parent's declared
5
- * requirement actually excludes every clean version.
5
+ * requirement actually excludes every clean version AND nothing about the
6
+ * parent's releases could be read.
6
7
  *
7
- * Runs on the steps the plan returns, within a time budget. A step whose
8
- * lookups fail or run out of time keeps its unverified answer and says so in
9
- * `fixPathChecked`.
8
+ * When a parent blocks the refresh and has a newer release, the step names
9
+ * the move (readiness audit H1, 2026-10-16; the 4DA app's
10
+ * `evidence/upgrade_parent.rs::parent_move`): the smallest parent release
11
+ * whose own declared requirement admits a clean version, read from the
12
+ * registry, and that is itself clear of the parent's own advisories; then
13
+ * the dependency the project DECLARES that moves it, the parent itself or
14
+ * every declared dependency it arrives through (readiness review
15
+ * 2026-10-17). A parent the walk cannot tie to a declaration names no lever
16
+ * and no command: `npm install <transitive>@v` would add it as a direct
17
+ * dependency. When the new requirement still admits the installed copy, an
18
+ * install keeps that copy (npm 10.9.2, checked 2026-10-17: express 4.21.2 ->
19
+ * 4.22.0 kept path-to-regexp 0.1.12 under `~0.1.12`), so the step names the
20
+ * refresh that follows.
21
+ *
22
+ * Runs on the steps the plan returns, within a time budget, each on a draft
23
+ * of its step: a lookup that runs out of time never writes to the step
24
+ * afterwards. A step whose lookups fail or run out of time keeps its
25
+ * unverified answer and says so in `fixPathChecked`.
10
26
  */
11
27
  import type { LiveIntelligence } from "../live/index.js";
28
+ import { type LockfileFormat } from "../live/lockfile-parents.js";
29
+ /** One dependency the project declares that moves a blocking parent. */
30
+ export interface ParentLever {
31
+ name: string;
32
+ version: string;
33
+ /** The release of it that brings the fix in; null when it could not be worked out (the reason says what to check). */
34
+ to: string | null;
35
+ /** True when `to` crosses a major version (a 0.x minor counts). */
36
+ breaking: boolean;
37
+ /** The command that moves it to `to`, per declaring project directory (the declared operator kept). */
38
+ commands?: Array<{
39
+ dir: string;
40
+ command: string;
41
+ refresh?: true;
42
+ }>;
43
+ /** A manifest edit, where no single verified command makes it (a workspace-inherited crate, a requirements pin). */
44
+ instructions?: Array<{
45
+ dir: string;
46
+ instruction: string;
47
+ }>;
48
+ /** Set only when `to` was lifted above it (one release per dependency): the lever's OWN smallest release that brings the fix in. */
49
+ smallest?: string;
50
+ /** Through a transitive parent: the parent's smallest release that admits a clean copy, which the lever must bring in at least. */
51
+ floor?: string;
52
+ /** Through a transitive parent: the parent release `to` brings in. */
53
+ brings?: string;
54
+ /**
55
+ * Set when this lever names no release although the parent's release is known (the app's
56
+ * per-lever ParentStanding, pr-011): it is flagged malicious, every release that would bring the
57
+ * fix in brings in an advisory it lacks, none does, or it is already at its latest release.
58
+ */
59
+ outcome?: "malicious" | StuckOutcome;
60
+ }
61
+ /**
62
+ * One blocking parent that can move: the release of it that admits a clean
63
+ * version of the package, and the declared dependencies that move it.
64
+ */
65
+ export interface ParentUpgrade {
66
+ /** The installed parent whose requirement excludes every clean version. */
67
+ parent: string;
68
+ from: string;
69
+ /** The smallest release of `parent` clear of its own advisories whose requirement admits a clean version (raised to the one release its lever names); null when its requirements could not be read, or when it leaves the tree (`goesAway`). */
70
+ to: string | null;
71
+ /** True when `to` crosses a major version of `parent` (mongodb 2.2.36 -> 3.1.3). */
72
+ breaking: boolean;
73
+ /** `to`'s requirement on the package; null when `to` no longer depends on it. Absent when `to` is null. */
74
+ requirement?: string | null;
75
+ /** True when `to` declares the package only as a peer dependency (npm 7+ installs it). */
76
+ peer?: boolean;
77
+ /** The version `to`'s requirement resolves the package to (its newest match), when the registry's version list was read. */
78
+ resolvesTo?: string | null;
79
+ /** When `to` is null: the parent's newest release, which may or may not admit a clean version. */
80
+ latest?: string;
81
+ /** Set only when `to` was lifted above it (one release per dependency): the parent's own smallest release that admits a clean copy. */
82
+ smallest?: string;
83
+ /**
84
+ * The parent will not move (no release admits a clean version, or it is already its newest), but
85
+ * every declared dependency it arrives through has a release that no longer brings it in: the
86
+ * levers name those releases, and the vulnerable copy goes away from that path with them (`to`
87
+ * is then null, as the parent leaves the tree).
88
+ */
89
+ goesAway?: boolean;
90
+ /**
91
+ * What the project declares and bumps: the parent itself when a manifest declares it, else
92
+ * every declared dependency it arrives through (at most three; `moreLevers` counts the
93
+ * rest). Empty when none could be tied to a declaration: then `leverUnknown` says why and
94
+ * no command is named.
95
+ */
96
+ levers: ParentLever[];
97
+ moreLevers?: number;
98
+ leverUnknown?: string;
99
+ /**
100
+ * The refresh to run after the lever's install: the new requirement still admits the
101
+ * installed copy, which an install keeps (npm 10.9.2), so the install alone does not fix it.
102
+ */
103
+ refreshCommands?: Array<{
104
+ dir: string;
105
+ command: string;
106
+ }>;
107
+ }
12
108
  /** The planner fields this step reads and rewrites. */
13
109
  export interface FixPathStep {
14
110
  package: string;
@@ -28,6 +124,27 @@ export interface FixPathStep {
28
124
  dir: string;
29
125
  command: string;
30
126
  }>;
127
+ /** lockfile_refresh: true when not every parent's requirement could be read and the verdict comes from semver compatibility. */
128
+ refreshInferred?: boolean;
129
+ /** upgrade_direct: the command that moves the declared dependency to targetVersion, per declaring project directory. */
130
+ upgradeCommands?: Array<{
131
+ dir: string;
132
+ command: string;
133
+ refresh?: true;
134
+ }>;
135
+ /** upgrade_direct: a manifest edit where no single verified command makes it. */
136
+ upgradeInstructions?: Array<{
137
+ dir: string;
138
+ instruction: string;
139
+ }>;
140
+ /** upgrade_parent / override_or_replace: the releases of the blocking parents that admit a clean version. */
141
+ parentUpgrades?: ParentUpgrade[];
142
+ /**
143
+ * Releases the registry withdrew (yanked, deprecated) that the target steps over; with no
144
+ * target, the only releases that fixed it (FX-H5). The reasons say which and why.
145
+ */
146
+ withheld?: string[];
147
+ isDev?: boolean;
31
148
  }
32
149
  export interface FixPathInput {
33
150
  step: FixPathStep;
@@ -35,6 +152,48 @@ export interface FixPathInput {
35
152
  fixes: string[];
36
153
  /** Manifest directories that pin this copy (the scan's sourceDirs). */
37
154
  sourceDirs: string[];
155
+ /** Advisories (one per alias group) that name no fixed version: `id` to print, `ids` every id of the group (M2). */
156
+ unfixed?: Array<{
157
+ id: string;
158
+ ids: string[];
159
+ }>;
38
160
  }
161
+ type Intel = Pick<LiveIntelligence, "getFixPathSources" | "lockfileFor" | "getProjectRoot" | "getResolvedDeps"> & Partial<Pick<LiveIntelligence, "queryAdvisories">>;
39
162
  /** Verify and rewrite the steps in place. Returns how many could not be verified (offline, failure, budget). */
40
- export declare function verifyFixPaths(inputs: FixPathInput[], liveIntel: Pick<LiveIntelligence, "getFixPathSources" | "lockfileFor" | "getProjectRoot">, budgetMs?: number): Promise<number>;
163
+ export declare function verifyFixPaths(inputs: FixPathInput[], liveIntel: Intel, budgetMs?: number): Promise<number>;
164
+ /**
165
+ * Each blocking parent of a planned step, in the app's ParentStanding terms: a release (the parent
166
+ * upgrade), or deprecated / latest / none_admits / unread / undecided / malicious.
167
+ */
168
+ export declare function parentOutcomes(step: FixPathStep): Array<{
169
+ parent: string;
170
+ from: string;
171
+ outcome: string;
172
+ upgrade?: ParentUpgrade;
173
+ }>;
174
+ /** The 4DA app's ParentStanding names, for what a parent cannot do (parent_release vectors). */
175
+ export type StuckOutcome = "deprecated" | "latest" | "none_admits" | "introduces";
176
+ /**
177
+ * One release per dependency across ALL rows (final release audit of 6.2.5, finding 4; readiness
178
+ * review 2026-10-17, finding 2). NodeGoat named express-session 1.18.1 on the cookie row, 1.18.2 on
179
+ * the on-headers row and 1.19.0 on its own row (and 1.18.1 still requires on-headers ~1.0.2,
180
+ * which is vulnerable); xsv named crossbeam-channel 0.4.4 on its own row and 0.5.0 as a lever.
181
+ *
182
+ * Per declared dependency, the release is the highest of what its rows need (each lever's
183
+ * release, its own row's target) that
184
+ * - crosses no major the installed version is not already crossed by a row's only fix (a row
185
+ * whose only fix is across a major makes the release breaking, and it is flagged);
186
+ * - no advisory of the dependency affects (the predicate dependency_check decides by);
187
+ * - still brings in every lever's fix (each lever's own check at that release).
188
+ * Every row that names the dependency then gives that release and that command, and everything
189
+ * derived from the release is recomputed at it (the parent's requirement and resolvesTo, the
190
+ * refresh that follows or not: 7b). A dependency for which no release passes is left as it is,
191
+ * and its rows say why.
192
+ *
193
+ * The advisory lookups are bounded (RECONCILE_BUDGET_MS); a release that could not be checked in
194
+ * time is not chosen, and nothing is written after the budget runs out.
195
+ */
196
+ export declare function reconcileDirectTargets(steps: FixPathStep[], inputs: FixPathInput[], liveIntel: Intel, budgetMs?: number): Promise<void>;
197
+ /** How to force one transitive version, per lockfile format. */
198
+ export declare function overrideHint(format: LockfileFormat, name: string, target: string): string;
199
+ export {};
@@ -25,6 +25,8 @@
25
25
  import type { FourDADatabase } from "../db.js";
26
26
  import type { LiveIntelligence } from "../live/index.js";
27
27
  import { type AppPlanResult } from "./app-plan.js";
28
+ import { type ParentUpgrade } from "./upgrade-planner-fixpaths.js";
29
+ import type { VulnerabilityCoverage } from "../live/types.js";
28
30
  export interface UpgradePlannerParams {
29
31
  include_dev?: boolean;
30
32
  max_recommendations?: number;
@@ -57,14 +59,39 @@ interface UpgradeRecommendation {
57
59
  /**
58
60
  * Direct deps you can bump yourself; `lockfile_refresh` = transitive, and
59
61
  * every parent's declared requirement already admits a clean version, so
60
- * `refreshCommands` fixes it; `waiting_on_upstream` = transitive, and a
61
- * parent's requirement excludes every clean version (or could not be
62
- * read); `reinstall` = the lockfile's version is fine
62
+ * `refreshCommands` fixes it (`refreshInferred` when that is inferred from
63
+ * semver because a requirement could not be read); `waiting_on_upstream` =
64
+ * transitive, and a parent's requirement excludes every clean version (or
65
+ * none could be read and the target leaves the semver-compatible range);
66
+ * `reinstall` = the lockfile's version is fine
63
67
  * but node_modules holds another one, so the fix is `installFix`;
68
+ * `upgrade_parent` = transitive, a parent's requirement excludes every clean
69
+ * version and a newer release of that parent exists: `parentUpgrades` names
70
+ * the smallest release whose own requirement admits a clean version (read
71
+ * from the registry), through the direct dependency it arrives by when the
72
+ * parent is transitive, or the parent's newest release to check when its
73
+ * requirements could not be read;
74
+ * `override_or_replace` = transitive, a parent's requirement excludes every
75
+ * clean version and that parent will not move (deprecated, already at its
76
+ * latest release, or no release up to the latest admits one): force the version (npm `overrides`, pnpm
77
+ * `pnpm.overrides`, yarn `resolutions`, Cargo `[patch]`) or replace the parent;
64
78
  * `no_fix_available` = vulnerable with no fixed release to move to;
65
- * `not_built_on_this_host` = every advisory is against code this host never compiles.
79
+ * `not_built_on_this_host` = every advisory is against code this host never compiles;
80
+ * `upgrade_parent_unverified` = transitive, a parent release that admits a clean version exists,
81
+ * but no release of the declared dependency that brings it in was verified (every lever's `to`
82
+ * is null): `parentUpgrades` says what to check; nothing is counted as fixed;
83
+ * `remove_dependency` = the installed copy is flagged malicious and no release is clean of
84
+ * it (crossenv: 1.0.0 and 1.0.1 are malware), or the installed version is a placeholder and every
85
+ * newer release is known-affected: remove or replace the dependency. A malware record with a
86
+ * clean later release (fsevents 1.2.9, MAL-2023-462, clean from 1.2.11) is an upgrade instead;
87
+ * `hold_current` = no advisory affects the installed version, and every newer release is
88
+ * known-affected or withdrawn: stay on it (the reasons say why).
89
+ * Every release a row offers, the newest one included, is checked against the package's
90
+ * advisories and the registry's withdrawals, as dependency_check checks one.
66
91
  */
67
- action: "upgrade_direct" | "lockfile_refresh" | "waiting_on_upstream" | "reinstall" | "no_fix_available" | "not_built_on_this_host";
92
+ action: "upgrade_direct" | "lockfile_refresh" | "upgrade_parent" | "upgrade_parent_unverified" | "waiting_on_upstream" | "override_or_replace" | "reinstall" | "no_fix_available" | "not_built_on_this_host" | "remove_dependency" | "hold_current";
93
+ /** upgrade_parent / override_or_replace: the parent release that admits a clean version, per blocking parent that can move. */
94
+ parentUpgrades?: ParentUpgrade[];
68
95
  /**
69
96
  * For a vulnerable package: whether the target was checked against EVERY
70
97
  * known advisory of the package (`all_advisories`), or only against the
@@ -73,11 +100,43 @@ interface UpgradeRecommendation {
73
100
  * affected by an advisory the installed version predates).
74
101
  */
75
102
  fixPathChecked?: "all_advisories" | "installed_advisories_only";
103
+ /**
104
+ * Releases the registry withdrew (crates.io/PyPI yanked; npm deprecated with a later clean
105
+ * release on its line) that targetVersion steps over, or, with no target, the only releases
106
+ * that fixed it: "no installable fix", not "no fix published". The reasons say why (FX-H5).
107
+ */
108
+ withheld?: string[];
76
109
  /** lockfile_refresh: the command to run, per lockfile directory. */
77
110
  refreshCommands?: Array<{
78
111
  dir: string;
79
112
  command: string;
80
113
  }>;
114
+ /**
115
+ * upgrade_direct: the command that moves the declared dependency to
116
+ * targetVersion where it is declared, per lockfile directory (npm install,
117
+ * pnpm up, yarn upgrade / yarn up, cargo add with --dev for a
118
+ * dev-dependency). Absent for tools no command is named for.
119
+ */
120
+ upgradeCommands?: Array<{
121
+ dir: string;
122
+ command: string;
123
+ refresh?: true;
124
+ }>;
125
+ /**
126
+ * upgrade_direct: a manifest edit where no single verified command makes it (a workspace-inherited
127
+ * crate, a requirements.txt pin). Also on a PyPI row, direct or transitive, whose pin sits in a
128
+ * compiled requirements file: the re-compile with `--upgrade-package` (audit 6.2.5 A-4).
129
+ */
130
+ upgradeInstructions?: Array<{
131
+ dir: string;
132
+ instruction: string;
133
+ }>;
134
+ /**
135
+ * lockfile_refresh: true when not every parent's requirement could be read
136
+ * (none read excludes the target) and the target is semver-compatible with
137
+ * the installed version, so the refresh is inferred rather than read.
138
+ */
139
+ refreshInferred?: boolean;
81
140
  /** False when the dependency is gated behind a target spec not active on this host (label, never suppressed). */
82
141
  platformActive: boolean;
83
142
  /** npm: the version node_modules holds, when it differs from `currentVersion` (the lockfile's). */
@@ -87,7 +146,7 @@ interface UpgradeRecommendation {
87
146
  /** No lockfile: the manifest's declared requirement; `currentVersion` is its floor, not an install. */
88
147
  declaredRange?: string;
89
148
  }
90
- interface UpgradePlanResult {
149
+ export interface UpgradePlanResult {
91
150
  generatedAt: string;
92
151
  projectPath: string;
93
152
  totalDeps: number;
@@ -98,8 +157,12 @@ interface UpgradePlanResult {
98
157
  waitingOnUpstream: number;
99
158
  /** Transitive steps a lockfile refresh fixes (every parent already admits a clean version). */
100
159
  lockfileRefreshes: number;
101
- /** False when no vulnerability scan has run this session — the ranking is then CVE-blind and says so. */
160
+ /** Transitive steps fixed by upgrading the parent that pins them (`parentUpgrades` names the release). */
161
+ parentUpgradeSteps: number;
162
+ /** False unless OSV answered for EVERY dependency in scope: a plan over missing or partial data is not CVE-aware. */
102
163
  vulnerabilityDataAvailable: boolean;
164
+ /** Present when the vulnerability check is missing or incomplete: what was not checked and why. */
165
+ vulnerabilityCoverage?: VulnerabilityCoverage;
103
166
  provenance: {
104
167
  mode: "standalone_heuristic";
105
168
  note: string;