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