@4da/mcp-server 6.0.2 → 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 +265 -0
- package/README.md +13 -10
- 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.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/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 +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 +5 -27
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>;
|