@4da/mcp-server 5.0.4 → 6.0.1

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 +698 -0
  2. package/README.md +70 -39
  3. package/dist/build-staleness.d.ts +35 -0
  4. package/dist/build-staleness.js +95 -0
  5. package/dist/db.d.ts +53 -58
  6. package/dist/db.js +226 -299
  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 +12 -1
  51. package/dist/live/osv-scanner.js +50 -15
  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 -136
  158. package/package.json +35 -35
  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,49 @@
1
+ /**
2
+ * Registry package archive reader (gzip tar), Node built-ins only.
3
+ *
4
+ * `upgrade_impact` reads the changelog a package ships INSIDE its own registry
5
+ * archive (npm `.tgz`, crates.io `.crate`). That keeps the privacy contract
6
+ * simple: the only host contacted is the package's own registry, which the
7
+ * agent was going to download from anyway — never GitHub or a docs site.
8
+ *
9
+ * Why a hand-rolled tar reader: adding `tar` as a dependency for the one job
10
+ * of pulling a few root-level text files would grow the published server's
11
+ * supply chain for no gain. The subset handled here is what real registry
12
+ * archives use: ustar headers with the `prefix` field, PAX `x` headers
13
+ * (`path=` for names over 100 bytes — npm's packer emits these) and GNU `L`
14
+ * long names (older cargo / GNU tar). Everything else is skipped, not guessed.
15
+ *
16
+ * Hard caps, because the archive is third-party input: compressed ≤ 20 MB
17
+ * (Content-Length AND bytes actually read — a server can lie about the
18
+ * former), uncompressed ≤ 80 MB (zlib `maxOutputLength`, so a gzip bomb stops
19
+ * inflating at the cap instead of exhausting memory), single file ≤ 2 MB.
20
+ */
21
+ export declare const MAX_COMPRESSED_BYTES: number;
22
+ export declare const MAX_UNCOMPRESSED_BYTES: number;
23
+ export declare const MAX_FILE_BYTES: number;
24
+ /** The fetch signature archive and registry readers go through, so tests can inject one. */
25
+ export type FetchFn = (url: string, init?: RequestInit, timeoutMs?: number) => Promise<Response>;
26
+ export declare const defaultFetch: FetchFn;
27
+ /** Raised for every refusal (cap exceeded, corrupt archive) with a message fit for tool output. */
28
+ export declare class ArchiveError extends Error {
29
+ }
30
+ interface TarEntry {
31
+ path: string;
32
+ type: string;
33
+ data: Buffer;
34
+ }
35
+ /** PAX records are `"<len> <key>=<value>\n"`, where len counts the whole record in BYTES. */
36
+ export declare function parsePaxHeaders(data: Buffer): Record<string, string>;
37
+ /** Walk an uncompressed tar buffer, yielding regular-file entries with their resolved names. */
38
+ export declare function iterateTar(tar: Buffer): Generator<TarEntry>;
39
+ /**
40
+ * Root-level text files from a gzip tar whose basename `accept` approves.
41
+ * "Root-level" = at most one directory deep, because registry archives wrap
42
+ * everything in a single top directory (`package/`, `<name>-<version>/`).
43
+ * Keys are the archive paths as stored. Oversized or binary (NUL-containing)
44
+ * files are skipped.
45
+ */
46
+ export declare function readRootTextFiles(gz: Buffer, accept: (basename: string) => boolean): Map<string, string>;
47
+ /** Download a registry archive (size-capped) and return the accepted root-level text files. */
48
+ export declare function fetchArchiveRootFiles(url: string, accept: (basename: string) => boolean, fetchFn?: FetchFn, headers?: Record<string, string>): Promise<Map<string, string>>;
49
+ export {};
@@ -0,0 +1,186 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Registry package archive reader (gzip tar), Node built-ins only.
4
+ *
5
+ * `upgrade_impact` reads the changelog a package ships INSIDE its own registry
6
+ * archive (npm `.tgz`, crates.io `.crate`). That keeps the privacy contract
7
+ * simple: the only host contacted is the package's own registry, which the
8
+ * agent was going to download from anyway — never GitHub or a docs site.
9
+ *
10
+ * Why a hand-rolled tar reader: adding `tar` as a dependency for the one job
11
+ * of pulling a few root-level text files would grow the published server's
12
+ * supply chain for no gain. The subset handled here is what real registry
13
+ * archives use: ustar headers with the `prefix` field, PAX `x` headers
14
+ * (`path=` for names over 100 bytes — npm's packer emits these) and GNU `L`
15
+ * long names (older cargo / GNU tar). Everything else is skipped, not guessed.
16
+ *
17
+ * Hard caps, because the archive is third-party input: compressed ≤ 20 MB
18
+ * (Content-Length AND bytes actually read — a server can lie about the
19
+ * former), uncompressed ≤ 80 MB (zlib `maxOutputLength`, so a gzip bomb stops
20
+ * inflating at the cap instead of exhausting memory), single file ≤ 2 MB.
21
+ */
22
+ import { gunzipSync } from "node:zlib";
23
+ import { fetchWithTimeout } from "./http-utils.js";
24
+ export const MAX_COMPRESSED_BYTES = 20 * 1024 * 1024;
25
+ export const MAX_UNCOMPRESSED_BYTES = 80 * 1024 * 1024;
26
+ export const MAX_FILE_BYTES = 2 * 1024 * 1024;
27
+ const BLOCK = 512;
28
+ const ARCHIVE_TIMEOUT_MS = 30_000;
29
+ export const defaultFetch = (url, init, timeoutMs) => fetchWithTimeout(url, init ?? {}, timeoutMs);
30
+ /** Raised for every refusal (cap exceeded, corrupt archive) with a message fit for tool output. */
31
+ export class ArchiveError extends Error {
32
+ }
33
+ function readString(buf, start, length) {
34
+ const slice = buf.subarray(start, start + length);
35
+ const nul = slice.indexOf(0);
36
+ return slice.subarray(0, nul === -1 ? slice.length : nul).toString("utf8");
37
+ }
38
+ function readOctal(buf, start, length) {
39
+ if (buf[start] & 0x80) {
40
+ // GNU base-256 size: only needed for entries ≥ 8 GB, far over every cap here.
41
+ throw new ArchiveError("tar entry uses a base-256 size field (entry too large)");
42
+ }
43
+ const text = readString(buf, start, length).trim();
44
+ if (text === "")
45
+ return 0;
46
+ if (!/^[0-7]+$/.test(text))
47
+ throw new ArchiveError(`corrupt tar header: size field "${text}"`);
48
+ return parseInt(text, 8);
49
+ }
50
+ function checksumMatches(header) {
51
+ const stored = readString(header, 148, 8).trim();
52
+ if (!/^[0-7]+$/.test(stored))
53
+ return false;
54
+ let sum = 0;
55
+ for (let i = 0; i < BLOCK; i++)
56
+ sum += i >= 148 && i < 156 ? 0x20 : header[i];
57
+ return sum === parseInt(stored, 8);
58
+ }
59
+ /** PAX records are `"<len> <key>=<value>\n"`, where len counts the whole record in BYTES. */
60
+ export function parsePaxHeaders(data) {
61
+ const out = {};
62
+ let pos = 0;
63
+ while (pos < data.length) {
64
+ const space = data.indexOf(0x20, pos);
65
+ if (space === -1)
66
+ break;
67
+ const len = Number(data.subarray(pos, space).toString("ascii"));
68
+ if (!Number.isInteger(len) || len <= 0 || pos + len > data.length)
69
+ break;
70
+ const record = data.subarray(space + 1, pos + len).toString("utf8").replace(/\n$/, "");
71
+ const eq = record.indexOf("=");
72
+ if (eq > 0)
73
+ out[record.slice(0, eq)] = record.slice(eq + 1);
74
+ pos += len;
75
+ }
76
+ return out;
77
+ }
78
+ /** Walk an uncompressed tar buffer, yielding regular-file entries with their resolved names. */
79
+ export function* iterateTar(tar) {
80
+ let offset = 0;
81
+ let paxPath = null;
82
+ let gnuLongName = null;
83
+ while (offset + BLOCK <= tar.length) {
84
+ const header = tar.subarray(offset, offset + BLOCK);
85
+ if (header.every((b) => b === 0))
86
+ return; // end-of-archive marker
87
+ if (!checksumMatches(header))
88
+ throw new ArchiveError("corrupt tar header: checksum mismatch");
89
+ const size = readOctal(header, 124, 12);
90
+ const type = String.fromCharCode(header[156] || 0x30); // NUL typeflag = regular file
91
+ const dataStart = offset + BLOCK;
92
+ if (dataStart + size > tar.length)
93
+ throw new ArchiveError("truncated tar entry");
94
+ const data = tar.subarray(dataStart, dataStart + size);
95
+ offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
96
+ if (type === "x") {
97
+ paxPath = parsePaxHeaders(data).path ?? null;
98
+ continue;
99
+ }
100
+ if (type === "L") {
101
+ gnuLongName = readString(data, 0, data.length);
102
+ continue;
103
+ }
104
+ if (type === "g" || type === "K")
105
+ continue; // global PAX / GNU long link: no effect on names here
106
+ const name = readString(header, 0, 100);
107
+ const magic = readString(header, 257, 6);
108
+ const prefix = magic.startsWith("ustar") ? readString(header, 345, 155) : "";
109
+ const path = paxPath ?? gnuLongName ?? (prefix ? `${prefix}/${name}` : name);
110
+ paxPath = null;
111
+ gnuLongName = null;
112
+ yield { path, type, data };
113
+ }
114
+ }
115
+ /** Path segments with `./` and empty segments removed. */
116
+ function segments(path) {
117
+ return path.split("/").filter((s) => s !== "" && s !== ".");
118
+ }
119
+ /**
120
+ * Root-level text files from a gzip tar whose basename `accept` approves.
121
+ * "Root-level" = at most one directory deep, because registry archives wrap
122
+ * everything in a single top directory (`package/`, `<name>-<version>/`).
123
+ * Keys are the archive paths as stored. Oversized or binary (NUL-containing)
124
+ * files are skipped.
125
+ */
126
+ export function readRootTextFiles(gz, accept) {
127
+ if (gz.length > MAX_COMPRESSED_BYTES) {
128
+ throw new ArchiveError(`archive is ${gz.length} bytes compressed (cap ${MAX_COMPRESSED_BYTES})`);
129
+ }
130
+ let tar;
131
+ try {
132
+ tar = gunzipSync(gz, { maxOutputLength: MAX_UNCOMPRESSED_BYTES });
133
+ }
134
+ catch (err) {
135
+ if (err instanceof RangeError || err.code === "ERR_BUFFER_TOO_LARGE") {
136
+ throw new ArchiveError(`archive exceeds ${MAX_UNCOMPRESSED_BYTES} bytes uncompressed`);
137
+ }
138
+ throw new ArchiveError(`archive is not valid gzip: ${err.message}`);
139
+ }
140
+ const files = new Map();
141
+ for (const entry of iterateTar(tar)) {
142
+ if (entry.type !== "0" && entry.type !== "7")
143
+ continue;
144
+ const parts = segments(entry.path);
145
+ if (parts.length === 0 || parts.length > 2)
146
+ continue;
147
+ if (!accept(parts[parts.length - 1]))
148
+ continue;
149
+ if (entry.data.length > MAX_FILE_BYTES || entry.data.includes(0))
150
+ continue;
151
+ files.set(entry.path, entry.data.toString("utf8"));
152
+ }
153
+ return files;
154
+ }
155
+ /** Read a response body, refusing at `cap` bytes instead of buffering whatever the server sends. */
156
+ async function readCapped(response, cap) {
157
+ const declared = Number(response.headers.get("content-length"));
158
+ if (Number.isFinite(declared) && declared > cap) {
159
+ throw new ArchiveError(`archive is ${declared} bytes compressed (cap ${cap})`);
160
+ }
161
+ if (!response.body)
162
+ return Buffer.from(await response.arrayBuffer());
163
+ const reader = response.body.getReader();
164
+ const chunks = [];
165
+ let total = 0;
166
+ for (;;) {
167
+ const { done, value } = await reader.read();
168
+ if (done)
169
+ break;
170
+ total += value.byteLength;
171
+ if (total > cap) {
172
+ await reader.cancel().catch(() => undefined);
173
+ throw new ArchiveError(`archive exceeds ${cap} bytes compressed`);
174
+ }
175
+ chunks.push(Buffer.from(value));
176
+ }
177
+ return Buffer.concat(chunks);
178
+ }
179
+ /** Download a registry archive (size-capped) and return the accepted root-level text files. */
180
+ export async function fetchArchiveRootFiles(url, accept, fetchFn = defaultFetch, headers = {}) {
181
+ const response = await fetchFn(url, { headers }, ARCHIVE_TIMEOUT_MS);
182
+ if (!response.ok)
183
+ throw new ArchiveError(`archive download failed: HTTP ${response.status}`);
184
+ const gz = await readCapped(response, MAX_COMPRESSED_BYTES);
185
+ return readRootTextFiles(gz, accept);
186
+ }
@@ -21,7 +21,9 @@ export class PyPIRegistry {
21
21
  }
22
22
  async getPackageInfo(name, currentVersion, isDev) {
23
23
  const normalizedName = name.toLowerCase().replace(/_/g, "-");
24
- const cacheKey = `pypi-reg:${name.toLowerCase()}`;
24
+ // Keyed by the installed version too: the cached answer carries currentVersion
25
+ // and its distance, so a name-only key served one project's answer to another.
26
+ const cacheKey = `pypi-reg:${name.toLowerCase()}@${currentVersion ?? ""}:${isDev ? 1 : 0}`;
25
27
  const cached = this.cache.get(cacheKey);
26
28
  if (cached !== null)
27
29
  return cached;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Registry health lookups for resolved dependencies (moved out of
3
+ * live/index.ts unchanged, so the coordinator stays within its size limit).
4
+ */
5
+ import type { NpmRegistry } from "./npm-registry.js";
6
+ import type { CratesRegistry } from "./crates-registry.js";
7
+ import type { PyPIRegistry } from "./pypi-registry.js";
8
+ import type { GoRegistry } from "./go-registry.js";
9
+ import type { RegistryPackageInfo, ResolvedDependency } from "./types.js";
10
+ export interface Registries {
11
+ npm: NpmRegistry;
12
+ crates: CratesRegistry;
13
+ pypi: PyPIRegistry;
14
+ go: GoRegistry;
15
+ }
16
+ export declare function fetchRegistryHealthFor(deps: ResolvedDependency[], registries: Registries, enabled: boolean): Promise<RegistryPackageInfo[]>;
@@ -0,0 +1,74 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Registry health lookups for resolved dependencies (moved out of
4
+ * live/index.ts unchanged, so the coordinator stays within its size limit).
5
+ */
6
+ import { computeSemverDistance } from "./semver-utils.js";
7
+ function unavailable(dep, fetchError) {
8
+ return {
9
+ name: dep.name, ecosystem: dep.ecosystem, currentVersion: dep.version,
10
+ latestVersion: null, latestStableVersion: null, versionsBehind: null,
11
+ deprecated: false, deprecationMessage: null, lastPublished: null,
12
+ license: null, weeklyDownloads: null, isDev: dep.isDev, fetchError,
13
+ };
14
+ }
15
+ export async function fetchRegistryHealthFor(deps, registries, enabled) {
16
+ if (!enabled)
17
+ return deps.map((d) => unavailable(d, "Offline mode"));
18
+ const registryForEcosystem = (eco) => {
19
+ switch (eco) {
20
+ case "npm": return registries.npm;
21
+ case "crates.io": return registries.crates;
22
+ case "PyPI": return registries.pypi;
23
+ case "Go": return registries.go;
24
+ default: return null;
25
+ }
26
+ };
27
+ const results = await Promise.all(deps.map(async (dep) => {
28
+ const registry = registryForEcosystem(dep.ecosystem);
29
+ if (!registry)
30
+ return unavailable(dep, `No registry fetcher for ${dep.ecosystem}`);
31
+ try {
32
+ const info = await registry.getPackageInfo(dep.name, dep.version, dep.isDev);
33
+ return restampRegistryContext(info, dep);
34
+ }
35
+ catch {
36
+ return unavailable(dep, "Registry fetch failed");
37
+ }
38
+ }));
39
+ // Bulk fetch npm downloads for npm deps
40
+ const npmDeps = deps.filter((d) => d.ecosystem === "npm");
41
+ if (npmDeps.length > 0) {
42
+ try {
43
+ const downloads = await registries.npm.getBulkDownloads(npmDeps.map((d) => d.name));
44
+ for (const result of results) {
45
+ if (result.ecosystem === "npm" && downloads.has(result.name)) {
46
+ result.weeklyDownloads = downloads.get(result.name) || null;
47
+ }
48
+ }
49
+ }
50
+ catch {
51
+ // Downloads are nice-to-have, not critical
52
+ }
53
+ }
54
+ return results;
55
+ }
56
+ /**
57
+ * Registry caches key by package NAME, but the cached record embeds the
58
+ * QUERYING dependency's `currentVersion`, `versionsBehind`, and `isDev`. Two
59
+ * instances of one package at different versions (better-sqlite3 11.10.0 and
60
+ * 12.11.1 across workspaces) therefore both came back wearing the first
61
+ * instance's version — the upgrade planner then showed two identical rows and
62
+ * lost the older instance entirely. Registry facts (latest version,
63
+ * deprecation, downloads) are per-package and cacheable; the per-instance
64
+ * fields are re-stamped here from the dep actually being asked about.
65
+ */
66
+ function restampRegistryContext(info, dep) {
67
+ const latest = info.latestStableVersion || info.latestVersion;
68
+ return {
69
+ ...info,
70
+ currentVersion: dep.version,
71
+ isDev: dep.isDev,
72
+ versionsBehind: dep.version && latest ? computeSemverDistance(dep.version, latest) : null,
73
+ };
74
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Per-release registry metadata, normalised across npm and crates.io.
3
+ *
4
+ * `dependency_check` judges ONE proposed version against the one installed
5
+ * (or, for a new dependency, against the previous release on the same line).
6
+ * That needs facts the version-freshness readers never kept: when each release
7
+ * was published, who/what published it, whether it runs install scripts, and
8
+ * which runtime dependencies it pulls in. Both registry readers project their
9
+ * raw responses into this shape so the signal logic never sees a registry's
10
+ * wire format.
11
+ *
12
+ * Privacy: every request behind this shape carries the package NAME only.
13
+ * Versions are selected locally from the full release list.
14
+ */
15
+ /**
16
+ * Publish trust of one release.
17
+ * 2 = published by a registry-verified trusted publisher (npm
18
+ * `_npmUser.trustedPublisher`, crates.io `trustpub_data`).
19
+ * 1 = carries a provenance attestation (npm `dist.attestations`).
20
+ * 0 = neither: a token / hand publish.
21
+ */
22
+ export type TrustLevel = 0 | 1 | 2;
23
+ export interface ReleaseRecord {
24
+ version: string;
25
+ /** ISO publish time, or null when the registry does not say. */
26
+ publishedAt: string | null;
27
+ trust: TrustLevel;
28
+ /** Human label for the trust evidence, e.g. "trusted publisher (github)". */
29
+ trustEvidence: string;
30
+ /** npm: install-time lifecycle scripts declared (preinstall/install/postinstall, or a gypfile). Null for crates. */
31
+ installScripts: string[] | null;
32
+ /** npm deprecation message, or null. */
33
+ deprecated: string | null;
34
+ /** crates.io: yanked. npm: unpublished (listed in `time` but gone from `versions`). */
35
+ withdrawn: "yanked" | "unpublished" | null;
36
+ /** Runtime dependency NAMES (npm dependencies + optionalDependencies; crates normal + build). Null when unknown. */
37
+ dependencies: string[] | null;
38
+ }
39
+ export interface PackageReleaseIndex {
40
+ ecosystem: "npm" | "crates.io";
41
+ name: string;
42
+ /** When the package itself was first published, or null. */
43
+ createdAt: string | null;
44
+ releases: Record<string, ReleaseRecord>;
45
+ }
46
+ /** The outcome of one registry read: data, a definite "no such package", or "could not reach it". */
47
+ export type RegistryLookup<T> = {
48
+ status: "ok";
49
+ data: T;
50
+ } | {
51
+ status: "not_found";
52
+ detail: string;
53
+ } | {
54
+ status: "unreachable";
55
+ detail: string;
56
+ };
57
+ /** What dependency_check needs from a registry. Live readers and test fakes both implement it. */
58
+ export interface ReleaseMetadataSource {
59
+ /**
60
+ * The package's release index. `wanted` names the versions the caller is
61
+ * about to read; a reader that pages (crates.io) may stop once it has them
62
+ * and the release just below `wanted[0]` on its line.
63
+ */
64
+ getReleases(name: string, wanted: string[]): Promise<RegistryLookup<PackageReleaseIndex>>;
65
+ /** When a package was first published (for a newly introduced transitive dependency). */
66
+ getCreatedAt(name: string): Promise<RegistryLookup<string | null>>;
67
+ }
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Per-release registry metadata, normalised across npm and crates.io.
4
+ *
5
+ * `dependency_check` judges ONE proposed version against the one installed
6
+ * (or, for a new dependency, against the previous release on the same line).
7
+ * That needs facts the version-freshness readers never kept: when each release
8
+ * was published, who/what published it, whether it runs install scripts, and
9
+ * which runtime dependencies it pulls in. Both registry readers project their
10
+ * raw responses into this shape so the signal logic never sees a registry's
11
+ * wire format.
12
+ *
13
+ * Privacy: every request behind this shape carries the package NAME only.
14
+ * Versions are selected locally from the full release list.
15
+ */
16
+ export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Dependency resolution that knows when it has gone stale.
3
+ *
4
+ * Measured 2026-09-10: three `node ./mcp-4da-server/dist/index.js` processes
5
+ * were live and two had started before the pull that brought hono 4.13.5.
6
+ * `vulnerability_scan` from one of them reported hono 4.13.3 (the lockfile
7
+ * value when it started) with `_meta.cached: false`. The live layer resolved
8
+ * versions once at init and never again, and `cached` described only the OSV
9
+ * lookup, so nothing in the answer could reveal that it was stale.
10
+ *
11
+ * A group records every file its resolution depended on as a stat signature:
12
+ * each lockfile candidate (present or absent, so one appearing or vanishing
13
+ * counts), the manifest when the resolver fell back to it, and for npm the
14
+ * node_modules install state. Signatures are taken BEFORE reading, so a write
15
+ * that lands mid-read still registers as a change. `resolveGroup` depends
16
+ * only on its inputs and the filesystem, so the SAME resolution can be re-run
17
+ * when a signature moves. Stat calls only: no timers, no watchers.
18
+ */
19
+ import { type FileSignature } from "./file-signature.js";
20
+ import type { InstallDriftRecord, ResolutionSourceRecord, ResolvedDependency } from "./types.js";
21
+ /** The inputs of one resolution, kept so the same resolution can be re-run. */
22
+ export interface ResolutionGroup {
23
+ dir: string;
24
+ language: string;
25
+ deps: string[];
26
+ devDeps: string[];
27
+ targets: Record<string, string>;
28
+ }
29
+ export interface GroupResolution {
30
+ group: ResolutionGroup;
31
+ /** Direct dependencies (dependency health, upgrade planning). */
32
+ resolved: ResolvedDependency[];
33
+ /** The full lockfile set plus drift entries (vulnerability scanning). */
34
+ audit: ResolvedDependency[];
35
+ drift: InstallDriftRecord[];
36
+ /** The file the versions were read from, or null when the group resolved from nothing. */
37
+ source: ResolutionSourceRecord | null;
38
+ signatures: Map<string, FileSignature>;
39
+ resolvedAt: string;
40
+ }
41
+ export declare function resolveGroup(group: ResolutionGroup): GroupResolution;
42
+ /** True when any file the resolution depended on changed, appeared, or vanished. */
43
+ export declare function groupIsStale(result: GroupResolution): boolean;
@@ -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
+ }