@dep-radius/core 0.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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/dist/analyze/brief.d.ts +18 -0
  4. package/dist/analyze/brief.d.ts.map +1 -0
  5. package/dist/analyze/brief.js +89 -0
  6. package/dist/analyze/verdict.d.ts +16 -0
  7. package/dist/analyze/verdict.d.ts.map +1 -0
  8. package/dist/analyze/verdict.js +134 -0
  9. package/dist/context.d.ts +4 -0
  10. package/dist/context.d.ts.map +1 -0
  11. package/dist/context.js +36 -0
  12. package/dist/debug.d.ts +2 -0
  13. package/dist/debug.d.ts.map +1 -0
  14. package/dist/debug.js +130 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +8 -0
  18. package/dist/infra/cache.d.ts +18 -0
  19. package/dist/infra/cache.d.ts.map +1 -0
  20. package/dist/infra/cache.js +66 -0
  21. package/dist/infra/hash.d.ts +4 -0
  22. package/dist/infra/hash.d.ts.map +1 -0
  23. package/dist/infra/hash.js +18 -0
  24. package/dist/infra/http.d.ts +24 -0
  25. package/dist/infra/http.d.ts.map +1 -0
  26. package/dist/infra/http.js +44 -0
  27. package/dist/infra/limit.d.ts +4 -0
  28. package/dist/infra/limit.d.ts.map +1 -0
  29. package/dist/infra/limit.js +34 -0
  30. package/dist/infra/log.d.ts +8 -0
  31. package/dist/infra/log.d.ts.map +1 -0
  32. package/dist/infra/log.js +13 -0
  33. package/dist/inventory/installed.d.ts +15 -0
  34. package/dist/inventory/installed.d.ts.map +1 -0
  35. package/dist/inventory/installed.js +241 -0
  36. package/dist/inventory/lockfiles/bun.d.ts +3 -0
  37. package/dist/inventory/lockfiles/bun.d.ts.map +1 -0
  38. package/dist/inventory/lockfiles/bun.js +24 -0
  39. package/dist/inventory/lockfiles/npm.d.ts +3 -0
  40. package/dist/inventory/lockfiles/npm.d.ts.map +1 -0
  41. package/dist/inventory/lockfiles/npm.js +29 -0
  42. package/dist/inventory/lockfiles/pnpm.d.ts +3 -0
  43. package/dist/inventory/lockfiles/pnpm.d.ts.map +1 -0
  44. package/dist/inventory/lockfiles/pnpm.js +84 -0
  45. package/dist/inventory/lockfiles/types.d.ts +9 -0
  46. package/dist/inventory/lockfiles/types.d.ts.map +1 -0
  47. package/dist/inventory/lockfiles/types.js +1 -0
  48. package/dist/inventory/lockfiles/yarn.d.ts +3 -0
  49. package/dist/inventory/lockfiles/yarn.d.ts.map +1 -0
  50. package/dist/inventory/lockfiles/yarn.js +46 -0
  51. package/dist/inventory/manifests.d.ts +9 -0
  52. package/dist/inventory/manifests.d.ts.map +1 -0
  53. package/dist/inventory/manifests.js +135 -0
  54. package/dist/inventory/specifiers.d.ts +8 -0
  55. package/dist/inventory/specifiers.d.ts.map +1 -0
  56. package/dist/inventory/specifiers.js +53 -0
  57. package/dist/labels.d.ts +2 -0
  58. package/dist/labels.d.ts.map +1 -0
  59. package/dist/labels.js +18 -0
  60. package/dist/model.d.ts +276 -0
  61. package/dist/model.d.ts.map +1 -0
  62. package/dist/model.js +2 -0
  63. package/dist/notes/changelog.d.ts +11 -0
  64. package/dist/notes/changelog.d.ts.map +1 -0
  65. package/dist/notes/changelog.js +60 -0
  66. package/dist/notes/collect.d.ts +13 -0
  67. package/dist/notes/collect.d.ts.map +1 -0
  68. package/dist/notes/collect.js +225 -0
  69. package/dist/notes/entries.d.ts +3 -0
  70. package/dist/notes/entries.d.ts.map +1 -0
  71. package/dist/notes/entries.js +209 -0
  72. package/dist/notes/github.d.ts +25 -0
  73. package/dist/notes/github.d.ts.map +1 -0
  74. package/dist/notes/github.js +132 -0
  75. package/dist/notes/match.d.ts +13 -0
  76. package/dist/notes/match.d.ts.map +1 -0
  77. package/dist/notes/match.js +75 -0
  78. package/dist/notes/repo-url.d.ts +9 -0
  79. package/dist/notes/repo-url.d.ts.map +1 -0
  80. package/dist/notes/repo-url.js +46 -0
  81. package/dist/options.d.ts +53 -0
  82. package/dist/options.d.ts.map +1 -0
  83. package/dist/options.js +27 -0
  84. package/dist/registry/candidates.d.ts +24 -0
  85. package/dist/registry/candidates.d.ts.map +1 -0
  86. package/dist/registry/candidates.js +139 -0
  87. package/dist/registry/npmrc.d.ts +13 -0
  88. package/dist/registry/npmrc.d.ts.map +1 -0
  89. package/dist/registry/npmrc.js +99 -0
  90. package/dist/registry/packument.d.ts +34 -0
  91. package/dist/registry/packument.d.ts.map +1 -0
  92. package/dist/registry/packument.js +85 -0
  93. package/dist/registry/tar.d.ts +2 -0
  94. package/dist/registry/tar.d.ts.map +1 -0
  95. package/dist/registry/tar.js +75 -0
  96. package/dist/registry/tarball.d.ts +16 -0
  97. package/dist/registry/tarball.d.ts.map +1 -0
  98. package/dist/registry/tarball.js +59 -0
  99. package/dist/render/groups.d.ts +16 -0
  100. package/dist/render/groups.d.ts.map +1 -0
  101. package/dist/render/groups.js +105 -0
  102. package/dist/render/index.d.ts +2 -0
  103. package/dist/render/index.d.ts.map +1 -0
  104. package/dist/render/index.js +3 -0
  105. package/dist/render/json.d.ts +119 -0
  106. package/dist/render/json.d.ts.map +1 -0
  107. package/dist/render/json.js +130 -0
  108. package/dist/render/limits.d.ts +3 -0
  109. package/dist/render/limits.d.ts.map +1 -0
  110. package/dist/render/limits.js +25 -0
  111. package/dist/render/markdown.d.ts +3 -0
  112. package/dist/render/markdown.d.ts.map +1 -0
  113. package/dist/render/markdown.js +62 -0
  114. package/dist/run.d.ts +10 -0
  115. package/dist/run.d.ts.map +1 -0
  116. package/dist/run.js +218 -0
  117. package/dist/surface/analyze.d.ts +13 -0
  118. package/dist/surface/analyze.d.ts.map +1 -0
  119. package/dist/surface/analyze.js +166 -0
  120. package/dist/surface/delta.d.ts +5 -0
  121. package/dist/surface/delta.d.ts.map +1 -0
  122. package/dist/surface/delta.js +173 -0
  123. package/dist/surface/entries.d.ts +16 -0
  124. package/dist/surface/entries.d.ts.map +1 -0
  125. package/dist/surface/entries.js +187 -0
  126. package/dist/surface/extract.d.ts +15 -0
  127. package/dist/surface/extract.d.ts.map +1 -0
  128. package/dist/surface/extract.js +460 -0
  129. package/dist/surface/signature.d.ts +6 -0
  130. package/dist/surface/signature.d.ts.map +1 -0
  131. package/dist/surface/signature.js +229 -0
  132. package/dist/surface/vfs-host.d.ts +5 -0
  133. package/dist/surface/vfs-host.d.ts.map +1 -0
  134. package/dist/surface/vfs-host.js +54 -0
  135. package/dist/symbol-path.d.ts +28 -0
  136. package/dist/symbol-path.d.ts.map +1 -0
  137. package/dist/symbol-path.js +223 -0
  138. package/dist/usage/files.d.ts +18 -0
  139. package/dist/usage/files.d.ts.map +1 -0
  140. package/dist/usage/files.js +83 -0
  141. package/dist/usage/link.d.ts +23 -0
  142. package/dist/usage/link.d.ts.map +1 -0
  143. package/dist/usage/link.js +588 -0
  144. package/dist/usage/opaque.d.ts +7 -0
  145. package/dist/usage/opaque.d.ts.map +1 -0
  146. package/dist/usage/opaque.js +193 -0
  147. package/dist/usage/parse.d.ts +73 -0
  148. package/dist/usage/parse.d.ts.map +1 -0
  149. package/dist/usage/parse.js +608 -0
  150. package/dist/usage/resolve-local.d.ts +35 -0
  151. package/dist/usage/resolve-local.d.ts.map +1 -0
  152. package/dist/usage/resolve-local.js +340 -0
  153. package/dist/usage/scan.d.ts +4 -0
  154. package/dist/usage/scan.d.ts.map +1 -0
  155. package/dist/usage/scan.js +92 -0
  156. package/package.json +79 -0
  157. package/schema/brief-v1.schema.json +319 -0
@@ -0,0 +1,225 @@
1
+ import semver from "semver";
2
+ import { splitEntries } from "./entries.js";
3
+ import { parseRepository } from "./repo-url.js";
4
+ import { changelogSections } from "./changelog.js";
5
+ import { CHANGELOG_FILE, getTarballFiles } from "../registry/tarball.js";
6
+ import { githubState, rawChangelog, releaseByTag, releasesPage, tagCandidates, } from "./github.js";
7
+ const MAX_TAG_LOOKUPS = 12;
8
+ export async function collectNotes(ctx, cfg, p, c) {
9
+ const versions = c.range;
10
+ const bodies = new Map();
11
+ const failures = new Map();
12
+ const missing = () => versions.filter((v) => !bodies.has(v));
13
+ // a major lists its breaking changes in its betas more often than in its x.0.0: read them wherever
14
+ // a source is already open, never at the cost of a request, and never counted in the coverage
15
+ const pre = prereleasesIn(p, versions);
16
+ const preBodies = new Map();
17
+ const takePre = (sections) => {
18
+ for (const v of pre) {
19
+ const body = sections.get(v);
20
+ if (body?.trim() && !preBodies.has(v))
21
+ preBodies.set(v, body);
22
+ }
23
+ };
24
+ // 1. a changelog shipped inside the target tarball covers every version up to it, for free
25
+ const target = p.versions[c.to];
26
+ if (target) {
27
+ const tb = await getTarballFiles(ctx, cfg, target);
28
+ if (tb.ok) {
29
+ for (const [path, buf] of tb.files) {
30
+ if (!CHANGELOG_FILE.test(path))
31
+ continue;
32
+ const sections = changelogSections(buf.toString("utf8"));
33
+ takePre(sections);
34
+ for (const v of missing()) {
35
+ const body = sections.get(v);
36
+ if (body !== undefined && body.trim())
37
+ bodies.set(v, {
38
+ kind: "tarball-changelog",
39
+ url: `${c.pkg}@${c.to}/${path}`,
40
+ body,
41
+ });
42
+ }
43
+ }
44
+ }
45
+ else if (tb.reason === "offline-uncached") {
46
+ for (const v of missing())
47
+ failures.set(v, "offline");
48
+ }
49
+ }
50
+ // 2. GitHub releases
51
+ const repo = parseRepository(target?.repository ?? p.repository);
52
+ let repoReachable = false;
53
+ if (missing().length > 0) {
54
+ if (!repo)
55
+ for (const v of missing())
56
+ failures.set(v, "no-repo");
57
+ else if (repo.host !== "github")
58
+ for (const v of missing())
59
+ failures.set(v, "unsupported-host");
60
+ else {
61
+ const page = await releasesPage(ctx, repo);
62
+ if (page.ok) {
63
+ repoReachable = true;
64
+ const byTag = new Map(page.value.map((r) => [r.tag, r]));
65
+ let scheme;
66
+ for (const v of versions) {
67
+ const cands = tagCandidates(c.pkg, v);
68
+ const hit = cands.findIndex((t) => byTag.has(t));
69
+ if (hit >= 0) {
70
+ scheme ??= hit;
71
+ const rel = byTag.get(cands[hit]);
72
+ if (!bodies.has(v))
73
+ acceptRelease(v, rel, repo.owner, repo.repo);
74
+ }
75
+ }
76
+ for (const v of pre) {
77
+ const rel = tagCandidates(c.pkg, v)
78
+ .map((t) => byTag.get(t))
79
+ .find(Boolean);
80
+ if (rel && !preBodies.has(v) && !isPointerOnly(rel.body))
81
+ preBodies.set(v, rel.body);
82
+ }
83
+ // versions pushed off the first page: look them up by tag, with the scheme the page taught us
84
+ let lookups = 0;
85
+ for (const v of missing()) {
86
+ if (lookups >= MAX_TAG_LOOKUPS)
87
+ break;
88
+ const cands = tagCandidates(c.pkg, v);
89
+ const tries = scheme !== undefined ? [cands[scheme]] : cands.slice(0, 3);
90
+ for (const tag of tries) {
91
+ lookups++;
92
+ const r = await releaseByTag(ctx, repo, tag);
93
+ if (r.ok) {
94
+ acceptRelease(v, r.value, repo.owner, repo.repo);
95
+ break;
96
+ }
97
+ if (r.reason !== "not-found") {
98
+ failures.set(v, r.reason === "offline"
99
+ ? "offline"
100
+ : r.reason === "rate-limited"
101
+ ? "rate-limited"
102
+ : "http-error");
103
+ break;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ else if (page.reason === "not-found") {
109
+ repoReachable = true;
110
+ }
111
+ else {
112
+ for (const v of missing())
113
+ failures.set(v, page.reason === "offline"
114
+ ? "offline"
115
+ : page.reason === "rate-limited"
116
+ ? "rate-limited"
117
+ : "http-error");
118
+ }
119
+ // 3. the repository's changelog
120
+ if (missing().length > 0) {
121
+ const raw = await rawChangelog(ctx, repo);
122
+ if (raw.ok) {
123
+ repoReachable = true;
124
+ const sections = changelogSections(raw.value.text);
125
+ takePre(sections);
126
+ for (const v of missing()) {
127
+ const body = sections.get(v);
128
+ if (body !== undefined && body.trim()) {
129
+ bodies.set(v, {
130
+ kind: "repo-changelog",
131
+ url: raw.value.url,
132
+ body,
133
+ });
134
+ failures.delete(v);
135
+ }
136
+ }
137
+ }
138
+ else if (raw.reason === "not-found") {
139
+ repoReachable = true;
140
+ }
141
+ }
142
+ }
143
+ }
144
+ function acceptRelease(v, r, owner, name) {
145
+ if (isPointerOnly(r.body)) {
146
+ failures.set(v, "stub-body");
147
+ return;
148
+ }
149
+ bodies.set(v, {
150
+ kind: "github-release",
151
+ url: `https://github.com/${owner}/${name}/releases/tag/${encodeURIComponent(r.tag)}`,
152
+ body: r.body,
153
+ });
154
+ failures.delete(v);
155
+ }
156
+ const perVersion = versions.map((v) => {
157
+ const b = bodies.get(v);
158
+ if (b)
159
+ return {
160
+ version: v,
161
+ status: "found",
162
+ source: { kind: b.kind, url: b.url },
163
+ };
164
+ const reason = failures.get(v);
165
+ if (reason && reason !== "stub-body")
166
+ return { version: v, status: "unavailable", reason };
167
+ return repoReachable || reason === "stub-body"
168
+ ? { version: v, status: "not-published", ...(reason ? { reason } : {}) }
169
+ : {
170
+ version: v,
171
+ status: "unavailable",
172
+ reason: reason ?? (githubState.rateLimited ? "rate-limited" : "no-repo"),
173
+ };
174
+ });
175
+ const entries = [...versions, ...preBodies.keys()]
176
+ .sort(semver.compare)
177
+ .flatMap((v) => {
178
+ const body = bodies.get(v)?.body ?? preBodies.get(v);
179
+ return body ? splitEntries(v, body) : [];
180
+ });
181
+ const found = perVersion.filter((x) => x.status === "found").length;
182
+ const unavailable = perVersion.filter((x) => x.status === "unavailable").length;
183
+ let coverage;
184
+ if (versions.length === 0 || found === versions.length)
185
+ coverage = "complete";
186
+ else if (found > 0)
187
+ coverage = "partial";
188
+ else if (unavailable > 0)
189
+ coverage = "unavailable";
190
+ else
191
+ coverage = "none-published";
192
+ return { perVersion, entries, coverage };
193
+ }
194
+ // "**Change log available at** <link>" and GitHub's "**Full Changelog**: v1...v2" read as notes and
195
+ // say nothing. Counted as found, they let a major release pass as quiet on no information, so a
196
+ // body is a pointer once its links are gone and what is left only points elsewhere.
197
+ export function isPointerOnly(body) {
198
+ const text = body
199
+ .replace(/<!--[\s\S]*?-->/g, "")
200
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
201
+ .replace(/https?:\/\/\S+/g, "")
202
+ .replace(/^\s*\**\s*full changelog\s*\**\s*:?.*$/gim, "")
203
+ .replace(/[*_#>`]/g, "")
204
+ .replace(/\s+/g, " ")
205
+ .trim();
206
+ if (!text)
207
+ return true;
208
+ return (text.length < 120 &&
209
+ /change ?log|release notes|see |available (at|on|here)|upgrade guide|migration guide|blog post|details/i.test(text));
210
+ }
211
+ const PRERELEASE_TAG = /^(alpha|beta|rc|pre|preview)$/i;
212
+ // Betas, release candidates and alphas of the stable versions in range. Canaries, nightlies and
213
+ // dated builds are left out: a busy package publishes hundreds of them.
214
+ export function prereleasesIn(p, stable) {
215
+ const inRange = new Set(stable);
216
+ return Object.keys(p.versions)
217
+ .filter((v) => {
218
+ const tags = semver.prerelease(v);
219
+ if (!tags || !PRERELEASE_TAG.test(String(tags[0])))
220
+ return false;
221
+ return inRange.has(`${semver.major(v)}.${semver.minor(v)}.${semver.patch(v)}`);
222
+ })
223
+ .sort(semver.compare)
224
+ .slice(-20);
225
+ }
@@ -0,0 +1,3 @@
1
+ import type { NoteEntry } from "../model.ts";
2
+ export declare function splitEntries(version: string, markdown: string): NoteEntry[];
3
+ //# sourceMappingURL=entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entries.d.ts","sourceRoot":"","sources":["../../src/notes/entries.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,aAAa,CAAA;AAmBxD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,CAkG3E"}
@@ -0,0 +1,209 @@
1
+ import { sha1 } from "../infra/hash.js";
2
+ // A heading says a whole section breaks; a title has to lead with it. "non-breaking" and a bullet that
3
+ // merely mentions breaking somewhere in its prose are not markers.
4
+ const BREAKING_HEADING = /(?<!non[- ])breaking|⚠️|:warning:|major changes/i;
5
+ const BREAKING_TITLE = /^\s*(\*\*|\[|\()?\s*(⚠️|:warning:|breaking|💥|:boom:)|BREAKING CHANGE/i;
6
+ const NOISE = /^(ci|chore|test|tests|docs|doc|build|style|refactor|perf\(bench\)|release|revert)(\([^)]*\))?!?:|^(deps?|dependencies)(\([^)]*\))?:|^bump\s+\S+\s+from\s|^merge (pull request|branch)|^update dependency\b|^v?\d+\.\d+\.\d+$/i;
7
+ // One markdown body (a release, or a changelog section) into entries: a top-level bullet with its
8
+ // nested lines, a heading whose body is prose or code, or a stray paragraph.
9
+ export function splitEntries(version, markdown) {
10
+ const drafts = [];
11
+ const stack = [];
12
+ let current;
13
+ let headingDraft;
14
+ let inFence = false;
15
+ let blankRun = 0;
16
+ const close = () => {
17
+ if (current && (current.kind !== "heading" || current.lines.length > 0))
18
+ drafts.push(current);
19
+ current = undefined;
20
+ };
21
+ const path = () => stack.map((s) => s.text);
22
+ const lines = preprocess(markdown).split(/\r?\n/);
23
+ for (let i = 0; i < lines.length; i++) {
24
+ const line = lines[i];
25
+ if (/^\s*(```|~~~)/.test(line)) {
26
+ inFence = !inFence;
27
+ if (!current) {
28
+ current =
29
+ headingDraft && !drafts.includes(headingDraft)
30
+ ? headingDraft
31
+ : newDraft("paragraph", firstWords(line), path());
32
+ }
33
+ continue;
34
+ }
35
+ if (inFence) {
36
+ current?.lines.push({ text: line, code: true });
37
+ continue;
38
+ }
39
+ const atx = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
40
+ const setext = !atx &&
41
+ line.trim() &&
42
+ lines[i + 1] !== undefined &&
43
+ /^(=+|-{3,})\s*$/.test(lines[i + 1]) &&
44
+ !/^\s*[-*+]\s/.test(line);
45
+ if (atx || setext) {
46
+ close();
47
+ const level = atx ? atx[1].length : lines[i + 1].startsWith("=") ? 1 : 2;
48
+ const text = clean(atx ? atx[2] : line.trim());
49
+ while (stack.length > 0 && stack[stack.length - 1].level >= level)
50
+ stack.pop();
51
+ headingDraft = newDraft("heading", text, path());
52
+ stack.push({ level, text });
53
+ current = headingDraft;
54
+ if (setext)
55
+ i++;
56
+ blankRun = 0;
57
+ continue;
58
+ }
59
+ if (!line.trim()) {
60
+ blankRun++;
61
+ continue;
62
+ }
63
+ const bullet = /^(\s{0,1})([-*+]|\d+[.)])\s+(.*)$/.exec(line);
64
+ const indent = line.length - line.trimStart().length;
65
+ if (bullet) {
66
+ close();
67
+ current = newDraft("bullet", clean(bullet[3] ?? ""), path());
68
+ blankRun = 0;
69
+ continue;
70
+ }
71
+ if (current?.kind === "bullet" && (indent >= 2 || blankRun === 0)) {
72
+ current.lines.push({ text: line, code: false });
73
+ blankRun = 0;
74
+ continue;
75
+ }
76
+ if (current?.kind === "heading" || current?.kind === "paragraph") {
77
+ current.lines.push({ text: line, code: false });
78
+ blankRun = 0;
79
+ continue;
80
+ }
81
+ close();
82
+ current = newDraft("paragraph", clean(firstSentence(line)), path());
83
+ current.lines.push({ text: line, code: false });
84
+ blankRun = 0;
85
+ }
86
+ close();
87
+ const kept = drafts.filter((d) => d.title || d.lines.some((l) => l.text.trim()));
88
+ // an "at a glance" bullet that restates a section of the same release: the section is the entry
89
+ const headingTitles = kept
90
+ .filter((d) => d.kind === "heading")
91
+ .map((d) => normTitle(d.title));
92
+ const entries = kept
93
+ .filter((d) => d.kind !== "bullet" ||
94
+ !headingTitles.some((h) => h.length >= 3 && startsWithTitle(normTitle(d.title), h)))
95
+ .map((d, index) => toEntry(version, d, index));
96
+ return dedupeByRefs(entries);
97
+ }
98
+ function normTitle(s) {
99
+ return s
100
+ .replace(/[`*_]|\u26A0\uFE0F?/gu, "")
101
+ .replace(/\s+/g, " ")
102
+ .trim()
103
+ .toLowerCase();
104
+ }
105
+ function startsWithTitle(bullet, heading) {
106
+ return (bullet === heading ||
107
+ (bullet.startsWith(heading) &&
108
+ /^[\s—–:(-]/.test(bullet.slice(heading.length))));
109
+ }
110
+ function newDraft(kind, title, headingPath) {
111
+ return { kind, title, headingPath, lines: [] };
112
+ }
113
+ function toEntry(version, d, index) {
114
+ const regions = [];
115
+ const pushText = (text, kind) => {
116
+ const parts = text.split(/(`[^`]+`)/);
117
+ for (const p of parts) {
118
+ if (!p)
119
+ continue;
120
+ if (p.startsWith("`") && p.endsWith("`") && p.length > 1)
121
+ regions.push({ kind: "inline-code", text: p.slice(1, -1) });
122
+ else if (p.trim())
123
+ regions.push({ kind, text: p });
124
+ }
125
+ };
126
+ pushText(d.title, "title");
127
+ let code = [];
128
+ const flush = () => {
129
+ if (code.length > 0)
130
+ regions.push({ kind: "code-block", text: code.join("\n") });
131
+ code = [];
132
+ };
133
+ for (const l of d.lines) {
134
+ if (l.code)
135
+ code.push(l.text);
136
+ else {
137
+ flush();
138
+ pushText(clean(l.text), "prose");
139
+ }
140
+ }
141
+ flush();
142
+ const all = [d.title, ...d.lines.map((l) => l.text)].join("\n");
143
+ const refs = [
144
+ ...new Set([...all.matchAll(/(?<![\w/])#(\d{1,7})\b/g)].map((m) => `#${m[1]}`)),
145
+ ];
146
+ const bareTitle = d.title
147
+ .replace(/`/g, "")
148
+ .replace(/^[0-9a-f]{7,40}\s+/, "")
149
+ .replace(/^\*\*[^*]+\*\*:?\s*/, "");
150
+ const titleForDisplay = d.title.replace(/`/g, "");
151
+ return {
152
+ id: sha1(`${version}\0${index}\0${all}`).slice(0, 12),
153
+ version,
154
+ title: titleForDisplay.length > 200
155
+ ? `${titleForDisplay.slice(0, 197)}...`
156
+ : titleForDisplay,
157
+ headingPath: d.headingPath,
158
+ regions,
159
+ breakingMarker: BREAKING_TITLE.test(d.title) ||
160
+ d.headingPath.some((h) => BREAKING_HEADING.test(h)),
161
+ noise: NOISE.test(bareTitle.trim()) ||
162
+ d.headingPath.some((h) => /^(commits|contributors|new contributors)$/i.test(h.trim())),
163
+ refs,
164
+ };
165
+ }
166
+ // The "Commits" list of a release repeats the pull requests the sections already describe.
167
+ function dedupeByRefs(entries) {
168
+ const covered = new Set();
169
+ for (const e of entries)
170
+ if (!e.noise &&
171
+ e.refs.length > 0 &&
172
+ e.regions.some((r) => r.kind !== "title"))
173
+ for (const r of e.refs)
174
+ covered.add(r);
175
+ return entries.filter((e) => !(e.refs.length > 0 &&
176
+ e.refs.every((r) => covered.has(r)) &&
177
+ e.regions.every((r) => r.kind === "title" || r.kind === "inline-code") &&
178
+ isShadowed(e, entries)));
179
+ }
180
+ function isShadowed(e, entries) {
181
+ return entries.some((o) => o !== e &&
182
+ o.regions.length > e.regions.length &&
183
+ e.refs.every((r) => o.refs.includes(r)));
184
+ }
185
+ function preprocess(md) {
186
+ return (md
187
+ .replace(/<!--[\s\S]*?-->/g, "")
188
+ // a pull request URL is a ref, keep it as one before links are dropped
189
+ .replace(/https?:\/\/github\.com\/[\w.-]+\/[\w.-]+\/(?:pull|issues)\/(\d+)/g, "#$1")
190
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, "")
191
+ .replace(/<img\b[^>]*>/gi, "")
192
+ .replace(/<\/?(details|summary|p|div|br|sup|sub|b|i|em|strong|span|a)\b[^>]*>/gi, " "));
193
+ }
194
+ function clean(s) {
195
+ return s
196
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
197
+ .replace(/\[([^\]]+)\]\[[^\]]*\]/g, "$1")
198
+ .replace(/https?:\/\/\S+/g, "")
199
+ .replace(/\s+/g, " ")
200
+ .trim();
201
+ }
202
+ function firstSentence(s) {
203
+ const t = clean(s);
204
+ const m = /^(.{20,160}?[.!?])(\s|$)/.exec(t);
205
+ return m?.[1] ?? t.slice(0, 160);
206
+ }
207
+ function firstWords(s) {
208
+ return s.replace(/[`~]/g, "").trim().slice(0, 40);
209
+ }
@@ -0,0 +1,25 @@
1
+ import type { Ctx } from "../options.ts";
2
+ import type { RepoRef } from "./repo-url.ts";
3
+ export interface Release {
4
+ tag: string;
5
+ body: string;
6
+ }
7
+ export type Fetched<T> = {
8
+ ok: true;
9
+ value: T;
10
+ } | {
11
+ ok: false;
12
+ reason: "offline" | "rate-limited" | "http-error" | "not-found";
13
+ };
14
+ export declare const githubState: {
15
+ rateLimited: boolean;
16
+ usedToken: boolean;
17
+ };
18
+ export declare function releasesPage(ctx: Ctx, repo: RepoRef): Promise<Fetched<Release[]>>;
19
+ export declare function releaseByTag(ctx: Ctx, repo: RepoRef, tag: string): Promise<Fetched<Release>>;
20
+ export declare function rawChangelog(ctx: Ctx, repo: RepoRef): Promise<Fetched<{
21
+ url: string;
22
+ text: string;
23
+ }>>;
24
+ export declare function tagCandidates(pkg: string, version: string): string[];
25
+ //# sourceMappingURL=github.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../src/notes/github.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAExC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAG5C,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,MAAM,OAAO,CAAC,CAAC,IACjB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACtB;IACE,EAAE,EAAE,KAAK,CAAA;IACT,MAAM,EAAE,SAAS,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,CAAA;CAChE,CAAA;AAML,eAAO,MAAM,WAAW;;;CAA2C,CAAA;AA2DnE,wBAAsB,YAAY,CAChC,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAc7B;AAED,wBAAsB,YAAY,CAChC,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,OAAO,EACb,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAY3B;AAGD,wBAAsB,YAAY,CAChC,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CA8CjD;AAGD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAWpE"}
@@ -0,0 +1,132 @@
1
+ import { safeKey } from "../infra/cache.js";
2
+ import { OfflineError } from "../infra/http.js";
3
+ const PAGE_TTL = 60 * 60 * 1000;
4
+ const BODY_TTL = 24 * 60 * 60 * 1000;
5
+ const MISS_TTL = 6 * 60 * 60 * 1000;
6
+ export const githubState = { rateLimited: false, usedToken: false };
7
+ async function apiGet(ctx, path, ttl, missTtl) {
8
+ const key = `github/${safeKey(path)}.json`;
9
+ const cached = await ctx.cache.getJson(key);
10
+ if (cached &&
11
+ (ctx.offline ||
12
+ ctx.now - cached.storedAt < (cached.value.found ? ttl : missTtl))) {
13
+ return cached.value.found
14
+ ? { ok: true, value: cached.value.data }
15
+ : { ok: false, reason: "not-found" };
16
+ }
17
+ if (ctx.offline)
18
+ return { ok: false, reason: "offline" };
19
+ if (githubState.rateLimited)
20
+ return { ok: false, reason: "rate-limited" };
21
+ const token = await ctx.githubToken();
22
+ if (token)
23
+ githubState.usedToken = true;
24
+ const headers = {
25
+ accept: "application/vnd.github+json",
26
+ "x-github-api-version": "2022-11-28",
27
+ "user-agent": "dep-radius",
28
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
29
+ };
30
+ try {
31
+ const res = await ctx.github(() => ctx.http.get({ url: `https://api.github.com${path}`, headers }));
32
+ if ((res.status === 403 || res.status === 429) &&
33
+ res.headers["x-ratelimit-remaining"] === "0") {
34
+ githubState.rateLimited = true;
35
+ return { ok: false, reason: "rate-limited" };
36
+ }
37
+ if (res.status === 404) {
38
+ await ctx.cache.setJson(key, { found: false }, ctx.now);
39
+ return { ok: false, reason: "not-found" };
40
+ }
41
+ if (res.status !== 200)
42
+ return { ok: false, reason: "http-error" };
43
+ const data = JSON.parse(res.body.toString("utf8"));
44
+ await ctx.cache.setJson(key, { found: true, data }, ctx.now);
45
+ return { ok: true, value: data };
46
+ }
47
+ catch (error) {
48
+ if (cached?.value.found)
49
+ return { ok: true, value: cached.value.data };
50
+ return {
51
+ ok: false,
52
+ reason: error instanceof OfflineError ? "offline" : "http-error",
53
+ };
54
+ }
55
+ }
56
+ export async function releasesPage(ctx, repo) {
57
+ const r = await apiGet(ctx, `/repos/${repo.owner}/${repo.repo}/releases?per_page=100`, PAGE_TTL, PAGE_TTL);
58
+ if (!r.ok)
59
+ return r;
60
+ return {
61
+ ok: true,
62
+ value: r.value
63
+ .filter((x) => !x.draft)
64
+ .map((x) => ({ tag: x.tag_name, body: x.body ?? "" })),
65
+ };
66
+ }
67
+ export async function releaseByTag(ctx, repo, tag) {
68
+ const r = await apiGet(ctx, `/repos/${repo.owner}/${repo.repo}/releases/tags/${encodeURIComponent(tag)}`, BODY_TTL, MISS_TTL);
69
+ if (!r.ok)
70
+ return r;
71
+ return {
72
+ ok: true,
73
+ value: { tag: r.value.tag_name, body: r.value.body ?? "" },
74
+ };
75
+ }
76
+ // raw.githubusercontent.com costs no API quota.
77
+ export async function rawChangelog(ctx, repo) {
78
+ const dirs = [...new Set([repo.directory, undefined])];
79
+ const names = ["CHANGELOG.md", "HISTORY.md", "History.md", "CHANGES.md"];
80
+ let sawError;
81
+ for (const dir of dirs) {
82
+ for (const name of names) {
83
+ const url = `https://raw.githubusercontent.com/${repo.owner}/${repo.repo}/HEAD/${dir ? `${dir}/` : ""}${name}`;
84
+ const key = `raw/${safeKey(url)}.json`;
85
+ const cached = await ctx.cache.getJson(key);
86
+ if (cached &&
87
+ (ctx.offline ||
88
+ ctx.now - cached.storedAt <
89
+ (cached.value.found ? BODY_TTL : MISS_TTL))) {
90
+ if (cached.value.found)
91
+ return { ok: true, value: { url, text: cached.value.text ?? "" } };
92
+ continue;
93
+ }
94
+ if (ctx.offline) {
95
+ sawError = { ok: false, reason: "offline" };
96
+ continue;
97
+ }
98
+ try {
99
+ const res = await ctx.net(() => ctx.http.get({ url, headers: { "user-agent": "dep-radius" } }));
100
+ if (res.status === 200) {
101
+ const text = res.body.toString("utf8");
102
+ await ctx.cache.setJson(key, { found: true, text }, ctx.now);
103
+ return { ok: true, value: { url, text } };
104
+ }
105
+ if (res.status === 404)
106
+ await ctx.cache.setJson(key, { found: false }, ctx.now);
107
+ else
108
+ sawError = { ok: false, reason: "http-error" };
109
+ }
110
+ catch (error) {
111
+ sawError = {
112
+ ok: false,
113
+ reason: error instanceof OfflineError ? "offline" : "http-error",
114
+ };
115
+ }
116
+ }
117
+ }
118
+ return sawError ?? { ok: false, reason: "not-found" };
119
+ }
120
+ // Fixed order: an index into this list is the "scheme" learned from a repository's tags.
121
+ export function tagCandidates(pkg, version) {
122
+ const unscoped = pkg.includes("/") ? pkg.split("/")[1] : pkg;
123
+ return [
124
+ `v${version}`,
125
+ version,
126
+ `${pkg}@${version}`,
127
+ `${unscoped}@${version}`,
128
+ `${pkg}-v${version}`,
129
+ `${unscoped}-v${version}`,
130
+ `release-${version}`,
131
+ ];
132
+ }
@@ -0,0 +1,13 @@
1
+ import type { NoteEntry, NoteMatch } from "../model.ts";
2
+ export declare function isCodeShaped(name: string): boolean;
3
+ export interface MatchResult {
4
+ matched: NoteMatch[];
5
+ unattributedBreaking: NoteEntry[];
6
+ total: number;
7
+ }
8
+ export interface MatchOptions {
9
+ demoteShare?: number;
10
+ }
11
+ export declare const DEMOTE_SHARE = 0.25;
12
+ export declare function matchNotes(entries: NoteEntry[], strong: string[], weak: string[], options?: MatchOptions): MatchResult;
13
+ //# sourceMappingURL=match.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../../src/notes/match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAW,SAAS,EAAc,MAAM,aAAa,CAAA;AAK5E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AA0BD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,oBAAoB,EAAE,SAAS,EAAE,CAAA;IACjC,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,YAAY;IAG3B,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,eAAO,MAAM,YAAY,OAAO,CAAA;AAEhC,wBAAgB,UAAU,CACxB,OAAO,EAAE,SAAS,EAAE,EACpB,MAAM,EAAE,MAAM,EAAE,EAChB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,YAAiB,GACzB,WAAW,CA8Db"}