Skip to content

Composition Block Type Extension

Canonical../StructureDefinition/composition-block-type
Statusdraft · 1.26.0
BaseExtension (constraint)
ContextComposition.section (element)
SourceFSH · JSON

How to render this section -- the section's RENDER KIND, not the tool that produced it.

FSH

The authored source of truth: extensions/CompositionBlockExtensions.fsh (edit on GitHub). One FSH file may define several related artifacts.

// CompositionBlockExtensions.fsh
// The two extensions a structured note section carries: HOW TO RENDER IT, and that render kind's
// own structured value. Both sit on `Composition.section`.
//
// WHY THESE EXIST. The encounter note is authored in a BLOCK EDITOR -- the note body is an ordered
// stack of independent pieces, each with its own identity, its own optional heading, and its own
// KIND. The kinds written today are the ones `note-block-type` names -- plain rich text, two levels
// of heading, a letter's merge-field body, a summary body whose claims each cite a source, an
// enclosures block and a diabetes review -- and vitals, flowsheet and form blocks are planned, each
// shipping with the clinical requirement that calls for it. The editor's extensibility
// requirement is that adding a kind must not re-architect the note, and the storage consequence is
// that the record has to hold a piece whose kind it has never heard of.
//
// R4 gives a section an identity (`id`), a heading (`title`) and narrative (`text`), and those carry
// three quarters of a block unaided. What it has nowhere to put is WHICH KIND wrote the section, and
// a kind's own structured value where that value is more than the narrative beside it. These two
// extensions are those two gaps and nothing else.
//
// THE MAPPING IS ONE SECTION PER BLOCK, POSITIONALLY ORDERED. `section` is an ordered element in R4,
// so nothing records a position. `section.id` is the block's identity, `section.title` its heading,
// `section.text` its narrative. A block the author raised and left blank is KEPT -- written with an
// empty-status narrative and an `emptyReason` of `notstarted` -- because a blank piece between two
// written ones is part of the shape of what the author wrote.
//
// EVERY SECTION DECLARES ITS KIND, AND THE KIND IS A RENDER AXIS -- NOT THE TOOL THAT PRODUCED IT.
// `composition-block-type` carries a `code` bound EXTENSIBLE to `note-block-types`, written on every
// structured section, `text` included.
//
// THIS IS THE DISTINCTION TO GET RIGHT, because getting it wrong makes two thirds of the sections in
// this estate unrepresentable. A section answers three independent questions and this extension is
// only one of them:
//
//   * WHAT IS THIS SECTION CLINICALLY?     `section.code`   -- a problem list is 11450-4.
//   * WHO PRODUCED IT?                     `section.author` -- a Practitioner, or a Device.
//   * HOW DO I RENDER IT?                  `composition-block-type`.
//
// A note editor widget, a letter template and an AI harness graph are all producers of section
// shapes, and a consumer renders what they produce through one dispatch. A `cited-claims` section is
// the same render kind whether a graph or a person assembled it; a `merge-field-body` is the
// "here is what we auto-filled" strip however it was filled. Reading this element as "which editor
// widget wrote it" would leave a letter and a generated summary with no kind at all, and force a
// consumer to infer rendering from `author`, which answers a different question.
//
// No kind is a default that may be left implied, and no kind has standing the others do not; `text`
// is simply the first to ship. This value set is where a kind is named once it has shipped, and a
// writer whose kind is in it MUST use that code rather than mint a synonym.
//
// A SECTION CARRYING NO KIND PREDATES THIS EXTENSION. It is read as `text`, which is what lets every
// note filed before block authoring existed -- the four-section SOAP notes this EMR wrote for years
// among them -- open without a migration. That is TOLERANCE FOR LEGACY DATA, not a default new data
// may lean on: nothing written today omits its kind, so the population that relies on this reading
// is fixed and shrinking, and the concession can be retired once it is empty.
//
// A KIND OUTSIDE THE SET IS CONFORMANT, AND A READER MUST NOT DESTROY IT. The binding was `required`
// through 1.21.x, which made a kind the guide had not yet named nonconformant -- and turned this
// specification into a release gate on every editor that ships a new block: the day a vitals block
// went out, every note carrying one failed validation until the IG cut a release. Extensible makes
// that a documentation lag instead of a break.
//
// The obligation this puts on a CLIENT is the part that matters, because a binding cannot enforce
// it. A client reading a section whose kind is ahead of its own vocabulary renders it as text -- it
// has no widget for it, and the narrative is authoritative, so this is correct -- but it MUST keep
// the unrecognised code and write it back unchanged. Rendering as text is a display decision;
// rewriting the kind as `text` is data loss, and the record then says a specialised block was a
// prose block. Round-tripping what you cannot interpret is the whole reason this binding loosened.
//
// This is the pattern SDC uses for `questionnaire-itemControl`, which answers the same question
// about which control authored or renders an item: extensible, with form fillers told to ignore
// controls they do not understand rather than rewrite them. openEHR, the one ecosystem that has
// solved composition-of-typed-authoring-units at scale, uses open-ended versioned archetype ids.
// Nobody closes this set.
//
// ────────────────────────────────────────────────────────────────────────────────────────────────
// THIS IG HAS BEEN HERE BEFORE, AND THAT HISTORY IS THE ARGUMENT FOR THESE. 1.11.0 removed an
// experimental clinical-document surface, `nexus-emr-embedded-section` and its siblings among it,
// because "the surface was experimental and unused; keeping it published invited implementers to
// build against a shape the IG was not committing to." Composition returned a week later, in 1.14.0
// as the encounter-note subset only, on the stated grounds that "that reasoning was sound and is
// now spent: there is a committed consumer" -- and explicitly NOT as "the
// four-section-type (text/form/canvas/file) surface with recursive containment and coordinate
// annotations, whose removal also cleared two unevaluable-slicing defects."
//
// These two extensions clear the same bar for the block surface, and are shaped by each objection
// that closed the old one:
//
//   * WHERE THE OLD SURFACE ENUMERATED FOUR SPECULATIVE KINDS, THIS ENUMERATES ONLY REAL ONES. This
//     is the objection the current design does NOT answer by abandoning enumeration, and the
//     difference is what was being enumerated. `nexus-emr-embedded-section-types` bound text, form,
//     canvas and attachment when NONE of them had shipped -- a list of kinds the IG was guessing at,
//     which is precisely what "a shape the IG was not committing to" meant. `note-block-types` binds
//     only kinds that exist, each with a named stream writing it. The set grows as kinds ship rather
//     than ahead of them, so the IG never publishes a kind with no producer -- which is why the two
//     kinds most often asked for next (`vitals`, `flowsheet`) are still not in it.
//   * WHERE THE OLD SURFACE NESTED, THIS ORDERS. Recursive containment inside another section's
//     content is gone; a block is one `Composition.section`, and R4's own ordering of that element
//     carries the sequence. There is nothing to recurse into and no coordinate system.
//   * WHERE THE OLD SURFACE SLICED, THIS IS FLAT. `nexus-emr-embedded-section` was a complex
//     extension with sliced sub-extensions, and its slicing was among the two unevaluable-slicing
//     defects the removal cleared. These are two independent extensions with a single `value[x]`
//     each and no slices, which is what `conventions.md` asks for -- prefer portable constraints
//     over unevaluable identifier/code slices.
//
// THE FAILURE MODE IS NOW THE INVERSE OF 1.11.0's, AND FAILS FOR THE SAME REASON. Then: a published
// shape with no implementer. Now: an implementer with no published shape. A reader of this IG sees
// `Composition.section` with `section.code` unbound and no block concept anywhere, while production
// writes typed blocks against these two URLs. Declaring them is what closes that gap.
//
// ────────────────────────────────────────────────────────────────────────────────────────────────
// TWO THINGS TO KNOW BEFORE BUILDING ON THESE, recorded here rather than left to be discovered by
// whoever reads the wire data first.
//
// 1. THE CANONICAL BASE IS THIS IG'S, AND THAT WAS A CHANGE. Both extensions publish under
//    `../StructureDefinition/...`, the base `conventions.md` names. They were
//    first minted under `http://services.well.com/fhir/...`, the hostname three older Composition
//    extensions in the same library already use (`UpdatedBy`, `composition-billing`,
//    `composition-ehr-share`), none of which this IG declares. One specification file minting two
//    namespaces left neither reader nor writer a rule for telling which applied, so these two moved
//    to the guide's own base before anything depended on the old one. That move was free exactly
//    once: no note carrying either extension had been filed yet. Published canonical URLs are
//    forever, and from this version these are published.
//
//    Two of those three -- `composition-billing` and `composition-ehr-share` -- are now declared
//    under this guide's base too, in `CompositionNoteExtensions.fsh`. They could not take the same
//    route these did: they have filed clinical data behind them, so the address they carry cannot be
//    changed by editing a constant. There the specification states where they belong and the
//    software has not caught up; read that file before writing either. `UpdatedBy` is a `Meta`
//    extension rather than a note concept and is out of scope for both files.
//
// 2. STRUCTURED VALUE VS `section.entry`. The Composition profile declares `section.entry` as "the
//    structured counterpart to `section.text` ... the element that carries structured note
//    generation when it arrives". `composition-block-data` is deliberately NOT that. The two answer
//    different questions and are expected to coexist: `entry` holds structured clinical facts as
//    real, separately-addressable resources (an Observation a scribe extracted, a Condition an SDC
//    form promoted), which is what a consumer querying the record wants. This extension holds one
//    block kind's own EDITOR STATE -- the shape is the block kind's, it is meaningful only to that
//    kind, and it is rewritten on every keystroke. Minting resources per keystroke to hold it would
//    put editor state into the clinical record and make `entry` unqueryable for what it is for.
//    Whether a block kind should ALSO promote its facts to `entry` on save is that kind's own
//    decision, and a good one for a vitals block to make.
// ────────────────────────────────────────────────────────────────────────────────────────────────
//
// WHAT A CONSUMER THAT IGNORES BOTH EXTENSIONS SEES. A correct, readable note. Every section carries
// its narrative regardless of kind, so nothing a reader needs is only in the structured value. That
// is the invariant these definitions are written to preserve, and a block kind that breaks it -- by
// putting clinical content in `data` and nothing in `text` -- is malformed however well it round
// trips.

Extension: CompositionBlockType
Id: composition-block-type
Title: "Composition Block Type Extension"
Description: """
How to render this section -- the section's RENDER KIND, not the tool that produced it.

A `code` from `note-block-types`, bound **extensible**: where this specification names a kind, use
its code; where an editor ships a kind the guide has not named yet, a code outside the set is
conformant. **Written on every structured section, whatever its kind** -- no kind is a default
that may be left implied, and none has standing the others do not.

A section carrying NO such extension predates it, and is read as `text`. That is what lets every note
filed before block authoring existed -- four-section SOAP notes among them -- open with no migration.
It is tolerance for legacy data rather than a default new data may rely on, so the branch is
retirable once no untyped section remains.

**A kind you do not recognise must be kept, not rewritten.** A client whose vocabulary is behind the
writer's has no widget for such a section and renders it as text -- correct, because the narrative is
authoritative. It MUST still write the unrecognised code back unchanged. Rendering as text is a
display decision; **storing `text` in its place is data loss**, and leaves the record asserting that
a specialised block was prose. No binding can enforce this, which is why it is stated here.

**FIRST PASS.** This extension arrived with its first consumer rather than from a modelling pass,
and block authoring is expected to grow around it: kinds join `note-block-types` as they ship, and
the surface may widen as it does. **The canonical is settled and will not be renamed.** It moved to
this guide's base before the first note carrying it was filed, which was the only moment that move
was free; published canonical URLs are forever. What may still change is what the vocabulary holds,
not where this extension lives.
"""
* ^url = "../StructureDefinition/composition-block-type"
* ^status = #draft
* ^context[0].type = #element
* ^context[0].expression = "Composition.section"
* extension 0..0
* url 1..1
* value[x] 1..1 MS
* value[x] only code
* value[x] from NoteBlockTypesVS (extensible)
* valueCode ^short = "How to render this section; absent only on sections predating this extension"
* valueCode ^definition = "The section's render kind, from `note-block-types`. It says HOW a consumer should render the section, not which tool produced it -- `section.author` answers that, and an editor widget, a letter template and a generated summary can all produce the same render kind. Written on every structured section, text included. Absent means the section was written before this extension existed and is read as `text` -- a legacy concession, not a default."

Extension: CompositionBlockData
Id: composition-block-data
Title: "Composition Block Data Extension"
Description: """
One section's structured value, for a render kind whose value is more than the narrative
beside it.

An `Attachment`, whose `contentType` says what the payload is -- typically
`application/json` -- because the shape belongs to the render kind rather than to this
specification and R4 has no element typed for it. This is the section's own structured value, NOT
its structured clinical content: `Composition.section.entry` is that, and the two are expected to
coexist on one section.

**REACH FOR SDC FIRST.** Where a render kind is really a FORM -- a screening instrument, an intake
questionnaire -- the modelled answer already exists and is not this extension: a `Questionnaire`
plus a `QuestionnaireResponse`, which is the problem SDC was written to solve and which gets you
`section.entry` for free.

The test is what the block's values ARE, not how the block looks. A block whose values are answers
to its own questions is a form, and belongs in SDC however plain it renders. A block that writes
each value to the chart record that owns it as the value is recorded -- an Observation, a vital, a
Procedure -- is not a form however form-like it renders: those values are clinical facts in the
record already, and what the block stores here is that visit's account of them. Use this extension
for a payload that is genuinely a render kind's own state and not a set of answers to questions.

**Why an Attachment and not a bare string.** A JSON string in a `valueString` is self-describing to
nobody: a reader cannot tell what it holds without recognising the render kind first, and no
published international IG carries editor state that way. `Attachment` is what R4 already provides
for an opaque payload, and `contentType` makes the envelope readable even where the contents are
not. A `text` section needs none of this at all.

**The narrative is authoritative for reading.** A consumer that has never heard of the render kind
renders `section.text` and is not missing clinical content, so nothing a reader needs may live only
in here. A value that cannot be parsed reads as no structured value rather than as an error: one
malformed section must not put a whole clinical document out of reach. **Unreadable is not absent**
-- a writer that cannot parse a stored value must round-trip it untouched rather than drop it.

**FIRST PASS.** The canonical is settled on the same terms as `composition-block-type`. One design
question is not: whether a render kind's structured value belongs in an extension at all, or in
`Composition.section.entry`, which this guide already nominates as a section's structured
counterpart. They hold different things today, for the reasons given above. The evidence that would
settle it is a render kind that promotes its own facts to `entry` on save -- a vitals block is the
obvious first -- because that is the case where the two would genuinely overlap.
"""
* ^url = "../StructureDefinition/composition-block-data"
* ^status = #draft
* ^context[0].type = #element
* ^context[0].expression = "Composition.section"
* extension 0..0
* url 1..1
* value[x] 1..1 MS
* value[x] only Attachment
* valueAttachment ^short = "The render kind's own structured value, as a typed attachment"
* valueAttachment ^definition = "The section's own structured value. `contentType` says what the payload is -- `application/json` for a kind carrying a JSON object -- so the envelope is readable even where the contents are not described here. Readers that do not recognise the render kind ignore this element and render the section narrative instead; a reader that cannot PARSE it must still write it back unchanged, because unreadable and absent are different facts."
* valueAttachment.contentType 1..1 MS
* valueAttachment.contentType ^short = "What the payload is; `application/json` for a JSON kind"
* valueAttachment.data 0..1 MS
* valueAttachment.data ^short = "The payload itself, base64-encoded per R4"