@4da/mcp-server 6.0.2 → 6.2.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 +433 -0
- package/README.md +26 -18
- package/dist/db.d.ts +73 -8
- package/dist/db.js +180 -43
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +15 -9
- package/dist/embeddings.js +19 -7
- package/dist/gitignore.js +9 -3
- package/dist/index.js +43 -4
- package/dist/live/cache.d.ts +31 -2
- package/dist/live/cache.js +113 -1
- package/dist/live/changelog-classify.d.ts +9 -3
- package/dist/live/changelog-classify.js +125 -12
- package/dist/live/changelog.d.ts +22 -3
- package/dist/live/changelog.js +132 -38
- package/dist/live/dev-scope.d.ts +2 -2
- package/dist/live/fix-paths.d.ts +118 -0
- package/dist/live/fix-paths.js +498 -0
- package/dist/live/github-releases.d.ts +69 -0
- package/dist/live/github-releases.js +252 -0
- package/dist/live/go-registry.d.ts +5 -0
- package/dist/live/go-registry.js +1 -1
- package/dist/live/go-releases.d.ts +48 -0
- package/dist/live/go-releases.js +196 -0
- package/dist/live/index.d.ts +34 -8
- package/dist/live/index.js +66 -7
- package/dist/live/lockfile-parents.d.ts +61 -0
- package/dist/live/lockfile-parents.js +360 -0
- package/dist/live/lockfile-parsers-pygo.d.ts +31 -0
- package/dist/live/lockfile-parsers-pygo.js +178 -28
- package/dist/live/lockfile-parsers.d.ts +4 -1
- package/dist/live/lockfile-parsers.js +57 -24
- package/dist/live/lockfile-types.d.ts +12 -3
- package/dist/live/lockfile-types.js +2 -1
- package/dist/live/osv-ranges.d.ts +85 -0
- package/dist/live/osv-ranges.js +136 -0
- package/dist/live/osv-scanner.d.ts +1 -0
- package/dist/live/osv-scanner.js +85 -78
- 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/range-satisfies.d.ts +21 -0
- package/dist/live/range-satisfies.js +190 -0
- package/dist/live/rate-limiter.d.ts +6 -0
- package/dist/live/rate-limiter.js +5 -0
- package/dist/live/registry-health.js +2 -0
- package/dist/live/release-metadata.d.ts +17 -8
- package/dist/live/release-metadata.js +6 -3
- package/dist/live/resolution.d.ts +11 -1
- package/dist/live/resolution.js +16 -6
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +10 -2
- package/dist/live/types.d.ts +17 -1
- 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 +30 -11
- package/dist/live/upgrade-sources.js +93 -21
- package/dist/live/zip-archive.d.ts +22 -0
- package/dist/live/zip-archive.js +98 -0
- package/dist/memory-store.d.ts +19 -0
- package/dist/memory-store.js +8 -0
- package/dist/native-bindings.d.ts +7 -2
- package/dist/native-bindings.js +4 -0
- package/dist/project-tree.d.ts +50 -8
- package/dist/project-tree.js +121 -29
- package/dist/schema-registry.js +5 -5
- package/dist/server-instructions.d.ts +6 -0
- package/dist/server-instructions.js +20 -15
- package/dist/sqlite-driver.d.ts +134 -0
- package/dist/sqlite-driver.js +359 -0
- package/dist/tool-dispatch.d.ts +8 -1
- package/dist/tool-dispatch.js +17 -2
- package/dist/tools/agent-memory.js +10 -2
- package/dist/tools/app-plan.d.ts +11 -1
- package/dist/tools/app-plan.js +32 -6
- package/dist/tools/briefing-task-scope.d.ts +21 -2
- package/dist/tools/briefing-task-scope.js +97 -4
- package/dist/tools/decision-memory.js +9 -3
- package/dist/tools/dependency-check-signals.d.ts +23 -5
- package/dist/tools/dependency-check-signals.js +62 -19
- 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 +27 -11
- package/dist/tools/dependency-check.js +321 -74
- package/dist/tools/dependency-health.d.ts +1 -1
- package/dist/tools/dependency-health.js +5 -0
- package/dist/tools/knowledge-gap-ranges.d.ts +1 -1
- package/dist/tools/knowledge-gap-ranges.js +1 -1
- package/dist/tools/knowledge-gap-scope.d.ts +2 -2
- package/dist/tools/project-scope.d.ts +45 -0
- package/dist/tools/project-scope.js +119 -0
- package/dist/tools/record-feedback.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-literals.js +1 -1
- package/dist/tools/upgrade-impact-report.d.ts +23 -0
- package/dist/tools/upgrade-impact-report.js +45 -6
- package/dist/tools/upgrade-impact.d.ts +13 -4
- package/dist/tools/upgrade-impact.js +236 -85
- package/dist/tools/upgrade-planner-fixpaths.d.ts +40 -0
- package/dist/tools/upgrade-planner-fixpaths.js +0 -0
- package/dist/tools/upgrade-planner.d.ts +31 -3
- package/dist/tools/upgrade-planner.js +74 -19
- package/dist/tools/vulnerability-scan-concise.d.ts +13 -1
- package/dist/tools/vulnerability-scan-concise.js +15 -3
- package/dist/tools/vulnerability-scan-format.d.ts +58 -1
- package/dist/tools/vulnerability-scan-format.js +107 -11
- package/dist/tools/vulnerability-scan.d.ts +104 -13
- package/dist/tools/vulnerability-scan.js +27 -3
- package/dist/tools/what-should-i-know.d.ts +22 -2
- package/dist/tools/what-should-i-know.js +122 -12
- package/package.json +10 -32
package/dist/index.js
CHANGED
|
@@ -51,7 +51,8 @@ import { validateToolArgs } from "./tool-args.js";
|
|
|
51
51
|
// Tool registry: listing, schema resources, category metadata
|
|
52
52
|
import { getCategoryManifest, getSchemaResources, getSlimToolList, getToolSchemaDocument, hasToolSchema, TOOL_REGISTRY, } from "./schema-registry.js";
|
|
53
53
|
// Map-based tool dispatch (replaces per-tool imports + switch statement)
|
|
54
|
-
import { dispatchTool } from "./tool-dispatch.js";
|
|
54
|
+
import { DATABASE_FREE_TOOLS, dispatchTool } from "./tool-dispatch.js";
|
|
55
|
+
import { SqliteUnavailableError, sqliteDriverStatus } from "./sqlite-driver.js";
|
|
55
56
|
import { getPrompt, listPrompts } from "./prompts.js";
|
|
56
57
|
import { checkBuildStaleness } from "./build-staleness.js";
|
|
57
58
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -107,7 +108,7 @@ function getDatabase() {
|
|
|
107
108
|
const cwd = resolveProjectDir();
|
|
108
109
|
// The root and every independently-locked project below it
|
|
109
110
|
// (project-tree.ts): a repo root's own lockfile is often not the only one.
|
|
110
|
-
const tree = scanProjectTree(cwd);
|
|
111
|
+
const { entries: tree, discovery } = scanProjectTree(cwd);
|
|
111
112
|
const scan = tree[0].scan;
|
|
112
113
|
db.populateFromScan(scan);
|
|
113
114
|
const groups = treeResolutionGroups(tree);
|
|
@@ -121,7 +122,7 @@ function getDatabase() {
|
|
|
121
122
|
// Initialize live intelligence with per-ecosystem resolved versions
|
|
122
123
|
// (targets carry platform-gated dep info so advisories can be flagged
|
|
123
124
|
// platform-relevant for the host).
|
|
124
|
-
liveIntel.initFromProjectTree(cwd, groups);
|
|
125
|
+
liveIntel.initFromProjectTree(cwd, groups, discovery);
|
|
125
126
|
if (liveIntel.isEnabled()) {
|
|
126
127
|
console.error(`[4DA] Live intelligence: enabled (OSV.dev)`);
|
|
127
128
|
// The vulnerability scan goes through the warmup so a briefing that
|
|
@@ -206,6 +207,39 @@ function getDatabase() {
|
|
|
206
207
|
}
|
|
207
208
|
return db;
|
|
208
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* No SQLite driver is usable (Node < 22.13 and better-sqlite3's native module
|
|
212
|
+
* unbuilt): the live layer alone, from a project scan and an in-memory cache,
|
|
213
|
+
* so the dependency tools (DATABASE_FREE_TOOLS) still answer. Every other
|
|
214
|
+
* tool returns the driver error, which says how to fix it.
|
|
215
|
+
*/
|
|
216
|
+
function initWithoutDatabase() {
|
|
217
|
+
if (liveIntel)
|
|
218
|
+
return;
|
|
219
|
+
liveIntel = new LiveIntelligence(null);
|
|
220
|
+
setLiveIntelligence(liveIntel);
|
|
221
|
+
const cwd = resolveProjectDir();
|
|
222
|
+
const { entries, discovery } = scanProjectTree(cwd);
|
|
223
|
+
const groups = treeResolutionGroups(entries);
|
|
224
|
+
console.error(`[4DA] No SQLite driver: the dependency tools run without a database (${cwd}).`);
|
|
225
|
+
if (groups.length === 0)
|
|
226
|
+
return;
|
|
227
|
+
liveIntel.initFromProjectTree(cwd, groups, discovery);
|
|
228
|
+
if (liveIntel.isEnabled())
|
|
229
|
+
liveIntel.startVulnerabilityWarmup(cwd);
|
|
230
|
+
}
|
|
231
|
+
/** The database for a tool call; null only for a database-free tool when no SQLite driver is usable. */
|
|
232
|
+
function databaseFor(tool) {
|
|
233
|
+
try {
|
|
234
|
+
return getDatabase();
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
if (!(error instanceof SqliteUnavailableError) || !DATABASE_FREE_TOOLS.has(tool))
|
|
238
|
+
throw error;
|
|
239
|
+
initWithoutDatabase();
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
209
243
|
/** Standalone vs desktop-app database, decided once by the cheap probe (no scan, no resolution). */
|
|
210
244
|
let standaloneMode = null;
|
|
211
245
|
function isStandaloneMode() {
|
|
@@ -372,7 +406,7 @@ export function buildServer() {
|
|
|
372
406
|
}
|
|
373
407
|
}
|
|
374
408
|
try {
|
|
375
|
-
const database =
|
|
409
|
+
const database = databaseFor(name);
|
|
376
410
|
return await dispatchTool(name, database, args);
|
|
377
411
|
}
|
|
378
412
|
catch (error) {
|
|
@@ -517,6 +551,11 @@ async function main() {
|
|
|
517
551
|
// -------------------------------------------------------------------------
|
|
518
552
|
const dbPath = process.env.FOURDA_DB_PATH || undefined;
|
|
519
553
|
const validation = FourDADatabase.validateDatabase(dbPath);
|
|
554
|
+
const sqlite = sqliteDriverStatus();
|
|
555
|
+
if (!sqlite.driver) {
|
|
556
|
+
console.error(`[4DA] ${sqlite.problem}`);
|
|
557
|
+
console.error(` Dependency tools (${[...DATABASE_FREE_TOOLS].join(", ")}) still run, without a database.`);
|
|
558
|
+
}
|
|
520
559
|
if (validation.valid) {
|
|
521
560
|
console.error(`[4DA] Database validated — ${validation.tables?.length ?? 0} tables found`);
|
|
522
561
|
}
|
package/dist/live/cache.d.ts
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SqliteDatabase } from "../sqlite-driver.js";
|
|
2
|
+
/**
|
|
3
|
+
* The server's own cache file (serverCacheDbPath(), `cache.db`), opened once
|
|
4
|
+
* per process. Falls back to an in-memory database when the file cannot be
|
|
5
|
+
* created (a read-only home directory): the server then runs uncached on disk
|
|
6
|
+
* rather than failing. Null when no SQLite driver is usable at all
|
|
7
|
+
* (sqlite-driver.ts); LiveCache then keeps entries in a Map for the session.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getServerCacheDb(): SqliteDatabase | null;
|
|
10
|
+
/** Close the shared cache connection (tests; the next getServerCacheDb() reopens it). */
|
|
11
|
+
export declare function closeServerCacheDb(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Registry, OSV and changelog responses with a TTL.
|
|
14
|
+
*
|
|
15
|
+
* The cache never lives in the desktop app's database. A read-only connection
|
|
16
|
+
* (the app's `4da.db`, see FourDADatabase) is replaced by the server's own
|
|
17
|
+
* cache file; until 6.0.2 the server created a `live_cache` table inside the
|
|
18
|
+
* app's database and filled it.
|
|
19
|
+
*/
|
|
2
20
|
export declare class LiveCache {
|
|
3
21
|
private db;
|
|
4
|
-
|
|
22
|
+
/** Without any SQLite driver: entries for this session only, keyed like the table. */
|
|
23
|
+
private memory;
|
|
24
|
+
constructor(db: SqliteDatabase | null);
|
|
25
|
+
/** The Map store's equivalent of datetime('now') comparisons. */
|
|
26
|
+
private live;
|
|
5
27
|
private ensureTable;
|
|
6
28
|
get<T>(key: string): T | null;
|
|
7
29
|
getStale<T>(key: string): {
|
|
@@ -9,6 +31,13 @@ export declare class LiveCache {
|
|
|
9
31
|
fetchedAt: string;
|
|
10
32
|
} | null;
|
|
11
33
|
set(key: string, data: unknown, source: string, ttlSeconds: number): void;
|
|
34
|
+
/**
|
|
35
|
+
* Many entries in ONE transaction. A scan caches one row per dependency;
|
|
36
|
+
* as separate autocommits each paid its own WAL commit, and superset's
|
|
37
|
+
* 4,516 rows took about 40 s of a 55 s cold scan whose network part ended
|
|
38
|
+
* at 12 s (2026-10-10).
|
|
39
|
+
*/
|
|
40
|
+
setMany(entries: Array<[key: string, data: unknown]>, source: string, ttlSeconds: number): void;
|
|
12
41
|
purgeExpired(): number;
|
|
13
42
|
invalidateSource(source: string): void;
|
|
14
43
|
invalidateAll(): void;
|
package/dist/live/cache.js
CHANGED
|
@@ -1,12 +1,70 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { openDatabase, sqliteDriverStatus } from "../sqlite-driver.js";
|
|
5
|
+
import { serverCacheDbPath } from "../db.js";
|
|
6
|
+
let serverCache = null;
|
|
7
|
+
/**
|
|
8
|
+
* The server's own cache file (serverCacheDbPath(), `cache.db`), opened once
|
|
9
|
+
* per process. Falls back to an in-memory database when the file cannot be
|
|
10
|
+
* created (a read-only home directory): the server then runs uncached on disk
|
|
11
|
+
* rather than failing. Null when no SQLite driver is usable at all
|
|
12
|
+
* (sqlite-driver.ts); LiveCache then keeps entries in a Map for the session.
|
|
13
|
+
*/
|
|
14
|
+
export function getServerCacheDb() {
|
|
15
|
+
if (serverCache?.open)
|
|
16
|
+
return serverCache;
|
|
17
|
+
if (!sqliteDriverStatus().driver)
|
|
18
|
+
return null;
|
|
19
|
+
const file = serverCacheDbPath();
|
|
20
|
+
try {
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
serverCache = openDatabase(file);
|
|
23
|
+
serverCache.pragma("journal_mode = WAL");
|
|
24
|
+
// A cache, not a record: in WAL mode NORMAL never corrupts the file, and
|
|
25
|
+
// a crash can only lose the last few writes, which are refetched.
|
|
26
|
+
serverCache.pragma("synchronous = NORMAL");
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error(`[4da] cannot open the cache at ${file} (${error instanceof Error ? error.message : String(error)}); caching in memory for this session`);
|
|
30
|
+
serverCache = openDatabase(":memory:");
|
|
31
|
+
}
|
|
32
|
+
return serverCache;
|
|
33
|
+
}
|
|
34
|
+
/** Close the shared cache connection (tests; the next getServerCacheDb() reopens it). */
|
|
35
|
+
export function closeServerCacheDb() {
|
|
36
|
+
if (serverCache?.open)
|
|
37
|
+
serverCache.close();
|
|
38
|
+
serverCache = null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Registry, OSV and changelog responses with a TTL.
|
|
42
|
+
*
|
|
43
|
+
* The cache never lives in the desktop app's database. A read-only connection
|
|
44
|
+
* (the app's `4da.db`, see FourDADatabase) is replaced by the server's own
|
|
45
|
+
* cache file; until 6.0.2 the server created a `live_cache` table inside the
|
|
46
|
+
* app's database and filled it.
|
|
47
|
+
*/
|
|
2
48
|
export class LiveCache {
|
|
3
49
|
db;
|
|
50
|
+
/** Without any SQLite driver: entries for this session only, keyed like the table. */
|
|
51
|
+
memory = null;
|
|
4
52
|
constructor(db) {
|
|
5
|
-
this.db = db;
|
|
53
|
+
this.db = !db || db.readonly ? getServerCacheDb() : db;
|
|
54
|
+
if (!this.db) {
|
|
55
|
+
this.memory = new Map();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
6
58
|
this.ensureTable();
|
|
7
59
|
this.purgeExpired();
|
|
8
60
|
}
|
|
61
|
+
/** The Map store's equivalent of datetime('now') comparisons. */
|
|
62
|
+
live(row) {
|
|
63
|
+
return Date.parse(row.expires_at) > Date.now();
|
|
64
|
+
}
|
|
9
65
|
ensureTable() {
|
|
66
|
+
if (!this.db)
|
|
67
|
+
return;
|
|
10
68
|
this.db.exec(`
|
|
11
69
|
CREATE TABLE IF NOT EXISTS live_cache (
|
|
12
70
|
cache_key TEXT PRIMARY KEY,
|
|
@@ -20,6 +78,10 @@ export class LiveCache {
|
|
|
20
78
|
`);
|
|
21
79
|
}
|
|
22
80
|
get(key) {
|
|
81
|
+
if (this.memory) {
|
|
82
|
+
const hit = this.memory.get(key);
|
|
83
|
+
return hit && this.live(hit) ? JSON.parse(hit.data) : null;
|
|
84
|
+
}
|
|
23
85
|
const row = this.db.prepare("SELECT data FROM live_cache WHERE cache_key = ? AND expires_at > datetime('now')").get(key);
|
|
24
86
|
if (!row)
|
|
25
87
|
return null;
|
|
@@ -31,6 +93,10 @@ export class LiveCache {
|
|
|
31
93
|
}
|
|
32
94
|
}
|
|
33
95
|
getStale(key) {
|
|
96
|
+
if (this.memory) {
|
|
97
|
+
const hit = this.memory.get(key);
|
|
98
|
+
return hit ? { data: JSON.parse(hit.data), fetchedAt: hit.fetched_at } : null;
|
|
99
|
+
}
|
|
34
100
|
const row = this.db.prepare("SELECT data, fetched_at FROM live_cache WHERE cache_key = ?").get(key);
|
|
35
101
|
if (!row)
|
|
36
102
|
return null;
|
|
@@ -42,19 +108,65 @@ export class LiveCache {
|
|
|
42
108
|
}
|
|
43
109
|
}
|
|
44
110
|
set(key, data, source, ttlSeconds) {
|
|
111
|
+
if (this.memory) {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const iso = (ms) => new Date(ms).toISOString();
|
|
114
|
+
this.memory.set(key, { cache_key: key, data: JSON.stringify(data), source, fetched_at: iso(now), expires_at: iso(now + ttlSeconds * 1000) });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
45
117
|
this.db.prepare(`
|
|
46
118
|
INSERT OR REPLACE INTO live_cache (cache_key, data, source, fetched_at, expires_at)
|
|
47
119
|
VALUES (?, ?, ?, datetime('now'), datetime('now', '+' || ? || ' seconds'))
|
|
48
120
|
`).run(key, JSON.stringify(data), source, ttlSeconds);
|
|
49
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Many entries in ONE transaction. A scan caches one row per dependency;
|
|
124
|
+
* as separate autocommits each paid its own WAL commit, and superset's
|
|
125
|
+
* 4,516 rows took about 40 s of a 55 s cold scan whose network part ended
|
|
126
|
+
* at 12 s (2026-10-10).
|
|
127
|
+
*/
|
|
128
|
+
setMany(entries, source, ttlSeconds) {
|
|
129
|
+
if (entries.length === 0)
|
|
130
|
+
return;
|
|
131
|
+
if (this.memory) {
|
|
132
|
+
for (const [key, data] of entries)
|
|
133
|
+
this.set(key, data, source, ttlSeconds);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const insert = this.db.prepare(`
|
|
137
|
+
INSERT OR REPLACE INTO live_cache (cache_key, data, source, fetched_at, expires_at)
|
|
138
|
+
VALUES (?, ?, ?, datetime('now'), datetime('now', '+' || ? || ' seconds'))
|
|
139
|
+
`);
|
|
140
|
+
this.db.transaction(() => {
|
|
141
|
+
for (const [key, data] of entries)
|
|
142
|
+
insert.run(key, JSON.stringify(data), source, ttlSeconds);
|
|
143
|
+
})();
|
|
144
|
+
}
|
|
50
145
|
purgeExpired() {
|
|
146
|
+
if (this.memory) {
|
|
147
|
+
let purged = 0;
|
|
148
|
+
for (const [key, row] of this.memory)
|
|
149
|
+
if (!this.live(row) && this.memory.delete(key))
|
|
150
|
+
purged++;
|
|
151
|
+
return purged;
|
|
152
|
+
}
|
|
51
153
|
const result = this.db.prepare("DELETE FROM live_cache WHERE expires_at <= datetime('now')").run();
|
|
52
154
|
return result.changes;
|
|
53
155
|
}
|
|
54
156
|
invalidateSource(source) {
|
|
157
|
+
if (this.memory) {
|
|
158
|
+
for (const [key, row] of this.memory)
|
|
159
|
+
if (row.source === source)
|
|
160
|
+
this.memory.delete(key);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
55
163
|
this.db.prepare("DELETE FROM live_cache WHERE source = ?").run(source);
|
|
56
164
|
}
|
|
57
165
|
invalidateAll() {
|
|
166
|
+
if (this.memory) {
|
|
167
|
+
this.memory.clear();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
58
170
|
this.db.prepare("DELETE FROM live_cache").run();
|
|
59
171
|
}
|
|
60
172
|
}
|
|
@@ -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,47 +236,82 @@ 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))
|
|
173
262
|
return "breaking";
|
|
174
|
-
|
|
263
|
+
// "Removed", "Removals", "Deprecated and Removed", "API changes": a category.
|
|
264
|
+
// "`Cargo.lock` Removed from Tracking" (sqlx 0.9.0) is a news item about the
|
|
265
|
+
// repository, and its three paragraphs were flagged breaking.
|
|
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))
|
|
175
268
|
return "removal";
|
|
176
269
|
if (/security|vulnerab/i.test(heading))
|
|
177
270
|
return "security";
|
|
178
271
|
if (/deprecat/i.test(heading))
|
|
179
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";
|
|
180
277
|
if (ADDITIVE_HEADING.test(heading))
|
|
181
278
|
return "additive";
|
|
182
279
|
return null;
|
|
183
280
|
}
|
|
184
281
|
/** Final kind: an explicit heading or bullet parent wins; removal and additive contexts refine the line's wording. */
|
|
185
|
-
export function classifyEntry(
|
|
282
|
+
export function classifyEntry(raw, context) {
|
|
186
283
|
if (context === "breaking" || context === "security" || context === "deprecation") {
|
|
187
284
|
// A security line under "Breaking" stays breaking: the heading is the author's own call.
|
|
188
285
|
return context;
|
|
189
286
|
}
|
|
287
|
+
const text = classificationText(raw);
|
|
190
288
|
if (context === "removal") {
|
|
191
289
|
if (META_BREAKING.test(text))
|
|
192
290
|
return "change";
|
|
193
291
|
if (EXPLICIT_BREAKING_MARKER.test(text))
|
|
194
292
|
return "breaking";
|
|
195
|
-
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))) {
|
|
196
297
|
return "deprecation";
|
|
197
|
-
|
|
298
|
+
}
|
|
299
|
+
return isInternal(text) ? "change" : "breaking";
|
|
198
300
|
}
|
|
199
|
-
if (context === "additive") {
|
|
301
|
+
if (context === "additive" || context === "fixes") {
|
|
200
302
|
if (!META_BREAKING.test(text) && EXPLICIT_BREAKING_MARKER.test(text))
|
|
201
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
|
+
}
|
|
202
315
|
if (SECURITY.test(text))
|
|
203
316
|
return "security";
|
|
204
317
|
return DEPRECATION.test(text) ? "deprecation" : "change";
|
package/dist/live/changelog.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* conventional `## [7.0.0](https://…/compare/…) (2025-06-24)`
|
|
8
8
|
* plain / prefixed `## 1.2.3`, `# v1.2.3 (2024-01-01)`, `### Version 1.2.3`, `## fastembed 5.0.0`
|
|
9
9
|
* setext `1.2.3` underlined with `===` / `---`
|
|
10
|
+
* reStructuredText `Version 3.0.0` underlined (or over- and underlined)
|
|
11
|
+
* with any of `= - ~ ^ * + # " '`, then `Released 2023-09-30`
|
|
10
12
|
* History.md (express) `1.2.3 / 2020-01-01`
|
|
11
13
|
* A heading only opens a section when the rest of it looks like a trailer
|
|
12
14
|
* (date, link, "yanked") — `## Upgrading from 1.x` or `#### Rust 1.70 support`
|
|
@@ -17,6 +19,13 @@
|
|
|
17
19
|
* reference definitions are skipped: they are examples and URLs, not changes.
|
|
18
20
|
*/
|
|
19
21
|
import { type EntryKind } from "./changelog-classify.js";
|
|
22
|
+
/**
|
|
23
|
+
* Version of the parser AND classifier output. A parsed changelog is cached
|
|
24
|
+
* for 7 days per package version, so a fix to either one stays invisible
|
|
25
|
+
* for a week unless this changes: bump it with every change to parseChangelog
|
|
26
|
+
* or changelog-classify.ts that can alter a section or an entry's kind.
|
|
27
|
+
*/
|
|
28
|
+
export declare const CHANGELOG_PARSER_VERSION = 4;
|
|
20
29
|
export interface ChangelogEntry {
|
|
21
30
|
kind: EntryKind;
|
|
22
31
|
text: string;
|
|
@@ -26,6 +35,14 @@ export interface ChangelogEntry {
|
|
|
26
35
|
* under actix-web's "Removed" reads as harmless, to an agent and to a rater alike.
|
|
27
36
|
*/
|
|
28
37
|
under?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Bullets nested under this entry (feature lists, "Breaking change: ..."
|
|
40
|
+
* notes). They belong to the entry, not beside it: sqlx 0.9.0's Breaking
|
|
41
|
+
* section has 23 top-level bullets, and counting every nested one reported
|
|
42
|
+
* "83 entries flagged breaking" (2026-10-07). A nested bullet that is more
|
|
43
|
+
* severe than its parent raises the parent's kind.
|
|
44
|
+
*/
|
|
45
|
+
details?: string[];
|
|
29
46
|
}
|
|
30
47
|
export interface ChangelogSection {
|
|
31
48
|
version: string;
|
|
@@ -49,8 +66,10 @@ export declare function parseVersionHeading(raw: string): {
|
|
|
49
66
|
} | null;
|
|
50
67
|
/** Parse a changelog into version sections, in document order (usually newest first). */
|
|
51
68
|
export declare function parseChangelog(text: string): ChangelogSection[];
|
|
52
|
-
/**
|
|
53
|
-
export
|
|
69
|
+
/** Version order: 1, -1, 0, or null when either side is unreadable. Semver precedence by default. */
|
|
70
|
+
export type VersionOrder = (a: string, b: string) => number | null;
|
|
71
|
+
/** True when `version` lies in (from, to] by `order`; unreadable versions are out. */
|
|
72
|
+
export declare function inRange(version: string, from: string, to: string, order?: VersionOrder): boolean;
|
|
54
73
|
/**
|
|
55
74
|
* Sections in (from, to], plus whether the changelog reaches both ends of the
|
|
56
75
|
* range: its newest section is at or past `to`, and its oldest at or before
|
|
@@ -58,7 +77,7 @@ export declare function inRange(version: string, from: string, to: string): bool
|
|
|
58
77
|
* stopped being maintained two majors ago is reported as not covering the
|
|
59
78
|
* range instead of silently presenting a partial history as the whole one.
|
|
60
79
|
*/
|
|
61
|
-
export declare function selectRange(sections: ChangelogSection[], from: string, to: string, firstInRange: string): {
|
|
80
|
+
export declare function selectRange(sections: ChangelogSection[], from: string, to: string, firstInRange: string, order?: VersionOrder): {
|
|
62
81
|
sections: ChangelogSection[];
|
|
63
82
|
coversRange: boolean;
|
|
64
83
|
};
|