From 32770a9de773b3a9c094698ae25d046569a2187b Mon Sep 17 00:00:00 2001 From: Noah Schatz Date: Sat, 7 Nov 2026 17:53:08 -0500 Subject: [PATCH 1/2] docs: lead with the lockup, link the integration services page, and write to the house style - README.md opens with the shared lockup tile pair in a picture element, as hl7's README does, with the alt text assets/alt-text.json declares for the light tile (BR4, ST20). - Every hire-us link, in README.md and the ten starter READMEs, goes to /integration-services (BI5, ST29), and every cosyte.com and docs.cosyte.com link carries utm_source=github, utm_medium=readme, utm_campaign=evergreen, and utm_content=examples (BI6, ST30). - AGENTS.md holds the build, the test, the gate, and the data rule, and CLAUDE.md imports it and keeps the CI notes, the hazard, and the rules (RD3, ST26). - US spelling (ST3): "Summarize" in x12-remittance's package.json, "labeled" in an x12-eligibility test name, "license" in a dash-gate comment. The serial comma in every list of three or more across the READMEs (ST6). The dash gate's error messages use a colon, not a spaced hyphen (ST8). - After the move to the 0.1.1 releases (x12 0.1.2): prose names the 0.1 line instead of 0.1.0 where the fact still holds, checked against the new releases; cli-tour's expected output shows the 0.1.1 version lines it now prints; ccda-summary drops the @xmldom/xmldom 0.9.10 deprecation and audit note, which ccda 0.1.1 no longer triggers, and says only that ccda pins its XML parser exactly (ST16). Every starter's expected output matches a fresh run, and npm ci and npm test pass in all ten starters on Node 22 and 24. --- AGENTS.md | 36 +++++++++ CLAUDE.md | 39 ++++------ README.md | 27 ++++--- astm-lab-results/README.md | 54 +++++++------- ccda-summary/README.md | 35 +++++---- cli-tour/README.md | 78 ++++++++++---------- deid-hl7/README.md | 20 ++--- dicom-metadata/README.md | 41 +++++----- hl7-mllp-listener/README.md | 22 +++--- hl7-to-fhir/README.md | 54 +++++++------- hl7-to-fhir/src/convert.js | 4 +- ncpdp-claim/README.md | 55 +++++++------- scripts/check-no-emdash.sh | 10 +-- x12-eligibility/README.md | 55 +++++++------- x12-eligibility/src/eligibility.js | 4 +- x12-eligibility/test/x12-eligibility.test.js | 7 +- x12-remittance/README.md | 32 ++++---- x12-remittance/package.json | 2 +- 18 files changed, 308 insertions(+), 267 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..239a36d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# AGENTS.md + +Runnable starters for Cosyte's `@cosyte/*` healthcare libraries. Each top-level folder with a +tracked `package.json` is one starter and its own npm project: a committed `package-lock.json`, +`src/`, `test/`, and a README. The repository ships no package: no root `package.json`, no +changeset, and no `CHANGELOG.md`. + +## Build and test + +Use Node.js 22 or later and npm. There is no build step, and nothing to install at the root. + +```sh +cd deid-hl7 # any starter +npm ci +npm start # runs the starter +npm test # runs it again and asserts its key output, offline +``` + +## The gate + +Run these from the repository root, in this order, before you push: + +1. `bash scripts/check-no-emdash.sh` +2. In every starter, `npm ci`, then `npm test`. CI lists the starters this way: + `git ls-files -- '*/package.json' | awk -F/ 'NF == 2 { print $1 }'` +3. Step 2 again on Node 22, the floor, for each starter the change touches: + `mise exec node@22 -- npm ci`, then `mise exec node@22 -- npm test`. +4. `actionlint`, when the change touches `.github/workflows/`. + +CI runs every starter on Node 22 and 24. + +## Data + +Synthetic data only. Every message, document, and image here is generated by `@cosyte/synth` or +written by hand, and must not pass for a real record. Real patient data never enters this +repository, a fixture, or a log. diff --git a/CLAUDE.md b/CLAUDE.md index 33c768a..ea1906c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,36 +1,27 @@ # cosyte/examples -Runnable starters for the `@cosyte/*` healthcare libraries. Each top-level folder with a tracked -`package.json` is one starter: its own committed `package-lock.json`, `src/`, `test/` and README, -`npm start` to run it and `npm test` to run it again and assert its key output, offline, on Node 22 -or later. The repo ships no package: no root `package.json`, no changeset, no `CHANGELOG.md`. +AGENTS.md, imported here, holds the build, the test, the gate, and the data rule. This card adds +what CI does with the gate, the hazard, and the rules that bind here. -## Gate - -Run these from the repo root, in this order. There is nothing to install at the root. +@AGENTS.md -1. `bash scripts/check-no-emdash.sh` -2. In every starter, `npm ci`, then `npm test`. CI lists the starters this way: - `git ls-files -- '*/package.json' | awk -F/ 'NF == 2 { print $1 }'` -3. Step 2 again on Node 22, the floor, for each starter the change touches: - `mise exec node@22 -- npm ci`, then `mise exec node@22 -- npm test`. -4. `actionlint`, when the change touches `.github/workflows/`. +## Gate -Step 1 is `no-emdash.yml`'s `no-emdash` check. On a pull request it also scans the title, the body -and the commit messages (the script's `--stdin` mode), since a squash merge makes them the commit on -`main`, and editing the title or body runs it again. Steps 2 and 3 are `starters.yml`: every starter -on Node 22 and 24, and `All starters` is the one check that stands for the whole matrix, so a new -starter folder is tested with no workflow edit. `starters.yml` also runs nightly on `main`, from the -lockfiles. Step 4 is `actionlint.yml`; `codeql.yml` and `scorecard.yml` call the shared workflows. -A new starter also adds its folder to `.github/dependabot.yml`. +The gate is the four steps in AGENTS.md. Step 1 is `no-emdash.yml`'s `no-emdash` check. On a pull +request it also scans the title, the body, and the commit messages (the script's `--stdin` mode), +since a squash merge makes them the commit on `main`, and editing the title or body runs it again. +Steps 2 and 3 are `starters.yml`: every starter on Node 22 and 24, and `All starters` is the one +check that stands for the whole matrix, so a new starter folder is tested with no workflow edit. +`starters.yml` also runs nightly on `main`, from the lockfiles. Step 4 is `actionlint.yml`; +`codeql.yml` and `scorecard.yml` call the shared workflows. A new starter also adds its folder to +`.github/dependabot.yml`. ## Hazard: a starter gets copied into production This repo is public, and a starter exists to be cloned and built on: what it does, an integration -will do with real patients. So every message, document and image is synthetic (generated by -`@cosyte/synth` or written by hand) and must not pass for a real record, and every pattern must be -safe to copy. The listener binds `127.0.0.1` only; identifiers never reach a log, an error, a -manifest or a summary meant to be value-free; a check exits nonzero when it fails rather than +will do with real patients. So the data stays synthetic, as AGENTS.md says, and every pattern must +be safe to copy. The listener binds `127.0.0.1` only; identifiers never reach a log, an error, a +manifest, or a summary meant to be value-free; a check exits nonzero when it fails rather than printing a result. A change that makes data look more real or a pattern less safe is the risk here, even when every test is green. diff --git a/README.md b/README.md index daa5c5a..2255de2 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,32 @@ + + + + The Cosyte logo on its own white ground: the icon beside the word Cosyte. + + + # Cosyte examples Runnable end-to-end starters for the `@cosyte/*` healthcare libraries. Each starter is a small, -complete program you can clone, install and run in a minute. Every message, document and image here -is synthetic: generated by `@cosyte/synth` or written by hand for the example. None of it is real -patient data. +complete program you can clone, install, and run in a minute. Every message, document, and image +here is synthetic: generated by `@cosyte/synth` or written by hand for the example. None of it is +real patient data. The libraries themselves live in their own repositories and are documented at -[docs.cosyte.com](https://docs.cosyte.com). +[docs.cosyte.com](https://docs.cosyte.com/?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). ## Starters | Starter | Libraries | What it shows | Run | | --- | --- | --- | --- | -| [hl7-mllp-listener](hl7-mllp-listener) | `@cosyte/mllp`, `@cosyte/hl7`, `@cosyte/synth` | An MLLP listener that parses each ADT^A01 it receives, prints a summary and answers with an ACK; a client sends it a synthetic message | `cd hl7-mllp-listener && npm install && npm start` | +| [hl7-mllp-listener](hl7-mllp-listener) | `@cosyte/mllp`, `@cosyte/hl7`, `@cosyte/synth` | An MLLP listener that parses each ADT^A01 it receives, prints a summary, and answers with an ACK; a client sends it a synthetic message | `cd hl7-mllp-listener && npm install && npm start` | | [hl7-to-fhir](hl7-to-fhir) | `@cosyte/transform`, `@cosyte/fhir`, `@cosyte/hl7`, `@cosyte/synth` | An ADT^A01 converted to a FHIR R4 message Bundle (Patient, Encounter, MessageHeader), each resource then validated | `cd hl7-to-fhir && npm install && npm start` | -| [x12-eligibility](x12-eligibility) | `@cosyte/x12`, `@cosyte/synth` | A 271 eligibility response read into payer, subscriber, coverage status and benefit lines, then the 270 inquiry it answers, built and matched by its trace | `cd x12-eligibility && npm install && npm start` | -| [x12-remittance](x12-remittance) | `@cosyte/x12`, `@cosyte/synth` | An 835 remittance summarized: the payment, each claim's status, amounts and adjustment reasons, and the balance check | `cd x12-remittance && npm install && npm start` | -| [ncpdp-claim](ncpdp-claim) | `@cosyte/ncpdp`, `@cosyte/synth` | An NCPDP Telecom B1 claim and a paid response, parsed into routing, product, pricing and the payer's answer | `cd ncpdp-claim && npm install && npm start` | +| [x12-eligibility](x12-eligibility) | `@cosyte/x12`, `@cosyte/synth` | A 271 eligibility response read into payer, subscriber, coverage status, and benefit lines, then the 270 inquiry it answers, built and matched by its trace | `cd x12-eligibility && npm install && npm start` | +| [x12-remittance](x12-remittance) | `@cosyte/x12`, `@cosyte/synth` | An 835 remittance summarized: the payment; each claim's status, amounts, and adjustment reasons; and the balance check | `cd x12-remittance && npm install && npm start` | +| [ncpdp-claim](ncpdp-claim) | `@cosyte/ncpdp`, `@cosyte/synth` | An NCPDP Telecom B1 claim and a paid response, parsed into routing, product, pricing, and the payer's answer | `cd ncpdp-claim && npm install && npm start` | | [astm-lab-results](astm-lab-results) | `@cosyte/astm`, `@cosyte/synth` | An analyzer upload (ASTM E1394 records in E1381 frames) decoded, checksums checked, into a results table | `cd astm-lab-results && npm install && npm start` | | [dicom-metadata](dicom-metadata) | `@cosyte/dicom`, `@cosyte/deid` | DICOM Part 10 metadata read from a synthetic file, then the header de-identified with a value-free manifest | `cd dicom-metadata && npm install && npm start` | -| [ccda-summary](ccda-summary) | `@cosyte/ccda`, `@cosyte/synth` | A C-CDA document parsed into problems, medications and allergies | `cd ccda-summary && npm install && npm start` | +| [ccda-summary](ccda-summary) | `@cosyte/ccda`, `@cosyte/synth` | A C-CDA document parsed into problems, medications, and allergies | `cd ccda-summary && npm install && npm start` | | [deid-hl7](deid-hl7) | `@cosyte/deid`, `@cosyte/hl7`, `@cosyte/synth` | An HL7 v2 admit message run through the Safe Harbor policy, the value-free manifest of what it removed or generalized, and the positions it passed through unexamined | `cd deid-hl7 && npm install && npm start` | | [cli-tour](cli-tour) | `@cosyte/cli`, `@cosyte/hl7`, `@cosyte/synth` | The `cosyte` command over a synthetic HL7 v2 message: parse, inspect, validate, convert to a FHIR R4 Bundle, and de-identify | `cd cli-tour && npm install && npm start` | @@ -44,4 +51,4 @@ Each starter sets its `@cosyte/*` versions in its own `package.json` and commits 24, on every pull request and every push to `main`. Changes reach `main` only through pull requests that pass `All starters` (every starter on both Node lines) and `no-emdash`. -Need it integrated? [Talk to us](https://cosyte.com/contact). +Need it integrated? [Talk to us](https://cosyte.com/integration-services?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). diff --git a/astm-lab-results/README.md b/astm-lab-results/README.md index 143910c..6f8bdf4 100644 --- a/astm-lab-results/README.md +++ b/astm-lab-results/README.md @@ -64,27 +64,28 @@ npm test The test runs `src/main.js` and checks the frame summary and one table row per result, with the expected values read from the parsed upload. It then checks that the framed upload and its unframed twin from `@cosyte/synth` parse to the same results, that each row carries the value, units, range, -flag and status its result record sent, and that a frame whose checksum fails is dropped before the +flag, and status its result record sent, and that a frame whose checksum fails is dropped before the record parser, leaving a frame warning and one result fewer. ## How it works 1. `generateAstmResultFramed({ seed: 1033, resultCount: 4 })` from `@cosyte/synth/astm` builds the - records through `@cosyte/astm`'s `buildAstmMessage` and frames them with `composeAstmFrames`. Each - record travels in its own frame: ``, a frame number, the record text, ``, a checksum (the - sum of the bytes from the frame number through ``, modulo 256, as two hex digits) and - ``. Frame numbers run 1 to 7 and wrap to 0. -2. `parseFramedAstm(bytes)` decodes the frames, checks each checksum and frame number, reassembles the - records from trusted frames only and parses them. A frame that fails is kept in `frames` with - `trusted: false` and reported in `frameWarnings` (`ASTM_FRAME_BAD_CHECKSUM`). Its record never - reaches the parser, so `message.warnings` does not mention it: check both lists. + records through `@cosyte/astm`'s `buildAstmMessage` and frames them with `composeAstmFrames`. + Each record travels in its own frame: ``, a frame number, the record text, ``, a + checksum (the sum of the bytes from the frame number through ``, modulo 256, as two hex + digits), and ``. Frame numbers run 1 to 7 and wrap to 0. +2. `parseFramedAstm(bytes)` decodes the frames, checks each checksum and frame number, reassembles + the records from trusted frames only, and parses them. A frame that fails is kept in `frames` + with `trusted: false` and reported in `frameWarnings` (`ASTM_FRAME_BAD_CHECKSUM`). Its record + never reaches the parser, so `message.warnings` does not mention it: check both lists. 3. `messages(message)` splits the stream at each `H` header, so a patient is only paired with the results of its own message, and `classifyMessage(m.records).kind` says what the message is. A host query (a `Q` record) asks the LIS for work and is never a result set. 4. Each row comes from the typed result model: `universalTestId.localCode` (component 4, the code - analyzers send), `value` and `units` verbatim, `range` with its bounds kept as text, `flag` with its - meaning (an unrecognized flag reads `undefined`, never `normal`) and `status` (only a plain `F` is - `isActiveFinal`). `commentsFor(message, result)` returns the comments attached to a result. + analyzers send), `value` and `units` verbatim, `range` with its bounds kept as text, `flag` with + its meaning (an unrecognized flag reads `undefined`, never `normal`), and `status` (only a plain + `F` is `isActiveFinal`). `commentsFor(message, result)` returns the comments attached to a + result. 5. `@cosyte/astm` models the header's delimiters and leaves the other header fields in `header.fields`, where `fields[i]` is field H.(i + 1). `headerFields` lists the populated ones, and the sender is read from H.5, where E1394 puts the sender name or ID. @@ -93,31 +94,32 @@ The code is in [`src/results.js`](src/results.js) and [`src/main.js`](src/main.j ## Limits -- `@cosyte/synth` 0.1.0 writes its sender and analyzer identifiers into H.3 and H.4, which E1394 - defines as the message control ID and the access password, so the sender reads `not sent` and both - identifiers are listed under other fields. -- `@cosyte/synth` 0.1.0 writes each test's name and LOINC after the local code, in components 5 and 6 - of the Universal Test ID, instead of components 2 and 1. `@cosyte/astm` models components 1 to 4 and - keeps the rest verbatim in `universalTestId.components` without reading them as a name or a code, so - the table shows the local code alone. A test name in component 2 prints beside the code. +- On the 0.1 line, `@cosyte/synth` writes its sender and analyzer identifiers into H.3 and H.4, + which E1394 defines as the message control ID and the access password, so the sender reads + `not sent` and both identifiers are listed under other fields. +- On the 0.1 line, `@cosyte/synth` writes each test's name and LOINC after the local code, in + components 5 and 6 of the Universal Test ID, instead of components 2 and 1. `@cosyte/astm` models + components 1 to 4 and keeps the rest verbatim in `universalTestId.components` without reading them + as a name or a code, so the table shows the local code alone. A test name in component 2 prints + beside the code. - `@cosyte/synth` pairs values and flags without clinical coherence. With seed 1033 every flag agrees with its range; another seed can put an `H` on a value inside its range. The parser reports the flag the analyzer sent and never recomputes it. - The upload is decoded as one byte stream after the fact. On a live serial or TCP link the analyzer - also runs the ENQ, ACK, NAK and EOT exchange; `ltpReduce` in `@cosyte/astm` models the receiver's + also runs the ENQ, ACK, NAK, and EOT exchange; `ltpReduce` in `@cosyte/astm` models the receiver's side of it and leaves the socket to you. ## Synthetic data -The upload comes from `@cosyte/synth`. The patient name comes from a fictional pool and the birth date -from its seeded generator; the practice, laboratory and specimen IDs carry the synthetic `PRA`, `LAB` -and `ACC` prefixes; the sender and analyzer names are fictional. The local test codes are invented and -the LOINC codes after them are public identifiers. None of it is real patient data or output from a -real instrument. +The upload comes from `@cosyte/synth`. The patient name comes from a fictional pool and the birth +date from its seeded generator; the practice, laboratory, and specimen IDs carry the synthetic +`PRA`, `LAB`, and `ACC` prefixes; the sender and analyzer names are fictional. The local test codes +are invented and the LOINC codes after them are public identifiers. None of it is real patient data +or output from a real instrument. ## Versions The `@cosyte/*` versions are set in [`package.json`](package.json). After changing one, run `npm install` to refresh `package-lock.json`, then `npm test`. -Need it integrated? [Talk to us](https://cosyte.com/contact). +Need it integrated? [Talk to us](https://cosyte.com/integration-services?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). diff --git a/ccda-summary/README.md b/ccda-summary/README.md index d335714..a7b1ccd 100644 --- a/ccda-summary/README.md +++ b/ccda-summary/README.md @@ -1,7 +1,7 @@ # ccda-summary Parse a C-CDA document with [`@cosyte/ccda`](https://github.com/cosyte/ccda) and print its document -type, the patient, and the problems, medications and allergies, read through the library's typed +type, the patient, and the problems, medications, and allergies, read through the library's typed section accessors. Libraries: [`@cosyte/ccda`](https://github.com/cosyte/ccda), [`@cosyte/synth`](https://github.com/cosyte/synth). @@ -53,11 +53,11 @@ Observation, and the starter never fills in a value the document does not carry. npm test ``` -The test runs `src/main.js` and checks the document type, the patient line and one entry per problem, -medication and allergy against a parse of the same document. It then checks the summary against the -XML: one row per Problem Observation, Medication Activity and Allergy Observation, every reported code -written in the document, every drug coded in RxNorm, and the medical record number taken from the -patient's identifiers. +The test runs `src/main.js` and checks the document type, the patient line, and one entry per +problem, medication, and allergy against a parse of the same document. It then checks the summary +against the XML: one row per Problem Observation, Medication Activity, and Allergy Observation, +every reported code written in the document, every drug coded in RxNorm, and the medical record +number taken from the patient's identifiers. ## How it works @@ -72,32 +72,31 @@ patient's identifiers. (drug, dose, route, frequency, status), and `getAllergies()` for the Allergy Concern Acts (the allergen, each reaction and its severity). 4. A C-CDA names each code system by OID. `@cosyte/ccda` exports the OIDs (`SNOMED_CT`, `RXNORM`, - `ICD10_CM` and others), and `src/ccda.js` maps them to the names we print. + `ICD10_CM`, and others), and `src/ccda.js` maps them to the names we print. The code is in [`src/ccda.js`](src/ccda.js) and [`src/main.js`](src/main.js). ## Limits -- `@cosyte/synth` 0.1.0 writes active entries only, one allergy with one reaction, and no Severity or - Criticality observation. It draws each code from its example pools on its own, so the record is not - clinically coherent: read it as parser input, not as a patient. +- On the 0.1 line, `@cosyte/synth` writes active entries only, one allergy with one reaction, and no + Severity or Criticality observation. It draws each code from its example pools on its own, so the + record is not clinically coherent: read it as parser input, not as a patient. - `@cosyte/ccda` checks that a code's system is one expected for its slot. It does not check that the code exists in that system; for that, pass your own terminology service as the `terminology` option of `parseCcda`. -- With `@cosyte/ccda` 0.1.0, `npm install` prints a deprecation notice for `@xmldom/xmldom` 0.9.10, - the XML parser that release pins exactly, and `npm audit` reports advisories against it (one of - them high severity) with no fix available, because the pin is exact. This starter does not override - the pin. +- `@cosyte/ccda` pins its XML parser, `@xmldom/xmldom`, to one exact version, so a fix to that + parser reaches this starter only through a new `@cosyte/ccda` release. This starter does not + override the pin. ## Synthetic data -The document comes from `@cosyte/synth`, which draws every name, identifier and date from reserved or -fictional ranges; the medical record number sits under a synthetic assigning-authority OID. It is not -real patient data. +The document comes from `@cosyte/synth`, which draws every name, identifier, and date from reserved +or fictional ranges; the medical record number sits under a synthetic assigning-authority OID. It is +not real patient data. ## Versions The `@cosyte/*` versions are set in [`package.json`](package.json). After changing one, run `npm install` to refresh `package-lock.json`, then `npm test`. -Need it integrated? [Talk to us](https://cosyte.com/contact). +Need it integrated? [Talk to us](https://cosyte.com/integration-services?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). diff --git a/cli-tour/README.md b/cli-tour/README.md index 5d6f6f0..8abe1af 100644 --- a/cli-tour/README.md +++ b/cli-tour/README.md @@ -1,8 +1,8 @@ # cli-tour -Walk the `cosyte` command from [`@cosyte/cli`](https://github.com/cosyte/cli) over a synthetic HL7 v2 -admit message: parse it to typed JSON, summarize it, validate it, convert it to a FHIR R4 Bundle and -de-identify it. Each step prints the command it runs, what the CLI printed and the exit code. +Walk the `cosyte` command from [`@cosyte/cli`](https://github.com/cosyte/cli) over a synthetic +HL7 v2 admit message: parse it to typed JSON, summarize it, validate it, convert it to a FHIR R4 +Bundle, and de-identify it. Each step prints the command it runs, what the CLI printed, and the exit code. Libraries: [`@cosyte/cli`](https://github.com/cosyte/cli), [`@cosyte/hl7`](https://github.com/cosyte/hl7), [`@cosyte/synth`](https://github.com/cosyte/synth). @@ -22,7 +22,7 @@ Node 22 or later, and bash. `npm start` runs `bash tour.sh`; after `npm install` ```text == The CLI: cosyte from node_modules/.bin $ cosyte --version -0.1.0 +0.1.1 exit 0 == Synthetic input from @cosyte/synth @@ -110,7 +110,7 @@ exit 0 == 6. redact: a de-identified copy via @cosyte/deid (exit 1, and no copy, if it blocks a locus) $ cosyte redact out/adt-a01.hl7 > out/adt-a01.redacted.hl7 -cosyte: redact: delegated to @cosyte/deid 0.1.0: Safe-Harbor-transformed per the configured policy +cosyte: redact: delegated to @cosyte/deid 0.1.1: Safe-Harbor-transformed per the configured policy cosyte: redact: DATES generalize MSH-7[0] x1 transformed DEID_RESIDUAL_RETAINED cosyte: redact: DATES generalize EVN-2[0] x1 transformed DEID_RESIDUAL_RETAINED cosyte: redact: MRN redact PID-3[0] x1 removed DEID_CATEGORY_REMOVED @@ -143,14 +143,14 @@ npm test ``` The test runs `tour.sh`, checks that every step exited 0, and checks the `inspect` summary, the -validation verdict, the conversion result and the redact manifest. It reads the files the tour wrote: -the parsed JSON holds every segment and `PID` value of the input, the Bundle's `Patient` name, birth -date and medical record number match `PID`, and the de-identified copy keeps every segment, no `PID` -identifier and only the year of the birth date. It then adds a visit number (`PV1-19`) to the same -message and checks that `redact` refuses it: exit `1`, nothing on stdout, and the blocked locus named -on stderr without its value. It also checks that no identifier from `PID` reaches the `inspect` -summaries or the notes on stderr. Expected values come from the synthetic input, not from pasted -output. +validation verdict, the conversion result, and the redact manifest. It reads the files the tour +wrote: the parsed JSON holds every segment and `PID` value of the input, the Bundle's `Patient` +name, birth date, and medical record number match `PID`, and the de-identified copy keeps every +segment, no `PID` identifier, and only the year of the birth date. It then adds a visit number +(`PV1-19`) to the same message and checks that `redact` refuses it: exit `1`, nothing on stdout, and +the blocked locus named on stderr without its value. It also checks that no identifier from `PID` +reaches the `inspect` summaries or the notes on stderr. Expected values come from the synthetic +input, not from pasted output. ## How it works @@ -161,22 +161,22 @@ output. 3. `cosyte parse` detects the format from the content (HL7 v2 here) and prints the parsed model as JSON on stdout. The tour saves it to `out/adt-a01.parsed.json` and shows its first lines. 4. `cosyte inspect` prints a structural summary with no field value in it: message type, version, - segment counts and the warning count. + segment counts, and the warning count. 5. `cosyte validate` puts the verdict in the exit code (`0` valid, `1` invalid, `65` unparseable) and prints it on stderr. -6. `cosyte convert --to fhir` passes the parsed message to `@cosyte/transform` and prints the FHIR R4 - message Bundle on stdout. Its findings go to stderr as a severity, a code and a locator (an HL7 - position and the FHIR path it maps to), never a field value. Here it reports seven: among them, - the phone number in `PID-13` and the location in `PV1-3` are dropped, a timestamp has no time - zone, and the `EVN` segment reaches no resource in the Bundle. An error-severity finding would make - it exit `1`. +6. `cosyte convert --to fhir` passes the parsed message to `@cosyte/transform` and prints the FHIR + R4 message Bundle on stdout. Its findings go to stderr as a severity, a code, and a locator (an + HL7 position and the FHIR path it maps to), never a field value. Here it reports seven: among + them, the phone number in `PID-13` and the location in `PV1-3` are dropped, a timestamp has no + time zone, and the `EVN` segment reaches no resource in the Bundle. An error-severity finding + would make it exit `1`. 7. `cosyte inspect` on the Bundle detects FHIR and prints the Bundle's type and its entries. 8. `cosyte redact` hands the message whole to `@cosyte/deid` and prints the de-identified copy on - stdout. The tour saves it to `out/adt-a01.redacted.hl7` and prints its segments. stderr carries the - library's value-free manifest: one line per locus with the category, the action, the position, the - count, the disposition and a stable code. Under the library's default policy the medical record - number, the name, the phone number and the SSN are removed, each date keeps only its year, and the - address keeps only a 3-digit ZIP. The CLI passes no policy of its own. + stdout. The tour saves it to `out/adt-a01.redacted.hl7` and prints its segments. stderr carries + the library's value-free manifest: one line per locus with the category, the action, the + position, the count, the disposition, and a stable code. Under the library's default policy the + medical record number, the name, the phone number, and the SSN are removed, each date keeps only + its year, and the address keeps only a 3-digit ZIP. The CLI passes no policy of its own. `convert` and the FHIR commands need `@cosyte/transform` and `@cosyte/fhir`. `npm install` adds both: the first is an optional dependency of `@cosyte/cli`, the second a peer dependency of the first. @@ -188,13 +188,13 @@ a step fails. The code is in [`tour.sh`](tour.sh) and [`src/make-fixtures.js`](s ## Limits -- `redact` refuses rather than under-redacts. The default policy of `@cosyte/deid` 0.1.0 blocks a - visit number (`PV1-19`) and a placer or filler order number (`ORC-2`/`ORC-3`, `OBR-2`/`OBR-3`) - instead of removing it, and then `cosyte redact` prints nothing on stdout, names the blocked locus - on stderr (`DEID_LOCUS_BLOCKED`) and exits `1` (`CLI_DEID_INCOMPLETE`). The synthetic message has - no `PV1-19`, so step 6 exits `0`. Many real ADT messages carry one, so check the exit code before - you use the copy. To choose a policy yourself, call `@cosyte/deid` from code, as the `deid-hl7` - starter in this repository does. +- `redact` refuses rather than under-redacts. On the 0.1 line, the default policy of `@cosyte/deid` + blocks a visit number (`PV1-19`) and a placer or filler order number (`ORC-2`/`ORC-3`, + `OBR-2`/`OBR-3`) instead of removing it, and then `cosyte redact` prints nothing on stdout, names + the blocked locus on stderr (`DEID_LOCUS_BLOCKED`), and exits `1` (`CLI_DEID_INCOMPLETE`). The + synthetic message has no `PV1-19`, so step 6 exits `0`. Many real ADT messages carry one, so check + the exit code before you use the copy. To choose a policy yourself, call `@cosyte/deid` from code, + as the `deid-hl7` starter in this repository does. - The de-identified copy is transformed per the library's Safe Harbor policy. It is not certified as de-identified. A position no rule covers passes through unchanged: here the message control ID (`MSH-10`) and the patient location (`PV1-3`). @@ -202,20 +202,20 @@ a step fails. The code is in [`tour.sh`](tour.sh) and [`src/make-fixtures.js`](s Bundle on stdout carry the patient's values, because that output is the data you asked for. - `validate` calls an HL7 v2 message valid when it parses; parser warnings are shown but do not fail it. It is not a conformance check against a profile. -- The `@cosyte/transform` that `@cosyte/cli` 0.1.0 installs (0.1.0) has no converter for phone +- On the 0.1 line, the `@cosyte/transform` that `@cosyte/cli` installs has no converter for phone numbers, so the Bundle's `Patient` has no `telecom`. The drop is reported as a finding. ## Synthetic data -The message comes from `@cosyte/synth`, which draws every name, identifier, date, phone and address -from reserved or fictional ranges. It is not real patient data. The tour generates it at run time into -`out/`, and nothing it generates is committed. +The message comes from `@cosyte/synth`, which draws every name, identifier, date, phone, and address +from reserved or fictional ranges. It is not real patient data. The tour generates it at run time +into `out/`, and nothing it generates is committed. ## Versions The `@cosyte/*` versions are set in [`package.json`](package.json). After changing one, run `npm install` to refresh `package-lock.json`, then `npm test`. The tour prints `cosyte --version` -first, so you can tell which CLI produced your output. `@cosyte/cli` 0.1.0 and this starter use the -same `@cosyte/hl7` 0.1.0, so npm installs one copy. +first, so you can tell which CLI produced your output. `@cosyte/cli` and this starter both depend on +the 0.1 line of `@cosyte/hl7`, so npm installs one copy. -Need it integrated? [Talk to us](https://cosyte.com/contact). +Need it integrated? [Talk to us](https://cosyte.com/integration-services?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). diff --git a/deid-hl7/README.md b/deid-hl7/README.md index ec739d8..4c85b3a 100644 --- a/deid-hl7/README.md +++ b/deid-hl7/README.md @@ -55,7 +55,7 @@ Found in the residual list: 0 ``` The medical record number in `PID-3` is removed, and its assigning authority and type code stay. The -name, phone and national identifier are removed too. The date of birth and the message and event +name, phone, and national identifier are removed too. The date of birth and the message and event timestamps (`MSH-7`, `EVN-2`) keep only their year, and the address keeps only the Safe Harbor three-digit ZIP (`000` for a restricted prefix). The unexamined residuals are the positions no rule named, from the message control ID in `MSH-10` to the patient location in `PV1-3`: the policy passed @@ -68,10 +68,10 @@ npm test ``` The test runs `src/main.js` and checks the manifest rows for the medical record number, name, date -of birth, phone, national identifier and the two timestamps. It then de-identifies the message again -in process and checks that no identifier read from the input `PID` segment appears in the output -message, the manifest or the residual list, and that each of the three dates keeps its year and -nothing finer. +of birth, phone, national identifier, and the two timestamps. It then de-identifies the message +again in process and checks that no identifier read from the input `PID` segment appears in the +output message, the manifest, or the residual list, and that each of the three dates keeps its year +and nothing finer. ## How it works @@ -80,7 +80,7 @@ nothing finer. 2. `parseHL7` from `@cosyte/hl7` parses it. `@cosyte/deid` locates identifiers structurally in that model (PID-5 is the patient name because the standard says so), not by pattern matching the text. 3. `deidentifyHl7(message)` from `@cosyte/deid/hl7` applies the built-in Safe Harbor policy and - returns a new message, the manifest and `unexaminedResiduals`: every value-bearing position the + returns a new message, the manifest, and `unexaminedResiduals`: every value-bearing position the pass handed through without a rule, counted by locus. The input message is never changed. 4. The Safe Harbor policy needs no key. A surrogate computed from the medical record number would be derived from the patient's own identifier, which 45 CFR 164.514(c) does not allow as a @@ -89,7 +89,7 @@ nothing finer. `profileOptions(LIMITED_DATA_SET_PROFILE, createDeidContext({ key, patientId }))`. That profile also shifts dates by a keyed per-patient offset, and it does not claim Safe Harbor. 5. `src/main.js` ends with a check you can keep in your own pipeline: every identifier read from the - input is searched for in the output, the manifest and the residual list, and the process exits 1 + input is searched for in the output, the manifest, and the residual list, and the process exits 1 if one is found. The code is in [`src/deid.js`](src/deid.js) and [`src/main.js`](src/main.js). @@ -106,12 +106,12 @@ The code is in [`src/deid.js`](src/deid.js) and [`src/main.js`](src/main.js). ## Synthetic data -The message comes from `@cosyte/synth`, which draws every name, identifier, date, phone and address -from reserved or fictional ranges. It is not real patient data. +The message comes from `@cosyte/synth`, which draws every name, identifier, date, phone, and +address from reserved or fictional ranges. It is not real patient data. ## Versions The `@cosyte/*` versions are set in [`package.json`](package.json). After changing one, run `npm install` to refresh `package-lock.json`, then `npm test`. -Need it integrated? [Talk to us](https://cosyte.com/contact). +Need it integrated? [Talk to us](https://cosyte.com/integration-services?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). diff --git a/dicom-metadata/README.md b/dicom-metadata/README.md index c339f66..4a5b2a3 100644 --- a/dicom-metadata/README.md +++ b/dicom-metadata/README.md @@ -1,8 +1,9 @@ # dicom-metadata -Read the patient, study and series metadata of a synthetic DICOM Part 10 file with +Read the patient, study, and series metadata of a synthetic DICOM Part 10 file with [`@cosyte/dicom`](https://github.com/cosyte/dicom), then de-identify the header with -[`@cosyte/deid`](https://github.com/cosyte/deid) and print the value-free manifest of what it changed. +[`@cosyte/deid`](https://github.com/cosyte/deid) and print the value-free manifest of what it +changed. Libraries: [`@cosyte/dicom`](https://github.com/cosyte/dicom), [`@cosyte/deid`](https://github.com/cosyte/deid). @@ -83,10 +84,10 @@ Found in the manifest: 0 ``` The output is the same on every run: the synthetic UIDs come from fixed names, and `@cosyte/dicom` -derives each replacement UID from its source UID. The patient attributes are emptied, Institution -Name, Issuer of Patient ID and the two descriptions are removed, and the Study, Series and SOP -Instance UIDs are replaced. The pass records itself in `(0012,0062)`, in the text of `(0012,0063)` -and as the code `113100` (Basic Application Confidentiality Profile) in `(0012,0064)`, and it sets +derives each replacement UID from its source UID. The patient attributes are emptied; Institution +Name, Issuer of Patient ID, and the two descriptions are removed; and the Study, Series, and SOP +Instance UIDs are replaced. The pass records itself in `(0012,0062)`, in the text of `(0012,0063)`, +and as the code `113100` (Basic Application Confidentiality Profile) in `(0012,0064)`. It also sets Longitudinal Temporal Information Modified `(0028,0303)` to `REMOVED`, which tells a receiver the dates and times were removed. @@ -96,19 +97,19 @@ dates and times were removed. npm test ``` -The test runs `src/main.js` and checks the metadata lines against the values parsed from the synthetic -file. It then de-identifies the header in process and checks that no identifying value from the input -appears in the written file or in the manifest, that each UID is replaced and recorded in the shared -UID map, and that the manifest names the category of the patient name, ID and birth date. Last, it -checks that the pass records itself as text and as a code, marks the dates removed, and writes the -data set in ascending tag order. +The test runs `src/main.js` and checks the metadata lines against the values parsed from the +synthetic file. It then de-identifies the header in process and checks that no identifying value +from the input appears in the written file or in the manifest, that each UID is replaced and +recorded in the shared UID map, and that the manifest names the category of the patient name, ID, +and birth date. Last, it checks that the pass records itself as text and as a code, marks the dates +removed, and writes the data set in ascending tag order. ## How it works -1. `syntheticCtHeader()` in `src/part10.js` writes a small Part 10 file: the 128-byte preamble, `DICM`, - the File Meta group naming Explicit VR Little Endian, then 20 patient, study, series and instance - attributes and no Pixel Data. `@cosyte/dicom` reads and re-writes Part 10, but it has no public API - for building a new object, so the starter writes these bytes itself. +1. `syntheticCtHeader()` in `src/part10.js` writes a small Part 10 file: the 128-byte preamble, + `DICM`, the File Meta group naming Explicit VR Little Endian, then 20 patient, study, series, and + instance attributes and no Pixel Data. `@cosyte/dicom` reads and re-writes Part 10, but it has no + public API for building a new object, so the starter writes these bytes itself. 2. `parseDicom(bytes)` from `@cosyte/dicom` parses the file. `readMetadata` reads it through the typed views (`fileMeta`, `patient`, `study`, `series`, `image`), which leave a missing value `undefined` rather than substituting a default, and `Dictionary.uid` names the transfer syntax and SOP class. @@ -125,7 +126,7 @@ Share one `uidMap` across every file of a study, so the replacement UIDs agree f map pairs each source UID with its replacement, which links the output back to the source: keep it private. -The code is in [`src/part10.js`](src/part10.js), [`src/dicom.js`](src/dicom.js) and +The code is in [`src/part10.js`](src/part10.js), [`src/dicom.js`](src/dicom.js), and [`src/main.js`](src/main.js). ## Limits @@ -135,8 +136,8 @@ The code is in [`src/part10.js`](src/part10.js), [`src/dicom.js`](src/dicom.js) - This is metadata-only de-identification: pixels are never inspected. The synthetic object has no Pixel Data. For an object whose Pixel Data is not marked `BurnedInAnnotation` `NO`, `burnedInAnnotationHazard` is `true`: do not release it before a pixel-capable review. -- With `@cosyte/deid` 0.1.0, the De-identification Method text written to `(0012,0063)` is 113 - characters, longer than the 64 an `LO` value allows, so `@cosyte/dicom` reports +- On the 0.1 line of `@cosyte/deid`, the De-identification Method text written to `(0012,0063)` is + 113 characters, longer than the 64 an `LO` value allows, so `@cosyte/dicom` reports `DICOM_DEIDENT_METHOD_VALUE_OVER_LENGTH`. A receiver that enforces the length may reject that attribute. The coded method in `(0012,0064)` names the same profile within the length limits. - The replacement UIDs are `2.25.` followed by 59 digits of a SHA-256 hash, while PS3.5 section B.2 @@ -154,4 +155,4 @@ real patient data, and the file is built at run time rather than committed. The `@cosyte/*` versions are set in [`package.json`](package.json). After changing one, run `npm install` to refresh `package-lock.json`, then `npm test`. -Need it integrated? [Talk to us](https://cosyte.com/contact). +Need it integrated? [Talk to us](https://cosyte.com/integration-services?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). diff --git a/hl7-mllp-listener/README.md b/hl7-mllp-listener/README.md index ed94700..2c7576f 100644 --- a/hl7-mllp-listener/README.md +++ b/hl7-mllp-listener/README.md @@ -24,7 +24,7 @@ npm run server # listens on 127.0.0.1:2575; set PORT to use another port npm run send # sends one synthetic ADT^A01 to 127.0.0.1:2575 and prints the whole ACK ``` -`npm run send` also reads `HOST`, `PORT` and `SEED` (the synth seed, default 12345), and exits 1 +`npm run send` also reads `HOST`, `PORT`, and `SEED` (the synth seed, default 12345), and exits 1 unless the answer is `AA` with a matching `MSA-2`. ## Expected output @@ -73,7 +73,7 @@ npm test ``` The test runs `src/main.js` and checks that each message was answered `MSA|AA|`. It then -starts a real listener on port 0, sends a synthetic admit and reads the ACK bytes: `MSA-1` is `AA` +starts a real listener on port 0, sends a synthetic admit, and reads the ACK bytes: `MSA-1` is `AA` and `MSA-2` equals the `MSH-10` that was sent. Last, it checks the negative answers: an `ORU^R01` gets `AR`, a payload that is not HL7 gets `AE`, and neither one reaches the admit handler. @@ -91,16 +91,18 @@ connections, and the auto-ACK path lives in the server. `AE`, or `AR` when it throws `MllpAckError` with `ackCode: "AR"`. Put your durable write (a queue, a database) in the handler: the `AA` never goes out before it finishes. 3. In the handler, `parseHL7(payload)` from `@cosyte/hl7` parses the bytes, and the summary reads - `message.meta` (MSH), `message.patient` (PID) and `message.visit` (PV1). A payload with no usable - `MSH` makes `parseHL7` throw, so it is answered `AE`. A message that is not ADT is answered `AR`. + `message.meta` (MSH), `message.patient` (PID), and `message.visit` (PV1). A payload with no + usable `MSH` makes `parseHL7` throw, so it is answered `AE`. A message that is not ADT is + answered `AR`. 4. `createStarterClient({ host, port })` connects, with auto-reconnect off because this client - sends and exits. `client.send(bytes)` frames the message, writes it and resolves with the ACK + sends and exits. `client.send(bytes)` frames the message, writes it, and resolves with the ACK that answers it. `interpretAck` from `@cosyte/hl7` reads `MSA-1` and `MSA-2` from that ACK. 5. `src/main.js` closes the client first and the listener second, so no socket stays open and the process exits 0. `npm run server` passes `handleSignals: true`, so Ctrl-C closes the listener. -The code is in [`src/listener.js`](src/listener.js) (the listener, the summary and the ACK check), -[`src/main.js`](src/main.js), [`src/server.js`](src/server.js) and [`src/client.js`](src/client.js). +The code is in [`src/listener.js`](src/listener.js) (the listener, the summary, and the ACK check), +[`src/main.js`](src/main.js), [`src/server.js`](src/server.js), and +[`src/client.js`](src/client.js). ## Limits @@ -119,12 +121,12 @@ The code is in [`src/listener.js`](src/listener.js) (the listener, the summary a ## Synthetic data -The messages come from `@cosyte/synth`, which draws every name, identifier, date, phone and address -from reserved or fictional ranges. It is not real patient data. +The messages come from `@cosyte/synth`, which draws every name, identifier, date, phone, and +address from reserved or fictional ranges. It is not real patient data. ## Versions The `@cosyte/*` versions are set in [`package.json`](package.json). After changing one, run `npm install` to refresh `package-lock.json`, then `npm test`. -Need it integrated? [Talk to us](https://cosyte.com/contact). +Need it integrated? [Talk to us](https://cosyte.com/integration-services?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). diff --git a/hl7-to-fhir/README.md b/hl7-to-fhir/README.md index a07714f..05169fe 100644 --- a/hl7-to-fhir/README.md +++ b/hl7-to-fhir/README.md @@ -66,29 +66,29 @@ Validation with @cosyte/fhir (strict mode, starter-kit profiles): npm test ``` -The test runs `src/main.js` and checks the Patient name and the validation total. For four seeds that -cover patient classes E, O and I and both sexes, it then converts the admit in process and checks the -Bundle against the parsed HL7 input: the Patient's name, identifier, gender, birth date and address -come from `PID`, the Encounter's class comes from `PV1-2` through the guide's map, its subject is the -Patient entry, and `@cosyte/fhir` reports no errors. It also checks that no value from the message -appears in the transform's diagnostics, and that without an identifier system the starter Patient -profile flags `Patient.identifier.system`. +The test runs `src/main.js` and checks the Patient name and the validation total. For four seeds +that cover patient classes E, O, and I, and both sexes, it then converts the admit in process and +checks the Bundle against the parsed HL7 input: the Patient's name, identifier, gender, birth date, +and address come from `PID`, the Encounter's class comes from `PV1-2` through the guide's map, its +subject is the Patient entry, and `@cosyte/fhir` reports no errors. It also checks that no value +from the message appears in the transform's diagnostics, and that without an identifier system the +starter Patient profile flags `Patient.identifier.system`. ## How it works 1. `generateAdt({ seed: 12345, trigger: "A01" })` from `@cosyte/synth/hl7` builds the admit message, and `parseHL7` from `@cosyte/hl7` parses it. 2. `toFhir(message, options)` from `@cosyte/transform` builds a FHIR R4 message `Bundle`: a - `MessageHeader` from `MSH`, a `Patient` from `PID` and an `Encounter` from `PV1`, grounded on the - maps of the HL7 Version 2 to FHIR implementation guide. Two options matter here: - - `namingSystem`: `createNamingSystem({ authorities: { "COSYTE-SYNTH": MRN_SYSTEM } })` says which - identifier system the assigning authority in `PID-3` stands for. Without it the transform keeps - the MRN, leaves `identifier.system` out and raises a warning, because it never builds a system - URI from a bare namespace. + `MessageHeader` from `MSH`, a `Patient` from `PID`, and an `Encounter` from `PV1`, grounded on + the maps of the HL7 Version 2 to FHIR implementation guide. Two options matter here: + - `namingSystem`: `createNamingSystem({ authorities: { "COSYTE-SYNTH": MRN_SYSTEM } })` says + which identifier system the assigning authority in `PID-3` stands for. Without it the transform + keeps the MRN, leaves `identifier.system` out, and raises a warning, because it never builds a + system URI from a bare namespace. - `generateId`: numbered `urn:uuid:` fullUrls, so the output is the same on every run. Leave it out in production and the transform uses `crypto.randomUUID`. The transform identifies entries by fullUrl and leaves `Resource.id` unset. -3. Every diagnostic the transform raises is value-free: a severity, a stable code, a v2 location +3. Every diagnostic the transform raises is value-free: a severity, a stable code, a v2 location, and, where one applies, a FHIR path. `MSH-7` carries no UTC offset and `Bundle.timestamp` needs one, so the transform leaves the timestamp out rather than guess one: that is the `TS.1` warning and the `MSH.7` row. If you know your sender's offset, pass `assumeTimezoneOffsetMinutes`; the @@ -111,24 +111,24 @@ The code is in [`src/convert.js`](src/convert.js) and [`src/main.js`](src/main.j ## Limits -- `@cosyte/fhir` 0.1.0 has no structural schema for `Bundle`, `MessageHeader` or `Encounter`. For - those three it checks the base resource elements and the safety rules, and reports - `RESOURCE_NOT_MODELED` (information) to say that their own elements were not checked. Zero errors - here is not a full FHIR conformance verdict. -- The Patient carries `PID-3` (identifiers), `PID-5` (name), `PID-7` (birth date), `PID-8` (sex) and - `PID-11` (address). `PID-13` (phone) and `PV1-3` (location) are dropped with a diagnostic. - `PID-19` (SSN) is dropped without one in `@cosyte/transform` 0.1.0, although the implementation - guide maps it to `Patient.identifier`. -- With `@cosyte/fhir` 0.1.0, `resolvePath` returns a repeating element named by the last path - segment (`entry`, `name.given`) as one list node rather than its items. `nodesAt` in +- On the 0.1 line, `@cosyte/fhir` has no structural schema for `Bundle`, `MessageHeader`, or + `Encounter`. For those three it checks the base resource elements and the safety rules, and + reports `RESOURCE_NOT_MODELED` (information) to say that their own elements were not checked. Zero + errors here is not a full FHIR conformance verdict. +- The Patient carries `PID-3` (identifiers), `PID-5` (name), `PID-7` (birth date), `PID-8` (sex), + and `PID-11` (address). `PID-13` (phone) and `PV1-3` (location) are dropped with a diagnostic. + `PID-19` (SSN) is dropped without one by `@cosyte/transform` on the 0.1 line, although the + implementation guide maps it to `Patient.identifier`. +- On the 0.1 line of `@cosyte/fhir`, `resolvePath` returns a repeating element named by the last + path segment (`entry`, `name.given`) as one list node rather than its items. `nodesAt` in [`src/convert.js`](src/convert.js) flattens it. - US Core profiles are not bundled with `@cosyte/fhir`. To validate against them, read each - StructureDefinition with `parseResource`, load it with `loadStructureDefinition` and pass the + StructureDefinition with `parseResource`, load it with `loadStructureDefinition`, and pass the results as `profiles`. ## Synthetic data -The message comes from `@cosyte/synth`, which draws every name, identifier, date, phone and address +The message comes from `@cosyte/synth`, which draws every name, identifier, date, phone, and address from reserved or fictional ranges. It is not real patient data. The identifier system uses the reserved `example.org` domain. @@ -137,4 +137,4 @@ reserved `example.org` domain. The `@cosyte/*` versions are set in [`package.json`](package.json). After changing one, run `npm install` to refresh `package-lock.json`, then `npm test`. -Need it integrated? [Talk to us](https://cosyte.com/contact). +Need it integrated? [Talk to us](https://cosyte.com/integration-services?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). diff --git a/hl7-to-fhir/src/convert.js b/hl7-to-fhir/src/convert.js index 73cdd5a..77c5205 100644 --- a/hl7-to-fhir/src/convert.js +++ b/hl7-to-fhir/src/convert.js @@ -54,8 +54,8 @@ export function convertToFhir(wire) { /** * The nodes an element path selects, in document order. * - * `resolvePath` flattens the repeating elements it walks through, but with @cosyte/fhir 0.1.0 a - * repeating element named by the last segment (`entry`, `name.given`) comes back as one list node + * `resolvePath` flattens the repeating elements it walks through, but on the @cosyte/fhir 0.1 line + * a repeating element named by the last segment (`entry`, `name.given`) comes back as one list node * rather than its items, so we flatten that list here too. * * @param {FhirNode} node diff --git a/ncpdp-claim/README.md b/ncpdp-claim/README.md index 736aacb..32cd817 100644 --- a/ncpdp-claim/README.md +++ b/ncpdp-claim/README.md @@ -1,9 +1,9 @@ # ncpdp-claim Parse a synthetic NCPDP Telecom D.0 billing claim (B1) and a paid response to it with -[`@cosyte/ncpdp`](https://github.com/cosyte/ncpdp), and print what a pharmacy biller checks: routing, -product, quantity and the amounts submitted, then the claim's status and the amounts paid, with every -amount read as text and never as a floating-point number. +[`@cosyte/ncpdp`](https://github.com/cosyte/ncpdp), and print what a pharmacy biller checks: +routing, product, quantity, and the amounts submitted, then the claim's status and the amounts paid, +with every amount read as text and never as a floating-point number. Libraries: [`@cosyte/ncpdp`](https://github.com/cosyte/ncpdp), [`@cosyte/synth`](https://github.com/cosyte/synth). @@ -69,45 +69,46 @@ npm test ``` The test runs `src/main.js` and checks the product line, the echoed prescription number, the paid -status and that neither parse raised a warning. It then reads the claim in process and checks the claim -view against the Claim segment's own fields, checks that the paid response echoes the claim and that -its total paid equals ingredient cost plus dispensing fee minus patient pay, and checks that a rejected -response reads as rejected with every reject code in wire order, even when its status field says paid. +status, and that neither parse raised a warning. It then reads the claim in process and checks the +claim view against the Claim segment's own fields, checks that the paid response echoes the claim +and that its total paid equals ingredient cost plus dispensing fee minus patient pay, and checks +that a rejected response reads as rejected with every reject code in wire order, even when its +status field says paid. ## How it works -1. `generateB1({ seed: 1 })` from `@cosyte/synth/ncpdp` builds a B1 claim through `@cosyte/ncpdp`'s own - `buildTelecomRequest` and `serializeTelecom`. It carries Patient, Insurance, Prescriber and Claim - segments but no Pricing segment, so `syntheticClaim` parses it and rebuilds it with a Pricing segment - (11) through the same builder, which refuses a field ID that is not two characters or a value that - holds a separator. +1. `generateB1({ seed: 1 })` from `@cosyte/synth/ncpdp` builds a B1 claim through `@cosyte/ncpdp`'s + own `buildTelecomRequest` and `serializeTelecom`. It carries Patient, Insurance, Prescriber, and + Claim segments but no Pricing segment, so `syntheticClaim` parses it and rebuilds it with a + Pricing segment (11) through the same builder, which refuses a field ID that is not two + characters or a value that holds a separator. 2. `parseTelecom` reads the claim: the fixed 56-byte header (BIN, version, transaction code, PCN, pharmacy, date of service) and the segments, each a list of two-character field IDs and their - values. `claim(transaction)` lifts the fields a biller checks: group, prescription and fill, product - and its qualifier, quantity and days supply. The library has no view over the Pricing segment, so - `submittedPricing` reads it by field ID with `findSegment` and `fieldValue`. + values. `claim(transaction)` lifts the fields a biller checks: group, prescription and fill, + product and its qualifier, quantity, and days supply. The library has no view over the Pricing + segment, so `submittedPricing` reads it by field ID with `findSegment` and `fieldValue`. 3. Money never becomes a JavaScript number. `telecomMoney` keeps the wire text in `source` and applies the implied decimal places and the sign string-wise into `amount`. The last character carries the sign: `{` and `A` to `I` are +0 to +9, `}` and `J` to `R` are -0 to -9. Quantity Dispensed works the same way with three implied decimal places, and qualifier `03` marks the product ID as an NDC. 4. `@cosyte/synth` generates Telecom requests only, and `@cosyte/ncpdp` builds requests only, so `syntheticResponse` writes the payer's answer as wire text: a response header that echoes the - transaction code, the pharmacy and the date of service, then a group separator and the Response - Status (21), Response Claim (22, echoing the prescription number) and Response Pricing (23) + transaction code, the pharmacy, and the date of service, then a group separator and the Response + Status (21), Response Claim (22, echoing the prescription number), and Response Pricing (23) segments. `parseTelecom` tells a response from a request by where `D0` sits in the header. -5. `adjudication(response)` returns the status, pricing and DUR views. Its `disposition` comes from the - status field and the reject codes together, and a reject always wins: a response whose status says - `P` but carries a reject code reads as rejected, with `statusConflict` set. Reject codes come back - verbatim, in wire order. `syntheticResponse(request, ["75"])` writes a rejected answer, and the test - reads one. +5. `adjudication(response)` returns the status, pricing, and DUR views. Its `disposition` comes from + the status field and the reject codes together, and a reject always wins: a response whose status + says `P` but carries a reject code reads as rejected, with `statusConflict` set. Reject codes + come back verbatim, in wire order. `syntheticResponse(request, ["75"])` writes a rejected answer, + and the test reads one. The code is in [`src/claim.js`](src/claim.js) and [`src/main.js`](src/main.js). ## Limits -- `@cosyte/synth` 0.1.0 writes Quantity Dispensed as a whole number of units (`31`), without the three - implied decimal places the field carries, so it reads as 0.031. A claim for 31 units carries `31000`. - We print what the wire says rather than correct it. +- On the 0.1 line, `@cosyte/synth` writes Quantity Dispensed as a whole number of units (`31`), + without the three implied decimal places the field carries, so it reads as 0.031. A claim for 31 + units carries `31000`. We print what the wire says rather than correct it. - `@cosyte/ncpdp` reads each group-separated block of a transmission as its own transaction, and the views (`claim`, `adjudication`) read the first. The response here puts all of its segments after one group separator. Segments placed before it, such as a Response Message (20) or Response Insurance @@ -115,7 +116,7 @@ The code is in [`src/claim.js`](src/claim.js) and [`src/main.js`](src/main.js). Check `transaction.warnings` before you trust a view. - The response header is decoded through the pharmacy ID. The date of service that follows it is on the wire but not in `responseHeader`. -- One claim, one response, and no DUR, coordination of benefits or compound segments. +- One claim, one response, and no DUR, coordination of benefits, or compound segments. `@cosyte/ncpdp` reads those too (`responseDur`, `responseCob`, `compound`). ## Synthetic data @@ -133,4 +134,4 @@ data. The `@cosyte/*` versions are set in [`package.json`](package.json). After changing one, run `npm install` to refresh `package-lock.json`, then `npm test`. -Need it integrated? [Talk to us](https://cosyte.com/contact). +Need it integrated? [Talk to us](https://cosyte.com/integration-services?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). diff --git a/scripts/check-no-emdash.sh b/scripts/check-no-emdash.sh index bb46a3e..057a74c 100755 --- a/scripts/check-no-emdash.sh +++ b/scripts/check-no-emdash.sh @@ -36,7 +36,7 @@ PATTERN='\x{2014}|\x{2013}|%E2%80%94|%E2%80%93|\\u2014|\\u2013|—|–|& # clean result is believed. `printf` emits U+2014 as its UTF-8 bytes, so this file # still never contains the literal character. if ! printf 'a\xe2\x80\x94b\n' | grep -qP "$PATTERN" || ! printf 'a\xe2\x80\x93b\n' | grep -qP "$PATTERN"; then - echo "ERROR: check-no-emdash - the scanner cannot match a known dash." >&2 + echo "ERROR: check-no-emdash: the scanner cannot match a known dash." >&2 echo " grep -P is unavailable or not in UTF-8 mode (LC_ALL=${LC_ALL})." >&2 echo " Refusing to report a clean tree on a scanner that cannot see." >&2 exit 1 @@ -46,7 +46,7 @@ fail_with_hits() { local what="$1" hits="$2" echo "$hits" >&2 echo "" >&2 - echo "ERROR: check-no-emdash - em or en dash (U+2014, U+2013, or an encoded form) found in ${what}." >&2 + echo "ERROR: check-no-emdash: em or en dash (U+2014, U+2013, or an encoded form) found in ${what}." >&2 echo " Cosyte never uses em or en dashes (06-brand/voice-and-tone.md)." >&2 echo " Rewrite with a period, colon, comma, or parentheses." >&2 exit 1 @@ -65,7 +65,7 @@ refuse_if_incomplete() { [ -s "$ERRLOG" ] || return 0 cat "$ERRLOG" >&2 echo "" >&2 - echo "ERROR: check-no-emdash - the scan reported errors, so it did not read all of" >&2 + echo "ERROR: check-no-emdash: the scan reported errors, so it did not read all of" >&2 echo " its input. Refusing to report green from an incomplete scan." >&2 exit 1 } @@ -107,7 +107,7 @@ cd "$(git rev-parse --show-toplevel)" # # no -I: -I skips any file grep reads as binary, which includes a text file holding # invalid UTF-8, so an em dash inside one would be skipped silently. This repo is -# markdown and a licence today, with no binaries at all. Losing -I makes a future +# markdown and a license today, with no binaries at all. Losing -I makes a future # binary a loud false positive (grep prints "Binary file X matches", the gate goes # red, a human looks) instead of a silent miss. Fail closed, not open. # @@ -125,7 +125,7 @@ cd "$(git rev-parse --show-toplevel)" git ls-files -z > "$FILELIST" if [ ! -s "$FILELIST" ]; then - echo "ERROR: check-no-emdash - no tracked files to scan. Refusing to report green" >&2 + echo "ERROR: check-no-emdash: no tracked files to scan. Refusing to report green" >&2 echo " from a scan that read nothing." >&2 exit 1 fi diff --git a/x12-eligibility/README.md b/x12-eligibility/README.md index 8631d64..85dee1a 100644 --- a/x12-eligibility/README.md +++ b/x12-eligibility/README.md @@ -2,8 +2,9 @@ Read the plan and coverage facts out of a synthetic X12 271 eligibility response with [`@cosyte/x12`](https://github.com/cosyte/x12): who answered, who is covered, the group number, the -trace to match against your 270, and each benefit line with its service types, coverage level, amount -and percent. Then build the 270 inquiry that the 271 answers, and match the two by their trace. +trace to match against your 270, and each benefit line with its service types, coverage level, +amount, and percent. Then build the 270 inquiry that the 271 answers, and match the two by their +trace. Libraries: [`@cosyte/x12`](https://github.com/cosyte/x12), [`@cosyte/synth`](https://github.com/cosyte/synth). @@ -103,13 +104,13 @@ npm test ``` The test runs `src/main.js` and checks the payer, member ID, group number, trace, benefit lines, -coverage status and 270 trace it prints against a fresh reading of the same 271. It then splits the +coverage status, and 270 trace it prints against a fresh reading of the same 271. It then splits the 271 into segments by hand, without the library, and checks each value the typed reader returned -against the raw `NM1`, `REF`, `TRN` and `EB` segments. A third test builds a small 271 with -co-payment, deductible and co-insurance lines through `build271` and checks their labels, and that +against the raw `NM1`, `REF`, `TRN`, and `EB` segments. A third test builds a small 271 with +co-payment, deductible, and co-insurance lines through `build271` and checks their labels, and that each amount and percent stays exactly as sent. A fourth reads the built 270 back with -`get270Inquiry` and checks its envelope, header, parties, member, service type and trace against the -271 and the raw segments. The last turns the synthetic 271 into a rejection, an `AAA` segment in +`get270Inquiry` and checks its envelope, header, parties, member, service type, and trace against +the 271 and the raw segments. The last turns the synthetic 271 into a rejection, an `AAA` segment in place of the benefit line, and checks that it reads as a rejection with its codes, not as a member with no benefits. @@ -120,14 +121,14 @@ with no benefits. 2. `parseX12` reads the interchange. The delimiters come from the ISA, and a tolerated deviation arrives on `warnings` with a stable code instead of an exception. 3. `get271Eligibility(interchange.delimiters, transaction)` returns the typed 271: each subscriber - with its payer, provider, name, member ID, `REF`, `DTP` and `TRN` rows, and its `EB` benefit + with its payer, provider, name, member ID, `REF`, `DTP`, and `TRN` rows, and its `EB` benefit lines, plus `aaaConditions`, one for each `AAA` segment in which the payer says it could not process the inquiry. Amounts and percents are `X12Decimal`, never a float, and each service type code comes with its description from the snapshot the package bundles. 4. `describeSubscriber` in `src/eligibility.js` turns that into the facts a front desk asks for. It labels the seven `EB-01` benefit types it knows (active coverage, inactive, co-insurance, co-payment, deductible, out of pocket, non-covered) and prints any other code as sent. Coverage - status comes from `EB-01` codes 1 to 8, cost sharing from `A`, `B`, `C` and `G`. Dates print + status comes from `EB-01` codes 1 to 8, cost sharing from `A`, `B`, `C`, and `G`. Dates print through `toISO`, which reads a `D8` day as `YYYY-MM-DD`. 5. The `AAA` conditions tell a rejected inquiry from a member with no benefit lines, since both come back with no benefits. `describeRejection` prints each one with the payer's reject reason @@ -144,32 +145,32 @@ The code is in [`src/eligibility.js`](src/eligibility.js) and [`src/main.js`](sr ## Limits -- `@cosyte/synth` 0.1.0 writes no 270, so the starter builds the 270 from the parties, member and - trace of the synthetic 271, and the trace matches by construction. The 271 echoes `TRN-02` but no - `TRN-03`, so the match here is on `TRN-02` alone. -- `@cosyte/x12` 0.1.0 bundles no descriptions for `AAA` codes (the package has no permission to - redistribute them), so a rejection prints its codes as sent, and each code also raises an - `X12_271_AAA_UNKNOWN_CODE` warning. +- On the 0.1 line, `@cosyte/synth` writes no 270, so the starter builds the 270 from the parties, + member, and trace of the synthetic 271, and the trace matches by construction. The 271 echoes + `TRN-02` but no `TRN-03`, so the match here is on `TRN-02` alone. +- On the 0.1 line, `@cosyte/x12` bundles no descriptions for `AAA` codes (the package has no + permission to redistribute them), so a rejection prints its codes as sent, and each code also + raises an `X12_271_AAA_UNKNOWN_CODE` warning. - The synthetic 271 carries one benefit line: active coverage with an amount and a percent. It has - no co-payment, co-insurance, deductible or out-of-pocket line, so the cost-sharing path runs only - in the test. With `@cosyte/synth` 0.1.0 its `EB-08` percent is a whole number (80, 90 or 100), - where the 271 guide defines a decimal fraction (0.8 for 80 percent): read it as a placeholder. It - also has no `BHT` segment after `ST`, which the 271 guide requires: `build271` in `@cosyte/x12` - 0.1.0, which synth builds through, writes none, and `get271Eligibility` reads the 271 without a - warning. The 270 has one, because `build270` writes it. -- The labels cover seven `EB-01` codes, two `DTP-01` qualifiers (`307`, `291`) and two `REF-01` - qualifiers (`6P`, `18`). Coverage level, network, time period and any other code print as sent. + no co-payment, co-insurance, deductible, or out-of-pocket line, so the cost-sharing path runs only + in the test. On the 0.1 line of `@cosyte/synth`, its `EB-08` percent is a whole number (80, 90, or + 100), where the 271 guide defines a decimal fraction (0.8 for 80 percent): read it as a + placeholder. It also has no `BHT` segment after `ST`, which the 271 guide requires: `build271` in + `@cosyte/x12` on the 0.1 line, which synth builds through, writes none, and `get271Eligibility` + reads the 271 without a warning. The 270 has one, because `build270` writes it. +- The labels cover seven `EB-01` codes, two `DTP-01` qualifiers (`307`, `291`), and two `REF-01` + qualifiers (`6P`, `18`). Coverage level, network, time period, and any other code print as sent. Dependents (Loop 2000D) are on `subscriber.dependents` and are not printed. -- The output shows a subscriber name, member ID and date of birth because the data is synthetic. A +- The output shows a subscriber name, member ID, and date of birth because the data is synthetic. A production service should not write PHI to its logs. ## Synthetic data -The 271 comes from `@cosyte/synth`, which draws every name, member ID, date, NPI and address from +The 271 comes from `@cosyte/synth`, which draws every name, member ID, date, NPI, and address from reserved or fictional ranges: the NPI fails its check digit on purpose and the ZIP is `00000`. It is not real patient data. The cost-sharing test builds its own 271 from fictional values only (`EXAMPLE PAYER`, `EXAMPLE CLINIC`, `ZZTEST`). The 270 reuses the synthetic 271's values; its -control numbers, its `BHT-03` reference (`SYNTHREQ0001`) and its `TRN-03` originator (`9SYNTHETIC`) +control numbers, its `BHT-03` reference (`SYNTHREQ0001`), and its `TRN-03` originator (`9SYNTHETIC`) are ours and fictional. The rejection test changes the synthetic 271 in memory; no fixture is committed. @@ -178,4 +179,4 @@ committed. The `@cosyte/*` versions are set in [`package.json`](package.json). After changing one, run `npm install` to refresh `package-lock.json`, then `npm test`. -Need it integrated? [Talk to us](https://cosyte.com/contact). +Need it integrated? [Talk to us](https://cosyte.com/integration-services?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). diff --git a/x12-eligibility/src/eligibility.js b/x12-eligibility/src/eligibility.js index 4a35ae3..b46fff5 100644 --- a/x12-eligibility/src/eligibility.js +++ b/x12-eligibility/src/eligibility.js @@ -5,8 +5,8 @@ import { build270, get270Inquiry, get271Eligibility, parseX12, serializeX12, toI /** * Labels for the EB-01 benefit types this starter names. They are values of ASC X12 data element * 1390 (Eligibility or Benefit Information Code), which EB-01 of the 005010X279A1 271 carries. - * @cosyte/x12 0.1.0 bundles no table for EB-01 (its `X12EligibilityBenefit` documentation names - * 1, 6 and I), so we keep this short list and print any other code as sent. + * The @cosyte/x12 0.1 line bundles no table for EB-01 (its `X12EligibilityBenefit` documentation + * names 1, 6, and I), so we keep this short list and print any other code as sent. * * @type {Readonly>} */ diff --git a/x12-eligibility/test/x12-eligibility.test.js b/x12-eligibility/test/x12-eligibility.test.js index 2e03f00..5bbda9d 100644 --- a/x12-eligibility/test/x12-eligibility.test.js +++ b/x12-eligibility/test/x12-eligibility.test.js @@ -109,10 +109,10 @@ test("the typed reading agrees with the raw segments of the synthetic 271", () = }); }); -test("co-payment, deductible and co-insurance lines are labelled and keep their amounts exactly", () => { +test("co-payment, deductible, and co-insurance lines are labeled and keep their amounts exactly", () => { /** @param {string} text */ const amount = (text) => X12Decimal.fromString(text) ?? assert.fail(`not a decimal: ${text}`); - // @cosyte/synth 0.1.0 writes no cost-sharing lines, so this test builds a small 271 through + // The @cosyte/synth 0.1 line writes no cost-sharing lines, so this test builds a small 271 through // @cosyte/x12's own build271. Every value in it is fictional. const built = build271({ envelope: { @@ -228,7 +228,8 @@ test("a rejected inquiry reads as a rejection, not as a member with no benefits" const [condition] = eligibility.aaaConditions; assert.ok(condition); assert.equal(describeRejection(condition), "subscriber (HL 3): reject reason 72, follow-up action C"); - // @cosyte/x12 0.1.0 bundles no AAA code descriptions, so each code it reads also raises a warning. + // The @cosyte/x12 0.1 line bundles no AAA code descriptions, so each code it reads also raises a + // warning. assert.deepEqual( eligibility.warnings.map((warning) => warning.code), ["X12_271_AAA_UNKNOWN_CODE", "X12_271_AAA_UNKNOWN_CODE"], diff --git a/x12-remittance/README.md b/x12-remittance/README.md index 8690604..11eb97a 100644 --- a/x12-remittance/README.md +++ b/x12-remittance/README.md @@ -1,8 +1,8 @@ # x12-remittance Summarize a synthetic X12 835 remittance with [`@cosyte/x12`](https://github.com/cosyte/x12): the -payment and its trace number, the payer and payee, each claim with its status, amounts and -adjustments, and a check that recomputes the 835's three balance equations with exact decimal +payment and its trace number; the payer and payee; each claim with its status, amounts, and +adjustments; and a check that recomputes the 835's three balance equations with exact decimal arithmetic. Libraries: [`@cosyte/x12`](https://github.com/cosyte/x12), @@ -85,13 +85,13 @@ says why. npm test ``` -The test runs `src/main.js` and checks the payment, claim status, amounts, adjustments and balance -lines it prints against a fresh reading of the same 835. It then splits the 835 into segments by hand, -without the library, and checks the typed reading against the raw `BPR`, `TRN`, `CLP` and `CAS` -segments. Two more tests change the 835 before reading it. Raising `BPR-02` by 1.00 must report the -payment out of balance, and `get835` must warn `X12_835_REMIT_BALANCE_MISMATCH`. Adding a 10.00 -provider-level take-back (`PLB`) and lowering `BPR-02` to match must still balance, which pins the -sign: a positive `PLB` amount reduces the payment. +The test runs `src/main.js` and checks the payment, claim status, amounts, adjustments, and balance +lines it prints against a fresh reading of the same 835. It then splits the 835 into segments by +hand, without the library, and checks the typed reading against the raw `BPR`, `TRN`, `CLP`, and +`CAS` segments. Two more tests change the 835 before reading it. Raising `BPR-02` by 1.00 must +report the payment out of balance, and `get835` must warn `X12_835_REMIT_BALANCE_MISMATCH`. Adding a +10.00 provider-level take-back (`PLB`) and lowering `BPR-02` to match must still balance, which pins +the sign: a positive `PLB` amount reduces the payment. ## How it works @@ -119,15 +119,15 @@ The code is in [`src/remittance.js`](src/remittance.js) and [`src/main.js`](src/ ## Limits -- With `@cosyte/synth` 0.1.0 the 835 carries one claim with one service line, one - patient-responsibility adjustment and no provider-level adjustment, so the `PLB` term here is +- On the 0.1 line of `@cosyte/synth`, the 835 carries one claim with one service line, one + patient-responsibility adjustment, and no provider-level adjustment, so the `PLB` term here is 0.00. The test adds a `PLB` to exercise it. - The synthetic 835 is built to exercise the reader, not to pass a trading partner's edits. For example it names `ACH` as the payment method without the bank routing elements that go with it, and the payer and payee share one address. - Descriptions come from the subsets `@cosyte/x12` bundles. A claim status or CARC outside them prints as the code with "(no bundled description)". Remark codes (`LQ`, `MOA`), supplemental - amounts (`AMT`) and service dates are on the model and are not printed. + amounts (`AMT`), and service dates are on the model and are not printed. - The balance check says whether the 835 adds up. It does not check the amounts against your contract or against the claim you sent. - The output shows the raw 835, which includes a patient name and member ID because the data is @@ -135,13 +135,13 @@ The code is in [`src/remittance.js`](src/remittance.js) and [`src/main.js`](src/ ## Synthetic data -The 835 comes from `@cosyte/synth`, which draws every name, identifier, date and amount from reserved -or fictional ranges: the NPIs fail their check digit on purpose and the ZIPs are `00000`. It is not -real patient data. Two tests change that synthetic 835 in memory; no fixture is committed. +The 835 comes from `@cosyte/synth`, which draws every name, identifier, date, and amount from +reserved or fictional ranges: the NPIs fail their check digit on purpose and the ZIPs are `00000`. +It is not real patient data. Two tests change that synthetic 835 in memory; no fixture is committed. ## Versions The `@cosyte/*` versions are set in [`package.json`](package.json). After changing one, run `npm install` to refresh `package-lock.json`, then `npm test`. -Need it integrated? [Talk to us](https://cosyte.com/contact). +Need it integrated? [Talk to us](https://cosyte.com/integration-services?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples). diff --git a/x12-remittance/package.json b/x12-remittance/package.json index 5bfac34..3658eb7 100644 --- a/x12-remittance/package.json +++ b/x12-remittance/package.json @@ -2,7 +2,7 @@ "name": "cosyte-example-x12-remittance", "version": "0.0.0", "private": true, - "description": "Summarise a synthetic X12 835 remittance with @cosyte/x12: the payment, each claim, its adjustments and the balance.", + "description": "Summarize a synthetic X12 835 remittance with @cosyte/x12: the payment, each claim, its adjustments, and the balance.", "license": "MIT", "type": "module", "engines": { From 9168586cf7ee3d4cc93da2287edd48d94e5dc223 Mon Sep 17 00:00:00 2001 From: Noah Schatz Date: Sat, 7 Nov 2026 18:00:02 -0500 Subject: [PATCH 2/2] docs: name Cosyte LLC in the copyright line ST2 puts the legal name in copyright lines: LICENSE now reads "Copyright (c) 2026 Cosyte LLC". No README carries a copyright line. --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 9f99611..f1daead 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Cosyte +Copyright (c) 2026 Cosyte LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal