Curated Value Set Profile¶
| Canonical | ../StructureDefinition/nexus-valueset |
|---|---|
| Status | draft · 1.26.0 |
| Base | ValueSet (constraint) |
| Source | FSH · JSON |
A curated pick-list as this estate stores one: the coded options a clinician chooses from in the chart, held per tenant and fetched by the application at the point of use.
FSH
The authored source of truth: resources/StructureDefinition-nexus-valueset.fsh (edit on GitHub). One FSH file may define several related artifacts.
// StructureDefinition-nexus-valueset.fsh
// The curated pick-lists a clinician chooses from: FHIR R4 `ValueSet`, as this estate stores and
// serves one.
//
// WHY A TYPE THIS GUIDE ALREADY USES IS ONLY NOW PROFILED. `ValueSet` appears throughout this
// specification as a TERMINOLOGY-BINDING artifact -- the definitional sets a profile binds an
// element to. That is a disjoint population from the one profiled here. These are the CURATED
// LISTS a tenant's store holds: the problem list, the procedure list, the allergen and reaction
// pickers, the schedule codes. They are created and updated per tenant by the initialization job,
// fetched by the application through a live search on the FHIR API, and served to an integrating
// partner. Nothing in this guide described their shape, while an extension published here already
// constrained a path inside them -- `valueset-secondary-coding`'s only context is
// `ValueSet.expansion.contains`. The guide had taken a position on the inside of a resource it
// declined to describe.
//
// THE FAILURE THIS PROFILE EXISTS TO PREVENT IS SILENT, WHICH IS WHY IT NEEDS A PROFILE RATHER
// THAN PROSE. The reader takes `expansion.contains` and defaults every member field it does not
// find. A ValueSet stored with no `expansion` is not an error to it; it is an empty list. A member
// with no `display` is not an error either; it is a blank row the clinician can select. So the two
// ways one of these can be wrong both present as a picker that opens, looks ordinary, and offers
// nothing or offers a blank -- never as a fault anyone can act on.
//
// WHAT IS CONSTRAINED, AND ON WHAT EVIDENCE. Only what a producer already guarantees and a
// consumer already depends on. The initialization job emits `expansion.timestamp`,
// `expansion.total` and a `contains` entry carrying system, code and display for every concept, so
// requiring those takes nothing away from the writer that exists. `compose` is left alone: the
// writer populates it and no reader in this estate reads it, and a profile that constrained it
// would be describing a shape nobody depends on.
//
// ORDER IS DISPLAY ORDER. The writer preserves its definition's concept order and the reader maps
// the array straight onto the picker, so the order a member sits at IS the order a clinician sees.
// FHIR arrays are ordered, so this needs recording rather than constraining -- but a producer that
// re-sorts an expansion has changed what the user sees, and a consumer must not re-sort one.
Profile: NexusValueSet
Parent: ValueSet
Id: nexus-valueset
Title: "Curated Value Set Profile"
Description: """
A curated pick-list as this estate stores one: the coded options a clinician chooses from in the
chart, held per tenant and fetched by the application at the point of use.
**This is not the definitional sense of `ValueSet`.** The sets this guide binds profile elements to
are a different population; those are terminology definitions and are not constrained here. This
profile describes the lists a tenant's store holds and the application renders.
**Resolve one by `url`, never by `id`.** Each tenant's copy carries a server-minted resource id, so
that tenants sharing a store do not collide on it -- which means the id is not stable across
tenants and the `url` is the only durable handle on a given list.
**An expansion is required, and that is the point of this profile.** A consumer reads
`expansion.contains` to build the picker. A set stored without an expansion does not fail for it --
it renders as an empty list, which looks exactly like a list that legitimately has no members. The
same is true one level down: a member without a `display` renders as a blank selectable row rather
than an error. Both failures are silent at the point they matter, so they are pinned here instead.
**Order is display order.** A producer's member order is what the clinician sees; do not re-sort an
expansion on either side.
"""
* ^url = "../StructureDefinition/nexus-valueset"
* ^status = #draft
* ^experimental = false
// CLOSED TO SMART APPS PENDING AN EXPOSURE DECISION, and it was briefly open.
//
// 1.26.0 exposed this type with the FULL grant -- read, write AND delete -- on the reasoning that
// "the application reads these through the FHIR API, and the initialization job creates and
// updates them there". Both halves are true and neither is about a third-party app: the first-party
// application authenticates through the identity provider and takes its permissions from role
// claims, and the initialization job is not a SMART client at all. Exposure is an authorization
// decision about APPS, and neither fact was one.
//
// What that granted, stated plainly: an application holding `patient/*.cud` could DELETE a tenant's
// curated pick-lists -- the only one of the seven types opened in 1.26.0 that carried delete.
//
// AND A PATIENT SCOPE CANNOT NARROW IT. R4's CompartmentDefinition-patient lists `ValueSet` and
// gives it NO param, so there is no element by which a value set is linked to a patient. A
// `patient/ValueSet.rs` scope therefore reads every tenant list rather than one patient's, which is
// not what the scope grammar leads an app author to expect. That was found by a guard nothing was
// running (`test:smart-exposed`), which is now in the build.
//
// What would decide it otherwise: an application that names why it needs to read the curated lists
// over the API, and on whose behalf -- and, if it needs to write, a ruling on what it means for an
// app to edit a list the tenant's own initialization job owns.
// Identity. `url` is the handle every consumer holds; the id is per-tenant and not stable.
* url 1..1 MS
* url ^short = "Canonical identity of this list -- the only stable handle on it"
* url ^comment = """
Resolve a curated list by this value, never by resource id. Each tenant's copy is created in that
tenant's store and carries a server-minted id, so the id differs per tenant while this value does
not.
"""
* name 0..1 MS
* status 1..1 MS
* status ^short = "Lifecycle of the list itself, not of its members"
// The expansion: what a picker is actually built from.
* expansion 1..1 MS
* expansion ^short = "The resolved members -- REQUIRED, because a consumer builds the picker from it"
* expansion ^comment = """
A curated list is stored with its expansion already resolved; a consumer is not expected to run an
expand operation to render one. An absent expansion reads downstream as a list with no members
rather than as a fault, so it is required here.
"""
* expansion.timestamp 1..1 MS
* expansion.timestamp ^short = "When these members were resolved"
* expansion.total 0..1 MS
* expansion.total ^short = "Member count, where the producer states it"
* expansion.total ^comment = """
Where present this is the producer's own count of `contains`. A consumer that disagrees with it has
either a truncated response or a stale copy, and should trust `contains` for what to render.
"""
* expansion.contains 1..* MS
* expansion.contains ^short = "The selectable members, in display order"
* expansion.contains ^comment = """
Order is meaningful: it is the order the options are presented in. Do not re-sort.
"""
// The three fields a member needs to be usable. Each is defaulted rather than rejected downstream,
// so an omission here is invisible at the point it does damage.
* expansion.contains.system 1..1 MS
* expansion.contains.system ^short = "The code system this member's code belongs to"
* expansion.contains.code 1..1 MS
* expansion.contains.code ^short = "The code stored when this member is chosen"
* expansion.contains.display 1..1 MS
* expansion.contains.display ^short = "What the clinician reads -- REQUIRED, an absent one renders blank"
* expansion.contains.display ^comment = """
This is the only text a chooser sees. A member without it presents as an empty selectable row, so
it is required here rather than left to a producer's discretion.
"""
// A second coding for a member that carries one. Already published; wired here because this is the
// resource its context names, and until now that context pointed into an unprofiled type.
* expansion.contains.extension contains ValueSetSecondaryCoding named secondaryCoding 0..*
* expansion.contains.extension[secondaryCoding] ^short = "An additional code for the same member"