@cosyte/cli 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,19 +48,17 @@ releases, not a fault in any command.
48
48
  sibling packages are now declared as real npm ranges, and the resulting tarball has been installed from
49
49
  outside this repository and exercised, which is the check a `npm publish --dry-run` cannot perform.
50
50
 
51
- **One dependency could not be made real, and it costs you FHIR support.** `@cosyte/fhir` is not on the
52
- npm registry, so it cannot be a dependency of this package at all; `@cosyte/transform` is on npm but
53
- requires it, so npm skips that too. In an installed copy:
54
-
55
- - FHIR `parse` / `inspect` / `fmt` / `validate` and the `convert` command report a value-free
56
- `CLI_PARSER_UNAVAILABLE` and exit `69`. They never guess, and they never blame your input.
57
- - HL7 v2, `map-codes`, and the six breadth formats (X12, C-CDA, DICOM, NCPDP, ASTM, MLLP) all work.
58
-
59
- To use the FHIR commands today, run the CLI from a source checkout (`pnpm install && pnpm build`, then
60
- invoke `dist/bin/cosyte.mjs`), where the FHIR library is supplied locally.
61
-
62
- > **Status:** pre-alpha (`0.0.x`). `0.0.1` and `0.0.2` are on npm and **cannot be installed** (see
63
- > above); a later version fixes that. The `cosyte` command wraps **all eight
51
+ **FHIR support arrives through a peer, not a declared dependency.** `@cosyte/fhir` is not in this
52
+ package's manifest. `npm install` brings it in as the peer dependency of `@cosyte/transform`, one of
53
+ this package's optional dependencies, and the FHIR `parse` / `inspect` / `fmt` / `validate` commands
54
+ and `convert` use that copy. If your package manager skips optional dependencies or does not install
55
+ peers, a command whose library is missing reports a value-free `CLI_PARSER_UNAVAILABLE` and exits
56
+ `69`: it never guesses, and it never blames your input. HL7 v2 and `map-codes` run on the two hard
57
+ dependencies, `@cosyte/hl7` and `@cosyte/terminology`, and always work.
58
+
59
+ > **Status:** `0.1`. While the package is below 1.0, a breaking change ships in a minor version and is
60
+ > called out in the changelog. `0.0.1` and `0.0.2` are on npm and **cannot be installed** (see above);
61
+ > every later version installs. The `cosyte` command wraps **all eight
64
62
  > cosyte formats** (**HL7 v2**, **FHIR R4**, **X12**, **ASTM**, **NCPDP SCRIPT**, **C-CDA**, **DICOM**,
65
63
  > and **MLLP**) plus the `@cosyte/transform` and `@cosyte/terminology` higher-layer libraries, with
66
64
  > conservative content-format autodetection and a documented exit-code contract:
@@ -77,8 +75,9 @@ invoke `dist/bin/cosyte.mjs`), where the FHIR library is supplied locally.
77
75
  > free issues on stderr, and a non-zero exit on an error-severity conversion issue.
78
76
  > - **`map-codes`**: translate a code through a BYO FHIR ConceptMap via `@cosyte/terminology`; the
79
77
  > target coding(s) on stdout, or a value-free unmapped signal + exit `1`.
80
- > - **`redact` / `deid`**: gated to an honest `CLI_NOT_IMPLEMENTED` (exit `69`) until the CLI wires
81
- > `@cosyte/deid`; it never reads the input and never emits a partial scrub dressed up as de-identified.
78
+ > - **`redact` / `deid`**: a de-identified copy on stdout for `ccda`, `fhir`, `hl7`, `x12`, delegated
79
+ > whole to `@cosyte/deid`, with that library's value-free manifest on stderr; every other format is a
80
+ > typed refusal and it never emits a partial scrub dressed up as de-identified.
82
81
  > - **`completion <bash|zsh|fish>`**: print a static shell completion script.
83
82
  >
84
83
  > **Support is honest per (format, operation)**: not every parser faithfully supports every command, so
@@ -94,20 +93,22 @@ invoke `dist/bin/cosyte.mjs`), where the FHIR library is supplied locally.
94
93
  > described above, which is why `0.0.1` and `0.0.2` published green and still cannot be installed: a
95
94
  > dry-run builds the tarball but never resolves it from a registry. **That swap has now been made**,
96
95
  > and installing the packed tarball from outside the repository is a release step in its own right.
97
- > `@cosyte/fhir` is the one dependency it could not cover, which is why FHIR support is absent from an
98
- > installed copy rather than merely deferred. See [RELEASING.md](./RELEASING.md).
96
+ > `@cosyte/fhir` reaches an installed copy as a peer of `@cosyte/transform` (see above). See
97
+ > [RELEASING.md](./RELEASING.md).
99
98
 
100
99
  ## Run it
101
100
 
102
- > **These commands do not work yet, and the reason is now only that no fixed version has shipped.**
103
- > The newest version on npm is `0.0.2`, and it is one of the two that cannot be installed. The
104
- > packaging defect is fixed in this repository and proven by installing the packed tarball outside it,
105
- > but a published version is immutable, so the fix reaches you only in the next release. Until then,
106
- > run it from a source checkout: `pnpm install && pnpm build`, then invoke `dist/bin/cosyte.mjs`.
101
+ Requires Node `>=22.0.0 <26.0.0`. This block runs as-is in an empty directory: it installs the CLI,
102
+ writes a short synthetic HL7 v2 message, and reads it back.
107
103
 
108
104
  ```bash
109
105
  npm install -g @cosyte/cli # put `cosyte` on your PATH
110
- cosyte parse message.hl7 # format autodetected → HL7 v2
106
+
107
+ # A synthetic ADT^A01 admit. HL7 v2 ends every segment with a carriage return (\r).
108
+ printf 'MSH|^~\\&|SENDINGAPP|SENDINGFAC|RECEIVINGAPP|RECEIVINGFAC|20240101120000||ADT^A01|MSG00001|P|2.5\rEVN|A01|20240101120000\rPID|1||MRN-000123^^^HOSP^MR||ZZSENTINELLAST^ZZSENTINELFIRST^M||19800101|M\rPV1|1|I|WARD^101^A\r' > message.hl7
109
+
110
+ cosyte parse message.hl7 # format autodetected → HL7 v2, typed JSON on stdout
111
+ cosyte inspect message.hl7 # a value-free summary: type, version, segment counts
111
112
  ```
112
113
 
113
114
  Or without installing, using `npx`:
@@ -146,7 +147,15 @@ astm | ncpdp | ccda | dicom | mllp`.
146
147
  **MLLP** stream, one record per frame, or any input under **`--ndjson`**, one record per non-empty
147
148
  line) streams as **NDJSON**, one `{ record, format, model, warnings }` line each, with per-record
148
149
  isolation: a record that fails to parse becomes a value-free `{ record, error }` line and the stream
149
- continues; the overall exit is a data error (`65`) if any record failed.
150
+ continues; the overall exit is a data error (`65`) if any record failed. Each line is written **as
151
+ its record is parsed**, before the rest of the input has been read, so a batch pipes straight into
152
+ the next process.
153
+
154
+ **Input size.** `parse` reads at most **67108864 bytes (64 MiB)** per invocation, the number
155
+ `cosyte --help` prints. A larger input is a value-free `CLI_INPUT_TOO_LARGE` refusal naming that
156
+ limit, with the data-error exit code (`65`) and never the internal-error code: split the input and
157
+ re-run. Since records are emitted as they are parsed, a refusal can land after some records have
158
+ already reached stdout; the exit code, not the output, says the run did not complete.
150
159
 
151
160
  Support is honest **per (format, operation)**: `x12`/`astm`/`ncpdp` support all of parse/inspect/fmt/
152
161
  validate; `ccda` supports inspect/fmt/validate (parse deferred); `dicom` supports inspect/validate
@@ -230,20 +239,39 @@ valid JSON or not a loadable ConceptMap is a `CLI_MAP_INVALID` data error (`65`)
230
239
 
231
240
  ## The exit-code contract
232
241
 
233
- Every command is safe to branch on in CI. The exit code carries the outcome (`sysexits.h`):
234
-
235
- | Code | Meaning |
236
- | ---- | ---------------------------------------------------------- |
237
- | `0` | success / **valid** (`validate`) |
238
- | `1` | **invalid**: `validate` found a parseable-but-bad message |
239
- | `2` | usage error (unknown flag, missing argument) |
240
- | `65` | data error (unparseable input, or format undetected) |
241
- | `66` | no input (missing/unreadable file) |
242
- | `69` | unavailable (a capability is not yet built, e.g. `redact`) |
243
- | `70` | internal error (a bug) |
242
+ Every command is safe to branch on in CI. The exit code carries the outcome. `0` is the conventional
243
+ success status and the codes from `65` up are the values the Unix `sysexits.h` header assigns to the
244
+ conditions they name; `1` and `2` are this CLI's own values, because that header defines no constant
245
+ for either:
246
+
247
+ | Code | Meaning |
248
+ | ---- | ------------------------------------------------------------------------------------------------------------------- |
249
+ | `0` | success / **valid** (`validate`) |
250
+ | `1` | **invalid**: `validate` found a parseable-but-bad message, or `redact` could not de-identify every locus |
251
+ | `2` | usage error (unknown flag, missing argument) |
252
+ | `65` | data error (unparseable input, format undetected, or input past the size limit) |
253
+ | `66` | no input (missing/unreadable file) |
254
+ | `69` | unavailable (a capability is not wired for this input, e.g. `redact` on a format `@cosyte/deid` has no adapter for) |
255
+ | `70` | internal error (a bug) |
256
+ | `74` | output error: the output stream closed before the CLI finished writing (a consumer that exited early) |
244
257
 
245
258
  The load-bearing rule: the CLI **never prints a reassuring line and exits `0`** on input it could not
246
- handle, or on an invalid message.
259
+ handle, on an invalid message, or on output that never reached its consumer.
260
+
261
+ ### A consumer that goes away
262
+
263
+ Piping into a reader that stops early is ordinary shell usage (a pager you quit, a `head` that has
264
+ seen enough), and it is not an error in your input:
265
+
266
+ ```bash
267
+ cosyte parse bulk.ndjson --ndjson | head -3 # three lines, then exit 74, and a value-free note
268
+ ```
269
+
270
+ The CLI terminates quietly under `74`: no stack trace, no platform error text, and no byte of the
271
+ input on stderr, just the stable `CLI_OUTPUT_WRITE_FAILED` code. Exit `74` is deliberately not the
272
+ internal-error code: nothing went wrong inside the CLI, the answer simply did not reach you. The
273
+ same holds when the whole result is one write, so a run whose output never arrived is never
274
+ reported as a success.
247
275
 
248
276
  ## PHI posture
249
277
 
@@ -268,15 +296,47 @@ cosyte parse broken.hl7 --format hl7 --unsafe-show-values # appends a bounded i
268
296
 
269
297
  ## `cosyte redact` / `cosyte deid`
270
298
 
271
- De-identification is the one operation whose _job_ is to strip PHI. It is **not implemented yet, on
272
- purpose.** It belongs to [`@cosyte/deid`](https://github.com/cosyte/deid). That package is now
273
- published, but the CLI does not wire it yet, and the wrapped parsers expose no de-identification API of
274
- their own. A built-in "minimal Safe-Harbor" pass over only
275
- the obvious fields would leave PHI behind and _look_ de-identified while silently under-redacting: the
276
- exact false-safety hazard `redact` exists to avoid. So `cosyte redact <file>` is an honest, typed
277
- `CLI_NOT_IMPLEMENTED` (exit `69`, `EX_UNAVAILABLE`) that **never reads your input** and **never emits a
278
- partial scrub dressed up as safe**. It will produce a real de-identified copy once the CLI wires
279
- `@cosyte/deid` and that integration is vetted.
299
+ De-identification is the one operation whose _job_ is to strip identifiers, and the CLI **owns none of
300
+ the policy**. It belongs to [`@cosyte/deid`](https://github.com/cosyte/deid): the CLI locates nothing,
301
+ transforms nothing, and has no fallback scrub, because a built-in "minimal Safe-Harbor" pass over only
302
+ the obvious fields would leave PHI behind and _look_ de-identified while silently under-redacting, the
303
+ exact false-safety hazard `redact` exists to avoid.
304
+
305
+ ```bash
306
+ cosyte redact adt.hl7 > clean.hl7 # stdout: the de-identified message. stderr: what was touched
307
+ echo $? # 0: clean.hl7 is the copy. 1: clean.hl7 is empty, see stderr
308
+ ```
309
+
310
+ **Covered formats: `ccda`, `fhir`, `hl7`, `x12`** (what the library covers and this CLI can serialize
311
+ onto a text stdout). Anything else emits nothing at all:
312
+
313
+ | Outcome | Diagnostic | Exit |
314
+ | ------------------------------------------------------------------------------------ | ------------------------ | ---- |
315
+ | every locus handled | the manifest | `0` |
316
+ | a locus the library reports it could not handle | `CLI_DEID_INCOMPLETE` | `1` |
317
+ | `astm`, `mllp`, `ncpdp`: no adapter in that library | `CLI_NOT_IMPLEMENTED` | `69` |
318
+ | `dicom`: covered there, but its de-identified form is binary and this stdout is text | `CLI_FORMAT_UNSUPPORTED` | `65` |
319
+ | `@cosyte/deid` not installed (an optional dependency) | `CLI_PARSER_UNAVAILABLE` | `69` |
320
+
321
+ stderr carries the library's **own value-free manifest** (category, transform, the structural path,
322
+ count, disposition and its stable code) and the library's **own published label and version**: the CLI
323
+ asserts no de-identification standard of its own. An absent library is decided **before your input is
324
+ read**.
325
+
326
+ What the library's default policy does decides which of the first two rows you get, and the CLI
327
+ passes it no policy of its own:
328
+
329
+ - **Medical record, account and member numbers are removed**, not replaced by a surrogate: their
330
+ manifest lines read `removed DEID_CATEGORY_REMOVED`.
331
+ - **A visit number or an order number is blocked, so the whole run is refused.** An HL7 visit number
332
+ (`PV1-19`), and a placer or filler order number (`ORC-2`/`ORC-3`, `OBR-2`/`OBR-3`), fall to the
333
+ library's catch-all identifier category, which its default policy blocks rather than removes. Such a
334
+ message exits with `CLI_DEID_INCOMPLETE` (exit `1`) and nothing on stdout, and stderr names each
335
+ blocked locus with the library's `DEID_LOCUS_BLOCKED` code. Many real ADT messages carry `PV1-19`,
336
+ so check the exit code before you use `clean.hl7`.
337
+
338
+ The CLI still keys every run with a **per-invocation ephemeral key** and says so on stderr: any
339
+ identifier surrogate is consistent within one output and deliberately not stable across runs.
280
340
 
281
341
  ## `cosyte completion`
282
342
 
@@ -315,6 +375,12 @@ and the agent get identical results. The PHI posture is inherited and hardened:
315
375
  tool _error_ carries only value-free diagnostics (a stable code + position, never an input value). A
316
376
  parsed-but-invalid `validate` is a **successful** call reporting the verdict, not a tool error.
317
377
 
378
+ **Every tool publishes an `outputSchema`**, so a client validates a result against a declared contract
379
+ rather than pattern-matching prose. Each result carries `ok`, a `status` of `success` / `verdict` /
380
+ `failed`, the `exit` code, a stable `code` on a failure, and the tool's own payload under `data`; the
381
+ text content block carries the serialized JSON of that same structured result. See the docs for the
382
+ per-tool payload shapes.
383
+
318
384
  The MCP SDK (`@modelcontextprotocol/sdk`) is the CLI's only third-party runtime dependency; it is
319
385
  declared **optional** and loaded only on the MCP path, so a `cosyte parse` invocation never pulls it and
320
386
  the core works with the SDK absent. The server surface is importable via the `@cosyte/cli/mcp` subpath