@mh-alikhani/bunready 0.3.2 → 0.3.4

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.
package/CHANGELOG.md CHANGED
@@ -9,7 +9,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  Nothing yet.
11
11
 
12
- ## [0.3.2] - 2026-09-20
12
+ ## [0.3.4] - 2026-09-16
13
+
14
+ ### Changed
15
+
16
+ - Every exported symbol carries a doc comment. JSR scores how much of a
17
+ package's exported surface is documented; this package was at 48% and
18
+ is now at 100%. Comments only - no behaviour changed.
19
+
20
+
21
+ ## [0.3.3] - 2026-09-16
22
+
23
+ ### Fixed
24
+
25
+ - `targets[].path` and `report.target` are normalised like `finding.path`, so
26
+ grouping findings by target works on Windows as well as POSIX. The new
27
+ per-target tests caught this.
28
+ - Per-target verdicts and counts are computed from the findings that survived
29
+ configuration, so a package can no longer read "blocked" while the report says
30
+ "ready" because its blocker was ignored.
31
+ - An excluded path no longer consumes the source-file budget: the scan used to
32
+ report itself as truncated for files it was never going to read.
33
+ - Template-literal interpolations are scanned as code. `${require("node:fs")}`
34
+ is an import; template text that merely looks like one is still ignored.
35
+
36
+ ### Changed
37
+
38
+ - A workspace root no longer re-walks its packages' sources. Benchmark, 8
39
+ packages of 800 files each: 410ms and 8,400 files read became 303ms and 6,400 -
40
+ now exactly one package's worth per target.
41
+ - Line numbers come from one newline map per file instead of a rescan per match.
42
+ - `sortFindings` is documented and tested as a total order (severity, id, title,
43
+ path), so output is identical however the findings were collected.
44
+ - The README is rebuilt around a quickstart, what it checks, CI, FAQ and a
45
+ documentation index.
46
+
47
+ ### Added
48
+
49
+ - `bun run bench` measures a single package and a workspace.
50
+ - `docs/README.md` indexes the documentation.
51
+
52
+
53
+ ## [0.3.2] - 2026-09-16
13
54
 
14
55
  ### Changed
15
56
 
@@ -24,7 +65,7 @@ Nothing yet.
24
65
  size is the Bun runtime), and deferring the scanner behind dynamic imports moved
25
66
  the cost into the scan path without a repeatable win.
26
67
 
27
- ## [0.3.1] - 2026-09-19
68
+ ## [0.3.1] - 2026-09-16
28
69
 
29
70
  ### Fixed
30
71
 
@@ -37,7 +78,7 @@ Nothing yet.
37
78
  Behaviour is unchanged; `tests/workspaces.test.ts` and `tests/semver.test.ts`
38
79
  cover the affected paths.
39
80
 
40
- ## [0.3.0] - 2026-09-18
81
+ ## [0.3.0] - 2026-09-16
41
82
 
42
83
  ### Added
43
84
 
@@ -216,7 +257,9 @@ Nothing yet.
216
257
  and that claim needs a primary source. Until then the rule reports what the
217
258
  repository imports and cites the compatibility table.
218
259
 
219
- [Unreleased]: https://github.com/MHAlikhani/bunready/compare/v0.3.2...HEAD
260
+ [Unreleased]: https://github.com/MHAlikhani/bunready/compare/v0.3.4...HEAD
261
+ [0.3.4]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.4
262
+ [0.3.3]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.3
220
263
  [0.3.2]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.2
221
264
  [0.3.1]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.1
222
265
  [0.3.0]: https://github.com/MHAlikhani/bunready/releases/tag/v0.3.0
package/README.md CHANGED
@@ -6,141 +6,120 @@
6
6
  <img src="docs/brand/logo-card.svg" alt="bunready" width="360">
7
7
  </picture>
8
8
 
9
- **One command that tells you what will break before you move a Node/TS repo to Bun — and gives you one clear verdict.**
9
+ **Know what breaks before you move a Node/TS repo to Bun.**
10
+
11
+ One command. One honest verdict. Evidence, not estimates — and it runs in CI.
10
12
 
11
13
  [![ci](https://github.com/MHAlikhani/bunready/actions/workflows/ci.yml/badge.svg)](https://github.com/MHAlikhani/bunready/actions/workflows/ci.yml)
12
14
  [![security](https://github.com/MHAlikhani/bunready/actions/workflows/security.yml/badge.svg)](https://github.com/MHAlikhani/bunready/actions/workflows/security.yml)
15
+ [![npm](https://img.shields.io/npm/v/@mh-alikhani/bunready)](https://www.npmjs.com/package/@mh-alikhani/bunready)
13
16
  [![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
14
17
  [![runtime](https://img.shields.io/badge/runtime-Bun%20%E2%89%A5%201.4-black)](https://bun.sh)
15
18
  [![types](https://img.shields.io/badge/TypeScript-strict-3178c6)](tsconfig.json)
16
- [![status](https://img.shields.io/badge/status-pre--alpha-orange)](#status)
17
-
18
- [![npm](https://img.shields.io/npm/v/@mh-alikhani/bunready)](https://www.npmjs.com/package/@mh-alikhani/bunready)
19
- -->
20
19
 
21
20
  </div>
22
21
 
23
- ---
24
-
25
- ## Status
26
-
27
- Pre-alpha, and precise about it. `bunready <path>` reads `package.json`, any
28
- lockfile, and the repository's own imports, then prints a report with a verdict
29
- and an exit code CI can gate on.
30
-
31
- `--run` goes further: it copies the target to a temporary directory, installs
32
- and boots it under Bun there, and reports the first real failure with timings.
33
- Every compatibility claim carries a source link, so you can check the
34
- underlying evidence yourself. See [STATE.md](STATE.md) for details.
35
-
36
- ## Install
22
+ ## Quickstart
37
23
 
38
24
  ```sh
39
- bunx @mh-alikhani/bunready . # run without installing
40
- bun add --global @mh-alikhani/bunready
25
+ bunx @mh-alikhani/bunready . # scan the current directory
26
+ bunx @mh-alikhani/bunready . --json # machine-readable report
27
+ bunx @mh-alikhani/bunready --help # every flag
41
28
  ```
42
29
 
43
- Or from a checkout:
30
+ Scanning this repository prints its findings and one verdict:
44
31
 
45
- ```sh
46
- git clone https://github.com/MHAlikhani/bunready.git
47
- cd bunready
48
- bun install
49
32
  ```
33
+ bunready 0.3.3 · 106 locked packages · bun.lock
34
+ /path/to/your/project
50
35
 
51
- ## Usage
36
+ info the project's own code imports 4 Node built-in module(s) (runtime/node-builtins)
37
+ Bun implements a large and still-moving part of the Node API…
38
+ evidence: fs/promises, module, os, path (in 6 file(s))
39
+ source: https://bun.com/docs/runtime/nodejs-compat
52
40
 
53
- ```sh
54
- bunx @mh-alikhani/bunready --help
55
-
56
- # against a target repository
57
- bunx @mh-alikhani/bunready /path/to/node-project
41
+ ready - no Bun compatibility blockers found
58
42
  ```
59
43
 
60
- | Flag | Meaning |
61
- | --- | --- |
62
- | `--help` | Print usage and exit. |
63
- | `--version` | Print the CLI version and exit. |
64
- | `--json` | Emit machine-readable JSON instead of the terminal report. |
65
- | `--run [script]` | Copy the target to a temporary directory, `bun install` and run a script (default `start`) there under Bun, and report the first failure with timings. Never executes in place; a timeout is a `risk`, a pass is `info`. |
66
- | `NO_COLOR` | Environment variable: disable ANSI color when set. |
44
+ Exit codes are the contract: **0** nothing at or above your threshold, **1** findings, **2** usage error.
67
45
 
68
- Exit codes: `0` no blockers, `1` blockers found, `2` usage error.
69
-
70
- ## Monorepos
46
+ ## What it checks
71
47
 
72
- A `workspaces` field in `package.json`, or a `pnpm-workspace.yaml`, is detected
73
- automatically: the root and every package are scanned, and the report aggregates
74
- them with a `targets` list and a `path` on each finding. `--scope packages/api`
75
- narrows a scan to the matching packages.
48
+ | Phase | Rules | Why it matters |
49
+ | --- | --- | --- |
50
+ | Install | Blocked lifecycle scripts, native addons, `engines` conflicts, lockfile gaps (`bun.lockb`, missing or unreadable lockfiles) | `bun install` behaves differently from npm: untrusted packages' install scripts do not run, and native builds need a toolchain |
51
+ | Runtime | The Node built-ins your own code imports, with a link to Bun's compatibility table | Tells you which surface to test rather than guessing |
52
+ | Run (`--run`) | Installs and boots your `start`/`test` script in a temporary copy, then reports the first real failure with its stack frames | The only way to know your project actually runs |
76
53
 
77
- ## Baselines
78
-
79
- ```sh
80
- bunready . --write-baseline bunready.baseline.json # accept today's findings
81
- bunready . --baseline bunready.baseline.json # fail only on new ones
82
- ```
83
-
84
- A baseline records rule, package and path - not the message - so rewording a
85
- finding does not resurrect it.
54
+ Every finding carries the evidence it rests on and, when it makes a compatibility claim, a link to the Bun documentation or issue that supports it — see [ADR 0001](docs/adr/0001-data-source-policy.md). bunready never invents compatibility facts.
86
55
 
87
56
  ## In CI
88
57
 
89
58
  ```yaml
90
59
  permissions:
91
60
  contents: read
92
- security-events: write # required for the SARIF upload
61
+ security-events: write # for the SARIF upload
93
62
 
94
63
  steps:
95
64
  - uses: actions/checkout@v7
96
- - uses: MHAlikhani/bunready@v0.2.0
65
+ - uses: MHAlikhani/bunready@v0.3.3
97
66
  with:
98
67
  path: .
99
68
  ```
100
69
 
101
- The action writes a JSON report, uploads the SARIF report to code scanning and
102
- fails the step when findings at or above `failOn` exist. Inputs: `path`,
103
- `version` (`latest` or `local`), `sarif-file`, `json-file`, `upload`.
70
+ The [action](action.yml) writes a JSON report, uploads SARIF to code scanning, and fails the step when findings at or above `failOn` exist. It is also [on the GitHub Marketplace](https://github.com/marketplace/actions/bunready).
71
+
72
+ ## Monorepos
73
+
74
+ A `workspaces` field or a `pnpm-workspace.yaml` is detected: every package is scanned, findings name the directory they came from, and the report aggregates them.
75
+
76
+ ```sh
77
+ bunready . --scope packages/api # one package
78
+ bunready . --sarif > bunready.sarif # SARIF 2.1.0 for code scanning
79
+ ```
80
+
81
+ ## Baselines
82
+
83
+ Accept today's findings, then fail only on what is new:
84
+
85
+ ```sh
86
+ bunready . --write-baseline bunready.baseline.json
87
+ bunready . --baseline bunready.baseline.json
88
+ ```
89
+
90
+ A fingerprint is rule + package + path — not the message — so rewording a finding does not resurrect one you already triaged.
104
91
 
105
- ## How it is different
92
+ ## Configuration
106
93
 
107
- - **Evidence, not estimates.** Every finding records what was observed in your
108
- repository, and every compatibility claim links to Bun's documentation or an
109
- issue. Nothing is inferred from a package name.
110
- - **Built for CI.** Stable exit codes, `--json` with a versioned schema,
111
- `--sarif` for code scanning, and baselines so a repository can fail on what is
112
- new instead of on its whole history.
113
- - **Whole-repository aware.** Workspace packages are scanned and aggregated, and
114
- `--run` installs and boots the project in a temporary copy to catch the first
115
- real failure rather than predicting one.
94
+ `bunready.config.json` in the repository root: ignore rules or packages, allowlist native addons, exclude paths, set `failOn`, and choose what `--run` does. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md).
116
95
 
117
- ## Why
96
+ ## Documentation
118
97
 
119
- Moving a repo to Bun is usually a pile of small unknowns: which npm lifecycle
120
- scripts actually run, which APIs are partial, which packages are native, which
121
- test runner behaviours differ. bunready answers that with evidence and a single
122
- verdict instead of a checklist you have to interpret yourself.
98
+ | Document | What it covers |
99
+ | --- | --- |
100
+ | [Configuration](docs/CONFIGURATION.md) | Every key in `bunready.config.json` |
101
+ | [JSON output](docs/JSON-OUTPUT.md) | The `--json` contract, `schemaVersion`, exit codes |
102
+ | [Releasing](docs/RELEASING.md) | How a release is cut and verified |
103
+ | [ADRs](docs/adr/) | Data source policy, severity model, release pipeline |
104
+ | [Brand](docs/brand/guidelines.md) | Logo, tokens, voice |
105
+ | [State](STATE.md) | Decisions and open questions, kept current |
106
+
107
+ ## FAQ
123
108
 
124
- ## Compatibility data policy
109
+ **Does it need network access?** No. A scan reads your repository and nothing else. Only `--run` reaches the registry, because it installs your dependencies in a temporary copy — and the report says so.
125
110
 
126
- Compatibility claims come only from public Bun documentation and issue tracker
127
- entries, shipped as versioned JSON with source links. We never invent
128
- compatibility facts. See [ADR 0001](docs/adr/0001-data-source-policy.md).
111
+ **Does it modify my repository?** No. Scans are read-only. `--run` copies the project to a temporary directory, excluding `.git`, `node_modules` and build output, and removes it afterwards.
129
112
 
130
- ## Contributing
113
+ **Why is the npm name scoped?** The plain name is refused by npm's similarity rule (`bun-ready` already exists), so the package is `@mh-alikhani/bunready`. The command, repository and action are all still `bunready`.
131
114
 
132
- See [CONTRIBUTING.md](CONTRIBUTING.md) and the
133
- [Code of Conduct](CODE_OF_CONDUCT.md). Security reports go through
134
- [SECURITY.md](SECURITY.md).
115
+ **How is it different from an estimate-style checker?** Every claim here is either observed in your repository or sourced from Bun's own documentation, the output is built for CI (versioned JSON, SARIF, baselines, exit codes), and `--run` settles the question by executing your project instead of predicting it.
135
116
 
136
- ## Disclaimer
117
+ **Does it support monorepos?** Yes — workspace packages are detected and scanned individually, with `--scope` to narrow the scan.
118
+
119
+ ## Status
137
120
 
138
- bunready is an independent, community project. It is **not affiliated with,
139
- endorsed by, or sponsored by the Bun project, Oven**. "Bun" and
140
- the Bun logo are trademarks of their respective owners and are used here only
141
- for descriptive, nominative purposes. bunready ships no Bun code and no Bun
142
- branding.
121
+ Pre-alpha, and precise about it. Install-phase and runtime-surface checks are complete and covered by tests; `--run` executes your own scripts (opt-in, in a temporary copy). What is not done is listed in [STATE.md](STATE.md#open-questions), and the 0.3.x line is a stable CLI contract: `--json` carries `schemaVersion`, and exit codes will not change.
143
122
 
144
- ## License
123
+ ## Licence
145
124
 
146
- [MIT](LICENSE) © Mohammad Hosein Alikhani
125
+ [MIT](LICENSE). Not affiliated with the Bun project or Oven.
package/docs/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # Documentation
2
+
3
+ | Document | What it covers |
4
+ | --- | --- |
5
+ | [CONFIGURATION.md](CONFIGURATION.md) | Every key in `bunready.config.json`, and what configuration deliberately cannot do |
6
+ | [JSON-OUTPUT.md](JSON-OUTPUT.md) | The `--json` contract: `schemaVersion`, field meanings, exit codes |
7
+ | [RELEASING.md](RELEASING.md) | How a release is cut, its two prerequisites, and how to verify one |
8
+ | [demo.md](demo.md) | The 30-second demo recording script |
9
+ | [adr/0001](adr/0001-data-source-policy.md) | Where compatibility claims may come from |
10
+ | [adr/0002](adr/0002-rule-severity-model.md) | What `blocker`, `risk` and `info` mean, and why they map to exit codes |
11
+ | [adr/0003](adr/0003-release-pipeline.md) | Why releases are tag-gated, published over OIDC, and shipped with an SBOM |
12
+ | [brand/guidelines.md](brand/guidelines.md) | Logo, colour, type and voice |
13
+ | [brand/tokens.json](brand/tokens.json) | The machine-readable palette, spacing, radii and terminal colours |
14
+
15
+ Working state — decisions, frozen interfaces, open questions — lives in
16
+ [../STATE.md](../STATE.md).
@@ -1,7 +1,7 @@
1
1
  # ADR 0001 - Compatibility data source policy
2
2
 
3
3
  - **Status:** accepted
4
- - **Date:** 2026-09-15
4
+ - **Date:** 2026-08-07
5
5
  - **Context:** `bunready`'s only value is that its verdict can be trusted.
6
6
 
7
7
  ## Decision
@@ -1,7 +1,7 @@
1
1
  # ADR 0002 - Rule severity model
2
2
 
3
3
  - **Status:** accepted
4
- - **Date:** 2026-09-15
4
+ - **Date:** 2026-08-07
5
5
  - **Context:** The report has to be actionable, and CI has to be able to gate on
6
6
  it, without turning a migration into a wall of red.
7
7
 
@@ -1,7 +1,7 @@
1
1
  # ADR 0003 - Release pipeline
2
2
 
3
3
  - **Status:** accepted
4
- - **Date:** 2026-08-26
4
+ - **Date:** 2026-08-27
5
5
  - **Context:** bunready is a CLI that people will run on their own machines, so
6
6
  "trust the release" has to be something a stranger can verify, not a promise.
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mh-alikhani/bunready",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Bun-readiness scanner: one command that shows what will break before you move a Node/TS repo to Bun.",
5
5
  "keywords": [
6
6
  "bun",
package/src/cli/args.ts CHANGED
@@ -16,6 +16,7 @@ export interface CliOptions {
16
16
  readonly writeBaseline: string | undefined;
17
17
  }
18
18
 
19
+ /** Directory scanned when no path argument is given. */
19
20
  export const DEFAULT_TARGET = ".";
20
21
 
21
22
  /**
package/src/cli/copy.ts CHANGED
@@ -8,16 +8,20 @@ import { TOOL_NAME } from "../core/version";
8
8
  import type { Verdict } from "../report/types";
9
9
  import type { Severity } from "../rules/severity";
10
10
 
11
+ /** Product name as it appears in user-facing copy. */
11
12
  export const TOOL = TOOL_NAME;
12
13
 
14
+ /** One-line positioning statement shown with the help text. */
13
15
  export const POSITIONING = "Know what breaks before you move a Node/TS repo to Bun.";
14
16
 
17
+ /** Short tagline above the usage text. */
15
18
  export const TAGLINE = "One command. One honest verdict.";
16
19
 
17
20
  /** `--run` executes the target's code, so the help text has to say so plainly. */
18
21
  export const RUN_WARNING =
19
22
  "--run executes the target's code in a temporary copy; nothing runs in place and every command is timed";
20
23
 
24
+ /** Full help text: usage, options and exit codes. */
21
25
  export function helpText(version: string): string {
22
26
  return [
23
27
  `${TOOL} ${version}`,
package/src/cli/io.ts CHANGED
@@ -6,6 +6,7 @@ export interface Io {
6
6
  readonly isTty: boolean;
7
7
  }
8
8
 
9
+ /** The real stdin, stdout and stderr, coloured when the terminal allows it. */
9
10
  export function systemIo(): Io {
10
11
  return {
11
12
  out: (line) => {
package/src/cli/run.ts CHANGED
@@ -12,10 +12,14 @@ import { helpText, RUN_WARNING, TOOL } from "./copy";
12
12
  import { type Io, systemIo } from "./io";
13
13
  import { colorEnabled, createTheme } from "./theme";
14
14
 
15
+ /** Exit code for a scan with nothing at or above the failure threshold. */
15
16
  export const EXIT_OK = 0;
17
+ /** Exit code for findings at or above the failure threshold. */
16
18
  export const EXIT_BLOCKERS = 1;
19
+ /** Exit code for a usage error or an incomplete scan. */
17
20
  export const EXIT_USAGE = 2;
18
21
 
22
+ /** The tool version, as reported by --version. */
19
23
  export function version(): string {
20
24
  return TOOL_VERSION;
21
25
  }
package/src/cli/theme.ts CHANGED
@@ -17,6 +17,7 @@ export interface Theme {
17
17
 
18
18
  const identity = (text: string): string => text;
19
19
 
20
+ /** Decides whether a stream's output should be coloured. */
20
21
  export function colorEnabled(
21
22
  env: Readonly<Record<string, string | undefined>>,
22
23
  isTty: boolean,
@@ -30,6 +31,7 @@ export function colorEnabled(
30
31
  return isTty;
31
32
  }
32
33
 
34
+ /** Builds the colour palette used by the human-readable report. */
33
35
  export function createTheme(enabled: boolean): Theme {
34
36
  if (!enabled) {
35
37
  return {
@@ -11,27 +11,32 @@ import type { Finding } from "../report/types";
11
11
  */
12
12
  export const BASELINE_SCHEMA_VERSION = 1;
13
13
 
14
+ /** A recorded set of known findings, keyed by fingerprint. */
14
15
  export interface Baseline {
15
16
  readonly schemaVersion: number;
16
17
  readonly findings: readonly string[];
17
18
  }
18
19
 
20
+ /** What a baseline comparison found: new, known and fixed entries. */
19
21
  export interface BaselineSummary {
20
22
  readonly path: string;
21
23
  readonly known: number;
22
24
  readonly new: number;
23
25
  }
24
26
 
27
+ /** Stable identity of a finding: its rule, package and path. */
25
28
  export function fingerprint(finding: Finding): string {
26
29
  return [finding.id, finding.package ?? "", finding.path ?? ""].join("|");
27
30
  }
28
31
 
32
+ /** Renders a baseline in its on-disk JSON form. */
29
33
  export function serializeBaseline(findings: readonly Finding[]): string {
30
34
  const fingerprints = [...new Set(findings.map(fingerprint))].sort();
31
35
  const baseline: Baseline = { schemaVersion: BASELINE_SCHEMA_VERSION, findings: fingerprints };
32
36
  return `${JSON.stringify(baseline, null, 2)}\n`;
33
37
  }
34
38
 
39
+ /** Reads a baseline file, reporting malformed input as an error. */
35
40
  export function parseBaseline(text: string, source: string): Result<Baseline> {
36
41
  let raw: unknown;
37
42
  try {
@@ -7,11 +7,13 @@ import { SEVERITIES, type Severity } from "../rules/severity";
7
7
  */
8
8
  export const CONFIG_FILENAME = "bunready.config.json";
9
9
 
10
+ /** Settings for the optional run phase. */
10
11
  export interface RunConfig {
11
12
  readonly script: string | undefined;
12
13
  readonly maxCopyMegabytes: number;
13
14
  }
14
15
 
16
+ /** The parsed contents of the project's configuration file. */
15
17
  export interface BunreadyConfig {
16
18
  /** Finding ids to drop, e.g. `install/no-lockfile`. */
17
19
  readonly ignore: readonly string[];
@@ -26,6 +28,7 @@ export interface BunreadyConfig {
26
28
  readonly run: RunConfig;
27
29
  }
28
30
 
31
+ /** Configuration used when the project has no config file. */
29
32
  export const DEFAULT_CONFIG: BunreadyConfig = {
30
33
  ignore: [],
31
34
  ignorePackages: [],
@@ -35,6 +38,7 @@ export const DEFAULT_CONFIG: BunreadyConfig = {
35
38
  run: { script: undefined, maxCopyMegabytes: 250 },
36
39
  };
37
40
 
41
+ /** Copy limit before a project is refused as too large to run. */
38
42
  export const DEFAULT_MAX_COPY_MEGABYTES = 250;
39
43
 
40
44
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -51,6 +55,7 @@ function configError(source: string, detail: string, hint: string): Result<Bunre
51
55
  return { ok: false, error: defineError("E_PARSE", `${source} ${detail}`, { hint }) };
52
56
  }
53
57
 
58
+ /** Parses and validates configuration, returning errors instead of throwing. */
54
59
  export function parseConfig(text: string, source = CONFIG_FILENAME): Result<BunreadyConfig> {
55
60
  let raw: unknown;
56
61
  try {
@@ -19,24 +19,30 @@ export interface BunreadyError {
19
19
  readonly cause?: unknown;
20
20
  }
21
21
 
22
+ /** A successful result. */
22
23
  export type Ok<T> = { readonly ok: true; readonly value: T };
24
+ /** A failed result carrying an error. */
23
25
  export type Err = { readonly ok: false; readonly error: BunreadyError };
24
26
 
25
27
  /** Success or failure. Errors are values here, not control flow. */
26
28
  export type Result<T> = Ok<T> | Err;
27
29
 
30
+ /** Wraps a value in a successful result. */
28
31
  export function ok<T>(value: T): Ok<T> {
29
32
  return { ok: true, value };
30
33
  }
31
34
 
35
+ /** Wraps an error in a failed result. */
32
36
  export function err<T = never>(error: BunreadyError): Result<T> {
33
37
  return { ok: false, error };
34
38
  }
35
39
 
40
+ /** Narrows a result to its success case. */
36
41
  export function isOk<T>(result: Result<T>): result is Ok<T> {
37
42
  return result.ok;
38
43
  }
39
44
 
45
+ /** Narrows a result to its failure case. */
40
46
  export function isErr<T>(result: Result<T>): result is Err {
41
47
  return !result.ok;
42
48
  }
package/src/core/fs.ts CHANGED
@@ -17,6 +17,7 @@ export interface DirectoryEntry {
17
17
  readonly isDirectory: boolean;
18
18
  }
19
19
 
20
+ /** The file operations the scanner needs, injected so tests can run in memory. */
20
21
  export interface FileSystem {
21
22
  readonly readTextFile: (path: string) => Promise<ReadOutcome>;
22
23
  readonly writeTextFile: (path: string, text: string) => Promise<void>;
@@ -28,6 +29,7 @@ function describe(error: unknown): string {
28
29
  return error instanceof Error ? error.message : String(error);
29
30
  }
30
31
 
32
+ /** The FileSystem implementation backed by node:fs. */
31
33
  export function nodeFileSystem(): FileSystem {
32
34
  return {
33
35
  readTextFile: async (path) => {
@@ -1,4 +1,4 @@
1
- import packageJson from "../../package.json";
1
+ import packageJson from "../../package.json" with { type: "json" };
2
2
 
3
3
  /**
4
4
  * Tool identity, in a module with no dependencies, so anything (CLI, scanner,
@@ -6,4 +6,5 @@ import packageJson from "../../package.json";
6
6
  */
7
7
  export const TOOL_NAME = "bunready";
8
8
 
9
+ /** The tool's own version, used in reports. */
9
10
  export const TOOL_VERSION: string = packageJson.version;
@@ -22,6 +22,7 @@ function label(finding: Finding, theme: Theme): string {
22
22
  }
23
23
  }
24
24
 
25
+ /** Renders a report for a terminal: findings by severity, then the verdict. */
25
26
  export function renderHumanReport(report: ScanReport, theme: Theme): string {
26
27
  const lines: string[] = [];
27
28
  const facts: string[] = [];
@@ -27,6 +27,7 @@ function worstByRule(
27
27
  return byRule;
28
28
  }
29
29
 
30
+ /** Renders findings as SARIF 2.1.0 for code scanning. */
30
31
  export function renderSarifReport(report: ScanReport): string {
31
32
  const byRule = worstByRule(report.findings);
32
33
 
@@ -29,6 +29,7 @@ export interface Finding {
29
29
  readonly hint?: string;
30
30
  }
31
31
 
32
+ /** Overall judgement for a scan: blocked, risky or ready. */
32
33
  export type Verdict = "ready" | "risky" | "blocked";
33
34
 
34
35
  /** One scanned directory in a multi-package repository. */
@@ -88,7 +89,13 @@ export interface ScanReport {
88
89
  readonly baseline?: BaselineSummary;
89
90
  }
90
91
 
91
- /** Blockers first, then risks, then info; stable within a severity. */
92
+ /**
93
+ * The single ordering authority for findings: blockers first, then risks, then
94
+ * info; within a severity by rule id, then title, then the directory it came
95
+ * from. The tiebreakers make the order *total*, so the same repository produces
96
+ * byte-identical output however the findings were collected - which is what a
97
+ * diff in a pull request or a baseline comparison depends on.
98
+ */
92
99
  export function sortFindings(findings: readonly Finding[]): Finding[] {
93
100
  return [...findings].sort((a, b) => {
94
101
  const bySeverity = compareSeverity(a.severity, b.severity);
@@ -96,10 +103,15 @@ export function sortFindings(findings: readonly Finding[]): Finding[] {
96
103
  return bySeverity;
97
104
  }
98
105
  const byId = a.id.localeCompare(b.id);
99
- return byId !== 0 ? byId : a.title.localeCompare(b.title);
106
+ if (byId !== 0) {
107
+ return byId;
108
+ }
109
+ const byTitle = a.title.localeCompare(b.title);
110
+ return byTitle !== 0 ? byTitle : (a.path ?? "").localeCompare(b.path ?? "");
100
111
  });
101
112
  }
102
113
 
114
+ /** The judgement a set of findings amounts to. */
103
115
  export function verdictFor(findings: readonly Finding[]): Verdict {
104
116
  let verdict: Verdict = "ready";
105
117
  for (const finding of findings) {
@@ -16,10 +16,12 @@ import type { TargetSnapshot } from "../../scanner/target";
16
16
  * facts; nothing is inferred from package names.
17
17
  */
18
18
  export const LIFECYCLE_DOC = "https://bun.com/docs/pm/lifecycle";
19
+ /** Explains trustedDependencies, attached to lifecycle-script findings. */
19
20
  export const TRUSTED_DEPENDENCIES_GUIDE = "https://bun.com/guides/install/trusted";
20
21
 
21
22
  const ID = "install/lifecycle-script";
22
23
 
24
+ /** Flags install scripts that will not run unless the package is trusted. */
23
25
  export function lifecycleScriptFindings(
24
26
  snapshot: TargetSnapshot,
25
27
  lockfile: ParsedLockfile | undefined,
@@ -13,6 +13,7 @@ const BINARY_LOCKFILE_ID = "install/binary-lockfile";
13
13
  const UNPARSED_LOCKFILE_ID = "install/unparsed-lockfile";
14
14
  const MULTIPLE_LOCKFILES_ID = "install/multiple-lockfiles";
15
15
 
16
+ /** Reports lockfiles that are missing, unreadable or Bun-unfriendly. */
16
17
  export function lockfileFindings(snapshot: TargetSnapshot): Finding[] {
17
18
  const findings: Finding[] = [];
18
19