@4da/mcp-server 6.1.0 → 6.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +183 -0
- package/README.md +14 -9
- package/dist/db.d.ts +15 -0
- package/dist/db.js +37 -2
- package/dist/live/changelog-classify.d.ts +9 -3
- package/dist/live/changelog-classify.js +122 -12
- package/dist/live/changelog.d.ts +8 -4
- package/dist/live/changelog.js +67 -29
- package/dist/live/fix-paths.js +1 -1
- package/dist/live/github-releases.d.ts +69 -0
- package/dist/live/github-releases.js +252 -0
- package/dist/live/go-releases.d.ts +48 -0
- package/dist/live/go-releases.js +196 -0
- package/dist/live/index.d.ts +4 -5
- package/dist/live/index.js +7 -1
- package/dist/live/osv-ranges.d.ts +6 -0
- package/dist/live/osv-ranges.js +3 -0
- package/dist/live/package-archive.d.ts +2 -0
- package/dist/live/package-archive.js +7 -4
- package/dist/live/pypi-releases.d.ts +44 -0
- package/dist/live/pypi-releases.js +103 -0
- package/dist/live/release-metadata.d.ts +17 -8
- package/dist/live/release-metadata.js +6 -3
- package/dist/live/upgrade-go.d.ts +62 -0
- package/dist/live/upgrade-go.js +130 -0
- package/dist/live/upgrade-pypi.d.ts +42 -0
- package/dist/live/upgrade-pypi.js +85 -0
- package/dist/live/upgrade-sources.d.ts +28 -11
- package/dist/live/upgrade-sources.js +86 -19
- package/dist/live/zip-archive.d.ts +22 -0
- package/dist/live/zip-archive.js +98 -0
- package/dist/schema-registry.js +3 -3
- package/dist/server-instructions.d.ts +6 -0
- package/dist/server-instructions.js +20 -15
- package/dist/tool-dispatch.d.ts +2 -0
- package/dist/tool-dispatch.js +41 -1
- package/dist/tools/briefing-stored-advisories.js +1 -1
- package/dist/tools/briefing-task-scope.js +2 -2
- package/dist/tools/dependency-check-signals.d.ts +16 -5
- package/dist/tools/dependency-check-signals.js +46 -18
- package/dist/tools/dependency-check-versions.d.ts +94 -0
- package/dist/tools/dependency-check-versions.js +213 -0
- package/dist/tools/dependency-check.d.ts +26 -10
- package/dist/tools/dependency-check.js +321 -74
- package/dist/tools/knowledge-gap-citations.js +1 -1
- package/dist/tools/upgrade-impact-callsites.d.ts +1 -0
- package/dist/tools/upgrade-impact-callsites.js +23 -5
- package/dist/tools/upgrade-impact-changelog.d.ts +41 -0
- package/dist/tools/upgrade-impact-changelog.js +79 -0
- package/dist/tools/upgrade-impact-imports.d.ts +42 -0
- package/dist/tools/upgrade-impact-imports.js +260 -0
- package/dist/tools/upgrade-impact-report.d.ts +13 -0
- package/dist/tools/upgrade-impact-report.js +13 -4
- package/dist/tools/upgrade-impact.d.ts +13 -4
- package/dist/tools/upgrade-impact.js +235 -84
- package/dist/tools/what-should-i-know.js +1 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,188 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 6.2.1 — 2026-10-11
|
|
4
|
+
|
|
5
|
+
### Feed tools leave out sources you turned off in the 4DA app
|
|
6
|
+
|
|
7
|
+
Since app schema 126, social and editorial sources (Hacker News, Reddit, RSS
|
|
8
|
+
and the rest) are opt-in "interests", off until turned on in Settings →
|
|
9
|
+
Sources, and every app reader skips their items. The server still returned
|
|
10
|
+
them. `get_relevant_content`, `get_actionable_signals`, `knowledge_gaps` and
|
|
11
|
+
`what_should_i_know` now apply the app's own rule
|
|
12
|
+
(`source_type NOT IN (SELECT source_type FROM sources WHERE enabled = 0)`), and
|
|
13
|
+
say so in a `sources_note` when any source is off. A source with no `sources`
|
|
14
|
+
row counts as on, as in the app; a database without `sources.enabled` is read
|
|
15
|
+
unfiltered, as before. `developer_dna` is unchanged: like the app's own, it
|
|
16
|
+
reports reading history. The vendored app-schema contract moves to schema 127.
|
|
17
|
+
|
|
18
|
+
## 6.2.0 — 2026-10-11
|
|
19
|
+
|
|
20
|
+
### `dependency_check`: the release-age hold no longer leaves only vulnerable choices
|
|
21
|
+
|
|
22
|
+
The 3-day hold on new releases is waived for a clean version when every safe
|
|
23
|
+
release on its line is inside the hold too and every older release on that line
|
|
24
|
+
has a known advisory: holding back would leave only vulnerable releases to
|
|
25
|
+
write. The `release_age` signal then says `proceed` with
|
|
26
|
+
`hold_waived: "no_settled_safe_release"`, names the line and the count of
|
|
27
|
+
vulnerable older releases, and asks the agent to tell the user the version's
|
|
28
|
+
age. A package whose only releases are new keeps the hold (that is the
|
|
29
|
+
supply-chain case it exists for). Found by G7 (golang.org/x/net on 2026-10-11:
|
|
30
|
+
the only safe releases were 1.8 and 2.8 days old, 59 of 61 v0 releases were
|
|
31
|
+
vulnerable, and every run left go.mod unchanged).
|
|
32
|
+
|
|
33
|
+
### `dependency_check`: PyPI and Go, safe versions in a constraint, and instructions that make agents call it
|
|
34
|
+
|
|
35
|
+
The G7 agent evaluation (49 dependency tasks x 3 trials, 2026-10-11) found that
|
|
36
|
+
when an agent called a 4DA tool, 0 of 60 runs wrote a vulnerable version, and
|
|
37
|
+
when it did not, 41 of 78 did. The misses were runs that never called one:
|
|
38
|
+
PyPI and Go adds (`dependency_check` took npm and crates.io only), pin tasks
|
|
39
|
+
("pin ws 7.x" wrote 7.5.10 from memory; body-parser 1.20.3), and agents told
|
|
40
|
+
they could not browse concluding they could not check versions at all.
|
|
41
|
+
|
|
42
|
+
- **PyPI.** Releases, upload times and yanks from the PyPI JSON API (a release
|
|
43
|
+
is yanked when every file is; one with no files is not installable), PEP 440
|
|
44
|
+
ordering, OSV.dev for the exact version.
|
|
45
|
+
- **Go.** Versions from proxy.golang.org, each judged version's date from its
|
|
46
|
+
`.info` (the list has none), `retract` directives in the latest go.mod
|
|
47
|
+
reported like a yank, `+incompatible` releases matched (`v2.0.0` finds
|
|
48
|
+
`v2.0.0+incompatible`), pseudo-versions dated and judged, and a major 2+
|
|
49
|
+
without its `/vN` path named as the different module it is.
|
|
50
|
+
- Signals the registry cannot supply (publisher trust, install scripts, new
|
|
51
|
+
transitive dependencies on PyPI and Go) say "not applicable" with the
|
|
52
|
+
reason, never a made-up value or an `unknown` that blocks the verdict.
|
|
53
|
+
- **Constraints.** `to` may be the constraint the agent was given: `"7.x"`,
|
|
54
|
+
`"^1.20"`, `"=0.17"`, `"~=3.1"`, `"3.1.*"`, `">=2,<3"`, `"v1"`. The item then
|
|
55
|
+
returns `range_check` (`smallest_safe_in_range`, `newest_safe_in_range`,
|
|
56
|
+
`safe_versions`, `smallest_safe_upgrade` above `from`), from every advisory
|
|
57
|
+
OSV.dev lists for the package evaluated against each release the constraint
|
|
58
|
+
admits, and the verdict is given for the version it picks: the smallest safe
|
|
59
|
+
upgrade above `from`, else the newest safe release past the 3-day hold. An
|
|
60
|
+
exact `to` an advisory affects gets the same `range_check` over its own line,
|
|
61
|
+
so "is ws 7.5.10 safe?" answers avoid and names 7.5.11. Withdrawn and
|
|
62
|
+
deprecated releases are never picked; maintenance notices do not make a
|
|
63
|
+
release unsafe. When nothing in the constraint is safe, the advisories that
|
|
64
|
+
affect all of it and the newest safe release outside it are named. A test
|
|
65
|
+
holds the local range evaluation to OSV.dev's own per-version answers on
|
|
66
|
+
recorded data (ws, Jinja2, golang.org/x/net).
|
|
67
|
+
- **Instructions and descriptions.** The server instructions now open with
|
|
68
|
+
the rule "before writing any dependency version into a manifest or
|
|
69
|
+
lockfile, call dependency_check on that exact version, or on the constraint
|
|
70
|
+
you were given", name add / pin / upgrade / bump / downgrade / choose a
|
|
71
|
+
version for all four ecosystems, and say the server reads the registries
|
|
72
|
+
for the agent (so "I cannot browse" is not a reason to skip it).
|
|
73
|
+
`vulnerability_scan` says it judges installed versions and points at
|
|
74
|
+
`dependency_check` for a version about to be written.
|
|
75
|
+
- The manifest-edit hook names a `dependency_check` call for PyPI and Go edits
|
|
76
|
+
too, and passes a range edit (`^7.0.0`) as a constraint instead of dropping it.
|
|
77
|
+
- `ecosystem` is matched case-insensitively (`pypi`, `go`).
|
|
78
|
+
|
|
79
|
+
### `upgrade_impact` reads PyPI and Go
|
|
80
|
+
|
|
81
|
+
Until 6.1 it answered for npm and crates.io only, and pointed Python and Go
|
|
82
|
+
upgrades at the briefing.
|
|
83
|
+
|
|
84
|
+
- **PyPI.** Versions, publish dates and yanks from the PyPI JSON API, ordered
|
|
85
|
+
by PEP 440 (`2.0`, `3.0.0rc1` and `1.0.post1` are releases, not unreadable
|
|
86
|
+
semver); a two-part version such as pydantic `2.0` is that release, not a
|
|
87
|
+
line. The changelog is read from the target release's sdist on
|
|
88
|
+
files.pythonhosted.org (`CHANGES.rst`, `HISTORY.md`, ...); a release with
|
|
89
|
+
wheels only says so. Import sites are matched on the MODULE the
|
|
90
|
+
distribution installs, established from the project's virtualenv metadata
|
|
91
|
+
(`.venv` / `venv` / `env`: `top_level.txt`, else `RECORD`), else a table of
|
|
92
|
+
well-known differences (`PyYAML` -> `yaml`, `beautifulsoup4` -> `bs4`,
|
|
93
|
+
`Pillow` -> `PIL`), else the distribution name, which the answer labels an
|
|
94
|
+
assumption (`your_code.import_name_source`, `import_name_note`) instead of
|
|
95
|
+
guessing silently.
|
|
96
|
+
- **Go.** Versions from proxy.golang.org (`/@v/list`, dates from `.info`), the
|
|
97
|
+
changelog from the module zip. A major version 2+ is a different module
|
|
98
|
+
path: `github.com/jackc/pgx/v4` -> `v5.0.0` reads the `/v5` module, lists
|
|
99
|
+
the rest of the v4 line and the v5 releases crossed, and reports
|
|
100
|
+
`module_path_change` with the files that import the old path. An up-to-date
|
|
101
|
+
module whose next major exists says so (`newer_major_module`). Import sites
|
|
102
|
+
come from `import "module/..."` specs, never counting another major's path.
|
|
103
|
+
- The parser reads reStructuredText changelogs: titles underlined (or over-
|
|
104
|
+
and underlined) with any RST adornment, their `Released <date>` lines, and
|
|
105
|
+
non-release setext/RST titles as sub-headings. `CHANGELOG_PARSER_VERSION` is
|
|
106
|
+
4 (with the classifier changes below), so changelogs cached by 6.1 are re-read.
|
|
107
|
+
- A zip reader (central directory only, stored and deflate entries, every
|
|
108
|
+
inflate capped) for Go module zips and zip sdists.
|
|
109
|
+
|
|
110
|
+
### GitHub release notes, opt-in (`FOURDA_GITHUB_CHANGELOG`)
|
|
111
|
+
|
|
112
|
+
Off by default: the default contract is unchanged, and only the package's
|
|
113
|
+
own registry and OSV.dev are contacted. With `FOURDA_GITHUB_CHANGELOG=true`,
|
|
114
|
+
when the archive has no changelog for the range, `upgrade_impact` reads the
|
|
115
|
+
repository's GitHub releases (for PyPI, first a changelog file the project
|
|
116
|
+
links on GitHub in `project_urls`), with the same parser and classifier.
|
|
117
|
+
|
|
118
|
+
- The repository comes from registry metadata; for Go from the proxy's
|
|
119
|
+
recorded VCS origin, so vanity paths (`google.golang.org/grpc`) and
|
|
120
|
+
multi-module repositories (`service/s3/v1.50.0` tags) resolve. Monorepo tags
|
|
121
|
+
for other packages are skipped.
|
|
122
|
+
- Unauthenticated REST API, or `GITHUB_TOKEN` when set. A rate limit is
|
|
123
|
+
reported with its reset time and how to raise it, and is not cached.
|
|
124
|
+
Results are cached under a key that carries `CHANGELOG_PARSER_VERSION`.
|
|
125
|
+
- With the fallback off and no usable changelog, the answer says the
|
|
126
|
+
changelog is unavailable and gives `github_fallback.how_to_enable` (or says
|
|
127
|
+
enabling it would not help: no GitHub repository). `_meta.sources` and
|
|
128
|
+
`_meta.privacy` name GitHub whenever it was contacted.
|
|
129
|
+
|
|
130
|
+
### Breaking-entry precision gate (G3)
|
|
131
|
+
|
|
132
|
+
A held-out panel, re-measured by the test suite on every run
|
|
133
|
+
(`src/__tests__/g3-breaking-precision.test.ts`, fixtures in
|
|
134
|
+
`src/__tests__/fixtures/g3/`): 40 real upgrade pairs, 10 each from npm,
|
|
135
|
+
crates.io, PyPI and Go, none of them a package the 6.1 rules were built on.
|
|
136
|
+
Each pair records the changelog source the tool read live (registry archive or
|
|
137
|
+
GitHub releases), trimmed to the range and checked to parse identically. Gold
|
|
138
|
+
for each of the 1,618 entries shown is the majority of three blind raters
|
|
139
|
+
working to a written rubric (`rubric.txt`), each label backed by a verbatim
|
|
140
|
+
quote from the entry (Fleiss kappa 0.984; the raters were model instances,
|
|
141
|
+
so agreement may be higher than independent human raters would reach). The
|
|
142
|
+
test fails below 90% precision, overall or on the held-out half, and on any
|
|
143
|
+
entry without a label.
|
|
144
|
+
|
|
145
|
+
| | flagged | precision | recall |
|
|
146
|
+
|---|---|---|---|
|
|
147
|
+
| 6.1 rules, all 40 pairs | 246 | 94.7% | 55.0% |
|
|
148
|
+
| 6.1 rules, 20 held-out pairs | 108 | 95.4% | 67.8% |
|
|
149
|
+
| 6.2 rules, all 40 pairs | 326 | 97.2% (95% CI 94.8-98.5) | 74.8% |
|
|
150
|
+
| 6.2 rules, 20 held-out pairs | 115 | 94.8% (95% CI 89.1-97.6) | 71.7% |
|
|
151
|
+
|
|
152
|
+
The 6.2 rule changes were made from the 20 dev pairs' errors only:
|
|
153
|
+
|
|
154
|
+
- An API change is read under any heading but a fix one. Covered: a leading
|
|
155
|
+
removal or rename of something API-shaped ("remove UMD build"), a changed
|
|
156
|
+
default, a type made private, a move or replacement, keyword-only
|
|
157
|
+
arguments, "now ... instead of", and "no longer comparable / usable /
|
|
158
|
+
subclasses".
|
|
159
|
+
- Dropped runtimes and raised minimums break whatever the reason given
|
|
160
|
+
("drop node 10.x to upgrade dev dependencies", "Replace go 1.21 with go 1.22
|
|
161
|
+
in go mod").
|
|
162
|
+
- "Other Changes" / "Misc" headings are neutral, not additive: pgx 5 filed its
|
|
163
|
+
type removals there.
|
|
164
|
+
- Fix headings are their own context.
|
|
165
|
+
- "Previously-deprecated APIs have been removed" is a removal list.
|
|
166
|
+
- `*Backwards-incompatible*` is an explicit marker.
|
|
167
|
+
- Link targets, bare URLs and GitHub's "by @user in <PR>" are dropped before
|
|
168
|
+
classifying (a `go.dev/doc/...` link read "Drop support for Go 1.21" as a
|
|
169
|
+
docs change); RST ``literals`` read as code.
|
|
170
|
+
- No longer breaking: entries that say they still work or have no effect on
|
|
171
|
+
users, private modules and `_names`, removing a deprecation notice,
|
|
172
|
+
restoring an API, relaxing a bound, renaming the repository, swapping a
|
|
173
|
+
dependency crate, and changes scoped to an example, docs or CI.
|
|
174
|
+
|
|
175
|
+
Per ecosystem over all 40 pairs: npm 99.0% / 84.2%, crates.io 99.0% /
|
|
176
|
+
81.3%, PyPI 95.7% / 72.5%, Go 92.0% / 54.1% (Go's GitHub release notes are
|
|
177
|
+
largely PR titles and prose). `CHANGELOG_PARSER_VERSION` is 4.
|
|
178
|
+
|
|
179
|
+
### Hook and briefing
|
|
180
|
+
|
|
181
|
+
- The manifest-edit hook names the exact `upgrade_impact` call for
|
|
182
|
+
`pyproject.toml`, `requirements.txt` and `go.mod` edits (`ecosystem: "PyPI"`
|
|
183
|
+
/ `"Go"`); `what_should_i_know`'s next step for a PyPI or Go package is
|
|
184
|
+
`upgrade_impact`.
|
|
185
|
+
|
|
3
186
|
## 6.1.0 — 2026-10-10
|
|
4
187
|
|
|
5
188
|
Fixes from an agent eval over stdio JSON-RPC against real projects (2026-10-07)
|
package/README.md
CHANGED
|
@@ -112,23 +112,25 @@ Measured against osv-scanner on 19 public repositories (npm, pnpm v5-v9, yarn v1
|
|
|
112
112
|
|
|
113
113
|
- **OSV.dev** for known vulnerabilities, matched to exact installed versions
|
|
114
114
|
- **npm registry, crates.io, PyPI, Go module proxy** for versions, deprecations and yanks
|
|
115
|
-
- **The package's own registry archive** (registry.npmjs.org, static.crates.io) for the changelog `upgrade_impact` reads;
|
|
116
|
-
- **npm full packument
|
|
115
|
+
- **The package's own registry archive** (registry.npmjs.org, static.crates.io, files.pythonhosted.org, proxy.golang.org) for the changelog `upgrade_impact` reads; GitHub only if you opt in with `FOURDA_GITHUB_CHANGELOG=true` (below)
|
|
116
|
+
- **npm full packument**, the **crates.io versions API**, the **PyPI JSON API** and the **Go proxy's version list** for `dependency_check` (publish times, yanks and retractions; on npm and crates.io also publishers, install scripts, per-version dependencies). These requests carry the package name only; the version you have installed is never sent to a registry, except that the Go proxy's list has no dates, so it is asked for the `.info` of the versions being judged (the same request `go get` makes). OSV.dev's advisory list for the package lets `dependency_check` name the safe releases in a constraint. Packuments are cached on disk and revalidated with `If-None-Match`; crates.io API reads are spaced one per second.
|
|
117
117
|
- **Hacker News Algolia API**, only when you call `ecosystem_pulse`
|
|
118
118
|
|
|
119
119
|
Results are cached (24h for registry data, 1h for vulnerabilities, 30min for news) and rate-limited per source.
|
|
120
120
|
|
|
121
121
|
**What's sent over the network:** package names and versions (the same data visible in your lockfile), and, only for `ecosystem_pulse`, the names of a few of your dependencies as search terms. No source code, no file paths, no personal data. The call-site scan of `upgrade_impact` runs locally. Set `FOURDA_OFFLINE=true` to disable all network calls.
|
|
122
122
|
|
|
123
|
+
**GitHub release notes (opt-in, off by default).** Many packages ship no changelog in their registry archive (vite, zod, cobra, most Go modules). With `FOURDA_GITHUB_CHANGELOG=true` in the server's environment, `upgrade_impact` reads the repository's GitHub releases when the archive has no changelog for the range (for PyPI, first a changelog file the project links on GitHub from its `project_urls`). GitHub then receives the repository name and the versions, nothing else. It uses the unauthenticated REST API (60 requests an hour) or `GITHUB_TOKEN` when set (5,000); a rate limit is reported with its reset time, never retried in a loop, and results are cached. With it off, an answer without a changelog says so and names the setting; nothing is guessed.
|
|
124
|
+
|
|
123
125
|
**What's written to disk, and where:** only files the server owns, in its folder in your user data directory (`%LOCALAPPDATA%\4da-mcp`, `~/Library/Application Support/4da-mcp` or `~/.local/share/4da-mcp`; `FOURDA_MCP_HOME` moves it): `cache.db` (registry, OSV and changelog responses; safe to delete) and `standalone.db` (without the desktop app: your project scan and the decisions, agent memory and feedback you record). Nothing is written inside your repository.
|
|
124
126
|
|
|
125
127
|
When the 4DA desktop app is installed, the server reads the app's database (`4da.db`) through a **read-only** connection and never creates or alters anything in it: no tables, no columns, no cache. The only writes are rows in the four app-owned tables the tools exist to fill, so the app sees them: decisions (`decision_memory` -> `developer_decisions`), agent memory (`agent_memory`), and feedback (`record_feedback` -> `interactions` and `feedback`, which the app's calibration reads). Those go through a separate write connection that waits briefly for the app's lock. If your app version lacks the table or a column a record needs, the record goes to `standalone.db` instead and the response says so in `_meta`. (Before 6.1 the server also created a `live_cache` table inside the app's database; it now keeps that cache in `cache.db`.)
|
|
126
128
|
|
|
127
129
|
> The one exception: if you *explicitly* configure an OpenAI embedding provider (`FOURDA_EMBED_PROVIDER=openai`) for semantic recall, the decision/memory text you store is sent to OpenAI to be embedded. The default — no embedding provider, or a local Ollama one — keeps everything on your machine, and `FOURDA_OFFLINE=true` overrides it regardless.
|
|
128
130
|
|
|
129
|
-
**Ecosystems supported:** npm, crates.io (Rust), PyPI (Python), Go. `upgrade_impact`:
|
|
131
|
+
**Ecosystems supported:** npm, crates.io (Rust), PyPI (Python), Go. `upgrade_impact` reads all four. PyPI: versions and dates from the PyPI JSON API, the changelog from the release's sdist, and import sites matched on the module the distribution installs: read from your virtualenv's metadata (`.venv`, `venv`, `env`: `top_level.txt` or `RECORD`), else a table of well-known differences (`PyYAML` -> `yaml`, `Pillow` -> `PIL`), else the distribution name, which the answer then labels an assumption. Go: versions and dates from proxy.golang.org, the changelog from the module zip, and a major version 2+ reported as the module path change it is (`github.com/jackc/pgx/v4` -> `/v5`), with the files that import the old path. `dependency_check` reads all four too; on PyPI and Go, where the registry exposes no publisher identity or install hooks, those signals say "not applicable".
|
|
130
132
|
|
|
131
|
-
**Known limits, stated plainly:** a `requirements.txt` without a lockfile names only your direct pins, so their transitive dependencies are not scanned (use `uv lock`, `poetry lock` or `pip-compile`). Many packages ship no changelog in their registry archive (fastembed, vite, zod among them); `upgrade_impact` then says so and gives the release-notes URL instead of guessing. Breaking entries are flagged from the changelog's headings and wording, and every entry carries the heading it sits under (`under`). Measured on
|
|
133
|
+
**Known limits, stated plainly:** a `requirements.txt` without a lockfile names only your direct pins, so their transitive dependencies are not scanned (use `uv lock`, `poetry lock` or `pip-compile`). Many packages ship no changelog in their registry archive (fastembed, vite, zod among them); `upgrade_impact` then says so and gives the release-notes URL instead of guessing. Breaking entries are flagged from the changelog's headings and wording, and every entry carries the heading it sits under (`under`). Measured on a panel of 40 real upgrades across npm, crates.io, PyPI and Go (1,618 entries, gold from three blind raters with verbatim evidence; the panel and the measurement are in the test suite): on the 20 upgrades held out from rule-making, 95% of entries flagged breaking were breaking (95% CI 89-98%) and about 72% of breaking entries were flagged; Go release notes written as prose are the weakest (about 54% recall). Treat the flags as a pre-sort: read every entry of a major upgrade. Each answer says this in `_meta.classification`.
|
|
132
134
|
|
|
133
135
|
## What You Can Ask
|
|
134
136
|
|
|
@@ -138,6 +140,7 @@ When the 4DA desktop app is installed, the server reads the app's database (`4da
|
|
|
138
140
|
"Scan for vulnerabilities" -> vulnerability_scan
|
|
139
141
|
"Which deps should I upgrade first?" -> upgrade_planner
|
|
140
142
|
"Is it safe to bump axios to 1.14.1?" -> dependency_check
|
|
143
|
+
"Pin ws to a 7.x version" -> dependency_check (to: "7.x")
|
|
141
144
|
"I'm about to bump fastembed 5 -> 7" -> what_should_i_know
|
|
142
145
|
"What's happening in the ecosystem?" -> ecosystem_pulse
|
|
143
146
|
"What's my tech stack?" -> get_context
|
|
@@ -152,11 +155,11 @@ When the 4DA desktop app is installed, the server reads the app's database (`4da
|
|
|
152
155
|
|
|
153
156
|
| Tool | What it does |
|
|
154
157
|
|------|-------------|
|
|
155
|
-
| `upgrade_impact` | What changes between the installed and a target version of one dependency: releases in between, changelog entries classified breaking / deprecation / security, the breaking ones that touch your code (symbols you import, and route or pattern syntax in your string literals, e.g. axum 0.8's `/:id` -> `/{id}`), the files that import it, advisories fixed. |
|
|
158
|
+
| `upgrade_impact` | What changes between the installed and a target version of one dependency (npm, crates.io, PyPI, Go): releases in between, changelog entries classified breaking / deprecation / security, the breaking ones that touch your code (symbols you import, and route or pattern syntax in your string literals, e.g. axum 0.8's `/:id` -> `/{id}`), the files that import it, advisories fixed. |
|
|
156
159
|
| `vulnerability_scan` | Every installed copy in every lockfile matched against OSV.dev. Scope-adjusted severity, an upgrade target checked against every advisory of the package (the highest per-advisory fix can itself be affected by another), where each version is pinned, and a `coverage` block whenever the scan is partial. Concise by default (one row per vulnerable package version, the 40 most severe, about 4k tokens on a 290-advisory project); `package` for one dependency; `response_format: "detailed"` for every advisory. |
|
|
157
160
|
| `dependency_health` | Version freshness, deprecation (of the version you run) and vulnerability counts per dependency. |
|
|
158
161
|
| `upgrade_planner` | The smallest version no advisory affects for each vulnerable package, majors flagged (a 0.x minor counts as a major). Transitive fixes: `lockfile_refresh` with the exact command (`npm update`, `pnpm update`, `yarn up -R`, `cargo update -p ... --precise`) when every parent's declared requirement already admits a clean version, else `waiting_on_upstream` naming the requirement that blocks it. For one project: `project_path` (default: the project the server was started in; `"*"` for every project the desktop app tracks). `package` for a one-package plan. |
|
|
159
|
-
| `dependency_check` | Call before
|
|
162
|
+
| `dependency_check` | Call before writing any dependency version into a manifest: adding, pinning, upgrading, bumping, downgrading. `to` is the exact version, or the constraint you were given (`"7.x"`, `"^1.20"`, `"~=3.1"`, `"v1"`): then `range_check` lists the smallest and newest release in it that no known advisory affects, and the verdict is for the one it picks. An exact version an advisory affects gets the safe releases on its line. Verdict per item (`proceed` / `wait` / `review` / `avoid` / `unknown`) with evidence: advisories on the target, release age (holds releases under 3 days unless they fix an advisory you have), publish-trust drop, new install scripts, brand-new transitive dependencies, yanked or deprecated, and a breaking version range (a major, or a 0.x minor) is `review` with a pointer to `upgrade_impact`. npm, crates.io, PyPI and Go (publisher trust, install scripts and new transitive dependencies are npm/crates.io signals and say "not applicable" elsewhere). |
|
|
160
163
|
|
|
161
164
|
### Intelligence
|
|
162
165
|
|
|
@@ -206,7 +209,7 @@ The MCP server works without the desktop app. It keeps a small local database in
|
|
|
206
209
|
| Tech stack detection + resolved versions | Yes | Yes |
|
|
207
210
|
| Decision memory + alignment checking | Yes | Yes |
|
|
208
211
|
| Agent memory (cross-session) | Yes | Yes |
|
|
209
|
-
|
|
|
212
|
+
| Developer news judged against your stack (the app's feed) | -- | Yes |
|
|
210
213
|
| Actionable signals + knowledge gaps | -- | Yes |
|
|
211
214
|
| The analysis layer (Signal Chains, Knowledge Gaps, temporal analysis) | -- | Yes |
|
|
212
215
|
|
|
@@ -255,6 +258,8 @@ npx @4da/mcp-server --version # Print version
|
|
|
255
258
|
|----------|-------------|---------|
|
|
256
259
|
| `FOURDA_DB_PATH` | Path to 4DA's SQLite database | Auto-detected |
|
|
257
260
|
| `FOURDA_OFFLINE` | Disable all network calls | `false` |
|
|
261
|
+
| `FOURDA_GITHUB_CHANGELOG` | `upgrade_impact`: read GitHub release notes when the registry archive has no changelog for the range (`true` / `1` / `yes` / `on`) | Off |
|
|
262
|
+
| `GITHUB_TOKEN` | Used only with `FOURDA_GITHUB_CHANGELOG`: raises GitHub's rate limit from 60 to 5,000 requests an hour | Unset |
|
|
258
263
|
| `FOURDA_MCP_HOME` | Folder for the files the server writes (`cache.db`, `standalone.db`) | Your user data folder + `4da-mcp` |
|
|
259
264
|
| `MCP_AUTH_SECRET` | Shared secret for verifying Bearer tokens on `--http` (HMAC-SHA256). Falls back to `JWT_SECRET`. Unset means no token is accepted. | Unset |
|
|
260
265
|
| `MCP_AUTH_REQUIRED` | Require auth on a **loopback** `--http` bind. Always required on a non-loopback bind. | `false` |
|
|
@@ -263,10 +268,10 @@ npx @4da/mcp-server --version # Print version
|
|
|
263
268
|
## FAQ
|
|
264
269
|
|
|
265
270
|
**Does this send my code anywhere?**
|
|
266
|
-
No. The server sends package names and versions to public APIs ([OSV.dev](https://osv.dev), npm registry, crates.io, PyPI, Go proxy), downloads the target version's archive from the package's own registry for `upgrade_impact
|
|
271
|
+
No. The server sends package names and versions to public APIs ([OSV.dev](https://osv.dev), npm registry, crates.io, PyPI, Go proxy), downloads the target version's archive from the package's own registry for `upgrade_impact` (and reads GitHub release notes only if you set `FOURDA_GITHUB_CHANGELOG=true`), and, only when you call `ecosystem_pulse`, sends a few dependency names as search terms to [HN Algolia](https://hn.algolia.com/api). No source code, no file paths, no personal data: the call-site scan runs locally. Set `FOURDA_OFFLINE=true` to disable all network calls. (The sole exception is opt-in OpenAI embeddings — see the network note above.)
|
|
267
272
|
|
|
268
273
|
**Do I need the 4DA desktop app?**
|
|
269
|
-
No. 11 tools work standalone: upgrade impact, pre-install dependency checks, vulnerability scanning, dependency health, upgrade planning, ecosystem news, pre-task briefings, project context, decision memory, alignment checking, and agent memory. The desktop app
|
|
274
|
+
No. 11 tools work standalone: upgrade impact, pre-install dependency checks, vulnerability scanning, dependency health, upgrade planning, ecosystem news, pre-task briefings, project context, decision memory, alignment checking, and agent memory. The desktop app is where the same engine runs day to day: one worklist across every project on the machine, a morning Brief of what changed in your stack, and an Upgrade Plan. With it installed, your agent also gets 5 more tools: signals, knowledge gaps, developer news judged against your stack, Developer DNA and feedback.
|
|
270
275
|
|
|
271
276
|
**Which AI tools does this work with?**
|
|
272
277
|
Any tool that supports [MCP](https://modelcontextprotocol.io): Claude Code, Claude Desktop, Cursor, Windsurf, VS Code (Copilot), and any custom MCP client.
|
package/dist/db.d.ts
CHANGED
|
@@ -257,6 +257,21 @@ export declare class FourDADatabase {
|
|
|
257
257
|
/** `MAX(sources.last_fetch)`, tolerant of a missing `sources` table (returns null). */
|
|
258
258
|
private safeMaxLastFetch;
|
|
259
259
|
hasTable(table: string): boolean;
|
|
260
|
+
/** Whether `sources.enabled` exists; probed once per connection (see enabledSourceClause). */
|
|
261
|
+
private sourcesEnabledColumn;
|
|
262
|
+
private hasSourcesEnabled;
|
|
263
|
+
/**
|
|
264
|
+
* ` AND <col> is a source the user has not turned off`, or `` when the
|
|
265
|
+
* database cannot say. The desktop app's rule, verbatim
|
|
266
|
+
* (`source_class::enabled_source_sql`, schema 126): every reader of source
|
|
267
|
+
* items skips a `source_type` whose `sources` row has `enabled = 0`. A type
|
|
268
|
+
* with no `sources` row counts as on, as in the app. A database without a
|
|
269
|
+
* `sources.enabled` column (or without `sources`) is read unfiltered, exactly
|
|
270
|
+
* as before this guard existed.
|
|
271
|
+
*/
|
|
272
|
+
enabledSourceClause(col: string): string;
|
|
273
|
+
/** Source types the user turned off in the app (`sources.enabled = 0`), sorted; [] when unknown. */
|
|
274
|
+
disabledSources(): string[];
|
|
260
275
|
hasColumn(table: string, column: string): boolean;
|
|
261
276
|
/**
|
|
262
277
|
* Idempotently add a column to a table if it does not already exist.
|
package/dist/db.js
CHANGED
|
@@ -1033,6 +1033,41 @@ export class FourDADatabase {
|
|
|
1033
1033
|
return false;
|
|
1034
1034
|
}
|
|
1035
1035
|
}
|
|
1036
|
+
/** Whether `sources.enabled` exists; probed once per connection (see enabledSourceClause). */
|
|
1037
|
+
sourcesEnabledColumn;
|
|
1038
|
+
hasSourcesEnabled() {
|
|
1039
|
+
if (this.sourcesEnabledColumn === undefined) {
|
|
1040
|
+
this.sourcesEnabledColumn = this.hasColumn("sources", "enabled");
|
|
1041
|
+
}
|
|
1042
|
+
return this.sourcesEnabledColumn;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* ` AND <col> is a source the user has not turned off`, or `` when the
|
|
1046
|
+
* database cannot say. The desktop app's rule, verbatim
|
|
1047
|
+
* (`source_class::enabled_source_sql`, schema 126): every reader of source
|
|
1048
|
+
* items skips a `source_type` whose `sources` row has `enabled = 0`. A type
|
|
1049
|
+
* with no `sources` row counts as on, as in the app. A database without a
|
|
1050
|
+
* `sources.enabled` column (or without `sources`) is read unfiltered, exactly
|
|
1051
|
+
* as before this guard existed.
|
|
1052
|
+
*/
|
|
1053
|
+
enabledSourceClause(col) {
|
|
1054
|
+
return this.hasSourcesEnabled()
|
|
1055
|
+
? ` AND ${col} NOT IN (SELECT source_type FROM sources WHERE enabled = 0)`
|
|
1056
|
+
: ``;
|
|
1057
|
+
}
|
|
1058
|
+
/** Source types the user turned off in the app (`sources.enabled = 0`), sorted; [] when unknown. */
|
|
1059
|
+
disabledSources() {
|
|
1060
|
+
if (!this.hasSourcesEnabled())
|
|
1061
|
+
return [];
|
|
1062
|
+
try {
|
|
1063
|
+
return this.db
|
|
1064
|
+
.prepare("SELECT source_type FROM sources WHERE enabled = 0 ORDER BY source_type")
|
|
1065
|
+
.all().map((r) => r.source_type);
|
|
1066
|
+
}
|
|
1067
|
+
catch {
|
|
1068
|
+
return [];
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1036
1071
|
hasColumn(table, column) {
|
|
1037
1072
|
try {
|
|
1038
1073
|
const cols = this.db.pragma(`table_info(${table})`);
|
|
@@ -1129,7 +1164,7 @@ export class FourDADatabase {
|
|
|
1129
1164
|
${depCount} AS dep_match_count
|
|
1130
1165
|
FROM source_items
|
|
1131
1166
|
WHERE relevance_score >= ?
|
|
1132
|
-
AND datetime(created_at) >= datetime(?)${versionGuard}${curationGuard}${advisoryFreshnessGuard}
|
|
1167
|
+
AND datetime(created_at) >= datetime(?)${versionGuard}${curationGuard}${advisoryFreshnessGuard}${this.enabledSourceClause("source_type")}
|
|
1133
1168
|
`;
|
|
1134
1169
|
const params = [minScore, sinceDate];
|
|
1135
1170
|
if (sourceType) {
|
|
@@ -1213,7 +1248,7 @@ export class FourDADatabase {
|
|
|
1213
1248
|
let query = `
|
|
1214
1249
|
SELECT id, source_type, source_id, url, title, content, content_hash, created_at, last_seen
|
|
1215
1250
|
FROM source_items
|
|
1216
|
-
WHERE datetime(created_at) >= datetime(?)${advisoryFreshnessGuard}
|
|
1251
|
+
WHERE datetime(created_at) >= datetime(?)${advisoryFreshnessGuard}${this.enabledSourceClause("source_type")}
|
|
1217
1252
|
`;
|
|
1218
1253
|
const params = [sinceDate];
|
|
1219
1254
|
if (sourceType) {
|
|
@@ -20,8 +20,14 @@ export type EntryKind = "breaking" | "deprecation" | "security" | "change";
|
|
|
20
20
|
export declare const MAX_ENTRY_CHARS = 400;
|
|
21
21
|
/** Strip control and invisible/bidi characters, collapse whitespace, cap length. */
|
|
22
22
|
export declare function sanitizeEntry(text: string, cap?: number): string;
|
|
23
|
+
/**
|
|
24
|
+
* The text classified: RST ``literals`` read as `literals`, link targets and
|
|
25
|
+
* bare URLs dropped (a `go.dev/doc/go1.21` link made "Drop support for Go 1.21"
|
|
26
|
+
* read as a docs change), GitHub's "by @user in <PR url>" suffix dropped.
|
|
27
|
+
*/
|
|
28
|
+
export declare function classificationText(text: string): string;
|
|
23
29
|
/** Kind of a single changelog line by its own wording. */
|
|
24
|
-
export declare function classifyText(
|
|
30
|
+
export declare function classifyText(raw: string): EntryKind;
|
|
25
31
|
/**
|
|
26
32
|
* What a sub-heading (or a label-only bullet such as express's "* remove:")
|
|
27
33
|
* says about the entries under it:
|
|
@@ -34,8 +40,8 @@ export declare function classifyText(text: string): EntryKind;
|
|
|
34
40
|
* where only an explicit marker makes an entry breaking ("Added a clear()
|
|
35
41
|
* function ... removed from an axios instance" is not a removal).
|
|
36
42
|
*/
|
|
37
|
-
export type EntryContext = EntryKind | "removal" | "additive";
|
|
43
|
+
export type EntryContext = EntryKind | "removal" | "additive" | "fixes";
|
|
38
44
|
/** Context implied by a heading or label, or null when it carries no signal ("### Changed"). */
|
|
39
45
|
export declare function classifyHeading(heading: string): EntryContext | null;
|
|
40
46
|
/** Final kind: an explicit heading or bullet parent wins; removal and additive contexts refine the line's wording. */
|
|
41
|
-
export declare function classifyEntry(
|
|
47
|
+
export declare function classifyEntry(raw: string, context: EntryContext | null): EntryKind;
|
|
@@ -35,7 +35,8 @@ export function sanitizeEntry(text, cap = MAX_ENTRY_CHARS) {
|
|
|
35
35
|
// `BREAKING` but not the file name `BREAKING-CHANGES.md` (ratatui links it from routine entries);
|
|
36
36
|
// a leading "Breaking -" / "[Breaking]" label too (webpack-merge 5, handlebars-rust).
|
|
37
37
|
// Case-sensitive on purpose: lowercase "breaking" is prose, handled (with negation) below.
|
|
38
|
-
|
|
38
|
+
// "*Backwards-incompatible*:" (marshmallow 4) is the same call in other words.
|
|
39
|
+
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)\]|\b[Bb]ackwards?[- ][Ii]ncompatible\b/;
|
|
39
40
|
/**
|
|
40
41
|
* Text ABOUT breaking changes rather than one: a pointer to the list ("⚠️ List
|
|
41
42
|
* of breaking changes can be found here"), a CI label check, a docs fix
|
|
@@ -126,7 +127,81 @@ const BREAKING_HEURISTIC = new RegExp([
|
|
|
126
127
|
"\\bincompatib",
|
|
127
128
|
].join("|"), "i");
|
|
128
129
|
/** 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 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|private|maintainers?|contributors?|invalid|broken|bogus|[\w-]+\.(?:md|json|ya?ml|toml|lock|txt|cfg)(?![\w(]))\b/i;
|
|
131
|
+
/**
|
|
132
|
+
* The removed thing is private by its name (`_prepare_conn`, a leading
|
|
133
|
+
* underscore): urllib3 2.0 lists such removals under "Removed".
|
|
134
|
+
*/
|
|
135
|
+
const PRIVATE_NAME = /^\W*(?:remove[sd]?|delete[sd]?|drop(?:s|ped)?)\b[^`]{0,40}`_[A-Za-z]/i;
|
|
136
|
+
const isInternal = (t) => INTERNAL_CHANGE.test(t) || PRIVATE_NAME.test(t);
|
|
137
|
+
/** Public API made private ("Assorted array helper methods and types made private"): a removal, though it says "private". */
|
|
138
|
+
const MADE_PRIVATE = /\b(?:made|make|makes|is now|are now)\b[^.]{0,60}\bprivate\b/i;
|
|
139
|
+
/**
|
|
140
|
+
* The entry says it changes nothing for users: "passing string or boolean still
|
|
141
|
+
* works as before", "This change should have no effect on users".
|
|
142
|
+
*/
|
|
143
|
+
const STATED_COMPATIBLE = /\b(?:still works?|still work as before|continues? to work|(?:is|are) still supported|remains? supported|(?:should|will|has|have) (?:have )?no (?:effect|impact) on (?:users|you|most users|existing code)|no user-facing)\b|(?<!\bnot |\bnon-?)\bbackwards?[- ]compatib(?:le|ility)\b/i;
|
|
144
|
+
/**
|
|
145
|
+
* Not an API change although worded like one: undoing a removal or deprecation
|
|
146
|
+
* ("removes the deprecation notice from", "restore the `X()` function"), a
|
|
147
|
+
* relaxation ("remove Send bounds for request `Body`"), a repository rename, a
|
|
148
|
+
* swapped dependency ("Move from `winapi` to `windows` crate").
|
|
149
|
+
*/
|
|
150
|
+
const UNDO = /\bremov\w*\s+(?:the\s+)?(?:[\w`]+\s+)?(?:bounds?|restrictions?|limitations?|limits?|requirements?|constraints?|need for)\b|\brenam\w*\s+(?:the\s+)?(?:github\s+)?repo(?:sitory)?\b|\bmov(?:e|ed|es)\s+from\s+\x60[^\x60]+\x60\s+to\s+\x60[^\x60]+\x60\s+(?:crate|package|library)\b|\bremov\w*\s+(?:the\s+)?deprecation\s+(?:notices?|warnings?|messages?)\b|\bun-?deprecat|\b(?:restore[sd]?|restoring|re-?add(?:ed|s)?|reintroduc\w*)\b/i;
|
|
151
|
+
/** A change confined to examples, docs, tests or CI: "Drop support for Go 1.21 in dice example". */
|
|
152
|
+
const SCOPED_NON_API = /\bin (?:the )?(?:[\w-]+ )?(?:examples?|docs?|documentation|tests?|ci|workflows?|benchmarks?|readme)\b/i;
|
|
153
|
+
const LEAD = String.raw `^\W*(?:\*\*[^*]{1,40}\*\*:?\s*)?`;
|
|
154
|
+
/**
|
|
155
|
+
* Runtime and language support: dropped versions and raised minimums ("drop
|
|
156
|
+
* node 10.x", "Drop support for Go 1.21", "only support node 16-20", "Replace
|
|
157
|
+
* go 1.21 with go 1.22 in go mod", "increase MSRV to 1.63"). Breaking whatever
|
|
158
|
+
* the reason the entry gives ("drop node 10.x to upgrade dev dependencies").
|
|
159
|
+
*/
|
|
160
|
+
const RUNTIME_SUPPORT = new RegExp([
|
|
161
|
+
String.raw `\b(?:drop(?:s|ped|ping)?|remove[sd]?|removing|end(?:s|ed)?)\s+(?:official\s+)?(?:support\s+(?:for\s+)?)?(?:node(?:\.?js)?|python|py|go|golang|ruby|php|java|jdk|rust|rustc)\s*v?\d`,
|
|
162
|
+
String.raw `\b(?:only supports?|now requires?|requires? at least|minimum (?:supported )?(?:version of )?)\s*(?:node(?:\.?js)?|python|go|golang|rust|ruby|php|java)\s*v?\d`,
|
|
163
|
+
String.raw `\bonly supports? (?:node(?:\.?js)?|python|go|golang)\b`,
|
|
164
|
+
String.raw `\bgo\s*1\.\d+[^.]{0,30}\bgo\.?\s?mod\b`,
|
|
165
|
+
String.raw `\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+`,
|
|
166
|
+
].join("|"), "i");
|
|
167
|
+
/**
|
|
168
|
+
* Wording that changes the API whatever heading it sits under (rule set added
|
|
169
|
+
* 2026-10-11 from the G3 panel's dev pairs): a leading removal or rename of
|
|
170
|
+
* something API-shaped, a changed default, a type made private, a move, a
|
|
171
|
+
* replacement, keyword-only arguments, "now ... instead of".
|
|
172
|
+
*/
|
|
173
|
+
const API_CHANGE = new RegExp([
|
|
174
|
+
// "remove UMD build", "remove `ReasonPhrase::from_bytes_unchecked()` method", "Remove previously deprecated code".
|
|
175
|
+
// The removed thing is named within the next few words, as in REMOVAL_HEURISTIC: "Remove a window when ...
|
|
176
|
+
// `cd`able by non-owners" (a security fix) has its backtick far later.
|
|
177
|
+
String.raw `${LEAD}(?:remove[sd]?|delete[sd]?|drop(?:s|ped)?)\b(?=(?:\s+[\w.:/-]+){0,4}\s*\(?\x60|(?:\s+[\w.:/-]+){0,5}?\s+(?:support|builds?|param(?:eter)?s?|options?|methods?|functions?|modules?|class(?:es)?|types?|exports?|features?|flags?|apis?|prefix(?:es)?|constructors?|requires|imports|entry ?points?|fields?|propert(?:y|ies)|arguments?|attributes?|traits?|structs?|enums?|macros?|commands?|settings?|config|keys?|aliases?|fallback|deprecated|code)\b)`,
|
|
178
|
+
String.raw `${LEAD}renamed?\b`,
|
|
179
|
+
String.raw `\x60[^\x60]+\x60\s+(?:is |was |has been )?renamed\b|\bchanged? (?:the )?name of\b`,
|
|
180
|
+
// Defaults: "Changed the default `blocksize` to 16KB", "now defaults `extended` to `false`", "default `header_read_timeout` to 30 seconds".
|
|
181
|
+
String.raw `${LEAD}(?:change[sd]?|set|make|made)\b[^.]{0,60}\bdefaults?\b[^.]{0,60}\bto\b`,
|
|
182
|
+
String.raw `${LEAD}default\s[^.]{0,60}\bto\b`,
|
|
183
|
+
String.raw `\bnow defaults?\b[^.]{0,40}\bto\b|\bdefaults? (?:value |values )?(?:is|are) now\b`,
|
|
184
|
+
String.raw `\b(?:keyword|positional)[- ]only\b`,
|
|
185
|
+
String.raw `\x60[^\x60]+\x60\s+(?:has been |have been |was |were |is |are )?moved (?:to|into|out of)\b`,
|
|
186
|
+
String.raw `${LEAD}moved?\b[^.]{0,80}\x60[^\x60]+\x60[^.]{0,60}\b(?:to|into|out of)\b`,
|
|
187
|
+
String.raw `\b(?:are|is|was|were|been)\s+(?:both\s+)?replaced\b|\x60[^\x60]+\x60\s+replaces\s+\x60`,
|
|
188
|
+
String.raw `\bnow\b[^.]{0,60}\binstead of\b`,
|
|
189
|
+
String.raw `\b(?:accepts?|takes?|returns?|receives?)\s+(?:a |an )?(?:pointer|reference|value|slice|copy)\b[^.]{0,30}\binstead of\b`,
|
|
190
|
+
String.raw `\bchang(?:e|ed|es)\b[^.]{0,80}\bfrom\s+\x60[^\x60]+\x60\s+to\s+\x60`,
|
|
191
|
+
String.raw `\bno longer (?:subclass(?:es)?|inherits?|comparable|usable|considers?|escapes?|initiali[sz]ed|quotes?|sets?|ha(?:s|ve) a default|safe|valid|recogni[sz]ed|parsed?|emitted|exposed|called)\b|\bis not set anymore\b|\bnot set anymore\b`,
|
|
192
|
+
].join("|"), "i");
|
|
193
|
+
/**
|
|
194
|
+
* The text classified: RST ``literals`` read as `literals`, link targets and
|
|
195
|
+
* bare URLs dropped (a `go.dev/doc/go1.21` link made "Drop support for Go 1.21"
|
|
196
|
+
* read as a docs change), GitHub's "by @user in <PR url>" suffix dropped.
|
|
197
|
+
*/
|
|
198
|
+
export function classificationText(text) {
|
|
199
|
+
return text
|
|
200
|
+
.replace(/``/g, "`")
|
|
201
|
+
.replace(/\]\([^)]*\)/g, "]")
|
|
202
|
+
.replace(/\bby @[\w-]+(?:\[bot\])? in\s+\S+/g, " ")
|
|
203
|
+
.replace(/https?:\/\/\S+/g, " ");
|
|
204
|
+
}
|
|
130
205
|
const DEPRECATION = /deprecat/i;
|
|
131
206
|
/**
|
|
132
207
|
* An entry whose main verb is "deprecate" is a deprecation even when it names
|
|
@@ -135,11 +210,14 @@ const DEPRECATION = /deprecat/i;
|
|
|
135
210
|
*/
|
|
136
211
|
const LEADING_DEPRECATION = /^\W*(?:\*\*[^*]{1,40}\*\*:?\s*)?(?:deprecated?|deprecates|deprecating)\b/i;
|
|
137
212
|
/** Kind of a single changelog line by its own wording. */
|
|
138
|
-
export function classifyText(
|
|
213
|
+
export function classifyText(raw) {
|
|
214
|
+
const text = classificationText(raw);
|
|
139
215
|
if (META_BREAKING.test(text))
|
|
140
216
|
return SECURITY.test(text) ? "security" : "change";
|
|
141
217
|
if (EXPLICIT_BREAKING_MARKER.test(text))
|
|
142
218
|
return "breaking";
|
|
219
|
+
if (STATED_COMPATIBLE.test(text))
|
|
220
|
+
return DEPRECATION.test(text) ? "deprecation" : "change";
|
|
143
221
|
if (NON_BREAKING_LABEL.test(text)) {
|
|
144
222
|
if (SECURITY.test(text))
|
|
145
223
|
return "security";
|
|
@@ -158,15 +236,26 @@ export function classifyText(text) {
|
|
|
158
236
|
return "security";
|
|
159
237
|
if (FUTURE_INTENT.test(text))
|
|
160
238
|
return DEPRECATION.test(text) ? "deprecation" : "change";
|
|
161
|
-
if (
|
|
162
|
-
return "
|
|
163
|
-
if (
|
|
239
|
+
if (UNDO.test(text))
|
|
240
|
+
return DEPRECATION.test(text) ? "deprecation" : "change";
|
|
241
|
+
if (!SCOPED_NON_API.test(text)) {
|
|
242
|
+
if (RUNTIME_SUPPORT.test(text) || BREAKING_HEURISTIC.test(text))
|
|
243
|
+
return "breaking";
|
|
244
|
+
if (MADE_PRIVATE.test(text) && !LEADING_FIX.test(text))
|
|
245
|
+
return "breaking";
|
|
246
|
+
if (API_CHANGE.test(text) && !isInternal(text) && !LEADING_FIX.test(text))
|
|
247
|
+
return "breaking";
|
|
248
|
+
}
|
|
249
|
+
if (REMOVAL_HEURISTIC.test(text) && !isInternal(text) && !LEADING_FIX.test(text))
|
|
164
250
|
return "breaking";
|
|
165
251
|
if (DEPRECATION.test(text))
|
|
166
252
|
return "deprecation";
|
|
167
253
|
return "change";
|
|
168
254
|
}
|
|
169
|
-
const
|
|
255
|
+
const FIX_HEADING = /^\W*(?:fixed|fixes|bug ?fixes|bugfixes|fix)\W*$/i;
|
|
256
|
+
// "Other changes" / "Misc" are not additive: pgx 5 files its type removals and renames
|
|
257
|
+
// under "Other Changes", and marshmallow 4 a changed base class.
|
|
258
|
+
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)?)\W*$/i;
|
|
170
259
|
/** Context implied by a heading or label, or null when it carries no signal ("### Changed"). */
|
|
171
260
|
export function classifyHeading(heading) {
|
|
172
261
|
if (/(?<!-)\bbreaking\b|⚠|incompatib|migration|\bmajor changes?\b/i.test(heading))
|
|
@@ -174,34 +263,55 @@ export function classifyHeading(heading) {
|
|
|
174
263
|
// "Removed", "Removals", "Deprecated and Removed", "API changes": a category.
|
|
175
264
|
// "`Cargo.lock` Removed from Tracking" (sqlx 0.9.0) is a news item about the
|
|
176
265
|
// repository, and its three paragraphs were flagged breaking.
|
|
177
|
-
|
|
266
|
+
// "Previously-deprecated APIs have been removed, including" (marshmallow 4) is a removal list, not a deprecation one.
|
|
267
|
+
if (/(?:^\W*|(?:\band|[&/,])\s*)remov(?:e|ed|als?)\b|\bapi changes?\b|\b(?:have|has) been removed\b|\b(?:were|are) removed\b/i.test(heading))
|
|
178
268
|
return "removal";
|
|
179
269
|
if (/security|vulnerab/i.test(heading))
|
|
180
270
|
return "security";
|
|
181
271
|
if (/deprecat/i.test(heading))
|
|
182
272
|
return "deprecation";
|
|
273
|
+
// Fixes are additive, and stricter: the author filed the entry as a bug fix, so API-shaped
|
|
274
|
+
// wording in it ("change default export to named function") is not read as a break.
|
|
275
|
+
if (FIX_HEADING.test(heading))
|
|
276
|
+
return "fixes";
|
|
183
277
|
if (ADDITIVE_HEADING.test(heading))
|
|
184
278
|
return "additive";
|
|
185
279
|
return null;
|
|
186
280
|
}
|
|
187
281
|
/** Final kind: an explicit heading or bullet parent wins; removal and additive contexts refine the line's wording. */
|
|
188
|
-
export function classifyEntry(
|
|
282
|
+
export function classifyEntry(raw, context) {
|
|
189
283
|
if (context === "breaking" || context === "security" || context === "deprecation") {
|
|
190
284
|
// A security line under "Breaking" stays breaking: the heading is the author's own call.
|
|
191
285
|
return context;
|
|
192
286
|
}
|
|
287
|
+
const text = classificationText(raw);
|
|
193
288
|
if (context === "removal") {
|
|
194
289
|
if (META_BREAKING.test(text))
|
|
195
290
|
return "change";
|
|
196
291
|
if (EXPLICIT_BREAKING_MARKER.test(text))
|
|
197
292
|
return "breaking";
|
|
198
|
-
if (
|
|
293
|
+
if (STATED_COMPATIBLE.test(text) || UNDO.test(text))
|
|
294
|
+
return DEPRECATION.test(text) ? "deprecation" : "change";
|
|
295
|
+
// A deprecation filed under Removed is one when it leads with it; "`x` (deprecated in 3.24.0)" was removed.
|
|
296
|
+
if (LEADING_DEPRECATION.test(text) || (DEPRECATION.test(text) && !/\bremov|\(deprecated (?:in|since)\b|\bno longer\b|\bwere replaced\b/i.test(text))) {
|
|
199
297
|
return "deprecation";
|
|
200
|
-
|
|
298
|
+
}
|
|
299
|
+
return isInternal(text) ? "change" : "breaking";
|
|
201
300
|
}
|
|
202
|
-
if (context === "additive") {
|
|
301
|
+
if (context === "additive" || context === "fixes") {
|
|
203
302
|
if (!META_BREAKING.test(text) && EXPLICIT_BREAKING_MARKER.test(text))
|
|
204
303
|
return "breaking";
|
|
304
|
+
// An API removal, rename or raised runtime minimum is breaking under "Features" too
|
|
305
|
+
// (uuid 11 lists "remove UMD build" there; hyper 1.0 "rename to `Http2ClientConnExec`").
|
|
306
|
+
const quiet = STATED_COMPATIBLE.test(text) || UNDO.test(text) || SCOPED_NON_API.test(text) || NON_BREAKING_LABEL.test(text);
|
|
307
|
+
if (!quiet && !META_BREAKING.test(text)) {
|
|
308
|
+
if (RUNTIME_SUPPORT.test(text))
|
|
309
|
+
return "breaking";
|
|
310
|
+
if (context === "additive" && MADE_PRIVATE.test(text) && !LEADING_FIX.test(text))
|
|
311
|
+
return "breaking";
|
|
312
|
+
if (context === "additive" && API_CHANGE.test(text) && !isInternal(text) && !LEADING_FIX.test(text))
|
|
313
|
+
return "breaking";
|
|
314
|
+
}
|
|
205
315
|
if (SECURITY.test(text))
|
|
206
316
|
return "security";
|
|
207
317
|
return DEPRECATION.test(text) ? "deprecation" : "change";
|