pgexplain 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-06-29
11
+
12
+ Initial release.
13
+
14
+ ### Added
15
+
16
+ - **`analyze`** (the default command): read a PostgreSQL `EXPLAIN (ANALYZE)`
17
+ plan from a file or stdin and produce a human-readable report. Point it at a
18
+ directory to analyze every plan in batch.
19
+ - **`run`**: connect to PostgreSQL, run `EXPLAIN` safely, and analyze the
20
+ result. Execution is wrapped in an auto-rolled-back, read-only transaction
21
+ with `statement_timeout` and `lock_timeout`; non-`SELECT` statements are
22
+ refused without `--force`. The `pg` driver is an optional, lazy-loaded
23
+ dependency.
24
+ - **`diff`**: compare two plans (`before` → `after`) and report regressions,
25
+ with CI gates via `--fail-on-slower` and `--fail-on-new-findings`.
26
+ - **`completion`**: print a shell completion script for bash, zsh, or fish.
27
+ - **16 advisor rules** with stable, greppable `PGX_*` codes: sequential scan on
28
+ a large table, nested loop with a large outer, high filter discard, sort
29
+ spill to disk, hash spill to disk, index-only heap fetches, lossy bitmap,
30
+ workers not launched, could-be-index-only, filter-could-be-index-condition,
31
+ correlated subplan, cartesian product, significant JIT time, trigger time,
32
+ row misestimate, and low cache hit ratio.
33
+ - **Actionable diagnostics**: every advisor finding and every operational error
34
+ tells you what happened, why, and exactly how to fix it — including
35
+ copy-pasteable SQL and shell commands and a link to the relevant PostgreSQL
36
+ docs. Operational errors come from a stable `PGX_*` catalog.
37
+ - **5 output formats**: terminal (color, heat, bars), markdown, json (stable,
38
+ `schemaVersion` 1), html (self-contained), and text. Controlled with
39
+ `-f/--format`, `-o/--output`, `--tldr`, `--redact`, `--ascii`,
40
+ `--color`/`--no-color`, and CI gates (`--fail-on`, `--strict`).
41
+ - **Safety wrapper**: credentials are scrubbed from all output; EXPLAIN ANALYZE
42
+ runs in a rolled-back, read-only transaction with timeouts; non-`SELECT` is
43
+ refused without `--force`; `--redact` strips literal values; no telemetry.
44
+ - **Configuration**: tune thresholds and per-rule `enabled`/`severity` via
45
+ `.pgexplainrc.json`, `.pgexplainrc`, or a `pgExplain` key in `package.json`.
46
+ - **Programmatic library API**: `import { analyze, render } from "pgexplain"`.
47
+ - **Stable exit codes** so scripts and CI can branch on the kind of failure:
48
+ `0` success, `1` CI gate tripped, `2` usage, `3` input, `4` parse,
49
+ `5` database, `70` internal, `130` interrupted.
50
+
51
+ [Unreleased]: https://github.com/OWNER/pgexplain/compare/v0.1.0...HEAD
52
+ [0.1.0]: https://github.com/OWNER/pgexplain/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pgexplain contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,338 @@
1
+ # pgexplain
2
+
3
+ Turn PostgreSQL `EXPLAIN (ANALYZE)` JSON into human-readable reports and detect plan anti-patterns — **every finding tells you what happened, why, and exactly how to fix it.**
4
+
5
+ [![npm version](https://img.shields.io/badge/npm-pgexplain-cb3837?logo=npm)](https://www.npmjs.com/package/pgexplain)
6
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
7
+ [![node](https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js&logoColor=white)](https://nodejs.org)
8
+
9
+ The npm package is `pgexplain`; the command it installs is `pg-explain`.
10
+
11
+ ---
12
+
13
+ ## Why pgexplain
14
+
15
+ Most EXPLAIN tools pretty-print the plan tree and stop there — you still have to know what a "lossy bitmap heap scan" or a 500× row misestimate *means* and what to do about it.
16
+
17
+ pgexplain goes further. It runs an **advisor** over the plan, flags anti-patterns by stable `PGX_*` code, and for each one prints three things:
18
+
19
+ - **What** happened (in plain language, with the real numbers from your plan).
20
+ - **Why** it matters.
21
+ - **Fix** — concrete steps and **copy-pasteable SQL/shell commands** (e.g. the exact `CREATE INDEX` or `ANALYZE` to run), plus a link to the relevant Postgres docs.
22
+
23
+ The same philosophy applies to operational errors: auth failures, timeouts, unreachable hosts, and malformed input all come back as actionable diagnostics rather than stack traces.
24
+
25
+ ---
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ # global
31
+ pnpm add -g pgexplain # or: npm install -g pgexplain
32
+
33
+ # one-off, no install
34
+ npx pgexplain plan.json
35
+ ```
36
+
37
+ Requires **Node.js >= 22**. The package is **ESM-only**.
38
+
39
+ > The `pg` driver is an **optional dependency**. You only need it for `pg-explain run` (connecting to a live database). Analyzing a saved plan from a file or stdin needs no driver. If `pg` is missing when you run `run`, pgexplain tells you exactly how to install it (`PGX_PG_DRIVER_MISSING`).
40
+
41
+ ---
42
+
43
+ ## Quickstart
44
+
45
+ Analyze a saved plan and write a Markdown report (the headline deliverable):
46
+
47
+ ```sh
48
+ pg-explain plan.json -o report.md -f markdown
49
+ ```
50
+
51
+ Pipe a plan straight from psql or a file:
52
+
53
+ ```sh
54
+ psql -XqAt -c "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) <query>" | pg-explain
55
+ pg-explain < plan.json
56
+ ```
57
+
58
+ Connect to a database, run EXPLAIN safely, and analyze the result in one step:
59
+
60
+ ```sh
61
+ pg-explain run --query "SELECT * FROM orders WHERE status = 'shipped'" --dsn "$DATABASE_URL"
62
+ ```
63
+
64
+ Point at a directory to analyze every plan in it (batch mode):
65
+
66
+ ```sh
67
+ pg-explain ./plans/
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Example output
73
+
74
+ Running pgexplain on a plan with a sequential scan over a large table:
75
+
76
+ ```text
77
+ $ pg-explain test/fixtures/seq-scan-large.json --no-color
78
+
79
+ pg-explain report
80
+ Verdict: 2 warnings, 2 notes — top cost: Seq Scan on orders (78% of time). Total 321.0 ms.
81
+
82
+ Plan tree
83
+ Aggregate ▇▇▁▁▁▁▁▁ rows=1 · self 70.5 ms (22%) · cache 2%
84
+ └─ Seq Scan on orders ▇▇▇▇▇▇▁▁ rows=500,000 (est 1,000, 500× under) · self 250.0 ms (78%) · cache 2%
85
+
86
+ Bottlenecks (by self time)
87
+ 1. Seq Scan on orders — 250.0 ms (78%)
88
+ 2. Aggregate — 70.5 ms (22%)
89
+
90
+ Findings
91
+
92
+ [WARNING] Sequential scan on orders (500,000 rows) PGX_SEQ_SCAN_LARGE
93
+ What: Postgres read orders sequentially, scanning roughly 500,000 rows.
94
+ Why: A row filter ((status = 'shipped'::text)) is applied after reading every row, so no index narrowed the scan.
95
+ Fix: Add an index covering the WHERE/JOIN predicate on orders so Postgres can skip non-matching rows. If the query genuinely needs most of the table, the seq scan is correct — reduce the rows touched instead.
96
+ - Identify the selective columns in the WHERE/JOIN predicate.
97
+ - Ensure they are sargable (no function-wrapping or implicit casts on the column).
98
+ - If selectivity is low, a partial index (WHERE …) may be better.
99
+ Index the predicate columns: CREATE INDEX ON orders (<predicate columns>) -- columns from the filter above;
100
+ docs: https://www.postgresql.org/docs/current/indexes-intro.html
101
+
102
+ [WARNING] 500x row underestimate on orders PGX_ROW_MISESTIMATE
103
+ What: Postgres estimated 1,000 rows but 500,000 were produced — a 500x underestimate on orders.
104
+ Why: The planner's row estimate is based on statistics that are stale, missing, or too coarse for this predicate (e.g. correlated columns the planner treats as independent).
105
+ Fix: Refresh and sharpen statistics for orders: run ANALYZE orders, raise per-column statistics targets on the predicate columns, and add extended statistics for correlated columns so the planner estimates rows correctly. Underestimates feeding a nested loop or hash join are the highest priority — fix these first.
106
+ - Refresh table statistics first; this alone often fixes the estimate.
107
+ - If the column has a skewed/uneven distribution, raise its statistics target and re-ANALYZE.
108
+ - If the predicate spans multiple correlated columns, create extended statistics so the planner stops assuming independence.
109
+ Refresh statistics: ANALYZE orders;
110
+ Raise per-column statistics target: ALTER TABLE orders ALTER COLUMN <column> SET STATISTICS 1000;
111
+ ANALYZE orders;
112
+ Add extended statistics for correlated columns: CREATE STATISTICS <stats_name> (dependencies, ndistinct) ON <col_a>, <col_b> FROM orders;
113
+ ANALYZE orders;
114
+ docs: https://www.postgresql.org/docs/current/planner-stats.html
115
+
116
+ [NOTE] Low cache hit ratio at Aggregate (2.3%) PGX_LOW_CACHE_HIT
117
+ What: Aggregate served only 2.3% of its shared-buffer accesses from cache, reading 5,000 blk (39.1 MiB) from disk.
118
+ Why: The pages this node needed were not resident in shared_buffers, so PostgreSQL had to read them from disk. On a first run this is an expected cold cache; if it persists, the working set is larger than the cache or the scan touches more pages than necessary.
119
+ Fix: Re-run the query to check whether this is just a cold cache — the ratio should climb on a warm run. If it stays low, the working set exceeds shared_buffers: size shared_buffers/effective_cache_size to your RAM, or add a selective index on the scanned relation so far fewer pages are read.
120
+ - Run the same EXPLAIN (ANALYZE, BUFFERS) a second time; a much higher hit ratio means the first run was a cold cache and no action is needed.
121
+ - If the ratio stays low, check whether shared_buffers (and effective_cache_size for planner costing) are sized to the machine's RAM.
122
+ - If the node reads far more pages than the rows it returns, add a selective index so only matching pages are fetched.
123
+ Inspect current buffer-cache sizing: SHOW shared_buffers; SHOW effective_cache_size;
124
+ Reduce pages read with a selective index: CREATE INDEX ON <table> (<predicate columns>);
125
+ docs: https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS
126
+
127
+ [NOTE] Low cache hit ratio at Seq Scan on orders (2.3%) PGX_LOW_CACHE_HIT
128
+ What: Seq Scan on orders served only 2.3% of its shared-buffer accesses from cache, reading 5,000 blk (39.1 MiB) from disk.
129
+ Why: The pages this node needed were not resident in shared_buffers, so PostgreSQL had to read them from disk. On a first run this is an expected cold cache; if it persists, the working set is larger than the cache or the scan touches more pages than necessary.
130
+ Fix: Re-run the query to check whether this is just a cold cache — the ratio should climb on a warm run. If it stays low, the working set exceeds shared_buffers: size shared_buffers/effective_cache_size to your RAM, or add a selective index on orders so far fewer pages are read.
131
+ - Run the same EXPLAIN (ANALYZE, BUFFERS) a second time; a much higher hit ratio means the first run was a cold cache and no action is needed.
132
+ - If the ratio stays low, check whether shared_buffers (and effective_cache_size for planner costing) are sized to the machine's RAM.
133
+ - If the node reads far more pages than the rows it returns, add a selective index so only matching pages are fetched.
134
+ Inspect current buffer-cache sizing: SHOW shared_buffers; SHOW effective_cache_size;
135
+ Reduce pages read with a selective index: CREATE INDEX ON orders (<predicate columns>);
136
+ docs: https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS
137
+
138
+ Total execution time: 321.0 ms
139
+ ```
140
+
141
+ (In a real terminal the tree uses color, severity heat, and proportional self-time bars.)
142
+
143
+ ---
144
+
145
+ ## Commands
146
+
147
+ | Command | What it does |
148
+ | --- | --- |
149
+ | `pg-explain [FILE]` | **Analyze** a plan from a file, `< stdin`, or every plan in a directory (batch mode). This is the default command. |
150
+ | `pg-explain run` | **Connect** to PostgreSQL, run `EXPLAIN` safely, and analyze the result. Needs the optional `pg` driver. |
151
+ | `pg-explain diff <before> <after>` | **Compare** two plan JSON files and report regressions. Designed as a CI gate. |
152
+ | `pg-explain completion <bash\|zsh\|fish>` | Print a shell **completion** script for the given shell. |
153
+
154
+ Run `pg-explain --help`, `pg-explain run --help`, or `pg-explain diff --help` for the full flag list.
155
+
156
+ ### `pg-explain run` (selected flags)
157
+
158
+ | Flag | Purpose |
159
+ | --- | --- |
160
+ | `--dsn` / `--host` `--port` `-d/--dbname` `-U/--user` | Connection target (or `PG*` env vars). |
161
+ | `--query` / `--file` | SQL to explain (a string or a `.sql` file). |
162
+ | `--statement <n>` | 1-based statement index when the file holds several. |
163
+ | `--param <v>` | Value for `$1`, `$2`, … (repeatable). |
164
+ | `--sslmode` `--sslrootcert` | `disable \| require \| verify-ca \| verify-full` and a CA bundle. |
165
+ | `--connect-timeout` `--statement-timeout` `--lock-timeout` | Time budgets (default `10s` / `30s` / `5s`). |
166
+ | `--force` | Allow a non-SELECT to execute (still auto-rolled-back). |
167
+ | `--no-rollback` | Do **not** wrap the run in a rolled-back transaction (dangerous). |
168
+ | `--no-analyze` | Plan estimates only — the query never executes. |
169
+ | `--no-buffers` `--explain-verbose` `--settings` `--wal` `--generic-plan` `--no-timing` `--no-costs` | Toggle individual EXPLAIN options. |
170
+ | `--compat` | Auto-omit EXPLAIN options the server is too old for. |
171
+
172
+ ---
173
+
174
+ ## Output formats
175
+
176
+ Choose with `-f`/`--format` (default `terminal`); write to a file with `-o`/`--output`.
177
+
178
+ | Format | Notes |
179
+ | --- | --- |
180
+ | `terminal` | Color, severity heat, and proportional self-time bars for interactive use. |
181
+ | `markdown` | The headline shareable deliverable — paste into a PR or ticket. |
182
+ | `json` | Stable, machine-readable (`schemaVersion = 1`). |
183
+ | `html` | Single self-contained file (no external assets). |
184
+ | `text` | Plain text, no escapes — good for logs. |
185
+
186
+ `diff` supports `terminal`, `markdown`, and `json`.
187
+
188
+ Shared output flags: `--tldr` (summary + findings, no plan tree), `--redact` (strip literal values so the report is safe to share), `--ascii` (ASCII tree glyphs), `--color auto|always|never` / `--no-color`, `--compact` (compact JSON), `--config <path>`, `-q/--quiet`, `--verbose`, `--debug`.
189
+
190
+ ---
191
+
192
+ ## Safety
193
+
194
+ `EXPLAIN ANALYZE` **executes the query**, so pgexplain is conservative by default when talking to a live database:
195
+
196
+ - **Auto-rollback.** Every `run` is wrapped in a transaction that is **always rolled back** (`BEGIN … ROLLBACK`), so nothing is committed. Opt out only with `--no-rollback`.
197
+ - **Read-only by default.** A data-modifying statement (`INSERT`/`UPDATE`/`DELETE`/`MERGE`/DDL) is **refused** (`PGX_NON_SELECT_REFUSED`) unless you pass `--force` — and even then it runs inside the rolled-back transaction. Or drop ANALYZE (`--no-analyze`) for an estimate-only plan that never runs.
198
+ - **Timeouts.** `statement_timeout` and `lock_timeout` are set on the session (`--statement-timeout`, `--lock-timeout`) so a runaway query or a lock wait can't hang.
199
+ - **Credential scrubbing.** Connection strings, passwords, and other secrets are scrubbed from **all** output, including error messages and `--debug` stack traces.
200
+ - **`--redact`.** Strips literal values from expressions before analysis so a shared report leaks no data.
201
+
202
+ ---
203
+
204
+ ## The advisor
205
+
206
+ The advisor ships **16 rules**, each identified by a stable, greppable `PGX_*` code (the rule id is the diagnostic code, and the config key). They run in roughly most-actionable-first order:
207
+
208
+ | Code | Flags when… |
209
+ | --- | --- |
210
+ | `PGX_CARTESIAN_PRODUCT` | A nested loop has no join condition (accidental cross join). |
211
+ | `PGX_SEQ_SCAN_LARGE` | A sequential scan reads a large table that an index could narrow. |
212
+ | `PGX_NESTED_LOOP_LARGE_OUTER` | A nested loop is driven by a large outer side (re-probes inner repeatedly). |
213
+ | `PGX_HIGH_FILTER_DISCARD` | A node reads many rows then discards most of them via a filter. |
214
+ | `PGX_SORT_SPILL_DISK` | A sort spilled to disk instead of staying in `work_mem`. |
215
+ | `PGX_HASH_SPILL_DISK` | A hash join's build side spilled to disk (multiple batches). |
216
+ | `PGX_CORRELATED_SUBPLAN` | A correlated subplan is re-executed once per outer row. |
217
+ | `PGX_ROW_MISESTIMATE` | Estimated vs actual row counts diverge sharply (stale/missing stats). |
218
+ | `PGX_FILTER_COULD_BE_INDEX_COND` | A residual filter could be pushed into an index condition. |
219
+ | `PGX_COULD_BE_INDEX_ONLY` | An index scan could become index-only with a covering index. |
220
+ | `PGX_INDEX_ONLY_HEAP_FETCHES` | An index-only scan still did many heap fetches (visibility map cold). |
221
+ | `PGX_BITMAP_LOSSY` | A bitmap heap scan went lossy (rechecks whole pages; `work_mem` too small). |
222
+ | `PGX_WORKERS_NOT_LAUNCHED` | Parallel workers were planned but not all launched. |
223
+ | `PGX_LOW_CACHE_HIT` | A node's shared-buffer cache hit ratio is low (heavy disk reads). |
224
+ | `PGX_SIGNIFICANT_JIT` | JIT compilation consumed a significant share of execution time. |
225
+ | `PGX_TRIGGER_TIME` | Triggers consumed a significant share of execution time. |
226
+
227
+ Every finding includes the *what / why / fix* triad shown in the example above. Rules can be tuned or disabled per project (see [Config](#config-file)).
228
+
229
+ > pgexplain also has an **operational error catalog** of stable `PGX_*` codes — auth failures, unreachable hosts, SSL problems, timeouts, malformed/empty input, missing driver, and more — each with a title, cause, remediation, and Postgres docs link.
230
+
231
+ ---
232
+
233
+ ## CI usage
234
+
235
+ pgexplain is built to gate pull requests on plan health.
236
+
237
+ **Fail when a finding is too severe:**
238
+
239
+ ```sh
240
+ # exit 1 if any finding at or above the given severity exists
241
+ pg-explain plan.json --fail-on warn
242
+
243
+ # shorthand for --fail-on warn
244
+ pg-explain plan.json --strict
245
+ ```
246
+
247
+ `--fail-on` takes `info`, `warn`, or `error`. Findings alone never change the exit code unless `--strict`/`--fail-on` is set.
248
+
249
+ **Fail on regressions between two plans:**
250
+
251
+ ```sh
252
+ # exit 1 if execution time regresses by >= 20%, or if any new finding appears
253
+ pg-explain diff before.json after.json \
254
+ --fail-on-slower 20 \
255
+ --fail-on-new-findings
256
+ ```
257
+
258
+ Branch on the **kind** of failure without parsing text:
259
+
260
+ | Exit | Meaning |
261
+ | --- | --- |
262
+ | `0` | Success — report produced. |
263
+ | `1` | CI gate tripped (`--strict` / `--fail-on`, or a `diff` gate). |
264
+ | `2` | Usage error (bad flags, refused non-SELECT, unsupported EXPLAIN option). |
265
+ | `3` | Input error (empty stdin and no file, or an unreadable file). |
266
+ | `4` | Parse error (not valid EXPLAIN JSON, or the wrong shape). |
267
+ | `5` | Database error (connect / auth / permission / timeout / cancel). |
268
+ | `70` | Internal error — a bug in pgexplain. |
269
+ | `130` | Interrupted by SIGINT. |
270
+
271
+ ---
272
+
273
+ ## Config file
274
+
275
+ pgexplain reads, in order, `.pgexplainrc.json`, `.pgexplainrc`, or a `pgExplain` key in `package.json` (override with `--config <path>`). Tune thresholds and enable/disable or re-severity individual rules by code:
276
+
277
+ ```jsonc
278
+ {
279
+ "thresholds": {
280
+ // numeric knobs the rules read (e.g. minimum rows for a "large" seq scan)
281
+ },
282
+ "rules": {
283
+ "PGX_SEQ_SCAN_LARGE": { "severity": "error" },
284
+ "PGX_LOW_CACHE_HIT": { "enabled": false }
285
+ }
286
+ }
287
+ ```
288
+
289
+ Each rule entry accepts `{ "enabled": boolean, "severity": "error" | "warn" | "info" }`.
290
+
291
+ ---
292
+
293
+ ## Library usage
294
+
295
+ pgexplain is also a typed library (ESM):
296
+
297
+ ```ts
298
+ import { analyze, render } from "pgexplain";
299
+
300
+ const explainJson = await fs.readFile("plan.json", "utf8");
301
+
302
+ const result = analyze(explainJson, { redact: true });
303
+ // result.diagnostics — findings with code/severity/what-why-fix
304
+ // result.worstSeverity — "error" | "warn" | "info" | null
305
+
306
+ const markdown = render(result, { format: "markdown" });
307
+ console.log(markdown);
308
+ ```
309
+
310
+ `analyze(input, options?)` parses the EXPLAIN JSON, optionally redacts literals, computes metrics, and runs the advisor. `render(result, options?)` emits any supported format. Other exports include `runAdvisor`, `parseExplainJson`, `computeMetrics`, `DEFAULT_CONFIG`, `FORMATS`, `JSON_SCHEMA_VERSION`, `ExitCode`, and the full type set.
311
+
312
+ ---
313
+
314
+ ## Exit codes
315
+
316
+ See the [CI usage](#ci-usage) table above — `0` success, `1` CI gate, `2` usage, `3` input, `4` parse, `5` database, `70` internal, `130` SIGINT. These are stable; scripts can branch on them directly.
317
+
318
+ ---
319
+
320
+ ## Contributing
321
+
322
+ This project uses **pnpm 9** and **Node >= 22**.
323
+
324
+ ```sh
325
+ pnpm install
326
+ pnpm build # tsup
327
+ pnpm test # vitest
328
+ pnpm lint # biome
329
+ pnpm typecheck # tsc --noEmit
330
+ ```
331
+
332
+ Issues and pull requests are welcome at <https://github.com/OWNER/pgexplain>.
333
+
334
+ ---
335
+
336
+ ## License
337
+
338
+ MIT © 2026 pgexplain contributors. See [LICENSE](./LICENSE).
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }