@4da/mcp-server 6.0.2 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/README.md +26 -18
  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.d.ts +9 -3
  13. package/dist/live/changelog-classify.js +125 -12
  14. package/dist/live/changelog.d.ts +22 -3
  15. package/dist/live/changelog.js +132 -38
  16. package/dist/live/dev-scope.d.ts +2 -2
  17. package/dist/live/fix-paths.d.ts +118 -0
  18. package/dist/live/fix-paths.js +498 -0
  19. package/dist/live/github-releases.d.ts +69 -0
  20. package/dist/live/github-releases.js +252 -0
  21. package/dist/live/go-registry.d.ts +5 -0
  22. package/dist/live/go-registry.js +1 -1
  23. package/dist/live/go-releases.d.ts +48 -0
  24. package/dist/live/go-releases.js +196 -0
  25. package/dist/live/index.d.ts +34 -8
  26. package/dist/live/index.js +66 -7
  27. package/dist/live/lockfile-parents.d.ts +61 -0
  28. package/dist/live/lockfile-parents.js +360 -0
  29. package/dist/live/lockfile-parsers-pygo.d.ts +31 -0
  30. package/dist/live/lockfile-parsers-pygo.js +178 -28
  31. package/dist/live/lockfile-parsers.d.ts +4 -1
  32. package/dist/live/lockfile-parsers.js +57 -24
  33. package/dist/live/lockfile-types.d.ts +12 -3
  34. package/dist/live/lockfile-types.js +2 -1
  35. package/dist/live/osv-ranges.d.ts +85 -0
  36. package/dist/live/osv-ranges.js +136 -0
  37. package/dist/live/osv-scanner.d.ts +1 -0
  38. package/dist/live/osv-scanner.js +85 -78
  39. package/dist/live/package-archive.d.ts +2 -0
  40. package/dist/live/package-archive.js +7 -4
  41. package/dist/live/pypi-releases.d.ts +44 -0
  42. package/dist/live/pypi-releases.js +103 -0
  43. package/dist/live/range-satisfies.d.ts +21 -0
  44. package/dist/live/range-satisfies.js +190 -0
  45. package/dist/live/rate-limiter.d.ts +6 -0
  46. package/dist/live/rate-limiter.js +5 -0
  47. package/dist/live/registry-health.js +2 -0
  48. package/dist/live/release-metadata.d.ts +17 -8
  49. package/dist/live/release-metadata.js +6 -3
  50. package/dist/live/resolution.d.ts +11 -1
  51. package/dist/live/resolution.js +16 -6
  52. package/dist/live/semver-utils.d.ts +8 -0
  53. package/dist/live/semver-utils.js +10 -2
  54. package/dist/live/types.d.ts +17 -1
  55. package/dist/live/upgrade-go.d.ts +62 -0
  56. package/dist/live/upgrade-go.js +130 -0
  57. package/dist/live/upgrade-pypi.d.ts +42 -0
  58. package/dist/live/upgrade-pypi.js +85 -0
  59. package/dist/live/upgrade-sources.d.ts +30 -11
  60. package/dist/live/upgrade-sources.js +93 -21
  61. package/dist/live/zip-archive.d.ts +22 -0
  62. package/dist/live/zip-archive.js +98 -0
  63. package/dist/memory-store.d.ts +19 -0
  64. package/dist/memory-store.js +8 -0
  65. package/dist/native-bindings.d.ts +7 -2
  66. package/dist/native-bindings.js +4 -0
  67. package/dist/project-tree.d.ts +50 -8
  68. package/dist/project-tree.js +121 -29
  69. package/dist/schema-registry.js +5 -5
  70. package/dist/server-instructions.d.ts +6 -0
  71. package/dist/server-instructions.js +20 -15
  72. package/dist/sqlite-driver.d.ts +134 -0
  73. package/dist/sqlite-driver.js +359 -0
  74. package/dist/tool-dispatch.d.ts +8 -1
  75. package/dist/tool-dispatch.js +17 -2
  76. package/dist/tools/agent-memory.js +10 -2
  77. package/dist/tools/app-plan.d.ts +11 -1
  78. package/dist/tools/app-plan.js +32 -6
  79. package/dist/tools/briefing-task-scope.d.ts +21 -2
  80. package/dist/tools/briefing-task-scope.js +97 -4
  81. package/dist/tools/decision-memory.js +9 -3
  82. package/dist/tools/dependency-check-signals.d.ts +23 -5
  83. package/dist/tools/dependency-check-signals.js +62 -19
  84. package/dist/tools/dependency-check-versions.d.ts +94 -0
  85. package/dist/tools/dependency-check-versions.js +213 -0
  86. package/dist/tools/dependency-check.d.ts +27 -11
  87. package/dist/tools/dependency-check.js +321 -74
  88. package/dist/tools/dependency-health.d.ts +1 -1
  89. package/dist/tools/dependency-health.js +5 -0
  90. package/dist/tools/knowledge-gap-ranges.d.ts +1 -1
  91. package/dist/tools/knowledge-gap-ranges.js +1 -1
  92. package/dist/tools/knowledge-gap-scope.d.ts +2 -2
  93. package/dist/tools/project-scope.d.ts +45 -0
  94. package/dist/tools/project-scope.js +119 -0
  95. package/dist/tools/record-feedback.js +1 -1
  96. package/dist/tools/upgrade-impact-callsites.d.ts +1 -0
  97. package/dist/tools/upgrade-impact-callsites.js +23 -5
  98. package/dist/tools/upgrade-impact-changelog.d.ts +41 -0
  99. package/dist/tools/upgrade-impact-changelog.js +79 -0
  100. package/dist/tools/upgrade-impact-imports.d.ts +42 -0
  101. package/dist/tools/upgrade-impact-imports.js +260 -0
  102. package/dist/tools/upgrade-impact-literals.js +1 -1
  103. package/dist/tools/upgrade-impact-report.d.ts +23 -0
  104. package/dist/tools/upgrade-impact-report.js +45 -6
  105. package/dist/tools/upgrade-impact.d.ts +13 -4
  106. package/dist/tools/upgrade-impact.js +236 -85
  107. package/dist/tools/upgrade-planner-fixpaths.d.ts +40 -0
  108. package/dist/tools/upgrade-planner-fixpaths.js +0 -0
  109. package/dist/tools/upgrade-planner.d.ts +31 -3
  110. package/dist/tools/upgrade-planner.js +74 -19
  111. package/dist/tools/vulnerability-scan-concise.d.ts +13 -1
  112. package/dist/tools/vulnerability-scan-concise.js +15 -3
  113. package/dist/tools/vulnerability-scan-format.d.ts +58 -1
  114. package/dist/tools/vulnerability-scan-format.js +107 -11
  115. package/dist/tools/vulnerability-scan.d.ts +104 -13
  116. package/dist/tools/vulnerability-scan.js +27 -3
  117. package/dist/tools/what-should-i-know.d.ts +22 -2
  118. package/dist/tools/what-should-i-know.js +122 -12
  119. package/package.json +10 -32
@@ -0,0 +1,252 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Opt-in changelog fallback for `upgrade_impact`: GitHub releases, and a
4
+ * changelog FILE a PyPI project links from `project_urls` when that file is
5
+ * on GitHub (Decision 1, option B, approved 2026-10-11).
6
+ *
7
+ * Off by default. The default contract stays option A: only the package's own
8
+ * registry and OSV.dev are contacted. A user who wants release notes for the
9
+ * many packages whose archive ships no changelog (vite, zod, cobra, most Go
10
+ * modules) sets FOURDA_GITHUB_CHANGELOG=true; GitHub then learns the
11
+ * repository and the version range, nothing else. GITHUB_TOKEN, when set, is
12
+ * sent as a bearer token (5,000 requests an hour instead of 60).
13
+ *
14
+ * What is parsed is what the project itself published as release notes: the
15
+ * release body (Markdown), read by the same parser and classifier as an
16
+ * archive changelog, one section per release whose tag names a version of
17
+ * this package. A docs page (HTML) is never parsed. Rate limits are reported,
18
+ * not retried: the answer says when the limit resets and how to raise it.
19
+ */
20
+ import { CHANGELOG_PARSER_VERSION, isChangelogName, parseChangelog } from "./changelog.js";
21
+ import { ArchiveError, MAX_FILE_BYTES } from "./package-archive.js";
22
+ export const GITHUB_CHANGELOG_ENV = "FOURDA_GITHUB_CHANGELOG";
23
+ const CACHE_SOURCE = "upgrade-impact";
24
+ /** A range whose target release is published: release notes rarely change after. */
25
+ const FOUND_TTL = 24 * 3600;
26
+ const MISS_TTL = 3600;
27
+ const PER_PAGE = 100;
28
+ const MAX_PAGES = 5;
29
+ const GITHUB_TIMEOUT_MS = 15_000;
30
+ const USER_AGENT = "4da-mcp-server (https://4da.ai; upgrade_impact)";
31
+ /** True when the user opted in: FOURDA_GITHUB_CHANGELOG is 1 / true / yes / on. */
32
+ export function githubChangelogEnabled(env = process.env) {
33
+ return /^(?:1|true|yes|on)$/i.test(String(env[GITHUB_CHANGELOG_ENV] ?? "").trim());
34
+ }
35
+ /** `owner/repo` of a GitHub URL (https, git+https, git@, shorthand), or null. */
36
+ export function parseGithubRepo(url) {
37
+ if (!url)
38
+ return null;
39
+ const raw = url.trim();
40
+ const m = /^(?:github:)?([\w.-]+)\/([\w.-]+)$/.exec(raw) ??
41
+ /github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:[/#?].*)?$/i.exec(raw);
42
+ if (!m)
43
+ return null;
44
+ const repo = m[2].replace(/\.git$/, "");
45
+ if (!repo || m[1] === "." || repo === "." || repo === "..")
46
+ return null;
47
+ return { owner: m[1], repo };
48
+ }
49
+ export class GithubRateLimitError extends Error {
50
+ }
51
+ function headers() {
52
+ const h = {
53
+ Accept: "application/vnd.github+json",
54
+ "X-GitHub-Api-Version": "2022-11-28",
55
+ "User-Agent": USER_AGENT,
56
+ };
57
+ const token = process.env.GITHUB_TOKEN?.trim();
58
+ if (token)
59
+ h.Authorization = `Bearer ${token}`;
60
+ return h;
61
+ }
62
+ /** A 403/429 that is a rate limit, with when it lifts. */
63
+ function rateLimitMessage(res) {
64
+ if (res.status !== 403 && res.status !== 429)
65
+ return null;
66
+ const remaining = res.headers.get("x-ratelimit-remaining");
67
+ const retryAfter = res.headers.get("retry-after");
68
+ if (remaining !== "0" && !retryAfter && res.status !== 429)
69
+ return null;
70
+ const reset = Number(res.headers.get("x-ratelimit-reset"));
71
+ const when = Number.isFinite(reset) && reset > 0
72
+ ? `resets at ${new Date(reset * 1000).toISOString()}`
73
+ : retryAfter
74
+ ? `retry after ${retryAfter} s`
75
+ : "try again later";
76
+ const auth = process.env.GITHUB_TOKEN?.trim()
77
+ ? "GITHUB_TOKEN is set; its hourly limit is spent"
78
+ : "set GITHUB_TOKEN to raise the limit from 60 to 5,000 requests an hour";
79
+ return `GitHub API rate limit reached (${when}); ${auth}`;
80
+ }
81
+ const KNOWN_TAG_PREFIX = /^(?:v|version[-_]?|release[-_]?|rel[-_]?)?$/i;
82
+ /**
83
+ * The version a release tag names for this package, or null when the tag is
84
+ * another package's (monorepos) or not a version. `tagPrefix` is a Go
85
+ * module's subdirectory (`service/s3`): its tags are `service/s3/v1.2.3`.
86
+ */
87
+ export function versionFromTag(tag, pkg, tagPrefix) {
88
+ let rest = tag.trim();
89
+ if (tagPrefix) {
90
+ if (!rest.startsWith(`${tagPrefix}/`))
91
+ return null;
92
+ rest = rest.slice(tagPrefix.length + 1);
93
+ }
94
+ const m = /^(.*?)(\d+\.\d+(?:\.\d+)*(?:[-.+]?[0-9A-Za-z][0-9A-Za-z.+-]*)?)$/.exec(rest);
95
+ if (!m)
96
+ return null;
97
+ const prefix = m[1];
98
+ if (KNOWN_TAG_PREFIX.test(prefix))
99
+ return m[2];
100
+ // A monorepo tag: `pkg@1.2.3`, `pkg-v1.2.3`, `@scope/pkg@1.2.3`, `pkg/v1.2.3`.
101
+ const fold = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "");
102
+ const bare = prefix.replace(/(?:[@/_-]v?|v)$/i, "");
103
+ const name = pkg.split("/").pop() ?? pkg;
104
+ return fold(bare) === fold(pkg) || fold(bare) === fold(name) ? m[2] : null;
105
+ }
106
+ /** Release-body lines that are bookkeeping, not changes. */
107
+ const NOISE = /made their first contribution|^\W*full changelog\W|^\W*(?:new )?contributors?\W*$/i;
108
+ /** One section per release that names a version of this package, entries parsed from its notes. */
109
+ export function releasesToSections(releases, pkg, tagPrefix) {
110
+ const sections = [];
111
+ for (const r of releases) {
112
+ if (r.draft || typeof r.tag_name !== "string")
113
+ continue;
114
+ const version = versionFromTag(r.tag_name, pkg, tagPrefix);
115
+ if (!version)
116
+ continue;
117
+ const date = (r.published_at ?? r.created_at ?? "").slice(0, 10) || null;
118
+ // A synthetic heading makes the body one release; a body that repeats its own
119
+ // version heading opens the same version again, and those entries are kept too.
120
+ const parsed = parseChangelog(`# ${version}\n\n${(r.body ?? "").slice(0, MAX_FILE_BYTES)}`);
121
+ const entries = parsed
122
+ .filter((s, i) => i === 0 || s.version.replace(/^v/, "") === version.replace(/^v/, ""))
123
+ .flatMap((s) => s.entries)
124
+ .filter((e) => !NOISE.test(e.text) && !(e.under && /^\W*new contributors\W*$/i.test(e.under)));
125
+ sections.push({ version, date, entries });
126
+ }
127
+ return sections;
128
+ }
129
+ /**
130
+ * Release notes for `pkg` from `owner/repo`, newest first, paging until a
131
+ * release at or below `from` is seen (at most MAX_PAGES pages of 100).
132
+ */
133
+ async function fetchReleases(net, repo, stop, pkg, tagPrefix) {
134
+ const out = [];
135
+ for (let page = 1; page <= MAX_PAGES; page++) {
136
+ const url = `https://api.github.com/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/releases?per_page=${PER_PAGE}&page=${page}`;
137
+ const res = await net.fetch(url, { headers: headers() }, GITHUB_TIMEOUT_MS);
138
+ const limited = rateLimitMessage(res);
139
+ if (limited)
140
+ throw new GithubRateLimitError(limited);
141
+ if (res.status === 404)
142
+ throw new Error(`github.com/${repo.owner}/${repo.repo} has no public releases API (not found or private)`);
143
+ if (!res.ok)
144
+ throw new Error(`GitHub API HTTP ${res.status}`);
145
+ const batch = (await res.json());
146
+ if (!Array.isArray(batch))
147
+ throw new Error("GitHub API returned an unexpected releases payload");
148
+ out.push(...batch);
149
+ const reachedFrom = batch.some((r) => {
150
+ const v = typeof r.tag_name === "string" ? versionFromTag(r.tag_name, pkg, tagPrefix) : null;
151
+ return v !== null && stop(v);
152
+ });
153
+ if (batch.length < PER_PAGE || reachedFrom)
154
+ break;
155
+ }
156
+ return out;
157
+ }
158
+ /** `https://github.com/o/r/blob/<ref>/<path>` (or raw.githubusercontent.com) to a raw file URL, when it names a changelog file. */
159
+ export function rawChangelogUrl(link) {
160
+ let m = /^https:\/\/github\.com\/([\w.-]+)\/([\w.-]+)\/(?:blob|raw)\/([^?#]+)$/i.exec(link.trim());
161
+ let rawUrl = m ? `https://raw.githubusercontent.com/${m[1]}/${m[2]}/${m[3]}` : null;
162
+ if (!rawUrl) {
163
+ m = /^https:\/\/raw\.githubusercontent\.com\/[\w.-]+\/[\w.-]+\/[^?#]+$/i.exec(link.trim());
164
+ rawUrl = m ? link.trim() : null;
165
+ }
166
+ if (!rawUrl)
167
+ return null;
168
+ const base = rawUrl.split("/").pop() ?? "";
169
+ return isChangelogName(base) ? rawUrl : null;
170
+ }
171
+ async function fetchChangelogFile(net, url) {
172
+ const res = await net.fetch(url, { headers: { "User-Agent": USER_AGENT } }, GITHUB_TIMEOUT_MS);
173
+ const limited = rateLimitMessage(res);
174
+ if (limited)
175
+ throw new GithubRateLimitError(limited);
176
+ if (!res.ok)
177
+ throw new Error(`HTTP ${res.status} for ${url}`);
178
+ const declared = Number(res.headers.get("content-length"));
179
+ if (Number.isFinite(declared) && declared > MAX_FILE_BYTES)
180
+ throw new ArchiveError(`${url} is over ${MAX_FILE_BYTES} bytes`);
181
+ const text = await res.text();
182
+ if (text.length > MAX_FILE_BYTES)
183
+ throw new ArchiveError(`${url} is over ${MAX_FILE_BYTES} bytes`);
184
+ return parseChangelog(text);
185
+ }
186
+ function inRangeCount(sections, from, to, order) {
187
+ return sections.filter((s) => (order(s.version, from) ?? 0) > 0 && (order(s.version, to) ?? 1) <= 0).length;
188
+ }
189
+ export function githubCacheKey(kind, where, input) {
190
+ return `upgrade-impact:github:p${CHANGELOG_PARSER_VERSION}:${kind}:${where}:${input.tagPrefix ?? ""}:${input.ecosystem}:${input.pkg}:${input.from}:${input.to}`;
191
+ }
192
+ /**
193
+ * The fallback: a GitHub changelog file the project links (PyPI), then the
194
+ * repository's releases. Caller checks the opt-in. Returns a not-found result
195
+ * with the reason (no GitHub repository, rate limit, no matching releases).
196
+ */
197
+ export async function getGithubChangelog(net, input) {
198
+ const reasons = [];
199
+ for (const link of input.changelogLinks ?? []) {
200
+ const raw = rawChangelogUrl(link);
201
+ if (!raw)
202
+ continue;
203
+ const key = githubCacheKey("file", raw, input);
204
+ const hit = net.cache?.get(key);
205
+ if (hit)
206
+ return hit;
207
+ try {
208
+ const sections = await fetchChangelogFile(net, raw);
209
+ if (inRangeCount(sections, input.from, input.to, input.order) > 0) {
210
+ const result = { found: true, file: raw.split("/").pop(), sections, source: "github-file", url: link };
211
+ net.cache?.set(key, result, CACHE_SOURCE, FOUND_TTL);
212
+ return result;
213
+ }
214
+ reasons.push(`the changelog the project links (${link}) has no section for these releases`);
215
+ }
216
+ catch (err) {
217
+ if (err instanceof GithubRateLimitError)
218
+ return { found: false, reason: err.message };
219
+ reasons.push(`the changelog the project links could not be read: ${err.message}`);
220
+ }
221
+ }
222
+ const repo = parseGithubRepo(input.repository);
223
+ if (!repo) {
224
+ reasons.push("the package names no GitHub repository, so there are no GitHub releases to read");
225
+ return { found: false, reason: reasons.join("; ") };
226
+ }
227
+ const where = `${repo.owner}/${repo.repo}`;
228
+ const key = githubCacheKey("releases", where, input);
229
+ const hit = net.cache?.get(key);
230
+ if (hit)
231
+ return hit;
232
+ const releasesUrl = `https://github.com/${where}/releases`;
233
+ try {
234
+ const releases = await fetchReleases(net, repo, (v) => (input.order(v, input.from) ?? 1) <= 0, input.pkg, input.tagPrefix ?? null);
235
+ const sections = releasesToSections(releases, input.pkg, input.tagPrefix ?? null);
236
+ const hits = inRangeCount(sections, input.from, input.to, input.order);
237
+ const result = hits > 0
238
+ ? { found: true, file: "GitHub releases", sections, source: "github-releases", url: releasesUrl }
239
+ : {
240
+ found: false,
241
+ url: releasesUrl,
242
+ reason: [...reasons, `${where} has no GitHub release for ${input.pkg} between ${input.from} and ${input.to}`].join("; "),
243
+ };
244
+ const targetFound = sections.some((s) => input.order(s.version, input.to) === 0);
245
+ net.cache?.set(key, result, CACHE_SOURCE, targetFound ? FOUND_TTL : MISS_TTL);
246
+ return result;
247
+ }
248
+ catch (err) {
249
+ // A rate limit or network failure is not remembered: the next call may succeed.
250
+ return { found: false, url: releasesUrl, reason: [...reasons, `GitHub releases could not be read: ${err.message}`].join("; ") };
251
+ }
252
+ }
@@ -16,3 +16,8 @@ export declare class GoRegistry {
16
16
  constructor(cache: LiveCache, rateLimiter: RateLimiter);
17
17
  getPackageInfo(name: string, currentVersion: string | null, isDev: boolean): Promise<RegistryPackageInfo>;
18
18
  }
19
+ /**
20
+ * Escape uppercase letters in Go module paths for the proxy URL.
21
+ * Go proxy convention: uppercase 'X' becomes '!x'.
22
+ */
23
+ export declare function escapeModulePath(mod: string): string;
@@ -93,7 +93,7 @@ export class GoRegistry {
93
93
  * Escape uppercase letters in Go module paths for the proxy URL.
94
94
  * Go proxy convention: uppercase 'X' becomes '!x'.
95
95
  */
96
- function escapeModulePath(mod) {
96
+ export function escapeModulePath(mod) {
97
97
  return mod.replace(/[A-Z]/g, (c) => "!" + c.toLowerCase());
98
98
  }
99
99
  /**
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Go module release index for dependency_check, from the module proxy
3
+ * (proxy.golang.org, the host `go get` downloads from):
4
+ *
5
+ * - `/@v/list`: the tagged versions, `+incompatible` ones included (a module
6
+ * that predates modules and tags v2+ without a `/v2` path).
7
+ * - `/@v/<version>.info`: one release's time. The list carries no dates, so
8
+ * this is read for the versions being judged only (cached; a published
9
+ * version never changes). A pseudo-version (`v0.0.0-20240102150405-abcdef123456`)
10
+ * is not in the list; its `.info` both confirms it exists and dates it.
11
+ * - The latest version's `go.mod`: its `retract` directives withdraw versions
12
+ * (`go get` skips them), reported like a yank.
13
+ * - An untagged module: `@latest` gives its newest pseudo-version.
14
+ *
15
+ * Major versions 2+ are different module paths (`.../v5`); the proxy lists
16
+ * each path's own versions only.
17
+ *
18
+ * Not applicable to Go and reported so by the signals: publisher identity
19
+ * (integrity is checked against sum.golang.org instead), install hooks (Go
20
+ * runs no package code at download), per-release dependency lists.
21
+ */
22
+ import type { LiveCache } from "./cache.js";
23
+ import type { RateLimiter } from "./rate-limiter.js";
24
+ import type { PackageReleaseIndex, RegistryLookup, ReleaseMetadataSource } from "./release-metadata.js";
25
+ export declare const GO_NOT_APPLICABLE = "not applicable to Go modules: the proxy carries no publisher identity (downloads are checked against sum.golang.org)";
26
+ /** A Go version: release, prerelease, pseudo-version, optional `+incompatible`. */
27
+ export declare const GO_VERSION: RegExp;
28
+ /** `v`-prefixed spelling of a Go version ("1.2.3" -> "v1.2.3"). */
29
+ export declare function goVersion(raw: string): string;
30
+ /** The commit time a pseudo-version embeds, as ISO, or null when `version` is not one. */
31
+ export declare function pseudoVersionTime(version: string): string | null;
32
+ export interface Retraction {
33
+ low: string;
34
+ high: string;
35
+ rationale: string | null;
36
+ }
37
+ /** `retract` directives of a go.mod: single versions, `[low, high]` intervals, and blocks of either. */
38
+ export declare function parseRetractions(goMod: string): Retraction[];
39
+ export declare class GoReleasesReader implements ReleaseMetadataSource {
40
+ private cache;
41
+ private rateLimiter;
42
+ constructor(cache: LiveCache, rateLimiter: RateLimiter);
43
+ getReleases(module: string, wanted: string[]): Promise<RegistryLookup<PackageReleaseIndex>>;
44
+ getCreatedAt(): Promise<RegistryLookup<string | null>>;
45
+ private list;
46
+ private info;
47
+ private get;
48
+ }
@@ -0,0 +1,196 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Go module release index for dependency_check, from the module proxy
4
+ * (proxy.golang.org, the host `go get` downloads from):
5
+ *
6
+ * - `/@v/list`: the tagged versions, `+incompatible` ones included (a module
7
+ * that predates modules and tags v2+ without a `/v2` path).
8
+ * - `/@v/<version>.info`: one release's time. The list carries no dates, so
9
+ * this is read for the versions being judged only (cached; a published
10
+ * version never changes). A pseudo-version (`v0.0.0-20240102150405-abcdef123456`)
11
+ * is not in the list; its `.info` both confirms it exists and dates it.
12
+ * - The latest version's `go.mod`: its `retract` directives withdraw versions
13
+ * (`go get` skips them), reported like a yank.
14
+ * - An untagged module: `@latest` gives its newest pseudo-version.
15
+ *
16
+ * Major versions 2+ are different module paths (`.../v5`); the proxy lists
17
+ * each path's own versions only.
18
+ *
19
+ * Not applicable to Go and reported so by the signals: publisher identity
20
+ * (integrity is checked against sum.golang.org instead), install hooks (Go
21
+ * runs no package code at download), per-release dependency lists.
22
+ */
23
+ import { fetchWithTimeout } from "./http-utils.js";
24
+ import { compareVersions } from "./version-compare.js";
25
+ import { escapeGoPath } from "./upgrade-go.js";
26
+ const GO_PROXY = "https://proxy.golang.org";
27
+ const TIMEOUT_MS = 15_000;
28
+ const LIST_TTL = 3600;
29
+ const INFO_TTL = 30 * 86_400;
30
+ /** `.info` reads per getReleases call. */
31
+ const MAX_INFO_READS = 6;
32
+ export const GO_NOT_APPLICABLE = "not applicable to Go modules: the proxy carries no publisher identity (downloads are checked against sum.golang.org)";
33
+ /** A Go version: release, prerelease, pseudo-version, optional `+incompatible`. */
34
+ export const GO_VERSION = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+incompatible)?$/;
35
+ const PSEUDO = /(?:^v\d+\.\d+\.\d+-|[.-])(?:0\.)?(\d{14})-[0-9a-f]{12}(?:\+incompatible)?$/;
36
+ /** `v`-prefixed spelling of a Go version ("1.2.3" -> "v1.2.3"). */
37
+ export function goVersion(raw) {
38
+ const v = raw.trim();
39
+ return /^\d/.test(v) ? `v${v}` : v;
40
+ }
41
+ /** The commit time a pseudo-version embeds, as ISO, or null when `version` is not one. */
42
+ export function pseudoVersionTime(version) {
43
+ const m = PSEUDO.exec(version);
44
+ if (!m)
45
+ return null;
46
+ const t = m[1];
47
+ return `${t.slice(0, 4)}-${t.slice(4, 6)}-${t.slice(6, 8)}T${t.slice(8, 10)}:${t.slice(10, 12)}:${t.slice(12, 14)}Z`;
48
+ }
49
+ /** `retract` directives of a go.mod: single versions, `[low, high]` intervals, and blocks of either. */
50
+ export function parseRetractions(goMod) {
51
+ const out = [];
52
+ const entry = (text) => {
53
+ const [body, ...comment] = text.split("//");
54
+ const rationale = comment.join("//").trim() || null;
55
+ const interval = /^\s*\[\s*(v\S+?)\s*,\s*(v\S+?)\s*\]\s*$/.exec(body);
56
+ if (interval)
57
+ out.push({ low: interval[1], high: interval[2], rationale });
58
+ else {
59
+ const single = /^\s*(v\S+)\s*$/.exec(body);
60
+ if (single)
61
+ out.push({ low: single[1], high: single[1], rationale });
62
+ }
63
+ };
64
+ const lines = goMod.split("\n");
65
+ for (let i = 0; i < lines.length; i++) {
66
+ const m = /^\s*retract\s+(.*)$/.exec(lines[i]);
67
+ if (!m)
68
+ continue;
69
+ if (m[1].trim().startsWith("(")) {
70
+ for (i++; i < lines.length && !/^\s*\)/.test(lines[i]); i++)
71
+ entry(lines[i]);
72
+ }
73
+ else {
74
+ entry(m[1]);
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+ export class GoReleasesReader {
80
+ cache;
81
+ rateLimiter;
82
+ constructor(cache, rateLimiter) {
83
+ this.cache = cache;
84
+ this.rateLimiter = rateLimiter;
85
+ }
86
+ async getReleases(module, wanted) {
87
+ const list = await this.list(module);
88
+ if (list.status !== "ok")
89
+ return list;
90
+ const { versions, retractions } = list.data;
91
+ const releases = {};
92
+ for (const version of versions)
93
+ releases[version] = record(version, null, retractions);
94
+ // Dates (and existence, for a pseudo-version) of the versions being judged.
95
+ const want = [...new Set(wanted.map(goVersion))].filter((v) => GO_VERSION.test(v)).slice(0, MAX_INFO_READS);
96
+ await Promise.all(want.map(async (version) => {
97
+ const info = await this.info(module, version);
98
+ // The proxy answers `v20.10.0` with the `v20.10.0+incompatible` release: file it under its own name.
99
+ if (info === null || (info.Version && info.Version !== version))
100
+ return;
101
+ const time = info.Time ?? pseudoVersionTime(version);
102
+ releases[version] = { ...(releases[version] ?? record(version, null, retractions)), publishedAt: time ?? null };
103
+ }));
104
+ return { status: "ok", data: { ecosystem: "Go", name: module, createdAt: null, releases } };
105
+ }
106
+ async getCreatedAt() {
107
+ return { status: "ok", data: null };
108
+ }
109
+ async list(module) {
110
+ const key = `go-releases:${module}`;
111
+ const hit = this.cache.get(key);
112
+ if (hit)
113
+ return { status: "ok", data: hit };
114
+ const base = `${GO_PROXY}/${escapeGoPath(module)}`;
115
+ let versions;
116
+ try {
117
+ const res = await this.get(`${base}/@v/list`);
118
+ // The proxy answers 404 / 410 for a path it cannot resolve.
119
+ if (res.status === 404 || res.status === 410) {
120
+ return { status: "not_found", detail: `"${module}" is not a module the Go proxy can resolve` };
121
+ }
122
+ if (!res.ok)
123
+ return { status: "unreachable", detail: `Go module proxy answered HTTP ${res.status}` };
124
+ versions = (await res.text()).split("\n").map((v) => v.trim()).filter((v) => GO_VERSION.test(v));
125
+ if (versions.length === 0) {
126
+ // Untagged: the newest pseudo-version.
127
+ const latest = await this.get(`${base}/@latest`);
128
+ if (latest.ok) {
129
+ const info = (await latest.json());
130
+ if (info.Version && GO_VERSION.test(info.Version)) {
131
+ versions = [info.Version];
132
+ if (info.Time)
133
+ this.cache.set(`go-info:${module}@${info.Version}`, info, "go", INFO_TTL);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ catch (err) {
139
+ return { status: "unreachable", detail: `Go module proxy: ${err instanceof Error ? err.message : String(err)}` };
140
+ }
141
+ // Retractions live in the newest version's go.mod.
142
+ let retractions = [];
143
+ let retractionsNote = null;
144
+ const newest = versions.reduce((max, v) => (max === null || compareVersions(v, max, "Go") > 0 ? v : max), null);
145
+ if (newest) {
146
+ try {
147
+ const res = await this.get(`${base}/@v/${escapeGoPath(newest)}.mod`);
148
+ if (res.ok)
149
+ retractions = parseRetractions(await res.text());
150
+ else
151
+ retractionsNote = `go.mod of ${newest}: HTTP ${res.status}`;
152
+ }
153
+ catch (err) {
154
+ retractionsNote = `go.mod of ${newest}: ${err instanceof Error ? err.message : String(err)}`;
155
+ }
156
+ }
157
+ const data = { versions, retractions, retractionsNote };
158
+ this.cache.set(key, data, "go", LIST_TTL);
159
+ return { status: "ok", data };
160
+ }
161
+ async info(module, version) {
162
+ const key = `go-info:${module}@${version}`;
163
+ const hit = this.cache.get(key);
164
+ if (hit)
165
+ return hit;
166
+ try {
167
+ const res = await this.get(`${GO_PROXY}/${escapeGoPath(module)}/@v/${escapeGoPath(version)}.info`);
168
+ if (!res.ok)
169
+ return null;
170
+ const info = (await res.json());
171
+ this.cache.set(key, info, "go", INFO_TTL);
172
+ return info;
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ }
178
+ async get(url) {
179
+ if (!this.rateLimiter.consume("go"))
180
+ throw new Error("Go module proxy rate limit reached for this minute");
181
+ return fetchWithTimeout(url, {}, TIMEOUT_MS);
182
+ }
183
+ }
184
+ function record(version, publishedAt, retractions) {
185
+ const hit = retractions.find((r) => compareVersions(version, r.low, "Go") >= 0 && compareVersions(version, r.high, "Go") <= 0);
186
+ return {
187
+ version,
188
+ publishedAt,
189
+ trust: 0,
190
+ trustEvidence: GO_NOT_APPLICABLE,
191
+ installScripts: null,
192
+ deprecated: hit?.rationale ? `retracted: ${hit.rationale}` : null,
193
+ withdrawn: hit ? "retracted" : null,
194
+ dependencies: null,
195
+ };
196
+ }
@@ -7,9 +7,11 @@
7
7
  * Privacy: Only sends package names/versions (public) and tech keywords (generic).
8
8
  * Set FOURDA_OFFLINE=true to disable all network calls.
9
9
  */
10
- import type Database from "better-sqlite3";
11
- import type { ReleaseMetadataSource } from "./release-metadata.js";
10
+ import type { SqliteDatabase } from "../sqlite-driver.js";
11
+ import { FixPathSources } from "./fix-paths.js";
12
+ import type { CheckEcosystem, ReleaseMetadataSource } from "./release-metadata.js";
12
13
  import { type ResolutionGroup } from "./resolution.js";
14
+ import type { TreeDiscovery } from "../project-tree.js";
13
15
  import type { InstallDriftRecord, ResolutionProvenance, ResolvedDependency, RegistryPackageInfo, VulnerabilityScanResult, LiveHeadline, LiveIntelligenceStatus } from "./types.js";
14
16
  export type { VulnerabilityScanResult, VulnerabilityEntry, LiveHeadline, LiveIntelligenceStatus, RegistryPackageInfo, DependencyHealthResult, InstallDriftRecord, ResolutionProvenance, } from "./types.js";
15
17
  export declare class LiveIntelligence {
@@ -24,6 +26,9 @@ export declare class LiveIntelligence {
24
26
  private goRegistry;
25
27
  private npmPackuments;
26
28
  private cratesVersions;
29
+ private pypiReleases;
30
+ private goReleases;
31
+ private fixPathSources;
27
32
  private enabled;
28
33
  private lastVulnScan;
29
34
  /** Whether the stored scan included direct devDependencies. */
@@ -37,6 +42,8 @@ export declare class LiveIntelligence {
37
42
  * runs, 2026-10-03 pre-publish verification).
38
43
  */
39
44
  private scanRequests;
45
+ /** Scans running now, by generation, scope and path: a request for one of them awaits it. */
46
+ private scansInFlight;
40
47
  private storedScanRequest;
41
48
  private lastHeadlines;
42
49
  private resolvedDeps;
@@ -45,6 +52,8 @@ export declare class LiveIntelligence {
45
52
  private projectRoot;
46
53
  /** One entry per resolution group: its inputs, result, and the file signatures that say when it went stale. */
47
54
  private groupResults;
55
+ /** The project-tree walk's report of what it left out, when the groups came from one. */
56
+ private discovery;
48
57
  /** When the dependency set in use was assembled: at init, or at the last re-resolution. */
49
58
  private resolvedAt;
50
59
  /**
@@ -61,7 +70,15 @@ export declare class LiveIntelligence {
61
70
  private warmup;
62
71
  /** When the last warmup came back offline; gates the retry so an offline host is not re-probed on every call. */
63
72
  private warmupFailedAt;
64
- constructor(db: Database.Database);
73
+ constructor(db: SqliteDatabase | null);
74
+ /** Advisory, registry and parent-requirement lookups for fix paths; null when offline. */
75
+ getFixPathSources(): FixPathSources | null;
76
+ /**
77
+ * The lockfile one manifest directory resolved this ecosystem from, or null
78
+ * when it resolved from a manifest or nothing. `dir` is spelled as the
79
+ * resolution group spells it (the scan's sourceDirs).
80
+ */
81
+ lockfileFor(dir: string, ecosystem: string): string | null;
65
82
  /**
66
83
  * Initialize with project data. Call once after project scan.
67
84
  * @deprecated Use initFromMultiEcosystem for correct per-ecosystem resolution.
@@ -81,7 +98,18 @@ export declare class LiveIntelligence {
81
98
  * project-tree.ts): each group resolves from its own directory and keeps
82
99
  * its manifest's platform targets; `root` is the reported project root.
83
100
  */
84
- initFromProjectTree(root: string, groups: ResolutionGroup[]): void;
101
+ initFromProjectTree(root: string, groups: ResolutionGroup[], discovery?: TreeDiscovery | null): void;
102
+ /**
103
+ * Manifests with no lockfile beside them, whose ranged dependencies were not
104
+ * scanned: their versions are declared ranges, not installs.
105
+ */
106
+ getUnresolvedManifests(): Array<{
107
+ dir: string;
108
+ manifest: string;
109
+ ranges: number;
110
+ }>;
111
+ /** What the project-tree walk left out (lockfiles past its bounds); null when the set did not come from a walk. */
112
+ getDiscovery(): TreeDiscovery | null;
85
113
  /**
86
114
  * Initialize from dependency groups that each carry their own resolution
87
115
  * directory. Used in 4DA database mode, where dependencies span multiple
@@ -133,6 +161,7 @@ export declare class LiveIntelligence {
133
161
  includeDev?: boolean;
134
162
  forceRefresh?: boolean;
135
163
  }): Promise<VulnerabilityScanResult>;
164
+ private runScan;
136
165
  /**
137
166
  * Start the vulnerability scan in the background without blocking startup.
138
167
  * Never rejects: a failed scan resolves to an empty OFFLINE result so the
@@ -183,10 +212,7 @@ export declare class LiveIntelligence {
183
212
  isInitialized(): boolean;
184
213
  fetchRegistryHealth(deps: ResolvedDependency[]): Promise<RegistryPackageInfo[]>;
185
214
  /** Per-release registry readers for dependency_check (name-only requests). */
186
- getReleaseSources(): {
187
- npm: ReleaseMetadataSource;
188
- "crates.io": ReleaseMetadataSource;
189
- };
215
+ getReleaseSources(): Record<CheckEcosystem, ReleaseMetadataSource>;
190
216
  /**
191
217
  * OSV advisories for explicit (ecosystem, name, version) triples that need
192
218
  * not be in the project, e.g. a version an agent is about to install. Same