@cosyte/cli 0.0.6 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +322 -0
- package/README.md +111 -45
- package/dist/bin/cosyte-mcp.cjs +461 -106
- package/dist/bin/cosyte-mcp.cjs.map +1 -1
- package/dist/bin/cosyte-mcp.mjs +461 -106
- package/dist/bin/cosyte-mcp.mjs.map +1 -1
- package/dist/bin/cosyte.cjs +824 -140
- package/dist/bin/cosyte.cjs.map +1 -1
- package/dist/bin/cosyte.mjs +825 -141
- package/dist/bin/cosyte.mjs.map +1 -1
- package/dist/index.cjs +592 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +515 -281
- package/dist/index.d.ts +515 -281
- package/dist/index.mjs +574 -104
- package/dist/index.mjs.map +1 -1
- package/dist/io-C9bSG2zF.d.cts +381 -0
- package/dist/io-C9bSG2zF.d.ts +381 -0
- package/dist/mcp.cjs +435 -103
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.d.cts +70 -9
- package/dist/mcp.d.ts +70 -9
- package/dist/mcp.mjs +435 -103
- package/dist/mcp.mjs.map +1 -1
- package/package.json +61 -37
- package/dist/io-B2EobRzC.d.cts +0 -57
- package/dist/io-B2EobRzC.d.ts +0 -57
package/CHANGELOG.md
CHANGED
|
@@ -15,8 +15,330 @@ still do. Each entry was assigned to the release whose tag first contains it, re
|
|
|
15
15
|
|
|
16
16
|
## [Unreleased]
|
|
17
17
|
|
|
18
|
+
**What 0.1 means for you.** This is the first release whose commands, output and exit codes we treat
|
|
19
|
+
as settled: command names, flags, the JSON output shapes, the exit-code contract (`0`, `1`, `2`,
|
|
20
|
+
`65`, `66`, `69`, `70`, `74`) and the diagnostic codes are the surface we keep stable. It covers the
|
|
21
|
+
`cosyte` command over all eight cosyte formats (`parse`, `validate`, `inspect`, `fmt`, `convert`,
|
|
22
|
+
`map-codes`, `redact` / `deid`, `completion`), value-free diagnostics with `--unsafe-show-values` as
|
|
23
|
+
the one opt-in, the `cosyte-mcp` stdio server and the programmatic `core` API. While the package is
|
|
24
|
+
below 1.0, a breaking change bumps the minor version and is called out here; a fix that changes no
|
|
25
|
+
output ships as a patch. Not covered yet: DICOM `parse` and `fmt`, C-CDA `parse`, and MLLP `fmt` and
|
|
26
|
+
`validate` (`CLI_FORMAT_UNSUPPORTED`), `validate --profile` (`CLI_NOT_IMPLEMENTED`), `redact` for
|
|
27
|
+
`astm`, `dicom`, `mllp` and `ncpdp`, a remote MCP transport and MCP tools for `redact` and
|
|
28
|
+
`map-codes`. The FHIR commands and `convert` use `@cosyte/fhir` as the peer of the optional
|
|
29
|
+
`@cosyte/transform`, and report `CLI_PARSER_UNAVAILABLE` (exit `69`) when a package manager leaves
|
|
30
|
+
it out.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **A downstream consumer that closes the CLI's stdout now ends the run quietly, under a documented
|
|
35
|
+
code of its own: `74` (`IOERR`, the value `sysexits.h` assigns `EX_IOERR`).** A pipeline into
|
|
36
|
+
`head`, a pager you quit, or any early-exiting reader gets the stable value-free
|
|
37
|
+
`CLI_OUTPUT_WRITE_FAILED` diagnostic and that exit code: no stack trace, no platform error text,
|
|
38
|
+
and no byte of the input on stderr. The contract is additive; no existing code is renamed,
|
|
39
|
+
repurposed or removed, and no existing code's meaning changes.
|
|
40
|
+
- **The code was chosen on a measurement, not a citation.** A write to a closed stdout is a Node
|
|
41
|
+
`EPIPE` on the stream, which is fatal when nothing is listening: measured here, the unguarded
|
|
42
|
+
shape prints a ten-frame stack trace and exits `1`. It is a handled condition the consumer owns,
|
|
43
|
+
so reporting it as `70` told an operator their pipeline had hit a bug in the CLI. The one
|
|
44
|
+
condition that moves onto the new code is that write failure itself, which previously reported
|
|
45
|
+
`70` on the record-stream path.
|
|
46
|
+
- **Neither write path reports a success over output that reached nobody.** The whole-result write
|
|
47
|
+
in the `cosyte` bin bypassed the guarded output sink altogether, and the record stream's sink
|
|
48
|
+
only consulted a flag the platform sets asynchronously, so a short enough run could have every
|
|
49
|
+
line enqueued before the closed consumer was reported. Either way a parse piped into a reader
|
|
50
|
+
that stopped early could print a summary and exit `0` over output that was never delivered.
|
|
51
|
+
Every chunk bound for stdout now waits for the platform's acknowledgement before the run may
|
|
52
|
+
resolve, so an undelivered result resolves to `74` whatever the command had computed, and the
|
|
53
|
+
summary describing that computed outcome is withdrawn with it.
|
|
54
|
+
- **`cosyte-mcp` terminates quietly when its client goes away.** A closed stdout used to reach the
|
|
55
|
+
process as an unhandled stream error, printing a Node stack trace naming this machine's install
|
|
56
|
+
paths. It is now a value-free diagnostic and the same exit code, and the server ends rather than
|
|
57
|
+
serving a channel nobody is reading.
|
|
58
|
+
- **A closed stderr cannot turn either termination into an unhandled error.** Both bins hear the
|
|
59
|
+
diagnostic channel's own error and fall back to the exit code as the whole signal.
|
|
60
|
+
- `test/pipe-close.test.ts` spawns real processes and closes their real streams, which nothing in
|
|
61
|
+
this suite did before: the existing coverage drove an injected sink inside the test process and
|
|
62
|
+
could say nothing about the process adapters. Its record-stream cases run at one and two records
|
|
63
|
+
on purpose, because a long stream fails on a later write and so is caught however the earlier
|
|
64
|
+
ones were handled, and `scripts/smoke.mjs` asks the same question of the PACKAGED bin under
|
|
65
|
+
plain `node`, where a faster start widens the window further.
|
|
66
|
+
|
|
67
|
+
- **The release pipeline now installs the packed tarball from outside this repository, and runs both
|
|
68
|
+
bins from that installed copy, before anything can publish.** `0.0.1` and `0.0.2` are on npm
|
|
69
|
+
permanently uninstallable, and the check that catches that shape was a sentence in a human
|
|
70
|
+
checklist: a `--dry-run` packs a tarball but never resolves its dependencies from a registry. It is
|
|
71
|
+
now the `install-gate` job in the release workflow, which the publishing job `needs:`, so a red
|
|
72
|
+
gate stops the publish rather than reporting on one that already happened.
|
|
73
|
+
- **It refuses a local-path dependency specifier before it packs anything.** A `file:`, `link:` or
|
|
74
|
+
bare filesystem path in `dependencies`, `optionalDependencies` or `peerDependencies` is what made
|
|
75
|
+
those two versions uninstallable, and it is now a refusal naming the field and the specifier.
|
|
76
|
+
`devDependencies` is deliberately exempt and reported rather than skipped silently: a consumer
|
|
77
|
+
never installs them, which is why this package can carry one on purpose.
|
|
78
|
+
- **Both bins are executed, not stat-ed.** `cosyte` must exit `0` from the installed copy, and
|
|
79
|
+
`cosyte-mcp`, a stdio server that never exits on its own, must not exit non-zero inside a bounded
|
|
80
|
+
window. A bin whose file is missing from the tarball is reported as a packaging defect, in words
|
|
81
|
+
that do not read like an install failure.
|
|
82
|
+
- **It fails closed.** An install that exceeds its time bound, a registry that does not answer and
|
|
83
|
+
a throw inside the gate each refuse with their own reason rather than warning and continuing. The
|
|
84
|
+
accepted cost is that a transient fault delays a correct release until the run is repeated.
|
|
85
|
+
- No dependency was added, no manifest field moved, and nothing a consumer receives changed.
|
|
86
|
+
|
|
87
|
+
- **Every MCP tool now publishes an `outputSchema`, and every tool result conforms to it.** An agent
|
|
88
|
+
calling a cosyte tool used to receive `structuredContent: { exit, ok }` with no schema to check it
|
|
89
|
+
against, so deciding whether a result held data or a diagnostic meant pattern-matching a text blob.
|
|
90
|
+
Each of the four tools now advertises an output schema and a `title` on `tools/list`, and every
|
|
91
|
+
dispatch path (success, negative verdict, hard failure, usage error, internal error) returns
|
|
92
|
+
structured content conforming to the schema its own tool declared.
|
|
93
|
+
- **The result carries `ok`, `status`, `exit`, `code` and `data`.** `status` is `success`,
|
|
94
|
+
`verdict` or `failed`, and it is the property that separates a negative verdict about the message
|
|
95
|
+
(the tool ran, the payload is present, the call is not an error) from a call that produced
|
|
96
|
+
nothing: the distinction no text blob could make reliably. `code` is the stable diagnostic code on
|
|
97
|
+
a failed call. `data` is the tool's own payload: the parsed model and warnings (or a record stream
|
|
98
|
+
for a multi-record input), the validation verdict and findings, the value-free structural summary,
|
|
99
|
+
or the converted `Bundle`.
|
|
100
|
+
- **On a failed call the structured result is value-free by construction**, not by review: every
|
|
101
|
+
property is drawn from a fixed set (the outcome vocabulary, the exit-code contract, the diagnostic
|
|
102
|
+
code registry), so no part of the caller's input can appear in it. One consequence is deliberate:
|
|
103
|
+
an unknown tool name is no longer echoed back, because a tool name is caller-supplied text like
|
|
104
|
+
any other argument.
|
|
105
|
+
- Tool names, tool count, `tools/list` ordering, input schemas, the exit-code contract, and the rule
|
|
106
|
+
that a parsed-but-invalid `validate` is a successful call are all unchanged, and no dependency was
|
|
107
|
+
added. The suite validates each emitted result against its own tool's declared schema with a
|
|
108
|
+
dependency-free checker that refuses any schema keyword it does not implement.
|
|
109
|
+
|
|
110
|
+
- **`redact` / `deid` produces a real de-identified copy, delegated whole to `@cosyte/deid`.** The
|
|
111
|
+
command that shipped as an honest `CLI_NOT_IMPLEMENTED` is wired. For `ccda`, `fhir`, `hl7` and
|
|
112
|
+
`x12` stdout carries the de-identified document (serialized exactly as `cosyte fmt` serializes that
|
|
113
|
+
format) and stderr carries the library's own value-free manifest, one line per locus with its
|
|
114
|
+
category, transform, structural path, count, disposition and stable code, plus the library's own
|
|
115
|
+
published output label and version. The CLI adds no policy, no locus map, no transform and no
|
|
116
|
+
fallback scrub, and asserts no de-identification standard of its own.
|
|
117
|
+
- **Every other outcome emits nothing at all**, because a partial pass offered as a de-identified
|
|
118
|
+
copy is the hazard the command exists to prevent. `astm`, `mllp` and `ncpdp` have no adapter in
|
|
119
|
+
that library: `CLI_NOT_IMPLEMENTED` (`69`). `dicom` is covered there, but its de-identified form
|
|
120
|
+
is a Part 10 byte stream this text stdout cannot carry: `CLI_FORMAT_UNSUPPORTED` (`65`), the
|
|
121
|
+
CLI's own limit, deliberately not blamed on the library. Any locus the library reports it could
|
|
122
|
+
not handle is the new `CLI_DEID_INCOMPLETE` diagnostic and exit `1`, with the blocked paths and
|
|
123
|
+
their codes named on stderr.
|
|
124
|
+
- **`@cosyte/deid` is an `optionalDependency`.** An install without it degrades to a value-free
|
|
125
|
+
`CLI_PARSER_UNAVAILABLE` (`69`), decided **before the input is read**, so a copy that cannot
|
|
126
|
+
de-identify never touches the bytes it cannot strip. No other command loads it: the boundary is
|
|
127
|
+
gated statically and observed at runtime in `test/deid-isolation.test.ts`.
|
|
128
|
+
- **Identifier surrogates are keyed with a per-invocation ephemeral key.** The library's default
|
|
129
|
+
policy pseudonymizes MRN / account / beneficiary numbers, and a keyed transform with no key is a
|
|
130
|
+
fatal there, never an unkeyed fallback. The CLI holds no key material and adds no key surface, so
|
|
131
|
+
it keys each run with a fresh random value: surrogates are consistent within one output and
|
|
132
|
+
deliberately not stable across runs. Stated on stderr and in the docs rather than discovered.
|
|
133
|
+
- **`redact` does not honour `--unsafe-show-values`.** The opt-in excerpt exists for debugging a
|
|
134
|
+
rejected parse; on this command an excerpt of the un-stripped input is precisely the leak it
|
|
135
|
+
exists to prevent, so its diagnostics stay value-free under every flag. The PHI-leak sentinel
|
|
136
|
+
matrix now carries a redact row per mode (covered, refused, blocked, library absent, `--format`
|
|
137
|
+
given and omitted, file and stdin).
|
|
138
|
+
- No published exit value moved and no existing `CLI_*` code was renamed. `redact` is still
|
|
139
|
+
deliberately absent from the MCP tool surface.
|
|
140
|
+
|
|
141
|
+
- **The two-file agent-guidance contract is now gated (`pnpm check:agent-notes`).** `CLAUDE.md` was
|
|
142
|
+
split from `documentation/agent-notes.md` on 2026-08-04, which made every anchor between them
|
|
143
|
+
load-bearing, and nothing checked them. `scripts/check-agent-notes.ts` now verifies that the
|
|
144
|
+
narrative file is tracked, that no section is emptied down to its heading (a container whose body is
|
|
145
|
+
its subsections is exempt and counted), and that every pointer at it resolves. It runs from
|
|
146
|
+
`test/scripts/agent-notes.test.ts`, so it rides the required `ci / verify` contexts and
|
|
147
|
+
`prepublishOnly` rather than adding a fourth workflow.
|
|
148
|
+
- **The matcher was derived by counting this tree, not by porting a sibling's.** Two pointer
|
|
149
|
+
spellings are live across these repositories, and porting without re-counting has produced a false
|
|
150
|
+
green twice: a qualified-only matcher would have covered 3 of `ncpdp`'s 38 pointers, and would
|
|
151
|
+
have matched nothing at all in `terminology` (42 bare, zero qualified) while still exiting 0.
|
|
152
|
+
**Measured here: every pointer is the qualified `documentation/agent-notes.md#<anchor>` form and no
|
|
153
|
+
bare pointer exists**, so the gate matches that form only. No count is written into the gate's
|
|
154
|
+
prose as a promise; the OK line prints all of them on every run, because it measures rather than
|
|
155
|
+
remembers.
|
|
156
|
+
- **A bare-form census keeps that scope honest.** Matching one spelling is safe only while the other
|
|
157
|
+
stays absent, so in **every opened file** each backticked `#<anchor>` span is enumerated: a
|
|
158
|
+
digits-only one is a pull-request reference and is counted and reported, and **any other one
|
|
159
|
+
refuses the run at exit 2** with an instruction to re-derive the matcher. Refusal rather than a
|
|
160
|
+
finding, because the tree has not necessarily broken but the evidence the scope rested on has.
|
|
161
|
+
A first draft scoped the census to the pair and justified that with tree files it claimed a
|
|
162
|
+
widening would red; **running it tree-wide proved none of them can, and that the only files that
|
|
163
|
+
could were the gate's own source and test**, which wrote bare spans out literally. That made the
|
|
164
|
+
narrow scope a self-exemption for the very files where a broken pointer would hide, so the samples
|
|
165
|
+
are now assembled from parts and the census covers the corpus. It closes the hole a pair-scoped
|
|
166
|
+
census leaves: a bare pointer in a third file was covered by neither the matcher nor the census.
|
|
167
|
+
- **Three claims were withdrawn after review rather than shipped.** The opening promise said the
|
|
168
|
+
gate catches "a rename"; it does not, because both halves match on **basename** and the directory
|
|
169
|
+
is never compared, so moving the file while pointers keep their prefix exits 0 with every rendered
|
|
170
|
+
link broken. That is now a narrowed promise and a disclosed miss rather than a bigger guard. A
|
|
171
|
+
**heading inside an HTML comment** mints a phantom anchor, which the list had omitted; it is now
|
|
172
|
+
disclosed and pinned. And a written-down count of sibling repos lacking the file was already
|
|
173
|
+
wrong, so **the number was dropped in favour of the class**.
|
|
174
|
+
- **Two further claims were withdrawn on later review passes, each arriving inside the fix for the
|
|
175
|
+
one before it.** The first fix asserted the HTML-comment miss is disclosed in "every sibling
|
|
176
|
+
copy"; that phrasing came from a review rather than from the tree, and it is false. `ncpdp`,
|
|
177
|
+
`terminology` and `astm` disclose it; `mllp`, `ccda`, `transform` and `docs` do not, and `mllp`
|
|
178
|
+
is the copy this gate's disclosed-miss block is transcribed from, so the shared ancestor never
|
|
179
|
+
carried the entry. The second fix then said the four that lack it are all owed it. Also false:
|
|
180
|
+
`mllp`, `ccda` and `transform` derive anchors by **slugging headings** and are owed it, while
|
|
181
|
+
`docs` resolves explicit `<a id>` anchors and cannot have the miss at all. **A claim about
|
|
182
|
+
another repository is not checkable from inside this one**, so it is measured against that
|
|
183
|
+
repository's source or not made.
|
|
184
|
+
- **It refuses rather than reporting green over a corpus it never opened.** There is no declared
|
|
185
|
+
scan root to be wrong about: the corpus is `git ls-files`, reconciled as **sets of paths**, and
|
|
186
|
+
zero pointers, zero tracked files, an unmerged path, a symlink, a non-regular file or two files
|
|
187
|
+
carrying the contract basename each refuse at exit 2. This repository has already shipped the
|
|
188
|
+
opposite defect once, in `phi-scan`, which printed `OK, no hits` over a root it never walked.
|
|
189
|
+
- **Every claim was watched to fail on a clone of the real tree** before the gate was believed: a
|
|
190
|
+
misspelled real anchor, an emptied real section, the narrative file deleted, a bare pointer that
|
|
191
|
+
resolves, a neutered matcher, and a tree with every pointer rewritten out of range. The last two
|
|
192
|
+
are the `terminology` scenario, and both refuse instead of going green.
|
|
193
|
+
- **Exit codes and corpus handling were re-derived from this repository, not inherited.** `0`, `1`
|
|
194
|
+
for a finding and `2` for a refusal come from `scripts/phi-scan.ts`. An unmerged path **refuses**
|
|
195
|
+
here even though `phi-scan` leaves that status unenumerated, because its reasoning (`git commit`
|
|
196
|
+
refuses an unmerged index) covers a staged route this gate does not have.
|
|
197
|
+
- **The NUL skip is a disclosed miss, not a pass**, and is required rather than tidy: the tree
|
|
198
|
+
tracks vendored `@cosyte/*` tarballs and a synthetic DICOM fixture, none readable as markdown or
|
|
199
|
+
editable to clear a red. The tell is the skipped count on the OK line. A draft claimed this
|
|
200
|
+
partition differs from `check-no-emdash.sh`'s; **that was false and was corrected by reading that
|
|
201
|
+
gate's own OK line.** Both key on an actual NUL byte; the wider set is git's own binary
|
|
202
|
+
classification, which is why neither gate may be reduced to `grep -I`.
|
|
203
|
+
|
|
204
|
+
- **`parse` has a documented input-size limit: 67108864 bytes (64 MiB) per invocation, refused as a
|
|
205
|
+
data error.** Node has two hard allocation ceilings (`buffer.constants.MAX_LENGTH` and
|
|
206
|
+
`buffer.constants.MAX_STRING_LENGTH`), and a legitimately large input that crossed one of them threw
|
|
207
|
+
past every handler and was reported as `CLI_INTERNAL` / exit `70`, which says "this is a bug in the
|
|
208
|
+
tool" about an input that is merely big. The CLI now declares its own limit far below both, checks it
|
|
209
|
+
against the **running byte count as the input arrives**, and refuses with a value-free
|
|
210
|
+
`CLI_INPUT_TOO_LARGE` naming the limit and exit `65`. The number is rendered from one constant into
|
|
211
|
+
`cosyte --help` and the command reference, and a test reds if the two ever disagree.
|
|
212
|
+
- The refusal fires before anything allocates memory proportional to the oversized input, which is
|
|
213
|
+
what makes it a refusal rather than a slower way to reach the same crash. The regression suite
|
|
214
|
+
proves it with sources that would run past `MAX_STRING_LENGTH` if anything drained them.
|
|
215
|
+
- The MLLP frame reader's own smaller default ceiling is raised to the CLI's limit on this path, so
|
|
216
|
+
the documented number is the binding one rather than an undocumented number underneath it.
|
|
217
|
+
|
|
218
|
+
### Changed
|
|
219
|
+
|
|
220
|
+
- **The CLI now runs on the 0.1 releases of every `@cosyte` library it uses except `@cosyte/deid`.**
|
|
221
|
+
The two required dependencies, `@cosyte/hl7` and `@cosyte/terminology`, and the optional
|
|
222
|
+
`@cosyte/astm`, `@cosyte/ccda`, `@cosyte/dicom`, `@cosyte/mllp`, `@cosyte/ncpdp`,
|
|
223
|
+
`@cosyte/transform` and `@cosyte/x12` move from their `0.0.x` releases to `^0.1.0`, so an install
|
|
224
|
+
takes their `0.1.x` patch releases and stops before `0.2.0`. `@cosyte/deid` follows in the next
|
|
225
|
+
entry, because it changes what `redact` does. The CLI's commands, flags, exit codes and diagnostic
|
|
226
|
+
codes are unchanged.
|
|
227
|
+
- **`redact` now delegates to the 0.1 releases of `@cosyte/deid`, and refuses more HL7 messages.**
|
|
228
|
+
The optional `@cosyte/deid` moves from `^0.0.9` (which pinned exactly `0.0.9`) to `^0.1.0`. The
|
|
229
|
+
library's default policy is the one `redact` applies, unmodified, and under it:
|
|
230
|
+
- **An HL7 visit number or order number refuses the run.** `PV1-19`, and a placer or filler order
|
|
231
|
+
number (`ORC-2`/`ORC-3`, `OBR-2`/`OBR-3`), fall to the catch-all identifier category, which the
|
|
232
|
+
default policy blocks (`DEID_LOCUS_BLOCKED`). `redact` answers with its existing refusal:
|
|
233
|
+
`CLI_DEID_INCOMPLETE`, exit `1`, nothing on stdout, and each blocked locus named on stderr with
|
|
234
|
+
the policy still attributed to the library. An ADT message that carries a visit number, which
|
|
235
|
+
`redact` used to de-identify, is now refused. The CLI passes no policy of its own to turn the
|
|
236
|
+
block into a removal: the library's own profile API rejects that weakening.
|
|
237
|
+
- **Medical record, account and member numbers are removed** (`DEID_CATEGORY_REMOVED`) rather than
|
|
238
|
+
replaced by a keyed surrogate. The per-invocation ephemeral key and its stderr disclosure stay,
|
|
239
|
+
so any keyed transform the library applies is never unkeyed.
|
|
240
|
+
- **HL7 `MSH-7` and `EVN-2` timestamps are now generalized.** The `0.0.9` policy left them
|
|
241
|
+
unchanged; a guard in the suite reds if a delegate that does so is ever resolved again.
|
|
242
|
+
- No exit code, diagnostic code or flag changed. The README and the command reference, limitations
|
|
243
|
+
and troubleshooting pages say which inputs produce output and which are refused.
|
|
244
|
+
- **The `js-yaml` dependency override now covers the advisory's extended range.** The pin moves from
|
|
245
|
+
`4.2.0` on `>=4.0.0 <4.2.0` to `4.3.0` on `>=4.0.0 <4.3.0`, so a transitive resolution landing
|
|
246
|
+
inside the newly covered window is remediated instead of being admitted silently. The `esbuild`
|
|
247
|
+
override is untouched, and the lockfile records the new pin.
|
|
248
|
+
- **`pnpm-workspace.yaml` declares the install hardening this package relies on**: a
|
|
249
|
+
`minimumReleaseAge` of 1440 minutes, which is the cooling-off window that blunts a compromised
|
|
250
|
+
release, and a `trustPolicy` of `no-downgrade`.
|
|
251
|
+
- **The `packageManager` pin moves to a pnpm release that honours both keys.** The previous pin
|
|
252
|
+
predates them, so it would have ignored the settings file entirely: a file that decorates rather
|
|
253
|
+
than defends is worse than an absent one, because it reads as a control that is in place.
|
|
254
|
+
- **Nothing an install resolves changed beyond the override above.** The full pre-publish ladder
|
|
255
|
+
(clean, typecheck, lint, test, build and the types check) runs green on the new pin.
|
|
256
|
+
- **The always-read agent guide is inside its declared line budget, and nothing it said was lost.**
|
|
257
|
+
Every rule and every trap stays there as a one-line imperative carrying a resolving pointer, and
|
|
258
|
+
the sentences that explained each one moved into the narrative document verbatim. The two-file
|
|
259
|
+
contract gate checks on every run that each pointer still lands on a section with a body, so the
|
|
260
|
+
compression cannot quietly become a deletion.
|
|
261
|
+
- **The declared Node range now matches the release lines this package is tested on.**
|
|
262
|
+
`engines.node` narrows from `>=22.0.0` to `>=22.0.0 <26.0.0`. Node 26 is supported upstream and
|
|
263
|
+
was admitted by the old open-ended range while the test matrix exercises 22 and 24 only, so an
|
|
264
|
+
install on Node 26 was a combination nobody here had run and the manifest said nothing about it.
|
|
265
|
+
The claim narrows to the tested set rather than the tested set widening to the claim: every matrix
|
|
266
|
+
cell is a required status check whose name is fixed by a repository setting no commit can edit, so
|
|
267
|
+
adding a line is a change that has to start outside this repository.
|
|
268
|
+
- **The tested set is stated here now.** The CI caller passes the Node majors explicitly rather
|
|
269
|
+
than inheriting them from the shared pipeline's input default. The same two versions run and
|
|
270
|
+
every status-check name is byte-identical; what changed is that the set is readable and diffable
|
|
271
|
+
in this repository instead of resolved somewhere else.
|
|
272
|
+
- **A guard reds when the two drift apart again.** It derives the lines upstream still supports
|
|
273
|
+
from a release schedule committed beside it, evaluated at the run date rather than pinned to a
|
|
274
|
+
number typed into a test, and reports a supported line the range admits but the matrix skips, a
|
|
275
|
+
matrix cell the range refuses, a range admitting a line the schedule does not carry, and any
|
|
276
|
+
prose surface restating the range or the matrix differently. It refuses to report agreement over
|
|
277
|
+
an absent, empty or provenance-less schedule.
|
|
278
|
+
- **The installation prerequisite states the upper bound too**, so the published documentation and
|
|
279
|
+
the published manifest cannot disagree about which runtimes are supported.
|
|
280
|
+
- **The MCP text content block is now the serialized JSON of the structured result**, replacing the
|
|
281
|
+
command's raw stdout (on a success) or its stderr diagnostic (on a failure). A client that reads
|
|
282
|
+
only text now sees exactly the value a schema-aware client validates: there is one value, serialized
|
|
283
|
+
once, so the two channels cannot disagree. A client that read the text block expecting the bare
|
|
284
|
+
command output will find the same payload one level down, under `data`.
|
|
285
|
+
- **Multi-record `parse` output (`--ndjson` and MLLP) is now emitted record by record, as each record
|
|
286
|
+
is parsed**, instead of being accumulated and written once at the end. The first line reaches stdout
|
|
287
|
+
before the rest of the input has been read, so a bulk batch pipes into the next process instead of
|
|
288
|
+
waiting on the whole file. Per-record isolation and the exit-code contract are unchanged: a record
|
|
289
|
+
that fails to parse is still a value-free `{ record, error }` line, the stream still continues, and
|
|
290
|
+
any failed record still resolves the invocation to exit `65`.
|
|
291
|
+
- **A fatal condition part way through keeps the lines already written and still exits non-zero.**
|
|
292
|
+
A truncated MLLP stream is the visible case: the frames that completed have already been emitted
|
|
293
|
+
when the unterminated one is detected at end of stream, so `stdout` is no longer empty for that
|
|
294
|
+
input. It was never a success and still is not: the exit code carries the failure, and a partial
|
|
295
|
+
record stream is never reported as a complete one.
|
|
296
|
+
- **A downstream consumer that closes the pipe part way through is now a value-free
|
|
297
|
+
`CLI_OUTPUT_WRITE_FAILED`** rather than an unhandled write error. One write per record is a
|
|
298
|
+
failure surface a single write did not have.
|
|
299
|
+
|
|
300
|
+
- **`CLAUDE.md` narrative was relocated into `documentation/agent-notes.md` to make room for the gate's
|
|
301
|
+
rules.** The branch-protection, PHI-scanner-residual and em-dash blocks were compressed to their
|
|
302
|
+
imperatives; every trap keeps a one-line rule and a pointer, and the reasoning each one compresses
|
|
303
|
+
was already in the narrative file. No trap was deleted and no ceiling was raised.
|
|
304
|
+
|
|
18
305
|
### Fixed
|
|
19
306
|
|
|
307
|
+
- **The PHI scanner reported on a run that had enumerated a target and never read it.** A logged
|
|
308
|
+
`--allow-fixture` bypass withdraws a file after the scan has already named it on the command line,
|
|
309
|
+
and the run then reported on whatever was left as though the corpus were whole. Over a corpus whose
|
|
310
|
+
only violator was withdrawn, that reads as a clean verdict about a file nobody opened.
|
|
311
|
+
- **The run now refuses, with a status distinct from both the clean status and the hits status.**
|
|
312
|
+
The refusal names every enumerated path that went unread and points at the token-level allow-list
|
|
313
|
+
as the remedy, because declaring an identifier keeps the file inside the scan while withdrawing it
|
|
314
|
+
removes the only evidence the run could have had about it.
|
|
315
|
+
- **Hits are reported FIRST, always.** A refusal raised before the findings were written would
|
|
316
|
+
discard every hit the run did find, which turns a stricter gate into a quieter one. The report
|
|
317
|
+
runs first and the refusal changes only the exit status.
|
|
318
|
+
- **The question is answered as a set difference, never as a count.** A count counts the targets
|
|
319
|
+
that DID get read, so it reads healthy at the exact moment a withdrawal empties the corpus, and it
|
|
320
|
+
cannot say which paths were missed.
|
|
321
|
+
- **An honest run is untouched.** With nothing withdrawn, the read set is the enumerated set, so the
|
|
322
|
+
pre-commit hook, the whole-tree sweep and a plain scan of named paths keep the exit codes they
|
|
323
|
+
already had.
|
|
324
|
+
- **The exit-code contract labelled exit `2` as `EX_USAGE`, and `sysexits.h` assigns `EX_USAGE` the
|
|
325
|
+
value `64` (EXIT-CODE-SYSEXITS-LABEL).** The label sat in two places in `src/core/exit-codes.ts`,
|
|
326
|
+
the module's contract table and the `USAGE` member's JSDoc, so it compiled into `dist/*.d.ts` and
|
|
327
|
+
rendered on a consumer's hover. `README.md` and the concepts page grounded their whole table in
|
|
328
|
+
that header while listing `1` and `2`, which it does not define at all. A developer aligning their
|
|
329
|
+
own tooling to `sysexits.h` therefore learned a false fact about it from our documentation.
|
|
330
|
+
- **Exit `2` now carries no sysexits constant, and `1` and `2` are both stated as this CLI's own
|
|
331
|
+
values.** The four labels that were already correct (`EX_DATAERR` 65, `EX_NOINPUT` 66,
|
|
332
|
+
`EX_UNAVAILABLE` 69, `EX_SOFTWARE` 70) are untouched.
|
|
333
|
+
- **No published number moved, and no member of the exported `EXIT` map was added, removed or
|
|
334
|
+
renamed.** This is a documentation correction and nothing else: `2` is still `2`, every command
|
|
335
|
+
still exits with the status it exited with before, and the golden exit-code matrix is unchanged.
|
|
336
|
+
A consumer who hard-coded `EX_USAGE = 2` against this CLI keeps working.
|
|
337
|
+
- **`test/exit-code-docs.test.ts` reds if a sysexits constant is ever again attached to a number
|
|
338
|
+
that header does not assign it**, across every source file, the README and every published docs
|
|
339
|
+
page, naming the offending surface, line and code. It carries the header's own table, reports a
|
|
340
|
+
constant that table does not define rather than passing it through, and is proved against the
|
|
341
|
+
exact text that shipped before this correction.
|
|
20
342
|
- **`pnpm phi-scan`'s all-mode walk was rooted at `test/__fixtures__` and `src` only, so 89 of this
|
|
21
343
|
repository's 123 tracked files were scanned by NEITHER of its two routes
|
|
22
344
|
(PHI-SCAN-WALK-ROOT-SCOPE).** The walk now roots at **`src`, `test` and `scripts`**, which opens 72
|
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
|
-
**
|
|
52
|
-
npm
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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`**:
|
|
81
|
-
> `@cosyte/deid
|
|
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`
|
|
98
|
-
>
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
| `
|
|
241
|
-
| `
|
|
242
|
-
| `
|
|
243
|
-
| `
|
|
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,
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|