Documentation
¶
Overview ¶
Package cda reads clinical documents.
The design starts from how these documents actually travel, which is not as files. A discharge summary leaves an EHR inside an HL7 v2 MDM^T02 message, with the whole document base64-encoded in OBX-5 and described by a TXA segment. That is thousands of messages a day in a mid-sized hospital, and it is why this package sits next to an HL7 v2 parser rather than in a document toolkit: the v2 half is already here, so the pipeline from "a message arrived" to "a FHIR DocumentReference exists" is short.
Three things are deliberately in scope and one is deliberately not.
In scope: reading a document and saying what it contains in plain terms; checking that the human-readable narrative and the coded entries agree; and converting to FHIR. The narrative check is the unusual one. Every C-CDA section carries the same information twice, once as text for a clinician and once as codes for a machine, and they are supposed to match. In practice they often do not, and nothing checks it - not the certification tooling, not the engines. When the text says a patient is allergic to penicillin and the coded entry says something else, one of the two readers acts on the wrong information. It is a patient safety defect that is mechanically detectable, so it is detected here.
Not in scope: generating conformant C-CDA. Writing one means satisfying several hundred template rules and a Schematron, and producing documents that are 95% right would fail certification in ways that are miserable to debug. Reading, checking and converting is most of the value for a fraction of the risk.
Index ¶
- func GeneratableSections() []string
- func Generate(d *Document, opts GenerateOptions) ([]byte, error)
- func IsDocumentMessage(m *hl7.Message) bool
- func MergeSections(kind string, docs ...*Document) (Section, []Note)
- func ParseEmbedded(m *hl7.Message) (*Document, []Embedded, error)
- func RecognisedDocuments() []string
- func RecognisedSections() []string
- func RenderPDF(d *Document, opts RenderOptions) ([]byte, error)
- type Address
- type AgreementReport
- type Author
- type Document
- func (d *Document) AllEntries() []Entry
- func (d *Document) SectionByKind(kind string) *Section
- func (d *Document) SortedNotes() []Note
- func (d *Document) Summarise() Summary
- func (d *Document) ToFHIR(opts FHIROptions) (*FHIRResult, error)
- func (d *Document) ToTOC(opts TOCOptions) (*FHIRResult, error)
- type DocumentInfo
- type Embedded
- type Encounter
- type Entry
- type FHIROptions
- type FHIRResult
- type Finding
- type GenerateOptions
- type Identifier
- type MedChange
- type Note
- type Patient
- type ReconcileReport
- type RenderOptions
- type RepairAction
- type RepairKind
- type RepairOptions
- type RepairReport
- type Section
- type Summary
- type TOCOptions
- type ValidationReport
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func GeneratableSections ¶
func GeneratableSections() []string
GeneratableSections lists the section kinds Generate can identify with a template and code.
Exposed so a caller assembling a document can check whether the section it wants to write will carry conformance identifiers, rather than discovering after the fact that it was emitted without them.
func Generate ¶
func Generate(d *Document, opts GenerateOptions) ([]byte, error)
Generate writes a C-CDA document from a Document struct.
The Document need not have come from Parse. A caller assembling one from HL7 v2 or from FHIR sets the patient, the sections and their entries, and this produces conformant XML.
Narrative is generated for any section that has entries but no NarrativeText, because a section whose narrative is empty displays as blank in most viewers regardless of what its entries say.
func IsDocumentMessage ¶
IsDocumentMessage reports whether a message is carrying a document, so a channel can route on it without decoding anything.
func MergeSections ¶
MergeSections combines the same section from several documents, deduplicating entries and concatenating narratives.
It collects every section whose Kind matches from all documents, unions their entries deduplicating by code (or normalised name when uncoded), concatenates narrative text with a blank line between sources, and returns Notes explaining anything dropped or that conflicted.
func ParseEmbedded ¶
ParseEmbedded extracts a document from a v2 message and parses it as CDA.
This is the pipeline the package exists for. Anything that is not a clinical document - a PDF, a scanned image - is reported as such rather than being forced through the parser, because a channel handling mixed attachments needs to tell the difference without an error.
func RecognisedDocuments ¶
func RecognisedDocuments() []string
RecognisedDocuments lists the document types this package names, so the interface can state what it supports rather than leaving somebody to discover the limits by trial.
func RecognisedSections ¶
func RecognisedSections() []string
RecognisedSections lists the section types this package names.
Types ¶
type Address ¶
type Address struct {
Use string `json:"use,omitempty"`
Lines []string `json:"lines,omitempty"`
City string `json:"city,omitempty"`
State string `json:"state,omitempty"`
Postal string `json:"postal,omitempty"`
Country string `json:"country,omitempty"`
}
Address is a postal address.
type AgreementReport ¶
type AgreementReport struct {
Findings []Finding `json:"findings"`
// Checked is how many sections were compared.
Checked int `json:"checked"`
// Skipped is how many were not, with Reasons saying why. A check that
// silently examined half a document would be misleading.
Skipped int `json:"skipped"`
Reasons []string `json:"reasons,omitempty"`
Errors int `json:"errors"`
Warnings int `json:"warnings"`
}
AgreementReport is the result of checking a document.
func CheckAgreement ¶
func CheckAgreement(d *Document) AgreementReport
CheckAgreement compares narrative and entries throughout a document.
func (AgreementReport) Agrees ¶
func (r AgreementReport) Agrees() bool
Agrees reports whether nothing of consequence was found.
type Author ¶
type Author struct {
Time string `json:"time,omitempty"`
Family string `json:"family,omitempty"`
Given string `json:"given,omitempty"`
ID string `json:"id,omitempty"`
Organisation string `json:"organisation,omitempty"`
}
Author is who wrote the document.
type Document ¶
type Document struct {
// Title is the document's own title, which is what a clinician recognises it
// by.
Title string `json:"title,omitempty"`
// TypeCode and TypeName come from the document's code element.
TypeCode string `json:"typeCode,omitempty"`
TypeSystem string `json:"typeSystem,omitempty"`
TypeName string `json:"typeName,omitempty"`
// TemplateIDs are the conformance claims the document makes. They are the
// only reliable way to know which implementation guide applies.
TemplateIDs []string `json:"templateIds,omitempty"`
// DocumentType is the recognised kind, derived from the template IDs and the
// code, in plain words.
DocumentType string `json:"documentType,omitempty"`
// ID and SetID identify the document and its revision series.
ID string `json:"id,omitempty"`
SetID string `json:"setId,omitempty"`
// Version is the revision number within the set.
Version string `json:"version,omitempty"`
EffectiveTime string `json:"effectiveTime,omitempty"`
Confidentiality string `json:"confidentiality,omitempty"`
LanguageCode string `json:"languageCode,omitempty"`
Patient Patient `json:"patient"`
Authors []Author `json:"authors,omitempty"`
Custodian string `json:"custodian,omitempty"`
Encounter *Encounter `json:"encounter,omitempty"`
Sections []Section `json:"sections"`
// Transport is what the HL7 v2 message carrying this document said about it,
// when it arrived that way.
Transport *DocumentInfo `json:"transport,omitempty"`
// Notes record anything about the document worth telling somebody, including
// parts that were present but not understood.
Notes []Note `json:"notes,omitempty"`
// Tree is the parsed XML, kept so the viewer can show anything this struct
// does not model.
Tree *xtree.Node `json:"-"`
}
Document is a parsed clinical document.
The structure is flattened deliberately. A C-CDA is nested five or six levels deep before reaching anything a person cares about, and every consumer here - the viewer, the narrative check, the FHIR conversion - wants the same short list: who it is about, what kind of document it is, and what the sections say. Keeping the tree available alongside means nothing is lost.
func (*Document) AllEntries ¶
AllEntries flattens the entry tree of every section, which is what the FHIR conversion and the agreement check both want.
func (*Document) SectionByKind ¶
SectionByKind finds a recognised section.
func (*Document) SortedNotes ¶
SortedNotes returns the notes worst first, so a reader sees what matters.
func (*Document) ToFHIR ¶
func (d *Document) ToFHIR(opts FHIROptions) (*FHIRResult, error)
ToFHIR converts a document.
func (*Document) ToTOC ¶ added in v0.1.23
func (d *Document) ToTOC(opts TOCOptions) (*FHIRResult, error)
ToTOC converts the document to a PACIO Transitions of Care document Bundle (FHIR R4). A document without an author person or a custodian is refused: the guide requires both, and inventing either would put a name on a clinical record that nobody gave.
type DocumentInfo ¶
type DocumentInfo struct {
// Type is TXA-2, the document type code.
Type string
// UniqueID is TXA-12, the document identifier the sending system uses. It is
// the key for detecting a replacement later.
UniqueID string
// Status is TXA-17: "AU" authenticated, "IN" incomplete, "DO" documented.
Status string
// Availability is TXA-19.
Availability string
// ActivityDate is TXA-4.
ActivityDate string
// ParentID is TXA-13, set when this document replaces another.
ParentID string
// Author is TXA-9.
Author string
}
DocumentInfo is what the TXA segment says about a document, which is metadata the document itself does not always carry.
func InfoFromV2 ¶
func InfoFromV2(m *hl7.Message) (DocumentInfo, bool)
InfoFromV2 reads the TXA segment.
func (DocumentInfo) Replaces ¶
func (d DocumentInfo) Replaces() bool
Replaces reports whether this document supersedes an earlier one.
It matters more than it looks. A corrected discharge summary arrives as a new document naming the old one, and a receiving system that files both leaves two contradictory summaries in the chart with nothing to say which is current.
func (DocumentInfo) StatusMeaning ¶
func (d DocumentInfo) StatusMeaning() string
StatusMeaning renders TXA-17 in words.
type Embedded ¶
type Embedded struct {
// Segment is which OBX the document came from, 1-based, so a message
// carrying several attachments can be described precisely.
Segment int
// Encoding is what OBX-5.2 or OBX-5.3 declared: "Base64", "A" for ASCII, and
// so on. It is reported even when the payload turned out to be something
// else, because the disagreement is itself worth knowing.
Encoding string
// MimeType is from OBX-5.3 where present.
MimeType string
// Data is the decoded payload.
Data []byte
// Notes record anything surprising about the extraction.
Notes []string
}
Embedded is a document found inside an HL7 v2 message.
func ExtractFromV2 ¶
ExtractFromV2 pulls every embedded document out of an HL7 v2 message.
It looks at OBX segments whose value type is ED or RP, which is how an encapsulated document is declared. A message with no such segment returns nothing and no error: most messages are not carrying a document, and treating that as a failure would make the function useless as a probe.
type Encounter ¶
type Encounter struct {
ID string `json:"id,omitempty"`
Code string `json:"code,omitempty"`
Start string `json:"start,omitempty"`
End string `json:"end,omitempty"`
Location string `json:"location,omitempty"`
}
Encounter is the visit the document describes.
type Entry ¶
type Entry struct {
// Kind is the CDA class: "Observation", "SubstanceAdministration", "Act",
// "Procedure", "Encounter", "Organizer".
Kind string `json:"kind,omitempty"`
Code string `json:"code,omitempty"`
CodeSystem string `json:"codeSystem,omitempty"`
CodeName string `json:"codeName,omitempty"`
// Value is the observation result as text.
Value string `json:"value,omitempty"`
// ValueCode and ValueName are set when the result is itself coded.
ValueCode string `json:"valueCode,omitempty"`
ValueName string `json:"valueName,omitempty"`
// ValueSystem is the value's own code system. It is often not the code's: an allergy observation's code is an
// HL7 act code (ASSERTION) and its value a SNOMED CT concept, and reading the value with the code's system
// named SNOMED concepts as HL7 act codes.
ValueSystem string `json:"valueSystem,omitempty"`
// The participant's code: what an allergy is to (participant/participantRole/playingEntity/code). An allergy
// observation's value says what kind of reaction it is ("allergy to substance"); the substance is here.
ParticipantCode string `json:"participantCode,omitempty"`
ParticipantSystem string `json:"participantSystem,omitempty"`
ParticipantName string `json:"participantName,omitempty"`
Unit string `json:"unit,omitempty"`
// StatusCode is the entry's status, which decides whether it is current.
StatusCode string `json:"statusCode,omitempty"`
EffectiveTime string `json:"effectiveTime,omitempty"`
Low string `json:"low,omitempty"`
High string `json:"high,omitempty"`
// NegationInd inverts the meaning of the entry. It is carried explicitly
// because ignoring it turns "no penicillin allergy" into "penicillin
// allergy", which is the most dangerous single mistake available here.
NegationInd bool `json:"negationInd,omitempty"`
// NilFlavor states why a value is absent.
NilFlavor string `json:"nilFlavor,omitempty"`
// Text is the narrative this entry points at, resolved through its reference.
Text string `json:"text,omitempty"`
// Children are nested entries, which is how a medication carries its dose or
// an organiser groups results.
Children []Entry `json:"children,omitempty"`
}
Entry is one coded item inside a section.
type FHIROptions ¶
type FHIROptions struct {
// Version is "R4", "R4B" or "R5".
Version string
// IdentifierSystems maps an OID root to a URI, so that an MRN becomes
// namespaced. Without one the identifier is emitted with an OID-derived
// system rather than being dropped, because losing the identifier makes the
// patient unmatchable.
IdentifierSystems map[string]string
// Original is the document bytes, attached to the DocumentReference. When
// absent the DocumentReference records only metadata and says so.
Original []byte
}
FHIROptions configures the conversion.
type FHIRResult ¶
type FHIRResult struct {
// Bundle is a transaction bundle of conditional upserts, so re-sending the
// same document updates rather than duplicating.
Bundle map[string]any `json:"bundle"`
// Counts is how many of each resource type were produced.
Counts map[string]int `json:"counts"`
// Notes explain every judgement the conversion made.
Notes []Note `json:"notes"`
}
FHIRResult is the outcome of converting a document.
type Finding ¶
type Finding struct {
// Severity is "error", "warning" or "info". Only a genuine contradiction is an
// error; a term missing from one side is a warning, because it may be a
// wording difference this check cannot see through.
Severity string `json:"severity"`
// Section is which section the finding is in.
Section string `json:"section"`
// Kind classifies it, so a recurring pattern can be recognised:
// "entries-without-narrative", "narrative-without-entries",
// "term-missing-from-entries", "term-missing-from-narrative",
// "negation-mismatch", "count-mismatch", "unreferenced-entry".
Kind string `json:"kind"`
Message string `json:"message"`
// Narrative and Coded are the two readings, so somebody can judge for
// themselves rather than taking the finding's word for it.
Narrative string `json:"narrative,omitempty"`
Coded string `json:"coded,omitempty"`
}
Finding is one disagreement.
type GenerateOptions ¶
type GenerateOptions struct {
// DocumentType selects the template and code claimed. Only "CCD" is supported; anything else
// is refused rather than guessed at, because a document claiming a template it does not satisfy
// is worse than one claiming none.
DocumentType string
// CustodianName is the organisation responsible for the document. C-CDA requires a custodian,
// so generation fails without one rather than emitting a document a receiver will reject.
CustodianName string
// Title overrides the default title for the document type.
Title string
// Indent writes the document with newlines and indentation. Off by default: the wire form is
// what gets sent, and whitespace inside a narrative element is significant.
Indent bool
}
GenerateOptions controls document generation.
type Identifier ¶
type Identifier struct {
// Root is the OID or UUID that namespaces the value. Without it an MRN is
// ambiguous between facilities, which is the single most common cause of
// unmatchable patients downstream.
Root string `json:"root,omitempty"`
Extension string `json:"extension,omitempty"`
// Assigner is a human-readable name for the root when one is recognised.
Assigner string `json:"assigner,omitempty"`
}
Identifier is a namespaced identifier.
type MedChange ¶
type MedChange struct {
// Kind is "added", "removed", "unchanged" or "changed".
Kind string `json:"kind"`
Medication string `json:"medication"`
Code string `json:"code,omitempty"`
CodeSystem string `json:"codeSystem,omitempty"`
// Before is nil for an addition, After is nil for a removal. Both are carried so a reader can see the entries
// the judgement was made from rather than taking the summary on trust.
Before *Entry `json:"before,omitempty"`
After *Entry `json:"after,omitempty"`
// Detail says what changed, for "changed".
Detail string `json:"detail,omitempty"`
}
MedChange describes one difference between two medication lists. JSON tags because this now leaves the process. It was written as an internal type and had no caller outside the package, which is why it had none.
type Note ¶
type Note struct {
// Severity is "error", "warning" or "info".
Severity string `json:"severity"`
// Path locates the problem in the document.
Path string `json:"path,omitempty"`
// Message is written for a person, not a machine.
Message string `json:"message"`
// Rule names the check, so a recurring note can be looked up.
Rule string `json:"rule,omitempty"`
}
Note is something worth telling somebody about a document.
type Patient ¶
type Patient struct {
Identifiers []Identifier `json:"identifiers,omitempty"`
Family string `json:"family,omitempty"`
Given []string `json:"given,omitempty"`
Prefix string `json:"prefix,omitempty"`
Suffix string `json:"suffix,omitempty"`
Gender string `json:"gender,omitempty"`
GenderName string `json:"genderName,omitempty"`
BirthTime string `json:"birthTime,omitempty"`
Address *Address `json:"address,omitempty"`
Phone string `json:"phone,omitempty"`
}
Patient is the subject of the document.
type ReconcileReport ¶
type ReconcileReport struct {
Added []MedChange `json:"added"`
Removed []MedChange `json:"removed"`
Changed []MedChange `json:"changed"`
Unchanged []MedChange `json:"unchanged"`
// Conflicts flags a medication that appears active in one document and completed or discontinued in the other.
// This is the clinically dangerous case and the reason to run this at all: the two documents disagree about
// whether the patient is currently taking something, and whichever the receiving clinician happens to read
// decides what they believe.
Conflicts []MedChange `json:"conflicts"`
}
ReconcileReport is the result of comparing medication lists across two documents.
func ReconcileMedications ¶
func ReconcileMedications(before, after *Document) ReconcileReport
ReconcileMedications compares medication lists across two documents.
It matches medications by code first (Code+CodeSystem), falling back to normalised name comparison (lowercase, trimmed) when either entry lacks a code. An entry with NegationInd true means the medication is explicitly NOT taken and is treated as absence, never as presence.
type RenderOptions ¶
type RenderOptions struct {
Footer string
// IncludeCodes prints the code and code system beside each entry in the tables.
//
// Off by default. A printout for a clinician wants drug names; a printout for somebody debugging an exchange
// wants the codes, and putting them in front of a clinician who does not need them is how a page of
// medications becomes unreadable.
IncludeCodes bool
}
RenderOptions controls the rendered document.
type RepairAction ¶
type RepairAction struct {
Kind RepairKind `json:"kind"`
// Section is the section title, empty for document-level repairs.
Section string `json:"section,omitempty"`
// What was wrong, in terms of what a person would have seen.
Problem string `json:"problem"`
// Consequence is what would have happened if it were left alone. Separate from Problem because the problem
// is technical and the consequence is clinical, and only one of them makes anybody act.
Consequence string `json:"consequence"`
// Action is what was done about it, or why nothing was.
Action string `json:"action"`
// Fixed distinguishes a repair from a report.
Fixed bool `json:"fixed"`
// Recovered is the narrative text that was reconstructed, so somebody can check it before trusting it. A
// repair nobody can inspect is a repair nobody should accept.
Recovered string `json:"recovered,omitempty"`
}
RepairAction is one thing that was changed, or one thing that could not be.
type RepairKind ¶
type RepairKind string
RepairKind identifies what was wrong, so a caller can group or suppress by cause.
const ( // RepairNarrativeGenerated means a section had entries and no narrative, so it would have displayed blank. RepairNarrativeGenerated RepairKind = "narrative-generated" // RepairNarrativeIncomplete means the narrative mentioned fewer facts than the entries contain. RepairNarrativeIncomplete RepairKind = "narrative-incomplete" // RepairCustodianAdded means the document lacked the custodian C-CDA requires. RepairCustodianAdded RepairKind = "custodian-added" // RepairNotAttempted means something is wrong that cannot be derived from what the document contains. RepairNotAttempted RepairKind = "not-attempted" )
type RepairOptions ¶
type RepairOptions struct {
// CustodianName is used when the document has no custodian. C-CDA requires one and Generate refuses without
// it, so a repair of a document missing its custodian needs the caller to say who is taking responsibility -
// which is the correct place for that decision, since it is an assertion about an organisation.
CustodianName string
// Indent writes the result readably. Off for anything going on a wire.
Indent bool
}
RepairOptions controls what the repaired document claims.
type RepairReport ¶
type RepairReport struct {
Repairs []RepairAction `json:"repairs"`
// Fixed and Reported are counted separately because they lead to different actions: one means the document is
// now usable, the other means somebody has to go back to the sender.
Fixed int `json:"fixed"`
Reported int `json:"reported"`
// InvisibleEntries is the count of coded facts that no reader would have seen. This is the number that matters
// and the reason to run this at all.
InvisibleEntries int `json:"invisibleEntries"`
// Document is the repaired XML, empty when nothing could be repaired.
Document string `json:"document,omitempty"`
}
RepairReport is what was found and what was done.
func Repair ¶
func Repair(d *Document, opts RepairOptions) (RepairReport, error)
Repair finds content that would be invisible to a reader and rebuilds it from the coded entries.
The report is returned even when nothing could be repaired, because "this document will display blank and here is why" is the finding, and the caller has to be able to act on it either way.
func (RepairReport) Usable ¶
func (r RepairReport) Usable() bool
Usable reports whether the repaired document would now display its content.
type Section ¶
type Section struct {
// Title is the section heading as written.
Title string `json:"title,omitempty"`
Code string `json:"code,omitempty"`
CodeSystem string `json:"codeSystem,omitempty"`
CodeName string `json:"codeName,omitempty"`
// TemplateIDs are the section's conformance claims.
TemplateIDs []string `json:"templateIds,omitempty"`
// Kind is the recognised section type in plain words, empty when the section
// is not one this package knows.
Kind string `json:"kind,omitempty"`
// NarrativeText is the human-readable content with markup removed. This is
// what a clinician actually reads, and half of what the narrative check
// compares.
NarrativeText string `json:"narrativeText,omitempty"`
// NarrativeHTML is the narrative converted to safe HTML for display, with
// the table structure preserved because CDA narrative is mostly tables and
// flattening them makes a result list unreadable.
NarrativeHTML string `json:"narrativeHtml,omitempty"`
// Entries are the coded, machine-readable observations.
Entries []Entry `json:"entries,omitempty"`
// Empty reports a section with neither narrative nor entries, which usually
// means the sending system had nothing to say rather than that the patient
// has nothing.
Empty bool `json:"empty,omitempty"`
// NilFlavor carries an explicit statement of absence, such as "no known
// allergies". It is important to keep distinct from an empty section: one
// says there is nothing, the other says nobody looked.
NilFlavor string `json:"nilFlavor,omitempty"`
// contains filtered or unexported fields
}
Section is one section of the document.
type Summary ¶
type Summary struct {
Title string `json:"title"`
DocumentType string `json:"documentType"`
Patient string `json:"patient"`
Effective string `json:"effective"`
Sections int `json:"sections"`
Entries int `json:"entries"`
Warnings int `json:"warnings"`
Errors int `json:"errors"`
}
Summary is a short account of a document for a list view.
type TOCOptions ¶ added in v0.1.23
type TOCOptions struct {
// IdentifierSystems maps an OID root to a URI, as for ToFHIR.
IdentifierSystems map[string]string
// Language is the document's language when the source does not say (BCP 47). Default en-US.
Language string
}
TOCOptions configures a transfer summary.
type ValidationReport ¶
type ValidationReport struct {
// Profile is which IG was checked, e.g. "C-CDA R2.1 Continuity of Care Document".
Profile string `json:"profile"`
// Findings reuse the existing Note type so they integrate with the rest of
// the package without conversion.
Findings []Note `json:"findings"`
Errors int `json:"errors"`
Warnings int `json:"warnings"`
Infos int `json:"infos"`
}
ValidationReport is the result of checking a document against an implementation guide.
func Validate ¶
func Validate(d *Document) ValidationReport
Validate checks a parsed document against the C-CDA R2.1 implementation guide.
It covers the conformance statements that actually cause rejection or misinterpretation in production: header completeness, required sections for the claimed document type, and entry-level invariants that receivers depend on. It does not attempt to replicate the full Schematron, which checks hundreds of things nobody acts on, because a report with two hundred findings teaches nobody anything.
Each finding carries a Rule name so a consumer can suppress or group by check, and a Severity that says whether the document would be rejected (error), would lose information (warning), or is worth knowing about (info).
func (ValidationReport) Conformant ¶
func (r ValidationReport) Conformant() bool
Conformant reports whether the document passes all error-level checks.