paseo-plugin-genui 0.0.0-stage → 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 (375) hide show
  1. package/LICENSE +202 -0
  2. package/OVERVIEW.md +21 -0
  3. package/README.md +77 -2
  4. package/assets/fonts/Excalifont-OFL.txt +109 -0
  5. package/assets/fonts/Excalifont-Regular-latin.woff2 +0 -0
  6. package/assets/icon.png +0 -0
  7. package/assets/icon.svg +1 -0
  8. package/assets/screenshot-chat.png +0 -0
  9. package/assets/screenshot-gallery.png +0 -0
  10. package/assets/screenshot-playground.png +0 -0
  11. package/client/components/content.tsx +468 -0
  12. package/client/components/data.tsx +967 -0
  13. package/client/components/dev.tsx +998 -0
  14. package/client/components/diagram.tsx +513 -0
  15. package/client/components/inputs.tsx +994 -0
  16. package/client/components/layout.tsx +239 -0
  17. package/client/components/media.tsx +428 -0
  18. package/client/components/project.tsx +641 -0
  19. package/client/components/widgets.tsx +836 -0
  20. package/client/gallery.tsx +449 -0
  21. package/client/live-ui.tsx +525 -0
  22. package/client/loaders.ts +342 -0
  23. package/client/markdown.tsx +172 -0
  24. package/client/node-renderer.tsx +149 -0
  25. package/client/render.tsx +186 -0
  26. package/client/runtime.ts +103 -0
  27. package/client/settings.tsx +57 -0
  28. package/client/web.ts +186 -0
  29. package/gallery/Acceptance-baseline.ui +1 -0
  30. package/gallery/Acceptance-checklist.ui +14 -0
  31. package/gallery/Acceptance-evidence.ui +9 -0
  32. package/gallery/Acceptance.ui +1 -0
  33. package/gallery/Badge.ui +3 -0
  34. package/gallery/Benchmark-context.ui +8 -0
  35. package/gallery/Benchmark-hyperfine.ui +1 -0
  36. package/gallery/Benchmark-runs-list.ui +9 -0
  37. package/gallery/Benchmark-runs.ui +1 -0
  38. package/gallery/Benchmark-vitest.ui +1 -0
  39. package/gallery/Benchmark.ui +9 -0
  40. package/gallery/Board-bb-tasks.ui +1 -0
  41. package/gallery/Board-gh-issues.ui +1 -0
  42. package/gallery/Board-gh-prs.ui +1 -0
  43. package/gallery/Board.ui +12 -0
  44. package/gallery/Button-destructive.ui +4 -0
  45. package/gallery/Button.ui +5 -0
  46. package/gallery/Calendar.ui +15 -0
  47. package/gallery/CalendarHeatmap-short.ui +11 -0
  48. package/gallery/CalendarHeatmap.ui +4 -0
  49. package/gallery/Callout.ui +4 -0
  50. package/gallery/Card.ui +4 -0
  51. package/gallery/Carousel.ui +8 -0
  52. package/gallery/Changes-paths.ui +1 -0
  53. package/gallery/Changes-uncommitted.ui +1 -0
  54. package/gallery/Changes.ui +1 -0
  55. package/gallery/Chart-area.ui +3 -0
  56. package/gallery/Chart-funnel.ui +3 -0
  57. package/gallery/Chart-heatmap.ui +11 -0
  58. package/gallery/Chart-pie.ui +3 -0
  59. package/gallery/Chart-radar.ui +3 -0
  60. package/gallery/Chart-scatter.ui +4 -0
  61. package/gallery/Chart-stacked.ui +5 -0
  62. package/gallery/Chart.ui +3 -0
  63. package/gallery/Checklist.ui +6 -0
  64. package/gallery/Choice.ui +8 -0
  65. package/gallery/Code.ui +3 -0
  66. package/gallery/Commits-limit.ui +1 -0
  67. package/gallery/Commits.ui +1 -0
  68. package/gallery/Compare-conditions.ui +4 -0
  69. package/gallery/Compare-three.ui +7 -0
  70. package/gallery/Compare.ui +4 -0
  71. package/gallery/Coverage-go.ui +1 -0
  72. package/gallery/Coverage-lcov.ui +1 -0
  73. package/gallery/Coverage-python.ui +1 -0
  74. package/gallery/Coverage.ui +1 -0
  75. package/gallery/DateInput.ui +5 -0
  76. package/gallery/Decision-percent.ui +10 -0
  77. package/gallery/Decision.ui +12 -0
  78. package/gallery/Dependencies-audit.ui +1 -0
  79. package/gallery/Dependencies-both.ui +1 -0
  80. package/gallery/Dependencies-list.ui +7 -0
  81. package/gallery/Dependencies-pip.ui +1 -0
  82. package/gallery/Dependencies-pnpm.ui +1 -0
  83. package/gallery/Dependencies.ui +1 -0
  84. package/gallery/Details.ui +4 -0
  85. package/gallery/Diagram-flow.ui +16 -0
  86. package/gallery/Diagram-sequence.ui +15 -0
  87. package/gallery/Diagram.ui +16 -0
  88. package/gallery/Diff.ui +3 -0
  89. package/gallery/Divider.ui +5 -0
  90. package/gallery/EditableTable.ui +9 -0
  91. package/gallery/Entity-list.ui +6 -0
  92. package/gallery/Entity.ui +8 -0
  93. package/gallery/Environments.ui +1 -0
  94. package/gallery/Estimate-points.ui +6 -0
  95. package/gallery/Estimate.ui +8 -0
  96. package/gallery/File.ui +4 -0
  97. package/gallery/Findings-biome.ui +1 -0
  98. package/gallery/Findings-eslint.ui +1 -0
  99. package/gallery/Findings-oxlint.ui +1 -0
  100. package/gallery/Findings-review.ui +1 -0
  101. package/gallery/Findings-ruff.ui +1 -0
  102. package/gallery/Findings-sarif.ui +1 -0
  103. package/gallery/Findings-ship.ui +11 -0
  104. package/gallery/Findings-tsc.ui +1 -0
  105. package/gallery/Findings.ui +7 -0
  106. package/gallery/Flashcards.ui +3 -0
  107. package/gallery/FollowUps.ui +4 -0
  108. package/gallery/Form.ui +8 -0
  109. package/gallery/Gallery-links.ui +8 -0
  110. package/gallery/Gallery-strip.ui +14 -0
  111. package/gallery/Gallery.ui +10 -0
  112. package/gallery/Gauge.ui +7 -0
  113. package/gallery/Grid.ui +8 -0
  114. package/gallery/Hotspots-photo.ui +8 -0
  115. package/gallery/Hotspots.ui +11 -0
  116. package/gallery/Icon.ui +5 -0
  117. package/gallery/Image.ui +4 -0
  118. package/gallery/ImageSearch-aspects.ui +6 -0
  119. package/gallery/ImageSearch.ui +4 -0
  120. package/gallery/Input.ui +5 -0
  121. package/gallery/Json-api.ui +1 -0
  122. package/gallery/Json.ui +8 -0
  123. package/gallery/KeyValue.ui +3 -0
  124. package/gallery/List-dots.ui +8 -0
  125. package/gallery/List.ui +8 -0
  126. package/gallery/ListEditor.ui +5 -0
  127. package/gallery/Logs-jsonl.ui +1 -0
  128. package/gallery/Logs-lines.ui +11 -0
  129. package/gallery/Logs-python.ui +1 -0
  130. package/gallery/Logs.ui +1 -0
  131. package/gallery/Map-city.ui +8 -0
  132. package/gallery/Map.ui +8 -0
  133. package/gallery/Metric.ui +3 -0
  134. package/gallery/OptionCards-multi.ui +10 -0
  135. package/gallery/OptionCards.ui +10 -0
  136. package/gallery/Pipeline-checks.ui +1 -0
  137. package/gallery/Pipeline-passed.ui +1 -0
  138. package/gallery/Pipeline-running.ui +1 -0
  139. package/gallery/Pipeline-stages.ui +13 -0
  140. package/gallery/Pipeline.ui +1 -0
  141. package/gallery/Plan.ui +9 -0
  142. package/gallery/Progress.ui +4 -0
  143. package/gallery/Provenance-deploy.ui +6 -0
  144. package/gallery/Provenance-no-receipt.ui +1 -0
  145. package/gallery/Provenance.ui +1 -0
  146. package/gallery/PullRequest.ui +1 -0
  147. package/gallery/Quiz.ui +6 -0
  148. package/gallery/Radio.ui +5 -0
  149. package/gallery/Rating.ui +5 -0
  150. package/gallery/Roadmap-sprint.ui +8 -0
  151. package/gallery/Roadmap-year.ui +9 -0
  152. package/gallery/Roadmap.ui +12 -0
  153. package/gallery/Row.ui +3 -0
  154. package/gallery/SegmentedBar-status.ui +8 -0
  155. package/gallery/SegmentedBar.ui +8 -0
  156. package/gallery/Select.ui +5 -0
  157. package/gallery/Slider.ui +7 -0
  158. package/gallery/Snippet.ui +3 -0
  159. package/gallery/Sources-chips.ui +4 -0
  160. package/gallery/Sources.ui +8 -0
  161. package/gallery/Sparkline.ui +6 -0
  162. package/gallery/Stack.ui +7 -0
  163. package/gallery/StackTrace-aggregate.ui +1 -0
  164. package/gallery/StackTrace-browser.ui +1 -0
  165. package/gallery/StackTrace-go.ui +1 -0
  166. package/gallery/StackTrace-java.ui +1 -0
  167. package/gallery/StackTrace-python.ui +1 -0
  168. package/gallery/StackTrace-rust.ui +1 -0
  169. package/gallery/StackTrace.ui +1 -0
  170. package/gallery/Status.ui +9 -0
  171. package/gallery/Stepper.ui +5 -0
  172. package/gallery/Steps.ui +8 -0
  173. package/gallery/Swatches-long-names.ui +4 -0
  174. package/gallery/Swatches-picker.ui +5 -0
  175. package/gallery/Swatches.ui +10 -0
  176. package/gallery/Table.ui +7 -0
  177. package/gallery/Tabs.ui +8 -0
  178. package/gallery/Tests-junit.ui +1 -0
  179. package/gallery/Tests-vitest-junit.ui +1 -0
  180. package/gallery/Tests-vitest.ui +1 -0
  181. package/gallery/Tests.ui +9 -0
  182. package/gallery/Text.ui +4 -0
  183. package/gallery/TextArea.ui +5 -0
  184. package/gallery/Threads-ids.ui +1 -0
  185. package/gallery/Threads.ui +1 -0
  186. package/gallery/Timeline.ui +8 -0
  187. package/gallery/Timer.ui +4 -0
  188. package/gallery/Toggle.ui +5 -0
  189. package/gallery/Transcript-text.ui +1 -0
  190. package/gallery/Transcript-untimed.ui +1 -0
  191. package/gallery/Transcript-whisper.ui +1 -0
  192. package/gallery/Transcript.ui +1 -0
  193. package/gallery/Tree.ui +3 -0
  194. package/gallery/Treemap-du.ui +1 -0
  195. package/gallery/Treemap-esbuild.ui +1 -0
  196. package/gallery/Treemap-tree.ui +7 -0
  197. package/gallery/Treemap.ui +18 -0
  198. package/gallery/Walkthrough.ui +9 -0
  199. package/gallery/Waterfall-har.ui +1 -0
  200. package/gallery/Waterfall-trace.ui +1 -0
  201. package/gallery/Waterfall.ui +11 -0
  202. package/gallery/data/acceptance/coil/viewport-lifecycle.json +25 -0
  203. package/gallery/data/acceptance/coil-gate.json +153 -0
  204. package/gallery/data/acceptance/orbit-baseline.json +163 -0
  205. package/gallery/data/acceptance/shots/gate-cell-opened.png +0 -0
  206. package/gallery/data/acceptance/shots/gate-light.png +0 -0
  207. package/gallery/data/acceptance/shots/gate-phone-dark.png +0 -0
  208. package/gallery/data/activity.json +1 -0
  209. package/gallery/data/bb-tasks-orb.json +420 -0
  210. package/gallery/data/biome.json +229 -0
  211. package/gallery/data/compare/status-after.png +0 -0
  212. package/gallery/data/compare/status-before.png +0 -0
  213. package/gallery/data/compare/status-dark-2x.png +0 -0
  214. package/gallery/data/compare/status-rerender.png +0 -0
  215. package/gallery/data/coverage-py.json +1 -0
  216. package/gallery/data/coverage-summary.json +1 -0
  217. package/gallery/data/coverage.out +23 -0
  218. package/gallery/data/deploys.json +1 -0
  219. package/gallery/data/device-runs.json +285 -0
  220. package/gallery/data/du-node-modules.txt +359 -0
  221. package/gallery/data/esbuild-meta.json +292 -0
  222. package/gallery/data/eslint.json +266 -0
  223. package/gallery/data/eslint.sarif +343 -0
  224. package/gallery/data/findings-review.json +76 -0
  225. package/gallery/data/gh-issues-nextjs.json +293 -0
  226. package/gallery/data/gh-pr-checks.json +272 -0
  227. package/gallery/data/gh-prs-nextjs.json +419 -0
  228. package/gallery/data/gh-run-failed.json +1 -0
  229. package/gallery/data/gh-run-passed.json +1 -0
  230. package/gallery/data/gh-run-running.json +1 -0
  231. package/gallery/data/github-release.json +57 -0
  232. package/gallery/data/hyperfine-startup.json +187 -0
  233. package/gallery/data/lcov.info +234 -0
  234. package/gallery/data/logs/media-sync.log +127 -0
  235. package/gallery/data/logs/thumbnailer.jsonl +17 -0
  236. package/gallery/data/logs/vitest-run.log +89 -0
  237. package/gallery/data/npm-audit.json +672 -0
  238. package/gallery/data/npm-outdated.json +179 -0
  239. package/gallery/data/oxlint.json +156 -0
  240. package/gallery/data/pip-outdated.json +68 -0
  241. package/gallery/data/pnpm-outdated.json +58 -0
  242. package/gallery/data/provenance/devicectl-install.json +35 -0
  243. package/gallery/data/provenance/install-timeout.log +4 -0
  244. package/gallery/data/provenance/iphone-install.json +72 -0
  245. package/gallery/data/provenance/iphone-no-receipt.json +57 -0
  246. package/gallery/data/provenance/live-home.jpg +0 -0
  247. package/gallery/data/provenance/live-probe.txt +10 -0
  248. package/gallery/data/provenance/xcodebuild-2811.log +19 -0
  249. package/gallery/data/provenance/xcodebuild-4712.log +19 -0
  250. package/gallery/data/pytest-junit.xml +64 -0
  251. package/gallery/data/python-org-trace.json +35 -0
  252. package/gallery/data/python-org.har +31 -0
  253. package/gallery/data/retry.ts +13 -0
  254. package/gallery/data/ruff.json +261 -0
  255. package/gallery/data/src/durations.py +63 -0
  256. package/gallery/data/src/json-logic.ts +156 -0
  257. package/gallery/data/src/mapMath.ts +135 -0
  258. package/gallery/data/src/semver/bump.go +73 -0
  259. package/gallery/data/traces/browser-error.json +5 -0
  260. package/gallery/data/traces/go/main.go +27 -0
  261. package/gallery/data/traces/go-panic.txt +16 -0
  262. package/gallery/data/traces/java-caused-by.txt +15 -0
  263. package/gallery/data/traces/node-aggregate.txt +30 -0
  264. package/gallery/data/traces/node-cause.txt +21 -0
  265. package/gallery/data/traces/py/deploy.json +5 -0
  266. package/gallery/data/traces/py/deploy.py +17 -0
  267. package/gallery/data/traces/py/settings.py +19 -0
  268. package/gallery/data/traces/python-chain.txt +30 -0
  269. package/gallery/data/traces/rust/main.rs +11 -0
  270. package/gallery/data/traces/rust-panic.txt +16 -0
  271. package/gallery/data/traces/shop/cart.mjs +19 -0
  272. package/gallery/data/traces/shop/pricing.mjs +12 -0
  273. package/gallery/data/traces/shop/status.mjs +9 -0
  274. package/gallery/data/traces/web/app.js +11 -0
  275. package/gallery/data/traces/web/index.html +1 -0
  276. package/gallery/data/traces/web/search.js +10 -0
  277. package/gallery/data/transcript/call-noisy.wav +0 -0
  278. package/gallery/data/transcript/call.json +362 -0
  279. package/gallery/data/transcript/call.wav +0 -0
  280. package/gallery/data/transcript/mlx-whisper-tiny.en-noisy.json +1 -0
  281. package/gallery/data/transcript/whisper-cpp-base.en-noisy.json +658 -0
  282. package/gallery/data/transcript/whisper-cpp-base.en.json +658 -0
  283. package/gallery/data/transcript/whisper-cpp-base.en.srt +16 -0
  284. package/gallery/data/transcript/whisper-cpp-tiny.en-noisy.json +619 -0
  285. package/gallery/data/tsc.txt +4 -0
  286. package/gallery/data/vitest-bench.json +119 -0
  287. package/gallery/data/vitest-junit.xml +133 -0
  288. package/gallery/data/vitest-report.json +612 -0
  289. package/gallery/images/bell.svg +10 -0
  290. package/gallery/images/bike.svg +47 -0
  291. package/gallery/images/footer-planet.webp +0 -0
  292. package/gallery/images/front-light.svg +13 -0
  293. package/gallery/images/golden-hour.webp +0 -0
  294. package/gallery/images/option-flowers.webp +0 -0
  295. package/gallery/images/option-hilltop.webp +0 -0
  296. package/gallery/images/option-meadow.webp +0 -0
  297. package/gallery/images/option-sky.webp +0 -0
  298. package/gallery/images/u-lock.svg +10 -0
  299. package/index.client.tsx +164 -0
  300. package/index.server.ts +216 -0
  301. package/package.json +50 -4
  302. package/paseo-plugin.json +14 -0
  303. package/server/data-source.ts +407 -0
  304. package/server/project.ts +981 -0
  305. package/server/web/address.ts +57 -0
  306. package/server/web/cache.ts +85 -0
  307. package/server/web/commons.ts +126 -0
  308. package/server/web/geocode.ts +40 -0
  309. package/server/web/image.ts +35 -0
  310. package/server/web/lookups.ts +248 -0
  311. package/server/web/net.ts +210 -0
  312. package/shared/core/acceptance.ts +744 -0
  313. package/shared/core/ast.ts +76 -0
  314. package/shared/core/benchmark.ts +743 -0
  315. package/shared/core/catalog.ts +733 -0
  316. package/shared/core/compare.ts +225 -0
  317. package/shared/core/coverage.ts +456 -0
  318. package/shared/core/data.ts +334 -0
  319. package/shared/core/decisions.ts +527 -0
  320. package/shared/core/dependencies.ts +604 -0
  321. package/shared/core/diagnostics.ts +24 -0
  322. package/shared/core/environments.ts +212 -0
  323. package/shared/core/evaluate.ts +737 -0
  324. package/shared/core/fields.ts +66 -0
  325. package/shared/core/findings.ts +1172 -0
  326. package/shared/core/form.ts +69 -0
  327. package/shared/core/index.ts +44 -0
  328. package/shared/core/junit.ts +252 -0
  329. package/shared/core/lexer.ts +275 -0
  330. package/shared/core/logs.ts +496 -0
  331. package/shared/core/markdown.ts +2374 -0
  332. package/shared/core/parser.ts +355 -0
  333. package/shared/core/perf-units.ts +228 -0
  334. package/shared/core/pipeline.ts +679 -0
  335. package/shared/core/project.ts +472 -0
  336. package/shared/core/provenance.ts +725 -0
  337. package/shared/core/stack-trace.ts +718 -0
  338. package/shared/core/streaming.ts +136 -0
  339. package/shared/core/test-results.ts +562 -0
  340. package/shared/core/thread-activity.ts +203 -0
  341. package/shared/core/tracking.ts +887 -0
  342. package/shared/core/transcript.ts +1364 -0
  343. package/shared/core/treemap.ts +446 -0
  344. package/shared/core/types.ts +70 -0
  345. package/shared/core/validate.ts +574 -0
  346. package/shared/core/waterfall.ts +536 -0
  347. package/shared/diagram/flowchart.ts +205 -0
  348. package/shared/diagram/layout.ts +440 -0
  349. package/shared/diagram/markdown.ts +135 -0
  350. package/shared/diagram/sequence.ts +122 -0
  351. package/shared/diagram/zoom.ts +114 -0
  352. package/shared/fallback.ts +120 -0
  353. package/shared/genui.test.ts +122 -0
  354. package/shared/instructions.ts +144 -0
  355. package/shared/logic/calendarMath.ts +130 -0
  356. package/shared/logic/chartMath.ts +289 -0
  357. package/shared/logic/choose-logic.ts +196 -0
  358. package/shared/logic/compare-logic.ts +270 -0
  359. package/shared/logic/content-model.ts +227 -0
  360. package/shared/logic/environments-logic.ts +53 -0
  361. package/shared/logic/explain-logic.ts +315 -0
  362. package/shared/logic/fieldLogic.ts +138 -0
  363. package/shared/logic/gallery-search.ts +71 -0
  364. package/shared/logic/icons.ts +322 -0
  365. package/shared/logic/json-logic.ts +153 -0
  366. package/shared/logic/mapMath.ts +133 -0
  367. package/shared/logic/project-logic.ts +39 -0
  368. package/shared/logic/references-model.ts +201 -0
  369. package/shared/logic/sketch.ts +165 -0
  370. package/shared/logic/supersession.ts +77 -0
  371. package/shared/logic/values.ts +133 -0
  372. package/shared/logic/widget-logic.ts +163 -0
  373. package/shared/rpc.ts +252 -0
  374. package/shared/segment.ts +81 -0
  375. package/shared/settings.ts +12 -0
@@ -0,0 +1,604 @@
1
+ // What `Dependencies` shows: outdated packages (`npm outdated --json`, `pnpm
2
+ // outdated --format json`, `pip list --outdated --format=json`, or a list
3
+ // `{name, current, latest, wanted?, type?, severity?, note?}`) grouped by
4
+ // the semver bump to their latest version, and vulnerable packages
5
+ // (`npm audit --json` v2, `pnpm audit --json`'s advisories) by severity.
6
+ // `{outdated, audit}` holds one of each. Also the version arithmetic the
7
+ // renderer and the markdown share. Pure: no React, no DOM.
8
+ import type { Value } from "./types";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Versions
12
+
13
+ export type Bump = "major" | "minor" | "patch" | "prerelease";
14
+
15
+ interface ParsedVersion {
16
+ /** The release numbers: 1.2.3 → [1, 2, 3]; Python's 2.10 → [2, 10]. */
17
+ readonly release: readonly number[];
18
+ /** What follows the release, lowercased, without its separator: "beta.2", "rc1"; "" for a release. */
19
+ readonly pre: string;
20
+ /** A post-release (PEP 440 `.post1`), which sorts after its release. */
21
+ readonly post: boolean;
22
+ }
23
+
24
+ /** `1.2.3`, `v1.2.3-beta.1+build`, `2.10`, `1.0rc1`, `2.0.0.post1`; null for ranges, tags and git URLs. */
25
+ export function parseVersion(text: string): ParsedVersion | null {
26
+ const match = /^\s*[v=]?(\d+(?:\.\d+)*)(?:[-.]?([0-9a-z][0-9a-z.-]*))?(?:\+[0-9a-z.-]+)?\s*$/i.exec(text);
27
+ if (match === null) return null;
28
+ const rest = (match[2] ?? "").toLowerCase();
29
+ return { release: match[1].split(".").map(Number), pre: /^post\d*/.test(rest) ? "" : rest, post: /^post\d*/.test(rest) };
30
+ }
31
+
32
+ /** Compares pre-release identifiers the semver way: numbers numerically, below words; fewer fields first. */
33
+ function comparePre(a: string, b: string): number {
34
+ if (a === b) return 0;
35
+ if (a === "") return 1;
36
+ if (b === "") return -1;
37
+ const x = a.split(/[.-]|(?<=\d)(?=[a-z])|(?<=[a-z])(?=\d)/);
38
+ const y = b.split(/[.-]|(?<=\d)(?=[a-z])|(?<=[a-z])(?=\d)/);
39
+ for (let index = 0; index < Math.max(x.length, y.length); index++) {
40
+ if (x[index] === undefined) return -1;
41
+ if (y[index] === undefined) return 1;
42
+ const m = /^\d+$/.test(x[index]);
43
+ const n = /^\d+$/.test(y[index]);
44
+ if (m && n && Number(x[index]) !== Number(y[index])) return Number(x[index]) - Number(y[index]);
45
+ if (m !== n) return m ? -1 : 1;
46
+ if (!m && x[index] !== y[index]) return x[index] < y[index] ? -1 : 1;
47
+ }
48
+ return 0;
49
+ }
50
+
51
+ /** Negative when `a` is older than `b`, 0 when equal; null when either isn't a version. */
52
+ export function compareVersions(a: string, b: string): number | null {
53
+ const x = parseVersion(a);
54
+ const y = parseVersion(b);
55
+ if (x === null || y === null) return null;
56
+ for (let index = 0; index < Math.max(x.release.length, y.release.length); index++) {
57
+ const difference = (x.release[index] ?? 0) - (y.release[index] ?? 0);
58
+ if (difference !== 0) return difference;
59
+ }
60
+ return comparePre(x.pre, y.pre) || Number(x.post) - Number(y.post);
61
+ }
62
+
63
+ /**
64
+ * The part of the version that changes from `from` to `to`: the first
65
+ * release number that differs (the third and later are all "patch"), else
66
+ * "prerelease" when only the pre-release or post-release differs; null when
67
+ * they are equal or either isn't a version.
68
+ */
69
+ export function versionBump(from: string, to: string): Bump | null {
70
+ const x = parseVersion(from);
71
+ const y = parseVersion(to);
72
+ if (x === null || y === null) return null;
73
+ for (let index = 0; index < Math.max(x.release.length, y.release.length); index++) {
74
+ if ((x.release[index] ?? 0) !== (y.release[index] ?? 0)) return index === 0 ? "major" : index === 1 ? "minor" : "patch";
75
+ }
76
+ return x.pre !== y.pre || x.post !== y.post ? "prerelease" : null;
77
+ }
78
+
79
+ /**
80
+ * `to` cut where it starts to differ from `from`, at a field boundary:
81
+ * 4.17.15 → 4.18.1 is ["4.", "18.1"], 1.0.0-beta.1 → 1.0.0-beta.3 is
82
+ * ["1.0.0-beta.", "3"]. Everything is "changed" when the first field differs.
83
+ */
84
+ export function splitChange(from: string, to: string): readonly [same: string, changed: string] {
85
+ const text = to.trim();
86
+ const fields = (version: string) => version.match(/[0-9]+|[a-z]+|[^0-9a-z]+/gi) ?? [];
87
+ const x = fields(from.trim());
88
+ const y = fields(text);
89
+ let same = 0;
90
+ let length = 0;
91
+ for (let index = 0; index < y.length; index++) {
92
+ if (x[index] !== y[index]) return [text.slice(0, same), text.slice(same)];
93
+ length += y[index].length;
94
+ // Cut after a separator only, so a field is emphasised whole.
95
+ if (!/[0-9a-z]/i.test(y[index])) same = length;
96
+ }
97
+ // `to` is `from` or a prefix of it: nothing in `to` changed.
98
+ return [text, ""];
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // The report
103
+
104
+ export type DependencyType = "prod" | "dev" | "optional" | "peer";
105
+
106
+ export type Severity = "critical" | "high" | "moderate" | "low" | "info";
107
+
108
+ export const SEVERITIES: readonly Severity[] = ["critical", "high", "moderate", "low", "info"];
109
+
110
+ export interface OutdatedPackage {
111
+ readonly name: string;
112
+ /** Installed version; null when the package is not installed. */
113
+ readonly current: string | null;
114
+ /** The newest version the manifest's range allows; null when the tool doesn't say. */
115
+ readonly wanted: string | null;
116
+ readonly latest: string | null;
117
+ readonly type: DependencyType | null;
118
+ /** current → latest; null when not installed or a version is not a plain version (a tag, git URL). */
119
+ readonly bump: Bump | null;
120
+ /** A 0.x minor bump: breaking under semver, so not part of "patch and minor". */
121
+ readonly breaking: boolean;
122
+ /** latest is older than current (a pre-release or `next` tag is installed). */
123
+ readonly ahead: boolean;
124
+ readonly deprecated: boolean;
125
+ /** The workspace or package that depends on it, where the tool names several. */
126
+ readonly dependent: string | null;
127
+ readonly severity: Severity | null;
128
+ readonly note: string | null;
129
+ /** The package's page: its registry entry, or the list's own `url`. */
130
+ readonly url: string | null;
131
+ }
132
+
133
+ export interface Advisory {
134
+ readonly title: string;
135
+ readonly url: string | null;
136
+ readonly severity: Severity | null;
137
+ /** The versions it affects. */
138
+ readonly range: string | null;
139
+ }
140
+
141
+ export interface Fix {
142
+ /** The package to change: the vulnerable one, or a direct dependency that pulls in a fixed version. */
143
+ readonly name: string | null;
144
+ readonly version: string | null;
145
+ /** The fix needs a major update (npm's `isSemVerMajor`). */
146
+ readonly major: boolean;
147
+ }
148
+
149
+ export interface VulnerablePackage {
150
+ readonly name: string;
151
+ readonly severity: Severity;
152
+ /** The vulnerable versions. */
153
+ readonly range: string | null;
154
+ /** A dependency of the project's own; null when the report doesn't say (a hand-written list). */
155
+ readonly direct: boolean | null;
156
+ /** The vulnerable packages it is affected through (npm's string `via` entries). */
157
+ readonly through: readonly string[];
158
+ /** The project's dependencies that pull it in (pnpm's dependency paths). */
159
+ readonly pulledInBy: readonly string[];
160
+ readonly advisories: readonly Advisory[];
161
+ /** null: no fix available. */
162
+ readonly fix: Fix | null;
163
+ }
164
+
165
+ export type DependencySource = "npm" | "pnpm" | "pip" | "list";
166
+
167
+ export interface DependencyReport {
168
+ /** The tool behind the outdated part; null when there is none. */
169
+ readonly source: DependencySource | null;
170
+ /** The tool behind the audit part ("npm audit", "pnpm audit"); null when there is none. */
171
+ readonly auditSource: string | null;
172
+ readonly outdated: readonly OutdatedPackage[];
173
+ readonly vulnerable: readonly VulnerablePackage[];
174
+ /** Vulnerabilities per severity: the audit's own totals where it gives them, else counted. */
175
+ readonly severities: Readonly<Record<Severity, number>>;
176
+ /** How many dependencies the audit looked at, where it says. */
177
+ readonly scanned: number | null;
178
+ }
179
+
180
+ export type DependencyResult = { readonly ok: true; readonly report: DependencyReport } | { readonly ok: false; readonly error: string };
181
+
182
+ const SHAPE_HINT =
183
+ "Dependencies needs npm outdated --json, pnpm outdated --format json, npm audit --json, pip list --outdated --format=json, or [{name, current, latest}]";
184
+
185
+ type Fields = Readonly<Record<string, Value | undefined>>;
186
+
187
+ function fieldsOf(value: Value | undefined): Fields | null {
188
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
189
+ if ("kind" in value && (value.kind === "node" || value.kind === "action" || value.kind === "binding")) return null;
190
+ return value as Fields;
191
+ }
192
+
193
+ function str(value: Value | undefined): string | null {
194
+ if (typeof value === "string") return value.trim() === "" ? null : value.trim();
195
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
196
+ return null;
197
+ }
198
+
199
+ const TYPE_WORDS: Readonly<Record<string, DependencyType>> = {
200
+ dependencies: "prod",
201
+ dependency: "prod",
202
+ prod: "prod",
203
+ production: "prod",
204
+ runtime: "prod",
205
+ main: "prod",
206
+ devdependencies: "dev",
207
+ devdependency: "dev",
208
+ dev: "dev",
209
+ development: "dev",
210
+ optionaldependencies: "optional",
211
+ optional: "optional",
212
+ peerdependencies: "peer",
213
+ peer: "peer",
214
+ };
215
+
216
+ const SEVERITY_WORDS: Readonly<Record<string, Severity>> = {
217
+ critical: "critical",
218
+ high: "high",
219
+ moderate: "moderate",
220
+ medium: "moderate",
221
+ low: "low",
222
+ info: "info",
223
+ informational: "info",
224
+ none: "info",
225
+ };
226
+
227
+ export function severityOf(value: Value | undefined): Severity | null {
228
+ const word = str(value)?.toLowerCase().replace(/[\s_-]+/g, "") ?? "";
229
+ return Object.hasOwn(SEVERITY_WORDS, word) ? SEVERITY_WORDS[word] : null;
230
+ }
231
+
232
+ function registryUrl(source: DependencySource, name: string): string | null {
233
+ if (source === "pip") return `https://pypi.org/project/${encodeURIComponent(name)}/`;
234
+ if (source === "list") return null;
235
+ return `https://www.npmjs.com/package/${name.split("/").map(encodeURIComponent).join("/").replace(/^%40/, "@")}`;
236
+ }
237
+
238
+ function outdatedPackage(
239
+ source: DependencySource,
240
+ name: string,
241
+ fields: {
242
+ current: string | null;
243
+ wanted: string | null;
244
+ latest: string | null;
245
+ type: Value | undefined;
246
+ deprecated?: boolean;
247
+ dependent?: string | null;
248
+ severity?: Value;
249
+ note?: string | null;
250
+ url?: string | null;
251
+ },
252
+ ): OutdatedPackage {
253
+ const { current, wanted, latest } = fields;
254
+ const typeWord = str(fields.type)?.toLowerCase().replace(/[\s_-]+/g, "") ?? "";
255
+ const bump = current === null || latest === null ? null : versionBump(current, latest);
256
+ const from = current === null ? null : parseVersion(current);
257
+ const order = current === null || latest === null ? null : compareVersions(current, latest);
258
+ return {
259
+ name,
260
+ current,
261
+ wanted,
262
+ latest,
263
+ type: Object.hasOwn(TYPE_WORDS, typeWord) ? TYPE_WORDS[typeWord] : null,
264
+ bump,
265
+ breaking: bump === "minor" && from !== null && from.release[0] === 0,
266
+ ahead: order !== null && order > 0,
267
+ deprecated: fields.deprecated === true,
268
+ dependent: fields.dependent ?? null,
269
+ severity: severityOf(fields.severity),
270
+ note: fields.note ?? null,
271
+ url: fields.url ?? registryUrl(source, name),
272
+ };
273
+ }
274
+
275
+ /** `npm outdated --json` (values may be lists, one per dependent workspace) or `pnpm outdated --format json`. */
276
+ function fromOutdatedObject(report: Fields): { source: DependencySource; outdated: OutdatedPackage[] } {
277
+ const entries = Object.entries(report);
278
+ const pnpm = entries.some(([, value]) => {
279
+ const fields = fieldsOf(value);
280
+ return fields !== null && (fields.dependencyType !== undefined || fields.isDeprecated !== undefined);
281
+ });
282
+ const source: DependencySource = pnpm ? "pnpm" : "npm";
283
+ const outdated: OutdatedPackage[] = [];
284
+ for (const [name, value] of entries) {
285
+ const records = Array.isArray(value) ? value : [value];
286
+ for (const record of records) {
287
+ const fields = fieldsOf(record);
288
+ if (fields === null) continue;
289
+ const dependents = Array.isArray(fields.dependentPackages)
290
+ ? fields.dependentPackages.flatMap((item) => str(fieldsOf(item)?.name) ?? [])
291
+ : [];
292
+ outdated.push(
293
+ outdatedPackage(source, name, {
294
+ // npm writes no `current` for a package that isn't installed (its table says MISSING).
295
+ current: str(fields.current),
296
+ wanted: str(fields.wanted),
297
+ latest: str(fields.latest),
298
+ type: fields.type ?? fields.dependencyType,
299
+ deprecated: fields.isDeprecated === true,
300
+ dependent: Array.isArray(value) && value.length > 1 ? str(fields.dependent) : dependents.length > 0 ? dependents.join(", ") : null,
301
+ }),
302
+ );
303
+ }
304
+ }
305
+ return { source, outdated };
306
+ }
307
+
308
+ /** `npm audit --json` (auditReportVersion 2): `vulnerabilities` by package. */
309
+ function fromNpmAudit(report: Fields): AuditPart {
310
+ const vulnerable: VulnerablePackage[] = [];
311
+ for (const [key, value] of Object.entries(fieldsOf(report.vulnerabilities) ?? {})) {
312
+ const fields = fieldsOf(value);
313
+ const severity = severityOf(fields?.severity);
314
+ if (fields === null || severity === null) continue;
315
+ const via = Array.isArray(fields.via) ? fields.via : [];
316
+ const advisories = via.flatMap((item): Advisory[] => {
317
+ const advisory = fieldsOf(item);
318
+ const title = advisory === null ? null : str(advisory.title);
319
+ if (advisory === null || title === null) return [];
320
+ return [{ title, url: webLink(advisory.url), severity: severityOf(advisory.severity), range: str(advisory.range) }];
321
+ });
322
+ const fixAvailable = fields.fixAvailable;
323
+ const fixFields = fieldsOf(fixAvailable);
324
+ const fix: Fix | null =
325
+ fixFields !== null
326
+ ? { name: str(fixFields.name), version: str(fixFields.version), major: fixFields.isSemVerMajor === true }
327
+ : fixAvailable === true
328
+ ? { name: null, version: null, major: false }
329
+ : null;
330
+ vulnerable.push({
331
+ name: str(fields.name) ?? key,
332
+ severity,
333
+ range: str(fields.range),
334
+ direct: fields.isDirect === true,
335
+ through: via.flatMap((item) => (typeof item === "string" ? [item] : [])),
336
+ pulledInBy: [],
337
+ advisories,
338
+ fix,
339
+ });
340
+ }
341
+ const metadata = fieldsOf(report.metadata);
342
+ return { vulnerable, severities: severityTotals(fieldsOf(metadata?.vulnerabilities)), scanned: scannedCount(metadata) };
343
+ }
344
+
345
+ /** `pnpm audit --json` (and npm 6's): `advisories` by id, one per advisory, grouped here by package. */
346
+ function fromAdvisories(report: Fields): AuditPart {
347
+ const byName = new Map<string, { severity: Severity; ranges: Set<string>; direct: boolean; paths: Set<string>; advisories: Advisory[]; patched: Set<string> }>();
348
+ for (const value of Object.values(fieldsOf(report.advisories) ?? {})) {
349
+ const fields = fieldsOf(value);
350
+ const name = fields === null ? null : str(fields.module_name);
351
+ const severity = severityOf(fields?.severity);
352
+ const title = fields === null ? null : str(fields.title);
353
+ if (fields === null || name === null || severity === null || title === null) continue;
354
+ const entry = byName.get(name) ?? { severity, ranges: new Set(), direct: false, paths: new Set(), advisories: [], patched: new Set() };
355
+ if (SEVERITIES.indexOf(severity) < SEVERITIES.indexOf(entry.severity)) entry.severity = severity;
356
+ const range = str(fields.vulnerable_versions);
357
+ if (range !== null) entry.ranges.add(range);
358
+ const patched = str(fields.patched_versions);
359
+ if (patched !== null) entry.patched.add(patched);
360
+ for (const finding of Array.isArray(fields.findings) ? fields.findings : []) {
361
+ for (const path of Array.isArray(fieldsOf(finding)?.paths) ? (fieldsOf(finding)!.paths as readonly Value[]) : []) {
362
+ const text = str(path);
363
+ if (text === null) continue;
364
+ // ".>hono" is the project's own dependency; ".>tsx>esbuild" comes through tsx.
365
+ const hops = text.split(">").filter((hop) => hop !== "" && hop !== ".");
366
+ if (hops.length <= 1) entry.direct = true;
367
+ else entry.paths.add(hops.slice(0, -1).join(" › "));
368
+ }
369
+ }
370
+ entry.advisories.push({ title, url: webLink(fields.url), severity, range });
371
+ byName.set(name, entry);
372
+ }
373
+ const vulnerable = [...byName].map(([name, entry]): VulnerablePackage => {
374
+ // "<0.0.0" is the advisory format's "no patched version".
375
+ const patched = [...entry.patched].filter((range) => range !== "<0.0.0");
376
+ return {
377
+ name,
378
+ severity: entry.severity,
379
+ range: entry.ranges.size === 0 ? null : [...entry.ranges].join(" || "),
380
+ direct: entry.direct,
381
+ through: [],
382
+ pulledInBy: [...entry.paths],
383
+ advisories: entry.advisories,
384
+ fix: patched.length === 0 ? null : { name, version: patched.length === 1 ? patched[0] : null, major: false },
385
+ };
386
+ });
387
+ const metadata = fieldsOf(report.metadata);
388
+ return { vulnerable, severities: severityTotals(fieldsOf(metadata?.vulnerabilities)), scanned: scannedCount(metadata) };
389
+ }
390
+
391
+ function webLink(value: Value | undefined): string | null {
392
+ const text = str(value);
393
+ return text !== null && /^https?:\/\/\S+$/i.test(text) ? text : null;
394
+ }
395
+
396
+ function severityTotals(fields: Fields | null): Record<Severity, number> | null {
397
+ if (fields === null) return null;
398
+ const totals = { critical: 0, high: 0, moderate: 0, low: 0, info: 0 };
399
+ for (const severity of SEVERITIES) {
400
+ const count = fields[severity];
401
+ if (typeof count === "number" && Number.isFinite(count) && count >= 0) totals[severity] = count;
402
+ }
403
+ return totals;
404
+ }
405
+
406
+ function scannedCount(metadata: Fields | null): number | null {
407
+ const dependencies = metadata?.dependencies;
408
+ if (typeof dependencies === "number") return dependencies + (typeof metadata!.devDependencies === "number" ? metadata!.devDependencies : 0);
409
+ const total = fieldsOf(dependencies)?.total ?? metadata?.totalDependencies;
410
+ return typeof total === "number" && Number.isFinite(total) ? total : null;
411
+ }
412
+
413
+ function isAudit(fields: Fields): boolean {
414
+ return (fieldsOf(fields.vulnerabilities) !== null && (fields.auditReportVersion !== undefined || fields.metadata !== undefined)) || fieldsOf(fields.advisories) !== null;
415
+ }
416
+
417
+ /** A list item: pip's `{name, version, latest_version}` or `{name, current, latest, wanted?, type?, severity?, note?, url?}`. */
418
+ function fromList(values: readonly Value[]): OutdatedPart {
419
+ const pip = values.some((value) => fieldsOf(value)?.latest_version !== undefined);
420
+ const source: DependencySource = pip ? "pip" : "list";
421
+ const outdated: OutdatedPackage[] = [];
422
+ for (const value of values) {
423
+ const fields = fieldsOf(value);
424
+ const name = fields === null ? null : (str(fields.name) ?? str(fields.package));
425
+ if (fields === null || name === null) continue;
426
+ outdated.push(
427
+ outdatedPackage(source, name, {
428
+ current: str(fields.current) ?? str(fields.version) ?? str(fields.installed),
429
+ wanted: str(fields.wanted),
430
+ latest: str(fields.latest) ?? str(fields.latest_version),
431
+ type: fields.type,
432
+ severity: fields.severity ?? undefined,
433
+ note: str(fields.note),
434
+ url: webLink(fields.url),
435
+ }),
436
+ );
437
+ }
438
+ return { source, outdated };
439
+ }
440
+
441
+ interface OutdatedPart {
442
+ readonly source: DependencySource;
443
+ readonly outdated: readonly OutdatedPackage[];
444
+ }
445
+
446
+ /** An audit's vulnerable packages and its own totals. */
447
+ interface AuditPart {
448
+ readonly vulnerable: readonly VulnerablePackage[];
449
+ readonly severities: Record<Severity, number> | null;
450
+ readonly scanned: number | null;
451
+ }
452
+
453
+ /** An outdated report or list, or why it can't be read. */
454
+ function readOutdated(part: Value | undefined): OutdatedPart | string {
455
+ if (Array.isArray(part)) {
456
+ const list = fromList(part);
457
+ return list.outdated.length === 0 && part.length > 0 ? "Dependencies items need a name" : list;
458
+ }
459
+ const object = fieldsOf(part);
460
+ return object === null ? SHAPE_HINT : fromOutdatedObject(object);
461
+ }
462
+
463
+ /** The report `value` describes, or why it can't be read. */
464
+ export function readDependencies(value: Value | undefined): DependencyResult {
465
+ const fields = fieldsOf(value);
466
+ const combined = fields !== null && (fields.outdated !== undefined || fields.audit !== undefined) && !isPackageMap(fields);
467
+ const auditFields = combined ? fieldsOf(fields.audit) : fields !== null && isAudit(fields) ? fields : null;
468
+ if (combined && fields.audit !== undefined && (auditFields === null || !isAudit(auditFields))) {
469
+ return { ok: false, error: "Dependencies' audit needs npm audit --json or pnpm audit --json" };
470
+ }
471
+ const outdated = combined
472
+ ? fields.outdated === undefined
473
+ ? null
474
+ : readOutdated(fields.outdated)
475
+ : auditFields === null
476
+ ? readOutdated(value)
477
+ : null;
478
+ if (typeof outdated === "string") return { ok: false, error: outdated };
479
+ const npmAudit = auditFields !== null && fieldsOf(auditFields.vulnerabilities) !== null;
480
+ const audit = auditFields === null ? null : npmAudit ? fromNpmAudit(auditFields) : fromAdvisories(auditFields);
481
+ const packages = outdated?.outdated ?? [];
482
+ // Without an audit, a list's own severities are its vulnerabilities, fixed by its latest version.
483
+ const listed = packages.flatMap((item): VulnerablePackage[] =>
484
+ item.severity === null
485
+ ? []
486
+ : [
487
+ {
488
+ name: item.name,
489
+ severity: item.severity,
490
+ range: null,
491
+ direct: null,
492
+ through: [],
493
+ pulledInBy: [],
494
+ advisories: item.note === null ? [] : [{ title: item.note, url: null, severity: item.severity, range: null }],
495
+ fix: item.latest === null || item.ahead ? null : { name: item.name, version: item.latest, major: item.bump === "major" || item.breaking },
496
+ },
497
+ ],
498
+ );
499
+ const vulnerable = audit?.vulnerable ?? listed;
500
+ const counted = { critical: 0, high: 0, moderate: 0, low: 0, info: 0 };
501
+ for (const item of vulnerable) counted[item.severity]++;
502
+ return {
503
+ ok: true,
504
+ report: {
505
+ source: outdated?.source ?? null,
506
+ auditSource: audit === null ? null : npmAudit ? "npm audit" : "pnpm audit",
507
+ outdated: packages,
508
+ vulnerable: [...vulnerable].sort(
509
+ (a, b) => SEVERITIES.indexOf(a.severity) - SEVERITIES.indexOf(b.severity) || Number(b.direct === true) - Number(a.direct === true) || a.name.localeCompare(b.name),
510
+ ),
511
+ severities: audit?.severities ?? counted,
512
+ scanned: audit?.scanned ?? null,
513
+ },
514
+ };
515
+ }
516
+
517
+ /** npm outdated's map could hold packages named "outdated" or "audit": a map's values all look like packages. */
518
+ function isPackageMap(fields: Fields): boolean {
519
+ return Object.values(fields).every((value) => {
520
+ const record = fieldsOf(Array.isArray(value) ? value[0] : value);
521
+ return record !== null && (record.latest !== undefined || record.wanted !== undefined);
522
+ });
523
+ }
524
+
525
+ // ---------------------------------------------------------------------------
526
+ // Grouping and the words the renderer and the markdown share
527
+
528
+ export type OutdatedGroupKey = Bump | "missing" | "other";
529
+
530
+ export const GROUP_ORDER: readonly OutdatedGroupKey[] = ["major", "minor", "patch", "prerelease", "missing", "other"];
531
+
532
+ export const GROUP_LABEL: Record<OutdatedGroupKey, string> = {
533
+ major: "Major",
534
+ minor: "Minor",
535
+ patch: "Patch",
536
+ prerelease: "Pre-release",
537
+ missing: "Not installed",
538
+ other: "Other",
539
+ };
540
+
541
+ export interface OutdatedGroup {
542
+ readonly key: OutdatedGroupKey;
543
+ readonly packages: readonly OutdatedPackage[];
544
+ }
545
+
546
+ /** Packages by bump in GROUP_ORDER, each group by name; empty groups left out. */
547
+ export function groupOutdated(packages: readonly OutdatedPackage[]): OutdatedGroup[] {
548
+ const groups = new Map<OutdatedGroupKey, OutdatedPackage[]>(GROUP_ORDER.map((key) => [key, []]));
549
+ for (const item of packages) groups.get(item.current === null ? "missing" : (item.bump ?? "other"))!.push(item);
550
+ return GROUP_ORDER.flatMap((key) => {
551
+ const list = groups.get(key)!;
552
+ return list.length === 0 ? [] : [{ key, packages: [...list].sort((a, b) => a.name.localeCompare(b.name)) }];
553
+ });
554
+ }
555
+
556
+ /** The highest version short of a major (or 0.x minor) update: latest when that is a minor or patch bump, else wanted when it is. */
557
+ export function safeTarget(item: OutdatedPackage): string | null {
558
+ if (item.current === null) return null;
559
+ if (item.latest !== null && !item.ahead && (item.bump === "minor" || item.bump === "patch") && !item.breaking) return item.latest;
560
+ if (item.wanted === null || item.wanted === item.current) return null;
561
+ const bump = versionBump(item.current, item.wanted);
562
+ const order = compareVersions(item.current, item.wanted);
563
+ return (bump === "minor" || bump === "patch") && order !== null && order < 0 && !(bump === "minor" && parseVersion(item.current)?.release[0] === 0)
564
+ ? item.wanted
565
+ : null;
566
+ }
567
+
568
+ /** What "Update patch and minor" sends; null when nothing qualifies. */
569
+ export function updateMessage(report: DependencyReport): string | null {
570
+ const targets = report.outdated.flatMap((item) => {
571
+ const target = safeTarget(item);
572
+ return target === null ? [] : [`${item.name} ${item.current} → ${target}`];
573
+ });
574
+ if (targets.length === 0) return null;
575
+ const shown = targets.slice(0, 25);
576
+ const more = targets.length > shown.length ? `, and ${targets.length - shown.length} more` : "";
577
+ return `Update these dependencies to their latest patch and minor versions, then run the tests: ${shown.join(", ")}${more}. Leave the major updates for now.`;
578
+ }
579
+
580
+ /** "4 critical, 9 high, 4 moderate, 3 low": severest first, zero counts left out. */
581
+ export function severityText(severities: Readonly<Record<Severity, number>>): string {
582
+ return SEVERITIES.filter((severity) => severities[severity] > 0)
583
+ .map((severity) => `${severities[severity]} ${severity}`)
584
+ .join(", ");
585
+ }
586
+
587
+ /** What "Fix vulnerabilities" sends; null when there are none. */
588
+ export function fixMessage(report: DependencyReport): string | null {
589
+ const total = SEVERITIES.reduce((sum, severity) => sum + report.severities[severity], 0);
590
+ if (total === 0) return null;
591
+ const tool = report.auditSource ?? "reported";
592
+ const majors = report.vulnerable.filter((item) => item.fix?.major === true);
593
+ const unfixable = report.vulnerable.filter((item) => item.fix === null);
594
+ const listed = report.auditSource === null ? report.outdated.filter((item) => item.severity !== null).map((item) => item.name) : [];
595
+ return [
596
+ `Fix the ${tool} vulnerabilities (${severityText(report.severities)})${listed.length > 0 ? ` in ${listed.join(", ")}` : ""}.`,
597
+ majors.length > 0
598
+ ? `These fixes need a major update, so check their breaking changes: ${majors.map((item) => `${item.fix!.name ?? item.name}${item.fix!.version === null ? "" : ` ${item.fix!.version}`}`).filter((text, index, all) => all.indexOf(text) === index).join(", ")}.`
599
+ : "",
600
+ unfixable.length > 0 ? `No fix is available yet for ${unfixable.map((item) => item.name).join(", ")}.` : "",
601
+ ]
602
+ .filter((part) => part !== "")
603
+ .join(" ");
604
+ }
@@ -0,0 +1,24 @@
1
+ import type { Diagnostic } from "./types";
2
+
3
+ /** A 1-based source position. Tokens and AST nodes carry one. */
4
+ export interface Pos {
5
+ readonly line: number;
6
+ readonly column: number;
7
+ }
8
+
9
+ const MAX_DIAGNOSTICS = 20;
10
+
11
+ /** Collects up to 20 diagnostics, dropping exact duplicates. */
12
+ export class DiagnosticList {
13
+ readonly items: Diagnostic[] = [];
14
+ hasErrors = false;
15
+ private readonly seen = new Set<string>();
16
+
17
+ report(pos: Pos, message: string, severity: Diagnostic["severity"] = "error"): void {
18
+ if (severity === "error") this.hasErrors = true;
19
+ const id = `${pos.line}:${pos.column}:${message}`;
20
+ if (this.items.length >= MAX_DIAGNOSTICS || this.seen.has(id)) return;
21
+ this.seen.add(id);
22
+ this.items.push({ severity, line: pos.line, column: pos.column, message });
23
+ }
24
+ }