@4da/mcp-server 5.0.4 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +667 -0
- package/README.md +66 -35
- package/dist/build-staleness.d.ts +35 -0
- package/dist/build-staleness.js +95 -0
- package/dist/db.d.ts +53 -2
- package/dist/db.js +233 -37
- package/dist/doctor.js +27 -7
- package/dist/embeddings.js +7 -1
- package/dist/freshness-markers.d.ts +34 -0
- package/dist/freshness-markers.js +74 -0
- package/dist/gitignore.d.ts +37 -0
- package/dist/gitignore.js +130 -0
- package/dist/index.js +127 -44
- package/dist/live/bun-lockfile.d.ts +26 -0
- package/dist/live/bun-lockfile.js +123 -0
- package/dist/live/cargo-platform.d.ts +38 -0
- package/dist/live/cargo-platform.js +178 -0
- package/dist/live/changelog-classify.d.ts +41 -0
- package/dist/live/changelog-classify.js +207 -0
- package/dist/live/changelog.d.ts +64 -0
- package/dist/live/changelog.js +318 -0
- package/dist/live/crates-registry.js +3 -1
- package/dist/live/crates-versions.d.ts +47 -0
- package/dist/live/crates-versions.js +201 -0
- package/dist/live/dependency-set.d.ts +13 -0
- package/dist/live/dependency-set.js +95 -0
- package/dist/live/dev-scope.d.ts +32 -0
- package/dist/live/dev-scope.js +102 -0
- package/dist/live/file-signature.d.ts +18 -0
- package/dist/live/file-signature.js +43 -0
- package/dist/live/go-registry.js +3 -1
- package/dist/live/hn-fetcher.js +9 -3
- package/dist/live/index.d.ts +105 -2
- package/dist/live/index.js +266 -139
- package/dist/live/install-state.d.ts +43 -0
- package/dist/live/install-state.js +150 -0
- package/dist/live/js-lockfile-readers.d.ts +46 -0
- package/dist/live/js-lockfile-readers.js +181 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +60 -0
- package/dist/live/lockfile-parsers-pygo.js +274 -0
- package/dist/live/lockfile-parsers.d.ts +69 -0
- package/dist/live/lockfile-parsers.js +311 -0
- package/dist/live/lockfile-types.d.ts +34 -0
- package/dist/live/lockfile-types.js +42 -0
- package/dist/live/maintenance.d.ts +33 -0
- package/dist/live/maintenance.js +37 -0
- package/dist/live/npm-packument.d.ts +47 -0
- package/dist/live/npm-packument.js +150 -0
- package/dist/live/npm-registry.js +67 -53
- package/dist/live/osv-scanner.d.ts +12 -1
- package/dist/live/osv-scanner.js +50 -15
- package/dist/live/package-archive.d.ts +49 -0
- package/dist/live/package-archive.js +186 -0
- package/dist/live/pypi-registry.js +3 -1
- package/dist/live/registry-health.d.ts +16 -0
- package/dist/live/registry-health.js +74 -0
- package/dist/live/release-metadata.d.ts +67 -0
- package/dist/live/release-metadata.js +16 -0
- package/dist/live/resolution.d.ts +43 -0
- package/dist/live/resolution.js +62 -0
- package/dist/live/semver-precedence.d.ts +30 -0
- package/dist/live/semver-precedence.js +83 -0
- package/dist/live/severity-scope.d.ts +57 -0
- package/dist/live/severity-scope.js +89 -0
- package/dist/live/types.d.ts +83 -0
- package/dist/live/upgrade-sources.d.ts +71 -0
- package/dist/live/upgrade-sources.js +198 -0
- package/dist/live/version-compare.d.ts +39 -0
- package/dist/live/version-compare.js +130 -0
- package/dist/live/version-resolver.d.ts +14 -1
- package/dist/live/version-resolver.js +93 -303
- package/dist/native-bindings.d.ts +19 -0
- package/dist/native-bindings.js +39 -0
- package/dist/project-manifests.d.ts +40 -0
- package/dist/project-manifests.js +193 -0
- package/dist/project-scanner.js +38 -48
- package/dist/project-tree.d.ts +41 -0
- package/dist/project-tree.js +110 -0
- package/dist/prompts.d.ts +28 -0
- package/dist/prompts.js +54 -0
- package/dist/schema-registry.d.ts +41 -29
- package/dist/schema-registry.js +98 -105
- package/dist/server-instructions.d.ts +11 -0
- package/dist/server-instructions.js +36 -0
- package/dist/tool-args.d.ts +28 -0
- package/dist/tool-args.js +93 -0
- package/dist/tool-dispatch.d.ts +1 -1
- package/dist/tool-dispatch.js +42 -8
- package/dist/tools/app-plan.d.ts +92 -0
- package/dist/tools/app-plan.js +155 -0
- package/dist/tools/briefing-stored-advisories.d.ts +20 -0
- package/dist/tools/briefing-stored-advisories.js +58 -0
- package/dist/tools/briefing-task-scope.d.ts +73 -0
- package/dist/tools/briefing-task-scope.js +197 -0
- package/dist/tools/briefing-wisdom.d.ts +31 -0
- package/dist/tools/briefing-wisdom.js +148 -0
- package/dist/tools/decision-enforcement.d.ts +0 -5
- package/dist/tools/decision-enforcement.js +0 -4
- package/dist/tools/decision-memory.js +18 -66
- package/dist/tools/dependency-check-signals.d.ts +74 -0
- package/dist/tools/dependency-check-signals.js +264 -0
- package/dist/tools/dependency-check.d.ts +98 -0
- package/dist/tools/dependency-check.js +295 -0
- package/dist/tools/dependency-health.js +74 -21
- package/dist/tools/developer-dna.d.ts +3 -0
- package/dist/tools/developer-dna.js +16 -15
- package/dist/tools/ecosystem-pulse.d.ts +6 -2
- package/dist/tools/ecosystem-pulse.js +53 -14
- package/dist/tools/get-actionable-signals.d.ts +26 -4
- package/dist/tools/get-actionable-signals.js +214 -251
- package/dist/tools/get-context.d.ts +2 -0
- package/dist/tools/get-context.js +9 -1
- package/dist/tools/get-relevant-content.d.ts +3 -1
- package/dist/tools/get-relevant-content.js +34 -3
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +4 -2
- package/dist/tools/install-drift-notes.d.ts +51 -0
- package/dist/tools/install-drift-notes.js +172 -0
- package/dist/tools/knowledge-gap-citations.d.ts +55 -0
- package/dist/tools/knowledge-gap-citations.js +132 -0
- package/dist/tools/knowledge-gap-exposure.d.ts +72 -0
- package/dist/tools/knowledge-gap-exposure.js +160 -0
- package/dist/tools/knowledge-gap-grading.d.ts +91 -0
- package/dist/tools/knowledge-gap-grading.js +157 -0
- package/dist/tools/knowledge-gap-installs.d.ts +65 -0
- package/dist/tools/knowledge-gap-installs.js +134 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +51 -0
- package/dist/tools/knowledge-gap-ranges.js +146 -0
- package/dist/tools/knowledge-gap-scope.d.ts +63 -0
- package/dist/tools/knowledge-gap-scope.js +118 -0
- package/dist/tools/knowledge-gaps.d.ts +12 -3
- package/dist/tools/knowledge-gaps.js +172 -121
- package/dist/tools/package-presence.d.ts +24 -0
- package/dist/tools/package-presence.js +35 -0
- package/dist/tools/record-feedback.d.ts +0 -1
- package/dist/tools/record-feedback.js +7 -8
- package/dist/tools/signal-classifier.d.ts +25 -0
- package/dist/tools/signal-classifier.js +40 -0
- package/dist/tools/upgrade-impact-callsites.d.ts +52 -0
- package/dist/tools/upgrade-impact-callsites.js +226 -0
- package/dist/tools/upgrade-impact-literals.d.ts +45 -0
- package/dist/tools/upgrade-impact-literals.js +118 -0
- package/dist/tools/upgrade-impact-report.d.ts +80 -0
- package/dist/tools/upgrade-impact-report.js +178 -0
- package/dist/tools/upgrade-impact.d.ts +84 -0
- package/dist/tools/upgrade-impact.js +305 -0
- package/dist/tools/upgrade-planner.d.ts +41 -8
- package/dist/tools/upgrade-planner.js +201 -53
- package/dist/tools/vulnerability-scan-concise.d.ts +72 -0
- package/dist/tools/vulnerability-scan-concise.js +97 -0
- package/dist/tools/vulnerability-scan-format.d.ts +137 -0
- package/dist/tools/vulnerability-scan-format.js +276 -0
- package/dist/tools/vulnerability-scan.d.ts +338 -18
- package/dist/tools/vulnerability-scan.js +84 -125
- package/dist/tools/what-should-i-know.d.ts +86 -19
- package/dist/tools/what-should-i-know.js +277 -283
- package/dist/types.d.ts +4 -1
- package/package.json +27 -30
- package/dist/schemas/agent-memory.json +0 -49
- package/dist/schemas/decision-enforcement.json +0 -22
- package/dist/schemas/decision-memory.json +0 -54
- package/dist/schemas/dependency-health.json +0 -27
- package/dist/schemas/developer-dna.json +0 -219
- package/dist/schemas/ecosystem-pulse.json +0 -17
- package/dist/schemas/get-actionable-signals.json +0 -29
- package/dist/schemas/get-context.json +0 -17
- package/dist/schemas/get-relevant-content.json +0 -26
- package/dist/schemas/knowledge-gaps.json +0 -14
- package/dist/schemas/record-feedback.json +0 -24
- package/dist/schemas/upgrade-planner.json +0 -22
- package/dist/schemas/vulnerability-scan.json +0 -26
- package/dist/schemas/what-should-i-know.json +0 -19
|
@@ -0,0 +1,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
|
+
};
|