@cosyte/cli 0.0.1

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,326 @@
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
+ Versions and publishing are managed with [Changesets](https://github.com/changesets/changesets);
8
+ this file is maintained by hand (Changesets handles the version bump and publish only).
9
+
10
+ ## [Unreleased]
11
+
12
+ The first pre-alpha release (`0.0.1`) will ship the initial public API surface. The package begins
13
+ its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` until first alpha).
14
+
15
+ ### Added
16
+
17
+ - **Phase 7: release hardening (the final roadmap phase; the CLI is feature-complete).** No new
18
+ runtime command surface: this phase is publish-readiness.
19
+ - **Fuzz gate over the two input boundaries.** `test/fuzz.property.test.ts` fuzzes the terminal
20
+ (`run`, over arbitrary argv vectors + stdin bytes) and the agent surface (`dispatchTool`, over an
21
+ arbitrary tool name + arguments), asserting neither ever throws an unhandled exception, always
22
+ resolves to a documented exit code, and never leaks a raw stack frame onto a secondary channel. The
23
+ case count scales via `CLI_FUZZ_RUNS`; a scheduled **Fuzz** workflow (`.github/workflows/fuzz.yml`)
24
+ runs it nightly at a high count, and `pnpm test:fuzz` runs it on demand.
25
+ - **Exit-code golden matrix.** `test/exit-code-matrix.test.ts` locks one representative invocation for
26
+ every code in the `0/1/2/65/66/69/70` contract, driven end-to-end through `run`, so a regression that
27
+ turns an invalid-input exit `1` into a `0` (or renumbers a code) fails CI. The exit-code map and the
28
+ stable `CLI_*` diagnostic codes are a stability surface: renaming one is a breaking change.
29
+ - **Publish dry-run proven.** A new `smoke` gate (`scripts/smoke.mjs`, wired into `verify.sh`)
30
+ exercises the **built** package (the dual ESM/CJS `.` and `./mcp` subpath exports, and **both**
31
+ `cosyte` / `cosyte-mcp` bins under `node`) and `npm publish --dry-run` assembles a clean tarball
32
+ (`dist` + `README`/`LICENSE`/`CHANGELOG`). `attw` remains a publish gate.
33
+ - **Honesty + release docs.** `docs-content/limitations.md` (wraps-not-implements, the non-goals, the
34
+ honest per-(format, operation) support matrix, the PHI-default posture), a man-page-style
35
+ `docs-content/reference-commands.md`, and `RELEASING.md` (the one-package-two-bins publish,
36
+ provenance/OIDC, the vendored-`file:`→npm dep swap, and the two standing founder stops).
37
+ - **Phase 6: six more formats + streaming + shell completion (ADR 0025).** The `cosyte` CLI now wraps
38
+ **all eight cosyte formats**, routed through a single lazy **per-format adapter registry**
39
+ (`src/core/parsers.ts`) that replaces the old per-command `hl7 ? : fhir` branches and makes support
40
+ **per (format, operation)**. An unsupported (format, op) is a value-free `CLI_FORMAT_UNSUPPORTED`,
41
+ never a fake (ADR 0018).
42
+ - **New formats and their honest capabilities.** `x12`, `astm`, `ncpdp` (SCRIPT) → **parse · inspect ·
43
+ fmt · validate**; `ccda` → **inspect · fmt** (XML re-serialize) **· validate** (`parse` deferred, no
44
+ library-blessed JSON model; XML is the canonical form); `dicom` → **inspect · validate** (`parse`/`fmt`
45
+ deferred. The model is binary); `mllp` → **parse · inspect** (a transport container the CLI de-frames
46
+ to its enclosed HL7 message(s)). Content autodetection now covers all eight (conservative + disjoint:
47
+ a leading `0x0B` VT byte routes to `mllp`, `ISA`→`x12`, an `H`-record→`astm`, `<ClinicalDocument>`→`ccda`,
48
+ a `<Message>` in the NCPDP namespace→`ncpdp`, `DICM`@128→`dicom`); `--format` accepts `mllp`.
49
+ - **Streaming / multi-message.** `parse` emits **NDJSON** with per-record isolation for inherently
50
+ multi-record inputs: an **MLLP** stream (one record per frame) and any input under the new
51
+ **`--ndjson`** flag (one record per non-empty line: the FHIR bulk-data convention). A record that
52
+ fails to parse becomes a value-free `{ record, error }` line and the stream continues; the overall
53
+ exit is a data error (`65`) if any record failed. A single message is unchanged (one pretty, or
54
+ `--json` compact, envelope + a value-free warning-count note).
55
+ - **Shell completion.** `cosyte completion <bash|zsh|fish>` prints a static, value-free completion
56
+ script generated from the command tree.
57
+ - **Dependencies. The cap stays 4 (no umbrella edit).** The six breadth parsers are vendored
58
+ **`optionalDependencies`**, lazy-loaded per format and **outside** the hard-runtime-dep closure
59
+ (ADR 0025, mirroring the MCP SDK isolation of ADR 0024). An absent optional parser degrades to a
60
+ value-free **`CLI_PARSER_UNAVAILABLE`** (exit `69`), never a crash. Pinned sibling commits: dicom
61
+ `d1ed590`, x12 `0c60606`, ccda `3753216`, ncpdp `184eecc`, astm `92ac210`, mllp `aecff75` (all
62
+ v0.0.1 except astm records-layer). Third-party CLI-core runtime deps stay **zero**.
63
+ - New diagnostic **`CLI_PARSER_UNAVAILABLE`** (exit `69`); the exit-code contract is otherwise
64
+ unchanged (`0/1/2/65/66/69/70`). New value-free inspect summaries per format; new programmatic
65
+ exports (`OP_SUPPORT`, `supportsOp`, `formatsSupporting`, `parseFormat`, `inspectFormat`, `fmtFormat`,
66
+ `validateFormat`, `deframeMllp`, `loadOptional`, `valueFreeLocator`, `DETECTABLE_FORMATS`,
67
+ `completionCommand`, and the result/summary types). The public `WIRED_FORMATS` set is **removed** in
68
+ favour of the per-op `OP_SUPPORT` matrix (pre-alpha `0.0.x` surface change).
69
+
70
+ - **Phase 5: the `cosyte-mcp` MCP server (the agent front door).** A **stdio Model Context Protocol
71
+ server** that exposes the shared command core to an LLM/agent as callable tools: the second adapter
72
+ over one core (ADR 0022, 0024). Reachable three ways: the new **`cosyte-mcp`** bin, the **`cosyte mcp`**
73
+ subcommand, and the **`@cosyte/cli/mcp`** subpath export. Tools: **`parse`**, **`validate`**,
74
+ **`inspect`**, and **`convert`**, each a thin wrapper that calls the same command handler the terminal
75
+ uses (with `--json`), so `cosyte parse` and the MCP `parse` tool agree by construction; the CLI
76
+ re-implements nothing.
77
+ - **PHI posture, inherited and hardened.** Every tool runs under the value-free posture. There is
78
+ **no** `--unsafe-show-values` door on the agent surface. A tool _result_ carries the requested data
79
+ (the parsed model / converted Bundle: the explicit request); a tool _error_ carries only the value-
80
+ free diagnostic (a stable code + positional context), never an input value. A parsed-but-invalid
81
+ `validate` verdict is a **successful** call reporting the verdict, not a tool error; only a hard
82
+ failure (unparseable / no input / usage) sets `isError`.
83
+ - **The MCP SDK is isolated and runtime-optional (ADR 0024).** `@modelcontextprotocol/sdk` (the CLI's
84
+ first and only third-party runtime dependency) is declared in **`optionalDependencies`** (pinned
85
+ `1.29.0`) and imported **only** in `src/mcp/server.ts`, reachable solely via the `./mcp` boundary
86
+ (the subpath, the `cosyte-mcp` bin, and a dynamic `import()` on the `cosyte mcp` branch). A `cosyte
87
+ parse` invocation never loads it; the core works with the SDK absent (`--omit=optional`). Because it
88
+ is not part of the hard runtime closure, the umbrella `verify-policy.json` cap on `cli` runtime
89
+ `dependencies` stays **4**: unchanged. A static isolation test proves no `core`/`commands` module
90
+ imports the SDK.
91
+ - New subpath export **`@cosyte/cli/mcp`** and new **`cosyte-mcp`** bin; new programmatic exports
92
+ (`createMcpServer`, `startStdioServer`, `dispatchTool`, `TOOL_DEFS`, and the MCP result types) on the
93
+ `./mcp` subpath. `redact`/`deid` (gated on `@cosyte/deid`) and `map-codes` are deliberately not
94
+ exposed as tools yet.
95
+ - **Phase 4: `convert` / `map-codes` (the consumer-of-consumers commands).** Two commands that wrap
96
+ the higher-layer libraries; the CLI adds **no** mapping or terminology logic of its own.
97
+ - **`convert <file|-> --to fhir [--json] [--quiet]`**: **HL7 v2 → FHIR R4** via
98
+ **`@cosyte/transform`**. Parses the input with `@cosyte/hl7`, hands the parsed message to
99
+ `transform.toFhir`, and emits the serialized FHIR **message `Bundle`** (the library's canonical
100
+ serialization) on **stdout**: `cosyte convert` equals `transform`'s programmatic output. The
101
+ conversion's value-free issues (a stable code + a v2-index → FHIRPath locator, never a field value)
102
+ render on stderr (or as a JSON envelope under `--json`); `--quiet` suppresses them. The
103
+ load-bearing rule mirrors `validate`: an **error-severity** transform issue drives exit **`1`**,
104
+ never `0`. `--to fhir` is required (the only target); a **non-HL7 source** (e.g. a FHIR document) is
105
+ a value-free `CLI_FORMAT_UNSUPPORTED` data error (`65`), never a fake conversion; an unparseable
106
+ HL7 input is `CLI_PARSE_FAILED` (`65`).
107
+ - **`map-codes <conceptmap|-> --code <code> [--system <uri>] [--version] [--display] [--json]
108
+ [--quiet]`**: translate a single source coding through a **BYO FHIR R4 ConceptMap** via
109
+ **`@cosyte/terminology`** (`$translate`). The positional is the ConceptMap document; the source
110
+ coding is named by flags. A ConceptMap and a code are **reference data, not PHI**, so the
111
+ translation result goes to **stdout**: a **match** → the target coding(s) + exit **`0`**; an
112
+ **unmapped** code → the never-fabricate `TERM_TRANSLATE_UNMAPPED` signal + exit **`1`**. A map that
113
+ is not valid JSON or not a loadable ConceptMap is the new value-free **`CLI_MAP_INVALID`** data
114
+ error (`65`), surfacing the stable terminology-loader code (e.g. `TERM_CONCEPTMAP_MALFORMED`),
115
+ never the map's bytes.
116
+ - New **`CLI_MAP_INVALID`** diagnostic code. New programmatic exports: `convertCommand`,
117
+ `convertOutcome`, `mapCodesCommand`. New runtime dependencies (ADR 0023): **`@cosyte/transform`**
118
+ (`e6c4531`, v0.0.0) and **`@cosyte/terminology`** (`e5ed368`, v0.0.1) as **hard, first-party,
119
+ lazy-loaded** deps: vendored as `pnpm pack` tarballs under `vendor/` until PUB-FLIP
120
+ (`pnpm vendor:refresh`; umbrella ADR 0008). The umbrella `verify-policy.json` cap on `cli` runtime
121
+ deps was raised **2 → 4**; third-party CLI-core runtime deps stay **zero** (both siblings are
122
+ lazy-loaded per command, so the `parse` fast path never loads them).
123
+ - **ADR `0023`**: wire `@cosyte/transform` + `@cosyte/terminology`; the deliberate 2 → 4 cap raise
124
+ (amends ADR 0021).
125
+ - **Phase 3: `validate` / `inspect` / `fmt`.** Three commands over the two wired parsers
126
+ (HL7 v2 + FHIR R4), each a thin wrapper that re-implements no library logic.
127
+ - **`validate <file|-> [--profile] [--json] [--quiet]`**: parse + run the wrapped parser's own
128
+ validation surface, with the **verdict in the exit code**: `0` valid, **`1` invalid** (parseable
129
+ but non-conformant), `65` unparseable, `66` no input, `2` usage. The load-bearing rule: a
130
+ validation failure is **never** exit `0`; "unparseable" (`65`) is a distinct signal from "parsed
131
+ but invalid" (`1`). Findings are **value-free**: a stable code, a severity, and a positional
132
+ locator (a FHIRPath, or an HL7 segment/field index), on stderr by default, or as value-free JSON
133
+ on stdout under `--json`; `--quiet` makes the exit code the whole signal. The CLI invents **no**
134
+ verdict: FHIR validity is `@cosyte/fhir`'s `validateResource().valid` (plus any error-severity
135
+ read issue); HL7 validity is "parseable" (its warnings are non-fatal by the library's design:
136
+ surfaced, never failing). **`--profile` is gated** to an honest `CLI_NOT_IMPLEMENTED` (exit `69`):
137
+ the CLI bundles no profiles yet, so it never fakes or silently drops a profile verdict.
138
+ - **`inspect <file|-> [--json]`**: a **value-free structural summary**: HL7 message type, version,
139
+ per-segment-type counts, and a warning count; FHIR `resourceType`, Bundle entry counts by type, and
140
+ a read-issue count. Counts and structural type codes only, never a field value.
141
+ - **`fmt <file|->`**: **canonical re-serialization** via the wrapped library's spec-clean
142
+ serializer (`Hl7Message.toString()` / `serializeResource`); its stdout **is** the data channel. An
143
+ unparseable input is a data error (`65`) with **no partial emit**.
144
+ - New `EXIT.INVALID` (`1`): the `validate` verdict code (the exit-code contract is now
145
+ `0/1/2/65/66/69/70`). All four commands share one input + format front door (`core/resolveInput`)
146
+ and one value-free parser-failure boundary (`core/wrap`), so the value-free-by-default posture and
147
+ the `--unsafe-show-values` chokepoint apply uniformly; `parse` was refactored onto the shared
148
+ helpers (behavior-preserving). New programmatic exports: `validateCommand`, `inspectCommand`,
149
+ `fmtCommand`, `resolveInput`, `parseFailureResult`, `formatHl7Position`, `errorResult`. No new
150
+ runtime dependencies: stays within the cap of 2.
151
+ - **Phase 2: PHI posture hardened + `redact`/`deid` + `--unsafe-show-values`.**
152
+ - **`--unsafe-show-values`**: a global, opt-in, PHI-exposing flag, resolved once and order-
153
+ independently and funnelled through a **single chokepoint** (`core/phi.ts`), so the "a value
154
+ reaches a secondary surface **iff** the flag is set" property holds in one place. Off by default;
155
+ with it set, a `CLI_PARSE_FAILED` diagnostic appends a bounded, single-line excerpt of the
156
+ offending input. Every other surface stays value-free, and a successful parse still puts values
157
+ only on the stdout data channel.
158
+ - **`redact` / `deid` (`<file|->`, `--format`)**: the de-identification command, shipped as an
159
+ **honest, typed `CLI_NOT_IMPLEMENTED`** (new exit code `69`, `EX_UNAVAILABLE`). It is **gated on
160
+ `@cosyte/deid`** (unpublished, `DEID-1` in flight), **never reads the input**, and never emits a
161
+ partial Safe-Harbor scrub presented as de-identified. A built-in redactor is **deliberately
162
+ withheld**: a partial scrub over only the obvious PHI loci would leave PHI behind and present a
163
+ false-safety impression (the cardinal hazard). It delegates to `@cosyte/deid` via a documented
164
+ seam (`core/deid.ts`) when that library ships and is vetted.
165
+ - **Never a PHI temp file / never a file log**: proven by test (no command creates a file in the
166
+ working directory) and by design (commands return a `RunResult`; only the thin `bin` writes to
167
+ process streams).
168
+ - New `CLI_NOT_IMPLEMENTED` diagnostic code and `EXIT.UNAVAILABLE` (`69`); new programmatic exports
169
+ (`PhiPosture`, `VALUE_FREE`/`SHOW_VALUES`, `extractPhiPosture`, `unsafeInputSuffix`, `deidStatus`,
170
+ `redactCommand`).
171
+ - **Phase 1: the `cosyte parse` foundation.** Reshaped the scaffold from a library skeleton into a
172
+ **`bin` package**: `package.json#bin` maps `cosyte` → `dist/bin/cosyte.mjs` (a shebang entry over a
173
+ testable `core`), argument-parsed with Node's built-in `util.parseArgs` + a hand-rolled subcommand
174
+ dispatcher (**no third-party CLI framework**).
175
+ - **`cosyte parse <file|->`**: reads a file argument or stdin (`-`); **autodetects the format by
176
+ content** (HL7 v2 `MSH` framing, FHIR JSON `resourceType`): conservative and fail-safe (a confident
177
+ single match parses; ambiguity/no-match is a data error asking for `--format`, never a guess); routes
178
+ to the wrapped parser (**lazy-loaded** per format); emits the parsed model as **typed JSON on
179
+ stdout**. Flags: `--format`, `--json`, `--quiet`, `--no-color`.
180
+ - **The exit-code contract** (`sysexits.h`-grounded, documented, tested): `0` success · `2` usage ·
181
+ `65` data/parse error (`EX_DATAERR`) · `66` no input (`EX_NOINPUT`) · `70` internal (`EX_SOFTWARE`).
182
+ The CLI never exits `0` on input it could not handle.
183
+ - **Value-free diagnostic channel** with stable `CLI_*` codes (`CLI_FORMAT_UNDETECTED`,
184
+ `CLI_FORMAT_AMBIGUOUS`, `CLI_FORMAT_UNSUPPORTED`, `CLI_NO_INPUT`, `CLI_EMPTY_INPUT`,
185
+ `CLI_PARSE_FAILED`, `CLI_USAGE`, `CLI_INTERNAL`). **stdout is the data channel; every stderr line is
186
+ value-free**: code + position only, never a field value. No temp files, no file logging.
187
+ - **Programmatic `core` API** (the `.` subpath): `detectFormat` / `classifyCandidates` /
188
+ `detectionError`, `EXIT`, `CLI_CODES` / `CliError`, `run`, `parseCommand`, `VERSION`.
189
+ - **Runtime dependencies (ADR 0021):** `@cosyte/hl7` (`46d50eb`, v0.0.1) and `@cosyte/fhir` (`7a099b2`,
190
+ v0.0.0) as **hard, first-party** deps (an `npx` bin cannot peer-depend) vendored as `pnpm pack`
191
+ tarballs under `vendor/` until PUB-FLIP (`pnpm vendor:refresh`; umbrella ADR 0008). Capped at **2**
192
+ by the umbrella `verify-policy.json`; third-party CLI-core runtime deps stay **zero**.
193
+ - **ADRs:** `0021` (developer-tooling tier is a `bin` that hard-depends on first-party siblings;
194
+ third-party runtime deps minimized) and `0022` (one repo, two bins: the CLI and the future
195
+ `cosyte-mcp` MCP server over one core; the web playground is out of scope).
196
+
197
+ ### Changed
198
+
199
+ - **The documented per-(format, operation) support matrix now states its negatives in words.** In
200
+ `docs-content/limitations.md` the table used a bare `U+2014` **as a cell value meaning "not
201
+ supported"**. The em-dash sweep below rewrote it as punctuation, so `dicom` `fmt`, `mllp` `fmt` and
202
+ `mllp` `validate` rendered as a stray colon: **"support absent" silently became "support unstated"**,
203
+ on the page whose whole job is honest capability disclosure, in a form a reader would read as a
204
+ rendering artifact rather than as a claim. Caught by the conformance refuter, not by CI, and nothing
205
+ in this repo's CI could have caught it (`test/docs-content.test.ts` only executes runnable `ts`
206
+ blocks, and Prettier's glob does not cover `docs-content/`). Those three cells now read
207
+ `not supported`. **The support facts are unchanged; the page now says them.** Before sweeping any
208
+ repo for this character, find the places it is a value rather than punctuation and convert those by
209
+ hand, to a word, first.
210
+
211
+ - **No cosyte surface in this repo uses an em dash any more, and a CI gate keeps it that way.** The
212
+ brand rule (`knowledgebase/06-brand/voice-and-tone.md`, "No em dashes. Ever.") bans `U+2014`
213
+ outright and names commit messages explicitly. Measured byte-level over **all 124 tracked files**,
214
+ not over markdown alone: **659 occurrences across 87 files**, all as the literal character and none
215
+ in an encoded form. **61 of the 87 are not markdown** (26 are), and one is `package.json`, whose
216
+ `description` is published to npm and rendered on the package page. Every occurrence was rewritten
217
+ in place with a period, a colon, a comma, or parentheses, never re-encoded. Consumer-visible text
218
+ that changed wording: the npm description, `README.md`, `docs-content/`, `cosyte --help`, the
219
+ generated `bash`/`zsh`/`fish` completion scripts, and the `redact`/`deid` and reserved `--profile`
220
+ unavailability messages. **No stable code, exit code, flag name, or JSON field changed**, so nothing
221
+ branching on an exit code or parsing `--json` is affected. Commit subjects already on `main` are
222
+ left alone: the message half runs on `pull_request` only, and history is not rewritten.
223
+ - **The gate.** `scripts/check-no-emdash.sh` (`pnpm check:no-emdash`) plus
224
+ `.github/workflows/no-emdash.yml` scan both halves the rule covers: every tracked file, and the
225
+ pull request title, body, and commit messages, on the non-default `edited` trigger so retitling a
226
+ pull request re-checks it. The new `no-emdash` check-run context is required by the
227
+ `ci-required-checks` ruleset.
228
+ - **Binary handling, which is why this repo does not run the text-only variant its siblings run.**
229
+ `cli` tracks eleven binaries (the ten `vendor/cosyte-*.tgz` packed sibling dependencies and
230
+ `test/__fixtures__/sample.dcm`), so the script partitions on an explicit NUL-byte rule rather than
231
+ on `grep -I`'s heuristic, which would silently skip a genuine text file with a broken encoding.
232
+ The reason is measured rather than precautionary: `vendor/cosyte-hl7-0.0.0.tgz` **already
233
+ contains the byte sequence `E2 80 94`** by compression coincidence, so a text-only scan reds on it
234
+ today, naming a compressed byte stream nobody wrote and offering no way to fix it.
235
+ - **Disclosed limits, in the script header rather than left implicit.** A tracked _text_ file
236
+ holding a NUL byte is exempt and its em dash would be missed (there is none today; the excluded
237
+ count on the OK line is the tell). Encoded-form matching is literal, so lowercase `%e2%80%94`, a
238
+ capital-X `&#X2014;`, and a semicolon-less `&#x2014` pass. The scan reads file contents, never
239
+ file names.
240
+
241
+ - **The published type declarations no longer carry internal project bookkeeping.** `dist/index.d.ts`
242
+ and `dist/mcp.d.ts` (and their `.d.cts` twins) are compiled from the JSDoc on every exported symbol,
243
+ and that JSDoc was citing item identifiers (`CLI-6`), ADR numbers (`ADR 0018`, `0021`, `0022`,
244
+ `0025`), the meta-repo roadmap in prose and by path (`cli roadmap §7`,
245
+ `operations/roadmaps/cli.md`), and phase language (`§Phase 4`, `Phase-5`, "in a later phase"). All
246
+ of it rendered on hover for anyone who installed the package. **Measured on `62fba77` with the rule
247
+ set that ships alongside: 61 occurrences across 23 tracked `src/` files, producing 56 across the
248
+ three declaration files a consumer receives (43 in `dist/index.d.ts`, 12 in `dist/mcp.d.ts` and 1
249
+ in the shared `dist/io-*.d.ts` chunk both entries import), each mirrored in its `.d.cts` twin. Now
250
+ 0 on every one of them.** (The shared chunk is easy to miss and was missed once here: a count that
251
+ scans only the named entry points under-reports.) Every removal is a cut, not a rewrite: the
252
+ surrounding guarantees (value-free stderr, the never-a-fake `CLI_FORMAT_UNSUPPORTED`, the gated
253
+ `redact`, the exit-code contract) are worded exactly as strongly as before. Internal traceability
254
+ stays where the convention puts it: this file, the changesets, the commits and the roadmap.
255
+ - **A dependency-budget figure left `src/core/deid.ts` too.** The module said wiring `@cosyte/deid`
256
+ "would breach the CLI's runtime-dep cap (2)". Only the number was stale: the cap has been 4 since
257
+ `convert`/`map-codes` landed, and the constraint itself still holds, because the package declares
258
+ exactly 4 hard runtime dependencies and a fifth would indeed breach it. The clause went because a
259
+ dependency-budget figure is internal bookkeeping a consumer cannot act on, not because the
260
+ constraint lapsed. The reasons that actually ground the refusal to ship a built-in redactor are
261
+ untouched: `@cosyte/deid` is unpublished, the wrapped parsers expose no de-identification API, and
262
+ a partial scrub would present a false-safety impression.
263
+ - **`CosyteFormat`'s documentation no longer understates autodetection.** It read as though content
264
+ detection recognised only HL7 v2 and FHIR, with the other six accepted by `--format` but "not yet
265
+ wired", which has not been true since all eight formats gained signatures. The stale sentence is
266
+ removed rather than restated.
267
+ - **A gate now enforces the public-surface rule, which is why the class stops regrowing.**
268
+ `scripts/check-no-internal-refs.sh` (`pnpm check:no-internal-refs`, on the `verify.sh cli` ladder)
269
+ plus `.github/workflows/no-internal-refs.yml` port the shape of `hl7`'s gate
270
+ ([hl7#62](https://github.com/cosyte/hl7/pull/62), [hl7#64](https://github.com/cosyte/hl7/pull/64))
271
+ and `ncpdp`'s ([ncpdp#36](https://github.com/cosyte/ncpdp/pull/36)) rather than the file. Four
272
+ passes: the public markdown surface line by line and paragraph-joined, the npm metadata, `src/`
273
+ doc comments, and `src/` string literals (the pass that would have caught the two identifiers this
274
+ package printed to a user's terminal). Seven rules. Re-derived for this repo: the scan surface, a
275
+ standards-designation exclusion list covering **all eight** formats (this is the package where
276
+ `HL7-V2`, `FHIR-R4`, `DICOM-SR`, `NCPDP-SCRIPT`, `X12-837P`, `CCDA-R2.1`, `MSH-2`, `NM1-03`,
277
+ `439-E4` and `ICD-10-CM` are live at once, so the `WORD-N` trap is at its widest), and a seventh
278
+ rule no sibling has, a **prose roadmap citation**, which was 30 of the 61 and which `hl7`'s
279
+ path-keyed rule cannot see. Both self-test halves run on every invocation: positive samples prove
280
+ each rule still matches, negative samples prove none has been widened into the `WORD-N` shape that
281
+ would delete the reference material the CLI's docs exist to provide. `CHANGELOG.md` is excluded on
282
+ purpose, as it is in `hl7` and `ncpdp`: it ships inside the npm tarball, yet the convention names it
283
+ as one of the places identifiers belong. That contradiction is ecosystem-wide, and it is recorded
284
+ here rather than settled by one repo.
285
+ - **`redact`/`deid` no longer names an internal tracking identifier on any consumer surface.** The
286
+ `CLI_NOT_IMPLEMENTED` text printed when de-identification is unavailable carried an internal work
287
+ item that means nothing to anyone running the command, and the same identifier reached the
288
+ published type declarations (`dist/index.d.ts` / `dist/index.d.cts`), where it surfaces in editor
289
+ tooltips. Both now state only the consumer-observable fact: the command delegates to
290
+ `@cosyte/deid`, which is unpublished, and the CLI ships no built-in redactor because a partial
291
+ scrub would present a false-safety impression. The stable `CLI_NOT_IMPLEMENTED` code and the exit
292
+ `69` are unchanged.
293
+ - **`CLI_PARSER_UNAVAILABLE`'s message no longer cites an internal decision record.** The error
294
+ raised when an optional per-format parser is not installed pointed at an ADR number, which is
295
+ meaningless to a caller; it now just names the package to install. The stable
296
+ `CLI_PARSER_UNAVAILABLE` code and the exit `69` are unchanged. The same sweep removed the
297
+ remaining roadmap-phase language from `README.md` and `docs-content/troubleshooting.md`.
298
+ - **The CI checks are now binding on `main`.** `ci / verify (22, ubuntu-latest)`,
299
+ `ci / verify (24, ubuntu-latest)`, `ci / actionlint` and `codeql / analyze (javascript-typescript)`
300
+ are required status checks, each pinned to the `github-actions` app, alongside branch deletion and
301
+ force-push protection. They were advisory before: a red check could not stop a merge. Dependabot
302
+ now watches the npm and GitHub Actions dependency surfaces weekly, which nothing did previously.
303
+ - **Reshaped the package from the parser-library scaffold to a `bin` package.** Removed the archetype
304
+ stubs (`parseCli`, `WARNING_CODES`, `FATAL_CODES`); replaced the library `src/index.ts` and the
305
+ round-trip property test with the command tree, the programmatic `core` API, and command-contract /
306
+ autodetection / PHI-leak / equivalence tests. Rewrote `docs-content/` and `README.md` for the CLI.
307
+
308
+ ### Deprecated
309
+
310
+ ### Removed
311
+
312
+ ### Fixed
313
+
314
+ - **README + guides now describe the shipped Phase-3 command surface.** The `README.md` and
315
+ `docs-content/guides-overview.md` "Status" blurbs read as a Phase-1-forward roadmap ("Phase 1 ships
316
+ `parse`…"); they now state the current surface directly (`parse` / `validate` / `inspect` / `fmt`
317
+ and the gated `redact`/`deid`) over the two wired parsers (HL7 v2 + FHIR R4). The pre-alpha,
318
+ not-yet-published-to-npm status is unchanged (accurate), and the `npx`/`npm install -g` examples now
319
+ carry a "not on npm yet" caveat (docs-only; README-ORG-SWEEP).
320
+ - **`phi-scan` now scans the real fixture directory.** The scanner's fixture root pointed at a
321
+ nonexistent `test/fixtures/`; it now walks `test/__fixtures__/` (and the same path in the staged
322
+ filter), so the PHI commit-gate actually covers the CLI's synthetic fixtures.
323
+
324
+ ### Security
325
+
326
+ [Unreleased]: https://github.com/cosyte/cli/commits/main
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cosyte
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,265 @@
1
+ # @cosyte/cli
2
+
3
+ > The **cosyte CLI**: a PHI-safe developer front door over the `@cosyte/*` healthcare parsers.
4
+
5
+ `@cosyte/cli` is a **`bin` package**: its primary artifact is the `cosyte` command on your `PATH`. Pipe
6
+ a raw message from a hospital feed into the terminal and get typed, structured JSON back in one line,
7
+ without writing code, without reading the spec, and **without ever being handed a confident wrong value
8
+ or a silent success on a malformed message**.
9
+
10
+ ```bash
11
+ cat adt.hl7 | cosyte parse -
12
+ ```
13
+
14
+ It is a thin, honest skin over libraries that already own correctness ([`@cosyte/hl7`](https://github.com/cosyte/hl7),
15
+ [`@cosyte/fhir`](https://github.com/cosyte/fhir), [`@cosyte/transform`](https://github.com/cosyte/transform),
16
+ [`@cosyte/terminology`](https://github.com/cosyte/terminology)): it routes, reads, and shapes output,
17
+ and owns two disciplines of its own: a documented **exit-code contract** and a **value-free
18
+ diagnostic** posture.
19
+
20
+ > **Status:** pre-alpha (`0.0.x`), **not yet published to npm**. The `cosyte` command wraps **all eight
21
+ > cosyte formats** (**HL7 v2**, **FHIR R4**, **X12**, **ASTM**, **NCPDP SCRIPT**, **C-CDA**, **DICOM**,
22
+ > and **MLLP**) plus the `@cosyte/transform` and `@cosyte/terminology` higher-layer libraries, with
23
+ > conservative content-format autodetection and a documented exit-code contract:
24
+ >
25
+ > - **`parse`**: autodetect the format and print the parsed model as typed JSON on stdout. Multi-record
26
+ > inputs (an **MLLP** stream, or any input under **`--ndjson`**) stream as **NDJSON** with per-record
27
+ > isolation.
28
+ > - **`validate`**: parse, then run the wrapped parser's own validation surface, with the **verdict in
29
+ > the exit code** (`0` valid · `1` invalid · `65` unparseable); findings are value-free.
30
+ > - **`inspect`**: a value-free structural summary (type/classification codes + structural counts).
31
+ > - **`fmt`**: canonical re-serialization through the library's spec-clean serializer; no partial emit
32
+ > on unparseable input.
33
+ > - **`convert`**: HL7 v2 → FHIR R4 via `@cosyte/transform`; the converted `Bundle` on stdout, value-
34
+ > free issues on stderr, and a non-zero exit on an error-severity conversion issue.
35
+ > - **`map-codes`**: translate a code through a BYO FHIR ConceptMap via `@cosyte/terminology`; the
36
+ > target coding(s) on stdout, or a value-free unmapped signal + exit `1`.
37
+ > - **`redact` / `deid`**: gated to an honest `CLI_NOT_IMPLEMENTED` (exit `69`) until `@cosyte/deid`
38
+ > ships; it never reads the input and never emits a partial scrub dressed up as de-identified.
39
+ > - **`completion <bash|zsh|fish>`**: print a static shell completion script.
40
+ >
41
+ > **Support is honest per (format, operation)**: not every parser faithfully supports every command, so
42
+ > the deferred cells (DICOM `parse`/`fmt`: binary model; C-CDA `parse`: XML is the canonical `fmt`
43
+ > surface; MLLP `fmt`/`validate`) are a value-free `CLI_FORMAT_UNSUPPORTED`, never a fake. PHI discipline
44
+ > runs throughout: value-free by default across every diagnostic, the loud opt-in `--unsafe-show-values`
45
+ > as the single door to a value on a secondary surface, and never a temp file with PHI. An **MCP server**
46
+ > (`cosyte-mcp`) exposes the same core to an LLM/agent as callable tools.
47
+ >
48
+ > The CLI is **feature-complete and release-hardened**: an argv+stdin+MCP fuzz
49
+ > gate, an exit-code golden matrix, a built-package smoke of both bins, and a clean `npm publish`
50
+ > dry-run. What remains is the two standing founder stops (flipping the repo public and the real
51
+ > `npm publish`) plus swapping the vendored sibling deps for real `@cosyte/*` npm ranges at that flip.
52
+ > See [RELEASING.md](./RELEASING.md).
53
+
54
+ ## Run it
55
+
56
+ > Not on npm yet. The commands below are how you'll install and run it **once it's published**.
57
+ > Until then, run it from a local checkout (`pnpm build`, then invoke `dist/bin/cosyte.mjs`).
58
+
59
+ ```bash
60
+ npx @cosyte/cli parse message.hl7 # no install; format autodetected → HL7 v2
61
+ npm install -g @cosyte/cli # or put `cosyte` on your PATH
62
+ cosyte --help
63
+ ```
64
+
65
+ ## `cosyte parse`
66
+
67
+ Read a file (or stdin via `-`), autodetect the format by content, and print the parsed model as typed
68
+ JSON on stdout:
69
+
70
+ ```bash
71
+ cosyte parse message.hl7 # → { "format": "hl7", "model": …, "warnings": [] }
72
+ cat patient.json | cosyte parse - # from a pipeline
73
+ cosyte parse --json message.hl7 | jq '.model' # compact machine output
74
+ cosyte parse --format hl7 msg.txt # override autodetection
75
+ cosyte parse stream.mllp # MLLP frames → one NDJSON record per frame
76
+ cosyte parse bulk.ndjson --ndjson # one FHIR resource per line → NDJSON records
77
+ ```
78
+
79
+ Autodetection is **conservative**: a confident single match parses; ambiguity or no match is a typed
80
+ data error asking for `--format`, **never a guessed parser**. `--format` accepts `hl7 | fhir | x12 |
81
+ astm | ncpdp | ccda | dicom | mllp`.
82
+
83
+ **Streaming / multi-message.** A single message prints one JSON envelope. A multi-record input (an
84
+ **MLLP** stream, one record per frame, or any input under **`--ndjson`**, one record per non-empty
85
+ line) streams as **NDJSON**, one `{ record, format, model, warnings }` line each, with per-record
86
+ isolation: a record that fails to parse becomes a value-free `{ record, error }` line and the stream
87
+ continues; the overall exit is a data error (`65`) if any record failed.
88
+
89
+ Support is honest **per (format, operation)**: `x12`/`astm`/`ncpdp` support all of parse/inspect/fmt/
90
+ validate; `ccda` supports inspect/fmt/validate (parse deferred); `dicom` supports inspect/validate
91
+ (parse/fmt deferred); `mllp` supports parse/inspect. A deferred cell is a value-free
92
+ `CLI_FORMAT_UNSUPPORTED`, never a fake result.
93
+
94
+ ## `cosyte validate`
95
+
96
+ Parse the input and run the wrapped parser's own validation surface, with the **verdict in the exit
97
+ code**, so it drops straight into a CI gate:
98
+
99
+ ```bash
100
+ cosyte validate message.hl7 # exit 0 valid · 1 invalid · 65 unparseable
101
+ cosyte validate patient.json --json # value-free { format, valid, findings } on stdout
102
+ cosyte validate patient.json --quiet # no output: the exit code is the whole signal
103
+ ```
104
+
105
+ Findings are **value-free**: a stable code, a severity, and a positional locator (a FHIRPath, or an
106
+ HL7 segment/field index), never a field value. The verdict is the wrapped library's, never invented:
107
+ FHIR validity is `@cosyte/fhir`'s `validateResource()`; an HL7 message is valid when it parses (its
108
+ warnings are non-fatal deviations, surfaced but never failing). `--profile` is reserved: the CLI
109
+ bundles no profiles yet, so it reports an honest "unavailable" (exit `69`) rather than fake a verdict.
110
+
111
+ The load-bearing rule: a validation failure is **never exit 0**, and "unparseable" (`65`) is a distinct
112
+ signal from "parsed, but invalid" (`1`).
113
+
114
+ ## `cosyte inspect`
115
+
116
+ A value-free structural summary, the "what shape is this?" answer, with no field value:
117
+
118
+ ```bash
119
+ cosyte inspect message.hl7 # message type, version, per-segment counts, warning count
120
+ cosyte inspect bundle.json --json
121
+ ```
122
+
123
+ ## `cosyte fmt`
124
+
125
+ Canonically re-serialize through the wrapped library's spec-clean serializer (HL7 CR-separated;
126
+ FHIR canonical JSON, decimals byte-exact). Its stdout **is** the data channel; an unparseable input is
127
+ a data error with **no partial emit**:
128
+
129
+ ```bash
130
+ cat messy.json | cosyte fmt - # → canonical FHIR JSON on stdout
131
+ cosyte fmt message.hl7 # → spec-clean HL7
132
+ ```
133
+
134
+ ## `cosyte convert`
135
+
136
+ Convert an **HL7 v2** message to **FHIR R4** via [`@cosyte/transform`](https://github.com/cosyte/transform).
137
+ The converted FHIR message `Bundle` is your explicit request, so it goes to **stdout**; the
138
+ conversion's value-free issues (a v2 index → FHIRPath locator + a stable code, never a field value) go
139
+ to stderr:
140
+
141
+ ```bash
142
+ cosyte convert adt.hl7 --to fhir # → a FHIR message Bundle on stdout
143
+ cat oru.hl7 | cosyte convert - --to fhir | jq '.entry[].resource.resourceType'
144
+ cosyte convert adt.hl7 --to fhir --json # { format, bundle, findings } on stdout
145
+ cosyte convert adt.hl7 --to fhir --quiet # bundle only; the exit code carries the outcome
146
+ ```
147
+
148
+ `--to fhir` is required (the only target today). The CLI adds no mapping of its own. The FHIR is
149
+ `@cosyte/transform`'s, faithfully surfaced. The load-bearing rule mirrors `validate`: an
150
+ **error-severity** conversion issue exits **`1`**, never `0`. A non-HL7 input (e.g. a FHIR document) is
151
+ a data error (`65`), never a fake conversion.
152
+
153
+ ## `cosyte map-codes`
154
+
155
+ Translate a single code through a **bring-your-own FHIR ConceptMap** via
156
+ [`@cosyte/terminology`](https://github.com/cosyte/terminology) (`$translate`). A ConceptMap and a code
157
+ are reference data, not PHI, so the translation result is your explicit request on **stdout**:
158
+
159
+ ```bash
160
+ cosyte map-codes gender.conceptmap.json \
161
+ --system http://hl7.org/fhir/administrative-gender --code male # → the target coding(s), exit 0
162
+ cat cm.json | cosyte map-codes - --code female --json # compact { source, result }
163
+ ```
164
+
165
+ The CLI ships no terminology content and never fabricates a target: a **match** is exit `0`; an
166
+ **unmapped** code is the value-free `TERM_TRANSLATE_UNMAPPED` signal + exit `1`; a map that is not
167
+ valid JSON or not a loadable ConceptMap is a `CLI_MAP_INVALID` data error (`65`).
168
+
169
+ ## The exit-code contract
170
+
171
+ Every command is safe to branch on in CI. The exit code carries the outcome (`sysexits.h`):
172
+
173
+ | Code | Meaning |
174
+ | ---- | ---------------------------------------------------------- |
175
+ | `0` | success / **valid** (`validate`) |
176
+ | `1` | **invalid**: `validate` found a parseable-but-bad message |
177
+ | `2` | usage error (unknown flag, missing argument) |
178
+ | `65` | data error (unparseable input, or format undetected) |
179
+ | `66` | no input (missing/unreadable file) |
180
+ | `69` | unavailable (a capability is not yet built, e.g. `redact`) |
181
+ | `70` | internal error (a bug) |
182
+
183
+ The load-bearing rule: the CLI **never prints a reassuring line and exits `0`** on input it could not
184
+ handle, or on an invalid message.
185
+
186
+ ## PHI posture
187
+
188
+ A CLI operates on real files a developer points at. So the channels are split: **stdout is the data
189
+ channel** (`parse` prints the parsed model there because that is your explicit request) while **every
190
+ diagnostic on stderr is value-free** (a stable code, a position, a file path, never a name, DOB, MRN,
191
+ or field value). The CLI writes no temp files and logs to no file.
192
+
193
+ ### `--unsafe-show-values`
194
+
195
+ Value-free-by-default is the whole point, but when you are debugging a rejected message locally, you
196
+ sometimes need to see the bytes. `--unsafe-show-values` is the **single, loud, opt-in door**: with it
197
+ set, a `CLI_PARSE_FAILED` diagnostic appends a bounded excerpt of the offending input. It is off by
198
+ default, it is **PHI-exposing** (the name carries the warning), and it is the _only_ configuration
199
+ under which a value can reach a secondary surface. A successful parse still puts values only on
200
+ stdout, never on stderr.
201
+
202
+ ```bash
203
+ cosyte parse broken.hl7 --format hl7 # value-free: a code + position only
204
+ cosyte parse broken.hl7 --format hl7 --unsafe-show-values # appends a bounded input excerpt (PHI!)
205
+ ```
206
+
207
+ ## `cosyte redact` / `cosyte deid`
208
+
209
+ De-identification is the one operation whose _job_ is to strip PHI. It is **not implemented yet, on
210
+ purpose.** It belongs to [`@cosyte/deid`](https://github.com/cosyte/deid), which is not published, and
211
+ the wrapped parsers expose no de-identification API. A built-in "minimal Safe-Harbor" pass over only
212
+ the obvious fields would leave PHI behind and _look_ de-identified while silently under-redacting: the
213
+ exact false-safety hazard `redact` exists to avoid. So `cosyte redact <file>` is an honest, typed
214
+ `CLI_NOT_IMPLEMENTED` (exit `69`, `EX_UNAVAILABLE`) that **never reads your input** and **never emits a
215
+ partial scrub dressed up as safe**. It will produce a real de-identified copy once `@cosyte/deid`
216
+ ships and is vetted.
217
+
218
+ ## `cosyte completion`
219
+
220
+ Print a shell completion script generated from the command tree, and source it:
221
+
222
+ ```bash
223
+ source <(cosyte completion bash) # bash
224
+ source <(cosyte completion zsh) # zsh
225
+ cosyte completion fish | source # fish
226
+ ```
227
+
228
+ ## The MCP server (agent front door)
229
+
230
+ The same `core` is exposed to an **LLM/agent** as a [Model Context Protocol](https://modelcontextprotocol.io)
231
+ server: the second adapter over one codebase (the terminal is the first). It is a **local stdio
232
+ subprocess**, not a hosted endpoint. Register it in an MCP client's config:
233
+
234
+ ```json
235
+ {
236
+ "mcpServers": {
237
+ "cosyte": { "command": "npx", "args": ["-y", "@cosyte/cli", "mcp"] }
238
+ }
239
+ }
240
+ ```
241
+
242
+ `cosyte mcp` and the standalone `cosyte-mcp` bin both start the stdio server. It exposes four tools
243
+ (`parse`, `validate`, `inspect`, `convert`), each calling the same command the terminal runs, so the CLI
244
+ and the agent get identical results. The PHI posture is inherited and hardened: there is **no
245
+ `--unsafe-show-values` door on the agent surface**, a tool _result_ carries the requested data, and a
246
+ tool _error_ carries only value-free diagnostics (a stable code + position, never an input value). A
247
+ parsed-but-invalid `validate` is a **successful** call reporting the verdict, not a tool error.
248
+
249
+ The MCP SDK (`@modelcontextprotocol/sdk`) is the CLI's only third-party runtime dependency; it is
250
+ declared **optional** and loaded only on the MCP path, so a `cosyte parse` invocation never pulls it and
251
+ the core works with the SDK absent. The server surface is importable via the `@cosyte/cli/mcp` subpath
252
+ (`createMcpServer`, `startStdioServer`, `dispatchTool`, `TOOL_DEFS`).
253
+
254
+ ## Programmatic API
255
+
256
+ The same `core` is importable (the `.` subpath): `detectFormat`, the `EXIT` map (now including
257
+ `EXIT.INVALID`), the `CLI_CODES` diagnostic registry, `resolveInput`, `run`, and each command
258
+ (`parseCommand`, `validateCommand`, `inspectCommand`, `fmtCommand`, `convertCommand`, `mapCodesCommand`,
259
+ `redactCommand`, plus `convertOutcome` for the conversion verdict). The **`./mcp` subpath** exports the
260
+ agent adapter (`createMcpServer`, `startStdioServer`, `dispatchTool`, `TOOL_DEFS`). See the docs for the
261
+ full surface.
262
+
263
+ ## License
264
+
265
+ MIT © Cosyte