Skip to content

S0088-cli-deid-1: redact produces a real de-identified copy, delegated to @cosyte/deid - #48

Merged
NSchatz merged 1 commit into
mainfrom
sdd/S0088-cli-deid-1
Aug 29, 2026
Merged

S0088-cli-deid-1: redact produces a real de-identified copy, delegated to @cosyte/deid#48
NSchatz merged 1 commit into
mainfrom
sdd/S0088-cli-deid-1

Conversation

@NSchatz

@NSchatz NSchatz commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Spec: work/specs/S0088-cli-deid-1/spec.md (umbrella).

cosyte redact <file|-> (alias deid) was an honest, typed refusal gated on a ground
layer that now exists. This wires @cosyte/deid behind the existing seam
(src/core/deid.ts) as an optionalDependency, on the lazy per-format dynamic-import
pattern the breadth parsers already use. The CLI adds no policy, no locus map, no
transform and no fallback scrub: it delegates, or it refuses.

What it does now

For ccda, fhir, hl7, x12: stdout carries the de-identified document,
serialized exactly as cosyte fmt serializes that format. stderr carries the library's
own value-free manifest (one line per locus: category, transform, structural path,
count, disposition, stable code) plus the library's own published output label and
version. The CLI asserts no de-identification standard of its own.

What it refuses, and why the codes differ

Outcome Diagnostic Exit
every locus handled the manifest 0
a locus the library could not handle CLI_DEID_INCOMPLETE (new) 1
astm, mllp, ncpdp: no adapter there CLI_NOT_IMPLEMENTED 69
dicom: covered there, binary result this text stdout cannot carry CLI_FORMAT_UNSUPPORTED 65
@cosyte/deid absent from the install CLI_PARSER_UNAVAILABLE 69

Every non-zero outcome emits nothing on stdout. The two refusals are deliberately
distinct: DICOM is the CLI's own channel limit, not a gap in the library, and blaming
the library for it would be a false attribution. @cosyte/deid/ncpdp is NCPDP
Telecom while this CLI resolves SCRIPT, so the subpath exists and the coverage
does not.

Two things worth a reviewer's attention

  • The happy path was measured, not assumed. Under the default policy the existing
    adt-a01.hl7 and patient.fhir.json come back clean, but 834.edi drives ten
    blocked loci, so it is now the fail-closed fixture and two synthetic X12/C-CDA
    fixtures carry the clean path. No filter hides a blocked entry.
  • The default policy needs a key. It pseudonymizes MRN / account / beneficiary
    numbers, and a keyed transform with no context is a fatal there, never an unkeyed
    fallback. The CLI holds no key material and adds no key flag, so it keys each
    invocation with an ephemeral random key and discloses on stderr and in the docs
    that surrogates are consistent within one output and not stable across runs.

Gates

typecheck, lint, format:check, phi-scan, check:no-emdash,
check:no-internal-refs, check:agent-notes, smoke and attw all green. 641 tests
(was 524); coverage core 97.8 / commands 98.2 against the 90 gate. The packed
tarball was installed from a directory outside the repo and both bins run.

No published exit value moved and no existing CLI_* code was renamed. redact stays
off the MCP tool surface, and it deliberately does not honour --unsafe-show-values.

…d to @cosyte/deid

`cosyte redact` (alias `deid`) was an honest refusal gated on a ground layer that
now exists. This wires @cosyte/deid behind the existing seam as an
optionalDependency, on the lazy per-format import pattern the breadth parsers use.

For ccda, fhir, hl7 and x12, stdout carries the de-identified document serialized
exactly as `cosyte fmt` serializes that format, and stderr carries the library's own
value-free manifest (category, transform, structural locus, count, disposition, stable
code) plus its own published label and version. The CLI adds no policy, no locus map,
no transform and no fallback scrub, and asserts no de-identification standard.

Anything short of a clean, fully-handled pass emits nothing:
- astm, mllp, ncpdp have no adapter there: CLI_NOT_IMPLEMENTED, exit 69.
  (@cosyte/deid/ncpdp is NCPDP Telecom; this CLI resolves SCRIPT.)
- dicom is covered there but its de-identified form is a Part 10 byte stream this
  text stdout cannot carry: CLI_FORMAT_UNSUPPORTED, exit 65, the CLI's own limit
  rather than a gap in the library.
- any locus the library reports it could not handle: new CLI_DEID_INCOMPLETE, exit 1,
  with the blocked paths and codes named on stderr.
- the library absent from the install: CLI_PARSER_UNAVAILABLE, exit 69, decided
  before the input is read.

The default policy pseudonymizes MRN / account / beneficiary numbers, which is a keyed
transform, and a keyed transform with no key context is a fatal there. The CLI holds no
key material and adds no key surface, so it keys each invocation with an ephemeral
random key and discloses that surrogates are consistent within one output and not
stable across runs.

No published exit value moved and no existing CLI_* code was renamed. redact stays off
the MCP tool surface, and it deliberately does not honour --unsafe-show-values.

641 tests (was 524); coverage on core 97.8 and commands 98.2, both over the 90 gate.
@NSchatz
NSchatz merged commit 9a17fed into main Aug 29, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant