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..2d2319c --- /dev/null +++ b/.changeset/short-hounds-author.md @@ -0,0 +1,70 @@ +--- +"@cosyte/ccda": minor +--- + +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 +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";