@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,318 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Changelog discovery and parsing for `upgrade_impact`.
4
+ *
5
+ * Changelogs follow no schema, so the parser recognises the version-heading
6
+ * styles actually found in registry archives rather than one spec:
7
+ * keep-a-changelog `## [1.2.3] - 2024-01-01`
8
+ * conventional `## [7.0.0](https://…/compare/…) (2025-06-24)`
9
+ * plain / prefixed `## 1.2.3`, `# v1.2.3 (2024-01-01)`, `### Version 1.2.3`, `## fastembed 5.0.0`
10
+ * setext `1.2.3` underlined with `===` / `---`
11
+ * History.md (express) `1.2.3 / 2020-01-01`
12
+ * A heading only opens a section when the rest of it looks like a trailer
13
+ * (date, link, "yanked") — `## Upgrading from 1.x` or `#### Rust 1.70 support`
14
+ * must stay sub-headings, or a migration guide would split a release in two.
15
+ *
16
+ * Entries are bullets, numbered items and loose paragraph lines; indented
17
+ * continuation lines join the entry above. Fenced code blocks and link
18
+ * reference definitions are skipped: they are examples and URLs, not changes.
19
+ */
20
+ import { compareVersionPrecedence } from "./semver-precedence.js";
21
+ import { classifyEntry, classifyHeading, sanitizeEntry } from "./changelog-classify.js";
22
+ const NAME_STEMS = ["changelog", "changes", "history", "releases", "release-notes", "release_notes", "releasenotes", "news"];
23
+ const NAME_EXTS = ["", ".md", ".markdown", ".txt", ".rst"];
24
+ /** True for a root-level basename that names a changelog (case-insensitive). */
25
+ export function isChangelogName(basename) {
26
+ const lower = basename.toLowerCase();
27
+ return NAME_STEMS.some((stem) => NAME_EXTS.some((ext) => lower === stem + ext));
28
+ }
29
+ /**
30
+ * The changelog among archive paths, preferring CHANGELOG over the others and
31
+ * Markdown over plain text. Null when there is none.
32
+ */
33
+ export function findChangelogFile(paths) {
34
+ let best = null;
35
+ for (const path of paths) {
36
+ const base = path.split("/").filter(Boolean).pop() ?? "";
37
+ if (!isChangelogName(base))
38
+ continue;
39
+ const lower = base.toLowerCase();
40
+ const stem = NAME_STEMS.findIndex((s) => lower.startsWith(s));
41
+ const ext = NAME_EXTS.indexOf(lower.slice(NAME_STEMS[stem].length));
42
+ const rank = stem * 10 + (ext === 1 || ext === 2 ? 0 : ext + 1);
43
+ if (!best || rank < best.rank)
44
+ best = { path, rank };
45
+ }
46
+ return best?.path ?? null;
47
+ }
48
+ const VERSION = String.raw `\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?`;
49
+ const ISO_DATE = /\b(\d{4})[-/.](\d{2})[-/.](\d{2})\b/;
50
+ const MONTH_DATE = /\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.? \d{1,2}(?:st|nd|rd|th)?,? \d{4}\b|\b\d{1,2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*,? \d{4}\b/i;
51
+ /** `[optional prefix] version rest`: prefix is a keyword, a `v`, or a package name. */
52
+ const HEADING = new RegExp(String.raw `^\[?(?:(version|release)\s+|([A-Za-z@][\w@/.-]*)(?:\s+|@))?\[?v?(${VERSION})\]?(.*)$`, "i");
53
+ const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
54
+ /** ISO date from a heading trailer: `2024-01-01`, `2024/01/01`, `August 1st, 2025`, `1 Aug 2025`. */
55
+ function extractDate(text) {
56
+ const iso = ISO_DATE.exec(text);
57
+ if (iso)
58
+ return `${iso[1]}-${iso[2]}-${iso[3]}`;
59
+ const named = MONTH_DATE.exec(text)?.[0];
60
+ if (!named)
61
+ return null;
62
+ const month = MONTHS.findIndex((m) => named.toLowerCase().includes(m)) + 1;
63
+ const day = /\b(\d{1,2})(?:st|nd|rd|th)?\b(?!\d)/.exec(named.replace(/\d{4}/, ""))?.[1];
64
+ const year = /\d{4}/.exec(named)?.[0];
65
+ if (!month || !day || !year)
66
+ return named;
67
+ return `${year}-${String(month).padStart(2, "0")}-${day.padStart(2, "0")}`;
68
+ }
69
+ /**
70
+ * Version and date from a heading's text, or null when it is not a release
71
+ * heading. Exported for tests.
72
+ */
73
+ export function parseVersionHeading(raw) {
74
+ const text = raw.trim().replace(/^\*\*|\*\*$/g, "");
75
+ const m = HEADING.exec(text);
76
+ if (!m)
77
+ return null;
78
+ const [, keyword, namePrefix, version, rest] = m;
79
+ // A free-form name prefix needs a full x.y.z — "Rust 1.70" is prose, not a release.
80
+ if (namePrefix && !keyword && !/^\d+\.\d+\.\d+/.test(version))
81
+ return null;
82
+ const trailer = rest
83
+ .replace(/\]?\([^)]*\)/g, " ")
84
+ .replace(/\[[^\]]*\]/g, " ")
85
+ .replace(ISO_DATE, " ")
86
+ .replace(MONTH_DATE, " ")
87
+ .replace(/\b(?:yanked|unreleased|released|latest|stable|on)\b/gi, " ")
88
+ .replace(/[-–—/:,.*_#()[\]<>|]/g, " ")
89
+ .trim();
90
+ if (trailer.split(/\s+/).filter(Boolean).length > 2)
91
+ return null;
92
+ return { version, date: extractDate(rest) };
93
+ }
94
+ const ATX = /^(#{1,6})\s+(.*?)\s*#*\s*$/;
95
+ const SETEXT_RULE = /^\s*(=+|-+)\s*$/;
96
+ const BULLET = /^(\s*)(?:[-*+•]|\d+[.)])\s+(.*)$/;
97
+ const LINK_DEF = /^\s*\[[^\]]+\]:\s*\S+/;
98
+ const FENCE = /^\s*(```|~~~)/;
99
+ const RULE = /^\s*(?:[-=*_]\s*){3,}$/;
100
+ /** A whole-line bold label ("**Breaking Changes:**") acts as a sub-heading. */
101
+ const BOLD_LABEL = /^\s*\*\*([^*]+)\*\*:?\s*$/;
102
+ /** A short unbulleted line ending in a colon: a label for what follows ("Security:", "Breaking changes:"). */
103
+ const PLAIN_LABEL = /^\s{0,3}([A-Za-z⚠][\w /&,'()⚠️-]{1,58}?)\s*:\s*$/;
104
+ /** A line that opens or closes layout markup rather than describing a change. */
105
+ const HTML_LAYOUT = /^<\/?(?:details|summary|div|p|br|hr|img|picture|source|table|tr|td|th|thead|tbody|center|sup|sub)\b/i;
106
+ /** A bullet that is only a label: "remove:", "**Breaking**:", "deps:". */
107
+ const LABEL_BULLET = /^\W*([A-Za-z][\w -]{0,30}?)\W*:\W*$/;
108
+ function flush(state) {
109
+ if (state.pending && state.current) {
110
+ const text = sanitizeEntry(state.pending.text);
111
+ const under = state.pending.under;
112
+ // Layout markup is not a change: actix-web 4 wraps pre-release notes in
113
+ // `<details> <summary>...</summary>`, and both lines were counted as removals.
114
+ if (text && !HTML_LAYOUT.test(text)) {
115
+ state.current.entries.push({ kind: classifyEntry(text, state.pending.context), text, ...(under ? { under } : {}) });
116
+ }
117
+ }
118
+ state.pending = null;
119
+ }
120
+ /**
121
+ * Headings that name a kind of change (keep-a-changelog and conventional-commit
122
+ * categories), as opposed to a new document part ("Migration guide", "Older releases").
123
+ */
124
+ const CATEGORY_HEADING = /^\W*(added|changed|changes|deprecated|deprecations|removed|removals|fixed|fixes|bug ?fixes|security|features?|new features|enhancements?|improvements?|performance( improvements)?|breaking( changes?)?|⚠️?\s*breaking( changes?)?|dependencies|dependency updates|documentation|docs|internal|misc(ellaneous)?|other( changes)?|chores?|refactor(ing)?|reverts?|build|tests?)\W*$/i;
125
+ function isCategoryHeading(raw) {
126
+ return CATEGORY_HEADING.test(raw.trim());
127
+ }
128
+ function openSection(state, heading, level) {
129
+ flush(state);
130
+ state.sectionLevel = level;
131
+ state.current = { version: heading.version, date: heading.date, entries: [] };
132
+ state.sections.push(state.current);
133
+ state.headingKind = null;
134
+ state.parentKind = null;
135
+ state.parentIndent = null;
136
+ state.headingText = null;
137
+ state.parentText = null;
138
+ }
139
+ /** A heading or parent bullet as context text: emphasis and code marks dropped, one line, at most 80 characters. */
140
+ function contextText(raw) {
141
+ return sanitizeEntry(raw.replace(/\*\*|__|`/g, "").replace(/:\s*$/, ""), 80) || null;
142
+ }
143
+ function addLine(state, line) {
144
+ const bullet = BULLET.exec(line);
145
+ if (bullet) {
146
+ flush(state);
147
+ const indent = bullet[1].length;
148
+ const text = bullet[2];
149
+ // The outermost bullet level of a list, wherever it is indented (express's
150
+ // History.md lists sit at two spaces), lends context to bullets nested under it.
151
+ const outer = state.parentIndent === null || indent <= state.parentIndent;
152
+ if (outer) {
153
+ state.parentIndent = indent;
154
+ // A label-only bullet ("* remove:", "- Breaking:") is a heading for its children, not an entry.
155
+ const label = LABEL_BULLET.exec(text);
156
+ if (label) {
157
+ state.parentKind = classifyHeading(label[1]) ?? state.headingKind;
158
+ state.parentText = contextText(label[1]);
159
+ return;
160
+ }
161
+ // A breaking bullet lends "breaking" to its sub-points (date-fns 3.0: "**BREAKING**: Functions
162
+ // that accept Interval arguments ..." with one nested bullet per affected function); one
163
+ // ending in ":" lends any signalling kind.
164
+ const own = classifyEntry(text, state.headingKind);
165
+ state.parentKind = own === "breaking" || (/:\s*$/.test(text) && own !== "change") ? own : null;
166
+ state.parentText = contextText(text);
167
+ }
168
+ const context = !outer && state.parentKind ? state.parentKind : state.headingKind;
169
+ const under = !outer && state.parentText ? state.parentText : state.headingText;
170
+ state.pending = { text, context, indent, under };
171
+ return;
172
+ }
173
+ if (line.trim() === "") {
174
+ flush(state);
175
+ return;
176
+ }
177
+ if (state.pending && /^\s+\S/.test(line)) {
178
+ state.pending.text += ` ${line.trim()}`;
179
+ return;
180
+ }
181
+ if (state.pending && state.pending.indent === -1) {
182
+ state.pending.text += ` ${line.trim()}`; // wrapped paragraph line
183
+ return;
184
+ }
185
+ flush(state);
186
+ state.pending = { text: line, context: state.headingKind, indent: -1, under: state.headingText };
187
+ }
188
+ /** Parse a changelog into version sections, in document order (usually newest first). */
189
+ export function parseChangelog(text) {
190
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
191
+ const state = {
192
+ sections: [],
193
+ current: null,
194
+ sectionLevel: 0,
195
+ headingKind: null,
196
+ parentKind: null,
197
+ parentIndent: null,
198
+ headingText: null,
199
+ parentText: null,
200
+ pending: null,
201
+ };
202
+ let inFence = false;
203
+ let inComment = false;
204
+ for (let i = 0; i < lines.length; i++) {
205
+ const line = lines[i];
206
+ if (FENCE.test(line)) {
207
+ flush(state);
208
+ inFence = !inFence;
209
+ continue;
210
+ }
211
+ if (inFence)
212
+ continue;
213
+ // HTML comments are skipped whole, including ones that span lines
214
+ // (release tooling leaves `<!--\n template notes\n-->` blocks in changelogs).
215
+ const trimmed = line.trim();
216
+ if (inComment) {
217
+ if (trimmed.includes("-->"))
218
+ inComment = false;
219
+ continue;
220
+ }
221
+ if (trimmed.startsWith("<!--")) {
222
+ if (!trimmed.slice(4).includes("-->"))
223
+ inComment = true;
224
+ continue;
225
+ }
226
+ if (LINK_DEF.test(line))
227
+ continue;
228
+ const atx = ATX.exec(line);
229
+ if (atx) {
230
+ const heading = parseVersionHeading(atx[2]);
231
+ const level = atx[1].length;
232
+ if (heading)
233
+ openSection(state, heading, level);
234
+ else {
235
+ flush(state);
236
+ // "## Migration guide" after the last release must not be read as part of it.
237
+ // A change-category heading ("## Changed", "## Breaking Changes") at the
238
+ // release's own level is still inside it: date-fns 3.0.0 writes
239
+ // "## v3.0.0" then "## Changed", and its ten BREAKING entries were dropped.
240
+ if (state.current && level <= state.sectionLevel && !isCategoryHeading(atx[2]))
241
+ state.current = null;
242
+ state.headingKind = classifyHeading(atx[2]);
243
+ state.headingText = contextText(atx[2]);
244
+ state.parentKind = null;
245
+ state.parentIndent = null;
246
+ state.parentText = null;
247
+ }
248
+ continue;
249
+ }
250
+ const next = lines[i + 1];
251
+ if (next !== undefined && SETEXT_RULE.test(next) && line.trim() !== "" && !BULLET.test(line)) {
252
+ const heading = parseVersionHeading(line);
253
+ if (heading) {
254
+ openSection(state, heading, next.trim().startsWith("=") ? 1 : 2);
255
+ i++;
256
+ continue;
257
+ }
258
+ }
259
+ // History.md style without an underline: `1.2.3 / 2020-01-01` alone on a line.
260
+ const history = new RegExp(String.raw `^v?(${VERSION})\s+/\s+(.+)$`).exec(line.trim());
261
+ if (history && extractDate(history[2])) {
262
+ openSection(state, { version: history[1], date: extractDate(history[2]) }, 2);
263
+ continue;
264
+ }
265
+ if (RULE.test(line)) {
266
+ flush(state);
267
+ continue;
268
+ }
269
+ // A release as a top-level bullet with its changes nested under it
270
+ // (indexmap's RELEASES.md: "- 2.0.0" then " - **MSRV**: Rust 1.64.0 ...").
271
+ const versionBullet = BULLET.exec(line);
272
+ if (versionBullet && versionBullet[1].length === 0 && /^\[?v?\d/.test(versionBullet[2])) {
273
+ const heading = parseVersionHeading(versionBullet[2]);
274
+ if (heading && !/\s\w+\s\w+/.test(versionBullet[2].replace(ISO_DATE, "").replace(MONTH_DATE, ""))) {
275
+ openSection(state, heading, 7);
276
+ continue;
277
+ }
278
+ }
279
+ // A whole-line label, bold or plain ("**Breaking Changes**", "BREAKING CHANGES:",
280
+ // "Deprecations / Removals / API Changes:") is a sub-heading, not an entry.
281
+ // Plain ones were entries until 2026-10-03: 6 of 17 false "breaking" flags
282
+ // on the fresh corpus-4 panel (highlight.js 11, handlebars 4.6, ts-loader 9).
283
+ const bold = BOLD_LABEL.exec(line) ?? PLAIN_LABEL.exec(line);
284
+ if (bold && state.current) {
285
+ flush(state);
286
+ state.headingKind = classifyHeading(bold[1]);
287
+ state.headingText = contextText(bold[1]);
288
+ state.parentKind = null;
289
+ state.parentIndent = null;
290
+ state.parentText = null;
291
+ continue;
292
+ }
293
+ if (state.current)
294
+ addLine(state, line);
295
+ }
296
+ flush(state);
297
+ return state.sections;
298
+ }
299
+ /** True when `version` lies in (from, to] by semver precedence; unreadable versions are out. */
300
+ export function inRange(version, from, to) {
301
+ const lower = compareVersionPrecedence(version, from);
302
+ const upper = compareVersionPrecedence(version, to);
303
+ return lower !== null && upper !== null && lower > 0 && upper <= 0;
304
+ }
305
+ /**
306
+ * Sections in (from, to], plus whether the changelog reaches both ends of the
307
+ * range: its newest section is at or past `to`, and its oldest at or before
308
+ * `firstInRange` (the earliest release the upgrade crosses). A changelog that
309
+ * stopped being maintained two majors ago is reported as not covering the
310
+ * range instead of silently presenting a partial history as the whole one.
311
+ */
312
+ export function selectRange(sections, from, to, firstInRange) {
313
+ const readable = sections.filter((s) => compareVersionPrecedence(s.version, s.version) !== null);
314
+ const selected = readable.filter((s) => inRange(s.version, from, to));
315
+ const reachesTop = readable.some((s) => (compareVersionPrecedence(s.version, to) ?? -1) >= 0);
316
+ const reachesBottom = readable.some((s) => (compareVersionPrecedence(s.version, firstInRange) ?? 1) <= 0);
317
+ return { sections: selected, coversRange: selected.length > 0 && reachesTop && reachesBottom };
318
+ }
@@ -19,7 +19,9 @@ export class CratesRegistry {
19
19
  this.rateLimiter = rateLimiter;
20
20
  }
21
21
  async getPackageInfo(name, currentVersion, isDev) {
22
- const cacheKey = `crates-reg:${name}`;
22
+ // Keyed by the installed version too: the cached answer carries currentVersion
23
+ // and its distance, so a name-only key served one project's answer to another.
24
+ const cacheKey = `crates-reg:${name}@${currentVersion ?? ""}:${isDev ? 1 : 0}`;
23
25
  const cached = this.cache.get(cacheKey);
24
26
  if (cached !== null)
25
27
  return cached;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * crates.io release reader for dependency_check.
3
+ *
4
+ * Three name-only reads:
5
+ * - `crates.io/api/v1/crates/<name>/versions` (paged, newest semver first):
6
+ * publish time, publisher, `trustpub_data`, yanked.
7
+ * - the sparse index (`index.crates.io`, the CDN file cargo itself reads):
8
+ * each version's dependency list, which the versions endpoint lacks.
9
+ * - `crates.io/api/v1/crates/<name>`: when a newly introduced dependency
10
+ * was first published.
11
+ *
12
+ * crates.io asks API clients for a descriptive User-Agent and at most one
13
+ * request per second; every API request here waits for its slot. Paging
14
+ * stops as soon as the versions the caller asked about (and the release just
15
+ * below them) have been seen, so a check costs one or two pages, not the
16
+ * crate's whole history.
17
+ *
18
+ * Privacy: requests carry the crate NAME only, never the installed version.
19
+ */
20
+ import type { LiveCache } from "./cache.js";
21
+ import type { RateLimiter } from "./rate-limiter.js";
22
+ import type { PackageReleaseIndex, RegistryLookup, ReleaseMetadataSource } from "./release-metadata.js";
23
+ export declare const CRATES_USER_AGENT = "4DA-MCP-Server/1.0 (https://4da.ai)";
24
+ export declare class CratesVersionsReader implements ReleaseMetadataSource {
25
+ private cache;
26
+ private rateLimiter;
27
+ /** crates.io's crawler policy: one request per second. Tests pass 0. */
28
+ private minIntervalMs;
29
+ /** Earliest time the next crates.io API request may start. Reserved synchronously so concurrent callers queue. */
30
+ private nextSlotMs;
31
+ constructor(cache: LiveCache, rateLimiter: RateLimiter,
32
+ /** crates.io's crawler policy: one request per second. Tests pass 0. */
33
+ minIntervalMs?: number);
34
+ getReleases(name: string, wanted: string[]): Promise<RegistryLookup<PackageReleaseIndex>>;
35
+ getCreatedAt(name: string): Promise<RegistryLookup<string | null>>;
36
+ private apiGet;
37
+ private waitForSlot;
38
+ /** version -> runtime dependency names (normal + build; renamed deps by their real crate name). */
39
+ private sparseDependencies;
40
+ }
41
+ /**
42
+ * True once every wanted version is present AND a release (not a prerelease)
43
+ * below all of them has been seen. Pages arrive in descending semver order,
44
+ * so at that point the release just below the target on its line — the
45
+ * new-dependency trust baseline — has been seen too, if it exists.
46
+ */
47
+ export declare function pagesSatisfy(seen: string[], wanted: string[]): boolean;
@@ -0,0 +1,201 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * crates.io release reader for dependency_check.
4
+ *
5
+ * Three name-only reads:
6
+ * - `crates.io/api/v1/crates/<name>/versions` (paged, newest semver first):
7
+ * publish time, publisher, `trustpub_data`, yanked.
8
+ * - the sparse index (`index.crates.io`, the CDN file cargo itself reads):
9
+ * each version's dependency list, which the versions endpoint lacks.
10
+ * - `crates.io/api/v1/crates/<name>`: when a newly introduced dependency
11
+ * was first published.
12
+ *
13
+ * crates.io asks API clients for a descriptive User-Agent and at most one
14
+ * request per second; every API request here waits for its slot. Paging
15
+ * stops as soon as the versions the caller asked about (and the release just
16
+ * below them) have been seen, so a check costs one or two pages, not the
17
+ * crate's whole history.
18
+ *
19
+ * Privacy: requests carry the crate NAME only, never the installed version.
20
+ */
21
+ import { fetchWithTimeout } from "./http-utils.js";
22
+ import { compareVersionPrecedence, parseSemverPrecedence } from "./semver-precedence.js";
23
+ const CRATES_API = "https://crates.io/api/v1/crates";
24
+ export const CRATES_USER_AGENT = "4DA-MCP-Server/1.0 (https://4da.ai)";
25
+ const TIMEOUT_MS = 15_000;
26
+ const MAX_PAGES = 25;
27
+ const VERSIONS_TTL = 3600;
28
+ const CREATED_TTL = 30 * 86_400; // a crate's creation date never changes
29
+ class HttpStatusError extends Error {
30
+ status;
31
+ constructor(status) {
32
+ super(`HTTP ${status}`);
33
+ this.status = status;
34
+ }
35
+ }
36
+ export class CratesVersionsReader {
37
+ cache;
38
+ rateLimiter;
39
+ minIntervalMs;
40
+ /** Earliest time the next crates.io API request may start. Reserved synchronously so concurrent callers queue. */
41
+ nextSlotMs = 0;
42
+ constructor(cache, rateLimiter,
43
+ /** crates.io's crawler policy: one request per second. Tests pass 0. */
44
+ minIntervalMs = 1000) {
45
+ this.cache = cache;
46
+ this.rateLimiter = rateLimiter;
47
+ this.minIntervalMs = minIntervalMs;
48
+ }
49
+ async getReleases(name, wanted) {
50
+ const key = `crates-versions:${name}`;
51
+ const cached = this.cache.get(key);
52
+ if (cached && (cached.complete || pagesSatisfy(Object.keys(cached.index.releases), wanted))) {
53
+ return { status: "ok", data: cached.index };
54
+ }
55
+ const releases = {};
56
+ let query = "?per_page=100&sort=semver";
57
+ let pages = 0;
58
+ try {
59
+ while (query && pages < MAX_PAGES) {
60
+ const page = await this.apiGet(`${CRATES_API}/${encodeURIComponent(name)}/versions${query}`);
61
+ pages++;
62
+ for (const v of page.versions ?? [])
63
+ releases[v.num] = toRecord(v);
64
+ query = page.meta?.next_page ?? null;
65
+ if (query && pagesSatisfy(Object.keys(releases), wanted))
66
+ break;
67
+ }
68
+ }
69
+ catch (err) {
70
+ if (err instanceof HttpStatusError && err.status === 404) {
71
+ return { status: "not_found", detail: `"${name}" is not on crates.io` };
72
+ }
73
+ return { status: "unreachable", detail: `crates.io: ${errorText(err)}` };
74
+ }
75
+ // Dependency lists come from the sparse index; without them the
76
+ // new-dependency signal reports "unknown" rather than "none".
77
+ const deps = await this.sparseDependencies(name);
78
+ if (deps) {
79
+ for (const [version, names] of Object.entries(deps)) {
80
+ if (releases[version])
81
+ releases[version].dependencies = names;
82
+ }
83
+ }
84
+ const index = { ecosystem: "crates.io", name, createdAt: null, releases };
85
+ this.cache.set(key, { index, complete: query === null }, "crates", VERSIONS_TTL);
86
+ return { status: "ok", data: index };
87
+ }
88
+ async getCreatedAt(name) {
89
+ const key = `crates-created:${name}`;
90
+ const cached = this.cache.get(key);
91
+ if (cached)
92
+ return { status: "ok", data: cached.createdAt };
93
+ try {
94
+ const body = await this.apiGet(`${CRATES_API}/${encodeURIComponent(name)}`);
95
+ const createdAt = body.crate?.created_at ?? null;
96
+ this.cache.set(key, { createdAt }, "crates", CREATED_TTL);
97
+ return { status: "ok", data: createdAt };
98
+ }
99
+ catch (err) {
100
+ if (err instanceof HttpStatusError && err.status === 404) {
101
+ return { status: "not_found", detail: `"${name}" is not on crates.io` };
102
+ }
103
+ return { status: "unreachable", detail: `crates.io: ${errorText(err)}` };
104
+ }
105
+ }
106
+ async apiGet(url) {
107
+ if (!this.rateLimiter.consume("crates"))
108
+ throw new Error("crates.io rate limit reached for this minute");
109
+ await this.waitForSlot();
110
+ const response = await fetchWithTimeout(url, { headers: { Accept: "application/json", "User-Agent": CRATES_USER_AGENT } }, TIMEOUT_MS);
111
+ if (!response.ok)
112
+ throw new HttpStatusError(response.status);
113
+ return (await response.json());
114
+ }
115
+ async waitForSlot() {
116
+ const now = Date.now();
117
+ const start = Math.max(now, this.nextSlotMs);
118
+ this.nextSlotMs = start + this.minIntervalMs;
119
+ if (start > now)
120
+ await new Promise((resolve) => setTimeout(resolve, start - now));
121
+ }
122
+ /** version -> runtime dependency names (normal + build; renamed deps by their real crate name). */
123
+ async sparseDependencies(name) {
124
+ const key = `crates-sparse-deps:${name}`;
125
+ const cached = this.cache.get(key);
126
+ if (cached)
127
+ return cached;
128
+ try {
129
+ const response = await fetchWithTimeout(sparseIndexUrl(name), { headers: { "User-Agent": CRATES_USER_AGENT } }, TIMEOUT_MS);
130
+ if (!response.ok)
131
+ return null;
132
+ const out = {};
133
+ for (const line of (await response.text()).split("\n")) {
134
+ if (!line.trim())
135
+ continue;
136
+ try {
137
+ const entry = JSON.parse(line);
138
+ const names = (entry.deps ?? [])
139
+ .filter((d) => d.kind !== "dev")
140
+ .map((d) => d.package ?? d.name);
141
+ out[entry.vers] = [...new Set(names)];
142
+ }
143
+ catch {
144
+ // skip a malformed line
145
+ }
146
+ }
147
+ this.cache.set(key, out, "crates", VERSIONS_TTL);
148
+ return out;
149
+ }
150
+ catch {
151
+ return null;
152
+ }
153
+ }
154
+ }
155
+ function toRecord(v) {
156
+ const tp = v.trustpub_data;
157
+ const who = v.published_by?.login ? ` (published by crates.io user "${v.published_by.login}")` : "";
158
+ return {
159
+ version: v.num,
160
+ publishedAt: v.created_at ?? null,
161
+ trust: tp ? 2 : 0,
162
+ trustEvidence: tp
163
+ ? `trusted publisher (${[tp.provider, tp.repository].filter(Boolean).join(" ") || "OIDC"})`
164
+ : `no trusted publisher${who}`,
165
+ installScripts: null,
166
+ deprecated: null,
167
+ withdrawn: v.yanked ? "yanked" : null,
168
+ dependencies: null,
169
+ };
170
+ }
171
+ /**
172
+ * True once every wanted version is present AND a release (not a prerelease)
173
+ * below all of them has been seen. Pages arrive in descending semver order,
174
+ * so at that point the release just below the target on its line — the
175
+ * new-dependency trust baseline — has been seen too, if it exists.
176
+ */
177
+ export function pagesSatisfy(seen, wanted) {
178
+ if (wanted.length === 0)
179
+ return false;
180
+ const have = new Set(seen);
181
+ if (!wanted.every((w) => have.has(w)))
182
+ return false;
183
+ return seen.some((v) => {
184
+ const p = parseSemverPrecedence(v);
185
+ if (!p || p.prerelease.length > 0)
186
+ return false;
187
+ return wanted.every((w) => (compareVersionPrecedence(v, w) ?? 0) < 0);
188
+ });
189
+ }
190
+ function sparseIndexUrl(name) {
191
+ const n = name.toLowerCase();
192
+ switch (n.length) {
193
+ case 1: return `https://index.crates.io/1/${n}`;
194
+ case 2: return `https://index.crates.io/2/${n}`;
195
+ case 3: return `https://index.crates.io/3/${n[0]}/${n}`;
196
+ default: return `https://index.crates.io/${n.slice(0, 2)}/${n.slice(2, 4)}/${n}`;
197
+ }
198
+ }
199
+ function errorText(err) {
200
+ return err instanceof Error ? err.message : String(err);
201
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Set operations over resolved dependencies, shared by the live layer.
3
+ * (Moved out of live/index.ts; behaviour unchanged except for the install
4
+ * fields, which did not exist before.)
5
+ */
6
+ import type { ResolvedDependency, VulnerabilityScanResult } from "./types.js";
7
+ /**
8
+ * Deepest common ancestor of a set of directories (segment-wise, both slash
9
+ * styles). Null for an empty set; a single dir is its own root.
10
+ */
11
+ export declare function commonPathRoot(dirs: string[]): string | null;
12
+ export declare function dedupeDependencies(deps: ResolvedDependency[]): ResolvedDependency[];
13
+ export declare function emptyVulnResult(projectPath: string, offline: boolean): VulnerabilityScanResult;