Attachment Packet Profile¶
| Canonical | ../StructureDefinition/nexus-attachment-packet |
|---|---|
| Status | draft · 1.26.0 |
| Base | List (constraint) |
| Source | FSH · JSON |
An attachment packet: the ordered set of chart items assembled to travel with an outbound artifact -- a letter today, an eReferral next, and forms, emails and chart prints after that.
FSH
The authored source of truth: resources/StructureDefinition-nexus-attachment-packet.fsh (edit on GitHub). One FSH file may define several related artifacts.
// StructureDefinition-nexus-attachment-packet.fsh
//
// A PACKET IS AN ORDERED SET OF CHART ITEMS ASSEMBLED TO TRAVEL WITH SOMETHING GOING OUT.
//
// The Attachment Manager assembles documents, notes, labs and forms to accompany a letter, and
// next an eReferral, and after that forms, emails and chart prints. This profile is that set.
//
// WHAT WAS PROPOSED AND WITHDRAWN. The first design modelled the packet AS the merged PDF's own
// DocumentReference, linked to its letter one-directionally through `context.related` and found
// by the inverse search. A second put the members in two `Composition.section`s. Three things were
// wrong, and the third decides the design:
//
// 1. The recipient had no home. R4 DocumentReference has no `recipient` element, so it was a
// house extension.
// 2. Per-member assembly state -- which pages of each member were left out -- had no home at
// all, and was heading for a second house extension.
// 3. THE PACKET WAS REACHABLE ONLY FROM A Composition. An eReferral is a ServiceRequest and has
// no sections, so it would have needed a second design. So would every other host in the
// requirement.
//
// R4 ALREADY HAS A RESOURCE FOR THIS AND IT ANSWERS ALL THREE. `DocumentManifest`, verbatim: "A
// collection of documents compiled for a purpose together with metadata that applies to the
// collection." It carries `content` (Reference(Any)), `recipient`, `related`, `subject` and its
// own identity. AND IT IS REMOVED IN R5, replaced by `List`. So the shape is right and the
// container it would ship in is a dead end.
//
// ADOPTED: THE PACKET IS A `List`. Membership and order are `List.entry`, `mode` is `working`,
// `subject` is the patient. The host points at the packet with the reference element it ALREADY
// HAS -- a letter uses one `Composition.section.entry`, a referral uses
// `ServiceRequest.supportingInfo`. Both are `Reference(Resource)` in base R4, and
// `ServiceRequest.supportingInfo` is what Ontario eReferral profiles for referral attachments
// while NexusCoreReferral already marks it MS. So a List is legal in both today, and what was
// missing is only the sanction.
//
// THIS IS THE RULING THE TEMPLATE PROFILE ALREADY MADE, for the same reason and in its own words:
// marking each resource type answers only the first case and answers it once per resource type,
// while a list of references answers all of them with one profile.
//
// WHY `entry.item` IS NOT TYPE-CONSTRAINED. Constraining it to the kinds we ship today
// (DocumentReference, Composition, DiagnosticReport) would re-introduce exactly the per-type
// enumeration this design exists to avoid, and the requirement already names hosts and members we
// have not modelled. An assembler puts on the packet what a clinician chose to send; a consumer
// renders what it can and says so for what it cannot.
//
// THE RENDERED PDF IS NOT STORED HERE. Not at assembly and not at preview, because the packet is
// the List and the render is derivable from it. It is stored ON SEND, because that artifact is the
// record of what went out and is the one version that cannot be rebuilt once its members change.
// That is a DocumentReference (type LOINC 11503-0, a document code, correct on `type` and wrong in
// a section slot) plus a Provenance whose derivation entity is the VERSIONED List: one pointer
// carries membership, order and exclusions as they stood, where naming the members individually
// loses the order and the exclusions. No snapshot List is minted, on the reasoning the template
// profile already gives for carrying no version -- FHIR version history retrieves prior revisions
// already.
//
// NOT CHANGED: DocumentReference GAINS NO `recipient`. R4 puts recipient on the packet prepared
// for someone rather than on the document, and a document faxed twice is one document with two
// sends. The house extension stays documented and unpromoted until the send workflow is modelled;
// a referral's recipient is already `ServiceRequest.performer`.
//
// THE TWO ASSEMBLY FACTS THAT ARE NOT MEMBERSHIP, and they sit in different places on purpose.
//
// * WHETHER THE RENDER LEADS WITH A COVER PAGE is an extension on the List itself,
// `nexus-packet-cover-page`, declared as a named slice below. It is a property of the assembly,
// it is generated at render time from what the packet already knows, and it is deliberately not
// a member -- see that extension for why absence is not `false`.
// * WHICH WAY OUT THE PACKET WAS BUILT FOR is a `meta.tag` coding under
// `CodeSystem/nexus-send-channel`. It explains the packet's page and byte limits and the cover
// default it started from.
//
// THE TAG IS NOT SLICED HERE, following the ruling the Task profile already made for its own tag:
// `meta.tag` is open on every resource, and a marker written by one producer is not part of the
// shape every conforming packet has. It is also the placement that keeps "every packet prepared for
// this channel" a `_tag` search rather than a custom SearchParameter. The vocabulary is named in the
// Description and constrained nowhere.
//
// NEITHER OF THEM IS A RECORD OF A SEND. Both say what was CHOSEN while the packet was being put
// together, which is the same thing the whole profile says. A reader must not conclude from a
// channel tag that anything was transmitted; that artifact is the render stored on send, above.
//
// TO BE MEASURED BEFORE THE BACK-POINTER SEARCH IS RELIED ON. The server CapabilityStatement
// declares two search parameters on DocumentReference, `identifier` and `description`. `related`
// is not among them, and this store's documented failure mode for a parameter it does not
// recognise is to DROP IT SILENTLY rather than error. So `DocumentReference?related=...` may be
// returning the whole compartment with the client rendering whatever came back. Measure it the way
// the template identifier search was measured before List exposure was declared: a bogus `related`
// value that returns rows means no filtering is happening.
Alias: $NexusAttachmentPacket = ../StructureDefinition/nexus-attachment-packet
Alias: $NexusAttachmentPacketNS = ../NamingSystem/nexus-attachment-packet
// Identity is one identifier under one system, and `List.identifier` is 0..* so a deployment may
// carry its own alongside. An INVARIANT rather than a slice, per this IG's evaluable-slicing
// convention: an identifier slice discriminated by system is not evaluable by a generic validator,
// and this states the same rule in portable FHIRPath.
Invariant: nexus-packet-has-identity
Description: "A packet carries exactly one identifier under the Nexus attachment-packet naming system -- that identifier is which packet this is, and it is what a search for the packets on a chart finds."
Severity: #error
Expression: "identifier.where(system = '../NamingSystem/nexus-attachment-packet').count() = 1"
// A member appearing twice makes the packet's ORDER ambiguous -- which occurrence is position 3 --
// and makes an exclusion ambiguous too, since the two occurrences can carry different excluded
// pages for the same document. Neither is a state an assembler can mean.
Invariant: nexus-packet-members-distinct
Description: "A resource appears at most once in a packet. Two entries naming the same resource make the packet's order ambiguous, and let one member carry two different sets of excluded pages."
Severity: #error
Expression: "entry.item.reference.empty() or entry.item.reference.isDistinct()"
// Pages are 1-based and counted within the member, so 0 and negatives are not a boundary case to
// tolerate -- they are a producer bug, and a packet carrying one will render wrongly and silently.
Invariant: nexus-packet-excluded-pages-are-positive
Description: "An excluded page number is 1-based and therefore at least 1. A zero or negative value is a producer defect, not an edge case: it names no page, and a renderer that ignores it drops a page the assembler meant to keep."
Severity: #error
Expression: "entry.extension.where(url = '../StructureDefinition/nexus-attachment-packet-excluded-page').all(value.ofType(integer) >= 1)"
// @intro
// An ordered set of chart items assembled to travel with something going out of the EMR: a List
// whose entries are the members, in order, each able to say which of its pages were left out.
Profile: NexusAttachmentPacket
Parent: List
Id: nexus-attachment-packet
Title: "Attachment Packet Profile"
Description: """
An **attachment packet**: the ordered set of chart items assembled to travel with an outbound
artifact -- a letter today, an eReferral next, and forms, emails and chart prints after that.
Membership and order are `List.entry`. The host points at the packet with the reference element it
already has: a letter uses one `Composition.section.entry`, a referral uses
`ServiceRequest.supportingInfo`. Both are `Reference(Resource)` in base R4, so a packet is legal in
each without a new element anywhere.
**The packet is not the sent document.** It is the assembly. What the recipient received is the
rendered file stored when the host is SENT, which carries a `Provenance` deriving it from this
List **at the version it had** -- because membership, order and per-member exclusions all move.
**A letter names its packets in one coded section.** A `Composition` profiled as
[`nexus-emr-core-composition`](StructureDefinition-nexus-emr-core-composition.md) MAY carry a
section coded LOINC `28562-7` whose `entry` references packets; that is the only place a letter
records what travels with it, and the `enclosures` slice on that profile is where the join is
declared.
**Two assembly facts ride beside the membership.** Whether the render leads with a generated cover
page is the [`nexus-packet-cover-page`](StructureDefinition-nexus-packet-cover-page.md) extension,
declared as the `coverPage` slice below -- read it before treating an absent value as `false`, which
it is not. Which way out of the EMR the packet was built for is a `meta.tag` coding under
[`nexus-send-channel`](CodeSystem-nexus-send-channel.md); this profile does not slice or otherwise
constrain `meta.tag`, because the element is open on every resource and that marker belongs to one
producer rather than to the shape every packet conforms to.
**Neither of those records a send.** Both say what was chosen while the packet was being assembled.
A channel tag is not evidence that anything was transmitted.
"""
// A packet has no effective date. It is an assembly, not a clinical event: `List.date` is when the
// packet was put together, which is not something that happened to the patient.
* insert NoEffectiveDate
* insert SmartExposedNoDelete(list)
* ^url = $NexusAttachmentPacket
* ^status = #draft
* insert NoResourceNoteExtension
* ^publisher = "WELLSTAR Technologies Corp."
* obeys nexus-packet-has-identity and nexus-packet-members-distinct and nexus-packet-excluded-pages-are-positive
// --- Element Constraints ---
// THE COVER-PAGE CHOICE, joined to the profile it is meaningful on. Optional, and it stays optional:
// a producer with nothing to say omits it, and a reader that folds that absence into `false` reports
// a cover page declined on every packet written before the flag existed.
* extension contains PacketCoverPageExtension named coverPage 0..1 MS
* extension[coverPage] ^short = "Does the render lead with a generated cover page? (absent = not stated, NOT false)"
* identifier 1..* MS
* identifier ^short = "Stable identity of this packet, and the marker that this List is one"
* status MS
* status ^short = "current (live assembly) | retired (superseded) | entered-in-error"
// The membership is maintained in place rather than captured at a moment, which is what R4 means
// by `working`. A packet is edited until its host is sent.
* mode = #working (exactly)
// THE PACKET'S NAME, and declaring it is not optional housekeeping. `List.title` is `1..1 MS` on
// the template profile, and the facade for a resource type exposes what its profiles AGREE on -- so
// leaving `title` undeclared here removed `List.title` from all three ports' published surface. The
// surface manifest caught it as three REMOVED lines, which is exactly the breaking change it exists
// to surface: an accessor other code compiles against, gone because a second profile on the same
// type stayed silent about an element.
//
// It is also right on its own merits. A packet is shown to the user who assembled it and to whoever
// reviews what went out, so it needs a human name -- and every packet this guide's examples carry
// has one.
* title 1..1 MS
* title ^short = "The packet's name, as shown where it is assembled and reviewed"
* title ^definition = "A human-readable name for this assembly, shown in the attachment surface that built it and beside the record of what was sent. Names the packet's purpose rather than its members (\"Enclosures - Internal Medicine referral letter\"), because membership is `entry` and changes while the packet is being assembled."
* subject 1..1 MS
* subject only Reference(NexusEmrCorePatient)
* subject ^short = "The patient whose chart these items came from"
* entry 1..* MS
* entry ^short = "The members, in order"
* entry ^definition = """
One member of the packet. `List.entry` is ORDERED, and the order is the order the members appear in
the outbound render.
`entry.item` is deliberately unconstrained as to resource type: the packet carries what a clinician
chose to send, and enumerating the types would reintroduce the per-type modelling this profile
exists to replace. A consumer renders the kinds it knows and reports the ones it does not, rather
than dropping them silently.
An entry MAY carry `nexus-attachment-packet-excluded-page` extensions naming pages of that member
the assembler left out.
"""
* entry.item 1..1 MS
* entry.item ^short = "The chart item -- a document, note, result or form; SHALL be about this packet's subject"
* entry.item ^comment = """
**Every member SHALL be about the patient in `List.subject`.** A packet is assembled from one
chart and travels to an external recipient, so a member belonging to a different patient is
cross-patient disclosure at the moment the host is sent.
**No validator enforces this, and that is a limitation to design around rather than a gap to
ignore.** FHIRPath cannot resolve a reference, so the rule is not expressible as an invariant, and
`entry.item` is deliberately `Reference(Resource)` so no type constraint narrows it either. A
packet naming another patient's document is therefore structurally valid. Enforcement belongs where
the write happens and the reference can be resolved: a server accepting a packet SHALL reject a
member whose subject is not the packet's subject.
A consumer that did not assemble the packet should not assume the rule held: resolve the members
and check, rather than trusting `List.subject` to describe all of them.
"""
* entry.extension contains PacketEntryExcludedPageExtension named excludedPage 0..*
* entry.extension[excludedPage] ^short = "A page of this member excluded from the render (1-based; repeats)"
* . ^short = "Nexus EMR Attachment Packet"