@4da/mcp-server 6.1.0 → 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 (50) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/README.md +14 -9
  3. package/dist/live/changelog-classify.d.ts +9 -3
  4. package/dist/live/changelog-classify.js +122 -12
  5. package/dist/live/changelog.d.ts +8 -4
  6. package/dist/live/changelog.js +67 -29
  7. package/dist/live/fix-paths.js +1 -1
  8. package/dist/live/github-releases.d.ts +69 -0
  9. package/dist/live/github-releases.js +252 -0
  10. package/dist/live/go-releases.d.ts +48 -0
  11. package/dist/live/go-releases.js +196 -0
  12. package/dist/live/index.d.ts +4 -5
  13. package/dist/live/index.js +7 -1
  14. package/dist/live/osv-ranges.d.ts +6 -0
  15. package/dist/live/osv-ranges.js +3 -0
  16. package/dist/live/package-archive.d.ts +2 -0
  17. package/dist/live/package-archive.js +7 -4
  18. package/dist/live/pypi-releases.d.ts +44 -0
  19. package/dist/live/pypi-releases.js +103 -0
  20. package/dist/live/release-metadata.d.ts +17 -8
  21. package/dist/live/release-metadata.js +6 -3
  22. package/dist/live/upgrade-go.d.ts +62 -0
  23. package/dist/live/upgrade-go.js +130 -0
  24. package/dist/live/upgrade-pypi.d.ts +42 -0
  25. package/dist/live/upgrade-pypi.js +85 -0
  26. package/dist/live/upgrade-sources.d.ts +28 -11
  27. package/dist/live/upgrade-sources.js +86 -19
  28. package/dist/live/zip-archive.d.ts +22 -0
  29. package/dist/live/zip-archive.js +98 -0
  30. package/dist/schema-registry.js +3 -3
  31. package/dist/server-instructions.d.ts +6 -0
  32. package/dist/server-instructions.js +20 -15
  33. package/dist/tools/dependency-check-signals.d.ts +16 -5
  34. package/dist/tools/dependency-check-signals.js +46 -18
  35. package/dist/tools/dependency-check-versions.d.ts +94 -0
  36. package/dist/tools/dependency-check-versions.js +213 -0
  37. package/dist/tools/dependency-check.d.ts +26 -10
  38. package/dist/tools/dependency-check.js +321 -74
  39. package/dist/tools/upgrade-impact-callsites.d.ts +1 -0
  40. package/dist/tools/upgrade-impact-callsites.js +23 -5
  41. package/dist/tools/upgrade-impact-changelog.d.ts +41 -0
  42. package/dist/tools/upgrade-impact-changelog.js +79 -0
  43. package/dist/tools/upgrade-impact-imports.d.ts +42 -0
  44. package/dist/tools/upgrade-impact-imports.js +260 -0
  45. package/dist/tools/upgrade-impact-report.d.ts +13 -0
  46. package/dist/tools/upgrade-impact-report.js +13 -4
  47. package/dist/tools/upgrade-impact.d.ts +13 -4
  48. package/dist/tools/upgrade-impact.js +235 -84
  49. package/dist/tools/what-should-i-know.js +1 -1
  50. package/package.json +6 -6
@@ -8,6 +8,8 @@
8
8
  * conventional `## [7.0.0](https://…/compare/…) (2025-06-24)`
9
9
  * plain / prefixed `## 1.2.3`, `# v1.2.3 (2024-01-01)`, `### Version 1.2.3`, `## fastembed 5.0.0`
10
10
  * setext `1.2.3` underlined with `===` / `---`
11
+ * reStructuredText `Version 3.0.0` underlined (or over- and underlined)
12
+ * with any of `= - ~ ^ * + # " '`, then `Released 2023-09-30`
11
13
  * History.md (express) `1.2.3 / 2020-01-01`
12
14
  * A heading only opens a section when the rest of it looks like a trailer
13
15
  * (date, link, "yanked") — `## Upgrading from 1.x` or `#### Rust 1.70 support`
@@ -25,7 +27,7 @@ import { classifyEntry, classifyHeading, sanitizeEntry } from "./changelog-class
25
27
  * for a week unless this changes: bump it with every change to parseChangelog
26
28
  * or changelog-classify.ts that can alter a section or an entry's kind.
27
29
  */
28
- export const CHANGELOG_PARSER_VERSION = 2;
30
+ export const CHANGELOG_PARSER_VERSION = 4;
29
31
  const NAME_STEMS = ["changelog", "changes", "history", "releases", "release-notes", "release_notes", "releasenotes", "news"];
30
32
  const NAME_EXTS = ["", ".md", ".markdown", ".txt", ".rst"];
31
33
  /** True for a root-level basename that names a changelog (case-insensitive). */
@@ -106,7 +108,13 @@ export function parseVersionHeading(raw) {
106
108
  return { version, date: extractDate(rest) };
107
109
  }
108
110
  const ATX = /^(#{1,6})\s+(.*?)\s*#*\s*$/;
109
- const SETEXT_RULE = /^\s*(=+|-+)\s*$/;
111
+ /**
112
+ * A setext / reStructuredText underline. `=` and `-` at any length (Markdown);
113
+ * the other RST adornments need three, so a stray `**` is not a heading rule.
114
+ */
115
+ const SETEXT_RULE = /^\s*(=+|-+|~{3,}|\^{3,}|\*{3,}|\+{3,}|#{3,}|"{3,}|'{3,})\s*$/;
116
+ /** `Released 2023-09-30`, `Released on 2024-01-01`, `Release date: ...`, `Unreleased`: a date line under an RST heading. */
117
+ const RELEASED_LINE = /^\s*(?:\*{0,2}(?:released|release date|date)\*{0,2}(?:\s+on)?\s*:?\s*(.*)|unreleased|\(unreleased\))\s*$/i;
110
118
  const BULLET = /^(\s*)(?:[-*+•]|\d+[.)])\s+(.*)$/;
111
119
  const LINK_DEF = /^\s*\[[^\]]+\]:\s*\S+/;
112
120
  const FENCE = /^\s*(```|~~~)/;
@@ -238,6 +246,37 @@ function addLine(state, line) {
238
246
  state.parentEntry = null;
239
247
  state.pending = { text: line, context: state.headingKind, indent: -1, under: state.headingText };
240
248
  }
249
+ /** A heading that is not a release: a category inside the current release, or the end of it. */
250
+ function subHeading(state, raw, level, setext = false) {
251
+ flush(state);
252
+ // A setext title ends the release only from a strictly higher level: "text" over
253
+ // "---" is also how a paragraph followed by a rule reads.
254
+ if (setext && level >= state.sectionLevel)
255
+ level = state.sectionLevel + 1;
256
+ // "## Migration guide" after the last release must not be read as part of it.
257
+ // A change-category heading ("## Changed", "## Breaking Changes") at the
258
+ // release's own level is still inside it: date-fns 3.0.0 writes
259
+ // "## v3.0.0" then "## Changed", and its ten BREAKING entries were dropped.
260
+ if (state.current && level <= state.sectionLevel && !isCategoryHeading(raw))
261
+ state.current = null;
262
+ state.headingKind = classifyHeading(raw);
263
+ state.headingText = contextText(raw);
264
+ state.parentKind = null;
265
+ state.parentIndent = null;
266
+ state.parentText = null;
267
+ state.parentEntry = null;
268
+ }
269
+ /**
270
+ * Setext / RST title level by adornment. Markdown fixes `=` at 1 and `-` at 2;
271
+ * reStructuredText ranks adornments in the order a document first uses them,
272
+ * so the rest rank below both, in a fixed order: close enough to compare a
273
+ * sub-heading with the release title above it.
274
+ */
275
+ function adornmentLevel(ch) {
276
+ const order = "=-~^*+#\"'";
277
+ const i = order.indexOf(ch);
278
+ return i === -1 ? 3 : i + 1;
279
+ }
241
280
  /** Parse a changelog into version sections, in document order (usually newest first). */
242
281
  export function parseChangelog(text) {
243
282
  const lines = text.replace(/\r\n?/g, "\n").split("\n");
@@ -285,31 +324,30 @@ export function parseChangelog(text) {
285
324
  const level = atx[1].length;
286
325
  if (heading)
287
326
  openSection(state, heading, level);
288
- else {
289
- flush(state);
290
- // "## Migration guide" after the last release must not be read as part of it.
291
- // A change-category heading ("## Changed", "## Breaking Changes") at the
292
- // release's own level is still inside it: date-fns 3.0.0 writes
293
- // "## v3.0.0" then "## Changed", and its ten BREAKING entries were dropped.
294
- if (state.current && level <= state.sectionLevel && !isCategoryHeading(atx[2]))
295
- state.current = null;
296
- state.headingKind = classifyHeading(atx[2]);
297
- state.headingText = contextText(atx[2]);
298
- state.parentKind = null;
299
- state.parentIndent = null;
300
- state.parentText = null;
301
- state.parentEntry = null;
302
- }
327
+ else
328
+ subHeading(state, atx[2], level);
303
329
  continue;
304
330
  }
305
331
  const next = lines[i + 1];
306
- if (next !== undefined && SETEXT_RULE.test(next) && line.trim() !== "" && !BULLET.test(line)) {
332
+ if (next !== undefined && SETEXT_RULE.test(next) && line.trim() !== "" && !BULLET.test(line) && !SETEXT_RULE.test(line)) {
333
+ const level = adornmentLevel(next.trim()[0]);
307
334
  const heading = parseVersionHeading(line);
335
+ i++;
308
336
  if (heading) {
309
- openSection(state, heading, next.trim().startsWith("=") ? 1 : 2);
310
- i++;
337
+ openSection(state, heading, level);
311
338
  continue;
312
339
  }
340
+ // A non-release setext / RST title is a sub-heading ("Breaking Changes" over
341
+ // `~~~~`), as an ATX one is: it was an entry until parser version 3.
342
+ subHeading(state, line, level, true);
343
+ continue;
344
+ }
345
+ // `Released 2023-09-30` right under an RST release title is its date, not a change (flask, werkzeug, click).
346
+ const released = state.current && state.current.entries.length === 0 && !state.pending ? RELEASED_LINE.exec(line) : null;
347
+ if (released && (released[1] === undefined || extractDate(released[1]) || /^(?:unreleased|tbd|-)?$/i.test(released[1].trim()))) {
348
+ if (released[1])
349
+ state.current.date ??= extractDate(released[1]);
350
+ continue;
313
351
  }
314
352
  // History.md style without an underline: `1.2.3 / 2020-01-01` alone on a line.
315
353
  const history = new RegExp(String.raw `^v?(${VERSION})\s+/\s+(.+)$`).exec(line.trim());
@@ -352,10 +390,10 @@ export function parseChangelog(text) {
352
390
  flush(state);
353
391
  return state.sections;
354
392
  }
355
- /** True when `version` lies in (from, to] by semver precedence; unreadable versions are out. */
356
- export function inRange(version, from, to) {
357
- const lower = compareVersionPrecedence(version, from);
358
- const upper = compareVersionPrecedence(version, to);
393
+ /** True when `version` lies in (from, to] by `order`; unreadable versions are out. */
394
+ export function inRange(version, from, to, order = compareVersionPrecedence) {
395
+ const lower = order(version, from);
396
+ const upper = order(version, to);
359
397
  return lower !== null && upper !== null && lower > 0 && upper <= 0;
360
398
  }
361
399
  /**
@@ -365,10 +403,10 @@ export function inRange(version, from, to) {
365
403
  * stopped being maintained two majors ago is reported as not covering the
366
404
  * range instead of silently presenting a partial history as the whole one.
367
405
  */
368
- export function selectRange(sections, from, to, firstInRange) {
369
- const readable = sections.filter((s) => compareVersionPrecedence(s.version, s.version) !== null);
370
- const selected = readable.filter((s) => inRange(s.version, from, to));
371
- const reachesTop = readable.some((s) => (compareVersionPrecedence(s.version, to) ?? -1) >= 0);
372
- const reachesBottom = readable.some((s) => (compareVersionPrecedence(s.version, firstInRange) ?? 1) <= 0);
406
+ export function selectRange(sections, from, to, firstInRange, order = compareVersionPrecedence) {
407
+ const readable = sections.filter((s) => order(s.version, s.version) !== null);
408
+ const selected = readable.filter((s) => inRange(s.version, from, to, order));
409
+ const reachesTop = readable.some((s) => (order(s.version, to) ?? -1) >= 0);
410
+ const reachesBottom = readable.some((s) => (order(s.version, firstInRange) ?? 1) <= 0);
373
411
  return { sections: selected, coversRange: selected.length > 0 && reachesTop && reachesBottom };
374
412
  }
@@ -39,7 +39,7 @@ const VERSIONS_TTL = 6 * 3600;
39
39
  const VERSION_DEPS_TTL = 30 * 86_400;
40
40
  const MAX_OSV_PAGES = 10;
41
41
  /** Bump when the shape of anything this module caches changes. */
42
- const CACHE_VERSION = 2;
42
+ const CACHE_VERSION = 3;
43
43
  export class FixPathSources {
44
44
  cache;
45
45
  rateLimiter;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Opt-in changelog fallback for `upgrade_impact`: GitHub releases, and a
3
+ * changelog FILE a PyPI project links from `project_urls` when that file is
4
+ * on GitHub (Decision 1, option B, approved 2026-10-11).
5
+ *
6
+ * Off by default. The default contract stays option A: only the package's own
7
+ * registry and OSV.dev are contacted. A user who wants release notes for the
8
+ * many packages whose archive ships no changelog (vite, zod, cobra, most Go
9
+ * modules) sets FOURDA_GITHUB_CHANGELOG=true; GitHub then learns the
10
+ * repository and the version range, nothing else. GITHUB_TOKEN, when set, is
11
+ * sent as a bearer token (5,000 requests an hour instead of 60).
12
+ *
13
+ * What is parsed is what the project itself published as release notes: the
14
+ * release body (Markdown), read by the same parser and classifier as an
15
+ * archive changelog, one section per release whose tag names a version of
16
+ * this package. A docs page (HTML) is never parsed. Rate limits are reported,
17
+ * not retried: the answer says when the limit resets and how to raise it.
18
+ */
19
+ import { type ChangelogSection, type VersionOrder } from "./changelog.js";
20
+ import type { ChangelogResult, UpgradeNet } from "./upgrade-sources.js";
21
+ export declare const GITHUB_CHANGELOG_ENV = "FOURDA_GITHUB_CHANGELOG";
22
+ /** True when the user opted in: FOURDA_GITHUB_CHANGELOG is 1 / true / yes / on. */
23
+ export declare function githubChangelogEnabled(env?: NodeJS.ProcessEnv): boolean;
24
+ /** `owner/repo` of a GitHub URL (https, git+https, git@, shorthand), or null. */
25
+ export declare function parseGithubRepo(url: string | null | undefined): {
26
+ owner: string;
27
+ repo: string;
28
+ } | null;
29
+ interface GithubRelease {
30
+ tag_name?: string;
31
+ name?: string | null;
32
+ body?: string | null;
33
+ draft?: boolean;
34
+ published_at?: string | null;
35
+ created_at?: string | null;
36
+ html_url?: string;
37
+ }
38
+ export declare class GithubRateLimitError extends Error {
39
+ }
40
+ /**
41
+ * The version a release tag names for this package, or null when the tag is
42
+ * another package's (monorepos) or not a version. `tagPrefix` is a Go
43
+ * module's subdirectory (`service/s3`): its tags are `service/s3/v1.2.3`.
44
+ */
45
+ export declare function versionFromTag(tag: string, pkg: string, tagPrefix: string | null): string | null;
46
+ /** One section per release that names a version of this package, entries parsed from its notes. */
47
+ export declare function releasesToSections(releases: GithubRelease[], pkg: string, tagPrefix: string | null): ChangelogSection[];
48
+ export interface GithubFallbackInput {
49
+ pkg: string;
50
+ ecosystem: string;
51
+ repository: string | null;
52
+ /** Changelog links the project published (PyPI `project_urls`). */
53
+ changelogLinks?: string[];
54
+ from: string;
55
+ to: string;
56
+ /** Go: the module's subdirectory in its repository, which prefixes its tags. */
57
+ tagPrefix?: string | null;
58
+ order: VersionOrder;
59
+ }
60
+ /** `https://github.com/o/r/blob/<ref>/<path>` (or raw.githubusercontent.com) to a raw file URL, when it names a changelog file. */
61
+ export declare function rawChangelogUrl(link: string): string | null;
62
+ export declare function githubCacheKey(kind: string, where: string, input: GithubFallbackInput): string;
63
+ /**
64
+ * The fallback: a GitHub changelog file the project links (PyPI), then the
65
+ * repository's releases. Caller checks the opt-in. Returns a not-found result
66
+ * with the reason (no GitHub repository, rate limit, no matching releases).
67
+ */
68
+ export declare function getGithubChangelog(net: UpgradeNet, input: GithubFallbackInput): Promise<ChangelogResult>;
69
+ export {};
@@ -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
+ }
@@ -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
+ }