@cosyte/cli 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +416 -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/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as RunDeps } from './io-
|
|
2
|
-
export { r as readFileBytes,
|
|
1
|
+
import { a as CliError, b as RunResult, R as RunDeps } from './io-C9bSG2zF.cjs';
|
|
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.cjs';
|
|
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`)
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
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
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
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
|
-
*
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
* @
|
|
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
|
|
981
|
-
* touches
|
|
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`
|
|
1173
|
+
/** `true` when `@cosyte/deid` resolves in this install. */
|
|
985
1174
|
readonly available: boolean;
|
|
986
|
-
/** A value-free explanation
|
|
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.
|
|
991
|
-
*
|
|
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
|
-
* @
|
|
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; // =>
|
|
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
|
|
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.
|
|
1062
|
-
*
|
|
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
|
-
*
|
|
1334
|
-
*
|
|
1335
|
-
* **It
|
|
1336
|
-
* the CLI
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
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
|
-
* @
|
|
1355
|
-
*
|
|
1356
|
-
*
|
|
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
|
|
1362
|
-
*
|
|
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 {
|
|
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 };
|