@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,178 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Which crates in a Cargo workspace are actually built on THIS host.
4
+ *
5
+ * Cargo.lock is target-agnostic: it lists every crate for every platform the
6
+ * tree can compile for. So a Windows machine's lockfile still contains the
7
+ * whole Linux GTK3 stack that Tauri pulls in — `gtk`, `gdk`, `atk`, `glib`,
8
+ * `gdkx11`, `gdkwayland-sys`, `gtk3-macros` — none of which are ever compiled
9
+ * there. Advisories against them are unreachable noise on that host.
10
+ *
11
+ * `vulnerability_scan` claimed to filter for exactly this ("Advisories are
12
+ * filtered to the host platform") while reporting `platform_inactive_packages:
13
+ * 0` and listing nine Linux-only crates on a Windows box. The gate it relied on
14
+ * only ever saw `[target.'cfg(...)'.dependencies]` entries from a manifest,
15
+ * which covers DIRECT platform-gated deps and cannot see transitives — and the
16
+ * GTK3 cluster is entirely transitive.
17
+ *
18
+ * Cargo.lock is also FEATURE-agnostic: it lists every optional dependency,
19
+ * including the ones no enabled feature turns on. `cargo tree` resolves both
20
+ * axes — target AND features — and is what the build itself does.
21
+ *
22
+ * This used `cargo metadata --filter-platform <triple>`, which resolves only
23
+ * the target axis. Measured on 4DA's own workspace, 2026-09-08, host
24
+ * `x86_64-pc-windows-msvc`:
25
+ *
26
+ * | mechanism | distinct crates | `quinn-proto` |
27
+ * |---------------------------------------------|-----------------|---------------|
28
+ * | `Cargo.lock` | 788 | present |
29
+ * | `cargo metadata --filter-platform <triple>` | 560 | **present** |
30
+ * | `cargo tree` (host, default features) | 538 | absent |
31
+ *
32
+ * `cargo metadata` keeps the `reqwest -> quinn` edge even though `reqwest`'s
33
+ * enabled feature set contains no `http3` — so `quinn-proto`, a crate that has
34
+ * never been compiled on this machine, counted as built-on-host. It was
35
+ * Preemption's #1 item on 2026-09-07 (HIGH, "version-confirmed"). `cargo tree`
36
+ * excludes it. Same predicate, same answer, as the app's
37
+ * `src-tauri/src/ace/cargo_resolve.rs`.
38
+ *
39
+ * When cargo is unavailable the answer is `null` — *unknown*, never "nothing is
40
+ * inactive". Callers surface that distinction rather than quietly asserting a
41
+ * filter they did not run.
42
+ */
43
+ import { execFileSync } from "node:child_process";
44
+ import * as fs from "node:fs";
45
+ import * as os from "node:os";
46
+ import * as path from "node:path";
47
+ import { fileSignature } from "./file-signature.js";
48
+ /** Rust target triple for the current host, or null for a platform we do not map. */
49
+ export function hostTriple() {
50
+ const arch = process.arch === "x64" ? "x86_64" : process.arch === "arm64" ? "aarch64" : null;
51
+ if (!arch)
52
+ return null;
53
+ switch (process.platform) {
54
+ case "win32":
55
+ return `${arch}-pc-windows-msvc`;
56
+ case "darwin":
57
+ return `${arch}-apple-darwin`;
58
+ case "linux":
59
+ return `${arch}-unknown-linux-gnu`;
60
+ default:
61
+ return null;
62
+ }
63
+ }
64
+ /**
65
+ * Per-directory memo, valid only while the workspace's Cargo.lock and
66
+ * Cargo.toml are unchanged. The MCP server now re-resolves dependencies when a
67
+ * lockfile changes, and server processes live for days (2026-09-10: two were
68
+ * older than the lockfile they answered for). A memo keyed on the directory
69
+ * alone would hand a re-resolution the OLD crate set, and every crate the new
70
+ * lockfile added would read as not-built-on-host, hiding its advisories under
71
+ * platform_inactive.
72
+ */
73
+ const cache = new Map();
74
+ /** Reset the memo. Tests only. */
75
+ export function _resetCargoPlatformCache() {
76
+ cache.clear();
77
+ }
78
+ /**
79
+ * Crate names cargo resolves for this host in the workspace at `dir`.
80
+ *
81
+ * Returns `null` — meaning *unknown*, never "nothing" — when there is no
82
+ * Cargo.toml, no host triple mapping, or cargo is missing/fails. A `null` must
83
+ * never be treated as an empty set: that would mark every crate inactive and
84
+ * silently hide real advisories.
85
+ */
86
+ export function activeCratesForHost(dir) {
87
+ const key = path.resolve(dir);
88
+ const signature = `${fileSignature(path.join(key, "Cargo.lock"))}|${fileSignature(path.join(key, "Cargo.toml"))}`;
89
+ const memo = cache.get(key);
90
+ if (memo !== undefined && memo.signature === signature)
91
+ return memo.crates;
92
+ const crates = computeActiveCrates(key);
93
+ cache.set(key, { signature, crates });
94
+ return crates;
95
+ }
96
+ function computeActiveCrates(dir) {
97
+ if (!hostTriple())
98
+ return null;
99
+ if (!fs.existsSync(path.join(dir, "Cargo.toml")))
100
+ return null;
101
+ let raw;
102
+ try {
103
+ const { args, cwd, targetDir } = cargoTreeInvocation(dir);
104
+ fs.mkdirSync(cwd, { recursive: true });
105
+ raw = execFileSync("cargo", args, {
106
+ cwd,
107
+ env: { ...process.env, CARGO_TARGET_DIR: targetDir },
108
+ encoding: "utf8",
109
+ maxBuffer: 64 * 1024 * 1024,
110
+ stdio: ["ignore", "pipe", "ignore"],
111
+ timeout: 30_000,
112
+ windowsHide: true,
113
+ });
114
+ }
115
+ catch {
116
+ // cargo absent, not a workspace, offline resolution impossible, a stale
117
+ // lockfile, a held package-cache lock, or a timeout.
118
+ return null;
119
+ }
120
+ const names = parseTreeNames(raw);
121
+ // An empty result is not a credible answer for a real workspace; treat it
122
+ // as unknown so nothing gets marked inactive on a parse quirk.
123
+ return names.size > 0 ? names : null;
124
+ }
125
+ /**
126
+ * How `cargo tree` is invoked for the workspace at `dir`.
127
+ *
128
+ * cargo runs from a server-owned scratch directory and reaches the project
129
+ * only through `--manifest-path`. cargo and rustup take their configuration
130
+ * (`.cargo/config.toml`, `rust-toolchain.toml`) from the working directory and
131
+ * its parents, so running inside the scanned project would let files in that
132
+ * project configure the toolchain this server invokes. A scanned project is
133
+ * untrusted input. `CARGO_TARGET_DIR` points at the same scratch directory so
134
+ * cargo's metadata writes never land in the user's tree either.
135
+ *
136
+ * Exported for tests.
137
+ */
138
+ export function cargoTreeInvocation(dir) {
139
+ const scratch = path.join(os.tmpdir(), "4da-mcp-cargo-resolve");
140
+ return {
141
+ args: [
142
+ "tree",
143
+ "--manifest-path",
144
+ path.join(path.resolve(dir), "Cargo.toml"),
145
+ // Never touch the network or mutate the lockfile from a read-only
146
+ // scan. `--locked` also means a lockfile out of step with the manifest
147
+ // fails loudly here rather than being silently re-resolved.
148
+ "--offline",
149
+ "--locked",
150
+ // One package per line, no tree glyphs: `name vX.Y.Z [(...)]`.
151
+ "--prefix",
152
+ "none",
153
+ // dev-dependencies are compiled by `cargo test`, so an advisory
154
+ // against one is reachable. proc-macro deps ride along with normal.
155
+ "--edges",
156
+ "normal,build,dev",
157
+ ],
158
+ cwd: scratch,
159
+ targetDir: scratch,
160
+ };
161
+ }
162
+ /**
163
+ * Crate names from `cargo tree --prefix none` output.
164
+ *
165
+ * Exported for tests: the parser is the part that can silently degrade to an
166
+ * empty set, and an empty set is the one answer that must never be believed.
167
+ */
168
+ export function parseTreeNames(stdout) {
169
+ const names = new Set();
170
+ for (const line of stdout.split(/\r?\n/)) {
171
+ const token = line.split(/\s+/).find((t) => t.length > 0);
172
+ // A package line starts with a crate name. Anything else — a blank line,
173
+ // a `[dev-dependencies]` header from a future cargo, a warning — is not.
174
+ if (token && /^[A-Za-z0-9._+-]+$/.test(token))
175
+ names.add(token);
176
+ }
177
+ return names;
178
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Changelog entry classification and sanitisation.
3
+ *
4
+ * A changelog is third-party text that ends up in an agent's context, so
5
+ * every entry is cleaned before it leaves this module: ASCII control
6
+ * characters and the zero-width / bidi-override code points that make text
7
+ * render differently from how it reads (U+200B–U+200F, U+202A–U+202E,
8
+ * U+2066–U+2069, U+FEFF) are stripped, whitespace is collapsed, and each
9
+ * entry is capped at 400 characters so one runaway paragraph cannot crowd
10
+ * out the rest of the report.
11
+ *
12
+ * Classification is keyword-based on purpose: changelogs follow no schema,
13
+ * and a wrong guess is cheap here because every entry carries its text — the
14
+ * kind decides ordering and what concise mode keeps, never whether an entry
15
+ * exists. An explicit section heading ("### Breaking Changes", keep-a-
16
+ * changelog's "### Removed" / "### Deprecated" / "### Security") outranks
17
+ * keywords in the line itself.
18
+ */
19
+ export type EntryKind = "breaking" | "deprecation" | "security" | "change";
20
+ export declare const MAX_ENTRY_CHARS = 400;
21
+ /** Strip control and invisible/bidi characters, collapse whitespace, cap length. */
22
+ export declare function sanitizeEntry(text: string, cap?: number): string;
23
+ /** Kind of a single changelog line by its own wording. */
24
+ export declare function classifyText(text: string): EntryKind;
25
+ /**
26
+ * What a sub-heading (or a label-only bullet such as express's "* remove:")
27
+ * says about the entries under it:
28
+ * - an EntryKind: the author's explicit call ("BREAKING CHANGES", "⚠",
29
+ * changesets' "Major Changes", "Security", "Deprecated");
30
+ * - "removal": a removal list ("Removed", "remove:", "API changes"), breaking
31
+ * except for internal clean-up and deprecation notices filed under it (axios
32
+ * 1.0 removed "unused imports" there; nix files deprecations under Removed);
33
+ * - "additive": additions and fixes ("Added", "Fixed", "Features", "Other"),
34
+ * where only an explicit marker makes an entry breaking ("Added a clear()
35
+ * function ... removed from an axios instance" is not a removal).
36
+ */
37
+ export type EntryContext = EntryKind | "removal" | "additive";
38
+ /** Context implied by a heading or label, or null when it carries no signal ("### Changed"). */
39
+ export declare function classifyHeading(heading: string): EntryContext | null;
40
+ /** Final kind: an explicit heading or bullet parent wins; removal and additive contexts refine the line's wording. */
41
+ export declare function classifyEntry(text: string, context: EntryContext | null): EntryKind;
@@ -0,0 +1,207 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Changelog entry classification and sanitisation.
4
+ *
5
+ * A changelog is third-party text that ends up in an agent's context, so
6
+ * every entry is cleaned before it leaves this module: ASCII control
7
+ * characters and the zero-width / bidi-override code points that make text
8
+ * render differently from how it reads (U+200B–U+200F, U+202A–U+202E,
9
+ * U+2066–U+2069, U+FEFF) are stripped, whitespace is collapsed, and each
10
+ * entry is capped at 400 characters so one runaway paragraph cannot crowd
11
+ * out the rest of the report.
12
+ *
13
+ * Classification is keyword-based on purpose: changelogs follow no schema,
14
+ * and a wrong guess is cheap here because every entry carries its text — the
15
+ * kind decides ordering and what concise mode keeps, never whether an entry
16
+ * exists. An explicit section heading ("### Breaking Changes", keep-a-
17
+ * changelog's "### Removed" / "### Deprecated" / "### Security") outranks
18
+ * keywords in the line itself.
19
+ */
20
+ export const MAX_ENTRY_CHARS = 400;
21
+ const CONTROL = /[\u0000-\u0009\u000B-\u001F\u007F]/g;
22
+ const INVISIBLE = /[​-‏‪-‮⁦-⁩]/g;
23
+ /** Strip control and invisible/bidi characters, collapse whitespace, cap length. */
24
+ export function sanitizeEntry(text, cap = MAX_ENTRY_CHARS) {
25
+ const clean = text.replace(INVISIBLE, "").replace(CONTROL, " ").replace(/\s+/g, " ").trim();
26
+ return clean.length > cap ? `${clean.slice(0, cap - 1)}…` : clean;
27
+ }
28
+ /**
29
+ * Explicit markers: these win even over a security keyword in the same line.
30
+ * Upper-case BREAKING and conventional-commit `feat!:` are case-sensitive
31
+ * markers; the prose form must not be negated — tokio 1.44.0's entry
32
+ * "We determined this change is not a breaking change" was counted as
33
+ * breaking on the first live run.
34
+ */
35
+ // `BREAKING` but not the file name `BREAKING-CHANGES.md` (ratatui links it from routine entries);
36
+ // a leading "Breaking -" / "[Breaking]" label too (webpack-merge 5, handlebars-rust).
37
+ // Case-sensitive on purpose: lowercase "breaking" is prose, handled (with negation) below.
38
+ const EXPLICIT_BREAKING_MARKER = /\bBREAKING\b(?![-_.]\w)|⚠|^\s*\*{0,2}\w+(?:\([^)]*\))?!:|^\s*\*{0,2}[Bb]reaking\*{0,2}\s*(?::|[-–—]\s)|^\s*\[(?:[Bb]reaking|[Rr]emoved)\]/;
39
+ /**
40
+ * Text ABOUT breaking changes rather than one: a pointer to the list ("⚠️ List
41
+ * of breaking changes can be found here"), a CI label check, a docs fix
42
+ * (ratatui 0.24-0.26: 8 of 16 marker-based flags on 2026-10-03 were these).
43
+ */
44
+ const META_BREAKING = /\b(?:list|lists|see|found|documented|described|summary|guide|doc|docs|documentation|label|labels|check|technically|despite|mentioning)\b[^.]{0,40}\bbreaking[- ]changes?\b|\bbreaking[- ]changes?\b[^.]{0,30}\b(?:can be found|are (?:listed|documented|described)|described|label|labels|doc|docs|documentation)\b|BREAKING[-_]CHANGES/i;
45
+ /**
46
+ * Intent, not a change: "We reserve the right to drop support for ...",
47
+ * "will be removed in a future release".
48
+ */
49
+ const FUTURE_INTENT = /\b(?:reserve the right|will(?: be)?|may(?: be)?|might|plan(?:s|ning)? to|intend(?:s)? to|in (?:a|the) future|eventually)\b[^.]{0,40}\b(?:drop|dropp|remov|renam|deprecat)/i;
50
+ /** A fix-led entry ("Fixed nightly build where Generator was renamed to Coroutine"): removal words in it are prose. */
51
+ const LEADING_FIX = /^\W*(?:\*\*[^*]{1,40}\*\*:?\s*)?(?:fix|fixed|fixes|fixing)\b/i;
52
+ /**
53
+ * "breaking change" in prose, except when negated, when it is a compound
54
+ * ("reproducibility-breaking": rand 0.9's changed outputs, not its API), and
55
+ * when it names another crate's release (regex 1.10: "`regex-syntax` breaking
56
+ * change release"). Measured 2026-10-02 against a 3-rater panel.
57
+ */
58
+ const EXPLICIT_BREAKING_PROSE = /(?<!\bnot (?:a )?|\bnon[- ]|\bno |-)\bbreaking[- ]changes?\b(?! release)/i;
59
+ /** A yank notice mentions breakage without being a change to the API ("Yanked ... due to unforeseen breaking change"). */
60
+ const YANK_NOTICE = /\byanked\b/i;
61
+ /**
62
+ * Inline labels that say an entry is NOT a breaking change (axum's
63
+ * `**fixed:** Removed the warning about breaking changes from README`,
64
+ * conventional `fix:` / `docs:`). Such entries keep only their security and
65
+ * deprecation signal; removal/rename keywords inside them are prose.
66
+ */
67
+ const NON_BREAKING_LABEL = /^\s*\*{0,2}(?:fixed|fix|fixes|added|add|feat|features?|docs?|perf|chore|internal|improved|tests?|ci|build|refactor|style)(?:\([^)]*\))?\*{0,2}:\*{0,2}\s/i;
68
+ const SECURITY = /\bsecurity\b|\bCVE-\d{4}-\d+|\bGHSA-[\w-]+|\bRUSTSEC-\d{4}-\d+|vulnerab/i;
69
+ /**
70
+ * Wording that changes the API a caller compiles or runs against. Each pattern
71
+ * is narrower than the keyword it replaced: bare "no longer" also matched
72
+ * "No longer panics when ..." (a fix), bare "MSRV" matched "Add
73
+ * `Cargo.lock.msrv` file", and only past-tense "removed"/"renamed" were seen,
74
+ * so rand 0.9's "Rename fn `thread_rng()` to `rng()`" was missed.
75
+ *
76
+ * Removal and rename wording is breaking unless what went is internal (see
77
+ * INTERNAL_CHANGE); the other API wording stands whatever else the line mentions
78
+ * ("MSRV is now 1.70 because of a dependency update").
79
+ */
80
+ const REMOVAL_HEURISTIC = new RegExp([
81
+ "\\bremoved\\b",
82
+ "\\bremoves\\b",
83
+ "\\b(?:has|have|was|were) been dropped\\b",
84
+ "\\brenamed?\\b",
85
+ // The removed thing is the object of "remove": a code name within a few words
86
+ // ("Remove first parameter (`rng`) of ..."), not a backtick somewhere later
87
+ // ("Remove a window when ... `cd`able", a security fix).
88
+ "^\\W*(?:remove|delete)\\b(?:\\s+[\\w-]+){0,3}\\s*\\(?`",
89
+ // ... or a qualified name without backticks ("remove Socket#rooms object").
90
+ "^\\W*(?:remove|delete)\\b(?:\\s+[\\w-]+){0,2}\\s+[A-Za-z_]\\w*(?:#|::|\\.)[A-Za-z_]",
91
+ ].join("|"), "i");
92
+ /** Wording that changes the API or its requirements, whatever else the line mentions. */
93
+ const BREAKING_HEURISTIC = new RegExp([
94
+ // Not "no longer require(s)": that is a relaxation ("`Data::get_ref()` no longer
95
+ // requires `T: Sized`", four actix-web 4 entries the panel called non-breaking).
96
+ "\\bno longer (?:accepts?|returns?|supports?|exports?|exported|exposed|re-?exports?|available|provides?|allow(?:s|ed)?|implements?|public|includes?|ships?|compiles?|works? with)\\b",
97
+ "\\bcan no longer\\b",
98
+ "\\bnow (?:also )?returns?\\b",
99
+ // Rust: marking a public type non-exhaustive breaks exhaustive matches and struct literals.
100
+ "\\b(?:now )?marked (?:as )?`?#\\[non_exhaustive\\]",
101
+ "\\bdrop(?:ped|s)? (?:support|compatibility)\\b",
102
+ "\\bdropped\\b.*\\b(?:support|compatibility)\\b",
103
+ // A raise names a version: "Bump MSRV to 1.63"; "Update MSRV in cron job" (config 0.14, CI) does not.
104
+ // A window of .{0,60}, not [^.]: versions carry dots ("MSRV: Rust 1.64.0 or later is now required").
105
+ "\\b(?:bump|bumped|raise|raised|increase|increased|update|updated|require|requires|now)\\b.{0,60}\\b(?:MSRV|minimum supported rust version|rust-version)\\b.{0,30}\\d+\\.\\d+",
106
+ // A raise names the new version ("MSRV is now 1.70", "MSRV: Rust 1.64.0 or later");
107
+ // "Move MSRV metadata to `Cargo.toml`" names none.
108
+ "\\b(?:MSRV|minimum supported rust version)\\b.{0,60}?\\b(?:is now|to|bumped to|raised to|increased to)\\s+(?:rust\\s+|rustc\\s+)?v?\\d",
109
+ "\\b(?:MSRV|minimum supported rust version)\\b.{0,60}\\d+\\.\\d+.{0,20}\\bor later\\b",
110
+ "\\bnow (?:requires?|required|returns?|takes?|accepts? only)\\b",
111
+ "\\b(?:function|method|type|the) signatures?\\b",
112
+ "\\bsignatures? (?:of|has|have|changed)\\b",
113
+ "\\b(?:changed?|new|different)\\b[^.]{0,30}\\b(?:return|argument|parameter) types?\\b",
114
+ "\\b(?:return|argument|parameter) types? (?:have |has )?(?:changed|change)\\b",
115
+ "\\btypes have changed\\b",
116
+ // An API that now implements/returns/takes a different type; "Use `Cell` instead
117
+ // of `RefCell` in `Format`" is an internal choice, not an API change.
118
+ "\\b(?:implements?|returns?|takes?|accepts?|requires?|yields?|expects?)\\b[^.]{0,40}`[^`]+`\\s+instead of\\s+`[^`]+`",
119
+ "\\(instead of `",
120
+ // An API that now receives/uses a different type, or moved out of the crate
121
+ // (actix-web 4: "`guard::fn_guard` functions now receives a `&GuardContext`",
122
+ // "`test::start`; moved to new `actix-test` crate").
123
+ "\\bnow (?:receives?|uses?)\\b[^.]{0,30}`",
124
+ "\\bmoved (?:in)?to (?:the |a )?(?:new |separate )?`?[\\w-]+`? (?:crate|package|module)\\b",
125
+ "\\bto keep the (?:old|previous|former) behaviou?r\\b",
126
+ "\\bincompatib",
127
+ ].join("|"), "i");
128
+ /** Removals that change nothing a caller sees: "Removed unused imports", "Removed Webpack". */
129
+ const INTERNAL_CHANGE = /\b(?:unused|unneeded|unnecessary|internal(?:ly)?|dead code|tests?|ci|lint(?:ing)?|typos?|comments?|docs?|documentation|readme|redundant|duplicated?|webpack|dev-?dependenc(?:y|ies)|dependency|warnings?|examples?|benchmarks?|release process|build process|tooling|usage|incorrect|wrong|erroneous|stray|spurious)\b/i;
130
+ const DEPRECATION = /deprecat/i;
131
+ /**
132
+ * An entry whose main verb is "deprecate" is a deprecation even when it names
133
+ * the replacement ("Deprecated `Itertools::group_by` (renamed `chunk_by`)"):
134
+ * the old name still works.
135
+ */
136
+ const LEADING_DEPRECATION = /^\W*(?:\*\*[^*]{1,40}\*\*:?\s*)?(?:deprecated?|deprecates|deprecating)\b/i;
137
+ /** Kind of a single changelog line by its own wording. */
138
+ export function classifyText(text) {
139
+ if (META_BREAKING.test(text))
140
+ return SECURITY.test(text) ? "security" : "change";
141
+ if (EXPLICIT_BREAKING_MARKER.test(text))
142
+ return "breaking";
143
+ if (NON_BREAKING_LABEL.test(text)) {
144
+ if (SECURITY.test(text))
145
+ return "security";
146
+ return DEPRECATION.test(text) ? "deprecation" : "change";
147
+ }
148
+ if (YANK_NOTICE.test(text))
149
+ return "change";
150
+ if (LEADING_DEPRECATION.test(text))
151
+ return "deprecation";
152
+ // A rename that keeps the old name working ("Rename `x => y`, deprecating the old name").
153
+ if (/\brenam/i.test(text) && DEPRECATION.test(text) && !/\bremov/i.test(text))
154
+ return "deprecation";
155
+ if (EXPLICIT_BREAKING_PROSE.test(text))
156
+ return "breaking";
157
+ if (SECURITY.test(text))
158
+ return "security";
159
+ if (FUTURE_INTENT.test(text))
160
+ return DEPRECATION.test(text) ? "deprecation" : "change";
161
+ if (BREAKING_HEURISTIC.test(text))
162
+ return "breaking";
163
+ if (REMOVAL_HEURISTIC.test(text) && !INTERNAL_CHANGE.test(text) && !LEADING_FIX.test(text))
164
+ return "breaking";
165
+ if (DEPRECATION.test(text))
166
+ return "deprecation";
167
+ return "change";
168
+ }
169
+ const ADDITIVE_HEADING = /^\W*(?:added|adds|new|new features?|features?|enhancements?|improvements?|fixed|fixes|bug ?fixes|performance(?: improvements)?|perf|optimi[sz]ations?|docs?|documentation|tests?|testing|internal|chores?|ci|build|refactor(?:ing)?|misc(?:ellaneous)?|other(?: changes)?)\W*$/i;
170
+ /** Context implied by a heading or label, or null when it carries no signal ("### Changed"). */
171
+ export function classifyHeading(heading) {
172
+ if (/(?<!-)\bbreaking\b|⚠|incompatib|migration|\bmajor changes?\b/i.test(heading))
173
+ return "breaking";
174
+ if (/\bremov(?:e|ed|als?)\b|\bapi changes?\b/i.test(heading))
175
+ return "removal";
176
+ if (/security|vulnerab/i.test(heading))
177
+ return "security";
178
+ if (/deprecat/i.test(heading))
179
+ return "deprecation";
180
+ if (ADDITIVE_HEADING.test(heading))
181
+ return "additive";
182
+ return null;
183
+ }
184
+ /** Final kind: an explicit heading or bullet parent wins; removal and additive contexts refine the line's wording. */
185
+ export function classifyEntry(text, context) {
186
+ if (context === "breaking" || context === "security" || context === "deprecation") {
187
+ // A security line under "Breaking" stays breaking: the heading is the author's own call.
188
+ return context;
189
+ }
190
+ if (context === "removal") {
191
+ if (META_BREAKING.test(text))
192
+ return "change";
193
+ if (EXPLICIT_BREAKING_MARKER.test(text))
194
+ return "breaking";
195
+ if (DEPRECATION.test(text) && !/\bremov/i.test(text))
196
+ return "deprecation";
197
+ return INTERNAL_CHANGE.test(text) ? "change" : "breaking";
198
+ }
199
+ if (context === "additive") {
200
+ if (!META_BREAKING.test(text) && EXPLICIT_BREAKING_MARKER.test(text))
201
+ return "breaking";
202
+ if (SECURITY.test(text))
203
+ return "security";
204
+ return DEPRECATION.test(text) ? "deprecation" : "change";
205
+ }
206
+ return classifyText(text);
207
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Changelog discovery and parsing for `upgrade_impact`.
3
+ *
4
+ * Changelogs follow no schema, so the parser recognises the version-heading
5
+ * styles actually found in registry archives rather than one spec:
6
+ * keep-a-changelog `## [1.2.3] - 2024-01-01`
7
+ * conventional `## [7.0.0](https://…/compare/…) (2025-06-24)`
8
+ * plain / prefixed `## 1.2.3`, `# v1.2.3 (2024-01-01)`, `### Version 1.2.3`, `## fastembed 5.0.0`
9
+ * setext `1.2.3` underlined with `===` / `---`
10
+ * History.md (express) `1.2.3 / 2020-01-01`
11
+ * A heading only opens a section when the rest of it looks like a trailer
12
+ * (date, link, "yanked") — `## Upgrading from 1.x` or `#### Rust 1.70 support`
13
+ * must stay sub-headings, or a migration guide would split a release in two.
14
+ *
15
+ * Entries are bullets, numbered items and loose paragraph lines; indented
16
+ * continuation lines join the entry above. Fenced code blocks and link
17
+ * reference definitions are skipped: they are examples and URLs, not changes.
18
+ */
19
+ import { type EntryKind } from "./changelog-classify.js";
20
+ export interface ChangelogEntry {
21
+ kind: EntryKind;
22
+ text: string;
23
+ /**
24
+ * The heading, label or parent bullet the entry sits under ("Removed", "remove",
25
+ * "BREAKING: Functions that accept ..."). Without it "`rt::{Arbiter}` re-exports."
26
+ * under actix-web's "Removed" reads as harmless, to an agent and to a rater alike.
27
+ */
28
+ under?: string;
29
+ }
30
+ export interface ChangelogSection {
31
+ version: string;
32
+ date: string | null;
33
+ entries: ChangelogEntry[];
34
+ }
35
+ /** True for a root-level basename that names a changelog (case-insensitive). */
36
+ export declare function isChangelogName(basename: string): boolean;
37
+ /**
38
+ * The changelog among archive paths, preferring CHANGELOG over the others and
39
+ * Markdown over plain text. Null when there is none.
40
+ */
41
+ export declare function findChangelogFile(paths: Iterable<string>): string | null;
42
+ /**
43
+ * Version and date from a heading's text, or null when it is not a release
44
+ * heading. Exported for tests.
45
+ */
46
+ export declare function parseVersionHeading(raw: string): {
47
+ version: string;
48
+ date: string | null;
49
+ } | null;
50
+ /** Parse a changelog into version sections, in document order (usually newest first). */
51
+ export declare function parseChangelog(text: string): ChangelogSection[];
52
+ /** True when `version` lies in (from, to] by semver precedence; unreadable versions are out. */
53
+ export declare function inRange(version: string, from: string, to: string): boolean;
54
+ /**
55
+ * Sections in (from, to], plus whether the changelog reaches both ends of the
56
+ * range: its newest section is at or past `to`, and its oldest at or before
57
+ * `firstInRange` (the earliest release the upgrade crosses). A changelog that
58
+ * stopped being maintained two majors ago is reported as not covering the
59
+ * range instead of silently presenting a partial history as the whole one.
60
+ */
61
+ export declare function selectRange(sections: ChangelogSection[], from: string, to: string, firstInRange: string): {
62
+ sections: ChangelogSection[];
63
+ coversRange: boolean;
64
+ };