@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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { R as RunDeps } from './io-B2EobRzC.js';
2
- export { r as readFileBytes, a as readStreamBytes } from './io-B2EobRzC.js';
1
+ import { a as CliError, b as RunResult, R as RunDeps } from './io-C9bSG2zF.js';
2
+ export { c as CLI_CODES, C as CliCode, E as EXIT, d as ExitCode, e as errorResult, f as fileChunks, g as formatDiagnostic, r as readFileBytes, h as readStreamBytes, s as streamChunks, t as toCliError } from './io-C9bSG2zF.js';
3
3
  import 'node:stream';
4
4
 
5
5
  /**
@@ -28,229 +28,6 @@ import 'node:stream';
28
28
  */
29
29
  declare const VERSION: string;
30
30
 
31
- /**
32
- * The `cosyte` CLI **exit-code contract**: a designed, documented surface that CI pipelines and
33
- * shell scripts branch on. Grounded in the Unix `sysexits.h` conventions.
34
- *
35
- * The load-bearing rule: **the CLI never prints a reassuring line and exits `0` on input it could not
36
- * handle.** An undetectable format, an unreadable file, or a parser that throws each map to a
37
- * distinct, stable non-zero code, never a silent success.
38
- *
39
- * | Code | Name | Meaning |
40
- * |------|------------|----------------------------------------------------------------------|
41
- * | `0` | `OK` | success: the operation completed; `validate` found the input **valid** |
42
- * | `1` | `INVALID` | operation-level failure: `validate` found the input **invalid** (a real, expected CI signal: the message is bad, the tool worked) |
43
- * | `2` | `USAGE` | usage error: unknown command, bad flag, missing argument (EX_USAGE) |
44
- * | `65` | `DATAERR` | data error: input could not be parsed / format not detected (EX_DATAERR) |
45
- * | `66` | `NOINPUT` | no input: the file does not exist or is unreadable (EX_NOINPUT) |
46
- * | `69` | `UNAVAILABLE` | a required capability is not yet available: e.g. `redact` before `@cosyte/deid` is wired (EX_UNAVAILABLE) |
47
- * | `70` | `SOFTWARE` | internal error: an unexpected exception, i.e. a bug (EX_SOFTWARE) |
48
- *
49
- * The load-bearing `validate` rule: a **parseable-but-invalid** message is exit `1`, never exit `0`.
50
- * The CLI must never print a reassuring line and exit green on a bad message. Exit `65` is reserved
51
- * for input that could not be **parsed** at all (a distinct signal from "parsed, but does not
52
- * conform").
53
- *
54
- * @packageDocumentation
55
- */
56
- /**
57
- * The stable exit-code map. Adding a code is a documented, tested change to the CLI's contract;
58
- * renaming or repurposing one is a breaking change.
59
- *
60
- * @example
61
- * ```ts
62
- * import { EXIT } from "@cosyte/cli";
63
- *
64
- * process.exitCode = EXIT.OK; // => 0
65
- * ```
66
- */
67
- declare const EXIT: {
68
- /** Success: the operation completed; `validate` found the input **valid**. */
69
- readonly OK: 0;
70
- /** Operation-level failure: `validate` found the input **invalid** (parseable but non-conformant):
71
- * a real, expected CI signal that the message is bad and the tool worked. Never emitted for
72
- * unparseable input (that is `DATAERR`). */
73
- readonly INVALID: 1;
74
- /** Usage error: unknown command, bad flag, missing argument (`EX_USAGE`). */
75
- readonly USAGE: 2;
76
- /** Data error: input could not be parsed or its format could not be detected (`EX_DATAERR`). */
77
- readonly DATAERR: 65;
78
- /** No input: the named file does not exist or is unreadable (`EX_NOINPUT`). */
79
- readonly NOINPUT: 66;
80
- /** Unavailable: a required capability is not yet wired (e.g. `redact` before `@cosyte/deid`), a
81
- * distinct non-zero signal that is never a fake success (`EX_UNAVAILABLE`). */
82
- readonly UNAVAILABLE: 69;
83
- /** Internal error: an unexpected exception (a bug), distinct from a handled bad input (`EX_SOFTWARE`). */
84
- readonly SOFTWARE: 70;
85
- };
86
- /**
87
- * A value from {@link EXIT}: the exit code a CLI invocation resolves to.
88
- */
89
- type ExitCode = (typeof EXIT)[keyof typeof EXIT];
90
-
91
- /**
92
- * The result of running a CLI invocation, as data: decoupled from `process` so the whole command
93
- * tree is testable. The thin `bin` entry writes {@link RunResult.stdout} to `process.stdout`,
94
- * {@link RunResult.stderr} to `process.stderr`, and exits with {@link RunResult.exit}.
95
- *
96
- * The two channels carry the CLI's PHI posture: **stdout is the data channel** (the
97
- * user's explicit request: the parsed model), while **stderr is value-free**: codes and positions
98
- * only, never an input value.
99
- *
100
- * @packageDocumentation
101
- */
102
-
103
- /**
104
- * A fully-resolved CLI invocation.
105
- *
106
- * @example
107
- * ```ts
108
- * import { EXIT, type RunResult } from "@cosyte/cli";
109
- *
110
- * const r: RunResult = { stdout: "{}\n", stderr: "", exit: EXIT.OK };
111
- * r.exit; // => 0
112
- * ```
113
- */
114
- interface RunResult {
115
- /** The data channel: the parsed model or requested output. May contain the user's data by design. */
116
- readonly stdout: string;
117
- /** The diagnostic channel is **value-free**: codes, positions, file paths, format names only. */
118
- readonly stderr: string;
119
- /** The exit code the invocation resolves to. */
120
- readonly exit: ExitCode;
121
- }
122
-
123
- /**
124
- * The CLI's **value-free diagnostic channel**. Every error the CLI raises on its own behalf carries a
125
- * stable `CLI_*` code, a fixed exit code, and a **PHI-safe** message built only from positional /
126
- * structural context: a segment or field index, a byte offset, a file path, a format name, a warning
127
- * code. **A diagnostic never echoes an input value** (a name, a DOB, an MRN, a result). This is the
128
- * parsers' "warning = code + position, never a value" rule applied at the CLI edge.
129
- *
130
- * @packageDocumentation
131
- */
132
-
133
- /**
134
- * Stable **CLI diagnostic code** registry: errors the CLI owns (routing, I/O, argument handling),
135
- * distinct from the wrapped library's own warning/issue codes which are passed through unchanged.
136
- * Each code is its own value (`key === value`) so the set survives `Object.values(...)`; renaming or
137
- * removing one is a **breaking change** because scripts branch on the stderr text.
138
- *
139
- * @example
140
- * ```ts
141
- * import { CLI_CODES } from "@cosyte/cli";
142
- *
143
- * if (diagnostic.code === CLI_CODES.CLI_FORMAT_UNDETECTED) {
144
- * // ask the user for --format
145
- * }
146
- * ```
147
- */
148
- declare const CLI_CODES: {
149
- /** A required argument was missing or a flag was invalid. Exit `2`. */
150
- readonly CLI_USAGE: "CLI_USAGE";
151
- /** The named file does not exist or could not be read. Exit `66`. */
152
- readonly CLI_NO_INPUT: "CLI_NO_INPUT";
153
- /** The input was empty, no bytes to detect or parse. Exit `65`. */
154
- readonly CLI_EMPTY_INPUT: "CLI_EMPTY_INPUT";
155
- /** No format signature matched; the CLI will not guess. Names the candidates, never the bytes. Exit `65`. */
156
- readonly CLI_FORMAT_UNDETECTED: "CLI_FORMAT_UNDETECTED";
157
- /** More than one format signature matched; the CLI will not guess. Names the candidates, never the bytes. Exit `65`. */
158
- readonly CLI_FORMAT_AMBIGUOUS: "CLI_FORMAT_AMBIGUOUS";
159
- /** A recognised format whose parser does not support the requested operation (e.g. `parse` on a
160
- * format wired only for `inspect`). Names the format + operation, never the bytes. Exit `65`. */
161
- readonly CLI_FORMAT_UNSUPPORTED: "CLI_FORMAT_UNSUPPORTED";
162
- /** The optional parser package for a recognised format is not installed. The six breadth parsers
163
- * (dicom/x12/ccda/ncpdp/astm/mllp) are `optionalDependencies`: normally installed, but if
164
- * one is absent the CLI degrades to this value-free signal rather than crashing. Exit `69`. */
165
- readonly CLI_PARSER_UNAVAILABLE: "CLI_PARSER_UNAVAILABLE";
166
- /** The wrapped parser rejected the input. Positional context only, never the offending bytes. Exit `65`. */
167
- readonly CLI_PARSE_FAILED: "CLI_PARSE_FAILED";
168
- /** The BYO ConceptMap supplied to `map-codes` is not valid JSON or not a loadable FHIR ConceptMap.
169
- * Names the stable terminology-loader code, never the map's bytes. Exit `65`. */
170
- readonly CLI_MAP_INVALID: "CLI_MAP_INVALID";
171
- /** A command whose ground-layer library is not yet wired (e.g. `redact` before `@cosyte/deid` is
172
- * wired). Never a fake success: a distinct, value-free "unavailable" signal. Exit `69`. */
173
- readonly CLI_NOT_IMPLEMENTED: "CLI_NOT_IMPLEMENTED";
174
- /** An unexpected internal error (a bug). Exit `70`. */
175
- readonly CLI_INTERNAL: "CLI_INTERNAL";
176
- };
177
- /**
178
- * A value from {@link CLI_CODES}: the stable code carried by a {@link CliError}.
179
- */
180
- type CliCode = (typeof CLI_CODES)[keyof typeof CLI_CODES];
181
- /**
182
- * A typed, PHI-safe CLI error. Its {@link CliError.message | message} is **value-free by
183
- * construction**: callers must build it from codes, indices, offsets, format names, and file paths,
184
- * never from an input value. It carries the {@link ExitCode} the invocation resolves to.
185
- *
186
- * @example
187
- * ```ts
188
- * import { CliError, CLI_CODES, EXIT } from "@cosyte/cli";
189
- *
190
- * throw new CliError(CLI_CODES.CLI_NO_INPUT, EXIT.NOINPUT, "cannot read the named file");
191
- * ```
192
- */
193
- declare class CliError extends Error {
194
- /** The stable diagnostic code. */
195
- readonly code: CliCode;
196
- /** The exit code this error resolves the invocation to. */
197
- readonly exit: ExitCode;
198
- /**
199
- * @param code - A stable {@link CliCode}.
200
- * @param exit - The {@link ExitCode} to resolve to.
201
- * @param message - A **value-free** explanation (positional/structural context only, never PHI).
202
- */
203
- constructor(code: CliCode, exit: ExitCode, message: string);
204
- }
205
- /**
206
- * Render a {@link CliError} as a single value-free `stderr` line: `cosyte: <CODE>: <message>`. The
207
- * message is already value-free by the {@link CliError} contract; this only prefixes the tool name
208
- * and the code so scripts and humans can branch on a stable token.
209
- *
210
- * @param err - The CLI error to render.
211
- * @returns The stderr line (no trailing newline).
212
- * @example
213
- * ```ts
214
- * import { CliError, CLI_CODES, EXIT, formatDiagnostic } from "@cosyte/cli";
215
- *
216
- * formatDiagnostic(new CliError(CLI_CODES.CLI_USAGE, EXIT.USAGE, "missing <file> argument"));
217
- * // => "cosyte: CLI_USAGE: missing <file> argument"
218
- * ```
219
- */
220
- declare function formatDiagnostic(err: CliError): string;
221
- /**
222
- * Resolve a {@link CliError} into a value-free {@link RunResult}: empty `stdout` (nothing reaches the
223
- * data channel on an error), the rendered diagnostic on `stderr`, and the error's exit code. The
224
- * single place the CLI turns an owned error into a result, so every command and the dispatcher render
225
- * errors identically.
226
- *
227
- * @param err - The CLI error to resolve.
228
- * @returns The value-free {@link RunResult}.
229
- * @example
230
- * ```ts
231
- * import { CliError, CLI_CODES, EXIT, errorResult } from "@cosyte/cli";
232
- *
233
- * errorResult(new CliError(CLI_CODES.CLI_USAGE, EXIT.USAGE, "missing <file>")).exit; // => 2
234
- * ```
235
- */
236
- declare function errorResult(err: CliError): RunResult;
237
- /**
238
- * Coerce an unknown thrown value into a {@link CliError}. A {@link CliError} passes through; anything
239
- * else becomes a `CLI_INTERNAL` / {@link EXIT.SOFTWARE} error whose message is a **fixed, value-free
240
- * string**: the original message is deliberately discarded so a parser exception that embedded input
241
- * bytes can never reach stderr.
242
- *
243
- * @param err - The caught value.
244
- * @returns A {@link CliError} safe to render to stderr.
245
- * @example
246
- * ```ts
247
- * import { toCliError } from "@cosyte/cli";
248
- *
249
- * toCliError(new Error("boom")).code; // => "CLI_INTERNAL"
250
- * ```
251
- */
252
- declare function toCliError(err: unknown): CliError;
253
-
254
31
  /**
255
32
  * **Content-based format autodetection**: the feature that makes `cosyte parse <file>` feel magic,
256
33
  * and the one that must **fail safe**. Detection sniffs the leading bytes' *content*, never the file
@@ -298,6 +75,20 @@ interface DetectResult {
298
75
  /** The matching candidate formats (0 for `none`, 1 for `certain`, ≥2 for `ambiguous`). Value-free. */
299
76
  readonly candidates: readonly CosyteFormat[];
300
77
  }
78
+ /**
79
+ * How many leading bytes detection reads. Small: every signature lives in the first line (the
80
+ * deepest is DICOM's magic at byte 128). Exported because a **streaming** caller has to know how much
81
+ * of the input to hold back before it can detect the format, and reading a different amount than
82
+ * detection uses would make the two disagree.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * import { DETECT_PREFIX_BYTES } from "@cosyte/cli";
87
+ *
88
+ * DETECT_PREFIX_BYTES; // => 512
89
+ * ```
90
+ */
91
+ declare const DETECT_PREFIX_BYTES = 512;
301
92
  /** The formats content-autodetection can recognise (every format now carries a signature). */
302
93
  declare const DETECTABLE_FORMATS: readonly CosyteFormat[];
303
94
  /**
@@ -369,6 +160,194 @@ declare const KNOWN_FORMATS: readonly CosyteFormat[];
369
160
  */
370
161
  declare function asCosyteFormat(value: string): CosyteFormat | null;
371
162
 
163
+ /**
164
+ * The CLI's **own input-size ceiling**, and the value-free refusal it raises.
165
+ *
166
+ * Node has two hard allocation ceilings a byte pipeline can walk into: a single `Buffer` cannot
167
+ * exceed `buffer.constants.MAX_LENGTH` and a single string cannot exceed
168
+ * `buffer.constants.MAX_STRING_LENGTH`. Crossing either raises a *platform* exception, which is
169
+ * indistinguishable, at the CLI edge, from a bug: it would be reported as an internal error even
170
+ * though the input was merely large. That is a lie about whose fault it is, and it is the reason this
171
+ * module exists.
172
+ *
173
+ * So the CLI declares a limit of its **own**, far below both ceilings, checks it against the
174
+ * **running total as input is read** (never after a whole oversized input has been assembled), and
175
+ * refuses an over-limit invocation with a stable, value-free diagnostic and the **data-error** exit
176
+ * code. The number is a documented part of the command surface: it is stated in `cosyte --help` and
177
+ * in the command reference, and both are rendered from {@link MAX_INPUT_BYTES} so they cannot drift.
178
+ *
179
+ * @packageDocumentation
180
+ */
181
+
182
+ /**
183
+ * The maximum number of input bytes `cosyte parse` reads in one invocation: **67108864 bytes
184
+ * (64 MiB)**.
185
+ *
186
+ * Chosen to sit far below the smaller of Node's two allocation ceilings
187
+ * (`buffer.constants.MAX_STRING_LENGTH`, 536870888 characters), because the bytes read are not the
188
+ * peak: a parsed model rendered as JSON is routinely several times the size of the input that
189
+ * produced it, so the limit has to leave room for the output as well as the input. A power of two, so
190
+ * the byte count and its mebibyte rendering are both exact.
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * import { MAX_INPUT_BYTES } from "@cosyte/cli";
195
+ *
196
+ * MAX_INPUT_BYTES; // => 67108864
197
+ * ```
198
+ */
199
+ declare const MAX_INPUT_BYTES: number;
200
+ /**
201
+ * Render a byte limit as a **concrete number with an explicit byte-based unit**: `"67108864 bytes
202
+ * (64 MiB)"`. Every surface that states the limit (the refusal diagnostic, `cosyte --help`, the
203
+ * command reference) renders it through here, so the number a user reads is always the number the
204
+ * code enforces, and never a vague description of one.
205
+ *
206
+ * @param limit - The limit in bytes. Defaults to {@link MAX_INPUT_BYTES}.
207
+ * @returns The limit as bytes, plus a mebibyte rendering when it is a whole number of mebibytes.
208
+ * @example
209
+ * ```ts
210
+ * import { describeByteLimit } from "@cosyte/cli";
211
+ *
212
+ * describeByteLimit(67108864); // => "67108864 bytes (64 MiB)"
213
+ * describeByteLimit(1000); // => "1000 bytes"
214
+ * ```
215
+ */
216
+ declare function describeByteLimit(limit?: number): string;
217
+ /**
218
+ * Build the value-free over-limit refusal: a stable `CLI_INPUT_TOO_LARGE` code, the **data-error**
219
+ * exit (`65`, never the internal-error `70`), and a message that names the limit and nothing else.
220
+ * The offending input's own size is deliberately not reported either: the limit is the actionable
221
+ * fact, and a size read off the input is a property of the input.
222
+ *
223
+ * @param limit - The limit that was exceeded, in bytes. Defaults to {@link MAX_INPUT_BYTES}.
224
+ * @returns The value-free {@link CliError} to resolve the invocation with.
225
+ * @example
226
+ * ```ts
227
+ * import { inputTooLargeError } from "@cosyte/cli";
228
+ *
229
+ * inputTooLargeError().exit; // => 65
230
+ * inputTooLargeError().code; // => "CLI_INPUT_TOO_LARGE"
231
+ * ```
232
+ */
233
+ declare function inputTooLargeError(limit?: number): CliError;
234
+
235
+ /**
236
+ * **Record streaming**: turning a stream of input byte chunks into a stream of whole records, so a
237
+ * multi-record command can emit a record's output *before* the rest of its input has been read.
238
+ *
239
+ * Two input shapes are multi-record, and both arrive here as chunks rather than as one buffer: an
240
+ * **MLLP** stream (each VT-framed frame encloses one HL7 v2 message) and any input under
241
+ * **`--ndjson`** (each non-empty line is a record). Neither splitter re-implements a wire format: the
242
+ * MLLP one drives `@cosyte/mllp`'s own streaming `FrameReader`, and the NDJSON one splits on the line
243
+ * terminator, which is the bulk-data convention's own framing, not a parse of the records inside it.
244
+ *
245
+ * The size limit is enforced here too, as {@link withinLimit}: a running byte count over the chunk
246
+ * stream, so an over-limit input is refused **while it is arriving** rather than after it has been
247
+ * assembled.
248
+ *
249
+ * @packageDocumentation
250
+ */
251
+ /** A stream of input byte chunks, in arrival order: what every function here consumes. */
252
+ type ByteChunks = AsyncIterable<Uint8Array>;
253
+ /**
254
+ * Wrap `bytes` as a one-chunk stream, so the streaming record path can also serve a caller that
255
+ * hands the CLI a whole buffer (an injected reader, or the programmatic API). The path is the same;
256
+ * only the granularity the input arrives at differs.
257
+ *
258
+ * @param bytes - The whole input.
259
+ * @returns A stream yielding exactly one chunk (or none, when `bytes` is empty).
260
+ * @example
261
+ * ```ts
262
+ * import { oneChunk } from "@cosyte/cli";
263
+ *
264
+ * let n = 0;
265
+ * for await (const c of oneChunk(new Uint8Array([1, 2, 3]))) n += c.length;
266
+ * n; // => 3
267
+ * ```
268
+ */
269
+ declare function oneChunk(bytes: Uint8Array): AsyncGenerator<Uint8Array>;
270
+ /**
271
+ * Pass chunks through while counting them against the CLI's own size limit, and **refuse the moment
272
+ * the running total passes it**: the chunk that crosses the limit is never yielded, the source is
273
+ * never pulled again, and nothing downstream ever allocates memory proportional to an oversized
274
+ * input. This is why the refusal a caller sees is the CLI's own data error rather than a platform
275
+ * allocation failure reported as an internal error.
276
+ *
277
+ * @param chunks - The input chunk stream.
278
+ * @param limit - The maximum number of bytes to accept. Defaults to {@link MAX_INPUT_BYTES}.
279
+ * @returns The same chunks, up to the limit.
280
+ * @throws {CliError} `CLI_INPUT_TOO_LARGE` (exit `65`) as soon as the running total passes `limit`.
281
+ * @example
282
+ * ```ts
283
+ * import { oneChunk, withinLimit } from "@cosyte/cli";
284
+ *
285
+ * const src = withinLimit(oneChunk(new Uint8Array(10)), 4);
286
+ * await src.next().catch((e: unknown) => (e as { code: string }).code); // => "CLI_INPUT_TOO_LARGE"
287
+ * ```
288
+ */
289
+ declare function withinLimit(chunks: ByteChunks, limit?: number): AsyncGenerator<Uint8Array>;
290
+ /**
291
+ * Drain a chunk stream into one buffer: the single-message path, where the parser needs the whole
292
+ * message anyway. The size limit is already applied by {@link withinLimit} upstream, so this cannot
293
+ * accumulate more than the limit allows.
294
+ *
295
+ * @param chunks - The input chunk stream.
296
+ * @returns The concatenated bytes.
297
+ * @example
298
+ * ```ts
299
+ * import { collectChunks, oneChunk } from "@cosyte/cli";
300
+ *
301
+ * (await collectChunks(oneChunk(new TextEncoder().encode("MSH|")))).length; // => 4
302
+ * ```
303
+ */
304
+ declare function collectChunks(chunks: ByteChunks): Promise<Uint8Array>;
305
+ /**
306
+ * Split a chunk stream into **NDJSON records**: each non-empty, newline-terminated line, yielded as
307
+ * soon as its terminator arrives. A record split across two chunks is carried over; a final record
308
+ * with no trailing newline is yielded at end of stream; a blank line carries no record and is
309
+ * skipped.
310
+ *
311
+ * @param chunks - The input chunk stream.
312
+ * @returns A stream of record bytes, in input order.
313
+ * @example
314
+ * ```ts
315
+ * import { ndjsonRecords, oneChunk } from "@cosyte/cli";
316
+ *
317
+ * const src = oneChunk(new TextEncoder().encode('{"a":1}\n\n{"b":2}\n'));
318
+ * const out: string[] = [];
319
+ * for await (const r of ndjsonRecords(src)) out.push(new TextDecoder().decode(r));
320
+ * out.length; // => 2
321
+ * ```
322
+ */
323
+ declare function ndjsonRecords(chunks: ByteChunks): AsyncGenerator<Uint8Array>;
324
+ /**
325
+ * De-frame a chunk stream into **MLLP frame payloads** (each an enclosed HL7 v2 message), yielded as
326
+ * each frame completes. The framing is `@cosyte/mllp`'s own streaming `FrameReader`, fed chunk by
327
+ * chunk; the CLI only tracks where the frame bytes fell so it can tell a **truncated** stream from a
328
+ * complete one.
329
+ *
330
+ * **Truncation is a data error, never a silent drop.** An unterminated trailing frame (a start-of-block
331
+ * byte after the last end-of-block byte) is delivered by no callback, so it would otherwise vanish
332
+ * with a green exit. It is detected at end of stream and raised, after the frames that did complete
333
+ * have already been yielded: their output stands, and the invocation still resolves to the data error.
334
+ *
335
+ * @param chunks - The input chunk stream.
336
+ * @returns A stream of enclosed HL7 payloads, in frame order.
337
+ * @throws {CliError} `CLI_PARSER_UNAVAILABLE` (exit `69`) if `@cosyte/mllp` is absent;
338
+ * `CLI_PARSE_FAILED` (exit `65`) at end of stream on a truncated final frame.
339
+ * @example
340
+ * ```ts
341
+ * import { mllpFrames, oneChunk } from "@cosyte/cli";
342
+ *
343
+ * const framed = new Uint8Array([0x0b, 0x4d, 0x53, 0x48, 0x1c, 0x0d]);
344
+ * const seen: number[] = [];
345
+ * for await (const p of mllpFrames(oneChunk(framed))) seen.push(p.length);
346
+ * seen.length; // => 1
347
+ * ```
348
+ */
349
+ declare function mllpFrames(chunks: ByteChunks): AsyncGenerator<Uint8Array>;
350
+
372
351
  /**
373
352
  * The CLI's **value-free finding** shape and the positional locators that populate it. A finding says
374
353
  * *what kind* of thing was observed (a stable code + severity) and *where* (a positional locator),
@@ -752,6 +731,21 @@ declare function deframeMllp(bytes: Uint8Array): Promise<{
752
731
  payloads: Buffer[];
753
732
  warningCount: number;
754
733
  }>;
734
+ /**
735
+ * The value-free data error for a **truncated MLLP stream**: a frame opened and never closed, which
736
+ * the streaming reader buffers and delivers to no callback. Built here so the whole-buffer de-framer
737
+ * above and the chunk-by-chunk one in `core/records.ts` refuse the same input with the same code and
738
+ * the same words.
739
+ *
740
+ * @returns A `CLI_PARSE_FAILED` (exit `65`) {@link CliError}.
741
+ * @example
742
+ * ```ts
743
+ * import { truncatedMllpError } from "@cosyte/cli";
744
+ *
745
+ * truncatedMllpError().exit; // => 65
746
+ * ```
747
+ */
748
+ declare function truncatedMllpError(): CliError;
755
749
 
756
750
  /**
757
751
  * The shared **input + format resolution** every file-consuming command runs before it touches a
@@ -763,7 +757,13 @@ declare function deframeMllp(bytes: Uint8Array): Promise<{
763
757
  *
764
758
  * Every failure is a value-free {@link CliError} rendered to a {@link RunResult}: a missing argument
765
759
  * is a usage error (`2`), an unreadable file a no-input error (`66`), empty/undetected/unwired input a
766
- * data error (`65`). None ever echoes an input byte.
760
+ * data error (`65`), an input past the documented size limit a data error too (`65`, never the
761
+ * internal-error code). None ever echoes an input byte.
762
+ *
763
+ * Two front doors, one contract: {@link resolveInput} hands back the whole input as one buffer, and
764
+ * {@link resolveInputStream} hands back a chunk stream for a command that emits output before the
765
+ * input has finished arriving. They share the format-resolution and exit-code decisions, so a bad
766
+ * `--format`, an undetectable input and an unwired (format, op) resolve identically either way.
767
767
  *
768
768
  * @packageDocumentation
769
769
  */
@@ -794,7 +794,10 @@ type InputResolution = {
794
794
  * @param formatOverride - The raw `--format` value, or `undefined` to autodetect by content.
795
795
  * @param deps - Injected input readers ({@link RunDeps}).
796
796
  * @param op - The wrapping operation the caller will run; the resolved format is confirmed to support
797
- * it (else a value-free `CLI_FORMAT_UNSUPPORTED` naming the supporting formats).
797
+ * it (else a value-free `CLI_FORMAT_UNSUPPORTED` naming the supporting formats). Pass `null` when
798
+ * the caller answers for capability itself: `redact` delegates to `@cosyte/deid`, whose per-format
799
+ * coverage is a different matrix with its own two distinct refusals, so applying the parser
800
+ * registry's matrix first would answer with the wrong code.
798
801
  * @returns `{ ok: true, input }` when the bytes read and the format resolved to a parser supporting
799
802
  * `op`; else `{ ok: false, result }` with a value-free usage/no-input/data-error {@link RunResult}.
800
803
  * @throws Propagates a **non-`CliError`** read failure unchanged, so the dispatcher maps it to
@@ -811,7 +814,63 @@ type InputResolution = {
811
814
  * if (r.ok) r.input.format; // => "fhir"
812
815
  * ```
813
816
  */
814
- declare function resolveInput(source: string | undefined, formatOverride: string | undefined, deps: RunDeps, op: Op): Promise<InputResolution>;
817
+ declare function resolveInput(source: string | undefined, formatOverride: string | undefined, deps: RunDeps, op: Op | null): Promise<InputResolution>;
818
+ /**
819
+ * A successfully-resolved **streaming** input: the format, plus the input as a stream of chunks with
820
+ * the leading bytes detection consumed put back in front, so the consumer still sees the whole input
821
+ * in order.
822
+ */
823
+ interface ResolvedInputStream {
824
+ /** The resolved format: guaranteed to satisfy `supportsOp(format, op)` for the requested op. */
825
+ readonly format: CosyteFormat;
826
+ /** The whole input as chunks, size-limited: reading past the limit raises `CLI_INPUT_TOO_LARGE`. */
827
+ readonly chunks: ByteChunks;
828
+ }
829
+ /**
830
+ * The outcome of {@link resolveInputStream}: the resolved streaming input, or a ready-to-return
831
+ * value-free {@link RunResult}. The same discriminated-union shape as {@link InputResolution}.
832
+ */
833
+ type InputStreamResolution = {
834
+ readonly ok: true;
835
+ readonly input: ResolvedInputStream;
836
+ } | {
837
+ readonly ok: false;
838
+ readonly result: RunResult;
839
+ };
840
+ /**
841
+ * Resolve the input as a **stream of chunks** rather than one buffer, for a command that can act on a
842
+ * record before the whole input has arrived.
843
+ *
844
+ * It reads only as far as the detection window ({@link DETECT_PREFIX_BYTES}) before deciding the
845
+ * format, then hands back those bytes followed by the rest of the stream, unread. The size limit is
846
+ * applied to the running total from the first chunk, so an over-limit input is refused while it is
847
+ * still arriving and never assembled.
848
+ *
849
+ * When the caller's {@link RunDeps} carry no chunk readers, the whole-input readers are wrapped as a
850
+ * single chunk: the same code path, the same output, only the granularity differs.
851
+ *
852
+ * @param source - The positional `<file|->` argument (or `undefined` when it was omitted).
853
+ * @param formatOverride - The raw `--format` value, or `undefined` to autodetect by content.
854
+ * @param deps - Injected readers ({@link RunDeps}); the chunk readers are used when present.
855
+ * @param op - The wrapping operation the caller will run; the resolved format is confirmed to support it.
856
+ * @param limit - The maximum number of input bytes to accept. Defaults to {@link MAX_INPUT_BYTES}.
857
+ * @returns `{ ok: true, input }` with the format and the chunk stream, else `{ ok: false, result }`
858
+ * carrying the value-free usage / no-input / data-error {@link RunResult}.
859
+ * @throws Propagates a **non-`CliError`** read failure unchanged, so the dispatcher maps it to
860
+ * `CLI_INTERNAL`.
861
+ * @example
862
+ * ```ts
863
+ * import { resolveInputStream } from "@cosyte/cli";
864
+ *
865
+ * const deps = {
866
+ * readFile: async () => new TextEncoder().encode('{"resourceType":"Patient"}'),
867
+ * readStdin: async () => new Uint8Array(),
868
+ * };
869
+ * const r = await resolveInputStream("patient.json", undefined, deps, "parse");
870
+ * if (r.ok) r.input.format; // => "fhir"
871
+ * ```
872
+ */
873
+ declare function resolveInputStream(source: string | undefined, formatOverride: string | undefined, deps: RunDeps, op: Op, limit?: number): Promise<InputStreamResolution>;
815
874
 
816
875
  /**
817
876
  * The CLI's **global PHI display posture**: the single mechanism that gates whether *any* input
@@ -948,57 +1007,189 @@ declare function parseFailureResult(format: CosyteFormat, bytes: Uint8Array, pos
948
1007
 
949
1008
  /**
950
1009
  * The **de-identification seam**: the single, documented plug-in point where the `redact` / `deid`
951
- * command will delegate to **`@cosyte/deid`** once that integration lands and is vetted.
1010
+ * command delegates to **`@cosyte/deid`**. Everything the CLI knows about de-identification lives
1011
+ * here, and it is deliberately little: which formats the delegate covers, how to reach it, and how to
1012
+ * turn its result into the CLI's own value-free shapes.
1013
+ *
1014
+ * ## The CLI ships no redactor of its own (the boundary decision)
1015
+ *
1016
+ * A "minimal built-in Safe-Harbor pass over the obvious PHI loci" (the HL7 `PID` segment,
1017
+ * `Patient.name`/`Patient.address` in FHIR) is **deliberately rejected**. Real messages carry PHI far
1018
+ * beyond those loci: HL7 `NK1`/`GT1`/`IN1`/`OBX`/`NTE`, FHIR extensions and contained resources,
1019
+ * free-text notes. A redactor that scrubs the obvious fields and emits output that *looks*
1020
+ * de-identified while silently leaving PHI behind is a **false-safety impression**. A `redact` that
1021
+ * under-redacts is worse than no `redact`. So the CLI adds no policy, no locus map, no transform and
1022
+ * no fallback scrub: it delegates, or it refuses.
1023
+ *
1024
+ * ## What "covered" means, per format
1025
+ *
1026
+ * A format is covered when the delegate ships an adapter for the standard this CLI actually resolves
1027
+ * **and** the CLI can carry the de-identified result on its text data channel. Both halves bite:
1028
+ *
1029
+ * - `ncpdp`: the delegate's adapter is NCPDP **Telecom**, while this CLI resolves NCPDP **SCRIPT**.
1030
+ * The subpath exists; the coverage does not.
1031
+ * - `dicom`: the delegate covers it, but its result is a Part 10 **byte** stream and this CLI's data
1032
+ * channel is a `string`. That is the CLI's own limit, so it is reported as the CLI's own
1033
+ * unsupported (format, operation) cell rather than blamed on the delegate.
1034
+ * - `astm`, `mllp`: no adapter at all.
1035
+ *
1036
+ * ## The key context, and why it is ephemeral
1037
+ *
1038
+ * The delegate's default policy removes MRN / account / member identifiers rather than replacing
1039
+ * them with a keyed surrogate, so a default run needs no key. The CLI binds a key context anyway,
1040
+ * because a keyed transform with no key context is a fatal there, never an unkeyed fallback, and the
1041
+ * CLI holds no key material and offers no key surface. So it creates a **per-invocation ephemeral
1042
+ * key**: any surrogate the delegate mints is consistent within one output and deliberately **not**
1043
+ * stable across runs (no cross-run linkage). That property is disclosed on the diagnostic channel
1044
+ * rather than left to be discovered.
952
1045
  *
953
- * ## Why the CLI ships no built-in redactor (the boundary decision)
1046
+ * @packageDocumentation
1047
+ */
1048
+
1049
+ /** The npm package the whole de-identification capability is delegated to. */
1050
+ declare const DEID_PACKAGE = "@cosyte/deid";
1051
+ /**
1052
+ * What the delegate can do for one format, from this CLI's point of view:
954
1053
  *
955
- * The CLI does **not wire `@cosyte/deid` yet**, and the wrapped
956
- * parsers (`@cosyte/hl7`, `@cosyte/fhir`) expose **no de-identification API**: they redact only
957
- * their own *diagnostics*, not the parsed model. So there is nothing for the CLI to delegate to
958
- * today.
1054
+ * - `covered`: an adapter for the standard this CLI resolves, serializable onto the text channel.
1055
+ * - `no-adapter`: the delegate does not de-identify this format (`CLI_NOT_IMPLEMENTED`, exit `69`).
1056
+ * - `unserializable`: the delegate covers it, but the result cannot ride a text stdout
1057
+ * (`CLI_FORMAT_UNSUPPORTED`, exit `65`: the CLI's own limit, not the delegate's).
1058
+ */
1059
+ type DeidCoverage = "covered" | "no-adapter" | "unserializable";
1060
+ /** A format the delegate de-identifies and the CLI can serialize. */
1061
+ type DeidCoveredFormat = "hl7" | "fhir" | "x12" | "ccda";
1062
+ /**
1063
+ * The honest per-format de-identification matrix, the `redact` analogue of the parser registry's
1064
+ * operation matrix. Every one of the eight formats has an entry, so a new format cannot be added
1065
+ * without answering for it here.
959
1066
  *
960
- * A tempting alternative, a "minimal built-in Safe-Harbor pass over the obvious PHI loci" (e.g. the
961
- * HL7 `PID` segment, `Patient.name`/`Patient.address` in FHIR), is **deliberately rejected**. Real
962
- * messages carry PHI far beyond those loci: HL7 `NK1`/`GT1`/`IN1`/`OBX`/`NTE`, FHIR extensions and
963
- * contained resources, free-text notes. A redactor that scrubs the obvious fields and emits output
964
- * that *looks* de-identified while silently leaving PHI behind is a **false-safety impression**. A
965
- * `redact` that under-redacts is worse than no `redact`.
1067
+ * @example
1068
+ * ```ts
1069
+ * import { DEID_COVERAGE } from "@cosyte/cli";
966
1070
  *
967
- * So `redact` is an **honest, typed `CLI_NOT_IMPLEMENTED`** (never a fake success, never a partial
968
- * scrub presented as de-identified, never invent a capability the ground layer lacks),
969
- * gated on `@cosyte/deid`, with this seam as its one drop-in point. When that integration lands and is
970
- * conformance-graded, {@link deidStatus} flips to `available` and the command reads the input, calls
971
- * the delegated de-identifier, and emits the de-identified model: the command surface (`redact`,
972
- * `deid`, `<file|->`, `--format`) already exists so that change is additive.
1071
+ * DEID_COVERAGE.hl7; // => "covered"
1072
+ * DEID_COVERAGE.dicom; // => "unserializable"
1073
+ * ```
1074
+ */
1075
+ declare const DEID_COVERAGE: Readonly<Record<CosyteFormat, DeidCoverage>>;
1076
+ /**
1077
+ * What the delegate can do for `format`.
973
1078
  *
974
- * @packageDocumentation
1079
+ * @param format - The resolved format.
1080
+ * @returns That format's {@link DeidCoverage}.
1081
+ * @example
1082
+ * ```ts
1083
+ * import { deidCoverage } from "@cosyte/cli";
1084
+ *
1085
+ * deidCoverage("ncpdp"); // => "no-adapter"
1086
+ * ```
1087
+ */
1088
+ declare function deidCoverage(format: CosyteFormat): DeidCoverage;
1089
+ /**
1090
+ * Whether `format` is one the delegate de-identifies and the CLI can serialize.
1091
+ *
1092
+ * @param format - The resolved format.
1093
+ * @returns `true` iff `redact` produces output for it.
1094
+ * @example
1095
+ * ```ts
1096
+ * import { isDeidCovered } from "@cosyte/cli";
1097
+ *
1098
+ * isDeidCovered("x12"); // => true
1099
+ * ```
1100
+ */
1101
+ declare function isDeidCovered(format: CosyteFormat): format is DeidCoveredFormat;
1102
+ /**
1103
+ * The formats `redact` produces output for, sorted: the single source the help text, the published
1104
+ * documentation and the tests all read, so the three cannot drift.
1105
+ *
1106
+ * @returns The covered format names, sorted.
1107
+ * @example
1108
+ * ```ts
1109
+ * import { deidCoveredFormats } from "@cosyte/cli";
1110
+ *
1111
+ * deidCoveredFormats(); // => ["ccda", "fhir", "hl7", "x12"]
1112
+ * ```
1113
+ */
1114
+ declare function deidCoveredFormats(): readonly CosyteFormat[];
1115
+ /**
1116
+ * One entry of the delegate's value-free manifest, narrowed to the fields the CLI renders. The
1117
+ * `locus` is a **path, never a value**, by the delegate's own contract; nothing here carries an input
1118
+ * value, which is what lets the manifest go on the diagnostic channel at all.
1119
+ */
1120
+ interface DeidManifestRecord {
1121
+ /** The identifier category acted on. */
1122
+ readonly category: string;
1123
+ /** The transform applied. */
1124
+ readonly transform: string;
1125
+ /** The structural path acted on: never a value. */
1126
+ readonly locus: string;
1127
+ /** How many values at this locus were acted on. */
1128
+ readonly count: number;
1129
+ /** What happened: `transformed`, `removed`, or the fail-closed `blocked`. */
1130
+ readonly disposition: string;
1131
+ /** The delegate's stable disposition code. */
1132
+ readonly code: string;
1133
+ }
1134
+ /** The fail-closed disposition: the delegate reports it could not handle the element. */
1135
+ declare const DEID_BLOCKED = "blocked";
1136
+ /** A completed de-identification pass: the serialized document plus the delegate's manifest. */
1137
+ interface RedactOutcome {
1138
+ /** The de-identified document, serialized exactly as this format's wrapped serializer emits it. */
1139
+ readonly output: string;
1140
+ /** The delegate's value-free manifest, in locus order. */
1141
+ readonly manifest: readonly DeidManifestRecord[];
1142
+ }
1143
+ /**
1144
+ * The delegate, reduced to what the command needs: its own published honesty label and version, and
1145
+ * one call that de-identifies an input. Deliberately CLI-owned shapes, so the delegate's types never
1146
+ * reach this package's published surface.
1147
+ */
1148
+ interface DeidDelegate {
1149
+ /** The delegate's own output label, quoted verbatim; the CLI asserts no standard of its own. */
1150
+ readonly label: string;
1151
+ /** The delegate's own version string. */
1152
+ readonly version: string;
1153
+ /**
1154
+ * De-identify one input under the delegate's default policy.
1155
+ *
1156
+ * @param format - A covered format.
1157
+ * @param bytes - The input bytes.
1158
+ * @returns The serialized de-identified document and the value-free manifest.
1159
+ */
1160
+ readonly redact: (format: DeidCoveredFormat, bytes: Uint8Array) => Promise<RedactOutcome>;
1161
+ }
1162
+ /**
1163
+ * The value-free reason `redact`/`deid` reports when the de-identification library is absent from the
1164
+ * install. It is an `optionalDependency`, so a copy without it degrades rather than crashing.
975
1165
  */
976
- /** The value-free reason `redact`/`deid` reports while the ground-layer library is unavailable. */
977
1166
  declare const DEID_UNAVAILABLE_REASON: string;
978
1167
  /**
979
1168
  * Whether de-identification is available, and (while it is not) the value-free reason why. The one
980
- * function the `redact` command consults **before reading any input**, so a `redact` invocation never
981
- * touches the PHI it cannot yet safely strip.
1169
+ * function that answers "is the ground layer here?", consulted **before any input is read** so a
1170
+ * `redact` invocation never touches PHI it cannot strip.
982
1171
  */
983
1172
  interface DeidAvailability {
984
- /** `true` once `@cosyte/deid` is wired; `false` today. */
1173
+ /** `true` when `@cosyte/deid` resolves in this install. */
985
1174
  readonly available: boolean;
986
- /** A value-free explanation shown when {@link DeidAvailability.available} is `false`. */
1175
+ /** A value-free explanation: the delegate's own label and version, or why it is unavailable. */
987
1176
  readonly reason: string;
988
1177
  }
989
1178
  /**
990
- * Report the current de-identification availability. Today it is always unavailable (see the module
991
- * docs); this is the single line that flips when `@cosyte/deid` is wired.
1179
+ * Report the current de-identification availability. This is the single documented flip point: it
1180
+ * resolves the delegated library rather than asserting anything about it.
992
1181
  *
993
- * @returns The {@link DeidAvailability}: `{ available: false, reason }` until `@cosyte/deid` is wired.
1182
+ * @param load - The delegate loader; injectable so a test can drive the unavailable branch.
1183
+ * @returns `{ available: true }` with the delegate's own label and version, else `{ available: false }`
1184
+ * with the value-free {@link DEID_UNAVAILABLE_REASON}.
994
1185
  * @example
995
1186
  * ```ts
996
1187
  * import { deidStatus } from "@cosyte/cli";
997
1188
  *
998
- * deidStatus().available; // => false
1189
+ * (await deidStatus()).available; // => true when @cosyte/deid is installed
999
1190
  * ```
1000
1191
  */
1001
- declare function deidStatus(): DeidAvailability;
1192
+ declare function deidStatus(load?: () => Promise<DeidDelegate>): Promise<DeidAvailability>;
1002
1193
 
1003
1194
  /**
1004
1195
  * The top-level **subcommand dispatcher**: a small hand-rolled router over Node's built-in
@@ -1010,6 +1201,23 @@ declare function deidStatus(): DeidAvailability;
1010
1201
  * @packageDocumentation
1011
1202
  */
1012
1203
 
1204
+ /**
1205
+ * Build the value-free `--help` text. Names commands, flags, the input-size limit, and exit codes,
1206
+ * never any input.
1207
+ *
1208
+ * The limit is **rendered from the constant the code enforces**, not typed in: the help output and
1209
+ * the published command reference state one number because there is only one number to state.
1210
+ *
1211
+ * @param limitBytes - The input-size limit to state. Defaults to {@link MAX_INPUT_BYTES}.
1212
+ * @returns The full help text.
1213
+ * @example
1214
+ * ```ts
1215
+ * import { helpText } from "@cosyte/cli";
1216
+ *
1217
+ * helpText(1024).includes("1024 bytes"); // => true
1218
+ * ```
1219
+ */
1220
+ declare function helpText(limitBytes?: number): string;
1013
1221
  /**
1014
1222
  * Run a full `cosyte` invocation as data.
1015
1223
  *
@@ -1041,7 +1249,14 @@ declare function run(argv: string[], deps: RunDeps): Promise<RunResult>;
1041
1249
  * `{ record, error }` line and the stream continues, and the overall exit is a data error (`65`) if any
1042
1250
  * record failed. Two inputs are multi-record: an **MLLP** stream (each VT-framed frame is an enclosed
1043
1251
  * HL7 message) and any input under **`--ndjson`** (each non-empty line is a record: the FHIR bulk-data
1044
- * convention).
1252
+ * convention). Multi-record output is emitted **record by record, as each is parsed**, so the first
1253
+ * line reaches the data channel long before the last byte of input has been read.
1254
+ *
1255
+ * **The size limit.** One invocation reads at most the documented number of input bytes (see
1256
+ * `core/limits.ts`), counted against the running total as the input arrives. A larger input is a
1257
+ * value-free `CLI_INPUT_TOO_LARGE` **data error**, never the internal-error code, and never a partial
1258
+ * record stream presented as a complete one: whatever reached stdout before the refusal stands, and
1259
+ * the invocation still resolves to the failure's own non-zero exit code.
1045
1260
  *
1046
1261
  * The CLI adds **no** parsing of its own: it routes, reads, and shapes output; `cosyte parse` equals the
1047
1262
  * wrapped library's programmatic parse.
@@ -1053,13 +1268,16 @@ declare function run(argv: string[], deps: RunDeps): Promise<RunResult>;
1053
1268
  * Run the `parse` command.
1054
1269
  *
1055
1270
  * @param args - The arguments after the `parse` subcommand token.
1056
- * @param deps - Injected input readers ({@link RunDeps}).
1271
+ * @param deps - Injected I/O ({@link RunDeps}). When it carries the chunk readers and the output
1272
+ * sink, a multi-record input is read and emitted incrementally; when it does not, the same path
1273
+ * runs over one chunk and the output comes back on `stdout` instead.
1057
1274
  * @param posture - The resolved {@link PhiPosture}. Defaults to {@link VALUE_FREE}; under
1058
1275
  * `--unsafe-show-values` a bounded excerpt of the offending input is appended to a `CLI_PARSE_FAILED`
1059
1276
  * diagnostic (the single, opt-in value-echoing surface): single-record mode only.
1060
1277
  * @returns A {@link RunResult}: the typed-JSON model (or NDJSON records) on `stdout`, a value-free note
1061
- * (or nothing) on `stderr`, and the resolved exit code. Never throws a {@link CliError}: it resolves
1062
- * it to a result; unexpected exceptions are caught by the dispatcher and mapped to `CLI_INTERNAL`.
1278
+ * (or nothing) on `stderr`, and the resolved exit code. An input past the documented size limit is a
1279
+ * value-free data error naming the limit. Never throws a {@link CliError}: it resolves it to a
1280
+ * result; unexpected exceptions are caught by the dispatcher and mapped to `CLI_INTERNAL`.
1063
1281
  * @throws Never {@link CliError}; may propagate a truly unexpected error for the dispatcher to map.
1064
1282
  * @example
1065
1283
  * ```ts
@@ -1330,19 +1548,26 @@ declare function mapCodesCommand(args: string[], deps: RunDeps): Promise<RunResu
1330
1548
 
1331
1549
  /**
1332
1550
  * `cosyte redact <file|-> [--format …]` (alias `cosyte deid …`): the command whose *job* is to strip
1333
- * PHI, emitting a de-identified copy of the parsed model.
1334
- *
1335
- * **It is deliberately not implemented yet.** De-identification belongs to **`@cosyte/deid`**, which
1336
- * the CLI does not wire yet, and the wrapped parsers expose no de-id API to delegate to. A
1337
- * built-in "minimal Safe-Harbor" pass over only the obvious PHI loci would leave PHI behind and
1338
- * present a **false-safety impression**: the cardinal hazard `redact` exists to avoid. So this
1339
- * command is an **honest, typed `CLI_NOT_IMPLEMENTED`** (never a fake success, never a partial scrub
1340
- * dressed up as de-identified), gated on the de-identification seam in `core/deid.ts`.
1341
- *
1342
- * To keep that guarantee airtight, the command **never reads the input**: it consults
1343
- * {@link deidStatus} first, and (while de-id is unavailable) reports the value-free reason and exits
1344
- * `EX_UNAVAILABLE` (`69`) without ever touching the file's bytes. The command surface (positional
1345
- * `<file|->`, `--format`) already exists so wiring `@cosyte/deid` later is additive.
1551
+ * identifiers, emitting a de-identified copy of the message on the data channel.
1552
+ *
1553
+ * **It delegates; it never approximates.** The whole policy belongs to **`@cosyte/deid`** (the seam in
1554
+ * `core/deid.ts`): the CLI adds no locus map, no transform and no fallback scrub, because a partial
1555
+ * pass over only the obvious PHI loci would leave PHI behind and present a **false-safety
1556
+ * impression**, the cardinal hazard this command exists to avoid. So anything short of a clean,
1557
+ * fully-handled pass is a non-zero exit with **empty stdout**:
1558
+ *
1559
+ * - the delegate ships no adapter for the resolved format: `CLI_NOT_IMPLEMENTED`, exit `69`;
1560
+ * - the delegate covers it but the CLI cannot carry the result on a text stdout (DICOM's Part 10
1561
+ * bytes): `CLI_FORMAT_UNSUPPORTED`, exit `65`, the CLI's own limit rather than the delegate's;
1562
+ * - the delegate reports it could not handle an element: `CLI_DEID_INCOMPLETE`, exit `1`;
1563
+ * - the library is absent from the install: `CLI_PARSER_UNAVAILABLE`, exit `69`, decided **before any
1564
+ * input is read**.
1565
+ *
1566
+ * The diagnostic channel carries the delegate's own value-free manifest (category, transform, the
1567
+ * structural locus, count, disposition code) and the delegate's own published label and version. The
1568
+ * CLI claims no de-identification standard of its own, and never echoes an input value here: not even
1569
+ * under `--unsafe-show-values`, whose bounded excerpt is deliberately not honoured by *this* command,
1570
+ * because an excerpt of the un-stripped input is exactly what `redact` exists to prevent.
1346
1571
  *
1347
1572
  * @packageDocumentation
1348
1573
  */
@@ -1351,18 +1576,27 @@ declare function mapCodesCommand(args: string[], deps: RunDeps): Promise<RunResu
1351
1576
  * Run the `redact` / `deid` command.
1352
1577
  *
1353
1578
  * @param args - The arguments after the `redact`/`deid` subcommand token.
1354
- * @returns A {@link RunResult}. While `@cosyte/deid` is unavailable: empty `stdout`, a value-free
1355
- * `CLI_NOT_IMPLEMENTED` diagnostic on `stderr`, and exit `EX_UNAVAILABLE` (`69`). A malformed
1356
- * invocation (an unknown flag) is a usage error (exit `2`). The input is **never read**.
1579
+ * @param deps - Injected input readers ({@link RunDeps}).
1580
+ * @param loadDelegate - The de-identification delegate loader; injectable so a test can drive the
1581
+ * library-absent path without uninstalling it.
1582
+ * @returns A {@link RunResult}. A clean pass over a covered format: the de-identified document on
1583
+ * `stdout`, the delegate's attribution and value-free manifest on `stderr`, exit `0`. Otherwise
1584
+ * empty `stdout` and a typed non-zero exit: `1` when the delegate could not handle an element, `69`
1585
+ * when it has no adapter for the format or is absent, `65` when the format cannot ride a text
1586
+ * stdout or the input did not parse, `66` for an unreadable file, `2` for a bad flag.
1587
+ * @throws Never {@link CliError}; may propagate a truly unexpected error for the dispatcher to map.
1357
1588
  * @example
1358
1589
  * ```ts
1359
1590
  * import { redactCommand } from "@cosyte/cli";
1360
1591
  *
1361
- * const r = redactCommand(["message.hl7"]);
1362
- * r.exit; // => 69
1592
+ * const deps = {
1593
+ * readFile: async () => new TextEncoder().encode('{"resourceType":"Patient","id":"x"}'),
1594
+ * readStdin: async () => new Uint8Array(),
1595
+ * };
1596
+ * (await redactCommand(["patient.json"], deps)).exit; // => 0
1363
1597
  * ```
1364
1598
  */
1365
- declare function redactCommand(args: string[]): RunResult;
1599
+ declare function redactCommand(args: string[], deps: RunDeps, loadDelegate?: () => Promise<DeidDelegate>): Promise<RunResult>;
1366
1600
 
1367
1601
  /**
1368
1602
  * `cosyte completion <bash|zsh|fish>`
@@ -1391,4 +1625,4 @@ declare function redactCommand(args: string[]): RunResult;
1391
1625
  */
1392
1626
  declare function completionCommand(args: string[]): RunResult;
1393
1627
 
1394
- export { CLI_CODES, type CliCode, CliError, type CosyteFormat, DEID_UNAVAILABLE_REASON, DETECTABLE_FORMATS, type DeidAvailability, type DetectResult, EXIT, type ExitCode, type Finding, type FmtResult, type InputResolution, type InspectSummary, KNOWN_FORMATS, OP_SUPPORT, type Op, type ParseResult, type ParseWarning, type PhiPosture, type ResolvedInput, RunDeps, type RunResult, SHOW_VALUES, UNSAFE_EXCERPT_MAX, UNSAFE_SHOW_VALUES_FLAG, VALUE_FREE, VERSION, type Verdict, asCosyteFormat, classifyCandidates, completionCommand, convertCommand, convertOutcome, deframeMllp, deidStatus, detectFormat, detectionError, errorResult, extractPhiPosture, extractStableCode, fmtCommand, fmtFormat, formatDiagnostic, formatHl7Position, formatsSupporting, inspectCommand, inspectFormat, loadOptional, loadOptionalPackage, mapCodesCommand, parseCommand, parseFailureResult, parseFormat, redactCommand, resolveInput, run, supportsOp, toCliError, unsafeInputSuffix, validateCommand, validateFormat, valueFreeLocator };
1628
+ export { type ByteChunks, CliError, type CosyteFormat, DEID_BLOCKED, DEID_COVERAGE, DEID_PACKAGE, DEID_UNAVAILABLE_REASON, DETECTABLE_FORMATS, DETECT_PREFIX_BYTES, type DeidAvailability, type DeidCoverage, type DeidCoveredFormat, type DeidDelegate, type DeidManifestRecord, type DetectResult, type Finding, type FmtResult, type InputResolution, type InputStreamResolution, type InspectSummary, KNOWN_FORMATS, MAX_INPUT_BYTES, OP_SUPPORT, type Op, type ParseResult, type ParseWarning, type PhiPosture, type RedactOutcome, type ResolvedInput, type ResolvedInputStream, RunDeps, RunResult, SHOW_VALUES, UNSAFE_EXCERPT_MAX, UNSAFE_SHOW_VALUES_FLAG, VALUE_FREE, VERSION, type Verdict, asCosyteFormat, classifyCandidates, collectChunks, completionCommand, convertCommand, convertOutcome, deframeMllp, deidCoverage, deidCoveredFormats, deidStatus, describeByteLimit, detectFormat, detectionError, extractPhiPosture, extractStableCode, fmtCommand, fmtFormat, formatHl7Position, formatsSupporting, helpText, inputTooLargeError, inspectCommand, inspectFormat, isDeidCovered, loadOptional, loadOptionalPackage, mapCodesCommand, mllpFrames, ndjsonRecords, oneChunk, parseCommand, parseFailureResult, parseFormat, redactCommand, resolveInput, resolveInputStream, run, supportsOp, truncatedMllpError, unsafeInputSuffix, validateCommand, validateFormat, valueFreeLocator, withinLimit };