@4da/mcp-server 6.0.1 → 6.1.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 +316 -0
- package/README.md +19 -13
- package/dist/db.d.ts +88 -7
- package/dist/db.js +248 -63
- 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 +54 -7
- package/dist/live/cache.d.ts +31 -2
- package/dist/live/cache.js +113 -1
- package/dist/live/changelog-classify.js +4 -1
- package/dist/live/changelog.d.ts +15 -0
- package/dist/live/changelog.js +67 -11
- 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/go-registry.d.ts +5 -0
- package/dist/live/go-registry.js +1 -1
- package/dist/live/index.d.ts +30 -3
- package/dist/live/index.js +59 -6
- 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 +79 -0
- package/dist/live/osv-ranges.js +133 -0
- package/dist/live/osv-scanner.d.ts +1 -0
- package/dist/live/osv-scanner.js +85 -78
- 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/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-sources.d.ts +2 -0
- package/dist/live/upgrade-sources.js +7 -2
- 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 +2 -2
- package/dist/setup.d.ts +76 -3
- package/dist/setup.js +313 -130
- package/dist/sqlite-driver.d.ts +134 -0
- package/dist/sqlite-driver.js +359 -0
- package/dist/tool-dispatch.d.ts +10 -1
- package/dist/tool-dispatch.js +37 -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 +7 -0
- package/dist/tools/dependency-check-signals.js +17 -2
- package/dist/tools/dependency-check.d.ts +1 -1
- package/dist/tools/dependency-check.js +1 -1
- 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-literals.js +1 -1
- package/dist/tools/upgrade-impact-report.d.ts +10 -0
- package/dist/tools/upgrade-impact-report.js +32 -2
- package/dist/tools/upgrade-impact.js +1 -1
- 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 +121 -11
- package/package.json +10 -31
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);
|
|
@@ -68,7 +69,7 @@ const SERVER_VERSION = (() => {
|
|
|
68
69
|
return "0.0.0";
|
|
69
70
|
}
|
|
70
71
|
})();
|
|
71
|
-
import { createDatabase, DEPENDENCY_GROUP_QUERY, FourDADatabase } from "./db.js";
|
|
72
|
+
import { createDatabase, DEPENDENCY_GROUP_QUERY, FourDADatabase, isUnreadableDbError, unreadableDbMessage, } from "./db.js";
|
|
72
73
|
// =============================================================================
|
|
73
74
|
// Server Setup
|
|
74
75
|
// =============================================================================
|
|
@@ -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,11 +406,18 @@ 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) {
|
|
379
|
-
|
|
413
|
+
// A database that turns out damaged mid-session (SQLITE_CORRUPT on a
|
|
414
|
+
// page the open did not touch) gets the same guidance as one that
|
|
415
|
+
// fails to open, instead of the bare SQLite text.
|
|
416
|
+
const errorMessage = db && isUnreadableDbError(error)
|
|
417
|
+
? unreadableDbMessage(db.dbPath, error)
|
|
418
|
+
: error instanceof Error
|
|
419
|
+
? error.message
|
|
420
|
+
: String(error);
|
|
380
421
|
return {
|
|
381
422
|
content: [
|
|
382
423
|
{
|
|
@@ -441,6 +482,7 @@ async function main() {
|
|
|
441
482
|
refused unless MCP_AUTH_SECRET is set; authentication
|
|
442
483
|
is then mandatory for every request.
|
|
443
484
|
--setup Detect editors and write MCP config
|
|
485
|
+
--dry-run With --setup: print each file and entry, write nothing
|
|
444
486
|
--doctor Validate database, bindings, and LLM providers
|
|
445
487
|
|
|
446
488
|
Environment:
|
|
@@ -463,7 +505,7 @@ async function main() {
|
|
|
463
505
|
}
|
|
464
506
|
// Setup command: configure editors
|
|
465
507
|
if (args.includes("--setup") || args.includes("setup")) {
|
|
466
|
-
runSetup();
|
|
508
|
+
runSetup(args.includes("--dry-run"));
|
|
467
509
|
return;
|
|
468
510
|
}
|
|
469
511
|
// Doctor command: validate installation health
|
|
@@ -509,6 +551,11 @@ async function main() {
|
|
|
509
551
|
// -------------------------------------------------------------------------
|
|
510
552
|
const dbPath = process.env.FOURDA_DB_PATH || undefined;
|
|
511
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
|
+
}
|
|
512
559
|
if (validation.valid) {
|
|
513
560
|
console.error(`[4DA] Database validated — ${validation.tables?.length ?? 0} tables found`);
|
|
514
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
|
}
|
|
@@ -171,7 +171,10 @@ const ADDITIVE_HEADING = /^\W*(?:added|adds|new|new features?|features?|enhancem
|
|
|
171
171
|
export function classifyHeading(heading) {
|
|
172
172
|
if (/(?<!-)\bbreaking\b|⚠|incompatib|migration|\bmajor changes?\b/i.test(heading))
|
|
173
173
|
return "breaking";
|
|
174
|
-
|
|
174
|
+
// "Removed", "Removals", "Deprecated and Removed", "API changes": a category.
|
|
175
|
+
// "`Cargo.lock` Removed from Tracking" (sqlx 0.9.0) is a news item about the
|
|
176
|
+
// repository, and its three paragraphs were flagged breaking.
|
|
177
|
+
if (/(?:^\W*|(?:\band|[&/,])\s*)remov(?:e|ed|als?)\b|\bapi changes?\b/i.test(heading))
|
|
175
178
|
return "removal";
|
|
176
179
|
if (/security|vulnerab/i.test(heading))
|
|
177
180
|
return "security";
|
package/dist/live/changelog.d.ts
CHANGED
|
@@ -17,6 +17,13 @@
|
|
|
17
17
|
* reference definitions are skipped: they are examples and URLs, not changes.
|
|
18
18
|
*/
|
|
19
19
|
import { type EntryKind } from "./changelog-classify.js";
|
|
20
|
+
/**
|
|
21
|
+
* Version of the parser AND classifier output. A parsed changelog is cached
|
|
22
|
+
* for 7 days per package version, so a fix to either one stays invisible
|
|
23
|
+
* for a week unless this changes: bump it with every change to parseChangelog
|
|
24
|
+
* or changelog-classify.ts that can alter a section or an entry's kind.
|
|
25
|
+
*/
|
|
26
|
+
export declare const CHANGELOG_PARSER_VERSION = 2;
|
|
20
27
|
export interface ChangelogEntry {
|
|
21
28
|
kind: EntryKind;
|
|
22
29
|
text: string;
|
|
@@ -26,6 +33,14 @@ export interface ChangelogEntry {
|
|
|
26
33
|
* under actix-web's "Removed" reads as harmless, to an agent and to a rater alike.
|
|
27
34
|
*/
|
|
28
35
|
under?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Bullets nested under this entry (feature lists, "Breaking change: ..."
|
|
38
|
+
* notes). They belong to the entry, not beside it: sqlx 0.9.0's Breaking
|
|
39
|
+
* section has 23 top-level bullets, and counting every nested one reported
|
|
40
|
+
* "83 entries flagged breaking" (2026-10-07). A nested bullet that is more
|
|
41
|
+
* severe than its parent raises the parent's kind.
|
|
42
|
+
*/
|
|
43
|
+
details?: string[];
|
|
29
44
|
}
|
|
30
45
|
export interface ChangelogSection {
|
|
31
46
|
version: string;
|
package/dist/live/changelog.js
CHANGED
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { compareVersionPrecedence } from "./semver-precedence.js";
|
|
21
21
|
import { classifyEntry, classifyHeading, sanitizeEntry } 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 const CHANGELOG_PARSER_VERSION = 2;
|
|
22
29
|
const NAME_STEMS = ["changelog", "changes", "history", "releases", "release-notes", "release_notes", "releasenotes", "news"];
|
|
23
30
|
const NAME_EXTS = ["", ".md", ".markdown", ".txt", ".rst"];
|
|
24
31
|
/** True for a root-level basename that names a changelog (case-insensitive). */
|
|
@@ -71,7 +78,14 @@ function extractDate(text) {
|
|
|
71
78
|
* heading. Exported for tests.
|
|
72
79
|
*/
|
|
73
80
|
export function parseVersionHeading(raw) {
|
|
74
|
-
|
|
81
|
+
// Inline HTML is markup, not heading text: stripe writes
|
|
82
|
+
// `## <a id="23-0-0"></a>23.0.0 - 2026-09-30`, and until 6.0.2 no stripe
|
|
83
|
+
// release was found (changelog.found: false, 2026-10-07).
|
|
84
|
+
const text = raw
|
|
85
|
+
.replace(/<\/?[A-Za-z][^<>]*>/g, " ")
|
|
86
|
+
.trim()
|
|
87
|
+
.replace(/^\*\*|\*\*$/g, "")
|
|
88
|
+
.trim();
|
|
75
89
|
const m = HEADING.exec(text);
|
|
76
90
|
if (!m)
|
|
77
91
|
return null;
|
|
@@ -105,17 +119,37 @@ const PLAIN_LABEL = /^\s{0,3}([A-Za-z⚠][\w /&,'()⚠️-]{1,58}?)\s*:\s*$/;
|
|
|
105
119
|
const HTML_LAYOUT = /^<\/?(?:details|summary|div|p|br|hr|img|picture|source|table|tr|td|th|thead|tbody|center|sup|sub)\b/i;
|
|
106
120
|
/** A bullet that is only a label: "remove:", "**Breaking**:", "deps:". */
|
|
107
121
|
const LABEL_BULLET = /^\W*([A-Za-z][\w -]{0,30}?)\W*:\W*$/;
|
|
122
|
+
const KIND_SEVERITY = { breaking: 3, security: 2, deprecation: 1, change: 0 };
|
|
123
|
+
/** Nested lines kept per entry, and the length of each. */
|
|
124
|
+
const MAX_DETAILS = 12;
|
|
125
|
+
const MAX_DETAIL_CHARS = 200;
|
|
108
126
|
function flush(state) {
|
|
109
|
-
|
|
110
|
-
const text = sanitizeEntry(state.pending.text);
|
|
111
|
-
const under = state.pending.under;
|
|
112
|
-
// Layout markup is not a change: actix-web 4 wraps pre-release notes in
|
|
113
|
-
// `<details> <summary>...</summary>`, and both lines were counted as removals.
|
|
114
|
-
if (text && !HTML_LAYOUT.test(text)) {
|
|
115
|
-
state.current.entries.push({ kind: classifyEntry(text, state.pending.context), text, ...(under ? { under } : {}) });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
127
|
+
const pending = state.pending;
|
|
118
128
|
state.pending = null;
|
|
129
|
+
if (!pending || !state.current)
|
|
130
|
+
return;
|
|
131
|
+
const text = sanitizeEntry(pending.text);
|
|
132
|
+
// Layout markup is not a change: actix-web 4 wraps pre-release notes in
|
|
133
|
+
// `<details> <summary>...</summary>`, and both lines were counted as removals.
|
|
134
|
+
if (!text || HTML_LAYOUT.test(text))
|
|
135
|
+
return;
|
|
136
|
+
const kind = classifyEntry(text, pending.context);
|
|
137
|
+
if (pending.into) {
|
|
138
|
+
const parent = pending.into;
|
|
139
|
+
const details = (parent.details ??= []);
|
|
140
|
+
if (details.length < MAX_DETAILS)
|
|
141
|
+
details.push(sanitizeEntry(pending.text, MAX_DETAIL_CHARS));
|
|
142
|
+
// "Breaking change: adds a DB type parameter" under a plain "fix: RawSql
|
|
143
|
+
// lifetime issues" bullet makes the entry breaking.
|
|
144
|
+
if (KIND_SEVERITY[kind] > KIND_SEVERITY[parent.kind])
|
|
145
|
+
parent.kind = kind;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const under = pending.under;
|
|
149
|
+
const entry = { kind, text, ...(under ? { under } : {}) };
|
|
150
|
+
state.current.entries.push(entry);
|
|
151
|
+
if (pending.outer)
|
|
152
|
+
state.parentEntry = entry;
|
|
119
153
|
}
|
|
120
154
|
/**
|
|
121
155
|
* Headings that name a kind of change (keep-a-changelog and conventional-commit
|
|
@@ -135,6 +169,7 @@ function openSection(state, heading, level) {
|
|
|
135
169
|
state.parentIndent = null;
|
|
136
170
|
state.headingText = null;
|
|
137
171
|
state.parentText = null;
|
|
172
|
+
state.parentEntry = null;
|
|
138
173
|
}
|
|
139
174
|
/** A heading or parent bullet as context text: emphasis and code marks dropped, one line, at most 80 characters. */
|
|
140
175
|
function contextText(raw) {
|
|
@@ -156,6 +191,8 @@ function addLine(state, line) {
|
|
|
156
191
|
if (label) {
|
|
157
192
|
state.parentKind = classifyHeading(label[1]) ?? state.headingKind;
|
|
158
193
|
state.parentText = contextText(label[1]);
|
|
194
|
+
// A label's children are entries of their own, under the label.
|
|
195
|
+
state.parentEntry = null;
|
|
159
196
|
return;
|
|
160
197
|
}
|
|
161
198
|
// A breaking bullet lends "breaking" to its sub-points (date-fns 3.0: "**BREAKING**: Functions
|
|
@@ -167,7 +204,9 @@ function addLine(state, line) {
|
|
|
167
204
|
}
|
|
168
205
|
const context = !outer && state.parentKind ? state.parentKind : state.headingKind;
|
|
169
206
|
const under = !outer && state.parentText ? state.parentText : state.headingText;
|
|
170
|
-
|
|
207
|
+
if (outer)
|
|
208
|
+
state.parentEntry = null;
|
|
209
|
+
state.pending = { text, context, indent, under, outer, into: outer ? null : state.parentEntry };
|
|
171
210
|
return;
|
|
172
211
|
}
|
|
173
212
|
if (line.trim() === "") {
|
|
@@ -182,7 +221,21 @@ function addLine(state, line) {
|
|
|
182
221
|
state.pending.text += ` ${line.trim()}`; // wrapped paragraph line
|
|
183
222
|
return;
|
|
184
223
|
}
|
|
224
|
+
// An indented paragraph after a blank line continues the list item above it
|
|
225
|
+
// (stripe 23.0.0: "* ⚠️ Remove ErrorType export", blank, " Remove the ErrorType
|
|
226
|
+
// interface ..."): a detail of that entry, not an entry of its own.
|
|
227
|
+
if (!state.pending && state.parentEntry && /^\s{2,}\S/.test(line)) {
|
|
228
|
+
state.pending = {
|
|
229
|
+
text: line.trim(),
|
|
230
|
+
context: state.parentKind ?? state.headingKind,
|
|
231
|
+
indent: -2,
|
|
232
|
+
under: state.headingText,
|
|
233
|
+
into: state.parentEntry,
|
|
234
|
+
};
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
185
237
|
flush(state);
|
|
238
|
+
state.parentEntry = null;
|
|
186
239
|
state.pending = { text: line, context: state.headingKind, indent: -1, under: state.headingText };
|
|
187
240
|
}
|
|
188
241
|
/** Parse a changelog into version sections, in document order (usually newest first). */
|
|
@@ -198,6 +251,7 @@ export function parseChangelog(text) {
|
|
|
198
251
|
headingText: null,
|
|
199
252
|
parentText: null,
|
|
200
253
|
pending: null,
|
|
254
|
+
parentEntry: null,
|
|
201
255
|
};
|
|
202
256
|
let inFence = false;
|
|
203
257
|
let inComment = false;
|
|
@@ -244,6 +298,7 @@ export function parseChangelog(text) {
|
|
|
244
298
|
state.parentKind = null;
|
|
245
299
|
state.parentIndent = null;
|
|
246
300
|
state.parentText = null;
|
|
301
|
+
state.parentEntry = null;
|
|
247
302
|
}
|
|
248
303
|
continue;
|
|
249
304
|
}
|
|
@@ -288,6 +343,7 @@ export function parseChangelog(text) {
|
|
|
288
343
|
state.parentKind = null;
|
|
289
344
|
state.parentIndent = null;
|
|
290
345
|
state.parentText = null;
|
|
346
|
+
state.parentEntry = null;
|
|
291
347
|
continue;
|
|
292
348
|
}
|
|
293
349
|
if (state.current)
|
package/dist/live/dev-scope.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* claim a certainty the app itself does not have. The grade would not change
|
|
14
14
|
* (unknown and runtime both get no discount), but `dev_scope_known` would lie.
|
|
15
15
|
*/
|
|
16
|
-
import type
|
|
16
|
+
import type { SqliteDatabase } from "../sqlite-driver.js";
|
|
17
17
|
import type { ResolvedDependency } from "./types.js";
|
|
18
18
|
/**
|
|
19
19
|
* A project path in the app's storage form: forward slashes, lowercased on
|
|
@@ -29,4 +29,4 @@ export declare function canonicalStoragePath(p: string): string;
|
|
|
29
29
|
* dependencies are untouched: their manifest already says. Returns new
|
|
30
30
|
* objects; never throws; a database without the table changes nothing.
|
|
31
31
|
*/
|
|
32
|
-
export declare function applyInstanceDevScope(db:
|
|
32
|
+
export declare function applyInstanceDevScope(db: SqliteDatabase | null, deps: ResolvedDependency[]): ResolvedDependency[];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix paths for vulnerable dependencies: WHICH version to move to, and HOW.
|
|
3
|
+
*
|
|
4
|
+
* Two defects found by the fix-path oracle (2026-10-10, 19 public repos;
|
|
5
|
+
* each recommendation applied in a copy, re-resolved, re-scanned):
|
|
6
|
+
*
|
|
7
|
+
* 1. The target could itself be vulnerable. It was the highest fix among the
|
|
8
|
+
* advisories that affect the installed version, so an advisory the
|
|
9
|
+
* installed version predates was never consulted: openssl 0.10.38 was sent
|
|
10
|
+
* to 0.10.79, which GHSA-phqj-4mhp-q6mq affects (0.10.50 up to 0.10.80).
|
|
11
|
+
* Now: the smallest published version at or above every per-advisory fix
|
|
12
|
+
* that NO advisory of the package affects (osv-ranges.ts).
|
|
13
|
+
* 2. Every transitive finding was "waiting on upstream", 3 wrong out of 3:
|
|
14
|
+
* minimist 1.2.5 (npm), braces 3.0.2 (pnpm) and mio 0.8.0 (Cargo) all had
|
|
15
|
+
* parents whose declared requirement already admitted the fix, and a
|
|
16
|
+
* lockfile refresh fixed each one. Now: the parents' requirements are
|
|
17
|
+
* read (from the lockfile, or from the parent's registry metadata for
|
|
18
|
+
* pnpm and Cargo) and a refresh is recommended when they all admit a
|
|
19
|
+
* clean version.
|
|
20
|
+
*
|
|
21
|
+
* Network: api.osv.dev (one /v1/query per vulnerable package) and the
|
|
22
|
+
* package's own registry; package names and versions only. Every lookup is
|
|
23
|
+
* cached, and any failure degrades to the old answer, never to a guess.
|
|
24
|
+
*/
|
|
25
|
+
import type { LiveCache } from "./cache.js";
|
|
26
|
+
import type { RateLimiter } from "./rate-limiter.js";
|
|
27
|
+
import { type AdvisoryRanges, type CleanTarget } from "./osv-ranges.js";
|
|
28
|
+
import { type LockfileFormat } from "./lockfile-parents.js";
|
|
29
|
+
import type { OsvEcosystem } from "./types.js";
|
|
30
|
+
export interface CrateDepReq {
|
|
31
|
+
req: string;
|
|
32
|
+
optional: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface CratesSparse {
|
|
35
|
+
/** Versions not yanked. */
|
|
36
|
+
versions: string[];
|
|
37
|
+
/** version -> dependency crate name -> requirements (one per target/kind entry; dev-dependencies left out). */
|
|
38
|
+
reqs: Record<string, Record<string, CrateDepReq[]>>;
|
|
39
|
+
}
|
|
40
|
+
export declare class FixPathSources {
|
|
41
|
+
private cache;
|
|
42
|
+
private rateLimiter;
|
|
43
|
+
constructor(cache: LiveCache, rateLimiter: RateLimiter);
|
|
44
|
+
/** Every non-withdrawn OSV advisory for a package, as ranges; null when OSV could not be read. */
|
|
45
|
+
packageAdvisories(ecosystem: OsvEcosystem, name: string): Promise<AdvisoryRanges[] | null>;
|
|
46
|
+
/** Published, installable versions (not yanked); null when the registry could not be read or is not supported. */
|
|
47
|
+
publishedVersions(ecosystem: OsvEcosystem, name: string): Promise<string[] | null>;
|
|
48
|
+
/**
|
|
49
|
+
* The requirements `parent@parentVersion` declares on `dep`, from the
|
|
50
|
+
* registry: npm (pnpm lockfiles record none) and crates.io (Cargo.lock
|
|
51
|
+
* records none). Null when unknown; [] when the parent declares none.
|
|
52
|
+
*/
|
|
53
|
+
parentRequirements(ecosystem: OsvEcosystem, parent: string, parentVersion: string, dep: string): Promise<string[] | null>;
|
|
54
|
+
private npmVersions;
|
|
55
|
+
/** A crate's sparse-index record: versions and per-version dependency requirements. */
|
|
56
|
+
cratesSparse(name: string): Promise<CratesSparse | null>;
|
|
57
|
+
private pypiVersions;
|
|
58
|
+
private goVersions;
|
|
59
|
+
}
|
|
60
|
+
export type FixTargetResult = CleanTarget;
|
|
61
|
+
/** The minimal clean target for one installed copy, or null when the advisories could not be read. */
|
|
62
|
+
export declare function cleanFixTarget(sources: FixPathSources, ecosystem: OsvEcosystem, name: string, perAdvisoryFixes: string[]): Promise<FixTargetResult | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Verified targets for many installed copies at once, within a time budget:
|
|
65
|
+
* the copies that could not be checked in time (or offline) are simply
|
|
66
|
+
* absent from the result, and the caller says so. Keyed by `key`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function verifyFixTargets(sources: FixPathSources | null, items: Array<{
|
|
69
|
+
key: string;
|
|
70
|
+
ecosystem: OsvEcosystem;
|
|
71
|
+
name: string;
|
|
72
|
+
fixes: string[];
|
|
73
|
+
}>, budgetMs: number, concurrency?: number): Promise<Map<string, CleanTarget>>;
|
|
74
|
+
/** One clause for a target that is not the highest per-advisory fix, or null when it is. */
|
|
75
|
+
export declare function targetNote(clean: CleanTarget): string | null;
|
|
76
|
+
export interface RefreshVerdict {
|
|
77
|
+
/** True when every parent's requirement already admits a clean version. */
|
|
78
|
+
refreshFixes: boolean;
|
|
79
|
+
format: LockfileFormat;
|
|
80
|
+
/** The command, run in the lockfile's directory. */
|
|
81
|
+
command: string;
|
|
82
|
+
/** Parent requirements that do NOT admit a clean version (why it is waiting on upstream). */
|
|
83
|
+
blocking: Array<{
|
|
84
|
+
parent: string;
|
|
85
|
+
parentVersion: string | null;
|
|
86
|
+
requirement: string;
|
|
87
|
+
}>;
|
|
88
|
+
/** The version a refresh resolves to, when the registry's version list was read. */
|
|
89
|
+
resolvesTo: string | null;
|
|
90
|
+
}
|
|
91
|
+
/** The refresh command for one lockfile format. */
|
|
92
|
+
export declare function refreshCommand(format: LockfileFormat, name: string, installed: string, target: string): string;
|
|
93
|
+
/**
|
|
94
|
+
* Whether refreshing `lockfile` moves `name@installed` to a clean version
|
|
95
|
+
* without any parent changing its declared requirement. Null when the
|
|
96
|
+
* lockfile or a requirement cannot be read (the caller keeps "waiting on
|
|
97
|
+
* upstream", the conservative answer).
|
|
98
|
+
*/
|
|
99
|
+
export declare function refreshVerdict(sources: FixPathSources, ecosystem: OsvEcosystem, lockfile: string, name: string, installed: string, target: string, advisories: AdvisoryRanges[] | null, published: string[] | null): Promise<RefreshVerdict | null>;
|
|
100
|
+
/**
|
|
101
|
+
* The locked crates `cargo update -p name@installed --precise target` cannot
|
|
102
|
+
* move by itself, as `crate@lockedVersion` specs to update first; null when it
|
|
103
|
+
* cannot be worked out (a registry read failed, or the walk is too large).
|
|
104
|
+
*
|
|
105
|
+
* Measured on nushell (2026-10-10): `cargo update -p openssl@0.10.38
|
|
106
|
+
* --precise 0.10.80` fails. 0.10.80 needs openssl-macros 0.1.1 -> syn 2 ->
|
|
107
|
+
* quote ^1.0.25, and quote 1.0.15 is locked by rstest, outside openssl's own
|
|
108
|
+
* dependency tree, so cargo keeps it and reports a conflict. `cargo update -p
|
|
109
|
+
* quote` first, then the precise update, works. Crates in the installed
|
|
110
|
+
* version's own tree may move with it (openssl-sys did) and are not listed.
|
|
111
|
+
*
|
|
112
|
+
* The walk follows the target's normal and build dependencies (optional ones
|
|
113
|
+
* only when the crate is already in the lockfile): a requirement a locked copy
|
|
114
|
+
* satisfies is reused; one whose compatibility line holds a locked copy that
|
|
115
|
+
* does not satisfy it names that copy; anything else is a new crate, walked
|
|
116
|
+
* at its newest matching version.
|
|
117
|
+
*/
|
|
118
|
+
export declare function cargoPrecisePrerequisites(sources: FixPathSources, lockContent: string, name: string, installed: string, target: string): Promise<string[] | null>;
|