@cosyte/cli 0.0.6 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +326 -2
- package/README.md +118 -53
- 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 +30 -15
- package/dist/io-B2EobRzC.d.cts +0 -57
- package/dist/io-B2EobRzC.d.ts +0 -57
package/dist/index.mjs
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { open } from 'fs/promises';
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
2
3
|
import { parseArgs } from 'util';
|
|
3
4
|
|
|
4
5
|
// src/core/version.ts
|
|
5
|
-
var VERSION = "0.
|
|
6
|
+
var VERSION = "0.1.1";
|
|
6
7
|
|
|
7
8
|
// src/core/exit-codes.ts
|
|
8
9
|
var EXIT = {
|
|
9
10
|
/** Success: the operation completed; `validate` found the input **valid**. */
|
|
10
11
|
OK: 0,
|
|
11
|
-
/** Operation-level failure: `validate` found the input **invalid** (parseable but non-conformant)
|
|
12
|
-
* a real, expected CI signal that the
|
|
13
|
-
* unparseable input (that is `DATAERR`). */
|
|
12
|
+
/** Operation-level failure: `validate` found the input **invalid** (parseable but non-conformant),
|
|
13
|
+
* or `redact` could not de-identify every locus: a real, expected CI signal that the tool worked
|
|
14
|
+
* and the operation did not complete. Never emitted for unparseable input (that is `DATAERR`). */
|
|
14
15
|
INVALID: 1,
|
|
15
|
-
/** Usage error: unknown command, bad flag, missing argument
|
|
16
|
+
/** Usage error: unknown command, bad flag, missing argument. A value specific to this CLI, not a
|
|
17
|
+
* `sysexits.h` constant. */
|
|
16
18
|
USAGE: 2,
|
|
17
19
|
/** Data error: input could not be parsed or its format could not be detected (`EX_DATAERR`). */
|
|
18
20
|
DATAERR: 65,
|
|
19
21
|
/** No input: the named file does not exist or is unreadable (`EX_NOINPUT`). */
|
|
20
22
|
NOINPUT: 66,
|
|
21
|
-
/** Unavailable: a required capability is not
|
|
22
|
-
*
|
|
23
|
+
/** Unavailable: a required capability is not wired for this input (e.g. `redact` on a format
|
|
24
|
+
* `@cosyte/deid` has no adapter for, or with that optional library absent), a distinct non-zero
|
|
25
|
+
* signal that is never a fake success (`EX_UNAVAILABLE`). */
|
|
23
26
|
UNAVAILABLE: 69,
|
|
24
27
|
/** Internal error: an unexpected exception (a bug), distinct from a handled bad input (`EX_SOFTWARE`). */
|
|
25
|
-
SOFTWARE: 70
|
|
28
|
+
SOFTWARE: 70,
|
|
29
|
+
/** Output error: the stream the CLI was writing to closed before the output had been delivered, so
|
|
30
|
+
* the consumer holds nothing or holds only part of the answer. The shape a downstream consumer
|
|
31
|
+
* that exits early produces (a pager quit, a `head` that has seen enough). A handled condition the
|
|
32
|
+
* consumer owns, never a defect in the CLI, so it is deliberately distinct from `SOFTWARE`
|
|
33
|
+
* (`EX_IOERR`). */
|
|
34
|
+
IOERR: 74
|
|
26
35
|
};
|
|
27
36
|
|
|
28
37
|
// src/core/diagnostics.ts
|
|
@@ -46,12 +55,27 @@ var CLI_CODES = {
|
|
|
46
55
|
CLI_PARSER_UNAVAILABLE: "CLI_PARSER_UNAVAILABLE",
|
|
47
56
|
/** The wrapped parser rejected the input. Positional context only, never the offending bytes. Exit `65`. */
|
|
48
57
|
CLI_PARSE_FAILED: "CLI_PARSE_FAILED",
|
|
58
|
+
/** The input is larger than the documented size limit the CLI reads in one invocation. Raised
|
|
59
|
+
* against the running byte count while the input is read, so the refusal always arrives before an
|
|
60
|
+
* allocation ceiling could. Names the limit, never the input. Exit `65`, never `70`. */
|
|
61
|
+
CLI_INPUT_TOO_LARGE: "CLI_INPUT_TOO_LARGE",
|
|
62
|
+
/** The output stream could not be written: a consumer that closes the pipe ends the invocation
|
|
63
|
+
* here rather than as an unhandled platform error, whether it went away part way through a record
|
|
64
|
+
* stream or before a single-result write was delivered. Names no input, and never the platform's
|
|
65
|
+
* own error text. Exit `74`. */
|
|
66
|
+
CLI_OUTPUT_WRITE_FAILED: "CLI_OUTPUT_WRITE_FAILED",
|
|
49
67
|
/** The BYO ConceptMap supplied to `map-codes` is not valid JSON or not a loadable FHIR ConceptMap.
|
|
50
68
|
* Names the stable terminology-loader code, never the map's bytes. Exit `65`. */
|
|
51
69
|
CLI_MAP_INVALID: "CLI_MAP_INVALID",
|
|
52
|
-
/** A command whose ground-layer library
|
|
53
|
-
*
|
|
70
|
+
/** A command whose ground-layer library does not cover the resolved format (e.g. `redact` on a
|
|
71
|
+
* format `@cosyte/deid` ships no adapter for). Never a fake success: a distinct, value-free
|
|
72
|
+
* "unavailable" signal. Exit `69`. */
|
|
54
73
|
CLI_NOT_IMPLEMENTED: "CLI_NOT_IMPLEMENTED",
|
|
74
|
+
/** The delegated de-identifier reported that it could not handle every element of the input (a
|
|
75
|
+
* fail-closed `blocked` locus, or its own fatal). The run is **not** a de-identified copy, so no
|
|
76
|
+
* output is emitted: an operation-level failure, never a bug. Names loci and codes, never a
|
|
77
|
+
* value. Exit `1`. */
|
|
78
|
+
CLI_DEID_INCOMPLETE: "CLI_DEID_INCOMPLETE",
|
|
55
79
|
/** An unexpected internal error (a bug). Exit `70`. */
|
|
56
80
|
CLI_INTERNAL: "CLI_INTERNAL"
|
|
57
81
|
};
|
|
@@ -89,7 +113,8 @@ function toCliError(err) {
|
|
|
89
113
|
}
|
|
90
114
|
|
|
91
115
|
// src/core/format.ts
|
|
92
|
-
var
|
|
116
|
+
var DETECT_PREFIX_BYTES = 512;
|
|
117
|
+
var SNIFF_BYTES = DETECT_PREFIX_BYTES;
|
|
93
118
|
function leadingText(bytes) {
|
|
94
119
|
const slice = bytes.subarray(0, SNIFF_BYTES);
|
|
95
120
|
let text = new TextDecoder("utf-8", { fatal: false }).decode(slice);
|
|
@@ -186,6 +211,21 @@ function asCosyteFormat(value) {
|
|
|
186
211
|
return KNOWN_FORMATS.includes(value) ? value : null;
|
|
187
212
|
}
|
|
188
213
|
|
|
214
|
+
// src/core/limits.ts
|
|
215
|
+
var BYTES_PER_MIB = 1024 * 1024;
|
|
216
|
+
var MAX_INPUT_BYTES = 64 * BYTES_PER_MIB;
|
|
217
|
+
function describeByteLimit(limit = MAX_INPUT_BYTES) {
|
|
218
|
+
const mib = limit / BYTES_PER_MIB;
|
|
219
|
+
return Number.isInteger(mib) ? `${String(limit)} bytes (${String(mib)} MiB)` : `${String(limit)} bytes`;
|
|
220
|
+
}
|
|
221
|
+
function inputTooLargeError(limit = MAX_INPUT_BYTES) {
|
|
222
|
+
return new CliError(
|
|
223
|
+
CLI_CODES.CLI_INPUT_TOO_LARGE,
|
|
224
|
+
EXIT.DATAERR,
|
|
225
|
+
`input is larger than the ${describeByteLimit(limit)} this command reads in one invocation; split the input and re-run`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
189
229
|
// src/core/findings.ts
|
|
190
230
|
function formatHl7Position(pos) {
|
|
191
231
|
let out = `seg[${String(pos.segmentIndex)}]`;
|
|
@@ -535,11 +575,7 @@ async function validateFormat(format, bytes) {
|
|
|
535
575
|
}
|
|
536
576
|
async function deframeMllp(bytes) {
|
|
537
577
|
if (bytes.lastIndexOf(11) > bytes.lastIndexOf(28)) {
|
|
538
|
-
throw
|
|
539
|
-
CLI_CODES.CLI_PARSE_FAILED,
|
|
540
|
-
EXIT.DATAERR,
|
|
541
|
-
"truncated MLLP stream: an unterminated frame at the end of input (no closing FS/CR)"
|
|
542
|
-
);
|
|
578
|
+
throw truncatedMllpError();
|
|
543
579
|
}
|
|
544
580
|
const { FrameReader } = await loadOptional("mllp", () => import('@cosyte/mllp'));
|
|
545
581
|
const payloads = [];
|
|
@@ -551,6 +587,13 @@ async function deframeMllp(bytes) {
|
|
|
551
587
|
reader.push(Buffer.from(bytes));
|
|
552
588
|
return { payloads, warningCount };
|
|
553
589
|
}
|
|
590
|
+
function truncatedMllpError() {
|
|
591
|
+
return new CliError(
|
|
592
|
+
CLI_CODES.CLI_PARSE_FAILED,
|
|
593
|
+
EXIT.DATAERR,
|
|
594
|
+
"truncated MLLP stream: an unterminated frame at the end of input (no closing FS/CR)"
|
|
595
|
+
);
|
|
596
|
+
}
|
|
554
597
|
function decode(bytes) {
|
|
555
598
|
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
556
599
|
}
|
|
@@ -566,6 +609,84 @@ function unsupportedInternal(format, op) {
|
|
|
566
609
|
);
|
|
567
610
|
}
|
|
568
611
|
|
|
612
|
+
// src/core/records.ts
|
|
613
|
+
var LINE_FEED = 10;
|
|
614
|
+
var CARRIAGE_RETURN = 13;
|
|
615
|
+
var VERTICAL_TAB = 11;
|
|
616
|
+
var FILE_SEPARATOR = 28;
|
|
617
|
+
function asBuffer(chunk) {
|
|
618
|
+
return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.length);
|
|
619
|
+
}
|
|
620
|
+
async function* oneChunk(bytes) {
|
|
621
|
+
await Promise.resolve();
|
|
622
|
+
if (bytes.length > 0) yield bytes;
|
|
623
|
+
}
|
|
624
|
+
async function* withinLimit(chunks, limit = MAX_INPUT_BYTES) {
|
|
625
|
+
let total = 0;
|
|
626
|
+
for await (const chunk of chunks) {
|
|
627
|
+
total += chunk.length;
|
|
628
|
+
if (total > limit) throw inputTooLargeError(limit);
|
|
629
|
+
yield chunk;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async function collectChunks(chunks) {
|
|
633
|
+
const parts = [];
|
|
634
|
+
for await (const chunk of chunks) parts.push(asBuffer(chunk));
|
|
635
|
+
return Buffer.concat(parts);
|
|
636
|
+
}
|
|
637
|
+
function isBlank(line) {
|
|
638
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(line).trim().length === 0;
|
|
639
|
+
}
|
|
640
|
+
function stripCarriageReturn(line) {
|
|
641
|
+
return line.length > 0 && line[line.length - 1] === CARRIAGE_RETURN ? line.subarray(0, line.length - 1) : line;
|
|
642
|
+
}
|
|
643
|
+
async function* ndjsonRecords(chunks) {
|
|
644
|
+
let carry = Buffer.alloc(0);
|
|
645
|
+
for await (const chunk of chunks) {
|
|
646
|
+
const buf = carry.length > 0 ? Buffer.concat([carry, asBuffer(chunk)]) : asBuffer(chunk);
|
|
647
|
+
let start = 0;
|
|
648
|
+
for (; ; ) {
|
|
649
|
+
const end = buf.indexOf(LINE_FEED, start);
|
|
650
|
+
if (end < 0) break;
|
|
651
|
+
const line = stripCarriageReturn(buf.subarray(start, end));
|
|
652
|
+
if (!isBlank(line)) yield line;
|
|
653
|
+
start = end + 1;
|
|
654
|
+
}
|
|
655
|
+
carry = start === 0 ? asBuffer(buf) : Buffer.from(buf.subarray(start));
|
|
656
|
+
}
|
|
657
|
+
const last = stripCarriageReturn(carry);
|
|
658
|
+
if (!isBlank(last)) yield last;
|
|
659
|
+
}
|
|
660
|
+
async function* mllpFrames(chunks) {
|
|
661
|
+
const { FrameReader } = await loadOptional("mllp", () => import('@cosyte/mllp'));
|
|
662
|
+
const ready = [];
|
|
663
|
+
const reader = new FrameReader({
|
|
664
|
+
onFrame: (payload) => ready.push(Buffer.from(payload)),
|
|
665
|
+
onWarning: () => void 0,
|
|
666
|
+
// The CLI's own documented input limit is the binding one, so the reader's smaller default frame
|
|
667
|
+
// ceiling is raised to it: an over-limit MLLP stream is refused by the limit the help output and
|
|
668
|
+
// the command reference state, not by an undocumented one underneath it.
|
|
669
|
+
maxFrameSizeBytes: MAX_INPUT_BYTES
|
|
670
|
+
});
|
|
671
|
+
let lastStart = -1;
|
|
672
|
+
let lastEnd = -1;
|
|
673
|
+
let offset = 0;
|
|
674
|
+
for await (const chunk of chunks) {
|
|
675
|
+
const buf = asBuffer(chunk);
|
|
676
|
+
const start = buf.lastIndexOf(VERTICAL_TAB);
|
|
677
|
+
if (start >= 0) lastStart = offset + start;
|
|
678
|
+
const end = buf.lastIndexOf(FILE_SEPARATOR);
|
|
679
|
+
if (end >= 0) lastEnd = offset + end;
|
|
680
|
+
offset += buf.length;
|
|
681
|
+
reader.push(buf);
|
|
682
|
+
while (ready.length > 0) {
|
|
683
|
+
const frame = ready.shift();
|
|
684
|
+
if (frame !== void 0) yield frame;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (lastStart > lastEnd) throw truncatedMllpError();
|
|
688
|
+
}
|
|
689
|
+
|
|
569
690
|
// src/core/input.ts
|
|
570
691
|
async function resolveInput(source, formatOverride, deps, op) {
|
|
571
692
|
if (source === void 0) {
|
|
@@ -587,34 +708,91 @@ async function resolveInput(source, formatOverride, deps, op) {
|
|
|
587
708
|
if (bytes.length === 0) {
|
|
588
709
|
return fail(new CliError(CLI_CODES.CLI_EMPTY_INPUT, EXIT.DATAERR, "input is empty"));
|
|
589
710
|
}
|
|
711
|
+
const format = resolveFormat(bytes, formatOverride, op);
|
|
712
|
+
if (format instanceof CliError) return fail(format);
|
|
713
|
+
return { ok: true, input: { format, bytes } };
|
|
714
|
+
}
|
|
715
|
+
async function resolveInputStream(source, formatOverride, deps, op, limit = MAX_INPUT_BYTES) {
|
|
716
|
+
if (source === void 0) {
|
|
717
|
+
return fail(
|
|
718
|
+
new CliError(
|
|
719
|
+
CLI_CODES.CLI_USAGE,
|
|
720
|
+
EXIT.USAGE,
|
|
721
|
+
"missing <file> argument; pass a path or `-` to read stdin"
|
|
722
|
+
)
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
const iterator = withinLimit(openChunks(source, deps), limit)[Symbol.asyncIterator]();
|
|
726
|
+
const need = formatOverride === void 0 ? DETECT_PREFIX_BYTES : 1;
|
|
727
|
+
const head = [];
|
|
728
|
+
let headBytes = 0;
|
|
729
|
+
try {
|
|
730
|
+
while (headBytes < need) {
|
|
731
|
+
const step = await iterator.next();
|
|
732
|
+
if (step.done === true) break;
|
|
733
|
+
head.push(step.value);
|
|
734
|
+
headBytes += step.value.length;
|
|
735
|
+
}
|
|
736
|
+
} catch (e) {
|
|
737
|
+
if (e instanceof CliError) return fail(e);
|
|
738
|
+
throw e;
|
|
739
|
+
}
|
|
740
|
+
if (headBytes === 0) {
|
|
741
|
+
return fail(new CliError(CLI_CODES.CLI_EMPTY_INPUT, EXIT.DATAERR, "input is empty"));
|
|
742
|
+
}
|
|
743
|
+
const prefix = Buffer.concat(
|
|
744
|
+
head.map((c) => Buffer.from(c.buffer, c.byteOffset, c.length)),
|
|
745
|
+
Math.min(headBytes, DETECT_PREFIX_BYTES)
|
|
746
|
+
);
|
|
747
|
+
const format = resolveFormat(prefix, formatOverride, op);
|
|
748
|
+
if (format instanceof CliError) {
|
|
749
|
+
await iterator.return(void 0);
|
|
750
|
+
return fail(format);
|
|
751
|
+
}
|
|
752
|
+
return { ok: true, input: { format, chunks: rejoin(head, iterator) } };
|
|
753
|
+
}
|
|
754
|
+
function openChunks(source, deps) {
|
|
755
|
+
if (source === "-") {
|
|
756
|
+
return deps.openStdin === void 0 ? readerAsChunks(() => deps.readStdin()) : deps.openStdin();
|
|
757
|
+
}
|
|
758
|
+
return deps.openFile === void 0 ? readerAsChunks(() => deps.readFile(source)) : deps.openFile(source);
|
|
759
|
+
}
|
|
760
|
+
async function* readerAsChunks(read) {
|
|
761
|
+
yield* oneChunk(await read());
|
|
762
|
+
}
|
|
763
|
+
async function* rejoin(head, rest) {
|
|
764
|
+
for (const chunk of head) yield chunk;
|
|
765
|
+
for (; ; ) {
|
|
766
|
+
const step = await rest.next();
|
|
767
|
+
if (step.done === true) return;
|
|
768
|
+
yield step.value;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function resolveFormat(prefix, formatOverride, op) {
|
|
590
772
|
let format;
|
|
591
773
|
if (formatOverride !== void 0) {
|
|
592
774
|
const narrowed = asCosyteFormat(formatOverride);
|
|
593
775
|
if (narrowed === null) {
|
|
594
|
-
return
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
"unknown --format value; expected one of hl7, fhir, dicom, x12, ccda, ncpdp, astm, mllp"
|
|
599
|
-
)
|
|
776
|
+
return new CliError(
|
|
777
|
+
CLI_CODES.CLI_USAGE,
|
|
778
|
+
EXIT.USAGE,
|
|
779
|
+
"unknown --format value; expected one of hl7, fhir, dicom, x12, ccda, ncpdp, astm, mllp"
|
|
600
780
|
);
|
|
601
781
|
}
|
|
602
782
|
format = narrowed;
|
|
603
783
|
} else {
|
|
604
|
-
const detected = detectFormat(
|
|
605
|
-
if (detected.format === null) return
|
|
784
|
+
const detected = detectFormat(prefix);
|
|
785
|
+
if (detected.format === null) return detectionError(detected);
|
|
606
786
|
format = detected.format;
|
|
607
787
|
}
|
|
608
|
-
if (!supportsOp(format, op)) {
|
|
609
|
-
return
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
`format '${format}' does not support \`${op}\` in this CLI build (${op} supports: ${formatsSupporting(op).join(", ")})`
|
|
614
|
-
)
|
|
788
|
+
if (op !== null && !supportsOp(format, op)) {
|
|
789
|
+
return new CliError(
|
|
790
|
+
CLI_CODES.CLI_FORMAT_UNSUPPORTED,
|
|
791
|
+
EXIT.DATAERR,
|
|
792
|
+
`format '${format}' does not support \`${op}\` in this CLI build (${op} supports: ${formatsSupporting(op).join(", ")})`
|
|
615
793
|
);
|
|
616
794
|
}
|
|
617
|
-
return
|
|
795
|
+
return format;
|
|
618
796
|
}
|
|
619
797
|
function fail(e) {
|
|
620
798
|
return { ok: false, result: errorResult(e) };
|
|
@@ -656,29 +834,204 @@ function parseFailureResult(format, bytes, posture, e) {
|
|
|
656
834
|
exit: EXIT.DATAERR
|
|
657
835
|
};
|
|
658
836
|
}
|
|
659
|
-
|
|
837
|
+
function noInputError(path) {
|
|
838
|
+
return new CliError(
|
|
839
|
+
CLI_CODES.CLI_NO_INPUT,
|
|
840
|
+
EXIT.NOINPUT,
|
|
841
|
+
`cannot read input file: ${path} (does it exist and is it readable?)`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
async function readFileBytes(path, limit = MAX_INPUT_BYTES) {
|
|
845
|
+
let handle;
|
|
660
846
|
try {
|
|
661
|
-
|
|
847
|
+
handle = await open(path, "r");
|
|
662
848
|
} catch {
|
|
663
|
-
throw
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
);
|
|
849
|
+
throw noInputError(path);
|
|
850
|
+
}
|
|
851
|
+
try {
|
|
852
|
+
const info = await handle.stat();
|
|
853
|
+
if (info.isFile() && info.size > limit) throw inputTooLargeError(limit);
|
|
854
|
+
return await handle.readFile();
|
|
855
|
+
} catch (e) {
|
|
856
|
+
if (e instanceof CliError) throw e;
|
|
857
|
+
throw noInputError(path);
|
|
858
|
+
} finally {
|
|
859
|
+
await handle.close();
|
|
668
860
|
}
|
|
669
861
|
}
|
|
670
|
-
async function readStreamBytes(stream) {
|
|
862
|
+
async function readStreamBytes(stream, limit = MAX_INPUT_BYTES) {
|
|
671
863
|
const chunks = [];
|
|
864
|
+
let total = 0;
|
|
672
865
|
for await (const chunk of stream) {
|
|
673
|
-
|
|
866
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf-8") : chunk;
|
|
867
|
+
total += buf.length;
|
|
868
|
+
if (total > limit) throw inputTooLargeError(limit);
|
|
869
|
+
chunks.push(buf);
|
|
674
870
|
}
|
|
675
871
|
return Buffer.concat(chunks);
|
|
676
872
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
873
|
+
async function* streamChunks(stream) {
|
|
874
|
+
for await (const chunk of stream) {
|
|
875
|
+
yield typeof chunk === "string" ? Buffer.from(chunk, "utf-8") : chunk;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
async function* fileChunks(path) {
|
|
879
|
+
let handle;
|
|
880
|
+
try {
|
|
881
|
+
handle = await open(path, "r");
|
|
882
|
+
} catch {
|
|
883
|
+
throw noInputError(path);
|
|
884
|
+
}
|
|
885
|
+
try {
|
|
886
|
+
const stream = handle.createReadStream();
|
|
887
|
+
try {
|
|
888
|
+
for await (const chunk of stream) yield chunk;
|
|
889
|
+
} catch {
|
|
890
|
+
throw noInputError(path);
|
|
891
|
+
}
|
|
892
|
+
} finally {
|
|
893
|
+
await handle.close();
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
var DEID_PACKAGE = "@cosyte/deid";
|
|
897
|
+
var DEID_COVERAGE = {
|
|
898
|
+
hl7: "covered",
|
|
899
|
+
fhir: "covered",
|
|
900
|
+
x12: "covered",
|
|
901
|
+
ccda: "covered",
|
|
902
|
+
// The delegate's NCPDP adapter is Telecom; this CLI resolves SCRIPT.
|
|
903
|
+
ncpdp: "no-adapter",
|
|
904
|
+
astm: "no-adapter",
|
|
905
|
+
mllp: "no-adapter",
|
|
906
|
+
// Covered by the delegate, but its result is Part 10 bytes and stdout here is text.
|
|
907
|
+
dicom: "unserializable"
|
|
908
|
+
};
|
|
909
|
+
function deidCoverage(format) {
|
|
910
|
+
return DEID_COVERAGE[format];
|
|
911
|
+
}
|
|
912
|
+
function isDeidCovered(format) {
|
|
913
|
+
return DEID_COVERAGE[format] === "covered";
|
|
914
|
+
}
|
|
915
|
+
function deidCoveredFormats() {
|
|
916
|
+
return Object.keys(DEID_COVERAGE).filter(isDeidCovered).sort();
|
|
917
|
+
}
|
|
918
|
+
var DEID_BLOCKED = "blocked";
|
|
919
|
+
var DEID_UNAVAILABLE_REASON = `the ${DEID_PACKAGE} de-identification library is not installed; install it to use redact/deid (it is an optional dependency). The CLI ships no built-in redactor: a partial Safe-Harbor scrub over only the obvious PHI loci would leave PHI behind and present a false-safety impression, so nothing is emitted`;
|
|
920
|
+
function adapterUnavailable(format) {
|
|
921
|
+
return `the ${DEID_PACKAGE}/${format} de-identification adapter could not be loaded; it needs both ${DEID_PACKAGE} and the @cosyte/${format} parser to be installed`;
|
|
922
|
+
}
|
|
923
|
+
function parseOrFail(format, parse) {
|
|
924
|
+
try {
|
|
925
|
+
return parse();
|
|
926
|
+
} catch (e) {
|
|
927
|
+
const code = extractStableCode(e);
|
|
928
|
+
throw new CliError(
|
|
929
|
+
CLI_CODES.CLI_PARSE_FAILED,
|
|
930
|
+
EXIT.DATAERR,
|
|
931
|
+
`the ${format} parser rejected the input${code === null ? "" : ` (${code})`}`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function deidentifyOrFail(run2) {
|
|
936
|
+
try {
|
|
937
|
+
return run2();
|
|
938
|
+
} catch (e) {
|
|
939
|
+
const code = extractStableCode(e);
|
|
940
|
+
if (code === null) throw e;
|
|
941
|
+
throw new CliError(
|
|
942
|
+
CLI_CODES.CLI_DEID_INCOMPLETE,
|
|
943
|
+
EXIT.INVALID,
|
|
944
|
+
`the ${DEID_PACKAGE} de-identifier could not complete this input (${code}); no output was emitted`
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function decode2(bytes) {
|
|
949
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
950
|
+
}
|
|
951
|
+
function toRecords(manifest) {
|
|
952
|
+
return manifest.map((e) => ({
|
|
953
|
+
category: e.category,
|
|
954
|
+
transform: e.transform,
|
|
955
|
+
locus: e.locus,
|
|
956
|
+
count: e.count,
|
|
957
|
+
disposition: e.disposition,
|
|
958
|
+
code: e.code
|
|
959
|
+
}));
|
|
960
|
+
}
|
|
961
|
+
async function loadDeidDelegate(importCore = () => import('@cosyte/deid')) {
|
|
962
|
+
const core = await loadOptionalPackage(DEID_UNAVAILABLE_REASON, importCore);
|
|
963
|
+
const context = core.createDeidContext({ key: randomBytes(32) });
|
|
964
|
+
return {
|
|
965
|
+
label: core.OUTPUT_LABEL,
|
|
966
|
+
version: core.VERSION,
|
|
967
|
+
redact: async (format, bytes) => {
|
|
968
|
+
switch (format) {
|
|
969
|
+
case "hl7": {
|
|
970
|
+
const { parseHL7 } = await import('@cosyte/hl7');
|
|
971
|
+
const { deidentifyHl7 } = await loadOptionalPackage(
|
|
972
|
+
adapterUnavailable("hl7"),
|
|
973
|
+
() => import('@cosyte/deid/hl7')
|
|
974
|
+
);
|
|
975
|
+
const msg = parseOrFail("hl7", () => parseHL7(Buffer.from(bytes)));
|
|
976
|
+
return deidentifyOrFail(() => {
|
|
977
|
+
const { document, manifest } = deidentifyHl7(msg, { context });
|
|
978
|
+
return { output: document.toString(), manifest: toRecords(manifest) };
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
case "fhir": {
|
|
982
|
+
const { parseResource, serializeResource } = await loadFhir();
|
|
983
|
+
const { deidentifyFhir } = await loadOptionalPackage(
|
|
984
|
+
adapterUnavailable("fhir"),
|
|
985
|
+
() => import('@cosyte/deid/fhir')
|
|
986
|
+
);
|
|
987
|
+
const resource = parseOrFail("fhir", () => parseResource(decode2(bytes)).resource);
|
|
988
|
+
return deidentifyOrFail(() => {
|
|
989
|
+
const { document, manifest } = deidentifyFhir(resource, { context });
|
|
990
|
+
return { output: serializeResource(document), manifest: toRecords(manifest) };
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
case "x12": {
|
|
994
|
+
const { parseX12 } = await loadOptional("x12", () => import('@cosyte/x12'));
|
|
995
|
+
const { deidentifyX12 } = await loadOptionalPackage(
|
|
996
|
+
adapterUnavailable("x12"),
|
|
997
|
+
() => import('@cosyte/deid/x12')
|
|
998
|
+
);
|
|
999
|
+
const interchange = parseOrFail("x12", () => parseX12(Buffer.from(bytes)));
|
|
1000
|
+
return deidentifyOrFail(() => {
|
|
1001
|
+
const { x12, manifest } = deidentifyX12(interchange, { context });
|
|
1002
|
+
return { output: x12, manifest: toRecords(manifest) };
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
case "ccda": {
|
|
1006
|
+
const { parseCcda, serializeCcda } = await loadOptional(
|
|
1007
|
+
"ccda",
|
|
1008
|
+
() => import('@cosyte/ccda')
|
|
1009
|
+
);
|
|
1010
|
+
const { deidentifyCcda } = await loadOptionalPackage(
|
|
1011
|
+
adapterUnavailable("ccda"),
|
|
1012
|
+
() => import('@cosyte/deid/ccda')
|
|
1013
|
+
);
|
|
1014
|
+
const doc = parseOrFail("ccda", () => parseCcda(decode2(bytes)));
|
|
1015
|
+
return deidentifyOrFail(() => {
|
|
1016
|
+
const { document, manifest } = deidentifyCcda(doc, { context });
|
|
1017
|
+
return { output: serializeCcda(document), manifest: toRecords(manifest) };
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
async function deidStatus(load = loadDeidDelegate) {
|
|
1025
|
+
try {
|
|
1026
|
+
const delegate = await load();
|
|
1027
|
+
return {
|
|
1028
|
+
available: true,
|
|
1029
|
+
reason: `${DEID_PACKAGE} ${delegate.version}: ${delegate.label}`
|
|
1030
|
+
};
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
if (e instanceof CliError) return { available: false, reason: DEID_UNAVAILABLE_REASON };
|
|
1033
|
+
throw e;
|
|
1034
|
+
}
|
|
682
1035
|
}
|
|
683
1036
|
|
|
684
1037
|
// src/commands/completion.ts
|
|
@@ -1150,11 +1503,24 @@ async function parseCommand(args, deps, posture = VALUE_FREE) {
|
|
|
1150
1503
|
)
|
|
1151
1504
|
);
|
|
1152
1505
|
}
|
|
1153
|
-
const resolved = await
|
|
1506
|
+
const resolved = await resolveInputStream(positionals[0], values.format, deps, "parse");
|
|
1154
1507
|
if (!resolved.ok) return resolved.result;
|
|
1155
|
-
const { format,
|
|
1508
|
+
const { format, chunks } = resolved.input;
|
|
1156
1509
|
if (format === "mllp" || values.ndjson === true) {
|
|
1157
|
-
return await parseMulti(
|
|
1510
|
+
return await parseMulti(
|
|
1511
|
+
format,
|
|
1512
|
+
chunks,
|
|
1513
|
+
values.ndjson === true,
|
|
1514
|
+
values.quiet === true,
|
|
1515
|
+
deps.writeStdout
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
let bytes;
|
|
1519
|
+
try {
|
|
1520
|
+
bytes = await collectChunks(chunks);
|
|
1521
|
+
} catch (e) {
|
|
1522
|
+
if (e instanceof CliError) return errorResult(e);
|
|
1523
|
+
throw e;
|
|
1158
1524
|
}
|
|
1159
1525
|
return await parseSingle(format, bytes, values.json === true, values.quiet === true, posture);
|
|
1160
1526
|
}
|
|
@@ -1173,23 +1539,57 @@ async function parseSingle(format, bytes, json, quiet, posture) {
|
|
|
1173
1539
|
` : "";
|
|
1174
1540
|
return { stdout, stderr, exit: EXIT.OK };
|
|
1175
1541
|
}
|
|
1176
|
-
async function parseMulti(format,
|
|
1177
|
-
|
|
1178
|
-
|
|
1542
|
+
async function parseMulti(format, chunks, ndjson, quiet, writeStdout) {
|
|
1543
|
+
const recordFormat = format === "mllp" ? "hl7" : format;
|
|
1544
|
+
const records = format === "mllp" ? mllpFrames(chunks) : ndjsonRecords(chunks);
|
|
1545
|
+
const held = [];
|
|
1546
|
+
const emit = (line) => {
|
|
1547
|
+
if (writeStdout === void 0) {
|
|
1548
|
+
held.push(line);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
try {
|
|
1552
|
+
writeStdout(line);
|
|
1553
|
+
} catch {
|
|
1554
|
+
throw new CliError(
|
|
1555
|
+
CLI_CODES.CLI_OUTPUT_WRITE_FAILED,
|
|
1556
|
+
EXIT.IOERR,
|
|
1557
|
+
"could not write to the output stream; it closed before the record stream finished"
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
const written = () => held.join("");
|
|
1562
|
+
let total = 0;
|
|
1563
|
+
let failed = 0;
|
|
1564
|
+
let warnings = 0;
|
|
1179
1565
|
try {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1566
|
+
for await (const record of records) {
|
|
1567
|
+
let line;
|
|
1568
|
+
try {
|
|
1569
|
+
const { model, warnings: ws } = await parseFormat(recordFormat, record);
|
|
1570
|
+
warnings += ws.length;
|
|
1571
|
+
line = { record: total, format: recordFormat, model, warnings: ws };
|
|
1572
|
+
} catch (e) {
|
|
1573
|
+
if (e instanceof CliError) throw e;
|
|
1574
|
+
failed += 1;
|
|
1575
|
+
line = {
|
|
1576
|
+
record: total,
|
|
1577
|
+
format: recordFormat,
|
|
1578
|
+
error: extractStableCode(e) ?? "CLI_PARSE_FAILED"
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
total += 1;
|
|
1582
|
+
emit(JSON.stringify(line) + "\n");
|
|
1187
1583
|
}
|
|
1188
1584
|
} catch (e) {
|
|
1189
|
-
if (e instanceof CliError)
|
|
1190
|
-
|
|
1585
|
+
if (e instanceof CliError) {
|
|
1586
|
+
return { stdout: written(), stderr: `${formatDiagnostic(e)}
|
|
1587
|
+
`, exit: e.exit };
|
|
1588
|
+
}
|
|
1589
|
+
const rejected = parseFailureResult(format, new Uint8Array(), VALUE_FREE, e);
|
|
1590
|
+
return { stdout: written(), stderr: rejected.stderr, exit: rejected.exit };
|
|
1191
1591
|
}
|
|
1192
|
-
if (
|
|
1592
|
+
if (total === 0) {
|
|
1193
1593
|
return errorResult(
|
|
1194
1594
|
new CliError(
|
|
1195
1595
|
CLI_CODES.CLI_PARSE_FAILED,
|
|
@@ -1198,57 +1598,104 @@ async function parseMulti(format, bytes, ndjson, quiet) {
|
|
|
1198
1598
|
)
|
|
1199
1599
|
);
|
|
1200
1600
|
}
|
|
1201
|
-
const lines = [];
|
|
1202
|
-
let failed = 0;
|
|
1203
|
-
let warnings = 0;
|
|
1204
|
-
for (let i = 0; i < records.length; i += 1) {
|
|
1205
|
-
const rec = records[i];
|
|
1206
|
-
try {
|
|
1207
|
-
const { model, warnings: ws } = await parseFormat(recordFormat, rec);
|
|
1208
|
-
warnings += ws.length;
|
|
1209
|
-
lines.push({ record: i, format: recordFormat, model, warnings: ws });
|
|
1210
|
-
} catch (e) {
|
|
1211
|
-
if (e instanceof CliError) return errorResult(e);
|
|
1212
|
-
failed += 1;
|
|
1213
|
-
lines.push({
|
|
1214
|
-
record: i,
|
|
1215
|
-
format: recordFormat,
|
|
1216
|
-
error: extractStableCode(e) ?? "CLI_PARSE_FAILED"
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
const stdout = lines.map((l) => JSON.stringify(l)).join("\n") + "\n";
|
|
1221
1601
|
const exit = failed > 0 ? EXIT.DATAERR : EXIT.OK;
|
|
1222
|
-
const stderr = quiet ? "" : `cosyte: parsed ${String(
|
|
1223
|
-
return { stdout, stderr, exit };
|
|
1224
|
-
}
|
|
1225
|
-
function splitLines(bytes) {
|
|
1226
|
-
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
1227
|
-
const enc = new TextEncoder();
|
|
1228
|
-
return text.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => enc.encode(line));
|
|
1602
|
+
const stderr = quiet ? "" : `cosyte: parsed ${String(total)} ${recordFormat} record(s) (${String(warnings)} warning(s), ${String(failed)} failed)` + (ndjson ? " [ndjson]" : format === "mllp" ? " [mllp]" : "") + "\n";
|
|
1603
|
+
return { stdout: written(), stderr, exit };
|
|
1229
1604
|
}
|
|
1230
1605
|
var REDACT_OPTIONS = {
|
|
1231
1606
|
format: { type: "string" }
|
|
1232
1607
|
};
|
|
1233
|
-
function
|
|
1608
|
+
function manifestLine(entry) {
|
|
1609
|
+
return `cosyte: redact: ${entry.category} ${entry.transform} ${entry.locus} x${String(entry.count)} ${entry.disposition} ${entry.code}
|
|
1610
|
+
`;
|
|
1611
|
+
}
|
|
1612
|
+
function attribution(delegate) {
|
|
1613
|
+
return `cosyte: redact: delegated to ${DEID_PACKAGE} ${delegate.version}: ${delegate.label}
|
|
1614
|
+
`;
|
|
1615
|
+
}
|
|
1616
|
+
function manifestLines(outcome) {
|
|
1617
|
+
return outcome.manifest.map(manifestLine).join("");
|
|
1618
|
+
}
|
|
1619
|
+
function countOf(outcome, disposition) {
|
|
1620
|
+
return outcome.manifest.filter((e) => e.disposition === disposition).length;
|
|
1621
|
+
}
|
|
1622
|
+
function tally(format, outcome) {
|
|
1623
|
+
const transformed = countOf(outcome, "transformed");
|
|
1624
|
+
const removed = countOf(outcome, "removed");
|
|
1625
|
+
const blocked = countOf(outcome, DEID_BLOCKED);
|
|
1626
|
+
return `cosyte: redact: ${format}: ${String(outcome.manifest.length)} loci acted on (${String(transformed)} transformed, ${String(removed)} removed, ${String(blocked)} blocked)
|
|
1627
|
+
`;
|
|
1628
|
+
}
|
|
1629
|
+
var EPHEMERAL_KEY_NOTE = "cosyte: redact: identifier surrogates are keyed with a per-invocation ephemeral key: consistent within this output, not stable across runs\n";
|
|
1630
|
+
async function redactCommand(args, deps, loadDelegate = loadDeidDelegate) {
|
|
1631
|
+
let values;
|
|
1632
|
+
let positionals;
|
|
1234
1633
|
try {
|
|
1235
|
-
parseArgs({ args, options: REDACT_OPTIONS, allowPositionals: true });
|
|
1634
|
+
const parsed = parseArgs({ args, options: REDACT_OPTIONS, allowPositionals: true });
|
|
1635
|
+
values = parsed.values;
|
|
1636
|
+
positionals = parsed.positionals;
|
|
1236
1637
|
} catch {
|
|
1638
|
+
return errorResult(
|
|
1639
|
+
new CliError(
|
|
1640
|
+
CLI_CODES.CLI_USAGE,
|
|
1641
|
+
EXIT.USAGE,
|
|
1642
|
+
"invalid arguments to `redact` (see `cosyte --help`)"
|
|
1643
|
+
)
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
let delegate;
|
|
1647
|
+
try {
|
|
1648
|
+
delegate = await loadDelegate();
|
|
1649
|
+
} catch (e) {
|
|
1650
|
+
if (e instanceof CliError) return errorResult(e);
|
|
1651
|
+
throw e;
|
|
1652
|
+
}
|
|
1653
|
+
const resolved = await resolveInput(positionals[0], values.format, deps, null);
|
|
1654
|
+
if (!resolved.ok) return resolved.result;
|
|
1655
|
+
const { format, bytes } = resolved.input;
|
|
1656
|
+
if (!isDeidCovered(format)) return errorResult(uncovered(format));
|
|
1657
|
+
let outcome;
|
|
1658
|
+
try {
|
|
1659
|
+
outcome = await delegate.redact(format, bytes);
|
|
1660
|
+
} catch (e) {
|
|
1661
|
+
if (e instanceof CliError) return errorResult(e);
|
|
1662
|
+
throw e;
|
|
1663
|
+
}
|
|
1664
|
+
const blocked = outcome.manifest.filter((e) => e.disposition === DEID_BLOCKED);
|
|
1665
|
+
if (blocked.length > 0) {
|
|
1666
|
+
const incomplete = new CliError(
|
|
1667
|
+
CLI_CODES.CLI_DEID_INCOMPLETE,
|
|
1668
|
+
EXIT.INVALID,
|
|
1669
|
+
`${DEID_PACKAGE} blocked ${String(blocked.length)} of ${String(outcome.manifest.length)} loci in this ${format} input; no output was emitted`
|
|
1670
|
+
);
|
|
1237
1671
|
return {
|
|
1238
1672
|
stdout: "",
|
|
1239
|
-
stderr: `cosyte: ${
|
|
1673
|
+
stderr: attribution(delegate) + manifestLines(outcome) + tally(format, outcome) + `cosyte: ${incomplete.code}: ${incomplete.message}
|
|
1240
1674
|
`,
|
|
1241
|
-
exit:
|
|
1675
|
+
exit: incomplete.exit
|
|
1242
1676
|
};
|
|
1243
1677
|
}
|
|
1244
|
-
const status = deidStatus();
|
|
1245
1678
|
return {
|
|
1246
|
-
stdout:
|
|
1247
|
-
stderr: `cosyte: ${CLI_CODES.CLI_NOT_IMPLEMENTED}: ${status.reason}
|
|
1679
|
+
stdout: `${outcome.output}
|
|
1248
1680
|
`,
|
|
1249
|
-
|
|
1681
|
+
stderr: attribution(delegate) + manifestLines(outcome) + tally(format, outcome) + EPHEMERAL_KEY_NOTE,
|
|
1682
|
+
exit: EXIT.OK
|
|
1250
1683
|
};
|
|
1251
1684
|
}
|
|
1685
|
+
function uncovered(format) {
|
|
1686
|
+
if (deidCoverage(format) === "unserializable") {
|
|
1687
|
+
return new CliError(
|
|
1688
|
+
CLI_CODES.CLI_FORMAT_UNSUPPORTED,
|
|
1689
|
+
EXIT.DATAERR,
|
|
1690
|
+
`format '${format}' cannot be redacted onto this CLI's text output channel (its de-identified form is a binary stream, so \`parse\` and \`fmt\` are unwired too)`
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
return new CliError(
|
|
1694
|
+
CLI_CODES.CLI_NOT_IMPLEMENTED,
|
|
1695
|
+
EXIT.UNAVAILABLE,
|
|
1696
|
+
`${DEID_PACKAGE} does not de-identify the ${format} standard this CLI resolves, and the CLI ships no redactor of its own, so nothing is emitted`
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1252
1699
|
var VALIDATE_OPTIONS = {
|
|
1253
1700
|
format: { type: "string" },
|
|
1254
1701
|
profile: { type: "string" },
|
|
@@ -1317,7 +1764,8 @@ function renderReport(format, verdict) {
|
|
|
1317
1764
|
}
|
|
1318
1765
|
|
|
1319
1766
|
// src/core/run.ts
|
|
1320
|
-
|
|
1767
|
+
function helpText(limitBytes = MAX_INPUT_BYTES) {
|
|
1768
|
+
return `cosyte: a PHI-safe developer CLI over the @cosyte/* healthcare parsers
|
|
1321
1769
|
|
|
1322
1770
|
Usage:
|
|
1323
1771
|
cosyte <command> [options]
|
|
@@ -1329,7 +1777,7 @@ Commands:
|
|
|
1329
1777
|
fmt <file|-> Canonically re-serialize via the parser's spec-clean serializer
|
|
1330
1778
|
convert <file|-> Convert HL7 v2 \u2192 FHIR R4 via @cosyte/transform (use --to fhir)
|
|
1331
1779
|
map-codes <cmap|-> Translate a code through a BYO FHIR ConceptMap via @cosyte/terminology
|
|
1332
|
-
redact <file|-> De-identify a message (alias: deid):
|
|
1780
|
+
redact <file|-> De-identify a message via @cosyte/deid (alias: deid): ${deidCoveredFormats().join(" ")}
|
|
1333
1781
|
mcp Start the stdio MCP server (agent front door; also the cosyte-mcp bin)
|
|
1334
1782
|
completion <shell> Print a shell completion script (bash | zsh | fish)
|
|
1335
1783
|
|
|
@@ -1350,22 +1798,44 @@ Common options (parse / validate / inspect / fmt):
|
|
|
1350
1798
|
convert options:
|
|
1351
1799
|
--to fhir The conversion target (required; only HL7 v2 \u2192 FHIR R4 today)
|
|
1352
1800
|
|
|
1801
|
+
redact / deid (de-identification is delegated to @cosyte/deid; the CLI adds no policy of its own):
|
|
1802
|
+
Covered formats: ${deidCoveredFormats().join(", ")}. Every other format is refused rather than
|
|
1803
|
+
approximated: astm, mllp and ncpdp have no adapter (69), and dicom is covered by the library but
|
|
1804
|
+
its de-identified form is a binary stream this text stdout cannot carry (65). If the library
|
|
1805
|
+
reports it could not handle an element, the run exits 1 and emits no output at all: a partial
|
|
1806
|
+
scrub presented as de-identified is the one outcome this command exists to prevent.
|
|
1807
|
+
stdout carries the de-identified document; stderr carries the library's own value-free manifest
|
|
1808
|
+
(category, transform, locus, count, disposition code) plus its published label and version.
|
|
1809
|
+
Identifier surrogates are keyed with a per-invocation ephemeral key: consistent within one
|
|
1810
|
+
output, not stable across runs.
|
|
1811
|
+
|
|
1353
1812
|
map-codes options (the positional is a BYO FHIR ConceptMap; a code is not PHI):
|
|
1354
1813
|
--code <code> The source code to translate (required)
|
|
1355
1814
|
--system <uri> The source code system (optional; selects the ConceptMap group)
|
|
1356
1815
|
--version <v> The source code system version (optional)
|
|
1357
1816
|
--display <d> The source display (optional)
|
|
1358
1817
|
|
|
1818
|
+
Input size:
|
|
1819
|
+
parse reads at most ${describeByteLimit(limitBytes)} of input per invocation. A larger input is
|
|
1820
|
+
refused with CLI_INPUT_TOO_LARGE and the data-error exit code (65), never the internal-error
|
|
1821
|
+
code; split the input and re-run. Multi-record output (--ndjson, MLLP) is written record by
|
|
1822
|
+
record as it is parsed, so a refusal can arrive after some records have already been emitted:
|
|
1823
|
+
the exit code, not the output, is the signal that the run did not complete.
|
|
1824
|
+
|
|
1359
1825
|
Exit codes:
|
|
1360
|
-
0 success / valid 65 data error (unparseable / undetected format)
|
|
1826
|
+
0 success / valid 65 data error (unparseable / undetected format / input too large)
|
|
1361
1827
|
1 invalid (validate) 66 no input (missing/unreadable file)
|
|
1362
|
-
|
|
1828
|
+
or de-identification incomplete (redact)
|
|
1829
|
+
2 usage error 69 unavailable (a capability is not wired here, e.g. redact on a
|
|
1830
|
+
format @cosyte/deid has no adapter for)
|
|
1363
1831
|
70 internal error
|
|
1364
1832
|
|
|
1365
1833
|
PHI posture: the parsed model goes to stdout (the data channel you chose); every diagnostic on
|
|
1366
1834
|
stderr is value-free (codes and positions only, never a field value) unless you pass the loud,
|
|
1367
1835
|
opt-in --unsafe-show-values (which permits a bounded input excerpt in a failure diagnostic).
|
|
1368
1836
|
`;
|
|
1837
|
+
}
|
|
1838
|
+
var HELP = helpText();
|
|
1369
1839
|
function wantsHelp(argv) {
|
|
1370
1840
|
return argv.includes("--help") || argv.includes("-h");
|
|
1371
1841
|
}
|
|
@@ -1395,7 +1865,7 @@ async function run(argv, deps) {
|
|
|
1395
1865
|
return await mapCodesCommand(rest, deps);
|
|
1396
1866
|
case "redact":
|
|
1397
1867
|
case "deid":
|
|
1398
|
-
return redactCommand(rest);
|
|
1868
|
+
return await redactCommand(rest, deps);
|
|
1399
1869
|
case "completion":
|
|
1400
1870
|
return completionCommand(rest);
|
|
1401
1871
|
default:
|
|
@@ -1416,6 +1886,6 @@ function resolve(e) {
|
|
|
1416
1886
|
`, exit: e.exit };
|
|
1417
1887
|
}
|
|
1418
1888
|
|
|
1419
|
-
export { CLI_CODES, CliError, DEID_UNAVAILABLE_REASON, DETECTABLE_FORMATS, EXIT, KNOWN_FORMATS, OP_SUPPORT, SHOW_VALUES, UNSAFE_EXCERPT_MAX, UNSAFE_SHOW_VALUES_FLAG, VALUE_FREE, VERSION, 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, readFileBytes, readStreamBytes, redactCommand, resolveInput, run, supportsOp, toCliError, unsafeInputSuffix, validateCommand, validateFormat, valueFreeLocator };
|
|
1889
|
+
export { CLI_CODES, CliError, DEID_BLOCKED, DEID_COVERAGE, DEID_PACKAGE, DEID_UNAVAILABLE_REASON, DETECTABLE_FORMATS, DETECT_PREFIX_BYTES, EXIT, KNOWN_FORMATS, MAX_INPUT_BYTES, OP_SUPPORT, SHOW_VALUES, UNSAFE_EXCERPT_MAX, UNSAFE_SHOW_VALUES_FLAG, VALUE_FREE, VERSION, asCosyteFormat, classifyCandidates, collectChunks, completionCommand, convertCommand, convertOutcome, deframeMllp, deidCoverage, deidCoveredFormats, deidStatus, describeByteLimit, detectFormat, detectionError, errorResult, extractPhiPosture, extractStableCode, fileChunks, fmtCommand, fmtFormat, formatDiagnostic, formatHl7Position, formatsSupporting, helpText, inputTooLargeError, inspectCommand, inspectFormat, isDeidCovered, loadOptional, loadOptionalPackage, mapCodesCommand, mllpFrames, ndjsonRecords, oneChunk, parseCommand, parseFailureResult, parseFormat, readFileBytes, readStreamBytes, redactCommand, resolveInput, resolveInputStream, run, streamChunks, supportsOp, toCliError, truncatedMllpError, unsafeInputSuffix, validateCommand, validateFormat, valueFreeLocator, withinLimit };
|
|
1420
1890
|
//# sourceMappingURL=index.mjs.map
|
|
1421
1891
|
//# sourceMappingURL=index.mjs.map
|