@4da/mcp-server 5.0.3 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +667 -0
- package/README.md +66 -35
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -2
- package/dist/db.js +242 -36
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +31 -1
- package/dist/live/osv-scanner.js +119 -10
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -1
- package/package.json +27 -30
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Dependency resolution that knows when it has gone stale.
|
|
4
|
+
*
|
|
5
|
+
* Measured 2026-09-10: three `node ./mcp-4da-server/dist/index.js` processes
|
|
6
|
+
* were live and two had started before the pull that brought hono 4.13.5.
|
|
7
|
+
* `vulnerability_scan` from one of them reported hono 4.13.3 (the lockfile
|
|
8
|
+
* value when it started) with `_meta.cached: false`. The live layer resolved
|
|
9
|
+
* versions once at init and never again, and `cached` described only the OSV
|
|
10
|
+
* lookup, so nothing in the answer could reveal that it was stale.
|
|
11
|
+
*
|
|
12
|
+
* A group records every file its resolution depended on as a stat signature:
|
|
13
|
+
* each lockfile candidate (present or absent, so one appearing or vanishing
|
|
14
|
+
* counts), the manifest when the resolver fell back to it, and for npm the
|
|
15
|
+
* node_modules install state. Signatures are taken BEFORE reading, so a write
|
|
16
|
+
* that lands mid-read still registers as a change. `resolveGroup` depends
|
|
17
|
+
* only on its inputs and the filesystem, so the SAME resolution can be re-run
|
|
18
|
+
* when a signature moves. Stat calls only: no timers, no watchers.
|
|
19
|
+
*/
|
|
20
|
+
import { anySignatureChanged, signatureMap, signatureMtime } from "./file-signature.js";
|
|
21
|
+
import { checkInstallState } from "./install-state.js";
|
|
22
|
+
import { lockfileCandidates, manifestCandidate, resolveVersionSource } from "./lockfile-parsers.js";
|
|
23
|
+
import { mapEcosystem, resolveAuditVersionsFrom, resolveVersionsFrom } from "./version-resolver.js";
|
|
24
|
+
export function resolveGroup(group) {
|
|
25
|
+
const resolvedAt = new Date().toISOString();
|
|
26
|
+
const ecosystem = mapEcosystem(group.language);
|
|
27
|
+
const manifest = manifestCandidate(group.dir, ecosystem);
|
|
28
|
+
const signatures = signatureMap([
|
|
29
|
+
...lockfileCandidates(group.dir, ecosystem),
|
|
30
|
+
...(manifest ? [manifest] : []),
|
|
31
|
+
]);
|
|
32
|
+
const read = resolveVersionSource(group.dir, ecosystem);
|
|
33
|
+
// The manifest only matters when the resolver fell back to it; editing a
|
|
34
|
+
// script in package.json must not throw away a scan of an unchanged lockfile.
|
|
35
|
+
if (manifest && read.kind === "lockfile")
|
|
36
|
+
signatures.delete(manifest);
|
|
37
|
+
let resolved = resolveVersionsFrom(group.dir, group.deps, group.devDeps, ecosystem, read.versions, group.targets);
|
|
38
|
+
let drift = [];
|
|
39
|
+
let driftAudit = [];
|
|
40
|
+
// Only a lockfile pins exact versions; comparing node_modules against a
|
|
41
|
+
// manifest's specifier floor would report drift that is not there.
|
|
42
|
+
if (ecosystem === "npm" && read.kind === "lockfile" && read.source) {
|
|
43
|
+
const install = checkInstallState(group.dir, resolved, read.source);
|
|
44
|
+
resolved = install.resolved;
|
|
45
|
+
drift = install.drift;
|
|
46
|
+
driftAudit = install.driftAuditDeps;
|
|
47
|
+
for (const [file, signature] of install.signatures)
|
|
48
|
+
signatures.set(file, signature);
|
|
49
|
+
}
|
|
50
|
+
const audit = [
|
|
51
|
+
...resolveAuditVersionsFrom(group.dir, group.deps, group.devDeps, ecosystem, read.versions, group.targets, resolved, read.instances),
|
|
52
|
+
...driftAudit,
|
|
53
|
+
];
|
|
54
|
+
const source = read.source && read.kind
|
|
55
|
+
? { path: read.source, kind: read.kind, mtimeMs: signatureMtime(signatures.get(read.source)) }
|
|
56
|
+
: null;
|
|
57
|
+
return { group, resolved, audit, drift, source, signatures, resolvedAt };
|
|
58
|
+
}
|
|
59
|
+
/** True when any file the resolution depended on changed, appeared, or vanished. */
|
|
60
|
+
export function groupIsStale(result) {
|
|
61
|
+
return anySignatureChanged(result.signatures);
|
|
62
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic-version PRECEDENCE (semver.org section 11) for advisory range checks.
|
|
3
|
+
*
|
|
4
|
+
* `compareSemver` compares MAJOR.MINOR.PATCH only. That is the right tool for
|
|
5
|
+
* picking the highest of several fix versions and the wrong one for deciding
|
|
6
|
+
* whether an install sits inside an advisory range: it reads `5.0.0-rc.1` and
|
|
7
|
+
* `5.0.0-rc.2` as EQUAL, so an install at 5.0.0-rc.1 was judged outside
|
|
8
|
+
* `[5.0.0-beta.1, 5.0.0-rc.2)` — a range GHSA-82fw-gwwq-j7x9 really stores for
|
|
9
|
+
* vitest. Range checks use this comparator; `compareSemver` keeps its
|
|
10
|
+
* behaviour for its other callers.
|
|
11
|
+
*
|
|
12
|
+
* Parsing follows the Rust reference (`src-tauri/src/osv/matching.rs`
|
|
13
|
+
* `parse_version`, built on the `semver` crate): a leading `v` is dropped,
|
|
14
|
+
* `MAJOR.MINOR` reads as `MAJOR.MINOR.0`, and anything else that is not
|
|
15
|
+
* strict semver is unreadable (null) rather than guessed at.
|
|
16
|
+
*/
|
|
17
|
+
/** A parsed version, ordered by semver precedence. */
|
|
18
|
+
export interface SemverPrecedence {
|
|
19
|
+
major: number;
|
|
20
|
+
minor: number;
|
|
21
|
+
patch: number;
|
|
22
|
+
/** Dot-separated prerelease identifiers; empty for a release. */
|
|
23
|
+
prerelease: string[];
|
|
24
|
+
}
|
|
25
|
+
/** Parse a version for precedence comparison, or null when it is not readable semver. */
|
|
26
|
+
export declare function parseSemverPrecedence(version: string): SemverPrecedence | null;
|
|
27
|
+
/** Semver precedence: 1 if a > b, -1 if a < b, 0 if equal. Build metadata is ignored. */
|
|
28
|
+
export declare function comparePrecedence(a: SemverPrecedence, b: SemverPrecedence): number;
|
|
29
|
+
/** Precedence of two version strings, or null when either is unreadable. */
|
|
30
|
+
export declare function compareVersionPrecedence(a: string, b: string): number | null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Semantic-version PRECEDENCE (semver.org section 11) for advisory range checks.
|
|
4
|
+
*
|
|
5
|
+
* `compareSemver` compares MAJOR.MINOR.PATCH only. That is the right tool for
|
|
6
|
+
* picking the highest of several fix versions and the wrong one for deciding
|
|
7
|
+
* whether an install sits inside an advisory range: it reads `5.0.0-rc.1` and
|
|
8
|
+
* `5.0.0-rc.2` as EQUAL, so an install at 5.0.0-rc.1 was judged outside
|
|
9
|
+
* `[5.0.0-beta.1, 5.0.0-rc.2)` — a range GHSA-82fw-gwwq-j7x9 really stores for
|
|
10
|
+
* vitest. Range checks use this comparator; `compareSemver` keeps its
|
|
11
|
+
* behaviour for its other callers.
|
|
12
|
+
*
|
|
13
|
+
* Parsing follows the Rust reference (`src-tauri/src/osv/matching.rs`
|
|
14
|
+
* `parse_version`, built on the `semver` crate): a leading `v` is dropped,
|
|
15
|
+
* `MAJOR.MINOR` reads as `MAJOR.MINOR.0`, and anything else that is not
|
|
16
|
+
* strict semver is unreadable (null) rather than guessed at.
|
|
17
|
+
*/
|
|
18
|
+
const IDENT = "[0-9A-Za-z-]+";
|
|
19
|
+
const STRICT = new RegExp(`^(\\d+)\\.(\\d+)\\.(\\d+)(?:-(${IDENT}(?:\\.${IDENT})*))?(?:\\+${IDENT}(?:\\.${IDENT})*)?$`);
|
|
20
|
+
const TWO_PART = /^(\d+)\.(\d+)$/;
|
|
21
|
+
/** Parse a version for precedence comparison, or null when it is not readable semver. */
|
|
22
|
+
export function parseSemverPrecedence(version) {
|
|
23
|
+
const v = version.trim().replace(/^v+/, "");
|
|
24
|
+
const strict = STRICT.exec(v);
|
|
25
|
+
if (strict) {
|
|
26
|
+
return {
|
|
27
|
+
major: Number(strict[1]),
|
|
28
|
+
minor: Number(strict[2]),
|
|
29
|
+
patch: Number(strict[3]),
|
|
30
|
+
prerelease: strict[4] ? strict[4].split(".") : [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const two = TWO_PART.exec(v);
|
|
34
|
+
return two ? { major: Number(two[1]), minor: Number(two[2]), patch: 0, prerelease: [] } : null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Prerelease identifiers: numeric ones compare numerically and rank below
|
|
38
|
+
* alphanumeric ones; alphanumeric ones compare in ASCII order.
|
|
39
|
+
*/
|
|
40
|
+
function compareIdentifier(a, b) {
|
|
41
|
+
const aNumeric = /^\d+$/.test(a);
|
|
42
|
+
const bNumeric = /^\d+$/.test(b);
|
|
43
|
+
if (aNumeric && bNumeric) {
|
|
44
|
+
// Compared as digit strings: an identifier may exceed Number's safe range.
|
|
45
|
+
const x = a.replace(/^0+(?=\d)/, "");
|
|
46
|
+
const y = b.replace(/^0+(?=\d)/, "");
|
|
47
|
+
if (x.length !== y.length)
|
|
48
|
+
return x.length > y.length ? 1 : -1;
|
|
49
|
+
return x === y ? 0 : x > y ? 1 : -1;
|
|
50
|
+
}
|
|
51
|
+
if (aNumeric)
|
|
52
|
+
return -1;
|
|
53
|
+
if (bNumeric)
|
|
54
|
+
return 1;
|
|
55
|
+
return a === b ? 0 : a > b ? 1 : -1;
|
|
56
|
+
}
|
|
57
|
+
/** Semver precedence: 1 if a > b, -1 if a < b, 0 if equal. Build metadata is ignored. */
|
|
58
|
+
export function comparePrecedence(a, b) {
|
|
59
|
+
if (a.major !== b.major)
|
|
60
|
+
return a.major > b.major ? 1 : -1;
|
|
61
|
+
if (a.minor !== b.minor)
|
|
62
|
+
return a.minor > b.minor ? 1 : -1;
|
|
63
|
+
if (a.patch !== b.patch)
|
|
64
|
+
return a.patch > b.patch ? 1 : -1;
|
|
65
|
+
// A release outranks every prerelease of the same MAJOR.MINOR.PATCH.
|
|
66
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
67
|
+
return Math.sign(b.prerelease.length - a.prerelease.length);
|
|
68
|
+
}
|
|
69
|
+
const shared = Math.min(a.prerelease.length, b.prerelease.length);
|
|
70
|
+
for (let i = 0; i < shared; i++) {
|
|
71
|
+
const cmp = compareIdentifier(a.prerelease[i], b.prerelease[i]);
|
|
72
|
+
if (cmp !== 0)
|
|
73
|
+
return cmp;
|
|
74
|
+
}
|
|
75
|
+
// A longer identifier list outranks its own prefix (1.0.0-alpha < 1.0.0-alpha.1).
|
|
76
|
+
return Math.sign(a.prerelease.length - b.prerelease.length);
|
|
77
|
+
}
|
|
78
|
+
/** Precedence of two version strings, or null when either is unreadable. */
|
|
79
|
+
export function compareVersionPrecedence(a, b) {
|
|
80
|
+
const pa = parseSemverPrecedence(a);
|
|
81
|
+
const pb = parseSemverPrecedence(b);
|
|
82
|
+
return pa && pb ? comparePrecedence(pa, pb) : null;
|
|
83
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One severity rule for every surface (AD-046).
|
|
3
|
+
*
|
|
4
|
+
* The desktop app grades an advisory by where the vulnerable package sits in
|
|
5
|
+
* the dependency graph, and every 4DA surface presents that one grade. This
|
|
6
|
+
* module is the MCP server's copy of the rule. Its Rust twin is
|
|
7
|
+
* `osv::identity::scope_adjusted_urgency` (src-tauri/src/osv/identity.rs):
|
|
8
|
+
* change one, change both, or the MCP server and the app disagree about the
|
|
9
|
+
* same advisory again.
|
|
10
|
+
*
|
|
11
|
+
* Measured 2026-09-10: `vulnerability_scan` graded `sandbox@3.1.2` (a
|
|
12
|
+
* transitive of paddle-webhook, dev/runtime scope unknown) CRITICAL
|
|
13
|
+
* (`by_severity.critical: 1`, and the briefing read "CRITICAL: Sandbox
|
|
14
|
+
* Breakout") while the app graded the same advisory High.
|
|
15
|
+
*
|
|
16
|
+
* The rule, applied in this order:
|
|
17
|
+
* (a) transitive-only clamps critical to high;
|
|
18
|
+
* (b) dev-only drops ONE level: critical to high, high to medium, medium to
|
|
19
|
+
* low; low stays low.
|
|
20
|
+
* `unknown` stays `unknown` through both steps. Unknown dev scope is not dev:
|
|
21
|
+
* no discount without evidence. The advisory's own tier is never discarded;
|
|
22
|
+
* surfaces carry it beside the presented grade as `advisory_severity`.
|
|
23
|
+
*/
|
|
24
|
+
import type { VulnerabilityEntry } from "./types.js";
|
|
25
|
+
export type SeverityTier = VulnerabilityEntry["severity"];
|
|
26
|
+
export interface DependencyScope {
|
|
27
|
+
isDirect: boolean;
|
|
28
|
+
isDev: boolean;
|
|
29
|
+
devScopeKnown: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Ordering for sorts and "at or above" filters; unknown ranks lowest. */
|
|
32
|
+
export declare const SEVERITY_RANK: Record<SeverityTier, number>;
|
|
33
|
+
/**
|
|
34
|
+
* THE rule. Twin of the Rust `osv::identity::scope_adjusted_urgency`.
|
|
35
|
+
*
|
|
36
|
+
* direct runtime critical -> critical; transitive runtime critical -> high;
|
|
37
|
+
* direct dev critical -> high; transitive dev critical -> medium;
|
|
38
|
+
* direct dev high -> medium; transitive dev high -> medium;
|
|
39
|
+
* transitive runtime high -> high.
|
|
40
|
+
*/
|
|
41
|
+
export declare function scopeAdjustedSeverity(advisory: SeverityTier, scope: DependencyScope): SeverityTier;
|
|
42
|
+
type ScopedEntry = Pick<VulnerabilityEntry, "severity" | "isDirect" | "isDev" | "devScopeKnown">;
|
|
43
|
+
/**
|
|
44
|
+
* The grade a surface presents for one scan entry. A missing `isDirect`
|
|
45
|
+
* (never produced today; defensive for foreign cache rows) is treated as
|
|
46
|
+
* direct, and a missing `devScopeKnown` as unknown: both mean no discount.
|
|
47
|
+
*/
|
|
48
|
+
export declare function presentedSeverity(v: ScopedEntry): SeverityTier;
|
|
49
|
+
/**
|
|
50
|
+
* Why the presented grade differs from the advisory's own tier, in words, or
|
|
51
|
+
* null when it does not differ.
|
|
52
|
+
*/
|
|
53
|
+
export declare function scopeAdjustmentReason(v: ScopedEntry): string | null;
|
|
54
|
+
export type SeverityCounts = Record<SeverityTier, number>;
|
|
55
|
+
/** Count entries per tier, using whichever tier function the caller names. */
|
|
56
|
+
export declare function countBySeverity<T>(entries: T[], tierOf: (entry: T) => SeverityTier): SeverityCounts;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* One severity rule for every surface (AD-046).
|
|
4
|
+
*
|
|
5
|
+
* The desktop app grades an advisory by where the vulnerable package sits in
|
|
6
|
+
* the dependency graph, and every 4DA surface presents that one grade. This
|
|
7
|
+
* module is the MCP server's copy of the rule. Its Rust twin is
|
|
8
|
+
* `osv::identity::scope_adjusted_urgency` (src-tauri/src/osv/identity.rs):
|
|
9
|
+
* change one, change both, or the MCP server and the app disagree about the
|
|
10
|
+
* same advisory again.
|
|
11
|
+
*
|
|
12
|
+
* Measured 2026-09-10: `vulnerability_scan` graded `sandbox@3.1.2` (a
|
|
13
|
+
* transitive of paddle-webhook, dev/runtime scope unknown) CRITICAL
|
|
14
|
+
* (`by_severity.critical: 1`, and the briefing read "CRITICAL: Sandbox
|
|
15
|
+
* Breakout") while the app graded the same advisory High.
|
|
16
|
+
*
|
|
17
|
+
* The rule, applied in this order:
|
|
18
|
+
* (a) transitive-only clamps critical to high;
|
|
19
|
+
* (b) dev-only drops ONE level: critical to high, high to medium, medium to
|
|
20
|
+
* low; low stays low.
|
|
21
|
+
* `unknown` stays `unknown` through both steps. Unknown dev scope is not dev:
|
|
22
|
+
* no discount without evidence. The advisory's own tier is never discarded;
|
|
23
|
+
* surfaces carry it beside the presented grade as `advisory_severity`.
|
|
24
|
+
*/
|
|
25
|
+
const ONE_LEVEL_DOWN = {
|
|
26
|
+
critical: "high",
|
|
27
|
+
high: "medium",
|
|
28
|
+
medium: "low",
|
|
29
|
+
low: "low",
|
|
30
|
+
unknown: "unknown",
|
|
31
|
+
};
|
|
32
|
+
/** Ordering for sorts and "at or above" filters; unknown ranks lowest. */
|
|
33
|
+
export const SEVERITY_RANK = {
|
|
34
|
+
critical: 4,
|
|
35
|
+
high: 3,
|
|
36
|
+
medium: 2,
|
|
37
|
+
low: 1,
|
|
38
|
+
unknown: 0,
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* THE rule. Twin of the Rust `osv::identity::scope_adjusted_urgency`.
|
|
42
|
+
*
|
|
43
|
+
* direct runtime critical -> critical; transitive runtime critical -> high;
|
|
44
|
+
* direct dev critical -> high; transitive dev critical -> medium;
|
|
45
|
+
* direct dev high -> medium; transitive dev high -> medium;
|
|
46
|
+
* transitive runtime high -> high.
|
|
47
|
+
*/
|
|
48
|
+
export function scopeAdjustedSeverity(advisory, scope) {
|
|
49
|
+
let tier = advisory;
|
|
50
|
+
if (!scope.isDirect && tier === "critical")
|
|
51
|
+
tier = "high";
|
|
52
|
+
if (scope.devScopeKnown && scope.isDev)
|
|
53
|
+
tier = ONE_LEVEL_DOWN[tier];
|
|
54
|
+
return tier;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The grade a surface presents for one scan entry. A missing `isDirect`
|
|
58
|
+
* (never produced today; defensive for foreign cache rows) is treated as
|
|
59
|
+
* direct, and a missing `devScopeKnown` as unknown: both mean no discount.
|
|
60
|
+
*/
|
|
61
|
+
export function presentedSeverity(v) {
|
|
62
|
+
return scopeAdjustedSeverity(v.severity, {
|
|
63
|
+
isDirect: v.isDirect !== false,
|
|
64
|
+
isDev: v.isDev === true,
|
|
65
|
+
devScopeKnown: v.devScopeKnown === true,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Why the presented grade differs from the advisory's own tier, in words, or
|
|
70
|
+
* null when it does not differ.
|
|
71
|
+
*/
|
|
72
|
+
export function scopeAdjustmentReason(v) {
|
|
73
|
+
if (presentedSeverity(v) === v.severity)
|
|
74
|
+
return null;
|
|
75
|
+
const transitive = v.isDirect === false;
|
|
76
|
+
const dev = v.devScopeKnown === true && v.isDev === true;
|
|
77
|
+
if (transitive && dev)
|
|
78
|
+
return "transitive, dev-only dependency";
|
|
79
|
+
if (dev)
|
|
80
|
+
return "dev-only dependency";
|
|
81
|
+
return "transitive-only dependency";
|
|
82
|
+
}
|
|
83
|
+
/** Count entries per tier, using whichever tier function the caller names. */
|
|
84
|
+
export function countBySeverity(entries, tierOf) {
|
|
85
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 };
|
|
86
|
+
for (const entry of entries)
|
|
87
|
+
counts[tierOf(entry)]++;
|
|
88
|
+
return counts;
|
|
89
|
+
}
|
package/dist/live/types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type OsvEcosystem = "npm" | "crates.io" | "PyPI" | "Go" | "Maven" | "NuGet" | "RubyGems" | "Packagist" | "Pub";
|
|
2
|
+
/** The reinstall command that brings node_modules back in line with a directory's lockfile. */
|
|
3
|
+
export type InstallFixCommand = "pnpm install" | "npm ci" | "yarn install" | "bun install";
|
|
2
4
|
export interface ResolvedDependency {
|
|
3
5
|
name: string;
|
|
4
6
|
version: string | null;
|
|
@@ -10,6 +12,56 @@ export interface ResolvedDependency {
|
|
|
10
12
|
target: string | null;
|
|
11
13
|
/** False when `target` is not active on the host platform (advisory is not relevant). */
|
|
12
14
|
platformActive: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Manifest directories this exact (ecosystem, name, version) resolved from.
|
|
17
|
+
*
|
|
18
|
+
* A scan root can span several independent workspaces — `src-tauri/`,
|
|
19
|
+
* `relay/`, `victauri-gauntlet/` all sit under one repo and pin their own
|
|
20
|
+
* versions. Without this, dedupe collapsed them and the report named a
|
|
21
|
+
* vulnerable version with no way to say WHICH workspace pinned it, so a
|
|
22
|
+
* patched primary crate looked vulnerable because a sibling lagged.
|
|
23
|
+
*/
|
|
24
|
+
sourceDirs: string[];
|
|
25
|
+
/**
|
|
26
|
+
* npm DIRECT deps resolved from a lockfile only: the version node_modules
|
|
27
|
+
* actually holds (walking up for hoisted workspaces), or null when it is not
|
|
28
|
+
* installed or unreadable. Undefined when the check did not run (another
|
|
29
|
+
* ecosystem, no node_modules, or versions read from a manifest).
|
|
30
|
+
*/
|
|
31
|
+
installedVersion?: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Set on the extra audit entry that asks OSV about an INSTALLED version the
|
|
34
|
+
* lockfile does not pin: the lockfile's version of the same package. The
|
|
35
|
+
* entry exists so `vulnerable_installed` is OSV's answer, not a guess.
|
|
36
|
+
*/
|
|
37
|
+
installDriftOf?: string;
|
|
38
|
+
/** On drift entries: the command that reinstalls from the lockfile. */
|
|
39
|
+
installFix?: InstallFixCommand;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One npm direct dependency whose installed copy differs from its lockfile.
|
|
43
|
+
* `dir` is the manifest directory, spelled as the resolution group spells it.
|
|
44
|
+
*/
|
|
45
|
+
export interface InstallDriftRecord {
|
|
46
|
+
package: string;
|
|
47
|
+
dir: string;
|
|
48
|
+
lockfileVersion: string;
|
|
49
|
+
installedVersion: string;
|
|
50
|
+
fix: InstallFixCommand;
|
|
51
|
+
isDev: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** A file one resolution group read its versions from. */
|
|
54
|
+
export interface ResolutionSourceRecord {
|
|
55
|
+
/** Absolute path. */
|
|
56
|
+
path: string;
|
|
57
|
+
kind: "lockfile" | "manifest";
|
|
58
|
+
/** Modification time when it was read, or null if it vanished since. */
|
|
59
|
+
mtimeMs: number | null;
|
|
60
|
+
}
|
|
61
|
+
/** When the dependency set in use was assembled, and from which files. */
|
|
62
|
+
export interface ResolutionProvenance {
|
|
63
|
+
resolvedAt: string;
|
|
64
|
+
sources: ResolutionSourceRecord[];
|
|
13
65
|
}
|
|
14
66
|
/**
|
|
15
67
|
* An OSV record. The `/v1/querybatch` endpoint returns only `{ id, modified }`
|
|
@@ -71,6 +123,19 @@ export interface VulnerabilityEntry {
|
|
|
71
123
|
target: string | null;
|
|
72
124
|
/** False when the affected dep is not active on the host platform. */
|
|
73
125
|
platformActive: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Manifest directories that pin this vulnerable version. Answers the first
|
|
128
|
+
* question a reader asks — *which project do I go fix?* — which a flat
|
|
129
|
+
* package-name list cannot.
|
|
130
|
+
*/
|
|
131
|
+
sourceDirs: string[];
|
|
132
|
+
/**
|
|
133
|
+
* Set when this row is about an INSTALLED version the lockfile does not pin
|
|
134
|
+
* (node_modules drift): the lockfile's version of the same package.
|
|
135
|
+
*/
|
|
136
|
+
installDriftOf?: string;
|
|
137
|
+
/** On drift rows: the command that reinstalls from the lockfile. */
|
|
138
|
+
installFix?: InstallFixCommand;
|
|
74
139
|
}
|
|
75
140
|
export interface VulnerabilityScanResult {
|
|
76
141
|
scannedAt: string;
|
|
@@ -126,6 +191,12 @@ export interface RegistryPackageInfo {
|
|
|
126
191
|
weeklyDownloads: number | null;
|
|
127
192
|
isDev: boolean;
|
|
128
193
|
fetchError: string | null;
|
|
194
|
+
/**
|
|
195
|
+
* The version node_modules actually holds, present only when it differs
|
|
196
|
+
* from `currentVersion` (the lockfile's). Several differing installs across
|
|
197
|
+
* workspaces are listed comma-separated.
|
|
198
|
+
*/
|
|
199
|
+
installedVersion?: string;
|
|
129
200
|
}
|
|
130
201
|
export interface SemverDistance {
|
|
131
202
|
major: number;
|
|
@@ -140,7 +211,19 @@ export interface DependencyHealthResult {
|
|
|
140
211
|
totalDeps: number;
|
|
141
212
|
outdatedCount: number;
|
|
142
213
|
deprecatedCount: number;
|
|
214
|
+
/**
|
|
215
|
+
* Distinct packages with an advisory that is built on this host and is not a
|
|
216
|
+
* maintenance notice — the same set `vulnerability_scan` reports as
|
|
217
|
+
* `total_vulnerable_packages`, so the two tools agree over one scan.
|
|
218
|
+
*/
|
|
143
219
|
vulnerableCount: number;
|
|
220
|
+
/**
|
|
221
|
+
* Every advisory row the scan returned, before the platform and maintenance
|
|
222
|
+
* splits and before alias collapsing. Present so nothing is hidden by the
|
|
223
|
+
* `vulnerableCount` filter: the difference is platform-inactive packages and
|
|
224
|
+
* unmaintained-dependency notices.
|
|
225
|
+
*/
|
|
226
|
+
advisoryCount: number;
|
|
144
227
|
healthScore: number;
|
|
145
228
|
dependencies: RegistryPackageInfo[];
|
|
146
229
|
vulnerabilitySummary: {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network sources for `upgrade_impact`: registry version index, the target
|
|
3
|
+
* version's changelog (read from its registry archive) and OSV advisories.
|
|
4
|
+
*
|
|
5
|
+
* Privacy rule (Decision 1, option A): the only hosts contacted are the
|
|
6
|
+
* package's own registry (registry.npmjs.org; crates.io + static.crates.io)
|
|
7
|
+
* and api.osv.dev. A changelog is never fetched from GitHub or a docs site,
|
|
8
|
+
* even when the archive ships none — the report then hands the agent a
|
|
9
|
+
* `release_notes_url` built from registry metadata, and the agent decides
|
|
10
|
+
* whether to fetch it. Every request goes through `UpgradeNet`, which records
|
|
11
|
+
* the host so `_meta.sources` is a fact, not a claim, and which tests replace
|
|
12
|
+
* with a fake fetch.
|
|
13
|
+
*
|
|
14
|
+
* Request budget per call: index (1) + archive (1) + OSV for from/to (2) = 4,
|
|
15
|
+
* each cached (index 6 h, parsed changelog 7 d, OSV 1 h). The npm packument
|
|
16
|
+
* is large (vite's is ~39 MB, measured 2026-10-02), so only a reduced index
|
|
17
|
+
* is cached — never the raw document.
|
|
18
|
+
*/
|
|
19
|
+
import { type FetchFn } from "./package-archive.js";
|
|
20
|
+
import { type ChangelogSection } from "./changelog.js";
|
|
21
|
+
export type UpgradeEcosystem = "npm" | "crates.io";
|
|
22
|
+
/** crates.io refuses API requests without a descriptive User-Agent. */
|
|
23
|
+
export declare const CRATES_USER_AGENT = "4da-mcp-server (https://4da.ai; upgrade_impact)";
|
|
24
|
+
export interface PublishedVersion {
|
|
25
|
+
version: string;
|
|
26
|
+
published: string | null;
|
|
27
|
+
deprecated?: string;
|
|
28
|
+
yanked?: boolean;
|
|
29
|
+
/** Archive URL (npm `dist.tarball`); crates archives are derived from name + version. */
|
|
30
|
+
tarball?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface RegistryIndex {
|
|
33
|
+
/** The registry's canonical spelling (crates.io folds `-`/`_`). */
|
|
34
|
+
name: string;
|
|
35
|
+
ecosystem: UpgradeEcosystem;
|
|
36
|
+
versions: PublishedVersion[];
|
|
37
|
+
repository: string | null;
|
|
38
|
+
}
|
|
39
|
+
export interface ChangelogResult {
|
|
40
|
+
found: boolean;
|
|
41
|
+
file?: string;
|
|
42
|
+
sections?: ChangelogSection[];
|
|
43
|
+
reason?: string;
|
|
44
|
+
}
|
|
45
|
+
/** The cache surface used here; `LiveCache` satisfies it. */
|
|
46
|
+
export interface UpgradeCache {
|
|
47
|
+
get<T>(key: string): T | null;
|
|
48
|
+
set(key: string, data: unknown, source: string, ttlSeconds: number): void;
|
|
49
|
+
}
|
|
50
|
+
/** Network + cache seam. `contacted` collects every host actually requested. */
|
|
51
|
+
export interface UpgradeNet {
|
|
52
|
+
fetch: FetchFn;
|
|
53
|
+
cache: UpgradeCache | null;
|
|
54
|
+
contacted: Set<string>;
|
|
55
|
+
}
|
|
56
|
+
export declare function createUpgradeNet(cache: UpgradeCache | null, fetchFn?: FetchFn): UpgradeNet;
|
|
57
|
+
export declare function isValidPackageName(name: string, ecosystem: UpgradeEcosystem): boolean;
|
|
58
|
+
/** Registry index with versions in ascending precedence order (unreadable versions dropped). Null = unknown package. */
|
|
59
|
+
export declare function getRegistryIndex(net: UpgradeNet, ecosystem: UpgradeEcosystem, name: string): Promise<RegistryIndex | null>;
|
|
60
|
+
/** The registry archive URL for one version, or null when it is not on the package's own registry. */
|
|
61
|
+
export declare function archiveUrl(index: RegistryIndex, version: PublishedVersion): string | null;
|
|
62
|
+
/** Parsed changelog shipped inside one version's archive (cached 7 days per version). */
|
|
63
|
+
export declare function getChangelog(net: UpgradeNet, index: RegistryIndex, version: PublishedVersion): Promise<ChangelogResult>;
|
|
64
|
+
/** OSV advisory ids affecting one version, or null when OSV could not be reached. */
|
|
65
|
+
export declare function getOsvAdvisories(net: UpgradeNet, ecosystem: UpgradeEcosystem, name: string, version: string): Promise<string[] | null>;
|
|
66
|
+
/**
|
|
67
|
+
* Release-notes page derived from the registry's own `repository` field: a
|
|
68
|
+
* GitHub repo becomes `https://github.com/<owner>/<repo>/releases`; any other
|
|
69
|
+
* https repository URL is returned as-is; anything else is null.
|
|
70
|
+
*/
|
|
71
|
+
export declare function releaseNotesUrl(repository: string | null): string | null;
|