From 3baec1eeac6fe51e50e389f29112534a44487d7b Mon Sep 17 00:00:00 2001 From: Noah Schatz Date: Fri, 25 Sep 2026 21:27:54 +0000 Subject: [PATCH 1/2] Revert "Version Packages (#132)" This reverts commit b77aedf79f57c4407c090997e35ddb9e1cbaa1fa. --- .changeset/brave-moons-measure.md | 84 ++++ .changeset/eighty-jars-argue.md | 34 ++ .changeset/first-minor-release.md | 33 ++ .changeset/lucky-pandas-refuse.md | 50 ++ .changeset/olive-bats-report.md | 48 ++ .changeset/olive-otters-gather.md | 36 ++ .changeset/olive-pumas-greet.md | 39 ++ .changeset/pink-cobras-invent.md | 65 +++ .changeset/plenty-donuts-attack.md | 31 ++ .changeset/quiet-jars-repeat.md | 74 +++ .changeset/rare-owls-listen.md | 69 +++ .changeset/short-hounds-author.md | 70 +++ .changeset/spotty-cases-admit.md | 70 +++ .changeset/wide-clocks-convert.md | 63 +++ CHANGELOG.md | 721 ----------------------------- README.md | 2 +- package.json | 2 +- src/index.ts | 2 +- 18 files changed, 769 insertions(+), 724 deletions(-) create mode 100644 .changeset/brave-moons-measure.md create mode 100644 .changeset/eighty-jars-argue.md create mode 100644 .changeset/first-minor-release.md create mode 100644 .changeset/lucky-pandas-refuse.md create mode 100644 .changeset/olive-bats-report.md create mode 100644 .changeset/olive-otters-gather.md create mode 100644 .changeset/olive-pumas-greet.md create mode 100644 .changeset/pink-cobras-invent.md create mode 100644 .changeset/plenty-donuts-attack.md create mode 100644 .changeset/quiet-jars-repeat.md create mode 100644 .changeset/rare-owls-listen.md create mode 100644 .changeset/short-hounds-author.md create mode 100644 .changeset/spotty-cases-admit.md create mode 100644 .changeset/wide-clocks-convert.md diff --git a/.changeset/brave-moons-measure.md b/.changeset/brave-moons-measure.md new file mode 100644 index 0000000..30a6727 --- /dev/null +++ b/.changeset/brave-moons-measure.md @@ -0,0 +1,84 @@ +--- +"@cosyte/ccda": minor +--- + +feat: measure conformance against the normative artifacts, and fix what the measurement found + +A built document's conformance was expected rather than proven. The README said so: +the builder's gap had been "grounded against the raw C-CDA R2.1 IG text rather than a +validator run". It is measured now, and measuring it found 86 error-severity results +across the populated document the builder emits, then eight more once the measured set +covered every section it can emit rather than one populated init shape. + +`pnpm conformance` fetches the normative C-CDA R2.1 Schematron, its vocabulary file +and the CDA R2 XML schema at run time from pinned immutable references, validates +every document the builder emits against the schema and then the Schematron's +error-severity phase, re-runs the Schematron over a public sample corpus parsed and +re-serialized through this library, and writes `documentation/conformance-report.md`. +The run fails if the committed report differs from what it just produced, and the +ordinary suite fails unless the README's conformance statement agrees with the report +on the artifact revision, the document types validated, the count of error-severity +results and the count of round trips whose error sets differed. The current result is +zero and zero. + +**This is why the bump is minor rather than patch.** It adds one exported type +(`BuildCcdaSelfCareActivity`) and one warning code (`MISSING_SELF_CARE_ACTIVITY`), and +it moves what the builder emits and therefore what a consumer reads back from it. The +emitted changes: the patient carries `raceCode` and `ethnicGroupCode` as +`nullFlavor="UNK"`; the device author carries a `representedOrganization`; a CCD +carries `documentationOf/serviceEvent`; a Referral Note carries an +`informationRecipient`; an empty required section declares the entries-required +`templateId` alongside the entries-optional one; both Concern Acts carry the `CONC` +code the guide fixes and the CDA R2 `Act` sequence makes mandatory; a Reaction +Observation carries an `id`; a no-known-allergy entry carries the substance +participant with a `nullFlavor="NA"` code; the Vital Signs Organizer carries the +LOINC code its 2.1-only branch requires rather than the SNOMED one; an Immunization +Activity states `negationInd` on both arms; a Result Organizer emits its +`effectiveTime` as an interval; and an act-variant procedure carries the +`effectiveTime` that variant alone requires at error severity. + +Two reads change with them. A no-known-allergy entry's `allergen` is now present and +explicitly `nullFlavor="NA"` rather than absent, which is what an entry from any other +system has always parsed to here; `noKnownAllergy` and `negated` are unchanged and +there is no code to mistake for an allergen. An administered immunization now reads +back `refused: false` rather than `undefined`, because the document states it. + +Widening the measured set from one populated init shape to every section the builder +emits found eight more, and closing them moved five more things. The Age Observation's +fixed `code` is SNOMED CT `445518008` "Age At Onset" rather than `397659008` "Age"; the +Family History Death Observation's fixed `value` is `419099009` "Dead" rather than +`419620001` "Death"; a Family History Observation's fixed `code` carries the LOINC +`75315-2` translation its template requires; a Mental Status Observation's fixed `code` +carries the LOINC `75275-8` translation its template requires; and a Functional Status +Organizer is written only when it carries the Self-Care Activities (ADL and IADL) +observation the template SHALL contain. + +That last one is the only change a caller has to do anything about. +`BuildCcdaFunctionalStatusOrganizer` takes `selfCareActivities`, each written as a +`…22.4.128` observation and read back as an ordinary functional-status finding. Supply +none and the organizer is not written: its findings go out as standalone Functional +Status Observations, every one of them present and unchanged, and the returned document +carries `MISSING_SELF_CARE_ACTIVITY` saying the grouping, its categorization code and +its `effectiveTime` were dropped and why. Fabricating an all-`nullFlavor` activity would +assert an assessment nobody performed, and writing the organizer anyway would stamp a +template the document does not satisfy. + +The XML schema the harness validates against is the SDTC-extended tree at the same +pinned commit rather than `schema/normative`, because the builder emits the HL7-approved +`sdtc:deceasedInd` extension and the normative schema declares no extension elements. + +Every fix keeps the never-invent-content rule: a `SHALL` slot with no known value is +`nullFlavor`, and no clinical time, dose, route or code is invented to satisfy a +cardinality. + +**What the measurement does not cover**, stated where the capability is rather than +below it: the two document types the builder emits and not the other ten, the +error-severity phase and not the warning phase, value-set membership only where the +Schematron's own vocabulary file checks it, and a round trip that is differential +rather than absolute. It is an assessment against a published artifact, not a +certification, and no accredited body has reviewed it. The harness needs network +egress and fails explicitly without it; there is no offline pass. Nothing it fetches +is committed or ever sits in a tracked path. + +Two development dependencies, neither reaching the runtime set: `xmllint-wasm` for +XML Schema validation and `xpath` for XPath 1.0 evaluation. diff --git a/.changeset/eighty-jars-argue.md b/.changeset/eighty-jars-argue.md new file mode 100644 index 0000000..e123487 --- /dev/null +++ b/.changeset/eighty-jars-argue.md @@ -0,0 +1,34 @@ +--- +"@cosyte/ccda": patch +--- + +docs: cover the profile, terminology and conformance surfaces, and gate the bundle + +The published narrative documentation gained a page and a guard. Three surfaces +that a caller has to configure correctly were described only in passing inside +the limitations page, with nothing a reader could copy and run: the vendor +profile system, the bring-your-own `TerminologyAdapter`, and the required-section +conformance status. Each now has its own section on a new "Conformance, profiles +& terminology" page with an executable example, and every one of those examples +is compiled and run against the built package by the existing doc/code agreement +suite, so a documented call that stops working fails the build rather than +misleading a reader. The terminology example uses an in-process stub adapter: it +opens no socket and needs no licensed terminology service, which is also what +makes it a usable template for testing an adapter of your own. + +Three pages claimed the package was "published on npm at `0.0.3`" while it was +twelve patches past that. No page names a published version now; they point at +`npm view @cosyte/ccda version`, which cannot go stale. Every historical note +that dates a behaviour change to a past version is untouched and is now held in +place by a retention floor, because those sentences are the change record for a +reader pinned to an older version, not staleness. + +Page frontmatter was inconsistent (`sidebar_label` on five pages of nine) and +`sidebar_position` collided across five pages. Every page now carries the same +four keys, and each position agrees with the page's place in `sidebars.json` +rather than being a second, disagreeing answer to what comes next. + +A new guard holds all of it: every symbol the public entry point exports, types +included, must be named by a page or carry a stated reason in a committed +exemption record that cannot outlive the symbol it excuses. The formatter now +covers the bundle as well, so the pages cannot drift out of format. diff --git a/.changeset/first-minor-release.md b/.changeset/first-minor-release.md new file mode 100644 index 0000000..1b1df64 --- /dev/null +++ b/.changeset/first-minor-release.md @@ -0,0 +1,33 @@ +--- +"@cosyte/ccda": minor +--- + +**This is 0.1.0, the first release whose public API we treat as settled.** + +What is covered, and what you can build against: + +- Reading C-CDA R2.1 documents: all twelve US Realm document types are recognized, the header and + the patient are typed, and fourteen entry families are extracted (problems, medications, + allergies, results, vital signs, immunizations, procedures, encounters, smoking status, plan of + treatment, functional status, mental status, family history and past medical history), each coded + value keeping the code system it was sent with. +- Lenient parsing with stable warning codes and a `{ strict: true }` mode that refuses the first + tolerated deviation; hostile XML (a DTD, entity expansion, an oversized or over-deep document) is + always refused. +- Building a spec-clean CCD, Referral Note or inpatient Discharge Summary with `buildCcda`, editing + a parsed document section by section with `editCcda` (which stamps a CDA R2 revision), and + re-serializing a parsed document, where parse and serialize are a fixed point. +- Required-section validation that reports, for every document type, how much of its obligation it + asserts, and UCUM checks on the quantities in results and vital signs. + +What the version promises. The exported names, options, return shapes and warning codes are the +surface we keep stable. While the package is below 1.0, a breaking change bumps the minor version +(0.1 to 0.2) and is called out in this changelog with its migration; a fix that changes no public +value ships as a patch. + +What is not covered yet. `buildCcda` emits three of the twelve document types and throws for the +other nine. Editing replaces or adds whole sections only: no entry-level append and no section +removal. Required-section validation under-warns for most document types, so a quiet parse is not a +conformance result. A terminology adapter is consulted at five coded slots only, and this package +ships value set identifiers, never their member codes. UCUM checking is grammatical over a curated +set of atoms. diff --git a/.changeset/lucky-pandas-refuse.md b/.changeset/lucky-pandas-refuse.md new file mode 100644 index 0000000..b5412fe --- /dev/null +++ b/.changeset/lucky-pandas-refuse.md @@ -0,0 +1,50 @@ +--- +"@cosyte/ccda": minor +--- + +required sections: every document type now reports how far it was verified + +Six of the twelve required-section (SHALL) tables asserted nothing, and nothing +in the API said whether that meant "no requirement", "not checked yet" or "the +requirement is unassertable". A consumer could not tell a verified all-clear from +an unverified silence, and only the prose warned them not to. + +Five of those six were read off the normative C-CDA R2.1 base implementation +guide's document-level conformance statements. Consultation Note now asserts +History of Present Illness (CONF:1198-28907), Allergies and Intolerances +(-28911) and Problems (-28929); each is scoped to the R2.1 `@extension` stamp its +constraint's rule context carries, so an R1.1-origin document is asserted exactly +as before. Progress Note, Procedure Note, Operative Note and Diagnostic Imaging +Report assert nothing still, and that is now a traced result rather than an +unread table: every SHALL section their templates name is either outside this +parser's recognized catalog or a choice, and each is reported by name with the +reason. Unstructured Document carries no `structuredBody` at all (its component +SHALL be a `nonXMLBody`, CONF:1198-31086), so it has no section obligation. + +The addition is **additive**: nothing is removed, narrowed or renamed. +`requiredSectionKeys` and `missingRequiredSections` keep their signatures and +their shape, and the six document types that already asserted keys (CCD, +Discharge Summary, Referral Note, History and Physical, Care Plan, Transfer +Summary) return exactly the same values as before, including under the unstamped +reading, with every conformance id they already carried left in place. + +New exports: + +- `requiredSectionStatus(documentType, options?)`: the asserted `keys` plus a + `verification` of `traced-complete`, `traced-partial`, `untraced` or + `not-applicable`, the `traced` provenance (conformance id and the source's own + section name) behind each traced key, and every SHALL section left + `unasserted` with the reason it is unassertable. +- `requiredSectionStatuses(options?)`: all twelve, so the whole picture is + enumerable by a consumer. +- `DOCUMENT_TYPES`: the runtime enumeration behind the `DocumentType` union. +- The types `RequiredSectionStatus`, `RequiredSectionVerification`, + `TracedRequiredSection`, `UnassertedRequiredSection` and + `UnassertedSectionReason`. + +The only behaviour change is the one the trace bought: a Consultation Note that +carries the R2.1 stamp and omits History of Present Illness, Allergies or +Problems now draws `REQUIRED_SECTION_MISSING` (a warning, never a fatal) where it +previously parsed silent. A traced state is a claim about what was read for that +type, so the six types nobody re-read report `untraced` whether or not their keys +already cite a conformance id. diff --git a/.changeset/olive-bats-report.md b/.changeset/olive-bats-report.md new file mode 100644 index 0000000..4b2f204 --- /dev/null +++ b/.changeset/olive-bats-report.md @@ -0,0 +1,48 @@ +--- +"@cosyte/ccda": minor +--- + +feat: report a coded value that sits outside the value set its slot is bound to + +A consumer could learn that a coded value's code system was unexpected, and, with a terminology +adapter, that its code was not a real member of that system. Neither question is the one a +certifier asks. C-CDA binds each checked slot to a **value set**, and where that binding is +Required the standard states membership as a SHALL, so a document can carry the right system and a +real code and still be non-conformant at a safety-critical slot. Nothing in this package could see +that case. + +`parseCcda(xml, { valueSets })` now takes a bring-your-own `ValueSetSource` beside the existing +`terminology` adapter. It answers one question, `isMember`, and it declares the `release` of the +package it answers from. A code the source reports outside a Required binding's value set raises +`VALUE_SET_BINDING_VIOLATED`; a value set the source holds no expansion for raises +`VALUE_SET_BINDING_NOT_EVALUATED`, never silence, because a skipped value set reads exactly like a +clean one; returning `undefined` is "no opinion" and is silent. An exception your source raises +reaches you rather than being swallowed into a document that merely looks checked. + +**This package ships value set identifiers and never their member codes.** Expansions are licensed +data. `valueSetBinding(slot)` and `valueSetBindings()` expose the declaration: for each of the five +checked slots, the bound value set's OID, whether C-CDA R2.1 binds it as `required` or `preferred`, +and the conformance statement, template and artifact revision the row was read from. Every row was +read off the normative C-CDA R2.1 Schematron this repository already pins by commit and digest, so +no binding is asserted that the normative source was not read for. + +**Four of the five bindings are Required and one is Preferred, and the difference is reported, not +rounded off.** A Preferred binding never produces the Required-binding finding: the Problem +Observation's value SHALL be present and its code only SHOULD come from the Problem value set, and +reporting a departure from that as a SHALL violation would turn a conformant problem list into a +defect. Binding strength is the verb attached to "be selected from", not the cardinality beside it: +a `routeCode` a template only SHOULD contain, whose code SHALL come from the route value set, is a +Required binding. + +Nothing a parse returns changes. No value is refused, rewritten, reordered or dropped on a negative +verdict, the document re-serializes identically, and a parse that supplies no source produces +exactly the findings it always did: membership is never inferred from the code system alone. + +**This is why the bump is minor rather than patch.** It adds two warning codes and eleven exported +symbols (`ValueSetSource`, `ValueSetMembershipQuery`, `ValueSetMembershipAnswer`, `ValueSetBinding`, +`ValueSetBindingStrength`, `ValueSetBindingSource`, `BoundValueSet`, `valueSetBinding`, +`valueSetBindings`, `valueSetBindingViolated`, `valueSetBindingNotEvaluated`), and renames, removes +and repurposes nothing. `CcdaWarning` gains two optional fields, `valueSet` and `valueSetRelease`, +carried only by the two new codes: a message still comes whole from the frozen registry, so the +value set OID and the release ride as data rather than as interpolated text, and the coded value +that caused the finding appears nowhere in it. diff --git a/.changeset/olive-otters-gather.md b/.changeset/olive-otters-gather.md new file mode 100644 index 0000000..84341af --- /dev/null +++ b/.changeset/olive-otters-gather.md @@ -0,0 +1,36 @@ +--- +"@cosyte/ccda": patch +--- + +Bring this repo to the shared package baseline: install hardening, the required +js-yaml override, and a CLAUDE.md under its line ceiling + +Three drifts the meta-repo's `config/drift-manifest.json` measures against this +repo are closed, and nothing else changes. + +`pnpm-workspace.yaml` is new and carries `minimumReleaseAge: 1440` and +`trustPolicy: no-downgrade`, the publication cooldown and trust policy the +standard requires of every `@cosyte/*` package repo. Both keys are switched off +by default in pnpm 10, and both are ignored outright by a pnpm older than +10.16.0 and 10.21.0 respectively, so `packageManager` is raised from `pnpm@10.0.0` +to `pnpm@10.34.5`: a settings file the pinned package manager ignores decorates +rather than defends. A cold-store `pnpm install --frozen-lockfile` was measured +against the new policy and raised no `ERR_PNPM_TRUST_DOWNGRADE`, so no +`trustPolicyExclude` entry is carried and the exemption lists are absent rather +than empty. + +`pnpm.overrides` gains the required `js-yaml@>=4.0.0 <4.3.0` entry. It is listed +BEFORE this repo's own `js-yaml@>=4.0.0 <4.3.2` entry on purpose: pnpm applies +the LAST matching override, measured on this tree, and the other order resolved +js-yaml 4.x down from 4.3.2 to 4.3.0, which would have been an advisory +remediation quietly undone by a compliance edit. The lockfile still resolves +js-yaml at 4.3.2, and no existing override was removed, narrowed or repointed. + +`CLAUDE.md` goes from 459 lines to 232, under the ceiling of 300 the standard +declares, entirely by RELOCATION. Every block of prose that left it is +reproduced verbatim in `documentation/agent-notes.md`, under the very heading +the imperative's `Why:` pointer already resolved to, and what is left in +`CLAUDE.md` is the rule plus that pointer. Nothing was deleted, no imperative +lost its file, and no claim was reworded, softened or strengthened. + +No published API, warning code, parser behaviour or emitted XML changes. diff --git a/.changeset/olive-pumas-greet.md b/.changeset/olive-pumas-greet.md new file mode 100644 index 0000000..efe8f35 --- /dev/null +++ b/.changeset/olive-pumas-greet.md @@ -0,0 +1,39 @@ +--- +"@cosyte/ccda": patch +--- + +README: standardize the page, and make its usage example a test + +The npm package page and the GitHub landing page are the same file, and for most +consumers it is the only page they will ever read. It was 1086 lines of deep +reference with no answer above the fold to the three questions a reader arrives +with: what is this for, is it safe to depend on, and what does it do with my +patients' data. It also carried a status line claiming the package was published +at `0.0.3`, twelve patch releases out of date, and a hand-written `alt` string on +the banner that disagreed with the one the brand assets declare for that tile. + +The page now opens with the banner (carrying the declared `alt` string), the +title, a one-line hook, the four house badges and the `package.json` +`description` verbatim, followed by a table of contents. Four sections are new: + +- **Why this exists**, which names the problem, the nearest alternative a reader + would otherwise reach for, and why this is not that. +- **Status**, which states the version this package declares, says what that + version does and does not claim about the public API, and names the surfaces + that are still moving instead of implying they are settled. +- **PHI and safety**, which states what the library does with patient data for + each of logging, in-memory retention and writing to disk, and names what the + consuming application still owns. +- **Contributing**, which says where to ask, whether unsolicited pull requests + are accepted, and every check a contribution has to clear. + +`Install` now states the Node engine floor and the module format. The parse +walkthrough is consolidated into a single `Usage` example that builds a complete +synthetic CCD, reads it back and shows what every call returns, and that block is +now executed by the test suite against the built entry point: an example whose +shown output stops matching what the code produces fails the build. Roughly half +of documentation traffic is agents that lift a usage block verbatim, so a wrong +example is wrong generated code at scale. + +No parse, emit, builder or profile behaviour changes, and no documented behaviour +was dropped: every reference section the page carried is still on it. diff --git a/.changeset/pink-cobras-invent.md b/.changeset/pink-cobras-invent.md new file mode 100644 index 0000000..92e3e53 --- /dev/null +++ b/.changeset/pink-cobras-invent.md @@ -0,0 +1,65 @@ +--- +"@cosyte/ccda": minor +--- + +required sections: a SHALL warning fires exactly where the normative source says it should + +The per-document-type required-section (SHALL) tables now cover all twelve +recognized types. Six of them (CCD, Discharge Summary, Referral Note, History and +Physical, Care Plan, Transfer Summary) had asserted sets that predated anyone +reading the normative source for them, and reported `verification: "untraced"` to +say so. Their document-level `errors` and `warnings` rules have been read off the +HL7 C-CDA R2.1 normative Schematron and the tables corrected. **No recognized +document type reports `untraced` any more.** + +Two defects went in opposite directions and both are closed. + +- **A conformant Discharge Summary drew a false warning.** The table asserted a + Discharge Medications section as a SHALL. The normative source states it as a + **SHOULD**, in that document's warnings rule (CONF:1198-30525), so a conformant + Discharge Summary that omits the section was drawing a + `REQUIRED_SECTION_MISSING` it had not earned. It is withdrawn, in the stamped + and the unstamped reading alike. +- **A non-conformant document passed quietly.** The same Discharge Summary table + omitted Plan of Treatment (CONF:1198-30528), which the errors rule requires + unconditionally; a History and Physical asserted one of the ten SHALL sections + its errors rule names; a Transfer Summary asserted three of six. Every one of + those sections that this parser's catalog recognizes is asserted now. + +**What changed on the published surface**, for `requiredSectionKeys`, +`missingRequiredSections`, `requiredSectionStatus` and the parser warnings that +follow from them: + +- **Discharge Summary**: `dischargeMedications` withdrawn, `planOfTreatment` + added. +- **History and Physical**: `familyHistory`, `pastMedicalHistory`, `medications`, + `results`, `socialHistory` and `vitalSigns` added beside `allergies`. +- **Transfer Summary**: `results`, `vitalSigns` and `reasonForReferral` added. +- **CCD, Referral Note and Care Plan**: confirmed unchanged. + +Expect a document of the three changed types to draw warnings it did not draw +before, and a conformant Discharge Summary with no Discharge Medications section +to fall silent. `REQUIRED_SECTION_MISSING` remains a Tier-2 warning, never a +fatal, and remains safety-critical, so no vendor profile can quiet it. + +**Every key an added rule contributed is scoped to the R2.1 stamp**, because each +of those document-level rules matches only a `ClinicalDocument` whose +`templateId` carries `@extension="2015-08-01"`. An unstamped, R1.1-origin document +is therefore asserted exactly as it was before, with one exception in the safe +direction: a key withdrawn because the source states it as a SHOULD or as a choice +is withdrawn from the unstamped reading too, since "no sentence made this +unconditional" does not depend on a version stamp. + +**Provenance is on the surface, not in a commit message.** Every asserted key now +carries the conformance statement it was read from and the source's own name for +the section; every SHALL section the source names that this package cannot assert +is enumerated with its reason (`outside-section-catalog` for Hospital Course, +General Status, Physical Exam and Review of Systems; `not-unconditionally-required` +for each choice); and `requiredSectionStatus(type).source` names the normative +artifact and **that artifact's own revision date**, so a reader holding a later +revision can tell a table is stale without re-deriving it. The new +`RequiredSectionSource` type is exported alongside it. + +This is document-level validation only: which sections a document type SHALL +contain, never what a section or an entry SHALL contain. A quiet parse is still +not a conformance result. diff --git a/.changeset/plenty-donuts-attack.md b/.changeset/plenty-donuts-attack.md new file mode 100644 index 0000000..a14efc1 --- /dev/null +++ b/.changeset/plenty-donuts-attack.md @@ -0,0 +1,31 @@ +--- +"@cosyte/ccda": patch +--- + +phi-scan: a target enumerated and never read refuses the scan + +`--allow-fixture` withdrew a path from the target list and the run reported on +whatever remained, so the withdrawal never showed up in the verdict: four argv +shapes printed `OK, no hits` at exit 0 over a corpus holding a live, detectable +violator, including a whole-run sweep whose only violator was the withdrawn file. +CI could print a clean verdict over a corpus it never opened. + +The scanner now refuses (exit 2), in every mode, over any target this run +enumerated and never read, naming the paths. The comparison is a set difference +rather than a count, because a count counts the targets that did get read. A +bypass naming a path the run does not enumerate refuses too, under its own +message, since such a flag subtracts nothing (in `paths` mode a nonexistent path +is refused earlier still, by `File not found`). `--allow-fixture` no longer +selects the mode and is unioned into the target list in `paths` mode, so it means +the same thing in every argv. + +What it costs, stated rather than left to be discovered: `--allow-fixture` can no +longer reach exit 0 in any mode. The flag, `phi-scan-overrides.md` and the log +gate all stay, so an attempt is recorded and then refused rather than silently +honored, and `scripts/phi-allow-list.txt` is now the only mechanism that reaches +a clean run. The hit footer no longer advertises the flag as a remedy. + +No published API, warning code or parser behaviour changes; this is the commit +gate only. There was no real unread corpus in this repo: the all-mode sweep +enumerates and reads the same set, and the four states were reproduced with +planted fixtures rather than found. diff --git a/.changeset/quiet-jars-repeat.md b/.changeset/quiet-jars-repeat.md new file mode 100644 index 0000000..67de4ab --- /dev/null +++ b/.changeset/quiet-jars-repeat.md @@ -0,0 +1,74 @@ +--- +"@cosyte/ccda": minor +--- + +version stamps: a document says which C-CDA release it was written for + +C-CDA 3.0.0 gave every document template a new `@extension`, `2024-05-01`, and +4.0.0 and 5.0.0 kept it, so a document written for a release later than R2.1 is +detectable from the `templateId` that resolves its type. Through `0.0.15` this +parser got that case wrong twice, once loudly and once in silence. + +Loudly: any `@extension` that was not `2015-08-01` drew +`TEMPLATE_EXTENSION_ABSENT`, whose frozen message reads "carries no @extension +version stamp ... (may pre-date R2.1)". For a `2024-05-01` document both halves +are false, because it carries a stamp and it post-dates R2.1. + +Silently, and worse: the same path reported the document as unstamped, and the +required-section tables read that as the R1.1-origin **reduction**, which drops +every R2.1-stamp-scoped SHALL key. A post-R2.1 CCD carrying neither Social +History nor Vital Signs therefore drew no `REQUIRED_SECTION_MISSING` at all. The +library had classified a document from the future as one from the past. + +**Two warning codes are added and none is renamed or removed.** +`TEMPLATE_EXTENSION_UNMODELED_RELEASE` fires when the resolving `templateId` +carries an `@extension` that is present and is not the R2.1 stamp; +`REQUIRED_SECTIONS_NOT_EVALUATED` fires once for such a document to say its SHALL +obligation was not computed. `TEMPLATE_EXTENSION_ABSENT` keeps its narrower +meaning ("no `@extension` at all", the R1.1-origin shape) and its registry message +byte for byte. + +**An unmodelled stamp is reported, never reduced.** Such a document draws no +`REQUIRED_SECTION_MISSING` in either reading, and +`requiredSectionStatus(type, { stamp: "unmodeled-release" })` reports +`evaluation: "not-evaluated"` so an empty key set still says which emptiness it +is. `evaluation` is a new field on `RequiredSectionStatus` and a separate axis +from `verification`, which remains a claim about the document type that no option +moves. + +**The release this package validates against is an exported value now.** +`CCDA_CONFORMANCE_RELEASE` is `"R2.1"`, beside the closed table of stamps this +package can name (`CCDA_RELEASE_STAMPS`, plus `R30_EXTENSION`, +`releaseForTemplateExtension` and the three-state `readTemplateStamp`). **This +does not retarget the library**: regulation still adopts R2.1 and SVAP use is +voluntary, so a later release is recognized and named, never read against. +4.0.0 also relaxed the US Realm Header and 5.0.0 added a Pregnancy Section, and +neither is modelled here. + +Four things are decisions rather than details: + +- **A reported stamp comes from a closed set of literals this package owns.** A + message names a member of `CCDA_RELEASE_STAMPS` or names no stamp at all, so a + sender-controlled `@extension` can never reach a `CcdaWarning.message` (or, + under `{ strict: true }`, a thrown error's stack). No factory gained a + parameter that a message interpolates. +- **The existential R2.1 rule is unchanged and beats a later stamp.** A document + carrying the resolving root twice, once stamped `2015-08-01` and once for a + later release, is inside the Schematron rule's context and is evaluated under + the full R2.1 obligation, in either sibling order. +- **`{ r21Stamped }` behaves exactly as it did.** + `requiredSectionKeys("ccd", { r21Stamped: false })` returns the same four keys + in the same order. A boolean cannot express three states, so the three-state + `{ stamp }` was added beside it rather than repurposing a published option. +- **`legacyR11` does not tolerate the new code.** It is receive-tolerance for + documents from the past, grounded in the receive-both-R2.1-and-R1.1 + requirement; quieting a future-release stamp there would restore the silence + this change removes. Neither new code is safety-critical, so a consumer who has + read the later guide can still write their own profile for it. + +Nothing became stricter. No fatal was added, no content the C-CDA open-template +rule permits is refused, and a post-R2.1 document's clinical reading (patient, +sections, entries, values) and its byte-faithful re-serialization are identical +to the same document stamped `2015-08-01`. Under `{ strict: true }` the new +warnings escalate like every other Tier-2 warning, which is pre-existing +caller-opted behaviour. diff --git a/.changeset/rare-owls-listen.md b/.changeset/rare-owls-listen.md new file mode 100644 index 0000000..0556a61 --- /dev/null +++ b/.changeset/rare-owls-listen.md @@ -0,0 +1,69 @@ +--- +"@cosyte/ccda": minor +--- + +subject override: an entry is never read as the patient's unless it is + +CDA R2 gives `Section.subject` cardinality `0..1` and defines it as the "primary +target of the entries recorded in a section"; C-CDA admits the same override on a +clinical statement. So a conformant document can carry a relative's, a donor's or +a contact's clinical statement inside the patient's document. This parser read +`` in exactly one place (a Family History Organizer's `relatedSubject`) +and every other extractor handed such an entry back as the patient's, silently. +That is another person's clinical data attributed to this patient, in a read path +documented as the patient's own. + +A top-level `` that a subject declaration governs is now **withheld** from +every record-target read path and flagged with a new safety-critical Tier-2 +warning, `SUBJECT_CONTEXT_OVERRIDE`, which no vendor profile may tolerate. An +entry is governed when it carries a declaration itself, when a clinical statement +nested inside it carries one, or when an enclosing section carries one (the +nearest enclosing declaration wins, which is the standard's own conduction rule). + +Four things are decisions rather than details: + +- **Presence is the trigger.** A declaration is an override whatever it names. + Nothing is compared with the record target and nothing is resolved to the + patient, because that would make a clinical answer depend on vendor identifier + hygiene. A document that redundantly restates the patient as an entry subject + therefore loses those entries from the record-target read paths and gains a + warning for each: the safe direction of the error, and the accepted cost. +- **The whole top-level entry is withheld**, its own statement and every statement + nested inside it, and the same unit does the counting. A declaration on the + second Problem Observation of a Problem Concern Act withholds that whole concern + act and emits one warning at the act's locus, never a concern act handed back + one observation short. +- **Family History is untouched.** A Family History Organizer's own subject slot + is that template's mechanism for naming the relative, whatever it contains, so + it is never an override, draws no warning, and re-overrides an enclosing section + declaration. `getFamilyHistory()`, the `familyHistory` field, + `extractFamilyHistory` and the aggregate's family-history slot return exactly + what they returned before, in every document shape. That carve-out is read-side + and reaches only the organizer the family-history path itself reads: an entry + that carries the Family History Organizer template beside one a record-target + read path returns (a Result Organizer, a Problem Concern Act) is withheld and + reported like any other, because a `templateId` is one element and C-CDA entries + carry several. A declaration nested deeper inside an entry is never that slot. +- **Withholding is read-side only.** The withheld entry is not dropped: the + byte-faithful round trip through `toString()` / `serializeCcda` reproduces it + unchanged, and section narrative is returned unredacted, unreordered and + unannotated. + +The count is per section and sums over the document: N governed top-level entries +in a section produce exactly N instances, in document order, each naming that +entry's own bounded locus (an element path from the CDA vocabulary, the section's +LOINC code, line and column, with `` for anything that fails its bound), +and a section that declares an override but governs no entry anywhere beneath it +produces exactly one instance at its own locus instead. The same withholding +applies to a per-family or aggregate extraction invoked directly on a section, +including a nested subsection governed by an ancestor's declaration, with the +warnings delivered on the parse context those functions already accept: no +signature, parameter or return type changes. `extractFamilyHistory` delivers them +too, while returning its own contents whole. + +Two behaviour changes to expect. A document carrying an override loses those +entries from `getProblems()` / `getMedications()` / `getAllergies()` and the other +ten record-target families where it previously returned them as the patient's, +and under `{ strict: true }` it now throws, because the new code escalates like +every other safety-critical Tier-2 warning. Nothing is modelled for third-party +subjects: a withheld entry is reachable only through the re-serialized document. diff --git a/.changeset/short-hounds-author.md b/.changeset/short-hounds-author.md new file mode 100644 index 0000000..d080094 --- /dev/null +++ b/.changeset/short-hounds-author.md @@ -0,0 +1,70 @@ +--- +"@cosyte/ccda": minor +--- + +feat: read the header participations, so a parsed document can say who authored it + +A consumer could ask whose document it was and what was in it, but not who wrote it. +`CcdaHeader` carried ten fields and not one of them was a participation. Every +conforming US Realm document carries at least one `author` and exactly one `custodian`, +and an inpatient Discharge Summary carries the encounter it summarises, so the parser +was dropping data the source document was required to supply. Provenance is the answer +to "who said this", which is what a clinician asks of a medication list assembled by +three systems. + +`header.authorship` carries every document-level `` in document order: its +identifiers, its assigned person's name or its assigned authoring device's identity, +the organization the document states it acted for, and its author `time` at exactly the +precision the document stated. A partial author time stays partial. `header.custodian` +carries the custodian organization with its identifiers and its name. +`header.encompassingEncounter` carries the `componentOf/encompassingEncounter` frame: +its `effectiveTime` bounds, each keeping its own precision and any `nullFlavor`, and its +`dischargeDispositionCode`. + +**An inherited author reading is marked, never asserted.** CDA conducts an author down +from the document to a section and on to that section's entries, and this reports the +conduction rather than performing it silently. `CcdaSection.authorship` and each entry's +reading in `CcdaSection.entryAuthorship` carry an `inherited` flag: `false` when the +level stated these authors itself, `true` when it stated none and these are the nearest +enclosing level's. "The nearest enclosing author is this clinician" and "this entry was +authored by this clinician" are different claims and only the first is one the document +made. When no level carries an author the reading is absent at every level: the record +target, the custodian, a legal authenticator and an informant are never read as the +author, and no `componentOf` means no encounter frame rather than one derived from the +document `effectiveTime` or a `documentationOf` service event. + +One warning code is added and none is renamed, removed or repurposed. An `` +whose `assignedAuthor` carries neither an `assignedPerson` nor an +`assignedAuthoringDevice`, which the US Realm Header requires one of (CONF:1198-8456), +is kept, marked `unidentified`, and reported with `UNIDENTIFIED_AUTHOR`. It still +conducts to nested levels. Dropping it would turn "the document names an author whose +identity it never states" into "the document names no author", which is a more +reassuring claim than the document supports. The factory takes only a position and the +message comes whole from the frozen registry, so a clinician's name cannot reach a +diagnostic. + +Entries an overriding `` declaration governs are absent from `entryAuthorship` +entirely, not even their `ids`: it is a record-target read path, and a governed entry +appearing in one would put another person's entry back on the model through a second +door. + +**This is why the bump is minor rather than patch.** It adds seven exported types +(`CcdaAuthor`, `CcdaAuthoringDevice`, `CcdaAuthorship`, `CcdaCustodian`, +`CcdaEncompassingEncounter`, `CcdaEntryAuthorship`, `CcdaOrganization`) and one warning +code. `buildSection` takes an optional third argument, the enclosing author reading; +every existing call keeps working and reads the section as having no enclosing author, +which is what a caller framing a detached `
` is looking at. + +**Nothing here is breaking, and the new `CcdaSection` field is optional so that stays +true.** `CcdaSection` is an input surface as well as an output one, reachable through +`CcdaDocumentInit.sections`, which the public `CcdaDocument` constructor takes: a +required `entryAuthorship` would stop a consumer's existing section literal from +compiling, which is not a minor change. The parser populates the field on every section +it frames, empty where the section has no entry act to read. + +Framing a section reads each entry act's ``s without reporting on them, because the +entry-extraction walk already parses those same elements. No document's warning output +moves: not the count, not the order, and not whether `strict: true` throws. + +This is the READ side only. Nothing the builder emits changes, and emitting +`componentOf` is not part of it. diff --git a/.changeset/spotty-cases-admit.md b/.changeset/spotty-cases-admit.md new file mode 100644 index 0000000..09d3a16 --- /dev/null +++ b/.changeset/spotty-cases-admit.md @@ -0,0 +1,70 @@ +--- +"@cosyte/ccda": minor +--- + +Build an inpatient Discharge Summary, the third of the twelve document types + +`buildCcda` emits a Discharge Summary for `documentType: "dischargeSummary"`, +alongside the CCD and the Referral Note. `minor` rather than `patch` because the +package entry point gains two exported types: `BuildableDocumentType` (the union +of the three types the builder emits) and `BuildCcdaEncompassingEncounter`, and +the published `UnassertedSectionReason` union gains a third member, +`"assertion-would-tighten-parse"`. Nothing published was removed or renamed, and a +CCD or a Referral Note built from an init that names no Discharge Summary input +does not change by a byte. + +The document specializes the US Realm header with its own document `templateId` +root (`2.16.840.1.113883.10.20.22.1.8`, stamped `2015-08-01`), its LOINC document +code (`18842-5`), and the four SHALL sections its errors rule names: Allergies, +the narrative-only Hospital Course (`1.3.6.1.4.1.19376.1.5.3.1.3.5`, LOINC +`8648-8`), Discharge Diagnosis (V3) and Plan of Treatment (V2). Neither Problems +nor Medications is in that set, so unlike a CCD or a Referral Note a Discharge +Summary emits them only when you supply content, the medications in the +Medications Section; Discharge Medications is a SHOULD in the document's warnings +rule and is not emitted at all. Three new inputs carry the content: +`hospitalCourse`, `dischargeDiagnoses` and `encompassingEncounter`. + +**What you supply is emitted or refused, never dropped.** Those three inputs throw +a `TypeError` on a CCD or a Referral Note instead of being ignored, and +`assessment` and `reasonForReferral`, the Referral Note's narratives, throw on a +Discharge Summary. A CCD still ignores `assessment` and `reasonForReferral`, as it +always has. + +It is the only type carrying `componentOf/encompassingEncounter`, the frame +naming the stay. Every slot in it is required by the template and none is ever +invented: an encounter period bound or a discharge disposition you did not supply +is emitted as an explicit `nullFlavor="UNK"`, which satisfies the cardinality, +says the fact is unknown rather than stating one, and reparses as absent. None of +the three is ever derived from the document `effectiveTime` or from a +`documentationOf` service event. A supplied bound keeps exactly the precision you +gave it. The disposition's `codeSystem` defaults to the NUBC UB-04 Patient +Discharge Status code set, the system every member of the value set the template +names carries, and your own `codeSystem` is emitted verbatim instead when you +supply one. + +**All three types reparse with zero warnings.** The Hospital Course Section is an +IHE PCC template outside the section catalog; `parseCcda` now recognizes it by its +root (`findSection("hospitalCourse")` resolves), in every document and not only a +built one, so a section carrying that root and matching nothing in the catalog no +longer raises `UNKNOWN_SECTION_CODE`. That is the only parse output that moves: +the root is consulted after every catalog root and the LOINC fallback, so no +section changes key; the LOINC code `8648-8` alone still raises +`UNKNOWN_SECTION_CODE`; no entry in such a section is newly flagged as misplaced; +and the section is not asserted as required, so no Discharge Summary that omits it +gains a `REQUIRED_SECTION_MISSING`. `requiredSectionStatus("dischargeSummary")` +names it as unasserted with the new reason `"assertion-would-tighten-parse"`. + +Asking for one of the other nine types still throws a `TypeError` naming what you +asked for. That refusal is now derived from the builder's own specialization +table rather than from a list of strings, so a thirteenth recognized type is +refused the day it is recognized, and the membership test is `Object.hasOwn` +rather than `in`, so an untyped caller passing a prototype key is refused instead +of crashing partway through a build. Only an omitted (`undefined`) `documentType` +defaults to a CCD: `null`, which used to build a CCD, is refused, and so is any +value that is not a string, before any lookup and without converting it. + +Measured, not asserted: `pnpm conformance` validates fifteen built documents, +two of them Discharge Summaries at both ends of the input range, against the CDA +R2 XML schema and the error-severity phase of the normative C-CDA R2.1 +Schematron, with zero error-severity results. The README conformance statement +and the tracked report move with it, from two of twelve to three. diff --git a/.changeset/wide-clocks-convert.md b/.changeset/wide-clocks-convert.md new file mode 100644 index 0000000..62d8ce1 --- /dev/null +++ b/.changeset/wide-clocks-convert.md @@ -0,0 +1,63 @@ +--- +"@cosyte/ccda": minor +--- + +Add the shared `toObject` / `toISO` / `toDate` conversion surface for a parsed `TS` + +Every `@cosyte/*` standard parser now exports the same three conversions, under the same +names, with the same return shapes and the same timezone rule, so moving between two of +them costs nothing to relearn. Five names are added to the package entry point: +`toObject`, `toISO`, `toDate`, and the `DateParts` and `ToDateOptions` types. + +- `toObject(ts)` returns a frozen `DateParts` carrying **only** the components the document + stated. Nothing is zero-filled, so `Object.keys()` on a year-precision value is `["year"]` + and the value's precision survives the conversion. `month` is the spec-native 1 to 12, the + component names are singular, and there is no `raw`, `precision` or `valid` key. + `millisecond` is the first three digits of the stated fraction taken verbatim and + right-padded (`.5` is 500, `.0500` is 50), never a floating-point fraction multiplied by + 1000. `offsetMinutes` is present if and only if the value carried an explicit offset, a + stated zero included. Deleting `offsetMinutes` leaves an object + `Temporal.PlainDateTime.from` and luxon's `DateTime.fromObject` accept unchanged; neither + library is a dependency and neither is imported. +- `toISO(ts)` renders ISO-8601 truncated to that same precision and pads nothing out, with + fractional digits rendered exactly as written. A stated offset is appended (`Z` for a + stated zero, otherwise `+HH:MM` / `-HH:MM`); with no stated offset nothing is appended and + no `Z` is fabricated. Because a stated zero renders `Z`, this is not a byte round-trip of + the wire value, and `serializeCcda` remains the round-tripping route. +- `toDate(ts, options?)` returns a `Date` only when the zone is determinate: the value + carried an offset, or the caller passed `assumeOffsetMinutes` (signed minutes east of UTC, + where an explicit `0` means "read this naive value as UTC"). With neither, the answer is + `undefined`. The host machine's timezone is never read and UTC is never assumed. A value's + own offset always beats an assumed one, a year below 100 stays that year rather than being + remapped into the 1900s, and components below the stated precision fill to their lowest + legal value for the instant only. An `assumeOffsetMinutes` that names no usable zone is + refused the same way: `NaN` and the two infinities are not a number of minutes, and a finite + offset large enough to push the result outside the range a JS `Date` holds denotes no + instant. Both answer `undefined`. What never comes back is an `Invalid Date`, which + satisfies the declared return type and defeats its point. +- An offset the **document** states is bounded as well, at 23 hours 59 minutes, and past that + bound the value is refused whole by all three functions. The v3 `±ZZZZ` token is four digits + wide and constrains neither field, so `+2400`, `+9999` and `-9999` are legal literals stating + 1440, 6039 and -6039 minutes: finite numbers, none of them a zone, and none of them + expressible in the shared `+HH:MM` / `-HH:MM` slot, whose `HH` is a two-digit hour of the + day. Refusing them together is what keeps `toObject` from reporting a hundred-hour zone that + `toISO` renders as `+100:39`, a string no ISO-8601 reader accepts. `@cosyte/hl7` bounds at + the same 23:59; `@cosyte/dicom` bounds tighter, at 14:59. + +None of the three ever throws, for any input: an absent value, a `TS` with no `@value`, a +`TS` whose `@nullFlavor` contradicts a populated `@value` (the grounds on which `parseTs` +already withholds `date`), and a `@value` this package parses as malformed all answer +`undefined`. + +**`TS.date` is unchanged and stays unchanged.** It remains eager: it zero-fills a truncated +value, resolves an offset-less one as if it had said UTC, and resolves an unstatable stated +offset too, so there are values where `ts.date` is a populated `Date` and `toDate(ts)` is +`undefined`, on the same object. `parseTs`, `parseV3DateTime` and the warning codes are +untouched, and an out-of-range offset raises no new warning. That +divergence is deliberate rather than an oversight, it is documented in `README.md` and in the +docs bundle, and the old answer stays reachable by asking for it with +`{ assumeOffsetMinutes: 0 }`. Nothing reading `TS.date` today reads anything different. + +Parts are derived by re-parsing `TS.raw`, the document's own bytes; `TS.date` is never read. +No dependency of any kind was added, runtime or development, and `engines.node` is unchanged +at `>=22.0.0`. diff --git a/CHANGELOG.md b/CHANGELOG.md index bd90067..1ec4731 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,726 +1,5 @@ # Changelog -## 0.1.0 - -### Minor Changes - -- 23c8223: feat: measure conformance against the normative artifacts, and fix what the measurement found - - A built document's conformance was expected rather than proven. The README said so: - the builder's gap had been "grounded against the raw C-CDA R2.1 IG text rather than a - validator run". It is measured now, and measuring it found 86 error-severity results - across the populated document the builder emits, then eight more once the measured set - covered every section it can emit rather than one populated init shape. - - `pnpm conformance` fetches the normative C-CDA R2.1 Schematron, its vocabulary file - and the CDA R2 XML schema at run time from pinned immutable references, validates - every document the builder emits against the schema and then the Schematron's - error-severity phase, re-runs the Schematron over a public sample corpus parsed and - re-serialized through this library, and writes `documentation/conformance-report.md`. - The run fails if the committed report differs from what it just produced, and the - ordinary suite fails unless the README's conformance statement agrees with the report - on the artifact revision, the document types validated, the count of error-severity - results and the count of round trips whose error sets differed. The current result is - zero and zero. - - **This is why the bump is minor rather than patch.** It adds one exported type - (`BuildCcdaSelfCareActivity`) and one warning code (`MISSING_SELF_CARE_ACTIVITY`), and - it moves what the builder emits and therefore what a consumer reads back from it. The - emitted changes: the patient carries `raceCode` and `ethnicGroupCode` as - `nullFlavor="UNK"`; the device author carries a `representedOrganization`; a CCD - carries `documentationOf/serviceEvent`; a Referral Note carries an - `informationRecipient`; an empty required section declares the entries-required - `templateId` alongside the entries-optional one; both Concern Acts carry the `CONC` - code the guide fixes and the CDA R2 `Act` sequence makes mandatory; a Reaction - Observation carries an `id`; a no-known-allergy entry carries the substance - participant with a `nullFlavor="NA"` code; the Vital Signs Organizer carries the - LOINC code its 2.1-only branch requires rather than the SNOMED one; an Immunization - Activity states `negationInd` on both arms; a Result Organizer emits its - `effectiveTime` as an interval; and an act-variant procedure carries the - `effectiveTime` that variant alone requires at error severity. - - Two reads change with them. A no-known-allergy entry's `allergen` is now present and - explicitly `nullFlavor="NA"` rather than absent, which is what an entry from any other - system has always parsed to here; `noKnownAllergy` and `negated` are unchanged and - there is no code to mistake for an allergen. An administered immunization now reads - back `refused: false` rather than `undefined`, because the document states it. - - Widening the measured set from one populated init shape to every section the builder - emits found eight more, and closing them moved five more things. The Age Observation's - fixed `code` is SNOMED CT `445518008` "Age At Onset" rather than `397659008` "Age"; the - Family History Death Observation's fixed `value` is `419099009` "Dead" rather than - `419620001` "Death"; a Family History Observation's fixed `code` carries the LOINC - `75315-2` translation its template requires; a Mental Status Observation's fixed `code` - carries the LOINC `75275-8` translation its template requires; and a Functional Status - Organizer is written only when it carries the Self-Care Activities (ADL and IADL) - observation the template SHALL contain. - - That last one is the only change a caller has to do anything about. - `BuildCcdaFunctionalStatusOrganizer` takes `selfCareActivities`, each written as a - `…22.4.128` observation and read back as an ordinary functional-status finding. Supply - none and the organizer is not written: its findings go out as standalone Functional - Status Observations, every one of them present and unchanged, and the returned document - carries `MISSING_SELF_CARE_ACTIVITY` saying the grouping, its categorization code and - its `effectiveTime` were dropped and why. Fabricating an all-`nullFlavor` activity would - assert an assessment nobody performed, and writing the organizer anyway would stamp a - template the document does not satisfy. - - The XML schema the harness validates against is the SDTC-extended tree at the same - pinned commit rather than `schema/normative`, because the builder emits the HL7-approved - `sdtc:deceasedInd` extension and the normative schema declares no extension elements. - - Every fix keeps the never-invent-content rule: a `SHALL` slot with no known value is - `nullFlavor`, and no clinical time, dose, route or code is invented to satisfy a - cardinality. - - **What the measurement does not cover**, stated where the capability is rather than - below it: the two document types the builder emits and not the other ten, the - error-severity phase and not the warning phase, value-set membership only where the - Schematron's own vocabulary file checks it, and a round trip that is differential - rather than absolute. It is an assessment against a published artifact, not a - certification, and no accredited body has reviewed it. The harness needs network - egress and fails explicitly without it; there is no offline pass. Nothing it fetches - is committed or ever sits in a tracked path. - - Two development dependencies, neither reaching the runtime set: `xmllint-wasm` for - XML Schema validation and `xpath` for XPath 1.0 evaluation. - -- e726a86: **This is 0.1.0, the first release whose public API we treat as settled.** - - What is covered, and what you can build against: - - Reading C-CDA R2.1 documents: all twelve US Realm document types are recognized, the header and - the patient are typed, and fourteen entry families are extracted (problems, medications, - allergies, results, vital signs, immunizations, procedures, encounters, smoking status, plan of - treatment, functional status, mental status, family history and past medical history), each coded - value keeping the code system it was sent with. - - Lenient parsing with stable warning codes and a `{ strict: true }` mode that refuses the first - tolerated deviation; hostile XML (a DTD, entity expansion, an oversized or over-deep document) is - always refused. - - Building a spec-clean CCD, Referral Note or inpatient Discharge Summary with `buildCcda`, editing - a parsed document section by section with `editCcda` (which stamps a CDA R2 revision), and - re-serializing a parsed document, where parse and serialize are a fixed point. - - Required-section validation that reports, for every document type, how much of its obligation it - asserts, and UCUM checks on the quantities in results and vital signs. - - What the version promises. The exported names, options, return shapes and warning codes are the - surface we keep stable. While the package is below 1.0, a breaking change bumps the minor version - (0.1 to 0.2) and is called out in this changelog with its migration; a fix that changes no public - value ships as a patch. - - What is not covered yet. `buildCcda` emits three of the twelve document types and throws for the - other nine. Editing replaces or adds whole sections only: no entry-level append and no section - removal. Required-section validation under-warns for most document types, so a quiet parse is not a - conformance result. A terminology adapter is consulted at five coded slots only, and this package - ships value set identifiers, never their member codes. UCUM checking is grammatical over a curated - set of atoms. - -- 2f90283: required sections: every document type now reports how far it was verified - - Six of the twelve required-section (SHALL) tables asserted nothing, and nothing - in the API said whether that meant "no requirement", "not checked yet" or "the - requirement is unassertable". A consumer could not tell a verified all-clear from - an unverified silence, and only the prose warned them not to. - - Five of those six were read off the normative C-CDA R2.1 base implementation - guide's document-level conformance statements. Consultation Note now asserts - History of Present Illness (CONF:1198-28907), Allergies and Intolerances - (-28911) and Problems (-28929); each is scoped to the R2.1 `@extension` stamp its - constraint's rule context carries, so an R1.1-origin document is asserted exactly - as before. Progress Note, Procedure Note, Operative Note and Diagnostic Imaging - Report assert nothing still, and that is now a traced result rather than an - unread table: every SHALL section their templates name is either outside this - parser's recognized catalog or a choice, and each is reported by name with the - reason. Unstructured Document carries no `structuredBody` at all (its component - SHALL be a `nonXMLBody`, CONF:1198-31086), so it has no section obligation. - - The addition is **additive**: nothing is removed, narrowed or renamed. - `requiredSectionKeys` and `missingRequiredSections` keep their signatures and - their shape, and the six document types that already asserted keys (CCD, - Discharge Summary, Referral Note, History and Physical, Care Plan, Transfer - Summary) return exactly the same values as before, including under the unstamped - reading, with every conformance id they already carried left in place. - - New exports: - - `requiredSectionStatus(documentType, options?)`: the asserted `keys` plus a - `verification` of `traced-complete`, `traced-partial`, `untraced` or - `not-applicable`, the `traced` provenance (conformance id and the source's own - section name) behind each traced key, and every SHALL section left - `unasserted` with the reason it is unassertable. - - `requiredSectionStatuses(options?)`: all twelve, so the whole picture is - enumerable by a consumer. - - `DOCUMENT_TYPES`: the runtime enumeration behind the `DocumentType` union. - - The types `RequiredSectionStatus`, `RequiredSectionVerification`, - `TracedRequiredSection`, `UnassertedRequiredSection` and - `UnassertedSectionReason`. - - The only behaviour change is the one the trace bought: a Consultation Note that - carries the R2.1 stamp and omits History of Present Illness, Allergies or - Problems now draws `REQUIRED_SECTION_MISSING` (a warning, never a fatal) where it - previously parsed silent. A traced state is a claim about what was read for that - type, so the six types nobody re-read report `untraced` whether or not their keys - already cite a conformance id. - -- 6a8c4f8: feat: report a coded value that sits outside the value set its slot is bound to - - A consumer could learn that a coded value's code system was unexpected, and, with a terminology - adapter, that its code was not a real member of that system. Neither question is the one a - certifier asks. C-CDA binds each checked slot to a **value set**, and where that binding is - Required the standard states membership as a SHALL, so a document can carry the right system and a - real code and still be non-conformant at a safety-critical slot. Nothing in this package could see - that case. - - `parseCcda(xml, { valueSets })` now takes a bring-your-own `ValueSetSource` beside the existing - `terminology` adapter. It answers one question, `isMember`, and it declares the `release` of the - package it answers from. A code the source reports outside a Required binding's value set raises - `VALUE_SET_BINDING_VIOLATED`; a value set the source holds no expansion for raises - `VALUE_SET_BINDING_NOT_EVALUATED`, never silence, because a skipped value set reads exactly like a - clean one; returning `undefined` is "no opinion" and is silent. An exception your source raises - reaches you rather than being swallowed into a document that merely looks checked. - - **This package ships value set identifiers and never their member codes.** Expansions are licensed - data. `valueSetBinding(slot)` and `valueSetBindings()` expose the declaration: for each of the five - checked slots, the bound value set's OID, whether C-CDA R2.1 binds it as `required` or `preferred`, - and the conformance statement, template and artifact revision the row was read from. Every row was - read off the normative C-CDA R2.1 Schematron this repository already pins by commit and digest, so - no binding is asserted that the normative source was not read for. - - **Four of the five bindings are Required and one is Preferred, and the difference is reported, not - rounded off.** A Preferred binding never produces the Required-binding finding: the Problem - Observation's value SHALL be present and its code only SHOULD come from the Problem value set, and - reporting a departure from that as a SHALL violation would turn a conformant problem list into a - defect. Binding strength is the verb attached to "be selected from", not the cardinality beside it: - a `routeCode` a template only SHOULD contain, whose code SHALL come from the route value set, is a - Required binding. - - Nothing a parse returns changes. No value is refused, rewritten, reordered or dropped on a negative - verdict, the document re-serializes identically, and a parse that supplies no source produces - exactly the findings it always did: membership is never inferred from the code system alone. - - **This is why the bump is minor rather than patch.** It adds two warning codes and eleven exported - symbols (`ValueSetSource`, `ValueSetMembershipQuery`, `ValueSetMembershipAnswer`, `ValueSetBinding`, - `ValueSetBindingStrength`, `ValueSetBindingSource`, `BoundValueSet`, `valueSetBinding`, - `valueSetBindings`, `valueSetBindingViolated`, `valueSetBindingNotEvaluated`), and renames, removes - and repurposes nothing. `CcdaWarning` gains two optional fields, `valueSet` and `valueSetRelease`, - carried only by the two new codes: a message still comes whole from the frozen registry, so the - value set OID and the release ride as data rather than as interpolated text, and the coded value - that caused the finding appears nowhere in it. - -- 0ec5734: required sections: a SHALL warning fires exactly where the normative source says it should - - The per-document-type required-section (SHALL) tables now cover all twelve - recognized types. Six of them (CCD, Discharge Summary, Referral Note, History and - Physical, Care Plan, Transfer Summary) had asserted sets that predated anyone - reading the normative source for them, and reported `verification: "untraced"` to - say so. Their document-level `errors` and `warnings` rules have been read off the - HL7 C-CDA R2.1 normative Schematron and the tables corrected. **No recognized - document type reports `untraced` any more.** - - Two defects went in opposite directions and both are closed. - - **A conformant Discharge Summary drew a false warning.** The table asserted a - Discharge Medications section as a SHALL. The normative source states it as a - **SHOULD**, in that document's warnings rule (CONF:1198-30525), so a conformant - Discharge Summary that omits the section was drawing a - `REQUIRED_SECTION_MISSING` it had not earned. It is withdrawn, in the stamped - and the unstamped reading alike. - - **A non-conformant document passed quietly.** The same Discharge Summary table - omitted Plan of Treatment (CONF:1198-30528), which the errors rule requires - unconditionally; a History and Physical asserted one of the ten SHALL sections - its errors rule names; a Transfer Summary asserted three of six. Every one of - those sections that this parser's catalog recognizes is asserted now. - - **What changed on the published surface**, for `requiredSectionKeys`, - `missingRequiredSections`, `requiredSectionStatus` and the parser warnings that - follow from them: - - **Discharge Summary**: `dischargeMedications` withdrawn, `planOfTreatment` - added. - - **History and Physical**: `familyHistory`, `pastMedicalHistory`, `medications`, - `results`, `socialHistory` and `vitalSigns` added beside `allergies`. - - **Transfer Summary**: `results`, `vitalSigns` and `reasonForReferral` added. - - **CCD, Referral Note and Care Plan**: confirmed unchanged. - - Expect a document of the three changed types to draw warnings it did not draw - before, and a conformant Discharge Summary with no Discharge Medications section - to fall silent. `REQUIRED_SECTION_MISSING` remains a Tier-2 warning, never a - fatal, and remains safety-critical, so no vendor profile can quiet it. - - **Every key an added rule contributed is scoped to the R2.1 stamp**, because each - of those document-level rules matches only a `ClinicalDocument` whose - `templateId` carries `@extension="2015-08-01"`. An unstamped, R1.1-origin document - is therefore asserted exactly as it was before, with one exception in the safe - direction: a key withdrawn because the source states it as a SHOULD or as a choice - is withdrawn from the unstamped reading too, since "no sentence made this - unconditional" does not depend on a version stamp. - - **Provenance is on the surface, not in a commit message.** Every asserted key now - carries the conformance statement it was read from and the source's own name for - the section; every SHALL section the source names that this package cannot assert - is enumerated with its reason (`outside-section-catalog` for Hospital Course, - General Status, Physical Exam and Review of Systems; `not-unconditionally-required` - for each choice); and `requiredSectionStatus(type).source` names the normative - artifact and **that artifact's own revision date**, so a reader holding a later - revision can tell a table is stale without re-deriving it. The new - `RequiredSectionSource` type is exported alongside it. - - This is document-level validation only: which sections a document type SHALL - contain, never what a section or an entry SHALL contain. A quiet parse is still - not a conformance result. - -- ca77a09: version stamps: a document says which C-CDA release it was written for - - C-CDA 3.0.0 gave every document template a new `@extension`, `2024-05-01`, and - 4.0.0 and 5.0.0 kept it, so a document written for a release later than R2.1 is - detectable from the `templateId` that resolves its type. Through `0.0.15` this - parser got that case wrong twice, once loudly and once in silence. - - Loudly: any `@extension` that was not `2015-08-01` drew - `TEMPLATE_EXTENSION_ABSENT`, whose frozen message reads "carries no @extension - version stamp ... (may pre-date R2.1)". For a `2024-05-01` document both halves - are false, because it carries a stamp and it post-dates R2.1. - - Silently, and worse: the same path reported the document as unstamped, and the - required-section tables read that as the R1.1-origin **reduction**, which drops - every R2.1-stamp-scoped SHALL key. A post-R2.1 CCD carrying neither Social - History nor Vital Signs therefore drew no `REQUIRED_SECTION_MISSING` at all. The - library had classified a document from the future as one from the past. - - **Two warning codes are added and none is renamed or removed.** - `TEMPLATE_EXTENSION_UNMODELED_RELEASE` fires when the resolving `templateId` - carries an `@extension` that is present and is not the R2.1 stamp; - `REQUIRED_SECTIONS_NOT_EVALUATED` fires once for such a document to say its SHALL - obligation was not computed. `TEMPLATE_EXTENSION_ABSENT` keeps its narrower - meaning ("no `@extension` at all", the R1.1-origin shape) and its registry message - byte for byte. - - **An unmodelled stamp is reported, never reduced.** Such a document draws no - `REQUIRED_SECTION_MISSING` in either reading, and - `requiredSectionStatus(type, { stamp: "unmodeled-release" })` reports - `evaluation: "not-evaluated"` so an empty key set still says which emptiness it - is. `evaluation` is a new field on `RequiredSectionStatus` and a separate axis - from `verification`, which remains a claim about the document type that no option - moves. - - **The release this package validates against is an exported value now.** - `CCDA_CONFORMANCE_RELEASE` is `"R2.1"`, beside the closed table of stamps this - package can name (`CCDA_RELEASE_STAMPS`, plus `R30_EXTENSION`, - `releaseForTemplateExtension` and the three-state `readTemplateStamp`). **This - does not retarget the library**: regulation still adopts R2.1 and SVAP use is - voluntary, so a later release is recognized and named, never read against. - 4.0.0 also relaxed the US Realm Header and 5.0.0 added a Pregnancy Section, and - neither is modelled here. - - Four things are decisions rather than details: - - **A reported stamp comes from a closed set of literals this package owns.** A - message names a member of `CCDA_RELEASE_STAMPS` or names no stamp at all, so a - sender-controlled `@extension` can never reach a `CcdaWarning.message` (or, - under `{ strict: true }`, a thrown error's stack). No factory gained a - parameter that a message interpolates. - - **The existential R2.1 rule is unchanged and beats a later stamp.** A document - carrying the resolving root twice, once stamped `2015-08-01` and once for a - later release, is inside the Schematron rule's context and is evaluated under - the full R2.1 obligation, in either sibling order. - - **`{ r21Stamped }` behaves exactly as it did.** - `requiredSectionKeys("ccd", { r21Stamped: false })` returns the same four keys - in the same order. A boolean cannot express three states, so the three-state - `{ stamp }` was added beside it rather than repurposing a published option. - - **`legacyR11` does not tolerate the new code.** It is receive-tolerance for - documents from the past, grounded in the receive-both-R2.1-and-R1.1 - requirement; quieting a future-release stamp there would restore the silence - this change removes. Neither new code is safety-critical, so a consumer who has - read the later guide can still write their own profile for it. - - Nothing became stricter. No fatal was added, no content the C-CDA open-template - rule permits is refused, and a post-R2.1 document's clinical reading (patient, - sections, entries, values) and its byte-faithful re-serialization are identical - to the same document stamped `2015-08-01`. Under `{ strict: true }` the new - warnings escalate like every other Tier-2 warning, which is pre-existing - caller-opted behaviour. - -- 0151ab9: subject override: an entry is never read as the patient's unless it is - - CDA R2 gives `Section.subject` cardinality `0..1` and defines it as the "primary - target of the entries recorded in a section"; C-CDA admits the same override on a - clinical statement. So a conformant document can carry a relative's, a donor's or - a contact's clinical statement inside the patient's document. This parser read - `` in exactly one place (a Family History Organizer's `relatedSubject`) - and every other extractor handed such an entry back as the patient's, silently. - That is another person's clinical data attributed to this patient, in a read path - documented as the patient's own. - - A top-level `` that a subject declaration governs is now **withheld** from - every record-target read path and flagged with a new safety-critical Tier-2 - warning, `SUBJECT_CONTEXT_OVERRIDE`, which no vendor profile may tolerate. An - entry is governed when it carries a declaration itself, when a clinical statement - nested inside it carries one, or when an enclosing section carries one (the - nearest enclosing declaration wins, which is the standard's own conduction rule). - - Four things are decisions rather than details: - - **Presence is the trigger.** A declaration is an override whatever it names. - Nothing is compared with the record target and nothing is resolved to the - patient, because that would make a clinical answer depend on vendor identifier - hygiene. A document that redundantly restates the patient as an entry subject - therefore loses those entries from the record-target read paths and gains a - warning for each: the safe direction of the error, and the accepted cost. - - **The whole top-level entry is withheld**, its own statement and every statement - nested inside it, and the same unit does the counting. A declaration on the - second Problem Observation of a Problem Concern Act withholds that whole concern - act and emits one warning at the act's locus, never a concern act handed back - one observation short. - - **Family History is untouched.** A Family History Organizer's own subject slot - is that template's mechanism for naming the relative, whatever it contains, so - it is never an override, draws no warning, and re-overrides an enclosing section - declaration. `getFamilyHistory()`, the `familyHistory` field, - `extractFamilyHistory` and the aggregate's family-history slot return exactly - what they returned before, in every document shape. That carve-out is read-side - and reaches only the organizer the family-history path itself reads: an entry - that carries the Family History Organizer template beside one a record-target - read path returns (a Result Organizer, a Problem Concern Act) is withheld and - reported like any other, because a `templateId` is one element and C-CDA entries - carry several. A declaration nested deeper inside an entry is never that slot. - - **Withholding is read-side only.** The withheld entry is not dropped: the - byte-faithful round trip through `toString()` / `serializeCcda` reproduces it - unchanged, and section narrative is returned unredacted, unreordered and - unannotated. - - The count is per section and sums over the document: N governed top-level entries - in a section produce exactly N instances, in document order, each naming that - entry's own bounded locus (an element path from the CDA vocabulary, the section's - LOINC code, line and column, with `` for anything that fails its bound), - and a section that declares an override but governs no entry anywhere beneath it - produces exactly one instance at its own locus instead. The same withholding - applies to a per-family or aggregate extraction invoked directly on a section, - including a nested subsection governed by an ancestor's declaration, with the - warnings delivered on the parse context those functions already accept: no - signature, parameter or return type changes. `extractFamilyHistory` delivers them - too, while returning its own contents whole. - - Two behaviour changes to expect. A document carrying an override loses those - entries from `getProblems()` / `getMedications()` / `getAllergies()` and the other - ten record-target families where it previously returned them as the patient's, - and under `{ strict: true }` it now throws, because the new code escalates like - every other safety-critical Tier-2 warning. Nothing is modelled for third-party - subjects: a withheld entry is reachable only through the re-serialized document. - -- f521d8f: feat: read the header participations, so a parsed document can say who authored it - - A consumer could ask whose document it was and what was in it, but not who wrote it. - `CcdaHeader` carried ten fields and not one of them was a participation. Every - conforming US Realm document carries at least one `author` and exactly one `custodian`, - and an inpatient Discharge Summary carries the encounter it summarises, so the parser - was dropping data the source document was required to supply. Provenance is the answer - to "who said this", which is what a clinician asks of a medication list assembled by - three systems. - - `header.authorship` carries every document-level `` in document order: its - identifiers, its assigned person's name or its assigned authoring device's identity, - the organization the document states it acted for, and its author `time` at exactly the - precision the document stated. A partial author time stays partial. `header.custodian` - carries the custodian organization with its identifiers and its name. - `header.encompassingEncounter` carries the `componentOf/encompassingEncounter` frame: - its `effectiveTime` bounds, each keeping its own precision and any `nullFlavor`, and its - `dischargeDispositionCode`. - - **An inherited author reading is marked, never asserted.** CDA conducts an author down - from the document to a section and on to that section's entries, and this reports the - conduction rather than performing it silently. `CcdaSection.authorship` and each entry's - reading in `CcdaSection.entryAuthorship` carry an `inherited` flag: `false` when the - level stated these authors itself, `true` when it stated none and these are the nearest - enclosing level's. "The nearest enclosing author is this clinician" and "this entry was - authored by this clinician" are different claims and only the first is one the document - made. When no level carries an author the reading is absent at every level: the record - target, the custodian, a legal authenticator and an informant are never read as the - author, and no `componentOf` means no encounter frame rather than one derived from the - document `effectiveTime` or a `documentationOf` service event. - - One warning code is added and none is renamed, removed or repurposed. An `` - whose `assignedAuthor` carries neither an `assignedPerson` nor an - `assignedAuthoringDevice`, which the US Realm Header requires one of (CONF:1198-8456), - is kept, marked `unidentified`, and reported with `UNIDENTIFIED_AUTHOR`. It still - conducts to nested levels. Dropping it would turn "the document names an author whose - identity it never states" into "the document names no author", which is a more - reassuring claim than the document supports. The factory takes only a position and the - message comes whole from the frozen registry, so a clinician's name cannot reach a - diagnostic. - - Entries an overriding `` declaration governs are absent from `entryAuthorship` - entirely, not even their `ids`: it is a record-target read path, and a governed entry - appearing in one would put another person's entry back on the model through a second - door. - - **This is why the bump is minor rather than patch.** It adds seven exported types - (`CcdaAuthor`, `CcdaAuthoringDevice`, `CcdaAuthorship`, `CcdaCustodian`, - `CcdaEncompassingEncounter`, `CcdaEntryAuthorship`, `CcdaOrganization`) and one warning - code. `buildSection` takes an optional third argument, the enclosing author reading; - every existing call keeps working and reads the section as having no enclosing author, - which is what a caller framing a detached `
` is looking at. - - **Nothing here is breaking, and the new `CcdaSection` field is optional so that stays - true.** `CcdaSection` is an input surface as well as an output one, reachable through - `CcdaDocumentInit.sections`, which the public `CcdaDocument` constructor takes: a - required `entryAuthorship` would stop a consumer's existing section literal from - compiling, which is not a minor change. The parser populates the field on every section - it frames, empty where the section has no entry act to read. - - Framing a section reads each entry act's ``s without reporting on them, because the - entry-extraction walk already parses those same elements. No document's warning output - moves: not the count, not the order, and not whether `strict: true` throws. - - This is the READ side only. Nothing the builder emits changes, and emitting - `componentOf` is not part of it. - -- 2780242: Build an inpatient Discharge Summary, the third of the twelve document types - - `buildCcda` emits a Discharge Summary for `documentType: "dischargeSummary"`, - alongside the CCD and the Referral Note. `minor` rather than `patch` because the - package entry point gains two exported types: `BuildableDocumentType` (the union - of the three types the builder emits) and `BuildCcdaEncompassingEncounter`, and - the published `UnassertedSectionReason` union gains a third member, - `"assertion-would-tighten-parse"`. Nothing published was removed or renamed, and a - CCD or a Referral Note built from an init that names no Discharge Summary input - does not change by a byte. - - The document specializes the US Realm header with its own document `templateId` - root (`2.16.840.1.113883.10.20.22.1.8`, stamped `2015-08-01`), its LOINC document - code (`18842-5`), and the four SHALL sections its errors rule names: Allergies, - the narrative-only Hospital Course (`1.3.6.1.4.1.19376.1.5.3.1.3.5`, LOINC - `8648-8`), Discharge Diagnosis (V3) and Plan of Treatment (V2). Neither Problems - nor Medications is in that set, so unlike a CCD or a Referral Note a Discharge - Summary emits them only when you supply content, the medications in the - Medications Section; Discharge Medications is a SHOULD in the document's warnings - rule and is not emitted at all. Three new inputs carry the content: - `hospitalCourse`, `dischargeDiagnoses` and `encompassingEncounter`. - - **What you supply is emitted or refused, never dropped.** Those three inputs throw - a `TypeError` on a CCD or a Referral Note instead of being ignored, and - `assessment` and `reasonForReferral`, the Referral Note's narratives, throw on a - Discharge Summary. A CCD still ignores `assessment` and `reasonForReferral`, as it - always has. - - It is the only type carrying `componentOf/encompassingEncounter`, the frame - naming the stay. Every slot in it is required by the template and none is ever - invented: an encounter period bound or a discharge disposition you did not supply - is emitted as an explicit `nullFlavor="UNK"`, which satisfies the cardinality, - says the fact is unknown rather than stating one, and reparses as absent. None of - the three is ever derived from the document `effectiveTime` or from a - `documentationOf` service event. A supplied bound keeps exactly the precision you - gave it. The disposition's `codeSystem` defaults to the NUBC UB-04 Patient - Discharge Status code set, the system every member of the value set the template - names carries, and your own `codeSystem` is emitted verbatim instead when you - supply one. - - **All three types reparse with zero warnings.** The Hospital Course Section is an - IHE PCC template outside the section catalog; `parseCcda` now recognizes it by its - root (`findSection("hospitalCourse")` resolves), in every document and not only a - built one, so a section carrying that root and matching nothing in the catalog no - longer raises `UNKNOWN_SECTION_CODE`. That is the only parse output that moves: - the root is consulted after every catalog root and the LOINC fallback, so no - section changes key; the LOINC code `8648-8` alone still raises - `UNKNOWN_SECTION_CODE`; no entry in such a section is newly flagged as misplaced; - and the section is not asserted as required, so no Discharge Summary that omits it - gains a `REQUIRED_SECTION_MISSING`. `requiredSectionStatus("dischargeSummary")` - names it as unasserted with the new reason `"assertion-would-tighten-parse"`. - - Asking for one of the other nine types still throws a `TypeError` naming what you - asked for. That refusal is now derived from the builder's own specialization - table rather than from a list of strings, so a thirteenth recognized type is - refused the day it is recognized, and the membership test is `Object.hasOwn` - rather than `in`, so an untyped caller passing a prototype key is refused instead - of crashing partway through a build. Only an omitted (`undefined`) `documentType` - defaults to a CCD: `null`, which used to build a CCD, is refused, and so is any - value that is not a string, before any lookup and without converting it. - - Measured, not asserted: `pnpm conformance` validates fifteen built documents, - two of them Discharge Summaries at both ends of the input range, against the CDA - R2 XML schema and the error-severity phase of the normative C-CDA R2.1 - Schematron, with zero error-severity results. The README conformance statement - and the tracked report move with it, from two of twelve to three. - -- 33c543b: Add the shared `toObject` / `toISO` / `toDate` conversion surface for a parsed `TS` - - Every `@cosyte/*` standard parser now exports the same three conversions, under the same - names, with the same return shapes and the same timezone rule, so moving between two of - them costs nothing to relearn. Five names are added to the package entry point: - `toObject`, `toISO`, `toDate`, and the `DateParts` and `ToDateOptions` types. - - `toObject(ts)` returns a frozen `DateParts` carrying **only** the components the document - stated. Nothing is zero-filled, so `Object.keys()` on a year-precision value is `["year"]` - and the value's precision survives the conversion. `month` is the spec-native 1 to 12, the - component names are singular, and there is no `raw`, `precision` or `valid` key. - `millisecond` is the first three digits of the stated fraction taken verbatim and - right-padded (`.5` is 500, `.0500` is 50), never a floating-point fraction multiplied by 1000. `offsetMinutes` is present if and only if the value carried an explicit offset, a - stated zero included. Deleting `offsetMinutes` leaves an object - `Temporal.PlainDateTime.from` and luxon's `DateTime.fromObject` accept unchanged; neither - library is a dependency and neither is imported. - - `toISO(ts)` renders ISO-8601 truncated to that same precision and pads nothing out, with - fractional digits rendered exactly as written. A stated offset is appended (`Z` for a - stated zero, otherwise `+HH:MM` / `-HH:MM`); with no stated offset nothing is appended and - no `Z` is fabricated. Because a stated zero renders `Z`, this is not a byte round-trip of - the wire value, and `serializeCcda` remains the round-tripping route. - - `toDate(ts, options?)` returns a `Date` only when the zone is determinate: the value - carried an offset, or the caller passed `assumeOffsetMinutes` (signed minutes east of UTC, - where an explicit `0` means "read this naive value as UTC"). With neither, the answer is - `undefined`. The host machine's timezone is never read and UTC is never assumed. A value's - own offset always beats an assumed one, a year below 100 stays that year rather than being - remapped into the 1900s, and components below the stated precision fill to their lowest - legal value for the instant only. An `assumeOffsetMinutes` that names no usable zone is - refused the same way: `NaN` and the two infinities are not a number of minutes, and a finite - offset large enough to push the result outside the range a JS `Date` holds denotes no - instant. Both answer `undefined`. What never comes back is an `Invalid Date`, which - satisfies the declared return type and defeats its point. - - An offset the **document** states is bounded as well, at 23 hours 59 minutes, and past that - bound the value is refused whole by all three functions. The v3 `±ZZZZ` token is four digits - wide and constrains neither field, so `+2400`, `+9999` and `-9999` are legal literals stating - 1440, 6039 and -6039 minutes: finite numbers, none of them a zone, and none of them - expressible in the shared `+HH:MM` / `-HH:MM` slot, whose `HH` is a two-digit hour of the - day. Refusing them together is what keeps `toObject` from reporting a hundred-hour zone that - `toISO` renders as `+100:39`, a string no ISO-8601 reader accepts. `@cosyte/hl7` bounds at - the same 23:59; `@cosyte/dicom` bounds tighter, at 14:59. - - None of the three ever throws, for any input: an absent value, a `TS` with no `@value`, a - `TS` whose `@nullFlavor` contradicts a populated `@value` (the grounds on which `parseTs` - already withholds `date`), and a `@value` this package parses as malformed all answer - `undefined`. - - **`TS.date` is unchanged and stays unchanged.** It remains eager: it zero-fills a truncated - value, resolves an offset-less one as if it had said UTC, and resolves an unstatable stated - offset too, so there are values where `ts.date` is a populated `Date` and `toDate(ts)` is - `undefined`, on the same object. `parseTs`, `parseV3DateTime` and the warning codes are - untouched, and an out-of-range offset raises no new warning. That - divergence is deliberate rather than an oversight, it is documented in `README.md` and in the - docs bundle, and the old answer stays reachable by asking for it with - `{ assumeOffsetMinutes: 0 }`. Nothing reading `TS.date` today reads anything different. - - Parts are derived by re-parsing `TS.raw`, the document's own bytes; `TS.date` is never read. - No dependency of any kind was added, runtime or development, and `engines.node` is unchanged - at `>=22.0.0`. - -### Patch Changes - -- de5305e: docs: cover the profile, terminology and conformance surfaces, and gate the bundle - - The published narrative documentation gained a page and a guard. Three surfaces - that a caller has to configure correctly were described only in passing inside - the limitations page, with nothing a reader could copy and run: the vendor - profile system, the bring-your-own `TerminologyAdapter`, and the required-section - conformance status. Each now has its own section on a new "Conformance, profiles - & terminology" page with an executable example, and every one of those examples - is compiled and run against the built package by the existing doc/code agreement - suite, so a documented call that stops working fails the build rather than - misleading a reader. The terminology example uses an in-process stub adapter: it - opens no socket and needs no licensed terminology service, which is also what - makes it a usable template for testing an adapter of your own. - - Three pages claimed the package was "published on npm at `0.0.3`" while it was - twelve patches past that. No page names a published version now; they point at - `npm view @cosyte/ccda version`, which cannot go stale. Every historical note - that dates a behaviour change to a past version is untouched and is now held in - place by a retention floor, because those sentences are the change record for a - reader pinned to an older version, not staleness. - - Page frontmatter was inconsistent (`sidebar_label` on five pages of nine) and - `sidebar_position` collided across five pages. Every page now carries the same - four keys, and each position agrees with the page's place in `sidebars.json` - rather than being a second, disagreeing answer to what comes next. - - A new guard holds all of it: every symbol the public entry point exports, types - included, must be named by a page or carry a stated reason in a committed - exemption record that cannot outlive the symbol it excuses. The formatter now - covers the bundle as well, so the pages cannot drift out of format. - -- 4bc6aa8: Bring this repo to the shared package baseline: install hardening, the required - js-yaml override, and a CLAUDE.md under its line ceiling - - Three drifts the meta-repo's `config/drift-manifest.json` measures against this - repo are closed, and nothing else changes. - - `pnpm-workspace.yaml` is new and carries `minimumReleaseAge: 1440` and - `trustPolicy: no-downgrade`, the publication cooldown and trust policy the - standard requires of every `@cosyte/*` package repo. Both keys are switched off - by default in pnpm 10, and both are ignored outright by a pnpm older than - 10.16.0 and 10.21.0 respectively, so `packageManager` is raised from `pnpm@10.0.0` - to `pnpm@10.34.5`: a settings file the pinned package manager ignores decorates - rather than defends. A cold-store `pnpm install --frozen-lockfile` was measured - against the new policy and raised no `ERR_PNPM_TRUST_DOWNGRADE`, so no - `trustPolicyExclude` entry is carried and the exemption lists are absent rather - than empty. - - `pnpm.overrides` gains the required `js-yaml@>=4.0.0 <4.3.0` entry. It is listed - BEFORE this repo's own `js-yaml@>=4.0.0 <4.3.2` entry on purpose: pnpm applies - the LAST matching override, measured on this tree, and the other order resolved - js-yaml 4.x down from 4.3.2 to 4.3.0, which would have been an advisory - remediation quietly undone by a compliance edit. The lockfile still resolves - js-yaml at 4.3.2, and no existing override was removed, narrowed or repointed. - - `CLAUDE.md` goes from 459 lines to 232, under the ceiling of 300 the standard - declares, entirely by RELOCATION. Every block of prose that left it is - reproduced verbatim in `documentation/agent-notes.md`, under the very heading - the imperative's `Why:` pointer already resolved to, and what is left in - `CLAUDE.md` is the rule plus that pointer. Nothing was deleted, no imperative - lost its file, and no claim was reworded, softened or strengthened. - - No published API, warning code, parser behaviour or emitted XML changes. - -- af15008: README: standardize the page, and make its usage example a test - - The npm package page and the GitHub landing page are the same file, and for most - consumers it is the only page they will ever read. It was 1086 lines of deep - reference with no answer above the fold to the three questions a reader arrives - with: what is this for, is it safe to depend on, and what does it do with my - patients' data. It also carried a status line claiming the package was published - at `0.0.3`, twelve patch releases out of date, and a hand-written `alt` string on - the banner that disagreed with the one the brand assets declare for that tile. - - The page now opens with the banner (carrying the declared `alt` string), the - title, a one-line hook, the four house badges and the `package.json` - `description` verbatim, followed by a table of contents. Four sections are new: - - **Why this exists**, which names the problem, the nearest alternative a reader - would otherwise reach for, and why this is not that. - - **Status**, which states the version this package declares, says what that - version does and does not claim about the public API, and names the surfaces - that are still moving instead of implying they are settled. - - **PHI and safety**, which states what the library does with patient data for - each of logging, in-memory retention and writing to disk, and names what the - consuming application still owns. - - **Contributing**, which says where to ask, whether unsolicited pull requests - are accepted, and every check a contribution has to clear. - - `Install` now states the Node engine floor and the module format. The parse - walkthrough is consolidated into a single `Usage` example that builds a complete - synthetic CCD, reads it back and shows what every call returns, and that block is - now executed by the test suite against the built entry point: an example whose - shown output stops matching what the code produces fails the build. Roughly half - of documentation traffic is agents that lift a usage block verbatim, so a wrong - example is wrong generated code at scale. - - No parse, emit, builder or profile behaviour changes, and no documented behaviour - was dropped: every reference section the page carried is still on it. - -- c361b4a: phi-scan: a target enumerated and never read refuses the scan - - `--allow-fixture` withdrew a path from the target list and the run reported on - whatever remained, so the withdrawal never showed up in the verdict: four argv - shapes printed `OK, no hits` at exit 0 over a corpus holding a live, detectable - violator, including a whole-run sweep whose only violator was the withdrawn file. - CI could print a clean verdict over a corpus it never opened. - - The scanner now refuses (exit 2), in every mode, over any target this run - enumerated and never read, naming the paths. The comparison is a set difference - rather than a count, because a count counts the targets that did get read. A - bypass naming a path the run does not enumerate refuses too, under its own - message, since such a flag subtracts nothing (in `paths` mode a nonexistent path - is refused earlier still, by `File not found`). `--allow-fixture` no longer - selects the mode and is unioned into the target list in `paths` mode, so it means - the same thing in every argv. - - What it costs, stated rather than left to be discovered: `--allow-fixture` can no - longer reach exit 0 in any mode. The flag, `phi-scan-overrides.md` and the log - gate all stay, so an attempt is recorded and then refused rather than silently - honored, and `scripts/phi-allow-list.txt` is now the only mechanism that reaches - a clean run. The hit footer no longer advertises the flag as a remedy. - - No published API, warning code or parser behaviour changes; this is the commit - gate only. There was no real unread corpus in this repo: the all-mode sweep - enumerates and reads the same set, and the four states were reproduced with - planted fixtures rather than found. - ## 0.0.15 ### Patch Changes diff --git a/README.md b/README.md index bc0a43e..038f877 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ silence. ## Status -`package.json` declares **`0.1.0`**. From `0.1.0` the public API is settled and safe to depend on: +`package.json` declares **`0.0.15`**. From `0.1.0` the public API is settled and safe to depend on: the behaviour documented on this page is implemented and covered by tests, the exported names, options, return shapes and warning codes are the surface we keep stable, and below 1.0 a breaking change bumps the minor version and is called out in the changelog with its migration. On a `0.0.x` diff --git a/package.json b/package.json index fc31ba5..6f43ca7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cosyte/ccda", - "version": "0.1.0", + "version": "0.0.15", "description": "C-CDA parser, serializer, and builder for Node.js and TypeScript: lenient on parse, spec-clean on emit.", "keywords": [ "ccda", diff --git a/src/index.ts b/src/index.ts index dd54d87..a1415af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,7 +27,7 @@ * console.log(VERSION); * ``` */ -export const VERSION: string = "0.1.0"; +export const VERSION: string = "0.0.15"; // Parser entry point + options. export { parseCcda } from "./parser/index.js"; From 968bd29db3ec2be059aaeb7bbebb4dfe5cfd658c Mon Sep 17 00:00:00 2001 From: Noah Schatz Date: Fri, 25 Sep 2026 21:28:24 +0000 Subject: [PATCH 2/2] Revert the 0.1.0 version commit and fix a changeset headline the release gate refused The Release run for the version commit (run 36191393660) stopped before publishing: the release-notes gate cut the "feat:" prefix from short-hounds-author.md and the headline that was left trailed off. Nothing was published. As the gate directs, this reverts the version commit, which restores the consumed changesets, and rewrites that headline as one complete sentence, so Changesets opens a fresh Version Packages PR. Claude-Session: https://claude.ai/code/session_01U24bnHKNxQWMxmfN7ny7w9 --- .changeset/short-hounds-author.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/short-hounds-author.md b/.changeset/short-hounds-author.md index d080094..2d2319c 100644 --- a/.changeset/short-hounds-author.md +++ b/.changeset/short-hounds-author.md @@ -2,7 +2,7 @@ "@cosyte/ccda": minor --- -feat: read the header participations, so a parsed document can say who authored it +A parsed document now says who authored it: the parser reads the header participations. A consumer could ask whose document it was and what was in it, but not who wrote it. `CcdaHeader` carried ten fields and not one of them was a participation. Every