@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +13 -10
  3. package/dist/db.d.ts +73 -8
  4. package/dist/db.js +180 -43
  5. package/dist/doctor.d.ts +1 -1
  6. package/dist/doctor.js +15 -9
  7. package/dist/embeddings.js +19 -7
  8. package/dist/gitignore.js +9 -3
  9. package/dist/index.js +43 -4
  10. package/dist/live/cache.d.ts +31 -2
  11. package/dist/live/cache.js +113 -1
  12. package/dist/live/changelog-classify.js +4 -1
  13. package/dist/live/changelog.d.ts +15 -0
  14. package/dist/live/changelog.js +67 -11
  15. package/dist/live/dev-scope.d.ts +2 -2
  16. package/dist/live/fix-paths.d.ts +118 -0
  17. package/dist/live/fix-paths.js +498 -0
  18. package/dist/live/go-registry.d.ts +5 -0
  19. package/dist/live/go-registry.js +1 -1
  20. package/dist/live/index.d.ts +30 -3
  21. package/dist/live/index.js +59 -6
  22. package/dist/live/lockfile-parents.d.ts +61 -0
  23. package/dist/live/lockfile-parents.js +360 -0
  24. package/dist/live/lockfile-parsers-pygo.d.ts +31 -0
  25. package/dist/live/lockfile-parsers-pygo.js +178 -28
  26. package/dist/live/lockfile-parsers.d.ts +4 -1
  27. package/dist/live/lockfile-parsers.js +57 -24
  28. package/dist/live/lockfile-types.d.ts +12 -3
  29. package/dist/live/lockfile-types.js +2 -1
  30. package/dist/live/osv-ranges.d.ts +79 -0
  31. package/dist/live/osv-ranges.js +133 -0
  32. package/dist/live/osv-scanner.d.ts +1 -0
  33. package/dist/live/osv-scanner.js +85 -78
  34. package/dist/live/range-satisfies.d.ts +21 -0
  35. package/dist/live/range-satisfies.js +190 -0
  36. package/dist/live/rate-limiter.d.ts +6 -0
  37. package/dist/live/rate-limiter.js +5 -0
  38. package/dist/live/registry-health.js +2 -0
  39. package/dist/live/resolution.d.ts +11 -1
  40. package/dist/live/resolution.js +16 -6
  41. package/dist/live/semver-utils.d.ts +8 -0
  42. package/dist/live/semver-utils.js +10 -2
  43. package/dist/live/types.d.ts +17 -1
  44. package/dist/live/upgrade-sources.d.ts +2 -0
  45. package/dist/live/upgrade-sources.js +7 -2
  46. package/dist/memory-store.d.ts +19 -0
  47. package/dist/memory-store.js +8 -0
  48. package/dist/native-bindings.d.ts +7 -2
  49. package/dist/native-bindings.js +4 -0
  50. package/dist/project-tree.d.ts +50 -8
  51. package/dist/project-tree.js +121 -29
  52. package/dist/schema-registry.js +2 -2
  53. package/dist/sqlite-driver.d.ts +134 -0
  54. package/dist/sqlite-driver.js +359 -0
  55. package/dist/tool-dispatch.d.ts +8 -1
  56. package/dist/tool-dispatch.js +17 -2
  57. package/dist/tools/agent-memory.js +10 -2
  58. package/dist/tools/app-plan.d.ts +11 -1
  59. package/dist/tools/app-plan.js +32 -6
  60. package/dist/tools/briefing-task-scope.d.ts +21 -2
  61. package/dist/tools/briefing-task-scope.js +97 -4
  62. package/dist/tools/decision-memory.js +9 -3
  63. package/dist/tools/dependency-check-signals.d.ts +7 -0
  64. package/dist/tools/dependency-check-signals.js +17 -2
  65. package/dist/tools/dependency-check.d.ts +1 -1
  66. package/dist/tools/dependency-check.js +1 -1
  67. package/dist/tools/dependency-health.d.ts +1 -1
  68. package/dist/tools/dependency-health.js +5 -0
  69. package/dist/tools/knowledge-gap-ranges.d.ts +1 -1
  70. package/dist/tools/knowledge-gap-ranges.js +1 -1
  71. package/dist/tools/knowledge-gap-scope.d.ts +2 -2
  72. package/dist/tools/project-scope.d.ts +45 -0
  73. package/dist/tools/project-scope.js +119 -0
  74. package/dist/tools/record-feedback.js +1 -1
  75. package/dist/tools/upgrade-impact-literals.js +1 -1
  76. package/dist/tools/upgrade-impact-report.d.ts +10 -0
  77. package/dist/tools/upgrade-impact-report.js +32 -2
  78. package/dist/tools/upgrade-impact.js +1 -1
  79. package/dist/tools/upgrade-planner-fixpaths.d.ts +40 -0
  80. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  81. package/dist/tools/upgrade-planner.d.ts +31 -3
  82. package/dist/tools/upgrade-planner.js +74 -19
  83. package/dist/tools/vulnerability-scan-concise.d.ts +13 -1
  84. package/dist/tools/vulnerability-scan-concise.js +15 -3
  85. package/dist/tools/vulnerability-scan-format.d.ts +58 -1
  86. package/dist/tools/vulnerability-scan-format.js +107 -11
  87. package/dist/tools/vulnerability-scan.d.ts +104 -13
  88. package/dist/tools/vulnerability-scan.js +27 -3
  89. package/dist/tools/what-should-i-know.d.ts +22 -2
  90. package/dist/tools/what-should-i-know.js +121 -11
  91. package/package.json +5 -27
@@ -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
- if (/\bremov(?:e|ed|als?)\b|\bapi changes?\b/i.test(heading))
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";
@@ -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;
@@ -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
- const text = raw.trim().replace(/^\*\*|\*\*$/g, "");
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
- if (state.pending && state.current) {
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
- state.pending = { text, context, indent, under };
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)
@@ -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 Database from "better-sqlite3";
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: Database.Database | null, deps: ResolvedDependency[]): ResolvedDependency[];
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>;