@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 +326 -0
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/dist/bin/cosyte-mcp.cjs +1160 -0
- package/dist/bin/cosyte-mcp.cjs.map +1 -0
- package/dist/bin/cosyte-mcp.d.cts +1 -0
- package/dist/bin/cosyte-mcp.d.ts +1 -0
- package/dist/bin/cosyte-mcp.mjs +1158 -0
- package/dist/bin/cosyte-mcp.mjs.map +1 -0
- package/dist/bin/cosyte.cjs +1785 -0
- package/dist/bin/cosyte.cjs.map +1 -0
- package/dist/bin/cosyte.d.cts +1 -0
- package/dist/bin/cosyte.d.ts +1 -0
- package/dist/bin/cosyte.mjs +1783 -0
- package/dist/bin/cosyte.mjs.map +1 -0
- package/dist/index.cjs +1462 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1360 -0
- package/dist/index.d.ts +1360 -0
- package/dist/index.mjs +1414 -0
- package/dist/index.mjs.map +1 -0
- package/dist/io-B2EobRzC.d.cts +57 -0
- package/dist/io-B2EobRzC.d.ts +57 -0
- package/dist/mcp.cjs +1161 -0
- package/dist/mcp.cjs.map +1 -0
- package/dist/mcp.d.cts +143 -0
- package/dist/mcp.d.ts +143 -0
- package/dist/mcp.mjs +1155 -0
- package/dist/mcp.mjs.map +1 -0
- package/package.json +131 -0
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 `—`, and a semicolon-less `—` 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
|