@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.
- package/CHANGELOG.md +484 -0
- package/README.md +22 -6
- package/dist/db.d.ts +38 -0
- package/dist/db.js +78 -23
- package/dist/doctor.js +6 -3
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.js +16 -14
- package/dist/index.js +73 -103
- package/dist/live/advisory-groups.d.ts +68 -0
- package/dist/live/advisory-groups.js +131 -0
- package/dist/live/cache.js +11 -3
- package/dist/live/cargo-platform.d.ts +6 -0
- package/dist/live/cargo-platform.js +8 -2
- package/dist/live/changelog.js +38 -6
- package/dist/live/dependency-set.d.ts +9 -1
- package/dist/live/dependency-set.js +10 -2
- package/dist/live/fix-paths.d.ts +258 -9
- package/dist/live/fix-paths.js +790 -45
- package/dist/live/github-releases.js +3 -0
- package/dist/live/held-dependencies.d.ts +93 -0
- package/dist/live/held-dependencies.js +212 -0
- package/dist/live/held-targets.d.ts +30 -0
- package/dist/live/held-targets.js +107 -0
- package/dist/live/http-utils.js +13 -3
- package/dist/live/index.d.ts +7 -4
- package/dist/live/index.js +24 -19
- package/dist/live/install-state.js +6 -2
- package/dist/live/js-lockfile-readers.d.ts +8 -1
- package/dist/live/js-lockfile-readers.js +43 -6
- package/dist/live/lockfile-parents.d.ts +15 -0
- package/dist/live/lockfile-parents.js +85 -10
- package/dist/live/lockfile-parsers-pygo.d.ts +47 -2
- package/dist/live/lockfile-parsers-pygo.js +230 -12
- package/dist/live/lockfile-parsers-rbphp.d.ts +38 -0
- package/dist/live/lockfile-parsers-rbphp.js +123 -0
- package/dist/live/lockfile-parsers.d.ts +6 -0
- package/dist/live/lockfile-parsers.js +116 -4
- package/dist/live/lockfile-types.d.ts +28 -1
- package/dist/live/maintenance.d.ts +20 -4
- package/dist/live/maintenance.js +31 -2
- package/dist/live/malware.d.ts +20 -0
- package/dist/live/malware.js +31 -0
- package/dist/live/npm-packument.d.ts +4 -1
- package/dist/live/npm-packument.js +5 -2
- package/dist/live/offered-release.d.ts +53 -0
- package/dist/live/offered-release.js +117 -0
- package/dist/live/osv-ranges.d.ts +111 -0
- package/dist/live/osv-ranges.js +173 -14
- package/dist/live/osv-scanner.d.ts +87 -5
- package/dist/live/osv-scanner.js +310 -59
- package/dist/live/pinned-advisories.d.ts +2 -2
- package/dist/live/pinned-advisories.js +3 -3
- package/dist/live/pypi-advice.d.ts +36 -0
- package/dist/live/pypi-advice.js +158 -0
- package/dist/live/rate-limiter.d.ts +7 -0
- package/dist/live/rate-limiter.js +22 -1
- package/dist/live/release-metadata.d.ts +5 -0
- package/dist/live/resolution.d.ts +3 -0
- package/dist/live/resolution.js +6 -3
- package/dist/live/scan-coverage.d.ts +26 -0
- package/dist/live/scan-coverage.js +70 -0
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +26 -3
- package/dist/live/severity-scope.d.ts +26 -1
- package/dist/live/severity-scope.js +32 -0
- package/dist/live/shell-safe.d.ts +19 -0
- package/dist/live/shell-safe.js +36 -0
- package/dist/live/types.d.ts +134 -8
- package/dist/live/version-compare.d.ts +49 -0
- package/dist/live/version-compare.js +131 -0
- package/dist/live/withdrawn-releases.d.ts +50 -0
- package/dist/live/withdrawn-releases.js +125 -0
- package/dist/project-manifests.d.ts +7 -0
- package/dist/project-manifests.js +45 -5
- package/dist/project-scanner.js +6 -4
- package/dist/project-tree.d.ts +56 -0
- package/dist/project-tree.js +174 -5
- package/dist/schema-registry.d.ts +47 -4
- package/dist/schema-registry.js +76 -9
- package/dist/server-instructions.js +4 -2
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +21 -2
- package/dist/tool-dispatch.js +3 -3
- package/dist/tools/agent-memory.d.ts +5 -0
- package/dist/tools/agent-memory.js +9 -0
- package/dist/tools/briefing-task-scope.js +5 -2
- package/dist/tools/decision-memory.js +2 -0
- package/dist/tools/dependency-check-held.d.ts +44 -0
- package/dist/tools/dependency-check-held.js +98 -0
- package/dist/tools/dependency-check-signals.d.ts +19 -2
- package/dist/tools/dependency-check-signals.js +84 -13
- package/dist/tools/dependency-check-versions.d.ts +37 -4
- package/dist/tools/dependency-check-versions.js +48 -13
- package/dist/tools/dependency-check.d.ts +11 -1
- package/dist/tools/dependency-check.js +81 -29
- package/dist/tools/dependency-health.js +209 -45
- package/dist/tools/get-context.d.ts +3 -0
- package/dist/tools/get-context.js +4 -0
- package/dist/tools/project-scope.d.ts +8 -0
- package/dist/tools/project-scope.js +21 -0
- package/dist/tools/upgrade-impact-features.d.ts +37 -0
- package/dist/tools/upgrade-impact-features.js +176 -0
- package/dist/tools/upgrade-impact-literals.js +26 -4
- package/dist/tools/upgrade-impact-report.d.ts +5 -1
- package/dist/tools/upgrade-impact-report.js +26 -5
- package/dist/tools/upgrade-impact.js +10 -3
- package/dist/tools/upgrade-planner-fixpaths.d.ts +162 -5
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +59 -4
- package/dist/tools/upgrade-planner.js +237 -61
- package/dist/tools/vulnerability-scan-concise.d.ts +47 -2
- package/dist/tools/vulnerability-scan-concise.js +37 -3
- package/dist/tools/vulnerability-scan-format.d.ts +126 -24
- package/dist/tools/vulnerability-scan-format.js +193 -26
- package/dist/tools/vulnerability-scan.d.ts +224 -63
- package/dist/tools/vulnerability-scan.js +50 -15
- package/dist/tools/what-should-i-know.d.ts +6 -1
- package/dist/tools/what-should-i-know.js +72 -15
- package/package.json +2 -2
|
@@ -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
|
|
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(
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
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;
|
|
@@ -30,6 +126,25 @@ export interface FixPathStep {
|
|
|
30
126
|
}>;
|
|
31
127
|
/** lockfile_refresh: true when not every parent's requirement could be read and the verdict comes from semver compatibility. */
|
|
32
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;
|
|
33
148
|
}
|
|
34
149
|
export interface FixPathInput {
|
|
35
150
|
step: FixPathStep;
|
|
@@ -37,6 +152,48 @@ export interface FixPathInput {
|
|
|
37
152
|
fixes: string[];
|
|
38
153
|
/** Manifest directories that pin this copy (the scan's sourceDirs). */
|
|
39
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
|
+
}>;
|
|
40
160
|
}
|
|
161
|
+
type Intel = Pick<LiveIntelligence, "getFixPathSources" | "lockfileFor" | "getProjectRoot" | "getResolvedDeps"> & Partial<Pick<LiveIntelligence, "queryAdvisories">>;
|
|
41
162
|
/** Verify and rewrite the steps in place. Returns how many could not be verified (offline, failure, budget). */
|
|
42
|
-
export declare function verifyFixPaths(inputs: FixPathInput[], liveIntel:
|
|
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 {};
|
|
Binary file
|
|
@@ -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;
|
|
@@ -63,10 +65,33 @@ interface UpgradeRecommendation {
|
|
|
63
65
|
* none could be read and the target leaves the semver-compatible range);
|
|
64
66
|
* `reinstall` = the lockfile's version is fine
|
|
65
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;
|
|
66
78
|
* `no_fix_available` = vulnerable with no fixed release to move to;
|
|
67
|
-
* `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.
|
|
68
91
|
*/
|
|
69
|
-
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[];
|
|
70
95
|
/**
|
|
71
96
|
* For a vulnerable package: whether the target was checked against EVERY
|
|
72
97
|
* known advisory of the package (`all_advisories`), or only against the
|
|
@@ -75,11 +100,37 @@ interface UpgradeRecommendation {
|
|
|
75
100
|
* affected by an advisory the installed version predates).
|
|
76
101
|
*/
|
|
77
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[];
|
|
78
109
|
/** lockfile_refresh: the command to run, per lockfile directory. */
|
|
79
110
|
refreshCommands?: Array<{
|
|
80
111
|
dir: string;
|
|
81
112
|
command: string;
|
|
82
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
|
+
}>;
|
|
83
134
|
/**
|
|
84
135
|
* lockfile_refresh: true when not every parent's requirement could be read
|
|
85
136
|
* (none read excludes the target) and the target is semver-compatible with
|
|
@@ -95,7 +146,7 @@ interface UpgradeRecommendation {
|
|
|
95
146
|
/** No lockfile: the manifest's declared requirement; `currentVersion` is its floor, not an install. */
|
|
96
147
|
declaredRange?: string;
|
|
97
148
|
}
|
|
98
|
-
interface UpgradePlanResult {
|
|
149
|
+
export interface UpgradePlanResult {
|
|
99
150
|
generatedAt: string;
|
|
100
151
|
projectPath: string;
|
|
101
152
|
totalDeps: number;
|
|
@@ -106,8 +157,12 @@ interface UpgradePlanResult {
|
|
|
106
157
|
waitingOnUpstream: number;
|
|
107
158
|
/** Transitive steps a lockfile refresh fixes (every parent already admits a clean version). */
|
|
108
159
|
lockfileRefreshes: number;
|
|
109
|
-
/**
|
|
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. */
|
|
110
163
|
vulnerabilityDataAvailable: boolean;
|
|
164
|
+
/** Present when the vulnerability check is missing or incomplete: what was not checked and why. */
|
|
165
|
+
vulnerabilityCoverage?: VulnerabilityCoverage;
|
|
111
166
|
provenance: {
|
|
112
167
|
mode: "standalone_heuristic";
|
|
113
168
|
note: string;
|