@4da/mcp-server 5.0.3 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +667 -0
  2. package/README.md +66 -35
  3. package/dist/build-staleness.d.ts +35 -0
  4. package/dist/build-staleness.js +95 -0
  5. package/dist/db.d.ts +53 -2
  6. package/dist/db.js +242 -36
  7. package/dist/doctor.js +27 -7
  8. package/dist/embeddings.js +7 -1
  9. package/dist/freshness-markers.d.ts +34 -0
  10. package/dist/freshness-markers.js +74 -0
  11. package/dist/gitignore.d.ts +37 -0
  12. package/dist/gitignore.js +130 -0
  13. package/dist/index.js +127 -44
  14. package/dist/live/bun-lockfile.d.ts +26 -0
  15. package/dist/live/bun-lockfile.js +123 -0
  16. package/dist/live/cargo-platform.d.ts +38 -0
  17. package/dist/live/cargo-platform.js +178 -0
  18. package/dist/live/changelog-classify.d.ts +41 -0
  19. package/dist/live/changelog-classify.js +207 -0
  20. package/dist/live/changelog.d.ts +64 -0
  21. package/dist/live/changelog.js +318 -0
  22. package/dist/live/crates-registry.js +3 -1
  23. package/dist/live/crates-versions.d.ts +47 -0
  24. package/dist/live/crates-versions.js +201 -0
  25. package/dist/live/dependency-set.d.ts +13 -0
  26. package/dist/live/dependency-set.js +95 -0
  27. package/dist/live/dev-scope.d.ts +32 -0
  28. package/dist/live/dev-scope.js +102 -0
  29. package/dist/live/file-signature.d.ts +18 -0
  30. package/dist/live/file-signature.js +43 -0
  31. package/dist/live/go-registry.js +3 -1
  32. package/dist/live/hn-fetcher.js +9 -3
  33. package/dist/live/index.d.ts +105 -2
  34. package/dist/live/index.js +266 -139
  35. package/dist/live/install-state.d.ts +43 -0
  36. package/dist/live/install-state.js +150 -0
  37. package/dist/live/js-lockfile-readers.d.ts +46 -0
  38. package/dist/live/js-lockfile-readers.js +181 -0
  39. package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
  40. package/dist/live/lockfile-parsers-pygo.js +274 -0
  41. package/dist/live/lockfile-parsers.d.ts +69 -0
  42. package/dist/live/lockfile-parsers.js +311 -0
  43. package/dist/live/lockfile-types.d.ts +34 -0
  44. package/dist/live/lockfile-types.js +42 -0
  45. package/dist/live/maintenance.d.ts +33 -0
  46. package/dist/live/maintenance.js +37 -0
  47. package/dist/live/npm-packument.d.ts +47 -0
  48. package/dist/live/npm-packument.js +150 -0
  49. package/dist/live/npm-registry.js +67 -53
  50. package/dist/live/osv-scanner.d.ts +31 -1
  51. package/dist/live/osv-scanner.js +119 -10
  52. package/dist/live/package-archive.d.ts +49 -0
  53. package/dist/live/package-archive.js +186 -0
  54. package/dist/live/pypi-registry.js +3 -1
  55. package/dist/live/registry-health.d.ts +16 -0
  56. package/dist/live/registry-health.js +74 -0
  57. package/dist/live/release-metadata.d.ts +67 -0
  58. package/dist/live/release-metadata.js +16 -0
  59. package/dist/live/resolution.d.ts +43 -0
  60. package/dist/live/resolution.js +62 -0
  61. package/dist/live/semver-precedence.d.ts +30 -0
  62. package/dist/live/semver-precedence.js +83 -0
  63. package/dist/live/severity-scope.d.ts +57 -0
  64. package/dist/live/severity-scope.js +89 -0
  65. package/dist/live/types.d.ts +83 -0
  66. package/dist/live/upgrade-sources.d.ts +71 -0
  67. package/dist/live/upgrade-sources.js +198 -0
  68. package/dist/live/version-compare.d.ts +39 -0
  69. package/dist/live/version-compare.js +130 -0
  70. package/dist/live/version-resolver.d.ts +14 -1
  71. package/dist/live/version-resolver.js +93 -303
  72. package/dist/native-bindings.d.ts +19 -0
  73. package/dist/native-bindings.js +39 -0
  74. package/dist/project-manifests.d.ts +40 -0
  75. package/dist/project-manifests.js +193 -0
  76. package/dist/project-scanner.js +38 -48
  77. package/dist/project-tree.d.ts +41 -0
  78. package/dist/project-tree.js +110 -0
  79. package/dist/prompts.d.ts +28 -0
  80. package/dist/prompts.js +54 -0
  81. package/dist/schema-registry.d.ts +41 -29
  82. package/dist/schema-registry.js +98 -105
  83. package/dist/server-instructions.d.ts +11 -0
  84. package/dist/server-instructions.js +36 -0
  85. package/dist/tool-args.d.ts +28 -0
  86. package/dist/tool-args.js +93 -0
  87. package/dist/tool-dispatch.d.ts +1 -1
  88. package/dist/tool-dispatch.js +42 -8
  89. package/dist/tools/app-plan.d.ts +92 -0
  90. package/dist/tools/app-plan.js +155 -0
  91. package/dist/tools/briefing-stored-advisories.d.ts +20 -0
  92. package/dist/tools/briefing-stored-advisories.js +58 -0
  93. package/dist/tools/briefing-task-scope.d.ts +73 -0
  94. package/dist/tools/briefing-task-scope.js +197 -0
  95. package/dist/tools/briefing-wisdom.d.ts +31 -0
  96. package/dist/tools/briefing-wisdom.js +148 -0
  97. package/dist/tools/decision-enforcement.d.ts +0 -5
  98. package/dist/tools/decision-enforcement.js +0 -4
  99. package/dist/tools/decision-memory.js +18 -66
  100. package/dist/tools/dependency-check-signals.d.ts +74 -0
  101. package/dist/tools/dependency-check-signals.js +264 -0
  102. package/dist/tools/dependency-check.d.ts +98 -0
  103. package/dist/tools/dependency-check.js +295 -0
  104. package/dist/tools/dependency-health.js +74 -21
  105. package/dist/tools/developer-dna.d.ts +3 -0
  106. package/dist/tools/developer-dna.js +16 -15
  107. package/dist/tools/ecosystem-pulse.d.ts +6 -2
  108. package/dist/tools/ecosystem-pulse.js +53 -14
  109. package/dist/tools/get-actionable-signals.d.ts +26 -4
  110. package/dist/tools/get-actionable-signals.js +214 -251
  111. package/dist/tools/get-context.d.ts +2 -0
  112. package/dist/tools/get-context.js +9 -1
  113. package/dist/tools/get-relevant-content.d.ts +3 -1
  114. package/dist/tools/get-relevant-content.js +34 -3
  115. package/dist/tools/index.d.ts +4 -2
  116. package/dist/tools/index.js +4 -2
  117. package/dist/tools/install-drift-notes.d.ts +51 -0
  118. package/dist/tools/install-drift-notes.js +172 -0
  119. package/dist/tools/knowledge-gap-citations.d.ts +55 -0
  120. package/dist/tools/knowledge-gap-citations.js +132 -0
  121. package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
  122. package/dist/tools/knowledge-gap-exposure.js +160 -0
  123. package/dist/tools/knowledge-gap-grading.d.ts +91 -0
  124. package/dist/tools/knowledge-gap-grading.js +157 -0
  125. package/dist/tools/knowledge-gap-installs.d.ts +65 -0
  126. package/dist/tools/knowledge-gap-installs.js +134 -0
  127. package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
  128. package/dist/tools/knowledge-gap-ranges.js +146 -0
  129. package/dist/tools/knowledge-gap-scope.d.ts +63 -0
  130. package/dist/tools/knowledge-gap-scope.js +118 -0
  131. package/dist/tools/knowledge-gaps.d.ts +12 -3
  132. package/dist/tools/knowledge-gaps.js +172 -121
  133. package/dist/tools/package-presence.d.ts +24 -0
  134. package/dist/tools/package-presence.js +35 -0
  135. package/dist/tools/record-feedback.d.ts +0 -1
  136. package/dist/tools/record-feedback.js +7 -8
  137. package/dist/tools/signal-classifier.d.ts +25 -0
  138. package/dist/tools/signal-classifier.js +40 -0
  139. package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
  140. package/dist/tools/upgrade-impact-callsites.js +226 -0
  141. package/dist/tools/upgrade-impact-literals.d.ts +45 -0
  142. package/dist/tools/upgrade-impact-literals.js +118 -0
  143. package/dist/tools/upgrade-impact-report.d.ts +80 -0
  144. package/dist/tools/upgrade-impact-report.js +178 -0
  145. package/dist/tools/upgrade-impact.d.ts +84 -0
  146. package/dist/tools/upgrade-impact.js +305 -0
  147. package/dist/tools/upgrade-planner.d.ts +41 -8
  148. package/dist/tools/upgrade-planner.js +201 -53
  149. package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
  150. package/dist/tools/vulnerability-scan-concise.js +97 -0
  151. package/dist/tools/vulnerability-scan-format.d.ts +137 -0
  152. package/dist/tools/vulnerability-scan-format.js +276 -0
  153. package/dist/tools/vulnerability-scan.d.ts +338 -18
  154. package/dist/tools/vulnerability-scan.js +84 -125
  155. package/dist/tools/what-should-i-know.d.ts +86 -19
  156. package/dist/tools/what-should-i-know.js +277 -283
  157. package/dist/types.d.ts +4 -1
  158. package/package.json +27 -30
  159. package/dist/schemas/agent-memory.json +0 -49
  160. package/dist/schemas/decision-enforcement.json +0 -22
  161. package/dist/schemas/decision-memory.json +0 -54
  162. package/dist/schemas/dependency-health.json +0 -27
  163. package/dist/schemas/developer-dna.json +0 -219
  164. package/dist/schemas/ecosystem-pulse.json +0 -17
  165. package/dist/schemas/get-actionable-signals.json +0 -29
  166. package/dist/schemas/get-context.json +0 -17
  167. package/dist/schemas/get-relevant-content.json +0 -26
  168. package/dist/schemas/knowledge-gaps.json +0 -14
  169. package/dist/schemas/record-feedback.json +0 -24
  170. package/dist/schemas/upgrade-planner.json +0 -22
  171. package/dist/schemas/vulnerability-scan.json +0 -26
  172. package/dist/schemas/what-should-i-know.json +0 -19
@@ -0,0 +1,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
+ }
@@ -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;