Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 15 additions & 24 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
27 changes: 17 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
<a href="https://cosyte.com/?utm_source=github&utm_medium=readme&utm_campaign=evergreen&utm_content=examples">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://cosyte.com/tile/cosyte-lockup-tile-on-dark-1200x300.png">
<img alt="The Cosyte logo on its own white ground: the icon beside the word Cosyte." src="https://cosyte.com/tile/cosyte-lockup-tile-on-light-1200x300.png">
</picture>
</a>

# 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` |

Expand All @@ -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).
54 changes: 28 additions & 26 deletions astm-lab-results/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<STX>`, a frame number, the record text, `<ETX>`, a checksum (the
sum of the bytes from the frame number through `<ETX>`, modulo 256, as two hex digits) and
`<CR><LF>`. 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: `<STX>`, a frame number, the record text, `<ETX>`, a
checksum (the sum of the bytes from the frame number through `<ETX>`, modulo 256, as two hex
digits), and `<CR><LF>`. 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.
Expand All @@ -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).
Loading
Loading